Completed
Push — master ( be11e7...0e656e )
by Anton
23s queued 10s
created

src/ORM.php (1 issue)

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->factory = $factory;
67
        $this->schema = $schema ?? new Schema([]);
68
69
        $this->heap = new Heap();
70
        $this->generator = new CommandGenerator();
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)) {
89
                throw new ORMException("Unable to resolve role of `$class`");
90
            }
91
92
            $entity = $class;
93
        }
94
95
        return $this->schema->resolveAlias($entity);
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));
133
            }
134
        }
135
136
        // init entity class and prepared (typecasted) data
137
        [$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
        return $this->schema;
0 ignored issues
show
Bug Best Practice introduced by
The expression return $this->schema could return the type null which is incompatible with the type-hinted return Cycle\ORM\SchemaInterface. Consider adding an additional type-check to rule them out.
Loading history...
183
    }
184
185
    /**
186
     * @inheritdoc
187
     */
188
    public function withHeap(HeapInterface $heap): ORMInterface
189
    {
190
        $orm = clone $this;
191
        $orm->heap = $heap;
192
193
        return $orm;
194
    }
195
196
    /**
197
     * @inheritdoc
198
     */
199
    public function getHeap(): HeapInterface
200
    {
201
        return $this->heap;
202
    }
203
204
    /**
205
     * @inheritdoc
206
     */
207
    public function getMapper($entity): MapperInterface
208
    {
209
        $role = $this->resolveRole($entity);
210
        if (isset($this->mappers[$role])) {
211
            return $this->mappers[$role];
212
        }
213
214
        return $this->mappers[$role] = $this->factory->mapper($this, $this->schema, $role);
215
    }
216
217
    /**
218
     * @inheritdoc
219
     */
220
    public function getRepository($entity): RepositoryInterface
221
    {
222
        $role = $this->resolveRole($entity);
223
        if (isset($this->repositories[$role])) {
224
            return $this->repositories[$role];
225
        }
226
227
        $repository = $this->getSchema()->define($role, Schema::REPOSITORY) ?? Repository::class;
228
        $params = ['orm' => $this, 'role' => $role];
229
230
        if ($this->getSchema()->define($role, Schema::TABLE) !== null) {
231
            $params['select'] = new Select($this, $role);
232
            $params['select']->constrain($this->getSource($role)->getConstrain());
233
        }
234
235
        return $this->repositories[$role] = $this->factory->make($repository, $params);
236
    }
237
238
    /**
239
     * @inheritdoc
240
     */
241
    public function getSource(string $role): SourceInterface
242
    {
243
        if (isset($this->sources[$role])) {
244
            return $this->sources[$role];
245
        }
246
247
        $source = $this->schema->define($role, Schema::SOURCE) ?? Source::class;
248
        if ($source !== Source::class) {
249
            // custom implementation
250
            return $this->factory->make($source, ['orm' => $this, 'role' => $role]);
251
        }
252
253
        $source = new Source(
254
            $this->factory->database($this->schema->define($role, Schema::DATABASE)),
255
            $this->schema->define($role, Schema::TABLE)
256
        );
257
258
        $constrain = $this->schema->define($role, Schema::CONSTRAIN);
259
        if ($constrain !== null) {
260
            $source = $source->withConstrain(
261
                is_object($constrain) ? $constrain : $this->factory->make($constrain)
262
            );
263
        }
264
265
        return $this->sources[$role] = $source;
266
    }
267
268
    /**
269
     * Overlay existing promise factory.
270
     *
271
     * @param PromiseFactoryInterface $promiseFactory
272
     * @return ORM
273
     */
274
    public function withPromiseFactory(PromiseFactoryInterface $promiseFactory = null): self
275
    {
276
        $orm = clone $this;
277
        $orm->promiseFactory = $promiseFactory;
278
279
        return $orm;
280
    }
281
282
    /**
283
     * @inheritdoc
284
     *
285
     * Returns references by default.
286
     */
287
    public function promise(string $role, array $scope)
288
    {
289
        if (count($scope) === 1) {
290
            $e = $this->heap->find($role, key($scope), current($scope));
291
            if ($e !== null) {
292
                return $e;
293
            }
294
        }
295
296
        if ($this->promiseFactory !== null) {
297
            return $this->promiseFactory->promise($this, $role, $scope);
298
        }
299
300
        return new Reference($role, $scope);
301
    }
302
303
    /**
304
     * @inheritdoc
305
     */
306
    public function queueStore($entity, int $mode = TransactionInterface::MODE_CASCADE): ContextCarrierInterface
307
    {
308
        if ($entity instanceof ReferenceInterface) {
309
            // we do not expect to store promises
310
            return new Nil();
311
        }
312
313
        $mapper = $this->getMapper($entity);
314
315
        $node = $this->heap->get($entity);
316
        if (is_null($node)) {
317
            // automatic entity registration
318
            $node = new Node(Node::NEW, [], $mapper->getRole());
319
            $this->heap->attach($entity, $node);
320
        }
321
322
        $cmd = $this->generator->generateStore($mapper, $entity, $node);
323
        if ($mode != TransactionInterface::MODE_CASCADE) {
324
            return $cmd;
325
        }
326
327
        if ($this->schema->define($node->getRole(), Schema::RELATIONS) === []) {
328
            return $cmd;
329
        }
330
331
        // generate set of commands required to store entity relations
332
        return $this->getRelationMap($node->getRole())->queueRelations(
333
            $cmd,
334
            $entity,
335
            $node,
336
            $mapper->extract($entity)
337
        );
338
    }
339
340
    /**
341
     * @inheritdoc
342
     */
343
    public function queueDelete($entity, int $mode = TransactionInterface::MODE_CASCADE): CommandInterface
344
    {
345
        $node = $this->heap->get($entity);
346
        if ($entity instanceof ReferenceInterface || is_null($node)) {
347
            // nothing to do, what about promises?
348
            return new Nil();
349
        }
350
351
        // currently we rely on db to delete all nested records (or soft deletes)
352
        return $this->generator->generateDelete($this->getMapper($node->getRole()), $entity, $node);
353
    }
354
355
    /**
356
     * Reset related objects cache.
357
     */
358
    public function __clone()
359
    {
360
        $this->mappers = [];
361
        $this->relmaps = [];
362
        $this->indexes = [];
363
        $this->sources = [];
364
        $this->repositories = [];
365
    }
366
367
    /**
368
     * @return array
369
     */
370
    public function __debugInfo()
371
    {
372
        return [
373
            'schema' => $this->schema
374
        ];
375
    }
376
377
    /**
378
     * Get list of keys entity must be indexed in a Heap by.
379
     *
380
     * @param string $role
381
     * @return array
382
     */
383
    protected function getIndexes(string $role): array
384
    {
385
        if (isset($this->indexes[$role])) {
386
            return $this->indexes[$role];
387
        }
388
389
        $pk = $this->schema->define($role, Schema::PRIMARY_KEY);
390
        $keys = $this->schema->define($role, Schema::FIND_BY_KEYS) ?? [];
391
392
        return $this->indexes[$role] = array_merge([$pk], $keys);
393
    }
394
395
    /**
396
     * Get relation map associated with the given class.
397
     *
398
     * @param string $entity
399
     * @return RelationMap
400
     */
401
    protected function getRelationMap($entity): RelationMap
402
    {
403
        $role = $this->resolveRole($entity);
404
        if (isset($this->relmaps[$role])) {
405
            return $this->relmaps[$role];
406
        }
407
408
        $relations = [];
409
410
        $names = array_keys($this->schema->define($role, Schema::RELATIONS));
411
        foreach ($names as $relation) {
412
            $relations[$relation] = $this->factory->relation($this, $this->schema, $role, $relation);
413
        }
414
415
        return $this->relmaps[$role] = new RelationMap($this, $relations);
416
    }
417
}
418