Test Failed
Push — master ( ce60e5...378563 )
by Julien
12:41 queued 07:49
created

Relationship::getDirtyRelated()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 3
Code Lines 1

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 0
CRAP Score 2

Importance

Changes 1
Bugs 0 Features 0
Metric Value
cc 1
eloc 1
c 1
b 0
f 0
nc 1
nop 0
dl 0
loc 3
ccs 0
cts 2
cp 0
crap 2
rs 10
1
<?php
2
3
/**
4
 * This file is part of the Zemit Framework.
5
 *
6
 * (c) Zemit Team <[email protected]>
7
 *
8
 * For the full copyright and license information, please view the LICENSE.txt
9
 * file that was distributed with this source code.
10
 */
11
12
namespace Zemit\Mvc\Model;
13
14
use Exception;
15
use Phalcon\Db\Adapter\AdapterInterface;
16
use Phalcon\Db\Column;
17
use Phalcon\Messages\Message;
18
use Phalcon\Mvc\Model;
19
use Phalcon\Mvc\Model\Relation;
20
use Phalcon\Mvc\Model\RelationInterface;
21
use Phalcon\Mvc\Model\ResultsetInterface;
22
use Phalcon\Mvc\ModelInterface;
23
use Phalcon\Support\Collection\CollectionInterface;
24
use Zemit\Mvc\Model\AbstractTrait\AbstractEntity;
25
use Zemit\Mvc\Model\AbstractTrait\AbstractMetaData;
26
use Zemit\Mvc\Model\AbstractTrait\AbstractModelsManager;
27
28
/**
29
 * Allow to automagically save relationship
30
 */
31
trait Relationship
32
{
33
    use AbstractEntity;
34
    use AbstractMetaData;
35
    use AbstractModelsManager;
36
    
37
    private array $keepMissingRelated = [];
38
    
39
    private string $relationshipContext = '';
40
    
41
    protected $dirtyRelated;
42
    
43
    /**
44
     * Set the missing related configuration list
45
     */
46
    public function setKeepMissingRelated(array $keepMissingRelated): void
47
    {
48
        $this->keepMissingRelated = $keepMissingRelated;
49
    }
50
    
51
    /**
52
     * Return the missing related configuration list
53
     */
54
    public function getKeepMissingRelated(): array
55
    {
56
        return $this->keepMissingRelated;
57
    }
58
    
59
    /**
60
     * Return the keepMissing configuration for a specific relationship alias
61
     */
62
    public function getKeepMissingRelatedAlias(string $alias): bool
63
    {
64
        return (bool)$this->keepMissingRelated[$alias];
65
    }
66
    
67
    /**
68
     * Set the keepMissing configuration for a specific relationship alias
69
     */
70
    public function setKeepMissingRelatedAlias(string $alias, bool $keepMissing): void
71
    {
72
        $this->keepMissingRelated[$alias] = $keepMissing;
73
    }
74
    
75
    /**
76
     * Get the current relationship context
77
     */
78
    public function getRelationshipContext(): string
79
    {
80
        return $this->relationshipContext;
81
    }
82
    
83
    /**
84
     * Set the current relationship context
85
     */
86
    public function setRelationshipContext(string $context): void
87
    {
88
        $this->relationshipContext = $context;
89
    }
90
    
91
    /**
92
     * Return the dirtyRelated entities
93
     */
94
    public function getDirtyRelated(): ?array
95
    {
96
        return $this->dirtyRelated;
97
    }
98
    
99
    /**
100
     * Set the dirtyRelated entities
101
     */
102
    public function setDirtyRelated(?array $dirtyRelated = null): void
103
    {
104
        $this->dirtyRelated = $dirtyRelated;
105
    }
106
    
107
    /**
108
     * Return the dirtyRelated entities
109
     */
110
    public function getDirtyRelatedAlias(string $alias): mixed
111
    {
112
        return $this->dirtyRelated[$alias];
113
    }
114
    
115
    /**
116
     * Return the dirtyRelated entities
117
     */
118
    public function setDirtyRelatedAlias(string $alias, mixed $value): void
119
    {
120
        $this->dirtyRelated[$alias] = $value;
121
    }
122
    
123
    /**
124
     * Check whether the current entity has dirty related or not
125
     */
126
    public function hasDirtyRelated(): bool
127
    {
128
        return (bool)count($this->dirtyRelated);
129
    }
130
    
131
    /**
132
     * Check whether the current entity has dirty related or not
133
     */
134
    public function hasDirtyRelatedAlias(string $alias): bool
135
    {
136
        return isset($this->dirtyRelated[$alias]);
137
    }
138
    
139
    /**
140
     * {@inheritDoc}
141
     * @throws Exception
142
     */
143 1
    public function assign(array $data, $whiteList = null, $dataColumnMap = null): ModelInterface
144
    {
145 1
        $this->assignRelated($data, $whiteList, $dataColumnMap);
146 1
        return parent::assign($data, $whiteList, $dataColumnMap);
147
    }
148
    
149
    /**
150
     * Assign related
151
     *
152
     * Single
153
     * [alias => new Alias()] // create new alias
154
     *
155
     * Many
156
     * [alias => [new Alias()]] // create new alias
157
     * [alias => [1, 2, 3, 4]] // append / merge 1, 2, 3, 4
158
     * [alias => [false, 1, 2, 4]]; // delete 3
159
     *
160
     * @param array $data
161
     * @param array|null $whiteList
162
     * @param array|null $dataColumnMap
163
     *
164
     * @return ModelInterface
165
     * @throws Exception
166
     */
167 1
    public function assignRelated(array $data, ?array $whiteList = null, ?array $dataColumnMap = null): ModelInterface
168
    {
169 1
        assert($this instanceof Model);
170
        
171
        // no data, nothing to do
172 1
        if (empty($data)) {
173
            return $this;
174
        }
175
        
176
        // Get the current model class name
177 1
        $modelClass = get_class($this);
178
        
179 1
        $modelsManager = $this->getModelsManager();
180
        
181 1
        foreach ($data as $alias => $relationData) {
182
            
183 1
            $relation = $modelsManager->getRelationByAlias($modelClass, $alias);
184
            
185
            // alias is not whitelisted
186 1
            if (!is_null($whiteList) && (!isset($whiteList[$alias]) && !in_array($alias, $whiteList))) {
187
                continue;
188
            }
189
            
190
            // @todo add a recursive whiteList check & columnMap support
191 1
            if ($relation) {
192
                $type = $relation->getType();
193
                
194
                $fields = $relation->getFields();
195
                $fields = is_array($fields) ? $fields : [$fields];
196
                
197
                $referencedFields = $relation->getReferencedFields();
198
                $referencedFields = is_array($referencedFields) ? $referencedFields : [$referencedFields];
199
                
200
                $referencedModel = $relation->getReferencedModel();
201
                $assign = null;
202
                
203
                if (is_int($relationData) || is_string($relationData)) {
204
                    $relationData = [$referencedFields[0] => $relationData];
205
                }
206
                
207
                if ($relationData instanceof ModelInterface) {
208
                    if ($relationData instanceof $referencedModel) {
209
                        $assign = $relationData;
210
                    }
211
                    else {
212
                        throw new Exception('Instance of `' . get_class($relationData) . '` received on model `' . $modelClass . '` in alias `' . $alias . ', expected instance of `' . $referencedModel . '`', 400);
213
                    }
214
                }
215
                
216
                // array | traversable | resultset
217
                elseif (is_array($relationData) || $relationData instanceof \Traversable) {
218
                    $assign = [];
219
                    
220
                    $getEntityParams = [
221
                        'alias' => $alias,
222
                        'fields' => $referencedFields,
223
                        'modelClass' => $referencedModel,
224
                        'readFields' => $fields,
225
                        'type' => $type,
226
                        'whiteList' => $whiteList,
227
                        'dataColumnMap' => $dataColumnMap,
228
                    ];
229
                    
230
                    if (empty($relationData) && !in_array($type, [Relation::HAS_MANY_THROUGH, Relation::HAS_MANY])) {
231
                        $assign = $this->getEntityFromData($relationData, $getEntityParams);
232
                    }
233
                    else {
234
                        foreach ($relationData as $traversedKey => $traversedData) {
235
                            // Array of things
236
                            if (is_int($traversedKey)) {
237
                                $entity = null;
238
                                
239
                                // Using bool as behaviour to delete missing relationship or keep them
240
                                // @TODO find a better way
241
                                // if [alias => [true, ...]
242
                                if ($traversedData === 'false') {
243
                                    $traversedData = false;
244
                                }
245
                                if ($traversedData === 'true') {
246
                                    $traversedData = true;
247
                                }
248
                                
249
                                if (is_bool($traversedData)) {
250
                                    $this->setKeepMissingRelatedAlias($alias, $traversedData);
251
                                    continue;
252
                                }
253
                                
254
                                // if [alias => [1, 2, 3, ...]]
255
                                if (is_int($traversedData) || is_string($traversedData)) {
256
                                    $traversedData = [$referencedFields[0] => $traversedData];
257
                                }
258
                                
259
                                // if [alias => AliasModel]
260
                                if ($traversedData instanceof ModelInterface) {
261
                                    if ($traversedData instanceof $referencedModel) {
262
                                        $entity = $traversedData;
263
                                    }
264
                                    else {
265
                                        throw new Exception('Instance of `' . get_class($traversedData) . '` received on model `' . $modelClass . '` in alias `' . $alias . ', expected instance of `' . $referencedModel . '`', 400);
266
                                    }
267
                                }
268
                                
269
                                // if [alias => [[id => 1], [id => 2], [id => 3], ....]]
270
                                elseif (is_array($traversedData) || $traversedData instanceof \Traversable) {
271
                                    $entity = $this->getEntityFromData((array)$traversedData, $getEntityParams);
272
                                }
273
                                
274
                                if ($entity) {
275
                                    $assign [] = $entity;
276
                                }
277
                            }
278
                            
279
                            // if [alias => [id => 1]]
280
                            else {
281
                                $assign = $this->getEntityFromData((array)$relationData, $getEntityParams);
282
                                break;
283
                            }
284
                        }
285
                    }
286
                }
287
                
288
                // we got something to assign
289
                $keepMissingRelationship = $this->keepMissingRelated[$alias] ?? null;
290
                if (!empty($assign) || $keepMissingRelationship === false) {
291
                    
292
                    $assign = is_array($assign) ? array_values(array_filter($assign)) : $assign;
293
                    $this->{$alias} = $assign;
294
                    
295
                    // fix to force recursive parent save from children entities within _preSaveRelatedRecords method
296
                    if ($this->{$alias} && $this->{$alias} instanceof ModelInterface) {
297
                        $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...
298
                    }
299
                    
300
                    $this->dirtyRelated[mb_strtolower($alias)] = $this->{$alias} ?? false;
301
                    if (empty($assign)) {
302
                        $this->dirtyRelated[mb_strtolower($alias)] = [];
303
                    }
304
                }
305
            }
306
        }
307
        
308 1
        return $this;
309
    }
310
    
311
    /**
312
     * Saves related records that must be stored prior to save the master record
313
     * Refactored based on the native cphalcon version, so we can support :
314
     * - combined keys on relationship definition
315
     * - relationship context within the model messages based on the alias definition
316
     * @throws Exception
317
     */
318
    protected function preSaveRelatedRecords(AdapterInterface $connection, $related, CollectionInterface $visited): bool
319
    {
320
        $nesting = false;
321
        
322
        $connection->begin($nesting);
323
        $className = get_class($this);
324
        
325
        $modelsManager = $this->getModelsManager();
326
        
327
        foreach ($related as $alias => $record) {
328
            $relation = $modelsManager->getRelationByAlias($className, $alias);
329
            
330
            if ($relation) {
331
                $type = $relation->getType();
332
                
333
                // Only belongsTo are stored before save the master record
334
                if ($type === Relation::BELONGS_TO) {
335
                    
336
                    // Belongs-to relation: We only support model interface
337
                    if (!($record instanceof ModelInterface)) {
338
                        $connection->rollback($nesting);
339
                        throw new Exception(
340
                            'Instance of `' . get_class($record) . '` received on model `' . $className . '` in alias `' . $alias .
341
                            ', expected instance of `' . ModelInterface::class . '` as part of the belongs-to relation',
342
                            400
343
                        );
344
                    }
345
                    
346
                    $relationFields = $relation->getFields();
347
                    $relationFields = is_array($relationFields) ? $relationFields : [$relationFields];
348
                    
349
                    $referencedFields = $relation->getReferencedFields();
350
                    $referencedFields = is_array($referencedFields) ? $referencedFields : [$referencedFields];
351
                    
352
                    // Set the relationship context
353
                    if ($record instanceof RelationshipInterface) {
354
                        $currentRelationshipContext = $this->getRelationshipContext();
355
                        $relationshipPrefix = !empty($currentRelationshipContext)? $currentRelationshipContext . '.' : '';
356
                        $record->setRelationshipContext($relationshipPrefix . $alias);
357
                    }
358
                    
359
                    /**
360
                     * If dynamic update is enabled, saving the record must not take any action
361
                     * Only save if the model is dirty to prevent circular relations causing an infinite loop
362
                     */
363
                    assert($record instanceof Model);
364
                    if ($record->getDirtyState() !== Model::DIRTY_STATE_PERSISTENT && !$record->doSave($visited)) {
365
                        $this->appendMessagesFromRecord($record, $alias);
366
                        $connection->rollback($nesting);
367
                        return false;
368
                    }
369
                    
370
                    // assign referenced value to the current model
371
                    foreach ($referencedFields as $key => $referencedField) {
372
                        $this->{$relationFields[$key]} = $record->readAttribute($referencedField);
373
                    }
374
                }
375
            }
376
        }
377
        
378
        return true;
379
    }
380
    
381
    /**
382
     * NOTE: we need this, this behaviour only happens:
383
     * - in many to many nodes
384
     * Fix uniqueness on combined keys in node entities, and possibly more...
385
     * @link https://forum.phalconphp.com/discussion/2190/many-to-many-expected-behaviour
386
     * @link http://stackoverflow.com/questions/23374858/update-a-records-n-n-relationships
387
     * @link https://github.com/phalcon/cphalcon/issues/2871
388
     */
389
    protected function postSaveRelatedRecords(AdapterInterface $connection, $related, CollectionInterface $visited): bool
390
    {
391
        $nesting = false;
392
        
393
        if ($related) {
394
            foreach ($related as $lowerCaseAlias => $assign) {
395
                
396
                $modelsManager = $this->getModelsManager();
397
                $relation = $modelsManager->getRelationByAlias(get_class($this), $lowerCaseAlias);
398
                
399
                if (!$relation) {
400
                    if (is_array($assign)) {
401
                        $connection->rollback($nesting);
402
                        throw new Exception("There are no defined relations for the model '" . get_class($this) . "' using alias '" . $lowerCaseAlias . "'");
403
                    }
404
                }
405
                
406
                /**
407
                 * Discard belongsTo relations
408
                 */
409
                if ($relation->getType() === Relation::BELONGS_TO) {
410
                    continue;
411
                }
412
                
413
                if (!is_array($assign) && !is_object($assign)) {
414
                    $connection->rollback($nesting);
415
                    throw new Exception('Only objects/arrays can be stored as part of has-many/has-one/has-one-through/has-many-to-many relations');
416
                }
417
                
418
                /**
419
                 * Custom logic for single-to-many relationships
420
                 */
421
                if ($relation->getType() === Relation::HAS_MANY) {
422
                    
423
                    // auto-delete missing related if keepMissingRelated is false
424
                    if (!($this->keepMissingRelated[$lowerCaseAlias] ?? true)) {
425
                        $originFields = $relation->getFields();
426
                        $originFields = is_array($originFields) ? $originFields : [$originFields];
427
                        
428
                        $referencedFields = $relation->getReferencedFields();
429
                        $referencedFields = is_array($referencedFields) ? $referencedFields : [$referencedFields];
430
                        
431
                        $referencedModelClass = $relation->getReferencedModel();
432
                        $referencedModel = $modelsManager->load($referencedModelClass);
433
                        
434
                        $referencedPrimaryKeyAttributes = $referencedModel->getModelsMetaData()->getPrimaryKeyAttributes($referencedModel);
435
                        $referencedBindTypes = $referencedModel->getModelsMetaData()->getBindTypes($referencedModel);
436
                        
437
                        $originBind = [];
438
                        foreach ($originFields as $originField) {
439
                            $originBind [] = $this->readAttribute($originField);
440
                        }
441
                    
442
                        $idBindType = count($referencedPrimaryKeyAttributes) === 1 ? $referencedBindTypes[$referencedPrimaryKeyAttributes[0]] : Column::BIND_PARAM_STR;
443
                        
444
                        $idListToKeep = [0];
445
                        foreach ($assign as $entity) {
446
                            $buildPrimaryKey = [];
447
                            foreach ($referencedPrimaryKeyAttributes as $referencedPrimaryKey => $referencedPrimaryKeyAttribute) {
448
                                $buildPrimaryKey [] = $entity->readAttribute($referencedPrimaryKeyAttribute);
449
                            }
450
                            $idListToKeep [] = implode('.', $buildPrimaryKey);
451
                        }
452
                        
453
                        // fetch missing related entities
454
                        $referencedEntityToDeleteResultset = $referencedModel::find([
455
                            'conditions' => implode_sprintf(array_merge($referencedFields), ' and ', '[' . $referencedModelClass . '].[%s] = ?%s') .
456
                            ' and concat(' . implode_sprintf($referencedPrimaryKeyAttributes, ', \'.\', ', '[' . $referencedModelClass . '].[%s]') . ') not in ({id:array})',
457
                            'bind' => [...$originBind, 'id' => $idListToKeep],
458
                            'bindTypes' => [...array_fill(0, count($referencedFields), Column::BIND_PARAM_STR), 'id' => $idBindType],
459
                        ]);
460
                        
461
                        // delete missing related entities
462
                        if (!$referencedEntityToDeleteResultset->delete()) {
463
                            $this->appendMessagesFromResultset($referencedEntityToDeleteResultset, $lowerCaseAlias);
464
                            $this->appendMessage(new Message('Unable to delete node entity `' . $referencedModelClass . '`', $lowerCaseAlias, 'Bad Request', 400));
0 ignored issues
show
Bug introduced by
The method appendMessage() does not exist on Zemit\Mvc\Model\Relationship. Did you maybe mean appendMessagesFromResultset()? ( Ignorable by Annotation )

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

464
                            $this->/** @scrutinizer ignore-call */ 
465
                                   appendMessage(new Message('Unable to delete node entity `' . $referencedModelClass . '`', $lowerCaseAlias, 'Bad Request', 400));

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...
465
                            $connection->rollback($nesting);
466
                            return false;
467
                        }
468
                    }
469
                }
470
                
471
                /**
472
                 * Custom logic for many-to-many relationships
473
                 */
474
                elseif ($relation->getType() === Relation::HAS_MANY_THROUGH) {
475
                    $originFields = $relation->getFields();
476
                    $originFields = is_array($originFields) ? $originFields : [$originFields];
477
                    
478
                    $intermediateModelClass = $relation->getIntermediateModel();
479
                    $intermediateModel = $modelsManager->load($intermediateModelClass);
480
                    
481
                    $intermediateFields = $relation->getIntermediateFields();
482
                    $intermediateFields = is_array($intermediateFields) ? $intermediateFields : [$intermediateFields];
483
                    
484
                    $intermediateReferencedFields = $relation->getIntermediateReferencedFields();
485
                    $intermediateReferencedFields = is_array($intermediateReferencedFields) ? $intermediateReferencedFields : [$intermediateReferencedFields];
486
                    
487
                    $referencedFields = $relation->getReferencedFields();
488
                    $referencedFields = is_array($referencedFields) ? $referencedFields : [$referencedFields];
489
                    
490
                    $intermediatePrimaryKeyAttributes = $intermediateModel->getModelsMetaData()->getPrimaryKeyAttributes($intermediateModel);
491
                    $intermediateBindTypes = $intermediateModel->getModelsMetaData()->getBindTypes($intermediateModel);
492
                    
493
                    // get current model bindings
494
                    $originBind = [];
495
                    foreach ($originFields as $originField) {
496
                        $originBind [] = $this->readAttribute($originField);
497
//                        $originBind [] = $this->{'get' . ucfirst($originField)} ?? $this->$originField ?? null;
498
                    }
499
                    
500
                    $nodeIdListToKeep = [];
501
                    foreach ($assign as $key => $entity) {
502
                        assert($entity instanceof ModelInterface);
503
                        
504
                        // get referenced model bindings
505
                        $referencedBind = [];
506
                        foreach ($referencedFields as $referencedField) {
507
                            $referencedBind [] = $entity->readAttribute($referencedField);
0 ignored issues
show
Bug introduced by
The method readAttribute() does not exist on Phalcon\Mvc\ModelInterface. It seems like you code against a sub-type of said class. However, the method does not exist in Zemit\Models\Interfaces\AbstractInterface or Zemit\Models\Interfaces\SettingInterface or Zemit\Models\Interfaces\CategoryInterface or Zemit\Models\Interfaces\FeatureInterface or Zemit\Models\Interfaces\AuditInterface or Zemit\Models\Interfaces\UserGroupInterface or Zemit\Models\Interfaces\UserInterface or Zemit\Models\Interfaces\FieldInterface or Zemit\Models\Interfaces\PageInterface or Zemit\Models\Interfaces\GroupFeatureInterface or Zemit\Models\Interfaces\RoleRoleInterface or Zemit\Models\Interfaces\LogInterface or Zemit\Models\Interfaces\FileInterface or Zemit\Models\Interfaces\RoleInterface or Zemit\Models\Interfaces\GroupRoleInterface or Zemit\Models\Interfaces\TemplateInterface or Zemit\Models\Interfaces\AuditDetailInterface or Zemit\Models\Interfaces\UserTypeInterface or Zemit\Models\Interfaces\PhalconMigrationsInterface or Zemit\Models\Interfaces\PostInterface or Zemit\Models\Interfaces\PostCategoryInterface or Zemit\Models\Interfaces\SessionInterface or Zemit\Models\Interfaces\TranslateFieldInterface or Zemit\Models\Interfaces\ProfileInterface or Zemit\Models\Interfaces\UserFeatureInterface or Zemit\Models\Interfaces\TableInterface or Zemit\Models\Interfaces\TranslateInterface or Zemit\Models\Interfaces\WorkspaceLangInterface or Zemit\Models\Interfaces\EmailInterface or Zemit\Models\Interfaces\WorkspaceInterface or Zemit\Models\Interfaces\DataInterface or Zemit\Models\Interfaces\GroupInterface or Zemit\Models\Interfaces\LangInterface or Zemit\Models\Interfaces\EmailFileInterface or Zemit\Models\Interfaces\TranslateTableInterface or Zemit\Models\Interfaces\RoleFeatureInterface or Zemit\Models\Interfaces\UserRoleInterface or Zemit\Models\Interfaces\FlagInterface or Zemit\Models\Interfaces\MenuInterface or Zemit\Models\Interfaces\GroupTypeInterface or Zemit\Models\Interfaces\BackupInterface or Zemit\Models\Interfaces\JobInterface or Zemit\Models\Interfaces\TypeInterface or Zemit\Models\Interfaces\MetaInterface. Are you sure you never get one of those? ( Ignorable by Annotation )

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

507
                            /** @scrutinizer ignore-call */ 
508
                            $referencedBind [] = $entity->readAttribute($referencedField);
Loading history...
508
                        }
509
                        
510
                        $nodeEntity = $intermediateModel::findFirst([
511
                            'conditions' => implode_sprintf(array_merge($intermediateFields, $intermediateReferencedFields), ' and ', '[' . $intermediateModelClass . '].[%s] = ?%s'),
512
                            'bind' => [...$originBind, ...$referencedBind],
513
                            'bindTypes' => array_fill(0, count($intermediateFields) + count($intermediateReferencedFields), Column::BIND_PARAM_STR),
514
                        ]);
515
                        
516
                        if ($nodeEntity) {
517
                            $buildPrimaryKey = [];
518
                            foreach ($intermediatePrimaryKeyAttributes as $intermediatePrimaryKey => $intermediatePrimaryKeyAttribute) {
519
                                $buildPrimaryKey [] = $nodeEntity->readAttribute($intermediatePrimaryKeyAttribute);
520
                            }
521
                            $nodeIdListToKeep [] = implode('.', $buildPrimaryKey);
522
                            
523
                            // Restoring node entities if previously soft deleted
524
                            if (method_exists($nodeEntity, 'restore') && method_exists($nodeEntity, 'isDeleted')) {
525
                                if ($nodeEntity->isDeleted() && !$nodeEntity->restore()) {
526
                                    $this->appendMessagesFromRecord($nodeEntity, $lowerCaseAlias, $key);
527
                                    $this->appendMessage(new Message('Unable to restored previously deleted related node `' . $intermediateModelClass . '`', $lowerCaseAlias, 'Bad Request', 400));
528
                                    $connection->rollback($nesting);
529
                                    return false;
530
                                }
531
                            }
532
                            
533
                            // save edge record
534
                            if (!$entity->doSave($visited)) {
0 ignored issues
show
Bug introduced by
The method doSave() does not exist on Phalcon\Mvc\ModelInterface. It seems like you code against a sub-type of said class. However, the method does not exist in Zemit\Models\Interfaces\AbstractInterface or Zemit\Models\Interfaces\SettingInterface or Zemit\Models\Interfaces\CategoryInterface or Zemit\Models\Interfaces\FeatureInterface or Zemit\Models\Interfaces\AuditInterface or Zemit\Models\Interfaces\UserGroupInterface or Zemit\Models\Interfaces\UserInterface or Zemit\Models\Interfaces\FieldInterface or Zemit\Models\Interfaces\PageInterface or Zemit\Models\Interfaces\GroupFeatureInterface or Zemit\Models\Interfaces\RoleRoleInterface or Zemit\Models\Interfaces\LogInterface or Zemit\Models\Interfaces\FileInterface or Zemit\Models\Interfaces\RoleInterface or Zemit\Models\Interfaces\GroupRoleInterface or Zemit\Models\Interfaces\TemplateInterface or Zemit\Models\Interfaces\AuditDetailInterface or Zemit\Models\Interfaces\UserTypeInterface or Zemit\Models\Interfaces\PhalconMigrationsInterface or Zemit\Models\Interfaces\PostInterface or Zemit\Models\Interfaces\PostCategoryInterface or Zemit\Models\Interfaces\SessionInterface or Zemit\Models\Interfaces\TranslateFieldInterface or Zemit\Models\Interfaces\ProfileInterface or Zemit\Models\Interfaces\UserFeatureInterface or Zemit\Models\Interfaces\TableInterface or Zemit\Models\Interfaces\TranslateInterface or Zemit\Models\Interfaces\WorkspaceLangInterface or Zemit\Models\Interfaces\EmailInterface or Zemit\Models\Interfaces\WorkspaceInterface or Zemit\Models\Interfaces\DataInterface or Zemit\Models\Interfaces\GroupInterface or Zemit\Models\Interfaces\LangInterface or Zemit\Models\Interfaces\EmailFileInterface or Zemit\Models\Interfaces\TranslateTableInterface or Zemit\Models\Interfaces\RoleFeatureInterface or Zemit\Models\Interfaces\UserRoleInterface or Zemit\Models\Interfaces\FlagInterface or Zemit\Models\Interfaces\MenuInterface or Zemit\Models\Interfaces\GroupTypeInterface or Zemit\Models\Interfaces\BackupInterface or Zemit\Models\Interfaces\JobInterface or Zemit\Models\Interfaces\TypeInterface or Zemit\Models\Interfaces\MetaInterface. Are you sure you never get one of those? ( Ignorable by Annotation )

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

534
                            if (!$entity->/** @scrutinizer ignore-call */ doSave($visited)) {
Loading history...
535
                                $this->appendMessagesFromRecord($entity, $lowerCaseAlias, $key);
536
                                $this->appendMessage(new Message('Unable to save related entity `' . $intermediateModelClass . '`', $lowerCaseAlias, 'Bad Request', 400));
537
                                $connection->rollback($nesting);
538
                                return false;
539
                            }
540
                            
541
                            // remove it
542
                            unset($assign[$key]);
543
                            unset($related[$lowerCaseAlias][$key]);
544
545
//                            // add to assign
546
//                            $nodeAssign [] = $nodeEntity;
547
                        }
548
                    }
549
                    
550
                    if (!($this->keepMissingRelated[$lowerCaseAlias] ?? true)) {
551
                        $idBindType = count($intermediatePrimaryKeyAttributes) === 1 ? $intermediateBindTypes[$intermediatePrimaryKeyAttributes[0]] : Column::BIND_PARAM_STR;
552
                        $nodeIdListToKeep = empty($nodeIdListToKeep)? [0] : array_keys(array_flip($nodeIdListToKeep));
553
                        $nodeEntityToDeleteResultset = $intermediateModel::find([
554
                            'conditions' => implode_sprintf(array_merge($intermediateFields), ' and ', '[' . $intermediateModelClass . '].[%s] = ?%s')
555
                                . ' and concat(' . implode_sprintf($intermediatePrimaryKeyAttributes, ', \'.\', ', '[' . $intermediateModelClass . '].[%s]') . ') not in ({id:array})',
556
                            'bind' => [...$originBind, 'id' => $nodeIdListToKeep],
557
                            'bindTypes' => [...array_fill(0, count($intermediateFields), Column::BIND_PARAM_STR), 'id' => $idBindType],
558
                        ]);
559
                        
560
                        // delete missing related
561
                        if (!$nodeEntityToDeleteResultset->delete()) {
562
                            $this->appendMessagesFromResultset($nodeEntityToDeleteResultset, $lowerCaseAlias);
563
                            $this->appendMessage(new Message('Unable to delete node entity `' . $intermediateModelClass . '`', $lowerCaseAlias, 'Bad Request', 400));
564
                            $connection->rollback($nesting);
565
                            return false;
566
                        }
567
                    }
568
                }
569
                
570
                $relationFields = $relation->getFields();
571
                $relationFields = is_array($relationFields) ? $relationFields : [$relationFields];
572
                
573
                foreach ($relationFields as $relationField) {
574
                    if (!property_exists($this, $relationField)) {
575
                        $connection->rollback($nesting);
576
                        throw new Exception("The column '" . $relationField . "' needs to be present in the model");
577
                    }
578
                }
579
                
580
                $relatedRecords = $assign instanceof ModelInterface ? [$assign] : $assign;
581
                
582
                if ($this->postSaveRelatedThroughAfter($relation, $relatedRecords, $visited) === false) {
0 ignored issues
show
Bug introduced by
It seems like $relation can also be of type true; however, parameter $relation of Zemit\Mvc\Model\Relation...veRelatedThroughAfter() does only seem to accept Phalcon\Mvc\Model\RelationInterface, 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

582
                if ($this->postSaveRelatedThroughAfter(/** @scrutinizer ignore-type */ $relation, $relatedRecords, $visited) === false) {
Loading history...
583
                    $this->appendMessage(new Message('Unable to save related through after', $lowerCaseAlias, 'Bad Request', 400));
584
                    $connection->rollback($nesting);
585
                    return false;
586
                }
587
                
588
                if ($this->postSaveRelatedRecordsAfter($relation, $relatedRecords, $visited) === false) {
0 ignored issues
show
Bug introduced by
It seems like $relation can also be of type true; however, parameter $relation of Zemit\Mvc\Model\Relation...veRelatedRecordsAfter() does only seem to accept Phalcon\Mvc\Model\RelationInterface, 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

588
                if ($this->postSaveRelatedRecordsAfter(/** @scrutinizer ignore-type */ $relation, $relatedRecords, $visited) === false) {
Loading history...
589
                    $this->appendMessage(new Message('Unable to save related records after', $lowerCaseAlias, 'Bad Request', 400));
590
                    $connection->rollback($nesting);
591
                    return false;
592
                }
593
            }
594
        }
595
        
596
        /**
597
         * Commit the implicit transaction
598
         */
599
        $connection->commit($nesting);
600
        return true;
601
    }
602
    
603
    public function postSaveRelatedRecordsAfter(RelationInterface $relation, $relatedRecords, CollectionInterface $visited): ?bool
604
    {
605
        if ($relation->isThrough()) {
606
            return null;
607
        }
608
        
609
        $lowerCaseAlias = $relation->getOption('alias');
610
        
611
        $relationFields = $relation->getFields();
612
        $relationFields = is_array($relationFields) ? $relationFields : [$relationFields];
613
        
614
        $referencedFields = $relation->getReferencedFields();
615
        $referencedFields = is_array($referencedFields) ? $referencedFields : [$referencedFields];
616
        
617
        foreach ($relatedRecords as $recordAfter) {
618
//            $recordAfter->assign($relationFields);
619
            foreach ($relationFields as $key => $relationField) {
620
                $recordAfter->writeAttribute($referencedFields[$key], $this->readAttribute($relationField));
621
            }
622
            
623
            // Save the record and get messages
624
            if (!$recordAfter->doSave($visited)) {
625
                $this->appendMessagesFromRecord($recordAfter, $lowerCaseAlias);
626
                return false;
627
            }
628
        }
629
        
630
        return true;
631
    }
632
    
633
    public function postSaveRelatedThroughAfter(RelationInterface $relation, $relatedRecords, CollectionInterface $visited): ?bool
634
    {
635
        if (!$relation->isThrough()) {
636
            return null;
637
        }
638
        
639
        $modelsManager = $this->getModelsManager();
640
        $lowerCaseAlias = $relation->getOption('alias');
641
        
642
        $relationFields = $relation->getFields();
643
        $relationFields = is_array($relationFields) ? $relationFields : [$relationFields];
644
        
645
        $referencedFields = $relation->getReferencedFields();
646
        $referencedFields = is_array($referencedFields) ? $referencedFields : [$referencedFields];
647
        
648
        $intermediateModelClass = $relation->getIntermediateModel();
649
        
650
        $intermediateFields = $relation->getIntermediateFields();
651
        $intermediateFields = is_array($intermediateFields) ? $intermediateFields : [$intermediateFields];
652
        
653
        $intermediateReferencedFields = $relation->getIntermediateReferencedFields();
654
        $intermediateReferencedFields = is_array($intermediateReferencedFields) ? $intermediateReferencedFields : [$intermediateReferencedFields];
655
        
656
        foreach ($relatedRecords as $relatedAfterKey => $recordAfter) {
657
            // Save the record and get messages
658
            if (!$recordAfter->doSave($visited)) {
659
                $this->appendMessagesFromRecord($recordAfter, $lowerCaseAlias, $relatedAfterKey);
660
                return false;
661
            }
662
            
663
            // Create a new instance of the intermediate model
664
            $intermediateModel = $modelsManager->load($intermediateModelClass);
665
            
666
            /**
667
             *  Has-one-through relations can only use one intermediate model.
668
             *  If it already exists, it can be updated with the new referenced key.
669
             */
670
            if ($relation->getType() === Relation::HAS_ONE_THROUGH) {
671
                $bind = [];
672
                foreach ($relationFields as $relationField) {
673
                    $bind[] = $this->readAttribute($relationField);
674
                }
675
                
676
                $existingIntermediateModel = $intermediateModel::findFirst([
677
                    'conditions' => implode_sprintf($intermediateFields, ' and ', '[' . $intermediateModelClass . '].[%s] = ?%s'),
678
                    'bind' => $bind,
679
                    'bindTypes' => array_fill(0, count($bind), Column::BIND_PARAM_STR),
680
                ]);
681
                
682
                if ($existingIntermediateModel) {
683
                    $intermediateModel = $existingIntermediateModel;
684
                }
685
            }
686
            
687
            // Set intermediate model columns values
688
            foreach ($relationFields as $relationFieldKey => $relationField) {
689
                $intermediateModel->writeAttribute($intermediateFields[$relationFieldKey], $this->readAttribute($relationField));
0 ignored issues
show
Bug introduced by
The method writeAttribute() does not exist on Phalcon\Mvc\ModelInterface. It seems like you code against a sub-type of said class. However, the method does not exist in Zemit\Models\Interfaces\AbstractInterface or Zemit\Models\Interfaces\SettingInterface or Zemit\Models\Interfaces\CategoryInterface or Zemit\Models\Interfaces\FeatureInterface or Zemit\Models\Interfaces\AuditInterface or Zemit\Models\Interfaces\UserGroupInterface or Zemit\Models\Interfaces\UserInterface or Zemit\Models\Interfaces\FieldInterface or Zemit\Models\Interfaces\PageInterface or Zemit\Models\Interfaces\GroupFeatureInterface or Zemit\Models\Interfaces\RoleRoleInterface or Zemit\Models\Interfaces\LogInterface or Zemit\Models\Interfaces\FileInterface or Zemit\Models\Interfaces\RoleInterface or Zemit\Models\Interfaces\GroupRoleInterface or Zemit\Models\Interfaces\TemplateInterface or Zemit\Models\Interfaces\AuditDetailInterface or Zemit\Models\Interfaces\UserTypeInterface or Zemit\Models\Interfaces\PhalconMigrationsInterface or Zemit\Models\Interfaces\PostInterface or Zemit\Models\Interfaces\PostCategoryInterface or Zemit\Models\Interfaces\SessionInterface or Zemit\Models\Interfaces\TranslateFieldInterface or Zemit\Models\Interfaces\ProfileInterface or Zemit\Models\Interfaces\UserFeatureInterface or Zemit\Models\Interfaces\TableInterface or Zemit\Models\Interfaces\TranslateInterface or Zemit\Models\Interfaces\WorkspaceLangInterface or Zemit\Models\Interfaces\EmailInterface or Zemit\Models\Interfaces\WorkspaceInterface or Zemit\Models\Interfaces\DataInterface or Zemit\Models\Interfaces\GroupInterface or Zemit\Models\Interfaces\LangInterface or Zemit\Models\Interfaces\EmailFileInterface or Zemit\Models\Interfaces\TranslateTableInterface or Zemit\Models\Interfaces\RoleFeatureInterface or Zemit\Models\Interfaces\UserRoleInterface or Zemit\Models\Interfaces\FlagInterface or Zemit\Models\Interfaces\MenuInterface or Zemit\Models\Interfaces\GroupTypeInterface or Zemit\Models\Interfaces\BackupInterface or Zemit\Models\Interfaces\JobInterface or Zemit\Models\Interfaces\TypeInterface or Zemit\Models\Interfaces\MetaInterface. Are you sure you never get one of those? ( Ignorable by Annotation )

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

689
                $intermediateModel->/** @scrutinizer ignore-call */ 
690
                                    writeAttribute($intermediateFields[$relationFieldKey], $this->readAttribute($relationField));
Loading history...
690
                $intermediateValue = $recordAfter->readAttribute($referencedFields[$relationFieldKey]);
691
                $intermediateModel->writeAttribute($intermediateReferencedFields[$relationFieldKey], $intermediateValue);
692
            }
693
            
694
            // Save the record and get messages
695
            if (!$intermediateModel->doSave($visited)) {
696
                $this->appendMessagesFromRecord($intermediateModel, $lowerCaseAlias);
697
                $this->appendMessage(new Message('Unable to save intermediate model `' . $intermediateModelClass . '`', $lowerCaseAlias, 'Bad Request', 400));
698
                return false;
699
            }
700
        }
701
        
702
        return true;
703
    }
704
    
705
    /**
706
     * Get an entity from data
707
     */
708
    public function getEntityFromData(array $data, array $configuration = []): ModelInterface
709
    {
710
        $alias = $configuration['alias'] ?? null;
711
        $fields = $configuration['fields'] ?? null;
712
        $modelClass = $configuration['modelClass'] ?? null;
713
        $readFields = $configuration['readFields'] ?? null;
714
        $type = $configuration['type'] ?? null;
715
        $whiteList = $configuration['whiteList'] ?? null;
716
        $dataColumnMap = $configuration['dataColumnMap'] ?? null;
717
        
718
        $entity = false;
719
        
720
        if ($type === Relation::HAS_ONE || $type === Relation::BELONGS_TO) {
721
            
722
            // Set value to compare
723
            if (!empty($readFields)) {
724
                
725
                foreach ($readFields as $key => $field) {
726
                    
727
                    if (empty($data[$fields[$key]])) {
728
                        
729
                        // @todo maybe remove this if
730
                        $value = $this->readAttribute($field);
731
                        if (!empty($value)) {
732
                            
733
                            // @todo maybe remove this if
734
                            $data [$fields[$key]] = $value;
735
                        }
736
                    }
737
                }
738
            }
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
                $modelsMetaData = $this->getModelsMetaData();
750
                $primaryKeys = $modelsMetaData->getPrimaryKeyAttributes($this);
0 ignored issues
show
Bug introduced by
$this of type Zemit\Mvc\Model\Relationship is incompatible with the type Phalcon\Mvc\ModelInterface expected by parameter $model of Phalcon\Mvc\Model\MetaDa...tPrimaryKeyAttributes(). ( Ignorable by Annotation )

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

750
                $primaryKeys = $modelsMetaData->getPrimaryKeyAttributes(/** @scrutinizer ignore-type */ $this);
Loading history...
751
                
752
                // Force primary keys for single to many
753
                foreach ($primaryKeys as $primaryKey) {
754
                    
755
                    if (!in_array($primaryKey, $fields, true)) {
756
                        $dataKeys [$primaryKey] = $data[$primaryKey] ?? null;
757
                        $fields [] = $primaryKey;
758
                    }
759
                }
760
            }
761
            
762
            /** @var ModelInterface|string $modelClass */
763
            $entity = $modelClass::findFirst([
764
                'conditions' => implode_sprintf($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

764
                'conditions' => implode_sprintf($fields, ' and ', '[' . /** @scrutinizer ignore-type */ $modelClass . '].[%s] = ?%s'),
Loading history...
765
                'bind' => array_values($dataKeys),
766
                'bindTypes' => array_fill(0, count($dataKeys), Column::BIND_PARAM_STR),
767
            ]);
768
        }
769
        
770
        if (!$entity) {
771
            $entity = new $modelClass();
772
        }
773
        
774
        // assign new values
775
        // can be null to bypass, empty array for nothing or filled array
776
        $entity->assign($data, $whiteList[$alias] ?? null, $dataColumnMap[$alias] ?? null);
777
//        $entity->setDirtyState(self::DIRTY_STATE_TRANSIENT);
778
        
779
        return $entity;
780
    }
781
    
782
    public function appendMessages(array $messages = [], ?string $context = null, ?int $index = 0): void
783
    {
784
        foreach ($messages as $message) {
785
            assert($message instanceof Message);
786
            
787
            $message->setMetaData([
788
                'index' => $this->rebuildMessageIndex($message, $index),
789
                '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

789
                'context' => $this->rebuildMessageContext($message, /** @scrutinizer ignore-type */ $context),
Loading history...
790
            ]);
791
            
792
            $this->appendMessage($message);
793
        }
794
    }
795
    
796
    /**
797
     * Append a message to this model from another record,
798
     * also prepend a context to the previous context
799
     *
800
     * @param ResultsetInterface|ModelInterface $record
801
     * @param string|null $context
802
     */
803
    public function appendMessagesFromRecord($record, string $context = null, ?int $index = 0): void
804
    {
805
        if ($record) {
806
            $this->appendMessages($record->getMessages(), $context, $index);
807
        }
808
    }
809
    
810
    /**
811
     * Append a message to this model from another record,
812
     * also prepend a context to the previous context
813
     */
814
    public function appendMessagesFromResultset(?ResultsetInterface $resultset = null, ?string $context = null, ?int $index = 0): void
815
    {
816
        if ($resultset) {
817
            $this->appendMessages($resultset->getMessages(), $context, $index);
818
        }
819
    }
820
    
821
    /**
822
     * Append a message to this model from another record,
823
     * also prepend a context to the previous context
824
     */
825
    public function appendMessagesFromRecordList(?iterable $recordList = null, ?string $context = null, ?int $index = 0): void
826
    {
827
        if ($recordList) {
0 ignored issues
show
introduced by
$recordList is of type iterable|null, thus it always evaluated to false.
Loading history...
828
            foreach ($recordList as $key => $record) {
829
                $this->appendMessagesFromRecord($record, $context, $index . '.' . $key);
830
            }
831
        }
832
    }
833
    
834
    /**
835
     * Rebuilding context for meta data
836
     */
837
    public function rebuildMessageContext(Message $message, string $context): ?string
838
    {
839
        $metaData = $message->getMetaData();
840
        $previousContext = $metaData['context'] ?? '';
841
        return $context . (empty($previousContext) ? '' : '.' . $previousContext);
842
    }
843
    
844
    /**
845
     * Rebuilding context for meta data
846
     */
847
    public function rebuildMessageIndex(Message $message, ?int $index): ?string
848
    {
849
        $metaData = $message->getMetaData();
850
        $previousIndex = $metaData['index'] ?? '';
851
        return $index . (empty($previousIndex) ? '' : '.' . $previousIndex);
852
    }
853
    
854
    /**
855
     * Return the related instances as an array representation
856
     */
857
    public function relatedToArray(?array $columns = null, bool $useGetter = true): array
858
    {
859
        $ret = [];
860
        
861
        $columnMap = $this->getModelsMetaData()->getColumnMap($this);
0 ignored issues
show
Bug introduced by
$this of type Zemit\Mvc\Model\Relationship is incompatible with the type Phalcon\Mvc\ModelInterface expected by parameter $model of Phalcon\Mvc\Model\MetaDa...terface::getColumnMap(). ( Ignorable by Annotation )

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

861
        $columnMap = $this->getModelsMetaData()->getColumnMap(/** @scrutinizer ignore-type */ $this);
Loading history...
862
        
863
        foreach ($this->getDirtyRelated() as $attribute => $related) {
864
            
865
            // Map column if defined
866
            if ($columnMap && isset($columnMap[$attribute])) {
867
                $attributeField = $columnMap[$attribute];
868
            }
869
            else {
870
                $attributeField = $attribute;
871
            }
872
            
873
            // Skip or set the related columns
874
            if ($columns) {
875
                if (!key_exists($attributeField, $columns) && !in_array($attributeField, $columns)) {
876
                    continue;
877
                }
878
            }
879
            $relatedColumns = $columns[$attributeField] ?? null;
880
            
881
            // Run toArray on related records
882
            if ($related instanceof ModelInterface && method_exists($related, 'toArray')) {
883
                $ret[$attributeField] = $related->toArray($relatedColumns, $useGetter);
884
            }
885
            elseif (is_iterable($related)) {
886
                $ret[$attributeField] = [];
887
                foreach ($related as $entity) {
888
                    if ($entity instanceof ModelInterface && method_exists($entity, 'toArray')) {
889
                        $ret[$attributeField][] = $entity->toArray($relatedColumns, $useGetter);
890
                    }
891
                    elseif (is_array($entity)) {
892
                        $ret[$attributeField][] = $entity;
893
                    }
894
                }
895
            }
896
            else {
897
                $ret[$attributeField] = null;
898
            }
899
        }
900
        
901
        return $ret;
902
    }
903
    
904
    /**
905
     * Overriding default phalcon getRelated in order to fix an important issue
906
     * where the related record is being stored into the "related" property and then
907
     * passed from the collectRelatedToSave and is mistakenly saved without the user consent
908
     *
909
     * @param string $alias
910
     * @param $arguments
911
     * @return false|int|Model\Resultset\Simple
912
     * @throws Exception
913
     */
914
    public function getRelated(string $alias, $arguments = null)
915
    {
916
        $className = get_class($this);
917
        $manager = $this->getModelsManager();
918
        $lowerAlias = strtolower($alias);
919
        
920
        $relation = $manager->getRelationByAlias($className, $lowerAlias);
921
        if (!$relation) {
922
            throw new Exception(
923
                "There is no defined relations for the model '"
924
                . $className . "' using alias '" . $alias . "'"
925
            );
926
        }
927
928
        return $manager->getRelationRecords($relation, $this, $arguments);
0 ignored issues
show
Bug introduced by
It seems like $relation can also be of type true; however, parameter $relation of Phalcon\Mvc\Model\Manage...e::getRelationRecords() does only seem to accept Phalcon\Mvc\Model\RelationInterface, 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

928
        return $manager->getRelationRecords(/** @scrutinizer ignore-type */ $relation, $this, $arguments);
Loading history...
Bug introduced by
$this of type Zemit\Mvc\Model\Relationship is incompatible with the type Phalcon\Mvc\ModelInterface expected by parameter $record of Phalcon\Mvc\Model\Manage...e::getRelationRecords(). ( Ignorable by Annotation )

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

928
        return $manager->getRelationRecords($relation, /** @scrutinizer ignore-type */ $this, $arguments);
Loading history...
929
    }
930
    
931
    /**
932
     * {@inheritDoc}
933
     */
934 1
    public function toArray($columns = null, $useGetter = true): array
935
    {
936 1
        return array_merge(parent::toArray($columns, $useGetter), $this->relatedToArray($columns, $useGetter));
937
    }
938
}
939