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

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

Related

Cakephp 3: Countercache with conditions on the target model

I am trying to count the number of Spots associated with a Plan, but limited to Spots downloaded after the plans renewal date. Hope that makes sense. I would image something like this, but it doesn't work:
class SpotsTable extends Table
{
public function initialize(array $config)
{
$this->addBehavior('CounterCache', [
'Plan' => [
'creditsUsed' => [
'conditions' => [
'downloaded >' => 'Plan.renewed'
]
]
]
]);
...
}
...
}
Basically right now it acts as though Plan.renewed means NULL.
Is this possible, or am I on the wrong track?
Two problems
1. Identifiers cannot be passed as string values
When using the key => value format, the value side will always be subject to binding/escaping/casting unless it's an expression object, so since the downloaded column is probably a date/time type, you'll end up with Plan.renewed being bound as a string, thus the final SQL will be something like:
downloaded > 'Plan.renewed'
which probably always results in false. Long story short, use for example an identifier expression:
'Spots.downloaded >' => new \Cake\Database\Expression\IdentifierExpression('Plan.renewed')
2. The counter query doesn't have access to the associated table
Plan.renewed will not be accessible in the query generated by the counter cache behavior, it will not automatically contain/join associations, it will create a simple query with a condition based on the foreign key value in the currently processed Spot entity.
So you have to use a custom/modified query, for example using a custom finder, something like this:
'creditsUsed' => [
'finder' => 'downloadedAfterPlanRenewal'
]
// in SpotsTable
public function findDownloadedAfterPlanRenewal(\Cake\ORM\Query $query, array $options)
{
return $query
->innerJoinWith('Plan')
->where([
'Spots.downloaded >' => $query->identifier('Plan.renewed')
]);
}
This will properly join in the association, so that you can compare with a field from Plan. The original primary key conditions generated by the behavior will already be applied on the given query object.
See also
Cookbook > Database Access & ORM > Behaviors > CounterCache > Advanced Usage
Cookbook > Database Access & ORM > Retrieving Data & Results Sets > Custom Finder Methods

CakePHP 3 cannot save associated model

I am updating some log data in some tables. I can't save on the associated model ModulesEmployees only the top level CoursesEmployees. Cant see what is missing.
CoursesEmployeeTable.php
$this->hasMany('ModulesEmployees', [
'dependent' => true,
'foreignKey' => 'courses_employee_id'
]);
ModulesSlidesController.php
$this->loadModel('CoursesEmployees');
$CoursesEmployee = $this->CoursesEmployees
->get(65, [
'contain' => ['ModulesEmployees'],
'where' =>[
['ModulesEmployees.id' => 19]
]
]);
if (!$CoursesEmployee['modules_employees'][0]['started_on']) {
$CoursesEmployee['modules_employees'][0]['started_on'] = date('Y-m-d H:i:s');
};
$this->CoursesEmployees->save($CoursesEmployee, ['associated' => ['ModulesEmployees']]);
Returned Object
CoursesEmployee(array)
id65
employee_id3
course_id1
course_module_id1
completed_modules0
current_slide0
cid0
date_started(array)
progress0
modified(array)
created(array)
completed(false)
date_completed(null)
deleted(null)
modules_employees(array)
0(object)
id19
courses_employee_id65
course_module_id1
started_on2015-10-04 21:00:10
completed_on(null)
completed(false)
deleted(null)
[new](false)
[accessible](array)
[dirty](empty)
[original](empty)
[virtual](empty)
[errors](empty)
[repository]CoursesEmployees
Only dirty entities/properties are being saved, and what you are doing there, modifying a nested entity, will only mark that nested entity as dirty, the parent entity, CoursesEmployee will remain unchanged, and therefore the saving process won't touch the associations.
When not using the patching mechanism which automatically marks parent entity properties as dirty, then you'll have to do that on your own.
Quote from the docs:
[...]
If you are building or modifying association data after building your
entities you will have to mark the association property as modified
with dirty():
$company->author->name = 'Master Chef';
$company->dirty('author', true);
Cookbook > Database Access & ORM > Saving Data > Saving Associations
So in your case it's the modules_employees property that needs to be marked as dirty, like
$CoursesEmployee->dirty('modules_employees', true);

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');

CakePHP 3.0 saving belongsToMany data problems

I'm having trouble saving data in a belongsToMany association. This is the error I'm getting:
Fatal Error (1): Call to a member function get() on a non-object in [application/vendor/cakephp/cakephp/src/ORM/Association/BelongsToMany.php, line 854]
This is the piece of code causing trouble:
public function add(){
$session = $this->request->session();
$call = $this->Calls->newEntity([
"user_id" => $this->Auth->user("id"),
"customer_id" => $session->read("Customer.id"),
"comment" => $this->request->data["comment"],
"result_id" => $this->request->data["result_id"]
]);
if($this->request->data["result_id"] === 0){
$deliveryMode = $this->request->data["order"]["delivery"]["mode"];
$order = new Order([
"delivery" => $deliveryMode,
"products" => [
"_ids" => [1, 2]
]
]);
if($deliveryMode === 1){
$order->set("smartpost_delivery_id", $this->request->data["order"]["delivery"]["locations"][1]["id"]);
}
$call->order = $order;
}
$result = $this->Calls->save($call);
if($result){
$session->write("Customer.id", null);
echo json_encode(["id" => $result->id]);
} else{
echo json_encode($call->errors());
$this->response->statusCode(400);
}
}
As we can see I'm trying to save a Call that has an Order attached to it and the part that is causing trouble is that I'm also trying to attach a list of Products ids to the Order to be saved to a jointable. If I remove the product ids, the Call and the Order are saved successfully.
4 tables are involved: calls, orders, products, orders_products. Here's a visual representation of the tables. The table objects' code has been auto-baked by the cake app. If needed, I can provide the code for them. Associations: Calls hasOne Order, Orders belongsToMany Products
You cannot save with the _ids key, you'll need entities with the appropriate primary key values.
The special _ids key is a convenience format that is handled by the marshaller when it converts data into entities. It will fetch the appropriate data from the datasource and replace the association property with a list of entities.
Long story short, do not create the Order entity manually, let the marshaller do it. You'd nost probably want to do that anyways since you are inserting user data into the entity wich should be validated first, which is why you should also add the smartpost_delivery_id value to the data before converting it into an entity:
$orderData = [
"delivery" => $deliveryMode,
"products" => [
"_ids" => [1, 2]
]
];
if($deliveryMode === 1) {
$orderData["smartpost_delivery_id"] =
$this->request->data["order"]["delivery"]["locations"][1]["id"];
}
// Adjust the name of the association in case necessary
$order = $this->Calls->association('Orders')->newEntity($orderData);
See also
http://book.cakephp.org/3.0/en/orm/saving-data.html#converting-request-data-into-entities
http://book.cakephp.org/3.0/en/orm/saving-data.html#validating-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.

Resources