Completed
Branch feature/pre-split (9d6b17)
by Anton
03:28
created

ORM   B

Complexity

Total Complexity 35

Size/Duplication

Total Lines 365
Duplicated Lines 0 %

Coupling/Cohesion

Components 1
Dependencies 15

Importance

Changes 0
Metric Value
dl 0
loc 365
rs 8.25
c 0
b 0
f 0
wmc 35
lcom 1
cbo 15

15 Methods

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