1
|
|
|
<?php |
2
|
|
|
/** |
3
|
|
|
* Spiral Framework. |
4
|
|
|
* |
5
|
|
|
* @license MIT |
6
|
|
|
* @author Anton Titov (Wolfy-J) |
7
|
|
|
*/ |
8
|
|
|
namespace Spiral\ORM; |
9
|
|
|
|
10
|
|
|
use Spiral\Core\Component; |
11
|
|
|
use Spiral\Core\Container\SingletonInterface; |
12
|
|
|
use Spiral\Core\FactoryInterface; |
13
|
|
|
use Spiral\Core\HippocampusInterface; |
14
|
|
|
use Spiral\Database\DatabaseManager; |
15
|
|
|
use Spiral\Database\Entities\Database; |
16
|
|
|
use Spiral\Debug\Traits\LoggerTrait; |
17
|
|
|
use Spiral\Models\DataEntity; |
18
|
|
|
use Spiral\Models\SchematicEntity; |
19
|
|
|
use Spiral\ORM\Configs\ORMConfig; |
20
|
|
|
use Spiral\ORM\Entities\Loader; |
21
|
|
|
use Spiral\ORM\Entities\RecordSelector; |
22
|
|
|
use Spiral\ORM\Entities\RecordSource; |
23
|
|
|
use Spiral\ORM\Entities\SchemaBuilder; |
24
|
|
|
use Spiral\ORM\Entities\Schemas\RecordSchema; |
25
|
|
|
use Spiral\ORM\Exceptions\ORMException; |
26
|
|
|
use Spiral\Tokenizer\ClassLocatorInterface; |
27
|
|
|
|
28
|
|
|
/** |
29
|
|
|
* ORM component used to manage state of cached Record's schema, record creation and schema |
30
|
|
|
* analysis. |
31
|
|
|
* |
32
|
|
|
* Attention, do not forget to reset cache between requests. |
33
|
|
|
* |
34
|
|
|
* @todo Think about using views for complex queries? Using views for entities? ViewRecord? |
35
|
|
|
* @todo ability to merge multiple tables into one entity - like SearchEntity? Partial entities? |
36
|
|
|
* |
37
|
|
|
* @todo think about entity cache and potential use cases when model can be accessed from outside |
38
|
|
|
*/ |
39
|
|
|
class ORM extends Component implements SingletonInterface |
40
|
|
|
{ |
41
|
|
|
use LoggerTrait; |
42
|
|
|
|
43
|
|
|
/** |
44
|
|
|
* Declares to IoC that component instance should be treated as singleton. |
45
|
|
|
*/ |
46
|
|
|
const SINGLETON = self::class; |
47
|
|
|
|
48
|
|
|
/** |
49
|
|
|
* Memory section to store ORM schema. |
50
|
|
|
*/ |
51
|
|
|
const MEMORY = 'orm.schema'; |
52
|
|
|
|
53
|
|
|
/** |
54
|
|
|
* Normalized record constants. |
55
|
|
|
*/ |
56
|
|
|
const M_ROLE_NAME = 0; |
57
|
|
|
const M_SOURCE = 1; |
58
|
|
|
const M_TABLE = 2; |
59
|
|
|
const M_DB = 3; |
60
|
|
|
const M_HIDDEN = SchematicEntity::SH_HIDDEN; |
61
|
|
|
const M_SECURED = SchematicEntity::SH_SECURED; |
62
|
|
|
const M_FILLABLE = SchematicEntity::SH_FILLABLE; |
63
|
|
|
const M_MUTATORS = SchematicEntity::SH_MUTATORS; |
64
|
|
|
const M_VALIDATES = SchematicEntity::SH_VALIDATES; |
65
|
|
|
const M_COLUMNS = 9; |
66
|
|
|
const M_NULLABLE = 10; |
67
|
|
|
const M_RELATIONS = 11; |
68
|
|
|
const M_PRIMARY_KEY = 12; |
69
|
|
|
|
70
|
|
|
/** |
71
|
|
|
* Normalized relation options. |
72
|
|
|
*/ |
73
|
|
|
const R_TYPE = 0; |
74
|
|
|
const R_TABLE = 1; |
75
|
|
|
const R_DEFINITION = 2; |
76
|
|
|
const R_DATABASE = 3; |
77
|
|
|
|
78
|
|
|
/** |
79
|
|
|
* Pivot table data location in Record fields. Pivot data only provided when record is loaded |
80
|
|
|
* using many-to-many relation. |
81
|
|
|
*/ |
82
|
|
|
const PIVOT_DATA = '@pivot'; |
83
|
|
|
|
84
|
|
|
/** |
85
|
|
|
* @var EntityCache |
86
|
|
|
*/ |
87
|
|
|
private $cache = null; |
88
|
|
|
|
89
|
|
|
/** |
90
|
|
|
* @var ORMConfig |
91
|
|
|
*/ |
92
|
|
|
protected $config = null; |
93
|
|
|
|
94
|
|
|
/** |
95
|
|
|
* Cached records schema. |
96
|
|
|
* |
97
|
|
|
* @var array|null |
98
|
|
|
*/ |
99
|
|
|
protected $schema = null; |
100
|
|
|
|
101
|
|
|
/** |
102
|
|
|
* @invisible |
103
|
|
|
* @var DatabaseManager |
104
|
|
|
*/ |
105
|
|
|
protected $databases = null; |
106
|
|
|
|
107
|
|
|
/** |
108
|
|
|
* @invisible |
109
|
|
|
* @var FactoryInterface |
110
|
|
|
*/ |
111
|
|
|
protected $factory = null; |
112
|
|
|
|
113
|
|
|
/** |
114
|
|
|
* @invisible |
115
|
|
|
* @var HippocampusInterface |
116
|
|
|
*/ |
117
|
|
|
protected $memory = null; |
118
|
|
|
|
119
|
|
|
/** |
120
|
|
|
* @param ORMConfig $config |
121
|
|
|
* @param EntityCache $cache |
122
|
|
|
* @param HippocampusInterface $memory |
123
|
|
|
* @param DatabaseManager $databases |
124
|
|
|
* @param FactoryInterface $factory |
125
|
|
|
*/ |
126
|
|
|
public function __construct( |
127
|
|
|
ORMConfig $config, |
128
|
|
|
EntityCache $cache, |
129
|
|
|
HippocampusInterface $memory, |
130
|
|
|
DatabaseManager $databases, |
131
|
|
|
FactoryInterface $factory |
132
|
|
|
) { |
133
|
|
|
$this->config = $config; |
134
|
|
|
$this->memory = $memory; |
135
|
|
|
|
136
|
|
|
$this->cache = $cache; |
137
|
|
|
|
138
|
|
|
$this->schema = (array)$memory->loadData(static::MEMORY); |
139
|
|
|
|
140
|
|
|
$this->databases = $databases; |
141
|
|
|
$this->factory = $factory; |
142
|
|
|
} |
143
|
|
|
|
144
|
|
|
/** |
145
|
|
|
* @param EntityCache $cache |
146
|
|
|
* @return $this |
147
|
|
|
*/ |
148
|
|
|
public function setCache(EntityCache $cache) |
149
|
|
|
{ |
150
|
|
|
$this->cache = $cache; |
151
|
|
|
|
152
|
|
|
return $this; |
153
|
|
|
} |
154
|
|
|
|
155
|
|
|
/** |
156
|
|
|
* @return EntityCache |
157
|
|
|
*/ |
158
|
|
|
public function cache() |
159
|
|
|
{ |
160
|
|
|
return $this->cache; |
161
|
|
|
} |
162
|
|
|
|
163
|
|
|
/** |
164
|
|
|
* When ORM is cloned we are automatically flushing it's cache and creating new isolated area. |
165
|
|
|
* Basically we have cache enabled per selection. |
166
|
|
|
* |
167
|
|
|
* @see RecordSelector::getIterator() |
168
|
|
|
*/ |
169
|
|
|
public function __clone() |
170
|
|
|
{ |
171
|
|
|
$this->cache = clone $this->cache; |
172
|
|
|
|
173
|
|
|
if (!$this->cache->isEnabled()) { |
174
|
|
|
$this->logger()->warning("ORM are cloned with disabled state."); |
175
|
|
|
} |
176
|
|
|
} |
177
|
|
|
|
178
|
|
|
/** |
179
|
|
|
* Get database by it's name from DatabaseManager associated with ORM component. |
180
|
|
|
* |
181
|
|
|
* @param string $database |
182
|
|
|
* @return Database |
183
|
|
|
*/ |
184
|
|
|
public function database($database) |
185
|
|
|
{ |
186
|
|
|
return $this->databases->database($database); |
187
|
|
|
} |
188
|
|
|
|
189
|
|
|
/** |
190
|
|
|
* Construct instance of Record or receive it from cache (if enabled). Only records with |
191
|
|
|
* declared primary key can be cached. |
192
|
|
|
* |
193
|
|
|
* @param string $class Record class name. |
194
|
|
|
* @param array $data |
195
|
|
|
* @param bool $cache Add record to entity cache if enabled. |
196
|
|
|
* @return RecordInterface |
197
|
|
|
*/ |
198
|
|
|
public function record($class, array $data = [], $cache = true) |
199
|
|
|
{ |
200
|
|
|
$schema = $this->schema($class); |
201
|
|
|
|
202
|
|
|
if (!$this->cache->isEnabled() || !$cache) { |
203
|
|
|
//Entity cache is disabled, we can create record right now |
204
|
|
|
return new $class($data, !empty($data), $this, $schema); |
205
|
|
|
} |
206
|
|
|
|
207
|
|
|
//We have to find unique object criteria (will work for objects with primary key only) |
208
|
|
|
$primaryKey = null; |
209
|
|
|
|
210
|
|
|
if ( |
211
|
|
|
!empty($schema[self::M_PRIMARY_KEY]) |
212
|
|
|
&& !empty($data[$schema[self::M_PRIMARY_KEY]]) |
213
|
|
|
) { |
214
|
|
|
$primaryKey = $data[$schema[self::M_PRIMARY_KEY]]; |
215
|
|
|
} |
216
|
|
|
|
217
|
|
|
if ($this->cache->has($class, $primaryKey)) { |
218
|
|
|
/** |
219
|
|
|
* @var RecordInterface $entity |
220
|
|
|
*/ |
221
|
|
|
return $this->cache->get($class, $primaryKey); |
222
|
|
|
} |
223
|
|
|
|
224
|
|
|
return $this->cache->remember( |
225
|
|
|
new $class($data, !empty($data), $this, $schema) |
226
|
|
|
); |
227
|
|
|
} |
228
|
|
|
|
229
|
|
|
/** |
230
|
|
|
* Get ORM source for given class. |
231
|
|
|
* |
232
|
|
|
* @param string $class |
233
|
|
|
* @return RecordSource |
234
|
|
|
* @throws ORMException |
235
|
|
|
*/ |
236
|
|
View Code Duplication |
public function source($class) |
|
|
|
|
237
|
|
|
{ |
238
|
|
|
$schema = $this->schema($class); |
239
|
|
|
if (empty($source = $schema[self::M_SOURCE])) { |
240
|
|
|
//Default source |
241
|
|
|
$source = RecordSource::class; |
242
|
|
|
} |
243
|
|
|
|
244
|
|
|
return new $source($class, $this); |
245
|
|
|
} |
246
|
|
|
|
247
|
|
|
/** |
248
|
|
|
* Get ORM selector for given class. |
249
|
|
|
* |
250
|
|
|
* @param string $class |
251
|
|
|
* @param Loader $loader |
252
|
|
|
* @return RecordSelector |
253
|
|
|
*/ |
254
|
|
|
public function selector($class, Loader $loader = null) |
255
|
|
|
{ |
256
|
|
|
return new RecordSelector($class, $this, $loader); |
257
|
|
|
} |
258
|
|
|
|
259
|
|
|
/** |
260
|
|
|
* Get cached schema for specified record by it's name. |
261
|
|
|
* |
262
|
|
|
* @param string $record |
263
|
|
|
* @return array |
264
|
|
|
* @throws ORMException |
265
|
|
|
*/ |
266
|
|
View Code Duplication |
public function schema($record) |
|
|
|
|
267
|
|
|
{ |
268
|
|
|
if (!isset($this->schema[$record])) { |
269
|
|
|
$this->updateSchema(); |
270
|
|
|
} |
271
|
|
|
|
272
|
|
|
if (!isset($this->schema[$record])) { |
273
|
|
|
throw new ORMException("Undefined ORM schema item, unknown record '{$record}'."); |
274
|
|
|
} |
275
|
|
|
|
276
|
|
|
return $this->schema[$record]; |
277
|
|
|
} |
278
|
|
|
|
279
|
|
|
/** |
280
|
|
|
* Create record relation instance by given relation type, parent and definition (options). |
281
|
|
|
* |
282
|
|
|
* @param int $type |
283
|
|
|
* @param RecordInterface $parent |
284
|
|
|
* @param array $definition Relation definition. |
285
|
|
|
* @param array $data |
286
|
|
|
* @param bool $loaded |
287
|
|
|
* @return RelationInterface |
288
|
|
|
* @throws ORMException |
289
|
|
|
*/ |
290
|
|
View Code Duplication |
public function relation( |
|
|
|
|
291
|
|
|
$type, |
292
|
|
|
RecordInterface $parent, |
293
|
|
|
$definition, |
294
|
|
|
$data = null, |
295
|
|
|
$loaded = false |
296
|
|
|
) { |
297
|
|
|
if (!$this->config->hasRelation($type, 'class')) { |
298
|
|
|
throw new ORMException("Undefined relation type '{$type}'."); |
299
|
|
|
} |
300
|
|
|
|
301
|
|
|
$class = $this->config->relationClass($type, 'class'); |
302
|
|
|
|
303
|
|
|
//For performance reasons class constructed without container |
304
|
|
|
return new $class($this, $parent, $definition, $data, $loaded); |
305
|
|
|
} |
306
|
|
|
|
307
|
|
|
/** |
308
|
|
|
* Get instance of relation/selection loader based on relation type and definition. |
309
|
|
|
* |
310
|
|
|
* @param int $type Relation type. |
311
|
|
|
* @param string $container Container related to parent loader. |
312
|
|
|
* @param array $definition Relation definition. |
313
|
|
|
* @param Loader $parent Parent loader (if presented). |
314
|
|
|
* @return LoaderInterface |
315
|
|
|
* @throws ORMException |
316
|
|
|
*/ |
317
|
|
View Code Duplication |
public function loader($type, $container, array $definition, Loader $parent = null) |
|
|
|
|
318
|
|
|
{ |
319
|
|
|
if (!$this->config->hasRelation($type, 'loader')) { |
320
|
|
|
throw new ORMException("Undefined relation loader '{$type}'."); |
321
|
|
|
} |
322
|
|
|
|
323
|
|
|
$class = $this->config->relationClass($type, 'loader'); |
324
|
|
|
|
325
|
|
|
//For performance reasons class constructed without container |
326
|
|
|
return new $class($this, $container, $definition, $parent); |
327
|
|
|
} |
328
|
|
|
|
329
|
|
|
/** |
330
|
|
|
* Update ORM records schema, synchronize declared and database schemas and return instance of |
331
|
|
|
* SchemaBuilder. |
332
|
|
|
* |
333
|
|
|
* @param SchemaBuilder $builder User specified schema builder. |
334
|
|
|
* @param bool $syncronize Create all required tables and columns |
335
|
|
|
* @return SchemaBuilder |
336
|
|
|
*/ |
337
|
|
View Code Duplication |
public function updateSchema(SchemaBuilder $builder = null, $syncronize = false) |
|
|
|
|
338
|
|
|
{ |
339
|
|
|
if (empty($builder)) { |
340
|
|
|
$builder = $this->schemaBuilder(); |
341
|
|
|
} |
342
|
|
|
|
343
|
|
|
//Create all required tables and columns |
344
|
|
|
if ($syncronize) { |
345
|
|
|
$builder->synchronizeSchema(); |
346
|
|
|
} |
347
|
|
|
|
348
|
|
|
//Getting normalized (cached) version of schema |
349
|
|
|
$this->schema = $builder->normalizeSchema(); |
350
|
|
|
|
351
|
|
|
//Saving |
352
|
|
|
$this->memory->saveData(static::MEMORY, $this->schema); |
353
|
|
|
|
354
|
|
|
//Let's reinitialize records |
355
|
|
|
DataEntity::resetInitiated(); |
356
|
|
|
|
357
|
|
|
return $builder; |
358
|
|
|
} |
359
|
|
|
|
360
|
|
|
/** |
361
|
|
|
* Get instance of ORM SchemaBuilder. |
362
|
|
|
* |
363
|
|
|
* @param ClassLocatorInterface $locator |
364
|
|
|
* @return SchemaBuilder |
365
|
|
|
*/ |
366
|
|
|
public function schemaBuilder(ClassLocatorInterface $locator = null) |
367
|
|
|
{ |
368
|
|
|
return $this->factory->make(SchemaBuilder::class, [ |
369
|
|
|
'config' => $this->config, |
370
|
|
|
'orm' => $this, |
371
|
|
|
'locator' => $locator |
372
|
|
|
]); |
373
|
|
|
} |
374
|
|
|
|
375
|
|
|
/** |
376
|
|
|
* Create instance of relation schema based on relation type and given definition (declared in |
377
|
|
|
* record). Resolve using container to support any possible relation type. You can create your |
378
|
|
|
* own relations, loaders and schemas by altering ORM config. |
379
|
|
|
* |
380
|
|
|
* @param mixed $type |
381
|
|
|
* @param SchemaBuilder $builder |
382
|
|
|
* @param RecordSchema $record |
383
|
|
|
* @param string $name |
384
|
|
|
* @param array $definition |
385
|
|
|
* @return Schemas\RelationInterface |
386
|
|
|
*/ |
387
|
|
|
public function relationSchema( |
388
|
|
|
$type, |
389
|
|
|
SchemaBuilder $builder, |
390
|
|
|
RecordSchema $record, |
391
|
|
|
$name, |
392
|
|
|
array $definition |
393
|
|
|
) { |
394
|
|
|
if (!$this->config->hasRelation($type, 'schema')) { |
395
|
|
|
throw new ORMException("Undefined relation schema '{$type}'."); |
396
|
|
|
} |
397
|
|
|
|
398
|
|
|
//Getting needed relation schema builder |
399
|
|
|
return $this->factory->make( |
400
|
|
|
$this->config->relationClass($type, 'schema'), |
401
|
|
|
compact('builder', 'record', 'name', 'definition') |
402
|
|
|
); |
403
|
|
|
} |
404
|
|
|
} |
405
|
|
|
|
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.