CakePHP empty Fields of associations are saved instead of discard - php

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)

Related

Laravel 5.6 Eloquent tries to update non-existing column when nobody asked it to. (Many-to-Many relationship)

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

Using select2_from_ajax with a JSON datasource

I am new to Laravel and to Backpack for Laravel so please bear with me. I am trying to create a client registration form that features a "State" field which is populated dynamically depending on the value selected for the "Country" field.
I am following the instructions provided by Backpack's author here: https://backpackforlaravel.com/docs/3.5/crud-fields#select2_from_ajax
Both states and countries come from this dataset: https://github.com/antonioribeiro/countries. They are returned as collections but they are not read from the DB.
Table schema for Clients (simplified)
+-------------------+---------+--------+
| Field | Type | Length |
+-------------------+---------+--------+
| uuid | CHAR | 36 |
| name | VARCHAR | 128 |
| city | VARCHAR | 128 |
| state | VARCHAR | 64 |
| country_iso_cca2 | VARCHAR | 2 |
+-------------------|---------+--------+
The good part
The "Country" field works just fine. It fetches data from the JSON dataset at creation and reads/writes info from or to the DB on update/save:
// ClientCrudController.php
$countries = new Countries();
$allCountriesCodes = $countries->all()->pluck('name.common', 'cca2')->toArray();
$this->crud->addField([
'name' => 'country_iso_cca2',
'label' => 'Country',
'type' => 'select2_from_array',
'options' => $allCountriesCodes,
'allows_null' => false,
'default' => 'US',
]);
The bad (incomplete) part
// ClientCrudController.php
$this->crud->addField([
'name' => 'state',
'label' => 'State',
'type' => 'select2_from_ajax',
'entity' => 'foobar', // the method that defines the relationship in your Model
'attribute' => 'name',
'data_source' => url('api/states'),
'placeholder' => 'Select a country first...',
'dependencies' => ['country_iso_cca2'],
]);
Calling /admin/client/create will result in an error ("Call to undefined method App\Models\Client::foobar").
I understand that the error is raised because there is no model defined for States and hence no relationship. My problem is, I do not understand what the implementation is supposed to look like in a case like this where the two select fields do not represent separate entities at an ORM level.
Is it possible to implement this kind of dependency in a "backpack-native" way, without resorting to creating a custom field type?

Is there some option to create Yii2 ActiveDataProvider from query and static data?

I use ActiveDataProvider to display some data using GridView. Also I have some data retrieved from API and this is a array. For example:
$emails = [
'123' => 'john#test.io',
'234' => 'jane#test.io',
'345' => 'jake#test.io'
];
$query = new Query();
$query->select(['id', 'username'])->from('user');
$provider = new \yii\data\ActiveDataProvider([
'query' => $query,
'pagination' => [
'pageSize' => 100,
],
]);
Result of displaying this ActiveDataProvider will be something like this:
+------+--------------+
| id | username |
+------+--------------+
| 123 | John |
| 234 | Jane |
| 345 | Jake |
+------+--------------+
How can I join data from query and array to get something like this:
+------+--------------+-------------------+
| id | username | email |
+------+--------------+-------------------+
| 123 | John | john#test.io |
| 234 | Jane | jane#test.io |
| 345 | Jake | jake#test.io |
+------+--------------+-------------------+
I found the only one option - create an array from query and merge it with existing array, but it looks bad for me. Is there any other solution?
You are probably looking for this
https://www.yiiframework.com/doc/api/2.0/yii-data-arraydataprovider
An ArrayDataProvider acts in a lot of the ways the same as an ActiveDataProvider and you can use it as the base data for most of the Yii2 controls. I would get the ActiveDataProvider data, parse it, match it to what you already have by default and create an array that can be used as the base of the ArrayDataProvider.
If you are putting it into a DataGrid you can always do something like this for the email column
[
'class' => 'yii\grid\DataColumn',
'attribute' => 'email',
'format'=>'raw',
'value'=>function ($model) use ($emails) {
return $emails[$model->id];
},
],
A way , for complex or aggregated model, could be based on adding and afterFind method in your model
add the property you need and populate you value with a proper function eg:named getYourRelatedEmail()
or you could use an assigment depending the way you obatin the email array or the related values)
In your model
public $userEmail;
public function afterFind()
{
$this->userEmail = getYourRelatedEmail();
}
in this way you can acces in gridview to the model data field as common model field
In this way you could retrive all the data you need form different sources and build yuor composite model as you need ..

Cakephp 3 - Save associated belongsToMany (joinTable)

i have a question about saving a new entity with an association which is a belongsToMany relation. Just a quick introduction. I have something like a settings table and a setting_values table. The user is connected to the settings by a joinTable called users_settings. So in summary:
settings:
id | name
setting_values:
id | setting_id | name | value |
users_settings:
id | user_id | setting_id | setting_value_id
Now before the user is added I want to patch the entity of the user with all settings and the first setting_value for each setting. So that users_settings has now all settings connected to the user with the first value. But I can't get the patch or the newEntity to be work. All models are baked so this should be fine. Here's my code
$settings = $this->Settings->find('all', [
'contain' => [
'SettingValues'
]
]);
$settingsData = [];
foreach ($settings as $setting) {
$settingsData[] = [
'setting_id' => $setting->id,
'setting_value_id' => $setting->setting_values[0]->id,
];
}
$data = [
'users_settings' => $settingsData
];
$user = $this->Users->patchEntity($user, $data, [
'associated' => [
'Settings.UsersSettings'
]
]);
This is the result in the $user entity. As you can see nothing is corretly marshaled:
users_settings => [
(int) 0 => [
setting_id => (int) 1,
setting_value_id => (int) 1
],
(int) 1 => [
setting_id => (int) 2,
setting_value_id => (int) 5
]
]
Can someone give me an advice on this. Thanks
That's not how belongsToMany association data should be structured, you have to supply the target, not the join table. Additional join table data can be supplied via the special _joinData key, ie the data should look like:
$data = [
'settings' => [
[
'id' => 1,
'_joinData' => [
'setting_value_id' => 1
]
],
[
'id' => 2,
'_joinData' => [
'setting_value_id' => 5
]
]
]
];
That would populate the join table like:
+----+---------+------------+------------------+
| id | user_id | setting_id | setting_value_id |
+----+---------+------------+------------------+
| 1 | 1 | 1 | 1 |
| 2 | 1 | 2 | 5 |
+----+---------+------------+------------------+
And if you (need to) use the associated option, then make sure to specify the _joinData key too:
$user = $this->Users->patchEntity($user, $data, [
'associated' => [
'Settings._joinData'
]
]);
See also
Cookbook > Database Access & ORM > Saving Data > Saving BelongsToMany Associations
Cookbook > Database Access & ORM > Saving Data > Saving Data To The Join Table

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.

Categories