Completed
Branch feature/pre-split (4ff102)
by Anton
03:27
created

ORM::instantiator()   A

Complexity

Conditions 2
Paths 2

Size

Total Lines 19
Code Lines 9

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 2
eloc 9
nc 2
nop 1
dl 0
loc 19
rs 9.4285
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
     * Create version of ORM with different initial map or disable caching.
117
     *
118
     * @param EntityMap|null $map
119
     *
120
     * @return ORM
121
     */
122
    public function withMap(EntityMap $map = null): ORM
123
    {
124
        $orm = clone $this;
125
        $orm->map = $map;
126
127
        return $orm;
128
    }
129
130
    /**
131
     * Check if ORM has associated entity cache.
132
     *
133
     * @return bool
134
     */
135
    public function hasMap(): bool
136
    {
137
        return !empty($this->map);
138
    }
139
140
    /**
141
     * Create instance of ORM SchemaBuilder.
142
     *
143
     * @param bool $locate Set to true to automatically locate available records and record sources
144
     *                     sources in a project files (based on tokenizer scope).
145
     *
146
     * @return SchemaBuilder
147
     *
148
     * @throws SchemaException
149
     */
150
    public function schemaBuilder(bool $locate = true): SchemaBuilder
151
    {
152
        /**
153
         * @var SchemaBuilder $builder
154
         */
155
        $builder = $this->getFactory()->make(SchemaBuilder::class, ['manager' => $this->manager]);
156
157
        if ($locate) {
158
            foreach ($this->locator->locateSchemas() as $schema) {
159
                $builder->addSchema($schema);
160
            }
161
162
            foreach ($this->locator->locateSources() as $class => $source) {
163
                $builder->addSource($class, $source);
164
            }
165
        }
166
167
        return $builder;
168
    }
169
170
    /**
171
     * Specify behaviour schema for ORM to be used. Attention, you have to call renderSchema()
172
     * prior to passing builder into this method.
173
     *
174
     * @param SchemaBuilder $builder
175
     * @param bool          $remember Set to true to remember packed schema in memory.
176
     */
177
    public function buildSchema(SchemaBuilder $builder, bool $remember = false)
178
    {
179
        $this->schema = $builder->packSchema();
180
181
        if ($remember) {
182
            $this->memory->saveData(static::MEMORY, $this->schema);
183
        }
184
    }
185
186
    /**
187
     * {@inheritdoc}
188
     */
189
    public function define(string $class, int $property)
190
    {
191
        if (empty($this->schema)) {
192
            $this->buildSchema($this->schemaBuilder()->renderSchema(), true);
193
        }
194
195
        //Check value
196
        if (!isset($this->schema[$class])) {
197
            throw new ORMException("Undefined ORM schema item '{$class}', make sure schema is updated");
198
        }
199
200
        if (!array_key_exists($property, $this->schema[$class])) {
201
            throw new ORMException("Undefined ORM schema property '{$class}'.'{$property}'");
202
        }
203
204
        return $this->schema[$class][$property];
205
    }
206
207
    /**
208
     * Get source (selection repository) for specific entity class.
209
     *
210
     * @param string $class
211
     *
212
     * @return RecordSource
213
     */
214
    public function source(string $class): RecordSource
215
    {
216
        $source = $this->define($class, self::R_SOURCE_CLASS);
217
218
        if (empty($source)) {
219
            //Let's use default source
220
            $source = RecordSource::class;
221
        }
222
223
        $handles = $source::RECORD;
224
        if (empty($handles)) {
225
            //Force class to be handled
226
            $handles = $class;
227
        }
228
229
        return $this->getFactory()->make($source, [
230
            'class' => $handles,
231
            'odm'   => $this
232
        ]);
233
    }
234
235
    /**
236
     * {@inheritdoc}
237
     */
238
    public function selector(string $class): RecordSelector
239
    {
240
        //ORM is cloned in order to isolate cache scope.
241
        return new RecordSelector($class, clone $this);
242
    }
243
244
    /**
245
     * {@inheritdoc}
246
     */
247
    public function table(string $class): Table
248
    {
249
        return $this->manager->database(
250
            $this->define($class, self::R_DATABASE)
251
        )->table(
252
            $this->define($class, self::R_TABLE)
253
        );
254
    }
255
256
    /**
257
     * {@inheritdoc}
258
     */
259
    public function make(
260
        string $class,
261
        $fields = [],
262
        int $state = self::STATE_NEW,
263
        bool $cache = true
264
    ): RecordInterface {
265
        $instantiator = $this->instantiator($class);
266
267
        if ($state == self::STATE_NEW) {
268
            //No caching for entities created with user input
269
            $cache = false;
270
        }
271
272
        if ($fields instanceof \Traversable) {
273
            $fields = iterator_to_array($fields);
274
        }
275
276
        if (!$cache || !$this->hasMap()) {
277
            return $instantiator->make($fields, $state);
278
        }
279
280
        //Always expect PK in our records
281
        if (empty($identity = $fields[$this->define($class, self::R_PRIMARY_KEY)])) {
282
            //Unable to cache non identified instance
283
            return $instantiator->make($fields, $state);
284
        }
285
286
        if ($this->map->has($class, $identity)) {
287
            return $this->map->get($class, $identity);
288
        }
289
290
        //Storing entity in a cache right after creating it
291
        return $this->map->remember(
292
            $class,
293
            $identity,
294
            $instantiator->make($fields, $state)
295
        );
296
    }
297
298
    /**
299
     * {@inheritdoc}
300
     */
301
    public function makeLoader(string $class, string $relation): LoaderInterface
302
    {
303
        $schema = $this->define($class, self::R_RELATIONS);
304
305
        if (!isset($schema[$relation])) {
306
            throw new ORMException("Undefined relation '{$class}'.'{$relation}'");
307
        }
308
309
        $schema = $schema[$relation];
310
311
        if (!$this->config->hasRelation($schema[self::R_TYPE])) {
312
            throw new ORMException("Undefined relation type '{$schema[self::R_TYPE]}'");
313
        }
314
315
        //Generating relation
316
        return $this->getFactory()->make(
317
            $this->config->relationClass($schema[self::R_TYPE], RelationsConfig::LOADER_CLASS),
318
            [
319
                'class'    => $schema[self::R_CLASS],
320
                'relation' => $relation,
321
                'schema'   => $schema[self::R_SCHEMA],
322
                'orm'      => $this
323
            ]
324
        );
325
    }
326
327
    /**
328
     * {@inheritdoc}
329
     */
330
    public function makeRelation(string $class, string $relation): RelationInterface
331
    {
332
        $schema = $this->define($class, self::R_RELATIONS);
333
334
        if (!isset($schema[$relation])) {
335
            throw new ORMException("Undefined relation '{$class}'.'{$relation}'");
336
        }
337
338
        $schema = $schema[$relation];
339
340
        if (!$this->config->hasRelation($schema[self::R_TYPE], RelationsConfig::ACCESS_CLASS)) {
341
            throw new ORMException("Undefined relation type '{$schema[self::R_TYPE]}'");
342
        }
343
344
        //Generating relation
345
        return $this->getFactory()->make(
346
            $this->config->relationClass($schema[self::R_TYPE], RelationsConfig::ACCESS_CLASS),
347
            [
348
                'class'  => $schema[self::R_CLASS],
349
                'schema' => $schema[self::R_SCHEMA],
350
                'orm'    => $this
351
            ]
352
        );
353
    }
354
355
    /**
356
     * When ORM is cloned we are automatically cloning it's cache as well to create
357
     * new isolated area. Basically we have cache enabled per selection.
358
     *
359
     * @see RecordSelector::getIterator()
360
     */
361
    public function __clone()
362
    {
363
        //Each ORM clone must have isolated entity cache/map
364
        if (!empty($this->map)) {
365
            $this->map = clone $this->map;
366
        }
367
    }
368
369
    /**
370
     * Get object responsible for class instantiation.
371
     *
372
     * @param string $class
373
     *
374
     * @return InstantiatorInterface
375
     */
376
    protected function instantiator(string $class): InstantiatorInterface
377
    {
378
        if (isset($this->instantiators[$class])) {
379
            return $this->instantiators[$class];
380
        }
381
382
        //Potential optimization
383
        $instantiator = $this->getFactory()->make(
384
            $this->define($class, self::R_INSTANTIATOR),
385
            [
386
                'class'  => $class,
387
                'orm'    => $this,
388
                'schema' => $this->define($class, self::R_SCHEMA)
389
            ]
390
        );
391
392
        //Constructing instantiator and storing it in cache
393
        return $this->instantiators[$class] = $instantiator;
394
    }
395
396
    /**
397
     * Load packed schema from memory.
398
     *
399
     * @return array
400
     */
401
    protected function loadSchema(): array
402
    {
403
        return (array)$this->memory->loadData(static::MEMORY);
404
    }
405
406
    /**
407
     * Get ODM specific factory.
408
     *
409
     * @return FactoryInterface
410
     */
411
    protected function getFactory(): FactoryInterface
412
    {
413
        if ($this->container instanceof FactoryInterface) {
414
            return $this->container;
415
        }
416
417
        return $this->container->get(FactoryInterface::class);
418
    }
419
}
420