Passed
Pull Request — master (#257)
by
unknown
02:17
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 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
    /** @deprecated since Cycle ORM v1.8, this method will be removed in future releases. Use method with instead. */
183
    public function withFactory(FactoryInterface $factory): ORMInterface
184
    {
185
        return $this->with(null, $factory);
186
    }
187
188
    /**
189
     * @inheritdoc
190
     */
191
    public function getFactory(): FactoryInterface
192
    {
193
        return $this->factory;
194
    }
195
196
    /** @deprecated since Cycle ORM v1.8, this method will be removed in future releases. Use method with instead. */
197
    public function withSchema(SchemaInterface $schema): ORMInterface
198
    {
199
        return $this->with($schema);
200
    }
201
202
    /**
203
     * @inheritdoc
204
     */
205
    public function getSchema(): SchemaInterface
206
    {
207
        if ($this->schema === null) {
208
            throw new ORMException('ORM is not configured, schema is missing');
209
        }
210
211
        return $this->schema;
212
    }
213
214
    /** @deprecated since Cycle ORM v1.8, this method will be removed in future releases. Use method with instead. */
215
    public function withHeap(HeapInterface $heap): ORMInterface
216
    {
217
        return $this->with(null, null, $heap);
218
    }
219
220
    public function with(
221
        ?SchemaInterface $schema = null,
222
        ?FactoryInterface $factory = null,
223
        ?HeapInterface $heap = null
224
    ): ORMInterface {
225
        $orm = clone $this;
226
227
        if ($schema !== null) {
228
            $orm->schema = $schema;
229
        }
230
        if ($factory !== null) {
231
            $orm->factory = $factory;
232
        }
233
        if ($heap !== null) {
234
            $orm->heap = $heap;
235
        }
236
237
        return $orm;
238
    }
239
240
    /**
241
     * @inheritdoc
242
     */
243
    public function getHeap(): HeapInterface
244
    {
245
        return $this->heap;
246
    }
247
248
    /**
249
     * @inheritdoc
250
     */
251
    public function getMapper($entity): MapperInterface
252
    {
253
        $role = $this->resolveRole($entity);
254
        if (isset($this->mappers[$role])) {
255
            return $this->mappers[$role];
256
        }
257
258
        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

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

275
            $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...
276
        }
277
278
        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

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

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

426
            $relations[$relation] = $this->factory->relation($this, /** @scrutinizer ignore-type */ $this->schema, $role, $relation);
Loading history...
427
        }
428
429
        return $this->relmaps[$role] = new RelationMap($this, $relations);
430
    }
431
}
432