Passed
Push — master ( ff3639...8a879c )
by Anton
01:54
created

GenerateRelations   A

Complexity

Total Complexity 18

Size/Duplication

Total Lines 140
Duplicated Lines 0 %

Importance

Changes 0
Metric Value
eloc 66
dl 0
loc 140
rs 10
c 0
b 0
f 0
wmc 18

5 Methods

Rating   Name   Duplication   Size   Complexity  
A run() 0 11 3
A initRelation() 0 7 2
A inverse() 0 25 6
A register() 0 22 3
A __construct() 0 13 4
1
<?php declare(strict_types=1);
2
/**
3
 * Spiral Framework.
4
 *
5
 * @license   MIT
6
 * @author    Anton Titov (Wolfy-J)
7
 */
8
9
namespace Cycle\Schema\Generator;
10
11
use Cycle\ORM\Relation;
12
use Cycle\Schema\Definition\Entity;
13
use Cycle\Schema\Exception\RegistryException;
14
use Cycle\Schema\Exception\RelationException;
15
use Cycle\Schema\Exception\SchemaException;
16
use Cycle\Schema\GeneratorInterface;
17
use Cycle\Schema\InversableInterface;
18
use Cycle\Schema\Registry;
19
use Cycle\Schema\Relation\OptionSchema;
20
use Cycle\Schema\Relation\RelationSchema;
21
use Cycle\Schema\RelationInterface;
22
23
/**
24
 * Generate relations based on their schematic definitions.
25
 */
26
final class GenerateRelations implements GeneratorInterface
27
{
28
    // aliases between option names and their internal IDs
29
    public const OPTION_MAP = [
30
        'cascade'         => Relation::CASCADE,
31
        'nullable'        => Relation::NULLABLE,
32
        'innerKey'        => Relation::INNER_KEY,
33
        'outerKey'        => Relation::OUTER_KEY,
34
        'morphKey'        => Relation::MORPH_KEY,
35
        'though'          => Relation::THOUGH_ENTITY,
36
        'thoughInnerKey'  => Relation::THOUGH_INNER_KEY,
37
        'thoughOuterKey'  => Relation::THOUGH_OUTER_KEY,
38
        'thoughConstrain' => Relation::THOUGH_CONSTRAIN,
39
        'thoughWhere'     => Relation::THOUGH_WHERE,
40
        'constrain'       => Relation::CONSTRAIN,
41
        'where'           => Relation::WHERE,
42
        'fkCreate'        => RelationSchema::FK_CREATE,
43
        'fkAction'        => RelationSchema::FK_ACTION,
44
        'indexCreate'     => RelationSchema::INDEX_CREATE,
45
        'morphKeyLength'  => RelationSchema::MORPH_KEY_LENGTH
46
    ];
47
48
    /** @var OptionSchema */
49
    private $options;
50
51
    /** @var RelationInterface[] */
52
    private $relations = [];
53
54
    /**
55
     * @param array             $relations
56
     * @param OptionSchema|null $optionSchema
57
     */
58
    public function __construct(array $relations, OptionSchema $optionSchema = null)
59
    {
60
        $this->options = $optionSchema ?? new OptionSchema(self::OPTION_MAP);
61
62
        foreach ($relations as $id => $relation) {
63
            if (!$relation instanceof RelationInterface) {
64
                throw new \InvalidArgumentException(sprintf(
65
                    "Invalid relation type, RelationInterface excepted, `%s` given",
66
                    is_object($relation) ? get_class($relation) : gettype($relation)
67
                ));
68
            }
69
70
            $this->relations[$id] = $relation;
71
        }
72
    }
73
74
    /**
75
     * @param Registry $registry
76
     * @return Registry
77
     */
78
    public function run(Registry $registry): Registry
79
    {
80
        foreach ($registry as $entity) {
81
            $this->register($registry, $entity);
82
        }
83
84
        foreach ($registry as $entity) {
85
            $this->inverse($registry, $entity);
86
        }
87
88
        return $registry;
89
    }
90
91
    /**
92
     * @param Registry $registry
93
     * @param Entity   $entity
94
     */
95
    protected function register(Registry $registry, Entity $entity)
96
    {
97
        foreach ($entity->getRelations() as $name => $r) {
98
            $schema = $this->initRelation($r->getType())->withContext(
99
                $name,
100
                $entity->getRole(),
101
                $r->getTarget(),
102
                $this->options->withOptions($r->getOptions())
103
            );
104
105
            // compute relation values (field names, related entities and etc)
106
            try {
107
                $schema->compute($registry);
108
            } catch (RelationException $e) {
109
                throw new SchemaException(
110
                    "Unable to compute relation `{$entity->getRole()}`.`{$name}`",
111
                    $e->getCode(),
112
                    $e
113
                );
114
            }
115
116
            $registry->registerRelation($entity, $name, $schema);
117
        }
118
    }
119
120
    /**
121
     * @param Registry $registry
122
     * @param Entity   $entity
123
     */
124
    protected function inverse(Registry $registry, Entity $entity)
125
    {
126
        foreach ($entity->getRelations() as $name => $r) {
127
            if (!$r->isInversed()) {
128
                continue;
129
            }
130
131
            $schema = $registry->getRelation($entity, $name);
132
            if (!$schema instanceof InversableInterface) {
133
                throw new SchemaException("Unable to inverse relation of type " . get_class($schema));
134
            }
135
136
            foreach ($schema->inverseTargets($registry) as $target) {
137
                try {
138
                    $inversed = $schema->inverseRelation(
139
                        $this->initRelation($r->getInverseType()),
140
                        $r->getInverseName()
141
                    );
142
143
                    $registry->registerRelation($target, $r->getInverseName(), $inversed);
144
                } catch (RelationException $e) {
145
                    throw new SchemaException(
146
                        "Unable to inverse relation `{$entity->getRole()}`.`{$name}`",
147
                        $e->getCode(),
148
                        $e
149
                    );
150
                }
151
            }
152
        }
153
    }
154
155
    /**
156
     * @param string $type
157
     * @return RelationInterface
158
     */
159
    protected function initRelation(string $type): RelationInterface
160
    {
161
        if (!isset($this->relations[$type])) {
162
            throw new RegistryException("Undefined relation type `{$type}`");
163
        }
164
165
        return $this->relations[$type];
166
    }
167
}