CakePHP 3 - DB table assocciation with itself - php

I have a table in my DB that has a association with itself. I am trying to get all parent categories with their child categories, but I can't get it to work.
This is how the table looks:
id | name | description | image | is_child | forum_category_id | level
Now, obviously the "forum_category_id" is the parent id that refers to the same table.
I baked the model and this is in the table file:
$this->belongsTo('ForumCategories', [
'foreignKey' => 'forum_category_id'
]);
$this->hasMany('ForumCategories', [
'foreignKey' => 'forum_category_id'
]);
The code I use to load from DB is this:
debug($results = $this->find()
->order(['id' => 'ASC'])
->where(['is_child' => 0])
->toArray()
);
With this code, I do get the parent categories, but not the children.
So I thought to use "Contain", but that only returns empty parent categories.
debug($results = $this->find()
->order(['id' => 'ASC'])
->where(['is_child' => 0])
->contain([
'ForumCategories' => function ($q)
{
return $q
->where(['is_child' => 1]);
}
])
->toArray()
);
I have no idea how to get the child categories. I read something about using "Threaded" (no results/success so far) or the TreeBehaviour, but I don't really any idea on how to use them.
Any idea on how this is still possible is much appreciated!

You should use different aliases for the 2 association.
$this->belongsTo('ForumCategories', [
'foreignKey' => 'forum_category_id'
]);
$this->hasMany('ForumChildCategories', [
'className' => 'ForumCategories',
'foreignKey' => 'forum_category_id'
]);
By this $this->ForumCategories->find() will give you the parent and $this->ForumChilfCategories->find() the children.
Otherwise - if that is option - change your database schema and use tree behaviour.

Related

CakePHP 3: belongsToMany (through) and additional associations

I have defined the following associations:
class RecipesTable extends Table
{
$this->belongsToMany('Ingredients', [
'through' => 'RecipesIngredients',
'foreignKey' => 'recipe_id',
'targetForeignKey' => 'ingredient_id',
]);
class IngredientsTable extends Table
{
$this->belongsToMany('Recipes', [
'through' => 'RecipesIngredients',
'foreignKey' => 'ingredient_id',
'targetForeignKey' => 'recipe_id',
]);
class RecipesIngredientsTable extends Table
{
$this->belongsTo('Recipes');
$this->belongsTo('Ingredients');
$this->belongsTo('Units');
The table 'RecipesIngredients' has the following structure:
id | recipe_id | ingredient_id | unit_id | ...
Now I make a request like the one below to get Recipes and the associated Ingredients. But without the Units.
$data = $this->Recipe->find('all')
->where('Recipe.id' => 55)
->contain(['Ingredient', ...])
->all();
My question is: how do I get the data of the associated 'Units' in a call of $this->Recipe?
I tried different contains like ->contain(['Ingredient' => ['Unit'], ...]) (and so on) but this doesn't work. CakePHP just returns the associated ingredients and the contents of the 'through' join table without linking to the associated units. Or gives an error of missing associations.
That won't work using contain(), at least not with a belongsToMany association, as the on-the-fly created intermediate association for the join table is being created too late for the eager loader to recognize it.
What you can do is explicitly create the otherwise on-the-fly generated hasMany association for the join table manually, eg on the RecipesTable class add:
$this->hasMany('RecipesIngredients', [
'foreignKey' => 'recipe_id'
]);
Then you can contain your associations like:
->contain(['RecipesIngredients' => ['Ingredients', 'Units']])

CakePhp 3 - link and save associations with a table which holds two foreign keys of same model

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

Model association on same table (for menu and submenu concept) in cakephp 3.2

I have tried a lot for the model association in cake 3. There is one table menu,it consists of both the menu and sub menu .Here i want to bind parent menu with its child menu , but i have not yet succeeded to grab it.
Below i have written the code as well as attached a screenshot,
Thank you .
$this->hasMany('Menus', [
'className' => 'Menus',
'foreignKey' => 'parent_id',
]);
This is the bind in Menus model
$getListOfAllParentMenus = $this->Menus->find('all')->where(['Menus.parent_id' => 0])->contain('Menus')->order(['Menus.id DESC'])->toArray();
This is the controller code.
Thank you.
You should change the name of the associated model since you can't contain on the same model. Change Menus to SubMenus.
$this->hasMany('SubMenus', [
'className' => 'Menus',
'foreignKey' => 'parent_id',
]);
$getListOfAllParentMenus = $this->Menus->find('all')
->where(['Menus.parent_id' => 0])
->contain('SubMenus')
->order(['Menus.id DESC'])
->toArray();

CakePHP 3: TranslateBehavior on a model that works as an alias

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()

Grouping a select list (optgroup) in CakePHP 3

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;
}
]);

Categories