Passed
Push — master ( c7fbb2...6f0d6f )
by Julien
17:15
created

Relationship::_getEntityFromData()   C

Complexity

Conditions 14
Paths 36

Size

Total Lines 61
Code Lines 26

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 0
CRAP Score 210

Importance

Changes 7
Bugs 6 Features 1
Metric Value
eloc 26
c 7
b 6
f 1
dl 0
loc 61
ccs 0
cts 24
cp 0
rs 6.2666
cc 14
nc 36
nop 7
crap 210

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
                    if (empty($relationData)) {
152
                        $assign = $this->_getEntityFromData($relationData, $referencedFields, $referencedModel, $fields, $type, $whiteList, $dataColumnMap);
153
                    }
154
                    else {
155
                        foreach ($relationData as $traversedKey => $traversedData) {
156
                            // Array of things
157
                            if (is_int($traversedKey)) {
158
                                $entity = null;
159
                                
160
                                // Using bool as behaviour to delete missing relationship or keep them
161
                                // @TODO find a better way... :(
162
                                // if [alias => [true, ...]
163
                                switch($traversedData) {
0 ignored issues
show
Coding Style introduced by
Expected 1 space(s) after SWITCH keyword; 0 found
Loading history...
164
                                    case 'false':
165
                                        $traversedData = false;
166
                                        break;
167
                                    case 'true':
168
                                        $traversedData = true;
169
                                        break;
170
                                }
171
                                if (is_bool($traversedData)) {
172
                                    $this->_keepMissingRelated[$alias] = $traversedData;
173
                                    continue;
174
                                }
175
                                
176
                                // if [alias => [1, 2, 3, ...]]
177
                                if (is_int($traversedData) || is_string($traversedData)) {
178
                                    $traversedData = [$referencedFields[0] => $traversedData];
179
                                }
180
                                
181
                                // if [alias => AliasModel]
182
                                if ($traversedData instanceof ModelInterface) {
183
                                    if ($traversedData instanceof $referencedModel) {
184
                                        $entity = $traversedData;
185
                                    }
186
                                    else {
187
                                        throw new Exception('Instance of `' . get_class($traversedData) . '` received on model `' . $modelClass . '` in alias `' . $alias . ', expected instance of `' . $referencedModel . '`', 400);
188
                                    }
189
                                }
190
                                
191
                                // if [alias => [[id => 1], [id => 2], [id => 3], ....]]
192
                                else if (is_array($traversedData) || $traversedData instanceof \Traversable) {
193
                                    $entity = $this->_getEntityFromData((array)$traversedData, $referencedFields, $referencedModel, $fields, $type, $whiteList, $dataColumnMap);
194
                                }
195
                                
196
                                if ($entity) {
197
                                    $assign [] = $entity;
198
                                }
199
                            }
200
                            
201
                            // if [alias => [id => 1]]
202
                            else {
203
                                $assign = $this->_getEntityFromData((array)$relationData, $referencedFields, $referencedModel, $fields, $type, $whiteList, $dataColumnMap);
204
                                break;
205
                            }
206
                        }
207
                    }
208
                }
209
                
210
                // we got something to assign
211
                if (!empty($assign) || $this->_keepMissingRelated[$alias] === false) {
212
                    
213
                    $this->$alias = is_array($assign) ? array_values(array_filter($assign)) : $assign;
214
                    $this->dirtyRelated[$alias] = $this->$alias ?? false;
215
                }
216
            } // END RELATION
217
        } // END DATA LOOP
218
        
219
        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...
220
    }
221
    
222
    /**
223
     * Saves related records that must be stored prior to save the master record
224
     * Refactored based on the native cphalcon version so we can support :
225
     * - combined keys on relationship definition
226
     * - relationship context within the model messages based on the alias definition
227
     *
228
     * @param \Phalcon\Db\Adapter\AdapterInterface $connection
229
     * @param $related
230
     *
231
     * @return bool
232
     * @throws Exception
233
     *
234
     */
235
    protected function _preSaveRelatedRecords(\Phalcon\Db\Adapter\AdapterInterface $connection, $related): bool
236
    {
237
        $nesting = false;
238
        
239
        /**
240
         * Start an implicit transaction
241
         */
242
        $connection->begin($nesting);
243
        
244
        $className = get_class($this);
245
        
246
        /** @var ManagerInterface $manager */
247
        $manager = $this->getModelsManager();
248
        
249
        /**
250
         * @var string $alias alias
251
         * @var ModelInterface $record
252
         */
253
        foreach ($related as $alias => $record) {
254
            /**
255
             * Try to get a relation with the same name
256
             */
257
            $relation = $manager->getRelationByAlias($className, $alias);
258
            
259
            if ($relation) {
260
                /**
261
                 * Get the relation type
262
                 */
263
                $type = $relation->getType();
264
                
265
                /**
266
                 * Only belongsTo are stored before save the master record
267
                 */
268
                if ($type === Relation::BELONGS_TO) {
269
                    
270
                    /**
271
                     * We only support model interface for the belongs-to relation
272
                     */
273
                    if (!($record instanceof ModelInterface)) {
274
                        $connection->rollback($nesting);
275
                        throw new Exception(
276
                            'Instance of `' . get_class($record) . '` received on model `' . $className . '` in alias `' . $alias .
277
                            ', expected instance of `' . ModelInterface::class . '` as part of the belongs-to relation',
278
                            400
279
                        );
280
                    }
281
                    
282
                    /**
283
                     * Get columns and referencedFields as array
284
                     */
285
                    $referencedFields = $relation->getReferencedFields();
286
                    $columns = $relation->getFields();
287
                    $referencedFields = is_array($referencedFields) ? $referencedFields : [$referencedFields];
288
                    $columns = is_array($columns) ? $columns : [$columns];
289
                    
290
                    /**
291
                     * Set the relationship context
292
                     */
293
                    $record->_setRelationshipContext($this->_getRelationshipContext() . '.' . $alias);
294
                    
295
                    /**
296
                     * If dynamic update is enabled, saving the record must not take any action
297
                     * Only save if the model is dirty to prevent circular relations causing an infinite loop
298
                     */
299
                    if ($record->getDirtyState() !== Model::DIRTY_STATE_PERSISTENT && !$record->save()) {
300
                        
301
                        /**
302
                         * Append messages with context
303
                         */
304
                        $this->appendMessagesFromRecord($record, $alias);
305
                        
306
                        /**
307
                         * Rollback the implicit transaction
308
                         */
309
                        $connection->rollback($nesting);
310
                        
311
                        return false;
312
                    }
313
                    
314
                    /**
315
                     * Read the attributes from the referenced model and assign
316
                     * it to the current model
317
                     */
318
                    foreach ($referencedFields as $key => $referencedField) {
319
                        $this->{$columns[$key]} = $record->readAttribute($referencedField);
320
                    }
321
                }
322
            }
323
        }
324
        
325
        return true;
326
    }
327
    
328
    /**
329
     * NOTE: we need this, this behaviour only happens:
330
     * - in many to many nodes
331
     * Fix uniqueness on combined keys in node entities, and possibly more...
332
     * @link https://forum.phalconphp.com/discussion/2190/many-to-many-expected-behaviour
333
     * @link http://stackoverflow.com/questions/23374858/update-a-records-n-n-relationships
334
     * @link https://github.com/phalcon/cphalcon/issues/2871
335
     *
336
     * @param \Phalcon\Db\Adapter\AdapterInterface $connection
337
     * @param $related
338
     *
339
     * @return array|bool
340
     */
341
    protected function _postSaveRelatedRecords(\Phalcon\Db\Adapter\AdapterInterface $connection, $related): bool
342
    {
343
        $nesting = false;
344
        
345
        if ($related) {
346
            foreach ($related as $lowerCaseAlias => $assign) {
347
                
348
                /** @var Manager $modelManager */
349
                $modelManager = $this->getModelsManager();
350
                
351
                /** @var RelationInterface $relation */
352
                $relation = $modelManager->getRelationByAlias(get_class($this), $lowerCaseAlias);
353
                
354
                // only many to many
355
                if ($relation) {
356
                    $alias = $relation->getOption('alias');
0 ignored issues
show
Unused Code introduced by
The assignment to $alias is dead and can be removed.
Loading history...
357
                    
358
                    /**
359
                     * Discard belongsTo relations
360
                     */
361
                    if ($relation->getType() === Relation::BELONGS_TO) {
362
                        continue;
363
                    }
364
                    
365
                    if (!is_array($assign) && !is_object($assign)) {
366
                        $connection->rollback($nesting);
367
                        throw new Exception("Only objects/arrays can be stored as part of has-many/has-one/has-one-through/has-many-to-many relations");
368
                    }
369
                    
370
                    /**
371
                     * Custom logic for many-to-many relationships
372
                     */
373
                    if ($relation->getType() === Relation::HAS_MANY_THROUGH) {
374
//                        $nodeAssign = [];
375
                        
376
                        $originFields = $relation->getFields();
377
                        $originFields = is_array($originFields) ? $originFields : [$originFields];
378
                        
379
                        $intermediateModelClass = $relation->getIntermediateModel();
380
                        $intermediateFields = $relation->getIntermediateFields();
381
                        $intermediateFields = is_array($intermediateFields) ? $intermediateFields : [$intermediateFields];
382
                        
383
                        $intermediateReferencedFields = $relation->getIntermediateReferencedFields();
384
                        $intermediateReferencedFields = is_array($intermediateReferencedFields) ? $intermediateReferencedFields : [$intermediateReferencedFields];
385
                        
386
                        $referencedFields = $relation->getReferencedFields();
387
                        $referencedFields = is_array($referencedFields) ? $referencedFields : [$referencedFields];
388
                        
389
                        /** @var ModelInterface $intermediate */
390
                        /** @var ModelInterface|string $intermediateModelClass */
391
                        $intermediate = new $intermediateModelClass();
392
                        $intermediatePrimaryKeyAttributes = $intermediate->getModelsMetaData()->getPrimaryKeyAttributes($intermediate);
393
                        $intermediateBindTypes = $intermediate->getModelsMetaData()->getBindTypes($intermediate);
394
                        
395
                        // get current model bindings
396
                        $originBind = [];
397
                        foreach ($originFields as $originField) {
398
                            $originBind [] = $this->{'get' . ucfirst($originField)} ?? $this->$originField ?? null;
399
                        }
400
                        
401
                        $nodeIdListToKeep = [];
402
                        foreach ($assign as $key => $entity) {
403
                            // get referenced model bindings
404
                            $referencedBind = [];
405
                            foreach ($referencedFields as $referencedField) {
406
                                $referencedBind [] = $entity->{'get' . ucfirst($referencedField)} ?? $entity->$referencedField ?? null;
407
                            }
408
                            
409
                            /** @var ModelInterface $nodeEntity */
410
                            $nodeEntity = $intermediateModelClass::findFirst([
411
                                '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

411
                                'conditions' => Sprintf::implodeArrayMapSprintf(array_merge($intermediateFields, $intermediateReferencedFields), ' and ', '[' . /** @scrutinizer ignore-type */ $intermediateModelClass . '].[%s] = ?%s'),
Loading history...
412
                                'bind' => [...$originBind, ...$referencedBind],
413
                                'bindTypes' => array_fill(0, count($intermediateFields) + count($intermediateReferencedFields), Column::BIND_PARAM_STR),
414
                            ]);
415
                            
416
                            if ($nodeEntity) {
417
                                $buildPrimaryKey = [];
418
                                foreach ($intermediatePrimaryKeyAttributes as $intermediatePrimaryKey => $intermediatePrimaryKeyAttribute) {
419
                                    $buildPrimaryKey [] = $nodeEntity->readAttribute($intermediatePrimaryKeyAttribute);
420
                                }
421
                                $nodeIdListToKeep [] = implode('.', $buildPrimaryKey);
422
                                
423
                                // Restoring node entities if previously soft deleted
424
                                if (method_exists($nodeEntity, 'restore') && method_exists($nodeEntity, 'isDeleted')) {
425
                                    if ($nodeEntity->isDeleted() && !$nodeEntity->restore()) {
426
                                        
427
                                        /**
428
                                         * Append messages with context
429
                                         */
430
                                        $this->appendMessagesFromRecord($nodeEntity, $lowerCaseAlias);
431
                                        
432
                                        /**
433
                                         * Rollback the implicit transaction
434
                                         */
435
                                        $connection->rollback($nesting);
436
                                        
437
                                        return false;
438
                                    }
439
                                }
440
                                
441
                                // save edge record
442
                                if (!$entity->save()) {
443
                                    
444
                                    /**
445
                                     * Append messages with context
446
                                     */
447
                                    $this->appendMessagesFromRecord($entity, $lowerCaseAlias);
448
                                    
449
                                    /**
450
                                     * Rollback the implicit transaction
451
                                     */
452
                                    $connection->rollback($nesting);
453
                                    
454
                                    return false;
455
                                }
456
                                
457
                                // remove it
458
                                unset($assign[$key]);
459
                                unset($related[$lowerCaseAlias][$key]);
460
461
//                                // add to assign
462
//                                $nodeAssign [] = $nodeEntity;
463
                            }
464
                        }
465
                        
466
                        if (!($this->_keepMissingRelated[$lowerCaseAlias] ?? true)) {
467
                            // handle if we empty the related
468
                            if (empty($nodeIdListToKeep)) {
469
                                $nodeIdListToKeep = [0];
470
                            }
471
                            else {
472
                                $nodeIdListToKeep = array_values(array_filter(array_unique($nodeIdListToKeep)));
473
                            }
474
                            
475
                            $idBindType = count($intermediatePrimaryKeyAttributes) === 1 ? $intermediateBindTypes[$intermediatePrimaryKeyAttributes[0]] : Column::BIND_PARAM_STR;
476
                            
477
                            /** @var ModelInterface|string $intermediateModelClass */
478
                            $nodeEntityToDeleteList = $intermediateModelClass::find([
479
                                'conditions' => Sprintf::implodeArrayMapSprintf(array_merge($intermediateFields), ' and ', '[' . $intermediateModelClass . '].[%s] = ?%s')
480
                                    . ' and concat(' . Sprintf::implodeArrayMapSprintf($intermediatePrimaryKeyAttributes, ', \'.\', ', '[' . $intermediateModelClass . '].[%s]') . ') not in ({id:array})',
481
                                'bind' => [...$originBind, 'id' => $nodeIdListToKeep],
482
                                'bindTypes' => [...array_fill(0, count($intermediateFields), Column::BIND_PARAM_STR), 'id' => $idBindType],
483
                            ]);
484
                            
485
                            // delete missing related
486
                            if (!$nodeEntityToDeleteList->delete()) {
487
                                
488
                                /**
489
                                 * Append messages with context
490
                                 */
491
                                $this->appendMessagesFromRecord($nodeEntityToDeleteList, $lowerCaseAlias);
492
                                
493
                                /**
494
                                 * Rollback the implicit transaction
495
                                 */
496
                                $connection->rollback($nesting);
497
                                
498
                                return false;
499
                            }
500
                        }
501
                    }
502
                    
503
                    $columns = $relation->getFields();
504
                    $referencedFields = $relation->getReferencedFields();
505
                    $columns = is_array($columns) ? $columns : [$columns];
506
                    $referencedFields = is_array($referencedFields) ? $referencedFields : [$referencedFields];
507
                    
508
                    /**
509
                     * Create an implicit array for has-many/has-one records
510
                     */
511
                    $relatedRecords = is_object($assign) ? [$assign] : $assign;
512
                    
513
                    foreach ($columns as $column) {
514
                        if (!property_exists($this, $column)) {
515
                            $connection->rollback($nesting);
516
                            throw new Exception("The column '" . $column . "' needs to be present in the model");
517
                        }
518
                    }
519
                    
520
                    
521
                    /**
522
                     * Get the value of the field from the current model
523
                     * Check if the relation is a has-many-to-many
524
                     */
525
                    $isThrough = (bool)$relation->isThrough();
526
                    
527
                    /**
528
                     * Get the rest of intermediate model info
529
                     */
530
                    if ($isThrough) {
531
                        $intermediateModelClass = $relation->getIntermediateModel();
532
                        $intermediateFields = $relation->getIntermediateFields();
533
                        $intermediateReferencedFields = $relation->getIntermediateReferencedFields();
534
                        $intermediateFields = is_array($intermediateFields) ? $intermediateFields : [$intermediateFields];
535
                        $intermediateReferencedFields = is_array($intermediateReferencedFields) ? $intermediateReferencedFields : [$intermediateReferencedFields];
536
                    }
537
                    
538
                    
539
                    foreach ($relatedRecords as $recordAfter) {
540
                        if (!$isThrough) {
541
                            foreach ($columns as $key => $column) {
542
                                $recordAfter->writeAttribute($referencedFields[$key], $this->$column);
543
                            }
544
                        }
545
                        
546
                        /**
547
                         * Save the record and get messages
548
                         */
549
                        if (!$recordAfter->save()) {
550
                            
551
                            /**
552
                             * Append messages with context
553
                             */
554
                            $this->appendMessagesFromRecord($recordAfter, $lowerCaseAlias);
555
                            
556
                            /**
557
                             * Rollback the implicit transaction
558
                             */
559
                            $connection->rollback($nesting);
560
                            
561
                            return false;
562
                        }
563
                        
564
                        if ($isThrough) {
565
                            
566
                            /**
567
                             * Create a new instance of the intermediate model
568
                             */
569
                            $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...
570
                            
571
                            /**
572
                             *  Has-one-through relations can only use one intermediate model.
573
                             *  If it already exist, it can be updated with the new referenced key.
574
                             */
575
                            if ($relation->getType() === Relation::HAS_ONE_THROUGH) {
576
                                $bind = [];
577
                                foreach ($columns as $column) {
578
                                    $bind[] = $this->$column;
579
                                }
580
                                
581
                                $existingIntermediateModel = $intermediateModelClass::findFirst([
582
                                    '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...
583
                                    'bind' => $bind,
584
                                    'bindTypes' => array_fill(0, count($bind), Column::BIND_PARAM_STR),
585
                                ]);
586
                                
587
                                if ($existingIntermediateModel) {
588
                                    $intermediateModel = $existingIntermediateModel;
589
                                }
590
                            }
591
                            
592
                            foreach ($columns as $key => $column) {
593
                                /**
594
                                 * Write value in the intermediate model
595
                                 */
596
                                $intermediateModel->writeAttribute($intermediateFields[$key], $this->$column);
597
                                
598
                                /**
599
                                 * Get the value from the referenced model
600
                                 */
601
                                $intermediateValue = $recordAfter->readAttribute($referencedFields[$key]);
602
                                
603
                                /**
604
                                 * Write the intermediate value in the intermediate model
605
                                 */
606
                                $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...
607
                            }
608
                            
609
                            
610
                            /**
611
                             * Save the record and get messages
612
                             */
613
                            if (!$intermediateModel->save()) {
614
                                
615
                                /**
616
                                 * Append messages with context
617
                                 */
618
                                $this->appendMessagesFromRecord($intermediateModel, $lowerCaseAlias);
619
                                
620
                                /**
621
                                 * Rollback the implicit transaction
622
                                 */
623
                                $connection->rollback($nesting);
624
                                
625
                                return false;
626
                            }
627
                        }
628
                    }
629
                }
630
                else {
631
                    if (is_array($assign)) {
632
                        $connection->rollback($nesting);
633
                        throw new Exception("There are no defined relations for the model '" . get_class($this) . "' using alias '" . $lowerCaseAlias . "'");
634
                    }
635
                }
636
            }
637
        }
638
        
639
        /**
640
         * Commit the implicit transaction
641
         */
642
        $connection->commit($nesting);
643
        
644
        return true;
645
646
//        // no commit here cuz parent::_postSaveRelatedRecords will fire it
647
//        return [true, $connection, array_filter($related ?? [])];
648
    }
649
    
650
    /**
651
     * Get an entity from data
652
     *
653
     * @param array $data
654
     * @param array $fields
655
     * @param string $modelClass
656
     * @param array|null $readFields
657
     * @param int|null $type
658
     * @param array|null $whiteList
659
     * @param array|null $dataColumnMap
660
     *
661
     * @return ModelInterface
662
     * @todo unit test for this
663
     *
664
     * @return ModelInterface
665
     */
666
    public function _getEntityFromData(array $data, array $fields, string $modelClass, ?array $readFields = [], int $type = null, ?array $whiteList = null, ?array $dataColumnMap = null): ModelInterface
667
    {
668
        $entity = null;
669
        
670
        if ($type === Relation::HAS_ONE || $type === Relation::BELONGS_TO) {
671
            
672
            // Set value to compare
673
            if (!empty($readFields)) {
674
                foreach ($readFields as $key => $field) {
675
                    if (empty($data[$fields[$key]])) { // @todo maybe remove this if
676
                        $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

676
                        /** @scrutinizer ignore-call */ 
677
                        $value = $this->readAttribute($field);
Loading history...
677
                        if (!empty($value)) { // @todo maybe remove this if
678
                            $data [$fields[$key]] = $value;
679
                        }
680
                    }
681
                }
682
            }
683
            
0 ignored issues
show
Coding Style introduced by
Blank line found at end of control structure
Loading history...
684
        }
685
        
686
        // array_keys_exists (if $referencedFields keys exists)
687
        $dataKeys = array_intersect_key($data, array_flip($fields));
688
        
689
        // all keys were found
690
        if (count($dataKeys) === count($fields)) {
691
            
692
            if ($type === Relation::HAS_MANY) {
693
    
694
                /** @var MetaData $modelMetaData */
695
                $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

695
                /** @scrutinizer ignore-call */ 
696
                $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...
696
                $primaryKeys = $modelsMetaData->getPrimaryKeyAttributes($this);
697
                
698
                // Force primary keys for single to many
699
                foreach ($primaryKeys as $primaryKey) {
700
                    if (isset($data[$primaryKey]) && !in_array($primaryKey, $fields, true)) {
701
                        $dataKeys [$primaryKey] = $data[$primaryKey];
702
                        $fields []= $primaryKey;
703
                    }
704
                }
705
            }
706
            
707
            /** @var ModelInterface $entity */
708
            /** @var ModelInterface|string $modelClass */
709
            $entity = $modelClass::findFirst([
710
                '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

710
                'conditions' => Sprintf::implodeArrayMapSprintf($fields, ' and ', '[' . /** @scrutinizer ignore-type */ $modelClass . '].[%s] = ?%s'),
Loading history...
711
                'bind' => array_values($dataKeys),
712
                'bindTypes' => array_fill(0, count($dataKeys), Column::BIND_PARAM_STR),
713
            ]);
714
        }
715
        
716
        if (!$entity) {
717
            /** @var ModelInterface $entity */
718
            $entity = new $modelClass();
719
        }
720
        
721
        // assign new values
722
        if ($entity) {
723
            $entity->assign($data, $whiteList[$modelClass] ?? null, $dataColumnMap[$modelClass] ?? null);
724
        }
725
        
726
        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...
727
    }
728
    
729
    /**
730
     * Append a message to this model from another record,
731
     * also prepend a context to the previous context
732
     *
733
     * @param ResultsetInterface|ModelInterface $record
734
     * @param string|null $context
735
     */
736
    public function appendMessagesFromRecord($record, string $context = null)
737
    {
738
        /**
739
         * Get the validation messages generated by the
740
         * referenced model
741
         */
742
        foreach ($record->getMessages() as $message) {
743
            
744
            /**
745
             * Set the related model
746
             */
747
            $message->setMetaData([
748
//                'model' => $record,
749
                '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

749
                'context' => $this->rebuildMessageContext($message, /** @scrutinizer ignore-type */ $context),
Loading history...
750
            ]);
751
            
752
            /**
753
             * Appends the messages to the current model
754
             */
755
            $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

755
            $this->/** @scrutinizer ignore-call */ 
756
                   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...
756
        }
757
    }
758
    
759
    /**
760
     * Rebuilding context for meta data
761
     *
762
     * @param Message $message
763
     * @param string $context
764
     *
765
     * @return string
766
     */
767
    public function rebuildMessageContext(Message $message, string $context)
768
    {
769
        $metaData = $message->getMetaData();
770
        $previousContext = $metaData['context'] ?? null;
771
        
772
        return $context . (empty($previousContext) ? null : '.' . $previousContext);
773
    }
774
}
775