Passed
Push — master ( 5cecb8...af86f6 )
by Aleksei
02:44
created

ORM   C

Complexity

Total Complexity 54

Size/Duplication

Total Lines 410
Duplicated Lines 0 %

Importance

Changes 11
Bugs 0 Features 0
Metric Value
wmc 54
eloc 135
c 11
b 0
f 0
dl 0
loc 410
rs 6.4799

22 Methods

Rating   Name   Duplication   Size   Complexity  
A __construct() 0 7 1
A __clone() 0 8 1
A getSchema() 0 7 2
A __debugInfo() 0 4 1
A get() 0 14 3
A resolveRole() 0 17 4
A getFactory() 0 3 1
A make() 0 34 4
A withPromiseFactory() 0 6 1
A getSource() 0 7 2
A getIndexes() 0 10 2
A getHeap() 0 3 1
A getRepository() 0 15 3
A promise() 0 14 4
A with() 0 18 4
A getRelationMap() 0 15 3
A queueDelete() 0 14 5
A getMapper() 0 8 2
B queueStore() 0 35 7
A withFactory() 0 3 1
A withHeap() 0 3 1
A withSchema() 0 3 1

How to fix   Complexity   

Complex Class

Complex classes like ORM often do a lot of different things. To break such a class down, we need to identify a cohesive component within that class. A common approach to find such a component is to look for fields/methods that share the same prefixes, or suffixes.

Once you have determined the fields that belong together, you can apply the Extract Class refactoring. If the component makes sense as a sub-class, Extract Subclass is also a candidate, and is often faster.

While breaking up the class, it is a good idea to analyze how other classes use ORM, and based on these observations, apply Extract Interface, too.

1
<?php
2
3
/**
4
 * Cycle DataMapper ORM
5
 *
6
 * @license   MIT
7
 * @author    Anton Titov (Wolfy-J)
8
 */
9
10
declare(strict_types=1);
11
12
namespace Cycle\ORM;
13
14
use Cycle\ORM\Command\Branch\Nil;
15
use Cycle\ORM\Command\CommandInterface;
16
use Cycle\ORM\Command\ContextCarrierInterface;
17
use Cycle\ORM\Exception\ORMException;
18
use Cycle\ORM\Heap\Heap;
19
use Cycle\ORM\Heap\HeapInterface;
20
use Cycle\ORM\Heap\Node;
21
use Cycle\ORM\Promise\PromiseInterface;
22
use Cycle\ORM\Promise\Reference;
23
use Cycle\ORM\Promise\ReferenceInterface;
24
use Cycle\ORM\Select\SourceInterface;
25
26
/**
27
 * Central class ORM, provides access to various pieces of the system and manages schema state.
28
 */
29
final class ORM implements ORMInterface
30
{
31
    /** @var CommandGenerator */
32
    private $generator;
33
34
    /** @var FactoryInterface */
35
    private $factory;
36
37
    /** @var PromiseFactoryInterface|null */
38
    private $promiseFactory;
39
40
    /** @var HeapInterface */
41
    private $heap;
42
43
    /** @var SchemaInterface|null */
44
    private $schema;
45
46
    /** @var MapperInterface[] */
47
    private $mappers = [];
48
49
    /** @var RepositoryInterface[] */
50
    private $repositories = [];
51
52
    /** @var RelationMap[] */
53
    private $relmaps = [];
54
55
    /** @var array */
56
    private $indexes = [];
57
58
    /** @var SourceInterface[] */
59
    private $sources = [];
60
61
    /**
62
     * @param FactoryInterface     $factory
63
     * @param SchemaInterface|null $schema
64
     */
65
    public function __construct(FactoryInterface $factory, SchemaInterface $schema = null)
66
    {
67
        $this->factory = $factory;
68
        $this->schema = $schema ?? new Schema([]);
69
70
        $this->heap = new Heap();
71
        $this->generator = new CommandGenerator();
72
    }
73
74
    /**
75
     * Reset related objects cache.
76
     */
77
    public function __clone()
78
    {
79
        $this->heap = new Heap();
80
        $this->mappers = [];
81
        $this->relmaps = [];
82
        $this->indexes = [];
83
        $this->sources = [];
84
        $this->repositories = [];
85
    }
86
87
    /**
88
     * @return array
89
     */
90
    public function __debugInfo()
91
    {
92
        return [
93
            'schema' => $this->schema,
94
        ];
95
    }
96
97
    /**
98
     * Automatically resolve role based on object name or instance.
99
     *
100
     * @param object|string $entity
101
     *
102
     * @return string
103
     */
104
    public function resolveRole($entity): string
105
    {
106
        if (is_object($entity)) {
107
            $node = $this->getHeap()->get($entity);
108
            if ($node !== null) {
109
                return $node->getRole();
110
            }
111
112
            $class = get_class($entity);
113
            if (!$this->schema->defines($class)) {
0 ignored issues
show
Bug introduced by
The method defines() does not exist on null. ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-call  annotation

113
            if (!$this->schema->/** @scrutinizer ignore-call */ defines($class)) {

This check looks for calls to methods that do not seem to exist on a given type. It looks for the method on the type itself as well as in inherited classes or implemented interfaces.

This is most likely a typographical error or the method has been renamed.

Loading history...
114
                throw new ORMException("Unable to resolve role of `$class`");
115
            }
116
117
            $entity = $class;
118
        }
119
120
        return $this->schema->resolveAlias($entity);
0 ignored issues
show
Bug Best Practice introduced by
The expression return $this->schema->resolveAlias($entity) could return the type null which is incompatible with the type-hinted return string. Consider adding an additional type-check to rule them out.
Loading history...
121
    }
122
123
    /**
124
     * @inheritdoc
125
     */
126
    public function get(string $role, array $scope, bool $load = true)
127
    {
128
        $role = $this->resolveRole($role);
129
        $e = $this->heap->find($role, $scope);
130
131
        if ($e !== null) {
132
            return $e;
133
        }
134
135
        if (!$load) {
136
            return null;
137
        }
138
139
        return $this->getRepository($role)->findOne($scope);
140
    }
141
142
    /**
143
     * @inheritdoc
144
     */
145
    public function make(string $role, array $data = [], int $node = Node::NEW)
146
    {
147
        $role = $this->resolveRole($role);
148
        $m = $this->getMapper($role);
149
150
        // unique entity identifier
151
        $pk = $this->schema->define($role, Schema::PRIMARY_KEY);
152
        $id = $data[$pk] ?? null;
153
154
        if ($node !== Node::NEW && $id !== null) {
155
            $e = $this->heap->find($role, [$pk => $id]);
156
157
            if ($e !== null) {
158
                $node = $this->heap->get($e);
159
160
                // new set of data and relations always overwrite entity state
161
                return $m->hydrate(
162
                    $e,
163
                    $this->getRelationMap($role)->init($node, $data)
0 ignored issues
show
Bug introduced by
It seems like $node can also be of type null; however, parameter $node of Cycle\ORM\RelationMap::init() does only seem to accept Cycle\ORM\Heap\Node, maybe add an additional type check? ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-type  annotation

163
                    $this->getRelationMap($role)->init(/** @scrutinizer ignore-type */ $node, $data)
Loading history...
164
                );
165
            }
166
        }
167
168
        // init entity class and prepared (typecasted) data
169
        [$e, $prepared] = $m->init($data);
170
171
        $node = new Node($node, $prepared, $m->getRole());
172
173
        $this->heap->attach($e, $node, $this->getIndexes($m->getRole()));
174
175
        // hydrate entity with it's data, relations and proxies
176
        return $m->hydrate(
177
            $e,
178
            $this->getRelationMap($role)->init($node, $prepared)
179
        );
180
    }
181
182
    /**
183
     * @deprecated since Cycle ORM v1.8, this method will be removed in future releases.
184
     * Use method {@see with} instead.
185
     */
186
    public function withFactory(FactoryInterface $factory): ORMInterface
187
    {
188
        return $this->with(null, $factory);
189
    }
190
191
    /**
192
     * @inheritdoc
193
     */
194
    public function getFactory(): FactoryInterface
195
    {
196
        return $this->factory;
197
    }
198
199
    /**
200
     * @deprecated since Cycle ORM v1.8, this method will be removed in future releases.
201
     * Use method {@see with} instead.
202
     */
203
    public function withSchema(SchemaInterface $schema): ORMInterface
204
    {
205
        return $this->with($schema);
206
    }
207
208
    /**
209
     * @inheritdoc
210
     */
211
    public function getSchema(): SchemaInterface
212
    {
213
        if ($this->schema === null) {
214
            throw new ORMException('ORM is not configured, schema is missing');
215
        }
216
217
        return $this->schema;
218
    }
219
220
    /**
221
     * @deprecated since Cycle ORM v1.8, this method will be removed in future releases.
222
     * Use method {@see with} instead.
223
     */
224
    public function withHeap(HeapInterface $heap): ORMInterface
225
    {
226
        return $this->with(null, null, $heap);
227
    }
228
229
    public function with(
230
        ?SchemaInterface $schema = null,
231
        ?FactoryInterface $factory = null,
232
        ?HeapInterface $heap = null
233
    ): ORMInterface {
234
        $orm = clone $this;
235
236
        if ($schema !== null) {
237
            $orm->schema = $schema;
238
        }
239
        if ($factory !== null) {
240
            $orm->factory = $factory;
241
        }
242
        if ($heap !== null) {
243
            $orm->heap = $heap;
244
        }
245
246
        return $orm;
247
    }
248
249
    /**
250
     * @inheritdoc
251
     */
252
    public function getHeap(): HeapInterface
253
    {
254
        return $this->heap;
255
    }
256
257
    /**
258
     * @inheritdoc
259
     */
260
    public function getMapper($entity): MapperInterface
261
    {
262
        $role = $this->resolveRole($entity);
263
        if (isset($this->mappers[$role])) {
264
            return $this->mappers[$role];
265
        }
266
267
        return $this->mappers[$role] = $this->factory->mapper($this, $this->schema, $role);
0 ignored issues
show
Bug introduced by
It seems like $this->schema can also be of type null; however, parameter $schema of Cycle\ORM\FactoryInterface::mapper() does only seem to accept Cycle\ORM\SchemaInterface, maybe add an additional type check? ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-type  annotation

267
        return $this->mappers[$role] = $this->factory->mapper($this, /** @scrutinizer ignore-type */ $this->schema, $role);
Loading history...
268
    }
269
270
    /**
271
     * @inheritdoc
272
     */
273
    public function getRepository($entity): RepositoryInterface
274
    {
275
        $role = $this->resolveRole($entity);
276
        if (isset($this->repositories[$role])) {
277
            return $this->repositories[$role];
278
        }
279
280
        $select = null;
281
282
        if ($this->schema->define($role, Schema::TABLE) !== null) {
283
            $select = new Select($this, $role);
284
            $select->scope($this->getSource($role)->getConstrain());
0 ignored issues
show
Deprecated Code introduced by
The function Cycle\ORM\Select\SourceInterface::getConstrain() has been deprecated: Will be renamed to `getScope` in the Cycle ORM v2. ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-deprecated  annotation

284
            $select->scope(/** @scrutinizer ignore-deprecated */ $this->getSource($role)->getConstrain());

This function has been deprecated. The supplier of the function has supplied an explanatory message.

The explanatory message should give you some clue as to whether and when the function will be removed and what other function to use instead.

Loading history...
285
        }
286
287
        return $this->repositories[$role] = $this->factory->repository($this, $this->schema, $role, $select);
0 ignored issues
show
Bug introduced by
It seems like $this->schema can also be of type null; however, parameter $schema of Cycle\ORM\FactoryInterface::repository() does only seem to accept Cycle\ORM\SchemaInterface, maybe add an additional type check? ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-type  annotation

287
        return $this->repositories[$role] = $this->factory->repository($this, /** @scrutinizer ignore-type */ $this->schema, $role, $select);
Loading history...
288
    }
289
290
    /**
291
     * @inheritdoc
292
     */
293
    public function getSource(string $role): SourceInterface
294
    {
295
        if (isset($this->sources[$role])) {
296
            return $this->sources[$role];
297
        }
298
299
        return $this->sources[$role] = $this->factory->source($this, $this->schema, $role);
0 ignored issues
show
Bug introduced by
It seems like $this->schema can also be of type null; however, parameter $schema of Cycle\ORM\FactoryInterface::source() does only seem to accept Cycle\ORM\SchemaInterface, maybe add an additional type check? ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-type  annotation

299
        return $this->sources[$role] = $this->factory->source($this, /** @scrutinizer ignore-type */ $this->schema, $role);
Loading history...
300
    }
301
302
    /**
303
     * Overlay existing promise factory.
304
     *
305
     * @param PromiseFactoryInterface $promiseFactory
306
     *
307
     * @return ORM
308
     */
309
    public function withPromiseFactory(PromiseFactoryInterface $promiseFactory = null): self
310
    {
311
        $orm = clone $this;
312
        $orm->promiseFactory = $promiseFactory;
313
314
        return $orm;
315
    }
316
317
    /**
318
     * @inheritdoc
319
     *
320
     * Returns references by default.
321
     */
322
    public function promise(string $role, array $scope)
323
    {
324
        if (\count($scope) === 1) {
325
            $e = $this->heap->find($role, $scope);
326
            if ($e !== null) {
327
                return $e;
328
            }
329
        }
330
331
        if ($this->promiseFactory !== null) {
332
            return $this->promiseFactory->promise($this, $role, $scope);
333
        }
334
335
        return new Reference($role, $scope);
336
    }
337
338
    /**
339
     * @inheritdoc
340
     */
341
    public function queueStore($entity, int $mode = TransactionInterface::MODE_CASCADE): ContextCarrierInterface
342
    {
343
        if ($entity instanceof PromiseInterface && $entity->__loaded()) {
344
            $entity = $entity->__resolve();
345
        }
346
347
        if ($entity instanceof ReferenceInterface) {
348
            // we do not expect to store promises
349
            return new Nil();
350
        }
351
352
        $mapper = $this->getMapper($entity);
353
354
        $node = $this->heap->get($entity);
355
        if ($node === null) {
356
            // automatic entity registration
357
            $node = new Node(Node::NEW, [], $mapper->getRole());
358
            $this->heap->attach($entity, $node);
359
        }
360
361
        $cmd = $this->generator->generateStore($mapper, $entity, $node);
362
        if ($mode !== TransactionInterface::MODE_CASCADE) {
363
            return $cmd;
364
        }
365
366
        if ($this->schema->define($node->getRole(), Schema::RELATIONS) === []) {
367
            return $cmd;
368
        }
369
370
        // generate set of commands required to store entity relations
371
        return $this->getRelationMap($node->getRole())->queueRelations(
372
            $cmd,
373
            $entity,
374
            $node,
375
            $mapper->extract($entity)
376
        );
377
    }
378
379
    /**
380
     * @inheritdoc
381
     */
382
    public function queueDelete($entity, int $mode = TransactionInterface::MODE_CASCADE): CommandInterface
383
    {
384
        if ($entity instanceof PromiseInterface && $entity->__loaded()) {
385
            $entity = $entity->__resolve();
386
        }
387
388
        $node = $this->heap->get($entity);
389
        if ($entity instanceof ReferenceInterface || $node === null) {
390
            // nothing to do, what about promises?
391
            return new Nil();
392
        }
393
394
        // currently we rely on db to delete all nested records (or soft deletes)
395
        return $this->generator->generateDelete($this->getMapper($node->getRole()), $entity, $node);
396
    }
397
398
    /**
399
     * Get list of keys entity must be indexed in a Heap by.
400
     *
401
     * @param string $role
402
     *
403
     * @return array
404
     */
405
    protected function getIndexes(string $role): array
406
    {
407
        if (isset($this->indexes[$role])) {
408
            return $this->indexes[$role];
409
        }
410
411
        $pk = $this->schema->define($role, Schema::PRIMARY_KEY);
412
        $keys = $this->schema->define($role, Schema::FIND_BY_KEYS) ?? [];
413
414
        return $this->indexes[$role] = array_unique(array_merge([$pk], $keys));
415
    }
416
417
    /**
418
     * Get relation map associated with the given class.
419
     *
420
     * @param string $entity
421
     *
422
     * @return RelationMap
423
     */
424
    protected function getRelationMap($entity): RelationMap
425
    {
426
        $role = $this->resolveRole($entity);
427
        if (isset($this->relmaps[$role])) {
428
            return $this->relmaps[$role];
429
        }
430
431
        $relations = [];
432
433
        $names = array_keys($this->schema->define($role, Schema::RELATIONS));
434
        foreach ($names as $relation) {
435
            $relations[$relation] = $this->factory->relation($this, $this->schema, $role, $relation);
0 ignored issues
show
Bug introduced by
It seems like $this->schema can also be of type null; however, parameter $schema of Cycle\ORM\FactoryInterface::relation() does only seem to accept Cycle\ORM\SchemaInterface, maybe add an additional type check? ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-type  annotation

435
            $relations[$relation] = $this->factory->relation($this, /** @scrutinizer ignore-type */ $this->schema, $role, $relation);
Loading history...
436
        }
437
438
        return $this->relmaps[$role] = new RelationMap($this, $relations);
439
    }
440
}
441