CakePHP 3.0 saving belongsToMany data problems - cakephp

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

Related

Sorting on nested relationship

I have the following relationship:
Rounds hasMany Results
Results belongsTo Drivers
Results are shown on Rounds's view page: rounds/view/{id}. I want to sort Results based on Drivers.name.
This is my RoundsController::view method:
public function view($id = null)
{
$this->paginate = [
'sortableFields' => [
'Results.Drivers.name',
]
];
$round = $this->Rounds->get($id, [
'contain' => ['Championships' => ['Teams'], 'Tracks', 'Results' => ['Drivers', 'Constructors']],
]);
$this->set(compact('round'));
}
And in my rounds/view.ctp file, I have this:
<?= $this->Paginator->sort('Results.Drivers.name', __('Driver')); ?>
However, no matter what I change, it doesn't sort by driver name. Also, when I click it multiple times, the direction stays asc.
Why is it not working?
Like #ndm mentioned, your code is showing only one specific Round. View method/function is receiving an $id arg, this $id is related to the round id you want to show in view page.
You can not order this (you can do it but it doesnt make sense) because you are receiving only one Round, and it doesnt make sense to order only one row.
This view method is usually used with this goal: show individual/personal information about an Entity (Round entity in your case). Please, note I use the word USUALLY.
I think you could use the index method for this purpose (this method/function is usually used to list entities from a model and its association data). You could create a new method/function for your porpuse as well if you already are using index method for another goal.
I suggest something like this, assuming you will use the index function:
public function index()
{
$rounds = $this->Rounds->find('all')
->contain([
'Championships' => ['Teams'],
'Tracks',
'Results' => [
'Constructors'
[
'Drivers' => function (Query $q) {
return $q->order('Drivers.name');;
}
]
]
]);
$rounds= $this->paginate($rounds);
$this->set(compact('rounds'));
}
You could pass conditions into the contain or do the same into paginate method. I always use the first (just preferences).
NOTE: this code may not work, it is an idea of what you could do.
Finders documentation: https://book.cakephp.org/4/en/orm/retrieving-data-and-resultsets.html#using-finders-to-load-data
Passing conditions to contains: https://book.cakephp.org/4/en/orm/query-builder.html#passing-conditions-to-contain

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

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.

Resources