Problem description
I'm trying to configure a CakePHP 3.7 API to save associated data in a child-first manner. The entities - for the sake of example, lets call them Users and Persons - and their relationships are as follows:
UsersTable.php
...
$this->belongsTo('Persons', [
'foreignKey' => 'person_id',
'joinType' => 'LEFT',
'className' => 'MyPlugin.Persons',
]);
...
PersonsTable.php
$this->hasOne('Users', [
'foreignKey' => 'person_id',
'className' => 'MyPlugin.Users'
]);
In their respective entities, they each have one another's property visibility set to true. What I'm trying to do is POST to the /users/ route (UsersController.php) and have it also save the Persons object included. The payload is as such:
{
"username": "foo",
"password": "bar",
"persons": {
"dob": "1982-07-03",
}
}
The relevant part of the saving method is below, from UsersController.php:
if ($this->request->is('post') && !empty($this->request->getData())) {
$data = $this->request->getData();
$newEntity = $this->Users->newEntity($data, ['associated' => 'Persons']);
$savedEntity = $this->Users->save($newEntity);
...
The error
This produces the following SQL error.
PDOException: SQLSTATE[23502]: Not null violation: 7 ERROR: null value in column 'person_id' violates not-null constraint
DETAIL: Failing row contains (1, null, foo, bar)
I understand this is because Cake is attempting to save to Users without having a person_id to satisfy the foreign key constraint. It's not possible to reverse this FK relationship in my application domain as we desire leftward one-to-many relationship (User -> 1 Person).
I suspect sending an id in the persons object of the JSON payload will allow this to save correctly. However, for various reasons, this isn't possible at runtime. For example, this is how it's shown in the "Saving Data" CakePHP Book page...
$data = [
'title' => 'First Post',
'user' => [
'id' => 1,
'username' => 'mark'
]
];
...
$article = $articles->newEntity($data, [
'associated' => ['Users']
]);
$articles->save($article);
I know the following would also likely work as suggested by xPfqHZ for a similar issue, as Persons can save to Users, but it feels less suitable as compared to what I'm trying to do and feels as if there is a way via the associations on Users.
if ($this->request->is('post') && !empty($this->request->getData())) {
$data = $this->request->getData();
$newEntity = $this->Users->Persons->newEntity($data, ['associated' => 'Persons']);
$savedEntity = $this->Users->Persons->save($newEntity);
...
Workings
Now I believe this used to be possible in CakePHP 2.X, as stated in this answer by ndm on a similar question where a person is attempting to save the belongsTo associated entity and it's parent hasOne entity in one request via the belongsTo entity.
That's the expected behavior, saveAssociated() is not meant to save only the associated records, it will save the main record as well, so you should use saveAssociated() only, no need to manually set the foreign key, etc, CakePHP will do that automatically.
Controller
public function create() {
if ($this->request->is('post') && !empty($this->request->data)):
$this->CandidatesProblemReport->create();
if ($this->CandidatesProblemReport->saveAssociated($this->request->data)):
// ...
endif;
endif;
}
However, I'm not able to find or use the saveAssociated() method upon the Cake\ORM\Table object which the Users entity inherits from, in the documentation. Calling it produces a method not found error. This method only appears to exist on the Cake\ORM\Association object as detailed in the documentation. Unless I'm missing the obvious, is there a way to use this or is it used internally by BelongsTo() and its sibling methods?
Logging / Dumping entity
Using Cake\Log\Log::error($newEntity); or die(var_dump($newEntity)); shows the Users data of the payload hydrated into an object, but I don't see the Persons object attached (see below).
object(MyPlugin\Model\Entity\User)[299]
public 'username' => string 'foo' (length=3)
public 'password' => string 'bar' (length=3)
public '[new]' => boolean true
public '[accessible]' =>
array (size=5)
'*' => boolean false
'person_id' => boolean true
'username' => boolean true
'password' => boolean true
'person' => boolean true
public '[dirty]' =>
array (size=2)
'username' => boolean true
'password' => boolean true
public '[original]' =>
array (size=0)
empty
public '[virtual]' =>
array (size=0)
empty
public '[hasErrors]' => boolean false
public '[errors]' =>
array (size=0)
empty
public '[invalid]' =>
array (size=0)
empty
public '[repository]' => string 'MyPlugin.Users' (length=17)
Attempting to \Cake\Log\Log::error($savedEntity); shows nothing in the log file.
save() associations arguments
Another solution I considered was using the $options['associated] of save() as shown in the documentation (extract below). With this set to true as below, the error still occurred.
save( Cake\Datasource\EntityInterface $entity , array $options [] )
... associated: If true it will save 1st level associated entities as they are found in the passed $entity whenever the property defined for the association is marked as dirty. If an array, it will be interpreted as the list of associations to be saved. It is possible to provide different options for saving on associated table objects using this key by making the custom options the array value. If false no associated records will be saved. (default: true) ...
UsersController.php:
if ($this->request->is('post') && !empty($this->request->getData())) {
$data = $this->request->getData();
$newEntity = $this->Users->newEntity($data, ['associated' => 'Persons']);
$savedEntity = $this->Users->save($newEntity, ['associated' => true]);
...
Summary
Without going through the PersonsController.php and utilising its hasOne relationship, I'm not having much luck getting my Users and Persons data to save through the UsersController.php.
If I've missed any important information, or you have questions/need more, please ask! I might have missed something obvious, but I'd appreciate any suggestions/solutions possible.
As #ndm identified, the error lay in the posted data. As per the "Saving Data: Saving BelongsTo Associations" page of the documentation:
When saving belongsTo associations, the ORM expects a single nested entity named with the singular, underscored version of the association name.
The posted key persons should have been person. Equally, if the entity were named PersonSnapshots, the relevant key in the payload hydrated into the entities would need to have been person_snapshot.
Related
I would like to create a junction table tbl_guid_cost_centre that gets taken care of without me manually saving it to the database. I tried adding this to my relations:
'costCentre' => [
self::HAS_ONE,
'CostCentre',
'guid_to',
'foreignKey' => 'guid',
'tbl_guid_cost_centre(guid_to, cost_center_id)',
"order" => "id desc"],
so that my when saving the costCentre, a row is created for it in my tbl_guid_cost_centre. However I'm getting the error:
Property "CHasOneRelation.0" is not defined.
Any suggestion?
You can have your junction table with the keyword through in your relations:
public function relations() {
'guidCostCentre' => [
self::HAS_ONE,
'GuidCostCentre',
['guid_to' => 'guid']
],
'costCentre' => [
self::HAS_ONE,
'CostCentre',
'cost_centre_id',
'through' => 'guidCostCentre'
]
}
You're defining HAS_ONE relation in a wrong way. The first three elements of relation configuration array should be: relation type, related model name and foreign keys definition. All further elements should be indexed by keys related to relation properties. 'tbl_guid_cost_centre(guid_to, cost_center_id)', probably generates this error, because it does not have a key, so it is treaded as a value for 0 property. You didn't share any details, so it is hard to guess what you want to achieve, but you should start from something like this:
'costCentre' => [
self::HAS_ONE,
'CostCentre',
'guid_to',
'order' => 'id desc',
],
And add additional settings at the end array with correct key.
Some examples and explanation you can find in the documentation: https://www.yiiframework.com/doc/guide/1.1/en/database.arr#declaring-relationship
i'd like to set hasOne and hasMany to same model,in a part of my code i need only 1 result, but in other part i need all result (Objects from type Client that will return for a table in my site):
$this->hasOne('Vendas')
->setForeignKey('id_cliente')
->setBindingKey('id')
;
$this->hasMany('Vendas')
->setForeignKey('id_cliente')
->setBidingKey('id');
This is possible, or i made a mistake?
Read the manual https://book.cakephp.org/3.0/en/orm/associations.html. Read the whole page carefully.
className: the class name of the table being associated to the current model. If you’re defining a ‘User hasOne Address’ relationship, the className key should equal ‘Addresses’.
conditions: an array of find() compatible conditions such as ['Addresses.primary' => true]
Define the class name and conditions you need for your assocs.
$this->hasOne('Foo', [
'className' => 'Foo',
'conditions' => [/* whatever you need*/]
]);
$this->hasMany('Bar', [
'className' => 'Foo',
'conditions' => [/* whatever you need*/]
]);
If the relationship is 1-* you should define the relationship as hasMany(). Then you write a query for one result and a query for multiple results
I am using CakePHP 2.9.1, have 2 tables in a HABTM relationship, I want to save the header record and save many associations in one go, but the child record is already existing, I seem to be able to save the data using a hardcoded list but not using a variable, I'm sure it's something silly I'm doing, I'm running inside a plugin, the model I want to link too is in the main code.
The child record must not be altered because it's handled by the main system, I'm just extending it's functionality by linking to it in our plugin.
// Header Record
class templatedoc extends TemplateModuleAppModel
{
public $useTable = 'templatedocs';
public $hasAndBelongsToMany = [
'Servicetemplate' => [
'className' => 'Servicetemplate',
'joinTable' => 'templatedocs_to_servicetemplates',
'foreignKey' => 'templatedoc_id',
'associationForeignKey' => 'servicetemplate_id',
'unique' => true,
'dependent' => false, // We don't want to delete the Service Template by mistake!
]
];
}
Here is my save, this works:
$this->templatedoc->create ();
$data = [
'templatedoc' => [
'warning_adjust' => $prioritywarn,
'summary' => $summary,
],
'Servicetemplate' => [
1,2,3,10 // Dynamic data needed here!
]
];
$result = $this->templatedoc->SaveAll ($data);
But I can't have the "Servicetemplate" hardcoded, I've tried passing an array inside that array and removing it, imploding an array to make a comma separated list all I end up with is the header 'templatedoc' record being created but nothing in my link table
Only thing I can think is a difference is that the hardcoded list are int values, by data is coming from the database so will be an int inside a string, but I can't see that being an issue.
Any ideas what I'm doing wrong? Sorry if it's something completely stupid.
What I had to do in the end was store the items forcing an int, when associating in my foreach loop.
$items[] = (int) $id
Then this worked:
$this->templatedoc->create ();
$data = [
'templatedoc' => [
'warning_adjust' => $prioritywarn,
'summary' => $summary,
],
'Servicetemplate' => [
$items // Dynamic data needed here!
]
];
$result = $this->templatedoc->SaveAll ($data);
Seems very fussy it has to be type int, adding here in case someone else gets caught out, would love to know if there is a better solution.
I'm adding a virtual property within a Symfony entity class. This property shall be computed based on another table data - specifically on a column that is of the Doctrine array type.
class RelatedEntity
{
/* ... */
/**
* #ORM\Column(type="array")
*/
protected $type;
The point is I would like to use Doctrine Criteria for this as it's supposed to be optimized on SQL level. So I did this:
public function getCreated()
{
$criteria = Criteria::create()->where(Criteria::expr()->contains('type', 'create'));
$relatedEntity = $this->getRelatedEntities()->matching($criteria);
if (!$relatedEntity) {
return null;
}
return $relatedEntity->getTimestamp();
}
But I get an empty result set. Even though Doctrine is building a correct SQL statement, which works when I type it manually into the PostgreSQL database.
...WHERE type LIKE '%create%'
What is wrong with this approach and how can it be solved? Right now I did the trick with the ArrayCollection filter method, but it loads all related entities I don't need.
Thank you for any ideas.
EDIT: This is not a duplicate of the mentioned question as I cannot use EntityManager or EntityRepository inside an entity. I need to use Criteria, so the solution proposed in the question doesn't work for me.
Check the results of getRelatedEntities()
Depending on how this collection was created, any one of several things may be happening. In particular, it may be using entity aliases, or may not be returning any which match your Criteria.
Collection populated from an aliased entity (i.e.: via a QueryBuilder join/select).
If getRelatedEntities is populated by Doctrine via QueryBuilder, you've likely aliased the Entities.
EX.: $queryBuilder->addSelect('thing')->leftJoin('root_alias.entity',
'thing')
In such a case, the Criteria must use the alias:
Criteria::expr()->contains('thing.type', 'create')
No matches for Criteria.
Dump your collection before filtering it, this could be a simple case of your query having already filtered out any potential matches.
Test your Criteria
All things considered, without any clue as to the structure of the collection you're trying to filter, we can only assess your criteria. Thus, test your criteria, and check the contents of the collection you are attempting to filter.
$criteria = Criteria::create()->where(Criteria::expr()->contains('type', 'create'));
$collection = new ArrayCollection([
[
'key' => 1,
'type' => 'somethingcreatesomething',
],
[
'key' => 2,
'type' => 'abra',
],
[
'key' => 3,
'type' => 'cadabra',
],
[
'key' => 4,
'type' => 'alacreate',
],
]);
dump($collection->matching($criteria));
Result
Doctrine\Common\Collections\ArrayCollection {#2536
-elements: array:2 [
0 => array:2 [
"key" => 1
"type" => "somethingcreatesomething"
]
3 => array:2 [
"key" => 4
"type" => "alacreate"
]
]
}
I know the Question title is a bit murky, but here's what I'm trying to do:
I'm retrieving a list of groups that a user belongs to from a third party api. In some cases, the user will be an 'admin' for a group and other times, just a 'member'.
Specifics aside, I'm calling a method on my api class from my controller that hits the api, retrieves the user's groups, decides if they are an 'admin' or not, then returns an array of arrays with each group's information including a 'role' key that denotes whether or not they are an 'admin'. So my returned array looks something like this:
[
0 => [
'unique_id' => 1243657,
'name' => 'Group1',
'city' => 'Bluesville',
'state' => 'IN',
'role' => 'admin'
],
1 => [
'unique_id' => 4324567,
'name' => 'Group2',
'city' => 'New Curtsbourough',
'state' => 'WI',
'role' => 'member'
],
2=> [
'unique_id' => 87463652,
'name' => 'Group3',
'city' => 'Samsonite',
'state' => 'MN',
'role' => 'member'
]
]
Now, I need to take those groups and store them in the database, which I'm doing by checking first that the group doesn't exist in the database, then adding it if needed. Of course, I'm leaving off the role, as it is only relevant to the current user.
Next, I need to connect the current user to these groups that were just retrieved. I have a pivot table set up that currently holds the user_id and group_id.
The question is, how to best handle this. Before I decided that I needed to know whether or not a member was an 'admin' or not, I simply had my 'createGroups' method return an array of primary keys to me, then passed that array to a call to
$user->groups()->sync($array_of_ids);
However, with the added 'role' information, it's not as cut and dry.
Basically, at this point in the lifecycle, I have access to an array of groups that contains a field 'role'. My thinking says to add a 'role' field to the pivot table, which would then contain 'user_id', 'group_id' and 'role'. This means I'll not only need the $groups array with the retrieved groups, but the ids of those groups as they pertain to my database.
I could make something work, but I'm afraid it would be extremely messy and inefficient.
Thoughts anyone??
Ok, as happens many times on Stackoverflow, I've come to a solution for my own question. I'm posting so that in the off-chance someone stumbles upon my question needing to do something similar, they can at least see how one person handled it.
According to the Laravel docs, if you want to sync relationships with an added column, you need to call sync in the following way:
$user->groups()->sync([
1 => ['role' => 'admin'],
2 => ['role' => 'member'],
3 => ['role' => 'member']
]);
So before I could sync, I needed an array that resembled the array that is being passed to 'sync'.
Since I had an array of 'groups' that included a field called 'role' for each group, I created a 'createGroups' method that basically looped over the $groups array and called the 'insertGetId' method that Laravel provides. This method persists the object to the database and returns the primary key of the created record. For my 'createGroups' method, I did the following:
public function createGroups($groups)
{
$added = array();
foreach($groups as $group){
$id = $this->createGroup($group);
$added[$id] = ['role' => $group['role']];
}
return $added;
}
So as I'm inserting 'groups' into the database, I'm building up the array that is needed by the 'sync' method. Since the 'createGroup' method uses Laravel's 'insertGetId' method, it returns the primary key for that group. Then I use that id as the key to the array. After all groups are inserted, my 'added' array that is returned to my controller, looks like this:
[
1 => ['role' => 'admin'],
2 => ['role' => 'member'],
3 => ['role' => 'member']
]
which is exactly what the 'sync' method needs to do it's thing.
Happy coding!