Completed
Branch feature/pre-split (2ed6c7)
by Anton
04:25
created

ORM   B

Complexity

Total Complexity 31

Size/Duplication

Total Lines 338
Duplicated Lines 0 %

Coupling/Cohesion

Components 1
Dependencies 15

Importance

Changes 0
Metric Value
dl 0
loc 338
rs 8.9833
c 0
b 0
f 0
wmc 31
lcom 1
cbo 15

15 Methods

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