Completed
Branch feature/pre-split (4c50c1)
by Anton
03:17
created

ORM   A

Complexity

Total Complexity 30

Size/Duplication

Total Lines 332
Duplicated Lines 0 %

Coupling/Cohesion

Components 1
Dependencies 15

Importance

Changes 0
Metric Value
dl 0
loc 332
rs 9.1666
c 0
b 0
f 0
wmc 30
lcom 1
cbo 15

14 Methods

Rating   Name   Duplication   Size   Complexity  
A __construct() 0 21 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 __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\ORM\Configs\RelationsConfig;
19
use Spiral\ORM\Entities\EntityCache;
20
use Spiral\ORM\Entities\RecordSelector;
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
use Spiral\ORM\Schemas\SchemaLocator;
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 SchemaLocator
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 EntityCache|null        $cache
89
     * @param LocatorInterface|null   $locator
90
     * @param MemoryInterface|null    $memory
91
     * @param ContainerInterface|null $container
92
     */
93
    public function __construct(
94
        DatabaseManager $manager,
95
        RelationsConfig $config,
96
        EntityCache $cache = null,
97
        LocatorInterface $locator = 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->cache = $cache;
106
107
        $this->locator = $locator ?? new NullLocator();
0 ignored issues
show
Documentation Bug introduced by
$locator ?? new \Spiral\ORM\Schemas\NullLocator() is of type object<Spiral\ORM\Schemas\LocatorInterface>, but the property $locator was declared to be of type object<Spiral\ORM\Schemas\SchemaLocator>. Are you sure that you always receive this specific sub-class here, or does it make sense to add an instanceof check?

Our type inference engine has found a suspicous assignment of a value to a property. This check raises an issue when a value that can be of a given class or a super-class is assigned to a property that is type hinted more strictly.

Either this assignment is in error or an instanceof check should be added for that assignment.

class Alien {}

class Dalek extends Alien {}

class Plot
{
    /** @var  Dalek */
    public $villain;
}

$alien = new Alien();
$plot = new Plot();
if ($alien instanceof Dalek) {
    $plot->villain = $alien;
}
Loading history...
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 cache or disabled cache.
117
     *
118
     * @param EntityCache|null $cache
119
     *
120
     * @return ORM
121
     */
122
    public function withCache(EntityCache $cache = null): ORM
123
    {
124
        $orm = clone $this;
125
        $orm->cache = $cache;
126
127
        return $orm;
128
    }
129
130
    /**
131
     * Check if ORM has associated entity cache.
132
     *
133
     * @return bool
134
     */
135
    public function hasCache(): bool
136
    {
137
        return !empty($this->cache);
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
     * {@inheritdoc}
209
     */
210
    public function selector(string $class): RecordSelector
211
    {
212
        //ORM is cloned in order to isolate cache scope.
213
        return new RecordSelector($class, clone $this);
214
    }
215
216
    /**
217
     * {@inheritdoc}
218
     */
219
    public function table(string $class): Table
220
    {
221
        return $this->manager->database(
222
            $this->define($class, self::R_DATABASE)
223
        )->table(
224
            $this->define($class, self::R_TABLE)
225
        );
226
    }
227
228
    /**
229
     * {@inheritdoc}
230
     */
231
    public function make(
232
        string $class,
233
        $fields = [],
234
        bool $filter = true,
235
        bool $cache = true
236
    ): RecordInterface {
237
        $instantiator = $this->instantiator($class);
238
239
        if ($filter) {
240
            //No caching for entities created with user input
241
            $cache = false;
242
        }
243
244
        if (!$cache || !$this->hasCache()) {
245
            return $instantiator->make($fields, $filter);
246
        }
247
248
        //Looking for an entity in a cache
249
        $identity = $instantiator->identify($fields);
250
251
        if (is_null($identity)) {
252
            //Unable to cache non identified instance
253
            return $instantiator->make($fields, $filter);
254
        }
255
256
        if ($this->cache->has($class, $identity)) {
257
            return $this->cache->get($class, $identity);
258
        }
259
260
        //Storing entity in a cache right after creating it
261
        return $this->cache->remember(
262
            $class,
263
            $identity,
264
            $instantiator->make($fields, $filter)
265
        );
266
    }
267
268
    /**
269
     * {@inheritdoc}
270
     */
271
    public function makeLoader(string $class, string $relation): LoaderInterface
272
    {
273
        $schema = $this->define($class, self::R_RELATIONS);
274
275
        if (!isset($schema[$relation])) {
276
            throw new ORMException("Undefined relation '{$class}'.'{$relation}'");
277
        }
278
279
        $schema = $schema[$relation];
280
281
        if (!$this->config->hasRelation($schema[self::R_TYPE])) {
282
            throw new ORMException("Undefined relation type '{$schema[self::R_TYPE]}'");
283
        }
284
285
        //Generating relation
286
        return $this->getFactory()->make(
287
            $this->config->relationClass($schema[self::R_TYPE], RelationsConfig::LOADER_CLASS),
288
            [
289
                'class'    => $schema[self::R_CLASS],
290
                'relation' => $relation,
291
                'schema'   => $schema[self::R_SCHEMA],
292
                'orm'      => $this
293
            ]
294
        );
295
    }
296
297
    /**
298
     * When ORM is cloned we are automatically cloning it's cache as well to create
299
     * new isolated area. Basically we have cache enabled per selection.
300
     *
301
     * @see RecordSelector::getIterator()
302
     */
303
    public function __clone()
304
    {
305
        //Each ORM clone must have isolated entity cache
306
        $this->cache = clone $this->cache;
307
    }
308
309
    /**
310
     * Get object responsible for class instantiation.
311
     *
312
     * @param string $class
313
     *
314
     * @return InstantiatorInterface
315
     */
316
    protected function instantiator(string $class): InstantiatorInterface
317
    {
318
        if (isset($this->instantiators[$class])) {
319
            return $this->instantiators[$class];
320
        }
321
322
        //Potential optimization
323
        $instantiator = $this->getFactory()->make(
324
            $this->define($class, self::R_INSTANTIATOR),
325
            [
326
                'class'  => $class,
327
                'orm'    => $this,
328
                'schema' => $this->define($class, self::R_SCHEMA)
329
            ]
330
        );
331
332
        //Constructing instantiator and storing it in cache
333
        return $this->instantiators[$class] = $instantiator;
334
    }
335
336
    /**
337
     * Load packed schema from memory.
338
     *
339
     * @return array
340
     */
341
    protected function loadSchema(): array
342
    {
343
        return (array)$this->memory->loadData(static::MEMORY);
344
    }
345
346
    /**
347
     * Get ODM specific factory.
348
     *
349
     * @return FactoryInterface
350
     */
351
    protected function getFactory(): FactoryInterface
352
    {
353
        if ($this->container instanceof FactoryInterface) {
354
            return $this->container;
355
        }
356
357
        return $this->container->get(FactoryInterface::class);
358
    }
359
}