Passed
Push — master ( 355930...71a503 )
by Anton
01:51
created

ORM::getSource()   A

Complexity

Conditions 2
Paths 2

Size

Total Lines 7
Code Lines 3

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 2
eloc 3
nc 2
nop 1
dl 0
loc 7
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 Spiral\Cycle;
11
12
use Spiral\Cycle\Command\Branch\Nil;
13
use Spiral\Cycle\Command\Branch\Split;
14
use Spiral\Cycle\Command\CommandInterface;
15
use Spiral\Cycle\Command\ContextCarrierInterface;
16
use Spiral\Cycle\Command\InitCarrierInterface;
17
use Spiral\Cycle\Exception\ORMException;
18
use Spiral\Cycle\Heap\Heap;
19
use Spiral\Cycle\Heap\HeapInterface;
20
use Spiral\Cycle\Heap\Node;
21
use Spiral\Cycle\Mapper\MapperInterface;
22
use Spiral\Cycle\Promise\PromiseInterface;
23
use Spiral\Cycle\Select\SourceFactoryInterface;
24
use Spiral\Cycle\Select\SourceInterface;
25
26
/**
27
 * Central class ORM, provides access to various pieces of the system and manages schema state.
28
 */
29
class ORM implements ORMInterface, SourceFactoryInterface
30
{
31
    /** @var FactoryInterface|SourceFactoryInterface */
32
    private $factory;
33
34
    /** @var HeapInterface */
35
    private $heap;
36
37
    /** @var SchemaInterface */
38
    private $schema;
39
40
    /** @var MapperInterface[] */
41
    private $mappers = [];
42
43
    /** @var RelationMap[] */
44
    private $relmaps = [];
45
46
    /** @var array */
47
    private $indexes = [];
48
49
    /** @var SourceInterface[] */
50
    private $sources = [];
51
52
    /**
53
     * @param FactoryInterface|SourceFactoryInterface $factory
54
     * @param SchemaInterface|null                    $schema
55
     */
56
    public function __construct(FactoryInterface $factory, SchemaInterface $schema = null)
57
    {
58
        if (!$factory instanceof SourceFactoryInterface) {
59
            throw new ORMException("Source factory is missing");
60
        }
61
62
        $this->factory = $factory;
63
64
        if (!is_null($schema)) {
65
            $this->schema = $schema;
66
            $this->factory = $this->factory->withSchema($this, $schema);
67
        }
68
69
        $this->heap = new Heap();
70
    }
71
72
    /**
73
     * Automatically resolve role based on object name.
74
     *
75
     * @param string|object $entity
76
     * @return string
77
     */
78
    public function resolveRole($entity): string
79
    {
80
        if (is_object($entity)) {
81
            $class = get_class($entity);
82
            if (!$this->schema->defines($class)) {
83
                $node = $this->getHeap()->get($entity);
84
                if (is_null($node)) {
85
                    throw new ORMException("Unable to resolve role of `$class`");
86
                }
87
88
                return $node->getRole();
89
            }
90
91
            $entity = $class;
92
        }
93
94
        return $this->schema->resolveRole($entity);
0 ignored issues
show
Bug Best Practice introduced by
The expression return $this->schema->resolveRole($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...
95
    }
96
97
    /**
98
     * @inheritdoc
99
     */
100
    public function get(string $role, $id, bool $load = true)
101
    {
102
        $role = $this->resolveRole($role);
103
        $pk = $this->schema->define($role, Schema::PRIMARY_KEY);
104
105
        if (!is_null($e = $this->heap->find($role, $pk, $id))) {
106
            return $e;
107
        }
108
109
        if (!$load) {
110
            return null;
111
        }
112
113
        return $this->getMapper($role)->getRepository()->findByPK($id);
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
            if (!empty($e = $this->heap->find($role, $pk, $id))) {
129
                $node = $this->getHeap()->get($e);
130
131
                // entity already been loaded, let's update it's relations with new context
132
                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 Spiral\Cycle\RelationMap::init() does only seem to accept Spiral\Cycle\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->getRelmap($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->getRelmap($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
        if (!is_null($orm->schema)) {
156
            $orm->factory = $factory->withSchema($orm, $orm->schema);
157
        }
158
159
        return $orm;
160
    }
161
162
    /**
163
     * @inheritdoc
164
     */
165
    public function getFactory(): FactoryInterface
166
    {
167
        return $this->factory;
0 ignored issues
show
Bug Best Practice introduced by
The expression return $this->factory could return the type Spiral\Cycle\Select\SourceFactoryInterface which is incompatible with the type-hinted return Spiral\Cycle\FactoryInterface. Consider adding an additional type-check to rule them out.
Loading history...
168
    }
169
170
    /**
171
     * @inheritdoc
172
     */
173
    public function withSchema(SchemaInterface $schema): ORMInterface
174
    {
175
        $orm = clone $this;
176
        $orm->schema = $schema;
177
        $orm->factory = $orm->factory->withSchema($orm, $orm->schema);
0 ignored issues
show
Bug introduced by
The method withSchema() does not exist on Spiral\Cycle\Select\SourceFactoryInterface. Since it exists in all sub-types, consider adding an abstract or default implementation to Spiral\Cycle\Select\SourceFactoryInterface. ( Ignorable by Annotation )

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

177
        /** @scrutinizer ignore-call */ 
178
        $orm->factory = $orm->factory->withSchema($orm, $orm->schema);
Loading history...
178
179
        return $orm;
180
    }
181
182
    /**
183
     * @inheritdoc
184
     */
185
    public function getSchema(): SchemaInterface
186
    {
187
        if (is_null($this->schema)) {
188
            throw new ORMException("ORM is not configured, schema is missing");
189
        }
190
191
        return $this->schema;
192
    }
193
194
    /**
195
     * @inheritdoc
196
     */
197
    public function withHeap(HeapInterface $heap): ORMInterface
198
    {
199
        $orm = clone $this;
200
        $orm->heap = $heap;
201
        $orm->factory = $orm->factory->withSchema($orm, $orm->schema);
202
203
        return $orm;
204
    }
205
206
    /**
207
     * @inheritdoc
208
     */
209
    public function getHeap(): HeapInterface
210
    {
211
        return $this->heap;
212
    }
213
214
    /**
215
     * @inheritdoc
216
     */
217
    public function getMapper($entity): MapperInterface
218
    {
219
        $role = $this->resolveRole($entity);
220
        if (isset($this->mappers[$role])) {
221
            return $this->mappers[$role];
222
        }
223
224
        return $this->mappers[$role] = $this->factory->mapper($role);
0 ignored issues
show
Bug introduced by
The method mapper() does not exist on Spiral\Cycle\Select\SourceFactoryInterface. It seems like you code against a sub-type of Spiral\Cycle\Select\SourceFactoryInterface such as Spiral\Cycle\Factory. ( Ignorable by Annotation )

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

224
        return $this->mappers[$role] = $this->factory->/** @scrutinizer ignore-call */ mapper($role);
Loading history...
225
    }
226
227
    /**
228
     * @inheritdoc
229
     */
230
    public function getSource(string $role): SourceInterface
231
    {
232
        if (isset($this->sources[$role])) {
233
            return $this->sources[$role];
234
        }
235
236
        return $this->sources[$role] = $this->factory->getSource($role);
0 ignored issues
show
Bug introduced by
The method getSource() does not exist on Spiral\Cycle\FactoryInterface. Since it exists in all sub-types, consider adding an abstract or default implementation to Spiral\Cycle\FactoryInterface. ( Ignorable by Annotation )

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

236
        return $this->sources[$role] = $this->factory->/** @scrutinizer ignore-call */ getSource($role);
Loading history...
237
    }
238
239
    /**
240
     * @inheritdoc
241
     */
242
    public function queueStore($entity, int $mode = TransactionInterface::MODE_CASCADE): ContextCarrierInterface
243
    {
244
        if ($entity instanceof PromiseInterface) {
245
            // we do not expect to store promises
246
            return new Nil();
247
        }
248
249
        $mapper = $this->getMapper($entity);
250
251
        $node = $this->heap->get($entity);
252
        if (is_null($node)) {
253
            // automatic entity registration
254
            $node = new Node(Node::NEW, [], $mapper->getRole());
255
            $this->heap->attach($entity, $node);
256
        }
257
258
        $cmd = $this->store($mapper, $entity, $node);
259
        if ($mode != TransactionInterface::MODE_CASCADE) {
260
            return $cmd;
261
        }
262
263
        // generate set of commands required to store entity relations
264
        return $this->getRelmap($node->getRole())->queueRelations(
265
            $cmd,
266
            $entity,
267
            $node,
268
            $mapper->extract($entity)
269
        );
270
    }
271
272
    /**
273
     * @inheritdoc
274
     */
275
    public function queueDelete($entity, int $mode = TransactionInterface::MODE_CASCADE): CommandInterface
276
    {
277
        $node = $this->heap->get($entity);
278
        if ($entity instanceof PromiseInterface || is_null($node)) {
279
            // nothing to do, what about promises?
280
            return new Nil();
281
        }
282
283
        // currently we rely on db to delete all nested records (or soft deletes)
284
        return $this->getMapper($node->getRole())->queueDelete($entity, $node->getState());
285
    }
286
287
    /**
288
     * Reset related objects cache.
289
     */
290
    public function __clone()
291
    {
292
        $this->mappers = [];
293
        $this->relmaps = [];
294
        $this->indexes = [];
295
        $this->sources = [];
296
    }
297
298
    /**
299
     * Get list of keys entity must be indexed in a Heap by.
300
     *
301
     * @param string $role
302
     * @return array
303
     */
304
    protected function getIndexes(string $role): array
305
    {
306
        if (isset($this->indexes[$role])) {
307
            return $this->indexes[$role];
308
        }
309
310
        $pk = $this->schema->define($role, Schema::PRIMARY_KEY);
311
        $keys = $this->schema->define($role, Schema::FIND_BY_KEYS) ?? [];
312
313
        return $this->indexes[$role] = array_merge([$pk], $keys);
314
    }
315
316
    /**
317
     * Get relation map associated with the given class.
318
     *
319
     * @param string $entity
320
     * @return RelationMap
321
     */
322
    protected function getRelmap($entity): RelationMap
323
    {
324
        $role = $this->resolveRole($entity);
325
        if (isset($this->relmaps[$role])) {
326
            return $this->relmaps[$role];
327
        }
328
329
        $relations = [];
330
331
        $names = array_keys($this->schema->define($role, Schema::RELATIONS));
332
        foreach ($names as $relation) {
333
            $relations[$relation] = $this->factory->relation($role, $relation);
0 ignored issues
show
Bug introduced by
The method relation() does not exist on Spiral\Cycle\Select\SourceFactoryInterface. It seems like you code against a sub-type of Spiral\Cycle\Select\SourceFactoryInterface such as Spiral\Cycle\Factory. ( Ignorable by Annotation )

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

333
            /** @scrutinizer ignore-call */ 
334
            $relations[$relation] = $this->factory->relation($role, $relation);
Loading history...
334
        }
335
336
        return $this->relmaps[$role] = new RelationMap($this, $relations);
337
    }
338
339
    /**
340
     * @param MapperInterface $mapper
341
     * @param object          $entity
342
     * @param Node            $node
343
     * @return ContextCarrierInterface
344
     */
345
    protected function store(MapperInterface $mapper, $entity, Node $node): ContextCarrierInterface
346
    {
347
        if ($node->getStatus() == Node::NEW) {
348
            $cmd = $mapper->queueCreate($entity, $node->getState());
349
            $node->getState()->setCommand($cmd);
350
351
            return $cmd;
352
        }
353
354
        $lastCommand = $node->getState()->getCommand();
355
        if (empty($lastCommand)) {
356
            return $mapper->queueUpdate($entity, $node->getState());
357
        }
358
359
        // Command can aggregate multiple operations on soft basis.
360
        if (!$lastCommand instanceof InitCarrierInterface) {
361
            return $lastCommand;
362
        }
363
364
        // in cases where we have to update new entity we can merge two commands into one
365
        $split = new Split($lastCommand, $mapper->queueUpdate($entity, $node->getState()));
366
        $node->getState()->setCommand($split);
367
368
        return $split;
369
    }
370
}