Passed
Push — master ( 6ea451...1ef48a )
by Anton
01:35
created

ORM::promise()   A

Complexity

Conditions 3
Paths 3

Size

Total Lines 12
Code Lines 6

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 3
eloc 6
nc 3
nop 2
dl 0
loc 12
rs 10
c 0
b 0
f 0
1
<?php
2
declare(strict_types=1);
3
/**
4
 * Spiral Framework.
5
 *
6
 * @license   MIT
7
 * @author    Anton Titov (Wolfy-J)
8
 */
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
use Cycle\ORM\Select\SourceProviderInterface;
25
26
/**
27
 * Central class ORM, provides access to various pieces of the system and manages schema state.
28
 */
29
class ORM implements ORMInterface
30
{
31
    /** @var CommandGenerator */
32
    private $generator;
33
34
    /** @var FactoryInterface|SourceProviderInterface */
35
    private $factory;
36
37
    /** @var ProxyFactoryInterface|null */
38
    private $proxyFactory;
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|SourceProviderInterface $factory
63
     * @param SchemaInterface|null                     $schema
64
     */
65
    public function __construct(FactoryInterface $factory, SchemaInterface $schema = null)
66
    {
67
        $this->generator = new CommandGenerator();
68
        $this->factory = $factory;
69
        $this->schema = $schema;
70
71
        $this->heap = new Heap();
72
    }
73
74
    /**
75
     * Automatically resolve role based on object name.
76
     *
77
     * @param string|object $entity
78
     * @return string
79
     */
80
    public function resolveRole($entity): string
81
    {
82
        if (is_object($entity)) {
83
            $class = get_class($entity);
84
            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

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

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

219
        return $this->mappers[$role] = $this->factory->mapper($this, /** @scrutinizer ignore-type */ $this->schema, $role);
Loading history...
Bug introduced by
The method mapper() does not exist on Cycle\ORM\Select\SourceProviderInterface. ( Ignorable by Annotation )

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

219
        return $this->mappers[$role] = $this->factory->/** @scrutinizer ignore-call */ mapper($this, $this->schema, $role);

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...
220
    }
221
222
    /**
223
     * @inheritdoc
224
     */
225
    public function getRepository($entity): RepositoryInterface
226
    {
227
        $role = $this->resolveRole($entity);
228
        if (isset($this->repositories[$role])) {
229
            return $this->repositories[$role];
230
        }
231
232
        $selector = new Select($this, $role);
233
        $selector->constrain($this->getSource($role)->getConstrain());
234
235
        $repositoryClass = $this->getSchema()->define($role, Schema::REPOSITORY) ?? Repository::class;
236
237
        return $this->repositories[$role] = new $repositoryClass($selector);
238
    }
239
240
    /**
241
     * @inheritdoc
242
     */
243
    public function getSource(string $role): SourceInterface
244
    {
245
        if (isset($this->sources[$role])) {
246
            return $this->sources[$role];
247
        }
248
249
        $source = $this->schema->define($role, Schema::SOURCE);
250
        if ($source !== null) {
251
            return $this->factory->get($source);
0 ignored issues
show
Bug introduced by
The method get() does not exist on Cycle\ORM\Select\SourceProviderInterface. Since it exists in all sub-types, consider adding an abstract or default implementation to Cycle\ORM\Select\SourceProviderInterface. ( Ignorable by Annotation )

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

251
            return $this->factory->/** @scrutinizer ignore-call */ get($source);
Loading history...
252
        }
253
254
        $source = new Source(
255
            $this->factory->database($this->schema->define($role, Schema::DATABASE)),
0 ignored issues
show
Bug introduced by
The method database() does not exist on Cycle\ORM\Select\SourceProviderInterface. ( Ignorable by Annotation )

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

255
            $this->factory->/** @scrutinizer ignore-call */ 
256
                            database($this->schema->define($role, Schema::DATABASE)),

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...
256
            $this->schema->define($role, Schema::TABLE)
257
        );
258
259
        $constrain = $this->schema->define($role, Schema::CONSTRAIN);
260
        if ($constrain !== null) {
261
            $source = $source->withConstrain($this->factory->get($constrain));
262
        }
263
264
        return $this->sources[$role] = $source;
265
    }
266
267
    /**
268
     * Overlay existing promise factory.
269
     *
270
     * @param ProxyFactoryInterface $proxyFactory
271
     * @return ORM
272
     */
273
    public function withProxyFactory(ProxyFactoryInterface $proxyFactory): self
274
    {
275
        $orm = clone $this;
276
        $orm->proxyFactory = $proxyFactory;
277
278
        return $orm;
279
    }
280
281
    /**
282
     * @inheritdoc
283
     *
284
     * Returns references by default.
285
     */
286
    public function promise(string $role, array $scope)
287
    {
288
        $e = $this->heap->find($role, key($scope), current($scope));
289
        if ($e !== null) {
290
            return $e;
291
        }
292
293
        if ($this->proxyFactory !== null) {
294
            return $this->proxyFactory->proxy($this, $role, $scope);
295
        }
296
297
        return new Reference($role, $scope);
298
    }
299
300
    /**
301
     * @inheritdoc
302
     */
303
    public function queueStore($entity, int $mode = TransactionInterface::MODE_CASCADE): ContextCarrierInterface
304
    {
305
        if ($entity instanceof ReferenceInterface) {
306
            // we do not expect to store promises
307
            return new Nil();
308
        }
309
310
        $mapper = $this->getMapper($entity);
311
312
        $node = $this->heap->get($entity);
313
        if (is_null($node)) {
314
            // automatic entity registration
315
            $node = new Node(Node::NEW, [], $mapper->getRole());
316
            $this->heap->attach($entity, $node);
317
        }
318
319
        $cmd = $this->generator->generateStore($mapper, $entity, $node);
320
        if ($mode != TransactionInterface::MODE_CASCADE) {
321
            return $cmd;
322
        }
323
324
        // generate set of commands required to store entity relations
325
        return $this->getRelmap($node->getRole())->queueRelations(
326
            $cmd,
327
            $entity,
328
            $node,
329
            $mapper->extract($entity)
330
        );
331
    }
332
333
    /**
334
     * @inheritdoc
335
     */
336
    public function queueDelete($entity, int $mode = TransactionInterface::MODE_CASCADE): CommandInterface
337
    {
338
        $node = $this->heap->get($entity);
339
        if ($entity instanceof ReferenceInterface || is_null($node)) {
340
            // nothing to do, what about promises?
341
            return new Nil();
342
        }
343
344
        // currently we rely on db to delete all nested records (or soft deletes)
345
        return $this->generator->generateDelete($this->getMapper($node->getRole()), $entity, $node);
346
    }
347
348
    /**
349
     * Reset related objects cache.
350
     */
351
    public function __clone()
352
    {
353
        $this->mappers = [];
354
        $this->relmaps = [];
355
        $this->indexes = [];
356
        $this->sources = [];
357
        $this->repositories = [];
358
    }
359
360
    /**
361
     * Get list of keys entity must be indexed in a Heap by.
362
     *
363
     * @param string $role
364
     * @return array
365
     */
366
    protected function getIndexes(string $role): array
367
    {
368
        if (isset($this->indexes[$role])) {
369
            return $this->indexes[$role];
370
        }
371
372
        $pk = $this->schema->define($role, Schema::PRIMARY_KEY);
373
        $keys = $this->schema->define($role, Schema::FIND_BY_KEYS) ?? [];
374
375
        return $this->indexes[$role] = array_merge([$pk], $keys);
376
    }
377
378
    /**
379
     * Get relation map associated with the given class.
380
     *
381
     * @param string $entity
382
     * @return RelationMap
383
     */
384
    protected function getRelmap($entity): RelationMap
385
    {
386
        $role = $this->resolveRole($entity);
387
        if (isset($this->relmaps[$role])) {
388
            return $this->relmaps[$role];
389
        }
390
391
        $relations = [];
392
393
        $names = array_keys($this->schema->define($role, Schema::RELATIONS));
394
        foreach ($names as $relation) {
395
            $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

395
            $relations[$relation] = $this->factory->relation($this, /** @scrutinizer ignore-type */ $this->schema, $role, $relation);
Loading history...
Bug introduced by
The method relation() does not exist on Cycle\ORM\Select\SourceProviderInterface. ( Ignorable by Annotation )

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

395
            /** @scrutinizer ignore-call */ 
396
            $relations[$relation] = $this->factory->relation($this, $this->schema, $role, $relation);

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...
396
        }
397
398
        return $this->relmaps[$role] = new RelationMap($this, $relations);
399
    }
400
}
401