From 938639a590a283a58a7353d3723654caf31af153 Mon Sep 17 00:00:00 2001 From: Dave Mc Nicoll Date: Sat, 28 Mar 2020 22:13:29 -0400 Subject: [PATCH 1/3] - Multiple bugfixe linked to query fragments - Added a new SearchRequest package to handle search request efficiently --- src/Common/EntityField.php | 2 +- src/Common/PdoObject.php | 9 +-- src/EntityTrait.php | 17 ++++- src/Query/Fragment.php | 2 +- src/Query/Insert.php | 2 +- src/Query/Offset.php | 1 + src/Query/OrderBy.php | 3 + src/Query/Update.php | 4 +- src/Query/Values.php | 2 +- src/QueryBuilder.php | 2 +- src/Repository.php | 67 ++++++++++++++++--- src/SearchRequest/SearchRequestInterface.php | 17 +++++ .../SearchRequestPaginationTrait.php | 28 ++++++++ src/SearchRequest/SearchableInterface.php | 11 +++ src/Ulmus.php | 8 ++- 15 files changed, 148 insertions(+), 27 deletions(-) create mode 100644 src/SearchRequest/SearchRequestInterface.php create mode 100644 src/SearchRequest/SearchRequestPaginationTrait.php create mode 100644 src/SearchRequest/SearchableInterface.php diff --git a/src/Common/EntityField.php b/src/Common/EntityField.php index 1af3f96..52e218e 100644 --- a/src/Common/EntityField.php +++ b/src/Common/EntityField.php @@ -27,7 +27,7 @@ class EntityField # Must use REFLECTION before throwing this value. # Should first check if it's a relation field, and if it is, # it's real key must be returned (PK usually) - return $useAlias ? "{$this->alias}.\"{$this->name}\"" : "\"{$this->name}\""; + return $useAlias ? "{$this->alias}.`{$this->name}`" : "`{$this->name}`"; } public static function isScalarType($type) : bool diff --git a/src/Common/PdoObject.php b/src/Common/PdoObject.php index fc6c33e..e9af73a 100644 --- a/src/Common/PdoObject.php +++ b/src/Common/PdoObject.php @@ -8,8 +8,6 @@ use PDO, class PdoObject extends PDO { public function select(string $sql, array $parameters = []): PDOStatement { - # var_dump($sql, $parameters); die(); - try { if (false !== ( $statement = $this->prepare($sql) )) { $statement = $this->execute($statement, $parameters, false); @@ -23,8 +21,6 @@ class PdoObject extends PDO { } public function runQuery(string $sql, array $parameters = []): ? PDOStatement { - # dump($sql, $parameters); return null; - try { if (false !== ( $statement = $this->prepare($sql) )) { return $this->execute($statement, $parameters, true); @@ -50,8 +46,9 @@ class PdoObject extends PDO { } return $statement; - } else { - throw new PDOException('Could not begin transaction or given statement is invalid.'); + } + else { + throw new \PDOException($statement->errorCode() . " - " . json_encode($statement->errorInfo())); } } catch (\PDOException $e) { $this->rollback(); diff --git a/src/EntityTrait.php b/src/EntityTrait.php index 0c33a76..089e30a 100644 --- a/src/EntityTrait.php +++ b/src/EntityTrait.php @@ -153,8 +153,21 @@ trait EntityTrait { foreach($entityResolver->fieldList() as $key => $field) { $annotation = $entityResolver->searchFieldAnnotation($key, new Field() ); - if ( isset($this->$key) ) { - $dataset[ $annotation->name ?? $key ] = is_object($this->$key) ? Ulmus::convertObject($this->$key) : $this->$key; + if ( isset($this->$key) ) { + $realKey = $annotation->name ?? $key; + + switch (true) { + case is_object($this->$key): + $dataset[$realKey] = Ulmus::convertObject($this->$key); + break; + + case is_array($this->$key): + $dataset[$realKey] = Ulmus::encodeArray($this->$key); + break; + + default: + $dataset[$realKey] = $this->$key; + } } elseif ( $field['nullable'] ) { $dataset[ $annotation->name ?? $key ] = null; diff --git a/src/Query/Fragment.php b/src/Query/Fragment.php index d17c3de..7754a2c 100644 --- a/src/Query/Fragment.php +++ b/src/Query/Fragment.php @@ -10,6 +10,6 @@ abstract class Fragment { protected function renderSegments(array $segments, string $glue = " ") : string { - return implode($glue, array_filter($segments)); + return implode($glue, array_filter($segments, function($i) { return ! is_null($i) && $i !== false && $i !== ""; })); } } diff --git a/src/Query/Insert.php b/src/Query/Insert.php index 6aac5bf..124ca48 100644 --- a/src/Query/Insert.php +++ b/src/Query/Insert.php @@ -31,6 +31,6 @@ class Insert extends Fragment { protected function renderTable() : string { - return $this->table; + return "`$this->table`"; } } diff --git a/src/Query/Offset.php b/src/Query/Offset.php index 5b84d2b..b9d8d7e 100644 --- a/src/Query/Offset.php +++ b/src/Query/Offset.php @@ -11,6 +11,7 @@ class Offset extends Fragment { public function set($offset) : self { $this->offset = $offset; + return $this; } diff --git a/src/Query/OrderBy.php b/src/Query/OrderBy.php index b88a1d4..c4143af 100644 --- a/src/Query/OrderBy.php +++ b/src/Query/OrderBy.php @@ -10,12 +10,14 @@ class OrderBy extends Fragment { public function set(array $order) : self { $this->orderBy = $order; + return $this; } public function add(string $field, ? string $direction = null) : self { $this->orderBy[] = [ $field, $direction ]; + return $this; } @@ -23,6 +25,7 @@ class OrderBy extends Fragment { { $list = array_map(function($item) { list($field, $direction) = $item; + return $field . ( $direction ? " $direction" : "" ); }, $this->orderBy); diff --git a/src/Query/Update.php b/src/Query/Update.php index d825a40..dfd0050 100644 --- a/src/Query/Update.php +++ b/src/Query/Update.php @@ -24,12 +24,12 @@ class Update extends Fragment { 'UPDATE', ( $this->priority ?? false ), ( $this->ignore ? 'IGNORE' : false ), - $this->table, + $this->renderTable(), ]); } protected function renderTable() : string { - return $this->table; + return "`$this->table`"; } } diff --git a/src/Query/Values.php b/src/Query/Values.php index b7e8416..45fc348 100644 --- a/src/Query/Values.php +++ b/src/Query/Values.php @@ -33,7 +33,7 @@ class Values extends Fragment { public function render() : string { $this->queryBuilder->addValues($this->flattenRowsArray()); - + return $this->renderSegments([ 'VALUES', $this->renderParameterPlaceholders(), ]); diff --git a/src/QueryBuilder.php b/src/QueryBuilder.php index 42865b5..800b731 100644 --- a/src/QueryBuilder.php +++ b/src/QueryBuilder.php @@ -165,7 +165,7 @@ class QueryBuilder $this->where = $where = new Query\Where($this); $this->push($where); } - + $this->conditionOperator = $operator; $where->add($field, $value, $operator, $condition, $not); diff --git a/src/Repository.php b/src/Repository.php index d5c8c2b..7de41a3 100644 --- a/src/Repository.php +++ b/src/Repository.php @@ -53,15 +53,11 @@ class Repository public function count() : int { - $this->select("count(*) as totalItem")->selectSqlQuery(); + $this->select("count(*)")->selectSqlQuery(); $this->finalizeQuery(); - - foreach(Ulmus::iterateQueryBuilder($this->queryBuilder, $this->adapter) as $entityData) { - return $entityData['totalItem']; - } - return 0; + return Ulmus::runQuery($this->queryBuilder, $this->adapter)->fetchColumn(0); } public function deleteOne() @@ -218,6 +214,15 @@ class Repository return $this; } + public function wheres(array $fieldValues, string $operator = Query\Where::OPERATOR_EQUAL) : self + { + foreach($fieldValues as $field => $value) { + $this->where($field, $value, $operator); + } + + return $this; + } + public function and($field, $value, string $operator = Query\Where::OPERATOR_EQUAL) : self { return $this->where($field, $value, $operator); @@ -237,9 +242,9 @@ class Repository return $this; } - public function orNot($field, $value, string $operator = Query\Where::OPERATOR_EQUAL) : self + public function orNot($field, $value, string $operator = Query\Where::OPERATOR_NOT_EQUAL) : self { - $this->queryBuilder->notWhere($condition, Query\Where::CONDITION_OR, true); + $this->queryBuilder->notWhere($field, $value, $operator, Query\Where::CONDITION_OR, true); return $this; } @@ -289,11 +294,21 @@ class Repository public function notLike($field, $value) : self { - $this->queryBuilder->where($field, $value, Query\Where::OPERATOR_LIKE, Query\Where::CONDITION_AND, true); + $this->queryBuilder->notWhere($field, $value, Query\Where::OPERATOR_LIKE, Query\Where::CONDITION_AND, true); return $this; } + + public function likes(array $fieldValues) : self + { + foreach($fieldValues as $field => $value) { + $this->like($field, $value); + } + + return $this; + } + public function match() : self { @@ -316,10 +331,15 @@ class Repository public function groupBy() : self { - #$this->queryBuilder->groupBy(); return $this; } + public function groups(array $groups) : self + { + # foreach($this->groups) + return $this; + } + public function orderBy($field, ? string $direction = null) : self { $this->queryBuilder->orderBy($field, $direction); @@ -327,6 +347,16 @@ class Repository return $this; } + public function orders(array $orderList) : self + { + foreach($orderList as $field => $direction) { + $this->queryBuilder->orderBy($field, $direction); + } + + return $this; + } + + public function limit(int $value) : self { $this->queryBuilder->limit($value); @@ -362,9 +392,24 @@ class Repository return $this->where($primaryKeyField[$pkField]->name ?? $pkField, $value); } + public function filterServerRequest(SearchRequest\SearchRequestInterface $searchRequest) : self + { + $searchRequest->count = (clone $this)->wheres($searchRequest->wheres()) + ->likes($searchRequest->likes()) + ->orders($searchRequest->orders()) + ->groups($searchRequest->groups()) + ->count(); + + return $this->wheres($searchRequest->wheres()) + ->likes($searchRequest->likes()) + ->orders($searchRequest->orders()) + ->groups($searchRequest->groups()) + ->offset($searchRequest->offset()) + ->limit($searchRequest->limit()); + } + protected function collectionFromQuery() : EntityCollection { - $class = $this->entityClass; $entityCollection = new EntityCollection(); diff --git a/src/SearchRequest/SearchRequestInterface.php b/src/SearchRequest/SearchRequestInterface.php new file mode 100644 index 0000000..59b6d7a --- /dev/null +++ b/src/SearchRequest/SearchRequestInterface.php @@ -0,0 +1,17 @@ +count = $itemCount; + $this->page = $page; + } + + public function pageCount() : int + { + return ceil($this->count / $this->limit()); + } + + public function hasPagination() : int + { + return $this->pageCount() > 1; + } +} diff --git a/src/SearchRequest/SearchableInterface.php b/src/SearchRequest/SearchableInterface.php new file mode 100644 index 0000000..1df871f --- /dev/null +++ b/src/SearchRequest/SearchableInterface.php @@ -0,0 +1,11 @@ +closeCursor(); + $queryBuilder->reset(); return [ @@ -68,12 +69,17 @@ abstract class Ulmus { return ( static::$objectInstanciator ?? static::$objectInstanciator = new Entity\ObjectInstanciator() )->instanciate($type, $arguments); } - + public static function convertObject(object $obj) { return ( static::$objectInstanciator ?? static::$objectInstanciator = new Entity\ObjectInstanciator() )->convert($obj); } + public static function encodeArray(array $array) + { + return json_encode($array); + } + public static function registerAdapter(ConnectionAdapter $adapter, bool $default = false) : void { if ($default) { From 84da4dbb7b056b01b1717f28dd9f1dae242af8c0 Mon Sep 17 00:00:00 2001 From: Dave Mc Nicoll Date: Tue, 31 Mar 2020 13:24:15 -0400 Subject: [PATCH 2/3] - Bugfixes done linked to SearchRequest and Searchable features. - Fixed a bug introduced into PdoObject exception on latest commit. - Corrected the open() close() enclosure of query condition. --- src/Annotation/Property/Relation.php | 6 +- src/Common/PdoObject.php | 17 ++++- src/Query/Limit.php | 6 +- src/Query/Offset.php | 4 ++ src/Query/Where.php | 2 +- src/QueryBuilder.php | 20 ++++-- src/Repository.php | 62 ++++++++++++++++--- .../SearchRequestPaginationTrait.php | 12 ++++ src/SearchRequest/SearchableInterface.php | 2 +- 9 files changed, 111 insertions(+), 20 deletions(-) diff --git a/src/Annotation/Property/Relation.php b/src/Annotation/Property/Relation.php index 22465f9..ccc60ac 100644 --- a/src/Annotation/Property/Relation.php +++ b/src/Annotation/Property/Relation.php @@ -28,7 +28,11 @@ class Relation implements \Ulmus\Annotation\Annotation { } public function entity() { - $e = $this->entity; + try { + $e = $this->entity; + } catch (\Throwable $ex) { + throw new \Exception("Your @Relation annotation seems to be missing an `entity` entry."); + } return new $e(); } diff --git a/src/Common/PdoObject.php b/src/Common/PdoObject.php index 94a4208..346b4a3 100644 --- a/src/Common/PdoObject.php +++ b/src/Common/PdoObject.php @@ -16,7 +16,13 @@ class PdoObject extends PDO { return $statement; } } catch (\PDOException $e) { - throw $e; + switch ( $e->getCode() ) { + case 42000: + throw new \PdoException($e->getMessage() . " `$sql` with data:" . json_encode($parameters)); + + default: + throw $e; + } } } @@ -26,7 +32,14 @@ class PdoObject extends PDO { return $this->execute($statement, $parameters, true); } } catch (\PDOException $e) { - throw $e; + switch ( $e->getCode() ) { + case 42000: + throw new \PdoException($e->getMessage() . " `$sql` with data:" . json_encode($parameters)); + + default: + throw $e; + } + } return null; diff --git a/src/Query/Limit.php b/src/Query/Limit.php index c855097..e022a96 100644 --- a/src/Query/Limit.php +++ b/src/Query/Limit.php @@ -19,8 +19,12 @@ class Limit extends Fragment { public function render() : string { + if ( $this->limit < 0 ) { + throw new \Exception("An error occured trying to render the LIMIT fragment ; given value has to be > 0. Received {$this->limit}"); + } + return $this->renderSegments([ - sprintf($this->keyword, $this->limit) + sprintf($this->keyword, abs($this->limit)) ]); } } diff --git a/src/Query/Offset.php b/src/Query/Offset.php index b9d8d7e..a92c2f0 100644 --- a/src/Query/Offset.php +++ b/src/Query/Offset.php @@ -17,6 +17,10 @@ class Offset extends Fragment { public function render() : string { + if ( $this->offset < 0 ) { + throw new \Exception("An error occured trying to render the OFFSET fragment ; given value has to be > 0. Received {$this->offset}"); + } + return $this->renderSegments([ 'OFFSET', $this->offset, ]); diff --git a/src/Query/Where.php b/src/Query/Where.php index 6c7e5b9..a660db1 100644 --- a/src/Query/Where.php +++ b/src/Query/Where.php @@ -51,7 +51,7 @@ class Where extends Fragment { public function render() : string { $stack = []; - + foreach ($this->conditionList ?? [] as $key => $item) { if ( $item instanceof Where ) { if ( $item->conditionList ?? false ) { diff --git a/src/QueryBuilder.php b/src/QueryBuilder.php index 800b731..0903b18 100644 --- a/src/QueryBuilder.php +++ b/src/QueryBuilder.php @@ -5,7 +5,7 @@ namespace Ulmus; class QueryBuilder { public Query\Where $where; - + /** * Those are the parameters we are going to bind to PDO. */ @@ -139,7 +139,13 @@ class QueryBuilder public function open(string $condition = Query\Where::CONDITION_AND) : self { - if ( null !== ($this->where ?? false) ) { + if ( null !== ($this->where ?? null) ) { + $this->where->conditionList[] = $new = new Query\Where($this, $condition); + $this->where = $new; + } + else { + $this->where = new Query\Where($this, $condition); + $this->push($this->where); $this->where->conditionList[] = $new = new Query\Where($this, $condition); $this->where = $new; } @@ -149,7 +155,13 @@ class QueryBuilder public function close() : self { - if ( null !== ($this->where ?? false) && $this->where->parent ) { + if ( null !== ($this->where ?? null) && $this->where->parent ) { + + # if an enclosure was opened, and nothing done, we must remove the unused node + if ( empty($this->where->conditionList) && (count($this->where->parent->conditionList) === 1) ) { + unset($this->where->parent->conditionList); + } + $this->where = $this->where->parent; } @@ -167,6 +179,7 @@ class QueryBuilder } $this->conditionOperator = $operator; + $where->add($field, $value, $operator, $condition, $not); return $this; @@ -179,7 +192,6 @@ class QueryBuilder public function groupBy() : self { - //$this->queryBuilder->groupBy(); return $this; } diff --git a/src/Repository.php b/src/Repository.php index 7de41a3..605dbfe 100644 --- a/src/Repository.php +++ b/src/Repository.php @@ -217,7 +217,24 @@ class Repository public function wheres(array $fieldValues, string $operator = Query\Where::OPERATOR_EQUAL) : self { foreach($fieldValues as $field => $value) { - $this->where($field, $value, $operator); + if ( is_array($value) ) { + switch ($value[1]) { + case Query\Where::CONDITION_AND: + $this->where($field, $value[0], $operator); + break; + + case Query\Where::CONDITION_OR: + $this->or($field, $value[0], $operator); + break; + + case Query\Where::CONDITION_NOT: + $this->notWhere($field, $value[0], $operator); + break; + } + } + else { + $this->where($field, $value, $operator); + } } return $this; @@ -287,23 +304,46 @@ class Repository public function like($field, $value) : self { - $this->queryBuilder->where($field, $value, Query\Where::OPERATOR_LIKE, Query\Where::CONDITION_AND); + $this->where($field, $value, Query\Where::OPERATOR_LIKE); + + return $this; + } + + public function orLike($field, $value) : self + { + $this->or($field, $value, Query\Where::OPERATOR_LIKE); return $this; } public function notLike($field, $value) : self { - $this->queryBuilder->notWhere($field, $value, Query\Where::OPERATOR_LIKE, Query\Where::CONDITION_AND, true); + $this->notWhere($field, $value, Query\Where::OPERATOR_LIKE); return $this; } - - public function likes(array $fieldValues) : self + public function likes(array $fieldValues, string $condition = Query\Where::CONDITION_AND) : self { foreach($fieldValues as $field => $value) { - $this->like($field, $value); + if ( is_array($value) ) { + switch ($value[1]) { + case Query\Where::CONDITION_AND: + $this->like($field, $value[0]); + break; + + case Query\Where::CONDITION_OR: + $this->orLike($field, $value[0]); + break; + + case Query\Where::CONDITION_NOT: + $this->notLike($field, $value[0]); + break; + } + } + else { + $this->like($field, $value); + } } return $this; @@ -394,14 +434,16 @@ class Repository public function filterServerRequest(SearchRequest\SearchRequestInterface $searchRequest) : self { - $searchRequest->count = (clone $this)->wheres($searchRequest->wheres()) - ->likes($searchRequest->likes()) + $searchRequest->count = $searchRequest->filter( clone $this ) + ->wheres($searchRequest->wheres(), Query\Where::OPERATOR_EQUAL, Query\Where::CONDITION_AND) + ->likes($searchRequest->likes(), Query\Where::CONDITION_OR) ->orders($searchRequest->orders()) ->groups($searchRequest->groups()) ->count(); - return $this->wheres($searchRequest->wheres()) - ->likes($searchRequest->likes()) + return $searchRequest->filter($this) + ->wheres($searchRequest->wheres(), Query\Where::OPERATOR_EQUAL, Query\Where::CONDITION_AND) + ->likes($searchRequest->likes(), Query\Where::CONDITION_OR) ->orders($searchRequest->orders()) ->groups($searchRequest->groups()) ->offset($searchRequest->offset()) diff --git a/src/SearchRequest/SearchRequestPaginationTrait.php b/src/SearchRequest/SearchRequestPaginationTrait.php index 904fb66..c4d0de9 100644 --- a/src/SearchRequest/SearchRequestPaginationTrait.php +++ b/src/SearchRequest/SearchRequestPaginationTrait.php @@ -10,6 +10,18 @@ trait SearchRequestPaginationTrait { public int $pageCount = 0; + public int $limit = 25; + + public function limit(): int + { + return $this->limit; + } + + public function offset(): int + { + return abs( ( $this->page - 1 ) * $this->limit() ); + } + public function pagination(int $page, int $itemCount) : void { $this->count = $itemCount; diff --git a/src/SearchRequest/SearchableInterface.php b/src/SearchRequest/SearchableInterface.php index 1df871f..7e03292 100644 --- a/src/SearchRequest/SearchableInterface.php +++ b/src/SearchRequest/SearchableInterface.php @@ -6,6 +6,6 @@ use \Psr\Http\Message\ServerRequestInterface; interface SearchableInterface { - public static function searchRequest(ServerRequestInterface $request) : SearchRequestInterface; + public static function searchRequest() : SearchRequestInterface; } From 1456fe91f2de299481dc80972e854a76f92b12af Mon Sep 17 00:00:00 2001 From: Dave Mc Nicoll Date: Thu, 9 Apr 2020 09:46:22 -0400 Subject: [PATCH 3/3] - Fixed the SearchRequestInterface missing filter() function. --- src/SearchRequest/SearchRequestInterface.php | 4 ++++ src/SearchRequest/SearchableInterface.php | 2 +- 2 files changed, 5 insertions(+), 1 deletion(-) diff --git a/src/SearchRequest/SearchRequestInterface.php b/src/SearchRequest/SearchRequestInterface.php index 59b6d7a..b2ab579 100644 --- a/src/SearchRequest/SearchRequestInterface.php +++ b/src/SearchRequest/SearchRequestInterface.php @@ -2,7 +2,11 @@ namespace Ulmus\SearchRequest; +use Ulmus\Repository; + interface SearchRequestInterface { + public function filter(Repository $repository) : Repository; + public function wheres() : iterable; public function likes() : iterable; diff --git a/src/SearchRequest/SearchableInterface.php b/src/SearchRequest/SearchableInterface.php index 7e03292..bbdc1db 100644 --- a/src/SearchRequest/SearchableInterface.php +++ b/src/SearchRequest/SearchableInterface.php @@ -6,6 +6,6 @@ use \Psr\Http\Message\ServerRequestInterface; interface SearchableInterface { - public static function searchRequest() : SearchRequestInterface; + public static function searchRequest(...$arguments) : SearchRequestInterface; }