DibiEntityAssistant::getEntityProperties()   A
last analyzed

Complexity

Conditions 4
Paths 2

Size

Total Lines 15
Code Lines 10

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 11
CRAP Score 4

Importance

Changes 0
Metric Value
cc 4
eloc 10
nc 2
nop 1
dl 0
loc 15
ccs 11
cts 11
cp 1
crap 4
rs 9.9332
c 0
b 0
f 0
1
<?php
2
namespace SpareParts\Pillar\Assistant\Dibi;
3
4
use SpareParts\Pillar\Entity\IEntity;
5
use SpareParts\Pillar\Mapper\Dibi\ColumnInfo;
6
use SpareParts\Pillar\Mapper\Dibi\IEntityMapping;
7
use SpareParts\Pillar\Mapper\Dibi\TableInfo;
8
use SpareParts\Pillar\Mapper\EntityMappingException;
9
use SpareParts\Pillar\Mapper\IMapper;
10
11
class DibiEntityAssistant
12
{
13
	/**
14
	 * @var IMapper
15
	 */
16
	protected $mapper;
17
18
	/**
19
	 * @var IConnectionProvider
20
	 */
21
	protected $connectionProvider;
22
23
	/**
24
	 * @var IEntityFactory
25
	 */
26
	protected $entityFactory;
27
28
	/**
29
	 * @var array
30
	 */
31
	private $entityPropertiesCache = [];
32
33
34
	/**
35
	 * @param IMapper $mapper
36
	 * @param IConnectionProvider $connectionProvider
37
	 * @param IEntityFactory $entityFactory
38
	 */
39 6
	public function __construct(IMapper $mapper, IEntityFactory $entityFactory, IConnectionProvider $connectionProvider)
40
	{
41 6
		$this->mapper = $mapper;
42 6
		$this->connectionProvider = $connectionProvider;
43 6
		$this->entityFactory = $entityFactory;
44 6
	}
45
46
	/**
47
	 * @param string|IEntity $entityClassOrInstance
48
	 * @param bool $returnEntities If set to false, does not try to format fetched data using EntityFactory
49
	 * @return Fluent
50
	 * @throws \SpareParts\Pillar\Mapper\EntityMappingException
51
	 */
52 1
	public function fluent($entityClassOrInstance, $returnEntities = true)
53
	{
54 1
		$mapping = $this->mapper->getEntityMapping($entityClassOrInstance);
55 1
		if ($mapping->isVirtualEntity()) {
56
			throw new UnableToSaveException('Virtual property cannot be persisted!');
57
		}
58
59 1
		$fluent = new Fluent(
60 1
			$this->connectionProvider->getConnection(),
61 1
			$mapping,
62 1
			$returnEntities ? $this->entityFactory : null
63
		);
64 1
		return $fluent;
65
	}
66
67
68
	/**
69
	 * This method returns "blank" fluent, does not force you to use IEntity resulting class (i.e. fetches plain array)
70
	 *
71
	 * @param string|IEntity $entityClassOrInstance
72
	 * @return Fluent
73
	 * @throws \SpareParts\Pillar\Mapper\EntityMappingException
74
	 */
75 3
	public function fluentForAggregateCalculations($entityClassOrInstance)
76
	{
77 3
		$fluent = new Fluent(
78 3
			$this->connectionProvider->getConnection(),
79 3
			$this->mapper->getEntityMapping($entityClassOrInstance)
80
		);
81 3
		return $fluent;
82
	}
83
84
	/**
85
	 * Update entity database representation to current entity values.
86
	 *
87
	 * @param IEntity $entity
88
	 * @param array|null $tables
89
	 *
90
	 * @return int Number of affected rows. Can be higher than 1, if multiple tables were changed!
91
	 *
92
	 * @throws EntityMappingException
93
	 * @throws UnableToSaveException
94
	 * @throws \Dibi\Exception
95
	 * @throws \InvalidArgumentException
96
	 */
97 1
	public function update(IEntity $entity, array $tables = null)
98
	{
99 1
		$mapping = $this->mapper->getEntityMapping($entity);
100 1
		if ($mapping->isVirtualEntity()) {
101
			throw new UnableToSaveException('Virtual property cannot be persisted!');
102
		}
103 1
		$tableInfos = $this->sanitizeTableInfos($entity, $tables);
104
105
		// we iterate over tables and update each of them that has changed
106 1
		$affectedRows = 0;
107 1
		foreach ($tableInfos as $tableInfo) {
108 1
			$columnValuesToStore = $this->getValuesToUpdate($entity, $mapping, $tableInfo);
109
110
			// nothing to update in this table
111 1
			if (!count($columnValuesToStore)) {
112
				continue;
113
			}
114
115 1
			$fluent = $this->connectionProvider->getConnection()
116 1
				->update($tableInfo->getName(), $columnValuesToStore)
117 1
				->limit(1);
118
119 1
			$columnInfos = $this->getColumnInfosForTable($mapping, $tableInfo);
120 1
			$fluent = $this->addPKToFluent($entity, $tableInfo->getIdentifier(), $columnInfos, $fluent);
121 1
			$affectedRows += $fluent->execute(\dibi::AFFECTED_ROWS);
122
		}
123 1
		return $affectedRows;
124
	}
125
126
	/**
127
	 * @param IEntity $entity
128
	 * @param string $tableName
129
	 *
130
	 * @return int|null|string Primary key in case the new record was inserted, null otherwise
131
	 *
132
	 * @throws UnableToSaveException
133
	 * @throws \InvalidArgumentException
134
	 * @throws \Dibi\Exception
135
	 */
136 1
	public function insert(IEntity $entity, $tableName)
137
	{
138 1
		$mapping = $this->mapper->getEntityMapping($entity);
139 1
		if ($mapping->isVirtualEntity()) {
140
			throw new UnableToSaveException('Virtual property cannot be persisted!');
141
		}
142 1
		$tableInfo = $this->getTableInfoByTableName($entity, $mapping, $tableName);
143 1
		$columnValuesToStore = $this->getValuesToInsert($entity, $mapping, $tableInfo);
144
145 1
		if (!$columnValuesToStore) {
146
			return null;
147
		}
148
149 1
		$fluent = $this->connectionProvider->getConnection()
150 1
			->insert($tableInfo->getName(), $columnValuesToStore);
151
152
		try {
153 1
			return $fluent->execute(\dibi::IDENTIFIER);
0 ignored issues
show
Bug Best Practice introduced by
The expression return $fluent->execute(dibi::IDENTIFIER) also could return the type Dibi\Result which is incompatible with the documented return type integer|null|string.
Loading history...
154
		} catch (\Dibi\Exception $exception) {
155
			// let's assume this is because the PK wasn't AUTO_INCREMENT...
156
			// *waiting for pull request with better way to do this :)*
157
			if ($exception->getMessage() !== 'Cannot retrieve last generated ID.') {
158
				throw $exception;
159
			}
160
		}
161
		return null;
162
	}
163
164
	/**
165
	 * !!! This method "spends" values in the autoincrement columns (even when not inserting)), so use it wisely.
166
	 * This is caused by how mysql treats ON DUPLICATE KEY INSERT clause commands.
167
	 *
168
	 * @param IEntity $entity
169
	 * @param $tableName
170
	 *
171
	 * @return int|string|null
172
	 * @throws \Dibi\Exception
173
	 * @internal param \string[] $tables
174
	 */
175
	public function insertOrUpdate(IEntity $entity, $tableName)
176
	{
177
		$mapping = $this->mapper->getEntityMapping($entity);
178
		if ($mapping->isVirtualEntity()) {
179
			throw new UnableToSaveException('Virtual property cannot be persisted!');
180
		}
181
		$tableInfo = $this->getTableInfoByTableName($entity, $mapping, $tableName);
182
		$columnValuesToStore = $this->getValuesToInsert($entity, $mapping, $tableInfo);
183
		$columnValuesToUpdate = $this->getValuesToUpdate($entity, $mapping, $tableInfo);
184
185
		if (!$columnValuesToStore) {
186
			return null;
187
		}
188
189
		$fluent = $this->connectionProvider->getConnection()
190
			->insert($tableInfo->getName(), $columnValuesToStore);
191
192
		if ($columnValuesToUpdate) {
0 ignored issues
show
Bug Best Practice introduced by
The expression $columnValuesToUpdate of type array<mixed,mixed> is implicitly converted to a boolean; are you sure this is intended? If so, consider using ! empty($expr) instead to make it clear that you intend to check for an array without elements.

This check marks implicit conversions of arrays to boolean values in a comparison. While in PHP an empty array is considered to be equal (but not identical) to false, this is not always apparent.

Consider making the comparison explicit by using empty(..) or ! empty(...) instead.

Loading history...
193
			$fluent = $fluent->onDuplicateKeyUpdate('%a', $columnValuesToUpdate);
194
		}
195
196
		try {
197
			return $fluent->execute(\dibi::IDENTIFIER);
0 ignored issues
show
Bug Best Practice introduced by
The expression return $fluent->execute(dibi::IDENTIFIER) also could return the type Dibi\Result which is incompatible with the documented return type integer|null|string.
Loading history...
198
		} catch (\Dibi\Exception $exception) {
199
			// let's assume this is because the PK wasn't AUTO_INCREMENT...
200
			// *waiting for pull request with better way to do this :)*
201
			if ($exception->getMessage() !== 'Cannot retrieve last generated ID.') {
202
				throw $exception;
203
			}
204
		}
205
		return null;
206
	}
207
208
	/**
209
	 * @param IEntity $entity
210
	 * @param $tableName
211
	 *
212
	 * @return int
213
	 *
214
	 * @throws EntityMappingException
215
	 * @throws UnableToSaveException
216
	 * @throws \Dibi\Exception
217
	 */
218
	public function delete(IEntity $entity, $tableName)
219
	{
220
		$mapping = $this->mapper->getEntityMapping($entity);
221
222
		if ($mapping->isVirtualEntity()) {
223
			throw new UnableToSaveException('Virtual property cannot be persisted!');
224
		}
225
226
		$tableInfo = $this->getTableInfoByTableName($entity, $mapping, $tableName);
227
228
		$fluent = $this->connectionProvider->getConnection()
229
			->delete($tableInfo->getName())
230
			->limit(1);
231
232
		// we need array key of ColumnInfo[] to be the column name for easy handling
233
		/** @var ColumnInfo[] $columnInfos */
234
		$columnInfos = $this->getColumnInfosForTable($mapping, $tableInfo);
235
		$fluent = $this->addPKToFluent($entity, $tableInfo->getName(), $columnInfos, $fluent);
236
237
		return $fluent->execute(\dibi::AFFECTED_ROWS);
0 ignored issues
show
Bug Best Practice introduced by
The expression return $fluent->execute(dibi::AFFECTED_ROWS) also could return the type Dibi\Result which is incompatible with the documented return type integer.
Loading history...
238
	}
239
240
	/**
241
	 * @param IEntity $entity
242
	 * @param string $tableName
243
	 * @param ColumnInfo[] $columns
244
	 * @param \Dibi\Fluent $fluent
245
	 *
246
	 * @return \Dibi\Fluent
247
	 *
248
	 * @throws UnableToSaveException
249
	 */
250
	private function addPKToFluent(IEntity $entity, $tableName, $columns, \Dibi\Fluent $fluent)
251
	{
252
		/** @var ColumnInfo[] $pkColumns */
253 1
		$pkColumns = array_filter($columns, function (ColumnInfo $columnInfo) {
254 1
			return $columnInfo->isPrimaryKey();
255 1
		});
256 1
		if (!count($pkColumns)) {
257
			throw new UnableToSaveException(sprintf('No primary key exists for table %s of entity %s', $tableName,
258
				get_class($entity)));
259
		}
260
		// all columns marked as "primary" are considered to be a part of primary key
261 1
		foreach ($pkColumns as $pkColumnInfo) {
262 1
			$pkValue = $this->getEntityPropertyValue($entity, $pkColumnInfo);
263 1
			if (is_null($pkValue)) {
264
				throw new UnableToSaveException(sprintf('Entity: `%s` should have its table: `%s` saved, but primary key column\'s : `%s` value is empty (null),', get_class($entity), $tableName, $pkColumnInfo->getColumnName()));
265
			}
266
267 1
			$fluent->where(
268 1
				'%n = %i',
269 1
				$pkColumnInfo->getColumnName(),
270 1
				$pkValue
271
			);
272
		}
273 1
		return $fluent;
274
	}
275
276
	/**
277
	 * @param $entityClassName
278
	 * @return array
279
	 * @throws \SpareParts\Pillar\Mapper\EntityMappingException
280
	 */
281 1
	private function getEntityProperties($entityClassName)
282
	{
283 1
		if (!isset($this->entityPropertiesCache[$entityClassName])) {
284 1
			$mapping = $this->mapper->getEntityMapping($entityClassName);
285 1
			$tables = $mapping->getTables();
286 1
			$properties = [];
287 1
			foreach ($tables as $tableInfo) {
288 1
				$columns = $mapping->getColumnsForTable($tableInfo->getIdentifier());
289 1
				foreach ($columns as $columnInfo) {
290 1
					$properties[] = $columnInfo->getPropertyName();
291
				}
292
			}
293 1
			$this->entityPropertiesCache[$entityClassName] = array_unique($properties);
294
		}
295 1
		return $this->entityPropertiesCache[$entityClassName];
296
	}
297
298
299
	/**
300
	 * @param IEntity $entity
301
	 * @param ColumnInfo[] $columnInfoList
302
	 * @return mixed[]
303
	 */
304 2
	protected function getEntityPropertyValuesMappedToColumns(IEntity $entity, array $columnInfoList)
305
	{
306 2
		$columnValues = [];
307 2
		foreach ($columnInfoList as $columnInfo) {
308 2
			if ($columnInfo->isDeprecated()) {
309
				continue;
310
			}
311 2
			$columnValues[$columnInfo->getColumnName()] = $this->getEntityPropertyValue($entity, $columnInfo);
312
		}
313 2
		return $columnValues;
314
	}
315
316
	/**
317
	 * @param IEntity $entity
318
	 * @param ColumnInfo $property
319
	 * @return mixed
320
	 */
321 2
	protected function getEntityPropertyValue(IEntity $entity, ColumnInfo $property)
322
	{
323 2
		$propName = $property->getPropertyName();
324 2
		$getterPk = \Closure::bind(function () use ($propName) {
325 2
			return $this->{$propName};
326 2
		}, $entity, get_class($entity));
327
328 2
		return $getterPk();
329
	}
330
331
	/**
332
	 * @param ColumnInfo[] $columnInfos
333
	 * @param string[] $changedProperties
334
	 * @return ColumnInfo[]
335
	 */
336 1
	private function prepareListToUpdate(array $columnInfos, array $changedProperties)
337
	{
338 1
		$columnInfoListToUpdate = array_filter(
339 1
			$columnInfos,
340 1
			function (ColumnInfo $columnInfo) use ($changedProperties) {
341
				// never update a PK
342 1
				if ($columnInfo->isPrimaryKey()) {
343 1
					return false;
344
				}
345
				// if there is any `deprecated` column, ignore its value
346 1
				if ($columnInfo->isDeprecated()) {
347
					return false;
348
				}
349
				// update changed properties
350 1
				if (in_array($columnInfo->getPropertyName(), $changedProperties)) {
351 1
					return true;
352
				}
353
354 1
				return false;
355 1
			}
356
		);
357
358 1
		return $columnInfoListToUpdate;
359
	}
360
361
	/**
362
	 * @param IEntity $entity
363
	 * @param array|null $tables
364
	 * @return TableInfo[]
365
	 * @throws EntityMappingException
366
	 */
367 1
	private function sanitizeTableInfos(IEntity $entity, array $tables = null)
368
	{
369 1
		$mapping = $this->mapper->getEntityMapping($entity);
370 1
		$tableInfos = $mapping->getTables();
371 1
		if (!is_null($tables)) {
372
			// grab TableInfo of given $tables
373 1
			$tableInfos = array_map(function ($tableName) use ($tableInfos, $entity) {
374 1
				if (!isset($tableInfos[ $tableName ])) {
375
					throw new UnableToSaveException(sprintf('Unable to save entity: `%s`, unknown table `%s`',
376
						get_class($entity), $tableName));
377
				}
378
379 1
				return $tableInfos[ $tableName ];
380 1
			}, $tables);
381
		}
382
383 1
		return $tableInfos;
384
	}
385
386
	/**
387
	 * @param IEntityMapping $mapping
388
	 * @param TableInfo $tableInfo
389
	 * @return ColumnInfo[]
390
	 */
391 2
	private function getColumnInfosForTable(IEntityMapping $mapping, TableInfo $tableInfo)
392
	{
393
		/** @var ColumnInfo[] $columnInfos */
394 2
		$columnInfos = [];
395 2
		foreach ($mapping->getColumnsForTable($tableInfo->getIdentifier()) as $columnInfo) {
396 2
			$columnInfos[ $columnInfo->getPropertyName() ] = $columnInfo;
397
		}
398
399 2
		return $columnInfos;
400
	}
401
402
	/**
403
	 * @param IEntity $entity
404
	 * @param IEntityMapping $entityMapping
405
	 * @param TableInfo $tableInfo
406
	 * @return mixed[]|null
407
	 */
408 1
	private function getValuesToInsert(
409
		IEntity $entity,
410
		IEntityMapping $entityMapping,
411
		TableInfo $tableInfo
412
	) {
413
		// we need array key of ColumnInfo[] to be the column name for easy handling
414 1
		$columnInfos = $this->getColumnInfosForTable($entityMapping, $tableInfo);
415
416
		// entity will tell us which properties did change
417 1
		$changedProperties = $entity->getChangedProperties($this->getEntityProperties($entityMapping->getEntityClassName()));
418
		// ... then we can find DB mapping for those properties
419 1
		$changedColumnInfos = array_filter(
420 1
			$columnInfos,
421 1
			function (ColumnInfo $columnInfo) use ($changedProperties) {
422 1
				return in_array($columnInfo->getPropertyName(), $changedProperties);
423 1
			}
424
		);
425
		// now we know database columns and can prepare data for inserting into DB
426 1
		$columnValuesToStore = $this->getEntityPropertyValuesMappedToColumns($entity, $changedColumnInfos);
427
428
		// nothing to store in this table?
429 1
		if (!count($columnValuesToStore)) {
430
			return null;
431
		}
432
433
		// if there are any PK set by linking from another table (possible FK), we need to respect that values
434
		// if there are any `deprecated` columns, we need to insert their values
435 1
		foreach ($columnInfos as $columnInfo) {
436 1
			if (!$columnInfo->isPrimaryKey() && !$columnInfo->isDeprecated()) {
437 1
				continue;
438
			}
439 1
			$value = $this->getEntityPropertyValue($entity, $columnInfo);
440
441
			// we are not storing NULLs to PK...
442 1
			if (is_null($value)) {
443 1
				continue;
444
			}
445
			$columnValuesToStore[$columnInfo->getColumnName()] = $value;
446
		}
447
448 1
		return $columnValuesToStore;
449
	}
450
451
	/**
452
	 * @param IEntity $entity
453
	 * @param IEntityMapping $mapping
454
	 * @param TableInfo $tableInfo
455
	 * @return mixed[]
456
	 */
457 1
	private function getValuesToUpdate(IEntity $entity, $mapping, TableInfo $tableInfo)
458
	{
459
		// we need array key of ColumnInfo[] to be the column name for easy handling
460 1
		$columnInfos = $this->getColumnInfosForTable($mapping, $tableInfo);
461 1
		$propertyList = array_keys($columnInfos);
462
463
		// properties that changed in this table
464 1
		$changedProperties = $entity->getChangedProperties($propertyList);
465
466
		// now that we have list of changed properties, we can make a list of DB columns that should be updated
467 1
		$columnInfoListToUpdate = $this->prepareListToUpdate($columnInfos, $changedProperties);
468
469
		// map property values to columns
470 1
		$columnValuesToStore = $this->getEntityPropertyValuesMappedToColumns($entity, $columnInfoListToUpdate);
471 1
		return $columnValuesToStore;
472
	}
473
474
	/**
475
	 * @param IEntity $entity
476
	 * @param IEntityMapping $mapping
477
	 * @param string $tableName
478
	 * @return TableInfo
479
	 */
480 1
	private function getTableInfoByTableName(IEntity $entity, IEntityMapping $mapping, $tableName)
481
	{
482 1
		$tableInfos = $mapping->getTables();
483 1
		if (!isset($tableInfos[$tableName])) {
484
			throw new UnableToSaveException(sprintf('Unable to save entity: `%s`, unknown table `%s`', get_class($entity), $tableName));
485
		}
486 1
		$tableInfo = $tableInfos[$tableName];
487 1
		return $tableInfo;
488
	}
489
}
490