From c64188dbc78af61d9e5ad8b1dfaf919947bdc7ff Mon Sep 17 00:00:00 2001 From: Sorin Valer Stanila Date: Thu, 9 Jun 2016 18:10:37 +0200 Subject: [PATCH 1/6] support for joins --- lib/Query.php | 48 ++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 48 insertions(+) diff --git a/lib/Query.php b/lib/Query.php index 7eabe78..05c450a 100644 --- a/lib/Query.php +++ b/lib/Query.php @@ -223,6 +223,54 @@ public function select() return $this; } + /** + * Join (passthrough to DBAL QueryBuilder) + * + * @return $this + */ + public function join() + { + call_user_func_array([$this->builder(), __FUNCTION__], $this->escapeIdentifier(func_get_args())); + + return $this; + } + + /** + * Inner Join (passthrough to DBAL QueryBuilder) + * + * @return $this + */ + public function innerJoin() + { + call_user_func_array([$this->builder(), __FUNCTION__], $this->escapeIdentifier(func_get_args())); + + return $this; + } + + /** + * Left Join (passthrough to DBAL QueryBuilder) + * + * @return $this + */ + public function leftJoin() + { + call_user_func_array([$this->builder(), __FUNCTION__], $this->escapeIdentifier(func_get_args())); + + return $this; + } + + /** + * Right Join (passthrough to DBAL QueryBuilder) + * + * @return $this + */ + public function rightJoin() + { + call_user_func_array([$this->builder(), __FUNCTION__], $this->escapeIdentifier(func_get_args())); + + return $this; + } + /** * Delete (passthrough to DBAL QueryBuilder) * From 5f4d636f064fc2545c1c9de0048ad9adc5bd3864 Mon Sep 17 00:00:00 2001 From: Sorin Valer Stanila Date: Sun, 12 Jun 2016 17:40:24 +0200 Subject: [PATCH 2/6] using entity name instead of table name for joins this is reducing the complexity of the implementation and we can use the data already stored in Locator --- lib/Query.php | 98 +++++++++++++++++++++++++++++++++++++++++---------- 1 file changed, 79 insertions(+), 19 deletions(-) diff --git a/lib/Query.php b/lib/Query.php index 05c450a..01219df 100644 --- a/lib/Query.php +++ b/lib/Query.php @@ -95,6 +95,13 @@ class Query implements \Countable, \IteratorAggregate, \ArrayAccess, \JsonSerial */ protected static $_whereOperatorObjects = []; + /** + * For future use + * Store the type of relation with the selected mapper + * @var + */ + protected $mapping; + /** * Constructor Method * @@ -225,50 +232,55 @@ public function select() /** * Join (passthrough to DBAL QueryBuilder) - * + * @param string $fromAlias + * @param string $entityName + * @param string $alias + * @param null $condition * @return $this */ - public function join() + public function join($fromAlias, $entityName, $alias, $condition = null) { - call_user_func_array([$this->builder(), __FUNCTION__], $this->escapeIdentifier(func_get_args())); + return $this->makeJoin(__FUNCTION__, $fromAlias, $entityName, $alias, $condition); - return $this; } - /** * Inner Join (passthrough to DBAL QueryBuilder) - * + * @param string $fromAlias + * @param string $entityName + * @param string $alias + * @param null $condition * @return $this */ - public function innerJoin() + public function innerJoin($fromAlias, $entityName, $alias, $condition = null) { - call_user_func_array([$this->builder(), __FUNCTION__], $this->escapeIdentifier(func_get_args())); + return $this->makeJoin(__FUNCTION__, $fromAlias, $entityName, $alias, $condition); - return $this; } - /** * Left Join (passthrough to DBAL QueryBuilder) - * + * @param string $fromAlias + * @param string $entityName + * @param string $alias + * @param null $condition * @return $this */ - public function leftJoin() + public function leftJoin($fromAlias, $entityName, $alias, $condition = null) { - call_user_func_array([$this->builder(), __FUNCTION__], $this->escapeIdentifier(func_get_args())); + return $this->makeJoin(__FUNCTION__, $fromAlias, $entityName, $alias, $condition); - return $this; } - /** * Right Join (passthrough to DBAL QueryBuilder) - * + * @param string $fromAlias + * @param string $entityName + * @param string $alias + * @param null $condition * @return $this */ - public function rightJoin() + public function rightJoin($fromAlias, $entityName, $alias, $condition = null) { - call_user_func_array([$this->builder(), __FUNCTION__], $this->escapeIdentifier(func_get_args())); + return $this->makeJoin(__FUNCTION__, $fromAlias, $entityName, $alias, $condition); - return $this; } /** @@ -867,4 +879,52 @@ public function __call($method, $args) throw new \BadMethodCallException("Method '" . __CLASS__ . "::" . $method . "' not found"); } } + + /** + * Store the mapping of tables and mapper + * @param string $type + * @param array $data + */ + private function addMapping($type, array $data) + { + $type = (string)$type; + $this->mapping[$type] = $data; + } + + /** + * Add a join of type $type + * @param string $type + * @param string $fromAlias + * @param string $entityName + * @param string $alias + * @param string $condition + * @return $this + */ + public function makeJoin($type, $fromAlias, $entityName, $alias, $condition) + { + $joinTable = $this->mapper()->getMapper($entityName)->table(); + $conditionString = (string)$condition; + + $this->addMapping( + 'join', + array( + $fromAlias => array( + 'joinTable' => $joinTable, + 'joinAlias' => $alias, + 'joinEntity' => $entityName, + + ), + ) + ); +// $conditionString = implode(' =', $condition); + //@FIXME: now parameters are double escaped, because the initial are double escaped also :( + $this->builder()->$type( + $this->escapeIdentifier($fromAlias), + $this->escapeIdentifier($joinTable), + $this->escapeIdentifier($alias), + $conditionString + ); + + return $this; + } } From 5f30297dc86ec9229a9ed65a48701bfbd28ddf40 Mon Sep 17 00:00:00 2001 From: Sorin Valer Stanila Date: Sun, 12 Jun 2016 20:46:17 +0200 Subject: [PATCH 3/6] extended support for join added support for custom conditions as array added missing code after merge from upstream --- lib/Mapper.php | 20 +++++- lib/Query.php | 80 +++++++++++++++++------ lib/Query/Operator/Equals.php | 4 ++ lib/Query/Operator/FullText.php | 4 ++ lib/Query/Operator/FullTextBoolean.php | 4 ++ lib/Query/Operator/GreaterThan.php | 4 ++ lib/Query/Operator/GreaterThanOrEqual.php | 4 ++ lib/Query/Operator/In.php | 4 ++ lib/Query/Operator/LessThan.php | 4 ++ lib/Query/Operator/LessThanOrEqual.php | 4 ++ lib/Query/Operator/Like.php | 4 ++ lib/Query/Operator/Not.php | 7 +- lib/Query/Operator/RegExp.php | 4 ++ 13 files changed, 123 insertions(+), 24 deletions(-) diff --git a/lib/Mapper.php b/lib/Mapper.php index 8e27fe9..2bf1e52 100644 --- a/lib/Mapper.php +++ b/lib/Mapper.php @@ -91,12 +91,14 @@ public function collectionClass() /** * Entity manager class for storing information and meta-data about entities - * + * @param string $entityName * @return \Spot\Entity\Manager */ - public function entityManager() + public function entityManager($entityName = null) { - $entityName = $this->entity(); + if (!$entityName) { + $entityName = $this->entity(); + } if (!isset(self::$entityManager[$entityName])) { self::$entityManager[$entityName] = new Entity\Manager($entityName); } @@ -570,6 +572,18 @@ public function query($sql, array $params = []) return false; } + /** + * Execute custom query with no handling - just return affected rows + * Useful for UPDATE, DELETE, and INSERT queries + * + * @param string $sql Raw query or SQL to run against the datastore + * @param array Optional $conditions Array of binds in column => value pairs to use for prepared statement + */ + public function exec($sql, array $params = []) + { + return $this->connection()->executeUpdate($sql, $params); + } + /** * Find all records * diff --git a/lib/Query.php b/lib/Query.php index 01219df..daad478 100644 --- a/lib/Query.php +++ b/lib/Query.php @@ -140,24 +140,10 @@ public function builder() public function noQuote($noQuote = true) { $this->_noQuote = $noQuote; - $this->reEscapeFrom(); return $this; } - /** - * Re-escape from part of query according to new noQuote value - */ - protected function reEscapeFrom() - { - $part = $this->builder()->getQueryPart('from'); - $this->builder()->resetQueryPart('from'); - - foreach($part as $node) { - $this->from($this->unescapeIdentifier($node['table']), $this->unescapeIdentifier($node['alias'])); - } - } - /** * Return DBAL Query builder expression * @@ -421,10 +407,11 @@ public function whereSql($sql) * * @param array $where Array of conditions for this clause * @param bool $useAlias + * @param string $entityName * @return array SQL fragment strings for WHERE clause * @throws Exception */ - private function parseWhereToSQLFragments(array $where, $useAlias = true) + private function parseWhereToSQLFragments(array $where, $useAlias = true, $entityName=null) { $builder = $this->builder(); @@ -457,9 +444,14 @@ private function parseWhereToSQLFragments(array $where, $useAlias = true) // Prefix column name with alias if ($useAlias === true) { - $col = $this->fieldWithAlias($col); + $col = $this->fieldWithAlias($col, true, $entityName); } + if ( $this->stringIsExistingField($entityName, $value) ){ + $value = function () use ($value){ + return $value; + }; + } $sqlFragments[] = $operatorCallable($builder, $col, $value); } @@ -712,6 +704,11 @@ public function execute() */ public function toSql() { + if ($this->_noQuote) { + $escapeCharacter = $this->mapper()->connection()->getDatabasePlatform()->getIdentifierQuoteCharacter(); + return str_replace($escapeCharacter, '', $this->builder()->getSQL()); + } + return $this->builder()->getSQL(); } @@ -777,18 +774,25 @@ public function escapeIdentifier($identifier) * Get field name with table alias appended * @param string $field * @param bool $escaped + * @param string $entityName * @return string */ - public function fieldWithAlias($field, $escaped = true) + public function fieldWithAlias($field, $escaped = true, $entityName = null) { - $fieldInfo = $this->_mapper->entityManager()->fields(); + $fieldInfo = $this->_mapper->entityManager($entityName)->fields(); + //extract table alias if present + list($field, $table) = $this->extractTableAndFieldFromString($field); // Determine real field name (column alias support) if (isset($fieldInfo[$field])) { $field = $fieldInfo[$field]['column']; } - $field = $this->_tableName . '.' . $field; + if (!$table) { + $table = $this->_mapper->entityManager($entityName)->table(); + } + + $field = $table . '.' . $field; return $escaped ? $this->escapeIdentifier($field) : $field; } @@ -903,7 +907,7 @@ private function addMapping($type, array $data) public function makeJoin($type, $fromAlias, $entityName, $alias, $condition) { $joinTable = $this->mapper()->getMapper($entityName)->table(); - $conditionString = (string)$condition; +// $conditionString = (string)$condition; $this->addMapping( 'join', @@ -916,6 +920,8 @@ public function makeJoin($type, $fromAlias, $entityName, $alias, $condition) ), ) ); +// $testCondition = explode('=', $condition); + $conditionString = implode(' AND ', $this->parseWhereToSQLFragments($condition, true, $entityName)); // $conditionString = implode(' =', $condition); //@FIXME: now parameters are double escaped, because the initial are double escaped also :( $this->builder()->$type( @@ -927,4 +933,38 @@ public function makeJoin($type, $fromAlias, $entityName, $alias, $condition) return $this; } + + /** + * Extract data from string, for strings which contains "point" + * @Example: table.field + * @param $string + * @return array + */ + public function extractTableAndFieldFromString($string) + { + $pointPosition = strpos($string, '.'); + if ($pointPosition !== false) { + $table = substr($string, 0, $pointPosition); + $field = substr($string, $pointPosition + 1); + } else { + $table = null; + $field = $string; + } + + return [$field, $table]; + + } + + public function stringIsExistingField($entityName, $value){ + $fieldInfo = array_merge($this->_mapper->entityManager($entityName)->fields(), $this->_mapper->entityManager()->fields()); + $field = null; + //extract table alias if present + list($extractedField, $table) = $this->extractTableAndFieldFromString($value); + // Determine real field name (column alias support) + if (isset($fieldInfo[$extractedField])) { + $field = $fieldInfo[$extractedField]['column']; + } + + return $field; + } } diff --git a/lib/Query/Operator/Equals.php b/lib/Query/Operator/Equals.php index 4548b1b..8ea1a89 100644 --- a/lib/Query/Operator/Equals.php +++ b/lib/Query/Operator/Equals.php @@ -28,6 +28,10 @@ public function __invoke(QueryBuilder $builder, $column, $value) return $column . ' IS NULL'; } + if ($value instanceof \Closure) { + return $column . ' = ' . $value(); + } + return $column . ' = ' . $builder->createPositionalParameter($value); } } diff --git a/lib/Query/Operator/FullText.php b/lib/Query/Operator/FullText.php index 0ea6661..c069292 100644 --- a/lib/Query/Operator/FullText.php +++ b/lib/Query/Operator/FullText.php @@ -17,6 +17,10 @@ class FullText */ public function __invoke(QueryBuilder $builder, $column, $value) { + if ($value instanceof \Closure) { + return $column . ' = ' . $value(); + } + return 'MATCH(' . $column . ') AGAINST (' . $builder->createPositionalParameter($value) . ')'; } } diff --git a/lib/Query/Operator/FullTextBoolean.php b/lib/Query/Operator/FullTextBoolean.php index 2b5e1b5..383ceb1 100644 --- a/lib/Query/Operator/FullTextBoolean.php +++ b/lib/Query/Operator/FullTextBoolean.php @@ -17,6 +17,10 @@ class FullTextBoolean */ public function __invoke(QueryBuilder $builder, $column, $value) { + if ($value instanceof \Closure) { + return $column . ' = ' . $value(); + } + return 'MATCH(' . $column . ') AGAINST (' . $builder->createPositionalParameter($value) . ' IN BOOLEAN MODE)'; } } diff --git a/lib/Query/Operator/GreaterThan.php b/lib/Query/Operator/GreaterThan.php index af9e8be..1dd7799 100644 --- a/lib/Query/Operator/GreaterThan.php +++ b/lib/Query/Operator/GreaterThan.php @@ -17,6 +17,10 @@ class GreaterThan */ public function __invoke(QueryBuilder $builder, $column, $value) { + if ($value instanceof \Closure) { + return $column . ' = ' . $value(); + } + return $column . ' > ' . $builder->createPositionalParameter($value); } } diff --git a/lib/Query/Operator/GreaterThanOrEqual.php b/lib/Query/Operator/GreaterThanOrEqual.php index 8003674..db9e522 100644 --- a/lib/Query/Operator/GreaterThanOrEqual.php +++ b/lib/Query/Operator/GreaterThanOrEqual.php @@ -17,6 +17,10 @@ class GreaterThanOrEqual */ public function __invoke(QueryBuilder $builder, $column, $value) { + if ($value instanceof \Closure) { + return $column . ' = ' . $value(); + } + return $column . ' >= ' . $builder->createPositionalParameter($value); } } diff --git a/lib/Query/Operator/In.php b/lib/Query/Operator/In.php index 1985044..05c9ac9 100644 --- a/lib/Query/Operator/In.php +++ b/lib/Query/Operator/In.php @@ -24,6 +24,10 @@ public function __invoke(QueryBuilder $builder, $column, $value) throw new Exception("Use of IN operator expects value to be array. Got " . gettype($value) . "."); } + if ($value instanceof \Closure) { + return $column . ' = ' . $value(); + } + return $column . ' IN (' . $builder->createPositionalParameter($value, Connection::PARAM_STR_ARRAY) . ')'; } } diff --git a/lib/Query/Operator/LessThan.php b/lib/Query/Operator/LessThan.php index a888704..e57a7c7 100644 --- a/lib/Query/Operator/LessThan.php +++ b/lib/Query/Operator/LessThan.php @@ -17,6 +17,10 @@ class LessThan */ public function __invoke(QueryBuilder $builder, $column, $value) { + if ($value instanceof \Closure) { + return $column . ' < ' . $value(); + } + return $column . ' < ' . $builder->createPositionalParameter($value); } } diff --git a/lib/Query/Operator/LessThanOrEqual.php b/lib/Query/Operator/LessThanOrEqual.php index 59b0789..1828504 100644 --- a/lib/Query/Operator/LessThanOrEqual.php +++ b/lib/Query/Operator/LessThanOrEqual.php @@ -17,6 +17,10 @@ class LessThanOrEqual */ public function __invoke(QueryBuilder $builder, $column, $value) { + if ($value instanceof \Closure) { + return $column . ' <= ' . $value(); + } + return $column . ' <= ' . $builder->createPositionalParameter($value); } } diff --git a/lib/Query/Operator/Like.php b/lib/Query/Operator/Like.php index d2c1fc5..019c68a 100644 --- a/lib/Query/Operator/Like.php +++ b/lib/Query/Operator/Like.php @@ -17,6 +17,10 @@ class Like */ public function __invoke(QueryBuilder $builder, $column, $value) { + if ($value instanceof \Closure) { + return $column . ' = ' . $value(); + } + return $column . ' LIKE ' . $builder->createPositionalParameter($value); } } diff --git a/lib/Query/Operator/Not.php b/lib/Query/Operator/Not.php index b0ce871..0044ad8 100644 --- a/lib/Query/Operator/Not.php +++ b/lib/Query/Operator/Not.php @@ -21,13 +21,18 @@ class Not public function __invoke(QueryBuilder $builder, $column, $value) { if (is_array($value) && !empty($value)) { - return $column . ' NOT IN (' . $builder->createPositionalParameter($value, Connection::PARAM_STR_ARRAY) . ')'; + return $column . ' NOT IN (' . $builder->createPositionalParameter($value, + Connection::PARAM_STR_ARRAY) . ')'; } if ($value === null || (is_array($value) && empty($value))) { return $column . ' IS NOT NULL'; } + if ($value instanceof \Closure) { + return $column . ' = ' . $value(); + } + return $column . ' != ' . $builder->createPositionalParameter($value); } } diff --git a/lib/Query/Operator/RegExp.php b/lib/Query/Operator/RegExp.php index e33cfc2..e66eb39 100644 --- a/lib/Query/Operator/RegExp.php +++ b/lib/Query/Operator/RegExp.php @@ -17,6 +17,10 @@ class RegExp */ public function __invoke(QueryBuilder $builder, $column, $value) { + if ($value instanceof \Closure) { + return $column . ' = ' . $value(); + } + return $column . ' REGEXP ' . $builder->createPositionalParameter($value); } } From fc9b54818e669a9a7f40381efc631c2454a1b1ad Mon Sep 17 00:00:00 2001 From: Sorin Valer Stanila Date: Wed, 27 Jul 2016 17:51:27 +0200 Subject: [PATCH 4/6] tests for Mapper->entityManager() --- tests/Mapper.php | 63 ++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 63 insertions(+) diff --git a/tests/Mapper.php b/tests/Mapper.php index 4bd1f3e..af20423 100644 --- a/tests/Mapper.php +++ b/tests/Mapper.php @@ -20,4 +20,67 @@ public function testGetCustomEntityMapper() $query = $mapper->testQuery(); $this->assertInstanceOf('Spot\Query', $query); } + + /** + * entityManager() return the current entity if $entityName is empty + */ + public function testEntityManagerReturnTheCurrentEntityIfEntityNameIsEmpty() + { + $mapper = test_spot_mapper('SpotTest\Entity\NotNullOverride'); + $manager = $mapper->entityManager(); + $currentEntity = $mapper->entity(); + $reflection = new \ReflectionClass(get_class($manager)); + $privateProperty = $reflection->getProperty('entityName'); + $privateProperty->setAccessible(true); + $privateValue = $privateProperty->getValue($manager); + $this->assertEquals($currentEntity, $privateValue); + } + + + /** + * entityManager() return the same entity if we passed as parameter + */ + public function testEntityManagerReturnTheSameEntityIfWePassedAsParameter() + { + $mapper = test_spot_mapper('SpotTest\Entity\NotNullOverride'); + test_spot_mapper('\SpotTest\Entity\Author'); + $manager = $mapper->entityManager('\SpotTest\Entity\Author'); + $currentEntity = $mapper->entity(); + $reflection = new \ReflectionClass(get_class($manager)); + $privateProperty = $reflection->getProperty('entityName'); + $privateProperty->setAccessible(true); + $privateValue = $privateProperty->getValue($manager); + $this->assertNotEquals($currentEntity, $privateValue); + } + + + /** + * entityManager() return will return different entity from current entity if we passed as parameter + */ + public function testEntityManagerReturnWillReturnDifferentEntityFromCurrentEntityIfWePassedAsParameter() + { + $mapper = test_spot_mapper('SpotTest\Entity\NotNullOverride'); + test_spot_mapper('\SpotTest\Entity\Author'); + $manager = $mapper->entityManager('\SpotTest\Entity\Author'); + $reflection = new \ReflectionClass(get_class($manager)); + $privateProperty = $reflection->getProperty('entityName'); + $privateProperty->setAccessible(true); + $privateValue = $privateProperty->getValue($manager); + $this->assertEquals('\SpotTest\Entity\Author', $privateValue); + } + + + /** + * entityManager() always return instance of Entity\Manager + */ + public function testEntityManagerAlwaysReturnInstanceOfEntityManager() + { + $mapper = test_spot_mapper('\SpotTest\Entity\NotNullOverride'); + test_spot_mapper('\SpotTest\Entity\Author'); + $manager = $mapper->entityManager('\SpotTest\Entity\Author'); + $managerCurrent = $mapper->entityManager(); + $this->assertInstanceOf('\Spot\Entity\Manager', $manager); + $this->assertInstanceOf('\Spot\Entity\Manager', $managerCurrent); + } + } From 8378598e27f1d2e3d44090f10b20063175069302 Mon Sep 17 00:00:00 2001 From: Sorin Valer Stanila Date: Thu, 28 Jul 2016 13:52:01 +0200 Subject: [PATCH 5/6] fixed errors for checking existing fields passed as value to the query --- lib/Query.php | 20 ++++++++++++++++---- 1 file changed, 16 insertions(+), 4 deletions(-) diff --git a/lib/Query.php b/lib/Query.php index daad478..f80968b 100644 --- a/lib/Query.php +++ b/lib/Query.php @@ -2,8 +2,6 @@ namespace Spot; -use Doctrine\DBAL\Types\Type; - /** * Query Object - Used to build adapter-independent queries PHP-style * @@ -955,8 +953,22 @@ public function extractTableAndFieldFromString($string) } - public function stringIsExistingField($entityName, $value){ - $fieldInfo = array_merge($this->_mapper->entityManager($entityName)->fields(), $this->_mapper->entityManager()->fields()); + /** + * Determine if the value is a existing field + * @param string $entityName + * @param string $value + * @return string + */ + public function stringIsExistingField($entityName, $value) + { + $field = ''; + if (is_array($value)) { + return $field; + } + $fieldInfo = array_merge( + $this->_mapper->entityManager($entityName)->fields(), + $this->_mapper->entityManager()->fields() + ); $field = null; //extract table alias if present list($extractedField, $table) = $this->extractTableAndFieldFromString($value); From 06491b2106264c8b786cf88072395e95eb50b942 Mon Sep 17 00:00:00 2001 From: Sorin Valer Stanila Date: Thu, 16 Nov 2017 13:33:23 +0100 Subject: [PATCH 6/6] test file --- test | 1 + 1 file changed, 1 insertion(+) create mode 100644 test diff --git a/test b/test new file mode 100644 index 0000000..9daeafb --- /dev/null +++ b/test @@ -0,0 +1 @@ +test