Passed
Push — master ( 71a503...2dde58 )
by Anton
01:45
created

ORM::withFactory()   A

Complexity

Conditions 2
Paths 2

Size

Total Lines 10
Code Lines 5

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 2
eloc 5
nc 2
nop 1
dl 0
loc 10
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\CommandInterface;
14
use Spiral\Cycle\Command\ContextCarrierInterface;
15
use Spiral\Cycle\Exception\ORMException;
16
use Spiral\Cycle\Heap\Heap;
17
use Spiral\Cycle\Heap\HeapInterface;
18
use Spiral\Cycle\Heap\Node;
19
use Spiral\Cycle\Mapper\MapperInterface;
20
use Spiral\Cycle\Promise\PromiseInterface;
21
use Spiral\Cycle\Select\SourceFactoryInterface;
22
use Spiral\Cycle\Select\SourceInterface;
23
24
/**
25
 * Central class ORM, provides access to various pieces of the system and manages schema state.
26
 */
27
class ORM implements ORMInterface, SourceFactoryInterface
28
{
29
    /** @var CommandGenerator */
30
    private $generator;
31
32
    /** @var FactoryInterface|SourceFactoryInterface */
33
    private $factory;
34
35
    /** @var HeapInterface */
36
    private $heap;
37
38
    /** @var SchemaInterface */
39
    private $schema;
40
41
    /** @var MapperInterface[] */
42
    private $mappers = [];
43
44
    /** @var RelationMap[] */
45
    private $relmaps = [];
46
47
    /** @var array */
48
    private $indexes = [];
49
50
    /** @var SourceInterface[] */
51
    private $sources = [];
52
53
    /**
54
     * @param FactoryInterface|SourceFactoryInterface $factory
55
     * @param SchemaInterface|null                    $schema
56
     */
57
    public function __construct(FactoryInterface $factory, SchemaInterface $schema = null)
58
    {
59
        if (!$factory instanceof SourceFactoryInterface) {
60
            throw new ORMException("Source factory is missing");
61
        }
62
63
        $this->generator = new CommandGenerator();
64
        $this->factory = $factory;
65
66
        if (!is_null($schema)) {
67
            $this->schema = $schema;
68
            $this->factory = $this->factory->withSchema($this, $schema);
69
        }
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)) {
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->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...
97
    }
98
99
    /**
100
     * @inheritdoc
101
     */
102
    public function get(string $role, $id, bool $load = true)
103
    {
104
        $role = $this->resolveRole($role);
105
        $pk = $this->schema->define($role, Schema::PRIMARY_KEY);
106
107
        if (!is_null($e = $this->heap->find($role, $pk, $id))) {
108
            return $e;
109
        }
110
111
        if (!$load) {
112
            return null;
113
        }
114
115
        return $this->getMapper($role)->getRepository()->findByPK($id);
116
    }
117
118
    /**
119
     * @inheritdoc
120
     */
121
    public function make(string $role, array $data = [], int $node = Node::NEW)
122
    {
123
        $m = $this->getMapper($role);
124
125
        // unique entity identifier
126
        $pk = $this->schema->define($role, Schema::PRIMARY_KEY);
127
        $id = $data[$pk] ?? null;
128
129
        if ($node !== Node::NEW && !empty($id)) {
130
            if (!empty($e = $this->heap->find($role, $pk, $id))) {
131
                $node = $this->getHeap()->get($e);
132
133
                // entity already been loaded, let's update it's relations with new context
134
                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

134
                return $m->hydrate($e, $this->getRelmap($role)->init(/** @scrutinizer ignore-type */ $node, $data));
Loading history...
135
            }
136
        }
137
138
        // init entity class and prepared (typecasted) data
139
        list($e, $prepared) = $m->init($data);
140
141
        $node = new Node($node, $prepared, $m->getRole());
142
143
        $this->heap->attach($e, $node, $this->getIndexes($m->getRole()));
144
145
        // hydrate entity with it's data, relations and proxies
146
        return $m->hydrate($e, $this->getRelmap($role)->init($node, $prepared));
147
    }
148
149
    /**
150
     * @inheritdoc
151
     */
152
    public function withFactory(FactoryInterface $factory): ORMInterface
153
    {
154
        $orm = clone $this;
155
        $orm->factory = $factory;
156
157
        if (!is_null($orm->schema)) {
158
            $orm->factory = $factory->withSchema($orm, $orm->schema);
159
        }
160
161
        return $orm;
162
    }
163
164
    /**
165
     * @inheritdoc
166
     */
167
    public function getFactory(): FactoryInterface
168
    {
169
        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...
170
    }
171
172
    /**
173
     * @inheritdoc
174
     */
175
    public function withSchema(SchemaInterface $schema): ORMInterface
176
    {
177
        $orm = clone $this;
178
        $orm->schema = $schema;
179
        $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

179
        /** @scrutinizer ignore-call */ 
180
        $orm->factory = $orm->factory->withSchema($orm, $orm->schema);
Loading history...
180
181
        return $orm;
182
    }
183
184
    /**
185
     * @inheritdoc
186
     */
187
    public function getSchema(): SchemaInterface
188
    {
189
        if (is_null($this->schema)) {
190
            throw new ORMException("ORM is not configured, schema is missing");
191
        }
192
193
        return $this->schema;
194
    }
195
196
    /**
197
     * @inheritdoc
198
     */
199
    public function withHeap(HeapInterface $heap): ORMInterface
200
    {
201
        $orm = clone $this;
202
        $orm->heap = $heap;
203
        $orm->factory = $orm->factory->withSchema($orm, $orm->schema);
204
205
        return $orm;
206
    }
207
208
    /**
209
     * @inheritdoc
210
     */
211
    public function getHeap(): HeapInterface
212
    {
213
        return $this->heap;
214
    }
215
216
    /**
217
     * @inheritdoc
218
     */
219
    public function getMapper($entity): MapperInterface
220
    {
221
        $role = $this->resolveRole($entity);
222
        if (isset($this->mappers[$role])) {
223
            return $this->mappers[$role];
224
        }
225
226
        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

226
        return $this->mappers[$role] = $this->factory->/** @scrutinizer ignore-call */ mapper($role);
Loading history...
227
    }
228
229
    /**
230
     * @inheritdoc
231
     */
232
    public function getSource(string $role): SourceInterface
233
    {
234
        if (isset($this->sources[$role])) {
235
            return $this->sources[$role];
236
        }
237
238
        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

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

335
            /** @scrutinizer ignore-call */ 
336
            $relations[$relation] = $this->factory->relation($role, $relation);
Loading history...
336
        }
337
338
        return $this->relmaps[$role] = new RelationMap($this, $relations);
339
    }
340
}