Passed
Push — master ( 5a66a8...b9585a )
by Julien
04:31
created

Relationship::preSaveRelatedRecords()   B

Complexity

Conditions 10
Paths 16

Size

Total Lines 91
Code Lines 27

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 0
CRAP Score 110

Importance

Changes 1
Bugs 1 Features 0
Metric Value
eloc 27
c 1
b 1
f 0
dl 0
loc 91
ccs 0
cts 25
cp 0
rs 7.6666
cc 10
nc 16
nop 2
crap 110

How to fix   Long Method    Complexity   

Long Method

Small methods make your code easier to understand, in particular if combined with a good name. Besides, if your method is small, finding a good name is usually much easier.

For example, if you find yourself adding comments to a method's body, this is usually a good sign to extract the commented part to a new method, and use the comment as a starting point when coming up with a good name for this new method.

Commonly applied refactorings include:

1
<?php
2
/**
3
 * This file is part of the Zemit Framework.
4
 *
5
 * (c) Zemit Team <[email protected]>
6
 *
7
 * For the full copyright and license information, please view the LICENSE.txt
8
 * file that was distributed with this source code.
9
 */
10
11
namespace Zemit\Mvc\Model;
12
13
use Exception;
14
use Phalcon\Db\Adapter\AdapterInterface;
15
use Phalcon\Db\Column;
16
use Phalcon\Messages\Message;
17
use Phalcon\Mvc\Model\Manager;
18
use Phalcon\Mvc\Model\ManagerInterface;
19
use Phalcon\Mvc\Model\MetaData;
20
use Phalcon\Mvc\Model\Relation;
21
use Phalcon\Mvc\Model\RelationInterface;
22
use Phalcon\Mvc\Model\ResultsetInterface;
23
use Phalcon\Mvc\ModelInterface;
24
use Zemit\Mvc\Model;
25
use Zemit\Utils\Sprintf;
26
27
/**
28
 * Trait Relationship
29
 * Allow to automagically save all kind of relationships
30
 *
31
 * @author Julien Turbide <[email protected]>
32
 * @copyright Zemit Team <[email protected]>
33
 *
34
 * @since 1.0
35
 * @version 1.0
36
 *
37
 * @package Zemit\Mvc\Model
38
 */
39
trait Relationship
40
{
41
    protected array $_keepMissingRelated = [];
42
    protected array $_relationshipContext = [];
43
    
44
    protected $dirtyRelated;
45
    
46
    /**
47
     * Returns the models manager related to the entity instance
48
     *
49
     * @return ManagerInterface
50
     */
51
    abstract public function getModelsManager();
52
    
53
    /**
54
     * @return string
55
     */
56
    protected function _getRelationshipContext()
57
    {
58
        return implode('.', $this->_relationshipContext);
59
    }
60
    
61
    /**
62
     * @param $context
63
     */
64
    protected function _setRelationshipContext($context)
65
    {
66
        $this->_relationshipContext = array_filter(is_array($context) ? $context : explode('.', $context));
67
    }
68
    
69
    /**
70
     * @param array $data
71
     * @param null $whiteList
0 ignored issues
show
Documentation Bug introduced by
Are you sure the doc-type for parameter $whiteList is correct as it would always require null to be passed?
Loading history...
72
     * @param null $dataColumnMap
0 ignored issues
show
Documentation Bug introduced by
Are you sure the doc-type for parameter $dataColumnMap is correct as it would always require null to be passed?
Loading history...
73
     *
74
     * @return ModelInterface
75
     * @throws Exception
76
     */
77
    public function assign(array $data, $whiteList = null, $dataColumnMap = null): ModelInterface
78
    {
79
        $this->assignRelated(...func_get_args());
0 ignored issues
show
Bug introduced by
func_get_args() is expanded, but the parameter $data of Zemit\Mvc\Model\Relationship::assignRelated() does not expect variable arguments. ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-type  annotation

79
        $this->assignRelated(/** @scrutinizer ignore-type */ ...func_get_args());
Loading history...
80
        
81
        return parent::assign(...func_get_args());
82
    }
83
    
84
    /**
85
     * Assign related
86
     *
87
     * Single
88
     * [alias => new Alias()] // create new alias
89
     *
90
     * Many
91
     * [alias => [new Alias()]] // create new alias
92
     * [alias => [1, 2, 3, 4]] // append / merge 1, 2, 3, 4
93
     * [alias => [false, 1, 2, 4]]; // delete 3
94
     *
95
     * @param array $data
96
     * @param null $whiteList
0 ignored issues
show
Documentation Bug introduced by
Are you sure the doc-type for parameter $whiteList is correct as it would always require null to be passed?
Loading history...
97
     * @param null $dataColumnMap
0 ignored issues
show
Documentation Bug introduced by
Are you sure the doc-type for parameter $dataColumnMap is correct as it would always require null to be passed?
Loading history...
98
     *
99
     * @return $this|ModelInterface
100
     * @throws Exception
101
     */
102
    public function assignRelated(array $data, $whiteList = null, $dataColumnMap = null): ModelInterface
103
    {
104
        // no data, nothing to do
105
        if (empty($data)) {
106
            return $this;
0 ignored issues
show
Bug Best Practice introduced by
The expression return $this returns the type Zemit\Mvc\Model\Relationship which is incompatible with the type-hinted return Phalcon\Mvc\ModelInterface.
Loading history...
107
        }
108
        
109
        // Get the current model class name
110
        $modelClass = get_class($this);
111
        
112
        /** @var Manager $modelManager */
113
        $modelManager = $this->getModelsManager();
114
        
115
        foreach ($data as $alias => $relationData) {
116
            
117
            /** @var \Phalcon\Mvc\Model\Relation $relation */
118
            $relation = $modelManager->getRelationByAlias($modelClass, $alias);
119
            
120
            // @todo add a resursive whiteList check & columnMap support
121
            if ($relation) {
122
                
123
                $type = $relation->getType();
124
                
125
                $fields = $relation->getFields();
126
                $fields = is_array($fields) ? $fields : [$fields];
127
                
128
                $referencedFields = $relation->getReferencedFields();
129
                $referencedFields = is_array($referencedFields) ? $referencedFields : [$referencedFields];
130
                
131
                $referencedModel = $relation->getReferencedModel();
132
                $assign = null;
133
                
134
                if (is_int($relationData) || is_string($relationData)) {
135
                    $relationData = [$referencedFields[0] => $relationData];
136
                }
137
                
138
                if ($relationData instanceof ModelInterface) {
139
                    if ($relationData instanceof $referencedModel) {
140
                        $assign = $relationData;
141
                    }
142
                    else {
143
                        throw new Exception('Instance of `' . get_class($relationData) . '` received on model `' . $modelClass . '` in alias `' . $alias . ', expected instance of `' . $referencedModel . '`', 400);
144
                    }
145
                }
146
                
147
                // array | traversable | resultset
148
                else if (is_array($relationData) || $relationData instanceof \Traversable) {
149
                    $assign = [];
150
                    
151
                    $getEntityParams = [
152
                        'alias' => $alias,
153
                        'fields' => $referencedFields,
154
                        'modelClass' => $referencedModel,
155
                        'readFields' => $fields,
156
                        'type' => $type,
157
                        'whiteList' => $whiteList,
158
                        'dataColumnMap'=> $dataColumnMap,
159
                    ];
160
                    
161
                    if (empty($relationData)) {
162
                        $assign = $this->_getEntityFromData($relationData, $getEntityParams);
163
                    }
164
                    else {
165
                        foreach ($relationData as $traversedKey => $traversedData) {
166
                            // Array of things
167
                            if (is_int($traversedKey)) {
168
                                $entity = null;
169
                                
170
                                // Using bool as behaviour to delete missing relationship or keep them
171
                                // @TODO find a better way... :(
172
                                // if [alias => [true, ...]
173
                                switch($traversedData) {
0 ignored issues
show
Coding Style introduced by
Expected 1 space(s) after SWITCH keyword; 0 found
Loading history...
174
                                    case 'false':
175
                                        $traversedData = false;
176
                                        break;
177
                                    case 'true':
178
                                        $traversedData = true;
179
                                        break;
180
                                }
181
                                if (is_bool($traversedData)) {
182
                                    $this->_keepMissingRelated[$alias] = $traversedData;
183
                                    continue;
184
                                }
185
                                
186
                                // if [alias => [1, 2, 3, ...]]
187
                                if (is_int($traversedData) || is_string($traversedData)) {
188
                                    $traversedData = [$referencedFields[0] => $traversedData];
189
                                }
190
                                
191
                                // if [alias => AliasModel]
192
                                if ($traversedData instanceof ModelInterface) {
193
                                    if ($traversedData instanceof $referencedModel) {
194
                                        $entity = $traversedData;
195
                                    }
196
                                    else {
197
                                        throw new Exception('Instance of `' . get_class($traversedData) . '` received on model `' . $modelClass . '` in alias `' . $alias . ', expected instance of `' . $referencedModel . '`', 400);
198
                                    }
199
                                }
200
                                
201
                                // if [alias => [[id => 1], [id => 2], [id => 3], ....]]
202
                                else if (is_array($traversedData) || $traversedData instanceof \Traversable) {
203
                                    $entity = $this->_getEntityFromData((array)$traversedData, $getEntityParams);
204
                                }
205
                                
206
                                if ($entity) {
207
                                    $assign [] = $entity;
208
                                }
209
                            }
210
                            
211
                            // if [alias => [id => 1]]
212
                            else {
213
                                $assign = $this->_getEntityFromData((array)$relationData, $getEntityParams);
214
                                break;
215
                            }
216
                        }
217
                    }
218
                }
219
                
220
                // we got something to assign
221
                if (!empty($assign) || $this->_keepMissingRelated[$alias] === false) {
222
    
223
                    $this->$alias = is_array($assign) ? array_values(array_filter($assign)) : $assign;
224
                    
225
                    // fix to force recursive parent save from children entities within _preSaveRelatedRecords method
226
                    if ($this->$alias && $this->$alias instanceof ModelInterface) {
227
                        $this->$alias->setDirtyState(self::DIRTY_STATE_TRANSIENT);
0 ignored issues
show
Bug introduced by
The constant Zemit\Mvc\Model\Relation...::DIRTY_STATE_TRANSIENT was not found. Maybe you did not declare it correctly or list all dependencies?
Loading history...
228
                    }
229
                    
230
                    $this->dirtyRelated[mb_strtolower($alias)] = $this->$alias ?? false;
231
                }
232
            } // END RELATION
233
        } // END DATA LOOP
234
        
235
        return $this;
0 ignored issues
show
Bug Best Practice introduced by
The expression return $this returns the type Zemit\Mvc\Model\Relationship which is incompatible with the type-hinted return Phalcon\Mvc\ModelInterface.
Loading history...
236
    }
237
    
238
    /**
239
     * Saves related records that must be stored prior to save the master record
240
     *
241
     * @todo Remove in v5.0
242
     * @deprecated Use preSaveRelatedRecords()
243
     *
244
     * @param \Phalcon\Mvc\ModelInterface[] related
0 ignored issues
show
Bug introduced by
The type Zemit\Mvc\Model\related was not found. Maybe you did not declare it correctly or list all dependencies?

The issue could also be caused by a filter entry in the build configuration. If the path has been excluded in your configuration, e.g. excluded_paths: ["lib/*"], you can move it to the dependency path list as follows:

filter:
    dependency_paths: ["lib/*"]

For further information see https://scrutinizer-ci.com/docs/tools/php/php-scrutinizer/#list-dependency-paths

Loading history...
245
     */
246
    protected function _preSaveRelatedRecords(AdapterInterface $connection, $related): bool
247
    {
248
        return $this->preSaveRelatedRecords($connection, $related);
249
    }
250
    
251
    /**
252
     * Saves related records that must be stored prior to save the master record
253
     * Refactored based on the native cphalcon version so we can support :
254
     * - combined keys on relationship definition
255
     * - relationship context within the model messages based on the alias definition
256
     *
257
     * @param \Phalcon\Db\Adapter\AdapterInterface $connection
258
     * @param $related
259
     *
260
     * @return bool
261
     * @throws Exception
262
     *
263
     */
264
    protected function preSaveRelatedRecords(\Phalcon\Db\Adapter\AdapterInterface $connection, $related): bool
265
    {
266
        $nesting = false;
267
        
268
        /**
269
         * Start an implicit transaction
270
         */
271
        $connection->begin($nesting);
272
        
273
        $className = get_class($this);
274
        
275
        /** @var ManagerInterface $manager */
276
        $manager = $this->getModelsManager();
277
        
278
        /**
279
         * @var string $alias alias
280
         * @var ModelInterface $record
281
         */
282
        foreach ($related as $alias => $record) {
283
            /**
284
             * Try to get a relation with the same name
285
             */
286
            $relation = $manager->getRelationByAlias($className, $alias);
287
            
288
            if ($relation) {
289
                /**
290
                 * Get the relation type
291
                 */
292
                $type = $relation->getType();
293
                
294
                /**
295
                 * Only belongsTo are stored before save the master record
296
                 */
297
                if ($type === Relation::BELONGS_TO) {
298
                    
299
                    /**
300
                     * We only support model interface for the belongs-to relation
301
                     */
302
                    if (!($record instanceof ModelInterface)) {
303
                        $connection->rollback($nesting);
304
                        throw new Exception(
305
                            'Instance of `' . get_class($record) . '` received on model `' . $className . '` in alias `' . $alias .
306
                            ', expected instance of `' . ModelInterface::class . '` as part of the belongs-to relation',
307
                            400
308
                        );
309
                    }
310
                    
311
                    /**
312
                     * Get columns and referencedFields as array
313
                     */
314
                    $referencedFields = $relation->getReferencedFields();
315
                    $columns = $relation->getFields();
316
                    $referencedFields = is_array($referencedFields) ? $referencedFields : [$referencedFields];
317
                    $columns = is_array($columns) ? $columns : [$columns];
318
                    
319
                    /**
320
                     * Set the relationship context
321
                     */
322
                    $record->_setRelationshipContext($this->_getRelationshipContext() . '.' . $alias);
323
                    
324
                    /**
325
                     * If dynamic update is enabled, saving the record must not take any action
326
                     * Only save if the model is dirty to prevent circular relations causing an infinite loop
327
                     */
328
                    if ($record->getDirtyState() !== Model::DIRTY_STATE_PERSISTENT && !$record->save()) {
329
                        
330
                        /**
331
                         * Append messages with context
332
                         */
333
                        $this->appendMessagesFromRecord($record, $alias);
334
                        
335
                        /**
336
                         * Rollback the implicit transaction
337
                         */
338
                        $connection->rollback($nesting);
339
                        
340
                        return false;
341
                    }
342
                    
343
                    /**
344
                     * Read the attributes from the referenced model and assign
345
                     * it to the current model
346
                     */
347
                    foreach ($referencedFields as $key => $referencedField) {
348
                        $this->{$columns[$key]} = $record->readAttribute($referencedField);
349
                    }
350
                }
351
            }
352
        }
353
        
354
        return true;
355
    }
356
    
357
    /**
358
     * Save the related records assigned in the has-one/has-many relations
359
     *
360
     * @todo Remove in v5.0
361
     * @deprecated Use postSaveRelatedRecords()
362
     *
363
     * @param \Phalcon\Mvc\ModelInterface[] related
364
     * @return bool
365
     */
366
    protected function _postSaveRelatedRecords(AdapterInterface $connection, $related): bool
367
    {
368
        return $this->postSaveRelatedRecords($connection, $related);
369
    }
370
    
371
    /**
372
     * NOTE: we need this, this behaviour only happens:
373
     * - in many to many nodes
374
     * Fix uniqueness on combined keys in node entities, and possibly more...
375
     * @link https://forum.phalconphp.com/discussion/2190/many-to-many-expected-behaviour
376
     * @link http://stackoverflow.com/questions/23374858/update-a-records-n-n-relationships
377
     * @link https://github.com/phalcon/cphalcon/issues/2871
378
     *
379
     * @param \Phalcon\Db\Adapter\AdapterInterface $connection
380
     * @param $related
381
     *
382
     * @return array|bool
383
     */
384
    protected function postSaveRelatedRecords(\Phalcon\Db\Adapter\AdapterInterface $connection, $related): bool
385
    {
386
        $nesting = false;
387
        
388
        if ($related) {
389
            foreach ($related as $lowerCaseAlias => $assign) {
390
                
391
                /** @var Manager $modelManager */
392
                $modelManager = $this->getModelsManager();
393
                
394
                /** @var RelationInterface $relation */
395
                $relation = $modelManager->getRelationByAlias(get_class($this), $lowerCaseAlias);
396
                
397
                // only many to many
398
                if ($relation) {
399
                    $alias = $relation->getOption('alias');
0 ignored issues
show
Unused Code introduced by
The assignment to $alias is dead and can be removed.
Loading history...
400
                    
401
                    /**
402
                     * Discard belongsTo relations
403
                     */
404
                    if ($relation->getType() === Relation::BELONGS_TO) {
405
                        continue;
406
                    }
407
                    
408
                    if (!is_array($assign) && !is_object($assign)) {
409
                        $connection->rollback($nesting);
410
                        throw new Exception("Only objects/arrays can be stored as part of has-many/has-one/has-one-through/has-many-to-many relations");
411
                    }
412
                    
413
                    /**
414
                     * Custom logic for many-to-many relationships
415
                     */
416
                    if ($relation->getType() === Relation::HAS_MANY_THROUGH) {
417
//                        $nodeAssign = [];
418
                        
419
                        $originFields = $relation->getFields();
420
                        $originFields = is_array($originFields) ? $originFields : [$originFields];
421
                        
422
                        $intermediateModelClass = $relation->getIntermediateModel();
423
                        $intermediateFields = $relation->getIntermediateFields();
424
                        $intermediateFields = is_array($intermediateFields) ? $intermediateFields : [$intermediateFields];
425
                        
426
                        $intermediateReferencedFields = $relation->getIntermediateReferencedFields();
427
                        $intermediateReferencedFields = is_array($intermediateReferencedFields) ? $intermediateReferencedFields : [$intermediateReferencedFields];
428
                        
429
                        $referencedFields = $relation->getReferencedFields();
430
                        $referencedFields = is_array($referencedFields) ? $referencedFields : [$referencedFields];
431
                        
432
                        /** @var ModelInterface $intermediate */
433
                        /** @var ModelInterface|string $intermediateModelClass */
434
                        $intermediate = new $intermediateModelClass();
435
                        $intermediatePrimaryKeyAttributes = $intermediate->getModelsMetaData()->getPrimaryKeyAttributes($intermediate);
436
                        $intermediateBindTypes = $intermediate->getModelsMetaData()->getBindTypes($intermediate);
437
                        
438
                        // get current model bindings
439
                        $originBind = [];
440
                        foreach ($originFields as $originField) {
441
                            $originBind [] = $this->{'get' . ucfirst($originField)} ?? $this->$originField ?? null;
442
                        }
443
                        
444
                        $nodeIdListToKeep = [];
445
                        foreach ($assign as $key => $entity) {
446
                            // get referenced model bindings
447
                            $referencedBind = [];
448
                            foreach ($referencedFields as $referencedField) {
449
                                $referencedBind [] = $entity->{'get' . ucfirst($referencedField)} ?? $entity->$referencedField ?? null;
450
                            }
451
                            
452
                            /** @var ModelInterface $nodeEntity */
453
                            $nodeEntity = $intermediateModelClass::findFirst([
454
                                'conditions' => Sprintf::implodeArrayMapSprintf(array_merge($intermediateFields, $intermediateReferencedFields), ' and ', '[' . $intermediateModelClass . '].[%s] = ?%s'),
0 ignored issues
show
Bug introduced by
Are you sure $intermediateModelClass of type Phalcon\Mvc\ModelInterface|string can be used in concatenation? ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-type  annotation

454
                                'conditions' => Sprintf::implodeArrayMapSprintf(array_merge($intermediateFields, $intermediateReferencedFields), ' and ', '[' . /** @scrutinizer ignore-type */ $intermediateModelClass . '].[%s] = ?%s'),
Loading history...
455
                                'bind' => [...$originBind, ...$referencedBind],
456
                                'bindTypes' => array_fill(0, count($intermediateFields) + count($intermediateReferencedFields), Column::BIND_PARAM_STR),
457
                            ]);
458
                            
459
                            if ($nodeEntity) {
460
                                $buildPrimaryKey = [];
461
                                foreach ($intermediatePrimaryKeyAttributes as $intermediatePrimaryKey => $intermediatePrimaryKeyAttribute) {
462
                                    $buildPrimaryKey [] = $nodeEntity->readAttribute($intermediatePrimaryKeyAttribute);
463
                                }
464
                                $nodeIdListToKeep [] = implode('.', $buildPrimaryKey);
465
                                
466
                                // Restoring node entities if previously soft deleted
467
                                if (method_exists($nodeEntity, 'restore') && method_exists($nodeEntity, 'isDeleted')) {
468
                                    if ($nodeEntity->isDeleted() && !$nodeEntity->restore()) {
469
                                        
470
                                        /**
471
                                         * Append messages with context
472
                                         */
473
                                        $this->appendMessagesFromRecord($nodeEntity, $lowerCaseAlias);
474
                                        
475
                                        /**
476
                                         * Rollback the implicit transaction
477
                                         */
478
                                        $connection->rollback($nesting);
479
                                        
480
                                        return false;
481
                                    }
482
                                }
483
                                
484
                                // save edge record
485
                                if (!$entity->save()) {
486
                                    
487
                                    /**
488
                                     * Append messages with context
489
                                     */
490
                                    $this->appendMessagesFromRecord($entity, $lowerCaseAlias);
491
                                    
492
                                    /**
493
                                     * Rollback the implicit transaction
494
                                     */
495
                                    $connection->rollback($nesting);
496
                                    
497
                                    return false;
498
                                }
499
                                
500
                                // remove it
501
                                unset($assign[$key]);
502
                                unset($related[$lowerCaseAlias][$key]);
503
504
//                                // add to assign
505
//                                $nodeAssign [] = $nodeEntity;
506
                            }
507
                        }
508
                        
509
                        if (!($this->_keepMissingRelated[$lowerCaseAlias] ?? true)) {
510
                            // handle if we empty the related
511
                            if (empty($nodeIdListToKeep)) {
512
                                $nodeIdListToKeep = [0];
513
                            }
514
                            else {
515
                                $nodeIdListToKeep = array_values(array_filter(array_unique($nodeIdListToKeep)));
516
                            }
517
                            
518
                            $idBindType = count($intermediatePrimaryKeyAttributes) === 1 ? $intermediateBindTypes[$intermediatePrimaryKeyAttributes[0]] : Column::BIND_PARAM_STR;
519
                            
520
                            /** @var ModelInterface|string $intermediateModelClass */
521
                            $nodeEntityToDeleteList = $intermediateModelClass::find([
522
                                'conditions' => Sprintf::implodeArrayMapSprintf(array_merge($intermediateFields), ' and ', '[' . $intermediateModelClass . '].[%s] = ?%s')
523
                                    . ' and concat(' . Sprintf::implodeArrayMapSprintf($intermediatePrimaryKeyAttributes, ', \'.\', ', '[' . $intermediateModelClass . '].[%s]') . ') not in ({id:array})',
524
                                'bind' => [...$originBind, 'id' => $nodeIdListToKeep],
525
                                'bindTypes' => [...array_fill(0, count($intermediateFields), Column::BIND_PARAM_STR), 'id' => $idBindType],
526
                            ]);
527
                            
528
                            // delete missing related
529
                            if (!$nodeEntityToDeleteList->delete()) {
530
                                
531
                                /**
532
                                 * Append messages with context
533
                                 */
534
                                $this->appendMessagesFromRecord($nodeEntityToDeleteList, $lowerCaseAlias);
535
                                
536
                                /**
537
                                 * Rollback the implicit transaction
538
                                 */
539
                                $connection->rollback($nesting);
540
                                
541
                                return false;
542
                            }
543
                        }
544
                    }
545
                    
546
                    $columns = $relation->getFields();
547
                    $referencedFields = $relation->getReferencedFields();
548
                    $columns = is_array($columns) ? $columns : [$columns];
549
                    $referencedFields = is_array($referencedFields) ? $referencedFields : [$referencedFields];
550
                    
551
                    /**
552
                     * Create an implicit array for has-many/has-one records
553
                     */
554
                    $relatedRecords = $assign instanceof ModelInterface ? [$assign] : $assign;
555
                    
556
                    foreach ($columns as $column) {
557
                        if (!property_exists($this, $column)) {
558
                            $connection->rollback($nesting);
559
                            throw new Exception("The column '" . $column . "' needs to be present in the model");
560
                        }
561
                    }
562
                    
563
                    
564
                    /**
565
                     * Get the value of the field from the current model
566
                     * Check if the relation is a has-many-to-many
567
                     */
568
                    $isThrough = (bool)$relation->isThrough();
569
                    
570
                    /**
571
                     * Get the rest of intermediate model info
572
                     */
573
                    if ($isThrough) {
574
                        $intermediateModelClass = $relation->getIntermediateModel();
575
                        $intermediateFields = $relation->getIntermediateFields();
576
                        $intermediateReferencedFields = $relation->getIntermediateReferencedFields();
577
                        $intermediateFields = is_array($intermediateFields) ? $intermediateFields : [$intermediateFields];
578
                        $intermediateReferencedFields = is_array($intermediateReferencedFields) ? $intermediateReferencedFields : [$intermediateReferencedFields];
579
                    }
580
                    
581
                    
582
                    foreach ($relatedRecords as $recordAfter) {
583
                        if (!$isThrough) {
584
                            foreach ($columns as $key => $column) {
585
                                $recordAfter->writeAttribute($referencedFields[$key], $this->$column);
586
                            }
587
                        }
588
                        
589
                        /**
590
                         * Save the record and get messages
591
                         */
592
                        if (!$recordAfter->save()) {
593
                            
594
                            /**
595
                             * Append messages with context
596
                             */
597
                            $this->appendMessagesFromRecord($recordAfter, $lowerCaseAlias);
598
                            
599
                            /**
600
                             * Rollback the implicit transaction
601
                             */
602
                            $connection->rollback($nesting);
603
                            
604
                            return false;
605
                        }
606
                        
607
                        if ($isThrough) {
608
                            
609
                            /**
610
                             * Create a new instance of the intermediate model
611
                             */
612
                            $intermediateModel = $modelManager->load($intermediateModelClass);
0 ignored issues
show
Comprehensibility Best Practice introduced by
The variable $intermediateModelClass does not seem to be defined for all execution paths leading up to this point.
Loading history...
613
                            
614
                            /**
615
                             *  Has-one-through relations can only use one intermediate model.
616
                             *  If it already exist, it can be updated with the new referenced key.
617
                             */
618
                            if ($relation->getType() === Relation::HAS_ONE_THROUGH) {
619
                                $bind = [];
620
                                foreach ($columns as $column) {
621
                                    $bind[] = $this->$column;
622
                                }
623
                                
624
                                $existingIntermediateModel = $intermediateModelClass::findFirst([
625
                                    'conditions' => Sprintf::implodeArrayMapSprintf($intermediateFields, ' and ', '[' . $intermediateModelClass . '].[%s] = ?%s'),
0 ignored issues
show
Comprehensibility Best Practice introduced by
The variable $intermediateFields does not seem to be defined for all execution paths leading up to this point.
Loading history...
626
                                    'bind' => $bind,
627
                                    'bindTypes' => array_fill(0, count($bind), Column::BIND_PARAM_STR),
628
                                ]);
629
                                
630
                                if ($existingIntermediateModel) {
631
                                    $intermediateModel = $existingIntermediateModel;
632
                                }
633
                            }
634
                            
635
                            foreach ($columns as $key => $column) {
636
                                /**
637
                                 * Write value in the intermediate model
638
                                 */
639
                                $intermediateModel->writeAttribute($intermediateFields[$key], $this->$column);
640
                                
641
                                /**
642
                                 * Get the value from the referenced model
643
                                 */
644
                                $intermediateValue = $recordAfter->readAttribute($referencedFields[$key]);
645
                                
646
                                /**
647
                                 * Write the intermediate value in the intermediate model
648
                                 */
649
                                $intermediateModel->writeAttribute($intermediateReferencedFields[$key], $intermediateValue);
0 ignored issues
show
Comprehensibility Best Practice introduced by
The variable $intermediateReferencedFields does not seem to be defined for all execution paths leading up to this point.
Loading history...
650
                            }
651
                            
652
                            
653
                            /**
654
                             * Save the record and get messages
655
                             */
656
                            if (!$intermediateModel->save()) {
657
                                
658
                                /**
659
                                 * Append messages with context
660
                                 */
661
                                $this->appendMessagesFromRecord($intermediateModel, $lowerCaseAlias);
662
                                
663
                                /**
664
                                 * Rollback the implicit transaction
665
                                 */
666
                                $connection->rollback($nesting);
667
                                
668
                                return false;
669
                            }
670
                        }
671
                    }
672
                }
673
                else {
674
                    if (is_array($assign)) {
675
                        $connection->rollback($nesting);
676
                        throw new Exception("There are no defined relations for the model '" . get_class($this) . "' using alias '" . $lowerCaseAlias . "'");
677
                    }
678
                }
679
            }
680
        }
681
        
682
        /**
683
         * Commit the implicit transaction
684
         */
685
        $connection->commit($nesting);
686
        
687
        return true;
688
689
//        // no commit here cuz parent::_postSaveRelatedRecords will fire it
690
//        return [true, $connection, array_filter($related ?? [])];
691
    }
692
    
693
    /**
694
     * Get an entity from data
695
     *
696
     * @param array $data
697
     * @param array $configuration
698
     *
699
     * @param string $alias deprecated
700
     * @param array $fields deprecated
701
     * @param string $modelClass deprecated
702
     * @param array|null $readFields deprecated
703
     * @param int|null $type deprecated
704
     * @param array|null $whiteList deprecated
705
     * @param array|null $dataColumnMap deprecated
706
     *
707
     * @return ModelInterface
708
     * @todo unit test for this
709
     *
710
     * @return ModelInterface
711
     */
712
    public function _getEntityFromData(array $data, array $configuration = []): ModelInterface
713
    {
714
        $alias = $configuration['alias'] ?? null;
715
        $fields = $configuration['fields'] ?? null;
716
        $modelClass = $configuration['modelClass'] ?? null;
717
        $readFields = $configuration['readFields'] ?? null;
718
        $type = $configuration['type'] ?? null;
719
        $whiteList = $configuration['whiteList'] ?? null;
720
        
721
        $dataColumnMap = $configuration['dataColumnMap'] ?? null;
722
        
723
        $entity = null;
724
        
725
        if ($type === Relation::HAS_ONE || $type === Relation::BELONGS_TO) {
726
            
727
            // Set value to compare
728
            if (!empty($readFields)) {
729
                foreach ($readFields as $key => $field) {
730
                    if (empty($data[$fields[$key]])) { // @todo maybe remove this if
731
                        $value = $this->readAttribute($field);
0 ignored issues
show
Bug introduced by
It seems like readAttribute() must be provided by classes using this trait. How about adding it as abstract method to this trait? ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-call  annotation

731
                        /** @scrutinizer ignore-call */ 
732
                        $value = $this->readAttribute($field);
Loading history...
732
                        if (!empty($value)) { // @todo maybe remove this if
733
                            $data [$fields[$key]] = $value;
734
                        }
735
                    }
736
                }
737
            }
738
            
0 ignored issues
show
Coding Style introduced by
Blank line found at end of control structure
Loading history...
739
        }
740
        
741
        // array_keys_exists (if $referencedFields keys exists)
742
        $dataKeys = array_intersect_key($data, array_flip($fields));
743
        
744
        // all keys were found
745
        if (count($dataKeys) === count($fields)) {
746
            
747
            if ($type === Relation::HAS_MANY) {
748
    
749
                /** @var MetaData $modelMetaData */
750
                $modelsMetaData = $this->getModelsMetaData();
0 ignored issues
show
Bug introduced by
The method getModelsMetaData() does not exist on Zemit\Mvc\Model\Relationship. Did you maybe mean getModelsManager()? ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-call  annotation

750
                /** @scrutinizer ignore-call */ 
751
                $modelsMetaData = $this->getModelsMetaData();

This check looks for calls to methods that do not seem to exist on a given type. It looks for the method on the type itself as well as in inherited classes or implemented interfaces.

This is most likely a typographical error or the method has been renamed.

Loading history...
751
                $primaryKeys = $modelsMetaData->getPrimaryKeyAttributes($this);
752
                
753
                // Force primary keys for single to many
754
                foreach ($primaryKeys as $primaryKey) {
755
                    if (isset($data[$primaryKey]) && !in_array($primaryKey, $fields, true)) {
756
                        $dataKeys [$primaryKey] = $data[$primaryKey];
757
                        $fields []= $primaryKey;
758
                    }
759
                }
760
            }
761
            
762
            /** @var ModelInterface $entity */
763
            /** @var ModelInterface|string $modelClass */
764
            $entity = $modelClass::findFirst([
765
                'conditions' => Sprintf::implodeArrayMapSprintf($fields, ' and ', '[' . $modelClass . '].[%s] = ?%s'),
0 ignored issues
show
Bug introduced by
Are you sure $modelClass of type Phalcon\Mvc\ModelInterface|string can be used in concatenation? ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-type  annotation

765
                'conditions' => Sprintf::implodeArrayMapSprintf($fields, ' and ', '[' . /** @scrutinizer ignore-type */ $modelClass . '].[%s] = ?%s'),
Loading history...
766
                'bind' => array_values($dataKeys),
767
                'bindTypes' => array_fill(0, count($dataKeys), Column::BIND_PARAM_STR),
768
            ]);
769
        }
770
        
771
        if (!$entity) {
772
            /** @var ModelInterface $entity */
773
            $entity = new $modelClass();
774
        }
775
        
776
        // assign new values
777
        if ($entity) {
778
            
779
            // can be null to bypass, empty array for nothing or filled array
780
            $assignWhiteList = isset($whiteList[$modelClass]) || isset($whiteList[$alias]);
781
            $assignColumnMap = isset($dataColumnMap[$modelClass]) || isset($dataColumnMap[$alias]);
782
            $assignWhiteList = $assignWhiteList? array_merge_recursive($whiteList[$modelClass] ?? [], $whiteList[$alias] ?? []) : null;
783
            $assignColumnMap = $assignColumnMap? array_merge_recursive($dataColumnMap[$modelClass] ?? [], $dataColumnMap[$alias] ?? []) : null;
784
            
785
            $entity->assign($data, $assignWhiteList, $assignColumnMap);
786
//            $entity->setDirtyState(self::DIRTY_STATE_TRANSIENT);
787
        }
788
        
789
        
790
        return $entity;
0 ignored issues
show
Bug Best Practice introduced by
The expression return $entity could return the type boolean|null which is incompatible with the type-hinted return Phalcon\Mvc\ModelInterface. Consider adding an additional type-check to rule them out.
Loading history...
791
    }
792
    
793
    /**
794
     * Append a message to this model from another record,
795
     * also prepend a context to the previous context
796
     *
797
     * @param ResultsetInterface|ModelInterface $record
798
     * @param string|null $context
799
     */
800
    public function appendMessagesFromRecord($record, string $context = null)
801
    {
802
        /**
803
         * Get the validation messages generated by the
804
         * referenced model
805
         */
806
        foreach ($record->getMessages() as $message) {
807
            
808
            /**
809
             * Set the related model
810
             */
811
            $message->setMetaData([
812
//                'model' => $record,
813
                'context' => $this->rebuildMessageContext($message, $context),
0 ignored issues
show
Bug introduced by
It seems like $context can also be of type null; however, parameter $context of Zemit\Mvc\Model\Relation...rebuildMessageContext() does only seem to accept string, maybe add an additional type check? ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-type  annotation

813
                'context' => $this->rebuildMessageContext($message, /** @scrutinizer ignore-type */ $context),
Loading history...
814
            ]);
815
            
816
            /**
817
             * Appends the messages to the current model
818
             */
819
            $this->appendMessage($message);
0 ignored issues
show
Bug introduced by
The method appendMessage() does not exist on Zemit\Mvc\Model\Relationship. Did you maybe mean appendMessagesFromRecord()? ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-call  annotation

819
            $this->/** @scrutinizer ignore-call */ 
820
                   appendMessage($message);

This check looks for calls to methods that do not seem to exist on a given type. It looks for the method on the type itself as well as in inherited classes or implemented interfaces.

This is most likely a typographical error or the method has been renamed.

Loading history...
820
        }
821
    }
822
    
823
    /**
824
     * Rebuilding context for meta data
825
     *
826
     * @param Message $message
827
     * @param string $context
828
     *
829
     * @return string
830
     */
831
    public function rebuildMessageContext(Message $message, string $context)
832
    {
833
        $metaData = $message->getMetaData();
834
        $previousContext = $metaData['context'] ?? null;
835
        
836
        return $context . (empty($previousContext) ? null : '.' . $previousContext);
837
    }
838
}
839