Completed
Pull Request — MetadataImplementationStep (#116)
by Alex
01:49
created

MetadataProvider::calculateRoundTripRelations()   B

Complexity

Conditions 6
Paths 12

Size

Total Lines 27
Code Lines 16

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 0
CRAP Score 42

Importance

Changes 0
Metric Value
c 0
b 0
f 0
dl 0
loc 27
ccs 0
cts 20
cp 0
rs 8.439
cc 6
eloc 16
nc 12
nop 0
crap 42
1
<?php
2
3
namespace AlgoWeb\PODataLaravel\Providers;
4
5
use AlgoWeb\PODataLaravel\Models\MetadataGubbinsHolder;
6
use AlgoWeb\PODataLaravel\Models\ObjectMap\Entities\Associations\Association;
7
use AlgoWeb\PODataLaravel\Models\ObjectMap\Entities\Associations\AssociationMonomorphic;
8
use AlgoWeb\PODataLaravel\Models\ObjectMap\Entities\Associations\AssociationPolymorphic;
9
use AlgoWeb\PODataLaravel\Models\ObjectMap\Entities\Associations\AssociationStubRelationType;
10
use AlgoWeb\PODataLaravel\Models\ObjectMap\Entities\Associations\AssociationType;
11
use AlgoWeb\PODataLaravel\Models\ObjectMap\Entities\EntityFieldType;
12
use AlgoWeb\PODataLaravel\Models\ObjectMap\Entities\EntityGubbins;
13
use AlgoWeb\PODataLaravel\Models\ObjectMap\Map;
14
use Illuminate\Database\Eloquent\Model;
15
use Illuminate\Support\Facades\App;
16
use Illuminate\Support\Facades\Cache;
17
use Illuminate\Support\Facades\Schema as Schema;
18
use POData\Providers\Metadata\ResourceEntityType;
19
use POData\Providers\Metadata\ResourceSet;
20
use POData\Providers\Metadata\ResourceStreamInfo;
21
use POData\Providers\Metadata\SimpleMetadataProvider;
22
use POData\Providers\Metadata\Type\TypeCode;
23
24
class MetadataProvider extends MetadataBaseProvider
25
{
26
    protected $multConstraints = ['0..1' => ['1'], '1' => ['0..1', '*'], '*' => ['1', '*']];
27
    protected static $metaNAMESPACE = 'Data';
28
    protected static $relationCache;
29
    protected static $isBooted = false;
30
    const POLYMORPHIC = 'polyMorphicPlaceholder';
31
    const POLYMORPHIC_PLURAL = 'polyMorphicPlaceholders';
32
33
    /**
34
     * @var Map The completed object map set at post Implement;
35
     */
36
    private $completedObjectMap;
37
38
    /**
39
     * @return \AlgoWeb\PODataLaravel\Models\ObjectMap\Map
40
     */
41
    public function getObjectMap()
42
    {
43
        return $this->completedObjectMap;
44
    }
45
46
    protected static $afterExtract;
47
    protected static $afterUnify;
48
    protected static $afterVerify;
49
    protected static $afterImplement;
50
51
    public static function setAfterExtract(callable $method)
52
    {
53
        self::$afterExtract = $method;
54
    }
55
56
    public static function setAfterUnify(callable $method)
57
    {
58
        self::$afterUnify = $method;
59
    }
60
61
    public static function setAfterVerify(callable $method)
62
    {
63
        self::$afterVerify = $method;
64
    }
65
66
    public static function setAfterImplement(callable $method)
67
    {
68
        self::$afterImplement = $method;
69
    }
70
71
72
    protected $relationHolder;
73
74
    public function __construct($app)
75
    {
76
        parent::__construct($app);
77
        $this->relationHolder = new MetadataGubbinsHolder();
78
        self::$isBooted = false;
79
    }
80
81
    private function extract(array $modelNames)
82
    {
83
        $objectMap = new Map();
84
        foreach ($modelNames as $modelName) {
85
            $modelInstance = App::make($modelName);
86
            $objectMap->addEntity($modelInstance->extractGubbins());
87
        }
88
        if (null != self::$afterExtract) {
89
            $func = self::$afterExtract;
90
            $func($objectMap);
91
        }
92
        return $objectMap;
93
    }
94
95
    private function unify(Map $ObjectMap)
0 ignored issues
show
Coding Style introduced by
$ObjectMap does not seem to conform to the naming convention (^[a-z][a-zA-Z0-9]*$).

This check examines a number of code elements and verifies that they conform to the given naming conventions.

You can set conventions for local variables, abstract classes, utility classes, constant, properties, methods, parameters, interfaces, classes, exceptions and special methods.

Loading history...
Coding Style Naming introduced by
The parameter $ObjectMap is not named in camelCase.

This check marks parameter names that have not been written in camelCase.

In camelCase names are written without any punctuation, the start of each new word being marked by a capital letter. Thus the name database connection string becomes databaseConnectionString.

Loading history...
96
    {
97
        $mgh = $this->getRelationHolder();
98
        foreach ($ObjectMap->getEntities() as $entity) {
0 ignored issues
show
Coding Style introduced by
$ObjectMap does not seem to conform to the naming convention (^[a-z][a-zA-Z0-9]*$).

This check examines a number of code elements and verifies that they conform to the given naming conventions.

You can set conventions for local variables, abstract classes, utility classes, constant, properties, methods, parameters, interfaces, classes, exceptions and special methods.

Loading history...
99
            $mgh->addEntity($entity);
100
        }
101
        $ObjectMap->setAssociations($mgh->getRelations());
0 ignored issues
show
Coding Style introduced by
$ObjectMap does not seem to conform to the naming convention (^[a-z][a-zA-Z0-9]*$).

This check examines a number of code elements and verifies that they conform to the given naming conventions.

You can set conventions for local variables, abstract classes, utility classes, constant, properties, methods, parameters, interfaces, classes, exceptions and special methods.

Loading history...
102
        if (null != self::$afterUnify) {
103
            $func = self::$afterUnify;
104
            $func($ObjectMap);
0 ignored issues
show
Coding Style introduced by
$ObjectMap does not seem to conform to the naming convention (^[a-z][a-zA-Z0-9]*$).

This check examines a number of code elements and verifies that they conform to the given naming conventions.

You can set conventions for local variables, abstract classes, utility classes, constant, properties, methods, parameters, interfaces, classes, exceptions and special methods.

Loading history...
105
        }
106
        return $ObjectMap;
0 ignored issues
show
Coding Style introduced by
$ObjectMap does not seem to conform to the naming convention (^[a-z][a-zA-Z0-9]*$).

This check examines a number of code elements and verifies that they conform to the given naming conventions.

You can set conventions for local variables, abstract classes, utility classes, constant, properties, methods, parameters, interfaces, classes, exceptions and special methods.

Loading history...
107
    }
108
109
    private function verify(Map $objectModel)
110
    {
111
        $failMessage = '';
112
        $objectModel->isOK($failMessage);
0 ignored issues
show
Unused Code introduced by
The call to Map::isOK() has too many arguments starting with $failMessage.

This check compares calls to functions or methods with their respective definitions. If the call has more arguments than are defined, it raises an issue.

If a function is defined several times with a different number of parameters, the check may pick up the wrong definition and report false positives. One codebase where this has been known to happen is Wordpress.

In this case you can add the @ignore PhpDoc annotation to the duplicate definition and it will be ignored.

Loading history...
113
        if (null != self::$afterVerify) {
114
            $func = self::$afterVerify;
115
            $func($objectModel);
116
        }
117
    }
118
119
    private function implement(Map $objectModel)
120
    {
121
        $meta = App::make('metadata');
122
        $entities = $objectModel->getEntities();
123
        foreach ($entities as $entity) {
124
            $baseType = $entity->isPolymorphicAffected() ? $meta->resolveResourceType('polyMorphicPlaceholder') : null;
125
            $className = $entity->getClassName();
126
            $entityName = $entity->getName();
127
            $EntityType = $meta->addEntityType(new \ReflectionClass($className), $entityName, false, $baseType);
0 ignored issues
show
Coding Style introduced by
$EntityType does not seem to conform to the naming convention (^[a-z][a-zA-Z0-9]*$).

This check examines a number of code elements and verifies that they conform to the given naming conventions.

You can set conventions for local variables, abstract classes, utility classes, constant, properties, methods, parameters, interfaces, classes, exceptions and special methods.

Loading history...
128
            assert($EntityType->hasBaseType() === isset($baseType));
0 ignored issues
show
Coding Style introduced by
$EntityType does not seem to conform to the naming convention (^[a-z][a-zA-Z0-9]*$).

This check examines a number of code elements and verifies that they conform to the given naming conventions.

You can set conventions for local variables, abstract classes, utility classes, constant, properties, methods, parameters, interfaces, classes, exceptions and special methods.

Loading history...
129
            $entity->setOdataResourceType($EntityType);
0 ignored issues
show
Coding Style introduced by
$EntityType does not seem to conform to the naming convention (^[a-z][a-zA-Z0-9]*$).

This check examines a number of code elements and verifies that they conform to the given naming conventions.

You can set conventions for local variables, abstract classes, utility classes, constant, properties, methods, parameters, interfaces, classes, exceptions and special methods.

Loading history...
130
            $this->implementProperties($entity);
131
            $meta->addResourceSet($entity->getClassName(), $EntityType);
0 ignored issues
show
Coding Style introduced by
$EntityType does not seem to conform to the naming convention (^[a-z][a-zA-Z0-9]*$).

This check examines a number of code elements and verifies that they conform to the given naming conventions.

You can set conventions for local variables, abstract classes, utility classes, constant, properties, methods, parameters, interfaces, classes, exceptions and special methods.

Loading history...
132
            $meta->oDataEntityMap[$className] = $meta->oDataEntityMap[$entityName];
133
        }
134
        $metaCount = count($meta->oDataEntityMap);
135
        $entityCount = count($entities);
136
        assert($metaCount == 2 * $entityCount + 1);
137
138
        if (null === $objectModel->getAssociations()) {
139
            return;
140
        }
141
        $assoc = $objectModel->getAssociations();
142
        $assoc = null === $assoc ? [] : $assoc;
143
        foreach ($assoc as $association) {
144
            assert($association->isOk());
145
            if ($association instanceof AssociationMonomorphic) {
146
                $this->implementAssociationsMonomorphic($objectModel, $association);
147
            } elseif ($association instanceof AssociationPolymorphic) {
148
                $this->implementAssociationsPolymorphic($objectModel, $association);
149
            }
150
        }
151
        if (null != self::$afterImplement) {
152
            $func = self::$afterImplement;
153
            $func($objectModel);
154
        }
155
    }
156
157
    private function implementAssociationsMonomorphic(Map $objectModel, AssociationMonomorphic $associationUnderHammer)
158
    {
159
        $meta = App::make('metadata');
160
        $first = $associationUnderHammer->getFirst();
161
        $last = $associationUnderHammer->getLast();
162
        switch ($associationUnderHammer->getAssociationType()) {
163
            case AssociationType::NULL_ONE_TO_NULL_ONE():
164
            case AssociationType::NULL_ONE_TO_ONE():
165 View Code Duplication
            case AssociationType::ONE_TO_ONE():
0 ignored issues
show
Duplication introduced by
This code seems to be duplicated across your project.

Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.

You can also find more detailed suggestions in the “Code” section of your repository.

Loading history...
166
                $meta->addResourceReferenceSinglePropertyBidirectional(
167
                    $objectModel->getEntities()[$first->getBaseType()]->getOdataResourceType(),
168
                    $objectModel->getEntities()[$last->getBaseType()]->getOdataResourceType(),
169
                    $first->getRelationName(),
170
                    $last->getRelationName()
171
                );
172
                break;
173
            case AssociationType::NULL_ONE_TO_MANY():
174
            case AssociationType::ONE_TO_MANY():
175
                if ($first->getMultiplicity()->getValue() == AssociationStubRelationType::MANY) {
176
                    $oneSide = $last;
177
                    $manySide = $first;
178
                } else {
179
                    $oneSide = $first;
180
                    $manySide = $last;
181
                }
182
                $meta->addResourceReferencePropertyBidirectional(
183
                    $objectModel->getEntities()[$oneSide->getBaseType()]->getOdataResourceType(),
184
                    $objectModel->getEntities()[$manySide->getBaseType()]->getOdataResourceType(),
185
                    $oneSide->getRelationName(),
186
                    $manySide->getRelationName()
187
                );
188
                break;
189 View Code Duplication
            case AssociationType::MANY_TO_MANY():
0 ignored issues
show
Duplication introduced by
This code seems to be duplicated across your project.

Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.

You can also find more detailed suggestions in the “Code” section of your repository.

Loading history...
190
                $meta->addResourceSetReferencePropertyBidirectional(
191
                    $objectModel->getEntities()[$first->getBaseType()]->getOdataResourceType(),
192
                    $objectModel->getEntities()[$last->getBaseType()]->getOdataResourceType(),
193
                    $first->getRelationName(),
194
                    $last->getRelationName()
195
                );
196
        }
197
    }
198
199
    /**
200
     * @param Map                    $objectModel
201
     * @param AssociationPolymorphic $association
202
     */
203
    private function implementAssociationsPolymorphic(Map $objectModel, AssociationPolymorphic $association)
204
    {
205
        $meta = App::make('metadata');
206
        $first = $association->getFirst();
207
208
        $polySet = $meta->resolveResourceSet(static::POLYMORPHIC_PLURAL);
209
        assert($polySet instanceof ResourceSet);
210
211
        $principalType = $objectModel->getEntities()[$first->getBaseType()]->getOdataResourceType();
212
        assert($principalType instanceof ResourceEntityType);
213
        $principalSet = $principalType->getCustomState();
214
        assert($principalSet instanceof ResourceSet);
215
        $principalProp = $first->getRelationName();
216
        $isPrincipalAdded = null !== $principalType->resolveProperty($principalProp);
217
218
        if (!$isPrincipalAdded) {
219
            if ($first->getMultiplicity()->getValue() !== AssociationStubRelationType::MANY) {
220
                $meta->addResourceReferenceProperty($principalType, $principalProp, $polySet);
221
            } else {
222
                $meta->addResourceSetReferenceProperty($principalType, $principalProp, $polySet);
223
            }
224
        }
225
226
        $types = $association->getAssociationType();
227
        $final = $association->getLast();
228
        $numRows = count($types);
229
        assert($numRows == count($final));
230
231
        for ($i = 0; $i < $numRows; $i++) {
232
            $type = $types[$i];
233
            $last = $final[$i];
234
235
            $dependentType = $objectModel->getEntities()[$last->getBaseType()]->getOdataResourceType();
236
            assert($dependentType instanceof ResourceEntityType);
237
            $dependentSet = $dependentType->getCustomState();
238
            assert($dependentSet instanceof ResourceSet);
239
            $dependentProp = $last->getRelationName();
240
            $isDependentAdded = null !== $dependentType->resolveProperty($dependentProp);
241
242
            switch ($type) {
243
                case AssociationType::NULL_ONE_TO_NULL_ONE():
244
                case AssociationType::NULL_ONE_TO_ONE():
245
                case AssociationType::ONE_TO_ONE():
246
                    if (!$isDependentAdded) {
247
                        $meta->addResourceReferenceProperty($dependentType, $dependentProp, $principalSet);
248
                    }
249
                    break;
250
                case AssociationType::NULL_ONE_TO_MANY():
251
                case AssociationType::ONE_TO_MANY():
252
                    if (!$isDependentAdded) {
253
                        $meta->addResourceSetReferenceProperty($dependentType, $dependentProp, $principalSet);
254
                    }
255
                    break;
256
                case AssociationType::MANY_TO_MANY():
257
                    if (!$isDependentAdded) {
258
                        $meta->addResourceSetReferenceProperty($dependentType, $dependentProp, $principalSet);
259
                    }
260
            }
261
        }
262
    }
263
264
    private function implementProperties(EntityGubbins $unifiedEntity)
265
    {
266
        $meta = App::make('metadata');
267
        $odataEntity = $unifiedEntity->getOdataResourceType();
268
        if (!$unifiedEntity->isPolymorphicAffected()) {
269
            foreach ($unifiedEntity->getKeyFields() as $keyField) {
270
                $meta->addKeyProperty($odataEntity, $keyField->getName(), $keyField->getEdmFieldType());
271
            }
272
        }
273
        foreach ($unifiedEntity->getFields() as $field) {
274
            if (in_array($field, $unifiedEntity->getKeyFields())) {
275
                continue;
276
            }
277
            if ($field->getPrimitiveType() == 'blob') {
278
                $odataEntity->setMediaLinkEntry(true);
279
                $streamInfo = new ResourceStreamInfo($field->getName());
280
                assert($odataEntity->isMediaLinkEntry());
281
                $odataEntity->addNamedStream($streamInfo);
282
                continue;
283
            }
284
            $meta->addPrimitiveProperty(
285
                $odataEntity,
286
                $field->getName(),
287
                $field->getEdmFieldType(),
288
                $field->getFieldType() == EntityFieldType::PRIMITIVE_BAG(),
289
                $field->getDefaultValue(),
290
                $field->getIsNullable()
291
            );
292
        }
293
    }
294
295
    /**
296
     * Bootstrap the application services.  Post-boot.
297
     *
298
     * @param  mixed $reset
299
     *
300
     * @return void
301
     */
302
    public function boot($reset = true)
303
    {
304
        self::$metaNAMESPACE = env('ODataMetaNamespace', 'Data');
305
        // If we aren't migrated, there's no DB tables to pull metadata _from_, so bail out early
306
        try {
307
            if (!Schema::hasTable(config('database.migrations'))) {
308
                return;
309
            }
310
        } catch (\Exception $e) {
311
            return;
312
        }
313
314
        assert(false === self::$isBooted, 'Provider booted twice');
315
        $isCaching = true === $this->getIsCaching();
316
        $meta = Cache::get('metadata');
317
        $hasCache = null != $meta;
318
319
        if ($isCaching && $hasCache) {
320
            App::instance('metadata', $meta);
321
            return;
322
        }
323
        $meta = App::make('metadata');
324
        if (false !== $reset) {
325
            $this->reset();
326
        }
327
328
        $stdRef = new \ReflectionClass(Model::class);
329
        $abstract = $meta->addEntityType($stdRef, static::POLYMORPHIC, true, null);
330
        $meta->addKeyProperty($abstract, 'PrimaryKey', TypeCode::STRING);
331
332
        $meta->addResourceSet(static::POLYMORPHIC, $abstract);
333
334
        $modelNames = $this->getCandidateModels();
335
        $objectModel = $this->extract($modelNames);
336
        $objectModel = $this->unify($objectModel);
337
        $this->verify($objectModel);
338
        $this->implement($objectModel);
339
        $this->completedObjectMap = $objectModel;
340
        $key = 'metadata';
341
        $this->handlePostBoot($isCaching, $hasCache, $key, $meta);
342
        self::$isBooted = true;
343
    }
344
345
    /**
346
     * Register the application services.  Boot-time only.
347
     *
348
     * @return void
349
     */
350
    public function register()
351
    {
352
        $this->app->singleton('metadata', function ($app) {
0 ignored issues
show
Unused Code introduced by
The parameter $app is not used and could be removed.

This check looks from parameters that have been defined for a function or method, but which are not used in the method body.

Loading history...
353
            return new SimpleMetadataProvider('Data', self::$metaNAMESPACE);
354
        });
355
    }
356
357
    /**
358
     * @return array
359
     */
360
    protected function getCandidateModels()
361
    {
362
        $classes = $this->getClassMap();
363
        $ends = [];
364
        $startName = defined('PODATA_LARAVEL_APP_ROOT_NAMESPACE') ? PODATA_LARAVEL_APP_ROOT_NAMESPACE : 'App';
365
        foreach ($classes as $name) {
366
            if (\Illuminate\Support\Str::startsWith($name, $startName)) {
367
                if (in_array('AlgoWeb\\PODataLaravel\\Models\\MetadataTrait', class_uses($name))) {
368
                    $ends[] = $name;
369
                }
370
            }
371
        }
372
        return $ends;
373
    }
374
375
    /**
376
     * @return MetadataGubbinsHolder
377
     */
378
    public function getRelationHolder()
379
    {
380
        return $this->relationHolder;
381
    }
382
383
    public function calculateRoundTripRelations()
384
    {
385
        $modelNames = $this->getCandidateModels();
386
387
        foreach ($modelNames as $name) {
388
            if (!$this->getRelationHolder()->hasClass($name)) {
389
                $model = App::make($name);
390
                $gubbinz = $model->extractGubbins();
391
                $this->getRelationHolder()->addEntity($gubbinz);
392
            }
393
        }
394
395
        $rels = $this->getRelationHolder()->getRelations();
396
397
        $result = [];
398
        foreach ($rels as $payload) {
399
            assert($payload instanceof Association);
400
            $raw = $payload->getArrayPayload();
401
            if (is_array($raw)) {
402
                foreach ($raw as $line) {
403
                    $result[] = $line;
404
                }
405
            }
406
        }
407
408
        return $result;
409
    }
410
411
    public function getPolymorphicRelationGroups()
412
    {
413
        $modelNames = $this->getCandidateModels();
414
415
        $knownSide = [];
416
        $unknownSide = [];
417
418
        $hooks = [];
419
        // fish out list of polymorphic-affected models for further processing
420
        foreach ($modelNames as $name) {
421
            $model = new $name();
422
            $isPoly = false;
423
            if ($model->isKnownPolymorphSide()) {
424
                $knownSide[$name] = [];
425
                $isPoly = true;
426
            }
427
            if ($model->isUnknownPolymorphSide()) {
428
                $unknownSide[$name] = [];
429
                $isPoly = true;
430
            }
431
            if (false === $isPoly) {
432
                continue;
433
            }
434
435
            $rels = $model->getRelationships();
436
            // it doesn't matter if a model has no relationships here, that lack will simply be skipped over
437
            // during hookup processing
438
            $hooks[$name] = $rels;
439
        }
440
        // ensure we've only loaded up polymorphic-affected models
441
        $knownKeys = array_keys($knownSide);
442
        $unknownKeys = array_keys($unknownSide);
443
        $dualKeys = array_intersect($knownKeys, $unknownKeys);
444
        assert(count($hooks) == (count($unknownKeys) + count($knownKeys) - count($dualKeys)));
445
        // if either list is empty, bail out - there's nothing to do
446
        if (0 === count($knownSide) || 0 === count($unknownSide)) {
447
            return [];
448
        }
449
450
        // commence primary ignition
451
452
        foreach ($unknownKeys as $key) {
453
            assert(isset($hooks[$key]));
454
            $hook = $hooks[$key];
455
            foreach ($hook as $barb) {
456
                foreach ($barb as $knownType => $propData) {
457
                    $propName = array_keys($propData)[0];
458
                    if (in_array($knownType, $knownKeys)) {
459
                        if (!isset($knownSide[$knownType][$key])) {
460
                            $knownSide[$knownType][$key] = [];
461
                        }
462
                        assert(isset($knownSide[$knownType][$key]));
463
                        $knownSide[$knownType][$key][] = $propData[$propName]['property'];
464
                    }
465
                }
466
            }
467
        }
468
469
        return $knownSide;
470
    }
471
472
    /**
473
     * Get round-trip relations after inserting polymorphic-powered placeholders.
474
     *
475
     * @return array
476
     */
477
    public function getRepairedRoundTripRelations()
478
    {
479
        if (!isset(self::$relationCache)) {
480
            $rels = $this->calculateRoundTripRelations();
481
            $groups = $this->getPolymorphicRelationGroups();
482
483
            if (0 === count($groups)) {
484
                self::$relationCache = $rels;
485
                return $rels;
486
            }
487
488
            $placeholder = static::POLYMORPHIC;
489
490
            $groupKeys = array_keys($groups);
491
492
            // we have at least one polymorphic relation, need to dig it out
493
            $numRels = count($rels);
494
            for ($i = 0; $i < $numRels; $i++) {
495
                $relation = $rels[$i];
496
                $principalType = $relation['principalType'];
497
                $dependentType = $relation['dependentType'];
498
                $principalPoly = in_array($principalType, $groupKeys);
499
                $dependentPoly = in_array($dependentType, $groupKeys);
500
                $rels[$i]['principalRSet'] = $principalPoly ? $placeholder : $principalType;
501
                $rels[$i]['dependentRSet'] = $dependentPoly ? $placeholder : $dependentType;
502
            }
503
            self::$relationCache = $rels;
504
        }
505
        return self::$relationCache;
506
    }
507
508
509
    public function reset()
510
    {
511
        self::$relationCache = null;
512
        self::$isBooted = false;
513
        self::$afterExtract = null;
514
        self::$afterUnify = null;
515
        self::$afterVerify = null;
516
        self::$afterImplement = null;
517
    }
518
519
    /**
520
     * Resolve possible reverse relation property names.
521
     *
522
     * @param Model $source
523
     * @param Model $target
524
     * @param       $propName
525
     *
526
     * @return string|null
527
     */
528
    public function resolveReverseProperty(Model $source, Model $target, $propName)
529
    {
530
        assert(is_string($propName), 'Property name must be string');
531
        $entity = $this->getObjectMap()->resolveEntity(get_class($source));
532
        if (null === $entity) {
533
            $msg = 'Source model not defined';
534
            throw new \InvalidArgumentException($msg);
535
        }
536
        $association = $entity->resolveAssociation($propName);
537
        if (null === $association) {
538
            return null;
539
        }
540
        $isFirst = $propName === $association->getFirst()->getRelationName();
541
        if (!$isFirst) {
542
            return $association->getFirst()->getRelationName();
543
        }
544
545
        if ($association instanceof AssociationMonomorphic) {
546
            return $association->getLast()->getRelationName();
547
        }
548
        assert($association instanceof AssociationPolymorphic);
549
550
        $lasts = $association->getLast();
551
        foreach ($lasts as $stub) {
552
            if ($stub->getBaseType() == get_class($target)) {
553
                return $stub->getRelationName();
554
            }
555
        }
556
    }
557
}
558