Passed
Push — master ( a0406a...5d779f )
by Anton
06:34 queued 04:24
created

ORM::queueStore()   B

Complexity

Conditions 7
Paths 14

Size

Total Lines 35
Code Lines 19

Duplication

Lines 0
Ratio 0 %

Importance

Changes 3
Bugs 0 Features 0
Metric Value
cc 7
eloc 19
nc 14
nop 2
dl 0
loc 35
rs 8.8333
c 3
b 0
f 0
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 string|object $entity
101
     * @return string
102
     */
103
    public function resolveRole($entity): string
104
    {
105
        if (is_object($entity)) {
106
            $node = $this->getHeap()->get($entity);
107
            if ($node !== null) {
108
                return $node->getRole();
109
            }
110
111
            $class = get_class($entity);
112
            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

112
            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...
113
                throw new ORMException("Unable to resolve role of `$class`");
114
            }
115
116
            $entity = $class;
117
        }
118
119
        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...
120
    }
121
122
    /**
123
     * @inheritdoc
124
     */
125
    public function get(string $role, array $scope, bool $load = true)
126
    {
127
        $role = $this->resolveRole($role);
128
        $e = $this->heap->find($role, $scope);
129
130
        if ($e !== null) {
131
            return $e;
132
        }
133
134
        if (!$load) {
135
            return null;
136
        }
137
138
        return $this->getRepository($role)->findOne($scope);
139
    }
140
141
    /**
142
     * @inheritdoc
143
     */
144
    public function make(string $role, array $data = [], int $node = Node::NEW)
145
    {
146
        $m = $this->getMapper($role);
147
148
        // unique entity identifier
149
        $pk = $this->schema->define($role, Schema::PRIMARY_KEY);
150
        $id = $data[$pk] ?? null;
151
152
        if ($node !== Node::NEW && $id !== null) {
153
            $e = $this->heap->find($role, [$pk => $id]);
154
155
            if ($e !== null) {
156
                $node = $this->heap->get($e);
157
158
                // entity already been loaded, let's update it's relations with new context
159
                // update will only be applied for non-resolved cyclic relation promises
160
                return $m->hydrate(
161
                    $e,
162
                    $this->getRelationMap($role)->merge($node, $data, $m->extract($e))
0 ignored issues
show
Bug introduced by
It seems like $node can also be of type null; however, parameter $node of Cycle\ORM\RelationMap::merge() 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

162
                    $this->getRelationMap($role)->merge(/** @scrutinizer ignore-type */ $node, $data, $m->extract($e))
Loading history...
163
                );
164
            }
165
        }
166
167
        // init entity class and prepared (typecasted) data
168
        [$e, $prepared] = $m->init($data);
169
170
        $node = new Node($node, $prepared, $m->getRole());
171
172
        $this->heap->attach($e, $node, $this->getIndexes($m->getRole()));
173
174
        // hydrate entity with it's data, relations and proxies
175
        return $m->hydrate(
176
            $e,
177
            $this->getRelationMap($role)->init($node, $prepared)
178
        );
179
    }
180
181
    /**
182
     * @inheritdoc
183
     */
184
    public function withFactory(FactoryInterface $factory): ORMInterface
185
    {
186
        $orm = clone $this;
187
        $orm->factory = $factory;
188
189
        return $orm;
190
    }
191
192
    /**
193
     * @inheritdoc
194
     */
195
    public function getFactory(): FactoryInterface
196
    {
197
        return $this->factory;
198
    }
199
200
    /**
201
     * @inheritdoc
202
     */
203
    public function withSchema(SchemaInterface $schema): ORMInterface
204
    {
205
        $orm = clone $this;
206
        $orm->schema = $schema;
207
208
        return $orm;
209
    }
210
211
    /**
212
     * @inheritdoc
213
     */
214
    public function getSchema(): SchemaInterface
215
    {
216
        if ($this->schema === null) {
217
            throw new ORMException('ORM is not configured, schema is missing');
218
        }
219
220
        return $this->schema;
221
    }
222
223
    /**
224
     * @inheritdoc
225
     */
226
    public function withHeap(HeapInterface $heap): ORMInterface
227
    {
228
        $orm = clone $this;
229
        $orm->heap = $heap;
230
231
        return $orm;
232
    }
233
234
    /**
235
     * @inheritdoc
236
     */
237
    public function getHeap(): HeapInterface
238
    {
239
        return $this->heap;
240
    }
241
242
    /**
243
     * @inheritdoc
244
     */
245
    public function getMapper($entity): MapperInterface
246
    {
247
        $role = $this->resolveRole($entity);
248
        if (isset($this->mappers[$role])) {
249
            return $this->mappers[$role];
250
        }
251
252
        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

252
        return $this->mappers[$role] = $this->factory->mapper($this, /** @scrutinizer ignore-type */ $this->schema, $role);
Loading history...
253
    }
254
255
    /**
256
     * @inheritdoc
257
     */
258
    public function getRepository($entity): RepositoryInterface
259
    {
260
        $role = $this->resolveRole($entity);
261
        if (isset($this->repositories[$role])) {
262
            return $this->repositories[$role];
263
        }
264
265
        $select = null;
266
267
        if ($this->schema->define($role, Schema::TABLE) !== null) {
268
            $select = new Select($this, $role);
269
            $select->constrain($this->getSource($role)->getConstrain());
270
        }
271
272
        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

272
        return $this->repositories[$role] = $this->factory->repository($this, /** @scrutinizer ignore-type */ $this->schema, $role, $select);
Loading history...
273
    }
274
275
    /**
276
     * @inheritdoc
277
     */
278
    public function getSource(string $role): SourceInterface
279
    {
280
        if (isset($this->sources[$role])) {
281
            return $this->sources[$role];
282
        }
283
284
        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

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

417
            $relations[$relation] = $this->factory->relation($this, /** @scrutinizer ignore-type */ $this->schema, $role, $relation);
Loading history...
418
        }
419
420
        return $this->relmaps[$role] = new RelationMap($this, $relations);
421
    }
422
}
423