1
|
|
|
<?php |
2
|
|
|
/** |
3
|
|
|
* Licensed under The GPL-3.0 License |
4
|
|
|
* For full copyright and license information, please see the LICENSE.txt |
5
|
|
|
* Redistributions of files must retain the above copyright notice. |
6
|
|
|
* |
7
|
|
|
* @since 2.0.0 |
8
|
|
|
* @author Christopher Castro <[email protected]> |
9
|
|
|
* @link http://www.quickappscms.org |
10
|
|
|
* @license http://opensource.org/licenses/gpl-3.0.html GPL-3.0 License |
11
|
|
|
*/ |
12
|
|
|
namespace Field\Model\Behavior; |
13
|
|
|
|
14
|
|
|
use Cake\Collection\CollectionInterface; |
15
|
|
|
use Cake\Datasource\EntityInterface; |
16
|
|
|
use Cake\Error\FatalErrorException; |
17
|
|
|
use Cake\Event\Event; |
18
|
|
|
use Cake\ORM\Behavior; |
19
|
|
|
use Cake\ORM\Entity; |
20
|
|
|
use Cake\ORM\Query; |
21
|
|
|
use Cake\ORM\Table; |
22
|
|
|
use Cake\ORM\TableRegistry; |
23
|
|
|
use Cake\Validation\Validator; |
24
|
|
|
use Eav\Model\Behavior\EavBehavior; |
25
|
|
|
use Field\Collection\FieldCollection; |
26
|
|
|
use Field\Model\Entity\Field; |
27
|
|
|
use \ArrayObject; |
28
|
|
|
|
29
|
|
|
/** |
30
|
|
|
* Fieldable Behavior. |
31
|
|
|
* |
32
|
|
|
* A more flexible EAV approach. Allows additional fields to be attached to Tables. |
33
|
|
|
* Any Table (Contents, Users, etc.) can use this behavior to make itself `fieldable` |
34
|
|
|
* and thus allow fields to be attached to it. |
35
|
|
|
* |
36
|
|
|
* The Field API defines two primary data structures, FieldInstance and FieldValue: |
37
|
|
|
* |
38
|
|
|
* - FieldInstance: is a Field attached to a single Table. (Schema equivalent: column) |
39
|
|
|
* - FieldValue: is the stored data for a particular [FieldInstance, Entity] |
40
|
|
|
* tuple of your Table. (Schema equivalent: cell value) |
41
|
|
|
* |
42
|
|
|
* **This behavior allows you to add _virtual columns_ to your table schema.** |
43
|
|
|
* |
44
|
|
|
* @link https://github.com/quickapps/docs/blob/2.x/en/developers/field-api.rst |
45
|
|
|
*/ |
46
|
|
|
class FieldableBehavior extends EavBehavior |
47
|
|
|
{ |
48
|
|
|
|
49
|
|
|
/** |
50
|
|
|
* Used for reduce BD queries and allow inter-method communication. |
51
|
|
|
* Example, it allows to pass some information from beforeDelete() to |
52
|
|
|
* afterDelete(). |
53
|
|
|
* |
54
|
|
|
* @var array |
55
|
|
|
*/ |
56
|
|
|
protected $_cache = []; |
57
|
|
|
|
58
|
|
|
/** |
59
|
|
|
* Default configuration. |
60
|
|
|
* |
61
|
|
|
* These are merged with user-provided configuration when the behavior is used. |
62
|
|
|
* Available options are: |
63
|
|
|
* |
64
|
|
|
* - `bundle`: Bundle within this the table. Can be a string or a callable |
65
|
|
|
* method that must return a string to use as bundle. Default null. If set to |
66
|
|
|
* a callable function, it will receive the entity being saved as first |
67
|
|
|
* argument, so you can calculate a bundle name for each particular entity. |
68
|
|
|
* |
69
|
|
|
* - `enabled`: True enables this behavior or false for disable. Default to |
70
|
|
|
* true. |
71
|
|
|
* |
72
|
|
|
* - `cache`: Column-based cache. See EAV plugin's documentation. |
73
|
|
|
* |
74
|
|
|
* Bundles are usually set to dynamic values. For example, for the "contents" |
75
|
|
|
* table we have "content" entities, but we may have "article contents", "page |
76
|
|
|
* contents", etc. depending on the "type of content" they are; is said that |
77
|
|
|
* "article" and "page" **are bundles** of "contents" table. |
78
|
|
|
* |
79
|
|
|
* @var array |
80
|
|
|
*/ |
81
|
|
|
protected $_fieldableDefaultConfig = [ |
82
|
|
|
'bundle' => null, |
83
|
|
|
'implementedMethods' => [ |
84
|
|
|
'attachFields' => 'attachEntityFields', |
85
|
|
|
'fieldable' => 'fieldable', |
86
|
|
|
], |
87
|
|
|
]; |
88
|
|
|
|
89
|
|
|
/** |
90
|
|
|
* Instance of EavAttributes table. |
91
|
|
|
* |
92
|
|
|
* @var \Eav\Model\Table\EavAttributesTable |
93
|
|
|
*/ |
94
|
|
|
public $Attributes = null; |
95
|
|
|
|
96
|
|
|
/** |
97
|
|
|
* Constructor. |
98
|
|
|
* |
99
|
|
|
* @param \Cake\ORM\Table $table The table this behavior is attached to |
100
|
|
|
* @param array $config Configuration array for this behavior |
101
|
|
|
*/ |
102
|
|
|
public function __construct(Table $table, array $config = []) |
103
|
|
|
{ |
104
|
|
|
$this->_defaultConfig = array_merge($this->_defaultConfig, $this->_fieldableDefaultConfig); |
105
|
|
|
$this->Attributes = TableRegistry::get('Eav.EavAttributes'); |
106
|
|
|
$this->Attributes->hasOne('Instance', [ |
107
|
|
|
'className' => 'Field.FieldInstances', |
108
|
|
|
'foreignKey' => 'eav_attribute_id', |
109
|
|
|
'propertyName' => 'instance', |
110
|
|
|
]); |
111
|
|
|
parent::__construct($table, $config); |
112
|
|
|
} |
113
|
|
|
|
114
|
|
|
/** |
115
|
|
|
* Returns a list of events this class is implementing. When the class is |
116
|
|
|
* registered in an event manager, each individual method will be associated |
117
|
|
|
* with the respective event. |
118
|
|
|
* |
119
|
|
|
* @return void |
120
|
|
|
*/ |
121
|
|
|
public function implementedEvents() |
122
|
|
|
{ |
123
|
|
|
$events = [ |
124
|
|
|
'Model.beforeFind' => ['callable' => 'beforeFind', 'priority' => 15], |
125
|
|
|
'Model.beforeSave' => ['callable' => 'beforeSave', 'priority' => 15], |
126
|
|
|
'Model.afterSave' => ['callable' => 'afterSave', 'priority' => 15], |
127
|
|
|
'Model.beforeDelete' => ['callable' => 'beforeDelete', 'priority' => 15], |
128
|
|
|
'Model.afterDelete' => ['callable' => 'afterDelete', 'priority' => 15], |
129
|
|
|
]; |
130
|
|
|
|
131
|
|
|
return $events; |
132
|
|
|
} |
133
|
|
|
|
134
|
|
|
/** |
135
|
|
|
* Modifies the query object in order to merge custom fields records into each |
136
|
|
|
* entity under the `_fields` property. |
137
|
|
|
* |
138
|
|
|
* You can enable or disable this behavior for a single `find()` or `get()` |
139
|
|
|
* operation by setting `fieldable` or `eav` to false in the options array for |
140
|
|
|
* find method. e.g.: |
141
|
|
|
* |
142
|
|
|
* ```php |
143
|
|
|
* $contents = $this->Contents->find('all', ['fieldable' => false]); |
144
|
|
|
* $content = $this->Contents->get($id, ['fieldable' => false]); |
145
|
|
|
* ``` |
146
|
|
|
* |
147
|
|
|
* It also looks for custom fields in WHERE clause. This will search entities in |
148
|
|
|
* all bundles this table may have, if you need to restrict the search to an |
149
|
|
|
* specific bundle you must use the `bundle` key in find()'s options: |
150
|
|
|
* |
151
|
|
|
* ```php |
152
|
|
|
* $this->Contents |
153
|
|
|
* ->find('all', ['bundle' => 'articles']) |
154
|
|
|
* ->where(['article-title' => 'My first article!']); |
155
|
|
|
* ``` |
156
|
|
|
* |
157
|
|
|
* The `bundle` option has no effects if no custom fields are given in the |
158
|
|
|
* WHERE clause. |
159
|
|
|
* |
160
|
|
|
* @param \Cake\Event\Event $event The beforeFind event that was triggered |
161
|
|
|
* @param \Cake\ORM\Query $query The original query to modify |
162
|
|
|
* @param \ArrayObject $options Additional options given as an array |
163
|
|
|
* @param bool $primary Whether this find is a primary query or not |
164
|
|
|
* @return void |
165
|
|
|
*/ |
166
|
|
|
public function beforeFind(Event $event, Query $query, ArrayObject $options, $primary) |
167
|
|
|
{ |
168
|
|
|
$status = array_key_exists('fieldable', $options) ? $options['fieldable'] : $this->config('status'); |
|
|
|
|
169
|
|
|
if ($status) { |
170
|
|
|
return; |
171
|
|
|
} |
172
|
|
|
|
173
|
|
|
if (array_key_exists('eav', $options)) { |
174
|
|
|
unset($options['eav']); |
175
|
|
|
} |
176
|
|
|
|
177
|
|
|
return parent::beforeFind($event, $query, $options, $primary); |
178
|
|
|
} |
179
|
|
|
|
180
|
|
|
/** |
181
|
|
|
* {@inheritDoc} |
182
|
|
|
*/ |
183
|
|
|
protected function _hydrateEntities(CollectionInterface $entities, array $args) |
184
|
|
|
{ |
185
|
|
|
return $entities->map(function ($entity) use ($args) { |
186
|
|
|
if ($entity instanceof EntityInterface) { |
187
|
|
|
$entity = $this->_prepareCachedColumns($entity); |
188
|
|
|
$entity = $this->_attachEntityFields($entity, $args); |
189
|
|
|
|
190
|
|
|
if ($entity === null) { |
191
|
|
|
return self::NULL_ENTITY; |
192
|
|
|
} |
193
|
|
|
} |
194
|
|
|
|
195
|
|
|
return $entity; |
196
|
|
|
}) |
197
|
|
|
->filter(function ($entity) { |
198
|
|
|
return $entity !== self::NULL_ENTITY; |
199
|
|
|
}); |
200
|
|
|
} |
201
|
|
|
|
202
|
|
|
/** |
203
|
|
|
* Attaches entity's field under the `_fields` property, this method is invoked |
204
|
|
|
* by `beforeFind()` when iterating results sets. |
205
|
|
|
* |
206
|
|
|
* @param \Cake\Datasource\EntityInterface $entity The entity being altered |
207
|
|
|
* @param array $args Arguments given to the originating `beforeFind()` |
208
|
|
|
*/ |
209
|
|
|
protected function _attachEntityFields(EntityInterface $entity, array $args) |
210
|
|
|
{ |
211
|
|
|
$entity = $this->attachEntityFields($entity); |
212
|
|
|
foreach ($entity->get('_fields') as $field) { |
213
|
|
|
$result = $field->beforeFind((array)$args['options'], $args['primary']); |
214
|
|
|
if ($result === null) { |
215
|
|
|
return null; // remove entity from collection |
216
|
|
|
} |
217
|
|
|
} |
218
|
|
|
|
219
|
|
|
return $entity; |
220
|
|
|
} |
221
|
|
|
|
222
|
|
|
/** |
223
|
|
|
* Before an entity is saved. |
224
|
|
|
* |
225
|
|
|
* Here is where we dispatch each custom field's `$_POST` information to its |
226
|
|
|
* corresponding Field Handler, so they can operate over their values. |
227
|
|
|
* |
228
|
|
|
* Fields Handler's `beforeSave()` method is automatically invoked for each |
229
|
|
|
* attached field for the entity being processed, your field handler should look |
230
|
|
|
* as follow: |
231
|
|
|
* |
232
|
|
|
* ```php |
233
|
|
|
* use Field\Handler; |
234
|
|
|
* |
235
|
|
|
* class TextField extends Handler |
236
|
|
|
* { |
237
|
|
|
* public function beforeSave(Field $field, $post) |
238
|
|
|
* { |
239
|
|
|
* // alter $field, and do nifty things with $post |
240
|
|
|
* // return FALSE; will halt the operation |
241
|
|
|
* } |
242
|
|
|
* } |
243
|
|
|
* ``` |
244
|
|
|
* |
245
|
|
|
* Field Handlers should **alter** `$field->value` and `$field->extra` according |
246
|
|
|
* to its needs using the provided **$post** argument. |
247
|
|
|
* |
248
|
|
|
* **NOTE:** Returning boolean FALSE will halt the whole Entity's save operation. |
249
|
|
|
* |
250
|
|
|
* @param \Cake\Event\Event $event The event that was triggered |
251
|
|
|
* @param \Cake\Datasource\EntityInterface $entity The entity being saved |
252
|
|
|
* @param \ArrayObject $options Additional options given as an array |
253
|
|
|
* @throws \Cake\Error\FatalErrorException When using this behavior in non-atomic mode |
254
|
|
|
* @return bool True if save operation should continue |
255
|
|
|
*/ |
256
|
|
|
public function beforeSave(Event $event, EntityInterface $entity, ArrayObject $options) |
257
|
|
|
{ |
258
|
|
|
if (!$this->config('status')) { |
|
|
|
|
259
|
|
|
return true; |
260
|
|
|
} |
261
|
|
|
|
262
|
|
|
if (!$options['atomic']) { |
263
|
|
|
throw new FatalErrorException(__d('field', 'Entities in fieldable tables can only be saved using transaction. Set [atomic = true]')); |
264
|
|
|
} |
265
|
|
|
|
266
|
|
|
if (!$this->_validation($entity)) { |
267
|
|
|
return false; |
268
|
|
|
} |
269
|
|
|
|
270
|
|
|
$this->_cache['createValues'] = []; |
271
|
|
|
foreach ($this->_attributesForEntity($entity) as $attr) { |
272
|
|
|
if (!$this->_toolbox->propertyExists($entity, $attr->get('name'))) { |
273
|
|
|
continue; |
274
|
|
|
} |
275
|
|
|
|
276
|
|
|
$field = $this->_prepareMockField($entity, $attr); |
277
|
|
|
$result = $field->beforeSave($this->_fetchPost($field)); |
278
|
|
|
|
279
|
|
|
if ($result === false) { |
280
|
|
|
$this->attachEntityFields($entity); |
281
|
|
|
|
282
|
|
|
return false; |
283
|
|
|
} |
284
|
|
|
|
285
|
|
|
$data = [ |
286
|
|
|
'eav_attribute_id' => $field->get('metadata')->get('attribute_id'), |
287
|
|
|
'entity_id' => $this->_toolbox->getEntityId($entity), |
288
|
|
|
"value_{$field->metadata['type']}" => $field->get('value'), |
289
|
|
|
'extra' => $field->get('extra'), |
290
|
|
|
]; |
291
|
|
|
|
292
|
|
|
if ($field->get('metadata')->get('value_id')) { |
293
|
|
|
$valueEntity = TableRegistry::get('Eav.EavValues')->get($field->get('metadata')->get('value_id')); |
294
|
|
|
$valueEntity = TableRegistry::get('Eav.EavValues')->patchEntity($valueEntity, $data, ['validate' => false]); |
295
|
|
|
} else { |
296
|
|
|
$valueEntity = TableRegistry::get('Eav.EavValues')->newEntity($data, ['validate' => false]); |
297
|
|
|
} |
298
|
|
|
|
299
|
|
|
if ($entity->isNew() || $valueEntity->isNew()) { |
300
|
|
|
$this->_cache['createValues'][] = $valueEntity; |
301
|
|
|
} elseif (!TableRegistry::get('Eav.EavValues')->save($valueEntity)) { |
302
|
|
|
$this->attachEntityFields($entity); |
303
|
|
|
$event->stopPropagation(); |
304
|
|
|
|
305
|
|
|
return false; |
306
|
|
|
} |
307
|
|
|
} |
308
|
|
|
|
309
|
|
|
$this->attachEntityFields($entity); |
310
|
|
|
|
311
|
|
|
return true; |
312
|
|
|
} |
313
|
|
|
|
314
|
|
|
/** |
315
|
|
|
* After an entity is saved. |
316
|
|
|
* |
317
|
|
|
* @param \Cake\Event\Event $event The event that was triggered |
318
|
|
|
* @param \Cake\Datasource\EntityInterface $entity The entity that was saved |
319
|
|
|
* @param \ArrayObject $options Additional options given as an array |
320
|
|
|
* @return bool True always |
321
|
|
|
*/ |
322
|
|
|
public function afterSave(Event $event, EntityInterface $entity, ArrayObject $options) |
323
|
|
|
{ |
324
|
|
|
if (!$this->config('status')) { |
|
|
|
|
325
|
|
|
return true; |
326
|
|
|
} |
327
|
|
|
|
328
|
|
|
// as we don't know entity's ID on beforeSave, we must delay values storage; |
329
|
|
|
// all this occurs inside a transaction so we are safe |
330
|
|
|
if (!empty($this->_cache['createValues'])) { |
331
|
|
|
foreach ($this->_cache['createValues'] as $valueEntity) { |
332
|
|
|
$valueEntity->set('entity_id', $this->_toolbox->getEntityId($entity)); |
333
|
|
|
$valueEntity->unsetProperty('id'); |
334
|
|
|
TableRegistry::get('Eav.EavValues')->save($valueEntity); |
335
|
|
|
} |
336
|
|
|
$this->_cache['createValues'] = []; |
337
|
|
|
} |
338
|
|
|
|
339
|
|
|
foreach ($this->_attributesForEntity($entity) as $attr) { |
340
|
|
|
$field = $this->_prepareMockField($entity, $attr); |
341
|
|
|
$field->afterSave(); |
342
|
|
|
} |
343
|
|
|
|
344
|
|
|
if ($this->config('cacheMap')) { |
|
|
|
|
345
|
|
|
$this->updateEavCache($entity); |
346
|
|
|
} |
347
|
|
|
|
348
|
|
|
return true; |
349
|
|
|
} |
350
|
|
|
|
351
|
|
|
/** |
352
|
|
|
* Deletes an entity from a fieldable table. |
353
|
|
|
* |
354
|
|
|
* @param \Cake\Event\Event $event The event that was triggered |
355
|
|
|
* @param \Cake\Datasource\EntityInterface $entity The entity being deleted |
356
|
|
|
* @param \ArrayObject $options Additional options given as an array |
357
|
|
|
* @return bool |
358
|
|
|
* @throws \Cake\Error\FatalErrorException When using this behavior in non-atomic mode |
359
|
|
|
*/ |
360
|
|
|
public function beforeDelete(Event $event, EntityInterface $entity, ArrayObject $options) |
361
|
|
|
{ |
362
|
|
|
if (!$this->config('status')) { |
|
|
|
|
363
|
|
|
return true; |
364
|
|
|
} |
365
|
|
|
|
366
|
|
|
if (!$options['atomic']) { |
367
|
|
|
throw new FatalErrorException(__d('field', 'Entities in fieldable tables can only be deleted using transaction. Set [atomic = true]')); |
368
|
|
|
} |
369
|
|
|
|
370
|
|
|
foreach ($this->_attributesForEntity($entity) as $attr) { |
371
|
|
|
$field = $this->_prepareMockField($entity, $attr); |
372
|
|
|
$result = $field->beforeDelete(); |
373
|
|
|
|
374
|
|
|
if ($result === false) { |
375
|
|
|
$event->stopPropagation(); |
376
|
|
|
|
377
|
|
|
return false; |
378
|
|
|
} |
379
|
|
|
|
380
|
|
|
// holds in cache field mocks, so we can catch them on afterDelete |
381
|
|
|
$this->_cache['afterDelete'][] = $field; |
382
|
|
|
} |
383
|
|
|
|
384
|
|
|
return true; |
385
|
|
|
} |
386
|
|
|
|
387
|
|
|
/** |
388
|
|
|
* After an entity was removed from database. |
389
|
|
|
* |
390
|
|
|
* **NOTE:** This method automatically removes all field values from |
391
|
|
|
* `eav_values` database table for each entity. |
392
|
|
|
* |
393
|
|
|
* @param \Cake\Event\Event $event The event that was triggered |
394
|
|
|
* @param \Cake\Datasource\EntityInterface $entity The entity that was deleted |
395
|
|
|
* @param \ArrayObject $options Additional options given as an array |
396
|
|
|
* @throws \Cake\Error\FatalErrorException When using this behavior in non-atomic mode |
397
|
|
|
* @return void |
398
|
|
|
*/ |
399
|
|
|
public function afterDelete(Event $event, EntityInterface $entity, ArrayObject $options) |
400
|
|
|
{ |
401
|
|
|
if (!$this->config('status')) { |
|
|
|
|
402
|
|
|
return; |
403
|
|
|
} |
404
|
|
|
|
405
|
|
|
if (!$options['atomic']) { |
406
|
|
|
throw new FatalErrorException(__d('field', 'Entities in fieldable tables can only be deleted using transactions. Set [atomic = true]')); |
407
|
|
|
} |
408
|
|
|
|
409
|
|
|
if (!empty($this->_cache['afterDelete'])) { |
410
|
|
|
foreach ((array)$this->_cache['afterDelete'] as $field) { |
411
|
|
|
$field->afterDelete(); |
412
|
|
|
} |
413
|
|
|
$this->_cache['afterDelete'] = []; |
414
|
|
|
} |
415
|
|
|
|
416
|
|
|
parent::afterDelete($event, $entity, $options); |
417
|
|
|
} |
418
|
|
|
|
419
|
|
|
/** |
420
|
|
|
* Gets/sets fieldable behavior status. |
421
|
|
|
* |
422
|
|
|
* @param array|bool|null $status If set to a boolean value then turns on/off |
423
|
|
|
* this behavior |
424
|
|
|
* @return bool|void |
425
|
|
|
*/ |
426
|
|
|
public function fieldable($status = null) |
427
|
|
|
{ |
428
|
|
|
return $this->eav($status); |
|
|
|
|
429
|
|
|
} |
430
|
|
|
|
431
|
|
|
/** |
432
|
|
|
* The method which actually fetches custom fields. |
433
|
|
|
* |
434
|
|
|
* Fetches all Entity's fields under the `_fields` property. |
435
|
|
|
* |
436
|
|
|
* @param \Cake\Datasource\EntityInterface $entity The entity where to fetch fields |
437
|
|
|
* @return \Cake\Datasource\EntityInterface |
438
|
|
|
*/ |
439
|
|
|
public function attachEntityFields(EntityInterface $entity) |
440
|
|
|
{ |
441
|
|
|
$_fields = []; |
442
|
|
|
foreach ($this->_attributesForEntity($entity) as $attr) { |
443
|
|
|
$field = $this->_prepareMockField($entity, $attr); |
444
|
|
|
if ($entity->has($field->get('name'))) { |
445
|
|
|
$this->_fetchPost($field); |
446
|
|
|
} |
447
|
|
|
|
448
|
|
|
$field->fieldAttached(); |
449
|
|
|
$_fields[] = $field; |
450
|
|
|
} |
451
|
|
|
|
452
|
|
|
$entity->set('_fields', new FieldCollection($_fields)); |
453
|
|
|
|
454
|
|
|
return $entity; |
455
|
|
|
} |
456
|
|
|
|
457
|
|
|
/** |
458
|
|
|
* Triggers before/after validate events. |
459
|
|
|
* |
460
|
|
|
* @param \Cake\Datasource\EntityInterface $entity The entity being validated |
461
|
|
|
* @return bool True if save operation should continue, false otherwise |
462
|
|
|
*/ |
463
|
|
|
protected function _validation(EntityInterface $entity) |
464
|
|
|
{ |
465
|
|
|
$validator = new Validator(); |
466
|
|
|
$hasErrors = false; |
467
|
|
|
|
468
|
|
|
foreach ($this->_attributesForEntity($entity) as $attr) { |
469
|
|
|
$field = $this->_prepareMockField($entity, $attr); |
470
|
|
|
$result = $field->validate($validator); |
471
|
|
|
|
472
|
|
|
if ($result === false) { |
473
|
|
|
$this->attachEntityFields($entity); |
474
|
|
|
|
475
|
|
|
return false; |
476
|
|
|
} |
477
|
|
|
|
478
|
|
|
$errors = $validator->errors($entity->toArray(), $entity->isNew()); |
479
|
|
|
$entity->errors($errors); |
480
|
|
|
|
481
|
|
|
if (!empty($errors)) { |
482
|
|
|
$hasErrors = true; |
483
|
|
|
if ($entity->has('_fields')) { |
484
|
|
|
$entityErrors = $entity->errors(); |
485
|
|
|
foreach ($entity->get('_fields') as $field) { |
486
|
|
|
$postData = $entity->get($field->name); |
487
|
|
|
if (!empty($entityErrors[$field->name])) { |
488
|
|
|
$field->set('value', $postData); |
489
|
|
|
$field->metadata->set('errors', (array)$entityErrors[$field->name]); |
490
|
|
|
} |
491
|
|
|
} |
492
|
|
|
} |
493
|
|
|
} |
494
|
|
|
} |
495
|
|
|
|
496
|
|
|
return !$hasErrors; |
497
|
|
|
} |
498
|
|
|
|
499
|
|
|
/** |
500
|
|
|
* Alters the given $field and fetches incoming POST data, both "value" and |
501
|
|
|
* "extra" property will be automatically filled for the given $field entity. |
502
|
|
|
* |
503
|
|
|
* @param \Field\Model\Entity\Field $field The field entity for which |
504
|
|
|
* fetch POST information |
505
|
|
|
* @return mixed Raw POST information |
506
|
|
|
*/ |
507
|
|
|
protected function _fetchPost(Field $field) |
508
|
|
|
{ |
509
|
|
|
$post = $field |
510
|
|
|
->get('metadata') |
511
|
|
|
->get('entity') |
512
|
|
|
->get($field->get('name')); |
513
|
|
|
|
514
|
|
|
// auto-magic |
515
|
|
|
if (is_array($post)) { |
516
|
|
|
$field->set('extra', $post); |
517
|
|
|
$field->set('value', null); |
518
|
|
|
} else { |
519
|
|
|
$field->set('extra', null); |
520
|
|
|
$field->set('value', $post); |
521
|
|
|
} |
522
|
|
|
|
523
|
|
|
return $post; |
524
|
|
|
} |
525
|
|
|
|
526
|
|
|
/** |
527
|
|
|
* Gets all attributes that should be attached to the given entity, this entity |
528
|
|
|
* will be used as context to calculate the proper bundle. |
529
|
|
|
* |
530
|
|
|
* @param \Cake\Datasource\EntityInterface $entity Entity context |
531
|
|
|
* @return array |
532
|
|
|
*/ |
533
|
|
|
protected function _attributesForEntity(EntityInterface $entity) |
534
|
|
|
{ |
535
|
|
|
$bundle = $this->_resolveBundle($entity); |
536
|
|
|
$attrs = $this->_toolbox->attributes($bundle); |
537
|
|
|
$attrByIds = []; // attrs indexed by id |
538
|
|
|
$attrByNames = []; // attrs indexed by name |
539
|
|
|
|
540
|
|
|
foreach ($attrs as $name => $attr) { |
541
|
|
|
$attrByNames[$name] = $attr; |
542
|
|
|
$attrByIds[$attr->get('id')] = $attr; |
543
|
|
|
$attr->set(':value', null); |
544
|
|
|
} |
545
|
|
|
|
546
|
|
|
if (!empty($attrByIds)) { |
547
|
|
|
$instances = $this->Attributes->Instance |
548
|
|
|
->find() |
549
|
|
|
->where(['eav_attribute_id IN' => array_keys($attrByIds)]) |
550
|
|
|
->all(); |
551
|
|
|
foreach ($instances as $instance) { |
552
|
|
|
if (!empty($attrByIds[$instance->get('eav_attribute_id')])) { |
553
|
|
|
$attr = $attrByIds[$instance->get('eav_attribute_id')]; |
554
|
|
|
if (!$attr->has('instance')) { |
555
|
|
|
$attr->set('instance', $instance); |
556
|
|
|
} |
557
|
|
|
} |
558
|
|
|
} |
559
|
|
|
} |
560
|
|
|
|
561
|
|
|
$values = $this->_fetchValues($entity, array_keys($attrByNames)); |
562
|
|
|
foreach ($values as $value) { |
563
|
|
|
if (!empty($attrByNames[$value->get('eav_attribute')->get('name')])) { |
564
|
|
|
$attrByNames[$value->get('eav_attribute')->get('name')]->set(':value', $value); |
565
|
|
|
} |
566
|
|
|
} |
567
|
|
|
|
568
|
|
|
return $this->_toolbox->attributes($bundle); |
569
|
|
|
} |
570
|
|
|
|
571
|
|
|
/** |
572
|
|
|
* Retrives stored values for all virtual properties by name. This gets all |
573
|
|
|
* values at once. |
574
|
|
|
* |
575
|
|
|
* This method is used to reduce the number of SQl queries, so we get all |
576
|
|
|
* values at once in a single Select instead of creating a select for every |
577
|
|
|
* field attached to the given entity. |
578
|
|
|
* |
579
|
|
|
* @param \Cake\Datasource\EntityInterface $entity The entuity for which |
580
|
|
|
* get related values |
581
|
|
|
* @param array $attrNames List of attribute names for which get their |
582
|
|
|
* values |
583
|
|
|
* @return \Cake\Datasource\ResultSetInterface |
584
|
|
|
*/ |
585
|
|
|
protected function _fetchValues(EntityInterface $entity, array $attrNames = []) |
586
|
|
|
{ |
587
|
|
|
$bundle = $this->_resolveBundle($entity); |
588
|
|
|
$conditions = [ |
589
|
|
|
'EavAttribute.table_alias' => $this->_table->table(), |
|
|
|
|
590
|
|
|
'EavValues.entity_id' => $entity->get((string)$this->_table->primaryKey()), |
|
|
|
|
591
|
|
|
]; |
592
|
|
|
|
593
|
|
|
if ($bundle) { |
594
|
|
|
$conditions['EavAttribute.bundle'] = $bundle; |
595
|
|
|
} |
596
|
|
|
|
597
|
|
|
if (!empty($attrNames)) { |
598
|
|
|
$conditions['EavAttribute.name IN'] = $attrNames; |
599
|
|
|
} |
600
|
|
|
|
601
|
|
|
$storedValues = TableRegistry::get('Eav.EavValues') |
602
|
|
|
->find() |
603
|
|
|
->contain(['EavAttribute']) |
604
|
|
|
->where($conditions) |
605
|
|
|
->all(); |
606
|
|
|
|
607
|
|
|
return $storedValues; |
608
|
|
|
} |
609
|
|
|
|
610
|
|
|
/** |
611
|
|
|
* Creates a new Virtual "Field" to be attached to the given entity. |
612
|
|
|
* |
613
|
|
|
* This mock Field represents a new property (table column) of the entity. |
614
|
|
|
* |
615
|
|
|
* @param \Cake\Datasource\EntityInterface $entity The entity where the |
616
|
|
|
* generated virtual field will be attached |
617
|
|
|
* @param \Cake\Datasource\EntityInterface $attribute The attribute where to get |
618
|
|
|
* the information when creating the mock field. |
619
|
|
|
* @return \Field\Model\Entity\Field |
620
|
|
|
*/ |
621
|
|
|
protected function _prepareMockField(EntityInterface $entity, EntityInterface $attribute) |
622
|
|
|
{ |
623
|
|
|
$type = $this->_toolbox->mapType($attribute->get('type')); |
624
|
|
|
if (!$attribute->has(':value')) { |
625
|
|
|
$bundle = $this->_resolveBundle($entity); |
626
|
|
|
$conditions = [ |
627
|
|
|
'EavAttribute.table_alias' => $this->_table->table(), |
|
|
|
|
628
|
|
|
'EavAttribute.name' => $attribute->get('name'), |
629
|
|
|
'EavValues.entity_id' => $entity->get((string)$this->_table->primaryKey()), |
|
|
|
|
630
|
|
|
]; |
631
|
|
|
|
632
|
|
|
if ($bundle) { |
633
|
|
|
$conditions['EavAttribute.bundle'] = $bundle; |
634
|
|
|
} |
635
|
|
|
|
636
|
|
|
$storedValue = TableRegistry::get('Eav.EavValues') |
637
|
|
|
->find() |
638
|
|
|
->contain(['EavAttribute']) |
639
|
|
|
->select(['id', "value_{$type}", 'extra']) |
640
|
|
|
->where($conditions) |
641
|
|
|
->limit(1) |
642
|
|
|
->first(); |
643
|
|
|
} else { |
644
|
|
|
$storedValue = $attribute->get(':value'); |
645
|
|
|
} |
646
|
|
|
|
647
|
|
|
$mockField = new Field([ |
648
|
|
|
'name' => $attribute->get('name'), |
649
|
|
|
'label' => $attribute->get('instance')->get('label'), |
650
|
|
|
'value' => null, |
651
|
|
|
'extra' => null, |
652
|
|
|
'metadata' => new Entity([ |
653
|
|
|
'value_id' => null, |
654
|
|
|
'instance_id' => $attribute->get('instance')->get('id'), |
655
|
|
|
'attribute_id' => $attribute->get('id'), |
656
|
|
|
'entity_id' => $this->_toolbox->getEntityId($entity), |
657
|
|
|
'table_alias' => $attribute->get('table_alias'), |
658
|
|
|
'type' => $type, |
659
|
|
|
'bundle' => $attribute->get('bundle'), |
660
|
|
|
'handler' => $attribute->get('instance')->get('handler'), |
661
|
|
|
'required' => $attribute->get('instance')->required, |
662
|
|
|
'description' => $attribute->get('instance')->description, |
663
|
|
|
'settings' => $attribute->get('instance')->settings, |
664
|
|
|
'view_modes' => $attribute->get('instance')->view_modes, |
665
|
|
|
'entity' => $entity, |
666
|
|
|
'errors' => [], |
667
|
|
|
]), |
668
|
|
|
]); |
669
|
|
|
|
670
|
|
|
if ($storedValue) { |
671
|
|
|
$mockField->set('value', $this->_toolbox->marshal($storedValue->get("value_{$type}"), $type)); |
672
|
|
|
$mockField->set('extra', $storedValue->get('extra')); |
673
|
|
|
$mockField->metadata->set('value_id', $storedValue->id); |
674
|
|
|
} |
675
|
|
|
|
676
|
|
|
$mockField->isNew($entity->isNew()); |
677
|
|
|
|
678
|
|
|
return $mockField; |
679
|
|
|
} |
680
|
|
|
|
681
|
|
|
/** |
682
|
|
|
* Resolves `bundle` name using $entity as context. |
683
|
|
|
* |
684
|
|
|
* @param \Cake\Datasource\EntityInterface $entity Entity to use as context when |
685
|
|
|
* resolving bundle |
686
|
|
|
* @return string Bundle name as string value, it may be an empty string if no |
687
|
|
|
* bundle should be applied |
688
|
|
|
*/ |
689
|
|
|
protected function _resolveBundle(EntityInterface $entity) |
690
|
|
|
{ |
691
|
|
|
$bundle = $this->config('bundle'); |
|
|
|
|
692
|
|
|
if (is_callable($bundle)) { |
693
|
|
|
$callable = $this->config('bundle'); |
|
|
|
|
694
|
|
|
$bundle = $callable($entity); |
695
|
|
|
} |
696
|
|
|
|
697
|
|
|
return (string)$bundle; |
698
|
|
|
} |
699
|
|
|
} |
700
|
|
|
|
This method has been deprecated. The supplier of the class has supplied an explanatory message.
The explanatory message should give you some clue as to whether and when the method will be removed from the class and what other method or class to use instead.