Skip to content

Commit ce9e7fb

Browse files
noescomnoescom
authored andcommitted
Extracted version info piggyback into its own class so it can be shared between insert and update
1 parent 03777e4 commit ce9e7fb

File tree

3 files changed

+203
-103
lines changed

3 files changed

+203
-103
lines changed

src/Persistence/InsertPersister.php

Lines changed: 66 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -58,6 +58,12 @@ class InsertPersister {
5858
*/
5959
private array $strategy_column_cache;
6060

61+
/**
62+
* Handles values with @Orm\Version annotations
63+
* @var VersionValueHandler
64+
*/
65+
private VersionValueHandler $valueHandler;
66+
6167
/**
6268
* InsertPersister constructor
6369
* @param UnitOfWork $unitOfWork The UnitOfWork that will coordinate insertion operations
@@ -69,6 +75,7 @@ public function __construct(UnitOfWork $unitOfWork, ?PrimaryKeyFactory $factory
6975
$this->entity_store = $unitOfWork->getEntityStore();
7076
$this->property_handler = $unitOfWork->getPropertyHandler();
7177
$this->connection = $unitOfWork->getConnection();
78+
$this->valueHandler = new VersionValueHandler($unitOfWork->getConnection(), $unitOfWork->getEntityStore(), $unitOfWork, $unitOfWork->getPropertyHandler());
7279
$this->primary_key_factory = $factory ?? new PrimaryKeyFactory();
7380
$this->strategy_column_cache = [];
7481
}
@@ -84,9 +91,15 @@ public function persist(object $entity): void {
8491
$tableName = $this->entity_store->getOwningTable($entity);
8592
$tableNameEscaped = str_replace('`', '``', $tableName);
8693

94+
// Fetch the column map
95+
$columnMap = array_flip($this->entity_store->getColumnMap($entity));
96+
8797
// Get the primary key property names and their corresponding column names
8898
$primaryKeys = $this->entity_store->getIdentifierKeys($entity);
8999

100+
// Get the column names that make up the primary key
101+
$primaryKeyColumnNames = $this->entity_store->getIdentifierColumnNames($entity);
102+
90103
// Iterate through each identified primary key for the entity
91104
foreach ($primaryKeys as $primaryKey) {
92105
// First check if the primary key already has a value
@@ -121,22 +134,40 @@ public function persist(object $entity): void {
121134
}
122135

123136
// Get the primary key property names and their corresponding column names
124-
$versionColumnNames = $this->entity_store->getVersionColumnNames($entity);
125-
126-
// Iterate through each identified primary key for the entity
127-
foreach ($versionColumnNames as $property => $versionColumn) {
128-
$this->property_handler->set($entity, $property, $this->getInitialVersionValue($versionColumn["column"]->getType()));
129-
}
137+
$versionColumns = $this->entity_store->getVersionColumnNames($entity);
138+
$versionColumnNames = array_flip(array_column($versionColumns, 'name'));
130139

131140
// Serialize the entity into an array of column name => value pairs
132141
$serializedEntity = $this->unit_of_work->getSerializer()->serialize($entity);
133-
142+
134143
// Create the SQL query for insertion
135-
// Generates a comma-separated list of "column=:value" pairs for the SET clause
136-
$sql = implode(",", array_map(
137-
fn($key) => "`" . str_replace('`', '``', $key) . "`=:{$key}",
138-
array_keys($serializedEntity)
139-
));
144+
$sqlParts = [];
145+
146+
foreach ($serializedEntity as $key => $value) {
147+
// Escape the identifier (add backticks)
148+
$escapedKey = $this->valueHandler->escapeIdentifier($key);
149+
150+
// Check if the column name exists
151+
if (isset($versionColumnNames[$key])) {
152+
// Fetch the column name from the map
153+
$columnName = $columnMap[$key];
154+
155+
// Fetch the value
156+
$initialVersion = $this->getInitialVersionValue($versionColumns[$columnName]["column"]->getType());
157+
158+
// Remove version property from bound parameters list
159+
unset($serializedEntity[$key]);
160+
161+
// Add initial value to SQL
162+
$sqlParts[] = $escapedKey . "=" . $initialVersion;
163+
} else {
164+
// normal bound parameter
165+
$sqlParts[] = $escapedKey . "=:" . $key;
166+
}
167+
}
168+
169+
// Implode the parts
170+
$sql = implode(",", $sqlParts);
140171

141172
// Execute the insert query with the serialized entity data as parameters
142173
$rs = $this->connection->Execute("INSERT INTO `{$tableNameEscaped}` SET {$sql}", $serializedEntity);
@@ -161,11 +192,30 @@ public function persist(object $entity): void {
161192
// Update the entity's property with the database-generated ID
162193
// This ensures the entity's state is synchronized with its database representation
163194
$this->property_handler->set($entity, $autoincrementColumn, (int)$autoIncrementId);
195+
196+
// Also set it in $serializedEntity
197+
$serializedEntity[$autoincrementColumn] = (int)$autoIncrementId;
164198
}
165199

166200
// If the auto-increment ID is 0, it may indicate no new ID was generated
167201
// (possibly due to a transaction rollback or other database condition)
168202
}
203+
204+
// Extract the primary key values from the original data
205+
// These will be used in the WHERE clause to identify the record to update
206+
$primaryKeyValues = array_intersect_key($serializedEntity, array_flip($primaryKeyColumnNames));
207+
208+
// Fetch version values from the database (if any)
209+
$fetchedDatetimeValues = $this->valueHandler->fetchUpdatedVersionValues(
210+
$tableName,
211+
$versionColumns,
212+
$primaryKeyColumnNames,
213+
$primaryKeyValues,
214+
);
215+
216+
// Update the entity with the new version values so the in-memory object
217+
// matches the database state and can be used for subsequent operations
218+
$this->valueHandler->updateEntityVersionValues($entity, $fetchedDatetimeValues);
169219
}
170220

171221
/**
@@ -175,6 +225,7 @@ public function persist(object $entity): void {
175225
* @return string The primary key strategy value
176226
*/
177227
protected function getPrimaryKeyStrategy(object $entity, string $primaryKey): string {
228+
// Fetch owning table
178229
$table = $this->entity_store->getOwningTable($entity);
179230

180231
// Fetch key from cache if present
@@ -203,7 +254,7 @@ protected function getPrimaryKeyStrategy(object $entity, string $primaryKey): st
203254
return $this->strategy_column_cache[$table][$primaryKey] = "identity";
204255
}
205256

206-
protected function getInitialVersionValue(string $columnType): \DateTimeImmutable|int|string {
257+
protected function getInitialVersionValue(string $columnType): \DateTime|int|string {
207258
switch ($columnType) {
208259
case 'int':
209260
case 'integer':
@@ -212,11 +263,11 @@ protected function getInitialVersionValue(string $columnType): \DateTimeImmutabl
212263

213264
case 'datetime':
214265
case 'timestamp':
215-
return new \DateTimeImmutable('now');
266+
return "NOW()";
216267

217268
case 'uuid':
218269
case 'guid':
219-
return Tools::createGUID();
270+
return "'" . Tools::createGUID() . "'";
220271

221272
default:
222273
throw new \RuntimeException("Invalid column type {$columnType} for Version annotation");

src/Persistence/UpdatePersister.php

Lines changed: 16 additions & 88 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,6 @@
22

33
namespace Quellabs\ObjectQuel\Persistence;
44

5-
use Quellabs\ObjectQuel\Annotations\Orm\Column;
65
use Quellabs\ObjectQuel\DatabaseAdapter\DatabaseAdapter;
76
use Quellabs\ObjectQuel\EntityStore;
87
use Quellabs\ObjectQuel\OrmException;
@@ -20,25 +19,25 @@ class UpdatePersister {
2019
* Reference to the UnitOfWork that manages persistence operations
2120
* This is a duplicate of the parent's unitOfWork property with a different naming convention
2221
*/
23-
protected UnitOfWork $unitOfWork;
22+
private UnitOfWork $unitOfWork;
2423

2524
/**
2625
* The EntityStore that maintains metadata about entities and their mappings
2726
* Used to retrieve information about entity tables, columns and identifiers
2827
*/
29-
protected EntityStore $entityStore;
28+
private EntityStore $entityStore;
3029

3130
/**
32-
* Utility for handling entity property access and manipulation
33-
* Provides methods to get and set entity properties regardless of their visibility
31+
* Database connection adapter used for executing SQL queries
32+
* Abstracts the underlying database system and provides a unified interface
3433
*/
35-
protected PropertyHandler $propertyHandler;
34+
private DatabaseAdapter $connection;
3635

3736
/**
38-
* Database connection adapter used for executing SQL queries
39-
* Abstracts the underlying database system and provides a unified interface
37+
* Handles values with @Orm\Version annotations
38+
* @var VersionValueHandler
4039
*/
41-
protected DatabaseAdapter $connection;
40+
private VersionValueHandler $valueHandler;
4241

4342
/**
4443
* UpdatePersister constructor
@@ -47,8 +46,8 @@ class UpdatePersister {
4746
public function __construct(UnitOfWork $unitOfWork) {
4847
$this->unitOfWork = $unitOfWork;
4948
$this->entityStore = $unitOfWork->getEntityStore();
50-
$this->propertyHandler = $unitOfWork->getPropertyHandler();
5149
$this->connection = $unitOfWork->getConnection();
50+
$this->valueHandler = new VersionValueHandler($unitOfWork->getConnection(), $unitOfWork->getEntityStore(), $unitOfWork, $unitOfWork->getPropertyHandler());
5251
}
5352

5453
/**
@@ -60,7 +59,7 @@ public function __construct(UnitOfWork $unitOfWork) {
6059
public function persist(object $entity): void {
6160
// Retrieve basic information needed for the update
6261
// Get the table name where the entity is stored
63-
$tableName = $this->escapeIdentifier($this->entityStore->getOwningTable($entity));
62+
$tableName = $this->valueHandler->escapeIdentifier($this->entityStore->getOwningTable($entity));
6463

6564
// Serialize the entity's current state into an array of column name => value pairs
6665
$serializedEntity = $this->unitOfWork->getSerializer()->serialize($entity);
@@ -117,7 +116,7 @@ public function persist(object $entity): void {
117116
}
118117

119118
// Fetch version values from the database (if any)
120-
$fetchedDatetimeValues = $this->fetchUpdatedVersionValues(
119+
$fetchedDatetimeValues = $this->valueHandler->fetchUpdatedVersionValues(
121120
$tableName,
122121
$versionColumns,
123122
$primaryKeyColumnNames,
@@ -126,16 +125,7 @@ public function persist(object $entity): void {
126125

127126
// Update the entity with the new version values so the in-memory object
128127
// matches the database state and can be used for subsequent operations
129-
$this->updateEntityVersionValues($entity, $fetchedDatetimeValues);
130-
}
131-
132-
/**
133-
* Escapes a database identifier (table or column name)
134-
* @param string $identifier The identifier to escape
135-
* @return string The escaped identifier wrapped in backticks
136-
*/
137-
protected function escapeIdentifier(string $identifier): string {
138-
return '`' . str_replace('`', '``', $identifier) . '`';
128+
$this->valueHandler->updateEntityVersionValues($entity, $fetchedDatetimeValues);
139129
}
140130

141131
/**
@@ -174,7 +164,7 @@ protected function buildVersionSetClause(array $versionColumns, array &$params):
174164

175165
// Process each version column according to its type
176166
foreach ($versionColumns as $property => $versionColumn) {
177-
$columnName = $this->escapeIdentifier($versionColumn['name']);
167+
$columnName = $this->valueHandler->escapeIdentifier($versionColumn['name']);
178168

179169
switch($versionColumn['column']->getType()) {
180170
case 'integer':
@@ -215,7 +205,7 @@ protected function buildFieldsSetClause(array $changedFields, array &$params): a
215205
$setClauseParts = [];
216206
foreach ($changedFields as $columnName => $value) {
217207
$paramName = "field_{$columnName}";
218-
$setClauseParts[] = $this->escapeIdentifier($columnName) . "=:{$paramName}";
208+
$setClauseParts[] = $this->valueHandler->escapeIdentifier($columnName) . "=:{$paramName}";
219209
$params[$paramName] = $value;
220210
}
221211

@@ -239,7 +229,7 @@ protected function buildWhereClause(array $primaryKeyColumnNames, array $primary
239229
// This includes primary key columns to identify the record
240230
foreach ($primaryKeyColumnNames as $columnName) {
241231
$paramName = "pk_{$columnName}";
242-
$whereClauseParts[] = $this->escapeIdentifier($columnName) . "=:{$paramName}";
232+
$whereClauseParts[] = $this->valueHandler->escapeIdentifier($columnName) . "=:{$paramName}";
243233
$params[$paramName] = $primaryKeyValues[$columnName];
244234
}
245235

@@ -249,7 +239,7 @@ protected function buildWhereClause(array $primaryKeyColumnNames, array $primary
249239
foreach ($versionColumns as $property => $versionColumn) {
250240
$columnName = $versionColumn['name'];
251241
$paramName = "where_version_{$columnName}";
252-
$whereClauseParts[] = $this->escapeIdentifier($columnName) . "=:{$paramName}";
242+
$whereClauseParts[] = $this->valueHandler->escapeIdentifier($columnName) . "=:{$paramName}";
253243

254244
// Use the original version value from our snapshot
255245
$params[$paramName] = $originalData[$columnName];
@@ -258,66 +248,4 @@ protected function buildWhereClause(array $primaryKeyColumnNames, array $primary
258248
// Combine all WHERE clause parts
259249
return implode(" AND ", $whereClauseParts);
260250
}
261-
262-
/**
263-
* Fetches version values back from the database after update
264-
* Required to ensure in-memory entity matches database state exactly
265-
* @param string $tableName Escaped table name
266-
* @param array $versionColumns All version column metadata
267-
* @param array $primaryKeyColumnNames Primary key column names
268-
* @param array $primaryKeyValues Primary key values
269-
* @return array Fetched version values as property_name => value pairs
270-
*/
271-
protected function fetchUpdatedVersionValues(string $tableName, array $versionColumns, array $primaryKeyColumnNames, array $primaryKeyValues): array {
272-
// Do nothing when no version columns exist
273-
if (empty($versionColumns)) {
274-
return [];
275-
}
276-
277-
// Build a SELECT query to retrieve all version columns
278-
$selectColumns = array_map(fn($vc) => $this->escapeIdentifier($vc['name']), $versionColumns);
279-
280-
// Build WHERE clause using only primary keys to identify the row we just updated
281-
$whereClauseParts = [];
282-
$selectParams = [];
283-
284-
foreach ($primaryKeyColumnNames as $columnName) {
285-
$paramName = "pk_{$columnName}";
286-
$whereClauseParts[] = $this->escapeIdentifier($columnName) . "=:{$paramName}";
287-
$selectParams[$paramName] = $primaryKeyValues[$columnName];
288-
}
289-
290-
// Build select query
291-
$selectSql = "SELECT " . implode(", ", $selectColumns) . " FROM {$tableName} WHERE " . implode(" AND ", $whereClauseParts);
292-
293-
// Execute select query
294-
$result = $this->connection->Execute($selectSql, $selectParams);
295-
296-
// Collect fetched datetime values
297-
if (!$result || !($row = $result->fetchAssoc())) {
298-
return [];
299-
}
300-
301-
return array_map(function ($vc) use ($row) {
302-
return $row[$vc['name']];
303-
}, $versionColumns);
304-
}
305-
306-
/**
307-
* Updates the entity with new version values from the database
308-
* @param object $entity The entity to update
309-
* @param array $fetchedValues Fetched version values as property_name => value pairs
310-
* @return void
311-
*/
312-
protected function updateEntityVersionValues(object $entity, array $fetchedValues): void {
313-
if (empty($fetchedValues)) {
314-
return;
315-
}
316-
317-
$annotations = $this->entityStore->getAnnotations($entity, Column::class);
318-
foreach ($fetchedValues as $property => $newValue) {
319-
$normalizedValue = $this->unitOfWork->getSerializer()->normalizeValue($annotations[$property], $newValue);
320-
$this->propertyHandler->set($entity, $property, $normalizedValue);
321-
}
322-
}
323251
}

0 commit comments

Comments
 (0)