possible bug in cakephp framework but not sure about that
I got following MySQL InnoDB tables:
database.users
+-----+---------------+----------+
| id | user_group_id | username |
+-----+---------------+----------+
| INT | INT | VARCHAR |
database.user_settings
+-----+---------------+----------+
| id | user_id | data |
+-----+---------------+----------+
| INT | INT | VARCHAR |
I got following intialization in table classes:
Model\Table\UsersTable:
$this->table('users');
$this->displayField('id');
$this->primaryKey('id');
$this->belongsTo('UserGroups', [
'foreignKey' => 'user_group_id'
]);
$this->hasOne('UserSettings', [
'foreignKey' => 'user_id'
]);
Model\Table\UserSettingsTable
$this->table('user_settings');
$this->displayField('id');
$this->primaryKey('id');
$this->belongsTo('Users', [
'foreignKey' => 'user_id'
]);
// And folowing validation rules:
$validator
->add('id', 'valid', ['rule' => 'numeric'])
->allowEmpty('id', 'create')
// UserSettings.user_id validation rule:
->add('user_id', 'valid', ['rule' => 'numeric'])
->requirePresence('user_id', 'create')
->notEmpty('user_id');
And I got following code:
$user = $this->Users->newEntity();
if ($this->request->is('post')) {
$user = $this->Users->patchEntity($user, $this->request->data, [
'associated' => ['Users.UserSettings']
]);
// Tried it also this way, won't change anything
//$user = $this->Users->patchEntity($user, $this->request->data, [
// 'associated' => ['user_setting']
//]);
$this->Users->save($user,['associated' => ['UserSettings']]);
}
Sample input data ($this->request->data):
[
'user_group_id' => 1, // Not related to question
'username' => 'test', // This will be saved without core modifications
'user_setting' => [
'data' => 'sample data' // Saved only after "fix" described below
]
];
This will save parent table (users) but not child table (user_settings).
Then I got following modification I want to get rid of:
If I put this change to ORM\Associations\HasOne::saveAssociated(...)
// $this->property() == 'user_setting'
if (is_array($targetEntity)) {
$targetEntity = $this->target()->newEntity($targetEntity);
}
It will instantly work the way I wanted to. HasOne actually has required data but it also checks if data is inside Entity, in this case it was in array.
I've played with different association naming combinations and it seems that this is how it should be.
Main question is how should I save optional hasOne association with parent row?
Can I somehow make sure that data will be converted into Entity object?
I think it should just work as there is all required data available and it also seems to handle relations correctly.
With help provided by #ndm I managed to solve this problem with custom marshaller.
I first did take a look on separate validators but for me it seems like too complicated and fragile way to solve very simple straightforward problem.
My honest opinion is that all this should actually be taken care inside framework core somehow.
Solving required fk with custom marshaller:
This way it should be sure that "custom validation" will be used only when UserSettings will be created through Users and user_settings.user_id should be readily available.
class UsersMarshaller extends Marshaller {
protected function _validate($data, $options, $isNew) {
$errors = parent::_validate($data, $options, $isNew);
if ($isNew) {
unset($errors['user_setting']['user_id']['_required']);
}
return $errors;
}
}
And in UsersTable class:
public function marshaller() {
return new UsersMarshaller($this);
}
Actual errors in original question:
patchEntity(...) call parameters was faulty even while those parameters did something that made them look like they might be correct.
Associations should be this way: ['associated' => ['UserSettings']]. Not Users.UserSettings or user_setting.
The correct value for the associated property would be UserSettings, what you have used, Users.UserSettings, would be a Users > Users > UserSettings association.
Related
I have multiple fields setup as FK to the same table. The FK can also be NULL.
I keep getting this error:
ExistsIn rule for 'late_agreement_exception_outcome_recommendation_id' is invalid. 'ExceptionOutcomes' is not associated with 'App\Model\Table\ExceptionsTable'.
Database structure:
exceptions
id,
request_id,
late_agreement_exception_outcome_recommendation_id (FK, exception_outcomes->id)
late_agreement_exception_outcome_id (FK, exception_outcomes->id),
...
exception_outcomes
id,
name
...
Column definition (ie. late_agreement_exception_outcome_recommendation_id):
Column relationship (ie. late_agreement_exception_outcome_recommendation_id):
ExceptionTable:
FK setup to ExceptionOutcomes
$this->belongsTo('LateAgreementExceptionOutcomeRecommendations', [
'class' => 'ExceptionOutcomes',
'foreignKey' => 'late_agreement_exception_outcome_recommendation_id',
'joinType' => 'INNER',
]);
Edited rules attempting to enable entry of a null value for the field value:
$rules->add(
function ($entity, $options) {
$rule = new ExistsIn('late_agreement_exception_outcome_recommendation_id', 'ExceptionOutcomes');
return $entity->late_agreement_exception_outcome_recommendation_id === NULL || $rule($entity, $options);
},
['errorField' => 'late_agreement_exception_outcome_recommendation_id']
);
Update #1
I changed the association name like so:
$rules->add(
function ($entity, $options) {
$rule = new ExistsIn('late_agreement_exception_outcome_recommendation_id', 'LateAgreementExceptionOutcomeRecommendations');
return $entity->late_agreement_exception_outcome_recommendation_id === NULL || $rule($entity, $options);
},
['errorField' => 'late_agreement_exception_outcome_recommendation_id']
);
And got the following issue simply saving the data:
SQLSTATE[42S02]: Base table or view not found: 1146 Table 'sdi_ips2.late_agreement_exception_outcome_recommendations' doesn't exist
Previously, I could save the column when providing a value. However, trying to revert to NULL would cause an issue.
Update #2
try
{
$this->Requests->save($request);
}
catch(Cake\Database\Exception\DatabaseException $e)
{
debug("here!");
exit;
}
Update #3
Here's what I see in the SQL log:
Generated Models
The following Table objects used Cake\ORM\Table instead of a concrete class:
LateAgreementExceptionOutcomeRecommendations
Solution:
Note the className attribute in belongsTo.
$this->belongsTo('LateAgreementExceptionOutcomeRecommendations', [
'className' => 'ExceptionOutcomes',
'foreignKey' => 'late_agreement_exception_outcome_recommendation_id',
'joinType' => 'INNER',
]);
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.
I'm trying to implement a many-to-many relationship in a Laravel 5.6 app, using Eloquent ORM. I have Customer and CustomerGroup models, that correspond with the database tables of the same name, and a pivot table customers_customergroups. Relatively identical case is provided in Laravel's documentation with users and roles. The customers_customergroups table looks like this:
+-------------+------------------+------------+------------+
| customer_id | customergroup_id | created_at | updated_at |
+-------------+------------------+------------+------------+
| 53210 | 2 | -- | -- |
| 53210 | 4 | -- | -- |
| 53211 | 3 | -- | -- |
| 53211 | 4 | -- | -- |
+-------------+------------------+------------+------------+
My Customer model has a method called customerGroups() which retrieves all groups the customer belongs to.
public function customerGroups()
{
return $this->belongsToMany('App\CustomerGroup', 'customers_customergroups', 'customer_id', 'customergroup_id');
}
The retrieval works fine, but when I try to update Customer by passing a Request object as an array to the update() Eloquent method, and in the same repository method I have to update the customers_customergroups table, the latter updates normally, but Customer::find($id)->update($request->toArray()) throws an Exception:
SQLSTATE[42S22]: Column not found: 1054 Unknown column 'groups' in 'field list'....
There is no parameter named groups in the Request object passed to Customer::find($id)->update();
What happens here, why does this exception get thrown, what am I doing wrong, and how to do it properly?
Thank you in advance!
Edit (posting the log requested in the comments below):
[2019-05-14 13:42:11] local.INFO: array (
'id' => 53211,
'name' => 'CUSTOMER ONE',
'surname' => NULL,
'Telephone' => '0123-4567890',
'email' => 'email#example.com',
'Street' => '22 BAKER STR.',
'City' => 'LONDON',
'Postal_code' => '1234',
'is_active' => '1',
'website' => '',
)
Edit (Adding code requested by HCK):
Customer.js
$scope.CreateCustomer = function(isValid) {
if (isValid)
{
PostData = {
'id': $scope.customer_id,
... all other parameters from the previous edit ...
}
if(($scope.izbraniGrupi).length > 0)
{
PostData.izbraniGrupi = $scope.groupsSelected;
}
$http({
method : 'PUT', // Laravel controller is a resource
url : 'customers',
data : PostData
}).then(function mySuccess(response) {
// Other stuff that happens on responseCode == 200.
});
};
CustomerController.php
// $this->repository is defined normally in __construct() and works as supposed to.
public function update(Request $request, $id)
{
try {
$this->repository->update($id, $request);
} catch(\Exception $e) {
return response()->json([
'message' => $e->getMessage()
]);
}
return response()->json([
'message' => 'Customer updated'
]);
}
CustomerRepository.php
public function update($id, $data)
{
if(isset($data['izbraniGrupi'])) {
DB::table('customers_customergroups')->where(['customer_id' => $id])->delete();
foreach ($data['izbraniGrupi'] as $group) {
DB::table('customers_customergroups')->insert([
'customer_id' => $id,
'customergroup_id' => $group['id']
]);
}
unset($data['izbraniGrupi']);
}
Customer::find($id)->update($data->toArray());
}
I think the problem is related to the use of the full Request class instead of just getting the payload. Try this:
CustomerController.php
...
$this->repository->update($id, $request->all());
// or even better: ^^^^^^^^
$this->repository->update($id, $request->only('the', 'fields', 'that', 'you', 'expect'));
// ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
Not related but I noticed that you are doing this inside your repository:
...
DB::table('customers_customergroups')->where(['customer_id' => $id])->delete();
foreach ($data['izbraniGrupi'] as $group) {
DB::table('customers_customergroups')->insert([
'customer_id' => $id,
'customergroup_id' => $group['id']
]);
}
...
This looks to me that you are detaching all the related records in order to re-attach the customer groups to customers. This looks a good case for Syncing Associations:
Customer::find($id)->customerGroups()->sync(collect($data['izbraniGrupi'])->pluck('id'));
My application allows a user to create scenarios by linking together soe_blocks. In turn, soe_blocks refer to a variable number of soe_entries.
To build scenarios, soe_blocks are linked to the scenario and ordered by an offset. The soe_blocks can be used in many different scenarios. soe_entries can relate only to a single soe_block
I think the relationship is defined as:
scenarios belongsToMany soe_blocks through scenarios_soe_blocks
soe_blocks belongsToMany scenarios through scenarios_soe_blocks
scenarios_soe_blocks is where the offset is kept
soe_entries haveOne soe_blocks
Tables:
scenarios: id | name
data: 0, 'scenario_1'
soe_blocks: id | name
data: 0, 'soe_block_1'
1, 'soe_block_2'
scenarios_soe_blocks: id | scenario_id | soe_block_id | offset
data: 1, 0, 1, 1
2, 0, 2, 2
Models:
class ScenariosTable extends Table
{
$this->belongsToMany('SoeBlocks', [
'foreignKey' => 'scenario_id',
'targetForeignKey' => 'soe_block_id',
'through' => 'ScenariosSoeBlocks',
'joinTable' => 'soe_blocks'
]);
}
class SoeBlocksTable extends Table
{
$this->belongsToMany('Scenarios', [
'foreignKey' => 'soe_block_id',
'targetForeignKey' => 'scenario_id',
'joinTable' => 'scenarios_soe_blocks',
'through' => 'ScenariosSoeBlocks'
]);
}
class ScenariosSoeBlocksTable extends Table
$this->belongsTo('SoeBlocks', [
'foreignKey' => 'soe_block_id',
'joinType' => 'INNER'
]);
}
Controllers:
public function view($id = null)
{
$scenario = $this->Scenarios->get($id, [
'contain' => ['SoeBlocks', 'RunStatus', 'ScenarioLog']
]);
$this->set('scenario', $scenario);
}
As far as I can make out from CakePHP Doc, this is all I need. But I couldn't get the ScenarioController->view() method to return the offsets from the scenarios_soe_blocks table associated with the soe_blocks.
I tried to add ScenariosSoeBlocks into the 'contain' clause in the ScenarioController, but got the error: Scenarios is not associated with ScenariosSoeBlocks. I found an SO article that suggested I add the following to the ScenarioTable:
$this->hasMany('ScenariosSoeBlocks', [
'foreignKey' => 'scenario_id'
]);
This seems to have worked, and now I can request ScenariosSoeBlocks in my controller like this:
$scenario = $this->Scenarios->get($id, [
'contain' => ['SoeBlocks', 'ScenariosSoeBlocks', 'RunStatus', 'ScenarioLog']
]);
Which at least gets the data into the view template, but not in the single object I'm hoping for. Eventually, I want to be able to CRUD the soe_blocks along with their associated soe_entries, in an object that looks like this:
offset | soe_block_id | soe_entry_id |
I have many other questions, like how to save etc., but I figured I need to get this working first.
So, my questions for now are:
are my associations correct?
how do I retrieve all the associations to view?
are my associations correct?
The first two are, but then it should be:
soe_blocks hasOne soe_entries
soe_entries belongsTo soe_blocks
how do I retrieve all the associations to view?
By containing them, just like you did in your first example. This question seems to originate from the question how to access the join table data, which is very simple, the join table data is being set on the target table entity (Scenario or SoeBlock, depending on from which side/table you issue the query), in a property named _joinData:
$joinTableEntity = $scenario->soe_blocks[0]->_joinData;
$offset = $joinTableEntity->offset;
You can easily gather information about the data structure by dumping your entity contents:
debug($scenario);
See also
Cookbook > Database Access & ORM > Associations - Linking Tables Together
Cookbook > Database Access & ORM > Saving Data > Saving Additional Data to the Join Table
I have a small project on CakePHP. It has a table named articles, and 5 tables of Fields like categories, tags, images, etc. The associations are mostly HasOne, and associated tables has multiple column.
When saving data on articles table everything look good, but on some associations, for example: Article -> Rating, if I did not fill up some fields of Rating tables, that are being saved as null:
+----+------------+--------------+
| id | article_id | rating_value |
+----+------------+--------------+
| 1 | 36 | 3 |
| 2 | 56 | 5454.56 |
| 3 | 57 | 4 |
| 5 | 51 | NULL |
+----+------------+--------------+
If I add some validations, then I can't save the article as it need to be validated. All I want is that if rating_value is empty, then it must not be created as null (entity rejected), and the article must be saved.
Deleting articles works as expected, all related entities are deleted.
I tried altering $data on Model.beforeMarshall but the class is private in both Tables Articles and Ratings (i think associations may be the problem).
Some code (controller add):
public function add()
{
$article = $this->Articles->newEntity();
if ($this->request->is('post')) {
$article = $this->Articles->patchEntity($article, $this->request->data, [
'associated' => [
'Ratings',
]
]);
if ($this->Articles->save($article)) {
$this->Flash->success(__('Saved.'));
return $this->redirect(['action' => 'index']);
}
}
$this->set('article', $article);
}
I deleted all validations of every associated Model because of this.
// Articles Table
$this->hasOne('Ratings', [
'className' => 'Ratings',
'dependent' => true,
]);
// Ratings Table
$this->belongsTo('Articles', [
'foreignKey' => 'article_id',
'joinType' => 'INNER'
]);
// Rating.php Entity
protected $_accessible = [
'*' => true,
'id' => false
];
// Article.php Entity
protected $_accessible = [
'*' => true,
];
If you are setting rating_value then it will try to save and validate with all validation rules.
You can remove the associated data if rating_value is empty like this
$dataToSave = $this->request->data;
if(empty($dataToSave['rating']['rating_value'])){
unset($dataToSave['rating']);
}
$article = $this->Articles->patchEntity($article, $dataToSave, [
'associated' => [
'Ratings',
]
]);
I thing every thing looks good.May be your field in table has default value as null. For example:
If 'rating_value' field in your table has default value null,
make that default full to none first.
If validation fails that don't give you save anything rather than saving null value.If that still don't work for you see:
debug($this->request->data);
And look there if data are coming correctly from your view(form).
Another important thing is that you can use different validation set for association. (see here)