Save associations with Cakephp 3 - cakephp

I have a problem with CakePHP 3 and saving new entity and its associations with one operation.
In my opinion I do it like recommended in the documentation.
Here my controller:
$articles = TableRegistry::get('Articles');
$article = $articles->newEntity($this->request->data);
if ($this->request->is('post')) {
$valid = $articles->validate($article, [
'associated' => ['Topics']
]);
if ( $valid ) {
$articles->save($article, [
'validate' => false,
'associated' => ['Topics']
]);
}
}
Thats my models:
class ArticlesTable extends Table {
public function initialize(array $config) {
$this->primaryKey('article_id');
$this->belongsTo ( 'Topics', [
'targetForeignKey' => 'topic_id'
]);
}
}
class TopicsTable extends Table {
public function initialize(array $config) {
$this->primaryKey('topic_id');
$this->hasMany ( 'Articles', [
'targetForeignKey' => 'article_id'
]);
}
And this is my database:
CREATE TABLE `articles` (
`article_id` int(11) NOT NULL AUTO_INCREMENT,
`topic_id` int(11) DEFAULT NULL,
PRIMARY KEY (`article_id`)
) ENGINE=InnoDB AUTO_INCREMENT=14 DEFAULT CHARSET=latin1;
CREATE TABLE `topics` (
`topic_id` int(11) NOT NULL AUTO_INCREMENT,
`topic_title` varchar(45) DEFAULT NULL,
PRIMARY KEY (`topic_id`)
) ENGINE=InnoDB DEFAULT CHARSET=latin1;
MY form looks like that:
<input type="text" id="articles-heading" maxlength="45" required="required" name="Articles[heading]">
<input type="text" id="articles-topics-topic-title" list="topics" name="Articles[Topics][topic_title]">
I want to create a form which contains all the associations and save it with one request.

Missing columns
There is no column named heading in your articles table.
Invalid foreign key configuration
Your configuration looks wrong, targetForeignKey is for BelongsToMany associations, and article_id (the primary key of the other table) is not the foreign key in an Topic hasMany Comment association, it's topic_id, and it's not necessary to specify it when you're following the naming conventions.
So when specifiying the foreign keys at all, it should look something like this:
class ArticlesTable extends Table {
public function initialize(array $config) {
$this->primaryKey('article_id');
$this->belongsTo ( 'Topics', [
'foreignKey' => 'topic_id'
]);
}
}
class TopicsTable extends Table {
public function initialize(array $config) {
$this->primaryKey('topic_id');
$this->hasMany ( 'Articles', [
'foreignKey' => 'topic_id'
]);
}
}
You could avoid a lot of confusion if you'd stick to the convention and name your primary keys id.
See also
Cookbook > Models > Table Objects > Basic Usage
Cookbook > Models > Table Objects > HasMany Associations
Possibly missing accessibility configuration
Since you are trying to save non-default fields (that is Topic data instead of Topic primary key), you'll have to make the field accessible for mass assignment via newEntity. This is possible for example via the Entitiy::$_accessible property, which is permanent for every single instance of that entity until overriden at runtime
class Article extends Entity {
protected $_accessible = [
// ...
'topic' => true
];
}
or by using the accessibleFields option for Table::newEntity(), which only applies to that one single instance.
$article = $articles->newEntity($this->request->data, [
'accessibleFields' => [
'topic' => true
]
]);
See also
Cookbook > Models > Entities > Mass Assignment
API > Cake\ORM\Table::newEntity()
Field Naming Conventions
Finally your form data is incorrectly formatted, names should by default be lowercase and underscored in order to match the entities properties, and they should be singular (unless it's a hasMany or belongsToMany association), ie it should be article and topic instead of Articles and Topics, also it's actually not necessary to use article at all.
echo $this->Form->input('heading');
echo $this->Form->input('topic.topic_title');
<input type="text" name="heading">
<input type="text" name="topic[topic_title]">
See also Cookbook > Helpers > FormHelper > Field Naming Conventions

Related

CakePHP: to many belongsTo associations contained leads to DB running into JOIN limit

We are currently on CakePHP 3.7.5, using MariaDB as our database engine (10.4.12 on dev, 10.2, 10.2 on client machines).
Some of our table classes have a lot of belongsTo associations. Many of them pointing to a contacts table, but even more to an options table. We use this table to store arbitrary values for different properties of models so that the clients can manage these lists themselves, add entries or change their textual representation while keeping the referenced id.
Main Table: entities
id INT(11)
name VARCHAR(256)
administrator_contact_id INT(11)
account_manager_contact_id INT(11)
property_manager_contact_id INT(11)
...
type_id INT(11)
classification_id INT(11)
status_id INT(11)
category_id INT(11)
...
contacts table
id INT(11)
first_name VARCHAR(256)
last_name VARCHAR(256)
options table
id INT(11)
name VARCHAR(256)
EntitiesTable class
class EntitiesTable extends Table
{
public function initialize(array $config)
{
parent::initialize($config);
$this->addAssociations([
'belongsTo' => [
'AdministratorContact' => [
'className' => 'Contacts',
],
'AccountManagerContact' => [
'className' => 'Contacts',
],
'PropertyManagerContact' => [
'className' => 'Contacts',
],
...
'Type' => [
'className' => 'Options',
],
'Classification' => [
'className' => 'Options',
],
'Status' => [
'className' => 'Options',
],
'Category' => [
'className' => 'Options',
],
...
],
...
]);
}
}
Example query in a Controller
$this->loadModel('Entities');
$query = $this->Entities->find('all', [
'conditions' => [
...
],
'contain' => [
'AdministratorContact',
'AccountManagerContact',
'PropertyManagerContact',
...
'Type',
'Classification',
'Status',
'Category',
....
],
]);
$results = $query->toArray();
Now, for the first time, we ran into MariaDBs limit of 61 JOINs. Our belongsTo associations are added using the default "join" strategy as it reduces the overall number of queries that have to be executed for serving a single request. Changing the strategy of all belongsTo associations to "select" would lead to way too many, unnecessary separate queries.
I was wondering if anybody heard of best practises, or whether there is a behavior that caters to this need. Things that come to my mind:
Before running a query, analyse how many associations are contained which would lead to JOINs, plus anything else that would lead to additional JOINs ("join" clause in the query, LinkableBehavior). If it exceeds the limit, turn some of them into separate SELECTs by amending their "strategy".
Somehow aggregate JOINS coming form belongsTo associations pointing to the same table by collecting the ids, and after the query has run "distribute" the results. Kind of what is done for hasMany associations, but there the results are turned into entities which are then all added as one property (containing an array of results), whereas for belongsTo we would then need to distribute the results back to different properties of the main model.

How to make relationships in cakephp3

I am trying to make the relationship between the tables, but can’t figure out what I doing wrong.
I read The documentations in cakephp3, and tons of post in stackoverflow, and still can’t get any result.
--------------------------
name |invoices |shares |
var |id |id |
var | |idBill |
--------------------------
The relationship should be from idBill in shares and id in invoices
class SharesTable extends Table {
public function initialize(array $config) {
parent::initialize($config);
$this->belongsTo('invoices', [
'className' => 'App\Model\Table\InvoicesTable',
'foreignKey' => 'idBill',
'targetForeignKey' => 'id'
]);
}
}
And in controller try to print the bill associate to the share like this:
public function view($id = null)
{
$share = $this->Shares->get($id, [
'contain' => []
]);
var_dump( $share->invoices );
$this->set('share', $share);
}
I just expect to print the bill but I always get null
Your contain is empty, so you won't receive any associations. Also the default entity property name for belongsTo (and hasOne) associations is the singular, underscored variant of the association name, ie invoice, not invoices.
It's also recommended to use association names that do match the table alias, that is Invoices (with a capital I) for InvoicesTable, that way it will find the class automatically and you don't have to specify it via the className option (also there is no targetForeignKey option for belongsTo associations).
$this->belongsTo('Invoices', [
'foreignKey' => 'idBill'
]);
$share = $this->Shares->get($id, [
'contain' => ['Invoices']
]);
debug($share->invoice);
See also
Cookbook > Database Access & ORM > Retrieving Data & Results Sets > Eager Loading Associations Via Contain
Cookbook > Database Access & ORM > Associations - Linking Tables Together

cakephp: how to display foreign key table field

basically,
database table:
Products: id, name
Comments: productId,comment
Model:
product.php
class Product extends AppModel {
public $hasMany = array('Comment'=>array('foreignkey'=>'productId'));
}
comment.php
class Comment extends AppModel {
public $belongsTo = array('Product');
}
In product index.ctp, how can I display one product comment? What I need to write in ProductsController.php and index.ctp?
If you follow the CakePHP conventions (CakePHP Convention over Configuration) then all of this will be done automagically for you, it requires foreign keys to be named product_id rather than productId (although you have setup the foreign key in the relationship - it is just easier to start from the beginning following the conventions).
You should also specify the class name in the relationship:
public $hasMany = array(
'Comment' => array(
'className' => 'Comment',
'foreignKey' => 'product_id'
)
);
In your case what you should do in the controller is:
$products = $this->Product->find('all');
This will fetch all your products and any associated comments on those Products (and also any other associated models you have declared in the Product Model)
If you want to read more about setting this up you can check out CakePHP - Retrieving your data

Cake Relations with diffent name

I got a Table Users and a Table Groups. Every group has one GroupLeader.
So the field i use in is groupLeader ($hasOne) which contains a foreign key of users.
I cant manage to get that relation. So my question is, how to define a relation on a field with a diffent name.
thanks for a hint.
endo
You model should looks:
class Group extends AppModel
{
public $name = 'Group';
public $belongsTo = array('GroupLeader' => array(
'className' => 'User',
'foreignKey' => 'groupLeader'
)
);
}
Try with the above code. And ask if it is not worked for you.

Has Many relationship how to

I have this model and controller and it returns fine if it is a hasOne relationship but now DishCategory hasMany dishes. When I change the code below to hasMany it gives me an error saying Dish.id is not a known column while if it is a hasOne it works fine. How can I make it so that it returns all the Dish.id's that have id=1? (still using the join).
class DishCategory extends AppModel{
public $hasOne = array(
'Dish' => array(
'className' => 'Dish',
'foreignKey' => 'dish_category_id'
)
);
}
class DishCategoriesController extends AppController {
function get_categories($id)
{
// find category with a dish of $id
$this->set('dishes', $this->DishCategory->find('all', array(
'conditions' => array(
'Dish.id' => $id
)
)));
// set master layout
$this->layout = 'master_layout';
}
}
hasOne relations can be joined into the primary SQL query. hasMany relations cannot and need to be queried in a separate query. The conditions you specify for the query only apply to the primary query, all separate relational queries are build just based on the ids retrieved in the primary query. Set debug to 2 and have a good look at the query log to see what I mean.
To find the category of a dish, fetch the dish and look at its related category record:
$dish = $this->Dish->find('first', array('conditions' => array('Dish.id' => $id)));
echo $dish['DishCategory']['name'];
Since I take it that a dish belongsTo one category, your query "find all categories which have a dish with id x" makes little sense; there should only be one anyway.

Resources