Enabling null on many FKs to same associated table - php

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

Related

CakePHP buildRules not firing on hasMany nested assocation delete

I am having issue with build rules not firing deleting 1 element from a HasMany association on a model in CakePHP 3. My base table (Settings) is structured with the following association:
<?php
// SettingsTable.php
public function initialize(array $config)
{
parent::initialize($config);
$this->hasMany('Mappings', [
'foreignKey' => 'mappings_id',
'saveStrategy' => 'replace'
]);
}
The associated mappings table has custom rule in it's model that checks for the existence of children in the values table, prior to letting a mapping be deleted:
<?php
// MappingsTable.php
public function initialize(array $config)
{
parent::initialize($config);
$this->hasMany('Values', [
'foreignKey' => 'values_id',
'saveStrategy' => 'replace'
]);
}
public function buildRules(RulesChecker $rules): RulesChecker
{
$rules->addDelete(/* Has children rule, prevents deletion if children are found in Values table */);
}
I am using the following to update the settings table, and delete a nested mapping table entry (id 3):
<?php
$requestData = [
'title' => 'A new updated title',
'mappings' => [
['id' => 1, 'sort_order' => '5'],
['id' => 2, 'sort_order' => '10'],
// mappings id 3 was previously defined, but removed to make patchEntity() / save() delete it
]
];
$settingsTable->patchEntity($settingEntity, $requestData, [
'associated' => ['Mappings', 'Mappings.Values']
]);
$settingsTable->save($settingEntity);
Based on what I read about setting the saveStradegy key to replace for the hasMany association in the cake docs here, it seems like this should work.
The delete on the mapping with id 3 does fire in cake using this code, but for some reason the buildRules() call in the 2nd code spinet never fires. $rules->addDelete() never fires if there are child values on a mapping, and cake is forced to default to show the MySQL foreign key constraint failure message, rather than a cleaner build rule failure:
"SQLSTATE[23000]: Integrity constraint violation: 1451 Cannot delete or update a parent row: a foreign key constraint fails (`db`.`values`, CONSTRAINT `values_fk` FOREIGN KEY (`field_id`) REFERENCES `fields` (`id`))"
What do I need to do to get the buildRules() on MappingsTable to fire when when a mapping is removed from a setting, like I am above?
Any help is appreciated!

CakePHP: Unable to save data to parent entity via belongsTo associated child

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.

How can I specify the database table name for a model in PHPixie?

I've got an error
SQLSTATE[42S02]: Base table or view not found: 1146 Table
'phpixie.persons' doesn't exist
when queuering table 'people':
public function getPerson($person_id)
{
return $this->components()->orm()->query('person')
->where('id', $person_id)
->findOne();
}
From documentation:
By default ORM assumes that the table name is the plural of the name
of the model, and that the name of the primary key is ‘id’.
How can i specify table name directly?
PS Or specify rules list like EnglishPluralizationService:
{"child", "children"} {"corpus","corpora"} {"person", "people"}
Depending on which version of PHPixie you are using, there are different solutions to specify the table name for a model:
2.x
Assuming you already have a Person model, you can specify the table name using the $table field:
class Person extends \PHPixie\ORM\Model
{
public $table = 'person';
}
For reference, see:
https://phpixie.com/2.x/orm.html
3.x
You can override the default assumptions in your configuration file:
return array(
'models' => array(
'person' => array(
'table'=> 'persons',
),
);
);
For reference, see:
https://phpixie.com/components/orm.html#configuration
Phpixie3:
<?php
// bundles/app/assets/config/orm.php
return [
'models' => [
'person' => [
'table' => 'people',
// if you want to change id field also:
'id' => 'pid',
],
],
];
Call with:
$builder->components()->orm()->query('person')->in(432)->findOne();
Should become
SELECT * FROM person WHERE id=432;
or, if you also overriden id field:
SELECT * FROM person WHERE pid=432;

CakePHP 3 Saving HasOne association

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.

Yii Relations with non-Primary keys

I have a mysql table and a mysql view I'm trying to build relations for.
The table(commissions) is as follows:
--commissions--
id(primary Key)
date_added
order_id
salesrep_id
customer_id
commission_total
status
The view(rep_view_customer) is as follows:
--rep_view_customer--
entity_id
email
first_name
last_name
company
I'm trying to relate rep_view_customer to commissions on commissions.customer_id = rep_view_customer.entity_id.
I've tried using the on option:
'rep_view_customer' => array(self::HAS_ONE, 'RepViewCustomer', '', 'on'=>'rep_view_customer.entity_id = t.customer_id')
I also tried setting the primary key for the rep_view_customer model using:
public function primaryKey(){
return 'entity_id';
}
But I always seem to end up with an error similar to:
CDbCommand failed to execute the SQL statement: SQLSTATE[42S22]:
Column not found: 1054 Unknown column 't.customer_id' in 'where
clause'. The SQL statement executed was: SELECT
rep_view_customer.entity_id AS t1_c0,
rep_view_customer.email AS t1_c1,
rep_view_customer.first_name AS t1_c2,
rep_view_customer.last_name AS t1_c3,
rep_view_customer.company AS t1_c4 FROM rep_view_customer
rep_view_customer WHERE (rep_view_customer.entity_id =
t.customer_id)
I'm at my wit's end what to try next
You will have to create a foreign key inside repview_customer to relate to commissions. You won't be able to do it with just a primary key.
I did it this way and it works:
so in the commissions model you put: (it does the relation through itself within the model, then the join output will be the same as the normal relation but with other unique key in commission model)
public function relations()
{
return array(
'commission' => array(self::HAS_ONE, 'Commission', 'entity_id', 'on' => 'commission.customer_id=rep_view_customer.entity_id'),
'rep_view_customer' => array(self::HAS_MANY, 'RepViewCustomer', '', 'through' => 'commission', 'condition' => '...'),
),
}

Categories