Mapper::mapToStorageQuery()   A
last analyzed

Complexity

Conditions 1
Paths 1

Size

Total Lines 13
Code Lines 6

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 1
eloc 6
c 0
b 0
f 0
nc 1
nop 1
dl 0
loc 13
rs 10
1
<?php
2
3
namespace Darya\ORM;
4
5
use Darya\ORM\Exception\EntityNotFoundException;
6
use Darya\Storage;
7
use ReflectionClass;
8
use ReflectionException;
9
use UnexpectedValueException;
10
11
/**
12
 * Darya's entity mapper.
13
 *
14
 * Maps a single type of entity to a queryable storage interface.
15
 *
16
 * TODO: Entity factory for instantiation; this could allow dynamically defined entities
17
 * TODO: Entity caching
18
 * TODO: Map to array, for lighter data mapping that avoids any instantiation
19
 * TODO: Consider other types of storage, e.g. a cache, which may only find by ID
20
 * TODO: Composite key support
21
 *
22
 * @author Chris Andrew <[email protected]>
23
 */
24
class Mapper
25
{
26
	/**
27
	 * The entity manager.
28
	 *
29
	 * @var EntityManager
30
	 */
31
	private $orm;
32
33
	/**
34
	 * The EntityMap to map to storage.
35
	 *
36
	 * @var EntityMap
37
	 */
38
	protected $entityMap;
39
40
	/**
41
	 * The storage to map to.
42
	 *
43
	 * @var Storage\Queryable
44
	 */
45
	protected $storage;
46
47
	/**
48
	 * Create a new mapper.
49
	 *
50
	 * @param EntityManager     $orm       The entity manager.
51
	 * @param EntityMap         $entityMap The entity map to use.
52
	 * @param Storage\Queryable $storage   The storage to map to.
53
	 */
54
	public function __construct(EntityManager $orm, EntityMap $entityMap, Storage\Queryable $storage)
55
	{
56
		$this->orm       = $orm;
57
		$this->entityMap = $entityMap;
58
		$this->storage   = $storage;
59
	}
60
61
	/**
62
	 * Check whether a single entity exists with the given ID.
63
	 *
64
	 * @param mixed $id The ID of the entity to check.
65
	 * @return bool
66
	 */
67
	public function has($id): bool
68
	{
69
		if ($id === null) {
70
			return false;
71
		}
72
73
		$entityMap  = $this->getEntityMap();
74
		$resource   = $entityMap->getResource();
75
		$storageKey = $entityMap->getStorageKey();
76
77
		$result = $this->storage->query($resource)
78
			->fields([$storageKey])
0 ignored issues
show
Bug introduced by
The method fields() does not exist on Darya\Storage\Query\Builder. Since you implemented __call, consider adding a @method annotation. ( Ignorable by Annotation )

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

78
			->/** @scrutinizer ignore-call */ fields([$storageKey])
Loading history...
79
			->where($storageKey, $id)
0 ignored issues
show
Bug introduced by
The method where() does not exist on Darya\Storage\Query\Builder. Since you implemented __call, consider adding a @method annotation. ( Ignorable by Annotation )

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

79
			->/** @scrutinizer ignore-call */ where($storageKey, $id)
Loading history...
Bug introduced by
The method where() does not exist on Darya\Storage\Result. ( Ignorable by Annotation )

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

79
			->/** @scrutinizer ignore-call */ where($storageKey, $id)

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...
80
			->limit(1)
0 ignored issues
show
Bug introduced by
The method limit() does not exist on Darya\Storage\Query\Builder. Since you implemented __call, consider adding a @method annotation. ( Ignorable by Annotation )

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

80
			->/** @scrutinizer ignore-call */ limit(1)
Loading history...
Bug introduced by
The method limit() does not exist on Darya\Storage\Result. ( Ignorable by Annotation )

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

80
			->/** @scrutinizer ignore-call */ limit(1)

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...
81
			->run();
0 ignored issues
show
Bug introduced by
The method run() does not exist on Darya\Storage\Result. ( Ignorable by Annotation )

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

81
			->/** @scrutinizer ignore-call */ run();

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...
82
83
		return $result->count > 0;
84
	}
85
86
	/**
87
	 * Find a single entity with the given ID.
88
	 *
89
	 * Returns null if the entity is not found.
90
	 *
91
	 * @param mixed $id The ID of the entity to find.
92
	 * @return object|null The entity, or null if it is not found.
93
	 */
94
	public function find($id)
95
	{
96
		$storageKey = $this->getEntityMap()->getStorageKey();
97
98
		$entities = $this->query()
99
			->where($storageKey, $id)
0 ignored issues
show
Bug introduced by
The method where() does not exist on Darya\ORM\Query\Builder. Since you implemented __call, consider adding a @method annotation. ( Ignorable by Annotation )

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

99
			->/** @scrutinizer ignore-call */ where($storageKey, $id)
Loading history...
100
			->run();
101
102
		if (!count($entities)) {
0 ignored issues
show
Bug introduced by
It seems like $entities can also be of type Darya\Storage\Result; however, parameter $value of count() does only seem to accept Countable|array, 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

102
		if (!count(/** @scrutinizer ignore-type */ $entities)) {
Loading history...
103
			return null;
104
		}
105
106
		return $entities[0];
107
	}
108
109
	/**
110
	 * Find a single entity with the given ID or error if it is not found.
111
	 *
112
	 * Throws an EntityNotFoundException if the entity is not found.
113
	 *
114
	 * @param mixed $id The ID of the entity to find.
115
	 * @return object The entity.
116
	 * @throws EntityNotFoundException When the entity is not found.
117
	 */
118
	public function findOrFail($id)
119
	{
120
		$entity = $this->find($id);
121
122
		if ($entity !== null) {
123
			return $entity;
124
		}
125
126
		$name = $this->getEntityMap()->getName();
127
128
		throw (new EntityNotFoundException())->setEntityName($name);
129
	}
130
131
	/**
132
	 * Find a single entity with the given ID or create a new one if it not found.
133
	 *
134
	 * @param mixed $id The ID of the entity to find.
135
	 * @return object The entity.
136
	 */
137
	public function findOrNew($id)
138
	{
139
		return $this->find($id) ?: $this->newInstance();
140
	}
141
142
	/**
143
	 * Find the entities with the given IDs.
144
	 *
145
	 * @param mixed[] $ids The IDs of the entities to find.
146
	 * @return object[]
147
	 */
148
	public function findMany(array $ids): array
149
	{
150
		$storageKey = $this->getEntityMap()->getStorageKey();
151
152
		return $this->query()
0 ignored issues
show
Bug Best Practice introduced by
The expression return $this->query()->w...torageKey, $ids)->run() could return the type Darya\Storage\Result which is incompatible with the type-hinted return array. Consider adding an additional type-check to rule them out.
Loading history...
153
			->where($storageKey, $ids)
154
			->run();
155
	}
156
157
	/**
158
	 * Find all entities.
159
	 *
160
	 * @return object[]
161
	 */
162
	public function all(): array
163
	{
164
		return $this->query()->run();
0 ignored issues
show
Bug Best Practice introduced by
The expression return $this->query()->run() could return the type Darya\Storage\Result which is incompatible with the type-hinted return array. Consider adding an additional type-check to rule them out.
Loading history...
165
	}
166
167
	/**
168
	 * Open a query to the storage that the entity is mapped to.
169
	 *
170
	 * @return Query\Builder
171
	 */
172
	public function query(): Query\Builder
173
	{
174
		$query = new Query\Builder(
175
			new Query($this),
176
			$this->orm
177
		);
178
179
		return $query;
180
	}
181
182
	/**
183
	 * Run a query.
184
	 *
185
	 * @param Query $query
186
	 * @return array TODO: Return a Storage\Result?
187
	 */
188
	public function run(Query $query): array
189
	{
190
		if ($this->equals($query->mapper)) {
191
			throw new UnexpectedValueException("Unexpected query mapper for entity '{$query->mapper->getEntityMap()->getName()}'");
192
		}
193
194
		// Load entities with relationship existence check
195
		$result   = $this->loadWhereHas($query);
196
		$entities = $this->newInstances($result->data);
197
198
		// Load related entities
199
		$this->loadWith($entities, $query);
200
201
		return $entities;
202
	}
203
204
	/**
205
	 * Load entities that match the query's relationship existence constraints.
206
	 *
207
	 * @param Query $query
208
	 * @return Storage\Result
209
	 */
210
	protected function loadWhereHas(Query $query): Storage\Result
211
	{
212
		// Load root entity IDs
213
		$storage    = $this->getStorage();
214
		$entityMap  = $this->getEntityMap();
215
		$resource   = $entityMap->getResource();
216
		$storageKey = $entityMap->getStorageKey();
217
218
		$result = $storage->query($resource)
219
			->copyFrom($query)
0 ignored issues
show
Bug introduced by
The method copyFrom() does not exist on Darya\Storage\Query\Builder. Since you implemented __call, consider adding a @method annotation. ( Ignorable by Annotation )

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

219
			->/** @scrutinizer ignore-call */ copyFrom($query)
Loading history...
220
			->fields($storageKey)
0 ignored issues
show
Bug introduced by
The method fields() does not exist on Darya\Storage\Result. ( Ignorable by Annotation )

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

220
			->/** @scrutinizer ignore-call */ fields($storageKey)

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...
221
			->run();
222
223
		$ids = array_column($result->data, $storageKey);
224
225
		// TODO: Check related entity existence ($query->has) to filter down IDs
226
		//       OR use a subquery in the below query (when count() is a thing)
227
		//          and when the storage for parent and related are the same
228
229
		// Filter down to entities matching the relationship existence check
230
		$query->where($storageKey, $ids);
231
232
		return $storage->run($query);
233
	}
234
235
	/**
236
	 * Eagerly-load relationships for, and match them to, the given entities.
237
	 *
238
	 * @param object[] $entities The entities to load relationships for.
239
	 * @param Query    $query    The query with the relationships to load.
240
	 * @return object[] The entities with the given relationships loaded.
241
	 */
242
	protected function loadWith(array $entities, Query $query): array
243
	{
244
		$graph         = $this->orm->graph();
245
		$entityName    = $this->getEntityMap()->getName();
246
		$relationships = $graph->getRelationships($entityName, $query->with);
247
248
		foreach ($relationships as $relationship) {
249
			$relationship      = $relationship->forParents($entities);
250
			$relatedEntityName = $relationship->getRelatedMap()->getName();
251
252
			$relatedMapper     = $this->orm->mapper($relatedEntityName);
253
			$relationshipQuery = $this->orm->prepareQuery($relationship);
254
255
			$relationship->match($entities, $relatedMapper->run($relationshipQuery));
256
		}
257
258
		return $entities;
259
	}
260
261
	/**
262
	 * Store an entity to its mapped storage.
263
	 *
264
	 * Creates or updates the entity in storage depending on whether it exists.
265
	 *
266
	 * If storage returns a key after a create query, it will be set on the entity
267
	 *
268
	 * TODO: Store relationships that aren't mapped to the same storage as the root entity.
269
	 * TODO: Implement storeMany($entities), or accept many in this $entity parameter
270
	 *
271
	 * @param object $entity
272
	 * @return object The mapped entity
273
	 */
274
	public function store($entity)
275
	{
276
		$entityMap   = $this->getEntityMap();
277
		$resource    = $entityMap->getResource();
278
		$storageKey  = $entityMap->getStorageKey();
279
		$storageData = $this->mapToStorage($entity);
280
281
		// Determine whether the entity exists in storage
282
		$id     = $storageData[$storageKey] ?? null;
283
		$exists = $this->has($id);
284
285
		// Update or create in storage accordingly
286
		$query = $this->storage->query($resource);
287
288
		if ($exists) {
289
			$query->update($storageData)
0 ignored issues
show
Bug introduced by
The method update() does not exist on Darya\Storage\Query\Builder. Since you implemented __call, consider adding a @method annotation. ( Ignorable by Annotation )

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

289
			$query->/** @scrutinizer ignore-call */ 
290
           update($storageData)
Loading history...
290
				->where($storageKey, $id)
291
				->run();
292
293
			return $entity;
294
		}
295
296
		$result = $query->create($storageData)->run();
0 ignored issues
show
Bug introduced by
The method create() does not exist on Darya\Storage\Query\Builder. Since you implemented __call, consider adding a @method annotation. ( Ignorable by Annotation )

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

296
		$result = $query->/** @scrutinizer ignore-call */ create($storageData)->run();
Loading history...
297
298
		// Set the insert ID as the entity's key, if one is returned
299
		if ($result->insertId) {
300
			$storageData              = $this->mapToStorage($entity);
301
			$storageData[$storageKey] = $result->insertId;
302
			$entity                   = $this->mapFromStorage($entity, $storageData);
303
		}
304
305
		return $entity;
306
	}
307
308
	/**
309
	 * Delete an entity from its mapped storage.
310
	 *
311
	 * @param object $entity
312
	 */
313
	public function delete($entity)
314
	{
315
		$entityMap   = $this->getEntityMap();
316
		$resource    = $entityMap->getResource();
317
		$storageKey  = $entityMap->getStorageKey();
318
		$storageData = $this->mapToStorage($entity);
319
320
		$this->storage->query($resource)
321
			->where($storageKey, $storageData[$storageKey])
322
			->delete();
0 ignored issues
show
Bug introduced by
The method delete() does not exist on Darya\Storage\Result. ( Ignorable by Annotation )

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

322
			->/** @scrutinizer ignore-call */ delete();

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...
Bug introduced by
The method delete() does not exist on Darya\Storage\Query\Builder. Since you implemented __call, consider adding a @method annotation. ( Ignorable by Annotation )

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

322
			->/** @scrutinizer ignore-call */ delete();
Loading history...
323
	}
324
325
	/**
326
	 * Create a new instance of the mapper's entity.
327
	 *
328
	 * TODO: Factory work should happen here, via the entity map or otherwise
329
	 *
330
	 * @throws ReflectionException
331
	 * @return object
332
	 */
333
	public function newInstance()
334
	{
335
		$reflection = new ReflectionClass($this->getEntityMap()->getClass());
336
337
		return $reflection->newInstance();
338
	}
339
340
	/**
341
	 * Create new instances of the mapper's entity from the given storage data.
342
	 *
343
	 * @param array $storageData The storage data to create entities from.
344
	 * @return array The new entities.
345
	 */
346
	public function newInstances(array $storageData): array
347
	{
348
		$entities = [];
349
350
		foreach ($storageData as $entityDatum) {
351
			$entities[] = $this->mapFromStorage($this->newInstance(), $entityDatum);
352
		}
353
354
		return $entities;
355
	}
356
357
	/**
358
	 * Get the entity map.
359
	 *
360
	 * @return EntityMap
361
	 */
362
	public function getEntityMap(): EntityMap
363
	{
364
		return $this->entityMap;
365
	}
366
367
	/**
368
	 * Get the storage to map to.
369
	 *
370
	 * @return Storage\Queryable
371
	 */
372
	public function getStorage(): Storage\Queryable
373
	{
374
		return $this->storage;
375
	}
376
377
	/**
378
	 * Map from storage data to an entity.
379
	 *
380
	 * @param object $entity      The entity to map to.
381
	 * @param array  $storageData The storage data to map from.
382
	 * @return object The resulting entity.
383
	 */
384
	public function mapFromStorage($entity, array $storageData)
385
	{
386
		return $this->getEntityMap()->mapFromStorage($entity, $storageData);
387
	}
388
389
	/**
390
	 * Map from an entity to storage data.
391
	 *
392
	 * @param object $entity The entity to map from.
393
	 * @return array The resulting storage data.
394
	 */
395
	public function mapToStorage($entity): array
396
	{
397
		return $this->getEntityMap()->mapToStorage($entity);
398
	}
399
400
	/**
401
	 * Map an ORM query to a storage query.
402
	 *
403
	 * This method does not map relationship loading in any way.
404
	 *
405
	 * @param Query $query The ORM query to map.
406
	 * @return Storage\Query The mapped storage query.
407
	 */
408
	protected function mapToStorageQuery(Query $query): Storage\Query
409
	{
410
		$entityMap = $this->getEntityMap();
411
		$resource  = $entityMap->getResource();
412
413
		$storageQuery = new Storage\Query($resource);
414
415
		// TODO: Map all other identifiers in the query; fields, filters, etc
416
417
		$storageQuery->copyFrom($query);
418
		$storageQuery->resource($resource);
419
420
		return $storageQuery;
421
	}
422
423
	/**
424
	 * Compare a mapper with this mapper.
425
	 *
426
	 * Compares the equivalence of the entity maps and storages of the given
427
	 * mapper with this mapper.
428
	 *
429
	 * @param Mapper $mapper
430
	 * @return bool
431
	 */
432
	public function equals(Mapper $mapper)
433
	{
434
		return !($mapper->getEntityMap() === $this->getEntityMap()) ||
435
			   !($mapper->getStorage() === $this->getStorage() || $mapper->getStorage() === $this->orm);
436
	}
437
}
438