From edbd364dd6d84a6b2b716801de11d122222b06a3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Marcin=20Pude=C5=82ek?= Date: Sun, 3 Dec 2017 15:07:56 +0100 Subject: [PATCH 01/11] codeStyles idea --- .idea/codeStyles/codeStyleConfig.xml | 5 +++++ 1 file changed, 5 insertions(+) create mode 100644 .idea/codeStyles/codeStyleConfig.xml diff --git a/.idea/codeStyles/codeStyleConfig.xml b/.idea/codeStyles/codeStyleConfig.xml new file mode 100644 index 0000000..cd74ae8 --- /dev/null +++ b/.idea/codeStyles/codeStyleConfig.xml @@ -0,0 +1,5 @@ + + + + \ No newline at end of file From 5e2f35a8e731927a88e10a301d375cdc58842182 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Marcin=20Pude=C5=82ek?= Date: Sun, 3 Dec 2017 23:37:12 +0100 Subject: [PATCH 02/11] return of get() depends on fetch mode --- .../QueryBuilder/QueryBuilderHandler.php | 25 +++++++++++-------- 1 file changed, 14 insertions(+), 11 deletions(-) diff --git a/src/Pecee/Pixie/QueryBuilder/QueryBuilderHandler.php b/src/Pecee/Pixie/QueryBuilder/QueryBuilderHandler.php index 159b38f..dc59aa0 100644 --- a/src/Pecee/Pixie/QueryBuilder/QueryBuilderHandler.php +++ b/src/Pecee/Pixie/QueryBuilder/QueryBuilderHandler.php @@ -261,7 +261,7 @@ protected function aggregate(string $type): int */ public function alias(string $alias, string $table = null) { - if($table === null && isset($this->statements['tables'][0]) === true) { + if ($table === null && isset($this->statements['tables'][0]) === true) { $table = $this->statements['tables'][0]; } else { $table = $this->tablePrefix . $table; @@ -447,7 +447,7 @@ public function from($tables) * Get all rows * * @throws Exception - * @return object[] + * @return array */ public function get(): array { @@ -815,9 +815,9 @@ public function orWhereIn($key, $values) /** * Adds OR WHERE NOT statement to the current query. * - * @param string|Raw|\Closure $key - * @param string|null $operator - * @param mixed|Raw|\Closure|null $value + * @param string|Raw|\Closure $key + * @param string|null $operator + * @param mixed|Raw|\Closure|null $value * * @return static */ @@ -1121,6 +1121,7 @@ public function subQuery(QueryBuilderHandler $queryBuilder, $alias = null): Raw * Sets the table that the query is using * * @param string|array $tables Single table or multiple tables as an array or as multiple parameters + * * @throws Exception * @return static * @@ -1163,6 +1164,7 @@ public function table($tables) * Performs the transaction * * @param \Closure $callback + * * @throws \Exception * @return static */ @@ -1239,9 +1241,9 @@ public function updateOrInsert($data) /** * Adds WHERE statement to the current query. * - * @param string|Raw|\Closure $key - * @param string|null $operator - * @param mixed|Raw|\Closure|null $value + * @param string|Raw|\Closure $key + * @param string|null $operator + * @param mixed|Raw|\Closure|null $value * * @return static */ @@ -1366,15 +1368,16 @@ public function whereNull($key) * Handles WHERE NULL statements. * * @param string|Raw|\Closure $key - * @param string $prefix - * @param string $operator + * @param string $prefix + * @param string $operator * * @return mixed */ protected function whereNullHandler($key, $prefix = '', $operator = '') { - $key = $this->adapterInstance->wrapSanitizer($this->addTablePrefix($key)); + $key = $this->adapterInstance->wrapSanitizer($this->addTablePrefix($key)); $prefix = ($prefix !== '') ? $prefix . ' ' : $prefix; + return $this->{$operator . 'Where'}($this->raw("$key IS {$prefix}NULL")); } } From 919b8f136ce15e8107f70f30b58d5971c0b520f1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Marcin=20Pude=C5=82ek?= Date: Sun, 3 Dec 2017 23:49:40 +0100 Subject: [PATCH 03/11] add return type to methods --- .../Pixie/ConnectionAdapters/BaseAdapter.php | 3 +- src/Pecee/Pixie/ConnectionAdapters/Sqlite.php | 6 ++-- .../QueryBuilder/Adapters/BaseAdapter.php | 28 +++++++++---------- .../QueryBuilder/QueryBuilderHandler.php | 8 +++--- src/Pecee/Pixie/QueryBuilder/QueryObject.php | 2 +- 5 files changed, 25 insertions(+), 22 deletions(-) diff --git a/src/Pecee/Pixie/ConnectionAdapters/BaseAdapter.php b/src/Pecee/Pixie/ConnectionAdapters/BaseAdapter.php index 5cfb9d7..5046df5 100644 --- a/src/Pecee/Pixie/ConnectionAdapters/BaseAdapter.php +++ b/src/Pecee/Pixie/ConnectionAdapters/BaseAdapter.php @@ -2,6 +2,7 @@ namespace Pecee\Pixie\ConnectionAdapters; +use PDO; use Viocon\Container; /** @@ -28,7 +29,7 @@ public function __construct(Container $container) * @param $config * @return \PDO */ - public function connect(array $config) + public function connect(array $config): PDO { if (isset($config['options']) === false) { $config['options'] = []; diff --git a/src/Pecee/Pixie/ConnectionAdapters/Sqlite.php b/src/Pecee/Pixie/ConnectionAdapters/Sqlite.php index 25a783d..4800dbf 100644 --- a/src/Pecee/Pixie/ConnectionAdapters/Sqlite.php +++ b/src/Pecee/Pixie/ConnectionAdapters/Sqlite.php @@ -2,6 +2,8 @@ namespace Pecee\Pixie\ConnectionAdapters; +use PDO; + /** * Class Sqlite * @@ -15,7 +17,7 @@ class Sqlite extends BaseAdapter * @return \PDO * @throws Exception */ - public function doConnect(array $config) + public function doConnect(array $config): PDO { if (\extension_loaded('pdo_sqlite') === false) { throw new Exception(sprintf('%s library not loaded', 'pdo_sqlite')); @@ -24,7 +26,7 @@ public function doConnect(array $config) $connectionString = 'sqlite:' . $config['database']; return $this->container->build( - \PDO::class, + PDO::class, [$connectionString, null, null, $config['options']] ); } diff --git a/src/Pecee/Pixie/QueryBuilder/Adapters/BaseAdapter.php b/src/Pecee/Pixie/QueryBuilder/Adapters/BaseAdapter.php index 50ad1b9..d335e61 100644 --- a/src/Pecee/Pixie/QueryBuilder/Adapters/BaseAdapter.php +++ b/src/Pecee/Pixie/QueryBuilder/Adapters/BaseAdapter.php @@ -77,7 +77,7 @@ protected function arrayStr(array $pieces, $glue = ',', $wrapSanitizer = true): * @throws Exception * @return array */ - protected function buildCriteria(array $statements, $bindValues = true) + protected function buildCriteria(array $statements, $bindValues = true): array { $criteria = ''; $bindings = [[]]; @@ -194,7 +194,7 @@ protected function buildCriteria(array $statements, $bindValues = true) * @return array * @throws Exception */ - protected function buildCriteriaWithType(array $statements, $key, $type, $bindValues = true) + protected function buildCriteriaWithType(array $statements, $key, $type, $bindValues = true): array { $criteria = ''; $bindings = []; @@ -219,7 +219,7 @@ protected function buildCriteriaWithType(array $statements, $key, $type, $bindVa * @return string * @throws Exception */ - protected function buildJoin(array $statements) + protected function buildJoin(array $statements): string { $sql = ''; @@ -260,7 +260,7 @@ protected function buildJoin(array $statements) * * @return string */ - protected function concatenateQuery(array $pieces) + protected function concatenateQuery(array $pieces): string { $str = ''; foreach ($pieces as $piece) { @@ -279,7 +279,7 @@ protected function concatenateQuery(array $pieces) * @return array * @throws Exception */ - public function criteriaOnly(array $statements, $bindValues = true) + public function criteriaOnly(array $statements, $bindValues = true): array { $sql = $bindings = []; if (isset($statements['criteria']) === false) { @@ -299,7 +299,7 @@ public function criteriaOnly(array $statements, $bindValues = true) * @return array * @throws Exception */ - public function delete(array $statements) + public function delete(array $statements): array { $table = end($statements['tables']); @@ -323,7 +323,7 @@ public function delete(array $statements) * @return array * @throws Exception */ - private function doInsert(array $statements, array $data, $type) + private function doInsert(array $statements, array $data, $type): array { $table = end($statements['tables']); @@ -371,7 +371,7 @@ private function doInsert(array $statements, array $data, $type) * * @return array */ - private function getUpdateStatement(array $data) + private function getUpdateStatement(array $data): array { $bindings = []; $statement = ''; @@ -402,7 +402,7 @@ private function getUpdateStatement(array $data) * @return array * @throws Exception */ - public function insert(array $statements, array $data) + public function insert(array $statements, array $data): array { return $this->doInsert($statements, $data, 'INSERT'); } @@ -416,7 +416,7 @@ public function insert(array $statements, array $data) * @return array * @throws Exception */ - public function insertIgnore(array $statements, array $data) + public function insertIgnore(array $statements, array $data): array { return $this->doInsert($statements, $data, 'INSERT IGNORE'); } @@ -430,7 +430,7 @@ public function insertIgnore(array $statements, array $data) * @return array * @throws Exception */ - public function replace(array $statements, array $data) + public function replace(array $statements, array $data): array { return $this->doInsert($statements, $data, 'REPLACE'); } @@ -443,7 +443,7 @@ public function replace(array $statements, array $data) * @throws Exception * @return array */ - public function select(array $statements) + public function select(array $statements): array { if (array_key_exists('selects', $statements) === false) { $statements['selects'] = ['*']; @@ -543,7 +543,7 @@ public function select(array $statements) * @return array * @throws Exception */ - public function update(array $statements, array $data) + public function update(array $statements, array $data): array { if (\count($data) < 1) { throw new Exception('No data given.', 4); @@ -582,7 +582,7 @@ public function update(array $statements, array $data) * * @return string */ - public function wrapSanitizer($value) + public function wrapSanitizer($value): string { // Its a raw query, just cast as string, object has __toString() if ($value instanceof Raw) { diff --git a/src/Pecee/Pixie/QueryBuilder/QueryBuilderHandler.php b/src/Pecee/Pixie/QueryBuilder/QueryBuilderHandler.php index dc59aa0..b538b0e 100644 --- a/src/Pecee/Pixie/QueryBuilder/QueryBuilderHandler.php +++ b/src/Pecee/Pixie/QueryBuilder/QueryBuilderHandler.php @@ -388,9 +388,9 @@ public function find($value, $fieldName = 'id') * @param string|int|float $value * * @throws Exception - * @return \stdClass[] + * @return array */ - public function findAll(string $fieldName, $value) + public function findAll(string $fieldName, $value): array { return $this->where($fieldName, '=', $value)->get(); } @@ -907,7 +907,7 @@ public function orderBy($fields, $defaultDirection = 'ASC') * * @return PDO */ - public function pdo() + public function pdo(): PDO { return $this->pdo; } @@ -1206,7 +1206,7 @@ public function transaction(\Closure $callback) * @throws Exception * @return \PDOStatement */ - public function update($data) + public function update($data): \PDOStatement { /** * @var $response \PDOStatement diff --git a/src/Pecee/Pixie/QueryBuilder/QueryObject.php b/src/Pecee/Pixie/QueryBuilder/QueryObject.php index 4bd1463..54823eb 100644 --- a/src/Pecee/Pixie/QueryBuilder/QueryObject.php +++ b/src/Pecee/Pixie/QueryBuilder/QueryObject.php @@ -77,7 +77,7 @@ public function getSql(): string * * @return string The interpolated query */ - protected function interpolateQuery($query, $params) + protected function interpolateQuery($query, $params): string { $keys = []; $values = $params; From bad86cd75f3b613445cbb026ba5ce6f8a880e5ea Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Marcin=20Pude=C5=82ek?= Date: Mon, 4 Dec 2017 00:13:34 +0100 Subject: [PATCH 04/11] composer lock --- composer.lock | 18 +++++++++--------- 1 file changed, 9 insertions(+), 9 deletions(-) diff --git a/composer.lock b/composer.lock index d03cd81..180ded1 100644 --- a/composer.lock +++ b/composer.lock @@ -57,32 +57,32 @@ "packages-dev": [ { "name": "doctrine/instantiator", - "version": "1.1.0", + "version": "1.0.5", "source": { "type": "git", "url": "https://github.com/doctrine/instantiator.git", - "reference": "185b8868aa9bf7159f5f953ed5afb2d7fcdc3bda" + "reference": "8e884e78f9f0eb1329e445619e04456e64d8051d" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/doctrine/instantiator/zipball/185b8868aa9bf7159f5f953ed5afb2d7fcdc3bda", - "reference": "185b8868aa9bf7159f5f953ed5afb2d7fcdc3bda", + "url": "https://api.github.com/repos/doctrine/instantiator/zipball/8e884e78f9f0eb1329e445619e04456e64d8051d", + "reference": "8e884e78f9f0eb1329e445619e04456e64d8051d", "shasum": "" }, "require": { - "php": "^7.1" + "php": ">=5.3,<8.0-DEV" }, "require-dev": { "athletic/athletic": "~0.1.8", "ext-pdo": "*", "ext-phar": "*", - "phpunit/phpunit": "^6.2.3", - "squizlabs/php_codesniffer": "^3.0.2" + "phpunit/phpunit": "~4.0", + "squizlabs/php_codesniffer": "~2.0" }, "type": "library", "extra": { "branch-alias": { - "dev-master": "1.2.x-dev" + "dev-master": "1.0.x-dev" } }, "autoload": { @@ -107,7 +107,7 @@ "constructor", "instantiate" ], - "time": "2017-07-22T11:58:36+00:00" + "time": "2015-06-14T21:17:01+00:00" }, { "name": "hamcrest/hamcrest-php", From c8e4b50c3fcf4a645e347aaba089d6e08ad92ded Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Marcin=20Pude=C5=82ek?= Date: Mon, 4 Dec 2017 00:19:22 +0100 Subject: [PATCH 05/11] bad returnet type of wrapSanitizer() --- src/Pecee/Pixie/QueryBuilder/Adapters/BaseAdapter.php | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/Pecee/Pixie/QueryBuilder/Adapters/BaseAdapter.php b/src/Pecee/Pixie/QueryBuilder/Adapters/BaseAdapter.php index d335e61..c405bb0 100644 --- a/src/Pecee/Pixie/QueryBuilder/Adapters/BaseAdapter.php +++ b/src/Pecee/Pixie/QueryBuilder/Adapters/BaseAdapter.php @@ -580,9 +580,9 @@ public function update(array $statements, array $data): array * * @param string|Raw|\Closure $value * - * @return string + * @return string|\Closure */ - public function wrapSanitizer($value): string + public function wrapSanitizer($value) { // Its a raw query, just cast as string, object has __toString() if ($value instanceof Raw) { From 1135d7a32a10a4ec605c98422a6ec0514982486a Mon Sep 17 00:00:00 2001 From: Simon Sessingo Date: Mon, 4 Dec 2017 01:35:31 +0100 Subject: [PATCH 06/11] Development - Added IQueryBuilderHandler interface for more implementations of return-types. - Added more return-types to methods. - Fixed return-type for some phpDoc blocks. - Changed batch-insert so it uses PDO transactions. --- .../QueryBuilder/IQueryBuilderHandler.php | 7 ++ .../Pixie/QueryBuilder/NestedCriteria.php | 2 +- .../QueryBuilder/QueryBuilderHandler.php | 110 ++++++++++-------- src/Pecee/Pixie/QueryBuilder/Raw.php | 2 +- src/Pecee/Pixie/QueryBuilder/Transaction.php | 6 +- 5 files changed, 73 insertions(+), 54 deletions(-) create mode 100644 src/Pecee/Pixie/QueryBuilder/IQueryBuilderHandler.php diff --git a/src/Pecee/Pixie/QueryBuilder/IQueryBuilderHandler.php b/src/Pecee/Pixie/QueryBuilder/IQueryBuilderHandler.php new file mode 100644 index 0000000..4441a67 --- /dev/null +++ b/src/Pecee/Pixie/QueryBuilder/IQueryBuilderHandler.php @@ -0,0 +1,7 @@ +addTablePrefix($key); $this->statements['criteria'][] = compact('key', 'operator', 'value', 'joiner'); diff --git a/src/Pecee/Pixie/QueryBuilder/QueryBuilderHandler.php b/src/Pecee/Pixie/QueryBuilder/QueryBuilderHandler.php index b538b0e..0969fde 100644 --- a/src/Pecee/Pixie/QueryBuilder/QueryBuilderHandler.php +++ b/src/Pecee/Pixie/QueryBuilder/QueryBuilderHandler.php @@ -11,7 +11,7 @@ * * @package Pecee\Pixie\QueryBuilder */ -class QueryBuilderHandler +class QueryBuilderHandler implements IQueryBuilderHandler { /** * Event name @@ -152,6 +152,7 @@ public function __construct(Connection $connection = null) * * @param string $key * @param mixed $value + * @return void */ protected function addStatement(string $key, $value) { @@ -259,7 +260,7 @@ protected function aggregate(string $type): int * * @return static */ - public function alias(string $alias, string $table = null) + public function alias(string $alias, string $table = null): IQueryBuilderHandler { if ($table === null && isset($this->statements['tables'][0]) === true) { $table = $this->statements['tables'][0]; @@ -344,27 +345,38 @@ private function doInsert(array $data, string $type) * @var $executionTime float */ list($result, $executionTime) = $this->statement($queryObject->getSql(), $queryObject->getBindings()); - $return = $result->rowCount() === 1 ? $this->pdo->lastInsertId() : null; - $this->fireEvents(static::EVENT_AFTER_INSERT, $queryObject, $return, $executionTime); + $insertId = $result->rowCount() === 1 ? $this->pdo->lastInsertId() : null; + $this->fireEvents(static::EVENT_AFTER_INSERT, $queryObject, $insertId, $executionTime); - return $return; + return $insertId; } // Perform batch insert + $insertIds = []; - $return = []; foreach ($data as $subData) { - $queryObject = $this->getQuery($type, $subData); - $this->fireEvents(static::EVENT_BEFORE_INSERT, $queryObject); - list($result, $executionTime) = $this->statement($queryObject->getSql(), $queryObject->getBindings()); - $result = $result->rowCount() === 1 ? $this->pdo->lastInsertId() : null; - $this->fireEvents(static::EVENT_AFTER_INSERT, $queryObject, $result, $executionTime); + $this->transaction(function (Transaction $qb) use(&$insertIds, $type, $subData) { + + $queryObject = $qb->getQuery($type, $subData); + + $this->fireEvents(static::EVENT_BEFORE_INSERT, $queryObject); + + /** + * @var $result \PDOStatement + * @var $executionTime float + */ + list($result, $executionTime) = $qb->statement($queryObject->getSql(), $queryObject->getBindings()); + $insertId = $result->rowCount() === 1 ? $qb->pdo->lastInsertId() : null; + $insertIds[] = $insertId; + + $this->fireEvents(static::EVENT_AFTER_INSERT, $queryObject, $result, $executionTime); + + }); - $return[] = $result; } - return $return; + return $insertIds; } /** @@ -374,7 +386,7 @@ private function doInsert(array $data, string $type) * @param string $fieldName * * @throws Exception - * @return null|\stdClass + * @return \stdClass|string|null */ public function find($value, $fieldName = 'id') { @@ -415,7 +427,7 @@ public function fireEvents($name, $parameters = null) * Returns the first row * * @throws Exception - * @return \stdClass|null + * @return \stdClass|string|null */ public function first() { @@ -431,7 +443,7 @@ public function first() * * @return static */ - public function from($tables) + public function from($tables): IQueryBuilderHandler { if (\is_array($tables) === false) { $tables = \func_get_args(); @@ -551,7 +563,7 @@ public function getStatements(): array * * @return static */ - public function groupBy($field) + public function groupBy($field): IQueryBuilderHandler { if (($field instanceof Raw) === false) { $field = $this->addTablePrefix($field); @@ -576,7 +588,7 @@ public function groupBy($field) * * @return static */ - public function having($key, $operator, $value, $joiner = 'AND') + public function having($key, $operator, $value, $joiner = 'AND'): IQueryBuilderHandler { $key = $this->addTablePrefix($key); $this->statements['havings'][] = compact('key', 'operator', 'value', 'joiner'); @@ -594,7 +606,7 @@ public function having($key, $operator, $value, $joiner = 'AND') * * @return static */ - public function innerJoin($table, $key, $operator = null, $value = null) + public function innerJoin($table, $key, $operator = null, $value = null): IQueryBuilderHandler { return $this->join($table, $key, $operator, $value); } @@ -652,7 +664,7 @@ public function insertIgnore($data) * }) * ``` */ - public function join($table, $key, $operator = null, $value = null, $type = 'inner') + public function join($table, $key, $operator = null, $value = null, $type = 'inner'): IQueryBuilderHandler { if (($key instanceof \Closure) === false) { $key = function (JoinBuilder $joinBuilder) use ($key, $operator, $value) { @@ -687,7 +699,7 @@ public function join($table, $key, $operator = null, $value = null, $type = 'inn * * @return static */ - public function leftJoin($table, $key, $operator = null, $value = null) + public function leftJoin($table, $key, $operator = null, $value = null): IQueryBuilderHandler { return $this->join($table, $key, $operator, $value, 'left'); } @@ -699,7 +711,7 @@ public function leftJoin($table, $key, $operator = null, $value = null) * * @return static */ - public function limit($limit) + public function limit($limit): IQueryBuilderHandler { $this->statements['limit'] = $limit; @@ -714,7 +726,7 @@ public function limit($limit) * @throws \Pecee\Pixie\Exception * @return static */ - public function newQuery(Connection $connection = null) + public function newQuery(Connection $connection = null): IQueryBuilderHandler { if ($connection === null) { $connection = $this->connection; @@ -730,7 +742,7 @@ public function newQuery(Connection $connection = null) * * @return static $this */ - public function offset($offset) + public function offset($offset): IQueryBuilderHandler { $this->statements['offset'] = $offset; @@ -744,7 +756,7 @@ public function offset($offset) * * @return static */ - public function onDuplicateKeyUpdate($data) + public function onDuplicateKeyUpdate($data): IQueryBuilderHandler { $this->addStatement('onduplicate', $data); @@ -760,7 +772,7 @@ public function onDuplicateKeyUpdate($data) * * @return static */ - public function orHaving($key, $operator, $value) + public function orHaving($key, $operator, $value): IQueryBuilderHandler { return $this->having($key, $operator, $value, 'OR'); } @@ -774,7 +786,7 @@ public function orHaving($key, $operator, $value) * * @return static */ - public function orWhere($key, $operator = null, $value = null) + public function orWhere($key, $operator = null, $value = null): IQueryBuilderHandler { // If two params are given then assume operator is = if (\func_num_args() === 2) { @@ -794,7 +806,7 @@ public function orWhere($key, $operator = null, $value = null) * * @return static */ - public function orWhereBetween($key, $valueFrom, $valueTo) + public function orWhereBetween($key, $valueFrom, $valueTo): IQueryBuilderHandler { return $this->whereHandler($key, 'BETWEEN', [$valueFrom, $valueTo], 'OR'); } @@ -807,7 +819,7 @@ public function orWhereBetween($key, $valueFrom, $valueTo) * * @return static */ - public function orWhereIn($key, $values) + public function orWhereIn($key, $values): IQueryBuilderHandler { return $this->whereHandler($key, 'IN', $values, 'OR'); } @@ -821,7 +833,7 @@ public function orWhereIn($key, $values) * * @return static */ - public function orWhereNot($key, $operator = null, $value = null) + public function orWhereNot($key, $operator = null, $value = null): IQueryBuilderHandler { // If two params are given then assume operator is = if (\func_num_args() === 2) { @@ -840,7 +852,7 @@ public function orWhereNot($key, $operator = null, $value = null) * * @return static */ - public function orWhereNotIn($key, $values) + public function orWhereNotIn($key, $values): IQueryBuilderHandler { return $this->whereHandler($key, 'NOT IN', $values, 'OR'); } @@ -852,7 +864,7 @@ public function orWhereNotIn($key, $values) * * @return static */ - public function orWhereNotNull($key) + public function orWhereNotNull($key): IQueryBuilderHandler { return $this->whereNullHandler($key, 'NOT', 'or'); } @@ -864,7 +876,7 @@ public function orWhereNotNull($key) * * @return static */ - public function orWhereNull($key) + public function orWhereNull($key): IQueryBuilderHandler { return $this->whereNullHandler($key, '', 'or'); } @@ -877,7 +889,7 @@ public function orWhereNull($key) * * @return static */ - public function orderBy($fields, $defaultDirection = 'ASC') + public function orderBy($fields, $defaultDirection = 'ASC'): IQueryBuilderHandler { if (\is_array($fields) === false) { $fields = [$fields]; @@ -920,7 +932,7 @@ public function pdo(): PDO * * @return static */ - public function query($sql, array $bindings = []) + public function query($sql, array $bindings = []): IQueryBuilderHandler { list($this->pdoStatement) = $this->statement($sql, $bindings); @@ -999,7 +1011,7 @@ public function replace($data) * * @return static */ - public function rightJoin($table, $key, $operator = null, $value = null) + public function rightJoin($table, $key, $operator = null, $value = null): IQueryBuilderHandler { return $this->join($table, $key, $operator, $value, 'right'); } @@ -1015,7 +1027,7 @@ public function rightJoin($table, $key, $operator = null, $value = null) * * @return static */ - public function select($fields) + public function select($fields): IQueryBuilderHandler { if (\is_array($fields) === false) { $fields = \func_get_args(); @@ -1165,7 +1177,7 @@ public function table($tables) * * @param \Closure $callback * - * @throws \Exception + * @throws Exception * @return static */ public function transaction(\Closure $callback) @@ -1194,7 +1206,7 @@ public function transaction(\Closure $callback) // something happened, rollback changes and throw Exception $queryTransaction->rollBack($inTransaction); - throw $e; + throw new Exception($e->getMessage(), $e->getCode(), $e->getPrevious()); } } @@ -1247,7 +1259,7 @@ public function updateOrInsert($data) * * @return static */ - public function where($key, $operator = null, $value = null) + public function where($key, $operator = null, $value = null): IQueryBuilderHandler { // If two params are given then assume operator is = if (\func_num_args() === 2) { @@ -1271,7 +1283,7 @@ public function where($key, $operator = null, $value = null) * * @return static */ - public function whereBetween($key, $valueFrom, $valueTo) + public function whereBetween($key, $valueFrom, $valueTo): IQueryBuilderHandler { return $this->whereHandler($key, 'BETWEEN', [$valueFrom, $valueTo]); } @@ -1286,7 +1298,7 @@ public function whereBetween($key, $valueFrom, $valueTo) * * @return static */ - protected function whereHandler($key, string $operator = null, $value = null, $joiner = 'AND') + protected function whereHandler($key, string $operator = null, $value = null, $joiner = 'AND'): IQueryBuilderHandler { $key = $this->addTablePrefix($key); $this->statements['wheres'][] = compact('key', 'operator', 'value', 'joiner'); @@ -1302,7 +1314,7 @@ protected function whereHandler($key, string $operator = null, $value = null, $j * * @return static */ - public function whereIn($key, $values) + public function whereIn($key, $values): IQueryBuilderHandler { return $this->whereHandler($key, 'IN', $values); } @@ -1316,7 +1328,7 @@ public function whereIn($key, $values) * * @return static */ - public function whereNot($key, $operator = null, $value = null) + public function whereNot($key, $operator = null, $value = null): IQueryBuilderHandler { // If two params are given then assume operator is = if (\func_num_args() === 2) { @@ -1335,7 +1347,7 @@ public function whereNot($key, $operator = null, $value = null) * * @return static */ - public function whereNotIn($key, $values) + public function whereNotIn($key, $values): IQueryBuilderHandler { return $this->whereHandler($key, 'NOT IN', $values); } @@ -1347,7 +1359,7 @@ public function whereNotIn($key, $values) * * @return static */ - public function whereNotNull($key) + public function whereNotNull($key): IQueryBuilderHandler { return $this->whereNullHandler($key, 'NOT'); } @@ -1359,7 +1371,7 @@ public function whereNotNull($key) * * @return static */ - public function whereNull($key) + public function whereNull($key): IQueryBuilderHandler { return $this->whereNullHandler($key); } @@ -1371,9 +1383,9 @@ public function whereNull($key) * @param string $prefix * @param string $operator * - * @return mixed + * @return static */ - protected function whereNullHandler($key, $prefix = '', $operator = '') + protected function whereNullHandler($key, $prefix = '', $operator = ''): IQueryBuilderHandler { $key = $this->adapterInstance->wrapSanitizer($this->addTablePrefix($key)); $prefix = ($prefix !== '') ? $prefix . ' ' : $prefix; diff --git a/src/Pecee/Pixie/QueryBuilder/Raw.php b/src/Pecee/Pixie/QueryBuilder/Raw.php index 20a3803..725aed4 100644 --- a/src/Pecee/Pixie/QueryBuilder/Raw.php +++ b/src/Pecee/Pixie/QueryBuilder/Raw.php @@ -35,7 +35,7 @@ public function __construct(string $value, array $bindings = []) /** * @return string */ - public function __toString() + public function __toString(): string { return (string)$this->value; } diff --git a/src/Pecee/Pixie/QueryBuilder/Transaction.php b/src/Pecee/Pixie/QueryBuilder/Transaction.php index d87332c..35951ad 100644 --- a/src/Pecee/Pixie/QueryBuilder/Transaction.php +++ b/src/Pecee/Pixie/QueryBuilder/Transaction.php @@ -26,7 +26,7 @@ public function inTransaction(): bool * * @return $this */ - public function begin(bool $inTransaction = false) + public function begin(bool $inTransaction = false): IQueryBuilderHandler { if (false === $inTransaction) { $this->pdo()->beginTransaction(); @@ -42,7 +42,7 @@ public function begin(bool $inTransaction = false) * * @return $this */ - public function commit(bool $inTransaction = false) + public function commit(bool $inTransaction = false): IQueryBuilderHandler { if (false === $inTransaction) { $this->pdo()->commit(); @@ -58,7 +58,7 @@ public function commit(bool $inTransaction = false) * * @return $this */ - public function rollBack(bool $inTransaction = false) + public function rollBack(bool $inTransaction = false): IQueryBuilderHandler { if (false === $inTransaction) { $this->pdo()->rollBack(); From 0e5764cf87117d960fd8935f478d5126fdef9415 Mon Sep 17 00:00:00 2001 From: Simon Sessingo Date: Tue, 5 Dec 2017 07:05:25 +0100 Subject: [PATCH 07/11] [WIP!!!] Development - Fixed issue where bindings with null-value was removed (issue: #35). - Added unit tests for bindings with null-value (issue: #35) - Transactions are now fully working. - Added unit-tests for Transactions. - Changed PDO-parameter behavior so it automaticially parses correct parameter-type. - Began implementing Transactions for batch-insert, update etc (issue: #31). --- src/Pecee/Pixie/Connection.php | 5 +- .../QueryBuilder/Adapters/BaseAdapter.php | 60 +++-- .../QueryBuilder/QueryBuilderHandler.php | 206 +++++++++--------- src/Pecee/Pixie/QueryBuilder/Transaction.php | 60 ++--- .../QueryBuilder/TransactionHaltException.php | 7 + .../Pecee/Pixie/QueryBuilderBehaviorTest.php | 1 + tests/Pecee/Pixie/QueryBuilderTest.php | 14 +- tests/Pecee/Pixie/TransactionTest.php | 78 +++++++ 8 files changed, 251 insertions(+), 180 deletions(-) create mode 100644 src/Pecee/Pixie/QueryBuilder/TransactionHaltException.php create mode 100644 tests/Pecee/Pixie/TransactionTest.php diff --git a/src/Pecee/Pixie/Connection.php b/src/Pecee/Pixie/Connection.php index c3df741..ea55f46 100644 --- a/src/Pecee/Pixie/Connection.php +++ b/src/Pecee/Pixie/Connection.php @@ -52,8 +52,7 @@ public function __construct(string $adapter, array $adapterConfig, Container $co $this ->setAdapter($adapter) ->setAdapterConfig($adapterConfig) - ->connect() - ; + ->connect(); // Create event dependency $this->eventHandler = $this->container->build(EventHandler::class); @@ -70,7 +69,7 @@ public static function getStoredConnection(): Connection /** * Create the connection adapter */ - protected function connect() + public function connect() { // Build a database connection if we don't have one connected diff --git a/src/Pecee/Pixie/QueryBuilder/Adapters/BaseAdapter.php b/src/Pecee/Pixie/QueryBuilder/Adapters/BaseAdapter.php index c405bb0..e5dc300 100644 --- a/src/Pecee/Pixie/QueryBuilder/Adapters/BaseAdapter.php +++ b/src/Pecee/Pixie/QueryBuilder/Adapters/BaseAdapter.php @@ -37,16 +37,16 @@ abstract class BaseAdapter public function __construct(Connection $connection) { $this->connection = $connection; - $this->container = $this->connection->getContainer(); + $this->container = $this->connection->getContainer(); } /** * Array concatenating method, like implode. * But it does wrap sanitizer and trims last glue * - * @param array $pieces + * @param array $pieces * @param string $glue - * @param bool $wrapSanitizer + * @param bool $wrapSanitizer * * @return string */ @@ -72,7 +72,7 @@ protected function arrayStr(array $pieces, $glue = ',', $wrapSanitizer = true): * Build generic criteria string and bindings from statements, like "a = b and c = ?" * * @param array $statements - * @param bool $bindValues + * @param bool $bindValues * * @throws Exception * @return array @@ -84,7 +84,7 @@ protected function buildCriteria(array $statements, $bindValues = true): array foreach ($statements as $statement) { - $key = $this->wrapSanitizer($statement['key']); + $key = $this->wrapSanitizer($statement['key']); $value = $statement['value']; if ($value === null && $key instanceof \Closure) { @@ -121,17 +121,17 @@ protected function buildCriteria(array $statements, $bindValues = true): array $criteria .= $statement['joiner'] . ' ' . $key . ' ' . $statement['operator']; if ($statement['operator'] === 'BETWEEN') { - $bindings[] = (array)$statement['value']; - $criteria .= ' ? AND ? '; + $bindings[] = [$statement['value']]; + $criteria .= ' ? AND ? '; } else { $valuePlaceholder = ''; foreach ((array)$statement['value'] as $subValue) { $valuePlaceholder .= '?, '; - $bindings[] = (array)$subValue; + $bindings[] = [$subValue]; } $valuePlaceholder = trim($valuePlaceholder, ', '); - $criteria .= ' (' . $valuePlaceholder . ') '; + $criteria .= ' (' . $valuePlaceholder . ') '; } continue; @@ -148,7 +148,7 @@ protected function buildCriteria(array $statements, $bindValues = true): array if ($bindValues === false) { // Specially for joins - we are not binding values, lets sanitize then - $value = $this->wrapSanitizer($value); + $value = $this->wrapSanitizer($value); $criteria .= $statement['joiner'] . ' ' . $key . ' ' . $statement['operator'] . ' ' . $value . ' '; continue; @@ -157,11 +157,11 @@ protected function buildCriteria(array $statements, $bindValues = true): array if ($statement['key'] instanceof Raw) { if ($statement['operator'] !== null) { - $criteria .= "{$statement['joiner']} {$key} {$statement['operator']} ? "; + $criteria .= "{$statement['joiner']} {$key} {$statement['operator']} ? "; $bindings[] = $statement['key']->getBindings(); - $bindings[] = (array)$value; + $bindings[] = [$value]; } else { - $criteria .= $statement['joiner'] . ' ' . $key . ' '; + $criteria .= $statement['joiner'] . ' ' . $key . ' '; $bindings[] = $statement['key']->getBindings(); } @@ -171,25 +171,23 @@ protected function buildCriteria(array $statements, $bindValues = true): array // WHERE $valuePlaceholder = '?'; - $bindings[] = [$value]; - $criteria .= $statement['joiner'] . ' ' . $key . ' ' . $statement['operator'] . ' ' . $valuePlaceholder . ' '; + $bindings[] = [$value]; + $criteria .= $statement['joiner'] . ' ' . $key . ' ' . $statement['operator'] . ' ' . $valuePlaceholder . ' '; } - $bindings = \array_merge(...$bindings); - // Clear all white spaces, and, or from beginning and white spaces from ending $criteria = \preg_replace('/^(\s?AND ?|\s?OR ?)|\s$/i', '', $criteria); - return [$criteria, $bindings]; + return [$criteria, array_merge(...$bindings)]; } /** * Build criteria string and binding with various types added, like WHERE and Having * - * @param array $statements + * @param array $statements * @param string $key * @param string $type - * @param bool $bindValues + * @param bool $bindValues * * @return array * @throws Exception @@ -274,7 +272,7 @@ protected function concatenateQuery(array $pieces): string * Build just criteria part of the query * * @param array $statements - * @param bool $bindValues + * @param bool $bindValues * * @return array * @throws Exception @@ -307,7 +305,7 @@ public function delete(array $statements): array list($whereCriteria, $whereBindings) = $this->buildCriteriaWithType($statements, 'wheres', 'WHERE'); $sqlArray = ['DELETE FROM', $this->wrapSanitizer($table), $whereCriteria]; - $sql = $this->concatenateQuery($sqlArray); + $sql = $this->concatenateQuery($sqlArray); $bindings = $whereBindings; return compact('sql', 'bindings'); @@ -316,8 +314,8 @@ public function delete(array $statements): array /** * Build a generic insert/ignore/replace query * - * @param array $statements - * @param array $data + * @param array $statements + * @param array $data * @param string $type * * @return array @@ -334,7 +332,7 @@ private function doInsert(array $statements, array $data, $type): array if ($value instanceof Raw) { $values[] = (string)$value; } else { - $values[] = '?'; + $values[] = '?'; $bindings[] = $value; } } @@ -355,7 +353,7 @@ private function doInsert(array $statements, array $data, $type): array list($updateStatement, $updateBindings) = $this->getUpdateStatement($statements['onduplicate']); $sqlArray[] = 'ON DUPLICATE KEY UPDATE ' . $updateStatement; - $bindings = array_merge($bindings, $updateBindings); + $bindings = array_merge($bindings, $updateBindings); } @@ -373,7 +371,7 @@ private function doInsert(array $statements, array $data, $type): array */ private function getUpdateStatement(array $data): array { - $bindings = []; + $bindings = []; $statement = ''; foreach ($data as $key => $value) { @@ -383,7 +381,7 @@ private function getUpdateStatement(array $data): array if ($value instanceof Raw) { $statement .= $value . ','; } else { - $statement .= '?,'; + $statement .= '?,'; $bindings[] = $value; } } @@ -451,7 +449,7 @@ public function select(array $statements): array // From $fromEnabled = false; - $tables = ''; + $tables = ''; if (isset($statements['tables']) === true) { $tables = []; @@ -472,7 +470,7 @@ public function select(array $statements): array $tables[] = $t; } - $tables = implode(',', $tables); + $tables = implode(',', $tables); $fromEnabled = true; } @@ -501,7 +499,7 @@ public function select(array $statements): array } // LIMIT AND OFFSET - $limit = isset($statements['limit']) ? 'LIMIT ' . $statements['limit'] : ''; + $limit = isset($statements['limit']) ? 'LIMIT ' . $statements['limit'] : ''; $offset = isset($statements['offset']) ? 'OFFSET ' . $statements['offset'] : ''; // HAVING diff --git a/src/Pecee/Pixie/QueryBuilder/QueryBuilderHandler.php b/src/Pecee/Pixie/QueryBuilder/QueryBuilderHandler.php index 0969fde..2932fe9 100644 --- a/src/Pecee/Pixie/QueryBuilder/QueryBuilderHandler.php +++ b/src/Pecee/Pixie/QueryBuilder/QueryBuilderHandler.php @@ -129,9 +129,9 @@ public function __construct(Connection $connection = null) throw new Exception('No database connection found.', 1); } - $this->container = $this->connection->getContainer(); - $this->pdo = $this->connection->getPdoInstance(); - $this->adapter = $this->connection->getAdapter(); + $this->container = $this->connection->getContainer(); + $this->pdo = $this->connection->getPdoInstance(); + $this->adapter = $this->connection->getAdapter(); $this->adapterConfig = $this->connection->getAdapterConfig(); if (isset($this->adapterConfig['prefix']) === true) { @@ -145,13 +145,16 @@ public function __construct(Connection $connection = null) ); $this->pdo->setAttribute(PDO::ATTR_ERRMODE, PDO::ERRMODE_EXCEPTION); + + // PDO will parse parameter datatypes automaticially + $this->pdo->setAttribute(PDO::ATTR_EMULATE_PREPARES, true); } /** * Add new statement to statement-list * * @param string $key - * @param mixed $value + * @param mixed $value * @return void */ protected function addStatement(string $key, $value) @@ -167,7 +170,7 @@ protected function addStatement(string $key, $value) * Add table prefix (if given) on given string. * * @param string|array|Raw|\Closure $values - * @param bool $tableFieldMix If we have mixes of field and table names with a "." + * @param bool $tableFieldMix If we have mixes of field and table names with a "." * * @return array|string */ @@ -230,7 +233,7 @@ protected function aggregate(string $type): int // Replace select with a scalar value like `count` $this->statements['selects'] = [$this->raw($type . '(*) AS `field`')]; - $row = $this->get(); + $row = $this->get(); // Set the select as it was if ($mainSelects !== null) { @@ -277,7 +280,7 @@ public function alias(string $alias, string $table = null): IQueryBuilderHandler * Fetch query results as object of specified type * * @param string $className - * @param array $constructorArgs + * @param array $constructorArgs * * @return static */ @@ -299,7 +302,7 @@ public function count(): int unset($this->statements['orderBys'], $this->statements['limit'], $this->statements['offset']); - $count = $this->aggregate('count'); + $count = $this->aggregate('count'); $this->statements = $originalStatements; return $count; @@ -327,7 +330,7 @@ public function delete(): \PDOStatement /** * Performs insert * - * @param array $data + * @param array $data * @param string $type * * @throws Exception @@ -335,8 +338,9 @@ public function delete(): \PDOStatement */ private function doInsert(array $data, string $type) { - // If first value is not an array - it's not a batch insert - if (\is_array(current($data)) === false) { + // Helper function + $doInsert = function ($data) use ($type) { + $queryObject = $this->getQuery($type, $data); $this->fireEvents(static::EVENT_BEFORE_INSERT, $queryObject); @@ -345,35 +349,24 @@ private function doInsert(array $data, string $type) * @var $executionTime float */ list($result, $executionTime) = $this->statement($queryObject->getSql(), $queryObject->getBindings()); + $insertId = $result->rowCount() === 1 ? $this->pdo->lastInsertId() : null; $this->fireEvents(static::EVENT_AFTER_INSERT, $queryObject, $insertId, $executionTime); return $insertId; + }; + + // If first value is not an array - it's not a batch insert + if (\is_array(current($data)) === false) { + return $doInsert($data); } // Perform batch insert $insertIds = []; + // Otherwise insert one by one... foreach ($data as $subData) { - - $this->transaction(function (Transaction $qb) use(&$insertIds, $type, $subData) { - - $queryObject = $qb->getQuery($type, $subData); - - $this->fireEvents(static::EVENT_BEFORE_INSERT, $queryObject); - - /** - * @var $result \PDOStatement - * @var $executionTime float - */ - list($result, $executionTime) = $qb->statement($queryObject->getSql(), $queryObject->getBindings()); - $insertId = $result->rowCount() === 1 ? $qb->pdo->lastInsertId() : null; - $insertIds[] = $insertId; - - $this->fireEvents(static::EVENT_AFTER_INSERT, $queryObject, $result, $executionTime); - - }); - + $insertIds[] = $doInsert($subData); } return $insertIds; @@ -383,7 +376,7 @@ private function doInsert(array $data, string $type) * Find by value and field name. * * @param string|int|float $value - * @param string $fieldName + * @param string $fieldName * * @throws Exception * @return \stdClass|string|null @@ -396,7 +389,7 @@ public function find($value, $fieldName = 'id') /** * Find all by field name and value * - * @param string $fieldName + * @param string $fieldName * @param string|int|float $value * * @throws Exception @@ -469,7 +462,7 @@ public function get(): array * @var $start float * @var $result array */ - $queryObject = null; + $queryObject = null; $executionTime = 0; if ($this->pdoStatement === null) { @@ -482,8 +475,8 @@ public function get(): array $start = microtime(true); $this->fireEvents(static::EVENT_BEFORE_SELECT, $queryObject); - $result = \call_user_func_array([$this->pdoStatement, 'fetchAll'], $this->fetchParameters); - $executionTime += microtime(true) - $start; + $result = \call_user_func_array([$this->pdoStatement, 'fetchAll'], $this->fetchParameters); + $executionTime += microtime(true) - $start; $this->pdoStatement = null; $this->fireEvents(static::EVENT_AFTER_SELECT, $queryObject, $result, $executionTime); @@ -503,7 +496,7 @@ public function getConnection(): Connection /** * Get event by event name * - * @param string $name + * @param string $name * @param string|null $table * * @return \Closure|null @@ -516,7 +509,7 @@ public function getEvent(string $name, string $table = null) /** * Returns Query-object. * - * @param string $type + * @param string $type * @param array|mixed|null $dataToBePassed * * @return QueryObject @@ -582,15 +575,15 @@ public function groupBy($field): IQueryBuilderHandler * Adds HAVING statement to the current query. * * @param string|Raw|\Closure $key - * @param string|mixed $operator - * @param string|mixed $value - * @param string $joiner + * @param string|mixed $operator + * @param string|mixed $value + * @param string $joiner * * @return static */ public function having($key, $operator, $value, $joiner = 'AND'): IQueryBuilderHandler { - $key = $this->addTablePrefix($key); + $key = $this->addTablePrefix($key); $this->statements['havings'][] = compact('key', 'operator', 'value', 'joiner'); return $this; @@ -599,9 +592,9 @@ public function having($key, $operator, $value, $joiner = 'AND'): IQueryBuilderH /** * Adds new INNER JOIN statement to the current query. * - * @param string|Raw|\Closure $table - * @param string|Raw|\Closure $key - * @param string|mixed|null $operator + * @param string|Raw|\Closure $table + * @param string|Raw|\Closure $key + * @param string|mixed|null $operator * @param string|Raw|\Closure|null $value * * @return static @@ -641,10 +634,10 @@ public function insertIgnore($data) * Adds new JOIN statement to the current query. * * @param string|Raw|\Closure|array $table - * @param string|Raw|\Closure $key - * @param string|null $operator - * @param string|Raw|\Closure $value - * @param string $type + * @param string|Raw|\Closure $key + * @param string|null $operator + * @param string|Raw|\Closure $value + * @param string $type * * @return static * ``` @@ -693,9 +686,9 @@ public function join($table, $key, $operator = null, $value = null, $type = 'inn * Adds new LEFT JOIN statement to the current query. * * @param string|Raw|\Closure|array $table - * @param string|Raw|\Closure $key - * @param string|null $operator - * @param string|Raw|\Closure|null $value + * @param string|Raw|\Closure $key + * @param string|null $operator + * @param string|Raw|\Closure|null $value * * @return static */ @@ -766,8 +759,8 @@ public function onDuplicateKeyUpdate($data): IQueryBuilderHandler /** * Adds OR HAVING statement to the current query. * - * @param string|Raw|\Closure $key - * @param string|Raw|\Closure $operator + * @param string|Raw|\Closure $key + * @param string|Raw|\Closure $operator * @param mixed|Raw|\Closure|null $value * * @return static @@ -780,8 +773,8 @@ public function orHaving($key, $operator, $value): IQueryBuilderHandler /** * Adds OR WHERE statement to the current query. * - * @param string|Raw|\Closure $key - * @param string|null $operator + * @param string|Raw|\Closure $key + * @param string|null $operator * @param mixed|Raw|\Closure|null $value * * @return static @@ -790,7 +783,7 @@ public function orWhere($key, $operator = null, $value = null): IQueryBuilderHan { // If two params are given then assume operator is = if (\func_num_args() === 2) { - $value = $operator; + $value = $operator; $operator = '='; } @@ -800,7 +793,7 @@ public function orWhere($key, $operator = null, $value = null): IQueryBuilderHan /** * Adds OR WHERE BETWEEN statement to the current query. * - * @param string|Raw|\Closure $key + * @param string|Raw|\Closure $key * @param string|integer|float $valueFrom * @param string|integer|float $valueTo * @@ -815,7 +808,7 @@ public function orWhereBetween($key, $valueFrom, $valueTo): IQueryBuilderHandler * Adds OR WHERE IN statement to the current query. * * @param string|Raw|\Closure $key - * @param array|Raw|\Closure $values + * @param array|Raw|\Closure $values * * @return static */ @@ -827,8 +820,8 @@ public function orWhereIn($key, $values): IQueryBuilderHandler /** * Adds OR WHERE NOT statement to the current query. * - * @param string|Raw|\Closure $key - * @param string|null $operator + * @param string|Raw|\Closure $key + * @param string|null $operator * @param mixed|Raw|\Closure|null $value * * @return static @@ -837,7 +830,7 @@ public function orWhereNot($key, $operator = null, $value = null): IQueryBuilder { // If two params are given then assume operator is = if (\func_num_args() === 2) { - $value = $operator; + $value = $operator; $operator = '='; } @@ -848,7 +841,7 @@ public function orWhereNot($key, $operator = null, $value = null): IQueryBuilder * Adds or WHERE NOT IN statement to the current query. * * @param string|Raw|\Closure $key - * @param array|Raw|\Closure $values + * @param array|Raw|\Closure $values * * @return static */ @@ -885,7 +878,7 @@ public function orWhereNull($key): IQueryBuilderHandler * Adds ORDER BY statement to the current query. * * @param string|Raw|\Closure|array $fields - * @param string $defaultDirection + * @param string $defaultDirection * * @return static */ @@ -897,11 +890,11 @@ public function orderBy($fields, $defaultDirection = 'ASC'): IQueryBuilderHandle foreach ((array)$fields as $key => $value) { $field = $key; - $type = $value; + $type = $value; if (\is_int($key) === true) { $field = $value; - $type = $defaultDirection; + $type = $defaultDirection; } if (($field instanceof Raw) === false) { @@ -928,7 +921,7 @@ public function pdo(): PDO * Performs query. * * @param string $sql - * @param array $bindings + * @param array $bindings * * @return static */ @@ -946,7 +939,7 @@ public function query($sql, array $bindings = []): IQueryBuilderHandler * * For example: $qb->where('result', '>', $qb->raw('COUNT(`score`))); * - * @param string $value + * @param string $value * @param array|null|mixed $bindings ... * * @return Raw @@ -964,9 +957,9 @@ public function raw($value, $bindings = null): Raw /** * Register new event * - * @param string $name + * @param string $name * @param string|null $table - * @param \Closure $action + * @param \Closure $action * * @return void */ @@ -978,7 +971,7 @@ public function registerEvent($name, $table = null, \Closure $action) /** * Remove event by event-name and/or table * - * @param string $name + * @param string $name * @param string|null $table * * @return void @@ -1005,9 +998,9 @@ public function replace($data) * Adds new right join statement to the current query. * * @param string|Raw|\Closure|array $table - * @param string|Raw|\Closure $key - * @param string|null $operator - * @param string|Raw|\Closure|null $value + * @param string|Raw|\Closure $key + * @param string|null $operator + * @param string|Raw|\Closure|null $value * * @return static */ @@ -1086,7 +1079,7 @@ public function setFetchMode($parameters = null) * Execute statement * * @param string $sql - * @param array $bindings + * @param array $bindings * * @return array PDOStatement and execution time as float */ @@ -1096,15 +1089,7 @@ public function statement(string $sql, array $bindings = []): array $pdoStatement = $this->pdo->prepare($sql); - foreach ($bindings as $key => $value) { - $pdoStatement->bindValue( - \is_int($key) ? $key + 1 : $key, - $value, - (\is_int($value) || \is_bool($value)) ? PDO::PARAM_INT : PDO::PARAM_STR - ); - } - - $pdoStatement->execute(); + $pdoStatement->execute($bindings); return [$pdoStatement, microtime(true) - $start]; } @@ -1114,7 +1099,7 @@ public function statement(string $sql, array $bindings = []): array * Call this method when you want to add a new sub-query in your where etc. * * @param QueryBuilderHandler $queryBuilder - * @param string|null $alias + * @param string|null $alias * * @throws Exception * @return Raw @@ -1180,7 +1165,7 @@ public function table($tables) * @throws Exception * @return static */ - public function transaction(\Closure $callback) + public function transaction(\Closure $callback): IQueryBuilderHandler { /** * Get the Transaction class @@ -1188,26 +1173,33 @@ public function transaction(\Closure $callback) * @var \Pecee\Pixie\QueryBuilder\Transaction $queryTransaction * @throws \Exception */ - $queryTransaction = $this->container->build(Transaction::class, [$this->connection]); - $inTransaction = $queryTransaction->inTransaction(); try { // Begin the PDO transaction - $queryTransaction->begin($inTransaction); + $this->pdo->beginTransaction(); + + $queryTransaction = $this->container->build(Transaction::class, [$this->connection]); // Call closure + // NOTE: this callback will return TransactionHaltException if user has already committed the transaction $callback($queryTransaction); // If no errors have been thrown or the transaction wasn't completed within the closure, commit the changes - $queryTransaction->commit($inTransaction); + $this->pdo->commit(); + + } catch (TransactionHaltException $e) { + // Commit or rollback behavior has been triggered in the closure, so exit return $this; } catch (\Exception $e) { + // something happened, rollback changes and throw Exception - $queryTransaction->rollBack($inTransaction); + $this->pdo->rollBack(); - throw new Exception($e->getMessage(), $e->getCode(), $e->getPrevious()); + throw new Exception($e->getMessage()); } + + return $this; } /** @@ -1253,8 +1245,8 @@ public function updateOrInsert($data) /** * Adds WHERE statement to the current query. * - * @param string|Raw|\Closure $key - * @param string|null $operator + * @param string|Raw|\Closure $key + * @param string|null $operator * @param mixed|Raw|\Closure|null $value * * @return static @@ -1263,7 +1255,7 @@ public function where($key, $operator = null, $value = null): IQueryBuilderHandl { // If two params are given then assume operator is = if (\func_num_args() === 2) { - $value = $operator; + $value = $operator; $operator = '='; } @@ -1277,7 +1269,7 @@ public function where($key, $operator = null, $value = null): IQueryBuilderHandl /** * Adds WHERE BETWEEN statement to the current query. * - * @param string|Raw|\Closure $key + * @param string|Raw|\Closure $key * @param string|integer|float $valueFrom * @param string|integer|float $valueTo * @@ -1291,16 +1283,16 @@ public function whereBetween($key, $valueFrom, $valueTo): IQueryBuilderHandler /** * Handles where statements * - * @param string|Raw|\Closure $key - * @param string|null $operator + * @param string|Raw|\Closure $key + * @param string|null $operator * @param string|Raw|\Closure|null $value - * @param string $joiner + * @param string $joiner * * @return static */ protected function whereHandler($key, string $operator = null, $value = null, $joiner = 'AND'): IQueryBuilderHandler { - $key = $this->addTablePrefix($key); + $key = $this->addTablePrefix($key); $this->statements['wheres'][] = compact('key', 'operator', 'value', 'joiner'); return $this; @@ -1310,7 +1302,7 @@ protected function whereHandler($key, string $operator = null, $value = null, $j * Adds WHERE IN statement to the current query. * * @param string|Raw|\Closure $key - * @param array|Raw|\Closure $values + * @param array|Raw|\Closure $values * * @return static */ @@ -1322,9 +1314,9 @@ public function whereIn($key, $values): IQueryBuilderHandler /** * Adds WHERE NOT statement to the current query. * - * @param string|Raw|\Closure $key + * @param string|Raw|\Closure $key * @param string|array|Raw|\Closure|null $operator - * @param mixed|Raw|\Closure|null $value + * @param mixed|Raw|\Closure|null $value * * @return static */ @@ -1332,7 +1324,7 @@ public function whereNot($key, $operator = null, $value = null): IQueryBuilderHa { // If two params are given then assume operator is = if (\func_num_args() === 2) { - $value = $operator; + $value = $operator; $operator = '='; } @@ -1343,7 +1335,7 @@ public function whereNot($key, $operator = null, $value = null): IQueryBuilderHa * Adds OR WHERE NOT IN statement to the current query. * * @param string|Raw|\Closure $key - * @param array|Raw|\Closure $values + * @param array|Raw|\Closure $values * * @return static */ @@ -1380,14 +1372,14 @@ public function whereNull($key): IQueryBuilderHandler * Handles WHERE NULL statements. * * @param string|Raw|\Closure $key - * @param string $prefix - * @param string $operator + * @param string $prefix + * @param string $operator * * @return static */ protected function whereNullHandler($key, $prefix = '', $operator = ''): IQueryBuilderHandler { - $key = $this->adapterInstance->wrapSanitizer($this->addTablePrefix($key)); + $key = $this->adapterInstance->wrapSanitizer($this->addTablePrefix($key)); $prefix = ($prefix !== '') ? $prefix . ' ' : $prefix; return $this->{$operator . 'Where'}($this->raw("$key IS {$prefix}NULL")); diff --git a/src/Pecee/Pixie/QueryBuilder/Transaction.php b/src/Pecee/Pixie/QueryBuilder/Transaction.php index 35951ad..bd13a9a 100644 --- a/src/Pecee/Pixie/QueryBuilder/Transaction.php +++ b/src/Pecee/Pixie/QueryBuilder/Transaction.php @@ -9,62 +9,50 @@ */ class Transaction extends QueryBuilderHandler { - /** - * Check if we are in transaction - * - * @return bool - */ - public function inTransaction(): bool - { - return $this->pdo()->inTransaction(); - } + + protected $transactionStatement; /** - * Begin transaction - * - * @param bool $inTransaction + * Commit transaction * - * @return $this + * @throws \PDOException|TransactionHaltException */ - public function begin(bool $inTransaction = false): IQueryBuilderHandler + public function commit() { - if (false === $inTransaction) { - $this->pdo()->beginTransaction(); - } - - return $this; + $this->pdo->commit(); + throw new TransactionHaltException('Commit triggered transaction-halt.'); } /** - * Commit transaction - * - * @param bool $inTransaction + * RollBack transaction * - * @return $this + * @throws \PDOException|TransactionHaltException */ - public function commit(bool $inTransaction = false): IQueryBuilderHandler + public function rollBack() { - if (false === $inTransaction) { - $this->pdo()->commit(); - } - - return $this; + $this->pdo->rollBack(); + throw new TransactionHaltException('Rollback triggered transaction-halt.'); } /** - * RollBack transaction + * Execute statement * - * @param bool $inTransaction + * @param string $sql + * @param array $bindings * - * @return $this + * @return array PDOStatement and execution time as float */ - public function rollBack(bool $inTransaction = false): IQueryBuilderHandler + public function statement(string $sql, array $bindings = []): array { - if (false === $inTransaction) { - $this->pdo()->rollBack(); + $start = microtime(true); + + if ($this->transactionStatement === null && $this->pdo->inTransaction() === true) { + $this->transactionStatement = $this->pdo->prepare($sql); } - return $this; + $this->transactionStatement->execute($bindings); + + return [$this->transactionStatement, microtime(true) - $start]; } diff --git a/src/Pecee/Pixie/QueryBuilder/TransactionHaltException.php b/src/Pecee/Pixie/QueryBuilder/TransactionHaltException.php new file mode 100644 index 0000000..1e30ed5 --- /dev/null +++ b/src/Pecee/Pixie/QueryBuilder/TransactionHaltException.php @@ -0,0 +1,7 @@ +builder->query($query, $bindings)->get(); $this->assertEquals( [ $query, - [[5, PDO::PARAM_INT], ['usman', PDO::PARAM_STR]], + [5, 'usman', null], ], $queryArr ); } + public function testNullableWhere() + { + $query = $this->builder->table('person')->where('name', [1,null,3]); + + $this->assertEquals($query->getQuery()->getRawSql(), 'SELECT * FROM `cb_person` WHERE `name` = (1, NULL, 3)'); + + } + } diff --git a/tests/Pecee/Pixie/TransactionTest.php b/tests/Pecee/Pixie/TransactionTest.php new file mode 100644 index 0000000..ea41f11 --- /dev/null +++ b/tests/Pecee/Pixie/TransactionTest.php @@ -0,0 +1,78 @@ + 'mysql', + 'host' => '127.0.0.1', + 'database' => 'test', + 'username' => 'root', + 'password' => '', + 'charset' => 'utf8mb4', // Optional + 'collation' => 'utf8mb4_unicode_ci', // Optional + 'prefix' => '', // Table prefix, optional + ]); + + $this->builder = $connection->getQueryBuilder(); + + } + + public function testTransactionResult() { + + $this->builder->statement('TRUNCATE `people`'); + + $ids = []; + + $this->builder->transaction(function(Transaction $q) use(&$ids) { + + $ids = $q->table('people')->insert([ + [ + 'name' => 'Simon', + 'age' => 12, + 'awesome' => true, + 'nickname' => 'ponylover94', + ], + [ + 'name' => 'Peter', + 'age' => 40, + 'awesome' => false, + 'nickname' => null, + ], + [ + 'name' => 'Bobby', + 'age' => 20, + 'awesome' => true, + 'nickname' => 'peter', + ], + ]); + + }); + + $this->assertEquals(1, $ids[0]); + $this->assertEquals(2, $ids[1]); + $this->assertEquals(3, $ids[2]); + + $this->assertEquals($this->builder->table('people')->count(), 3); + + } + +} From 0336b8f36b7bce322b10ee4eb74e228e79570ac6 Mon Sep 17 00:00:00 2001 From: Simon Sessingo Date: Tue, 5 Dec 2017 10:47:32 +0100 Subject: [PATCH 08/11] Transactions are now working correctly for batch-insert. --- README.md | 64 +++++++++++++------ .../QueryBuilder/QueryBuilderHandler.php | 43 ++++++++----- tests/Pecee/Pixie/TransactionTest.php | 33 ++++++++++ 3 files changed, 105 insertions(+), 35 deletions(-) diff --git a/README.md b/README.md index 30d0086..44bd7f6 100644 --- a/README.md +++ b/README.md @@ -704,20 +704,22 @@ are made. Here's a basic transaction: ```php -$queryBuilder->transaction(function (QueryBuilderHandler $qb) { - $qb - ->table('my_table') - ->insert(array( - 'name' => 'Test', - 'url' => 'example.com' - ); - - $qb - ->table('my_table') - ->insert(array( - 'name' => 'Test2', - 'url' => 'example.com' - )); +$queryBuilder + ->transaction(function (Transaction $transaction) { + + $transaction + ->table('my_table') + ->insert(array( + 'name' => 'Test', + 'url' => 'example.com' + ); + + $transaction + ->table('my_table') + ->insert(array( + 'name' => 'Test2', + 'url' => 'example.com' + )); }); ``` @@ -730,22 +732,46 @@ If you wish to manually commit or rollback your changes, you can use the ```php $queryBuilder - ->transaction(function (qb) + ->transaction(function (Transaction $transaction) { - $queryBuilder + $transaction ->table('my_table') ->insert($data); // Commit changes (data will be saved) - - $queryBuilder->commit(); + $transaction->commit(); // Rollback changes (data would be rejected) - $queryBuilder->rollback(); + $transaction->rollback(); } ); ``` +Transactions will automatically be used when inserting multiple records. For example: + +```php +$queryBuilder->table('people')->insert([ + [ + 'name' => 'Simon', + 'age' => 12, + 'awesome' => true, + 'nickname' => 'ponylover94', + ], + [ + 'name' => 'Peter', + 'age' => 40, + 'awesome' => false, + 'nickname' => null, + ], + [ + 'name' => 'Bobby', + 'age' => 20, + 'awesome' => true, + 'nickname' => 'peter', + ], +]); +``` + ### Get Built Query Sometimes you may need to get the query string, it's possible. diff --git a/src/Pecee/Pixie/QueryBuilder/QueryBuilderHandler.php b/src/Pecee/Pixie/QueryBuilder/QueryBuilderHandler.php index 2932fe9..5b01a59 100644 --- a/src/Pecee/Pixie/QueryBuilder/QueryBuilderHandler.php +++ b/src/Pecee/Pixie/QueryBuilder/QueryBuilderHandler.php @@ -146,7 +146,7 @@ public function __construct(Connection $connection = null) $this->pdo->setAttribute(PDO::ATTR_ERRMODE, PDO::ERRMODE_EXCEPTION); - // PDO will parse parameter datatypes automaticially + // PDO will parse parameter datatypes automatically $this->pdo->setAttribute(PDO::ATTR_EMULATE_PREPARES, true); } @@ -338,9 +338,9 @@ public function delete(): \PDOStatement */ private function doInsert(array $data, string $type) { - // Helper function - $doInsert = function ($data) use ($type) { + // Insert single item + if (\is_array(current($data)) === false) { $queryObject = $this->getQuery($type, $data); $this->fireEvents(static::EVENT_BEFORE_INSERT, $queryObject); @@ -354,19 +354,27 @@ private function doInsert(array $data, string $type) $this->fireEvents(static::EVENT_AFTER_INSERT, $queryObject, $insertId, $executionTime); return $insertId; - }; - - // If first value is not an array - it's not a batch insert - if (\is_array(current($data)) === false) { - return $doInsert($data); } - // Perform batch insert $insertIds = []; + // If the current batch insert is not in a transaction, we create one... + + if ($this->pdo->inTransaction() === false) { + + $this->transaction(function (Transaction $transaction) use (&$insertIds, $data, $type) { + foreach ($data as $subData) { + $insertIds[] = $transaction->doInsert($subData, $type); + } + }); + + return $insertIds; + } + // Otherwise insert one by one... + foreach ($data as $subData) { - $insertIds[] = $doInsert($subData); + $insertIds[] = $this->doInsert($subData, $type); } return $insertIds; @@ -533,9 +541,11 @@ public function getQuery(string $type = 'select', $dataToBePassed = null): Query $queryArr = $this->adapterInstance->$type($this->statements, $dataToBePassed); - return $this->container->build( - QueryObject::class, - [$queryArr['sql'], $queryArr['bindings'], $this->pdo] + return $this->container->build(QueryObject::class, [ + $queryArr['sql'], + $queryArr['bindings'], + $this->pdo + ] ); } @@ -1178,6 +1188,7 @@ public function transaction(\Closure $callback): IQueryBuilderHandler $this->pdo->beginTransaction(); $queryTransaction = $this->container->build(Transaction::class, [$this->connection]); + $queryTransaction->statements = $this->statements; // Call closure // NOTE: this callback will return TransactionHaltException if user has already committed the transaction @@ -1188,12 +1199,12 @@ public function transaction(\Closure $callback): IQueryBuilderHandler } catch (TransactionHaltException $e) { - // Commit or rollback behavior has been triggered in the closure, so exit + // Commit or rollback behavior has been triggered in the closure return $this; } catch (\Exception $e) { - // something happened, rollback changes and throw Exception + // Something went wrong. Rollback and throw Exception $this->pdo->rollBack(); throw new Exception($e->getMessage()); @@ -1384,4 +1395,4 @@ protected function whereNullHandler($key, $prefix = '', $operator = ''): IQueryB return $this->{$operator . 'Where'}($this->raw("$key IS {$prefix}NULL")); } -} +} \ No newline at end of file diff --git a/tests/Pecee/Pixie/TransactionTest.php b/tests/Pecee/Pixie/TransactionTest.php index ea41f11..119336b 100644 --- a/tests/Pecee/Pixie/TransactionTest.php +++ b/tests/Pecee/Pixie/TransactionTest.php @@ -75,4 +75,37 @@ public function testTransactionResult() { } + public function testTransactionMultipleInsert() + { + $this->builder->statement('TRUNCATE `people`'); + + $ids = $this->builder->table('people')->insert([ + [ + 'name' => 'Simon', + 'age' => 12, + 'awesome' => true, + 'nickname' => 'ponylover94', + ], + [ + 'name' => 'Peter', + 'age' => 40, + 'awesome' => false, + 'nickname' => null, + ], + [ + 'name' => 'Bobby', + 'age' => 20, + 'awesome' => true, + 'nickname' => 'peter', + ], + ]); + + $this->assertEquals(1, $ids[0]); + $this->assertEquals(2, $ids[1]); + $this->assertEquals(3, $ids[2]); + + $this->assertEquals($this->builder->table('people')->count(), 3); + + } + } From 03a73962605e336073b7a6e7b636b35ac63f2d2d Mon Sep 17 00:00:00 2001 From: Simon Sessingo Date: Tue, 5 Dec 2017 13:03:47 +0100 Subject: [PATCH 09/11] Fixed nested transactions not working and added unit-tests. --- .../QueryBuilder/QueryBuilderHandler.php | 19 ++-- src/Pecee/Pixie/QueryBuilder/Transaction.php | 7 ++ tests/Pecee/Pixie/TransactionTest.php | 95 ++++++++++++++++++- 3 files changed, 111 insertions(+), 10 deletions(-) diff --git a/src/Pecee/Pixie/QueryBuilder/QueryBuilderHandler.php b/src/Pecee/Pixie/QueryBuilder/QueryBuilderHandler.php index 5b01a59..e0dd0a6 100644 --- a/src/Pecee/Pixie/QueryBuilder/QueryBuilderHandler.php +++ b/src/Pecee/Pixie/QueryBuilder/QueryBuilderHandler.php @@ -1183,15 +1183,16 @@ public function transaction(\Closure $callback): IQueryBuilderHandler * @var \Pecee\Pixie\QueryBuilder\Transaction $queryTransaction * @throws \Exception */ + $queryTransaction = $this->container->build(Transaction::class, [$this->connection]); + $queryTransaction->statements = $this->statements; + try { // Begin the PDO transaction - $this->pdo->beginTransaction(); - - $queryTransaction = $this->container->build(Transaction::class, [$this->connection]); - $queryTransaction->statements = $this->statements; + if($this->pdo->inTransaction() === false) { + $this->pdo->beginTransaction(); + } - // Call closure - // NOTE: this callback will return TransactionHaltException if user has already committed the transaction + // Call closure - this callback will return TransactionHaltException if user has already committed the transaction $callback($queryTransaction); // If no errors have been thrown or the transaction wasn't completed within the closure, commit the changes @@ -1205,12 +1206,14 @@ public function transaction(\Closure $callback): IQueryBuilderHandler } catch (\Exception $e) { // Something went wrong. Rollback and throw Exception - $this->pdo->rollBack(); + if($this->pdo->inTransaction() === true) { + $this->pdo->rollBack(); + } throw new Exception($e->getMessage()); } - return $this; + return $queryTransaction; } /** diff --git a/src/Pecee/Pixie/QueryBuilder/Transaction.php b/src/Pecee/Pixie/QueryBuilder/Transaction.php index bd13a9a..380bb2e 100644 --- a/src/Pecee/Pixie/QueryBuilder/Transaction.php +++ b/src/Pecee/Pixie/QueryBuilder/Transaction.php @@ -12,6 +12,13 @@ class Transaction extends QueryBuilderHandler protected $transactionStatement; + public function transaction(\Closure $callback): IQueryBuilderHandler + { + $callback($this); + + return $this; + } + /** * Commit transaction * diff --git a/tests/Pecee/Pixie/TransactionTest.php b/tests/Pecee/Pixie/TransactionTest.php index 119336b..cf416ba 100644 --- a/tests/Pecee/Pixie/TransactionTest.php +++ b/tests/Pecee/Pixie/TransactionTest.php @@ -36,13 +36,14 @@ public function setUp() } - public function testTransactionResult() { + public function testTransactionResult() + { $this->builder->statement('TRUNCATE `people`'); $ids = []; - $this->builder->transaction(function(Transaction $q) use(&$ids) { + $this->builder->transaction(function (Transaction $q) use (&$ids) { $ids = $q->table('people')->insert([ [ @@ -75,6 +76,96 @@ public function testTransactionResult() { } + + /** + * @throws Exception + */ + public function testNestedTransactions() + { + + $this->builder->statement('TRUNCATE `people`; TRUNCATE `animals`'); + + function getAnimals() + { + return [ + ['name' => 'mouse', 'number_of_legs' => '28'], + ['name' => 'horse', 'number_of_legs' => '4'], + ['name' => 'cat', 'number_of_legs' => '8'] + ]; + } + + function getPersons() + { + return + [ + [ + 'name' => 'Osama', + 'age' => '2', + 'awesome' => '1', + 'nickname' => 'jihad4evar', + ], + [ + 'name' => 'Leila', + 'age' => '76', + 'awesome' => '1', + 'nickname' => 'coolcatlady', + ], + [ + 'name' => 'Henry', + 'age' => '56', + 'awesome' => '1', + 'nickname' => 'ponylover95', + ] + ]; + } + + $this->builder->transaction(function (Transaction $qb) { + + function firstTrans(Transaction $oQuery) + { + + $oQuery->transaction(function (Transaction $qb) { + + $qb->table('animals')->insert([ + getAnimals() + ]); + + }); + } + + function secondTrans(Transaction $oQuery) + { + $oQuery->transaction(function (\Pecee\Pixie\QueryBuilder\Transaction $qb) { + + $qb->table('people')->insert([ + getPersons() + ]); + + }); + } + + firstTrans($qb); + secondTrans($qb); + + }); + + + $animals = $this->builder->table('animals')->select(['name', 'number_of_legs'])->get(); + $persons = $this->builder->table('people')->select(['name', 'age', 'awesome', 'nickname'])->get(); + + $originalPersons = getPersons(); + $originalAnimals = getAnimals(); + + $this->assertSameSize($persons, $originalPersons); + $this->assertEquals((array)$persons[0], $originalPersons[0]); + $this->assertEquals((array)$persons[1], $originalPersons[1]); + $this->assertEquals((array)$persons[2], $originalPersons[2]); + + $this->assertSameSize($animals, $originalAnimals); + $this->assertEquals((array)$animals[0], $originalAnimals[0]); + $this->assertEquals((array)$animals[1], $originalAnimals[1]); + } + public function testTransactionMultipleInsert() { $this->builder->statement('TRUNCATE `people`'); From 74b36672af7b00f7e46235df09ec66b4a948bd30 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Simon=20Sessing=C3=B8?= Date: Sun, 10 Dec 2017 22:58:39 +0100 Subject: [PATCH 10/11] Updated documentation --- README.md | 12 +++++++++--- 1 file changed, 9 insertions(+), 3 deletions(-) diff --git a/README.md b/README.md index 44bd7f6..27e2c01 100644 --- a/README.md +++ b/README.md @@ -266,17 +266,23 @@ $queryBuilder ->join('table2', 'table2.person_id', '=', 'foo1.id'); ``` -You can change the alias anytime by using +You can change the alias anytime by using: ```php -$queryBuilder->alias($table, $alias); +$queryBuilder->alias('foo1', 'table1'); + +// Simplified way... + +$queryBuilder->table('table1')->alias('foo1'); ``` +**Note:** If `$table` parameter is null - the querybuilder will use the table from latest call to `table($table)` method. + Output: ```sql SELECT * -FROM `table1` AS foo1 +FROM `table1` AS `foo1` INNER JOIN `cb_table2` ON `cb_table2`.`person_id` = `cb_foo1`.`id` ``` From 79d14d5b7dd3a69c9ced042cc3e48a32e88b5901 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Simon=20Sessing=C3=B8?= Date: Mon, 11 Dec 2017 00:54:38 +0100 Subject: [PATCH 11/11] Development - Removed `usmanhalalit/viocon` composer dependency (Container support) to increase library performance. - Moved event-types constants to `EventHandler` from `QueryBuilderHandler`. - Added return-types to more methods. - Updated documentation. - Updated unit-tests to reflect new changes. --- .idea/codeStyles/codeStyleConfig.xml | 2 +- README.md | 46 +- composer.json | 3 +- composer.lock | 93 +- src/Pecee/Pixie/Connection.php | 304 +- .../Pixie/ConnectionAdapters/BaseAdapter.php | 50 +- .../Pixie/ConnectionAdapters/Exception.php | 3 +- .../ConnectionAdapters/IConnectionAdapter.php | 24 + src/Pecee/Pixie/ConnectionAdapters/Mysql.php | 86 +- src/Pecee/Pixie/ConnectionAdapters/Pgsql.php | 78 +- src/Pecee/Pixie/ConnectionAdapters/Sqlite.php | 41 +- src/Pecee/Pixie/EventHandler.php | 296 +- src/Pecee/Pixie/Exception.php | 3 +- .../QueryBuilder/Adapters/BaseAdapter.php | 1153 ++++--- .../Pixie/QueryBuilder/Adapters/Mysql.php | 11 +- .../Pixie/QueryBuilder/Adapters/Pgsql.php | 11 +- .../Pixie/QueryBuilder/Adapters/Sqlite.php | 11 +- .../QueryBuilder/IQueryBuilderHandler.php | 1 + src/Pecee/Pixie/QueryBuilder/JoinBuilder.php | 71 +- .../Pixie/QueryBuilder/NestedCriteria.php | 30 +- .../QueryBuilder/QueryBuilderHandler.php | 2654 ++++++++--------- src/Pecee/Pixie/QueryBuilder/QueryObject.php | 190 +- src/Pecee/Pixie/QueryBuilder/Raw.php | 66 +- src/Pecee/Pixie/QueryBuilder/Transaction.php | 90 +- .../QueryBuilder/TransactionHaltException.php | 3 +- tests/Pecee/Pixie/ConnectionTest.php | 48 +- tests/Pecee/Pixie/NoTableSubQueryTest.php | 38 +- .../Pecee/Pixie/QueryBuilderBehaviorTest.php | 684 ++--- tests/Pecee/Pixie/QueryBuilderTest.php | 199 +- tests/Pecee/Pixie/TransactionTest.php | 367 ++- tests/TestCase.php | 214 +- 31 files changed, 3321 insertions(+), 3549 deletions(-) create mode 100644 src/Pecee/Pixie/ConnectionAdapters/IConnectionAdapter.php diff --git a/.idea/codeStyles/codeStyleConfig.xml b/.idea/codeStyles/codeStyleConfig.xml index cd74ae8..be10288 100644 --- a/.idea/codeStyles/codeStyleConfig.xml +++ b/.idea/codeStyles/codeStyleConfig.xml @@ -1,5 +1,5 @@ - \ No newline at end of file diff --git a/README.md b/README.md index 27e2c01..53876e5 100644 --- a/README.md +++ b/README.md @@ -15,6 +15,8 @@ This library is stable, maintained and are used by many sites, including: **Requirements:** - PHP version 7.0 or higher is required. +Versions prior to 3.x are available [here](https://github.com/skipperbent/pixie). + #### Feedback and development If you are missing a feature, experience problems or have ideas or feedback that you want us to hear, please feel free to create an issue. @@ -41,7 +43,7 @@ For example when pushing changes to version 3, the pull request should use the ` - When adding new stuff, please remember to add new unit-tests for the functionality. -#### Credits +#### Credits & features This project is based on the original [Pixie project by usmanhalalit](https://github.com/usmanhalalit/pixie) but has some extra features like: @@ -63,14 +65,10 @@ This project is based on the original [Pixie project by usmanhalalit](https://gi Most importantly this project is used on many live-sites and maintained. -#### Versions prior to 3.x - -Older versions prior to 3.x are available [https://github.com/skipperbent/pixie](https://github.com/skipperbent/pixie). - #### Note -`AliasFacade` used for calling the database-connection as a fixed constant has been removed to increase performance. -If this feature is required in your setup we encourage you to implement your own solution. +`Facades` and `Container` support has been removed to increase performance. To implement your own adapters, please extends the +`IConnectionAdapter` interface. ## Example ```php @@ -79,7 +77,7 @@ require 'vendor/autoload.php'; // Create a connection, once only. $config = array( - 'driver' => 'mysql', // Db driver + 'driver' => 'mysql', // Db driver or IConnectionAdapter class 'host' => 'localhost', 'database' => 'your-database', 'username' => 'root', @@ -908,21 +906,23 @@ Pixie comes with powerful query events to supercharge your application. These ev #### Available Events - - after-* - - before-* - - before-select - - after-select - - before-insert - - after-insert - - before-update - - after-update - - before-delete - - after-delete +| Event constant | Event value/name | Description | +| :------------------------------------ | :------------- | :------------ | +| `EventHandler::EVENT_BEFORE_ALL` | `before-*` | Event-type that fires before each query. | +| `EventHandler::EVENT_AFTER_ALL` | `after-*` | Event-type that fires after each query. | +| `EventHandler::EVENT_BEFORE_SELECT` | `before-select` | Event-type that fires before select query. | +| `EventHandler::EVENT_AFTER_SELECT` | `after-select` | Event-type that fires after insert query. | +| `EventHandler::EVENT_BEFORE_INSERT` | `before-insert` | Event-type that fires before insert query | +| `EventHandler::EVENT_AFTER_INSERT` | `after-insert` | Event-type that fires after insert query. | +| `EventHandler::EVENT_BEFORE_UPDATE` | `before-update` | Event-type that fires before update query. | +| `EventHandler::EVENT_AFTER_UPDATE` | `after-update` | Event-type that fires after update query. | +| `EventHandler::EVENT_BEFORE_DELETE` | `before-delete` | Event-type that fires before delete query. | +| `EventHandler::EVENT_AFTER_DELETE` | `after-delete` | Event-type that fires after delete query. | #### Registering Events ```php -$queryBuilder->registerEvent('before-select', 'users', function(QueryBuilderHandler $qb) +$queryBuilder->registerEvent(EventHandler::EVENT_BEFORE_SELECT, 'users', function(QueryBuilderHandler $qb) { $qb->where('status', '!=', 'banned'); }); @@ -938,7 +938,7 @@ If you want the event to be performed when **any table is being queried**, provi After inserting data into `my_table`, details will be inserted into another table ```php -$queryBuilder->registerEvent('after-insert', 'my_table', function(QueryBuilderHandler $qb, $insertId) +$queryBuilder->registerEvent(EventHandler::EVENT_AFTER_INSERT, 'my_table', function(QueryBuilderHandler $qb, $insertId) { $qb ->table('person_details')->insert(array( @@ -952,7 +952,7 @@ $queryBuilder->registerEvent('after-insert', 'my_table', function(QueryBuilderHa Whenever data is inserted into `person_details` table, set the timestamp field `created_at`, so we don't have to specify it everywhere: ```php -$queryBuilder->registerEvent('after-insert', 'person_details', function(QueryBuilderHandler $qb, $insertId) +$queryBuilder->registerEvent(EventHandler::EVENT_AFTER_INSERT, 'person_details', function(QueryBuilderHandler $qb, $insertId) { $qb ->table('person_details') @@ -966,7 +966,7 @@ $queryBuilder->registerEvent('after-insert', 'person_details', function(QueryBui After deleting from `my_table` delete the relations: ```php -$queryBuilder->registerEvent('after-delete', 'my_table', function(QueryBuilderHandler $qb, $queryObject) +$queryBuilder->registerEvent(EventHandler::EVENT_AFTER_DELETE, 'my_table', function(QueryBuilderHandler $qb, $queryObject) { $bindings = $queryObject->getBindings(); $qb @@ -990,7 +990,7 @@ Only on `after-*` events you get three parameters: **first** is the query builde #### Removing Events ```php -$queryBuilder->removeEvent('event-name', 'table-name'); +$queryBuilder->removeEvent($event, $table = null); ``` #### Some Use Cases diff --git a/composer.json b/composer.json index 3734d78..e229064 100644 --- a/composer.json +++ b/composer.json @@ -34,8 +34,7 @@ } ], "require": { - "php": ">=7.0", - "usmanhalalit/viocon": "1.0.1" + "php": ">=7.0" }, "require-dev": { "phpunit/phpunit": "^6.0", diff --git a/composer.lock b/composer.lock index 180ded1..22bca5f 100644 --- a/composer.lock +++ b/composer.lock @@ -4,56 +4,8 @@ "Read more about it at https://getcomposer.org/doc/01-basic-usage.md#composer-lock-the-lock-file", "This file is @generated automatically" ], - "content-hash": "431825fe68c6a6b27cb373d5aa657c4b", - "packages": [ - { - "name": "usmanhalalit/viocon", - "version": "1.0.1", - "source": { - "type": "git", - "url": "https://github.com/usmanhalalit/viocon.git", - "reference": "0878afee16f15355971fb95bf3c6d297aceff35d" - }, - "dist": { - "type": "zip", - "url": "https://api.github.com/repos/usmanhalalit/viocon/zipball/0878afee16f15355971fb95bf3c6d297aceff35d", - "reference": "0878afee16f15355971fb95bf3c6d297aceff35d", - "shasum": "" - }, - "require": { - "php": ">=5.3.0" - }, - "require-dev": { - "phpunit/phpunit": "3.7.*" - }, - "type": "library", - "autoload": { - "psr-0": { - "Viocon": "src/" - } - }, - "notification-url": "https://packagist.org/downloads/", - "license": [ - "MIT" - ], - "authors": [ - { - "name": "Muhammad Usman", - "email": "hi@usman.it", - "role": "Developer" - } - ], - "description": "A simple and flexible Dependency Injection container for PHP.", - "homepage": "https://github.com/usmanhalalit/viocon", - "keywords": [ - "container", - "di", - "ioc", - "test" - ], - "time": "2013-07-13T19:54:56+00:00" - } - ], + "content-hash": "a32a0e41d34ad5a9fb4b9fd4c1ba4b88", + "packages": [], "packages-dev": [ { "name": "doctrine/instantiator", @@ -586,16 +538,16 @@ }, { "name": "phpunit/php-code-coverage", - "version": "5.2.4", + "version": "5.3.0", "source": { "type": "git", "url": "https://github.com/sebastianbergmann/php-code-coverage.git", - "reference": "033ec97498cf530cc1be4199264cad568b19be26" + "reference": "661f34d0bd3f1a7225ef491a70a020ad23a057a1" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/sebastianbergmann/php-code-coverage/zipball/033ec97498cf530cc1be4199264cad568b19be26", - "reference": "033ec97498cf530cc1be4199264cad568b19be26", + "url": "https://api.github.com/repos/sebastianbergmann/php-code-coverage/zipball/661f34d0bd3f1a7225ef491a70a020ad23a057a1", + "reference": "661f34d0bd3f1a7225ef491a70a020ad23a057a1", "shasum": "" }, "require": { @@ -611,7 +563,6 @@ "theseer/tokenizer": "^1.1" }, "require-dev": { - "ext-xdebug": "^2.5", "phpunit/phpunit": "^6.0" }, "suggest": { @@ -620,7 +571,7 @@ "type": "library", "extra": { "branch-alias": { - "dev-master": "5.2.x-dev" + "dev-master": "5.3.x-dev" } }, "autoload": { @@ -635,7 +586,7 @@ "authors": [ { "name": "Sebastian Bergmann", - "email": "sb@sebastian-bergmann.de", + "email": "sebastian@phpunit.de", "role": "lead" } ], @@ -646,7 +597,7 @@ "testing", "xunit" ], - "time": "2017-11-27T09:00:30+00:00" + "time": "2017-12-06T09:29:45+00:00" }, { "name": "phpunit/php-file-iterator", @@ -836,16 +787,16 @@ }, { "name": "phpunit/phpunit", - "version": "6.5.2", + "version": "6.5.4", "source": { "type": "git", "url": "https://github.com/sebastianbergmann/phpunit.git", - "reference": "24b708f2fd725bcef1c8153b366043381aa324f2" + "reference": "1b2f933d5775f9237369deaa2d2bfbf9d652be4c" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/sebastianbergmann/phpunit/zipball/24b708f2fd725bcef1c8153b366043381aa324f2", - "reference": "24b708f2fd725bcef1c8153b366043381aa324f2", + "url": "https://api.github.com/repos/sebastianbergmann/phpunit/zipball/1b2f933d5775f9237369deaa2d2bfbf9d652be4c", + "reference": "1b2f933d5775f9237369deaa2d2bfbf9d652be4c", "shasum": "" }, "require": { @@ -859,11 +810,11 @@ "phar-io/version": "^1.0", "php": "^7.0", "phpspec/prophecy": "^1.7", - "phpunit/php-code-coverage": "^5.2.3", + "phpunit/php-code-coverage": "^5.3", "phpunit/php-file-iterator": "^1.4.3", "phpunit/php-text-template": "^1.2.1", "phpunit/php-timer": "^1.0.9", - "phpunit/phpunit-mock-objects": "^5.0.4", + "phpunit/phpunit-mock-objects": "^5.0.5", "sebastian/comparator": "^2.1", "sebastian/diff": "^2.0", "sebastian/environment": "^3.1", @@ -916,27 +867,27 @@ "testing", "xunit" ], - "time": "2017-12-02T05:36:24+00:00" + "time": "2017-12-10T08:06:19+00:00" }, { "name": "phpunit/phpunit-mock-objects", - "version": "5.0.4", + "version": "5.0.5", "source": { "type": "git", "url": "https://github.com/sebastianbergmann/phpunit-mock-objects.git", - "reference": "16b50f4167e5e85e81ca8a3dd105d0a5fd32009a" + "reference": "283b9f4f670e3a6fd6c4ff95c51a952eb5c75933" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/sebastianbergmann/phpunit-mock-objects/zipball/16b50f4167e5e85e81ca8a3dd105d0a5fd32009a", - "reference": "16b50f4167e5e85e81ca8a3dd105d0a5fd32009a", + "url": "https://api.github.com/repos/sebastianbergmann/phpunit-mock-objects/zipball/283b9f4f670e3a6fd6c4ff95c51a952eb5c75933", + "reference": "283b9f4f670e3a6fd6c4ff95c51a952eb5c75933", "shasum": "" }, "require": { "doctrine/instantiator": "^1.0.5", "php": "^7.0", "phpunit/php-text-template": "^1.2.1", - "sebastian/exporter": "^3.0" + "sebastian/exporter": "^3.1" }, "conflict": { "phpunit/phpunit": "<6.0" @@ -975,7 +926,7 @@ "mock", "xunit" ], - "time": "2017-12-02T05:31:19+00:00" + "time": "2017-12-10T08:01:53+00:00" }, { "name": "sebastian/code-unit-reverse-lookup", diff --git a/src/Pecee/Pixie/Connection.php b/src/Pecee/Pixie/Connection.php index ea55f46..3356769 100644 --- a/src/Pecee/Pixie/Connection.php +++ b/src/Pecee/Pixie/Connection.php @@ -2,173 +2,153 @@ namespace Pecee\Pixie; +use Pecee\Pixie\ConnectionAdapters\IConnectionAdapter; use Pecee\Pixie\QueryBuilder\QueryBuilderHandler; -use Viocon\Container; /** * Class Connection * * @package Pecee\Pixie */ -class Connection -{ - - /** - * @var Connection - */ - protected static $storedConnection; - /** - * @var Container - */ - protected $container; - /** - * Name of DB adapter (i.e. Mysql, Pgsql, Sqlite) - * - * @var string - */ - protected $adapter; - /** - * @var array - */ - protected $adapterConfig; - /** - * @var \PDO - */ - protected $pdoInstance; - /** - * @var EventHandler - */ - protected $eventHandler; - - /** - * @param string $adapter - * @param array $adapterConfig - * @param Container|null $container - */ - public function __construct(string $adapter, array $adapterConfig, Container $container = null) - { - $this->container = $container ?? new Container(); - - $this - ->setAdapter($adapter) - ->setAdapterConfig($adapterConfig) - ->connect(); - - // Create event dependency - $this->eventHandler = $this->container->build(EventHandler::class); - } - - /** - * @return Connection - */ - public static function getStoredConnection(): Connection - { - return static::$storedConnection; - } - - /** - * Create the connection adapter - */ - public function connect() - { - // Build a database connection if we don't have one connected - - $adapter = '\Pecee\Pixie\ConnectionAdapters\\' . ucfirst(strtolower($this->adapter)); - - $adapterInstance = $this->container->build($adapter, [$this->container]); - - $pdo = $adapterInstance->connect($this->adapterConfig); - $this->setPdoInstance($pdo); - - // Preserve the first database connection with a static property - if (static::$storedConnection === null) { - static::$storedConnection = $this; - } - } - - /** - * @return string - */ - public function getAdapter(): string - { - return $this->adapter; - } - - /** - * @return array - */ - public function getAdapterConfig(): array - { - return $this->adapterConfig; - } - - /** - * @return Container - */ - public function getContainer(): Container - { - return $this->container; - } - - /** - * @return EventHandler - */ - public function getEventHandler(): EventHandler - { - return $this->eventHandler; - } - - /** - * @return \PDO - */ - public function getPdoInstance(): \PDO - { - return $this->pdoInstance; - } - - /** - * Returns an instance of Query Builder - * - * @return QueryBuilderHandler - */ - public function getQueryBuilder(): QueryBuilderHandler - { - return $this->container->build(QueryBuilderHandler::class, [$this]); - } - - /** - * @param string $adapter - * - * @return \Pecee\Pixie\Connection - */ - public function setAdapter(string $adapter): Connection - { - $this->adapter = $adapter; - - return $this; - } - - /** - * @param array $adapterConfig - * - * @return \Pecee\Pixie\Connection - */ - public function setAdapterConfig(array $adapterConfig): Connection - { - $this->adapterConfig = $adapterConfig; - - return $this; - } - - /** - * @param \PDO $pdo - * - * @return \Pecee\Pixie\Connection - */ - public function setPdoInstance(\PDO $pdo): Connection - { - $this->pdoInstance = $pdo; - - return $this; - } +class Connection { + + /** + * @var Connection + */ + protected static $storedConnection; + + /** + * Connection adapter (i.e. Mysql, Pgsql, Sqlite) + * + * @var IConnectionAdapter + */ + protected $adapter; + + /** + * @var array + */ + protected $adapterConfig; + + /** + * @var \PDO + */ + protected $pdoInstance; + + /** + * @var EventHandler + */ + protected $eventHandler; + + /** + * @param string $adapter Adapter name or class + * @param array $adapterConfig + */ + public function __construct($adapter, array $adapterConfig) { + if (($adapter instanceof IConnectionAdapter) === false) { + /* @var $adapter IConnectionAdapter */ + $adapter = '\Pecee\Pixie\ConnectionAdapters\\' . ucfirst(strtolower($adapter)); + $adapter = new $adapter(); + } + + $this + ->setAdapter($adapter) + ->setAdapterConfig($adapterConfig) + ->connect(); + + // Create event dependency + $this->eventHandler = new EventHandler(); + } + + /** + * @return Connection + */ + public static function getStoredConnection(): Connection { + return static::$storedConnection; + } + + /** + * Create the connection adapter + */ + public function connect() { + // Build a database connection if we don't have one connected + + $pdo = $this->adapter->connect($this->adapterConfig); + $this->setPdoInstance($pdo); + + // Preserve the first database connection with a static property + if (static::$storedConnection === null) { + static::$storedConnection = $this; + } + } + + /** + * @return IConnectionAdapter + */ + public function getAdapter(): IConnectionAdapter { + return $this->adapter; + } + + /** + * @return array + */ + public function getAdapterConfig(): array { + return $this->adapterConfig; + } + + /** + * @return EventHandler + */ + public function getEventHandler(): EventHandler { + return $this->eventHandler; + } + + /** + * @return \PDO + */ + public function getPdoInstance(): \PDO { + return $this->pdoInstance; + } + + /** + * Returns an instance of Query Builder + * + * @return QueryBuilderHandler + * @throws Exception + */ + public function getQueryBuilder(): QueryBuilderHandler { + return new QueryBuilderHandler($this); + } + + /** + * @param IConnectionAdapter $adapter + * + * @return \Pecee\Pixie\Connection + */ + public function setAdapter(IConnectionAdapter $adapter): Connection { + $this->adapter = $adapter; + + return $this; + } + + /** + * @param array $adapterConfig + * + * @return \Pecee\Pixie\Connection + */ + public function setAdapterConfig(array $adapterConfig): Connection { + $this->adapterConfig = $adapterConfig; + + return $this; + } + + /** + * @param \PDO $pdo + * + * @return \Pecee\Pixie\Connection + */ + public function setPdoInstance(\PDO $pdo): Connection { + $this->pdoInstance = $pdo; + + return $this; + } } diff --git a/src/Pecee/Pixie/ConnectionAdapters/BaseAdapter.php b/src/Pecee/Pixie/ConnectionAdapters/BaseAdapter.php index 5046df5..cf88594 100644 --- a/src/Pecee/Pixie/ConnectionAdapters/BaseAdapter.php +++ b/src/Pecee/Pixie/ConnectionAdapters/BaseAdapter.php @@ -3,44 +3,30 @@ namespace Pecee\Pixie\ConnectionAdapters; use PDO; -use Viocon\Container; /** * Class BaseAdapter * * @package Pecee\Pixie\ConnectionAdapters */ -abstract class BaseAdapter -{ - /** - * @var \Viocon\Container - */ - protected $container; +abstract class BaseAdapter implements IConnectionAdapter { + /** + * @param array $config + * + * @return PDO + */ + public function connect(array $config): PDO { + if (isset($config['options']) === false) { + $config['options'] = []; + } - /** - * @param \Viocon\Container $container - */ - public function __construct(Container $container) - { - $this->container = $container; - } + return $this->doConnect($config); + } - /** - * @param $config - * @return \PDO - */ - public function connect(array $config): PDO - { - if (isset($config['options']) === false) { - $config['options'] = []; - } - - return $this->doConnect($config); - } - - /** - * @param array $config - * @return mixed - */ - abstract protected function doConnect(array $config); + /** + * @param array $config + * + * @return PDO + */ + abstract protected function doConnect(array $config): PDO; } diff --git a/src/Pecee/Pixie/ConnectionAdapters/Exception.php b/src/Pecee/Pixie/ConnectionAdapters/Exception.php index d688594..445a570 100644 --- a/src/Pecee/Pixie/ConnectionAdapters/Exception.php +++ b/src/Pecee/Pixie/ConnectionAdapters/Exception.php @@ -7,7 +7,6 @@ * * @package Pecee\Pixie\ConnectionAdapters */ -class Exception extends \Pecee\Pixie\Exception -{ +class Exception extends \Pecee\Pixie\Exception { } diff --git a/src/Pecee/Pixie/ConnectionAdapters/IConnectionAdapter.php b/src/Pecee/Pixie/ConnectionAdapters/IConnectionAdapter.php new file mode 100644 index 0000000..76e1691 --- /dev/null +++ b/src/Pecee/Pixie/ConnectionAdapters/IConnectionAdapter.php @@ -0,0 +1,24 @@ +container->build( - \PDO::class, - [$connectionString, $config['username'], $config['password'], $config['options']] - ); - - if (isset($config['charset'])) { - $connection->prepare("SET NAMES '{$config['charset']}'")->execute(); - } - - return $connection; - } +class Mysql extends BaseAdapter { + /** + * @param array $config + * + * @return PDO + * @throws Exception + */ + protected function doConnect(array $config): PDO { + if (\extension_loaded('pdo_mysql') === false) { + throw new Exception(sprintf('%s library not loaded', 'pdo_mysql')); + } + + $connectionString = "mysql:dbname={$config['database']}"; + + if (isset($config['host']) === true) { + $connectionString .= ";host={$config['host']}"; + } + + if (isset($config['port']) === true) { + $connectionString .= ";port={$config['port']}"; + } + + if (isset($config['unix_socket']) === true) { + $connectionString .= ";unix_socket={$config['unix_socket']}"; + } + + $connection = new PDO($connectionString, $config['username'], $config['password'], $config['options']); + + if (isset($config['charset'])) { + $connection->prepare("SET NAMES '{$config['charset']}'")->execute(); + } + + return $connection; + } + + /** + * Get query adapter class + * @return string + */ + public function getQueryAdapterClass(): string { + return \Pecee\Pixie\QueryBuilder\Adapters\Mysql::class; + } } diff --git a/src/Pecee/Pixie/ConnectionAdapters/Pgsql.php b/src/Pecee/Pixie/ConnectionAdapters/Pgsql.php index 171007a..38fb593 100644 --- a/src/Pecee/Pixie/ConnectionAdapters/Pgsql.php +++ b/src/Pecee/Pixie/ConnectionAdapters/Pgsql.php @@ -2,47 +2,49 @@ namespace Pecee\Pixie\ConnectionAdapters; +use PDO; + /** * Class Pgsql * * @package Pecee\Pixie\ConnectionAdapters */ -class Pgsql extends BaseAdapter -{ - /** - * @param array $config - * - * @return mixed - * @throws Exception - */ - protected function doConnect(array $config) - { - if (\extension_loaded('pdo_pgsql') === false) { - throw new Exception(sprintf('%s library not loaded', 'pdo_pgsql')); - } - - $connectionString = "pgsql:host={$config['host']};dbname={$config['database']}"; - - if (isset($config['port']) === true) { - $connectionString .= ";port={$config['port']}"; - } - - /** - * @var \PDO $connection - */ - $connection = $this->container->build( - \PDO::class, - [$connectionString, $config['username'], $config['password'], $config['options']] - ); - - if (isset($config['charset']) === true) { - $connection->prepare("SET NAMES '{$config['charset']}'")->execute(); - } - - if (isset($config['schema']) === true) { - $connection->prepare("SET search_path TO '{$config['schema']}'")->execute(); - } - - return $connection; - } +class Pgsql extends BaseAdapter { + /** + * @param array $config + * + * @return PDO + * @throws Exception + */ + protected function doConnect(array $config): PDO { + if (\extension_loaded('pdo_pgsql') === false) { + throw new Exception(sprintf('%s library not loaded', 'pdo_pgsql')); + } + + $connectionString = "pgsql:host={$config['host']};dbname={$config['database']}"; + + if (isset($config['port']) === true) { + $connectionString .= ";port={$config['port']}"; + } + + $connection = new PDO($connectionString, $config['username'], $config['password'], $config['options']); + + if (isset($config['charset']) === true) { + $connection->prepare("SET NAMES '{$config['charset']}'")->execute(); + } + + if (isset($config['schema']) === true) { + $connection->prepare("SET search_path TO '{$config['schema']}'")->execute(); + } + + return $connection; + } + + /** + * Get query adapter class + * @return string + */ + public function getQueryAdapterClass(): string { + return \Pecee\Pixie\QueryBuilder\Adapters\Pgsql::class; + } } diff --git a/src/Pecee/Pixie/ConnectionAdapters/Sqlite.php b/src/Pecee/Pixie/ConnectionAdapters/Sqlite.php index 4800dbf..7d4cda6 100644 --- a/src/Pecee/Pixie/ConnectionAdapters/Sqlite.php +++ b/src/Pecee/Pixie/ConnectionAdapters/Sqlite.php @@ -9,25 +9,28 @@ * * @package Pecee\Pixie\ConnectionAdapters */ -class Sqlite extends BaseAdapter -{ - /** - * @param array $config - * - * @return \PDO - * @throws Exception - */ - public function doConnect(array $config): PDO - { - if (\extension_loaded('pdo_sqlite') === false) { - throw new Exception(sprintf('%s library not loaded', 'pdo_sqlite')); - } +class Sqlite extends BaseAdapter { + /** + * @param array $config + * + * @return PDO + * @throws Exception + */ + public function doConnect(array $config): PDO { + if (\extension_loaded('pdo_sqlite') === false) { + throw new Exception(sprintf('%s library not loaded', 'pdo_sqlite')); + } - $connectionString = 'sqlite:' . $config['database']; + $connectionString = 'sqlite:' . $config['database']; - return $this->container->build( - PDO::class, - [$connectionString, null, null, $config['options']] - ); - } + return new PDO($connectionString, null, null, $config['options']); + } + + /** + * Get query adapter class + * @return string + */ + public function getQueryAdapterClass(): string { + return \Pecee\Pixie\QueryBuilder\Adapters\Sqlite::class; + } } diff --git a/src/Pecee/Pixie/EventHandler.php b/src/Pecee/Pixie/EventHandler.php index f98588d..26c8338 100644 --- a/src/Pecee/Pixie/EventHandler.php +++ b/src/Pecee/Pixie/EventHandler.php @@ -10,121 +10,183 @@ * * @package Pecee\Pixie */ -class EventHandler -{ - /** - * Fake table name for any table events - */ - const TABLE_ANY = ':any'; - /** - * @var array - */ - protected $events = []; - - /** - * @var array - */ - protected $firedEvents = []; - - /** - * @param QueryBuilderHandler $queryBuilder - * @param string $event - * - * @return mixed - */ - public function fireEvents(QueryBuilderHandler $queryBuilder, string $event) - { - $statements = $queryBuilder->getStatements(); - $tables = $statements['tables'] ?? []; - - // Events added with :any will be fired in case of any table, - // we are adding :any as a fake table at the beginning. - array_unshift($tables, static::TABLE_ANY); - - $handlerParams = \func_get_args(); - unset($handlerParams[1]); - - // Fire all events - foreach ($tables as $table) { - // Fire before events for :any table - $action = $this->getEvent($event, $table); - if ($action !== null) { - - // Make an event id, with event type and table - $eventId = $event . $table; - - // Fire event and add to fired list - $this->firedEvents[] = $eventId; - $result = \call_user_func_array($action, $handlerParams); - if ($result !== null) { - return $result; - } - } - } - - return null; - } - - /** - * @param string $event - * @param string|Raw|null $table - * - * @return \Closure|null - */ - public function getEvent(string $event, $table = null) - { - $table = $table ?? static::TABLE_ANY; - - if ($table instanceof Raw) { - return null; - } - - // Find event with * - if (isset($this->events[$table]) === true) { - foreach ((array)$this->events[$table] as $name => $e) { - if (strpos($name, '*') !== false) { - $name = substr($name, 0, strpos($name, '*')); - if (stripos($event, $name) !== false) { - return $e; - } - } - } - } - - return $this->events[$table][$event] ?? null; - } - - /** - * @return array - */ - public function getEvents(): array - { - return $this->events; - } - - /** - * @param string $event - * @param string $table - * @param \Closure $action - * - * @return void - */ - public function registerEvent(string $event, string $table = null, \Closure $action) - { - $table = $table ?? static::TABLE_ANY; - - $this->events[$table][$event] = $action; - } - - /** - * @param string $event - * @param string $table - * - * @return void - */ - public function removeEvent($event, $table = null) - { - $table = $table ?? static::TABLE_ANY; - unset($this->events[$table][$event]); - } +class EventHandler { + + /** + * Event-type that fires before each query + * + * @var string + */ + const EVENT_BEFORE_ALL = 'before-*'; + + /** + * Event-type that fires after each query + * + * @var string + */ + const EVENT_AFTER_ALL = 'after-*'; + + /** + * Event-type that fires before select + * + * @var string + */ + const EVENT_BEFORE_SELECT = 'before-select'; + + /** + * Event-type that fires after select + * + * @var string + */ + const EVENT_AFTER_SELECT = 'after-select'; + + /** + * Event-type that fires before insert + * + * @var string + */ + const EVENT_BEFORE_INSERT = 'before-insert'; + + /** + * Event-type that fires after insert + * + * @var string + */ + const EVENT_AFTER_INSERT = 'after-insert'; + + /** + * Event-type that fires before update + * + * @var string + */ + const EVENT_BEFORE_UPDATE = 'before-update'; + + /** + * Event-type that fires after update + * + * @var string + */ + const EVENT_AFTER_UPDATE = 'after-update'; + + /** + * Event-type that fires before delete + * + * @var string + */ + const EVENT_BEFORE_DELETE = 'before-delete'; + + /** + * Event-type that fires after delete + * + * @var string + */ + const EVENT_AFTER_DELETE = 'after-delete'; + + /** + * Fake table name for any table events + */ + const TABLE_ANY = ':any'; + /** + * @var array + */ + protected $events = []; + + /** + * @var array + */ + protected $firedEvents = []; + + /** + * @param QueryBuilderHandler $queryBuilder + * @param string $event + * + * @return mixed + */ + public function fireEvents(QueryBuilderHandler $queryBuilder, string $event) { + $statements = $queryBuilder->getStatements(); + $tables = $statements['tables'] ?? []; + + // Events added with :any will be fired in case of any table, + // we are adding :any as a fake table at the beginning. + array_unshift($tables, static::TABLE_ANY); + + $handlerParams = \func_get_args(); + unset($handlerParams[1]); + + // Fire all events + foreach ($tables as $table) { + // Fire before events for :any table + $action = $this->getEvent($event, $table); + if ($action !== null) { + + // Make an event id, with event type and table + $eventId = $event . $table; + + // Fire event and add to fired list + $this->firedEvents[] = $eventId; + $result = \call_user_func_array($action, $handlerParams); + if ($result !== null) { + return $result; + } + } + } + + return null; + } + + /** + * @param string $event + * @param string|Raw|null $table + * + * @return \Closure|null + */ + public function getEvent(string $event, $table = null) { + $table = $table ?? static::TABLE_ANY; + + if ($table instanceof Raw) { + return null; + } + + // Find event with wildcard (*) + if (isset($this->events[ $table ]) === true) { + foreach ((array)$this->events[ $table ] as $name => $e) { + if (strpos($name, '*') !== false) { + $name = substr($name, 0, strpos($name, '*')); + if (stripos($event, $name) !== false) { + return $e; + } + } + } + } + + return $this->events[ $table ][ $event ] ?? null; + } + + /** + * @return array + */ + public function getEvents(): array { + return $this->events; + } + + /** + * @param string $event + * @param string $table + * @param \Closure $action + * + * @return void + */ + public function registerEvent(string $event, string $table = null, \Closure $action) { + $this->events[ $table ?? static::TABLE_ANY ][ $event ] = $action; + } + + /** + * @param string $event + * @param string $table + * + * @return void + */ + public function removeEvent($event, $table = null) { + unset($this->events[ $table ?? static::TABLE_ANY ][ $event ]); + } } diff --git a/src/Pecee/Pixie/Exception.php b/src/Pecee/Pixie/Exception.php index 0e19686..a20c6db 100644 --- a/src/Pecee/Pixie/Exception.php +++ b/src/Pecee/Pixie/Exception.php @@ -7,7 +7,6 @@ * * @package Pecee\Pixie */ -class Exception extends \Exception -{ +class Exception extends \Exception { } diff --git a/src/Pecee/Pixie/QueryBuilder/Adapters/BaseAdapter.php b/src/Pecee/Pixie/QueryBuilder/Adapters/BaseAdapter.php index e5dc300..d9137ce 100644 --- a/src/Pecee/Pixie/QueryBuilder/Adapters/BaseAdapter.php +++ b/src/Pecee/Pixie/QueryBuilder/Adapters/BaseAdapter.php @@ -12,594 +12,567 @@ * * @package Pecee\Pixie\QueryBuilder\Adapters */ -abstract class BaseAdapter -{ - /** - * @var string - */ - const SANITIZER = '`'; - - /** - * @var \Pecee\Pixie\Connection - */ - protected $connection; - - /** - * @var \Viocon\Container - */ - protected $container; - - /** - * BaseAdapter constructor. - * - * @param \Pecee\Pixie\Connection $connection - */ - public function __construct(Connection $connection) - { - $this->connection = $connection; - $this->container = $this->connection->getContainer(); - } - - /** - * Array concatenating method, like implode. - * But it does wrap sanitizer and trims last glue - * - * @param array $pieces - * @param string $glue - * @param bool $wrapSanitizer - * - * @return string - */ - protected function arrayStr(array $pieces, $glue = ',', $wrapSanitizer = true): string - { - $str = ''; - foreach ($pieces as $key => $piece) { - if ($wrapSanitizer === true) { - $piece = $this->wrapSanitizer($piece); - } - - if (\is_int($key) === false) { - $piece = ($wrapSanitizer ? $this->wrapSanitizer($key) : $key) . ' AS ' . $piece; - } - - $str .= $piece . $glue; - } - - return trim($str, $glue); - } - - /** - * Build generic criteria string and bindings from statements, like "a = b and c = ?" - * - * @param array $statements - * @param bool $bindValues - * - * @throws Exception - * @return array - */ - protected function buildCriteria(array $statements, $bindValues = true): array - { - $criteria = ''; - $bindings = [[]]; - - foreach ($statements as $statement) { - - $key = $this->wrapSanitizer($statement['key']); - $value = $statement['value']; - - if ($value === null && $key instanceof \Closure) { - - /** - * We have a closure, a nested criteria - * Build a new NestedCriteria class, keep it by reference so any changes made in the closure should reflect here - */ - - /* @var $nestedCriteria NestedCriteria */ - $nestedCriteria = $this->container->build( - NestedCriteria::class, - [$this->connection] - ); - - // Call the closure with our new nestedCriteria object - $key($nestedCriteria); - - // Get the criteria only query from the nestedCriteria object - $queryObject = $nestedCriteria->getQuery('criteriaOnly', true); - - // Merge the bindings we get from nestedCriteria object - $bindings[] = $queryObject->getBindings(); - - // Append the sql we get from the nestedCriteria object - $criteria .= $statement['joiner'] . ' (' . $queryObject->getSql() . ') '; - - continue; - } - - if (\is_array($value) === true) { - - // Where in or between like query - $criteria .= $statement['joiner'] . ' ' . $key . ' ' . $statement['operator']; - - if ($statement['operator'] === 'BETWEEN') { - $bindings[] = [$statement['value']]; - $criteria .= ' ? AND ? '; - } else { - $valuePlaceholder = ''; - foreach ((array)$statement['value'] as $subValue) { - $valuePlaceholder .= '?, '; - $bindings[] = [$subValue]; - } - - $valuePlaceholder = trim($valuePlaceholder, ', '); - $criteria .= ' (' . $valuePlaceholder . ') '; - } - - continue; - - } - - if ($value instanceof Raw) { - $criteria .= "{$statement['joiner']} {$key} {$statement['operator']} $value "; - continue; - } - - - // Usual where like criteria - if ($bindValues === false) { - - // Specially for joins - we are not binding values, lets sanitize then - $value = $this->wrapSanitizer($value); - $criteria .= $statement['joiner'] . ' ' . $key . ' ' . $statement['operator'] . ' ' . $value . ' '; - - continue; - } - - if ($statement['key'] instanceof Raw) { - - if ($statement['operator'] !== null) { - $criteria .= "{$statement['joiner']} {$key} {$statement['operator']} ? "; - $bindings[] = $statement['key']->getBindings(); - $bindings[] = [$value]; - } else { - $criteria .= $statement['joiner'] . ' ' . $key . ' '; - $bindings[] = $statement['key']->getBindings(); - } - - continue; - - } - - // WHERE - $valuePlaceholder = '?'; - $bindings[] = [$value]; - $criteria .= $statement['joiner'] . ' ' . $key . ' ' . $statement['operator'] . ' ' . $valuePlaceholder . ' '; - } - - // Clear all white spaces, and, or from beginning and white spaces from ending - $criteria = \preg_replace('/^(\s?AND ?|\s?OR ?)|\s$/i', '', $criteria); - - return [$criteria, array_merge(...$bindings)]; - } - - /** - * Build criteria string and binding with various types added, like WHERE and Having - * - * @param array $statements - * @param string $key - * @param string $type - * @param bool $bindValues - * - * @return array - * @throws Exception - */ - protected function buildCriteriaWithType(array $statements, $key, $type, $bindValues = true): array - { - $criteria = ''; - $bindings = []; - - if (isset($statements[$key]) === true) { - // Get the generic/adapter agnostic criteria string from parent - list($criteria, $bindings) = $this->buildCriteria($statements[$key], $bindValues); - - if ($criteria !== null) { - $criteria = $type . ' ' . $criteria; - } - } - - return [$criteria, $bindings]; - } - - /** - * Build join string - * - * @param array $statements - * - * @return string - * @throws Exception - */ - protected function buildJoin(array $statements): string - { - $sql = ''; - - if (\array_key_exists('joins', $statements) === false || \count($statements['joins']) === 0) { - return $sql; - } - - foreach ((array)$statements['joins'] as $joinArr) { - if (\is_array($joinArr['table']) === true) { - list($mainTable, $aliasTable) = $joinArr['table']; - $table = $this->wrapSanitizer($mainTable) . ' AS ' . $this->wrapSanitizer($aliasTable); - } else { - $table = $joinArr['table'] instanceof Raw ? (string)$joinArr['table'] : $this->wrapSanitizer($joinArr['table']); - } - - /* @var $joinBuilder \Pecee\Pixie\QueryBuilder\QueryBuilderHandler */ - $joinBuilder = $joinArr['joinBuilder']; - - $sqlArr = [ - $sql, - strtoupper($joinArr['type']), - 'JOIN', - $table, - 'ON', - $joinBuilder->getQuery('criteriaOnly', false)->getSql(), - ]; - - $sql = $this->concatenateQuery($sqlArr); - } - - return $sql; - } - - /** - * Join different part of queries with a space. - * - * @param array $pieces - * - * @return string - */ - protected function concatenateQuery(array $pieces): string - { - $str = ''; - foreach ($pieces as $piece) { - $str = trim($str) . ' ' . trim($piece); - } - - return trim($str); - } - - /** - * Build just criteria part of the query - * - * @param array $statements - * @param bool $bindValues - * - * @return array - * @throws Exception - */ - public function criteriaOnly(array $statements, $bindValues = true): array - { - $sql = $bindings = []; - if (isset($statements['criteria']) === false) { - return compact('sql', 'bindings'); - } - - list($sql, $bindings) = $this->buildCriteria($statements['criteria'], $bindValues); - - return compact('sql', 'bindings'); - } - - /** - * Build delete query - * - * @param array $statements - * - * @return array - * @throws Exception - */ - public function delete(array $statements): array - { - $table = end($statements['tables']); - - // WHERE - list($whereCriteria, $whereBindings) = $this->buildCriteriaWithType($statements, 'wheres', 'WHERE'); - - $sqlArray = ['DELETE FROM', $this->wrapSanitizer($table), $whereCriteria]; - $sql = $this->concatenateQuery($sqlArray); - $bindings = $whereBindings; - - return compact('sql', 'bindings'); - } - - /** - * Build a generic insert/ignore/replace query - * - * @param array $statements - * @param array $data - * @param string $type - * - * @return array - * @throws Exception - */ - private function doInsert(array $statements, array $data, $type): array - { - $table = end($statements['tables']); - - $bindings = $keys = $values = []; - - foreach ($data as $key => $value) { - $keys[] = $key; - if ($value instanceof Raw) { - $values[] = (string)$value; - } else { - $values[] = '?'; - $bindings[] = $value; - } - } - - $sqlArray = [ - $type . ' INTO', - $this->wrapSanitizer($table), - '(' . $this->arrayStr($keys) . ')', - 'VALUES', - '(' . $this->arrayStr($values, ',', false) . ')', - ]; - - if (isset($statements['onduplicate']) === true) { - - if (\count($statements['onduplicate']) < 1) { - throw new Exception('No data given.', 4); - } - - list($updateStatement, $updateBindings) = $this->getUpdateStatement($statements['onduplicate']); - $sqlArray[] = 'ON DUPLICATE KEY UPDATE ' . $updateStatement; - $bindings = array_merge($bindings, $updateBindings); - - } - - $sql = $this->concatenateQuery($sqlArray); - - return compact('sql', 'bindings'); - } - - /** - * Build fields assignment part of SET ... or ON DUBLICATE KEY UPDATE ... statements - * - * @param array $data - * - * @return array - */ - private function getUpdateStatement(array $data): array - { - $bindings = []; - $statement = ''; - - foreach ($data as $key => $value) { - - $statement .= $this->wrapSanitizer($key) . '='; - - if ($value instanceof Raw) { - $statement .= $value . ','; - } else { - $statement .= '?,'; - $bindings[] = $value; - } - } - - $statement = trim($statement, ','); - - return [$statement, $bindings]; - } - - /** - * Build insert query - * - * @param array $statements - * @param array $data - * - * @return array - * @throws Exception - */ - public function insert(array $statements, array $data): array - { - return $this->doInsert($statements, $data, 'INSERT'); - } - - /** - * Build insert and ignore query - * - * @param array $statements - * @param array $data - * - * @return array - * @throws Exception - */ - public function insertIgnore(array $statements, array $data): array - { - return $this->doInsert($statements, $data, 'INSERT IGNORE'); - } - - /** - * Build replace query - * - * @param array $statements - * @param array $data - * - * @return array - * @throws Exception - */ - public function replace(array $statements, array $data): array - { - return $this->doInsert($statements, $data, 'REPLACE'); - } - - /** - * Build select query string and bindings - * - * @param array $statements - * - * @throws Exception - * @return array - */ - public function select(array $statements): array - { - if (array_key_exists('selects', $statements) === false) { - $statements['selects'] = ['*']; - } - - // From - $fromEnabled = false; - $tables = ''; - - if (isset($statements['tables']) === true) { - $tables = []; - - foreach ((array)$statements['tables'] as $table) { - if ($table instanceof Raw) { - $t = $table; - } else { - $prefix = $statements['aliases'][$table] ?? null; - - if ($prefix !== null) { - $t = sprintf('`%s` AS `%s`', $table, strtolower($prefix)); - } else { - $t = sprintf('`%s`', $table); - } - } - - $tables[] = $t; - } - - $tables = implode(',', $tables); - $fromEnabled = true; - } - - // SELECT - $selects = $this->arrayStr($statements['selects'], ', '); - - // WHERE - list($whereCriteria, $whereBindings) = $this->buildCriteriaWithType($statements, 'wheres', 'WHERE'); - - // GROUP BY - $groupBys = $this->arrayStr($statements['groupBys'], ', '); - if ($groupBys !== '' && isset($statements['groupBys']) === true) { - $groupBys = 'GROUP BY ' . $groupBys; - } - - // ORDER BY - $orderBys = ''; - if (isset($statements['orderBys']) && \is_array($statements['orderBys'])) { - foreach ($statements['orderBys'] as $orderBy) { - $orderBys .= $this->wrapSanitizer($orderBy['field']) . ' ' . $orderBy['type'] . ', '; - } - - if ($orderBys = trim($orderBys, ', ')) { - $orderBys = 'ORDER BY ' . $orderBys; - } - } - - // LIMIT AND OFFSET - $limit = isset($statements['limit']) ? 'LIMIT ' . $statements['limit'] : ''; - $offset = isset($statements['offset']) ? 'OFFSET ' . $statements['offset'] : ''; - - // HAVING - list($havingCriteria, $havingBindings) = $this->buildCriteriaWithType($statements, 'havings', 'HAVING'); - - // JOINS - $joinString = $this->buildJoin($statements); - - $sqlArray = [ - 'SELECT' . (isset($statements['distinct']) ? ' DISTINCT' : ''), - $selects, - $fromEnabled ? 'FROM' : '', - $tables, - $joinString, - $whereCriteria, - $groupBys, - $havingCriteria, - $orderBys, - $limit, - $offset, - ]; - - $sql = $this->concatenateQuery($sqlArray); - - $bindings = array_merge( - $whereBindings, - $havingBindings - ); - - return compact('sql', 'bindings'); - } - - /** - * Build update query - * - * @param array $statements - * @param array $data - * - * @return array - * @throws Exception - */ - public function update(array $statements, array $data): array - { - if (\count($data) < 1) { - throw new Exception('No data given.', 4); - } - - $table = end($statements['tables']); - - // UPDATE - list($updateStatement, $bindings) = $this->getUpdateStatement($data); - - // WHERE - list($whereCriteria, $whereBindings) = $this->buildCriteriaWithType($statements, 'wheres', 'WHERE'); - - // LIMIT - $limit = isset($statements['limit']) ? 'LIMIT ' . $statements['limit'] : ''; - - $sqlArray = [ - 'UPDATE', - $this->wrapSanitizer($table), - 'SET ' . $updateStatement, - $whereCriteria, - $limit, - ]; - - $sql = $this->concatenateQuery($sqlArray); - - $bindings = array_merge($bindings, $whereBindings); - - return compact('sql', 'bindings'); - } - - /** - * Wrap values with adapter's sanitizer like, '`' - * - * @param string|Raw|\Closure $value - * - * @return string|\Closure - */ - public function wrapSanitizer($value) - { - // Its a raw query, just cast as string, object has __toString() - if ($value instanceof Raw) { - return (string)$value; - } - - if ($value instanceof \Closure) { - return $value; - } - - // Separate our table and fields which are joined with a ".", like my_table.id - $valueArr = explode('.', $value, 2); - - foreach ($valueArr as $key => $subValue) { - // Don't wrap if we have *, which is not a usual field - $valueArr[$key] = trim($subValue) === '*' ? $subValue : static::SANITIZER . $subValue . static::SANITIZER; - } - - // Join these back with "." and return - return implode('.', $valueArr); - } +abstract class BaseAdapter { + /** + * @var string + */ + const SANITIZER = '`'; + + /** + * @var \Pecee\Pixie\Connection + */ + protected $connection; + + /** + * BaseAdapter constructor. + * + * @param \Pecee\Pixie\Connection $connection + */ + public function __construct(Connection $connection) { + $this->connection = $connection; + } + + /** + * Array concatenating method, like implode. + * But it does wrap sanitizer and trims last glue + * + * @param array $pieces + * @param string $glue + * @param bool $wrapSanitizer + * + * @return string + */ + protected function arrayStr(array $pieces, $glue = ',', $wrapSanitizer = true): string { + $str = ''; + foreach ($pieces as $key => $piece) { + if ($wrapSanitizer === true) { + $piece = $this->wrapSanitizer($piece); + } + + if (\is_int($key) === false) { + $piece = ($wrapSanitizer ? $this->wrapSanitizer($key) : $key) . ' AS ' . $piece; + } + + $str .= $piece . $glue; + } + + return trim($str, $glue); + } + + /** + * Build generic criteria string and bindings from statements, like "a = b and c = ?" + * + * @param array $statements + * @param bool $bindValues + * + * @throws Exception + * @return array + */ + protected function buildCriteria(array $statements, $bindValues = true): array { + $criteria = ''; + $bindings = [[]]; + + foreach ($statements as $statement) { + + $key = $this->wrapSanitizer($statement['key']); + $value = $statement['value']; + + if ($value === null && $key instanceof \Closure) { + + /** + * We have a closure, a nested criteria + * Build a new NestedCriteria class, keep it by reference so any changes made in the closure should reflect here + */ + + $nestedCriteria = new NestedCriteria($this->connection); + + // Call the closure with our new nestedCriteria object + $key($nestedCriteria); + + // Get the criteria only query from the nestedCriteria object + $queryObject = $nestedCriteria->getQuery('criteriaOnly', true); + + // Merge the bindings we get from nestedCriteria object + $bindings[] = $queryObject->getBindings(); + + // Append the sql we get from the nestedCriteria object + $criteria .= $statement['joiner'] . ' (' . $queryObject->getSql() . ') '; + + continue; + } + + if (\is_array($value) === true) { + + // Where in or between like query + $criteria .= $statement['joiner'] . ' ' . $key . ' ' . $statement['operator']; + + if ($statement['operator'] === 'BETWEEN') { + $bindings[] = [$statement['value']]; + $criteria .= ' ? AND ? '; + } else { + $valuePlaceholder = ''; + foreach ((array)$statement['value'] as $subValue) { + $valuePlaceholder .= '?, '; + $bindings[] = [$subValue]; + } + + $valuePlaceholder = trim($valuePlaceholder, ', '); + $criteria .= ' (' . $valuePlaceholder . ') '; + } + + continue; + + } + + if ($value instanceof Raw) { + $criteria .= "{$statement['joiner']} {$key} {$statement['operator']} $value "; + continue; + } + + + // Usual where like criteria + if ($bindValues === false) { + + // Specially for joins - we are not binding values, lets sanitize then + $value = $this->wrapSanitizer($value); + $criteria .= $statement['joiner'] . ' ' . $key . ' ' . $statement['operator'] . ' ' . $value . ' '; + + continue; + } + + if ($statement['key'] instanceof Raw) { + + if ($statement['operator'] !== null) { + $criteria .= "{$statement['joiner']} {$key} {$statement['operator']} ? "; + $bindings[] = $statement['key']->getBindings(); + $bindings[] = [$value]; + } else { + $criteria .= $statement['joiner'] . ' ' . $key . ' '; + $bindings[] = $statement['key']->getBindings(); + } + + continue; + + } + + // WHERE + $valuePlaceholder = '?'; + $bindings[] = [$value]; + $criteria .= $statement['joiner'] . ' ' . $key . ' ' . $statement['operator'] . ' ' . $valuePlaceholder . ' '; + } + + // Clear all white spaces, and, or from beginning and white spaces from ending + $criteria = \preg_replace('/^(\s?AND ?|\s?OR ?)|\s$/i', '', $criteria); + + return [$criteria, array_merge(...$bindings)]; + } + + /** + * Build criteria string and binding with various types added, like WHERE and Having + * + * @param array $statements + * @param string $key + * @param string $type + * @param bool $bindValues + * + * @return array + * @throws Exception + */ + protected function buildCriteriaWithType(array $statements, $key, $type, $bindValues = true): array { + $criteria = ''; + $bindings = []; + + if (isset($statements[ $key ]) === true) { + // Get the generic/adapter agnostic criteria string from parent + list($criteria, $bindings) = $this->buildCriteria($statements[ $key ], $bindValues); + + if ($criteria !== null) { + $criteria = $type . ' ' . $criteria; + } + } + + return [$criteria, $bindings]; + } + + /** + * Build join string + * + * @param array $statements + * + * @return string + * @throws Exception + */ + protected function buildJoin(array $statements): string { + $sql = ''; + + if (\array_key_exists('joins', $statements) === false || \count($statements['joins']) === 0) { + return $sql; + } + + foreach ((array)$statements['joins'] as $joinArr) { + if (\is_array($joinArr['table']) === true) { + list($mainTable, $aliasTable) = $joinArr['table']; + $table = $this->wrapSanitizer($mainTable) . ' AS ' . $this->wrapSanitizer($aliasTable); + } else { + $table = $joinArr['table'] instanceof Raw ? (string)$joinArr['table'] : $this->wrapSanitizer($joinArr['table']); + } + + /* @var $joinBuilder \Pecee\Pixie\QueryBuilder\QueryBuilderHandler */ + $joinBuilder = $joinArr['joinBuilder']; + + $sqlArr = [ + $sql, + strtoupper($joinArr['type']), + 'JOIN', + $table, + 'ON', + $joinBuilder->getQuery('criteriaOnly', false)->getSql(), + ]; + + $sql = $this->concatenateQuery($sqlArr); + } + + return $sql; + } + + /** + * Join different part of queries with a space. + * + * @param array $pieces + * + * @return string + */ + protected function concatenateQuery(array $pieces): string { + $str = ''; + foreach ($pieces as $piece) { + $str = trim($str) . ' ' . trim($piece); + } + + return trim($str); + } + + /** + * Build just criteria part of the query + * + * @param array $statements + * @param bool $bindValues + * + * @return array + * @throws Exception + */ + public function criteriaOnly(array $statements, $bindValues = true): array { + $sql = $bindings = []; + if (isset($statements['criteria']) === false) { + return compact('sql', 'bindings'); + } + + list($sql, $bindings) = $this->buildCriteria($statements['criteria'], $bindValues); + + return compact('sql', 'bindings'); + } + + /** + * Build delete query + * + * @param array $statements + * + * @return array + * @throws Exception + */ + public function delete(array $statements): array { + $table = end($statements['tables']); + + // WHERE + list($whereCriteria, $whereBindings) = $this->buildCriteriaWithType($statements, 'wheres', 'WHERE'); + + $sqlArray = ['DELETE FROM', $this->wrapSanitizer($table), $whereCriteria]; + $sql = $this->concatenateQuery($sqlArray); + $bindings = $whereBindings; + + return compact('sql', 'bindings'); + } + + /** + * Build a generic insert/ignore/replace query + * + * @param array $statements + * @param array $data + * @param string $type + * + * @return array + * @throws Exception + */ + private function doInsert(array $statements, array $data, $type): array { + $table = end($statements['tables']); + + $bindings = $keys = $values = []; + + foreach ($data as $key => $value) { + $keys[] = $key; + if ($value instanceof Raw) { + $values[] = (string)$value; + } else { + $values[] = '?'; + $bindings[] = $value; + } + } + + $sqlArray = [ + $type . ' INTO', + $this->wrapSanitizer($table), + '(' . $this->arrayStr($keys) . ')', + 'VALUES', + '(' . $this->arrayStr($values, ',', false) . ')', + ]; + + if (isset($statements['onduplicate']) === true) { + + if (\count($statements['onduplicate']) < 1) { + throw new Exception('No data given.', 4); + } + + list($updateStatement, $updateBindings) = $this->getUpdateStatement($statements['onduplicate']); + $sqlArray[] = 'ON DUPLICATE KEY UPDATE ' . $updateStatement; + $bindings = array_merge($bindings, $updateBindings); + + } + + $sql = $this->concatenateQuery($sqlArray); + + return compact('sql', 'bindings'); + } + + /** + * Build fields assignment part of SET ... or ON DUBLICATE KEY UPDATE ... statements + * + * @param array $data + * + * @return array + */ + private function getUpdateStatement(array $data): array { + $bindings = []; + $statement = ''; + + foreach ($data as $key => $value) { + + $statement .= $this->wrapSanitizer($key) . '='; + + if ($value instanceof Raw) { + $statement .= $value . ','; + } else { + $statement .= '?,'; + $bindings[] = $value; + } + } + + $statement = trim($statement, ','); + + return [$statement, $bindings]; + } + + /** + * Build insert query + * + * @param array $statements + * @param array $data + * + * @return array + * @throws Exception + */ + public function insert(array $statements, array $data): array { + return $this->doInsert($statements, $data, 'INSERT'); + } + + /** + * Build insert and ignore query + * + * @param array $statements + * @param array $data + * + * @return array + * @throws Exception + */ + public function insertIgnore(array $statements, array $data): array { + return $this->doInsert($statements, $data, 'INSERT IGNORE'); + } + + /** + * Build replace query + * + * @param array $statements + * @param array $data + * + * @return array + * @throws Exception + */ + public function replace(array $statements, array $data): array { + return $this->doInsert($statements, $data, 'REPLACE'); + } + + /** + * Build select query string and bindings + * + * @param array $statements + * + * @throws Exception + * @return array + */ + public function select(array $statements): array { + if (array_key_exists('selects', $statements) === false) { + $statements['selects'] = ['*']; + } + + // From + $fromEnabled = false; + $tables = ''; + + if (isset($statements['tables']) === true) { + $tables = []; + + foreach ((array)$statements['tables'] as $table) { + if ($table instanceof Raw) { + $t = $table; + } else { + $prefix = $statements['aliases'][ $table ] ?? null; + + if ($prefix !== null) { + $t = sprintf('`%s` AS `%s`', $table, strtolower($prefix)); + } else { + $t = sprintf('`%s`', $table); + } + } + + $tables[] = $t; + } + + $tables = implode(',', $tables); + $fromEnabled = true; + } + + // SELECT + $selects = $this->arrayStr($statements['selects'], ', '); + + // WHERE + list($whereCriteria, $whereBindings) = $this->buildCriteriaWithType($statements, 'wheres', 'WHERE'); + + // GROUP BY + $groupBys = $this->arrayStr($statements['groupBys'], ', '); + if ($groupBys !== '' && isset($statements['groupBys']) === true) { + $groupBys = 'GROUP BY ' . $groupBys; + } + + // ORDER BY + $orderBys = ''; + if (isset($statements['orderBys']) && \is_array($statements['orderBys'])) { + foreach ($statements['orderBys'] as $orderBy) { + $orderBys .= $this->wrapSanitizer($orderBy['field']) . ' ' . $orderBy['type'] . ', '; + } + + if ($orderBys = trim($orderBys, ', ')) { + $orderBys = 'ORDER BY ' . $orderBys; + } + } + + // LIMIT AND OFFSET + $limit = isset($statements['limit']) ? 'LIMIT ' . $statements['limit'] : ''; + $offset = isset($statements['offset']) ? 'OFFSET ' . $statements['offset'] : ''; + + // HAVING + list($havingCriteria, $havingBindings) = $this->buildCriteriaWithType($statements, 'havings', 'HAVING'); + + // JOINS + $joinString = $this->buildJoin($statements); + + $sqlArray = [ + 'SELECT' . (isset($statements['distinct']) ? ' DISTINCT' : ''), + $selects, + $fromEnabled ? 'FROM' : '', + $tables, + $joinString, + $whereCriteria, + $groupBys, + $havingCriteria, + $orderBys, + $limit, + $offset, + ]; + + $sql = $this->concatenateQuery($sqlArray); + + $bindings = array_merge( + $whereBindings, + $havingBindings + ); + + return compact('sql', 'bindings'); + } + + /** + * Build update query + * + * @param array $statements + * @param array $data + * + * @return array + * @throws Exception + */ + public function update(array $statements, array $data): array { + if (\count($data) < 1) { + throw new Exception('No data given.', 4); + } + + $table = end($statements['tables']); + + // UPDATE + list($updateStatement, $bindings) = $this->getUpdateStatement($data); + + // WHERE + list($whereCriteria, $whereBindings) = $this->buildCriteriaWithType($statements, 'wheres', 'WHERE'); + + // LIMIT + $limit = isset($statements['limit']) ? 'LIMIT ' . $statements['limit'] : ''; + + $sqlArray = [ + 'UPDATE', + $this->wrapSanitizer($table), + 'SET ' . $updateStatement, + $whereCriteria, + $limit, + ]; + + $sql = $this->concatenateQuery($sqlArray); + + $bindings = array_merge($bindings, $whereBindings); + + return compact('sql', 'bindings'); + } + + /** + * Wrap values with adapter's sanitizer like, '`' + * + * @param string|Raw|\Closure $value + * + * @return string|\Closure + */ + public function wrapSanitizer($value) { + // Its a raw query, just cast as string, object has __toString() + if ($value instanceof Raw) { + return (string)$value; + } + + if ($value instanceof \Closure) { + return $value; + } + + // Separate our table and fields which are joined with a ".", like my_table.id + $valueArr = explode('.', $value, 2); + + foreach ($valueArr as $key => $subValue) { + // Don't wrap if we have *, which is not a usual field + $valueArr[ $key ] = trim($subValue) === '*' ? $subValue : static::SANITIZER . $subValue . static::SANITIZER; + } + + // Join these back with "." and return + return implode('.', $valueArr); + } } diff --git a/src/Pecee/Pixie/QueryBuilder/Adapters/Mysql.php b/src/Pecee/Pixie/QueryBuilder/Adapters/Mysql.php index 9c7da48..b3d2af2 100644 --- a/src/Pecee/Pixie/QueryBuilder/Adapters/Mysql.php +++ b/src/Pecee/Pixie/QueryBuilder/Adapters/Mysql.php @@ -7,10 +7,9 @@ * * @package Pecee\Pixie\QueryBuilder\Adapters */ -class Mysql extends BaseAdapter -{ - /** - * @var string - */ - const SANITIZER = '`'; +class Mysql extends BaseAdapter { + /** + * @var string + */ + const SANITIZER = '`'; } diff --git a/src/Pecee/Pixie/QueryBuilder/Adapters/Pgsql.php b/src/Pecee/Pixie/QueryBuilder/Adapters/Pgsql.php index 06deb67..c875850 100644 --- a/src/Pecee/Pixie/QueryBuilder/Adapters/Pgsql.php +++ b/src/Pecee/Pixie/QueryBuilder/Adapters/Pgsql.php @@ -7,10 +7,9 @@ * * @package Pecee\Pixie\QueryBuilder\Adapters */ -class Pgsql extends BaseAdapter -{ - /** - * @var string - */ - const SANITIZER = '"'; +class Pgsql extends BaseAdapter { + /** + * @var string + */ + const SANITIZER = '"'; } diff --git a/src/Pecee/Pixie/QueryBuilder/Adapters/Sqlite.php b/src/Pecee/Pixie/QueryBuilder/Adapters/Sqlite.php index 5cdff63..9efecb7 100644 --- a/src/Pecee/Pixie/QueryBuilder/Adapters/Sqlite.php +++ b/src/Pecee/Pixie/QueryBuilder/Adapters/Sqlite.php @@ -7,10 +7,9 @@ * * @package Pecee\Pixie\QueryBuilder\Adapters */ -class Sqlite extends BaseAdapter -{ - /** - * @var string - */ - const SANITIZER = '"'; +class Sqlite extends BaseAdapter { + /** + * @var string + */ + const SANITIZER = '"'; } diff --git a/src/Pecee/Pixie/QueryBuilder/IQueryBuilderHandler.php b/src/Pecee/Pixie/QueryBuilder/IQueryBuilderHandler.php index 4441a67..4954840 100644 --- a/src/Pecee/Pixie/QueryBuilder/IQueryBuilderHandler.php +++ b/src/Pecee/Pixie/QueryBuilder/IQueryBuilderHandler.php @@ -1,4 +1,5 @@ joinHandler($key, $operator, $value); - } +class JoinBuilder extends QueryBuilderHandler { + /** + * @param string|Raw|\Closure $key + * @param string|Raw|\Closure $operator + * @param string|Raw|\Closure $value + * + * @return static + */ + public function on($key, $operator, $value) { + return $this->joinHandler($key, $operator, $value); + } - /** - * @param string|Raw|\Closure $key - * @param string|Raw|\Closure $operator - * @param string|Raw|\Closure $value - * @return static - */ - public function orOn($key, $operator, $value) - { - return $this->joinHandler($key, $operator, $value, 'OR'); - } + /** + * @param string|Raw|\Closure $key + * @param string|Raw|\Closure $operator + * @param string|Raw|\Closure $value + * + * @return static + */ + public function orOn($key, $operator, $value) { + return $this->joinHandler($key, $operator, $value, 'OR'); + } - /** - * @param string|Raw|\Closure $key - * @param string|Raw|\Closure|null $operator - * @param string|Raw|\Closure|null $value - * @param string $joiner - * @return static - */ - protected function joinHandler($key, $operator = null, $value = null, $joiner = 'AND') - { - $key = $this->addTablePrefix($key); - $value = $this->addTablePrefix($value); - $this->statements['criteria'][] = compact('key', 'operator', 'value', 'joiner'); + /** + * @param string|Raw|\Closure $key + * @param string|Raw|\Closure|null $operator + * @param string|Raw|\Closure|null $value + * @param string $joiner + * + * @return static + */ + protected function joinHandler($key, $operator = null, $value = null, $joiner = 'AND') { + $key = $this->addTablePrefix($key); + $value = $this->addTablePrefix($value); + $this->statements['criteria'][] = compact('key', 'operator', 'value', 'joiner'); - return $this; - } + return $this; + } } diff --git a/src/Pecee/Pixie/QueryBuilder/NestedCriteria.php b/src/Pecee/Pixie/QueryBuilder/NestedCriteria.php index 18a334c..265acb5 100644 --- a/src/Pecee/Pixie/QueryBuilder/NestedCriteria.php +++ b/src/Pecee/Pixie/QueryBuilder/NestedCriteria.php @@ -7,21 +7,19 @@ * * @package Pecee\Pixie\QueryBuilder */ -class NestedCriteria extends QueryBuilderHandler -{ - /** - * @param string|Raw|\Closure $key - * @param string|null $operator - * @param string|Raw|\Closure|null $value - * @param string $joiner - * - * @return static - */ - protected function whereHandler($key, string $operator = null, $value = null, $joiner = 'AND'): IQueryBuilderHandler - { - $key = $this->addTablePrefix($key); - $this->statements['criteria'][] = compact('key', 'operator', 'value', 'joiner'); +class NestedCriteria extends QueryBuilderHandler { + /** + * @param string|Raw|\Closure $key + * @param string|Raw|\Closure|null $operator + * @param string|Raw|\Closure|null $value + * @param string $joiner + * + * @return static + */ + protected function whereHandler($key, string $operator = null, $value = null, $joiner = 'AND'): IQueryBuilderHandler { + $key = $this->addTablePrefix($key); + $this->statements['criteria'][] = compact('key', 'operator', 'value', 'joiner'); - return $this; - } + return $this; + } } diff --git a/src/Pecee/Pixie/QueryBuilder/QueryBuilderHandler.php b/src/Pecee/Pixie/QueryBuilder/QueryBuilderHandler.php index e0dd0a6..bd36926 100644 --- a/src/Pecee/Pixie/QueryBuilder/QueryBuilderHandler.php +++ b/src/Pecee/Pixie/QueryBuilder/QueryBuilderHandler.php @@ -4,6 +4,7 @@ use PDO; use Pecee\Pixie\Connection; +use Pecee\Pixie\EventHandler; use Pecee\Pixie\Exception; /** @@ -11,1391 +12,1270 @@ * * @package Pecee\Pixie\QueryBuilder */ -class QueryBuilderHandler implements IQueryBuilderHandler -{ - /** - * Event name - * - * @var string - */ - const EVENT_BEFORE_DELETE = 'before-delete'; - /** - * Event name - * - * @var string - */ - const EVENT_BEFORE_INSERT = 'before-insert'; - /** - * Event name - * - * @var string - */ - const EVENT_BEFORE_UPDATE = 'before-update'; - /** - * Event name - * - * @var string - */ - const EVENT_BEFORE_SELECT = 'before-select'; - /** - * Event name - * - * @var string - */ - const EVENT_AFTER_DELETE = 'after-delete'; - /** - * Event name - * - * @var string - */ - const EVENT_AFTER_INSERT = 'after-insert'; - /** - * Event name - * - * @var string - */ - const EVENT_AFTER_UPDATE = 'after-update'; - /** - * Event name - * - * @var string - */ - const EVENT_AFTER_SELECT = 'after-select'; - - /** - * @var \Viocon\Container - */ - protected $container; - - /** - * @var Connection - */ - protected $connection; - - /** - * @var array - */ - protected $statements = [ - 'groupBys' => [], - ]; - - /** - * @var PDO - */ - protected $pdo; - - /** - * @var null|\PDOStatement - */ - protected $pdoStatement; - - /** - * @var null|string - */ - protected $tablePrefix; - - /** - * @var \Pecee\Pixie\QueryBuilder\Adapters\BaseAdapter - */ - protected $adapterInstance; - - /** - * The PDO fetch parameters to use - * - * @var array - */ - protected $fetchParameters = [\PDO::FETCH_OBJ]; - - /** - * @var string - */ - protected $adapter; - - /** - * @var array - */ - protected $adapterConfig; - - /** - * @param \Pecee\Pixie\Connection|null $connection - * - * @throws \Pecee\Pixie\Exception - */ - public function __construct(Connection $connection = null) - { - $this->connection = $connection ?? Connection::getStoredConnection(); - - if ($this->connection === null) { - throw new Exception('No database connection found.', 1); - } - - $this->container = $this->connection->getContainer(); - $this->pdo = $this->connection->getPdoInstance(); - $this->adapter = $this->connection->getAdapter(); - $this->adapterConfig = $this->connection->getAdapterConfig(); - - if (isset($this->adapterConfig['prefix']) === true) { - $this->tablePrefix = $this->adapterConfig['prefix']; - } - - // Query builder adapter instance - $this->adapterInstance = $this->container->build( - '\Pecee\Pixie\QueryBuilder\Adapters\\' . ucfirst($this->adapter), - [$this->connection] - ); - - $this->pdo->setAttribute(PDO::ATTR_ERRMODE, PDO::ERRMODE_EXCEPTION); - - // PDO will parse parameter datatypes automatically - $this->pdo->setAttribute(PDO::ATTR_EMULATE_PREPARES, true); - } - - /** - * Add new statement to statement-list - * - * @param string $key - * @param mixed $value - * @return void - */ - protected function addStatement(string $key, $value) - { - if (array_key_exists($key, $this->statements) === false) { - $this->statements[$key] = (array)$value; - } else { - $this->statements[$key] = array_merge($this->statements[$key], (array)$value); - } - } - - /** - * Add table prefix (if given) on given string. - * - * @param string|array|Raw|\Closure $values - * @param bool $tableFieldMix If we have mixes of field and table names with a "." - * - * @return array|string - */ - public function addTablePrefix($values, bool $tableFieldMix = true) - { - if ($this->tablePrefix === null) { - return $values; - } - - // $value will be an array and we will add prefix to all table names - // If supplied value is not an array then make it one - - $single = false; - if (\is_array($values) === false) { - $values = [$values]; - - // We had single value, so should return a single value - $single = true; - } - - $return = []; - - foreach ($values as $key => $value) { - // It's a raw query, just add it to our return array and continue next - if ($value instanceof Raw || $value instanceof \Closure) { - $return[$key] = $value; - continue; - } - - // If key is not integer, it is likely a alias mapping, so we need to change prefix target - $target = &$value; - - if (\is_int($key) === false) { - $target = &$key; - } - - if ($tableFieldMix === false || ($tableFieldMix && strpos($target, '.') !== false)) { - $target = $this->tablePrefix . $target; - } - - $return[$key] = $value; - } - - // If we had single value then we should return a single value (end value of the array) - return $single ? end($return) : $return; - } - - /** - * Performs special queries like COUNT, SUM etc based on the current query. - * - * @param string $type - * - * @throws Exception - * @return int - */ - protected function aggregate(string $type): int - { - // Get the current selects - $mainSelects = $this->statements['selects'] ?? null; - - // Replace select with a scalar value like `count` - $this->statements['selects'] = [$this->raw($type . '(*) AS `field`')]; - $row = $this->get(); - - // Set the select as it was - if ($mainSelects !== null) { - $this->statements['selects'] = $mainSelects; - } else { - unset($this->statements['selects']); - } - - if (isset($row[0]) === true) { - if (\is_array($row[0]) === true) { - return (int)$row[0]['field']; - } - if (\is_object($row[0]) === true) { - return (int)$row[0]->field; - } - } - - return 0; - } - - /** - * Add or change table alias - * Example: table AS alias - * - * @param string $alias - * @param string $table - * - * @return static - */ - public function alias(string $alias, string $table = null): IQueryBuilderHandler - { - if ($table === null && isset($this->statements['tables'][0]) === true) { - $table = $this->statements['tables'][0]; - } else { - $table = $this->tablePrefix . $table; - } - - $this->statements['aliases'][$table] = strtolower($alias); - - return $this; - } - - /** - * Fetch query results as object of specified type - * - * @param string $className - * @param array $constructorArgs - * - * @return static - */ - public function asObject(string $className, array $constructorArgs = []): QueryBuilderHandler - { - return $this->setFetchMode(PDO::FETCH_CLASS, $className, $constructorArgs); - } - - /** - * Get count of rows - * - * @throws Exception - * @return int - */ - public function count(): int - { - // Get the current statements - $originalStatements = $this->statements; - - unset($this->statements['orderBys'], $this->statements['limit'], $this->statements['offset']); - - $count = $this->aggregate('count'); - $this->statements = $originalStatements; - - return $count; - } - - /** - * Forms delete on the current query. - * - * @return \PDOStatement - * @throws Exception - */ - public function delete(): \PDOStatement - { - /* @var $response \PDOStatement */ - $queryObject = $this->getQuery('delete'); - - $this->fireEvents(static::EVENT_BEFORE_DELETE, $queryObject); - - list($response, $executionTime) = $this->statement($queryObject->getSql(), $queryObject->getBindings()); - $this->fireEvents(static::EVENT_AFTER_DELETE, $queryObject, $executionTime); - - return $response; - } - - /** - * Performs insert - * - * @param array $data - * @param string $type - * - * @throws Exception - * @return array|string|null - */ - private function doInsert(array $data, string $type) - { - // Insert single item - - if (\is_array(current($data)) === false) { - $queryObject = $this->getQuery($type, $data); - - $this->fireEvents(static::EVENT_BEFORE_INSERT, $queryObject); - /** - * @var $result \PDOStatement - * @var $executionTime float - */ - list($result, $executionTime) = $this->statement($queryObject->getSql(), $queryObject->getBindings()); - - $insertId = $result->rowCount() === 1 ? $this->pdo->lastInsertId() : null; - $this->fireEvents(static::EVENT_AFTER_INSERT, $queryObject, $insertId, $executionTime); - - return $insertId; - } - - $insertIds = []; - - // If the current batch insert is not in a transaction, we create one... - - if ($this->pdo->inTransaction() === false) { - - $this->transaction(function (Transaction $transaction) use (&$insertIds, $data, $type) { - foreach ($data as $subData) { - $insertIds[] = $transaction->doInsert($subData, $type); - } - }); - - return $insertIds; - } - - // Otherwise insert one by one... - - foreach ($data as $subData) { - $insertIds[] = $this->doInsert($subData, $type); - } - - return $insertIds; - } - - /** - * Find by value and field name. - * - * @param string|int|float $value - * @param string $fieldName - * - * @throws Exception - * @return \stdClass|string|null - */ - public function find($value, $fieldName = 'id') - { - return $this->where($fieldName, '=', $value)->first(); - } - - /** - * Find all by field name and value - * - * @param string $fieldName - * @param string|int|float $value - * - * @throws Exception - * @return array - */ - public function findAll(string $fieldName, $value): array - { - return $this->where($fieldName, '=', $value)->get(); - } - - /** - * Fires event by given event name - * - * @param string $name - * @param ... $parameters - * - * @return mixed|null - */ - public function fireEvents($name, $parameters = null) - { - $params = \func_get_args(); - array_unshift($params, $this); - - return \call_user_func_array([$this->connection->getEventHandler(), 'fireEvents'], $params); - } - - /** - * Returns the first row - * - * @throws Exception - * @return \stdClass|string|null - */ - public function first() - { - $result = $this->limit(1)->get(); - - return ($result !== null && \count($result) > 0) ? $result[0] : null; - } - - /** - * Adds FROM statement to the current query. - * - * @param string|array $tables - * - * @return static - */ - public function from($tables): IQueryBuilderHandler - { - if (\is_array($tables) === false) { - $tables = \func_get_args(); - } - - $tables = $this->addTablePrefix($tables, false); - $this->addStatement('tables', $tables); - - return $this; - } - - /** - * Get all rows - * - * @throws Exception - * @return array - */ - public function get(): array - { - /** - * @var $queryObject \Pecee\Pixie\QueryBuilder\QueryObject - * @var $executionTime float - * @var $start float - * @var $result array - */ - $queryObject = null; - $executionTime = 0; - - if ($this->pdoStatement === null) { - $queryObject = $this->getQuery(); - list($this->pdoStatement, $executionTime) = $this->statement( - $queryObject->getSql(), - $queryObject->getBindings() - ); - } - - $start = microtime(true); - $this->fireEvents(static::EVENT_BEFORE_SELECT, $queryObject); - $result = \call_user_func_array([$this->pdoStatement, 'fetchAll'], $this->fetchParameters); - $executionTime += microtime(true) - $start; - $this->pdoStatement = null; - $this->fireEvents(static::EVENT_AFTER_SELECT, $queryObject, $result, $executionTime); - - return $result; - } - - /** - * Get connection object - * - * @return Connection - */ - public function getConnection(): Connection - { - return $this->connection; - } - - /** - * Get event by event name - * - * @param string $name - * @param string|null $table - * - * @return \Closure|null - */ - public function getEvent(string $name, string $table = null) - { - return $this->connection->getEventHandler()->getEvent($name, $table); - } - - /** - * Returns Query-object. - * - * @param string $type - * @param array|mixed|null $dataToBePassed - * - * @return QueryObject - * @throws Exception - */ - public function getQuery(string $type = 'select', $dataToBePassed = null): QueryObject - { - $allowedTypes = [ - 'select', - 'insert', - 'insertignore', - 'replace', - 'delete', - 'update', - 'criteriaonly', - ]; - - if (\in_array(strtolower($type), $allowedTypes, true) === false) { - throw new Exception($type . ' is not a known type.', 2); - } - - $queryArr = $this->adapterInstance->$type($this->statements, $dataToBePassed); - - return $this->container->build(QueryObject::class, [ - $queryArr['sql'], - $queryArr['bindings'], - $this->pdo - ] - ); - } - - /** - * Returns statements - * - * @return array - */ - public function getStatements(): array - { - return $this->statements; - } - - /** - * Adds GROUP BY to the current query. - * - * @param string|Raw|\Closure|array $field - * - * @return static - */ - public function groupBy($field): IQueryBuilderHandler - { - if (($field instanceof Raw) === false) { - $field = $this->addTablePrefix($field); - } - - if (\is_array($field) === true) { - $this->statements['groupBys'] = array_merge($this->statements['groupBys'], $field); - } else { - $this->statements['groupBys'][] = $field; - } - - return $this; - } - - /** - * Adds HAVING statement to the current query. - * - * @param string|Raw|\Closure $key - * @param string|mixed $operator - * @param string|mixed $value - * @param string $joiner - * - * @return static - */ - public function having($key, $operator, $value, $joiner = 'AND'): IQueryBuilderHandler - { - $key = $this->addTablePrefix($key); - $this->statements['havings'][] = compact('key', 'operator', 'value', 'joiner'); - - return $this; - } - - /** - * Adds new INNER JOIN statement to the current query. - * - * @param string|Raw|\Closure $table - * @param string|Raw|\Closure $key - * @param string|mixed|null $operator - * @param string|Raw|\Closure|null $value - * - * @return static - */ - public function innerJoin($table, $key, $operator = null, $value = null): IQueryBuilderHandler - { - return $this->join($table, $key, $operator, $value); - } - - /** - * Insert key/value array - * - * @param array $data - * - * @throws Exception - * @return array|string - */ - public function insert(array $data) - { - return $this->doInsert($data, 'insert'); - } - - /** - * Insert with ignore key/value array - * - * @param array $data - * - * @throws Exception - * @return array|string - */ - public function insertIgnore($data) - { - return $this->doInsert($data, 'insertignore'); - } - - /** - * Adds new JOIN statement to the current query. - * - * @param string|Raw|\Closure|array $table - * @param string|Raw|\Closure $key - * @param string|null $operator - * @param string|Raw|\Closure $value - * @param string $type - * - * @return static - * ``` - * Examples: - * - basic usage - * ->join('table2', 'table2.person_id', '=', 'table1.id'); - * - * - as alias 'bar' - * ->join(['table2','bar'], 'bar.person_id', '=', 'table1.id'); - * - * - complex usage - * ->join('another_table', function($table) - * { - * $table->on('another_table.person_id', '=', 'my_table.id'); - * $table->on('another_table.person_id2', '=', 'my_table.id2'); - * $table->orOn('another_table.age', '>', $queryBuilder->raw(1)); - * }) - * ``` - */ - public function join($table, $key, $operator = null, $value = null, $type = 'inner'): IQueryBuilderHandler - { - if (($key instanceof \Closure) === false) { - $key = function (JoinBuilder $joinBuilder) use ($key, $operator, $value) { - $joinBuilder->on($key, $operator, $value); - }; - } - - /** - * Build a new JoinBuilder class, keep it by reference so any changes made - * in the closure should reflect here - */ - - $joinBuilder = $this->container->build(JoinBuilder::class, [$this->connection]); - - // Call the closure with our new joinBuilder object - $key($joinBuilder); - $table = $this->addTablePrefix($table, false); - - // Get the criteria only query from the joinBuilder object - $this->statements['joins'][] = compact('type', 'table', 'joinBuilder'); - - return $this; - } - - /** - * Adds new LEFT JOIN statement to the current query. - * - * @param string|Raw|\Closure|array $table - * @param string|Raw|\Closure $key - * @param string|null $operator - * @param string|Raw|\Closure|null $value - * - * @return static - */ - public function leftJoin($table, $key, $operator = null, $value = null): IQueryBuilderHandler - { - return $this->join($table, $key, $operator, $value, 'left'); - } - - /** - * Adds LIMIT statement to the current query. - * - * @param int $limit - * - * @return static - */ - public function limit($limit): IQueryBuilderHandler - { - $this->statements['limit'] = $limit; - - return $this; - } - - /** - * Creates and returns new query. - * - * @param \Pecee\Pixie\Connection|null $connection - * - * @throws \Pecee\Pixie\Exception - * @return static - */ - public function newQuery(Connection $connection = null): IQueryBuilderHandler - { - if ($connection === null) { - $connection = $this->connection; - } - - return new static($connection); - } - - /** - * Adds OFFSET statement to the current query. - * - * @param int $offset - * - * @return static $this - */ - public function offset($offset): IQueryBuilderHandler - { - $this->statements['offset'] = $offset; - - return $this; - } - - /** - * Add on duplicate key statement. - * - * @param string|array $data - * - * @return static - */ - public function onDuplicateKeyUpdate($data): IQueryBuilderHandler - { - $this->addStatement('onduplicate', $data); - - return $this; - } - - /** - * Adds OR HAVING statement to the current query. - * - * @param string|Raw|\Closure $key - * @param string|Raw|\Closure $operator - * @param mixed|Raw|\Closure|null $value - * - * @return static - */ - public function orHaving($key, $operator, $value): IQueryBuilderHandler - { - return $this->having($key, $operator, $value, 'OR'); - } - - /** - * Adds OR WHERE statement to the current query. - * - * @param string|Raw|\Closure $key - * @param string|null $operator - * @param mixed|Raw|\Closure|null $value - * - * @return static - */ - public function orWhere($key, $operator = null, $value = null): IQueryBuilderHandler - { - // If two params are given then assume operator is = - if (\func_num_args() === 2) { - $value = $operator; - $operator = '='; - } - - return $this->whereHandler($key, $operator, $value, 'OR'); - } - - /** - * Adds OR WHERE BETWEEN statement to the current query. - * - * @param string|Raw|\Closure $key - * @param string|integer|float $valueFrom - * @param string|integer|float $valueTo - * - * @return static - */ - public function orWhereBetween($key, $valueFrom, $valueTo): IQueryBuilderHandler - { - return $this->whereHandler($key, 'BETWEEN', [$valueFrom, $valueTo], 'OR'); - } - - /** - * Adds OR WHERE IN statement to the current query. - * - * @param string|Raw|\Closure $key - * @param array|Raw|\Closure $values - * - * @return static - */ - public function orWhereIn($key, $values): IQueryBuilderHandler - { - return $this->whereHandler($key, 'IN', $values, 'OR'); - } - - /** - * Adds OR WHERE NOT statement to the current query. - * - * @param string|Raw|\Closure $key - * @param string|null $operator - * @param mixed|Raw|\Closure|null $value - * - * @return static - */ - public function orWhereNot($key, $operator = null, $value = null): IQueryBuilderHandler - { - // If two params are given then assume operator is = - if (\func_num_args() === 2) { - $value = $operator; - $operator = '='; - } - - return $this->whereHandler($key, $operator, $value, 'OR NOT'); - } - - /** - * Adds or WHERE NOT IN statement to the current query. - * - * @param string|Raw|\Closure $key - * @param array|Raw|\Closure $values - * - * @return static - */ - public function orWhereNotIn($key, $values): IQueryBuilderHandler - { - return $this->whereHandler($key, 'NOT IN', $values, 'OR'); - } - - /** - * Adds OR WHERE NOT NULL statement to the current query. - * - * @param string|Raw|\Closure $key - * - * @return static - */ - public function orWhereNotNull($key): IQueryBuilderHandler - { - return $this->whereNullHandler($key, 'NOT', 'or'); - } - - /** - * Adds OR WHERE NULL statement to the current query. - * - * @param string|Raw|\Closure $key - * - * @return static - */ - public function orWhereNull($key): IQueryBuilderHandler - { - return $this->whereNullHandler($key, '', 'or'); - } - - /** - * Adds ORDER BY statement to the current query. - * - * @param string|Raw|\Closure|array $fields - * @param string $defaultDirection - * - * @return static - */ - public function orderBy($fields, $defaultDirection = 'ASC'): IQueryBuilderHandler - { - if (\is_array($fields) === false) { - $fields = [$fields]; - } - - foreach ((array)$fields as $key => $value) { - $field = $key; - $type = $value; - - if (\is_int($key) === true) { - $field = $value; - $type = $defaultDirection; - } - - if (($field instanceof Raw) === false) { - $field = $this->addTablePrefix($field); - } - - $this->statements['orderBys'][] = compact('field', 'type'); - } - - return $this; - } - - /** - * Return PDO instance - * - * @return PDO - */ - public function pdo(): PDO - { - return $this->pdo; - } - - /** - * Performs query. - * - * @param string $sql - * @param array $bindings - * - * @return static - */ - public function query($sql, array $bindings = []): IQueryBuilderHandler - { - list($this->pdoStatement) = $this->statement($sql, $bindings); - - return $this; - } - - /** - * Adds a raw string to the current query. - * This query will be ignored from any parsing or formatting by the Query builder - * and should be used in conjunction with other statements in the query. - * - * For example: $qb->where('result', '>', $qb->raw('COUNT(`score`))); - * - * @param string $value - * @param array|null|mixed $bindings ... - * - * @return Raw - */ - public function raw($value, $bindings = null): Raw - { - if (\is_array($bindings) === false) { - $bindings = \func_get_args(); - array_shift($bindings); - } - - return $this->container->build(Raw::class, [$value, $bindings]); - } - - /** - * Register new event - * - * @param string $name - * @param string|null $table - * @param \Closure $action - * - * @return void - */ - public function registerEvent($name, $table = null, \Closure $action) - { - $this->connection->getEventHandler()->registerEvent($name, $table, $action); - } - - /** - * Remove event by event-name and/or table - * - * @param string $name - * @param string|null $table - * - * @return void - */ - public function removeEvent($name, $table = null) - { - $this->connection->getEventHandler()->removeEvent($name, $table); - } - - /** - * Replace key/value array - * - * @param array $data - * - * @throws Exception - * @return array|string - */ - public function replace($data) - { - return $this->doInsert($data, 'replace'); - } - - /** - * Adds new right join statement to the current query. - * - * @param string|Raw|\Closure|array $table - * @param string|Raw|\Closure $key - * @param string|null $operator - * @param string|Raw|\Closure|null $value - * - * @return static - */ - public function rightJoin($table, $key, $operator = null, $value = null): IQueryBuilderHandler - { - return $this->join($table, $key, $operator, $value, 'right'); - } - - /** - * Adds fields to select on the current query (defaults is all). - * You can use key/value array to create alias. - * Sub-queries and raw-objects are also supported. - * - * Example: ['field' => 'alias'] will become `field` AS `alias` - * - * @param string|array $fields,... - * - * @return static - */ - public function select($fields): IQueryBuilderHandler - { - if (\is_array($fields) === false) { - $fields = \func_get_args(); - } - - $fields = $this->addTablePrefix($fields); - $this->addStatement('selects', $fields); - - return $this; - } - - /** - * Performs select distinct on the current query. - * - * @param string|Raw|\Closure|array $fields - * - * @return static - */ - public function selectDistinct($fields) - { - $this->select($fields); - $this->addStatement('distinct', true); - - return $this; - } - - /** - * Set connection object - * - * @param Connection $connection - * - * @return static - */ - public function setConnection(Connection $connection) - { - $this->connection = $connection; - - return $this; - } - - /** - * Add fetch parameters to the PDO-query. - * - * @param mixed $parameters ... - * - * @return static - */ - public function setFetchMode($parameters = null) - { - $this->fetchParameters = \func_get_args(); - - return $this; - } - - /** - * Execute statement - * - * @param string $sql - * @param array $bindings - * - * @return array PDOStatement and execution time as float - */ - public function statement(string $sql, array $bindings = []): array - { - $start = microtime(true); - - $pdoStatement = $this->pdo->prepare($sql); - - $pdoStatement->execute($bindings); - - return [$pdoStatement, microtime(true) - $start]; - } - - /** - * Performs new sub-query. - * Call this method when you want to add a new sub-query in your where etc. - * - * @param QueryBuilderHandler $queryBuilder - * @param string|null $alias - * - * @throws Exception - * @return Raw - */ - public function subQuery(QueryBuilderHandler $queryBuilder, $alias = null): Raw - { - $sql = '(' . $queryBuilder->getQuery()->getRawSql() . ')'; - if ($alias !== null) { - $sql = $sql . ' AS ' . $this->adapterInstance->wrapSanitizer($alias); - } - - return $queryBuilder->raw($sql); - } - - /** - * Sets the table that the query is using - * - * @param string|array $tables Single table or multiple tables as an array or as multiple parameters - * - * @throws Exception - * @return static - * - * ``` - * Examples: - * - basic usage - * ->table('table_one') - * ->table(['table_one']) - * - * - with aliasing - * ->table(['table_one' => 'one']) - * ->table($qb->raw('table_one as one')) - * ``` - */ - public function table($tables) - { - $tTables = []; - if (\is_array($tables) === false) { - // Because a single table is converted to an array anyways, this makes sense. - $tables = \func_get_args(); - } - - $instance = new static($this->connection); - - foreach ($tables as $key => $value) { - if (\is_string($key)) { - $instance->alias($value, $key); - $tTables[] = $key; - } else { - $tTables[] = $value; - } - } - $tTables = $this->addTablePrefix($tTables, false); - $instance->addStatement('tables', $tTables); - - return $instance; - } - - /** - * Performs the transaction - * - * @param \Closure $callback - * - * @throws Exception - * @return static - */ - public function transaction(\Closure $callback): IQueryBuilderHandler - { - /** - * Get the Transaction class - * - * @var \Pecee\Pixie\QueryBuilder\Transaction $queryTransaction - * @throws \Exception - */ - $queryTransaction = $this->container->build(Transaction::class, [$this->connection]); - $queryTransaction->statements = $this->statements; - - try { - // Begin the PDO transaction - if($this->pdo->inTransaction() === false) { - $this->pdo->beginTransaction(); - } - - // Call closure - this callback will return TransactionHaltException if user has already committed the transaction - $callback($queryTransaction); - - // If no errors have been thrown or the transaction wasn't completed within the closure, commit the changes - $this->pdo->commit(); - - } catch (TransactionHaltException $e) { - - // Commit or rollback behavior has been triggered in the closure - return $this; - - } catch (\Exception $e) { - - // Something went wrong. Rollback and throw Exception - if($this->pdo->inTransaction() === true) { - $this->pdo->rollBack(); - } - - throw new Exception($e->getMessage()); - } - - return $queryTransaction; - } - - /** - * Update key/value array - * - * @param array $data - * - * @throws Exception - * @return \PDOStatement - */ - public function update($data): \PDOStatement - { - /** - * @var $response \PDOStatement - */ - $queryObject = $this->getQuery('update', $data); - - $this->fireEvents(static::EVENT_BEFORE_UPDATE, $queryObject); - - list($response, $executionTime) = $this->statement($queryObject->getSql(), $queryObject->getBindings()); - $this->fireEvents(static::EVENT_AFTER_UPDATE, $queryObject, $executionTime); - - return $response; - } - - /** - * Update or insert key/value array - * - * @param array $data - * - * @return array|\PDOStatement|string - * @throws Exception - */ - public function updateOrInsert($data) - { - if ($this->first() !== null) { - return $this->update($data); - } - - return $this->insert($data); - } - - /** - * Adds WHERE statement to the current query. - * - * @param string|Raw|\Closure $key - * @param string|null $operator - * @param mixed|Raw|\Closure|null $value - * - * @return static - */ - public function where($key, $operator = null, $value = null): IQueryBuilderHandler - { - // If two params are given then assume operator is = - if (\func_num_args() === 2) { - $value = $operator; - $operator = '='; - } - - if (\is_bool($value) === true) { - $value = (int)$value; - } - - return $this->whereHandler($key, $operator, $value); - } - - /** - * Adds WHERE BETWEEN statement to the current query. - * - * @param string|Raw|\Closure $key - * @param string|integer|float $valueFrom - * @param string|integer|float $valueTo - * - * @return static - */ - public function whereBetween($key, $valueFrom, $valueTo): IQueryBuilderHandler - { - return $this->whereHandler($key, 'BETWEEN', [$valueFrom, $valueTo]); - } - - /** - * Handles where statements - * - * @param string|Raw|\Closure $key - * @param string|null $operator - * @param string|Raw|\Closure|null $value - * @param string $joiner - * - * @return static - */ - protected function whereHandler($key, string $operator = null, $value = null, $joiner = 'AND'): IQueryBuilderHandler - { - $key = $this->addTablePrefix($key); - $this->statements['wheres'][] = compact('key', 'operator', 'value', 'joiner'); - - return $this; - } - - /** - * Adds WHERE IN statement to the current query. - * - * @param string|Raw|\Closure $key - * @param array|Raw|\Closure $values - * - * @return static - */ - public function whereIn($key, $values): IQueryBuilderHandler - { - return $this->whereHandler($key, 'IN', $values); - } - - /** - * Adds WHERE NOT statement to the current query. - * - * @param string|Raw|\Closure $key - * @param string|array|Raw|\Closure|null $operator - * @param mixed|Raw|\Closure|null $value - * - * @return static - */ - public function whereNot($key, $operator = null, $value = null): IQueryBuilderHandler - { - // If two params are given then assume operator is = - if (\func_num_args() === 2) { - $value = $operator; - $operator = '='; - } - - return $this->whereHandler($key, $operator, $value, 'AND NOT'); - } - - /** - * Adds OR WHERE NOT IN statement to the current query. - * - * @param string|Raw|\Closure $key - * @param array|Raw|\Closure $values - * - * @return static - */ - public function whereNotIn($key, $values): IQueryBuilderHandler - { - return $this->whereHandler($key, 'NOT IN', $values); - } - - /** - * Adds WHERE NOT NULL statement to the current query. - * - * @param string|Raw|\Closure $key - * - * @return static - */ - public function whereNotNull($key): IQueryBuilderHandler - { - return $this->whereNullHandler($key, 'NOT'); - } - - /** - * Adds WHERE NULL statement to the current query. - * - * @param string|Raw|\Closure $key - * - * @return static - */ - public function whereNull($key): IQueryBuilderHandler - { - return $this->whereNullHandler($key); - } - - /** - * Handles WHERE NULL statements. - * - * @param string|Raw|\Closure $key - * @param string $prefix - * @param string $operator - * - * @return static - */ - protected function whereNullHandler($key, $prefix = '', $operator = ''): IQueryBuilderHandler - { - $key = $this->adapterInstance->wrapSanitizer($this->addTablePrefix($key)); - $prefix = ($prefix !== '') ? $prefix . ' ' : $prefix; - - return $this->{$operator . 'Where'}($this->raw("$key IS {$prefix}NULL")); - } +class QueryBuilderHandler implements IQueryBuilderHandler { + + /** + * @var Connection + */ + protected $connection; + + /** + * @var array + */ + protected $statements = [ + 'groupBys' => [], + ]; + + /** + * @var PDO + */ + protected $pdo; + + /** + * @var null|\PDOStatement + */ + protected $pdoStatement; + + /** + * @var null|string + */ + protected $tablePrefix; + + /** + * @var \Pecee\Pixie\QueryBuilder\Adapters\BaseAdapter + */ + protected $adapterInstance; + + /** + * The PDO fetch parameters to use + * + * @var array + */ + protected $fetchParameters = [\PDO::FETCH_OBJ]; + + /** + * @var string + */ + protected $adapter; + + /** + * @var array + */ + protected $adapterConfig; + + /** + * @param \Pecee\Pixie\Connection|null $connection + * + * @throws \Pecee\Pixie\Exception + */ + public function __construct(Connection $connection = null) { + $this->connection = $connection ?? Connection::getStoredConnection(); + + if ($this->connection === null) { + throw new Exception('No database connection found.', 1); + } + + $this->pdo = $this->connection->getPdoInstance(); + $this->adapter = $this->connection->getAdapter(); + $this->adapterConfig = $this->connection->getAdapterConfig(); + + if (isset($this->adapterConfig['prefix']) === true) { + $this->tablePrefix = $this->adapterConfig['prefix']; + } + + // Query builder adapter instance + $adapterClass = $this->adapter->getQueryAdapterClass(); + $this->adapterInstance = new $adapterClass($this->connection); + + $this->pdo->setAttribute(PDO::ATTR_ERRMODE, PDO::ERRMODE_EXCEPTION); + + // PDO will parse parameter datatypes automatically + $this->pdo->setAttribute(PDO::ATTR_EMULATE_PREPARES, true); + } + + /** + * Add new statement to statement-list + * + * @param string $key + * @param mixed $value + * + * @return void + */ + protected function addStatement(string $key, $value) { + if (array_key_exists($key, $this->statements) === false) { + $this->statements[ $key ] = (array)$value; + } else { + $this->statements[ $key ] = array_merge($this->statements[ $key ], (array)$value); + } + } + + /** + * Add table prefix (if given) on given string. + * + * @param string|array|Raw|\Closure $values + * @param bool $tableFieldMix If we have mixes of field and table names with a "." + * + * @return array|string + */ + public function addTablePrefix($values, bool $tableFieldMix = true) { + if ($this->tablePrefix === null) { + return $values; + } + + // $value will be an array and we will add prefix to all table names + // If supplied value is not an array then make it one + + $single = false; + if (\is_array($values) === false) { + $values = [$values]; + + // We had single value, so should return a single value + $single = true; + } + + $return = []; + + foreach ($values as $key => $value) { + // It's a raw query, just add it to our return array and continue next + if ($value instanceof Raw || $value instanceof \Closure) { + $return[ $key ] = $value; + continue; + } + + // If key is not integer, it is likely a alias mapping, so we need to change prefix target + $target = &$value; + + if (\is_int($key) === false) { + $target = &$key; + } + + if ($tableFieldMix === false || ($tableFieldMix && strpos($target, '.') !== false)) { + $target = $this->tablePrefix . $target; + } + + $return[ $key ] = $value; + } + + // If we had single value then we should return a single value (end value of the array) + return $single ? end($return) : $return; + } + + /** + * Performs special queries like COUNT, SUM etc based on the current query. + * + * @param string $type + * + * @throws Exception + * @return int + */ + protected function aggregate(string $type): int { + // Get the current selects + $mainSelects = $this->statements['selects'] ?? null; + + // Replace select with a scalar value like `count` + $this->statements['selects'] = [$this->raw($type . '(*) AS `field`')]; + $row = $this->get(); + + // Set the select as it was + if ($mainSelects !== null) { + $this->statements['selects'] = $mainSelects; + } else { + unset($this->statements['selects']); + } + + if (isset($row[0]) === true) { + if (\is_array($row[0]) === true) { + return (int)$row[0]['field']; + } + if (\is_object($row[0]) === true) { + return (int)$row[0]->field; + } + } + + return 0; + } + + /** + * Add or change table alias + * Example: table AS alias + * + * @param string $alias + * @param string $table + * + * @return static + */ + public function alias(string $alias, string $table = null): IQueryBuilderHandler { + if ($table === null && isset($this->statements['tables'][0]) === true) { + $table = $this->statements['tables'][0]; + } else { + $table = $this->tablePrefix . $table; + } + + $this->statements['aliases'][ $table ] = strtolower($alias); + + return $this; + } + + /** + * Fetch query results as object of specified type + * + * @param string $className + * @param array $constructorArgs + * + * @return static + */ + public function asObject(string $className, array $constructorArgs = []): QueryBuilderHandler { + return $this->setFetchMode(PDO::FETCH_CLASS, $className, $constructorArgs); + } + + /** + * Get count of rows + * + * @throws Exception + * @return int + */ + public function count(): int { + // Get the current statements + $originalStatements = $this->statements; + + unset($this->statements['orderBys'], $this->statements['limit'], $this->statements['offset']); + + $count = $this->aggregate('count'); + $this->statements = $originalStatements; + + return $count; + } + + /** + * Forms delete on the current query. + * + * @return \PDOStatement + * @throws Exception + */ + public function delete(): \PDOStatement { + /* @var $response \PDOStatement */ + $queryObject = $this->getQuery('delete'); + + $this->fireEvents(EventHandler::EVENT_BEFORE_DELETE, $queryObject); + + list($response, $executionTime) = $this->statement($queryObject->getSql(), $queryObject->getBindings()); + $this->fireEvents(EventHandler::EVENT_AFTER_DELETE, $queryObject, $executionTime); + + return $response; + } + + /** + * Performs insert + * + * @param array $data + * @param string $type + * + * @throws Exception + * @return array|string|null + */ + private function doInsert(array $data, string $type) { + // Insert single item + + if (\is_array(current($data)) === false) { + $queryObject = $this->getQuery($type, $data); + + $this->fireEvents(EventHandler::EVENT_BEFORE_INSERT, $queryObject); + /** + * @var $result \PDOStatement + * @var $executionTime float + */ + list($result, $executionTime) = $this->statement($queryObject->getSql(), $queryObject->getBindings()); + + $insertId = $result->rowCount() === 1 ? $this->pdo->lastInsertId() : null; + $this->fireEvents(EventHandler::EVENT_AFTER_INSERT, $queryObject, $insertId, $executionTime); + + return $insertId; + } + + $insertIds = []; + + // If the current batch insert is not in a transaction, we create one... + + if ($this->pdo->inTransaction() === false) { + + $this->transaction(function (Transaction $transaction) use (&$insertIds, $data, $type) { + foreach ($data as $subData) { + $insertIds[] = $transaction->doInsert($subData, $type); + } + }); + + return $insertIds; + } + + // Otherwise insert one by one... + + foreach ($data as $subData) { + $insertIds[] = $this->doInsert($subData, $type); + } + + return $insertIds; + } + + /** + * Find by value and field name. + * + * @param string|int|float $value + * @param string $fieldName + * + * @throws Exception + * @return \stdClass|string|null + */ + public function find($value, $fieldName = 'id') { + return $this->where($fieldName, '=', $value)->first(); + } + + /** + * Find all by field name and value + * + * @param string $fieldName + * @param string|int|float $value + * + * @throws Exception + * @return array + */ + public function findAll(string $fieldName, $value): array { + return $this->where($fieldName, '=', $value)->get(); + } + + /** + * Fires event by given event name + * + * @param string $name + * @param ... $parameters + * + * @return mixed|null + */ + public function fireEvents($name, $parameters = null) { + $params = \func_get_args(); + array_unshift($params, $this); + + return \call_user_func_array([$this->connection->getEventHandler(), 'fireEvents'], $params); + } + + /** + * Returns the first row + * + * @throws Exception + * @return \stdClass|string|null + */ + public function first() { + $result = $this->limit(1)->get(); + + return ($result !== null && \count($result) > 0) ? $result[0] : null; + } + + /** + * Adds FROM statement to the current query. + * + * @param string|array $tables + * + * @return static + */ + public function from($tables): IQueryBuilderHandler { + if (\is_array($tables) === false) { + $tables = \func_get_args(); + } + + $tables = $this->addTablePrefix($tables, false); + $this->addStatement('tables', $tables); + + return $this; + } + + /** + * Get all rows + * + * @throws Exception + * @return array + */ + public function get(): array { + /** + * @var $queryObject \Pecee\Pixie\QueryBuilder\QueryObject + * @var $executionTime float + * @var $start float + * @var $result array + */ + $queryObject = null; + $executionTime = 0; + + if ($this->pdoStatement === null) { + $queryObject = $this->getQuery(); + list($this->pdoStatement, $executionTime) = $this->statement( + $queryObject->getSql(), + $queryObject->getBindings() + ); + } + + $start = microtime(true); + $this->fireEvents(EventHandler::EVENT_BEFORE_SELECT, $queryObject); + $result = \call_user_func_array([$this->pdoStatement, 'fetchAll'], $this->fetchParameters); + $executionTime += microtime(true) - $start; + $this->pdoStatement = null; + $this->fireEvents(EventHandler::EVENT_AFTER_SELECT, $queryObject, $result, $executionTime); + + return $result; + } + + /** + * Get connection object + * + * @return Connection + */ + public function getConnection(): Connection { + return $this->connection; + } + + /** + * Get event by event name + * + * @param string $name + * @param string|null $table + * + * @return \Closure|null + */ + public function getEvent(string $name, string $table = null) { + return $this->connection->getEventHandler()->getEvent($name, $table); + } + + /** + * Returns Query-object. + * + * @param string $type + * @param array|mixed|null $dataToBePassed + * + * @return QueryObject + * @throws Exception + */ + public function getQuery(string $type = 'select', $dataToBePassed = null): QueryObject { + $allowedTypes = [ + 'select', + 'insert', + 'insertignore', + 'replace', + 'delete', + 'update', + 'criteriaonly', + ]; + + if (\in_array(strtolower($type), $allowedTypes, true) === false) { + throw new Exception($type . ' is not a known type.', 2); + } + + $queryArr = $this->adapterInstance->$type($this->statements, $dataToBePassed); + + return new QueryObject($queryArr['sql'], $queryArr['bindings'], $this->pdo); + } + + /** + * Returns statements + * + * @return array + */ + public function getStatements(): array { + return $this->statements; + } + + /** + * Adds GROUP BY to the current query. + * + * @param string|Raw|\Closure|array $field + * + * @return static + */ + public function groupBy($field): IQueryBuilderHandler { + if (($field instanceof Raw) === false) { + $field = $this->addTablePrefix($field); + } + + if (\is_array($field) === true) { + $this->statements['groupBys'] = array_merge($this->statements['groupBys'], $field); + } else { + $this->statements['groupBys'][] = $field; + } + + return $this; + } + + /** + * Adds HAVING statement to the current query. + * + * @param string|Raw|\Closure $key + * @param string|mixed $operator + * @param string|mixed $value + * @param string $joiner + * + * @return static + */ + public function having($key, $operator, $value, $joiner = 'AND'): IQueryBuilderHandler { + $key = $this->addTablePrefix($key); + $this->statements['havings'][] = compact('key', 'operator', 'value', 'joiner'); + + return $this; + } + + /** + * Adds new INNER JOIN statement to the current query. + * + * @param string|Raw|\Closure $table + * @param string|Raw|\Closure $key + * @param string|mixed|null $operator + * @param string|Raw|\Closure|null $value + * + * @return static + * @throws Exception + */ + public function innerJoin($table, $key, $operator = null, $value = null): IQueryBuilderHandler { + return $this->join($table, $key, $operator, $value); + } + + /** + * Insert key/value array + * + * @param array $data + * + * @throws Exception + * @return array|string + */ + public function insert(array $data) { + return $this->doInsert($data, 'insert'); + } + + /** + * Insert with ignore key/value array + * + * @param array $data + * + * @throws Exception + * @return array|string + */ + public function insertIgnore($data) { + return $this->doInsert($data, 'insertignore'); + } + + /** + * Adds new JOIN statement to the current query. + * + * @param string|Raw|\Closure|array $table + * @param string|Raw|\Closure $key + * @param string|null $operator + * @param string|Raw|\Closure $value + * @param string $type + * + * @return static + * @throws Exception + * + * ``` + * Examples: + * - basic usage + * ->join('table2', 'table2.person_id', '=', 'table1.id'); + * + * - as alias 'bar' + * ->join(['table2','bar'], 'bar.person_id', '=', 'table1.id'); + * + * - complex usage + * ->join('another_table', function($table) + * { + * $table->on('another_table.person_id', '=', 'my_table.id'); + * $table->on('another_table.person_id2', '=', 'my_table.id2'); + * $table->orOn('another_table.age', '>', $queryBuilder->raw(1)); + * }) + * ``` + */ + public function join($table, $key, $operator = null, $value = null, $type = 'inner'): IQueryBuilderHandler { + if (($key instanceof \Closure) === false) { + $key = function (JoinBuilder $joinBuilder) use ($key, $operator, $value) { + $joinBuilder->on($key, $operator, $value); + }; + } + + /** + * Build a new JoinBuilder class, keep it by reference so any changes made + * in the closure should reflect here + */ + + $joinBuilder = new JoinBuilder($this->connection); + + // Call the closure with our new joinBuilder object + $key($joinBuilder); + $table = $this->addTablePrefix($table, false); + + // Get the criteria only query from the joinBuilder object + $this->statements['joins'][] = compact('type', 'table', 'joinBuilder'); + + return $this; + } + + /** + * Adds new LEFT JOIN statement to the current query. + * + * @param string|Raw|\Closure|array $table + * @param string|Raw|\Closure $key + * @param string|null $operator + * @param string|Raw|\Closure|null $value + * + * @return static + * @throws Exception + */ + public function leftJoin($table, $key, $operator = null, $value = null): IQueryBuilderHandler { + return $this->join($table, $key, $operator, $value, 'left'); + } + + /** + * Adds LIMIT statement to the current query. + * + * @param int $limit + * + * @return static + */ + public function limit($limit): IQueryBuilderHandler { + $this->statements['limit'] = $limit; + + return $this; + } + + /** + * Creates and returns new query. + * + * @param \Pecee\Pixie\Connection|null $connection + * + * @throws \Pecee\Pixie\Exception + * @return static + */ + public function newQuery(Connection $connection = null): IQueryBuilderHandler { + if ($connection === null) { + $connection = $this->connection; + } + + return new static($connection); + } + + /** + * Adds OFFSET statement to the current query. + * + * @param int $offset + * + * @return static $this + */ + public function offset($offset): IQueryBuilderHandler { + $this->statements['offset'] = $offset; + + return $this; + } + + /** + * Add on duplicate key statement. + * + * @param string|array $data + * + * @return static + */ + public function onDuplicateKeyUpdate($data): IQueryBuilderHandler { + $this->addStatement('onduplicate', $data); + + return $this; + } + + /** + * Adds OR HAVING statement to the current query. + * + * @param string|Raw|\Closure $key + * @param string|Raw|\Closure $operator + * @param mixed|Raw|\Closure|null $value + * + * @return static + */ + public function orHaving($key, $operator, $value): IQueryBuilderHandler { + return $this->having($key, $operator, $value, 'OR'); + } + + /** + * Adds OR WHERE statement to the current query. + * + * @param string|Raw|\Closure $key + * @param string|null $operator + * @param mixed|Raw|\Closure|null $value + * + * @return static + */ + public function orWhere($key, $operator = null, $value = null): IQueryBuilderHandler { + // If two params are given then assume operator is = + if (\func_num_args() === 2) { + $value = $operator; + $operator = '='; + } + + return $this->whereHandler($key, $operator, $value, 'OR'); + } + + /** + * Adds OR WHERE BETWEEN statement to the current query. + * + * @param string|Raw|\Closure $key + * @param string|integer|float $valueFrom + * @param string|integer|float $valueTo + * + * @return static + */ + public function orWhereBetween($key, $valueFrom, $valueTo): IQueryBuilderHandler { + return $this->whereHandler($key, 'BETWEEN', [$valueFrom, $valueTo], 'OR'); + } + + /** + * Adds OR WHERE IN statement to the current query. + * + * @param string|Raw|\Closure $key + * @param array|Raw|\Closure $values + * + * @return static + */ + public function orWhereIn($key, $values): IQueryBuilderHandler { + return $this->whereHandler($key, 'IN', $values, 'OR'); + } + + /** + * Adds OR WHERE NOT statement to the current query. + * + * @param string|Raw|\Closure $key + * @param string|null $operator + * @param mixed|Raw|\Closure|null $value + * + * @return static + */ + public function orWhereNot($key, $operator = null, $value = null): IQueryBuilderHandler { + // If two params are given then assume operator is = + if (\func_num_args() === 2) { + $value = $operator; + $operator = '='; + } + + return $this->whereHandler($key, $operator, $value, 'OR NOT'); + } + + /** + * Adds or WHERE NOT IN statement to the current query. + * + * @param string|Raw|\Closure $key + * @param array|Raw|\Closure $values + * + * @return static + */ + public function orWhereNotIn($key, $values): IQueryBuilderHandler { + return $this->whereHandler($key, 'NOT IN', $values, 'OR'); + } + + /** + * Adds OR WHERE NOT NULL statement to the current query. + * + * @param string|Raw|\Closure $key + * + * @return static + */ + public function orWhereNotNull($key): IQueryBuilderHandler { + return $this->whereNullHandler($key, 'NOT', 'or'); + } + + /** + * Adds OR WHERE NULL statement to the current query. + * + * @param string|Raw|\Closure $key + * + * @return static + */ + public function orWhereNull($key): IQueryBuilderHandler { + return $this->whereNullHandler($key, '', 'or'); + } + + /** + * Adds ORDER BY statement to the current query. + * + * @param string|Raw|\Closure|array $fields + * @param string $defaultDirection + * + * @return static + */ + public function orderBy($fields, $defaultDirection = 'ASC'): IQueryBuilderHandler { + if (\is_array($fields) === false) { + $fields = [$fields]; + } + + foreach ((array)$fields as $key => $value) { + $field = $key; + $type = $value; + + if (\is_int($key) === true) { + $field = $value; + $type = $defaultDirection; + } + + if (($field instanceof Raw) === false) { + $field = $this->addTablePrefix($field); + } + + $this->statements['orderBys'][] = compact('field', 'type'); + } + + return $this; + } + + /** + * Return PDO instance + * + * @return PDO + */ + public function pdo(): PDO { + return $this->pdo; + } + + /** + * Performs query. + * + * @param string $sql + * @param array $bindings + * + * @return static + */ + public function query($sql, array $bindings = []): IQueryBuilderHandler { + list($this->pdoStatement) = $this->statement($sql, $bindings); + + return $this; + } + + /** + * Adds a raw string to the current query. + * This query will be ignored from any parsing or formatting by the Query builder + * and should be used in conjunction with other statements in the query. + * + * For example: $qb->where('result', '>', $qb->raw('COUNT(`score`))); + * + * @param string $value + * @param array|null|mixed $bindings ... + * + * @return Raw + */ + public function raw($value, $bindings = null): Raw { + if (\is_array($bindings) === false) { + $bindings = \func_get_args(); + array_shift($bindings); + } + + return new Raw($value, $bindings); + } + + /** + * Register new event + * + * @param string $name + * @param string|null $table + * @param \Closure $action + * + * @return void + */ + public function registerEvent($name, $table = null, \Closure $action) { + $this->connection->getEventHandler()->registerEvent($name, $table, $action); + } + + /** + * Remove event by event-name and/or table + * + * @param string $name + * @param string|null $table + * + * @return void + */ + public function removeEvent($name, $table = null) { + $this->connection->getEventHandler()->removeEvent($name, $table); + } + + /** + * Replace key/value array + * + * @param array $data + * + * @throws Exception + * @return array|string + */ + public function replace($data) { + return $this->doInsert($data, 'replace'); + } + + /** + * Adds new right join statement to the current query. + * + * @param string|Raw|\Closure|array $table + * @param string|Raw|\Closure $key + * @param string|null $operator + * @param string|Raw|\Closure|null $value + * + * @return static + * @throws Exception + */ + public function rightJoin($table, $key, $operator = null, $value = null): IQueryBuilderHandler { + return $this->join($table, $key, $operator, $value, 'right'); + } + + /** + * Adds fields to select on the current query (defaults is all). + * You can use key/value array to create alias. + * Sub-queries and raw-objects are also supported. + * + * Example: ['field' => 'alias'] will become `field` AS `alias` + * + * @param string|array $fields,... + * + * @return static + */ + public function select($fields): IQueryBuilderHandler { + if (\is_array($fields) === false) { + $fields = \func_get_args(); + } + + $fields = $this->addTablePrefix($fields); + $this->addStatement('selects', $fields); + + return $this; + } + + /** + * Performs select distinct on the current query. + * + * @param string|Raw|\Closure|array $fields + * + * @return static + */ + public function selectDistinct($fields) { + $this->select($fields); + $this->addStatement('distinct', true); + + return $this; + } + + /** + * Set connection object + * + * @param Connection $connection + * + * @return static + */ + public function setConnection(Connection $connection) { + $this->connection = $connection; + + return $this; + } + + /** + * Add fetch parameters to the PDO-query. + * + * @param mixed $parameters ... + * + * @return static + */ + public function setFetchMode($parameters = null) { + $this->fetchParameters = \func_get_args(); + + return $this; + } + + /** + * Execute statement + * + * @param string $sql + * @param array $bindings + * + * @return array PDOStatement and execution time as float + */ + public function statement(string $sql, array $bindings = []): array { + $start = microtime(true); + + $pdoStatement = $this->pdo->prepare($sql); + + $pdoStatement->execute($bindings); + + return [$pdoStatement, microtime(true) - $start]; + } + + /** + * Performs new sub-query. + * Call this method when you want to add a new sub-query in your where etc. + * + * @param QueryBuilderHandler $queryBuilder + * @param string|null $alias + * + * @throws Exception + * @return Raw + */ + public function subQuery(QueryBuilderHandler $queryBuilder, $alias = null): Raw { + $sql = '(' . $queryBuilder->getQuery()->getRawSql() . ')'; + if ($alias !== null) { + $sql = $sql . ' AS ' . $this->adapterInstance->wrapSanitizer($alias); + } + + return $queryBuilder->raw($sql); + } + + /** + * Sets the table that the query is using + * + * @param string|array $tables Single table or multiple tables as an array or as multiple parameters + * + * @throws Exception + * @return static + * + * ``` + * Examples: + * - basic usage + * ->table('table_one') + * ->table(['table_one']) + * + * - with aliasing + * ->table(['table_one' => 'one']) + * ->table($qb->raw('table_one as one')) + * ``` + */ + public function table($tables) { + $tTables = []; + if (\is_array($tables) === false) { + // Because a single table is converted to an array anyways, this makes sense. + $tables = \func_get_args(); + } + + $instance = new static($this->connection); + + foreach ($tables as $key => $value) { + if (\is_string($key)) { + $instance->alias($value, $key); + $tTables[] = $key; + } else { + $tTables[] = $value; + } + } + $tTables = $this->addTablePrefix($tTables, false); + $instance->addStatement('tables', $tTables); + + return $instance; + } + + /** + * Performs the transaction + * + * @param \Closure $callback + * + * @throws Exception + * @return Transaction + */ + public function transaction(\Closure $callback): Transaction { + /** + * Get the Transaction class + * + * @var \Pecee\Pixie\QueryBuilder\Transaction $queryTransaction + * @throws \Exception + */ + $queryTransaction = new Transaction($this->connection); + $queryTransaction->statements = $this->statements; + + try { + // Begin the PDO transaction + if ($this->pdo->inTransaction() === false) { + $this->pdo->beginTransaction(); + } + + // Call closure - this callback will return TransactionHaltException if user has already committed the transaction + $callback($queryTransaction); + + // If no errors have been thrown or the transaction wasn't completed within the closure, commit the changes + $this->pdo->commit(); + + } catch (TransactionHaltException $e) { + + // Commit or rollback behavior has been triggered in the closure + return $queryTransaction; + + } catch (\Exception $e) { + + // Something went wrong. Rollback and throw Exception + if ($this->pdo->inTransaction() === true) { + $this->pdo->rollBack(); + } + + throw new Exception($e->getMessage()); + } + + return $queryTransaction; + } + + /** + * Update key/value array + * + * @param array $data + * + * @throws Exception + * @return \PDOStatement + */ + public function update($data): \PDOStatement { + /** + * @var $response \PDOStatement + */ + $queryObject = $this->getQuery('update', $data); + + $this->fireEvents(EventHandler::EVENT_BEFORE_UPDATE, $queryObject); + + list($response, $executionTime) = $this->statement($queryObject->getSql(), $queryObject->getBindings()); + $this->fireEvents(EventHandler::EVENT_AFTER_UPDATE, $queryObject, $executionTime); + + return $response; + } + + /** + * Update or insert key/value array + * + * @param array $data + * + * @return array|\PDOStatement|string + * @throws Exception + */ + public function updateOrInsert($data) { + if ($this->first() !== null) { + return $this->update($data); + } + + return $this->insert($data); + } + + /** + * Adds WHERE statement to the current query. + * + * @param string|Raw|\Closure $key + * @param string|null $operator + * @param mixed|Raw|\Closure|null $value + * + * @return static + */ + public function where($key, $operator = null, $value = null): IQueryBuilderHandler { + // If two params are given then assume operator is = + if (\func_num_args() === 2) { + $value = $operator; + $operator = '='; + } + + if (\is_bool($value) === true) { + $value = (int)$value; + } + + return $this->whereHandler($key, $operator, $value); + } + + /** + * Adds WHERE BETWEEN statement to the current query. + * + * @param string|Raw|\Closure $key + * @param string|integer|float $valueFrom + * @param string|integer|float $valueTo + * + * @return static + */ + public function whereBetween($key, $valueFrom, $valueTo): IQueryBuilderHandler { + return $this->whereHandler($key, 'BETWEEN', [$valueFrom, $valueTo]); + } + + /** + * Handles where statements + * + * @param string|Raw|\Closure $key + * @param string|null $operator + * @param string|Raw|\Closure|null $value + * @param string $joiner + * + * @return static + */ + protected function whereHandler($key, string $operator = null, $value = null, $joiner = 'AND'): IQueryBuilderHandler { + $key = $this->addTablePrefix($key); + $this->statements['wheres'][] = compact('key', 'operator', 'value', 'joiner'); + + return $this; + } + + /** + * Adds WHERE IN statement to the current query. + * + * @param string|Raw|\Closure $key + * @param array|Raw|\Closure $values + * + * @return static + */ + public function whereIn($key, $values): IQueryBuilderHandler { + return $this->whereHandler($key, 'IN', $values); + } + + /** + * Adds WHERE NOT statement to the current query. + * + * @param string|Raw|\Closure $key + * @param string|array|Raw|\Closure|null $operator + * @param mixed|Raw|\Closure|null $value + * + * @return static + */ + public function whereNot($key, $operator = null, $value = null): IQueryBuilderHandler { + // If two params are given then assume operator is = + if (\func_num_args() === 2) { + $value = $operator; + $operator = '='; + } + + return $this->whereHandler($key, $operator, $value, 'AND NOT'); + } + + /** + * Adds OR WHERE NOT IN statement to the current query. + * + * @param string|Raw|\Closure $key + * @param array|Raw|\Closure $values + * + * @return static + */ + public function whereNotIn($key, $values): IQueryBuilderHandler { + return $this->whereHandler($key, 'NOT IN', $values); + } + + /** + * Adds WHERE NOT NULL statement to the current query. + * + * @param string|Raw|\Closure $key + * + * @return static + */ + public function whereNotNull($key): IQueryBuilderHandler { + return $this->whereNullHandler($key, 'NOT'); + } + + /** + * Adds WHERE NULL statement to the current query. + * + * @param string|Raw|\Closure $key + * + * @return static + */ + public function whereNull($key): IQueryBuilderHandler { + return $this->whereNullHandler($key); + } + + /** + * Handles WHERE NULL statements. + * + * @param string|Raw|\Closure $key + * @param string $prefix + * @param string $operator + * + * @return static + */ + protected function whereNullHandler($key, $prefix = '', $operator = ''): IQueryBuilderHandler { + $key = $this->adapterInstance->wrapSanitizer($this->addTablePrefix($key)); + $prefix = ($prefix !== '') ? $prefix . ' ' : $prefix; + + return $this->{$operator . 'Where'}($this->raw("$key IS {$prefix}NULL")); + } } \ No newline at end of file diff --git a/src/Pecee/Pixie/QueryBuilder/QueryObject.php b/src/Pecee/Pixie/QueryBuilder/QueryObject.php index 54823eb..90ae76c 100644 --- a/src/Pecee/Pixie/QueryBuilder/QueryObject.php +++ b/src/Pecee/Pixie/QueryBuilder/QueryObject.php @@ -2,106 +2,102 @@ namespace Pecee\Pixie\QueryBuilder; +use PDO; + /** * Class QueryObject * * @package Pecee\Pixie\QueryBuilder */ -class QueryObject -{ - - /** - * @var string - */ - protected $sql; - - /** - * @var array - */ - protected $bindings = []; - - /** - * @var \PDO - */ - protected $pdo; - - /** - * QueryObject constructor. - * - * @param string $sql - * @param array $bindings - * @param \PDO $pdo - */ - public function __construct(string $sql, array $bindings, \PDO $pdo) - { - $this->sql = $sql; - $this->bindings = $bindings; - $this->pdo = $pdo; - } - - /** - * @return array - */ - public function getBindings(): array - { - return $this->bindings; - } - - /** - * Get the raw/bound sql - * - * @return string - */ - public function getRawSql(): string - { - return $this->interpolateQuery($this->sql, $this->bindings); - } - - /** - * @return string - */ - public function getSql(): string - { - return $this->sql; - } - - /** - * Replaces any parameter placeholders in a query with the value of that - * parameter. Useful for debugging. Assumes anonymous parameters from - * $params are are in the same order as specified in $query - * - * Reference: http://stackoverflow.com/a/1376838/656489 - * - * @param string $query The sql query with parameter placeholders - * @param array $params The array of substitution parameters - * - * @return string The interpolated query - */ - protected function interpolateQuery($query, $params): string - { - $keys = []; - $values = $params; - - // build a regular expression for each parameter - foreach ($params as $key => $value) { - $keys[] = '/' . (\is_string($key) ? ':' . $key : '[?]') . '/'; - - if (\is_string($value) === true) { - $values[$key] = $this->pdo->quote($value); - continue; - } - - if (\is_array($value) === true) { - $values[$key] = $this->pdo->quote(implode(',', $value)); - continue; - } - - if ($value === null) { - $values[$key] = 'NULL'; - continue; - } - } - - return preg_replace($keys, $values, $query, 1, $count); - } +class QueryObject { + + /** + * @var string + */ + protected $sql; + + /** + * @var array + */ + protected $bindings = []; + + /** + * @var PDO + */ + protected $pdo; + + /** + * QueryObject constructor. + * + * @param string $sql + * @param array $bindings + * @param PDO $pdo + */ + public function __construct(string $sql, array $bindings, PDO $pdo) { + $this->sql = $sql; + $this->bindings = $bindings; + $this->pdo = $pdo; + } + + /** + * @return array + */ + public function getBindings(): array { + return $this->bindings; + } + + /** + * Get the raw/bound sql + * + * @return string + */ + public function getRawSql(): string { + return $this->interpolateQuery($this->sql, $this->bindings); + } + + /** + * @return string + */ + public function getSql(): string { + return $this->sql; + } + + /** + * Replaces any parameter placeholders in a query with the value of that + * parameter. Useful for debugging. Assumes anonymous parameters from + * $params are are in the same order as specified in $query + * + * Reference: http://stackoverflow.com/a/1376838/656489 + * + * @param string $query The sql query with parameter placeholders + * @param array $params The array of substitution parameters + * + * @return string The interpolated query + */ + protected function interpolateQuery($query, $params): string { + $keys = []; + $values = $params; + + // build a regular expression for each parameter + foreach ($params as $key => $value) { + $keys[] = '/' . (\is_string($key) ? ':' . $key : '[?]') . '/'; + + if (\is_string($value) === true) { + $values[ $key ] = $this->pdo->quote($value); + continue; + } + + if (\is_array($value) === true) { + $values[ $key ] = $this->pdo->quote(implode(',', $value)); + continue; + } + + if ($value === null) { + $values[ $key ] = 'NULL'; + continue; + } + } + + return preg_replace($keys, $values, $query, 1, $count); + } } diff --git a/src/Pecee/Pixie/QueryBuilder/Raw.php b/src/Pecee/Pixie/QueryBuilder/Raw.php index 725aed4..589f63f 100644 --- a/src/Pecee/Pixie/QueryBuilder/Raw.php +++ b/src/Pecee/Pixie/QueryBuilder/Raw.php @@ -7,44 +7,40 @@ * * @package Pecee\Pixie\QueryBuilder */ -class Raw -{ +class Raw { - /** - * @var string - */ - protected $value; + /** + * @var string + */ + protected $value; - /** - * @var array - */ - protected $bindings; + /** + * @var array + */ + protected $bindings; - /** - * Raw constructor. - * - * @param string $value - * @param array|string $bindings - */ - public function __construct(string $value, array $bindings = []) - { - $this->value = $value; - $this->bindings = $bindings; - } + /** + * Raw constructor. + * + * @param string $value + * @param array|string $bindings + */ + public function __construct(string $value, array $bindings = []) { + $this->value = $value; + $this->bindings = $bindings; + } - /** - * @return string - */ - public function __toString(): string - { - return (string)$this->value; - } + /** + * @return string + */ + public function __toString(): string { + return (string)$this->value; + } - /** - * @return array - */ - public function getBindings(): array - { - return $this->bindings; - } + /** + * @return array + */ + public function getBindings(): array { + return $this->bindings; + } } diff --git a/src/Pecee/Pixie/QueryBuilder/Transaction.php b/src/Pecee/Pixie/QueryBuilder/Transaction.php index 380bb2e..9f2f310 100644 --- a/src/Pecee/Pixie/QueryBuilder/Transaction.php +++ b/src/Pecee/Pixie/QueryBuilder/Transaction.php @@ -7,60 +7,60 @@ * * @package Pecee\Pixie\QueryBuilder */ -class Transaction extends QueryBuilderHandler -{ +class Transaction extends QueryBuilderHandler { - protected $transactionStatement; + protected $transactionStatement; - public function transaction(\Closure $callback): IQueryBuilderHandler - { - $callback($this); + /** + * @param \Closure $callback + * + * @return static + */ + public function transaction(\Closure $callback): Transaction { + $callback($this); - return $this; - } + return $this; + } - /** - * Commit transaction - * - * @throws \PDOException|TransactionHaltException - */ - public function commit() - { - $this->pdo->commit(); - throw new TransactionHaltException('Commit triggered transaction-halt.'); - } + /** + * Commit transaction + * + * @throws \PDOException|TransactionHaltException + */ + public function commit() { + $this->pdo->commit(); + throw new TransactionHaltException('Commit triggered transaction-halt.'); + } - /** - * RollBack transaction - * - * @throws \PDOException|TransactionHaltException - */ - public function rollBack() - { - $this->pdo->rollBack(); - throw new TransactionHaltException('Rollback triggered transaction-halt.'); - } + /** + * RollBack transaction + * + * @throws \PDOException|TransactionHaltException + */ + public function rollBack() { + $this->pdo->rollBack(); + throw new TransactionHaltException('Rollback triggered transaction-halt.'); + } - /** - * Execute statement - * - * @param string $sql - * @param array $bindings - * - * @return array PDOStatement and execution time as float - */ - public function statement(string $sql, array $bindings = []): array - { - $start = microtime(true); + /** + * Execute statement + * + * @param string $sql + * @param array $bindings + * + * @return array PDOStatement and execution time as float + */ + public function statement(string $sql, array $bindings = []): array { + $start = microtime(true); - if ($this->transactionStatement === null && $this->pdo->inTransaction() === true) { - $this->transactionStatement = $this->pdo->prepare($sql); - } + if ($this->transactionStatement === null && $this->pdo->inTransaction() === true) { + $this->transactionStatement = $this->pdo->prepare($sql); + } - $this->transactionStatement->execute($bindings); + $this->transactionStatement->execute($bindings); - return [$this->transactionStatement, microtime(true) - $start]; - } + return [$this->transactionStatement, microtime(true) - $start]; + } } diff --git a/src/Pecee/Pixie/QueryBuilder/TransactionHaltException.php b/src/Pecee/Pixie/QueryBuilder/TransactionHaltException.php index 1e30ed5..09024b4 100644 --- a/src/Pecee/Pixie/QueryBuilder/TransactionHaltException.php +++ b/src/Pecee/Pixie/QueryBuilder/TransactionHaltException.php @@ -2,6 +2,5 @@ namespace Pecee\Pixie\QueryBuilder; -class TransactionHaltException extends \Exception -{ +class TransactionHaltException extends \Exception { } \ No newline at end of file diff --git a/tests/Pecee/Pixie/ConnectionTest.php b/tests/Pecee/Pixie/ConnectionTest.php index a48ce63..bd39d3c 100644 --- a/tests/Pecee/Pixie/ConnectionTest.php +++ b/tests/Pecee/Pixie/ConnectionTest.php @@ -1,41 +1,37 @@ mysqlConnectionMock = m::mock(Mysql::class); - $this->mysqlConnectionMock->shouldReceive('connect')->andReturn($this->mockPdo); + $this->mysqlConnectionMock = m::mock(Mysql::class); + $this->mysqlConnectionMock->shouldReceive('connect')->andReturn($this->mockPdo); - $this->container->setInstance('\Pecee\Pixie\ConnectionAdapters\Mysqlmock', $this->mysqlConnectionMock); - $this->connection = new Connection('mysqlmock', array('prefix' => 'cb_'), $this->container); - } + $this->connection = new Connection($this->mysqlConnectionMock, array('prefix' => 'cb_')); + } - public function testConnection() - { - $this->assertEquals($this->mockPdo, $this->connection->getPdoInstance()); - $this->assertInstanceOf('\PDO', $this->connection->getPdoInstance()); - $this->assertEquals('mysqlmock', $this->connection->getAdapter()); - $this->assertEquals(array('prefix' => 'cb_'), $this->connection->getAdapterConfig()); - } + public function testConnection() { + $this->assertEquals($this->mockPdo, $this->connection->getPdoInstance()); + $this->assertInstanceOf(\PDO::class, $this->connection->getPdoInstance()); + $this->assertInstanceOf(IConnectionAdapter::class, $this->connection->getAdapter()); + $this->assertEquals(array('prefix' => 'cb_'), $this->connection->getAdapterConfig()); + } } diff --git a/tests/Pecee/Pixie/NoTableSubQueryTest.php b/tests/Pecee/Pixie/NoTableSubQueryTest.php index f99cd64..082dca5 100644 --- a/tests/Pecee/Pixie/NoTableSubQueryTest.php +++ b/tests/Pecee/Pixie/NoTableSubQueryTest.php @@ -9,33 +9,29 @@ * * @package Pecee\Pixie */ -class NoTableSubQueryTest extends TestCase -{ - /** - * @var QueryBuilderHandler - */ - protected $builder; +class NoTableSubQueryTest extends TestCase { + /** + * @var QueryBuilderHandler + */ + protected $builder; - public function setUp() - { - parent::setUp(); + public function setUp() { + parent::setUp(); - $this->builder = new QueryBuilderHandler($this->mockConnection); - } + $this->builder = new QueryBuilderHandler($this->mockConnection); + } - public function testRawQuery() - { + public function testRawQuery() { - $subQuery1 = $this->builder->table('mail')->select($this->builder->raw('COUNT(*)')); - $subQuery2 = $this->builder->table('event_message')->select($this->builder->raw('COUNT(*)')); + $subQuery1 = $this->builder->table('mail')->select($this->builder->raw('COUNT(*)')); + $subQuery2 = $this->builder->table('event_message')->select($this->builder->raw('COUNT(*)')); - $count = $this->builder->select($this->builder->subQuery($subQuery1, 'row1'), - $this->builder->subQuery($subQuery2, 'row2'))->first() - ; + $count = $this->builder->select($this->builder->subQuery($subQuery1, 'row1'), + $this->builder->subQuery($subQuery2, 'row2'))->first(); - $this->assertEquals('SELECT (SELECT COUNT(*) FROM `cb_mail`) AS `row1`, (SELECT COUNT(*) FROM `cb_event_message`) AS `row2` LIMIT 1', - $count); + $this->assertEquals('SELECT (SELECT COUNT(*) FROM `cb_mail`) AS `row1`, (SELECT COUNT(*) FROM `cb_event_message`) AS `row2` LIMIT 1', + $count); - } + } } diff --git a/tests/Pecee/Pixie/QueryBuilderBehaviorTest.php b/tests/Pecee/Pixie/QueryBuilderBehaviorTest.php index 09dfc01..0cb9e35 100644 --- a/tests/Pecee/Pixie/QueryBuilderBehaviorTest.php +++ b/tests/Pecee/Pixie/QueryBuilderBehaviorTest.php @@ -3,368 +3,340 @@ namespace Pecee\Pixie; use Pecee\Pixie\QueryBuilder\QueryBuilderHandler; -use Pecee\Pixie\QueryBuilder\Transaction; /** * Class QueryBuilderTest * * @package Pecee\Pixie */ -class QueryBuilderTest extends TestCase -{ - /** - * @var QueryBuilderHandler - */ - private $builder; - - public function setUp() - { - parent::setUp(); - - $this->builder = new QueryBuilder\QueryBuilderHandler($this->mockConnection); - } - - /** - * Test alias - */ - public function testAlias() - { - $query = $this->builder - ->table(['table1']) - ->alias('t1') - ->join('table2', 'table2.person_id', '=', 'foo2.id') - ; - - $this->assertEquals('SELECT * FROM `cb_table1` AS `t1` INNER JOIN `cb_table2` ON `cb_table2`.`person_id` = `cb_foo2`.`id`', - $query->getQuery()->getRawSql()); - } - - /** - * Test delete - */ - public function testDeleteQuery() - { - $this->builder = new QueryBuilder\QueryBuilderHandler($this->mockConnection); - - $builder = $this->builder->table('my_table')->where('value', '=', 'Amrin'); - - $this->assertEquals("DELETE FROM `cb_my_table` WHERE `value` = 'Amrin'" - , $builder->getQuery('delete')->getRawSql()); - } - - public function testEventPropagation() - { - $builder = $this->builder; - - $events = [ - 'before-insert', - 'after-insert', - 'before-select', - 'after-select', - 'before-update', - 'after-update', - 'before-delete', - 'after-delete', - ]; - - $triggeredEvents = []; - - foreach ($events as $event) { - $builder->registerEvent($event, ':any', function ($qb) use (&$triggeredEvents, $event) { - $triggeredEvents[] = $event; - }); - } - - $builder->table('foo')->insert(['bar' => 'baz']); - $builder->from('foo')->select('bar')->get(); - $builder->table('foo')->update(['bar' => 'baz']); - $builder->from('foo')->delete(); - - $this->assertEquals($triggeredEvents, $events); - } - - public function testInsertIgnoreQuery() - { - $builder = $this->builder->from('my_table'); - $data = [ - 'key' => 'Name', - 'value' => 'Sana', - ]; - - $this->assertEquals("INSERT IGNORE INTO `cb_my_table` (`key`,`value`) VALUES ('Name','Sana')" - , $builder->getQuery('insertignore', $data)->getRawSql()); - } - - public function testInsertOnDuplicateKeyUpdateQuery() - { - $builder = $this->builder; - $data = [ - 'name' => 'Sana', - 'counter' => 1, - ]; - $dataUpdate = [ - 'name' => 'Sana', - 'counter' => 2, - ]; - $builder->from('my_table')->onDuplicateKeyUpdate($dataUpdate); - $this->assertEquals("INSERT INTO `cb_my_table` (`name`,`counter`) VALUES ('Sana',1) ON DUPLICATE KEY UPDATE `name`='Sana',`counter`=2" - , $builder->getQuery('insert', $data)->getRawSql()); - } - - public function testInsertQuery() - { - $builder = $this->builder->from('my_table'); - $data = [ - 'key' => 'Name', - 'value' => 'Sana', - ]; - - $this->assertEquals("INSERT INTO `cb_my_table` (`key`,`value`) VALUES ('Name','Sana')" - , $builder->getQuery('insert', $data)->getRawSql()); - } - - public function testIsPossibleToUseSubqueryInWhereClause() - { - $sub = clone $this->builder; - $query = $this->builder->from('my_table')->whereIn('foo', $this->builder->subQuery( - $sub->from('some_table')->select('foo')->where('id', 1) - )) - ; - $this->assertEquals( - "SELECT * FROM `cb_my_table` WHERE `foo` IN (SELECT `foo` FROM `cb_some_table` WHERE `id` = 1)", - $query->getQuery()->getRawSql() - ); - } - - public function testIsPossibleToUseSubqueryInWhereNotClause() - { - $sub = clone $this->builder; - $query = $this->builder->from('my_table')->whereNotIn('foo', $this->builder->subQuery( - $sub->from('some_table')->select('foo')->where('id', 1) - )) - ; - $this->assertEquals( - "SELECT * FROM `cb_my_table` WHERE `foo` NOT IN (SELECT `foo` FROM `cb_some_table` WHERE `id` = 1)", - $query->getQuery()->getRawSql() - ); - } - - public function testOrderByFlexibility() - { - $query = $this->builder - ->from('t') - ->orderBy('foo', 'DESC') - ->orderBy(['bar', 'baz' => 'ASC', $this->builder->raw('raw1')], 'DESC') - ->orderBy($this->builder->raw('raw2'), 'DESC') - ; - - $this->assertEquals( - 'SELECT * FROM `cb_t` ORDER BY `foo` DESC, `bar` DESC, `baz` ASC, raw1 DESC, raw2 DESC', - $query->getQuery()->getRawSql(), - 'ORDER BY is flexible enough!' - ); - } - - public function testRawStatementsWithinCriteria() - { - $query = $this->builder->from('my_table') - ->where('simple', 'criteria') - ->where($this->builder->raw('RAW')) - ->where($this->builder->raw('PARAMETERIZED_ONE(?)', 'foo')) - ->where($this->builder->raw('PARAMETERIZED_SEVERAL(?, ?, ?)', [1, '2', 'foo'])) - ; - - $this->assertEquals( - "SELECT * FROM `cb_my_table` WHERE `simple` = 'criteria' AND RAW AND PARAMETERIZED_ONE('foo') AND PARAMETERIZED_SEVERAL(1, '2', 'foo')", - $query->getQuery()->getRawSql() - ); - } - - public function testReplaceQuery() - { - $builder = $this->builder->from('my_table'); - $data = [ - 'key' => 'Name', - 'value' => 'Sana', - ]; - - $this->assertEquals("REPLACE INTO `cb_my_table` (`key`,`value`) VALUES ('Name','Sana')" - , $builder->getQuery('replace', $data)->getRawSql()); - } - - public function testSelectAliases() - { - $query = $this->builder->from('my_table')->select('foo')->select(['bar' => 'baz', 'qux']); - - $this->assertEquals( - "SELECT `foo`, `bar` AS `baz`, `qux` FROM `cb_my_table`", - $query->getQuery()->getRawSql() - ); - } - - public function testSelectDistinct() - { - $query = $this->builder->selectDistinct(['name', 'surname'])->from('my_table'); - $this->assertEquals("SELECT DISTINCT `name`, `surname` FROM `cb_my_table`", $query->getQuery()->getRawSql()); - } - - public function testSelectDistinctAndSelectCalls() - { - $query = $this->builder->select('name')->selectDistinct('surname')->select(['birthday', 'address'])->from('my_table'); - $this->assertEquals("SELECT DISTINCT `name`, `surname`, `birthday`, `address` FROM `cb_my_table`", $query->getQuery()->getRawSql()); - } - - public function testSelectDistinctWithSingleColumn() - { - $query = $this->builder->selectDistinct('name')->from('my_table'); - $this->assertEquals("SELECT DISTINCT `name` FROM `cb_my_table`", $query->getQuery()->getRawSql()); - } - - public function testSelectFlexibility() - { - $query = $this->builder - ->select('foo') - ->select(['bar', 'baz']) - ->select('qux', 'lol', 'wut') - ->from('t') - ; - $this->assertEquals( - 'SELECT `foo`, `bar`, `baz`, `qux`, `lol`, `wut` FROM `cb_t`', - $query->getQuery()->getRawSql(), - 'SELECT is pretty flexible!' - ); - } - - public function testSelectQuery() - { - $subQuery = $this->builder->table('person_details')->select('details')->where('person_id', '=', 3); - - $query = $this->builder->table('my_table') - ->select('my_table.*') - ->select([$this->builder->raw('count(cb_my_table.id) AS `tot`'), $this->builder->subQuery($subQuery, 'pop')]) - ->where('value', '=', 'Ifrah') - ->whereNot('my_table.id', -1) - ->orWhereNot('my_table.id', -2) - ->orWhereIn('my_table.id', [1, 2]) - ->groupBy(['value', 'my_table.id', 'person_details.id']) - ->orderBy('my_table.id', 'DESC') - ->orderBy('value') - ->having('tot', '<', 2) - ->limit(1) - ->offset(0) - ->join( - 'person_details', - 'person_details.person_id', - '=', - 'my_table.id' - ) - ; - - $nestedQuery = $this->builder->table($this->builder->subQuery($query, 'bb'))->select('*'); - $this->assertEquals("SELECT * FROM (SELECT `cb_my_table`.*, count(cb_my_table.id) AS `tot`, (SELECT `details` FROM `cb_person_details` WHERE `person_id` = 3) AS `pop` FROM `cb_my_table` INNER JOIN `cb_person_details` ON `cb_person_details`.`person_id` = `cb_my_table`.`id` WHERE `value` = 'Ifrah' AND NOT `cb_my_table`.`id` = -1 OR NOT `cb_my_table`.`id` = -2 OR `cb_my_table`.`id` IN (1, 2) GROUP BY `value`, `cb_my_table`.`id`, `cb_person_details`.`id` HAVING `tot` < 2 ORDER BY `cb_my_table`.`id` DESC, `value` ASC LIMIT 1 OFFSET 0) AS `bb`" - , $nestedQuery->getQuery()->getRawSql()); - } - - public function testSelectQueryWithNestedCriteriaAndJoins() - { - $builder = $this->builder; - - $query = $builder->table('my_table') - ->where('my_table.id', '>', 1) - ->orWhere('my_table.id', 1) - ->where(function ($q) { - $q->where('value', 'LIKE', '%sana%'); - $q->orWhere(function ($q2) { - $q2->where('key', 'LIKE', '%sana%'); - $q2->orWhere('value', 'LIKE', '%sana%'); - }); - }) - ->join(['person_details', 'a'], 'a.person_id', '=', 'my_table.id') - ->leftJoin(['person_details', 'b'], function ($table) use ($builder) { - $table->on('b.person_id', '=', 'my_table.id'); - $table->on('b.deleted', '=', $builder->raw(0)); - $table->orOn('b.age', '>', $builder->raw(1)); - }) - ; - - $this->assertEquals("SELECT * FROM `cb_my_table` INNER JOIN `cb_person_details` AS `cb_a` ON `cb_a`.`person_id` = `cb_my_table`.`id` LEFT JOIN `cb_person_details` AS `cb_b` ON `cb_b`.`person_id` = `cb_my_table`.`id` AND `cb_b`.`deleted` = 0 OR `cb_b`.`age` > 1 WHERE `cb_my_table`.`id` > 1 OR `cb_my_table`.`id` = 1 AND (`value` LIKE '%sana%' OR (`key` LIKE '%sana%' OR `value` LIKE '%sana%'))" - , $query->getQuery()->getRawSql()); - } - - public function testSelectQueryWithNull() - { - $query = $this->builder->from('my_table') - ->whereNull('key1') - ->orWhereNull('key2') - ->whereNotNull('key3') - ->orWhereNotNull('key4') - ->orWhere('key5', '=', null) - ; - - $this->assertEquals( - "SELECT * FROM `cb_my_table` WHERE `key1` IS NULL OR `key2` IS NULL AND `key3` IS NOT NULL OR `key4` IS NOT NULL OR `key5` = NULL", - $query->getQuery()->getRawSql() - ); - } - - public function testSelectWithQueryEvents() - { - $builder = $this->builder; - - $builder->registerEvent('before-select', ':any', function ($qb) { - $qb->whereIn('status', [1, 2]); - }); - - $query = $builder->table('some_table')->where('name', 'Some'); - $query->get(); - $actual = $query->getQuery()->getRawSql(); - - $this->assertEquals("SELECT * FROM `cb_some_table` WHERE `name` = 'Some' AND `status` IN (1, 2)", $actual); - } - - public function testStandaloneWhereNot() - { - $query = $this->builder->table('my_table')->whereNot('foo', 1); - $this->assertEquals("SELECT * FROM `cb_my_table` WHERE NOT `foo` = 1", $query->getQuery()->getRawSql()); - } - - public function testUpdateQuery() - { - $builder = $this->builder->table('my_table')->where('value', 'Sana'); - - $data = [ - 'key' => 'Sana', - 'value' => 'Amrin', - ]; - - $this->assertEquals("UPDATE `cb_my_table` SET `key`='Sana',`value`='Amrin' WHERE `value` = 'Sana'" - , $builder->getQuery('update', $data)->getRawSql()); - } - - public function testFromSubQuery() { - - $subQuery = $this->builder->table('person'); - $builder = $this->builder->table($this->builder->subQuery($subQuery))->where('id', '=', 2); - - $this->assertEquals('SELECT * FROM (SELECT * FROM `cb_person`) WHERE `id` = 2', $builder->getQuery()->getRawSql()); - - } - - public function testTableAlias() { - - $builder = $this->builder->table('persons')->alias('staff'); - - $this->assertEquals('SELECT * FROM `cb_persons` AS `staff`', $builder->getQuery()->getRawSql()); - - } - - public function testWhereNotNullSubQuery() { - $subQuery = $this->builder->table('persons')->alias('staff'); - - $query = $this->builder->whereNull($this->builder->subQuery($subQuery)); - - $this->assertEquals('SELECT * WHERE (SELECT * FROM `cb_persons` AS `staff`) IS NULL', $query->getQuery()->getRawSql()); +class QueryBuilderTest extends TestCase { + /** + * @var QueryBuilderHandler + */ + private $builder; + + public function setUp() { + parent::setUp(); + + $this->builder = new QueryBuilder\QueryBuilderHandler($this->mockConnection); + } + + /** + * Test alias + */ + public function testAlias() { + $query = $this->builder + ->table(['table1']) + ->alias('t1') + ->join('table2', 'table2.person_id', '=', 'foo2.id'); + + $this->assertEquals('SELECT * FROM `cb_table1` AS `t1` INNER JOIN `cb_table2` ON `cb_table2`.`person_id` = `cb_foo2`.`id`', + $query->getQuery()->getRawSql()); + } + + /** + * Test delete + */ + public function testDeleteQuery() { + $this->builder = new QueryBuilder\QueryBuilderHandler($this->mockConnection); + + $builder = $this->builder->table('my_table')->where('value', '=', 'Amrin'); + + $this->assertEquals("DELETE FROM `cb_my_table` WHERE `value` = 'Amrin'" + , $builder->getQuery('delete')->getRawSql()); + } + + public function testEventPropagation() { + $builder = $this->builder; + + $events = [ + 'before-insert', + 'after-insert', + 'before-select', + 'after-select', + 'before-update', + 'after-update', + 'before-delete', + 'after-delete', + ]; + + $triggeredEvents = []; + + foreach ($events as $event) { + $builder->registerEvent($event, ':any', function ($qb) use (&$triggeredEvents, $event) { + $triggeredEvents[] = $event; + }); + } + + $builder->table('foo')->insert(['bar' => 'baz']); + $builder->from('foo')->select('bar')->get(); + $builder->table('foo')->update(['bar' => 'baz']); + $builder->from('foo')->delete(); + + $this->assertEquals($triggeredEvents, $events); + } + + public function testInsertIgnoreQuery() { + $builder = $this->builder->from('my_table'); + $data = [ + 'key' => 'Name', + 'value' => 'Sana', + ]; + + $this->assertEquals("INSERT IGNORE INTO `cb_my_table` (`key`,`value`) VALUES ('Name','Sana')" + , $builder->getQuery('insertignore', $data)->getRawSql()); + } + + public function testInsertOnDuplicateKeyUpdateQuery() { + $builder = $this->builder; + $data = [ + 'name' => 'Sana', + 'counter' => 1, + ]; + $dataUpdate = [ + 'name' => 'Sana', + 'counter' => 2, + ]; + $builder->from('my_table')->onDuplicateKeyUpdate($dataUpdate); + $this->assertEquals("INSERT INTO `cb_my_table` (`name`,`counter`) VALUES ('Sana',1) ON DUPLICATE KEY UPDATE `name`='Sana',`counter`=2" + , $builder->getQuery('insert', $data)->getRawSql()); + } + + public function testInsertQuery() { + $builder = $this->builder->from('my_table'); + $data = [ + 'key' => 'Name', + 'value' => 'Sana', + ]; + + $this->assertEquals("INSERT INTO `cb_my_table` (`key`,`value`) VALUES ('Name','Sana')" + , $builder->getQuery('insert', $data)->getRawSql()); + } + + public function testIsPossibleToUseSubqueryInWhereClause() { + $sub = clone $this->builder; + $query = $this->builder->from('my_table')->whereIn('foo', $this->builder->subQuery( + $sub->from('some_table')->select('foo')->where('id', 1) + )); + $this->assertEquals( + "SELECT * FROM `cb_my_table` WHERE `foo` IN (SELECT `foo` FROM `cb_some_table` WHERE `id` = 1)", + $query->getQuery()->getRawSql() + ); + } + + public function testIsPossibleToUseSubqueryInWhereNotClause() { + $sub = clone $this->builder; + $query = $this->builder->from('my_table')->whereNotIn('foo', $this->builder->subQuery( + $sub->from('some_table')->select('foo')->where('id', 1) + )); + $this->assertEquals( + "SELECT * FROM `cb_my_table` WHERE `foo` NOT IN (SELECT `foo` FROM `cb_some_table` WHERE `id` = 1)", + $query->getQuery()->getRawSql() + ); + } + + public function testOrderByFlexibility() { + $query = $this->builder + ->from('t') + ->orderBy('foo', 'DESC') + ->orderBy(['bar', 'baz' => 'ASC', $this->builder->raw('raw1')], 'DESC') + ->orderBy($this->builder->raw('raw2'), 'DESC'); + + $this->assertEquals( + 'SELECT * FROM `cb_t` ORDER BY `foo` DESC, `bar` DESC, `baz` ASC, raw1 DESC, raw2 DESC', + $query->getQuery()->getRawSql(), + 'ORDER BY is flexible enough!' + ); + } + + public function testRawStatementsWithinCriteria() { + $query = $this->builder->from('my_table') + ->where('simple', 'criteria') + ->where($this->builder->raw('RAW')) + ->where($this->builder->raw('PARAMETERIZED_ONE(?)', 'foo')) + ->where($this->builder->raw('PARAMETERIZED_SEVERAL(?, ?, ?)', [1, '2', 'foo'])); + + $this->assertEquals( + "SELECT * FROM `cb_my_table` WHERE `simple` = 'criteria' AND RAW AND PARAMETERIZED_ONE('foo') AND PARAMETERIZED_SEVERAL(1, '2', 'foo')", + $query->getQuery()->getRawSql() + ); + } + + public function testReplaceQuery() { + $builder = $this->builder->from('my_table'); + $data = [ + 'key' => 'Name', + 'value' => 'Sana', + ]; + + $this->assertEquals("REPLACE INTO `cb_my_table` (`key`,`value`) VALUES ('Name','Sana')" + , $builder->getQuery('replace', $data)->getRawSql()); + } + + public function testSelectAliases() { + $query = $this->builder->from('my_table')->select('foo')->select(['bar' => 'baz', 'qux']); + + $this->assertEquals( + "SELECT `foo`, `bar` AS `baz`, `qux` FROM `cb_my_table`", + $query->getQuery()->getRawSql() + ); + } + + public function testSelectDistinct() { + $query = $this->builder->selectDistinct(['name', 'surname'])->from('my_table'); + $this->assertEquals("SELECT DISTINCT `name`, `surname` FROM `cb_my_table`", $query->getQuery()->getRawSql()); + } + + public function testSelectDistinctAndSelectCalls() { + $query = $this->builder->select('name')->selectDistinct('surname')->select([ + 'birthday', + 'address' + ])->from('my_table'); + $this->assertEquals("SELECT DISTINCT `name`, `surname`, `birthday`, `address` FROM `cb_my_table`", $query->getQuery()->getRawSql()); + } + + public function testSelectDistinctWithSingleColumn() { + $query = $this->builder->selectDistinct('name')->from('my_table'); + $this->assertEquals("SELECT DISTINCT `name` FROM `cb_my_table`", $query->getQuery()->getRawSql()); + } + + public function testSelectFlexibility() { + $query = $this->builder + ->select('foo') + ->select(['bar', 'baz']) + ->select('qux', 'lol', 'wut') + ->from('t'); + $this->assertEquals( + 'SELECT `foo`, `bar`, `baz`, `qux`, `lol`, `wut` FROM `cb_t`', + $query->getQuery()->getRawSql(), + 'SELECT is pretty flexible!' + ); + } + + public function testSelectQuery() { + $subQuery = $this->builder->table('person_details')->select('details')->where('person_id', '=', 3); + + $query = $this->builder->table('my_table') + ->select('my_table.*') + ->select([ + $this->builder->raw('count(cb_my_table.id) AS `tot`'), + $this->builder->subQuery($subQuery, 'pop') + ]) + ->where('value', '=', 'Ifrah') + ->whereNot('my_table.id', - 1) + ->orWhereNot('my_table.id', - 2) + ->orWhereIn('my_table.id', [1, 2]) + ->groupBy(['value', 'my_table.id', 'person_details.id']) + ->orderBy('my_table.id', 'DESC') + ->orderBy('value') + ->having('tot', '<', 2) + ->limit(1) + ->offset(0) + ->join( + 'person_details', + 'person_details.person_id', + '=', + 'my_table.id' + ); + + $nestedQuery = $this->builder->table($this->builder->subQuery($query, 'bb'))->select('*'); + $this->assertEquals("SELECT * FROM (SELECT `cb_my_table`.*, count(cb_my_table.id) AS `tot`, (SELECT `details` FROM `cb_person_details` WHERE `person_id` = 3) AS `pop` FROM `cb_my_table` INNER JOIN `cb_person_details` ON `cb_person_details`.`person_id` = `cb_my_table`.`id` WHERE `value` = 'Ifrah' AND NOT `cb_my_table`.`id` = -1 OR NOT `cb_my_table`.`id` = -2 OR `cb_my_table`.`id` IN (1, 2) GROUP BY `value`, `cb_my_table`.`id`, `cb_person_details`.`id` HAVING `tot` < 2 ORDER BY `cb_my_table`.`id` DESC, `value` ASC LIMIT 1 OFFSET 0) AS `bb`" + , $nestedQuery->getQuery()->getRawSql()); + } + + public function testSelectQueryWithNestedCriteriaAndJoins() { + $builder = $this->builder; + + $query = $builder->table('my_table') + ->where('my_table.id', '>', 1) + ->orWhere('my_table.id', 1) + ->where(function ($q) { + $q->where('value', 'LIKE', '%sana%'); + $q->orWhere(function ($q2) { + $q2->where('key', 'LIKE', '%sana%'); + $q2->orWhere('value', 'LIKE', '%sana%'); + }); + }) + ->join(['person_details', 'a'], 'a.person_id', '=', 'my_table.id') + ->leftJoin(['person_details', 'b'], function ($table) use ($builder) { + $table->on('b.person_id', '=', 'my_table.id'); + $table->on('b.deleted', '=', $builder->raw(0)); + $table->orOn('b.age', '>', $builder->raw(1)); + }); + + $this->assertEquals("SELECT * FROM `cb_my_table` INNER JOIN `cb_person_details` AS `cb_a` ON `cb_a`.`person_id` = `cb_my_table`.`id` LEFT JOIN `cb_person_details` AS `cb_b` ON `cb_b`.`person_id` = `cb_my_table`.`id` AND `cb_b`.`deleted` = 0 OR `cb_b`.`age` > 1 WHERE `cb_my_table`.`id` > 1 OR `cb_my_table`.`id` = 1 AND (`value` LIKE '%sana%' OR (`key` LIKE '%sana%' OR `value` LIKE '%sana%'))" + , $query->getQuery()->getRawSql()); + } + + public function testSelectQueryWithNull() { + $query = $this->builder->from('my_table') + ->whereNull('key1') + ->orWhereNull('key2') + ->whereNotNull('key3') + ->orWhereNotNull('key4') + ->orWhere('key5', '=', null); + + $this->assertEquals( + "SELECT * FROM `cb_my_table` WHERE `key1` IS NULL OR `key2` IS NULL AND `key3` IS NOT NULL OR `key4` IS NOT NULL OR `key5` = NULL", + $query->getQuery()->getRawSql() + ); + } + + public function testSelectWithQueryEvents() { + $builder = $this->builder; + + $builder->registerEvent('before-select', ':any', function ($qb) { + $qb->whereIn('status', [1, 2]); + }); + + $query = $builder->table('some_table')->where('name', 'Some'); + $query->get(); + $actual = $query->getQuery()->getRawSql(); + + $this->assertEquals("SELECT * FROM `cb_some_table` WHERE `name` = 'Some' AND `status` IN (1, 2)", $actual); + } + + public function testStandaloneWhereNot() { + $query = $this->builder->table('my_table')->whereNot('foo', 1); + $this->assertEquals("SELECT * FROM `cb_my_table` WHERE NOT `foo` = 1", $query->getQuery()->getRawSql()); + } + + public function testUpdateQuery() { + $builder = $this->builder->table('my_table')->where('value', 'Sana'); + + $data = [ + 'key' => 'Sana', + 'value' => 'Amrin', + ]; + + $this->assertEquals("UPDATE `cb_my_table` SET `key`='Sana',`value`='Amrin' WHERE `value` = 'Sana'" + , $builder->getQuery('update', $data)->getRawSql()); + } + + public function testFromSubQuery() { + + $subQuery = $this->builder->table('person'); + $builder = $this->builder->table($this->builder->subQuery($subQuery))->where('id', '=', 2); + + $this->assertEquals('SELECT * FROM (SELECT * FROM `cb_person`) WHERE `id` = 2', $builder->getQuery()->getRawSql()); + + } + + public function testTableAlias() { + + $builder = $this->builder->table('persons')->alias('staff'); + + $this->assertEquals('SELECT * FROM `cb_persons` AS `staff`', $builder->getQuery()->getRawSql()); + + } + + public function testWhereNotNullSubQuery() { + $subQuery = $this->builder->table('persons')->alias('staff'); - } + $query = $this->builder->whereNull($this->builder->subQuery($subQuery)); + + $this->assertEquals('SELECT * WHERE (SELECT * FROM `cb_persons` AS `staff`) IS NULL', $query->getQuery()->getRawSql()); + + } } diff --git a/tests/Pecee/Pixie/QueryBuilderTest.php b/tests/Pecee/Pixie/QueryBuilderTest.php index 8a9d45d..163091e 100644 --- a/tests/Pecee/Pixie/QueryBuilderTest.php +++ b/tests/Pecee/Pixie/QueryBuilderTest.php @@ -2,7 +2,6 @@ namespace Pecee\Pixie; -use PDO; use Pecee\Pixie\QueryBuilder\QueryBuilderHandler; /** @@ -10,112 +9,96 @@ * * @package Pecee\Pixie */ -class QueryBuilder extends TestCase -{ - /** - * @var QueryBuilderHandler - */ - protected $builder; - - /** - * Setup - */ - public function setUp() - { - parent::setUp(); - - $this->builder = new QueryBuilderHandler($this->mockConnection); - } - - public function testFalseBoolWhere() - { - $result = $this->builder->table('test')->where('id', '=', false); - $this->assertEquals('SELECT * FROM `cb_test` WHERE `id` = 0', $result->getQuery()->getRawSql()); - } - - public function testInsertQueryReturnsIdForInsert() - { - $this->mockPdoStatement - ->expects($this->once()) - ->method('rowCount') - ->will($this->returnValue(1)) - ; - - $this->mockPdo - ->expects($this->once()) - ->method('lastInsertId') - ->will($this->returnValue(11)) - ; - - $id = $this->builder->table('test')->insert([ - 'id' => 5, - 'name' => 'usman', - ]) - ; - - $this->assertEquals(11, $id); - } - - public function testInsertQueryReturnsIdForInsertIgnore() - { - $this->mockPdoStatement - ->expects($this->once()) - ->method('rowCount') - ->will($this->returnValue(1)) - ; - - $this->mockPdo - ->expects($this->once()) - ->method('lastInsertId') - ->will($this->returnValue(11)) - ; - - $id = $this->builder->table('test')->insertIgnore([ - 'id' => 5, - 'name' => 'usman', - ]) - ; - - $this->assertEquals(11, $id); - } - - public function testInsertQueryReturnsNullForIgnoredInsert() - { - $this->mockPdoStatement - ->expects($this->once()) - ->method('rowCount') - ->will($this->returnValue(0)) - ; - - $id = $this->builder->table('test')->insertIgnore([ - 'id' => 5, - 'name' => 'usman', - ]) - ; - - $this->assertEquals(null, $id); - } - - public function testRawQuery() - { - $query = 'select * from cb_my_table where id = ? and name = ? and hipster = null'; - $bindings = [5, 'usman', null]; - $queryArr = $this->builder->query($query, $bindings)->get(); - $this->assertEquals( - [ - $query, - [5, 'usman', null], - ], - $queryArr - ); - } - - public function testNullableWhere() - { - $query = $this->builder->table('person')->where('name', [1,null,3]); - - $this->assertEquals($query->getQuery()->getRawSql(), 'SELECT * FROM `cb_person` WHERE `name` = (1, NULL, 3)'); - - } +class QueryBuilder extends TestCase { + /** + * @var QueryBuilderHandler + */ + protected $builder; + + /** + * Setup + */ + public function setUp() { + parent::setUp(); + + $this->builder = new QueryBuilderHandler($this->mockConnection); + } + + public function testFalseBoolWhere() { + $result = $this->builder->table('test')->where('id', '=', false); + $this->assertEquals('SELECT * FROM `cb_test` WHERE `id` = 0', $result->getQuery()->getRawSql()); + } + + public function testInsertQueryReturnsIdForInsert() { + $this->mockPdoStatement + ->expects($this->once()) + ->method('rowCount') + ->will($this->returnValue(1)); + + $this->mockPdo + ->expects($this->once()) + ->method('lastInsertId') + ->will($this->returnValue(11)); + + $id = $this->builder->table('test')->insert([ + 'id' => 5, + 'name' => 'usman', + ]); + + $this->assertEquals(11, $id); + } + + public function testInsertQueryReturnsIdForInsertIgnore() { + $this->mockPdoStatement + ->expects($this->once()) + ->method('rowCount') + ->will($this->returnValue(1)); + + $this->mockPdo + ->expects($this->once()) + ->method('lastInsertId') + ->will($this->returnValue(11)); + + $id = $this->builder->table('test')->insertIgnore([ + 'id' => 5, + 'name' => 'usman', + ]); + + $this->assertEquals(11, $id); + } + + public function testInsertQueryReturnsNullForIgnoredInsert() { + $this->mockPdoStatement + ->expects($this->once()) + ->method('rowCount') + ->will($this->returnValue(0)); + + $id = $this->builder->table('test')->insertIgnore([ + 'id' => 5, + 'name' => 'usman', + ]); + + $this->assertEquals(null, $id); + } + + public function testRawQuery() { + $query = 'select * from cb_my_table where id = ? and name = ? and hipster = null'; + $bindings = [5, 'usman', null]; + $queryArr = $this->builder->query($query, $bindings)->get(); + $this->assertEquals( + [ + $query, + [5, 'usman', null], + ], + $queryArr + ); + } + + public function testNullableWhere() { + $query = $this->builder->table('person')->where('name', [1, null, 3]); + + $this->assertEquals($query->getQuery()->getRawSql(), 'SELECT * FROM `cb_person` WHERE `name` = (1, NULL, 3)'); + + } } diff --git a/tests/Pecee/Pixie/TransactionTest.php b/tests/Pecee/Pixie/TransactionTest.php index cf416ba..48e8ffe 100644 --- a/tests/Pecee/Pixie/TransactionTest.php +++ b/tests/Pecee/Pixie/TransactionTest.php @@ -10,193 +10,184 @@ * * @package Pecee\Pixie */ -class TransactionTest extends \PHPUnit\Framework\TestCase -{ - /** - * @var QueryBuilderHandler - */ - private $builder; - - public function setUp() - { - // NOTE: This test will require a live PDO connection - - $connection = new \Pecee\Pixie\Connection('mysql', [ - 'driver' => 'mysql', - 'host' => '127.0.0.1', - 'database' => 'test', - 'username' => 'root', - 'password' => '', - 'charset' => 'utf8mb4', // Optional - 'collation' => 'utf8mb4_unicode_ci', // Optional - 'prefix' => '', // Table prefix, optional - ]); - - $this->builder = $connection->getQueryBuilder(); - - } - - public function testTransactionResult() - { - - $this->builder->statement('TRUNCATE `people`'); - - $ids = []; - - $this->builder->transaction(function (Transaction $q) use (&$ids) { - - $ids = $q->table('people')->insert([ - [ - 'name' => 'Simon', - 'age' => 12, - 'awesome' => true, - 'nickname' => 'ponylover94', - ], - [ - 'name' => 'Peter', - 'age' => 40, - 'awesome' => false, - 'nickname' => null, - ], - [ - 'name' => 'Bobby', - 'age' => 20, - 'awesome' => true, - 'nickname' => 'peter', - ], - ]); - - }); - - $this->assertEquals(1, $ids[0]); - $this->assertEquals(2, $ids[1]); - $this->assertEquals(3, $ids[2]); - - $this->assertEquals($this->builder->table('people')->count(), 3); - - } - - - /** - * @throws Exception - */ - public function testNestedTransactions() - { - - $this->builder->statement('TRUNCATE `people`; TRUNCATE `animals`'); - - function getAnimals() - { - return [ - ['name' => 'mouse', 'number_of_legs' => '28'], - ['name' => 'horse', 'number_of_legs' => '4'], - ['name' => 'cat', 'number_of_legs' => '8'] - ]; - } - - function getPersons() - { - return - [ - [ - 'name' => 'Osama', - 'age' => '2', - 'awesome' => '1', - 'nickname' => 'jihad4evar', - ], - [ - 'name' => 'Leila', - 'age' => '76', - 'awesome' => '1', - 'nickname' => 'coolcatlady', - ], - [ - 'name' => 'Henry', - 'age' => '56', - 'awesome' => '1', - 'nickname' => 'ponylover95', - ] - ]; - } - - $this->builder->transaction(function (Transaction $qb) { - - function firstTrans(Transaction $oQuery) - { - - $oQuery->transaction(function (Transaction $qb) { - - $qb->table('animals')->insert([ - getAnimals() - ]); - - }); - } - - function secondTrans(Transaction $oQuery) - { - $oQuery->transaction(function (\Pecee\Pixie\QueryBuilder\Transaction $qb) { - - $qb->table('people')->insert([ - getPersons() - ]); - - }); - } - - firstTrans($qb); - secondTrans($qb); - - }); - - - $animals = $this->builder->table('animals')->select(['name', 'number_of_legs'])->get(); - $persons = $this->builder->table('people')->select(['name', 'age', 'awesome', 'nickname'])->get(); - - $originalPersons = getPersons(); - $originalAnimals = getAnimals(); - - $this->assertSameSize($persons, $originalPersons); - $this->assertEquals((array)$persons[0], $originalPersons[0]); - $this->assertEquals((array)$persons[1], $originalPersons[1]); - $this->assertEquals((array)$persons[2], $originalPersons[2]); - - $this->assertSameSize($animals, $originalAnimals); - $this->assertEquals((array)$animals[0], $originalAnimals[0]); - $this->assertEquals((array)$animals[1], $originalAnimals[1]); - } - - public function testTransactionMultipleInsert() - { - $this->builder->statement('TRUNCATE `people`'); - - $ids = $this->builder->table('people')->insert([ - [ - 'name' => 'Simon', - 'age' => 12, - 'awesome' => true, - 'nickname' => 'ponylover94', - ], - [ - 'name' => 'Peter', - 'age' => 40, - 'awesome' => false, - 'nickname' => null, - ], - [ - 'name' => 'Bobby', - 'age' => 20, - 'awesome' => true, - 'nickname' => 'peter', - ], - ]); - - $this->assertEquals(1, $ids[0]); - $this->assertEquals(2, $ids[1]); - $this->assertEquals(3, $ids[2]); - - $this->assertEquals($this->builder->table('people')->count(), 3); - - } +class TransactionTest extends \PHPUnit\Framework\TestCase { + /** + * @var QueryBuilderHandler + */ + private $builder; + + public function setUp() { + // NOTE: This test will require a live PDO connection + + $connection = new \Pecee\Pixie\Connection('mysql', [ + 'driver' => 'mysql', + 'host' => '127.0.0.1', + 'database' => 'test', + 'username' => 'root', + 'password' => '123456', + 'charset' => 'utf8mb4', // Optional + 'collation' => 'utf8mb4_unicode_ci', // Optional + 'prefix' => '', // Table prefix, optional + ]); + + $this->builder = $connection->getQueryBuilder(); + + } + + public function testTransactionResult() { + + $this->builder->statement('TRUNCATE `people`'); + + $ids = []; + + $this->builder->transaction(function (Transaction $q) use (&$ids) { + + $ids = $q->table('people')->insert([ + [ + 'name' => 'Simon', + 'age' => 12, + 'awesome' => true, + 'nickname' => 'ponylover94', + ], + [ + 'name' => 'Peter', + 'age' => 40, + 'awesome' => false, + 'nickname' => null, + ], + [ + 'name' => 'Bobby', + 'age' => 20, + 'awesome' => true, + 'nickname' => 'peter', + ], + ]); + + }); + + $this->assertEquals(1, $ids[0]); + $this->assertEquals(2, $ids[1]); + $this->assertEquals(3, $ids[2]); + + $this->assertEquals($this->builder->table('people')->count(), 3); + + } + + + /** + * @throws Exception + */ + public function testNestedTransactions() { + + $this->builder->statement('TRUNCATE `people`; TRUNCATE `animal`'); + + function getAnimals() { + return [ + ['name' => 'mouse', 'number_of_legs' => '28'], + ['name' => 'horse', 'number_of_legs' => '4'], + ['name' => 'cat', 'number_of_legs' => '8'] + ]; + } + + function getPersons() { + return + [ + [ + 'name' => 'Osama', + 'age' => '2', + 'awesome' => '1', + 'nickname' => 'jihad4evar', + ], + [ + 'name' => 'Leila', + 'age' => '76', + 'awesome' => '1', + 'nickname' => 'coolcatlady', + ], + [ + 'name' => 'Henry', + 'age' => '56', + 'awesome' => '1', + 'nickname' => 'ponylover95', + ] + ]; + } + + $this->builder->transaction(function (Transaction $qb) { + + function firstTrans(Transaction $oQuery) { + + $oQuery->transaction(function (Transaction $qb) { + + $qb->table('animal')->insert([ + getAnimals() + ]); + + }); + } + + function secondTrans(Transaction $oQuery) { + $oQuery->transaction(function (\Pecee\Pixie\QueryBuilder\Transaction $qb) { + + $qb->table('people')->insert([ + getPersons() + ]); + + }); + } + + firstTrans($qb); + secondTrans($qb); + + }); + + + $animals = $this->builder->table('animal')->select(['name', 'number_of_legs'])->get(); + $persons = $this->builder->table('people')->select(['name', 'age', 'awesome', 'nickname'])->get(); + + $originalPersons = getPersons(); + $originalAnimals = getAnimals(); + + $this->assertSameSize($persons, $originalPersons); + $this->assertEquals((array)$persons[0], $originalPersons[0]); + $this->assertEquals((array)$persons[1], $originalPersons[1]); + $this->assertEquals((array)$persons[2], $originalPersons[2]); + + $this->assertSameSize($animals, $originalAnimals); + $this->assertEquals((array)$animals[0], $originalAnimals[0]); + $this->assertEquals((array)$animals[1], $originalAnimals[1]); + } + + public function testTransactionMultipleInsert() { + $this->builder->statement('TRUNCATE `people`'); + + $ids = $this->builder->table('people')->insert([ + [ + 'name' => 'Simon', + 'age' => 12, + 'awesome' => true, + 'nickname' => 'ponylover94', + ], + [ + 'name' => 'Peter', + 'age' => 40, + 'awesome' => false, + 'nickname' => null, + ], + [ + 'name' => 'Bobby', + 'age' => 20, + 'awesome' => true, + 'nickname' => 'peter', + ], + ]); + + $this->assertEquals(1, $ids[0]); + $this->assertEquals(2, $ids[1]); + $this->assertEquals(3, $ids[2]); + + $this->assertEquals($this->builder->table('people')->count(), 3); + + } } diff --git a/tests/TestCase.php b/tests/TestCase.php index afcba2b..7211d8f 100644 --- a/tests/TestCase.php +++ b/tests/TestCase.php @@ -3,6 +3,7 @@ namespace Pecee\Pixie; use Mockery as m; +use Pecee\Pixie\ConnectionAdapters\Mysql; use Viocon\Container; /** @@ -10,110 +11,99 @@ * * @package Pecee\Pixie */ -class TestCase extends \PHPUnit\Framework\TestCase -{ - /** - * @var Container - */ - protected $container; - /** - * @var \Mockery\Mock - */ - protected $mockConnection; - /** - * @var \PDO - */ - protected $mockPdo; - /** - * @var \Mockery\Mock - */ - protected $mockPdoStatement; - - /** - * @return array - */ - public function callbackMock() - { - $args = func_get_args(); - - return count($args) == 1 ? $args[0] : $args; - } - - public function setUp() - { - $this->container = new Container(); - - $this->mockPdoStatement = $this->getMockBuilder(\PDOStatement::class)->getMock(); - - $mockPdoStatement = &$this->mockPdoStatement; - - $mockPdoStatement->bindings = []; - - $this->mockPdoStatement - ->expects($this->any()) - ->method('bindValue') - ->will($this->returnCallback(function ($parameter, $value, $dataType) use ($mockPdoStatement) { - $mockPdoStatement->bindings[] = [$value, $dataType]; - })) - ; - - $this->mockPdoStatement - ->expects($this->any()) - ->method('execute') - ->will($this->returnCallback(function ($bindings = null) use ($mockPdoStatement) { - if ($bindings) { - $mockPdoStatement->bindings = $bindings; - } - })) - ; - - - $this->mockPdoStatement - ->expects($this->any()) - ->method('fetchAll') - ->will($this->returnCallback(function () use ($mockPdoStatement) { - return [$mockPdoStatement->sql, $mockPdoStatement->bindings]; - })) - ; - - $this->mockPdo = $this - ->getMockBuilder(MockPdo::class) - ->setMethods(['prepare', 'setAttribute', 'quote', 'lastInsertId']) - ->getMock() - ; - - $this->mockPdo - ->expects($this->any()) - ->method('prepare') - ->will($this->returnCallback(function ($sql) use ($mockPdoStatement) { - $mockPdoStatement->sql = $sql; - - return $mockPdoStatement; - })) - ; - - $this->mockPdo - ->expects($this->any()) - ->method('quote') - ->will($this->returnCallback(function ($value) { - return "'$value'"; - })) - ; - - $eventHandler = new EventHandler(); - - $this->mockConnection = m::mock(Connection::class); - $this->mockConnection->shouldReceive('getPdoInstance')->andReturn($this->mockPdo); - $this->mockConnection->shouldReceive('getAdapter')->andReturn('mysql'); - $this->mockConnection->shouldReceive('getAdapterConfig')->andReturn(['prefix' => 'cb_']); - $this->mockConnection->shouldReceive('getContainer')->andReturn($this->container); - $this->mockConnection->shouldReceive('getEventHandler')->andReturn($eventHandler); - } - - public function tearDown() - { - m::close(); - } +class TestCase extends \PHPUnit\Framework\TestCase { + /** + * @var Container + */ + protected $container; + /** + * @var \Mockery\Mock + */ + protected $mockConnection; + /** + * @var \PDO + */ + protected $mockPdo; + /** + * @var \Mockery\Mock + */ + protected $mockPdoStatement; + + /** + * @return array + */ + public function callbackMock() { + $args = func_get_args(); + + return count($args) == 1 ? $args[0] : $args; + } + + public function setUp() { + $this->container = new Container(); + + $this->mockPdoStatement = $this->getMockBuilder(\PDOStatement::class)->getMock(); + + $mockPdoStatement = &$this->mockPdoStatement; + + $mockPdoStatement->bindings = []; + + $this->mockPdoStatement + ->expects($this->any()) + ->method('bindValue') + ->will($this->returnCallback(function ($parameter, $value, $dataType) use ($mockPdoStatement) { + $mockPdoStatement->bindings[] = [$value, $dataType]; + })); + + $this->mockPdoStatement + ->expects($this->any()) + ->method('execute') + ->will($this->returnCallback(function ($bindings = null) use ($mockPdoStatement) { + if ($bindings) { + $mockPdoStatement->bindings = $bindings; + } + })); + + + $this->mockPdoStatement + ->expects($this->any()) + ->method('fetchAll') + ->will($this->returnCallback(function () use ($mockPdoStatement) { + return [$mockPdoStatement->sql, $mockPdoStatement->bindings]; + })); + + $this->mockPdo = $this + ->getMockBuilder(MockPdo::class) + ->setMethods(['prepare', 'setAttribute', 'quote', 'lastInsertId']) + ->getMock(); + + $this->mockPdo + ->expects($this->any()) + ->method('prepare') + ->will($this->returnCallback(function ($sql) use ($mockPdoStatement) { + $mockPdoStatement->sql = $sql; + + return $mockPdoStatement; + })); + + $this->mockPdo + ->expects($this->any()) + ->method('quote') + ->will($this->returnCallback(function ($value) { + return "'$value'"; + })); + + $eventHandler = new EventHandler(); + + $this->mockConnection = m::mock(Connection::class); + $this->mockConnection->shouldReceive('getPdoInstance')->andReturn($this->mockPdo); + $this->mockConnection->shouldReceive('getAdapter')->andReturn(new Mysql()); + $this->mockConnection->shouldReceive('getAdapterConfig')->andReturn(['prefix' => 'cb_']); + $this->mockConnection->shouldReceive('getEventHandler')->andReturn($eventHandler); + } + + public function tearDown() { + m::close(); + } } /** @@ -121,13 +111,11 @@ public function tearDown() * * @package Pecee\Pixie */ -class MockPdo extends \PDO -{ - /** - * MockPdo constructor. - */ - public function __construct() - { - - } +class MockPdo extends \PDO { + /** + * MockPdo constructor. + */ + public function __construct() { + + } }