Passed
Push — master ( d3d298...939d28 )
by Anton
01:39
created

ORM::__debugInfo()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 4
Code Lines 2

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 1
eloc 2
nc 1
nop 0
dl 0
loc 4
rs 10
c 0
b 0
f 0
1
<?php
2
/**
3
 * Cycle DataMapper ORM
4
 *
5
 * @license   MIT
6
 * @author    Anton Titov (Wolfy-J)
7
 */
8
declare(strict_types=1);
9
10
namespace Cycle\ORM;
11
12
use Cycle\ORM\Command\Branch\Nil;
13
use Cycle\ORM\Command\CommandInterface;
14
use Cycle\ORM\Command\ContextCarrierInterface;
15
use Cycle\ORM\Exception\ORMException;
16
use Cycle\ORM\Heap\Heap;
17
use Cycle\ORM\Heap\HeapInterface;
18
use Cycle\ORM\Heap\Node;
19
use Cycle\ORM\Promise\Reference;
20
use Cycle\ORM\Promise\ReferenceInterface;
21
use Cycle\ORM\Select\Repository;
22
use Cycle\ORM\Select\Source;
23
use Cycle\ORM\Select\SourceInterface;
24
25
/**
26
 * Central class ORM, provides access to various pieces of the system and manages schema state.
27
 */
28
final class ORM implements ORMInterface
29
{
30
    /** @var CommandGenerator */
31
    private $generator;
32
33
    /** @var FactoryInterface */
34
    private $factory;
35
36
    /** @var PromiseFactoryInterface|null */
37
    private $promiseFactory;
38
39
    /** @var HeapInterface */
40
    private $heap;
41
42
    /** @var SchemaInterface|null */
43
    private $schema;
44
45
    /** @var MapperInterface[] */
46
    private $mappers = [];
47
48
    /** @var RepositoryInterface[] */
49
    private $repositories = [];
50
51
    /** @var RelationMap[] */
52
    private $relmaps = [];
53
54
    /** @var array */
55
    private $indexes = [];
56
57
    /** @var SourceInterface[] */
58
    private $sources = [];
59
60
    /**
61
     * @param FactoryInterface     $factory
62
     * @param SchemaInterface|null $schema
63
     */
64
    public function __construct(FactoryInterface $factory, SchemaInterface $schema = null)
65
    {
66
        $this->generator = new CommandGenerator();
67
        $this->factory = $factory;
68
        $this->schema = $schema;
69
70
        $this->heap = new Heap();
71
    }
72
73
    /**
74
     * Automatically resolve role based on object name or instance.
75
     *
76
     * @param string|object $entity
77
     * @return string
78
     */
79
    public function resolveRole($entity): string
80
    {
81
        if (is_object($entity)) {
82
            $node = $this->getHeap()->get($entity);
83
            if (!is_null($node)) {
84
                return $node->getRole();
85
            }
86
87
            $class = get_class($entity);
88
            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

88
            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...
89
                throw new ORMException("Unable to resolve role of `$class`");
90
            }
91
92
            $entity = $class;
93
        }
94
95
        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...
96
    }
97
98
    /**
99
     * @inheritdoc
100
     */
101
    public function get(string $role, string $key, $value, bool $load = true)
102
    {
103
        $role = $this->resolveRole($role);
104
        if (!is_null($e = $this->heap->find($role, $key, $value))) {
105
            return $e;
106
        }
107
108
        if (!$load) {
109
            return null;
110
        }
111
112
        return $this->getRepository($role)->findOne([$key => $value]);
113
    }
114
115
    /**
116
     * @inheritdoc
117
     */
118
    public function make(string $role, array $data = [], int $node = Node::NEW)
119
    {
120
        $m = $this->getMapper($role);
121
122
        // unique entity identifier
123
        $pk = $this->schema->define($role, Schema::PRIMARY_KEY);
124
        $id = $data[$pk] ?? null;
125
126
        if ($node !== Node::NEW && !empty($id)) {
127
            $e = $this->heap->find($role, $pk, $id);
128
            if ($e !== null) {
129
                $node = $this->heap->get($e);
130
131
                // entity already been loaded, let's update it's relations with new context
132
                return $m->hydrate($e, $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

132
                return $m->hydrate($e, $this->getRelationMap($role)->init(/** @scrutinizer ignore-type */ $node, $data));
Loading history...
133
            }
134
        }
135
136
        // init entity class and prepared (typecasted) data
137
        list($e, $prepared) = $m->init($data);
138
139
        $node = new Node($node, $prepared, $m->getRole());
140
141
        $this->heap->attach($e, $node, $this->getIndexes($m->getRole()));
142
143
        // hydrate entity with it's data, relations and proxies
144
        return $m->hydrate($e, $this->getRelationMap($role)->init($node, $prepared));
145
    }
146
147
    /**
148
     * @inheritdoc
149
     */
150
    public function withFactory(FactoryInterface $factory): ORMInterface
151
    {
152
        $orm = clone $this;
153
        $orm->factory = $factory;
154
155
        return $orm;
156
    }
157
158
    /**
159
     * @inheritdoc
160
     */
161
    public function getFactory(): FactoryInterface
162
    {
163
        return $this->factory;
164
    }
165
166
    /**
167
     * @inheritdoc
168
     */
169
    public function withSchema(SchemaInterface $schema): ORMInterface
170
    {
171
        $orm = clone $this;
172
        $orm->schema = $schema;
173
174
        return $orm;
175
    }
176
177
    /**
178
     * @inheritdoc
179
     */
180
    public function getSchema(): SchemaInterface
181
    {
182
        if (is_null($this->schema)) {
183
            throw new ORMException("ORM is not configured, schema is missing");
184
        }
185
186
        return $this->schema;
187
    }
188
189
    /**
190
     * @inheritdoc
191
     */
192
    public function withHeap(HeapInterface $heap): ORMInterface
193
    {
194
        $orm = clone $this;
195
        $orm->heap = $heap;
196
197
        return $orm;
198
    }
199
200
    /**
201
     * @inheritdoc
202
     */
203
    public function getHeap(): HeapInterface
204
    {
205
        return $this->heap;
206
    }
207
208
    /**
209
     * @inheritdoc
210
     */
211
    public function getMapper($entity): MapperInterface
212
    {
213
        $role = $this->resolveRole($entity);
214
        if (isset($this->mappers[$role])) {
215
            return $this->mappers[$role];
216
        }
217
218
        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

218
        return $this->mappers[$role] = $this->factory->mapper($this, /** @scrutinizer ignore-type */ $this->schema, $role);
Loading history...
219
    }
220
221
    /**
222
     * @inheritdoc
223
     */
224
    public function getRepository($entity): RepositoryInterface
225
    {
226
        $role = $this->resolveRole($entity);
227
        if (isset($this->repositories[$role])) {
228
            return $this->repositories[$role];
229
        }
230
231
        $repository = $this->getSchema()->define($role, Schema::REPOSITORY) ?? Repository::class;
232
        $params = ['orm' => $this, 'role' => $role];
233
234
        if ($this->getSchema()->define($role, Schema::TABLE) !== null) {
235
            $params['select'] = new Select($this, $role);
236
            $params['select']->constrain($this->getSource($role)->getConstrain());
237
        }
238
239
        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...
240
    }
241
242
    /**
243
     * @inheritdoc
244
     */
245
    public function getSource(string $role): SourceInterface
246
    {
247
        if (isset($this->sources[$role])) {
248
            return $this->sources[$role];
249
        }
250
251
        $source = $this->schema->define($role, Schema::SOURCE) ?? Source::class;
252
        if ($source !== Source::class) {
253
            // custom implementation
254
            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...
255
        }
256
257
        $source = new Source(
258
            $this->factory->database($this->schema->define($role, Schema::DATABASE)),
259
            $this->schema->define($role, Schema::TABLE)
260
        );
261
262
        $constrain = $this->schema->define($role, Schema::CONSTRAIN);
263
        if ($constrain !== null) {
264
            $source = $source->withConstrain(
265
                is_object($constrain) ? $constrain : $this->factory->make($constrain)
266
            );
267
        }
268
269
        return $this->sources[$role] = $source;
270
    }
271
272
    /**
273
     * Overlay existing promise factory.
274
     *
275
     * @param PromiseFactoryInterface $promiseFactory
276
     * @return ORM
277
     */
278
    public function withPromiseFactory(PromiseFactoryInterface $promiseFactory): self
279
    {
280
        $orm = clone $this;
281
        $orm->promiseFactory = $promiseFactory;
282
283
        return $orm;
284
    }
285
286
    /**
287
     * @inheritdoc
288
     *
289
     * Returns references by default.
290
     */
291
    public function promise(string $role, array $scope)
292
    {
293
        $e = $this->heap->find($role, key($scope), current($scope));
294
        if ($e !== null) {
295
            return $e;
296
        }
297
298
        if ($this->promiseFactory !== null) {
299
            return $this->promiseFactory->promise($this, $role, $scope);
300
        }
301
302
        return new Reference($role, $scope);
303
    }
304
305
    /**
306
     * @inheritdoc
307
     */
308
    public function queueStore($entity, int $mode = TransactionInterface::MODE_CASCADE): ContextCarrierInterface
309
    {
310
        if ($entity instanceof ReferenceInterface) {
311
            // we do not expect to store promises
312
            return new Nil();
313
        }
314
315
        $mapper = $this->getMapper($entity);
316
317
        $node = $this->heap->get($entity);
318
        if (is_null($node)) {
319
            // automatic entity registration
320
            $node = new Node(Node::NEW, [], $mapper->getRole());
321
            $this->heap->attach($entity, $node);
322
        }
323
324
        $cmd = $this->generator->generateStore($mapper, $entity, $node);
325
        if ($mode != TransactionInterface::MODE_CASCADE) {
326
            return $cmd;
327
        }
328
329
        if ($this->schema->define($node->getRole(), Schema::RELATIONS) === []) {
330
            return $cmd;
331
        }
332
333
        // generate set of commands required to store entity relations
334
        return $this->getRelationMap($node->getRole())->queueRelations(
335
            $cmd,
336
            $entity,
337
            $node,
338
            $mapper->extract($entity)
339
        );
340
    }
341
342
    /**
343
     * @inheritdoc
344
     */
345
    public function queueDelete($entity, int $mode = TransactionInterface::MODE_CASCADE): CommandInterface
346
    {
347
        $node = $this->heap->get($entity);
348
        if ($entity instanceof ReferenceInterface || is_null($node)) {
349
            // nothing to do, what about promises?
350
            return new Nil();
351
        }
352
353
        // currently we rely on db to delete all nested records (or soft deletes)
354
        return $this->generator->generateDelete($this->getMapper($node->getRole()), $entity, $node);
355
    }
356
357
    /**
358
     * Reset related objects cache.
359
     */
360
    public function __clone()
361
    {
362
        $this->mappers = [];
363
        $this->relmaps = [];
364
        $this->indexes = [];
365
        $this->sources = [];
366
        $this->repositories = [];
367
    }
368
369
    /**
370
     * @return array
371
     */
372
    public function __debugInfo()
373
    {
374
        return [
375
            'schema' => $this->schema
376
        ];
377
    }
378
379
    /**
380
     * Get list of keys entity must be indexed in a Heap by.
381
     *
382
     * @param string $role
383
     * @return array
384
     */
385
    protected function getIndexes(string $role): array
386
    {
387
        if (isset($this->indexes[$role])) {
388
            return $this->indexes[$role];
389
        }
390
391
        $pk = $this->schema->define($role, Schema::PRIMARY_KEY);
392
        $keys = $this->schema->define($role, Schema::FIND_BY_KEYS) ?? [];
393
394
        return $this->indexes[$role] = array_merge([$pk], $keys);
395
    }
396
397
    /**
398
     * Get relation map associated with the given class.
399
     *
400
     * @param string $entity
401
     * @return RelationMap
402
     */
403
    protected function getRelationMap($entity): RelationMap
404
    {
405
        $role = $this->resolveRole($entity);
406
        if (isset($this->relmaps[$role])) {
407
            return $this->relmaps[$role];
408
        }
409
410
        $relations = [];
411
412
        $names = array_keys($this->schema->define($role, Schema::RELATIONS));
413
        foreach ($names as $relation) {
414
            $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

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