Completed
Branch feature/pre-split (7f7f80)
by Anton
04:48
created

ORM::__construct()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 22
Code Lines 14

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 1
eloc 14
nc 1
nop 6
dl 0
loc 22
rs 9.2
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\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
    public function hasTransaction(): bool
228
    {
229
230
    }
231
232
    public function getTransaction(): TransactionInterface
233
    {
234
235
    }
236
237
    public function beginTransaction(): TransactionInterface
238
    {
239
240
    }
241
242
    /**
243
     * {@inheritdoc}
244
     */
245
    public function make(
246
        string $class,
247
        $fields = [],
248
        int $state = self::STATE_NEW,
249
        bool $cache = true
250
    ): RecordInterface {
251
        $instantiator = $this->instantiator($class);
252
253
        if ($state == self::STATE_NEW) {
254
            //No caching for entities created with user input
255
            $cache = false;
256
        }
257
258
        if (!$cache || !$this->hasCache()) {
259
            return $instantiator->make($fields, $state);
260
        }
261
262
        //Looking for an entity in a cache
263
        $identity = $instantiator->identify($fields);
264
265
        if (is_null($identity)) {
266
            //Unable to cache non identified instance
267
            return $instantiator->make($fields, $state);
268
        }
269
270
        if ($this->cache->has($class, $identity)) {
271
            return $this->cache->get($class, $identity);
272
        }
273
274
        //Storing entity in a cache right after creating it
275
        return $this->cache->remember(
276
            $class,
277
            $identity,
278
            $instantiator->make($fields, $state)
279
        );
280
    }
281
282
    /**
283
     * {@inheritdoc}
284
     */
285
    public function makeLoader(string $class, string $relation): LoaderInterface
286
    {
287
        $schema = $this->define($class, self::R_RELATIONS);
288
289
        if (!isset($schema[$relation])) {
290
            throw new ORMException("Undefined relation '{$class}'.'{$relation}'");
291
        }
292
293
        $schema = $schema[$relation];
294
295
        if (!$this->config->hasRelation($schema[self::R_TYPE])) {
296
            throw new ORMException("Undefined relation type '{$schema[self::R_TYPE]}'");
297
        }
298
299
        //Generating relation
300
        return $this->getFactory()->make(
301
            $this->config->relationClass($schema[self::R_TYPE], RelationsConfig::LOADER_CLASS),
302
            [
303
                'class'    => $schema[self::R_CLASS],
304
                'relation' => $relation,
305
                'schema'   => $schema[self::R_SCHEMA],
306
                'orm'      => $this
307
            ]
308
        );
309
    }
310
311
    /**
312
     * {@inheritdoc}
313
     */
314
    public function makeRelation(string $class, string $relation): RelationInterface
315
    {
316
        $schema = $this->define($class, self::R_RELATIONS);
317
318
        if (!isset($schema[$relation])) {
319
            throw new ORMException("Undefined relation '{$class}'.'{$relation}'");
320
        }
321
322
        $schema = $schema[$relation];
323
324
        if (!$this->config->hasRelation($schema[self::R_TYPE], RelationsConfig::ACCESS_CLASS)) {
325
            throw new ORMException("Undefined relation type '{$schema[self::R_TYPE]}'");
326
        }
327
328
        //Generating relation
329
        return $this->getFactory()->make(
330
            $this->config->relationClass($schema[self::R_TYPE], RelationsConfig::ACCESS_CLASS),
331
            [
332
                'class'  => $schema[self::R_CLASS],
333
                'schema' => $schema[self::R_SCHEMA],
334
                'orm'    => $this
335
            ]
336
        );
337
    }
338
339
    /**
340
     * When ORM is cloned we are automatically cloning it's cache as well to create
341
     * new isolated area. Basically we have cache enabled per selection.
342
     *
343
     * @see RecordSelector::getIterator()
344
     */
345
    public function __clone()
346
    {
347
        //Each ORM clone must have isolated entity cache
348
        $this->cache = clone $this->cache;
349
    }
350
351
    /**
352
     * Get object responsible for class instantiation.
353
     *
354
     * @param string $class
355
     *
356
     * @return InstantiatorInterface
357
     */
358
    protected function instantiator(string $class): InstantiatorInterface
359
    {
360
        if (isset($this->instantiators[$class])) {
361
            return $this->instantiators[$class];
362
        }
363
364
        //Potential optimization
365
        $instantiator = $this->getFactory()->make(
366
            $this->define($class, self::R_INSTANTIATOR),
367
            [
368
                'class'  => $class,
369
                'orm'    => $this,
370
                'schema' => $this->define($class, self::R_SCHEMA)
371
            ]
372
        );
373
374
        //Constructing instantiator and storing it in cache
375
        return $this->instantiators[$class] = $instantiator;
376
    }
377
378
    /**
379
     * Load packed schema from memory.
380
     *
381
     * @return array
382
     */
383
    protected function loadSchema(): array
384
    {
385
        return (array)$this->memory->loadData(static::MEMORY);
386
    }
387
388
    /**
389
     * Get ODM specific factory.
390
     *
391
     * @return FactoryInterface
392
     */
393
    protected function getFactory(): FactoryInterface
394
    {
395
        if ($this->container instanceof FactoryInterface) {
396
            return $this->container;
397
        }
398
399
        return $this->container->get(FactoryInterface::class);
400
    }
401
}
402