Completed
Push — master ( a8cb34...ce5009 )
by Anton
03:37
created

ORM::setSchema()   A

Complexity

Conditions 2
Paths 2

Size

Total Lines 8
Code Lines 4

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 2
eloc 4
nc 2
nop 2
dl 0
loc 8
rs 9.4285
c 0
b 0
f 0
1
<?php
2
/**
3
 * Spiral, Core Components
4
 *
5
 * @author Wolfy-J
6
 */
7
8
namespace Spiral\ORM;
9
10
use Interop\Container\ContainerInterface;
11
use Spiral\Core\Component;
12
use Spiral\Core\Container;
13
use Spiral\Core\Container\SingletonInterface;
14
use Spiral\Core\FactoryInterface;
15
use Spiral\Core\MemoryInterface;
16
use Spiral\Core\NullMemory;
17
use Spiral\Database\DatabaseManager;
18
use Spiral\Database\Entities\Database;
19
use Spiral\Database\Entities\Table;
20
use Spiral\ORM\Configs\RelationsConfig;
21
use Spiral\ORM\Entities\RecordSelector;
22
use Spiral\ORM\Entities\RecordSource;
23
use Spiral\ORM\Exceptions\ORMException;
24
use Spiral\ORM\Exceptions\SchemaException;
25
use Spiral\ORM\Schemas\LocatorInterface;
26
use Spiral\ORM\Schemas\NullLocator;
27
use Spiral\ORM\Schemas\SchemaBuilder;
28
29
class ORM extends Component implements ORMInterface, SingletonInterface
30
{
31
    /**
32
     * Memory section to store ORM schema.
33
     */
34
    const MEMORY = 'orm.schema';
35
36
    /**
37
     * @invisible
38
     * @var EntityMap|null
39
     */
40
    private $map = null;
41
42
    /**
43
     * @var LocatorInterface
44
     */
45
    private $locator;
46
47
    /**
48
     * Already created instantiators.
49
     *
50
     * @invisible
51
     * @var InstantiatorInterface[]
52
     */
53
    private $instantiators = [];
54
55
    /**
56
     * ORM schema.
57
     *
58
     * @invisible
59
     * @var array
60
     */
61
    private $schema = [];
62
63
    /**
64
     * @var DatabaseManager
65
     */
66
    protected $manager;
67
68
    /**
69
     * @var RelationsConfig
70
     */
71
    protected $config;
72
73
    /**
74
     * @invisible
75
     * @var MemoryInterface
76
     */
77
    protected $memory;
78
79
    /**
80
     * Container defines working scope for all Documents and DocumentEntities.
81
     *
82
     * @var ContainerInterface
83
     */
84
    protected $container;
85
86
    /**
87
     * @param DatabaseManager         $manager
88
     * @param RelationsConfig         $config
89
     * @param LocatorInterface|null   $locator
90
     * @param EntityMap|null          $map
91
     * @param MemoryInterface|null    $memory
92
     * @param ContainerInterface|null $container
93
     */
94
    public function __construct(
95
        DatabaseManager $manager,
96
        RelationsConfig $config,
97
        //Following arguments can be resolved automatically
98
        LocatorInterface $locator = null,
99
        EntityMap $map = null,
100
        MemoryInterface $memory = null,
101
        ContainerInterface $container = null
102
    ) {
103
        $this->manager = $manager;
104
        $this->config = $config;
105
106
        //If null is passed = no caching is expected
107
        $this->map = $map;
108
109
        $this->locator = $locator ?? new NullLocator();
110
        $this->memory = $memory ?? new NullMemory();
111
        $this->container = $container ?? new Container();
112
113
        //Loading schema from memory (if any)
114
        $this->schema = $this->loadSchema();
115
    }
116
117
    /**
118
     * {@inheritdoc}
119
     */
120
    public function hasMap(): bool
121
    {
122
        return !empty($this->map);
123
    }
124
125
    /**
126
     * {@inheritdoc}
127
     */
128
    public function getMap(): EntityMap
129
    {
130
        if (empty($this->map)) {
131
            throw new ORMException("Attempts to access entity map in mapless mode");
132
        }
133
134
        return $this->map;
135
    }
136
137
    /**
138
     * {@inheritdoc}
139
     */
140
    public function withMap(EntityMap $map = null): ORMInterface
141
    {
142
        $orm = clone $this;
143
        $orm->map = $map;
144
145
        return $orm;
146
    }
147
148
    /**
149
     * Create instance of ORM SchemaBuilder.
150
     *
151
     * @param bool $locate Set to true to automatically locate available records and record sources
152
     *                     sources in a project files (based on tokenizer scope).
153
     *
154
     * @return SchemaBuilder
155
     *
156
     * @throws SchemaException
157
     */
158
    public function schemaBuilder(bool $locate = true): SchemaBuilder
159
    {
160
        /**
161
         * @var SchemaBuilder $builder
162
         */
163
        $builder = $this->getFactory()->make(SchemaBuilder::class, ['manager' => $this->manager]);
164
165
        if ($locate) {
166
            foreach ($this->locator->locateSchemas() as $schema) {
167
                $builder->addSchema($schema);
168
            }
169
170
            foreach ($this->locator->locateSources() as $class => $source) {
171
                $builder->addSource($class, $source);
172
            }
173
        }
174
175
        return $builder;
176
    }
177
178
    /**
179
     * Specify behaviour schema for ORM to be used. Attention, you have to call renderSchema()
180
     * prior to passing builder into this method.
181
     *
182
     * @param SchemaBuilder $builder
183
     * @param bool          $remember Set to true to remember packed schema in memory.
184
     */
185
    public function setSchema(SchemaBuilder $builder, bool $remember = false)
186
    {
187
        $this->schema = $builder->packSchema();
188
189
        if ($remember) {
190
            $this->memory->saveData(static::MEMORY, $this->schema);
191
        }
192
    }
193
194
    /**
195
     * {@inheritdoc}
196
     */
197
    public function define(string $class, int $property)
198
    {
199
        if (empty($this->schema)) {
200
            $this->setSchema($this->schemaBuilder()->renderSchema(), true);
201
        }
202
203
        //Check value
204
        if (!isset($this->schema[$class])) {
205
            throw new ORMException("Undefined ORM schema item '{$class}', make sure schema is updated");
206
        }
207
208
        if (!array_key_exists($property, $this->schema[$class])) {
209
            throw new ORMException("Undefined ORM schema property '{$class}'.'{$property}'");
210
        }
211
212
        return $this->schema[$class][$property];
213
    }
214
215
    /**
216
     * Get source (selection repository) for specific entity class.
217
     *
218
     * @param string $class
219
     *
220
     * @return RecordSource
221
     */
222
    public function source(string $class): RecordSource
223
    {
224
        $source = $this->define($class, self::R_SOURCE_CLASS);
225
226
        if (empty($source)) {
227
            //Let's use default source
228
            $source = RecordSource::class;
229
        }
230
231
        $handles = $source::RECORD;
232
        if (empty($handles)) {
233
            //Force class to be handled
234
            $handles = $class;
235
        }
236
237
        return $this->getFactory()->make($source, [
238
            'class' => $handles,
239
            'orm'   => $this
240
        ]);
241
    }
242
243
    /**
244
     * {@inheritdoc}
245
     *
246
     * @param bool $isolated Set to true (by default) to create new isolated entity map for
247
     *                       selection.
248
     */
249
    public function selector(string $class, bool $isolated = true): RecordSelector
250
    {
251
        //ORM is cloned in order to isolate cache scope.
252
        return new RecordSelector($class, $isolated ? clone $this : $this);
253
    }
254
255
    /**
256
     * {@inheritdoc}
257
     */
258
    public function table(string $class): Table
259
    {
260
        return $this->manager->database(
261
            $this->define($class, self::R_DATABASE)
262
        )->table(
263
            $this->define($class, self::R_TABLE)
264
        );
265
    }
266
267
    /**
268
     * {@inheritdoc}
269
     */
270
    public function database(string $alias = null): Database
271
    {
272
        return $this->manager->database($alias);
273
    }
274
275
    /**
276
     * {@inheritdoc}
277
     */
278
    public function make(
279
        string $class,
280
        $fields = [],
281
        int $state = self::STATE_NEW,
282
        bool $cache = true
283
    ): RecordInterface {
284
        $instantiator = $this->instantiator($class);
285
286
        if ($state == self::STATE_NEW) {
287
            //No caching for entities created with user input
288
            $cache = false;
289
        }
290
291
        if ($fields instanceof \Traversable) {
292
            $fields = iterator_to_array($fields);
293
        }
294
295
        if (!$cache || !$this->hasMap()) {
296
            return $instantiator->make($fields, $state);
297
        }
298
299
        //Always expect PK in our records
300
        if (
301
            !isset($fields[$this->define($class, self::R_PRIMARY_KEY)])
302
            || empty($identity = $fields[$this->define($class, self::R_PRIMARY_KEY)])
303
        ) {
304
            //Unable to cache non identified instance
305
            return $instantiator->make($fields, $state);
306
        }
307
308
        if ($this->map->has($class, $identity)) {
309
            return $this->map->get($class, $identity);
310
        }
311
312
        //Storing entity in a cache right after creating it
313
        return $this->map->remember($instantiator->make($fields, $state));
314
    }
315
316
    /**
317
     * {@inheritdoc}
318
     */
319
    public function makeLoader(string $class, string $relation): LoaderInterface
320
    {
321
        $schema = $this->define($class, self::R_RELATIONS);
322
323
        if (!isset($schema[$relation])) {
324
            throw new ORMException("Undefined relation '{$class}'.'{$relation}'");
325
        }
326
327
        $schema = $schema[$relation];
328
329
        if (!$this->config->hasRelation($schema[self::R_TYPE])) {
330
            throw new ORMException("Undefined relation type '{$schema[self::R_TYPE]}'");
331
        }
332
333
        //Generating relation
334
        return $this->getFactory()->make(
335
            $this->config->relationClass($schema[self::R_TYPE], RelationsConfig::LOADER_CLASS),
336
            [
337
                'class'    => $schema[self::R_CLASS],
338
                'relation' => $relation,
339
                'schema'   => $schema[self::R_SCHEMA],
340
                'orm'      => $this
341
            ]
342
        );
343
    }
344
345
    /**
346
     * {@inheritdoc}
347
     */
348
    public function makeRelation(string $class, string $relation): RelationInterface
349
    {
350
        $schema = $this->define($class, self::R_RELATIONS);
351
352
        if (!isset($schema[$relation])) {
353
            throw new ORMException("Undefined relation '{$class}'.'{$relation}'");
354
        }
355
356
        $schema = $schema[$relation];
357
358
        if (!$this->config->hasRelation($schema[self::R_TYPE], RelationsConfig::ACCESS_CLASS)) {
359
            throw new ORMException("Undefined relation type '{$schema[self::R_TYPE]}'");
360
        }
361
362
        //Generating relation (might require performance improvements)
363
        return $this->getFactory()->make(
364
            $this->config->relationClass($schema[self::R_TYPE], RelationsConfig::ACCESS_CLASS),
365
            [
366
                'class'  => $schema[self::R_CLASS],
367
                'schema' => $schema[self::R_SCHEMA],
368
                'orm'    => $this
369
            ]
370
        );
371
    }
372
373
    /**
374
     * When ORM is cloned we are automatically cloning it's cache as well to create
375
     * new isolated area. Basically we have cache enabled per selection.
376
     *
377
     * @see RecordSelector::getIterator()
378
     */
379
    public function __clone()
380
    {
381
        //Each ORM clone must have isolated entity cache/map
382
        if (!empty($this->map)) {
383
            $this->map = clone $this->map;
384
        }
385
    }
386
387
    /**
388
     * Get object responsible for class instantiation.
389
     *
390
     * @param string $class
391
     *
392
     * @return InstantiatorInterface
393
     */
394
    protected function instantiator(string $class): InstantiatorInterface
395
    {
396
        if (isset($this->instantiators[$class])) {
397
            return $this->instantiators[$class];
398
        }
399
400
        //Potential optimization
401
        $instantiator = $this->getFactory()->make(
402
            $this->define($class, self::R_INSTANTIATOR),
403
            [
404
                'class'  => $class,
405
                'orm'    => $this,
406
                'schema' => $this->define($class, self::R_SCHEMA)
407
            ]
408
        );
409
410
        //Constructing instantiator and storing it in cache
411
        return $this->instantiators[$class] = $instantiator;
412
    }
413
414
    /**
415
     * Load packed schema from memory.
416
     *
417
     * @return array
418
     */
419
    protected function loadSchema(): array
420
    {
421
        return (array)$this->memory->loadData(static::MEMORY);
422
    }
423
424
    /**
425
     * Get ODM specific factory.
426
     *
427
     * @return FactoryInterface
428
     */
429
    protected function getFactory(): FactoryInterface
430
    {
431
        if ($this->container instanceof FactoryInterface) {
432
            return $this->container;
433
        }
434
435
        return $this->container->get(FactoryInterface::class);
436
    }
437
}
438