Passed
Pull Request — master (#48)
by
unknown
02:04
created

ORM::withDefaultRepository()   A

Complexity

Conditions 2
Paths 2

Size

Total Lines 11
Code Lines 5

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 2
eloc 5
nc 2
nop 1
dl 0
loc 11
rs 10
c 0
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\Exception\RepositoryException;
19
use Cycle\ORM\Heap\Heap;
20
use Cycle\ORM\Heap\HeapInterface;
21
use Cycle\ORM\Heap\Node;
22
use Cycle\ORM\Promise\Reference;
23
use Cycle\ORM\Promise\ReferenceInterface;
24
use Cycle\ORM\Select\Repository;
25
use Cycle\ORM\Select\Source;
26
use Cycle\ORM\Select\SourceInterface;
27
28
/**
29
 * Central class ORM, provides access to various pieces of the system and manages schema state.
30
 */
31
final class ORM implements ORMInterface
32
{
33
    /** @var CommandGenerator */
34
    private $generator;
35
36
    /** @var FactoryInterface */
37
    private $factory;
38
39
    /** @var PromiseFactoryInterface|null */
40
    private $promiseFactory;
41
42
    /** @var HeapInterface */
43
    private $heap;
44
45
    /** @var SchemaInterface|null */
46
    private $schema;
47
48
    /** @var MapperInterface[] */
49
    private $mappers = [];
50
51
    /** @var RepositoryInterface[] */
52
    private $repositories = [];
53
54
    /** @var RelationMap[] */
55
    private $relmaps = [];
56
57
    /** @var array */
58
    private $indexes = [];
59
60
    /** @var SourceInterface[] */
61
    private $sources = [];
62
63
    /**
64
     * @var string
65
     */
66
    private $defaultRepositoryClass = Repository::class;
67
68
    /**
69
     * @param FactoryInterface $factory
70
     * @param SchemaInterface|null $schema
71
     */
72
    public function __construct(FactoryInterface $factory, SchemaInterface $schema = null)
73
    {
74
        $this->factory = $factory;
75
        $this->schema = $schema ?? new Schema([]);
76
77
        $this->heap = new Heap();
78
        $this->generator = new CommandGenerator();
79
    }
80
81
    /**
82
     * Reset related objects cache.
83
     */
84
    public function __clone()
85
    {
86
        $this->heap = new Heap();
87
        $this->mappers = [];
88
        $this->relmaps = [];
89
        $this->indexes = [];
90
        $this->sources = [];
91
        $this->repositories = [];
92
    }
93
94
    /**
95
     * @return array
96
     */
97
    public function __debugInfo()
98
    {
99
        return [
100
            'schema' => $this->schema
101
        ];
102
    }
103
104
    /**
105
     * Automatically resolve role based on object name or instance.
106
     *
107
     * @param string|object $entity
108
     * @return string
109
     */
110
    public function resolveRole($entity): string
111
    {
112
        if (is_object($entity)) {
113
            $node = $this->getHeap()->get($entity);
114
            if (!is_null($node)) {
115
                return $node->getRole();
116
            }
117
118
            $class = get_class($entity);
119
            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

119
            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...
120
                throw new ORMException("Unable to resolve role of `$class`");
121
            }
122
123
            $entity = $class;
124
        }
125
126
        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...
127
    }
128
129
    /**
130
     * @inheritdoc
131
     */
132
    public function get(string $role, array $scope, bool $load = true)
133
    {
134
        $role = $this->resolveRole($role);
135
        $e = $this->heap->find($role, $scope);
136
137
        if ($e !== null) {
138
            return $e;
139
        }
140
141
        if (!$load) {
142
            return null;
143
        }
144
145
        return $this->getRepository($role)->findOne($scope);
146
    }
147
148
    /**
149
     * @inheritdoc
150
     */
151
    public function make(string $role, array $data = [], int $node = Node::NEW)
152
    {
153
        $m = $this->getMapper($role);
154
155
        // unique entity identifier
156
        $pk = $this->schema->define($role, Schema::PRIMARY_KEY);
157
        $id = $data[$pk] ?? null;
158
159
        if ($node !== Node::NEW && !empty($id)) {
160
            $e = $this->heap->find($role, [$pk => $id]);
161
162
            if ($e !== null) {
163
                $node = $this->heap->get($e);
164
165
                // entity already been loaded, let's update it's relations with new context
166
                // update will only be applied for non-resolved cyclic relation promises
167
                return $m->hydrate(
168
                    $e,
169
                    $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

169
                    $this->getRelationMap($role)->merge(/** @scrutinizer ignore-type */ $node, $data, $m->extract($e))
Loading history...
170
                );
171
            }
172
        }
173
174
        // init entity class and prepared (typecasted) data
175
        [$e, $prepared] = $m->init($data);
176
177
        $node = new Node($node, $prepared, $m->getRole());
178
179
        $this->heap->attach($e, $node, $this->getIndexes($m->getRole()));
180
181
        // hydrate entity with it's data, relations and proxies
182
        return $m->hydrate($e, $this->getRelationMap($role)->init($node, $prepared));
183
    }
184
185
    /**
186
     * @inheritdoc
187
     */
188
    public function withFactory(FactoryInterface $factory): ORMInterface
189
    {
190
        $orm = clone $this;
191
        $orm->factory = $factory;
192
193
        return $orm;
194
    }
195
196
    /**
197
     * @inheritdoc
198
     */
199
    public function getFactory(): FactoryInterface
200
    {
201
        return $this->factory;
202
    }
203
204
    /**
205
     * @inheritdoc
206
     */
207
    public function withSchema(SchemaInterface $schema): ORMInterface
208
    {
209
        $orm = clone $this;
210
        $orm->schema = $schema;
211
212
        return $orm;
213
    }
214
215
    /**
216
     * Add possibility for defining default repository class instead of builtin
217
     * @param string $repositoryClass
218
     * @return ORMInterface
219
     */
220
    public function withDefaultRepository(string $repositoryClass): ORMInterface
221
    {
222
        $orm = clone $this;
223
224
        if (!in_array(RepositoryInterface::class, class_implements($repositoryClass))) {
225
            throw new RepositoryException($repositoryClass);
226
        }
227
228
        $orm->defaultRepositoryClass = $repositoryClass;
229
230
        return $orm;
231
    }
232
233
234
    /**
235
     * @inheritdoc
236
     */
237
    public function getSchema(): SchemaInterface
238
    {
239
        if ($this->schema === null) {
240
            throw new ORMException('ORM is not configured, schema is missing');
241
        }
242
243
        return $this->schema;
244
    }
245
246
    /**
247
     * @inheritdoc
248
     */
249
    public function withHeap(HeapInterface $heap): ORMInterface
250
    {
251
        $orm = clone $this;
252
        $orm->heap = $heap;
253
254
        return $orm;
255
    }
256
257
    /**
258
     * @inheritdoc
259
     */
260
    public function getHeap(): HeapInterface
261
    {
262
        return $this->heap;
263
    }
264
265
    /**
266
     * @inheritdoc
267
     */
268
    public function getMapper($entity): MapperInterface
269
    {
270
        $role = $this->resolveRole($entity);
271
        if (isset($this->mappers[$role])) {
272
            return $this->mappers[$role];
273
        }
274
275
        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

275
        return $this->mappers[$role] = $this->factory->mapper($this, /** @scrutinizer ignore-type */ $this->schema, $role);
Loading history...
276
    }
277
278
    /**
279
     * @inheritdoc
280
     */
281
    public function getRepository($entity): RepositoryInterface
282
    {
283
        $role = $this->resolveRole($entity);
284
        if (isset($this->repositories[$role])) {
285
            return $this->repositories[$role];
286
        }
287
288
        $repository = $this->getSchema()->define($role, Schema::REPOSITORY) ?? $this->defaultRepositoryClass;
289
        $params = ['orm' => $this, 'role' => $role];
290
291
        if ($this->getSchema()->define($role, Schema::TABLE) !== null) {
292
            $params['select'] = new Select($this, $role);
293
            $params['select']->constrain($this->getSource($role)->getConstrain());
294
        }
295
296
        return $this->repositories[$role] = $this->factory->make($repository, $params);
0 ignored issues
show
Bug Best Practice introduced by
The expression return $this->repositori...e($repository, $params) could return the type null which is incompatible with the type-hinted return Cycle\ORM\RepositoryInterface. Consider adding an additional type-check to rule them out.
Loading history...
297
    }
298
299
    /**
300
     * @inheritdoc
301
     */
302
    public function getSource(string $role): SourceInterface
303
    {
304
        if (isset($this->sources[$role])) {
305
            return $this->sources[$role];
306
        }
307
308
        $source = $this->schema->define($role, Schema::SOURCE) ?? Source::class;
309
        if ($source !== Source::class) {
310
            // custom implementation
311
            return $this->factory->make($source, ['orm' => $this, 'role' => $role]);
0 ignored issues
show
Bug Best Practice introduced by
The expression return $this->factory->m...this, 'role' => $role)) could return the type null which is incompatible with the type-hinted return Cycle\ORM\Select\SourceInterface. Consider adding an additional type-check to rule them out.
Loading history...
312
        }
313
314
        $source = new Source(
315
            $this->factory->database($this->schema->define($role, Schema::DATABASE)),
316
            (string)$this->schema->define($role, Schema::TABLE)
317
        );
318
319
        $constrain = $this->schema->define($role, Schema::CONSTRAIN);
320
        if ($constrain !== null) {
321
            $source = $source->withConstrain(
322
                is_object($constrain) ? $constrain : $this->factory->make((string)$constrain)
323
            );
324
        }
325
326
        return $this->sources[$role] = $source;
327
    }
328
329
    /**
330
     * Overlay existing promise factory.
331
     *
332
     * @param PromiseFactoryInterface $promiseFactory
333
     * @return ORM
334
     */
335
    public function withPromiseFactory(PromiseFactoryInterface $promiseFactory = null): self
336
    {
337
        $orm = clone $this;
338
        $orm->promiseFactory = $promiseFactory;
339
340
        return $orm;
341
    }
342
343
    /**
344
     * @inheritdoc
345
     *
346
     * Returns references by default.
347
     */
348
    public function promise(string $role, array $scope)
349
    {
350
        if (\count($scope) === 1) {
351
            $e = $this->heap->find($role, $scope);
352
            if ($e !== null) {
353
                return $e;
354
            }
355
        }
356
357
        if ($this->promiseFactory !== null) {
358
            return $this->promiseFactory->promise($this, $role, $scope);
359
        }
360
361
        return new Reference($role, $scope);
362
    }
363
364
    /**
365
     * @inheritdoc
366
     */
367
    public function queueStore($entity, int $mode = TransactionInterface::MODE_CASCADE): ContextCarrierInterface
368
    {
369
        if ($entity instanceof ReferenceInterface) {
370
            // we do not expect to store promises
371
            return new Nil();
372
        }
373
374
        $mapper = $this->getMapper($entity);
375
376
        $node = $this->heap->get($entity);
377
        if ($node === null) {
378
            // automatic entity registration
379
            $node = new Node(Node::NEW, [], $mapper->getRole());
380
            $this->heap->attach($entity, $node);
381
        }
382
383
        $cmd = $this->generator->generateStore($mapper, $entity, $node);
384
        if ($mode != TransactionInterface::MODE_CASCADE) {
385
            return $cmd;
386
        }
387
388
        if ($this->schema->define($node->getRole(), Schema::RELATIONS) === []) {
389
            return $cmd;
390
        }
391
392
        // generate set of commands required to store entity relations
393
        return $this->getRelationMap($node->getRole())->queueRelations(
394
            $cmd,
395
            $entity,
396
            $node,
397
            $mapper->extract($entity)
398
        );
399
    }
400
401
    /**
402
     * @inheritdoc
403
     */
404
    public function queueDelete($entity, int $mode = TransactionInterface::MODE_CASCADE): CommandInterface
405
    {
406
        $node = $this->heap->get($entity);
407
        if ($entity instanceof ReferenceInterface || $node === null) {
408
            // nothing to do, what about promises?
409
            return new Nil();
410
        }
411
412
        // currently we rely on db to delete all nested records (or soft deletes)
413
        return $this->generator->generateDelete($this->getMapper($node->getRole()), $entity, $node);
414
    }
415
416
    /**
417
     * Get list of keys entity must be indexed in a Heap by.
418
     *
419
     * @param string $role
420
     * @return array
421
     */
422
    protected function getIndexes(string $role): array
423
    {
424
        if (isset($this->indexes[$role])) {
425
            return $this->indexes[$role];
426
        }
427
428
        $pk = $this->schema->define($role, Schema::PRIMARY_KEY);
429
        $keys = $this->schema->define($role, Schema::FIND_BY_KEYS) ?? [];
430
431
        return $this->indexes[$role] = array_merge([$pk], $keys);
432
    }
433
434
    /**
435
     * Get relation map associated with the given class.
436
     *
437
     * @param string $entity
438
     * @return RelationMap
439
     */
440
    protected function getRelationMap($entity): RelationMap
441
    {
442
        $role = $this->resolveRole($entity);
443
        if (isset($this->relmaps[$role])) {
444
            return $this->relmaps[$role];
445
        }
446
447
        $relations = [];
448
449
        $names = array_keys($this->schema->define($role, Schema::RELATIONS));
450
        foreach ($names as $relation) {
451
            $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

451
            $relations[$relation] = $this->factory->relation($this, /** @scrutinizer ignore-type */ $this->schema, $role, $relation);
Loading history...
452
        }
453
454
        return $this->relmaps[$role] = new RelationMap($this, $relations);
455
    }
456
}
457