I have this model:
Proforma
->hasMany('ItemProformas', ['foreignKey' => 'proforma_id']);
->belongsTo('Customers', ['foreignKey' => 'customer_id']);
->belongsTo('ProformaStates', ['foreignKey' => 'proforma_state_id']);
->hasMany('Invoices', ['foreignKey' => 'proforma_id']);
ItemProformas
->belongsTo('Proformas', ['foreignKey' => 'proforma_id', 'joinType' => 'INNER']);
->belongsTo('ItemDeliveryNotes', ['foreignKey' => 'item_delivery_note_id']);
ItemDeliveryNotes
->belongsTo('DeliveryNotes', ['foreignKey' => 'delivery_note_id', 'joinType' => 'INNER']);
->belongsTo('ItemOrders', ['foreignKey' => 'item_order_id']);
->belongsTo('ItemOrdersTypes', ['foreignKey' => 'item_orders_type_id']);
->belongsTo('Products', ['foreignKey' => 'product_id']);
Each ItemProforma may have one ItemDeliveryNotes, otherwise the foreign key will be null. Here my paginate call:
$this->paginate = [
'contain' => [
'Customers',
'ProformaStates',
'ItemProformas' => ['ItemDeliveryNotes' => ['DeliveryNotes']]
]
];
With this model, I get all the itemProforma that have item_delivery_note_id set. Instead I'm interesed to get them all, even if item_delivery_note_id is null.
I'm not sure if belongsTo is correct here (I mean in ItemProformas definition). But hasOne implies it has one associated row, not may have one.
What is the correct syntax to retrieve all itemProformas even if they don't have any ItemDeliveryNote associated? But if they have, I need to retrieve the ItemDeliveryNote object as well.
The association type depends on your schema. If the foreign key is in the source table, then it's belongsTo, if the foreign key is in the target table, then it's hasOne.
Whether a related record must exist primarily depends on the schema too, not on the type of association. If the foreign key is nullable, then the related record is optional. If and how you implement enforcing that constraint on application level is a different story.
That being said, ItemDeliveryNotes and DeliveryNotes are both belongsTo that will use joins by default, so both associations will be joined into the same query, and since you've configured the DeliveryNotes association to use an INNER join, it will exclude rows where no DeliveryNotes exist, which of course is also the case when no ItemDeliveryNotes exist.
Assuming your schema is modeled correctly/properly, you could for example change your association config to use a LEFT join by default in case applicable, or you could change the configuration for the containment on a per query basis (being it manually, or by using a custom finder):
$this->paginate = [
'contain' => [
'Customers',
'ProformaStates',
'ItemProformas' => [
'ItemDeliveryNotes' => [
'DeliveryNotes' => [
'joinType' => \Cake\Database\Query::JOIN_TYPE_LEFT,
],
],
],
],
];
Changing the fetching strategy for ItemDeliveryNotes could work too (though it might be quite taxing depending on the amount of records), ie using the select strategy instead of the join strategy, then the associated ItemDeliveryNotes records are being retrieved in a separate query, and thus won't affect retrieval of ItemProformas:
$this->paginate = [
'contain' => [
'Customers',
'ProformaStates',
'ItemProformas' => [
'ItemDeliveryNotes' => [
'strategy' => \Cake\ORM\Association::STRATEGY_SELECT,
'DeliveryNotes',
],
],
],
];
Related
I would like to create a junction table tbl_guid_cost_centre that gets taken care of without me manually saving it to the database. I tried adding this to my relations:
'costCentre' => [
self::HAS_ONE,
'CostCentre',
'guid_to',
'foreignKey' => 'guid',
'tbl_guid_cost_centre(guid_to, cost_center_id)',
"order" => "id desc"],
so that my when saving the costCentre, a row is created for it in my tbl_guid_cost_centre. However I'm getting the error:
Property "CHasOneRelation.0" is not defined.
Any suggestion?
You can have your junction table with the keyword through in your relations:
public function relations() {
'guidCostCentre' => [
self::HAS_ONE,
'GuidCostCentre',
['guid_to' => 'guid']
],
'costCentre' => [
self::HAS_ONE,
'CostCentre',
'cost_centre_id',
'through' => 'guidCostCentre'
]
}
You're defining HAS_ONE relation in a wrong way. The first three elements of relation configuration array should be: relation type, related model name and foreign keys definition. All further elements should be indexed by keys related to relation properties. 'tbl_guid_cost_centre(guid_to, cost_center_id)', probably generates this error, because it does not have a key, so it is treaded as a value for 0 property. You didn't share any details, so it is hard to guess what you want to achieve, but you should start from something like this:
'costCentre' => [
self::HAS_ONE,
'CostCentre',
'guid_to',
'order' => 'id desc',
],
And add additional settings at the end array with correct key.
Some examples and explanation you can find in the documentation: https://www.yiiframework.com/doc/guide/1.1/en/database.arr#declaring-relationship
i have the following two tables:
recipes
similiar_recipes
As you can see similar_recipes has two foreign keys which both point to recipes. Now I want two things. First of the linking. I read on stackoverflow some similar stuff and come up with the following configuration:
RecipesTable.php
$this->hasMany('Recipes', [
'foreignKey' => 'recipe_id',
'className' => 'SimilarRecipes'
]);
$this->hasMany('SimilarRecipes', [
'foreignKey' => 'similar_recipe_id',
'className' => 'SimilarRecipes'
]);
SimilarRecipesTable.php
$this->belongsTo('Recipes', [
'foreignKey' => 'recipe_id',
'joinType' => 'INNER',
'className' => 'Recipes'
]);
$this->belongsTo('SimilarRecipes', [
'foreignKey' => 'similar_recipe_id',
'joinType' => 'INNER',
'className' => 'Recipes'
]);
The configuration should be correct. Now the other question is the correct associated saving or lets ask better is it possible to do the following:
Recipes Data
Now in Cake I want to add a recipe and the associated recipes which are delivered in the request->data as an id-array
$newRecipe = $this->Recipes->newEntity();
$newRecipe = $this->Recipes->patchEntity($newRecipe, $this->request->data);
$this->Recipes->save($newRecipe, ['associated' => ['SimilarRecipes']])
This should be the result:
In conclusion I saved a new recipe which gets the id 3. In the request->data I select the similar recipes 1 and 2.
Could someone give me an advice. Is my configuration wrong. Or what do I have to pass to the save method? By the way I don't get any errors.
Use belongsToMany associations instead
I'd say your association approach is wrong (or at least makes things unnecessarily complicated), I'd suggest to use belongsToMany associations instead, as what you seem to create there is a self-joining many-to-many relation.
Name the table recipes_similar_recipes, that's the convention CakePHP uses, it helps to avoid association name conflicts/confusion, and allows relying on magic configuration. Your tables associations/configuration should then look something like:
RecipesTable.php
$this->belongsToMany('SimilarRecipes', [
'foreignKey' => 'recipe_id',
'targetForeignKey' => 'similar_recipe_id',
'joinTable' => 'recipes_similar_recipes'
]);
SimilarRecipesTable.php
$this->table('recipes');
$this->primaryKey('id');
$this->belongsToMany('Recipes', [
'foreignKey' => 'similar_recipe_id',
'targetForeignKey' => 'recipe_id',
'joinTable' => 'recipes_similar_recipes'
]);
With such a setup you could then use the array of IDs variant, eg use the _ids key to define the existing (similar) recipies that should be associated with your new recipe, ie the request data should look something like:
[
// there is no title column in your example,
// this should just clarify the data structure
'title' => 'New recipe that will get the ID 3',
'similar_recipes' => [
'_ids' => [
1, 2
]
]
]
which should populate the recipes_similar_recipes table like:
+----+-----------+-------------------+
| id | recipe_id | similar_recipe_id |
+----+-----------+-------------------+
| 1 | 3 | 1 |
| 2 | 3 | 2 |
+----+-----------+-------------------+
You should then also consider making recipe_id and similar_recipe_id a compound primary key, or at least create a unique index with them.
See also
Cookbook > Database Access & ORM > Associations - Linking Tables Together > BelongsToMany Associations
Cookbook > Database Access & ORM > Saving Data > Converting BelongsToMany Data
I know the Question title is a bit murky, but here's what I'm trying to do:
I'm retrieving a list of groups that a user belongs to from a third party api. In some cases, the user will be an 'admin' for a group and other times, just a 'member'.
Specifics aside, I'm calling a method on my api class from my controller that hits the api, retrieves the user's groups, decides if they are an 'admin' or not, then returns an array of arrays with each group's information including a 'role' key that denotes whether or not they are an 'admin'. So my returned array looks something like this:
[
0 => [
'unique_id' => 1243657,
'name' => 'Group1',
'city' => 'Bluesville',
'state' => 'IN',
'role' => 'admin'
],
1 => [
'unique_id' => 4324567,
'name' => 'Group2',
'city' => 'New Curtsbourough',
'state' => 'WI',
'role' => 'member'
],
2=> [
'unique_id' => 87463652,
'name' => 'Group3',
'city' => 'Samsonite',
'state' => 'MN',
'role' => 'member'
]
]
Now, I need to take those groups and store them in the database, which I'm doing by checking first that the group doesn't exist in the database, then adding it if needed. Of course, I'm leaving off the role, as it is only relevant to the current user.
Next, I need to connect the current user to these groups that were just retrieved. I have a pivot table set up that currently holds the user_id and group_id.
The question is, how to best handle this. Before I decided that I needed to know whether or not a member was an 'admin' or not, I simply had my 'createGroups' method return an array of primary keys to me, then passed that array to a call to
$user->groups()->sync($array_of_ids);
However, with the added 'role' information, it's not as cut and dry.
Basically, at this point in the lifecycle, I have access to an array of groups that contains a field 'role'. My thinking says to add a 'role' field to the pivot table, which would then contain 'user_id', 'group_id' and 'role'. This means I'll not only need the $groups array with the retrieved groups, but the ids of those groups as they pertain to my database.
I could make something work, but I'm afraid it would be extremely messy and inefficient.
Thoughts anyone??
Ok, as happens many times on Stackoverflow, I've come to a solution for my own question. I'm posting so that in the off-chance someone stumbles upon my question needing to do something similar, they can at least see how one person handled it.
According to the Laravel docs, if you want to sync relationships with an added column, you need to call sync in the following way:
$user->groups()->sync([
1 => ['role' => 'admin'],
2 => ['role' => 'member'],
3 => ['role' => 'member']
]);
So before I could sync, I needed an array that resembled the array that is being passed to 'sync'.
Since I had an array of 'groups' that included a field called 'role' for each group, I created a 'createGroups' method that basically looped over the $groups array and called the 'insertGetId' method that Laravel provides. This method persists the object to the database and returns the primary key of the created record. For my 'createGroups' method, I did the following:
public function createGroups($groups)
{
$added = array();
foreach($groups as $group){
$id = $this->createGroup($group);
$added[$id] = ['role' => $group['role']];
}
return $added;
}
So as I'm inserting 'groups' into the database, I'm building up the array that is needed by the 'sync' method. Since the 'createGroup' method uses Laravel's 'insertGetId' method, it returns the primary key for that group. Then I use that id as the key to the array. After all groups are inserted, my 'added' array that is returned to my controller, looks like this:
[
1 => ['role' => 'admin'],
2 => ['role' => 'member'],
3 => ['role' => 'member']
]
which is exactly what the 'sync' method needs to do it's thing.
Happy coding!
In a project I have two models, Products and Packages. Packages can be seen as containers of Products and to define the items in a package I've created a model PackageItem (which is basically a Product so its using the same table). Now Products (and so PackageItems) have translatable fields such as as a title and description.
ProductsTable.php contains:
$this->addBehavior('Translate', [
'fields' => ['title', 'description'],
'translationTable' => 'products_translations'
]);
$this->belongsToMany('PackageItems', [
'foreignKey' => 'package_id',
'joinType' => 'LEFT',
'joinTable'=>'products_package_items'
]);
PackageItemsTable contains:
$this->table('products');
$this->addBehavior('Translate', [
'fields' => ['title', 'description'],
'translationTable' => 'products_translations'
]);
$this->belongsTo('Products', [
'foreignKey' => 'package_item_id',
'joinType' => 'LEFT'
]);
Using TranslateBehavior I'm able return the translations on the Product but I can't figure out how to write the query I need to also return the translation on the PackageItems. This is my current query:
$package = $this->Products->find('translations')
->where(['business_id'=>$q['business_id'], 'id'=>$id, 'type'=>'Package'])
->contain([
'PackageItems'=>[
'Prices'=>function($q) {
return $q->where(['product_id'=>$this->product_id]);
}
]
])
->first();
You need two things
1) Set the proper reference name
The translate behavior on the PackageItemsTable class needs to be configured to use the same reference name (the value that is stored in the model column) as the behavior on the ProductsTable class, otherwise you'd never receive any translations, as it would by default look for PackageItems.
This is what the referenceName option can be used for. The reference name is being derived from the class name (not the alias), or for auto-tables, from the database table name or the alias. So for your ProductsTable class it would be Products.
Either set the name manually
$this->addBehavior('Translate', [
'fields' => ['title', 'description'],
'translationTable' => 'products_translations',
'referenceName' => 'Products' // there it goes
]);
or retrieve it dynamically from the behavior on the ProductsTable, like
$referenceName = $this->Products
->target()
->behaviors()
->get('Translate')
->config('referenceName');
This however would need to be done after adding the corresponding belongsTo association for the Products table!
2) Use the translations finder for the containment
You need to configure the PackageItems containment to use the translations finder, which is as simple as
contain([
'PackageItems' => [
'finder' => 'translations', // there you go
'Prices' => function ($q) {
return $q->where(['product_id' => $this->product_id]);
}
]
])
See also
API > \Cake\ORM\Behavior\TranslateBehavior::_referenceName()
API > \Cake\ORM\Behavior\TranslateBehavior::$_defaultConfig
API > \Cake\ORM\Query::contain()
I am trying to make a list of grouped things in CakePHP 3, to create a grouped list of things in a select list in a form. I'm not sure if I am missing something or if I'm expecting too much of Cake and should be doing more myself.
I have a controller called Issues and a self-referencing column called RelatedIssues. Each Issue belongs to a System, and it's the systems I want the issues grouped by.
In my IssuesTable.php:
$this->belongsTo('RelatedIssues', [
'className' => 'Issues',
'foreignKey' => 'issue_id'
]);
$this->belongsTo('Systems', [
'foreignKey' => 'system_id',
'joinType' => 'INNER'
]);
...and in my IssuesController's edit method:
$relatedIssues = $this->Issues->RelatedIssues->find('list', [
'groupField' => 'system_id'
]);
When I get to the drop-down list, items are grouped by system_id as specified, but I cannot figure out how to get them grouped by the System's title field. Is this even possible, or do I have to write a nice nested foreach structure to do this myself?
should be (can'try it now):
$relatedIssues = $this->Issues->RelatedIssues->find('list', [
'groupField' => 'system.title'
])->contain('Systems');
Consider the following, is more clear:
$relatedIssues = $this->Issues->RelatedIssues->find('list', [
'contain' => ['Systems'],
'order' => [ 'Systems.title' => 'ASC', 'RelatedIssues.title' => 'ASC'],
'groupField' => function($entity) {
return $entity->system->title;
}
]);