How to get contain clause to generate the correct code in CakePHP - cakephp

I have the following code in a Table definition.
$paginationQuery = $this->find()
->select([
'Members__id','Members__member_type','Members__first_name',
'Members__middle_name','Members__last_name','Members__suffix'
])
->contain([
'SocialMembers' => [
'foreignKey' => false,
'queryBuilder' => function (Query $q) {
return $q->where([
'Members.Members__id' => 'SocialMembers.full_member_id'
]);
}
]
])
->from([
$this->getAlias() => $query
])
->order([
'Members__last_name' => 'ASC',
'Members__first_name' => 'ASC'
]);
return $paginationQuery;
This is to paginate the results of a union of two sets of extracted data.
The problem comes from the queryBuilder function. The left join that is generated looks like this:
LEFT JOIN members SocialMembers ON (
SocialMembers.member_type = 2
AND Members.Members__id = 'SocialMembers.full_member_id'
)
There is an unneeded pair of single quotes around SocialMembers.full_member_id. queryBuilder appears to correctly handle the Members.Members__id, but not the value field of the array. Is there any way to get this to generate correctly?

Was able to resolve this by moving the contains clause to the preceding finder methods.

Related

Select Distinc on cakephp 3 return wrong fields

this function should return the field of the table I want, but this doesn't happen, return all field of the table, with simply sql work fine "SELECT DISTINCT especie FROM packages"
public function listSpicies()
{
$packages = $this->Packages->find('all')
->select('especie')
->distinct('especie');
$this->set([
'success' => true,
'data' => $packages,
'_serialize' => ['success', 'data']
]);
}
I think You can use something like this:
$packages = $this->Packages->find('all' , [
'fields' => [
'anyAlias' => 'DISTINCT(espiece)'
]
])
->toArray();
Notice. If this collection is serialized and outputted as a JSON, check \App\Model\Entity\Package - if espiece is inside $_hidden array - remove this from array

CakePHP3: cannot get matching working properly

If I write the following query:
$sites = $this->Sites->find()
->contain(['Agthemes'])
->matching('Agthemes', function ($q) {
return $q->where([
'Agthemes.site_id IS NOT' => null
]);
})
->all();
I only get Sites which have existing Agthemes.
Now I write a similar query but with one additional association level:
$users = $this->Users->find('all')
->contain([
'Sites.Agthemes'
])
->matching('Sites.Agthemes', function ($q) {
return $q->where([
'Agthemes.site_id IS NOT' => null
]);
})
->distinct(['Users.id'])
->limit(5)
->all();
And in that case, I also get Sites with empty Agthemes.
Could you tell me why?
EDIT
I add the relationships
SitesTable
$this->hasMany('Agthemes', [
'dependent' => true,
'cascadeCallbacks' => true,
]);
$this->belongsToMany('Users', [
'joinTable' => 'sites_users',
]);
UsersTable
$this->belongsToMany('Sites', [
'targetForeignKey' => 'site_id',
'joinTable' => 'sites_users',
]);
AgthemesTable
$this->belongsTo('Sites');
In DebugKit, look at the queries being run. Using 'contain' often runs completely separate queries, then combines the results (depends on the association type).
If you want to be sure to limit the results based on conditions against an associated model, use 'joins' instead of 'contain'.
See this page for details about how the associations use (or don't use) joins, and how you can change the join strategy...etc:
https://book.cakephp.org/3.0/en/orm/associations.html

Cakephp3.1: Using matching() and notMatching() on the same associated model at once

I want do implement a search function for recipes and their associated ingredients. The user should specify ingredients that he wants to exclude from the search and at the same time ingredients that are contained in the recipes he is looking for.
Those are my two finders:
public function findByContainingIngredients(Query $query, array $params)
{
$ingredients = preg_replace('/\s+/', '', $params['containing_ingredients']);
if($ingredients) {
$ingredients = explode(',', $ingredients);
$query->distinct(['Recipes.id']);
$query->matching('Ingredients', function ($query) use($ingredients) {
return $query->where(function ($exp, $query) use($ingredients) {
return $exp->in('Ingredients.title', $ingredients);
});
});
}
return $query;
}
public function findByExcludingIngredients(Query $query, array $params)
{
$ingredients = preg_replace('/\s+/', '', $params['excluding_ingredients']);
if($ingredients) {
$ingredients = explode(',', $ingredients);
$query->distinct(['Recipes.id']);
$query->notMatching('Ingredients', function ($query) use ($ingredients) {
return $query->where(function ($exp, $query) use ($ingredients) {
return $exp->in('Ingredients.title', $ingredients);
});
});
}
return $query;
}
In the controller I call:
$recipes = $this->Recipes->find()
->find('byExcludingIngredients', $this->request->data)
->find('byContainingIngredients', $this->request->data);
If the user excludes an ingredient from the search and specifies one ore more ingredient that he wants to include, there are zero results.
When I take a look at the generated SQL I see the problem:
SELECT
Recipes.id AS `Recipes__id`,
Recipes.title AS `Recipes__title`,
.....
FROM
recipes Recipes
INNER JOIN ingredients Ingredients ON (
Ingredients.title IN (: c0)
AND Ingredients.title IN (: c1)
AND Recipes.id = (Ingredients.recipe_id)
)
WHERE
(
Recipes.title like '%%'
AND (Ingredients.id) IS NULL
)
GROUP BY
Recipes.id,
Recipes.id
The problem is "AND (Ingredients.id) IS NULL". This line makes the results from the including ingredients disappear.
My approaches:
Creating an alias when calling notMatching() on the association twice. I think this is not possible in Cake3.1
Using a left join on the PK/FK and the excluded title and creating an alias. Basically writing my own notMatching function. This works, but it does not feel right.
Are there other solutions?
To anybody coming to this page and concluding you cannot combine a matching() and notMatching() on the same associated class:
Yes, it is possible (as of Cake 3.4.9 anyway) to do such a find. But you have to use a different alias for the target table - that is an alias that is different to the usual class name.
So in OP's situation, you would put this in RecipesTable.php :
public function initialize(array $config) {
... usual stuff
$this->belongsToMany('Ingredients', [
'foreignKey' => 'recipe_id',
'targetForeignKey' => 'ingredient_id',
'joinTable' => 'ingredients_recipes'
]);
// the next association uses an alias,
// but is otherwise *exactly* the same as the previous assoc.
$this->belongsToMany('ExcludedIngredients', [
'className' => 'Ingredients',
'foreignKey' => 'recipe_id',
'targetForeignKey' => 'ingredient_id',
'joinTable' => 'ingredients_recipes'
]);
}
And you should be able to write a find statement like this:
$this->find()
-> ... usual stuff
->matching('Ingredients',function($q) use($okIngredients) {
... check for ingredients ...
})
->notMatching('ExcludedIngredients', function($q) use($excludedIngredients) {
... check for ingredients ...
});
This does work. Unfortunately, when I used it in an analogous situation with thousands of rows in my 'Recipes' table the query took 40 seconds to run. So I had to go back and replace the notMatching() by a hand-crafted join anyway.
I think what you could do is manually join ingridients table once more with different alias (http://book.cakephp.org/3.0/en/orm/query-builder.html#adding-joins) and then use it in matching/notMatching

Count in contain Cakephp 3

I have a table Post and this has a has-many association with a table Stars.
I can get all the associated data using:
$this->Posts->find()->contain(['Stars']);
That works well.
But I want to count the Stars. I have tried this but its not working:
$this->Posts->find->contain([
'Stars' => function($q) {
return $q->select(['total' => $q->func()->count('Stars.post_id')]);
}
]);
//I've also tried this
...
...$q->select(['total' => "COUNT(Stars.post_id)"]);
...
//Also fail
This does not return the number of associated Stars.
Is there something wrong or should do it some other way?
Thanks
you have to select also the foreign key otherwise cake is not able to join the tables. And you have also to group the result
'Stars' => function($q) {
$q->select([
'Stars.post_id',
'total' => $q->func()->count('Stars.post_id')
])
->group(['Stars.post_id']);
return $q;
}
As here we have used total as virtual field, can be create more like this in same model as:
public function index()
{
$checklist = TableRegistry::get('Checklists');
$query = $checklist->find()
->where('Checklists.is_deleted = 0')
->contain([
'ChecklistTitles' => function($q) {
return $q -> select([
'ChecklistTitles.title',
'ChecklistTitles.checklist_id'
]);
},
'ChecklistTypes' => function($w){
return $w->select(['ChecklistTypes.type']);
},
'AssignedChecklists' => function($e){
$e->select([
'AssignedChecklists.checklist_id',
'completed' => $e->func()
->count('AssignedChecklists.checklist_id'),
])
->group(['AssignedChecklists.checklist_id'])
->where(['AssignedChecklists.is_deleted = 0 AND AssignedChecklists.checklist_status = 2']);
return $e;
}
]);
// ->toArray();
// pr($query);die;
$this->paginate = [
'limit' => 20,
'sortWhitelist' => [
'id', 'checklist_title', 'checklist_type'
]
];
$this->set('query', $this->paginate($query));
$this->set(compact('checklists','query'));
$this->set('_serialize', ['checklists','query']);
}
As here I have calculated completed, I want to calculate cancelled with different where condition, what will be the syntax for it in cakephp3?
Try this:
$total = $this->Posts->find()->contain(['Stars'])->count();
As refereed in the cookbook.

Cakephp 3 : How to add order in queryBuilder for associative data?

Here I have a events field which have a realtion with passwords.I need to order all passwords by download_count DESC. So I have tried below code. It's not giving me any error but desc is not working.
$event = $this->Events->get($id, [
'contain' => [
'EventPasswordAll.Passwords' => [
'queryBuilder' => function (Query $q) {
return $q->order(['download_count' => 'DESC']);
}
]
]
]);
I have also tried
return $q->order(['Passwords.download_count' => 'DESC']);
I get the query
SELECT EventPasswordAll.id AS `EventPasswordAll__id`, EventPasswordAll.event_id AS `EventPasswordAll__event_id`, EventPasswordAll.password_id AS `EventPasswordAll__password_id`, Passwords.id AS `Passwords__id`, Passwords.name AS `Passwords__name`, Passwords.collection_id AS `Passwords__collection_id`, Passwords.photo_count AS `Passwords__photo_count`, Passwords.download_count AS `Passwords__download_count`, Passwords.created AS `Passwords__created`, Passwords.expires_at AS `Passwords__expires_at`, Passwords.recommended AS `Passwords__recommended`, Passwords.accepted AS `Passwords__accepted`, Passwords.user_id AS `Passwords__user_id` FROM event_password_all EventPasswordAll INNER JOIN passwords Passwords ON Passwords.id = (EventPasswordAll.password_id) WHERE EventPasswordAll.event_id in (14)
How to add order in queryBuilder for associative data ?
With an association of EventPasswordAll belongsTo Passwords, the passwords table record will be retrieved via a join, as can be seen in the query. In this case there is no query for Passwords, and thus the query builder is not being invoked.
You have to set the order for the EventPasswordAll query builder instead, like
$event = $this->Events->get($id, [
'contain' => [
'EventPasswordAll' => [
'queryBuilder' => function (Query $q) {
return $q->order(['Passwords.download_count' => 'DESC']);
},
'Passwords'
]
]
]);

Resources