Completed
Branch feature/split-orm (fa2f8e)
by Anton
03:35
created

InheritanceHelper::findChildren()   C

Complexity

Conditions 8
Paths 6

Size

Total Lines 33
Code Lines 15

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 8
eloc 15
nc 6
nop 2
dl 0
loc 33
rs 5.3846
c 0
b 0
f 0
1
<?php
2
/**
3
 * components
4
 *
5
 * @author    Wolfy-J
6
 */
7
namespace Spiral\ODM\Schemas;
8
9
use Spiral\Models\Reflections\ReflectionEntity;
10
use Spiral\ODM\Exceptions\DefinitionException;
11
12
/**
13
 * Helps to define sequence of fields needed to clearly distinguish one model from another based on
14
 * given data.
15
 */
16
class InheritanceHelper
17
{
18
    /**
19
     * @var DocumentSchema
20
     */
21
    private $schema;
22
23
    /**
24
     * All other registered schemas.
25
     *
26
     * @var SchemaInterface[]
27
     */
28
    private $schemas = [];
29
30
    /**
31
     * @param DocumentSchema    $schema
32
     * @param SchemaInterface[] $schemas
33
     */
34
    public function __construct(DocumentSchema $schema, array $schemas)
35
    {
36
        $this->schema = $schema;
37
        $this->schemas = $schemas;
38
    }
39
40
    /**
41
     * Compile information required to resolve class instance using given set of fields. Fields
42
     * based definition will analyze unique fields in every child model to create association
43
     * between model class and required set of fields. Only document from same collection will be
44
     * involved in definition creation. Definition built only for child of first order.
45
     *
46
     * @return array|string
47
     *
48
     * @throws DefinitionException
49
     */
50
    public function makeDefinition()
51
    {
52
        //Find only first level children stored in the same collection
53
        $children = $this->findChildren(true, true);
54
55
        if (empty($children)) {
56
            //Nothing to inherit
57
            return $this->schema->getClass();
58
        }
59
60
        //We must sort child in order or unique fields
61
        uasort($children, [$this, 'sortChildren']);
62
63
        //Fields which are common for parent and child models
64
        $commonFields = $this->schema->getReflection()->getFields();
65
66
        $definition = [];
67
        foreach ($children as $schema) {
68
            //Child document fields
69
            $fields = $schema->getReflection()->getFields();
70
71
            if (empty($fields)) {
72
                throw new DefinitionException(
73
                    "Child document '{$schema->getClass()}' of {$this->schema->getClass()} does not have any fields"
74
                );
75
            }
76
77
            $uniqueField = null;
78
            if (empty($commonFields)) {
79
                //Parent did not declare any fields, happen sometimes
80
                $commonFields = $fields;
81
                $uniqueField = key($fields);
82
            } else {
83
                foreach ($fields as $field => $type) {
84
                    if (!isset($commonFields[$field])) {
85
                        if (empty($uniqueField)) {
86
                            $uniqueField = $field;
87
                        }
88
89
                        //New non unique field (must be excluded from analysis)
90
                        $commonFields[$field] = true;
91
                    }
92
                }
93
            }
94
95
            if (empty($uniqueField)) {
96
                throw new DefinitionException(
97
                    "Child document {$schema} of {$this} does not have any unique field"
98
                );
99
            }
100
101
            $definition[$uniqueField] = $schema->getClass();
102
        }
103
104
        return $definition;
105
    }
106
107
    /**
108
     * Get Document child classes.
109
     *
110
     * Example:
111
     * Class A
112
     * Class B extends A
113
     * Class D extends A
114
     * Class E extends D
115
     *
116
     * Result: B, D, E
117
     *
118
     * @see getPrimary()
119
     *
120
     * @param bool $sameCollection Find only children related to same collection as parent.
121
     * @param bool $directChildren Only child extended directly from current document.
122
     *
123
     * @return DocumentSchema[]
124
     */
125
    public function findChildren(bool $sameCollection = false, bool $directChildren = false)
126
    {
127
        $result = [];
128
        foreach ($this->schemas as $schema) {
129
            //Only Document and DocumentEntity classes supported
130
            if (!$schema instanceof DocumentSchema) {
131
                continue;
132
            }
133
134
            //ReflectionEntity
135
            $reflection = $schema->getReflection();
136
137
            if ($reflection->isSubclassOf($this->schema->getClass())) {
138
139
                if ($sameCollection && !$this->compareCollection($schema)) {
140
                    //Child changed collection or database
141
                    continue;
142
                }
143
144
                if (
145
                    $directChildren
146
                    && $reflection->getParentClass()->getName() != $this->schema->getClass()
147
                ) {
148
                    //Grandson
149
                    continue;
150
                }
151
152
                $result[] = $schema;
153
            }
154
        }
155
156
        return $result;
157
    }
158
159
    /**
160
     * Find primary class needed to represent model and model childs.
161
     *
162
     * @param bool $sameCollection Find only parent related to same collection as model.
163
     *
164
     * @return string
165
     */
166
    public function findPrimary(bool $sameCollection = true): string
167
    {
168
        $primary = $this->schema->getClass();
169
        foreach ($this->schemas as $schema) {
170
            //Only Document and DocumentEntity classes supported
171
            if (!$schema instanceof DocumentSchema) {
172
                continue;
173
            }
174
175
            if ($this->schema->getReflection()->isSubclassOf($schema->getClass())) {
176
                if ($sameCollection && !$this->compareCollection($schema)) {
177
                    //Child changed collection or database
178
                    continue;
179
                }
180
181
                $primary = $schema->getClass();
182
            }
183
        }
184
185
        return $primary;
186
    }
187
188
    /**
189
     * @return ReflectionEntity
190
     */
191
    protected function getReflection(): ReflectionEntity
192
    {
193
        return $this->schema->getReflection();
194
    }
195
196
    /**
197
     * Check if both document schemas belongs to same collection. Documents without declared
198
     * collection must be counted as documents from same collection.
199
     *
200
     * @param DocumentSchema $document
201
     *
202
     * @return bool
203
     */
204
    protected function compareCollection(DocumentSchema $document)
205
    {
206
        if ($document->getDatabase() != $this->schema->getDatabase()) {
207
            return false;
208
        }
209
210
        return $document->getCollection() == $this->schema->getCollection();
211
    }
212
213
    /**
214
     * Sort child documents in order or declared fields.
215
     *
216
     * @param DocumentSchema $childA
217
     * @param DocumentSchema $childB
218
     *
219
     * @return int
220
     */
221
    private function sortChildren(DocumentSchema $childA, DocumentSchema $childB)
222
    {
223
        return count($childA->getReflection()->getFields()) > count($childB->getReflection()->getFields());
224
    }
225
}