Creating Association with condition using other association in CakePHP 3 - cakephp

I'm building a cake php 3 app. My app model includes 3 Tables (amongst others):
Structures
MeasuringPoints
DeviceTypes
where each Strcuture can have multiple MeasuringPoints:
// StrcuturesTable.php
...
public function initialize(array $config)
{
parent::initialize($config);
...
$this->hasMany('MeasuringPoints', [
'foreignKey' => 'structure_id'
]);
}
Further, each measuring point is of a certain device type:
// MeasuringPointsTable.php
...
public function initialize(array $config)
{
parent::initialize($config);
...
$this->belongsTo('DeviceTypes', [
'foreignKey' => 'device_type_id',
'joinType' => 'INNER'
]);
}
What i'm lookong for, is how to create a 'SpecialMeasuringPoints' association in the Structure table.
Somewhat like:
// MeasuringPointsTable.php
...
$this->hasMany('SpecialMeasuringPoints',[
'className' => 'MeasuringPoints',
'foreignKey' => 'structure_id',
'conditions' => ['MeasuringPoints.DeviceTypes.id'=>1]
]);
As you may see, I want only those measuring points, whose associated device type has the id 1.
However, the previous association condition is not valid; and i have no clue how to correctly implement this.
Any help is appreciated.

Correct, that condition is invalid, for a number of reasons. First of all paths aren't supported at all, and even if they were, you already are in MeasuringPoints, respectively SpecialMeasuringPoints, so there would be no need to indicate that again.
While it would be possible to pass a condition like:
'DeviceTypes.id' => 1
That would require to alawys contain DeviceTypes when retrieving SpecialMeasuringPoints.
I would suggest to use a finder, that way you can easily include DeviceTypes and match against your required conditions. Something like:
$this->hasMany('SpecialMeasuringPoints',[
'className' => 'MeasuringPoints',
'foreignKey' => 'structure_id',
'finder' => 'specialMeasuringPoints'
]);
In your MeasuringPoints class define the appropriate finder, for example using matching(), and you should be good:
public function findSpecialMeasuringPoints(\Cake\ORM\Query $query) {
return $query
->matching('DeviceTypes', function (\Cake\ORM\Query $query) {
return $query
->where([
'DeviceTypes.id' => 1
]);
});
}
Similar could be done via the conditions option when passing a callback, which however is less DRY:
$this->hasMany('SpecialMeasuringPoints',[
'className' => 'MeasuringPoints',
'foreignKey' => 'structure_id',
'conditions' => function (
\Cake\Database\Expression\QueryExpression $exp,
\Cake\ORM\Query $query
) {
$query
->matching('DeviceTypes', function (\Cake\ORM\Query $query) {
return $query
->where([
'DeviceTypes.id' => 1
]);
return $exp;
}
]);
It should be noted that in both cases you need to be aware that such constructs are not compatible with cascading/dependent deletes, so do not try to unlink/delete via such associations!
See also
Cookbook > Database Access & ORM > Retrieving Data & Results Sets > Custom Finder Methods
Cookbook > Database Access & ORM > Retrieving Data & Results Sets > Filtering by Associated Data

Related

Insert/Delete methods for self-referencing belongsToMany association

I’m struggling with a self-referencing belongsToMany association. To be clear I have a Models table and each model can have multiple accessories, which are also models. So I have a linking table, Accessories, with a model_id (the “parent” model) and an accessory_id (the “child” model).
I finally found how to declare this in the ModelsTable :
$this->belongsToMany('AccessoryModels', [
'className' => 'Models',
'through' => 'Accessories',
'foreignKey' => 'model_id',
'targetForeignKey' => 'accessory_id'
]);
$this->belongsToMany('ParentAccessoryModels', [
'className' => 'Models',
'through' => 'Accessories',
'foreignKey' => 'accessory_id',
'targetForeignKey' => 'model_id'
]);
I also got it working to retrieve these data in the Models view.
But I now have some issues for the addAccessory (and deleteAccessory) method in the Models controller and views.
Here is it in the controller :
public function addAccessory($id = null)
{
$model = $this->Models->get($id, [
'contain' => []
]);
if ($this->getRequest()->is(['patch', 'post', 'put'])) {
$accessory = $this->getRequest()->getData();
if ($this->Models->link($model, [$accessory])) {
return $this->redirect(['action' => 'view', $model->id]);
}
}
$models = $this->Models
->find('list', ['groupField' => 'brand', 'valueField' => 'reference'])
->order(['brand' => 'ASC', 'reference' => 'ASC']);
$this->set(compact('model', 'models'));
}
The view is only a select dropdown with the list of all available models (I'm using a plugin, AlaxosForm, but it takes the original CakePHP control() function behaviour) :
echo $this->AlaxosForm->label('accessory_id', __('Accessory'), ['class' => 'col-sm-2 control-label']);
echo $this->AlaxosForm->control('accessory_id', ['options' => $models, 'empty' => true, 'label' => false, 'class' => 'form-control']);
echo $this->AlaxosForm->button(___('Submit'), ['class' => 'btn btn-default']);
The problem is that the addAccessory() function won't work when getting the submitted data from the form. I see the problem, as when posting the inserted values, only an array with one accessory_id is given (for example ['accessory_id' => 1] and the link() doesn't know what to do with it. So I think it’s an issue about data formatting but don’t see how to get it correctly.
Saving links (like any other ORM saving methods) requires to pass entities, anything else will either be ignored, or trigger errors.
So you have to use the accessory_id to obtain an entity first, something like:
$accessoryId = $this->getRequest()->getData('accessory_id');
$accessory = $this->Models->AccessoryModels->get($accessoryId);
Furthermore you need to use the model/table that corresponds to the target entities (second argument) that you want to link (to the first argument), ie you'd have to use AccessoryModels, like:
$this->Models->AccessoryModels->link($model, [$accessory])
See also
Coobook > Database Access & ORM > Saving Data > Associate Many To Many Records

How to use left and inner joins on two different tables in cakephp 3.x

I have table1 'leads' and I want to use two joins left as well as inner applying on two different tables such as 'lead_status' and 'assigned_leads'. I am very much new to cakephp 3.x concept hence could not understand how to write query using multiple joins. Please suggest me how to write that query. Thanks in advance.
this is my association made in leads Table Model:
class LeadsTable extends Table
{
public function initialize(array $config)
{
$this->hasMany('LeadStatus', [
'className' => 'LeadStatus',
'foreignKey' => 'lead_id',
'propertyName' => 'LeadStatus'
]);
$this->hasMany('AssignedLeads', [
'className' => 'AssignedLeads',
'foreignKey' => 'lead_id',
'propertyName' => 'AssignedLeads'
]);
}
}
I am using cake php 3.x. Please suggest how to write multiple joins.
I have tried like this but not giving proper conditionally data
$query = $table->find('all')->leftJoinWith('LeadStatus')->innerJoinWith('AssignedLeads')->contain(['LeadStatus' => function($q) {
return $q->contain(['LeadBuckets', 'LeadBucketSubStatus'])
->where(['LeadStatus.is_active' => 1]);
}],['AssignedLeads' => function($s) {
return $s->contain(['Users'])
->where(['AssignedLeads.is_active' => 1]);
}])->where(['Leads.sub_agent_id' => $subAgentId,
]);

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

cakephp3 condition based associations not works

I am using hasOne association.Here my code for UserMastersTable :
class UserMastersTable extends Table {
public function initialize(array $config) {
parent::initialize($config);
$this->table('user_masters');
$this->hasOne('PersonMasters', [
'className' => 'PersonMasters',
'foreign_key' => 'user_master_id',
'conditions' => ['PersonMasters.status' => 1],
'dependent' => true,
]);
} }
When use find() in my controller.It fetch all user_masters data and person_masters data whose status ='1'.
but problem is that i already assign condition where association bind..already give condition that only display that data whose person_masters.status=1.
so why it shows all data of user_masters ?
if i give condition in find() in controller then it works fine..
$this->UserMasters->find('all',
['contain' =>
['PersonMasters'],
'conditions' =>
['PersonMasters.status' => 1]
]);
so, how can i globally give condition that only fetch data of user_masters and person_masters where PersonMasters.status=1?
Try this might be it will resolve your issue
$this->UserMasters->find('all',[
'contain' =>
['PersonMasters' => [
'conditions' => ['status' => 1]
]
],
]);

Resources