Cake php - Saving HasMany Associations - cakephp

I created a hasMany association between a Projects & Comments tables.
From this url : /projects/add, the user can create a new project and write several comments.
The request return an array like this :
[
'name' => 'Project name',
'comments' => [
(int) 0 => [
'text' => 'This is a comment on this project'
],
(int) 1 => [
'text' => 'Comment'
]
]
]
In the validator comments, I added a minLength 30 rules.
In the example above, the validator will return an error on the second comment and block recording.
What I want is to save the project and the good comments, and return an error like this : " The project has been saved, But one or more comment was not saved ".
Thank you for your help !

This answer shows one way to do what you're looking for. Try the save and unbind the hasMany relationship if there are validation errors with the associated model.
I think it would be cleaner to just split the save calls though.
if (!$this->Project->save($this->request->data['Project'])) {
throw new Exception($this->Project->getErrors());
}
if (!$this->Project->Comment->save($this->request->data['Comment'])) {
$message = 'The project has been saved, But one or more comment was not saved';
}

Related

CakePHP 3 AuthComponent::login() equivalent -> id missing?

I'm trying to set up cakephp/acl and using this example as a guide. The only main difference is that I'm using email instead of username and have accounted for this with:
$this->loadComponent('Auth', [
'authenticate' => [
'Form' => [
'fields' => ['username' => 'email']
]
],
...
The login itself seems to work but when I navigate to a controller eg: /posts/index I get the following error
AclNode::node() - Couldn't find Aros node identified by "Array ( [Aros0.model] => Users [Aros0.foreign_key] => 1 ) "
My knowledge of Acl is rudimentary, so I'm having trouble figuring this out.
A couple of similar issues I've seen are based on CakePHP 2, and talk about having the id set in the AuthComponent::login. Here and Here but I have no idea how this translates into CakePHP 3, or why I am having this issue in the first place.
Any help appreciated.

Getting entities' dirty fields after saving with associations

I'm trying to log each action (insert/update/delete) in the application and I'm doing this by getting the dirty and original values after saving the entity. The problem is that all values of the associated entities are returned as dirty and even is_new flag is set to true but actually I'm updating. What causes this behavior and how can I avoid it?
Example:
$data = [
'name' => $name,
'something' => $something,
'Table1' => [
'id' => $idWhereUpdatingTable1,
'field1' => $field1,
'field2' => $field2,
],
'Table2' => [
'id' => $idWhereUpdatingTable2,
'field3' => $field3,
'field4' => $field4,
],
];
$options = ['associated' => ['Table1', 'Table2']];
$updatedEntity = $this->patchEntity($entity, $data, $options);
$save = $this->save($updatedEntity);
// Successfully logging the changes in the main entity
// Trying to log the changes in the associated entities
foreach($save->table1 as $entity)
{
// everything here is set to dirty (even ID field but it's not an insert) and I'm not able to fetch the updated fields only. Also getOriginal() doesn't return the old values.
}
I did some digging into the dirty() function within an Entity and according to the API if you do not explicitly ask it to check a property then it will just tell you if the Entity has any dirty properties.
So doing
$entity->dirty('title'); Tells you if the tile is dirty but running $entity->dirty(); will just tell you if any property in the entity is dirty.
http://api.cakephp.org/3.1/class-Cake.ORM.Entity.html#_dirty
You may want to make code conditional based on whether or not fields have changed in an entity.
For example, you may only want to validate fields when they change:
// See if the title has been modified. CakePHP version 3.5 and above
$entity->isDirty('title');
// CakePHP 3.4 and Below use dirty()
$entity->dirty('title');

Cake 3: How to add new entity to database with primaryKey set?

I want to populate my database with 'flat' data extracted from an excel sheet. All records are provided as arrays (similar to $request->data) but have their primaryKeys set which values must be kept.
My code:
$imported = 0;
foreach ($data as $record) {
$entity = $table->findOrCreate([$table->primaryKey() => $record[$table->primaryKey()]]);
$entity = $table->patchEntity($entity, $record);
if ($table->save($entity)) {
$imported++;
}
}
The code works, but I'm wondering if there is a better solution?
To clarify: What I want is adding something like
[
['id' => 25, 'title'=>'some title'],
['id'=> 3, 'title' => 'some other title'],
['id' => 4356, 'title' => 'another title']
]
to my empty database. findOrCreate() does the job. But I think it shouldn't be necessary to test every record that it not already exists in database before inserting.
A common problem with records mysteriously losing some of the data being provided to a new Entity is that the Entity does not define the field(s) in question as _accessible.
Cake's BakeShell will skip the primary key fields when generating new Entity classes for you, for example:
<?php
namespace App\Model\Entity;
use Cake\ORM\Entity;
/**
* Widget Entity.
*/
class Widget extends Entity {
/**
* Fields that can be mass assigned using newEntity() or patchEntity().
*
* #var array
*/
protected $_accessible = [
// `id` is NOT accessible by default!
'title' => true,
];
}
There are a few ways to work around this.
You can modify your Entity class to make the id field permanently assignable:
protected $_accessible = [
'id' => true, // Now `id` can always be mass-assigned.
'title' => true,
];
Or you can adjust your call to newEntity() to disable mass assignment protection:
$entities = $table->newEntity($data, [
'accessibleFields' => ['id' => true],
]);
I've found the most important take-away when you're having issues with Cake 3 DB data is to double-check the Entity as soon as it's created or patched and compare it against your input data. You still need to have a sharp eye, but doing so would reveal that the Entities did not have their ->id property set at all even though $data defined them.
If you really only ever work with empty tables, then you can simply save the data straight away, no need to find and patch, just save with disabled existence check.
Also from looking at your code, the data seems to be in a format that can be turned into entities right away, so you may want to create them all at once.
$entities = $table->newEntities($data, [
// don't forget to restrict assignment one way or
// another when working with external input, for
// example by using the `fieldList` option
'fieldList' => [
'id',
'title'
]
]);
// you may want to check the validation results here before saving
foreach ($entities as $entity) {
if ($table->save($entity, ['checkExisting' => false])) {
// ...
}
// ...
}
See also
Saving Entities
Converting Request Data
Avoiding Property Mass Assignment Attacks
Calidating Data Before Building Entities

how to save one-to-many relationships to the database using zend form and doctrine

I'm writing an application that uses Zend Framework 2 and Doctrine (both the latest stable version).
There is much documenation (mainly tutorials and blog posts) that deal with saving doctrine entities to the database in combination with Zend Form. Unfortunately they only deal with simple entities that do not have one-to-many or many-to-many relationships.
This is one of those examples that i have adopted into my own code.
http://www.jasongrimes.org/2012/01/using-doctrine-2-in-zend-framework-2/
I understand that in the Album Entity of this example, the artist is a string to keep the (already lengthy) tutorial as simple as possible. But in a real world situation this would of course be a one-to-many releationship with an Artist Entity (or even a many-to-many). In the view, a select-box could be displayed where the artist can be selected, listing all the artist-entities that could be found in the database, so the right one can be selected.
Following the example with the album, this is how i've set up an 'edit' Action in my controller:
public function editAction()
{
// determine the id of the album we're editing
$id = $this->params()->fromRoute("id", false);
$request = $this->getRequest();
// load and set up form
$form = new AlbumForm();
$form->prepareElements();
$form->get("submit")->setAttribute("label", "Edit");
// retrieve album from the service layer
$album = $this->getSl()->get("Application\Service\AlbumService")->findOneByAlbumId($id);
$form->setBindOnValidate(false);
$form->bind($album);
if ($request->isPost()) {
$form->setData($request->getPost());
if ($form->isValid()) {
// bind formvalues to entity and save it
$form->bindValues();
$this->getEm()->flush();
// redirect to album
return $this->redirect()->toRoute("index/album/view", array("id"=>$id));
}
}
$data = array(
"album" => $album,
"form" => $form
);
return new ViewModel($data);
}
How would this example need to be altered if the artist wasn't a string, but an Artist Entity?
And suppose the album also has multiple Track Entities, how would those be processed?
The example would not need to be changed at all, the changes will happen with your entity and your form.
This is a good reference: Doctrine Orm Mapping
So to save yourself a lot of extra work, your OnToMany relationship would use: cascade = persist:
/**
* #ORM\OneToMany(targetEntity="Artist" , mappedBy="album" , cascade={"persist"})
*/
private $artist;
When it comes to persisting the form object, the entity knows it must save the associated entity as well. If you did not include this, then you would have to do it manually using a collection.
To make like easier with your form, you can use Doctrines Object Select like this:
$this->add(
[
'type' => 'DoctrineModule\Form\Element\ObjectSelect',
'name' => 'artist',
'options' => [
'object_manager' => $this->objectManager,
'target_class' => 'Artist\Entity\Artist',
'property' => 'name', //the name field in Artist, can be any field
'label' => 'Artist',
'instructions' => 'Artists connected to this album'
],
'attributes' => [
'class' => '', //Add any classes you want in your form here
'multiple' => true, //You can select more than one artist
'required' => 'required',
]
]
);
So now your form takes care of the collection for you, the controller as per your example does not need to change since the entity will take care of the persisting...
Hope this gets you on track.

HABTM form validation in CakePHP

I have a Projects table and a Users table which are linked by a HABTM relation. In the "add" new Project page I have a multiple checkbox section to select Users for the new project. I want to have at least one User for the Project. What's the best way to approach this in CakePHP ?
Try this:
// app/models/project.php
/**
* An additional validation check to ensure at least one User is
* selected. Spoofs Cake into thinking that there are validation
* errors on the Project model by invalidating a non-existent field
* on the Project model, then also invalidates the habtm field as
* well, so when the form is re-displayed, the error is displayed
* on the User field.
**/
function beforeValidate() {
if (!isset($this->data['User']['User'])
|| empty($this->data['User']['User'])) {
$this->invalidate('non_existent_field'); // fake validation error on Project
$this->User->invalidate('User', 'Please select at least one user');
}
return true;
}
I stumbled on the same issue, but now - 3 years later - with CakePHP 2.3.
To be clear; Group has and belongs to User. I've had a form like this:
// View/Groups/add.ctp
echo $this->Form->input('name');
echo $this->Form->input('User');
With the validation rule like in user448164's answer:
// Model/Group.php
public $validate = array(
'User' => array(
'rule' => array('multiple', array('min' => 1)),
'message' => 'Please select one or more users'
)
);
That didn't work, after Googling for it, I found this question which couldn't still be the best solution. Then I tried several things, and discovered this to work just fine:
// View/Groups/add.ctp
echo $this->Form->input('name');
echo $this->Form->input('Group.User');
Way too easy solution, but had to dig into it to find out it works this way.
Hopefully it helps somebody one day.
Update for CakePHP 2.4.x (possibly 2.3.x as well)
When I wrote this answer, I was using CakePHP 2.3.x. Back then it worked perfectly for both validating and saving the data. Now when applying the same code on a new project, using CakePHP 2.4.x, it didn't work anymore.
I created a test case, using the following code:
$data = array(
'User' => array(
'Client' => array(8)
),
);
$this->User->create();
$this->User->saveAll($data);
My first thought was: Saving all means saving all "root" models, what actually makes sense to me. To save deeper than just the "root" ones, you'll have to add the deep option. So I ended up with the following code:
$data = array(
'User' => array(
'Client' => array(8)
),
);
$this->User->create();
$this->User->saveAll($data, array('deep' => true));
Works like a charm! Happy coding. :)
Update (2014/03/06)
Struggling with the same problem again, in this case with hasMany instead of habtm. Seems like it behaves the same way. But I found myself looking for this answer again, and got confused.
I'd like to make clear that it's key to use Group.User instead of User in your input. Else it won't use the User model validation.
I've just been looking at his problem myself on a project and came across a slightly more elegant solution, as long as you're only dealing with a habtm relationship and you need to ensure that at least one checkbox is selected.
so for example you're editing a Project and you want it to be associated with at least one user
Add this to beforeValidate()
// check habtm model and add to data
foreach($this->hasAndBelongsToMany as $k=>$v) {
if(isset($this->data[$k][$k]))
{
$this->data[$this->alias][$k] = $this->data[$k][$k];
}
}
In the validation rules add the following:
'User' => array(
'rule' => array('multiple', array('min' => 1)),
'message' => 'Please select one or more users'
)
teknoid's blog has a pretty in depth solution to your issue here. The most Cakey way of doing this would be to add custom validation to your model, as you mention in your comment above. Check out http://teknoid.wordpress.com/2008/10/16/how-to-validate-habtm-data/
From the article, where Tag HABTM Post (:: Project HABTM Users):
First, we validate the Tag model, by
using the data from the form to ensure
that at least one Tag was selected. If
so, we save the Post and the relevant
Tags.
2016 update for CakePhp 2.7
Full answer here : HABTM form validation with CakePHP 2.x
TL;DR;
AppModel.php
public function beforeValidate($options = array()){
foreach (array_keys($this->hasAndBelongsToMany) as $model){
if(isset($this->data[$model][$model]))
$this->data[$this->name][$model] = $this->data[$model][$model];
}
return true;
}
public function afterValidate($options = array()){
foreach (array_keys($this->hasAndBelongsToMany) as $model){
unset($this->data[$this->name][$model]);
if(isset($this->validationErrors[$model]))
$this->$model->validationErrors[$model] = $this->validationErrors[$model];
}
return true;
}
In the main model of your HABTM :
public $validate = array(
'Tag' => array(
'rule' => array('multiple', array('min' => 1)),
'required' => true,
'message' => 'Please select at least one Tag for this Post.'
)
);
If you are using CakePHP 2.3.x, you may need to add this code to your model in addition to the code that GuidoH provided, otherwise your HABTM model data may not save:
public function beforeSave($options = array()){
foreach (array_keys($this->hasAndBelongsToMany) as $model){
if(isset($this->data[$this->name][$model])){
$this->data[$model][$model] = $this->data[$this->name][$model];
unset($this->data[$this->name][$model]);
}
}
return true;
}
As per my comment on Guido's answer above, I use Guido's answer exactly, however I modify the data with the beforeSave callback before it saves to the database.
I have this issue on Cake 2.4.5+
public function beforeSave($options = array()) {
$temp = $this->data['Group']['User'];
unset($this->data['Group']['User']);
$this->data['User']['User'] = $temp;
return true;
}

Resources