Passed
Push — master ( 429ea4...522764 )
by Anton
01:43
created

ORM::getRepository()   A

Complexity

Conditions 2
Paths 2

Size

Total Lines 13
Code Lines 7

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 2
eloc 7
nc 2
nop 1
dl 0
loc 13
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\PromiseInterface;
20
use Cycle\ORM\Select\Repository;
21
use Cycle\ORM\Select\SourceFactoryInterface;
22
use Cycle\ORM\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 RepositoryInterface[] */
45
    private $repositories = [];
46
47
    /** @var RelationMap[] */
48
    private $relmaps = [];
49
50
    /** @var array */
51
    private $indexes = [];
52
53
    /** @var SourceInterface[] */
54
    private $sources = [];
55
56
    /**
57
     * @param FactoryInterface|SourceFactoryInterface $factory
58
     * @param SchemaInterface|null                    $schema
59
     */
60
    public function __construct(FactoryInterface $factory, SchemaInterface $schema = null)
61
    {
62
        if (!$factory instanceof SourceFactoryInterface) {
63
            throw new ORMException("Source factory is missing");
64
        }
65
66
        $this->generator = new CommandGenerator();
67
        $this->factory = $factory;
68
69
        if (!is_null($schema)) {
70
            $this->schema = $schema;
71
            $this->factory = $this->factory->withSchema($this, $schema);
72
        }
73
74
        $this->heap = new Heap();
75
    }
76
77
    /**
78
     * Automatically resolve role based on object name.
79
     *
80
     * @param string|object $entity
81
     * @return string
82
     */
83
    public function resolveRole($entity): string
84
    {
85
        if (is_object($entity)) {
86
            $class = get_class($entity);
87
            if (!$this->schema->defines($class)) {
88
                $node = $this->getHeap()->get($entity);
89
                if (is_null($node)) {
90
                    throw new ORMException("Unable to resolve role of `$class`");
91
                }
92
93
                return $node->getRole();
94
            }
95
96
            $entity = $class;
97
        }
98
99
        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...
100
    }
101
102
    /**
103
     * @inheritdoc
104
     */
105
    public function get(string $role, $id, bool $load = true)
106
    {
107
        $role = $this->resolveRole($role);
108
        $pk = $this->schema->define($role, Schema::PRIMARY_KEY);
109
110
        if (!is_null($e = $this->heap->find($role, $pk, $id))) {
111
            return $e;
112
        }
113
114
        if (!$load) {
115
            return null;
116
        }
117
118
        return $this->getRepository($role)->findByPK($id);
119
    }
120
121
    /**
122
     * @inheritdoc
123
     */
124
    public function make(string $role, array $data = [], int $node = Node::NEW)
125
    {
126
        $m = $this->getMapper($role);
127
128
        // unique entity identifier
129
        $pk = $this->schema->define($role, Schema::PRIMARY_KEY);
130
        $id = $data[$pk] ?? null;
131
132
        if ($node !== Node::NEW && !empty($id)) {
133
            if (!empty($e = $this->heap->find($role, $pk, $id))) {
134
                $node = $this->getHeap()->get($e);
135
136
                // entity already been loaded, let's update it's relations with new context
137
                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

137
                return $m->hydrate($e, $this->getRelmap($role)->init(/** @scrutinizer ignore-type */ $node, $data));
Loading history...
138
            }
139
        }
140
141
        // init entity class and prepared (typecasted) data
142
        list($e, $prepared) = $m->init($data);
143
144
        $node = new Node($node, $prepared, $m->getRole());
145
146
        $this->heap->attach($e, $node, $this->getIndexes($m->getRole()));
147
148
        // hydrate entity with it's data, relations and proxies
149
        return $m->hydrate($e, $this->getRelmap($role)->init($node, $prepared));
150
    }
151
152
    /**
153
     * @inheritdoc
154
     */
155
    public function withFactory(FactoryInterface $factory): ORMInterface
156
    {
157
        $orm = clone $this;
158
        $orm->factory = $factory;
159
160
        if (!is_null($orm->schema)) {
161
            $orm->factory = $factory->withSchema($orm, $orm->schema);
162
        }
163
164
        return $orm;
165
    }
166
167
    /**
168
     * @inheritdoc
169
     */
170
    public function getFactory(): FactoryInterface
171
    {
172
        return $this->factory;
0 ignored issues
show
Bug Best Practice introduced by
The expression return $this->factory could return the type Cycle\ORM\Select\SourceFactoryInterface which is incompatible with the type-hinted return Cycle\ORM\FactoryInterface. Consider adding an additional type-check to rule them out.
Loading history...
173
    }
174
175
    /**
176
     * @inheritdoc
177
     */
178
    public function withSchema(SchemaInterface $schema): ORMInterface
179
    {
180
        $orm = clone $this;
181
        $orm->schema = $schema;
182
        $orm->factory = $orm->factory->withSchema($orm, $orm->schema);
0 ignored issues
show
Bug introduced by
The method withSchema() does not exist on Cycle\ORM\Select\SourceFactoryInterface. Since it exists in all sub-types, consider adding an abstract or default implementation to Cycle\ORM\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

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

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

229
        return $this->mappers[$role] = $this->factory->/** @scrutinizer ignore-call */ mapper($role);
Loading history...
230
    }
231
232
    /**
233
     * @inheritdoc
234
     */
235
    public function getRepository($entity): RepositoryInterface
236
    {
237
        $role = $this->resolveRole($entity);
238
        if (isset($this->repositories[$role])) {
239
            return $this->repositories[$role];
240
        }
241
242
        $selector = new Select($this, $role);
243
        $selector->constrain($this->getSource($role)->getConstrain());
244
245
        $repositoryClass = $this->getSchema()->define($role, Schema::REPOSITORY) ?? Repository::class;
246
247
        return $this->repositories[$role] = new $repositoryClass($selector);
248
    }
249
250
    /**
251
     * @inheritdoc
252
     */
253
    public function getSource(string $role): SourceInterface
254
    {
255
        if (isset($this->sources[$role])) {
256
            return $this->sources[$role];
257
        }
258
259
        return $this->sources[$role] = $this->factory->getSource($role);
0 ignored issues
show
Bug introduced by
The method getSource() does not exist on Cycle\ORM\FactoryInterface. Since it exists in all sub-types, consider adding an abstract or default implementation to Cycle\ORM\FactoryInterface. ( Ignorable by Annotation )

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

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

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

356
            /** @scrutinizer ignore-call */ 
357
            $relations[$relation] = $this->factory->relation($role, $relation);
Loading history...
357
        }
358
359
        return $this->relmaps[$role] = new RelationMap($this, $relations);
360
    }
361
}