Completed
Branch feature/pre-split (0a985a)
by Anton
05:37
created

ORM::getMap()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 4
Code Lines 2

Duplication

Lines 0
Ratio 0 %

Importance

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