Multiple matching() and contain(), condition on related records count - cakephp

With CakePHP 3.x, I have 3 models : StudentProfile, Diploma1 and Diploma2.
StudentProfile hasMany Diploma1
StudentProfile hasMany Diploma2
Diploma1 has an integer "state" field.
I need to get StudentProfiles which :
have one (or more) related Diploma1 where Diploma1.state = 2
OR
have one (or more) Diploma2 (no condition on Diploma2 fields)
I need to retrieve the matching Diploma1 and Diploma2 with my StudentProfiles.
I'm using the Search and Paginator components, so I have to do this with one query.
For now, I'v been able to get the first part by doing :
$query = $this->StudentProfiles
->find('search', $this->StudentProfiles->filterParams($this->request->query))
->contain(['Diploma1' => function ($q) {
return $q->where(['Diploma1.state' => 2]);
}])
->matching('Diploma1', function($q) {
return $q->where(['Diploma1.state' => 2]);
})
->distinct(['StudentProfiles.id'])
;
$this->set('studentProfiles', $this->paginate($query));
Combining matching and contain allows me to add the condition and get related Diploma1 (as I understand it).
Now I need to get also all the StudentProfiles with a related Diploma2, this is where I get stuck. If I add
->contain(['Diploma2'])
...to my query, I only get Diploma2 for StudentProfiles that have a matching Diploma1 (where state=2), but I don't get StudentProfiles with related Diploma2 only (without matching Diploma1), which is perfectly normal.
So I have 2 questions :
how can I get all StudentProfiles that have a related Diploma2 (i.e add a condition using count(...) > 0 maybe ?)
how can I combine this with a matching clause with a condition (state=2) ?
I hope this is clear.
Thanks

A slightly different approach, but maybe it helps
$query = $this->Profile->find();
$query->select([
'Profile.id',
'Profile.name',
'D1.id',
'D1.name',
'D1.status',
'D1.profile_id',
'D2.id',
'D2.name',
'D2.status',
'D2.profile_id',
'd1c' => 'COUNT(D1.id)',
'd2c' => 'COUNT(D2.id)',
]);
$query->contain([
'D1' => function($q) {
$q->where(['D1.status' => 2]);
return $q;
},
'D2'
]);
$query->leftJoinWith('D1', function($q) {
return $q->where(['D1.status' => 2]);
});
$query->leftJoinWith('D2', function($q) {
return $q;
});
$query->having(function($q) {
return $q->or_([
'd1c >' => 0,
'd2c >' => 0,
]);
});
$query->group('Profile.id');
I couldn't really get contain to create a join, so I had to add those leftJoinWith

Related

Require one of several values

I have a table with 5 boolean columns. I want to force the user to pick at least one. Is there a way to do this using the CakePHP $validator object in the Table PHP?
I can accomplish this in the controller easy enough, but utilizing the built-in stuff seems less wrong. Nothing in the docs jumps out at me. .
Controller example:
if (false === $val1 === $val2 === $val3 === $val4 === $val5){
//return with error
}
This would seem to be a situation for a rule, not a validation.
public function buildRules(RulesChecker $rules) {
$rules->add(function (EntityInterface $entity, Array $options) {
return $entity->val1 || $entity->val2 || $entity->val3 || $entity->val4 || $entity->val5;
}, 'selectOne', [
'errorField' => 'val1',
'message' => __('You must select at least one of these fields.'),
]);
return $rules;
}

How to use laravel pagination with filter

I have getCanSeeAttribute function in post model, and I try to get posts with pagination using filter()
$posts = Post::where(function ($query1) use ($users) {
$query1->where('posted_type', 'user')->whereIn('posted_id', $users);
})
->orWhere(function ($query2) use ($stores) {
$query2->where('posted_type', 'store')->whereIn('posted_id', $stores);
})
->with('likes', 'postable')
->withCount('comments', 'likes')
->latest()->paginate($paginate)->filter(function($post){
return $post->can_see == true;
});
The problem is when I use filter, it gets data attribute only, but I need all pagination attributes.
first_page_url": "http://localhost:8000/api/timeline?page=1",
"from": 1,
"last_page": 1,
"last_page_url": "http://localhost:8000/api/timeline?page=1",
"next_page_url": null,
"path": "http://localhost:8000/api/timeline",
"per_page": 10,
"prev_page_url": null,
"to": 6,
"total": 6
can_see not column in table it is Accessor
First of all, I hope you know what are you doing. Assuming you need to get results that has can_see field set to true, you should rather use:
$posts = Post::where('can_see', true)
->where(function($q) {
$q->where(function ($query1) use ($users) {
$query1->where('posted_type', 'user')->whereIn('posted_id', $users);
})->orWhere(function ($query2) use ($stores) {
$query2->where('posted_type', 'store')->whereIn('posted_id', $stores);
})
})->with('likes', 'postable')
->withCount('comments', 'likes')
->latest()
->paginate($paginate);
As you see I additionally wrapped (where .. orWhere) in additional where closure to make sure valid query will be generated.
Otherwise you should use:
$posts = Post::where(function($q) {
$q->where(function ($query1) use ($users) {
$query1->where('posted_type', 'user')->whereIn('posted_id', $users);
})->orWhere(function ($query2) use ($stores) {
$query2->where('posted_type', 'store')->whereIn('posted_id', $stores);
})
})->with('likes', 'postable')
->withCount('comments', 'likes')
->latest()
->paginate($paginate);
$posts = $posts->setCollection($posts->getCollection()->filter(function($post){
return $post->can_see == true;
})
);
However it's very unlikely the 2nd way is better one in your case. Assuming you have 1 mln of matching records and then of them has can_see set to true, and the rest have it set to false, it would cause that you will get 1 mln of records from database and then you will filter only 10 of them what would be performance killer for your application.
You can add this following code, define after $post
$post->appends($request->only('posted_type'));

cakephp 3 matching not working

I have categories, products and seller_products table and want to select all from three tables group by categories where seller_products.stock > 0 and seller_products.status = 1
This is my code
$pros1 = $this->Products->Categories->find()
->where([
])
->select(['Categories.id', 'Categories.title'])
->distinct(['Categories.id'])
->contain(['Products'])
->matching('Products.SellerProducts', function(\Cake\ORM\Query $q) {
return $q->where(['SellerProducts.stock >' => 0, 'SellerProducts.status' => 1]);
});
But this is not working. I'm getting all products group by categories by matching is not working and still getting products whose stock is 0 or status is not 1
Their associations are
$categories->hasMany('Products');
$products->hasMany('SellerProducts');
You need to understand that contain() and matching() are different. You are asking for categories having products where there is stock. But if you want what products are those that matched, you need to filter them in contain() as well:
$matcher = function(\Cake\ORM\Query $q) {
return $q->where(['SellerProducts.stock >' => 0, 'SellerProducts.status' => 1]);
};
$pros1 = $this->Products->Categories->find()
->select(['Categories.id', 'Categories.title'])
->distinct(['Categories.id'])
->contain(['Products' => function ($q) use ($matcher) {
return $q->matching('SellerProducts', $matcher);
}])
->matching('Products.SellerProducts', $matcher);

Using 'chunks' together with 'with' on Laravel query builder

dunno if this is possible at all.. I'm trying to query a large set of data with relations like so:
Parent::with([
'child' => function($query) {
$query->('published', '=', true);
$query->with('child.of.child', 'some.other.child');
$query->chunk(400, function($childs) {
// how is it now possible to add the $childs to the parent result??
});
}
]);
$parent = [];
Parent::with(
[
'childs' => function ($query) use (&$parent) {
$query->where('STATUS', '!=', 'DELETED');
$query->with('some.child', 'some.other.child');
$parent['models'] = $query->getParent()
->getModels();
$query->chunk(
400, function ($result) use ($query, &parent) {
$query->match($parent['models'], $result, 'child-relations-name');
});
}
])
->get();
Now $parent['models'] contains the tree with all the nested child relations... Dunno if this is the smartest way to do so, but it works for now.

CakePHP: Rename field name without change the hierarchy

I have this function write in a CakePHP model:
public function getPeopleByName($name){
$this->unbindModel(array('hasMany' => array('OfficePersonTask')));
$options['fields'] = array("Person.id", "CONCAT(Person.first_name, ' ', Person.last_name) AS full_name");
return $this->find('all', $options);
}
This gave me the following json:
{
People:[
{
0:{
full_name:"Groucho Marx"
},
Person:{
id:"1"
}
},
{
0:{
full_name:"Giovanni Ferretti"
},
Person:{
id:"2"
}
}
]
}
I would that *full_name* will be part of Person group (actually is in a group called 0, all alone). How I can do that?
Use a virtual field in the model rather than a MySQL function in your find. There are some ways to query for data as you are trying to, but you have to account for data being returned in an indexed array rather than the normal associative.

Resources