Cakephp 3, Updating associated belongsto Data - cakephp

So I have 2 models: Plugins and Imports.
The association is Plugins hasOne Imports:
$this->hasOne('Imports', [
'foreignKey' => 'plugin_id'
]);
and Imports belongsTo Plugins:
$this->belongsTo('Plugins', [
'foreignKey' => 'plugin_id',
'joinType' => 'INNER'
]);
My plugins-table dump looks like this:
CREATE TABLE `plugins` (
`id` int(10) unsigned NOT NULL AUTO_INCREMENT,
`type` varchar(50) NOT NULL,
`pluginkey` varchar(100) NOT NULL,
`pluginname` varchar(255) DEFAULT NULL,
`description` text,
`integrationinstructions` text,
`date_available` datetime DEFAULT NULL,
`date_last_changed` datetime DEFAULT NULL,
`author` varchar(255) DEFAULT NULL,
`version` varchar(10) DEFAULT NULL,
PRIMARY KEY (`id`)
) ENGINE=InnoDB AUTO_INCREMENT=1 DEFAULT CHARSET=utf8;
and my imports table is:
CREATE TABLE imports (
id INT UNSIGNED AUTO_INCREMENT PRIMARY KEY,
plugintype VARCHAR(50) NOT NULL,
importedpluginkey VARCHAR(255) NOT NULL,
imported DATETIME NULL DEFAULT NULL,
plugin_id INT UNSIGNED NOT NULL,
FOREIGN KEY plugin_key (plugin_id) REFERENCES plugins(id)
);
Now i baked both controllers and everything works fine. I can save an import(will later be used to import xml files) and it is linked to a plugin.
But now I want to let the Plugindata update through data I add in an import.
For example let's say i have a Plugin called "sale" with the type Wordpress and the key "wp_sale". Now i want to update this plugin by adding an import. For example changing it's description or name.
If i understand this right, I would only need to pass the data I want to change in the plugins alongside the id.
But when I check the patchEntity that is going to be safed the Plugin Entity is always '[new]'=> true, and the id I passed is gone.
This is what my controller looks like:
public function add()
{
$import = $this->Imports->newEntity();
if ($this->request->is('post')) {
$import = $this->Imports->patchEntity($import, $this->request->data, ['associated' => ['Plugins']]);
if ($this->Imports->save($import)) {
$this->Flash->success(__('The import has been saved.'));
return $this->redirect(['action' => 'index']);
} else {
$this->Flash->error(__('The import could not be saved. Please, try again.'));
}
}
$plugins = $this->Imports->Plugins->find('list', ['limit' => 200]);
$this->set(compact('import', 'plugins'));
$this->set('_serialize', ['import']);
}
And this is what my form looks like:
echo $this->Form->input('plugintype');
echo $this->Form->input('importedpluginkey');
echo $this->Form->input('imported');
echo $this->Form->input('plugin_id', ['options' => $plugins]);
echo $this->Form->input('plugin.pluginkey');
echo $this->Form->input('plugin.type');
echo $this->Form->input('plugin.id', ['value' => 1]);
I gave the plugin.id the value 1 just for the sake of testing, because I wanted to pass an id for the Plugin.
My postdata looks like this:
[
'plugintype' => 'wordpress',
'importedpluginkey' => 'wp_sale',
'imported' => [
'year' => '2015',
'month' => '11',
'day' => '24',
'hour' => '10',
'minute' => '38'
],
'plugin_id' => '1',
'plugin' => [
'pluginkey' => 'wp_sale',
'type' => 'wordpress',
'id' => '1'
]
]
And the patched Entity looks like this:
object(App\Model\Entity\Import) {
'plugintype' => 'wordpress',
'importedpluginkey' => 'wp_sale',
'imported' => object(Cake\I18n\Time) {
'time' => '2015-11-24T11:04:00+0000',
'timezone' => 'UTC',
'fixedNowTime' => false
},
'plugin_id' => (int) 1,
'plugin' => object(App\Model\Entity\Plugin) {
'pluginkey' => 'wp_sale',
'type' => 'wordpress',
'[new]' => true,
'[accessible]' => [
'*' => true
],
'[dirty]' => [
'pluginkey' => true,
'type' => true
],
'[original]' => [],
'[virtual]' => [],
'[errors]' => [],
'[repository]' => 'Plugins'
},
'[new]' => true,
'[accessible]' => [
'*' => true
],
'[dirty]' => [
'plugintype' => true,
'importedpluginkey' => true,
'imported' => true,
'plugin_id' => true,
'plugin' => true
],
'[original]' => [],
'[virtual]' => [],
'[errors]' => [],
'[repository]' => 'Imports'
}
Many Questions on this site are about association saving. But what i want is update associated data when adding new data in the import model.
The reasoning behind it is, that I want to be able to update my Plugins without editing everyone of them, and just use the xml import.
If the Question still is unclear feel free to ask and I will explain myself further. Thanks in advance!
Edit: To be clear, I basically need to change the data of a plugin using the import controller.
I now got the plugin entity in another function now, and i can save that with the import entity. However, it doesnt change anything in the plugin, so basically that does nothing.

The example in the linked docs seem to assume that the entities are set up in a way that they allow the primary key to be mass assigned, see
Cookbook > Database Access & ORM > Entities > Mass Assignment
The primary key needs to be accessible
By default, baked entities do not allow the primary key to be mass assigned, so you have to take care of that, for example by overriding the fields accessible state via the patchEntitity() options, like
$import = $this->Imports->patchEntity(
$import,
$this->request->data,
[
'associated' => [
'Plugins' => [
'accessibleFields' => ['id' => true]
]
]
]
);
See also Cookbook > Database Access & ORM > Saving Data > Changing Accessible Fields
Note that while the entity will still be flagged as "new", the saving process will (as long as the checkExisting option is not being set to false) test whether a record with the given primary key exists, and in case necessary mark the entity as "non-new" before proceeding, so that you'll end up with an update instead of an insert.
See also Cookbook > Database Access & ORM > Saving Data > Saving Entities

Just add dependent here, I think it will work then.
$this->hasOne('Imports', [
'foreignKey' => 'plugin_id',
'dependent' => true,
]);
Please see Cascading Deletes

Related

belongsToMany: allow and reuse an associated entry with existing UNIQUE title

What I have
My belongsToMany association is similar to the one from CakePHP Cookbook. However, I have set the UNIQUE constraint on tag titles.
(Another difference, which may be irrelevant, is I have added a site_id field next to every tag in the Tags table, and another composite UNIQUE constraint is set on both tag and site_id.)
What doesn't work
Submitting a duplicate tag title results in an error.
When I debug my new Article entity before saving it, I can see that the duplicate tag titles are rejected after a validation attempt.
'tags' => [
// This submitted tag title already exists in Tags
(int) 0 => object(App\Model\Entity\Tag) id:1 {
'site_id' => (int) 2
'[new]' => true
'[accessible]' => [
'site_id' => true,
'title' => true,
'created' => true,
'modified' => true,
'site' => true,
'articles' => true,
]
'[dirty]' => [
'site_id' => true,
]
'[original]' => [
]
'[virtual]' => [
]
'[hasErrors]' => true
'[errors]' => [
'title' => [
'unique' => 'The provided value is invalid', // ← error
],
]
'[invalid]' => [
'title' => 'test',
]
'[repository]' => 'Tags'
},
// …
// This submitted tag title does *not* already exist in Tags
(int) 3 => object(App\Model\Entity\Tag) id:4 {
'title' => 'tag'
'site_id' => (int) 2
'[new]' => true
'[accessible]' => [
'site_id' => true,
'title' => true,
'created' => true,
'modified' => true,
'site' => true,
'articles' => true,
]
'[dirty]' => [
'title' => true, // ← no error
'site_id' => true,
]
'[original]' => [
]
'[virtual]' => [
]
'[hasErrors]' => false
'[errors]' => [
]
'[invalid]' => [
]
'[repository]' => 'Tags'
},
]
How I expected it to work?
The behaviour I'm looking for is if a tag already exists, then take its ID and just link the submitted article entry to that existing ID. So ON DUPLICATE KEY clause, in a way.
Is there a flag that I'm missing that would tell/allow the ORM to do this, or should I maybe start trying some ->epilog() tricks?
There is no such functionality for the ORM saving process, no, and you cannot make use of epilog() with the default ORM save() process, you'd have to actually create the insert query manually then, however you cannot really use entities then, and it wouldn't solve the validation problem, and you'd have to more or less manually apply validation and application rules (you don't want to blindly insert data into insert queries, even tough Query::values() binds data).
I'd probably suggest to check if a solution that modifies the data before marshalling would be a good fit, that would integrate transparently into process. You could use your unique index columns to look up existing rows, and inject their primary key values into the request data, then the patching/marshalling process will be able to properly look up the existing records and update them accordingly.
Depending on the specific use case this could be more work than manually constructing insert queries, but it will IMHO integrate nicer. In your specific case it's probably easier, as using manual insert queries would require you to insert data for all the different tables separately, as you cannot make use of the ORM's association saving functionality with manually constructed insert queries.
To finish things off, here's some untested quick & dirty example code to illustrate the concept:
// in ArticlesTable
public function beforeMarshal(
\Cake\Event\EventInterface $event,
\ArrayAccess $data,
\ArrayObject $options
): void {
// extract lookup keys from request data
$keys = collection($data['tags'])
->extract(function ($row) {
return [
$row['tag'],
$row['site_id'],
];
})
->toArray();
// query possibly existing rows based on the extracted lookup keys
$query = $this->Tags
->find()
->select(['id', 'tag', 'site_id'])
->where(
new \Cake\Database\Expression\TupleComparison(
['tag', 'site_id'],
$keys,
['string', 'integer'],
'IN'
)
)
->disableHydration();
// create a map of lookup keys and primary keys from the queried rows
$map = $query
->all()
->combine(
function ($row) {
return $row['tag'] . ';' . $row['site_id'];
},
'id'
)
->toArray();
// inject primary keys based on whether lookup keys exist in the map
$data['tags'] = collection($data['tags'])
->map(function ($row) use ($map) {
$key = $row['tag'] . ';' . $row['site_id'];
if (isset($map[$key])) {
$row['id'] = $map[$key];
}
return $row;
})
->toArray();
}
With the primary keys of existing records injected, marshalling, validation, rules and saving should be able to properly distinguish what's to update and what's to insert, ie you should be able to continue using the default ORM saving process just like you're used to.
See also
Cookbook > Database Access & ORM > Saving Data > Modifying Request Data Before Building Entities
Cookbook > Database Access & ORM > Query Builder > Inserting Data
Cookbook > Database Access & ORM > Table Objects > Lifecycle Callbacks > beforeMarshal

cakephp 3 save empty belongsToMany relation

I have a Shops table which can have Products. Other Shops can have the same Products so I used a belongsToMany relation table ShopsProducts.
I can add an infinite number of Products to a Shop and remove them by saving the Shop entity including the relation.
All works fine, but if I want to unlink all Products from a Shop in my form and save, the relation is of cause empty so the Shop will always have 1 Product that I can't delete over the relation but only directly.
This is what the request looks like from the Shops form with a Product
data => [
'name' => 'some',
'is_active' => '1',
'slug' => 'some',
'product_id' => '',
'products' => [
(int) 5 => [
'id' => '5',
'_joinData' => [
'priority' => '0'
]
]
],
]
And this is the request without
data => [
'name' => 'some',
'is_active' => '1',
'slug' => 'some',
'product_id' => '',
]
What is the cake way to handle this issue?
What's your save strategy?
https://book.cakephp.org/3.0/en/orm/saving-data.html#saving-belongstomany-associations
Try replace instead of append.
If that doesn't work for you, check in the before() or afterSave() if products->get('products') is empty, if it is manually call a deleteAll() on the join table for that product an shop.
You will need to set your association to [] to blank it out.
$shops->products = [];
$shops->setDirty('products ', true);

Baking a Controller with Cakephp 3.2 using SQLite3 produces an error in the add() and edit() methods, Why is the code being generated incorrectly?

The error is that when creating the controller code a variable for an associated model is left blank, and that Model's name is left blank. The error happens when creating the controllers for all of my tables, but here is a complete example of one of them. I've replicated the problem on a fresh install of cakephp 3.2.
Here is the 2 lines of incorrect code generated, I've included the full details below:
$ = $this->Customers->->find('list', ['limit' => 200]);
$this->set(compact('customer', ''));
My database configuration in config/app.php:
'default' => [
'className' => 'Cake\Database\Connection',
'driver' => 'Cake\Database\Driver\Sqlite',
'persistent' => false,
'host' => 'localhost',
//'port' => 'non_standard_port_number',
'username' => null,
'password' => null,
'database' => 'tgr.db',
'encoding' => 'utf8',
'timezone' => 'UTC',
'flags' => [],
'cacheMetadata' => true,
'log' => false,
'quoteIdentifiers' => false,
'url' => env('DATABASE_URL', null),
]
The code created for the add() method which contains the incorrectly created code.
public function add(){
$customer = $this->Customers->newEntity();
if ($this->request->is('post')) {
$customer = $this->Customers->patchEntity($customer, $this->request->data);
if ($this->Customers->save($customer)) {
$this->Flash->success(__('The customer has been saved.'));
return $this->redirect(['action' => 'index']);
} else {
$this->Flash->error(__('The customer could not be saved. Please, try again.'));
}
}
$ = $this->Customers->->find('list', ['limit' => 200]);
$this->set(compact('customer', ''));
$this->set('_serialize', ['customer']);
}
My database is stored int the application root and the app finds it and connects correctly from what I can tell. Database Schema:
-- Text encoding used: UTF-8
--
PRAGMA foreign_keys = off;
BEGIN TRANSACTION;
-- Table: customers
CREATE TABLE customers (_id INTEGER PRIMARY KEY,district_key INTEGER NOT NULL, name TEXT UNIQUE NOT NULL, short_name TEXT UNIQUE NOT NULL, job_count INTEGER, report_count INTEGER, FOREIGN KEY (district_key) REFERENCES districts (_id) );
COMMIT TRANSACTION;
PRAGMA foreign_keys = on;
I found the answer. My database id columns were _id (from the android default id column) and cakephp bakery was interpriting this to be a related Model with an empty name sense it was taking whatever comes before _id and creating a model for it. I've updated all of my database tables to reflect this and it is working correctly.

CakePHP - Add custom values to User Object

I have added 4 more columns to my CakePHP Users table and I am trying to figure out how I can include these columns in the $this->Auth->user() object.
I've added the column information to my Users Model and Entity but still no joy. Currently my user object looks like this;
[
'id' => (int) 1,
'username' => 'admin',
'name' => 'Web Admin',
'email' => 'webteam#',
'role' => 'admin',
'created' => object(Cake\I18n\Time) {
'time' => '2016-02-09T16:04:46+00:00',
'timezone' => 'UTC',
'fixedNowTime' => false
},
'modified' => object(Cake\I18n\Time) {
'time' => '2016-02-12T08:53:16+00:00',
'timezone' => 'UTC',
'fixedNowTime' => false
}
]
Where is this object created and is there a way I can add my custom values to it, without editing core CakePHP files?
Thanks for your help .
By default the built-in authenticators will fetch all fields in the tables schema.
You most probably just forgot to clear your cache (tmp/cache/models), which you should do whenever you make changes to your schemas.
In case one would want to specify what fields are being fetched, a custom finder would be needed.
See Cookbook > Controllers > Components > Authentication > Customizing Find Query
$this->loadComponent('Auth', [
'authenticate' => [
'Form' => [
'finder' => 'auth'
]
],
]);
In your UsersTable class
public function findAuth(\Cake\ORM\Query $query, array $options)
{
return $query
->select(['id', 'username', 'password', 'column_x', 'column_y']);
}
It should be noted that the fields required for authentication must always be included, ie like username and password!

Saving records with a given uuid

I want to save a bunch of static records in my database with a given uuid, this is for testing purposes, so that on every system the application starts with the exact same dataset.
When inserting with SQL this is no problem but I wanted to use the CakePHP way ( I use a migrations file for this, but that does not matter).
The problem is that I give cake a data array like this and save it:
$data = [
['id' => '5cedf79a-e4b9-f235-3d4d-9fbeef41c7e8', 'name' => 'test'],
['id' => 'c2bf879c-072c-51a4-83d8-edbf2d97e07e', 'name' => 'test2']
];
$table = TableRegistry::get('My_Model');
$entities = $table->newEntities($data, [
'accessibleFields' => ['*' => true],
'validate' => false
]);
array_map([$table, 'save'], $entities );
Everything saves, but all my items have been given a different uuid, If I debug a record after saving it shows the original uuid in the entity
'new' => false,
'accessible' => [
'*' => true
],
'properties' => [
'id' => '6b4524a8-4698-4297-84e5-5160f42f663b',
'name' => 'test',
],
'dirty' => [],
'original' => [
'id' => '5cedf79a-e4b9-f235-3d4d-9fbeef41c7e8'
],
So why does cake generate a new uuid for me? and how do I prevent it
This doesn't work because primary keys are unconditionally being generated before the insert operation, see
https://github.com/cakephp/cakephp/blob/3.0.0/src/ORM/Table.php#L1486-L1490
// ...
$id = (array)$this->_newId($primary) + $keys;
$primary = array_combine($primary, $id);
$filteredKeys = array_filter($primary, 'strlen');
$data = $filteredKeys + $data;
// ...
$statement = $this->query()->insert(array_keys($data))
->values($data)
->execute();
// ...
Currently the UUID type is the only type that implements generating IDs, so providing custom IDs works with other types.
You can workaround this by for example overriding the _newId() method in your table so that it returns null, which effectively results in the existing primary key not being overwritten.
protected function _newId($primary)
{
// maybe add some conditional logic here
// in case you don't want to be required
// to always manually provide a primary
// key for your insert operations
return null;
}

Resources