From 6f51161c34f3c808f538b41874a461e7552e0b6d Mon Sep 17 00:00:00 2001 From: Vance Lucas Date: Mon, 31 Aug 2015 13:03:18 -0500 Subject: [PATCH 1/3] Fix #97 with support for field/column aliases --- lib/Entity/Manager.php | 12 ++++++++-- lib/Query.php | 8 +++++++ lib/Query/Resolver.php | 16 +++++++------ tests/Entity/Legacy.php | 50 +++++++++++++++++++++++++++++++++++++++++ tests/FieldAlias.php | 48 +++++++++++++++++++++++++++++++++++++++ 5 files changed, 125 insertions(+), 9 deletions(-) create mode 100644 tests/Entity/Legacy.php create mode 100644 tests/FieldAlias.php diff --git a/lib/Entity/Manager.php b/lib/Entity/Manager.php index 336a105..0a96dfb 100644 --- a/lib/Entity/Manager.php +++ b/lib/Entity/Manager.php @@ -102,7 +102,7 @@ public function fields() // Table Options $entityTableOptions = $entityName::tableOptions(); - $this->tableOptions = (array)$entityTableOptions; + $this->tableOptions = (array) $entityTableOptions; // Custom Mapper $this->mapper = $entityName::mapper(); @@ -113,6 +113,7 @@ public function fields() 'default' => null, 'value' => null, 'length' => null, + 'column' => null, 'required' => false, 'notnull' => false, 'unsigned' => false, @@ -164,6 +165,11 @@ public function fields() $fieldOpts['notnull'] = true; } + // Set column name to field name/key as default + if (null === $fieldOpts['column']) { + $fieldOpts['column'] = $fieldName; + } + // Old Spot used 'serial' field to describe auto-increment // fields, so accomodate that here if (isset($fieldOpts['serial']) && $fieldOpts['serial'] === true) { @@ -214,7 +220,9 @@ public function fieldKeys() 'index' => [] ]; $usedKeyNames = []; - foreach ($formattedFields as $fieldName => $fieldInfo) { + foreach ($formattedFields as $fieldInfo) { + $fieldName = $fieldInfo['column']; + // Determine key field name (can't use same key name twice, so we have to append a number) $fieldKeyName = $table . '_' . $fieldName; while (in_array($fieldKeyName, $usedKeyNames)) { diff --git a/lib/Query.php b/lib/Query.php index f576ec0..84f989e 100644 --- a/lib/Query.php +++ b/lib/Query.php @@ -317,6 +317,7 @@ private function parseWhereToSQLFragments(array $where, $useAlias = true) $sqlFragments = []; foreach ($where as $column => $value) { + $whereClause = ""; // Column name with comparison operator $colData = explode(' ', $column); @@ -717,6 +718,13 @@ public function escapeIdentifier($identifier) */ public function fieldWithAlias($field, $escaped = true) { + $fieldInfo = $this->_mapper->entityManager()->fields(); + + // Determine real field name (column alias support) + if (isset($fieldInfo[$field])) { + $field = $fieldInfo[$field]['column']; + } + $field = $this->_tableName . '.' . $field; return $escaped ? $this->escapeIdentifier($field) : $field; diff --git a/lib/Query/Resolver.php b/lib/Query/Resolver.php index 07f6aef..f653d30 100644 --- a/lib/Query/Resolver.php +++ b/lib/Query/Resolver.php @@ -101,23 +101,25 @@ public function migrateCreateSchema() $schema = new \Doctrine\DBAL\Schema\Schema(); $table = $schema->createTable($this->escapeIdentifier($table)); - foreach ($fields as $field => $fieldInfo) { - $fieldType = $fieldInfo['type']; - unset($fieldInfo['type']); - $table->addColumn($field, $fieldType, $fieldInfo); + foreach ($fields as $field) { + $fieldType = $field['type']; + unset($field['type']); + $table->addColumn($this->escapeIdentifier($field['column']), $fieldType, $field); } // PRIMARY if ($fieldIndexes['primary']) { - $table->setPrimaryKey($fieldIndexes['primary']); + $resolver = $this; + $primaryKeys = array_map(function($value) use($resolver) { return $resolver->escapeIdentifier($value); }, $fieldIndexes['primary']); + $table->setPrimaryKey($primaryKeys); } // UNIQUE foreach ($fieldIndexes['unique'] as $keyName => $keyFields) { - $table->addUniqueIndex($keyFields, $keyName); + $table->addUniqueIndex($keyFields, $this->escapeIdentifier($keyName)); } // INDEX foreach ($fieldIndexes['index'] as $keyName => $keyFields) { - $table->addIndex($keyFields, $keyName); + $table->addIndex($keyFields, $this->escapeIdentifier($keyName)); } return $schema; diff --git a/tests/Entity/Legacy.php b/tests/Entity/Legacy.php new file mode 100644 index 0000000..4eea3de --- /dev/null +++ b/tests/Entity/Legacy.php @@ -0,0 +1,50 @@ + ['type' => 'integer', 'autoincrement' => true, 'primary' => true, 'column' => self::getIdFieldColumnName()], + 'name' => ['type' => 'string', 'required' => true, 'column' => self::getNameFieldColumnName()], + 'number' => ['type' => 'integer', 'required' => true, 'column' => self::getNumberFieldColumnName()], + 'date_created' => ['type' => 'datetime', 'value' => new \DateTime(), 'column' => self::getDateCreatedColumnName()], + ]; + } + + /** + * Helpers for field/column names - methods with public access so we can avoid duplication in tests + */ + public static function getIdFieldColumnName() + { + return 'obnoxiouslyObtuse_IdentityColumn'; + } + + public static function getNameFieldColumnName() + { + return 'string_54_LegacyDB_x8'; + } + + public static function getNumberFieldColumnName() + { + return 'xbf86_haikusInTheDark'; + } + + public static function getDateCreatedColumnName() + { + return 'dtCreatedAt'; + } +} diff --git a/tests/FieldAlias.php b/tests/FieldAlias.php new file mode 100644 index 0000000..09ebd8e --- /dev/null +++ b/tests/FieldAlias.php @@ -0,0 +1,48 @@ +migrate(); + } + } + + public static function tearDownAfterClass() + { + foreach (['Legacy'] as $entity) { + test_spot_mapper('\SpotTest\Entity\\' . $entity)->dropTable(); + } + } + + public function testLegacySelectFieldsAreAliases() + { + $mapper = test_spot_mapper('SpotTest\Entity\Legacy'); + $query = $mapper->select()->noQuote()->where(['number' => 2, 'name' => 'legacy_crud']); + $this->assertEquals("SELECT * FROM test_legacy test_legacy WHERE test_legacy." . self::$legacyTable->getNumberFieldColumnName() ." = ? AND test_legacy." . self::$legacyTable->getNameFieldColumnName() . " = ?", $query->toSql()); + } + + // Ordering + public function testLegacyOrderBy() + { + $mapper = test_spot_mapper('SpotTest\Entity\Legacy'); + $query = $mapper->select()->noQuote()->where(['number' => 2])->order(['date_created' => 'ASC']); + $this->assertContains("ORDER BY test_legacy." . self::$legacyTable->getDateCreatedColumnName() . " ASC", $query->toSql()); + } + + // Grouping + public function testLegacyGroupBy() + { + $mapper = test_spot_mapper('SpotTest\Entity\Legacy'); + $query = $mapper->select()->noQuote()->where(['name' => 'test_group'])->group(['id']); + $this->assertEquals("SELECT * FROM test_legacy test_legacy WHERE test_legacy." . self::$legacyTable->getNameFieldColumnName() . " = ? GROUP BY test_legacy." . self::$legacyTable->getIdFieldColumnName(), $query->toSql()); + } +} From 23f6c19144db09a6ad0612ccc635bc07c515d3a2 Mon Sep 17 00:00:00 2001 From: Vance Lucas Date: Wed, 2 Sep 2015 11:47:37 -0500 Subject: [PATCH 2/3] Add alias mapping support for bare entity objects --- lib/Entity/Manager.php | 18 ++++++++++++++++++ lib/Mapper.php | 21 ++++++++++++++++++--- lib/Query/Resolver.php | 19 +++++++++++++++---- tests/FieldAlias.php | 37 +++++++++++++++++++++++++++++++++++++ 4 files changed, 88 insertions(+), 7 deletions(-) diff --git a/lib/Entity/Manager.php b/lib/Entity/Manager.php index 0a96dfb..e26d9c7 100644 --- a/lib/Entity/Manager.php +++ b/lib/Entity/Manager.php @@ -21,6 +21,11 @@ class Manager */ protected $fields = []; + /** + * @var array + */ + protected $fieldAliasMappings = []; + /** * @var array */ @@ -168,6 +173,9 @@ public function fields() // Set column name to field name/key as default if (null === $fieldOpts['column']) { $fieldOpts['column'] = $fieldName; + } else { + // Store user-specified field alias mapping + $this->fieldAliasMappings[$fieldName] = $fieldOpts['column']; } // Old Spot used 'serial' field to describe auto-increment @@ -201,6 +209,16 @@ public function fields() return $returnFields; } + /** + * Field alias mappings (used for lookup) + * + * @return array Field alias => actual column name + */ + public function fieldAliasMappings() + { + return $this->fieldAliasMappings; + } + /** * Groups field keys into names arrays of fields with key name as index * diff --git a/lib/Mapper.php b/lib/Mapper.php index d320632..8cbba7e 100644 --- a/lib/Mapper.php +++ b/lib/Mapper.php @@ -740,7 +740,9 @@ public function insert($entity, array $options = []) if ($pkFieldInfo && $pkFieldInfo['autoincrement'] === true) { if ($this->connectionIs('pgsql')) { // Allow custom sequence name - $sequenceName = $table . '_' . $pkField . '_seq'; + $fieldAliasMappings = $this->entityManager()->fieldAliasMappings(); + $sequenceField = isset($fieldAliasMappings[$pkField]) ? $fieldAliasMappings[$pkField] : $pkField; + $sequenceName = $this->resolver()->escapeIdentifier($table . '_' . $sequenceField . '_seq'); if (isset($pkFieldInfo['sequence_name'])) { $sequenceName = $pkFieldInfo['sequence_name']; } @@ -816,7 +818,7 @@ public function update(EntityInterface $entity, array $options = []) $data = $this->convertToDatabaseValues($entityName, $data); if (count($data) > 0) { - $result = $this->connection()->update($this->table(), $data, [$this->primaryKeyField() => $this->primaryKey($entity)]); + $result = $this->resolver()->update($this->table(), $data, [$this->primaryKeyField() => $this->primaryKey($entity)]); // Run afterSave and afterUpdate if ( @@ -894,6 +896,10 @@ public function delete($conditions = []) /** * Prepare data to be dumped to the data store + * + * @param string $entityName + * @param array $data Key/value pairs of data to store + * @return array */ protected function convertToDatabaseValues($entityName, array $data) { @@ -909,15 +915,24 @@ protected function convertToDatabaseValues($entityName, array $data) } /** - * Retrieve data from the data store + * Retrieve data from the data store and map it to PHP values + * + * @param string $entityName + * @param array $data Key/value pairs of data to store + * @return array */ protected function convertToPHPValues($entityName, array $data) { $phpData = []; $fields = $entityName::fields(); + $fieldAliasMappings = $this->entityManager()->fieldAliasMappings(); $platform = $this->connection()->getDatabasePlatform(); $entityData = array_intersect_key($data, $fields); foreach ($data as $field => $value) { + if ($fieldAlias = array_search($field, $fieldAliasMappings)) { + $field = $fieldAlias; + } + // Field is in the Entity definitions if (isset($entityData[$field])) { $typeHandler = Type::getType($fields[$field]['type']); diff --git a/lib/Query/Resolver.php b/lib/Query/Resolver.php index f653d30..6ff0a3f 100644 --- a/lib/Query/Resolver.php +++ b/lib/Query/Resolver.php @@ -156,9 +156,7 @@ public function read(Query $query) public function create($table, array $data) { $connection = $this->mapper->connection(); - $result = $connection->insert($this->escapeIdentifier($table), $data); - - return $result; + return $connection->insert($this->escapeIdentifier($table), $this->dataWithFieldAliasMappings($data)); } /** @@ -173,8 +171,21 @@ public function create($table, array $data) public function update($table, array $data, array $where) { $connection = $this->mapper->connection(); + return $connection->update($this->escapeIdentifier($table), $this->dataWithFieldAliasMappings($data), $this->dataWithFieldAliasMappings($where)); + } - return $connection->update($this->escapeIdentifier($table), $data, $where); + /** + * Taken given field name/value inputs and map them to their aliased names + */ + public function dataWithFieldAliasMappings(array $data) + { + $fields = $this->mapper->entityManager()->fields(); + $fieldMappings = []; + foreach($data as $field => $value) { + $fieldAlias = $this->escapeIdentifier(isset($fields[$field]) ? $fields[$field]['column'] : $field); + $fieldMappings[$fieldAlias] = $value; + } + return $fieldMappings; } /** diff --git a/tests/FieldAlias.php b/tests/FieldAlias.php index 09ebd8e..91b2e9d 100644 --- a/tests/FieldAlias.php +++ b/tests/FieldAlias.php @@ -1,5 +1,6 @@ select()->noQuote()->where(['name' => 'test_group'])->group(['id']); $this->assertEquals("SELECT * FROM test_legacy test_legacy WHERE test_legacy." . self::$legacyTable->getNameFieldColumnName() . " = ? GROUP BY test_legacy." . self::$legacyTable->getIdFieldColumnName(), $query->toSql()); } + + // Insert + public function testLegacyInsert() + { + $legacy = new Legacy(); + $legacy->name = 'Something Here'; + $legacy->number = 5; + + $mapper = test_spot_mapper('SpotTest\Entity\Legacy'); + $mapper->save($legacy); + return $legacy; + } + + /** + * @depends testLegacyInsert + */ + public function testLegacyUpdate(Legacy $legacy) + { + $legacy->name = 'Something ELSE Here'; + $legacy->number = 6; + + $mapper = test_spot_mapper('SpotTest\Entity\Legacy'); + $mapper->save($legacy); + } + + /** + * @depends testLegacyInsert + */ + public function testLegacyEntityFieldMapping(Legacy $legacy) + { + $mapper = test_spot_mapper('SpotTest\Entity\Legacy'); + $savedLegacyItem = $mapper->first(); + + $this->assertEquals($legacy->name, $savedLegacyItem->name); + $this->assertEquals($legacy->number, $savedLegacyItem->number); + } } From fb2c97bd79460ad2d239e07f3e7e884adce900c9 Mon Sep 17 00:00:00 2001 From: Vance Lucas Date: Wed, 9 Sep 2015 16:02:16 -0500 Subject: [PATCH 3/3] Add relation test for column alias mapping --- tests/Entity/Legacy.php | 7 +++++++ tests/FieldAlias.php | 24 ++++++++++++++++++++++-- 2 files changed, 29 insertions(+), 2 deletions(-) diff --git a/tests/Entity/Legacy.php b/tests/Entity/Legacy.php index 4eea3de..e0c43e9 100644 --- a/tests/Entity/Legacy.php +++ b/tests/Entity/Legacy.php @@ -25,6 +25,13 @@ public static function fields() ]; } + public static function relations(MapperInterface $mapper, EntityInterface $entity) + { + return [ + 'polymorphic_comments' => $mapper->hasMany($entity, 'SpotTest\Entity\PolymorphicComment', 'item_id')->where(['item_type' => 'legacy']) + ]; + } + /** * Helpers for field/column names - methods with public access so we can avoid duplication in tests */ diff --git a/tests/FieldAlias.php b/tests/FieldAlias.php index 91b2e9d..ecf593a 100644 --- a/tests/FieldAlias.php +++ b/tests/FieldAlias.php @@ -12,14 +12,14 @@ class FieldAlias extends \PHPUnit_Framework_TestCase public static function setupBeforeClass() { self::$legacyTable = new \SpotTest\Entity\Legacy(); - foreach (['Legacy'] as $entity) { + foreach (['Legacy', 'PolymorphicComment'] as $entity) { test_spot_mapper('SpotTest\Entity\\' . $entity)->migrate(); } } public static function tearDownAfterClass() { - foreach (['Legacy'] as $entity) { + foreach (['Legacy', 'PolymorphicComment'] as $entity) { test_spot_mapper('\SpotTest\Entity\\' . $entity)->dropTable(); } } @@ -82,4 +82,24 @@ public function testLegacyEntityFieldMapping(Legacy $legacy) $this->assertEquals($legacy->name, $savedLegacyItem->name); $this->assertEquals($legacy->number, $savedLegacyItem->number); } + + /** + * @depends testLegacyInsert + */ + public function testLegacyRelations(Legacy $legacy) + { + // New Comment + $commentMapper = test_spot_mapper('SpotTest\Entity\PolymorphicComment'); + $comment = new \SpotTest\Entity\PolymorphicComment([ + 'item_id' => $legacy->id, + 'item_type' => 'legacy', + 'name' => 'Testy McTesterpants', + 'email' => 'tester@chester.com', + 'body' => '

Comment Text

' + ]); + $commentMapper->save($comment); + + $this->assertInstanceOf('Spot\Relation\HasMany', $legacy->polymorphic_comments); + $this->assertEquals(count($legacy->polymorphic_comments), 1); + } }