1
|
|
|
<?php |
2
|
|
|
/** |
3
|
|
|
* Spiral Framework. |
4
|
|
|
* |
5
|
|
|
* @license MIT |
6
|
|
|
* @author Anton Titov (Wolfy-J) |
7
|
|
|
* @copyright �2009-2015 |
8
|
|
|
*/ |
9
|
|
|
namespace Spiral\ODM\Entities; |
10
|
|
|
|
11
|
|
|
use Spiral\Core\Component; |
12
|
|
|
use Spiral\ODM\Configs\ODMConfig; |
13
|
|
|
use Spiral\ODM\Document; |
14
|
|
|
use Spiral\ODM\DocumentEntity; |
15
|
|
|
use Spiral\ODM\Entities\Schemas\CollectionSchema; |
16
|
|
|
use Spiral\ODM\Entities\Schemas\DocumentSchema; |
17
|
|
|
use Spiral\ODM\Exceptions\DefinitionException; |
18
|
|
|
use Spiral\ODM\Exceptions\SchemaException; |
19
|
|
|
use Spiral\ODM\ODM; |
20
|
|
|
use Spiral\Tokenizer\ClassLocatorInterface; |
21
|
|
|
|
22
|
|
|
/** |
23
|
|
|
* Schema builder responsible for static analysis of existed Documents, their schemas, validations, |
24
|
|
|
* requested indexes and etc. |
25
|
|
|
*/ |
26
|
|
|
class SchemaBuilder extends Component |
27
|
|
|
{ |
28
|
|
|
/** |
29
|
|
|
* @var DocumentSchema[] |
30
|
|
|
*/ |
31
|
|
|
private $documents = []; |
32
|
|
|
|
33
|
|
|
/** |
34
|
|
|
* @var CollectionSchema[] |
35
|
|
|
*/ |
36
|
|
|
private $collections = []; |
37
|
|
|
|
38
|
|
|
/** |
39
|
|
|
* @var ODMConfig |
40
|
|
|
*/ |
41
|
|
|
protected $config = null; |
42
|
|
|
|
43
|
|
|
/** |
44
|
|
|
* @invisible |
45
|
|
|
* @var ODM |
46
|
|
|
*/ |
47
|
|
|
protected $odm = null; |
48
|
|
|
|
49
|
|
|
/** |
50
|
|
|
* @param ODM $odm |
51
|
|
|
* @param ODMConfig $config |
52
|
|
|
* @param ClassLocatorInterface $locator |
53
|
|
|
*/ |
54
|
|
|
public function __construct(ODM $odm, ODMConfig $config, ClassLocatorInterface $locator) |
55
|
|
|
{ |
56
|
|
|
$this->config = $config; |
57
|
|
|
$this->odm = $odm; |
58
|
|
|
|
59
|
|
|
$this->locateDocuments($locator)->locateSources($locator); |
60
|
|
|
$this->describeCollections(); |
61
|
|
|
} |
62
|
|
|
|
63
|
|
|
/** |
64
|
|
|
* @return ODM |
65
|
|
|
*/ |
66
|
|
|
public function odm() |
67
|
|
|
{ |
68
|
|
|
return $this->odm; |
69
|
|
|
} |
70
|
|
|
|
71
|
|
|
/** |
72
|
|
|
* Resolve database alias. |
73
|
|
|
* |
74
|
|
|
* @param string $database |
75
|
|
|
* @return string |
76
|
|
|
*/ |
77
|
|
|
public function databaseAlias($database) |
78
|
|
|
{ |
79
|
|
|
if (empty($database)) { |
80
|
|
|
$database = $this->config->defaultDatabase(); |
81
|
|
|
} |
82
|
|
|
|
83
|
|
|
//Spiral support ability to link multiple virtual databases together using aliases |
84
|
|
|
return $this->config->resolveAlias($database); |
85
|
|
|
} |
86
|
|
|
|
87
|
|
|
/** |
88
|
|
|
* Check if Document class known to schema builder. |
89
|
|
|
* |
90
|
|
|
* @param string $class |
91
|
|
|
* @return bool |
92
|
|
|
*/ |
93
|
|
|
public function hasDocument($class) |
94
|
|
|
{ |
95
|
|
|
return isset($this->documents[$class]); |
96
|
|
|
} |
97
|
|
|
|
98
|
|
|
/** |
99
|
|
|
* Instance of DocumentSchema associated with given class name. |
100
|
|
|
* |
101
|
|
|
* @param string $class |
102
|
|
|
* @return DocumentSchema |
103
|
|
|
* @throws SchemaException |
104
|
|
|
*/ |
105
|
|
|
public function document($class) |
106
|
|
|
{ |
107
|
|
|
if ($class == DocumentEntity::class || $class == Document::class) { |
108
|
|
|
//No need to remember schema for abstract Document |
109
|
|
|
return new DocumentSchema($this, DocumentEntity::class); |
110
|
|
|
} |
111
|
|
|
|
112
|
|
|
if (!isset($this->documents[$class])) { |
113
|
|
|
throw new SchemaException("Unknown document class '{$class}'."); |
114
|
|
|
} |
115
|
|
|
|
116
|
|
|
return $this->documents[$class]; |
117
|
|
|
} |
118
|
|
|
|
119
|
|
|
/** |
120
|
|
|
* @return DocumentSchema[] |
121
|
|
|
*/ |
122
|
|
|
public function getDocuments() |
123
|
|
|
{ |
124
|
|
|
return $this->documents; |
125
|
|
|
} |
126
|
|
|
|
127
|
|
|
/** |
128
|
|
|
* @return CollectionSchema[] |
129
|
|
|
*/ |
130
|
|
|
public function getCollections() |
131
|
|
|
{ |
132
|
|
|
return $this->collections; |
133
|
|
|
} |
134
|
|
|
|
135
|
|
|
/** |
136
|
|
|
* Create every requested collection index. |
137
|
|
|
* |
138
|
|
|
* @throws \MongoException |
139
|
|
|
*/ |
140
|
|
|
public function createIndexes() |
141
|
|
|
{ |
142
|
|
|
foreach ($this->getCollections() as $collection) { |
143
|
|
|
if (empty($indexes = $collection->getIndexes())) { |
144
|
|
|
continue; |
145
|
|
|
} |
146
|
|
|
|
147
|
|
|
$odmCollection = $this->odm->database( |
148
|
|
|
$collection->getDatabase() |
149
|
|
|
)->selectCollection( |
150
|
|
|
$collection->getName() |
151
|
|
|
); |
152
|
|
|
|
153
|
|
|
foreach ($indexes as $index) { |
154
|
|
|
$options = []; |
155
|
|
|
if (isset($index[DocumentEntity::INDEX_OPTIONS])) { |
156
|
|
|
$options = $index[DocumentEntity::INDEX_OPTIONS]; |
157
|
|
|
unset($index[DocumentEntity::INDEX_OPTIONS]); |
158
|
|
|
} |
159
|
|
|
|
160
|
|
|
$odmCollection->createIndex($index, $options); |
161
|
|
|
} |
162
|
|
|
} |
163
|
|
|
} |
164
|
|
|
|
165
|
|
|
/** |
166
|
|
|
* Normalize document schema in lighter structure to be saved in ODM component memory. |
167
|
|
|
* |
168
|
|
|
* @return array |
169
|
|
|
* @throws SchemaException |
170
|
|
|
* @throws DefinitionException |
171
|
|
|
*/ |
172
|
|
|
public function normalizeSchema() |
173
|
|
|
{ |
174
|
|
|
$result = []; |
175
|
|
|
|
176
|
|
|
//Pre-packing collections |
177
|
|
|
foreach ($this->getCollections() as $collection) { |
178
|
|
|
$name = $collection->getDatabase() . '/' . $collection->getName(); |
179
|
|
|
$result[$name] = $collection->getParent()->getName(); |
180
|
|
|
} |
181
|
|
|
|
182
|
|
|
foreach ($this->getDocuments() as $document) { |
183
|
|
|
if ($document->isAbstract()) { |
184
|
|
|
continue; |
185
|
|
|
} |
186
|
|
|
|
187
|
|
|
$schema = [ |
188
|
|
|
ODM::D_DEFINITION => $this->packDefinition($document->classDefinition()), |
189
|
|
|
ODM::D_SOURCE => $document->getSource(), |
190
|
|
|
ODM::D_HIDDEN => $document->getHidden(), |
191
|
|
|
ODM::D_SECURED => $document->getSecured(), |
192
|
|
|
ODM::D_FILLABLE => $document->getFillable(), |
193
|
|
|
ODM::D_MUTATORS => $document->getMutators(), |
194
|
|
|
ODM::D_VALIDATES => $document->getValidates(), |
195
|
|
|
ODM::D_DEFAULTS => $document->getDefaults(), |
196
|
|
|
ODM::D_AGGREGATIONS => $this->packAggregations($document->getAggregations()), |
197
|
|
|
ODM::D_COMPOSITIONS => array_keys($document->getCompositions()) |
198
|
|
|
]; |
199
|
|
|
|
200
|
|
|
if (!$document->isEmbeddable()) { |
201
|
|
|
$schema[ODM::D_COLLECTION] = $document->getCollection(); |
202
|
|
|
$schema[ODM::D_DB] = $document->getDatabase(); |
203
|
|
|
} |
204
|
|
|
|
205
|
|
|
ksort($schema); |
206
|
|
|
$result[$document->getName()] = $schema; |
207
|
|
|
} |
208
|
|
|
|
209
|
|
|
return $result; |
210
|
|
|
} |
211
|
|
|
|
212
|
|
|
/** |
213
|
|
|
* Get all mutators associated with field type. |
214
|
|
|
* |
215
|
|
|
* @param string $type Field type. |
216
|
|
|
* @return array |
217
|
|
|
*/ |
218
|
|
|
public function getMutators($type) |
219
|
|
|
{ |
220
|
|
|
return $this->config->getMutators($type); |
221
|
|
|
} |
222
|
|
|
|
223
|
|
|
/** |
224
|
|
|
* Get mutator alias if presented. Aliases used to simplify schema definition. |
225
|
|
|
* |
226
|
|
|
* @param string $alias |
227
|
|
|
* @return string|array |
228
|
|
|
*/ |
229
|
|
|
public function mutatorAlias($alias) |
230
|
|
|
{ |
231
|
|
|
return $this->config->mutatorAlias($alias); |
232
|
|
|
} |
233
|
|
|
|
234
|
|
|
/** |
235
|
|
|
* Locate every available Document class. |
236
|
|
|
* |
237
|
|
|
* @param ClassLocatorInterface $locator |
238
|
|
|
* @return $this |
239
|
|
|
*/ |
240
|
|
|
protected function locateDocuments(ClassLocatorInterface $locator) |
241
|
|
|
{ |
242
|
|
|
foreach ($locator->getClasses(DocumentEntity::class) as $class => $definition) { |
243
|
|
|
if ($class == DocumentEntity::class || $class == Document::class) { |
244
|
|
|
continue; |
245
|
|
|
} |
246
|
|
|
|
247
|
|
|
$this->documents[$class] = new DocumentSchema($this, $class); |
248
|
|
|
} |
249
|
|
|
|
250
|
|
|
return $this; |
251
|
|
|
} |
252
|
|
|
|
253
|
|
|
/** |
254
|
|
|
* Locate ORM entities sources. |
255
|
|
|
* |
256
|
|
|
* @param ClassLocatorInterface $locator |
257
|
|
|
* @return $this |
258
|
|
|
*/ |
259
|
|
View Code Duplication |
protected function locateSources(ClassLocatorInterface $locator) |
|
|
|
|
260
|
|
|
{ |
261
|
|
|
foreach ($locator->getClasses(DocumentSource::class) as $class => $definition) { |
262
|
|
|
$reflection = new \ReflectionClass($class); |
263
|
|
|
|
264
|
|
|
if ( |
265
|
|
|
$reflection->isAbstract() |
266
|
|
|
|| empty($document = $reflection->getConstant('DOCUMENT')) |
267
|
|
|
) { |
268
|
|
|
continue; |
269
|
|
|
} |
270
|
|
|
|
271
|
|
|
if ($this->hasDocument($document)) { |
272
|
|
|
//Associating source with record |
273
|
|
|
$this->document($document)->setSource($class); |
274
|
|
|
} |
275
|
|
|
} |
276
|
|
|
|
277
|
|
|
return $this; |
278
|
|
|
} |
279
|
|
|
|
280
|
|
|
/** |
281
|
|
|
* Create instances of CollectionSchema associated with found DocumentSchema instances. |
282
|
|
|
* |
283
|
|
|
* @throws SchemaException |
284
|
|
|
*/ |
285
|
|
|
protected function describeCollections() |
286
|
|
|
{ |
287
|
|
|
foreach ($this->getDocuments() as $document) { |
288
|
|
|
if ($document->isEmbeddable()) { |
289
|
|
|
//Skip embedded models |
290
|
|
|
continue; |
291
|
|
|
} |
292
|
|
|
|
293
|
|
|
//Getting fully specified collection name (with specified db) |
294
|
|
|
$collection = $document->getDatabase() . '/' . $document->getCollection(); |
295
|
|
|
if (isset($this->collections[$collection])) { |
296
|
|
|
//Already described by parent |
297
|
|
|
continue; |
298
|
|
|
} |
299
|
|
|
|
300
|
|
|
//Collection must be described by parent Document |
301
|
|
|
$parent = $document->getParent(true); |
302
|
|
|
$this->collections[$collection] = new CollectionSchema($parent); |
303
|
|
|
} |
304
|
|
|
} |
305
|
|
|
|
306
|
|
|
/** |
307
|
|
|
* Pack (normalize) class definition. |
308
|
|
|
* |
309
|
|
|
* @param mixed $definition |
310
|
|
|
* @return array|string |
311
|
|
|
*/ |
312
|
|
|
private function packDefinition($definition) |
313
|
|
|
{ |
314
|
|
|
if (is_string($definition)) { |
315
|
|
|
//Single collection class |
316
|
|
|
return $definition; |
317
|
|
|
} |
318
|
|
|
|
319
|
|
|
return [ |
320
|
|
|
ODM::DEFINITION => $definition['type'], |
321
|
|
|
ODM::DEFINITION_OPTIONS => $definition['options'] |
322
|
|
|
]; |
323
|
|
|
} |
324
|
|
|
|
325
|
|
|
/** |
326
|
|
|
* Pack (normalize) document aggregations. |
327
|
|
|
* |
328
|
|
|
* @param array $aggregations |
329
|
|
|
* @return array |
330
|
|
|
*/ |
331
|
|
|
private function packAggregations(array $aggregations) |
332
|
|
|
{ |
333
|
|
|
$result = []; |
334
|
|
|
foreach ($aggregations as $name => $aggregation) { |
335
|
|
|
$result[$name] = [ |
336
|
|
|
ODM::AGR_TYPE => $aggregation['type'], |
337
|
|
|
ODM::ARG_CLASS => $aggregation['class'], |
338
|
|
|
ODM::AGR_QUERY => $aggregation['query'] |
339
|
|
|
]; |
340
|
|
|
} |
341
|
|
|
|
342
|
|
|
return $result; |
343
|
|
|
} |
344
|
|
|
} |
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.