CakePHP 3.0 Can't save hasMany associated data - cakephp

I've problem when saving data with hasMany association
This is my table
1) post table: each item has an unique id.
id | title | ...
1 | Aloha | ...
2) images table
id | post_id | image | ...
1 | 1 | abc.jpg | ...
2 | 1 | efg.jpg | ...
My Model (Table)
Posts Model
// PostsTable.php
<?php
namespace App\Model\Table;
use Cake\ORM\Query;
use Cake\ORM\Table;
use Cake\Validation\Validator;
class PostsTable extends Table {
public function initialize(array $config) {
$this->table('posts');
$this->displayField('title');
$this->primaryKey('id');
$this->addBehavior('Timestamp');
$this->hasMany('Images', [
'foreignKey' => 'id'
]);
}
}
...
Images Model
// ImagesTable.php
<?php
namespace App\Model\Table;
use Cake\ORM\Query;
use Cake\ORM\Table;
use Cake\Validation\Validator;
class ImagesTable extends Table {
public function initialize(array $config) {
$this->table('images');
$this->displayField('id');
$this->primaryKey('id');
$this->addBehavior('Timestamp');
$this->belongsTo('Posts');
}
}
...
My Controller
// PostsController.php
...
public function add() {
$post = $this->Posts->newEntity($this->request->data, [
'associated' => ['Images']
]);
if ($this->request->is('post')) {
if ($this->Posts->save($post, ['associated' => ['Images']])) {
$this->Flash->success('The post has been saved.');
return $this->redirect(['action' => 'index']);
} else {
$this->Flash->error('The post could not be saved. Please, try again.');
}
}
$this->set('post', $post);
}
...
My Template
// add.ctp
<?= $this->Form->create($post); ?>
<?php echo $this->Form->input('title'); ?>
<?php echo $this->Form->input('images.0.image'); ?>
<?php echo $this->Form->input('images.1.image'); ?>
<?php echo $this->Form->input('images.2.image'); ?>
<?= $this->Form->button(__('Submit'), ['class' => 'button-green']) ?>
<?= $this->Form->end() ?>
Input array result Debug
[
'title' => 'Hello',
'images' => [
(int) 0 => [
'image' => 'testa.jpeg'
],
(int) 1 => [
'image' => 'testb.jpeg'
],
(int) 2 => [
'image' => 'testc.jpeg'
]
]
]
(Update)
debug($post)
object(App\Model\Entity\Story) {
'new' => true,
'accessible' => [
'title' => true,
'images' => true
],
'properties' => [
'title' => 'Hello',
'images' => [
(int) 0 => object(App\Model\Entity\Image) {
'new' => true,
'accessible' => [
'post_id' => true,
'image' => true,
'post' => true
],
'properties' => [
'image' => 'testa.jpeg'
],
'dirty' => [
'image' => true
],
'original' => [],
'virtual' => [],
'errors' => [],
'repository' => 'Images'
},
(int) 1 => object(App\Model\Entity\Image) {
'new' => true,
'accessible' => [
'post_id' => true,
'image' => true,
'post' => true
],
'properties' => [
'image' => 'testb.jpeg'
],
'dirty' => [
'image' => true
],
'original' => [],
'virtual' => [],
'errors' => [],
'repository' => 'Images'
},
(int) 2 => object(App\Model\Entity\Image) {
'new' => true,
'accessible' => [
'post_id' => true,
'image' => true,
'post' => true
],
'properties' => [
'image' => 'testc.jpeg'
],
'dirty' => [
'image' => true
],
'original' => [],
'virtual' => [],
'errors' => [],
'repository' => 'Images'
}
]
],
'dirty' => [
'title' => true,
'images' => true
],
'original' => [],
'virtual' => [],
'errors' => [],
'repository' => 'Stories'
}
I can't figure out what I am doing wrong
Thanks

I haven't looked everything, but I saw there is an error in your association declaration:
$this->hasMany('Images', [
'foreignKey' => 'id'
]);
The docs say:
foreignKey: the name of the foreign key found in the other model. This is especially handy if you need to define multiple hasMany relationships. The default value for this key is the underscored, singular name of the actual model, suffixed with ‘_id’.
So it should be:
$this->hasMany('Images', [
'foreignKey' => 'post_id'
]);
or even:
$this->hasMany('Images');

I've just finished my 3-hour tour with saving hasMany content along with the main model. This struggle seems to be even worse, if considered saving many main model's objects with many associated items.
Foreign key in hasMany relationship is the singular name of the current model (entity) with _id suffix, so it's like:
class MainModel extends Table {
...
public function initialize(array $config) {
...
// remember the "s" at the end of the name
$this->hasMany('VeryWeirdCalleds', [
'className' => 'VeryWeirdCalleds',
'foreignKey' => 'main_model_id',
'propertyName' => 'very_weird_calleds'
]);
...
}
...
}
Then, you set the accessible in the MAIN Entity, so that the MAIN model can save the association based on "very_weird_calleds" index:
class MainModel extends Entity {
protected $_accessible = [
...
'very_weird_calleds' => true,
];
}
And the last (but not least): the Controller save. It's usually the hardest part to overcome, due to the fact, that docs do not clarify the whole process in detail:
class MainModelsController extends AppController {
public function add($data) {
$data = [
[
'name' => 'Hello',
'body' => 'Bla bla',
'very_weird_calleds' => [
[
'name' => 'Very Weird Called'
]
]
]
];
foreach ($data as $record) {
$main = $this->MainModels->newEntity($record);
if(isset($record['images']) && !empty($record['images'])) {
foreach($record['images'] as $record_image) {
$image = $this->MainModels->VeryWeirdCalleds->newEntity();
$image->IMAGE = $record_image['IMAGE'];
$import->very_weird_calleds[] = $image;
}
}
if (!$this->MainModels->save($main)) {
$this->Flash->error('The main model could not be saved. Please, try again.');
}
}
}
Explanation? First of all, we loop through the data, previously prepared as follows:
[ 'main', 'model', 'data', 'association_accessible_property' => [ 'associated_data' ] ] ]
Then we create new entries for associated data, using the same method as for the main model. The last thing, is to add those associated Entities to the main model Entity.
Pay an extreme attention to the names given in this example. The 's'-es and CamelCases are not coincidental.

try this :
<?php echo $this->Form->input('0.Images.image'); ?>
<?php echo $this->Form->input('1.images.image'); ?>
<?php echo $this->Form->input('2.images.image'); ?>
with int before , according with http://book.cakephp.org/3.0/en/views/helpers/form.html#field-naming-conventions

Related

Cakephp 4 : How to save hasone data

I have two database table 1) users 2) profiles
profiles has a field called bank_ac
I am trying to save it from user model.
I have created form input like
<?= $this->Form->create($user) ?>
<?= $this->Form->control('profile.bank_ac'); ?>
<?= $this->Form->end() ?>
User model I have added associative like
$this->hasOne('Profiles');
After debug getting data like
[
'name' => 'Jone',
'email' => 'abcd#yahoo.com',
'profile' => [
'bank_ac' => '1212212'
]
]
after debug patch entity
object(App\Model\Entity\User) {
'name' => 'Jone',
'email' => 'abcd#yahoo.com',
'[new]' => true,
'[accessible]' => [
'name' => true,
'email' => true,
'created' => true,
'modified' => true
],
'[dirty]' => [
'name' => true,
'email' => true,
'profile' => true
],
'[original]' => [],
'[virtual]' => [],
'[hasErrors]' => false,
'[errors]' => [],
'[invalid]' => [],
'[repository]' => 'Users'
}
In UsersController/add I have applied code like
public function add(){
$user = $this->Users->newEmptyEntity();
$user = $this->Users->patchEntity($user, $this->request->getData());
$this->Users->save($user, ['associated' => ['Profiles']]);
}
Profile data not saving , also not getting any error. How can I save this associative data ?
Looking at your entity debug result, the profile field is missing from the accessibility config, hence disallowing its use in mass assignment (patching).
Add it to your User::$_accessible property, and it should work:
protected $_accessible = [
// ...
'profile' => true,
];
See also
Cookbook > Database Access & ORM > Entities > Mass Assignment

How to configure a tree in a good way using Cakephp3?

I'm working on a project which needs a tree to structure categories.
The problem is i can't find how to join the tables like it should be between my models, or maybe it's related to the controller itself.
When i fill the form and post it, only the table Categories is written, nothing happens in Categorylangs.
I'm pretty sure it's a little mistake or something simple i misunderstood here.. thanks
public function add()
{
$category = $this->Categories->newEntity();
$category->categorylangs = $this->Categories->Categorylangs->newEntity();
if ($this->request->is(['patch', 'post', 'put'])) {
$category = $this->Categories->patchEntity($category, $this->request->getData());
debug($category);
die();
if ($this->Categories->save($category)) {
$this->Flash->success(__('The category has been saved.'));
return $this->redirect(['action' => 'index']);
}
$this->Flash->error(__('The category could not be saved. Please, try again.'));
}
$this->set(compact('category'));
}
class CategoriesTable extends Table
$this->setTable('categories');
$this->setDisplayField('id');
$this->setPrimaryKey('id');
$this->addBehavior('Tree');
$this->hasMany('ChildCategories', [
'className' => 'Categories',
'foreignKey' => 'parent_id'
]);
$this->hasMany('Categorylangs', [
'foreignKey' => 'category_id'
]);
// $this->hasMany('Privatequestions', [
// 'foreignKey' => 'category_id'
// ]);
// $this->hasMany('Publicquestions', [
// 'foreignKey' => 'category_id'
// ]);
// $this->hasMany('Userpros', [
// 'foreignKey' => 'category_id'
// ]);
}
/**
* Default validation rules.
*
* #param \Cake\Validation\Validator $validator Validator instance.
* #return \Cake\Validation\Validator
*/
public function validationDefault(Validator $validator)
{
$validator
->nonNegativeInteger('id')
->allowEmptyString('id', 'create');
$validator
->boolean('active')
->requirePresence('active', 'create')
->allowEmptyString('active', false);
return $validator;
}
/**
* Returns a rules checker object that will be used for validating
* application integrity.
*
* #param \Cake\ORM\RulesChecker $rules The rules object to be modified.
* #return \Cake\ORM\RulesChecker
*/
public function buildRules(RulesChecker $rules)
{
return $rules;
}
}
class CategorylangsTable extends Table
{
{
parent::initialize($config);
$this->setTable('categorylangs');
$this->setDisplayField('id');
$this->setPrimaryKey('id');
$this->belongsTo('Categories', [
'foreignKey' => 'category_id',
'joinType' => 'INNER'
]);
$this->belongsTo('Languages', [
'foreignKey' => 'language_id',
'joinType' => 'INNER'
]);
}
/**
* Default validation rules.
*
* #param \Cake\Validation\Validator $validator Validator instance.
* #return \Cake\Validation\Validator
*/
public function validationDefault(Validator $validator)
{
$validator
->nonNegativeInteger('id')
->allowEmptyString('id', 'create');
$validator
->scalar('label')
->maxLength('label', 350)
->requirePresence('label', 'create')
->allowEmptyString('label', false);
$validator
->scalar('slug')
->maxLength('slug', 450)
->requirePresence('slug', 'create')
->allowEmptyString('slug', false);
$validator
->scalar('description')
->requirePresence('description', 'create')
->allowEmptyString('description', false);
return $validator;
}
/**
* Returns a rules checker object that will be used for validating
* application integrity.
*
* #param \Cake\ORM\RulesChecker $rules The rules object to be modified.
* #return \Cake\ORM\RulesChecker
*/
public function buildRules(RulesChecker $rules)
{
$rules->add($rules->existsIn(['category_id'], 'Categories'));
$rules->add($rules->existsIn(['language_id'], 'Languages'));
return $rules;
}
}
The view with the form to fill the 2 tables:
<nav class="large-3 medium-4 columns" id="actions-sidebar">
<ul class="side-nav">
<li class="heading"><?= __('Actions') ?></li>
<li><?= $this->Html->link(__('List Categories'), ['action' => 'index']) ?></li>
</ul>
</nav>
<div class="categories form large-9 medium-8 columns content">
<?= $this->Form->create($category) ?>
<fieldset>
<legend><?= __('Add Category') ?></legend>
<?php
echo $this->Form->control('parent_id', ['options' => $parentCategories, 'empty' => __('root')]);
echo $this->Form->control('categorylangs.label');
echo $this->Form->control('categorylangs.description');
echo $this->Form->control('categorylangs.language_id');
echo $this->Form->control('active');
?>
</fieldset>
<?= $this->Form->button(__('Submit')) ?>
<?= $this->Form->end() ?>
</div>
Here is a debug from Categoriescontroller.php, showing the data are not transmitted like they should be but i cant figure how to fix it...
/src/Controller/Admin/CategoriesController.php (line 60)
object(App\Model\Entity\Category) {
'categorylangs' => [],
'active' => false,
'[new]' => true,
'[accessible]' => [
'parent_id' => true,
'lft' => true,
'rght' => true,
'active' => true,
'parent_category' => true,
'child_categories' => true,
'categorylangs' => true,
'privatequestions' => true,
'publicquestions' => true,
'userpros' => true
],
'[dirty]' => [
'categorylangs' => true,
'active' => true
],
'[original]' => [
'categorylangs' => object(App\Model\Entity\Categorylang) {
'[new]' => true,
'[accessible]' => [
'label' => true,
'slug' => true,
'description' => true,
'category_id' => true,
'language_id' => true,
'category' => true,
'language' => true
],
'[dirty]' => [],
'[original]' => [],
'[virtual]' => [],
'[hasErrors]' => false,
'[errors]' => [],
'[invalid]' => [],
'[repository]' => 'Categorylangs'
}
],
'[virtual]' => [],
'[hasErrors]' => false,
'[errors]' => [],
'[invalid]' => [],
'[repository]' => 'Categories'
}
Categorylangs is a hasMany relation, not hasOne, so your field names should be like categorylangs.0.label, not categorylangs.label. You need for it to generate an array of entities, even if the array has only a single entity in it.
And $category->categorylangs = $this->Categories->Categorylangs->newEntity(); is meaningless here, the value of categorylangs will be overwritten by the patchEntity call.

saving belongsToMany association with same keys but different _joinData

I have a belongsToMany relationship between two tables which is configured using a through table.
class UsersTable extends Table
{
public function initialize(array $config)
{
$this->belongsToMany('Groups', [
'through' => 'GroupsUsers',
]);
}
}
class GroupsTable extends Table
{
public function initialize(array $config)
{
$this->belongsToMany('Users', [
'through' => 'GroupsUsers',
]);
}
}
class GroupsUsersTable extends Table
{
public function initialize(array $config)
{
$this->belongsTo('Groups');
$this->belongsTo('Users');
}
}
I can make associations between users and groups, but not I can't figure out how to make multiple associates between the same user and groups, but with different _joinData.
This is how the join table is built:
$table = $this->table('groups_users');
$table->addColumn('user_id', 'integer');
$table->addColumn('group_id', 'integer');
$table->addColumn('role', 'string');
$table->create();
When I add or edit a group by adding users to it, the $data in beforeMarshal() is
object(ArrayObject) {
name => 'test group'
users => [
(int) 0 => [
'id' => (int) 1,
'_joinData' => [
'role' => 'writer'
]
],
(int) 1 => [
'id' => (int) 1,
'_joinData' => [
'role' => 'editor'
]
]
]
}
after patching the entity in the controller with
$entity = $this->Groups->patchEntity(
$entity,
$data,
['associated' => ['Users._joinData']]
);
I get the following $entity in beforeSave()
object(App\Model\Entity\Group) {
'id' => (int) 1,
'name' => 'test group',
'users' => [
(int) 0 => object(App\Model\Entity\User) {
'id' => (int) 1,
'username' => 'testuser',
'_joinData' => object(Cake\ORM\Entity) {
'role' => 'writer',
'[new]' => true,
'[accessible]' => [
'*' => true
],
'[dirty]' => [
'role' => true
],
'[original]' => [],
'[virtual]' => [],
'[hasErrors]' => false,
'[errors]' => [],
'[invalid]' => [],
'[repository]' => 'GroupsUsers'
},
'[new]' => false,
'[accessible]' => [
'username' => true,
'groups' => true,
'_joinData' => true
],
'[dirty]' => [
'_joinData' => true
],
'[original]' => [
'_joinData' => [
'role' => 'writer'
]
],
'[hasErrors]' => false,
'[errors]' => [],
'[invalid]' => [],
'[repository]' => 'Users'
}
],
'[new]' => false,
'[accessible]' => [
'name' => true,
'users' => true
],
'[dirty]' => [
'users' => true,
'modified' => true
],
'[original]' => [
'modified' => object(Cake\I18n\FrozenTime) {
'time' => '2019-05-15T14:41:49+00:00',
'timezone' => 'UTC',
'fixedNowTime' => false
}
],
'[hasErrors]' => false,
'[errors]' => [],
'[invalid]' => [],
'[repository]' => 'Groups'
}
So it looks like the Marshaller is removing the second "duplicate" user from the group, even though the _joinData is different. The default saveStrategy is replace, but manually changing it to append doesn't add the second association either.
How can I add the same user to the same group with different _joinData using the same join table.
The marshaller uses the primary key to identify records, and the collection of existing entities will then only hold one user entity instance, so even though the marshaller will add the different join data sets, it will set it on one and the same entity... long story short, the marshaller isn't (yet) capable of doing what you're trying to do. You may want to open an issue over at GitHub if there isn't one already.
For now you'll have to save the records separately, for example like this (untested, but you get the idea):
$group = $this->Groups->patchEntity($entity, $data, [
'fieldList' => ['name']
]);
$users = [];
foreach ($data['users'] as $userData) {
$user = $this->Groups->Users->newEntity();
$users[] = $this->Groups->Users->patchEntity($user, $userData);
}
$result = $this->Groups->getConnection()->transactional(function () use ($group, $users) {
if (!$this->Groups->save($group, ['atomic' => false])) {
return false;
}
if (!$this->Groups->Users->link($group, $users, ['atomic' => false])) {
return false;
}
return true;
});

HasOne associations table is not saving

Hi I'm trying to save data across 2 tables (Entries and Conditions) at the same time. Entries has a column called foreign_key which is a relation to the Conditions primary id.
I was testing the saving portion and one of my tables (Conditions) saved but my other table (Entries) did not.
Entry.php
protected $_accessible = [
'metadata' => true,
'type' => true,
'foreign_key' => true,
'created' => true,
'modified' => true,
'condition' => true
];
Condition.php
protected $_accessible = [
'user_id' => true,
'data' => true,
'created' => true,
'modified' => true,
'user' => true,
'entry' => true
];
ConditionsTable.php (where I declared the association)
public function initialize(array $config)
{
parent::initialize($config);
$this->setTable('conditions');
$this->setDisplayField('id');
$this->setPrimaryKey('id');
$this->addBehavior('Timestamp');
$this->belongsTo('Users', [
'foreignKey' => 'user_id',
'joinType' => 'INNER'
]);
$this->hasOne('Entries', [
'foreignKey' => 'foreign_key',
'bindingKey' => 'id',
'propertyName' => 'entries',
'joinType' => 'INNER'
]);
}
DataControlellr.php (where I'm testing the saving)
$this->loadModel('Conditions');
$data = [
'user_id' => 'b26ee991-a27c-441b-a78b-dd2a1dbf5164',
'data' => json_encode(['test'=>1,'test2' => 2]),
'entry' =>[
'meta' => json_encode(['test'=>1,'test2' => 2]),
'type' => 'conditions'
]
];
$entity = $this->Conditions->newEntity($data,['associated' => 'Entries']);
//dd($entity);
dd($this->Conditions->save($entity));
exit;
}
So again entries table is not saving a row and conditions is, I believe I'm using the right association (has one) but maybe that not the right logic? Much help is appreciated.
The key in your data and your association name need to match. You probably want to change your association to this:
$this->hasOne('Entries', [
'foreignKey' => 'foreign_key',
'bindingKey' => 'id',
'joinType' => 'INNER'
]);
assuming that the class for the fhir_entries table is called EntriesTable, not FhirEntriesTable.
Alternately, leave the association named as it is but change the propertyName to entry (hasOne properties should be singular). Or, change it to fhir_entry and the key in the data array from entry to fhir_entry to match.

How to display array data in Yii2 GridView using ActiveDataprovider?

In my db,
"email" : [
"amnop#mailinator.com",
"abc#mail.com"
],
When I print_r($model->email),
it shows
Array ( [0] => amnop#mailinator.com [1] => abc#mail.com )
In my GridView,
<?= GridView::widget([
'dataProvider' => $dataProvider,
----
'columns' => [
-----
'price',
[
'attribute' => 'email',
'value' => function($model) {
//I need help here... I prefer any foreach function
}
],
-----
]
?>
I have to display all the emails in the same column. How to do this?
Edit
I use ActiveDataprovider as I'm getting the values from my db.
Depending on what you want to achieve, you can just implode emails array:
[
'attribute' => 'email',
'value' => function($model) {
if (is_array($model->email)) {
return implode(', ', $model->email);
}
return $model->email;
}
],
assuming you an array as
$data = [
['email' => 'amnop#mailinator.com'],
['email' => 'abc#mail.com'],
...
['email' => 'youremail100#mail.com'],
];
you can use an ArrayDataProvider
$provider = new ArrayDataProvider([
'allModels' => $data,
'pagination' => [
'pageSize' => 10,
],
'sort' => [
'attributes' => [ 'email'],
],
]);
send the data provider to the as usual
so in gridview you can use ,
<?= GridView::widget([
'dataProvider' => $dataProvider,
----
'columns' => [
-----
'price',
[
'attribute' => 'email',
'value' => function($model) {
//I need help here...
}
],
-----
]
?>
you can take a look at yii2 guide https://www.yiiframework.com/doc/guide/2.0/en/output-data-providers
and doc https://www.yiiframework.com/doc/api/2.0/yii-data-arraydataprovider

Resources