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?