From 9d9bbf197c1dcfca34add8d9f908e509d41fc676 Mon Sep 17 00:00:00 2001 From: Glynn Quelch Date: Sat, 15 Jan 2022 01:14:27 +0000 Subject: [PATCH 01/69] Added a few tests for sub queries used for tables and where conditions --- tests/TestQueryBuilderHandler.php | 74 +++++++++++++++++++++++++++++-- 1 file changed, 70 insertions(+), 4 deletions(-) diff --git a/tests/TestQueryBuilderHandler.php b/tests/TestQueryBuilderHandler.php index cbe18f7..5d4e1c3 100644 --- a/tests/TestQueryBuilderHandler.php +++ b/tests/TestQueryBuilderHandler.php @@ -25,7 +25,7 @@ class TestQueryBuilderHandler extends WP_UnitTestCase /** Mocked WPDB instance. * @var Logable_WPDB - */ + */ private $wpdb; public function setUp(): void @@ -112,13 +112,13 @@ public function testTransactionWithRollback(): void $builder->rollback(); }); $this->assertSame(["START TRANSACTION", "ROLLBACK"], $this->wpdb->usage_log['query']); - $this->assertTrue(true, 'Avoids issues with no assertion in test!'); + $this->assertTrue(true, 'Avoids issues with no assertion in test!'); } /** @testdox It should be possible to use WPDB errors which are printed to the screen as a trigger for auto rollback with a transaction. This mimics PDO */ public function testTransactionCatchWPDBError(): void { - $this->queryBuilderProvider() + $this->queryBuilderProvider() ->transaction(function (Transaction $builder) { $builder->table('foo')->insert(['name' => 'Dave']); print('WPDB ERROR - Insert name=Dave'); @@ -130,7 +130,7 @@ public function testTransactionCatchWPDBError(): void /** @testdox It should be possible to catch an exceptions and trigger for auto rollback with a transaction. This mimics PDO */ public function testTransactionCatchException(): void { - $this->queryBuilderProvider() + $this->queryBuilderProvider() ->transaction(function (Transaction $builder) { $builder->table('foo')->insert(['name' => 'Dave']); throw new Exception("Error Processing Request", 1); @@ -217,4 +217,70 @@ public function testAddTablePrefix(): void ); $this->assertEquals('prefix_someTable', $prefixedSingle); } + + /** @testdox It should be possible to create a nested query using subQueries. (Example from Readme) */ + public function testSubQueryForTable(): void + { + $builder = $this->queryBuilderProvider(); + + $subQuery = $builder->table('person_details') + ->select('details') + ->where('person_id', '=', 3); + + + $query = $builder->table('my_table') + ->select('my_table.*') + ->select($builder->subQuery($subQuery, 'table_alias1')); + + $builder->table($builder->subQuery($query, 'table_alias2')) + ->select('*') + ->get(); + + $this->assertEquals( + 'SELECT * FROM (SELECT my_table.*, (SELECT details FROM person_details WHERE person_id = 3) as table_alias1 FROM my_table) as table_alias2', + $this->wpdb->usage_log['get_results'][0]['query'] + ); + } + + /** + * @see https://www.mysqltutorial.org/mysql-subquery/ + * @testdox ...find customers whose payments are greater than the average payment using a subquery + */ + public function testSubQueryExample() + { + $builder = $this->queryBuilderProvider(); + + $avgSubQuery = $builder->table('payments')->select("AVG(amount)"); + + $builder->select(['customerNumber', 'checkNumber', 'amount']) + ->from('payments') + ->where('amount', '>', $builder->subQuery($avgSubQuery)) + ->get(); + + $this->assertEquals( + 'SELECT customerNumber, checkNumber, amount FROM payments WHERE amount > (SELECT AVG(amount) FROM payments)', + $this->wpdb->usage_log['get_results'][0]['query'] + ); + } + + /** + * @see https://www.mysqltutorial.org/mysql-subquery/ + * @testdox ...you can use a subquery with NOT IN operator to find the customers who have not placed any orders + */ + public function testSubQueryInOperatorExample() + { + $builder = $this->queryBuilderProvider(); + + $avgSubQuery = $builder->table('orders')->selectDistinct("customerNumber"); + + $builder->table('customers') + ->select('customerName') + ->whereNotIn('customerNumber', $builder->subQuery($avgSubQuery)) + ->get(); + + $this->assertEquals( + 'SELECT customerName FROM customers WHERE customerNumber NOT IN (SELECT DISTINCT customerNumber FROM orders)', + $this->wpdb->usage_log['get_results'][0]['query'] + ); + } } From aca5164b81d67f28056d1e08d23523ac8d823717 Mon Sep 17 00:00:00 2001 From: Glynn Quelch Date: Sat, 15 Jan 2022 01:28:03 +0000 Subject: [PATCH 02/69] House Keeping --- tests/TestQueryBuilderHandler.php | 15 +++++++++++++++ 1 file changed, 15 insertions(+) diff --git a/tests/TestQueryBuilderHandler.php b/tests/TestQueryBuilderHandler.php index 5d4e1c3..2cf9bcf 100644 --- a/tests/TestQueryBuilderHandler.php +++ b/tests/TestQueryBuilderHandler.php @@ -283,4 +283,19 @@ public function testSubQueryInOperatorExample() $this->wpdb->usage_log['get_results'][0]['query'] ); } + + /** @testdox It should be possible to use partial expressions as strings and not have quotes added automatically by WPDB::prepare() */ + public function testUseRawValueForUnescapedMysqlConstants(): void + { + $this->queryBuilderProvider()->table('foo')->update(['bar' => new Raw('TIMESTAMP')]); + $this->assertEquals("UPDATE foo SET bar=TIMESTAMP", $this->wpdb->usage_log['get_results'][0]['query']); + + $this->queryBuilderProvider()->table('orders') + ->select(['Order_ID', 'Product_Name', new Raw("DATE_FORMAT(Order_Date,'%d--%m--%y') as new_date_formate") ]) + ->get(); + $this->assertEquals( + "SELECT Order_ID, Product_Name, DATE_FORMAT(Order_Date,'%d--%m--%y') as new_date_formate FROM orders", + $this->wpdb->usage_log['get_results'][1]['query'] + ); + } } From 51dee4b5ee9b9b71c71ecf00ff7748c5c222bb73 Mon Sep 17 00:00:00 2001 From: Glynn Quelch Date: Sat, 15 Jan 2022 01:29:26 +0000 Subject: [PATCH 03/69] House Keeping --- tests/TestQueryBuilderSQLGeneration.php | 81 +++++++++++++++++++++++++ 1 file changed, 81 insertions(+) diff --git a/tests/TestQueryBuilderSQLGeneration.php b/tests/TestQueryBuilderSQLGeneration.php index 0488ed9..c199236 100644 --- a/tests/TestQueryBuilderSQLGeneration.php +++ b/tests/TestQueryBuilderSQLGeneration.php @@ -723,4 +723,85 @@ public function testDelete() $this->assertEquals('DELETE FROM foo WHERE id > %d', $prepared['query']); $this->assertEquals('DELETE FROM foo WHERE id > 5', $query['query']); } + + /** @testdox It should be possible to create a nested query using subQueries. (Example from Readme) */ + public function testSubQueryForTable(): void + { + $builder = $this->queryBuilderProvider(); + + $subQuery = $builder->table('person_details') + ->select('details') + ->where('person_id', '=', 3); + + + $query = $builder->table('my_table') + ->select('my_table.*') + ->select($builder->subQuery($subQuery, 'table_alias1')); + + $builder->table($builder->subQuery($query, 'table_alias2')) + ->select('*') + ->get(); + + $this->assertEquals( + 'SELECT * FROM (SELECT my_table.*, (SELECT details FROM person_details WHERE person_id = 3) as table_alias1 FROM my_table) as table_alias2', + $this->wpdb->usage_log['get_results'][0]['query'] + ); + } + + /** + * @see https://www.mysqltutorial.org/mysql-subquery/ + * @testdox ...find customers whose payments are greater than the average payment using a subquery + */ + public function testSubQueryExample() + { + $builder = $this->queryBuilderProvider(); + + $avgSubQuery = $builder->table('payments')->select("AVG(amount)"); + + $builder->select(['customerNumber', 'checkNumber', 'amount']) + ->from('payments') + ->where('amount', '>', $builder->subQuery($avgSubQuery)) + ->get(); + + $this->assertEquals( + 'SELECT customerNumber, checkNumber, amount FROM payments WHERE amount > (SELECT AVG(amount) FROM payments)', + $this->wpdb->usage_log['get_results'][0]['query'] + ); + } + + /** + * @see https://www.mysqltutorial.org/mysql-subquery/ + * @testdox ...you can use a subquery with NOT IN operator to find the customers who have not placed any orders + */ + public function testSubQueryInOperatorExample() + { + $builder = $this->queryBuilderProvider(); + + $avgSubQuery = $builder->table('orders')->selectDistinct("customerNumber"); + + $builder->table('customers') + ->select('customerName') + ->whereNotIn('customerNumber', $builder->subQuery($avgSubQuery)) + ->get(); + + $this->assertEquals( + 'SELECT customerName FROM customers WHERE customerNumber NOT IN (SELECT DISTINCT customerNumber FROM orders)', + $this->wpdb->usage_log['get_results'][0]['query'] + ); + } + + /** @testdox It should be possible to use partial expressions as strings and not have quotes added automatically by WPDB::prepare() */ + public function testUseRawValueForUnescapedMysqlConstants(): void + { + $this->queryBuilderProvider()->table('foo')->update(['bar' => new Raw('TIMESTAMP')]); + $this->assertEquals("UPDATE foo SET bar=TIMESTAMP", $this->wpdb->usage_log['get_results'][0]['query']); + + $this->queryBuilderProvider()->table('orders') + ->select(['Order_ID', 'Product_Name', new Raw("DATE_FORMAT(Order_Date,'%d--%m--%y') as new_date_formate") ]) + ->get(); + $this->assertEquals( + "SELECT Order_ID, Product_Name, DATE_FORMAT(Order_Date,'%d--%m--%y') as new_date_formate FROM orders", + $this->wpdb->usage_log['get_results'][1]['query'] + ); + } } From 4b2387d327a5add9a6c5bbd52371f53adb9a42f2 Mon Sep 17 00:00:00 2001 From: Glynn Quelch Date: Sat, 15 Jan 2022 01:33:43 +0000 Subject: [PATCH 04/69] Updated phpstan config --- phpstan.neon.dist | 10 ++++ tests/TestQueryBuilderHandler.php | 81 ------------------------------- 2 files changed, 10 insertions(+), 81 deletions(-) diff --git a/phpstan.neon.dist b/phpstan.neon.dist index 655bebf..f8d4964 100644 --- a/phpstan.neon.dist +++ b/phpstan.neon.dist @@ -12,3 +12,13 @@ parameters: - %currentWorkingDirectory%/tests/* bootstrapFiles: - vendor/php-stubs/wordpress-stubs/wordpress-stubs.php + parallel: + processTimeout: 300.0 + jobSize: 10 + maximumNumberOfProcesses: 4 + minimumNumberOfJobsPerProcess: 4 + includes: + - vendor/phpstan/phpstan-strict-rules/rules.neon + - vendor/phpstan/phpstan-phpunit/extension.neon + - vendor/phpstan/phpstan-phpunit/rules.neon + - vendor/phpstan/phpstan-deprecation-rules/rules.neon diff --git a/tests/TestQueryBuilderHandler.php b/tests/TestQueryBuilderHandler.php index 2cf9bcf..65958cc 100644 --- a/tests/TestQueryBuilderHandler.php +++ b/tests/TestQueryBuilderHandler.php @@ -217,85 +217,4 @@ public function testAddTablePrefix(): void ); $this->assertEquals('prefix_someTable', $prefixedSingle); } - - /** @testdox It should be possible to create a nested query using subQueries. (Example from Readme) */ - public function testSubQueryForTable(): void - { - $builder = $this->queryBuilderProvider(); - - $subQuery = $builder->table('person_details') - ->select('details') - ->where('person_id', '=', 3); - - - $query = $builder->table('my_table') - ->select('my_table.*') - ->select($builder->subQuery($subQuery, 'table_alias1')); - - $builder->table($builder->subQuery($query, 'table_alias2')) - ->select('*') - ->get(); - - $this->assertEquals( - 'SELECT * FROM (SELECT my_table.*, (SELECT details FROM person_details WHERE person_id = 3) as table_alias1 FROM my_table) as table_alias2', - $this->wpdb->usage_log['get_results'][0]['query'] - ); - } - - /** - * @see https://www.mysqltutorial.org/mysql-subquery/ - * @testdox ...find customers whose payments are greater than the average payment using a subquery - */ - public function testSubQueryExample() - { - $builder = $this->queryBuilderProvider(); - - $avgSubQuery = $builder->table('payments')->select("AVG(amount)"); - - $builder->select(['customerNumber', 'checkNumber', 'amount']) - ->from('payments') - ->where('amount', '>', $builder->subQuery($avgSubQuery)) - ->get(); - - $this->assertEquals( - 'SELECT customerNumber, checkNumber, amount FROM payments WHERE amount > (SELECT AVG(amount) FROM payments)', - $this->wpdb->usage_log['get_results'][0]['query'] - ); - } - - /** - * @see https://www.mysqltutorial.org/mysql-subquery/ - * @testdox ...you can use a subquery with NOT IN operator to find the customers who have not placed any orders - */ - public function testSubQueryInOperatorExample() - { - $builder = $this->queryBuilderProvider(); - - $avgSubQuery = $builder->table('orders')->selectDistinct("customerNumber"); - - $builder->table('customers') - ->select('customerName') - ->whereNotIn('customerNumber', $builder->subQuery($avgSubQuery)) - ->get(); - - $this->assertEquals( - 'SELECT customerName FROM customers WHERE customerNumber NOT IN (SELECT DISTINCT customerNumber FROM orders)', - $this->wpdb->usage_log['get_results'][0]['query'] - ); - } - - /** @testdox It should be possible to use partial expressions as strings and not have quotes added automatically by WPDB::prepare() */ - public function testUseRawValueForUnescapedMysqlConstants(): void - { - $this->queryBuilderProvider()->table('foo')->update(['bar' => new Raw('TIMESTAMP')]); - $this->assertEquals("UPDATE foo SET bar=TIMESTAMP", $this->wpdb->usage_log['get_results'][0]['query']); - - $this->queryBuilderProvider()->table('orders') - ->select(['Order_ID', 'Product_Name', new Raw("DATE_FORMAT(Order_Date,'%d--%m--%y') as new_date_formate") ]) - ->get(); - $this->assertEquals( - "SELECT Order_ID, Product_Name, DATE_FORMAT(Order_Date,'%d--%m--%y') as new_date_formate FROM orders", - $this->wpdb->usage_log['get_results'][1]['query'] - ); - } } From 97940b607768736acb989dcaae763f13b78d0778 Mon Sep 17 00:00:00 2001 From: Glynn Quelch Date: Sat, 15 Jan 2022 01:42:30 +0000 Subject: [PATCH 05/69] added phpcs --- .php-cs-fixer.php | 49 +++++++++++++++++++++++++++++++++++++++++++++++ composer.json | 7 +++++-- 2 files changed, 54 insertions(+), 2 deletions(-) create mode 100644 .php-cs-fixer.php diff --git a/.php-cs-fixer.php b/.php-cs-fixer.php new file mode 100644 index 0000000..decc0e9 --- /dev/null +++ b/.php-cs-fixer.php @@ -0,0 +1,49 @@ +files() + ->name('*.php') + ->in(__DIR__ . '/src') + ->in(__DIR__ . '/tests') +; + +/** + * Cache file for PHP-CS + */ +$cacheFilePath = sprintf('%s%sphp_cs.cache-%s', sys_get_temp_dir(), DIRECTORY_SEPARATOR, md5(__DIR__)); + +/** + * Configuration + * + * @see https://mlocati.github.io/php-cs-fixer-configurator/# + */ +return (new Config('pecee-pixie')) + ->setCacheFile($cacheFilePath) + ->setRiskyAllowed(true) + ->setRules([ + // default + '@PSR2' => true, + '@Symfony' => true, + // additionally + 'array_syntax' => ['syntax' => 'short'], + 'concat_space' => false, + 'cast_spaces' => false, + 'no_unused_imports' => false, + 'no_useless_else' => true, + 'no_useless_return' => true, + 'no_superfluous_phpdoc_tags' => false, + 'ordered_imports' => true, + 'phpdoc_align' => true, + 'phpdoc_order' => true, + 'phpdoc_trim' => true, + 'phpdoc_summary' => false, + 'simplified_null_return' => false, + 'ternary_to_null_coalescing' => true, + 'binary_operator_spaces' => ['default' => 'align'], + ]) + ->setFinder($finder) + ; \ No newline at end of file diff --git a/composer.json b/composer.json index ed694f5..b3665af 100644 --- a/composer.json +++ b/composer.json @@ -40,7 +40,8 @@ "wp-phpunit/wp-phpunit": "^5.8", "symfony/var-dumper": "4.*", "yoast/phpunit-polyfills": "^1.0.0", - "gin0115/wpunit-helpers": "~1.0.0" + "gin0115/wpunit-helpers": "~1.0.0", + "friendsofphp/php-cs-fixer": "^3" }, "autoload": { "psr-4": { @@ -57,6 +58,8 @@ "test": "phpunit --coverage-clover clover.xml --testdox", "coverage": "phpunit --coverage-html coverage-report --testdox", "analyse": "vendor/bin/phpstan analyse src -l8", - "all": "composer test" + "all": "composer test", + "fixer": "php-cs-fixer fix --diff --show-progress=dots", + "lint": "php-cs-fixer fix --diff --dry-run" } } \ No newline at end of file From a0ceb79480bb83ea07db20a98bde99ff23d811f0 Mon Sep 17 00:00:00 2001 From: Glynn Quelch Date: Sat, 15 Jan 2022 01:49:36 +0000 Subject: [PATCH 06/69] Basic rules in place, BUT will assume types, so finish after phpstan --- .php-cs-fixer.php | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/.php-cs-fixer.php b/.php-cs-fixer.php index decc0e9..d6ecd0f 100644 --- a/.php-cs-fixer.php +++ b/.php-cs-fixer.php @@ -2,14 +2,14 @@ use PhpCsFixer\Config; use PhpCsFixer\Finder; + require __DIR__ . '/vendor/autoload.php'; $finder = (new Finder()) ->files() ->name('*.php') ->in(__DIR__ . '/src') - ->in(__DIR__ . '/tests') -; + ->in(__DIR__ . '/tests'); /** * Cache file for PHP-CS @@ -37,13 +37,13 @@ 'no_useless_return' => true, 'no_superfluous_phpdoc_tags' => false, 'ordered_imports' => true, - 'phpdoc_align' => true, - 'phpdoc_order' => true, - 'phpdoc_trim' => true, + 'phpdoc_align' => false, + 'phpdoc_order' => false, + 'phpdoc_trim' => false, 'phpdoc_summary' => false, 'simplified_null_return' => false, 'ternary_to_null_coalescing' => true, 'binary_operator_spaces' => ['default' => 'align'], + 'global_namespace_import' => ['import_classes' => true, 'import_functions' => true] ]) - ->setFinder($finder) - ; \ No newline at end of file + ->setFinder($finder); From 4e7e4addfbb4c8d458e490f22d009ef9b8eefc64 Mon Sep 17 00:00:00 2001 From: Glynn Quelch Date: Sat, 15 Jan 2022 16:14:42 +0000 Subject: [PATCH 07/69] Half way through updates to doc blocks and coding changes using level4 --- README.md | 4 +- composer.json | 2 +- phpstan.neon.dist | 2 +- src/QueryBuilder/QueryBuilderHandler.php | 233 ++++++++++++----------- src/QueryBuilder/WPDBAdapter.php | 107 +++++------ tests/TestIntegrationWithWPDB.php | 8 +- tests/TestQueryBuilderSQLGeneration.php | 2 +- 7 files changed, 187 insertions(+), 171 deletions(-) diff --git a/README.md b/README.md index d5baf33..c97edb8 100644 --- a/README.md +++ b/README.md @@ -174,9 +174,9 @@ var_dump($query->get()); ## Query You **must** use `table()` method before every query, except raw `query()`. -To select from multiple tables just pass an array. +To select from multiple tables you can use the variadic nature of the method ```PHP -QB::table(array('mytable1', 'mytable2')); +QB::table('mytable1', 'mytable2'); ``` diff --git a/composer.json b/composer.json index ed694f5..059f7a5 100644 --- a/composer.json +++ b/composer.json @@ -56,7 +56,7 @@ "scripts": { "test": "phpunit --coverage-clover clover.xml --testdox", "coverage": "phpunit --coverage-html coverage-report --testdox", - "analyse": "vendor/bin/phpstan analyse src -l8", + "analyse": "vendor/bin/phpstan analyse src -l4", "all": "composer test" } } \ No newline at end of file diff --git a/phpstan.neon.dist b/phpstan.neon.dist index 655bebf..38ab986 100644 --- a/phpstan.neon.dist +++ b/phpstan.neon.dist @@ -4,7 +4,7 @@ includes: - vendor/phpstan/phpstan/conf/bleedingEdge.neon - vendor/szepeviktor/phpstan-wordpress/extension.neon parameters: - level: max + level: 4 inferPrivatePropertyTypeFromConstructor: true paths: - %currentWorkingDirectory%/src/ diff --git a/src/QueryBuilder/QueryBuilderHandler.php b/src/QueryBuilder/QueryBuilderHandler.php index 298767d..9099ddc 100644 --- a/src/QueryBuilder/QueryBuilderHandler.php +++ b/src/QueryBuilder/QueryBuilderHandler.php @@ -37,7 +37,7 @@ class QueryBuilderHandler protected $dbInstance; /** - * @var null|string[] + * @var null|string|string[] */ protected $sqlStatement = null; @@ -47,7 +47,7 @@ class QueryBuilderHandler protected $tablePrefix = null; /** - * @var \Pixie\QueryBuilder\Adapters\BaseAdapter + * @var WPDBAdapter */ protected $adapterInstance; @@ -81,15 +81,11 @@ public function __construct( $connection = Connection::getStoredConnection(); } + // Set all dependencies from connection. $this->connection = $connection; $this->container = $this->connection->getContainer(); $this->dbInstance = $this->connection->getDbInstance(); - $this->adapter = 'wpdb'; - $this->adapterConfig = $this->connection->getAdapterConfig(); - - if (isset($this->adapterConfig['prefix'])) { - $this->tablePrefix = $this->adapterConfig['prefix']; - } + $this->setAdapterConfig($this->connection->getAdapterConfig()); // Set up optional hydration details. $this->setFetchMode($fetchMode); @@ -103,12 +99,25 @@ public function __construct( ); } + /** + * Sets the config for WPDB + * + * @param array $adapterConfig + * @return void + */ + protected function setAdapterConfig(array $adapterConfig): void + { + if (isset($adapterConfig['prefix'])) { + $this->tablePrefix = $adapterConfig['prefix']; + } + } + /** * Set the fetch mode * * @param string $mode * @param null|array $constructorArgs - * @return $this + * @return static */ public function setFetchMode(string $mode, ?array $constructorArgs = null): self { @@ -128,31 +137,43 @@ public function newQuery(Connection $connection = null) $connection = $this->connection; } - $new = new static($connection); - $new->setFetchMode($this->getFetchMode(), $this->hydratorConstructorArgs); - return $new; + $newQuery = $this->constructCurrentBuilderClass($connection); + $newQuery->setFetchMode($this->getFetchMode(), $this->hydratorConstructorArgs); + return $newQuery; } + /** - * @param $sql - * @param array $bindings + * Returns a new instance of the current, with the passed connection. * - * @return $this + * @param \Pixie\Connection $connection + * @return QueryBuilderHandler */ - public function query($sql, $bindings = array()) + protected function constructCurrentBuilderClass(Connection $connection): QueryBuilderHandler { - list($this->sqlStatement) = $this->statement($sql, $bindings); + $class = get_class(); + return new $class($connection); + } + /** + * @param string $sql + * @param array $bindings + * + * @return static + */ + public function query($sql, $bindings = array()): self + { + list($this->sqlStatement) = $this->statement($sql, $bindings); return $this; } /** - * @param $sql - * @param array $bindings + * @param string $sql + * @param array $bindings * - * @return array sqlStatement and execution time as float + * @return array{0:string, 1:float} */ - public function statement($sql, $bindings = array()) + public function statement(string $sql, $bindings = array()): array { $start = microtime(true); $sqlStatement = empty($bindings) ? $sql : $this->dbInstance->prepare($sql, $bindings); @@ -216,13 +237,15 @@ protected function getHydrator(): Hydrator */ protected function useHydrator(): bool { - return ! in_array($this->getFetchMode(), [\ARRAY_A, \ARRAY_N, \OBJECT, \OBJECT_K]); + return !in_array($this->getFetchMode(), [\ARRAY_A, \ARRAY_N, \OBJECT, \OBJECT_K]); } /** - * Get first row + * Find all matching a simple where condition. + * + * Shortcut of ->where('key','=','value')->limit(1)->get(); * - * @return \stdClass|array|null + * @return null|\stdClass\array|object Can return any object using hydrator */ public function first() { @@ -232,10 +255,14 @@ public function first() } /** - * @param $value + * Find all matching a simple where condition. + * + * Shortcut of ->where('key','=','value')->get(); + * * @param string $fieldName + * @param mixed $value * - * @return null|\stdClass + * @return null|\stdClass\array|object Can return any object using hydrator */ public function findAll($fieldName, $value) { @@ -244,10 +271,10 @@ public function findAll($fieldName, $value) } /** - * @param $value * @param string $fieldName + * @param mixed $value * - * @return null|\stdClass + * @return null|\stdClass\array|object Can return any object using hydrator */ public function find($value, $fieldName = 'id') { @@ -373,14 +400,14 @@ public function getQuery($type = 'select', $dataToBePassed = array()) /** * @param QueryBuilderHandler $queryBuilder - * @param null $alias + * @param string|null $alias * * @return Raw */ - public function subQuery(QueryBuilderHandler $queryBuilder, $alias = null) + public function subQuery(QueryBuilderHandler $queryBuilder, ?string $alias = null) { $sql = '(' . $queryBuilder->getQuery()->getRawSql() . ')'; - if ($alias) { + if (is_string($alias) && \mb_strlen($alias) !== 0) { $sql = $sql . ' as ' . $alias; } @@ -388,11 +415,13 @@ public function subQuery(QueryBuilderHandler $queryBuilder, $alias = null) } /** - * @param $data + * Handles the various insert operations based on the type. * - * @return array|string + * @param array $data + * @param string $type + * @return int|int[]|null|mixed Can return a single row id, array of row ids, null (for failed) or any other value short circuited from event. */ - private function doInsert($data, $type) + private function doInsert(array $data, string $type) { $eventResult = $this->fireEvents('before-insert'); if (!is_null($eventResult)) { @@ -431,9 +460,9 @@ private function doInsert($data, $type) } /** - * @param $data + * @param array $data Either key=>value array for single or array of arrays for bulk. * - * @return array|string + * @return int|int[]|null|mixed Can return a single row id, array of row ids, null (for failed) or any other value short circuited from event. */ public function insert($data) { @@ -441,9 +470,9 @@ public function insert($data) } /** - * @param $data * - * @return array|string + * @param array $data Either key=>value array for single or array of arrays for bulk. + * @return int|int[]|null|mixed Can return a single row id, array of row ids, null (for failed) or any other value short circuited from event. */ public function insertIgnore($data) { @@ -451,9 +480,9 @@ public function insertIgnore($data) } /** - * @param $data * - * @return array|string + * @param array $data Either key=>value array for single or array of arrays for bulk. + * @return int|int[]|null|mixed Can return a single row id, array of row ids, null (for failed) or any other value short circuited from event. */ public function replace($data) { @@ -461,9 +490,9 @@ public function replace($data) } /** - * @param $data + * @param array $data * - * @return bool + * @return int|null */ public function update($data) { @@ -484,9 +513,8 @@ public function update($data) } /** - * @param $data - * - * @return array|string + * @param array $data + * @return int|null Will return row id for insert and bool for success/fail on update. */ public function updateOrInsert($data) { @@ -498,9 +526,8 @@ public function updateOrInsert($data) } /** - * @param $data - * - * @return $this + * @param array $data + * @return static */ public function onDuplicateKeyUpdate($data) { @@ -528,20 +555,15 @@ public function delete() } /** - * @param string|array $tables Single table or array of tables + * @param string|Raw ...$tables Single table or array of tables * * @return QueryBuilderHandler * @throws Exception */ - public function table($tables) + public function table(...$tables): QueryBuilderHandler { - if (!is_array($tables)) { - // because a single table is converted to an array anyways, - // this makes sense. - $tables = func_get_args(); - } - $instance = new static($this->connection); + $instance = $this->constructCurrentBuilderClass($this->connection); $this->setFetchMode($this->getFetchMode(), $this->hydratorConstructorArgs); $tables = $this->addTablePrefix($tables, false); $instance->addStatement('tables', $tables); @@ -549,15 +571,12 @@ public function table($tables) } /** - * @param $tables + * @param string|Raw ...$tables Single table or array of tables * - * @return $this + * @return static */ - public function from($tables) + public function from(...$tables): self { - if (!is_array($tables)) { - $tables = func_get_args(); - } $tables = $this->addTablePrefix($tables, false); $this->addStatement('tables', $tables); @@ -565,11 +584,11 @@ public function from($tables) } /** - * @param $fields + * @param string|string[]|Raw[]|array $fields * - * @return $this + * @return static */ - public function select($fields) + public function select($fields): self { if (!is_array($fields)) { $fields = func_get_args(); @@ -581,9 +600,9 @@ public function select($fields) } /** - * @param $fields + * @param string|string[]|Raw[]|array $fields * - * @return $this + * @return static */ public function selectDistinct($fields) { @@ -595,9 +614,9 @@ public function selectDistinct($fields) /** * @param string|string[] $field Either the single field or an array of fields. * - * @return $this + * @return static */ - public function groupBy($field) + public function groupBy($field): self { $field = $this->addTablePrefix($field); $this->addStatement('groupBys', $field); @@ -608,9 +627,9 @@ public function groupBy($field) * @param string|string[] $fields * @param string $defaultDirection * - * @return $this + * @return static */ - public function orderBy($fields, $defaultDirection = 'ASC') + public function orderBy($fields, string $defaultDirection = 'ASC'): self { if (!is_array($fields)) { $fields = array($fields); @@ -633,36 +652,36 @@ public function orderBy($fields, $defaultDirection = 'ASC') } /** - * @param $limit + * @param int $limit * - * @return $this + * @return static */ - public function limit($limit) + public function limit(int $limit): self { $this->statements['limit'] = $limit; return $this; } /** - * @param $offset + * @param int $offset * - * @return $this + * @return static */ - public function offset($offset) + public function offset(int $offset): self { $this->statements['offset'] = $offset; return $this; } /** - * @param $key - * @param $operator - * @param $value + * @param string|string[]|Raw|Raw[] $key + * @param string $operator + * @param mixed $value * @param string $joiner * - * @return $this + * @return static */ - public function having($key, $operator, $value, $joiner = 'AND') + public function having($key, string $operator, $value, string $joiner = 'AND') { $key = $this->addTablePrefix($key); $this->statements['havings'][] = compact('key', 'operator', 'value', 'joiner'); @@ -670,11 +689,11 @@ public function having($key, $operator, $value, $joiner = 'AND') } /** - * @param $key - * @param $operator - * @param $value + * @param string|string[]|Raw|Raw[] $key + * @param string $operator + * @param mixed $value * - * @return $this + * @return static */ public function orHaving($key, $operator, $value) { @@ -682,13 +701,13 @@ public function orHaving($key, $operator, $value) } /** - * @param $key - * @param $operator - * @param $value + * @param string|raw $key + * @param string|null|mixed $operator Can be used as value, if 3rd arg not passed + * @param $value mixed|null * - * @return $this + * @return static */ - public function where($key, $operator = null, $value = null) + public function where($key, $operator = null, $value = null): void { // If two params are given then assume operator is = if (func_num_args() == 2) { @@ -703,7 +722,7 @@ public function where($key, $operator = null, $value = null) * @param $operator * @param $value * - * @return $this + * @return static */ public function orWhere($key, $operator = null, $value = null) { @@ -721,7 +740,7 @@ public function orWhere($key, $operator = null, $value = null) * @param $operator * @param $value * - * @return $this + * @return static */ public function whereNot($key, $operator = null, $value = null) { @@ -738,7 +757,7 @@ public function whereNot($key, $operator = null, $value = null) * @param $operator * @param $value * - * @return $this + * @return static */ public function orWhereNot($key, $operator = null, $value = null) { @@ -754,7 +773,7 @@ public function orWhereNot($key, $operator = null, $value = null) * @param $key * @param array $values * - * @return $this + * @return static */ public function whereIn($key, $values) { @@ -765,7 +784,7 @@ public function whereIn($key, $values) * @param $key * @param array $values * - * @return $this + * @return static */ public function whereNotIn($key, $values) { @@ -776,7 +795,7 @@ public function whereNotIn($key, $values) * @param $key * @param array $values * - * @return $this + * @return static */ public function orWhereIn($key, $values) { @@ -787,7 +806,7 @@ public function orWhereIn($key, $values) * @param $key * @param array $values * - * @return $this + * @return static */ public function orWhereNotIn($key, $values) { @@ -799,7 +818,7 @@ public function orWhereNotIn($key, $values) * @param $valueFrom * @param $valueTo * - * @return $this + * @return static */ public function whereBetween($key, $valueFrom, $valueTo) { @@ -811,7 +830,7 @@ public function whereBetween($key, $valueFrom, $valueTo) * @param $valueFrom * @param $valueTo * - * @return $this + * @return static */ public function orWhereBetween($key, $valueFrom, $valueTo) { @@ -869,7 +888,7 @@ protected function whereNullHandler($key, string $prefix = '', $operator = '') * @param $value * @param string $type * - * @return $this + * @return static */ public function join($table, $key, $operator = null, $value = null, $type = 'inner') { @@ -897,7 +916,7 @@ public function join($table, $key, $operator = null, $value = null, $type = 'inn * * @param $callback * - * @return $this + * @return static */ public function transaction(\Closure $callback) { @@ -956,7 +975,7 @@ protected function handleTransactionCall(callable $callback, Transaction $transa * @param null $operator * @param null $value * - * @return $this + * @return static */ public function leftJoin($table, $key, $operator = null, $value = null) { @@ -969,7 +988,7 @@ public function leftJoin($table, $key, $operator = null, $value = null) * @param null $operator * @param null $value * - * @return $this + * @return static */ public function rightJoin($table, $key, $operator = null, $value = null) { @@ -982,7 +1001,7 @@ public function rightJoin($table, $key, $operator = null, $value = null) * @param null $operator * @param null $value * - * @return $this + * @return static */ public function innerJoin($table, $key, $operator = null, $value = null) { @@ -1039,7 +1058,7 @@ public function dbInstance(): \wpdb /** * @param Connection $connection * - * @return $this + * @return static */ public function setConnection(Connection $connection) { @@ -1061,7 +1080,7 @@ public function getConnection() * @param $value * @param string $joiner * - * @return $this + * @return static */ protected function whereHandler($key, $operator = null, $value = null, $joiner = 'AND') { diff --git a/src/QueryBuilder/WPDBAdapter.php b/src/QueryBuilder/WPDBAdapter.php index 9d62327..fd4e084 100644 --- a/src/QueryBuilder/WPDBAdapter.php +++ b/src/QueryBuilder/WPDBAdapter.php @@ -33,12 +33,12 @@ public function __construct(Connection $connection) /** * Build select query string and bindings * - * @param $statements + * @param array $statements * * @throws Exception - * @return array + * @return array{sql:string,bindings:mixed[]} */ - public function select($statements) + public function select(array $statements): array { if (!array_key_exists('tables', $statements)) { throw new Exception('No table specified.', 3); @@ -82,6 +82,7 @@ public function select($statements) // Joins $joinString = $this->buildJoin($statements); + /** @var string[] */ $sqlArray = array( 'SELECT' . (isset($statements['distinct']) ? ' DISTINCT' : ''), $selects, @@ -109,12 +110,12 @@ public function select($statements) /** * Build just criteria part of the query * - * @param $statements + * @param array $statements * @param bool $bindValues * - * @return array + * @return array{sql:string[]|string, bindings:array} */ - public function criteriaOnly($statements, $bindValues = true) + public function criteriaOnly(array $statements, bool $bindValues = true): array { $sql = $bindings = array(); if (!isset($statements['criteria'])) { @@ -129,13 +130,14 @@ public function criteriaOnly($statements, $bindValues = true) /** * Build a generic insert/ignore/replace query * - * @param $statements - * @param array $data + * @param array $statements + * @param array $data + * @param string $type * - * @return array + * @return array{sql:string, bindings:mixed[]} * @throws Exception */ - private function doInsert($statements, array $data, $type) + private function doInsert(array $statements, array $data, string $type): array { if (!isset($statements['tables'])) { throw new Exception('No table specified', 3); @@ -160,7 +162,7 @@ private function doInsert($statements, array $data, $type) $this->wrapSanitizer($table), '(' . $this->arrayStr($keys, ',') . ')', 'VALUES', - '(' . $this->arrayStr($values, ',', false) . ')', + '(' . $this->arrayStr($values, ',') . ')', ); if (isset($statements['onduplicate'])) { @@ -180,10 +182,10 @@ private function doInsert($statements, array $data, $type) /** * Build Insert query * - * @param $statements - * @param array $data + * @param array $statements + * @param array $data $data * - * @return array + * @return array{sql:string, bindings:mixed[]} * @throws Exception */ public function insert($statements, array $data) @@ -194,10 +196,10 @@ public function insert($statements, array $data) /** * Build Insert Ignore query * - * @param $statements - * @param array $data + * @param array $statements + * @param array $data $data * - * @return array + * @return array{sql:string, bindings:mixed[]} * @throws Exception */ public function insertIgnore($statements, array $data) @@ -208,10 +210,10 @@ public function insertIgnore($statements, array $data) /** * Build Insert Ignore query * - * @param $statements - * @param array $data + * @param array $statements + * @param array $data $data * - * @return array + * @return array{sql:string, bindings:mixed[]} * @throws Exception */ public function replace($statements, array $data) @@ -222,11 +224,11 @@ public function replace($statements, array $data) /** * Build fields assignment part of SET ... or ON DUBLICATE KEY UPDATE ... statements * - * @param array $data + * @param array $data * - * @return array + * @return array{0:string,1:mixed[]} */ - private function getUpdateStatement($data) + private function getUpdateStatement(array $data): array { $bindings = array(); $statement = ''; @@ -247,10 +249,10 @@ private function getUpdateStatement($data) /** * Build update query * - * @param $statements - * @param array $data + * @param array $statements + * @param array $data * - * @return array + * @return array{sql:string, bindings:mixed[]} * @throws Exception */ public function update($statements, array $data) @@ -289,9 +291,9 @@ public function update($statements, array $data) /** * Build delete query * - * @param $statements + * @param array $statements * - * @return array + * @return array{sql:string, bindings:mixed[]} * @throws Exception */ public function delete($statements) @@ -319,22 +321,17 @@ public function delete($statements) * Array concatenating method, like implode. * But it does wrap sanitizer and trims last glue * - * @param array $pieces - * @param $glue - * @param bool $wrapSanitizer + * @param array $pieces + * @param string $glue * * @return string */ - protected function arrayStr(array $pieces, $glue, $wrapSanitizer = true) + protected function arrayStr(array $pieces, string $glue): string { $str = ''; foreach ($pieces as $key => $piece) { - if ($wrapSanitizer) { - $piece = $this->wrapSanitizer($piece); - } - if (!is_int($key)) { - $piece = ($wrapSanitizer ? $this->wrapSanitizer($key) : $key) . ' AS ' . $piece; + $piece = $key . ' AS ' . $piece; } $str .= $piece . $glue; @@ -346,11 +343,11 @@ protected function arrayStr(array $pieces, $glue, $wrapSanitizer = true) /** * Join different part of queries with a space. * - * @param array $pieces + * @param array $pieces * * @return string */ - protected function concatenateQuery(array $pieces) + protected function concatenateQuery(array $pieces): string { $str = ''; foreach ($pieces as $piece) { @@ -362,17 +359,17 @@ protected function concatenateQuery(array $pieces) /** * Build generic criteria string and bindings from statements, like "a = b and c = ?" * - * @param $statements + * @param array $statements * @param bool $bindValues * - * @return array + * @return array{0:string,1:string[]} */ - protected function buildCriteria($statements, $bindValues = true) + protected function buildCriteria(array $statements, bool $bindValues = true): array { $criteria = ''; $bindings = array(); foreach ($statements as $statement) { - $key = $this->wrapSanitizer($statement['key']); + $key = $statement['key']; $value = $statement['value']; if (is_null($value) && $key instanceof \Closure) { @@ -382,7 +379,7 @@ protected function buildCriteria($statements, $bindValues = true) // in the closure should reflect here $nestedCriteria = $this->container->build(NestedCriteria::class, array($this->connection)); - $nestedCriteria = & $nestedCriteria; + $nestedCriteria = &$nestedCriteria; // Call the closure with our new nestedCriteria object $key($nestedCriteria); // Get the criteria only query from the nestedCriteria object @@ -443,7 +440,7 @@ protected function buildCriteria($statements, $bindValues = true) // Clear all white spaces, and, or from beginning and white spaces from ending $criteria = preg_replace('/^(\s?AND ?|\s?OR ?)|\s$/i', '', $criteria); - return array($criteria, $bindings); + return array($criteria ?? '', $bindings); } /** @@ -470,9 +467,9 @@ public function inferType($value): string /** * Wrap values with adapter's sanitizer like, '`' * - * @param $value + * @param string|Raw|\Closure $value * - * @return string + * @return string|\Closure */ public function wrapSanitizer($value) { @@ -499,14 +496,14 @@ public function wrapSanitizer($value) /** * Build criteria string and binding with various types added, like WHERE and Having * - * @param $statements - * @param $key - * @param $type + * @param array $statements + * @param string $key + * @param string $type * @param bool $bindValues * - * @return array + * @return array{0:string, 1:string[]} */ - protected function buildCriteriaWithType($statements, $key, $type, $bindValues = true) + protected function buildCriteriaWithType(array $statements, string $key, string $type, bool $bindValues = true) { $criteria = ''; $bindings = array(); @@ -526,11 +523,11 @@ protected function buildCriteriaWithType($statements, $key, $type, $bindValues = /** * Build join string * - * @param $statements + * @param array $statements * - * @return array + * @return string */ - protected function buildJoin($statements) + protected function buildJoin(array $statements): string { $sql = ''; diff --git a/tests/TestIntegrationWithWPDB.php b/tests/TestIntegrationWithWPDB.php index 6a4d3fc..571c8de 100644 --- a/tests/TestIntegrationWithWPDB.php +++ b/tests/TestIntegrationWithWPDB.php @@ -32,7 +32,7 @@ class TestIntegrationWithWPDB extends WP_UnitTestCase public function setUp(): void { global $wpdb; - $this->wpdb = $wpdb; + $this->wpdb = clone $wpdb; parent::setUp(); if (! static::$createdTables) { @@ -245,7 +245,7 @@ public function testJoins(): void // Get all FRUITS (from mock_bar) with the TYPE (from mock_foo) $fruitsInner = $this->queryBuilderProvider('mock_') - ->select(['bar.string' => 'name', 'foo.string' => 'type']) + ->select(['bar.string' => 'name','foo.string' => 'type']) ->from('bar') ->join('foo', 'bar.number', '=', 'foo.number') ->setFetchMode(\ARRAY_A) @@ -266,7 +266,7 @@ public function testJoins(): void // Left Join $fruitsLeft = $this->queryBuilderProvider('mock_') - ->select(['bar.string' => 'name', 'foo.string' => 'type']) + ->select(['bar.string' => 'name','foo.string' => 'type']) ->from('bar') ->leftJoin('foo', 'bar.number', '=', 'foo.number') ->setFetchMode(\ARRAY_A) @@ -287,7 +287,7 @@ public function testJoins(): void // Right Join $fruitsRight = $this->queryBuilderProvider('mock_') - ->select(['bar.string' => 'name', 'foo.string' => 'type']) + ->select(['bar.string' => 'name','foo.string' => 'type']) ->from('bar') ->rightJoin('foo', 'bar.number', '=', 'foo.number') ->setFetchMode(\ARRAY_A) diff --git a/tests/TestQueryBuilderSQLGeneration.php b/tests/TestQueryBuilderSQLGeneration.php index c199236..d8bf073 100644 --- a/tests/TestQueryBuilderSQLGeneration.php +++ b/tests/TestQueryBuilderSQLGeneration.php @@ -60,7 +60,7 @@ public function testDefineFromTableNames(): void public function testMultiTableQuery(): void { $builder = $this->queryBuilderProvider() - ->table(['foo', 'bar']); + ->table('foo', 'bar'); $this->assertEquals('SELECT * FROM foo, bar', $builder->getQuery()->getSql()); } From d033da14483303d638a92619bdde268eba2b88cd Mon Sep 17 00:00:00 2001 From: Glynn Quelch Date: Sat, 15 Jan 2022 16:17:10 +0000 Subject: [PATCH 08/69] House Keeping --- src/QueryBuilder/QueryBuilderHandler.php | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/QueryBuilder/QueryBuilderHandler.php b/src/QueryBuilder/QueryBuilderHandler.php index 9099ddc..bd3516f 100644 --- a/src/QueryBuilder/QueryBuilderHandler.php +++ b/src/QueryBuilder/QueryBuilderHandler.php @@ -707,7 +707,7 @@ public function orHaving($key, $operator, $value) * * @return static */ - public function where($key, $operator = null, $value = null): void + public function where($key, $operator = null, $value = null): self { // If two params are given then assume operator is = if (func_num_args() == 2) { @@ -724,7 +724,7 @@ public function where($key, $operator = null, $value = null): void * * @return static */ - public function orWhere($key, $operator = null, $value = null) + public function orWhere($key, $operator = null, $value = null): self { // If two params are given then assume operator is = if (func_num_args() == 2) { @@ -742,7 +742,7 @@ public function orWhere($key, $operator = null, $value = null) * * @return static */ - public function whereNot($key, $operator = null, $value = null) + public function whereNot($key, $operator = null, $value = null): self { // If two params are given then assume operator is = if (func_num_args() == 2) { From 77b1cc842b7e0ab96c03b3596ce678de840cbe8b Mon Sep 17 00:00:00 2001 From: Glynn Quelch Date: Sun, 16 Jan 2022 01:11:22 +0000 Subject: [PATCH 09/69] Query builder class no whas valid doc types for PHPStan lv4 --- src/QueryBuilder/QueryBuilderHandler.php | 243 ++++++++++++----------- src/QueryBuilder/Raw.php | 2 +- 2 files changed, 125 insertions(+), 120 deletions(-) diff --git a/src/QueryBuilder/QueryBuilderHandler.php b/src/QueryBuilder/QueryBuilderHandler.php index bd3516f..b0de0ef 100644 --- a/src/QueryBuilder/QueryBuilderHandler.php +++ b/src/QueryBuilder/QueryBuilderHandler.php @@ -27,7 +27,7 @@ class QueryBuilderHandler protected $connection; /** - * @var array + * @var array */ protected $statements = array(); @@ -71,7 +71,7 @@ class QueryBuilderHandler * @param string $fetchMode * @throws Exception If no connection passed and not previously established. */ - public function __construct( + final public function __construct( Connection $connection = null, string $fetchMode = \OBJECT, ?array $hydratorConstructorArgs = null @@ -128,10 +128,10 @@ public function setFetchMode(string $mode, ?array $constructorArgs = null): self /** * @param null|Connection $connection - * @return QueryBuilderHandler + * @return static * @throws Exception */ - public function newQuery(Connection $connection = null) + public function newQuery(Connection $connection = null): self { if (is_null($connection)) { $connection = $this->connection; @@ -147,12 +147,11 @@ public function newQuery(Connection $connection = null) * Returns a new instance of the current, with the passed connection. * * @param \Pixie\Connection $connection - * @return QueryBuilderHandler + * @return static */ - protected function constructCurrentBuilderClass(Connection $connection): QueryBuilderHandler + protected function constructCurrentBuilderClass(Connection $connection): self { - $class = get_class(); - return new $class($connection); + return new static($connection); } /** @@ -557,7 +556,7 @@ public function delete() /** * @param string|Raw ...$tables Single table or array of tables * - * @return QueryBuilderHandler + * @return static * @throws Exception */ public function table(...$tables): QueryBuilderHandler @@ -624,7 +623,7 @@ public function groupBy($field): self } /** - * @param string|string[] $fields + * @param string|array $fields * @param string $defaultDirection * * @return static @@ -701,9 +700,9 @@ public function orHaving($key, $operator, $value) } /** - * @param string|raw $key + * @param string|Raw $key * @param string|null|mixed $operator Can be used as value, if 3rd arg not passed - * @param $value mixed|null + * @param mixed|null $value * * @return static */ @@ -718,9 +717,9 @@ public function where($key, $operator = null, $value = null): self } /** - * @param $key - * @param $operator - * @param $value + * @param string|Raw $key + * @param string|null|mixed $operator Can be used as value, if 3rd arg not passed + * @param mixed|null $value * * @return static */ @@ -736,9 +735,9 @@ public function orWhere($key, $operator = null, $value = null): self } /** - * @param $key - * @param $operator - * @param $value + * @param string|Raw $key + * @param string|null|mixed $operator Can be used as value, if 3rd arg not passed + * @param mixed|null $value * * @return static */ @@ -753,9 +752,9 @@ public function whereNot($key, $operator = null, $value = null): self } /** - * @param $key - * @param $operator - * @param $value + * @param string|Raw $key + * @param string|null|mixed $operator Can be used as value, if 3rd arg not passed + * @param mixed|null $value * * @return static */ @@ -770,110 +769,116 @@ public function orWhereNot($key, $operator = null, $value = null) } /** - * @param $key - * @param array $values + * @param string|Raw $key + * @param mixed[]|string|Raw $values * * @return static */ - public function whereIn($key, $values) + public function whereIn($key, $values): self { return $this->whereHandler($key, 'IN', $values, 'AND'); } /** - * @param $key - * @param array $values + * @param string|Raw $key + * @param mixed[]|string|Raw $values * * @return static */ - public function whereNotIn($key, $values) + public function whereNotIn($key, $values): self { return $this->whereHandler($key, 'NOT IN', $values, 'AND'); } /** - * @param $key - * @param array $values + * @param string|Raw $key + * @param mixed[]|string|Raw $values * * @return static */ - public function orWhereIn($key, $values) + public function orWhereIn($key, $values): self { return $this->whereHandler($key, 'IN', $values, 'OR'); } /** - * @param $key - * @param array $values + * @param string|Raw $key + * @param mixed[]|string|Raw $values * * @return static */ - public function orWhereNotIn($key, $values) + public function orWhereNotIn($key, $values): self { return $this->whereHandler($key, 'NOT IN', $values, 'OR'); } /** - * @param $key - * @param $valueFrom - * @param $valueTo + * @param string|Raw $key + * @param mixed $valueFrom + * @param mixed $valueTo * * @return static */ - public function whereBetween($key, $valueFrom, $valueTo) + public function whereBetween($key, $valueFrom, $valueTo): self { return $this->whereHandler($key, 'BETWEEN', array($valueFrom, $valueTo), 'AND'); } /** - * @param $key - * @param $valueFrom - * @param $valueTo + * @param string|Raw $key + * @param mixed $valueFrom + * @param mixed $valueTo * * @return static */ - public function orWhereBetween($key, $valueFrom, $valueTo) + public function orWhereBetween($key, $valueFrom, $valueTo): self { return $this->whereHandler($key, 'BETWEEN', array($valueFrom, $valueTo), 'OR'); } /** - * @param $key - * @return QueryBuilderHandler + * @param string|Raw $key + * @return static */ - public function whereNull($key) + public function whereNull($key): self { return $this->whereNullHandler($key); } /** - * @param $key - * @return QueryBuilderHandler + * @param string|Raw $key + * @return static */ - public function whereNotNull($key) + public function whereNotNull($key): self { return $this->whereNullHandler($key, 'NOT'); } /** - * @param $key - * @return QueryBuilderHandler + * @param string|Raw $key + * @return static */ - public function orWhereNull($key) + public function orWhereNull($key): self { return $this->whereNullHandler($key, '', 'or'); } /** - * @param $key - * @return QueryBuilderHandler + * @param string|Raw $key + * @return static */ - public function orWhereNotNull($key) + public function orWhereNotNull($key): self { return $this->whereNullHandler($key, 'NOT', 'or'); } - protected function whereNullHandler($key, string $prefix = '', $operator = '') + /** + * @param string|Raw $key + * @param string $prefix + * @param string $operator + * @return static + */ + protected function whereNullHandler($key, string $prefix = '', $operator = ''): self { $prefix = \strlen($prefix) === 0 ? '' : " {$prefix}"; @@ -882,15 +887,15 @@ protected function whereNullHandler($key, string $prefix = '', $operator = '') } /** - * @param $table - * @param $key - * @param $operator - * @param $value + * @param string|Raw $table + * @param string|Raw|\Closure $key + * @param string|null $operator + * @param mixed $value * @param string $type * * @return static */ - public function join($table, $key, $operator = null, $value = null, $type = 'inner') + public function join($table, $key, ?string $operator = null, $value = null, $type = 'inner') { if (!$key instanceof \Closure) { $key = function ($joinBuilder) use ($key, $operator, $value) { @@ -914,11 +919,10 @@ public function join($table, $key, $operator = null, $value = null, $type = 'inn /** * Runs a transaction * - * @param $callback - * + * @param \Closure(Transaction):void $callback * @return static */ - public function transaction(\Closure $callback) + public function transaction(\Closure $callback): self { try { // Begin the transaction @@ -948,11 +952,12 @@ public function transaction(\Closure $callback) * * Catches any WP Errors (printed) * - * @param callable $callback + * @param \Closure $callback * @param Transaction $transaction * @return void + * @throws Exception */ - protected function handleTransactionCall(callable $callback, Transaction $transaction): void + protected function handleTransactionCall(\Closure $callback, Transaction $transaction): void { try { ob_start(); @@ -970,10 +975,10 @@ protected function handleTransactionCall(callable $callback, Transaction $transa } /** - * @param $table - * @param $key - * @param null $operator - * @param null $value + * @param string|Raw $table + * @param string|Raw|\Closure $key + * @param string|null $operator + * @param mixed $value * * @return static */ @@ -983,10 +988,10 @@ public function leftJoin($table, $key, $operator = null, $value = null) } /** - * @param $table - * @param $key - * @param null $operator - * @param null $value + * @param string|Raw $table + * @param string|Raw|\Closure $key + * @param string|null $operator + * @param mixed $value * * @return static */ @@ -996,10 +1001,10 @@ public function rightJoin($table, $key, $operator = null, $value = null) } /** - * @param $table - * @param $key - * @param null $operator - * @param null $value + * @param string|Raw $table + * @param string|Raw|\Closure $key + * @param string|null $operator + * @param mixed $value * * @return static */ @@ -1009,11 +1014,11 @@ public function innerJoin($table, $key, $operator = null, $value = null) } /** - * @param string $table - * @param callable|string $key - * @param null|string $operator - * @param null|mixed $value - * @return this + * @param string|Raw $table + * @param string|Raw|\Closure $key + * @param string|null $operator + * @param mixed $value + * @return static */ public function crossJoin($table, $key, $operator = null, $value = null) { @@ -1021,11 +1026,11 @@ public function crossJoin($table, $key, $operator = null, $value = null) } /** - * @param string $table - * @param callable|string $key - * @param null|string $operator - * @param null|mixed $value - * @return this + * @param string|Raw $table + * @param string|Raw|\Closure $key + * @param string|null $operator + * @param mixed $value + * @return static */ public function outerJoin($table, $key, $operator = null, $value = null) { @@ -1035,20 +1040,20 @@ public function outerJoin($table, $key, $operator = null, $value = null) /** * Add a raw query * - * @param $value - * @param $bindings + * @param string|Raw $value + * @param mixed|mixed[] $bindings * - * @return mixed + * @return Raw */ - public function raw($value, $bindings = array()) + public function raw($value, $bindings = array()): Raw { - return $this->container->build(Raw::class, array($value, $bindings)); + return new Raw($value, $bindings); } /** * Return wpdb instance * - * @return wpdb + * @return \wpdb */ public function dbInstance(): \wpdb { @@ -1060,7 +1065,7 @@ public function dbInstance(): \wpdb * * @return static */ - public function setConnection(Connection $connection) + public function setConnection(Connection $connection): self { $this->connection = $connection; return $this; @@ -1075,11 +1080,11 @@ public function getConnection() } /** - * @param $key - * @param $operator - * @param $value + * @param string|Raw|\Closure $key + * @param null|string $operator + * @param null|mixed $value * @param string $joiner - * + * * @return static */ protected function whereHandler($key, $operator = null, $value = null, $joiner = 'AND') @@ -1092,12 +1097,12 @@ protected function whereHandler($key, $operator = null, $value = null, $joiner = /** * Add table prefix (if given) on given string. * - * @param string|string[] $values + * @param array|string|int|float|bool|Raw|\Closure $values * @param bool $tableFieldMix If we have mixes of field and table names with a "." * - * @return array|mixed + * @return mixed|mixed[] */ - public function addTablePrefix($values, $tableFieldMix = true) + public function addTablePrefix($values, bool $tableFieldMix = true) { if (is_null($this->tablePrefix)) { return $values; @@ -1129,7 +1134,7 @@ public function addTablePrefix($values, $tableFieldMix = true) $target = &$key; } - if (!$tableFieldMix || ($tableFieldMix && strpos($target, '.') !== false)) { + if (!$tableFieldMix || strpos($target, '.') !== false) { $target = $this->tablePrefix . $target; } @@ -1137,12 +1142,12 @@ public function addTablePrefix($values, $tableFieldMix = true) } // If we had single value then we should return a single value (end value of the array) - return $single ? end($return) : $return; + return true === $single ? end($return) : $return; } /** - * @param $key - * @param $value + * @param string|Raw|\Closure $key + * @param mixed|mixed[] $value */ protected function addStatement($key, $value) { @@ -1158,24 +1163,24 @@ protected function addStatement($key, $value) } /** - * @param $event - * @param $table + * @param string $event + * @param string|Raw $table * * @return callable|null */ - public function getEvent($event, $table = ':any') + public function getEvent(string $event, $table = ':any'): ?callable { return $this->connection->getEventHandler()->getEvent($event, $table); } /** - * @param $event - * @param string $table - * @param callable $action + * @param string $event + * @param string|Raw $table + * @param \Closure $action * * @return void */ - public function registerEvent($event, $table, \Closure $action) + public function registerEvent($event, $table, \Closure $action): void { $table = $table ?: ':any'; @@ -1183,31 +1188,31 @@ public function registerEvent($event, $table, \Closure $action) $table = $this->addTablePrefix($table, false); } - return $this->connection->getEventHandler()->registerEvent($event, $table, $action); + $this->connection->getEventHandler()->registerEvent($event, $table, $action); } /** - * @param $event - * @param string $table + * @param string $event + * @param string|Raw $table * * @return void */ - public function removeEvent($event, $table = ':any') + public function removeEvent(string $event, $table = ':any') { if ($table != ':any') { $table = $this->addTablePrefix($table, false); } - return $this->connection->getEventHandler()->removeEvent($event, $table); + $this->connection->getEventHandler()->removeEvent($event, $table); } /** - * @param $event + * @param string $event * @return mixed */ - public function fireEvents($event) + public function fireEvents(string $event) { - $params = func_get_args(); + $params = func_get_args(); // @todo Replace this with an easier to read alteratnive array_unshift($params, $this); return call_user_func_array(array($this->connection->getEventHandler(), 'fireEvents'), $params); } diff --git a/src/QueryBuilder/Raw.php b/src/QueryBuilder/Raw.php index 9030506..1b0d29b 100644 --- a/src/QueryBuilder/Raw.php +++ b/src/QueryBuilder/Raw.php @@ -16,7 +16,7 @@ class Raw protected $bindings; /** - * @param string|\Stringable|int|float|bool $value + * @param string|Raw $value * @param mixed|mixed[] $bindings */ public function __construct($value, $bindings = array()) From e45fd081981f4c2e4da20e3dc1776546e1deb630 Mon Sep 17 00:00:00 2001 From: Glynn Quelch Date: Sun, 16 Jan 2022 01:11:40 +0000 Subject: [PATCH 10/69] House Keeping --- src/QueryBuilder/QueryBuilderHandler.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/QueryBuilder/QueryBuilderHandler.php b/src/QueryBuilder/QueryBuilderHandler.php index b0de0ef..f422fa4 100644 --- a/src/QueryBuilder/QueryBuilderHandler.php +++ b/src/QueryBuilder/QueryBuilderHandler.php @@ -1218,7 +1218,7 @@ public function fireEvents(string $event) } /** - * @return array + * @return array */ public function getStatements() { From a093796028154384394db79323ed0f85ffcb12e8 Mon Sep 17 00:00:00 2001 From: Glynn Quelch Date: Sun, 16 Jan 2022 02:22:08 +0000 Subject: [PATCH 11/69] WPDBAdapter now l8 with phpstan, had to add in 2 methods for handling the possibility of closures with values that are sanitized. --- composer.json | 2 +- src/QueryBuilder/WPDBAdapter.php | 64 +++++++++++++++++++++++++++----- 2 files changed, 55 insertions(+), 11 deletions(-) diff --git a/composer.json b/composer.json index 059f7a5..ed694f5 100644 --- a/composer.json +++ b/composer.json @@ -56,7 +56,7 @@ "scripts": { "test": "phpunit --coverage-clover clover.xml --testdox", "coverage": "phpunit --coverage-html coverage-report --testdox", - "analyse": "vendor/bin/phpstan analyse src -l4", + "analyse": "vendor/bin/phpstan analyse src -l8", "all": "composer test" } } \ No newline at end of file diff --git a/src/QueryBuilder/WPDBAdapter.php b/src/QueryBuilder/WPDBAdapter.php index fd4e084..768ed0c 100644 --- a/src/QueryBuilder/WPDBAdapter.php +++ b/src/QueryBuilder/WPDBAdapter.php @@ -64,7 +64,11 @@ public function select(array $statements): array $orderBys = ''; if (isset($statements['orderBys']) && is_array($statements['orderBys'])) { foreach ($statements['orderBys'] as $orderBy) { - $orderBys .= $this->wrapSanitizer($orderBy['field']) . ' ' . $orderBy['type'] . ', '; + $field = $this->wrapSanitizer($orderBy['field']); + if ($field instanceof \Closure) { + continue; + } + $orderBys .= $field . ' ' . $orderBy['type'] . ', '; } if ($orderBys = trim($orderBys, ', ')) { @@ -174,11 +178,44 @@ private function doInsert(array $statements, array $data, string $type): array $bindings = array_merge($bindings, $updateBindings); } - $sql = $this->concatenateQuery($sqlArray); + $sql = $this->concatenateQuery($this->stringifyValues($sqlArray)); return compact('sql', 'bindings'); } + /** + * Attempts to stringify an array of values. + * + * @param array $values + * @return string[] + */ + protected function stringifyValues(array $values): array + { + $values = array_map([$this, 'stringifyValue'], $values); + /** @var string[] */ + return array_filter($values, 'is_string'); + } + + /** + * Attempts to stringify a single of values. + * + * @param string|\Closure|Raw $value + * @return string|null + */ + protected function stringifyValue($value): ?string + { + if ($value instanceof \Closure) { + $value = $value(); + return is_string($value) ? $value : null; + } + + if ($value instanceof Raw) { + return (string) $value; + } + + return $value; + } + /** * Build Insert query * @@ -235,9 +272,9 @@ private function getUpdateStatement(array $data): array foreach ($data as $key => $value) { if ($value instanceof Raw) { - $statement .= $this->wrapSanitizer($key) . '=' . $value . ','; + $statement .= $this->stringifyValue($this->wrapSanitizer($key)) . '=' . $value . ','; } else { - $statement .= $this->wrapSanitizer($key) . sprintf('=%s,', $this->inferType($value)); + $statement .= $this->stringifyValue($this->wrapSanitizer($key)) . sprintf('=%s,', $this->inferType($value)); $bindings[] = $value; } } @@ -282,7 +319,7 @@ public function update($statements, array $data) $limit ); - $sql = $this->concatenateQuery($sqlArray); + $sql = $this->concatenateQuery($this->stringifyValues($sqlArray)); $bindings = array_merge($bindings, $whereBindings); return compact('sql', 'bindings'); @@ -303,6 +340,11 @@ public function delete($statements) } $table = end($statements['tables']); + // Ensure table name is a string + $table = $this->stringifyValue($this->wrapSanitizer($table)); + if (null === $table) { + throw new Exception('Table must be a valid string.', 5); + } // Wheres list($whereCriteria, $whereBindings) = $this->buildCriteriaWithType($statements, 'wheres', 'WHERE'); @@ -310,7 +352,8 @@ public function delete($statements) // Limit $limit = isset($statements['limit']) ? 'LIMIT ' . $statements['limit'] : ''; - $sqlArray = array('DELETE FROM', $this->wrapSanitizer($table), $whereCriteria); + + $sqlArray = array('DELETE FROM', $table, $whereCriteria); $sql = $this->concatenateQuery($sqlArray); $bindings = $whereBindings; @@ -422,7 +465,7 @@ protected function buildCriteria(array $statements, bool $bindValues = true): ar // Specially for joins // We are not binding values, lets sanitize then - $value = $this->wrapSanitizer($value); + $value = $this->stringifyValue($this->wrapSanitizer($value)) ?? ''; $criteria .= $statement['joiner'] . ' ' . $key . ' ' . $statement['operator'] . ' ' . $value . ' '; } elseif ($statement['key'] instanceof Raw) { $criteria .= $statement['joiner'] . ' ' . $key . ' '; @@ -537,9 +580,9 @@ protected function buildJoin(array $statements): string foreach ($statements['joins'] as $joinArr) { if (is_array($joinArr['table'])) { - $mainTable = $joinArr['table'][0]; - $aliasTable = $joinArr['table'][1]; - $table = $this->wrapSanitizer($mainTable) . ' AS ' . $this->wrapSanitizer($aliasTable); + $mainTable = $this->stringifyValue($this->wrapSanitizer($joinArr['table'][0])); + $aliasTable = $this->stringifyValue($this->wrapSanitizer($joinArr['table'][1])); + $table = $mainTable . ' AS ' . $aliasTable; } else { $table = $joinArr['table'] instanceof Raw ? (string) $joinArr['table'] : @@ -547,6 +590,7 @@ protected function buildJoin(array $statements): string } $joinBuilder = $joinArr['joinBuilder']; + /** @var string[] */ $sqlArr = array( $sql, strtoupper($joinArr['type']), From 071fd3bc49b230d150fd53382434a86d0da4e69f Mon Sep 17 00:00:00 2001 From: Glynn Quelch Date: Sun, 16 Jan 2022 21:16:58 +0000 Subject: [PATCH 12/69] QueryBuilder, Join Builder, QueryObject, NestedQuery, WPDBAdapter all now passinging to phpstan lv8 --- src/QueryBuilder/JoinBuilder.php | 31 +++++++------- src/QueryBuilder/NestedCriteria.php | 6 +-- src/QueryBuilder/QueryBuilderHandler.php | 52 ++++++++++++++++-------- src/QueryBuilder/QueryObject.php | 21 ++++++---- src/QueryBuilder/Transaction.php | 4 +- tests/TestIntegrationWithWPDB.php | 3 +- 6 files changed, 69 insertions(+), 48 deletions(-) diff --git a/src/QueryBuilder/JoinBuilder.php b/src/QueryBuilder/JoinBuilder.php index fe54276..8cf677f 100644 --- a/src/QueryBuilder/JoinBuilder.php +++ b/src/QueryBuilder/JoinBuilder.php @@ -3,38 +3,37 @@ class JoinBuilder extends QueryBuilderHandler { /** - * @param $key - * @param $operator - * @param $value + * @param string|Raw $key + * @param string|null $operator + * @param mixed $value * - * @return $this + * @return static */ - public function on($key, $operator, $value) + public function on($key, ?string $operator, $value): self { return $this->joinHandler($key, $operator, $value, 'AND'); } /** - * @param $key - * @param $operator - * @param $value + * @param string|Raw $key + * @param string|null $operator + * @param mixed $value * - * @return $this + * @return static */ - public function orOn($key, $operator, $value) + public function orOn($key, ?string $operator, $value): self { return $this->joinHandler($key, $operator, $value, 'OR'); } /** - * @param $key - * @param null $operator - * @param null $value - * @param string $joiner + * @param string|Raw $key + * @param string|null $operator + * @param mixed $value * - * @return $this + * @return static */ - protected function joinHandler($key, $operator = null, $value = null, $joiner = 'AND') + protected function joinHandler($key, ?string $operator = null, $value = null, string $joiner = 'AND'): self { $key = $this->addTablePrefix($key); $value = $this->addTablePrefix($value); diff --git a/src/QueryBuilder/NestedCriteria.php b/src/QueryBuilder/NestedCriteria.php index 0f082f9..2d4f262 100644 --- a/src/QueryBuilder/NestedCriteria.php +++ b/src/QueryBuilder/NestedCriteria.php @@ -3,9 +3,9 @@ class NestedCriteria extends QueryBuilderHandler { /** - * @param $key - * @param null $operator - * @param null $value + * @param string|Raw $key + * @param string|null|mixed $operator Can be used as value, if 3rd arg not passed + * @param mixed|null $value * @param string $joiner * * @return $this diff --git a/src/QueryBuilder/QueryBuilderHandler.php b/src/QueryBuilder/QueryBuilderHandler.php index f422fa4..84254a7 100644 --- a/src/QueryBuilder/QueryBuilderHandler.php +++ b/src/QueryBuilder/QueryBuilderHandler.php @@ -27,7 +27,7 @@ class QueryBuilderHandler protected $connection; /** - * @var array + * @var array */ protected $statements = array(); @@ -69,6 +69,7 @@ class QueryBuilderHandler /** * @param null|\Pixie\Connection $connection * @param string $fetchMode + * @param mixed[] $hydratorConstructorArgs * @throws Exception If no connection passed and not previously established. */ final public function __construct( @@ -177,13 +178,17 @@ public function statement(string $sql, $bindings = array()): array $start = microtime(true); $sqlStatement = empty($bindings) ? $sql : $this->dbInstance->prepare($sql, $bindings); + if (!is_string($sqlStatement)) { + throw new Exception("Could not interpolate query", 1); + } + return array($sqlStatement, microtime(true) - $start); } /** * Get all rows * - * @return object|object[] + * @return array|null * @throws Exception */ public function get() @@ -194,24 +199,33 @@ public function get() }; $executionTime = 0; if (is_null($this->sqlStatement)) { - $queryObject = $this->getQuery('select'); - list($this->sqlStatement, $executionTime) = $this->statement( + $queryObject = $this->getQuery('select'); + $statement = $this->statement( $queryObject->getSql(), $queryObject->getBindings() ); + + $this->sqlStatement = $statement[0]; + $executionTime = $statement[1]; } $start = microtime(true); $result = $this->dbInstance()->get_results( - $this->sqlStatement, + is_array($this->sqlStatement) ? (end($this->sqlStatement) ?: '') : $this->sqlStatement, + // If we are using the hydrator, return as OBJECT and let the hydrator map the correct model. $this->useHydrator() ? OBJECT : $this->getFetchMode() ); $executionTime += microtime(true) - $start; $this->sqlStatement = null; + // Ensure we have an array of results. + if (!is_array($result) && null !== $result) { + $result = [$result]; + } + // Maybe hydrate the results. - if (is_array($result) && $this->useHydrator()) { + if (null !== $result && $this->useHydrator()) { $result = $this->getHydrator()->fromMany($result); } @@ -261,7 +275,7 @@ public function first() * @param string $fieldName * @param mixed $value * - * @return null|\stdClass\array|object Can return any object using hydrator + * @return null|array Can return any object using hydrator */ public function findAll($fieldName, $value) { @@ -377,12 +391,12 @@ public function max(string $field): float /** * @param string $type - * @param array $dataToBePassed + * @param bool|array $dataToBePassed * * @return mixed * @throws Exception */ - public function getQuery($type = 'select', $dataToBePassed = array()) + public function getQuery(string $type = 'select', $dataToBePassed = array()) { $allowedTypes = array('select', 'insert', 'insertignore', 'replace', 'delete', 'update', 'criteriaonly'); if (!in_array(strtolower($type), $allowedTypes)) { @@ -535,9 +549,9 @@ public function onDuplicateKeyUpdate($data) } /** - * + * @return int Number of rows effected. */ - public function delete() + public function delete(): int { $eventResult = $this->fireEvents('before-delete'); if (!is_null($eventResult)) { @@ -547,10 +561,10 @@ public function delete() $queryObject = $this->getQuery('delete'); list($preparedQuery, $executionTime) = $this->statement($queryObject->getSql(), $queryObject->getBindings()); - $response = $this->dbInstance()->get_results($preparedQuery); + $this->dbInstance()->get_results($preparedQuery); $this->fireEvents('after-delete', $queryObject, $executionTime); - return $response; + return $this->dbInstance()->rows_affected; } /** @@ -883,6 +897,9 @@ protected function whereNullHandler($key, string $prefix = '', $operator = ''): $prefix = \strlen($prefix) === 0 ? '' : " {$prefix}"; $key = $this->adapterInstance->wrapSanitizer($this->addTablePrefix($key)); + if ($key instanceof \Closure) { + throw new Exception("Key used for whereNull condition must be a string or raw exrpession.", 1); + } return $this->{$operator . 'Where'}($this->raw("{$key} IS{$prefix} NULL")); } @@ -962,7 +979,7 @@ protected function handleTransactionCall(\Closure $callback, Transaction $transa try { ob_start(); $callback($transaction); - $output = ob_get_clean(); + $output = ob_get_clean() ?: ''; } catch (\Throwable $th) { ob_end_clean(); throw $th; @@ -1134,7 +1151,7 @@ public function addTablePrefix($values, bool $tableFieldMix = true) $target = &$key; } - if (!$tableFieldMix || strpos($target, '.') !== false) { + if (!$tableFieldMix || (is_string($target) && strpos($target, '.') !== false)) { $target = $this->tablePrefix . $target; } @@ -1146,8 +1163,9 @@ public function addTablePrefix($values, bool $tableFieldMix = true) } /** - * @param string|Raw|\Closure $key - * @param mixed|mixed[] $value + * @param string $key + * @param mixed|mixed[]|bool $value + * @return void */ protected function addStatement($key, $value) { diff --git a/src/QueryBuilder/QueryObject.php b/src/QueryBuilder/QueryObject.php index 1ef59dd..c1dc7a3 100644 --- a/src/QueryBuilder/QueryObject.php +++ b/src/QueryBuilder/QueryObject.php @@ -20,7 +20,12 @@ class QueryObject */ protected $dbInstance; - public function __construct($sql, array $bindings, $dbInstance) + /** + * @param string $sql + * @param mixed[] $bindings + * @param \wpdb $dbInstance + */ + public function __construct(string $sql, array $bindings, \wpdb $dbInstance) { $this->sql = (string)$sql; $this->bindings = $bindings; @@ -54,20 +59,18 @@ public function getRawSql() } /** - * 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 + * Uses WPDB::prepare() to interpolate the query passed. + * * @param string $query The sql query with parameter placeholders - * @param array $params The array of substitution parameters + * @param mixed[] $params The array of substitution parameters * * @return string The interpolated query */ - protected function interpolateQuery($query, $params) + protected function interpolateQuery($query, $params): string { // Only call this when we have valid params (avoids wpdb::prepare() incorrectly called error) - return empty($params) ? $query : $this->dbInstance->prepare($query, $params); + $value = empty($params) ? $query : $this->dbInstance->prepare($query, $params); + return is_string($value) ? $value : ''; } } diff --git a/src/QueryBuilder/Transaction.php b/src/QueryBuilder/Transaction.php index 14280d4..f770d23 100644 --- a/src/QueryBuilder/Transaction.php +++ b/src/QueryBuilder/Transaction.php @@ -10,7 +10,7 @@ class Transaction extends QueryBuilderHandler * * @throws TransactionHaltException */ - public function commit() + public function commit(): void { $this->dbInstance->query('COMMIT'); throw new TransactionHaltException(); @@ -21,7 +21,7 @@ public function commit() * * @throws TransactionHaltException */ - public function rollback() + public function rollback(): void { $this->dbInstance->query('ROLLBACK'); throw new TransactionHaltException(); diff --git a/tests/TestIntegrationWithWPDB.php b/tests/TestIntegrationWithWPDB.php index 571c8de..c7ea356 100644 --- a/tests/TestIntegrationWithWPDB.php +++ b/tests/TestIntegrationWithWPDB.php @@ -399,7 +399,8 @@ public function testDeleteWhere(): void $builder = $this->queryBuilderProvider(); // Remove all with a NUMBER of 2 or more. - $builder->table('mock_foo')->where('number', '>=', 2)->delete(); + $r = $builder->table('mock_foo')->where('number', '>=', 2)->delete(); + dump($r,$this->wpdb); // Check we only have the first value. $rows = $builder->table('mock_foo')->get(); From 6a627930a0368cb2102fe8270510918ab733b72b Mon Sep 17 00:00:00 2001 From: Glynn Quelch Date: Sun, 16 Jan 2022 21:17:23 +0000 Subject: [PATCH 13/69] House Keeping --- tests/TestIntegrationWithWPDB.php | 1 - 1 file changed, 1 deletion(-) diff --git a/tests/TestIntegrationWithWPDB.php b/tests/TestIntegrationWithWPDB.php index c7ea356..ad7265c 100644 --- a/tests/TestIntegrationWithWPDB.php +++ b/tests/TestIntegrationWithWPDB.php @@ -400,7 +400,6 @@ public function testDeleteWhere(): void // Remove all with a NUMBER of 2 or more. $r = $builder->table('mock_foo')->where('number', '>=', 2)->delete(); - dump($r,$this->wpdb); // Check we only have the first value. $rows = $builder->table('mock_foo')->get(); From 0acd0c49cb25faa9581a6f5d40d139125e96b31b Mon Sep 17 00:00:00 2001 From: Glynn Quelch Date: Sun, 16 Jan 2022 22:09:23 +0000 Subject: [PATCH 14/69] Hydratror and eventhandler all passing phpstan l8. The generics around models isnt ideal, this need to be revisted as im not 100% sure how its suppoed to actually work. Having issues around definig a global type for MODELS that can be defined as class name stirng and as an instance. It errors when using a return of T as cantuse @template T unless its passed as a param. This seems by design! --- src/EventHandler.php | 39 +++++++++++++----------- src/Hydration/Hydrator.php | 30 ++++++++++-------- src/QueryBuilder/QueryBuilderHandler.php | 6 ++-- 3 files changed, 42 insertions(+), 33 deletions(-) diff --git a/src/EventHandler.php b/src/EventHandler.php index 5d202ad..319bd7d 100644 --- a/src/EventHandler.php +++ b/src/EventHandler.php @@ -1,22 +1,24 @@ -> */ protected $events = array(); /** - * @var array + * @var string[] */ protected $firedEvents = array(); /** - * @return array + * @return array> */ public function getEvents() { @@ -24,12 +26,12 @@ public function getEvents() } /** - * @param $event - * @param $table + * @param string $event + * @param string|Raw $table * - * @return callable|null + * @return \Closure|null */ - public function getEvent($event, $table = ':any') + public function getEvent(string $event, $table = ':any'): ?\Closure { if ($table instanceof Raw) { return null; @@ -38,22 +40,22 @@ public function getEvent($event, $table = ':any') } /** - * @param $event - * @param string $table - * @param callable $action + * @param string $event + * @param string|null $table + * @param \Closure $action * * @return void */ - public function registerEvent($event, $table, \Closure $action) + public function registerEvent(string $event, ?string $table, \Closure $action) { - $table = $table ?: ':any'; + $table = $table ?? ':any'; $this->events[$table][$event] = $action; } /** - * @param $event - * @param string $table + * @param string $event + * @param string $table * * @return void */ @@ -64,11 +66,12 @@ public function removeEvent($event, $table = ':any') /** * @param QueryBuilderHandler $queryBuilder - * @param $event + * @param string $event * @return mixed */ - public function fireEvents($queryBuilder, $event) + public function fireEvents(QueryBuilderHandler $queryBuilder, string $event) { + dump($this); $statements = $queryBuilder->getStatements(); $tables = isset($statements['tables']) ? $statements['tables'] : array(); diff --git a/src/Hydration/Hydrator.php b/src/Hydration/Hydrator.php index 6529ff8..6169636 100644 --- a/src/Hydration/Hydrator.php +++ b/src/Hydration/Hydrator.php @@ -8,12 +8,15 @@ namespace Pixie\Hydration; +/** + * @template T + */ class Hydrator { /** * The model to hydrate - * + * @var class-string */ protected $model; @@ -21,10 +24,15 @@ class Hydrator /** * The arguments used to create the new instance. * - * @var array + * @var array */ protected $constructorArgs; + /** + + * @param class-string $model + * @param array $constructorArgs + */ public function __construct(string $model = \stdClass::class, array $constructorArgs = []) { $this->model = $model; @@ -33,9 +41,8 @@ public function __construct(string $model = \stdClass::class, array $constructor /** * Map many models - * * @param array $sources - * @return T[] + * @return array */ public function fromMany(array $sources): array { @@ -44,7 +51,6 @@ public function fromMany(array $sources): array /** * Map a single model - * * @param object|mixed[] $source * @return T */ @@ -64,8 +70,7 @@ public function from($source) /** * Maps the model from an array of data. - * - * @param array $source + * @param array $source * @return T */ protected function fromArray(array $source) @@ -79,7 +84,6 @@ protected function fromArray(array $source) /** * Maps a model from an Object of data - * * @param object $source * @return T */ @@ -91,13 +95,14 @@ protected function fromObject($source) /** * Construct an instance of the model - * - * @return void + + * @return T */ protected function newInstance() { $class = $this->model; try { + /** @var T */ $instance = empty($this->constructorArgs) ? new $class() : new $class(...$this->constructorArgs); @@ -110,7 +115,6 @@ protected function newInstance() /** * Sets a property to the current model - * * @param T $model * @param string $property * @param mixed $value @@ -152,7 +156,9 @@ protected function setProperty($model, string $property, $value) */ protected function normaliseProperty(string $property): string { - return \trim(preg_replace('/[^a-z0-9]+/', '_', strtolower($property))); + return \trim( + preg_replace('/[^a-z0-9]+/', '_', strtolower($property)) ?: '' + ); } /** diff --git a/src/QueryBuilder/QueryBuilderHandler.php b/src/QueryBuilder/QueryBuilderHandler.php index 84254a7..5510cfc 100644 --- a/src/QueryBuilder/QueryBuilderHandler.php +++ b/src/QueryBuilder/QueryBuilderHandler.php @@ -10,7 +10,6 @@ use Pixie\QueryBuilder\JoinBuilder; use Pixie\QueryBuilder\QueryObject; use Pixie\QueryBuilder\Transaction; -// use Pixie\QueryBuilder\Adapters\wpdb use Pixie\QueryBuilder\WPDBAdapter; class QueryBuilderHandler @@ -238,9 +237,10 @@ public function get() * * @return Hydrator */ - protected function getHydrator(): Hydrator + protected function getHydrator(): Hydrator /** @phpstan-ignore-line */ { - return new Hydrator($this->getFetchMode(), $this->hydratorConstructorArgs ?? []); + $hydrator = new Hydrator($this->getFetchMode(), $this->hydratorConstructorArgs ?? []); /** @phpstan-ignore-line */ + return $hydrator; } /** From 9361ce56603b8fc95b2ea9f6f0125a354b1f729c Mon Sep 17 00:00:00 2001 From: Glynn Quelch Date: Sun, 16 Jan 2022 22:10:55 +0000 Subject: [PATCH 15/69] House Keeping --- src/EventHandler.php | 1 - tests/TestEvents.php | 2 +- 2 files changed, 1 insertion(+), 2 deletions(-) diff --git a/src/EventHandler.php b/src/EventHandler.php index 319bd7d..3af0c17 100644 --- a/src/EventHandler.php +++ b/src/EventHandler.php @@ -71,7 +71,6 @@ public function removeEvent($event, $table = ':any') */ public function fireEvents(QueryBuilderHandler $queryBuilder, string $event) { - dump($this); $statements = $queryBuilder->getStatements(); $tables = isset($statements['tables']) ? $statements['tables'] : array(); diff --git a/tests/TestEvents.php b/tests/TestEvents.php index 1b311e5..4538a00 100644 --- a/tests/TestEvents.php +++ b/tests/TestEvents.php @@ -36,7 +36,7 @@ public function testCanRegisterAndGetEvents(): void { $handler = new EventHandler(); $handler->registerEvent('for_bar_table', 'bar', $this->createClosure('bar_table')); - $handler->registerEvent('for_any_table', false, $this->createClosure('any_table')); + $handler->registerEvent('for_any_table', null, $this->createClosure('any_table')); // Get all events and check the keys are used correctly. $events = $handler->getEvents(); From 9d6885091bb2a757c76dd6c808bc873dc08797e4 Mon Sep 17 00:00:00 2001 From: Glynn Quelch Date: Sun, 16 Jan 2022 22:41:15 +0000 Subject: [PATCH 16/69] AliasFacade now passing phpstan l8 --- src/AliasFacade.php | 13 ++++++++----- 1 file changed, 8 insertions(+), 5 deletions(-) diff --git a/src/AliasFacade.php b/src/AliasFacade.php index 7003d8e..334fe24 100644 --- a/src/AliasFacade.php +++ b/src/AliasFacade.php @@ -13,13 +13,13 @@ class AliasFacade { /** - * @var QueryBuilderHandler + * @var QueryBuilderHandler|null */ protected static $queryBuilderInstance; /** - * @param $method - * @param $args + * @param string $method + * @param mixed[] $args * * @return mixed */ @@ -30,13 +30,16 @@ public static function __callStatic($method, $args) } // Call the non-static method from the class instance - return call_user_func_array(array(static::$queryBuilderInstance, $method), $args); + $callable = array(static::$queryBuilderInstance, $method); + return is_callable($callable) + ? call_user_func_array($callable, $args) + : null; } /** * @param QueryBuilderHandler $queryBuilderInstance */ - public static function setQueryBuilderInstance($queryBuilderInstance) + public static function setQueryBuilderInstance($queryBuilderInstance): void { static::$queryBuilderInstance = $queryBuilderInstance; } From 6f685167c9cdc9fc4773c82951a101bd08f7436a Mon Sep 17 00:00:00 2001 From: Glynn Quelch Date: Sun, 16 Jan 2022 22:45:13 +0000 Subject: [PATCH 17/69] House Keeping --- phpstan.neon.dist | 13 ++++--------- 1 file changed, 4 insertions(+), 9 deletions(-) diff --git a/phpstan.neon.dist b/phpstan.neon.dist index 023ba3b..4c4f7dd 100644 --- a/phpstan.neon.dist +++ b/phpstan.neon.dist @@ -13,12 +13,7 @@ parameters: bootstrapFiles: - vendor/php-stubs/wordpress-stubs/wordpress-stubs.php parallel: - processTimeout: 300.0 - jobSize: 10 - maximumNumberOfProcesses: 4 - minimumNumberOfJobsPerProcess: 4 - includes: - - vendor/phpstan/phpstan-strict-rules/rules.neon - - vendor/phpstan/phpstan-phpunit/extension.neon - - vendor/phpstan/phpstan-phpunit/rules.neon - - vendor/phpstan/phpstan-deprecation-rules/rules.neon + processTimeout: 300.0 + jobSize: 10 + maximumNumberOfProcesses: 4 + minimumNumberOfJobsPerProcess: 4 \ No newline at end of file From be0f0482f0165e9f62bd92b7ca80a3eae45de7d0 Mon Sep 17 00:00:00 2001 From: Glynn Quelch Date: Sun, 16 Jan 2022 22:47:45 +0000 Subject: [PATCH 18/69] House Keeping --- .php-cs-fixer.php | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/.php-cs-fixer.php b/.php-cs-fixer.php index d6ecd0f..352df4d 100644 --- a/.php-cs-fixer.php +++ b/.php-cs-fixer.php @@ -8,8 +8,7 @@ $finder = (new Finder()) ->files() ->name('*.php') - ->in(__DIR__ . '/src') - ->in(__DIR__ . '/tests'); + ->in(__DIR__ . '/src'); /** * Cache file for PHP-CS From b350be58db088e3865698da995a2894c0e7d1ca7 Mon Sep 17 00:00:00 2001 From: Glynn Quelch Date: Sun, 16 Jan 2022 22:57:24 +0000 Subject: [PATCH 19/69] All formatted into PSR12 standards and PhpStan and CS now hooked in on CLI call again --- composer.json | 6 +- src/AliasFacade.php | 9 +- src/Connection.php | 34 ++- src/EventHandler.php | 29 +- src/Exception.php | 7 +- src/Hydration/Hydrator.php | 51 +++- src/QueryBuilder/JoinBuilder.php | 9 +- src/QueryBuilder/NestedCriteria.php | 9 +- src/QueryBuilder/QueryBuilderHandler.php | 289 +++++++++++------- src/QueryBuilder/QueryObject.php | 16 +- src/QueryBuilder/Raw.php | 5 +- src/QueryBuilder/Transaction.php | 1 - src/QueryBuilder/TransactionHaltException.php | 4 +- src/QueryBuilder/WPDBAdapter.php | 129 ++++---- 14 files changed, 358 insertions(+), 240 deletions(-) diff --git a/composer.json b/composer.json index b3665af..2e8072a 100644 --- a/composer.json +++ b/composer.json @@ -58,8 +58,8 @@ "test": "phpunit --coverage-clover clover.xml --testdox", "coverage": "phpunit --coverage-html coverage-report --testdox", "analyse": "vendor/bin/phpstan analyse src -l8", - "all": "composer test", - "fixer": "php-cs-fixer fix --diff --show-progress=dots", - "lint": "php-cs-fixer fix --diff --dry-run" + "all": "composer test && composer analyse && php-cs-fixer fix --diff --dry-run --rules=@PSR12 --show-progress=dots --stop-on-violation", + "fixer": "php-cs-fixer fix --diff --rules=@PSR12 --show-progress=dots", + "lint": "php-cs-fixer fix --diff --dry-run --rules=@PSR12" } } \ No newline at end of file diff --git a/src/AliasFacade.php b/src/AliasFacade.php index 334fe24..90fb48c 100644 --- a/src/AliasFacade.php +++ b/src/AliasFacade.php @@ -1,4 +1,6 @@ - $adapterConfig - * @param null|string $alias - * @param null|Container $container + * @param string|null $alias + * @param Container|null $container */ public function __construct( - \wpdb $wpdb, + wpdb $wpdb, array $adapterConfig = [], ?string $alias = null, ?Container $container = null @@ -56,7 +56,7 @@ public function __construct( $this->dbInstance = $wpdb; $this->setAdapterConfig($adapterConfig); - $this->container = $container ?? new Container(); + $this->container = $container ?? new Container(); $this->eventHandler = $this->container->build(EventHandler::class); if ($alias) { @@ -64,7 +64,7 @@ public function __construct( } // Preserve the first database connection with a static property - if (! static::$storedConnection) { + if (!static::$storedConnection) { static::$storedConnection = $this; } } @@ -77,7 +77,7 @@ public function __construct( public function createAlias(string $alias): void { class_alias(AliasFacade::class, $alias); - $builder = $this->container->build(QueryBuilderHandler::class, array( $this )); + $builder = $this->container->build(QueryBuilderHandler::class, [$this]); AliasFacade::setQueryBuilderInstance($builder); } @@ -86,22 +86,23 @@ class_alias(AliasFacade::class, $alias); */ public function getQueryBuilder(): QueryBuilderHandler { - return $this->container->build(QueryBuilderHandler::class, array( $this )); + return $this->container->build(QueryBuilderHandler::class, [$this]); } /** - * @param \wpdb $wpdb + * @param wpdb $wpdb * * @return $this */ - public function setDbInstance(\wpdb $wpdb) + public function setDbInstance(wpdb $wpdb) { $this->dbInstance = $wpdb; + return $this; } /** - * @return \wpdb + * @return wpdb */ public function getDbInstance() { @@ -116,6 +117,7 @@ public function getDbInstance() public function setAdapterConfig(array $adapterConfig) { $this->adapterConfig = $adapterConfig; + return $this; } @@ -147,13 +149,15 @@ public function getEventHandler() * Returns the initial instance created. * * @return Connection + * * @throws Exception If connection not already established */ public static function getStoredConnection() { if (null === static::$storedConnection) { - throw new Exception("No initial instance of Connection created"); + throw new Exception('No initial instance of Connection created'); } + return static::$storedConnection; } } diff --git a/src/EventHandler.php b/src/EventHandler.php index 3af0c17..8d9ed7c 100644 --- a/src/EventHandler.php +++ b/src/EventHandler.php @@ -2,23 +2,24 @@ namespace Pixie; -use Pixie\QueryBuilder\Raw; +use Closure; use Pixie\QueryBuilder\QueryBuilderHandler; +use Pixie\QueryBuilder\Raw; class EventHandler { /** - * @var array> + * @var array> */ - protected $events = array(); + protected $events = []; /** * @var string[] */ - protected $firedEvents = array(); + protected $firedEvents = []; /** - * @return array> + * @return array> */ public function getEvents() { @@ -29,24 +30,25 @@ public function getEvents() * @param string $event * @param string|Raw $table * - * @return \Closure|null + * @return Closure|null */ - public function getEvent(string $event, $table = ':any'): ?\Closure + public function getEvent(string $event, $table = ':any'): ?Closure { if ($table instanceof Raw) { return null; } - return isset($this->events[$table][$event]) ? $this->events[$table][$event] : null; + + return $this->events[$table][$event] ?? null; } /** * @param string $event * @param string|null $table - * @param \Closure $action + * @param Closure $action * * @return void */ - public function registerEvent(string $event, ?string $table, \Closure $action) + public function registerEvent(string $event, ?string $table, Closure $action) { $table = $table ?? ':any'; @@ -67,12 +69,13 @@ public function removeEvent($event, $table = ':any') /** * @param QueryBuilderHandler $queryBuilder * @param string $event + * * @return mixed */ public function fireEvents(QueryBuilderHandler $queryBuilder, string $event) { $statements = $queryBuilder->getStatements(); - $tables = isset($statements['tables']) ? $statements['tables'] : array(); + $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. @@ -90,10 +93,10 @@ public function fireEvents(QueryBuilderHandler $queryBuilder, string $event) unset($handlerParams[1]); // we do not need $event // Add to fired list $this->firedEvents[] = $eventId; - $result = call_user_func_array($action, $handlerParams); + $result = call_user_func_array($action, $handlerParams); if (!is_null($result)) { return $result; - }; + } } } } diff --git a/src/Exception.php b/src/Exception.php index 2d25910..2755728 100644 --- a/src/Exception.php +++ b/src/Exception.php @@ -1,6 +1,7 @@ - */ protected $model; @@ -33,15 +41,17 @@ class Hydrator * @param class-string $model * @param array $constructorArgs */ - public function __construct(string $model = \stdClass::class, array $constructorArgs = []) + public function __construct(string $model = stdClass::class, array $constructorArgs = []) { - $this->model = $model; + $this->model = $model; $this->constructorArgs = $constructorArgs; } /** * Map many models + * * @param array $sources + * * @return array */ public function fromMany(array $sources): array @@ -51,7 +61,9 @@ public function fromMany(array $sources): array /** * Map a single model + * * @param object|mixed[] $source + * * @return T */ public function from($source) @@ -60,17 +72,19 @@ public function from($source) case is_array($source): return $this->fromArray($source); - case \is_object($source): + case is_object($source): return $this->fromObject($source); default: - throw new \Exception("Models can only be mapped from arrays or objects.", 1); + throw new Exception('Models can only be mapped from arrays or objects.', 1); } } /** * Maps the model from an array of data. + * * @param array $source + * * @return T */ protected function fromArray(array $source) @@ -79,23 +93,28 @@ protected function fromArray(array $source) foreach ($source as $key => $value) { $this->setProperty($model, $key, $value); } + return $model; } /** * Maps a model from an Object of data + * * @param object $source + * * @return T */ protected function fromObject($source) { $vars = get_object_vars($source); + return $this->fromArray($vars); } /** * Construct an instance of the model - + + * * @return T */ protected function newInstance() @@ -106,8 +125,8 @@ protected function newInstance() $instance = empty($this->constructorArgs) ? new $class() : new $class(...$this->constructorArgs); - } catch (\Throwable $th) { - throw new \Exception("Failed to construct model, {$th->getMessage()}", 1); + } catch (Throwable $th) { + throw new Exception("Failed to construct model, {$th->getMessage()}", 1); } return $instance; @@ -115,9 +134,11 @@ protected function newInstance() /** * Sets a property to the current model + * * @param T $model * @param string $property * @param mixed $value + * * @return T */ protected function setProperty($model, string $property, $value) @@ -127,12 +148,12 @@ protected function setProperty($model, string $property, $value) // Attempt to set. try { switch (true) { - case \method_exists($model, $this->generateSetterMethod($property)): + case method_exists($model, $this->generateSetterMethod($property)): $method = $this->generateSetterMethod($property); $model->$method($value); break; - case \method_exists($model, $this->generateSetterMethod($property, true)): + case method_exists($model, $this->generateSetterMethod($property, true)): $method = $this->generateSetterMethod($property, true); $model->$method($value); break; @@ -141,8 +162,8 @@ protected function setProperty($model, string $property, $value) $model->$property = $value; break; } - } catch (\Throwable $th) { - throw new \Exception(sprintf("Failed to set %s of %s model, %s", $property, get_class($model), $th->getMessage()), 1); + } catch (Throwable $th) { + throw new Exception(sprintf('Failed to set %s of %s model, %s', $property, get_class($model), $th->getMessage()), 1); } return $model; @@ -152,11 +173,12 @@ protected function setProperty($model, string $property, $value) * Normalises a property * * @param string $property + * * @return string */ protected function normaliseProperty(string $property): string { - return \trim( + return trim( preg_replace('/[^a-z0-9]+/', '_', strtolower($property)) ?: '' ); } @@ -166,12 +188,13 @@ protected function normaliseProperty(string $property): string * * @param string $property * @param bool $underscore + * * @return string */ protected function generateSetterMethod(string $property, bool $underscore = false): string { return $underscore ? "set_{$property}" - : "set" . \ucfirst($property); + : 'set' . ucfirst($property); } } diff --git a/src/QueryBuilder/JoinBuilder.php b/src/QueryBuilder/JoinBuilder.php index 8cf677f..3e18421 100644 --- a/src/QueryBuilder/JoinBuilder.php +++ b/src/QueryBuilder/JoinBuilder.php @@ -1,4 +1,6 @@ -addTablePrefix($key); - $value = $this->addTablePrefix($value); + $key = $this->addTablePrefix($key); + $value = $this->addTablePrefix($value); $this->statements['criteria'][] = compact('key', 'operator', 'value', 'joiner'); + return $this; } } diff --git a/src/QueryBuilder/NestedCriteria.php b/src/QueryBuilder/NestedCriteria.php index 2d4f262..9337a5f 100644 --- a/src/QueryBuilder/NestedCriteria.php +++ b/src/QueryBuilder/NestedCriteria.php @@ -1,10 +1,12 @@ -addTablePrefix($key); + $key = $this->addTablePrefix($key); $this->statements['criteria'][] = compact('key', 'operator', 'value', 'joiner'); + return $this; } } diff --git a/src/QueryBuilder/QueryBuilderHandler.php b/src/QueryBuilder/QueryBuilderHandler.php index 5510cfc..bba90c8 100644 --- a/src/QueryBuilder/QueryBuilderHandler.php +++ b/src/QueryBuilder/QueryBuilderHandler.php @@ -2,19 +2,23 @@ namespace Pixie\QueryBuilder; +use Closure; use PDO; -use Pixie\Exception; use Pixie\Connection; -use Pixie\QueryBuilder\Raw; +use Pixie\Exception; use Pixie\Hydration\Hydrator; use Pixie\QueryBuilder\JoinBuilder; use Pixie\QueryBuilder\QueryObject; +use Pixie\QueryBuilder\Raw; use Pixie\QueryBuilder\Transaction; use Pixie\QueryBuilder\WPDBAdapter; +use Throwable; +use wpdb; +use function mb_strlen; +use function strlen; class QueryBuilderHandler { - /** * @var \Viocon\Container */ @@ -28,20 +32,20 @@ class QueryBuilderHandler /** * @var array */ - protected $statements = array(); + protected $statements = []; /** - * @var \wpdb + * @var wpdb */ protected $dbInstance; /** - * @var null|string|string[] + * @var string|string[]|null */ protected $sqlStatement = null; /** - * @var null|string + * @var string|null */ protected $tablePrefix = null; @@ -66,10 +70,11 @@ class QueryBuilderHandler protected $hydratorConstructorArgs; /** - * @param null|\Pixie\Connection $connection + * @param \Pixie\Connection|null $connection * @param string $fetchMode * @param mixed[] $hydratorConstructorArgs - * @throws Exception If no connection passed and not previously established. + * + * @throws Exception if no connection passed and not previously established */ final public function __construct( Connection $connection = null, @@ -83,7 +88,7 @@ final public function __construct( // Set all dependencies from connection. $this->connection = $connection; - $this->container = $this->connection->getContainer(); + $this->container = $this->connection->getContainer(); $this->dbInstance = $this->connection->getDbInstance(); $this->setAdapterConfig($this->connection->getAdapterConfig()); @@ -91,11 +96,10 @@ final public function __construct( $this->setFetchMode($fetchMode); $this->hydratorConstructorArgs = $hydratorConstructorArgs; - // Query builder adapter instance $this->adapterInstance = $this->container->build( WPDBAdapter::class, - array($this->connection) + [$this->connection] ); } @@ -103,6 +107,7 @@ final public function __construct( * Sets the config for WPDB * * @param array $adapterConfig + * * @return void */ protected function setAdapterConfig(array $adapterConfig): void @@ -116,19 +121,23 @@ protected function setAdapterConfig(array $adapterConfig): void * Set the fetch mode * * @param string $mode - * @param null|array $constructorArgs + * @param array|null $constructorArgs + * * @return static */ public function setFetchMode(string $mode, ?array $constructorArgs = null): self { - $this->fetchMode = $mode; + $this->fetchMode = $mode; $this->hydratorConstructorArgs = $constructorArgs; + return $this; } /** - * @param null|Connection $connection + * @param Connection|null $connection + * * @return static + * * @throws Exception */ public function newQuery(Connection $connection = null): self @@ -139,14 +148,15 @@ public function newQuery(Connection $connection = null): self $newQuery = $this->constructCurrentBuilderClass($connection); $newQuery->setFetchMode($this->getFetchMode(), $this->hydratorConstructorArgs); + return $newQuery; } - /** * Returns a new instance of the current, with the passed connection. * * @param \Pixie\Connection $connection + * * @return static */ protected function constructCurrentBuilderClass(Connection $connection): self @@ -160,9 +170,10 @@ protected function constructCurrentBuilderClass(Connection $connection): self * * @return static */ - public function query($sql, $bindings = array()): self + public function query($sql, $bindings = []): self { list($this->sqlStatement) = $this->statement($sql, $bindings); + return $this; } @@ -172,22 +183,23 @@ public function query($sql, $bindings = array()): self * * @return array{0:string, 1:float} */ - public function statement(string $sql, $bindings = array()): array + public function statement(string $sql, $bindings = []): array { - $start = microtime(true); + $start = microtime(true); $sqlStatement = empty($bindings) ? $sql : $this->dbInstance->prepare($sql, $bindings); if (!is_string($sqlStatement)) { - throw new Exception("Could not interpolate query", 1); + throw new Exception('Could not interpolate query', 1); } - return array($sqlStatement, microtime(true) - $start); + return [$sqlStatement, microtime(true) - $start]; } /** * Get all rows * * @return array|null + * * @throws Exception */ public function get() @@ -195,21 +207,20 @@ public function get() $eventResult = $this->fireEvents('before-select'); if (!is_null($eventResult)) { return $eventResult; - }; + } $executionTime = 0; if (is_null($this->sqlStatement)) { - $queryObject = $this->getQuery('select'); - $statement = $this->statement( + $statement = $this->statement( $queryObject->getSql(), $queryObject->getBindings() ); $this->sqlStatement = $statement[0]; - $executionTime = $statement[1]; + $executionTime = $statement[1]; } - $start = microtime(true); + $start = microtime(true); $result = $this->dbInstance()->get_results( is_array($this->sqlStatement) ? (end($this->sqlStatement) ?: '') : $this->sqlStatement, // If we are using the hydrator, return as OBJECT and let the hydrator map the correct model. @@ -229,6 +240,7 @@ public function get() } $this->fireEvents('after-select', $result, $executionTime); + return $result; } @@ -237,9 +249,10 @@ public function get() * * @return Hydrator */ - protected function getHydrator(): Hydrator /** @phpstan-ignore-line */ + protected function getHydrator(): Hydrator /* @phpstan-ignore-line */ { - $hydrator = new Hydrator($this->getFetchMode(), $this->hydratorConstructorArgs ?? []); /** @phpstan-ignore-line */ + $hydrator = new Hydrator($this->getFetchMode(), $this->hydratorConstructorArgs ?? []); /* @phpstan-ignore-line */ + return $hydrator; } @@ -258,12 +271,13 @@ protected function useHydrator(): bool * * Shortcut of ->where('key','=','value')->limit(1)->get(); * - * @return null|\stdClass\array|object Can return any object using hydrator + * @return \stdClass\array|object|null Can return any object using hydrator */ public function first() { $this->limit(1); $result = $this->get(); + return empty($result) ? null : $result[0]; } @@ -275,11 +289,12 @@ public function first() * @param string $fieldName * @param mixed $value * - * @return null|array Can return any object using hydrator + * @return array|null Can return any object using hydrator */ public function findAll($fieldName, $value) { $this->where($fieldName, '=', $value); + return $this->get(); } @@ -287,11 +302,12 @@ public function findAll($fieldName, $value) * @param string $fieldName * @param mixed $value * - * @return null|\stdClass\array|object Can return any object using hydrator + * @return \stdClass\array|object|null Can return any object using hydrator */ public function find($value, $fieldName = 'id') { $this->where($fieldName, '=', $value); + return $this->first(); } @@ -302,16 +318,17 @@ public function find($value, $fieldName = 'id') * * @param string $type * @param string $field + * * @return float */ protected function aggregate(string $type, string $field = '*'): float { // Verify that field exists - if ($field !== '*' && isset($this->statements['selects']) === true && \in_array($field, $this->statements['selects'], true) === false) { + if ('*' !== $field && true === isset($this->statements['selects']) && false === \in_array($field, $this->statements['selects'], true)) { throw new \Exception(sprintf('Failed %s query - the column %s hasn\'t been selected in the query.', $type, $field)); } - if (isset($this->statements['tables']) === false) { + if (false === isset($this->statements['tables'])) { throw new Exception('No table selected'); } @@ -320,16 +337,18 @@ protected function aggregate(string $type, string $field = '*'): float ->select([$this->raw(sprintf('%s(%s) AS field', strtoupper($type), $field))]) ->first(); - return isset($count->field) === true ? (float)$count->field : 0; + return true === isset($count->field) ? (float)$count->field : 0; } /** * Get count of all the rows for the current query + * * @see Taken from the pecee-pixie library - https://github.com/skipperbent/pecee-pixie/ * * @param string $field * - * @return integer + * @return int + * * @throws Exception */ public function count(string $field = '*'): int @@ -339,10 +358,13 @@ public function count(string $field = '*'): int /** * Get the sum for a field in the current query + * * @see Taken from the pecee-pixie library - https://github.com/skipperbent/pecee-pixie/ * * @param string $field + * * @return float + * * @throws Exception */ public function sum(string $field): float @@ -352,10 +374,13 @@ public function sum(string $field): float /** * Get the average for a field in the current query + * * @see Taken from the pecee-pixie library - https://github.com/skipperbent/pecee-pixie/ * * @param string $field + * * @return float + * * @throws Exception */ public function average(string $field): float @@ -365,10 +390,13 @@ public function average(string $field): float /** * Get the minimum for a field in the current query + * * @see Taken from the pecee-pixie library - https://github.com/skipperbent/pecee-pixie/ * * @param string $field + * * @return float + * * @throws Exception */ public function min(string $field): float @@ -378,10 +406,13 @@ public function min(string $field): float /** * Get the maximum for a field in the current query + * * @see Taken from the pecee-pixie library - https://github.com/skipperbent/pecee-pixie/ * * @param string $field + * * @return float + * * @throws Exception */ public function max(string $field): float @@ -394,11 +425,12 @@ public function max(string $field): float * @param bool|array $dataToBePassed * * @return mixed + * * @throws Exception */ - public function getQuery(string $type = 'select', $dataToBePassed = array()) + public function getQuery(string $type = 'select', $dataToBePassed = []) { - $allowedTypes = array('select', 'insert', 'insertignore', 'replace', 'delete', 'update', 'criteriaonly'); + $allowedTypes = ['select', 'insert', 'insertignore', 'replace', 'delete', 'update', 'criteriaonly']; if (!in_array(strtolower($type), $allowedTypes)) { throw new Exception($type . ' is not a known type.', 2); } @@ -407,7 +439,7 @@ public function getQuery(string $type = 'select', $dataToBePassed = array()) return $this->container->build( QueryObject::class, - array($queryArr['sql'], $queryArr['bindings'], $this->dbInstance) + [$queryArr['sql'], $queryArr['bindings'], $this->dbInstance] ); } @@ -420,7 +452,7 @@ public function getQuery(string $type = 'select', $dataToBePassed = array()) public function subQuery(QueryBuilderHandler $queryBuilder, ?string $alias = null) { $sql = '(' . $queryBuilder->getQuery()->getRawSql() . ')'; - if (is_string($alias) && \mb_strlen($alias) !== 0) { + if (is_string($alias) && 0 !== mb_strlen($alias)) { $sql = $sql . ' as ' . $alias; } @@ -432,7 +464,8 @@ public function subQuery(QueryBuilderHandler $queryBuilder, ?string $alias = nul * * @param array $data * @param string $type - * @return int|int[]|null|mixed Can return a single row id, array of row ids, null (for failed) or any other value short circuited from event. + * + * @return int|int[]|mixed|null can return a single row id, array of row ids, null (for failed) or any other value short circuited from event */ private function doInsert(array $data, string $type) { @@ -449,10 +482,10 @@ private function doInsert(array $data, string $type) $this->dbInstance->get_results($preparedQuery); // Check we have a result. - $return = $this->dbInstance->rows_affected === 1 ? $this->dbInstance->insert_id : null; + $return = 1 === $this->dbInstance->rows_affected ? $this->dbInstance->insert_id : null; } else { // Its a batch insert - $return = array(); + $return = []; $executionTime = 0; foreach ($data as $subData) { $queryObject = $this->getQuery($type, $subData); @@ -461,7 +494,7 @@ private function doInsert(array $data, string $type) $this->dbInstance->get_results($preparedQuery); $executionTime += $time; - if ($this->dbInstance->rows_affected === 1) { + if (1 === $this->dbInstance->rows_affected) { $return[] = $this->dbInstance->insert_id; } } @@ -473,9 +506,9 @@ private function doInsert(array $data, string $type) } /** - * @param array $data Either key=>value array for single or array of arrays for bulk. + * @param array $data either key=>value array for single or array of arrays for bulk * - * @return int|int[]|null|mixed Can return a single row id, array of row ids, null (for failed) or any other value short circuited from event. + * @return int|int[]|mixed|null can return a single row id, array of row ids, null (for failed) or any other value short circuited from event */ public function insert($data) { @@ -484,8 +517,9 @@ public function insert($data) /** * - * @param array $data Either key=>value array for single or array of arrays for bulk. - * @return int|int[]|null|mixed Can return a single row id, array of row ids, null (for failed) or any other value short circuited from event. + * @param array $data either key=>value array for single or array of arrays for bulk + * + * @return int|int[]|mixed|null can return a single row id, array of row ids, null (for failed) or any other value short circuited from event */ public function insertIgnore($data) { @@ -494,8 +528,9 @@ public function insertIgnore($data) /** * - * @param array $data Either key=>value array for single or array of arrays for bulk. - * @return int|int[]|null|mixed Can return a single row id, array of row ids, null (for failed) or any other value short circuited from event. + * @param array $data either key=>value array for single or array of arrays for bulk + * + * @return int|int[]|mixed|null can return a single row id, array of row ids, null (for failed) or any other value short circuited from event */ public function replace($data) { @@ -514,42 +549,45 @@ public function update($data) return $eventResult; } - $queryObject = $this->getQuery('update', $data); + $queryObject = $this->getQuery('update', $data); list($preparedQuery, $executionTime) = $this->statement($queryObject->getSql(), $queryObject->getBindings()); $this->dbInstance()->get_results($preparedQuery); $this->fireEvents('after-update', $queryObject, $executionTime); - return $this->dbInstance()->rows_affected !== 0 + return 0 !== $this->dbInstance()->rows_affected ? $this->dbInstance()->rows_affected : null; } /** * @param array $data - * @return int|null Will return row id for insert and bool for success/fail on update. + * + * @return int|null will return row id for insert and bool for success/fail on update */ public function updateOrInsert($data) { if ($this->first()) { return $this->update($data); - } else { - return $this->insert($data); } + + return $this->insert($data); } /** * @param array $data + * * @return static */ public function onDuplicateKeyUpdate($data) { $this->addStatement('onduplicate', $data); + return $this; } /** - * @return int Number of rows effected. + * @return int number of rows effected */ public function delete(): int { @@ -571,15 +609,16 @@ public function delete(): int * @param string|Raw ...$tables Single table or array of tables * * @return static + * * @throws Exception */ public function table(...$tables): QueryBuilderHandler { - $instance = $this->constructCurrentBuilderClass($this->connection); $this->setFetchMode($this->getFetchMode(), $this->hydratorConstructorArgs); $tables = $this->addTablePrefix($tables, false); $instance->addStatement('tables', $tables); + return $instance; } @@ -590,9 +629,9 @@ public function table(...$tables): QueryBuilderHandler */ public function from(...$tables): self { - $tables = $this->addTablePrefix($tables, false); $this->addStatement('tables', $tables); + return $this; } @@ -609,6 +648,7 @@ public function select($fields): self $fields = $this->addTablePrefix($fields); $this->addStatement('selects', $fields); + return $this; } @@ -621,11 +661,12 @@ public function selectDistinct($fields) { $this->select($fields); $this->addStatement('distinct', true); + return $this; } /** - * @param string|string[] $field Either the single field or an array of fields. + * @param string|string[] $field either the single field or an array of fields * * @return static */ @@ -633,6 +674,7 @@ public function groupBy($field): self { $field = $this->addTablePrefix($field); $this->addStatement('groupBys', $field); + return $this; } @@ -645,15 +687,15 @@ public function groupBy($field): self public function orderBy($fields, string $defaultDirection = 'ASC'): self { if (!is_array($fields)) { - $fields = array($fields); + $fields = [$fields]; } foreach ($fields as $key => $value) { $field = $key; - $type = $value; + $type = $value; if (is_int($key)) { $field = $value; - $type = $defaultDirection; + $type = $defaultDirection; } if (!$field instanceof Raw) { $field = $this->addTablePrefix($field); @@ -672,6 +714,7 @@ public function orderBy($fields, string $defaultDirection = 'ASC'): self public function limit(int $limit): self { $this->statements['limit'] = $limit; + return $this; } @@ -683,6 +726,7 @@ public function limit(int $limit): self public function offset(int $offset): self { $this->statements['offset'] = $offset; + return $this; } @@ -696,8 +740,9 @@ public function offset(int $offset): self */ public function having($key, string $operator, $value, string $joiner = 'AND') { - $key = $this->addTablePrefix($key); + $key = $this->addTablePrefix($key); $this->statements['havings'][] = compact('key', 'operator', 'value', 'joiner'); + return $this; } @@ -715,7 +760,7 @@ public function orHaving($key, $operator, $value) /** * @param string|Raw $key - * @param string|null|mixed $operator Can be used as value, if 3rd arg not passed + * @param string|mixed|null $operator Can be used as value, if 3rd arg not passed * @param mixed|null $value * * @return static @@ -723,16 +768,17 @@ public function orHaving($key, $operator, $value) public function where($key, $operator = null, $value = null): self { // If two params are given then assume operator is = - if (func_num_args() == 2) { - $value = $operator; + if (2 == func_num_args()) { + $value = $operator; $operator = '='; } + return $this->whereHandler($key, $operator, $value); } /** * @param string|Raw $key - * @param string|null|mixed $operator Can be used as value, if 3rd arg not passed + * @param string|mixed|null $operator Can be used as value, if 3rd arg not passed * @param mixed|null $value * * @return static @@ -740,8 +786,8 @@ public function where($key, $operator = null, $value = null): self public function orWhere($key, $operator = null, $value = null): self { // If two params are given then assume operator is = - if (func_num_args() == 2) { - $value = $operator; + if (2 == func_num_args()) { + $value = $operator; $operator = '='; } @@ -750,7 +796,7 @@ public function orWhere($key, $operator = null, $value = null): self /** * @param string|Raw $key - * @param string|null|mixed $operator Can be used as value, if 3rd arg not passed + * @param string|mixed|null $operator Can be used as value, if 3rd arg not passed * @param mixed|null $value * * @return static @@ -758,16 +804,17 @@ public function orWhere($key, $operator = null, $value = null): self public function whereNot($key, $operator = null, $value = null): self { // If two params are given then assume operator is = - if (func_num_args() == 2) { - $value = $operator; + if (2 == func_num_args()) { + $value = $operator; $operator = '='; } + return $this->whereHandler($key, $operator, $value, 'AND NOT'); } /** * @param string|Raw $key - * @param string|null|mixed $operator Can be used as value, if 3rd arg not passed + * @param string|mixed|null $operator Can be used as value, if 3rd arg not passed * @param mixed|null $value * * @return static @@ -775,10 +822,11 @@ public function whereNot($key, $operator = null, $value = null): self public function orWhereNot($key, $operator = null, $value = null) { // If two params are given then assume operator is = - if (func_num_args() == 2) { - $value = $operator; + if (2 == func_num_args()) { + $value = $operator; $operator = '='; } + return $this->whereHandler($key, $operator, $value, 'OR NOT'); } @@ -835,7 +883,7 @@ public function orWhereNotIn($key, $values): self */ public function whereBetween($key, $valueFrom, $valueTo): self { - return $this->whereHandler($key, 'BETWEEN', array($valueFrom, $valueTo), 'AND'); + return $this->whereHandler($key, 'BETWEEN', [$valueFrom, $valueTo], 'AND'); } /** @@ -847,11 +895,12 @@ public function whereBetween($key, $valueFrom, $valueTo): self */ public function orWhereBetween($key, $valueFrom, $valueTo): self { - return $this->whereHandler($key, 'BETWEEN', array($valueFrom, $valueTo), 'OR'); + return $this->whereHandler($key, 'BETWEEN', [$valueFrom, $valueTo], 'OR'); } /** * @param string|Raw $key + * * @return static */ public function whereNull($key): self @@ -861,6 +910,7 @@ public function whereNull($key): self /** * @param string|Raw $key + * * @return static */ public function whereNotNull($key): self @@ -870,6 +920,7 @@ public function whereNotNull($key): self /** * @param string|Raw $key + * * @return static */ public function orWhereNull($key): self @@ -879,6 +930,7 @@ public function orWhereNull($key): self /** * @param string|Raw $key + * * @return static */ public function orWhereNotNull($key): self @@ -890,22 +942,24 @@ public function orWhereNotNull($key): self * @param string|Raw $key * @param string $prefix * @param string $operator + * * @return static */ protected function whereNullHandler($key, string $prefix = '', $operator = ''): self { - $prefix = \strlen($prefix) === 0 ? '' : " {$prefix}"; + $prefix = 0 === strlen($prefix) ? '' : " {$prefix}"; $key = $this->adapterInstance->wrapSanitizer($this->addTablePrefix($key)); - if ($key instanceof \Closure) { - throw new Exception("Key used for whereNull condition must be a string or raw exrpession.", 1); + if ($key instanceof Closure) { + throw new Exception('Key used for whereNull condition must be a string or raw exrpession.', 1); } + return $this->{$operator . 'Where'}($this->raw("{$key} IS{$prefix} NULL")); } /** * @param string|Raw $table - * @param string|Raw|\Closure $key + * @param string|Raw|Closure $key * @param string|null $operator * @param mixed $value * @param string $type @@ -914,7 +968,7 @@ protected function whereNullHandler($key, string $prefix = '', $operator = ''): */ public function join($table, $key, ?string $operator = null, $value = null, $type = 'inner') { - if (!$key instanceof \Closure) { + if (!$key instanceof Closure) { $key = function ($joinBuilder) use ($key, $operator, $value) { $joinBuilder->on($key, $operator, $value); }; @@ -922,7 +976,7 @@ public function join($table, $key, ?string $operator = null, $value = null, $typ // 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, array($this->connection)); + $joinBuilder = $this->container->build(JoinBuilder::class, [$this->connection]); $joinBuilder = &$joinBuilder; // Call the closure with our new joinBuilder object $key($joinBuilder); @@ -937,16 +991,17 @@ public function join($table, $key, ?string $operator = null, $value = null, $typ * Runs a transaction * * @param \Closure(Transaction):void $callback + * * @return static */ - public function transaction(\Closure $callback): self + public function transaction(Closure $callback): self { try { // Begin the transaction $this->dbInstance->query('START TRANSACTION'); // Get the Transaction class - $transaction = $this->container->build(Transaction::class, array($this->connection)); + $transaction = $this->container->build(Transaction::class, [$this->connection]); $this->handleTransactionCall($callback, $transaction); @@ -960,6 +1015,7 @@ public function transaction(\Closure $callback): self } catch (\Exception $e) { // something happened, rollback changes $this->dbInstance->query('ROLLBACK'); + return $this; } } @@ -969,31 +1025,33 @@ public function transaction(\Closure $callback): self * * Catches any WP Errors (printed) * - * @param \Closure $callback + * @param Closure $callback * @param Transaction $transaction + * * @return void + * * @throws Exception */ - protected function handleTransactionCall(\Closure $callback, Transaction $transaction): void + protected function handleTransactionCall(Closure $callback, Transaction $transaction): void { try { ob_start(); $callback($transaction); $output = ob_get_clean() ?: ''; - } catch (\Throwable $th) { + } catch (Throwable $th) { ob_end_clean(); throw $th; } // If we caught an error, throw an exception. - if (\mb_strlen($output) !== 0) { + if (0 !== mb_strlen($output)) { throw new Exception($output); } } /** * @param string|Raw $table - * @param string|Raw|\Closure $key + * @param string|Raw|Closure $key * @param string|null $operator * @param mixed $value * @@ -1006,7 +1064,7 @@ public function leftJoin($table, $key, $operator = null, $value = null) /** * @param string|Raw $table - * @param string|Raw|\Closure $key + * @param string|Raw|Closure $key * @param string|null $operator * @param mixed $value * @@ -1019,7 +1077,7 @@ public function rightJoin($table, $key, $operator = null, $value = null) /** * @param string|Raw $table - * @param string|Raw|\Closure $key + * @param string|Raw|Closure $key * @param string|null $operator * @param mixed $value * @@ -1032,9 +1090,10 @@ public function innerJoin($table, $key, $operator = null, $value = null) /** * @param string|Raw $table - * @param string|Raw|\Closure $key + * @param string|Raw|Closure $key * @param string|null $operator * @param mixed $value + * * @return static */ public function crossJoin($table, $key, $operator = null, $value = null) @@ -1044,9 +1103,10 @@ public function crossJoin($table, $key, $operator = null, $value = null) /** * @param string|Raw $table - * @param string|Raw|\Closure $key + * @param string|Raw|Closure $key * @param string|null $operator * @param mixed $value + * * @return static */ public function outerJoin($table, $key, $operator = null, $value = null) @@ -1062,7 +1122,7 @@ public function outerJoin($table, $key, $operator = null, $value = null) * * @return Raw */ - public function raw($value, $bindings = array()): Raw + public function raw($value, $bindings = []): Raw { return new Raw($value, $bindings); } @@ -1070,9 +1130,9 @@ public function raw($value, $bindings = array()): Raw /** * Return wpdb instance * - * @return \wpdb + * @return wpdb */ - public function dbInstance(): \wpdb + public function dbInstance(): wpdb { return $this->dbInstance; } @@ -1085,6 +1145,7 @@ public function dbInstance(): \wpdb public function setConnection(Connection $connection): self { $this->connection = $connection; + return $this; } @@ -1097,24 +1158,25 @@ public function getConnection() } /** - * @param string|Raw|\Closure $key - * @param null|string $operator - * @param null|mixed $value + * @param string|Raw|Closure $key + * @param string|null $operator + * @param mixed|null $value * @param string $joiner - * + * * @return static */ protected function whereHandler($key, $operator = null, $value = null, $joiner = 'AND') { - $key = $this->addTablePrefix($key); + $key = $this->addTablePrefix($key); $this->statements['wheres'][] = compact('key', 'operator', 'value', 'joiner'); + return $this; } /** * Add table prefix (if given) on given string. * - * @param array|string|int|float|bool|Raw|\Closure $values + * @param array|string|int|float|bool|Raw|Closure $values * @param bool $tableFieldMix If we have mixes of field and table names with a "." * * @return mixed|mixed[] @@ -1130,16 +1192,16 @@ public function addTablePrefix($values, bool $tableFieldMix = true) // If supplied value is not an array then make it one $single = false; if (!is_array($values)) { - $values = array($values); + $values = [$values]; // We had single value, so should return a single value $single = true; } - $return = array(); + $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) { + if ($value instanceof Raw || $value instanceof Closure) { $return[$key] = $value; continue; } @@ -1151,7 +1213,7 @@ public function addTablePrefix($values, bool $tableFieldMix = true) $target = &$key; } - if (!$tableFieldMix || (is_string($target) && strpos($target, '.') !== false)) { + if (!$tableFieldMix || (is_string($target) && false !== strpos($target, '.'))) { $target = $this->tablePrefix . $target; } @@ -1165,12 +1227,13 @@ public function addTablePrefix($values, bool $tableFieldMix = true) /** * @param string $key * @param mixed|mixed[]|bool $value + * * @return void */ protected function addStatement($key, $value) { if (!is_array($value)) { - $value = array($value); + $value = [$value]; } if (!array_key_exists($key, $this->statements)) { @@ -1194,15 +1257,15 @@ public function getEvent(string $event, $table = ':any'): ?callable /** * @param string $event * @param string|Raw $table - * @param \Closure $action + * @param Closure $action * * @return void */ - public function registerEvent($event, $table, \Closure $action): void + public function registerEvent($event, $table, Closure $action): void { $table = $table ?: ':any'; - if ($table != ':any') { + if (':any' != $table) { $table = $this->addTablePrefix($table, false); } @@ -1217,7 +1280,7 @@ public function registerEvent($event, $table, \Closure $action): void */ public function removeEvent(string $event, $table = ':any') { - if ($table != ':any') { + if (':any' != $table) { $table = $this->addTablePrefix($table, false); } @@ -1226,13 +1289,15 @@ public function removeEvent(string $event, $table = ':any') /** * @param string $event + * * @return mixed */ public function fireEvents(string $event) { $params = func_get_args(); // @todo Replace this with an easier to read alteratnive array_unshift($params, $this); - return call_user_func_array(array($this->connection->getEventHandler(), 'fireEvents'), $params); + + return call_user_func_array([$this->connection->getEventHandler(), 'fireEvents'], $params); } /** diff --git a/src/QueryBuilder/QueryObject.php b/src/QueryBuilder/QueryObject.php index c1dc7a3..c478950 100644 --- a/src/QueryBuilder/QueryObject.php +++ b/src/QueryBuilder/QueryObject.php @@ -2,9 +2,10 @@ namespace Pixie\QueryBuilder; +use wpdb; + class QueryObject { - /** * @var string */ @@ -13,22 +14,22 @@ class QueryObject /** * @var mixed[] */ - protected $bindings = array(); + protected $bindings = []; /** - * @var \wpdb + * @var wpdb */ protected $dbInstance; /** * @param string $sql * @param mixed[] $bindings - * @param \wpdb $dbInstance + * @param wpdb $dbInstance */ - public function __construct(string $sql, array $bindings, \wpdb $dbInstance) + public function __construct(string $sql, array $bindings, wpdb $dbInstance) { - $this->sql = (string)$sql; - $this->bindings = $bindings; + $this->sql = (string)$sql; + $this->bindings = $bindings; $this->dbInstance = $dbInstance; } @@ -71,6 +72,7 @@ protected function interpolateQuery($query, $params): string { // Only call this when we have valid params (avoids wpdb::prepare() incorrectly called error) $value = empty($params) ? $query : $this->dbInstance->prepare($query, $params); + return is_string($value) ? $value : ''; } } diff --git a/src/QueryBuilder/Raw.php b/src/QueryBuilder/Raw.php index 1b0d29b..bef8efd 100644 --- a/src/QueryBuilder/Raw.php +++ b/src/QueryBuilder/Raw.php @@ -4,7 +4,6 @@ class Raw { - /** * @var string */ @@ -19,9 +18,9 @@ class Raw * @param string|Raw $value * @param mixed|mixed[] $bindings */ - public function __construct($value, $bindings = array()) + public function __construct($value, $bindings = []) { - $this->value = (string)$value; + $this->value = (string)$value; $this->bindings = (array)$bindings; } diff --git a/src/QueryBuilder/Transaction.php b/src/QueryBuilder/Transaction.php index f770d23..8bdaa9f 100644 --- a/src/QueryBuilder/Transaction.php +++ b/src/QueryBuilder/Transaction.php @@ -4,7 +4,6 @@ class Transaction extends QueryBuilderHandler { - /** * Commit the database changes * diff --git a/src/QueryBuilder/TransactionHaltException.php b/src/QueryBuilder/TransactionHaltException.php index c6ba982..3d12bb9 100644 --- a/src/QueryBuilder/TransactionHaltException.php +++ b/src/QueryBuilder/TransactionHaltException.php @@ -2,6 +2,8 @@ namespace Pixie\QueryBuilder; -class TransactionHaltException extends \Exception +use Exception; + +class TransactionHaltException extends Exception { } diff --git a/src/QueryBuilder/WPDBAdapter.php b/src/QueryBuilder/WPDBAdapter.php index 768ed0c..237b54d 100644 --- a/src/QueryBuilder/WPDBAdapter.php +++ b/src/QueryBuilder/WPDBAdapter.php @@ -2,10 +2,13 @@ namespace Pixie\QueryBuilder; +use Closure; use Pixie\Exception; use Pixie\Connection; use Pixie\QueryBuilder\Raw; use Pixie\QueryBuilder\NestedCriteria; +use function is_bool; +use function is_float; class WPDBAdapter { @@ -27,15 +30,16 @@ class WPDBAdapter public function __construct(Connection $connection) { $this->connection = $connection; - $this->container = $this->connection->getContainer(); + $this->container = $this->connection->getContainer(); } /** * Build select query string and bindings * - * @param array $statements + * @param array $statements * * @throws Exception + * * @return array{sql:string,bindings:mixed[]} */ public function select(array $statements): array @@ -51,7 +55,6 @@ public function select(array $statements): array // Select $selects = $this->arrayStr($statements['selects'], ', '); - // Wheres list($whereCriteria, $whereBindings) = $this->buildCriteriaWithType($statements, 'wheres', 'WHERE'); // Group bys @@ -65,7 +68,7 @@ public function select(array $statements): array if (isset($statements['orderBys']) && is_array($statements['orderBys'])) { foreach ($statements['orderBys'] as $orderBy) { $field = $this->wrapSanitizer($orderBy['field']); - if ($field instanceof \Closure) { + if ($field instanceof Closure) { continue; } $orderBys .= $field . ' ' . $orderBy['type'] . ', '; @@ -77,7 +80,7 @@ public function select(array $statements): array } // Limit and offset - $limit = isset($statements['limit']) ? 'LIMIT ' . (int) $statements['limit'] : ''; + $limit = isset($statements['limit']) ? 'LIMIT ' . (int) $statements['limit'] : ''; $offset = isset($statements['offset']) ? 'OFFSET ' . (int) $statements['offset'] : ''; // Having @@ -87,7 +90,7 @@ public function select(array $statements): array $joinString = $this->buildJoin($statements); /** @var string[] */ - $sqlArray = array( + $sqlArray = [ 'SELECT' . (isset($statements['distinct']) ? ' DISTINCT' : ''), $selects, 'FROM', @@ -98,8 +101,8 @@ public function select(array $statements): array $havingCriteria, $orderBys, $limit, - $offset - ); + $offset, + ]; $sql = $this->concatenateQuery($sqlArray); @@ -114,14 +117,14 @@ public function select(array $statements): array /** * Build just criteria part of the query * - * @param array $statements + * @param array $statements * @param bool $bindValues * * @return array{sql:string[]|string, bindings:array} */ public function criteriaOnly(array $statements, bool $bindValues = true): array { - $sql = $bindings = array(); + $sql = $bindings = []; if (!isset($statements['criteria'])) { return compact('sql', 'bindings'); } @@ -134,11 +137,12 @@ public function criteriaOnly(array $statements, bool $bindValues = true): array /** * Build a generic insert/ignore/replace query * - * @param array $statements + * @param array $statements * @param array $data * @param string $type * * @return array{sql:string, bindings:mixed[]} + * * @throws Exception */ private function doInsert(array $statements, array $data, string $type): array @@ -149,33 +153,33 @@ private function doInsert(array $statements, array $data, string $type): array $table = end($statements['tables']); - $bindings = $keys = $values = array(); + $bindings = $keys = $values = []; foreach ($data as $key => $value) { $keys[] = $key; if ($value instanceof Raw) { $values[] = (string) $value; } else { - $values[] = $this->inferType($value); + $values[] = $this->inferType($value); $bindings[] = $value; } } - $sqlArray = array( + $sqlArray = [ $type . ' INTO', $this->wrapSanitizer($table), '(' . $this->arrayStr($keys, ',') . ')', 'VALUES', '(' . $this->arrayStr($values, ',') . ')', - ); + ]; if (isset($statements['onduplicate'])) { 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); + $sqlArray[] = 'ON DUPLICATE KEY UPDATE ' . $updateStatement; + $bindings = array_merge($bindings, $updateBindings); } $sql = $this->concatenateQuery($this->stringifyValues($sqlArray)); @@ -186,26 +190,27 @@ private function doInsert(array $statements, array $data, string $type): array /** * Attempts to stringify an array of values. * - * @param array $values + * @param array $values + * * @return string[] */ protected function stringifyValues(array $values): array { - $values = array_map([$this, 'stringifyValue'], $values); - /** @var string[] */ - return array_filter($values, 'is_string'); + return array_filter(array_map([$this, 'stringifyValue'], $values)); } /** * Attempts to stringify a single of values. * - * @param string|\Closure|Raw $value + * @param string|Closure|Raw $value + * * @return string|null */ protected function stringifyValue($value): ?string { - if ($value instanceof \Closure) { + if ($value instanceof Closure) { $value = $value(); + return is_string($value) ? $value : null; } @@ -219,10 +224,11 @@ protected function stringifyValue($value): ?string /** * Build Insert query * - * @param array $statements + * @param array $statements * @param array $data $data * * @return array{sql:string, bindings:mixed[]} + * * @throws Exception */ public function insert($statements, array $data) @@ -233,10 +239,11 @@ public function insert($statements, array $data) /** * Build Insert Ignore query * - * @param array $statements + * @param array $statements * @param array $data $data * * @return array{sql:string, bindings:mixed[]} + * * @throws Exception */ public function insertIgnore($statements, array $data) @@ -247,10 +254,11 @@ public function insertIgnore($statements, array $data) /** * Build Insert Ignore query * - * @param array $statements + * @param array $statements * @param array $data $data * * @return array{sql:string, bindings:mixed[]} + * * @throws Exception */ public function replace($statements, array $data) @@ -267,7 +275,7 @@ public function replace($statements, array $data) */ private function getUpdateStatement(array $data): array { - $bindings = array(); + $bindings = []; $statement = ''; foreach ($data as $key => $value) { @@ -280,16 +288,18 @@ private function getUpdateStatement(array $data): array } $statement = trim($statement, ','); - return array($statement, $bindings); + + return [$statement, $bindings]; } /** * Build update query * - * @param array $statements + * @param array $statements * @param array $data * * @return array{sql:string, bindings:mixed[]} + * * @throws Exception */ public function update($statements, array $data) @@ -311,26 +321,28 @@ public function update($statements, array $data) // Limit $limit = isset($statements['limit']) ? 'LIMIT ' . $statements['limit'] : ''; - $sqlArray = array( + $sqlArray = [ 'UPDATE', $this->wrapSanitizer($table), 'SET ' . $updateStatement, $whereCriteria, - $limit - ); + $limit, + ]; $sql = $this->concatenateQuery($this->stringifyValues($sqlArray)); $bindings = array_merge($bindings, $whereBindings); + return compact('sql', 'bindings'); } /** * Build delete query * - * @param array $statements + * @param array $statements * * @return array{sql:string, bindings:mixed[]} + * * @throws Exception */ public function delete($statements) @@ -352,9 +364,8 @@ public function delete($statements) // Limit $limit = isset($statements['limit']) ? 'LIMIT ' . $statements['limit'] : ''; - - $sqlArray = array('DELETE FROM', $table, $whereCriteria); - $sql = $this->concatenateQuery($sqlArray); + $sqlArray = ['DELETE FROM', $table, $whereCriteria]; + $sql = $this->concatenateQuery($sqlArray); $bindings = $whereBindings; return compact('sql', 'bindings'); @@ -396,13 +407,14 @@ protected function concatenateQuery(array $pieces): string foreach ($pieces as $piece) { $str = trim($str) . ' ' . trim($piece); } + return trim($str); } /** * Build generic criteria string and bindings from statements, like "a = b and c = ?" * - * @param array $statements + * @param array $statements * @param bool $bindValues * * @return array{0:string,1:string[]} @@ -410,17 +422,17 @@ protected function concatenateQuery(array $pieces): string protected function buildCriteria(array $statements, bool $bindValues = true): array { $criteria = ''; - $bindings = array(); + $bindings = []; foreach ($statements as $statement) { - $key = $statement['key']; + $key = $statement['key']; $value = $statement['value']; - if (is_null($value) && $key instanceof \Closure) { + if (is_null($value) && $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 = $this->container->build(NestedCriteria::class, array($this->connection)); + $nestedCriteria = $this->container->build(NestedCriteria::class, [$this->connection]); $nestedCriteria = &$nestedCriteria; // Call the closure with our new nestedCriteria object @@ -473,7 +485,7 @@ protected function buildCriteria(array $statements, bool $bindValues = true): ar } else { // For wheres $valuePlaceholder = $this->inferType($value); - $bindings[] = $value; + $bindings[] = $value; $criteria .= $statement['joiner'] . ' ' . $key . ' ' . $statement['operator'] . ' ' . $valuePlaceholder . ' '; } @@ -483,13 +495,14 @@ protected function buildCriteria(array $statements, bool $bindValues = true): ar // Clear all white spaces, and, or from beginning and white spaces from ending $criteria = preg_replace('/^(\s?AND ?|\s?OR ?)|\s$/i', '', $criteria); - return array($criteria ?? '', $bindings); + return [$criteria ?? '', $bindings]; } /** * Asserts the types place holder based on its value * * @param mixed $value + * * @return string */ public function inferType($value): string @@ -498,9 +511,9 @@ public function inferType($value): string case is_string($value): return '%s'; case \is_int($value): - case \is_bool($value): + case is_bool($value): return '%d'; - case \is_float($value): + case is_float($value): return '%f'; default: return ''; @@ -510,16 +523,16 @@ public function inferType($value): string /** * Wrap values with adapter's sanitizer like, '`' * - * @param string|Raw|\Closure $value + * @param string|Raw|Closure $value * - * @return string|\Closure + * @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; - } elseif ($value instanceof \Closure) { + } elseif ($value instanceof Closure) { return $value; } @@ -529,7 +542,7 @@ public function wrapSanitizer($value) foreach ($valueArr as $key => $subValue) { // Don't wrap if we have *, which is not a usual field - $valueArr[$key] = trim($subValue) == '*' ? $subValue : $this->sanitizer . $subValue . $this->sanitizer; + $valueArr[$key] = '*' == trim($subValue) ? $subValue : $this->sanitizer . $subValue . $this->sanitizer; } // Join these back with "." and return @@ -539,7 +552,7 @@ public function wrapSanitizer($value) /** * 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 @@ -549,7 +562,7 @@ public function wrapSanitizer($value) protected function buildCriteriaWithType(array $statements, string $key, string $type, bool $bindValues = true) { $criteria = ''; - $bindings = array(); + $bindings = []; if (isset($statements[$key])) { // Get the generic/adapter agnostic criteria string from parent @@ -560,13 +573,13 @@ protected function buildCriteriaWithType(array $statements, string $key, string } } - return array($criteria, $bindings); + return [$criteria, $bindings]; } /** * Build join string * - * @param array $statements + * @param array $statements * * @return string */ @@ -580,9 +593,9 @@ protected function buildJoin(array $statements): string foreach ($statements['joins'] as $joinArr) { if (is_array($joinArr['table'])) { - $mainTable = $this->stringifyValue($this->wrapSanitizer($joinArr['table'][0])); + $mainTable = $this->stringifyValue($this->wrapSanitizer($joinArr['table'][0])); $aliasTable = $this->stringifyValue($this->wrapSanitizer($joinArr['table'][1])); - $table = $mainTable . ' AS ' . $aliasTable; + $table = $mainTable . ' AS ' . $aliasTable; } else { $table = $joinArr['table'] instanceof Raw ? (string) $joinArr['table'] : @@ -591,14 +604,14 @@ protected function buildJoin(array $statements): string $joinBuilder = $joinArr['joinBuilder']; /** @var string[] */ - $sqlArr = array( + $sqlArr = [ $sql, strtoupper($joinArr['type']), 'JOIN', $table, 'ON', - $joinBuilder->getQuery('criteriaOnly', false)->getSql() - ); + $joinBuilder->getQuery('criteriaOnly', false)->getSql(), + ]; $sql = $this->concatenateQuery($sqlArr); } From 3b635b43197680e41e73d48145c097ebbb1af8a2 Mon Sep 17 00:00:00 2001 From: Glynn Quelch Date: Sun, 16 Jan 2022 22:58:53 +0000 Subject: [PATCH 20/69] House Keeping --- src/QueryBuilder/QueryBuilderHandler.php | 14 ++++++-------- 1 file changed, 6 insertions(+), 8 deletions(-) diff --git a/src/QueryBuilder/QueryBuilderHandler.php b/src/QueryBuilder/QueryBuilderHandler.php index bba90c8..080b4bb 100644 --- a/src/QueryBuilder/QueryBuilderHandler.php +++ b/src/QueryBuilder/QueryBuilderHandler.php @@ -2,20 +2,18 @@ namespace Pixie\QueryBuilder; +use wpdb; use Closure; -use PDO; -use Pixie\Connection; +use Throwable; use Pixie\Exception; +use Pixie\Connection; +use function mb_strlen; +use Pixie\QueryBuilder\Raw; use Pixie\Hydration\Hydrator; use Pixie\QueryBuilder\JoinBuilder; use Pixie\QueryBuilder\QueryObject; -use Pixie\QueryBuilder\Raw; use Pixie\QueryBuilder\Transaction; use Pixie\QueryBuilder\WPDBAdapter; -use Throwable; -use wpdb; -use function mb_strlen; -use function strlen; class QueryBuilderHandler { @@ -947,7 +945,7 @@ public function orWhereNotNull($key): self */ protected function whereNullHandler($key, string $prefix = '', $operator = ''): self { - $prefix = 0 === strlen($prefix) ? '' : " {$prefix}"; + $prefix = 0 === mb_strlen($prefix) ? '' : " {$prefix}"; $key = $this->adapterInstance->wrapSanitizer($this->addTablePrefix($key)); if ($key instanceof Closure) { From 6b42896a31d78036b50b2ac74559dd6f52748c38 Mon Sep 17 00:00:00 2001 From: Glynn Quelch Date: Sun, 16 Jan 2022 23:01:07 +0000 Subject: [PATCH 21/69] House Keeping --- src/QueryBuilder/QueryBuilderHandler.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/QueryBuilder/QueryBuilderHandler.php b/src/QueryBuilder/QueryBuilderHandler.php index 080b4bb..4d2fe8a 100644 --- a/src/QueryBuilder/QueryBuilderHandler.php +++ b/src/QueryBuilder/QueryBuilderHandler.php @@ -7,13 +7,13 @@ use Throwable; use Pixie\Exception; use Pixie\Connection; -use function mb_strlen; use Pixie\QueryBuilder\Raw; use Pixie\Hydration\Hydrator; use Pixie\QueryBuilder\JoinBuilder; use Pixie\QueryBuilder\QueryObject; use Pixie\QueryBuilder\Transaction; use Pixie\QueryBuilder\WPDBAdapter; +use function mb_strlen; class QueryBuilderHandler { From 3594a0353504b33f2038d7d510265d131a35d481 Mon Sep 17 00:00:00 2001 From: Glynn Quelch Date: Sun, 16 Jan 2022 23:03:07 +0000 Subject: [PATCH 22/69] House Keeping --- .php-cs-fixer.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.php-cs-fixer.php b/.php-cs-fixer.php index 352df4d..787eb9d 100644 --- a/.php-cs-fixer.php +++ b/.php-cs-fixer.php @@ -25,7 +25,7 @@ ->setRiskyAllowed(true) ->setRules([ // default - '@PSR2' => true, + '@PSR12' => true, '@Symfony' => true, // additionally 'array_syntax' => ['syntax' => 'short'], From 6a24545d73b80e56c5889192608cc1ed16e4a696 Mon Sep 17 00:00:00 2001 From: Glynn Quelch Date: Sun, 16 Jan 2022 23:37:48 +0000 Subject: [PATCH 23/69] Now allows the use of joinUsing --- src/QueryBuilder/QueryBuilderHandler.php | 25 ++++++++++++++-- tests/TestQueryBuilderHandler.php | 38 ++++++++++++++++++++++++ tests/TestQueryBuilderWPDBPrepare.php | 5 ++-- 3 files changed, 64 insertions(+), 4 deletions(-) diff --git a/src/QueryBuilder/QueryBuilderHandler.php b/src/QueryBuilder/QueryBuilderHandler.php index 4d2fe8a..ff27680 100644 --- a/src/QueryBuilder/QueryBuilderHandler.php +++ b/src/QueryBuilder/QueryBuilderHandler.php @@ -7,13 +7,13 @@ use Throwable; use Pixie\Exception; use Pixie\Connection; +use function mb_strlen; use Pixie\QueryBuilder\Raw; use Pixie\Hydration\Hydrator; use Pixie\QueryBuilder\JoinBuilder; use Pixie\QueryBuilder\QueryObject; use Pixie\QueryBuilder\Transaction; use Pixie\QueryBuilder\WPDBAdapter; -use function mb_strlen; class QueryBuilderHandler { @@ -981,7 +981,6 @@ public function join($table, $key, ?string $operator = null, $value = null, $typ $table = $this->addTablePrefix($table, false); // Get the criteria only query from the joinBuilder object $this->statements['joins'][] = compact('type', 'table', 'joinBuilder'); - return $this; } @@ -1112,6 +1111,28 @@ public function outerJoin($table, $key, $operator = null, $value = null) return $this->join($table, $key, $operator, $value, 'outer'); } + /** + * Shortcut to join 2 tables on the same key name with equals + * + * @param string|Raw $table + * @param string|Raw|Closure $key + * @param string $operator + * @return self + * @throws Exception If base table is set as more than 1 or 0 + */ + public function joinUsing($table, $key, $operator = '='): self + { + + if (!array_key_exists('tables', $this->statements) || count($this->statements['tables']) !== 1) { + throw new Exception("JoinUsing can only be used with a single table set as the base of the query", 1); + } + $baseTable = end($this->statements['tables']); + + $remoteKey = $table = $this->addTablePrefix("{$table}.{$key}", true); + $localKey = $table = $this->addTablePrefix("{$baseTable}.{$key}", true); + return $this->join($table, $remoteKey, '=', $localKey); + } + /** * Add a raw query * diff --git a/tests/TestQueryBuilderHandler.php b/tests/TestQueryBuilderHandler.php index 65958cc..c348c81 100644 --- a/tests/TestQueryBuilderHandler.php +++ b/tests/TestQueryBuilderHandler.php @@ -17,6 +17,7 @@ use Pixie\QueryBuilder\Raw; use Pixie\Tests\Logable_WPDB; use Pixie\QueryBuilder\Transaction; +use Pixie\Exception as PixieException; use Pixie\QueryBuilder\QueryBuilderHandler; use Pixie\QueryBuilder\TransactionHaltException; @@ -217,4 +218,41 @@ public function testAddTablePrefix(): void ); $this->assertEquals('prefix_someTable', $prefixedSingle); } + + /** @testdox It should be possible to create a simple joinUsing query for simple FROM tableA JOIN tableB ON tableA.key = tableB.key, using only the table and key. */ + public function testJoinUsing(): void + { + $this->queryBuilderProvider() + ->table('foo') + ->joinUsing('bar', 'id') + ->get(); + + $this->assertEquals( + "SELECT * FROM foo INNER JOIN foo.id ON bar.id = foo.id", + $this->wpdb->usage_log['get_results'][0]['query'] + ); + } + + /** @testdox When attempting to use joinUsing, a base table must be defined or an exception will be thrown */ + public function testJoinUsingThrowsIfNoTableSelected(): void + { + $this->expectExceptionMessage('JoinUsing can only be used with a single table set as the base of the query'); + $this->expectException(PixieException::class); + + $this->queryBuilderProvider() + ->joinUsing('bar', 'id') + ->get(); + } + + /** @testdox When attempting to use joinUsing, only a single base table must be defined or an exception will be thrown */ + public function testJoinUsingThrowsIfMultipleTableSelected(): void + { + $this->expectExceptionMessage('JoinUsing can only be used with a single table set as the base of the query'); + $this->expectException(PixieException::class); + + $this->queryBuilderProvider() + ->table('a', 'b') + ->joinUsing('bar', 'id') + ->get(); + } } diff --git a/tests/TestQueryBuilderWPDBPrepare.php b/tests/TestQueryBuilderWPDBPrepare.php index ee38faa..619702d 100644 --- a/tests/TestQueryBuilderWPDBPrepare.php +++ b/tests/TestQueryBuilderWPDBPrepare.php @@ -130,7 +130,7 @@ public function testGetWithSingleConditionArrayInValue(): void $this->assertEquals('string', $prepared['args'][2]); } - /** @testdox It should be possible to create a get call with value (BETWEEN) condition and have this generated and run through WPDB::prepare() */ + /** @testdox It should be possible to create a get call with value (BETWEEN) condition and have this generated and run through WPDB::prepare() */ public function testGetWithSingleConditionBetweenValue(): void { $builder = $this->queryBuilderProvider(); @@ -183,7 +183,7 @@ public function testInsertSingle(): void ->table('foo') ->insert($data); - // Query and values passed to prepare(); + // Query and values passed to prepare(); $prepared = $this->wpdb->usage_log['prepare'][0]; // Check that the query is passed to prepare. @@ -207,4 +207,5 @@ public function testInsertOnDuplicateKey(): void $this->wpdb->usage_log['prepare'][0]['query'] ); } + } From 966d67295bdebc9f21fa78e06ca10155707ea391 Mon Sep 17 00:00:00 2001 From: Glynn Quelch Date: Sun, 16 Jan 2022 23:40:34 +0000 Subject: [PATCH 24/69] House Keeping --- README.md | 16 +++++++++++++--- 1 file changed, 13 insertions(+), 3 deletions(-) diff --git a/README.md b/README.md index c97edb8..fe6cc3c 100644 --- a/README.md +++ b/README.md @@ -200,7 +200,7 @@ $query = QB::table('my_table')->select('*'); #### Multiple Selects ```PHP -->select(array('mytable.myfield1', 'mytable.myfield2', 'another_table.myfield3')); +->select('mytable.myfield1', 'mytable.myfield2', 'another_table.myfield3'); ``` Using select method multiple times `select('a')->select('b')` will also select `a` and `b`. Can be useful if you want to do conditional selects (within a PHP `if`). @@ -338,6 +338,18 @@ Available methods, - outerJoin() - crossJoin() +### Join Using + +It is possible to create a simple join statment between 2 tables, where they are matched on the same key names. + +```php +->table('foo')->join('bar', 'bar.id', '=', 'foo.id'); + +// Would become +->table('foo')->joinUsing('bar', 'id'); +``` +> Please note this only works with a single base table defined. + #### Multiple Join Criteria If you need more than one criterion to join a table then pass a closure as second parameter. @@ -352,8 +364,6 @@ If you need more than one criterion to join a table then pass a closure as secon > Closures can be used as for the $key -// GLYNN - ### Raw Query You can always use raw queries if you need, ```PHP From 266e25094bcf5df3c3029286ec3a200c59a03598 Mon Sep 17 00:00:00 2001 From: Glynn Quelch Date: Sun, 16 Jan 2022 23:44:48 +0000 Subject: [PATCH 25/69] House Keeping --- src/QueryBuilder/QueryBuilderHandler.php | 11 +++++------ 1 file changed, 5 insertions(+), 6 deletions(-) diff --git a/src/QueryBuilder/QueryBuilderHandler.php b/src/QueryBuilder/QueryBuilderHandler.php index ff27680..1c1902e 100644 --- a/src/QueryBuilder/QueryBuilderHandler.php +++ b/src/QueryBuilder/QueryBuilderHandler.php @@ -7,13 +7,13 @@ use Throwable; use Pixie\Exception; use Pixie\Connection; -use function mb_strlen; use Pixie\QueryBuilder\Raw; use Pixie\Hydration\Hydrator; use Pixie\QueryBuilder\JoinBuilder; use Pixie\QueryBuilder\QueryObject; use Pixie\QueryBuilder\Transaction; use Pixie\QueryBuilder\WPDBAdapter; +use function mb_strlen; class QueryBuilderHandler { @@ -1114,15 +1114,14 @@ public function outerJoin($table, $key, $operator = null, $value = null) /** * Shortcut to join 2 tables on the same key name with equals * - * @param string|Raw $table - * @param string|Raw|Closure $key + * @param string $table + * @param string $key * @param string $operator * @return self * @throws Exception If base table is set as more than 1 or 0 */ - public function joinUsing($table, $key, $operator = '='): self + public function joinUsing(string $table, string $key, string $operator = '='): self { - if (!array_key_exists('tables', $this->statements) || count($this->statements['tables']) !== 1) { throw new Exception("JoinUsing can only be used with a single table set as the base of the query", 1); } @@ -1130,7 +1129,7 @@ public function joinUsing($table, $key, $operator = '='): self $remoteKey = $table = $this->addTablePrefix("{$table}.{$key}", true); $localKey = $table = $this->addTablePrefix("{$baseTable}.{$key}", true); - return $this->join($table, $remoteKey, '=', $localKey); + return $this->join($table, $remoteKey, $operator, $localKey); } /** From b4d41ae1e0f9c061104dc7750f1030c3891009c8 Mon Sep 17 00:00:00 2001 From: Glynn Quelch Date: Mon, 17 Jan 2022 10:05:14 +0000 Subject: [PATCH 26/69] House Keeping --- src/QueryBuilder/QueryBuilderHandler.php | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/QueryBuilder/QueryBuilderHandler.php b/src/QueryBuilder/QueryBuilderHandler.php index 1c1902e..c3567b1 100644 --- a/src/QueryBuilder/QueryBuilderHandler.php +++ b/src/QueryBuilder/QueryBuilderHandler.php @@ -1116,11 +1116,11 @@ public function outerJoin($table, $key, $operator = null, $value = null) * * @param string $table * @param string $key - * @param string $operator + * @param string $type * @return self * @throws Exception If base table is set as more than 1 or 0 */ - public function joinUsing(string $table, string $key, string $operator = '='): self + public function joinUsing(string $table, string $key, string $type = 'INNER'): self { if (!array_key_exists('tables', $this->statements) || count($this->statements['tables']) !== 1) { throw new Exception("JoinUsing can only be used with a single table set as the base of the query", 1); @@ -1129,7 +1129,7 @@ public function joinUsing(string $table, string $key, string $operator = '='): s $remoteKey = $table = $this->addTablePrefix("{$table}.{$key}", true); $localKey = $table = $this->addTablePrefix("{$baseTable}.{$key}", true); - return $this->join($table, $remoteKey, $operator, $localKey); + return $this->join($table, $remoteKey, '=', $localKey, $type); } /** From e1c1094d3d623078db01f95727c36c5a18df5cbd Mon Sep 17 00:00:00 2001 From: Glynn Quelch Date: Mon, 17 Jan 2022 11:12:02 +0000 Subject: [PATCH 27/69] Basic bindings object created with tests, now needs wiring in --- src/Binding.php | 161 +++++++++++++++++++++++ src/QueryBuilder/QueryBuilderHandler.php | 3 +- src/QueryBuilder/WPDBAdapter.php | 4 +- tests/TestBinding.php | 130 ++++++++++++++++++ tests/TestQueryBuilderSQLGeneration.php | 50 ++++--- 5 files changed, 326 insertions(+), 22 deletions(-) create mode 100644 src/Binding.php create mode 100644 tests/TestBinding.php diff --git a/src/Binding.php b/src/Binding.php new file mode 100644 index 0000000..cfe8377 --- /dev/null +++ b/src/Binding.php @@ -0,0 +1,161 @@ +verifyType($type); + $this->value = $value; + $this->type = $type; + if (self::RAW === $type) { + $this->isRaw = true; + } + } + + /** + * Creates a binding for a String + * + * @param mixed $value + * @return self + */ + public static function asString($value): self + { + return new Binding($value, self::STRING); + } + + /** + * Creates a binding for a Float + * + * @param mixed $value + * @return self + */ + public static function asFloat($value): self + { + return new Binding($value, self::FLOAT); + } + + /** + * Creates a binding for a Int + * + * @param mixed $value + * @return self + */ + public static function asInt($value): self + { + return new Binding($value, self::INT); + } + + /** + * Creates a binding for a Bool + * + * @param mixed $value + * @return self + */ + public static function asBool($value): self + { + return new Binding($value, self::BOOL); + } + + /** + * Creates a binding for a JSON + * + * @param mixed $value + * @return self + */ + public static function asJSON($value): self + { + return new Binding($value, self::JSON); + } + + /** + * Creates a binding for a Raw + * + * @param mixed $value + * @return self + */ + public static function asRaw($value): self + { + return new Binding($value, self::RAW); + } + + /** + * Verifies that the passed type is allowed + * + * @param string|null $type + * @return void + * @throws Exception if not a valid type. + */ + protected function verifyType(?string $type): void + { + $validTypes = [self::STRING, self::BOOL, self::FLOAT, self::INT, self::JSON, self::RAW]; + if (null !== $type && !in_array($type, $validTypes, true)) { + throw new Exception("{$type} is not a valid type to use for Bindings.", 1); + } + } + + /** + * Checks if we have a type that will bind. + * + * @return bool + */ + public function hasTypeDefined(): bool + { + return !\in_array($this->type, [null, self::RAW], true); + } + + /** + * Returns the bindings values + * + * @return string|int|float|bool|Raw|null + */ + public function getValue() + { + return ! $this->hasTypeDefined() + ? new Raw($this->value) + : $this->value; + } + + /** + * Gets the types format Conversion Specifier + * + * @return string + */ + public function getType(): string + { + return $this->type; + } +} diff --git a/src/QueryBuilder/QueryBuilderHandler.php b/src/QueryBuilder/QueryBuilderHandler.php index c3567b1..a451f13 100644 --- a/src/QueryBuilder/QueryBuilderHandler.php +++ b/src/QueryBuilder/QueryBuilderHandler.php @@ -7,13 +7,13 @@ use Throwable; use Pixie\Exception; use Pixie\Connection; +use function mb_strlen; use Pixie\QueryBuilder\Raw; use Pixie\Hydration\Hydrator; use Pixie\QueryBuilder\JoinBuilder; use Pixie\QueryBuilder\QueryObject; use Pixie\QueryBuilder\Transaction; use Pixie\QueryBuilder\WPDBAdapter; -use function mb_strlen; class QueryBuilderHandler { @@ -183,6 +183,7 @@ public function query($sql, $bindings = []): self */ public function statement(string $sql, $bindings = []): array { + dump($sql); $start = microtime(true); $sqlStatement = empty($bindings) ? $sql : $this->dbInstance->prepare($sql, $bindings); diff --git a/src/QueryBuilder/WPDBAdapter.php b/src/QueryBuilder/WPDBAdapter.php index 237b54d..ae2154b 100644 --- a/src/QueryBuilder/WPDBAdapter.php +++ b/src/QueryBuilder/WPDBAdapter.php @@ -4,11 +4,11 @@ use Closure; use Pixie\Exception; +use function is_bool; use Pixie\Connection; +use function is_float; use Pixie\QueryBuilder\Raw; use Pixie\QueryBuilder\NestedCriteria; -use function is_bool; -use function is_float; class WPDBAdapter { diff --git a/tests/TestBinding.php b/tests/TestBinding.php new file mode 100644 index 0000000..6bb5bc6 --- /dev/null +++ b/tests/TestBinding.php @@ -0,0 +1,130 @@ + + */ + +namespace Pixie\Tests; + +use Pixie\Binding; +use Pixie\Exception; +use WP_UnitTestCase; +use Pixie\QueryBuilder\Raw; + +class TestBinding extends WP_UnitTestCase +{ + + /** @testdox It should be possible to create a bindings using the Value and its Type. */ + public function testCanCreateValidBinding() + { + $cases = [ + 'valueString' => Binding::STRING, + 'valueBool' => Binding::BOOL, + 'valueInt' => Binding::INT, + 'valueFloat' => Binding::FLOAT, + 'valueRaw' => Binding::RAW, + 'valueJSON' => Binding::JSON, + ]; + + foreach ($cases as $value => $type) { + $binding = new Binding($value, $type); + $this->assertEquals($type, $binding->getType()); + + // Raw should return an instance of Raw with the value callable using __toString() + if (Binding::RAW === $type) { + $this->assertInstanceOf(Raw::class, $binding->getValue()); + $this->assertEquals($value, (string)$binding->getValue()); + } else { + $this->assertEquals($value, $binding->getValue()); + } + } + } + + /** @testdox It should be possible to check if the binding has a value which can be bound. */ + public function testHasTypeDefined(): void + { + $cases = [ + 'asString' => true, + 'asFloat' => true, + 'asInt' => true, + 'asBool' => true, + 'asJSON' => true, + 'asRAW' => false, + ]; + + foreach ($cases as $method => $value) { + $binding = Binding::{$method}('Value'); + $this->assertEquals($value, $binding->hasTypeDefined(), "{$method} failed"); + } + } + + /** @testdox It should be possible to create a binding without passing a type or using null and having this treated as a RAW query. */ + public function testAllowNoTypeToBePassed() + { + $noType = new Binding('noType'); + $this->assertInstanceOf(Raw::class, $noType->getValue()); + + $nullType = new Binding('asNull', null); + $this->assertInstanceOf(Raw::class, $nullType->getValue()); + } + + /** @testdox When attempting to create a binding using a none supported type, an exception should be thrown. */ + public function testCanNotCreateBindingWithInvalidType(): void + { + $this->expectException(Exception::class); + new Binding('val', 'invalid'); + } + + /** @testdox It should be possible to use a static method as short syntax for defining a String based binding. */ + public function testAsString(): void + { + $binding = Binding::asString('String'); + $this->assertEquals('String', $binding->getValue()); + $this->assertEquals(Binding::STRING, $binding->getType()); + } + + /** @testdox It should be possible to use a static method as short syntax for defining a Float based binding. */ + public function testAsFloat(): void + { + $binding = Binding::asFloat('Float'); + $this->assertEquals('Float', $binding->getValue()); + $this->assertEquals(Binding::FLOAT, $binding->getType()); + } + + /** @testdox It should be possible to use a static method as short syntax for defining a Int based binding. */ + public function testAsInt(): void + { + $binding = Binding::asInt('Int'); + $this->assertEquals('Int', $binding->getValue()); + $this->assertEquals(Binding::INT, $binding->getType()); + } + + /** @testdox It should be possible to use a static method as short syntax for defining a Bool based binding. */ + public function testAsBool(): void + { + $binding = Binding::asBool('Bool'); + $this->assertEquals('Bool', $binding->getValue()); + $this->assertEquals(Binding::BOOL, $binding->getType()); + } + + /** @testdox It should be possible to use a static method as short syntax for defining a JSON based binding. */ + public function testAsJSON(): void + { + $binding = Binding::asJSON('JSON'); + $this->assertEquals('JSON', $binding->getValue()); + $this->assertEquals(Binding::JSON, $binding->getType()); + } + + /** @testdox It should be possible to use a static method as short syntax for defining a Raw based binding. */ + public function testAsRaw(): void + { + $binding = Binding::asRAW('Raw'); + $this->assertEquals('Raw', $binding->getValue()); + $this->assertEquals(Binding::RAW, $binding->getType()); + } +} diff --git a/tests/TestQueryBuilderSQLGeneration.php b/tests/TestQueryBuilderSQLGeneration.php index d8bf073..bf5fdfb 100644 --- a/tests/TestQueryBuilderSQLGeneration.php +++ b/tests/TestQueryBuilderSQLGeneration.php @@ -11,6 +11,7 @@ namespace Pixie\Tests; +use Pixie\Binding; use WP_UnitTestCase; use Pixie\Connection; use Pixie\QueryBuilder\Raw; @@ -23,7 +24,7 @@ class TestQueryBuilderSQLGeneration extends WP_UnitTestCase /** Mocked WPDB instance. * @var Logable_WPDB - */ + */ private $wpdb; public function setUp(): void @@ -159,9 +160,9 @@ public function testSelectCount(): void $this->assertEquals("SELECT COUNT(*) AS field FROM (SELECT * FROM foo WHERE key = 'value') as count LIMIT 1", $log['query']); } - ################################################ - ## WHERE CONDITIONS ## - ################################################ + ################################################ + ## WHERE CONDITIONS ## + ################################################ /** @testdox It should be possible to create a query which uses Where and Where not (using AND condition) */ @@ -344,9 +345,9 @@ public function testWhereAssumedEqualsOperator(): void $this->assertEquals("SELECT * FROM foo WHERE key = 'value' OR NOT key2 = 'value2'", $orWhereNot->getQuery()->getRawSql()); } - ################################################ - ## GROUP, ORDER BY, LIMIT/OFFSET & HAVING ## - ################################################ + ################################################ + ## GROUP, ORDER BY, LIMIT/OFFSET & HAVING ## + ################################################ /** @testdox It should be possible to create a grouped where condition */ public function testGroupedWhere(): void @@ -449,9 +450,9 @@ public function testOffset() $this->assertEquals("SELECT * FROM foo OFFSET 12", $builderOffset->getQuery()->getRawSql()); } - ################################################# - ## JOIN {INNER, LEFT, RIGHT, FULL OUTER} ## - ################################################# + ################################################# + ## JOIN {INNER, LEFT, RIGHT, FULL OUTER} ## + ################################################# /** @testdox It should be possible to create a query using (INNER) join for a relationship */ public function testJoin(): void @@ -543,9 +544,9 @@ public function testMultipleJoinOrViaClosure() $this->assertEquals("SELECT * FROM prefix_foo INNER JOIN prefix_bar ON prefix_bar.id != prefix_foo.id OR prefix_bar.baz != prefix_foo.baz", $builder->getQuery()->getRawSql()); } - ################################################# - ## SUB AND RAW QUERIES ## - ################################################# + ################################################# + ## SUB AND RAW QUERIES ## + ################################################# /** @testdox It should be possible to create a raw query which can be executed with or without binding values. */ public function testRawQuery(): void @@ -609,9 +610,9 @@ public function testNestedQueryWithRawExpressions(): void } - ################################################# - ## INSERT & UPDATE ## - ################################################# + ################################################# + ## INSERT & UPDATE ## + ################################################# /** @testdox It should be possible to insert a single row of data and get the row id/key returned. */ public function testInsertSingle(): void @@ -647,7 +648,7 @@ public function testInsertMultiple(): void ->table('foo') ->insert($data); - $this->assertEquals([7,7,7], $newIDs); // Will always return 7 as mocked. + $this->assertEquals([7, 7, 7], $newIDs); // Will always return 7 as mocked. // Check the actual queries. $this->assertEquals("INSERT INTO foo (name,description) VALUES ('Sana','Blah')", $this->wpdb->usage_log['get_results'][0]['query']); @@ -672,7 +673,7 @@ public function testInsertIgnore(): void ->table('foo') ->insertIgnore($data); - $this->assertEquals([89,89,89], $newIDs); + $this->assertEquals([89, 89, 89], $newIDs); // Check the actual queries. $this->assertEquals("INSERT IGNORE INTO foo (name,description) VALUES ('Sana','Blah')", $this->wpdb->usage_log['get_results'][0]['query']); @@ -797,11 +798,22 @@ public function testUseRawValueForUnescapedMysqlConstants(): void $this->assertEquals("UPDATE foo SET bar=TIMESTAMP", $this->wpdb->usage_log['get_results'][0]['query']); $this->queryBuilderProvider()->table('orders') - ->select(['Order_ID', 'Product_Name', new Raw("DATE_FORMAT(Order_Date,'%d--%m--%y') as new_date_formate") ]) + ->select(['Order_ID', 'Product_Name', new Raw("DATE_FORMAT(Order_Date,'%d--%m--%y') as new_date_formate")]) ->get(); $this->assertEquals( "SELECT Order_ID, Product_Name, DATE_FORMAT(Order_Date,'%d--%m--%y') as new_date_formate FROM orders", $this->wpdb->usage_log['get_results'][1]['query'] ); } + + /** USING BINDING OBJECT */ + + public function testUsingBindingOnWhere(): void + { + $builderWhere = $this->queryBuilderProvider() + ->table('foo') + ->where('key', '=', Binding::asString('value')) + ->where('key2', '=', 'value2')->get(); + // $this->assertEquals("SELECT * FROM foo WHERE key = 'value' AND key2 = 'value2'", $builderWhere->getQuery()->getRawSql()); + } } From 73ceea82eb164b933e6db4faf993f301221b816e Mon Sep 17 00:00:00 2001 From: Glynn Quelch Date: Tue, 18 Jan 2022 00:39:25 +0000 Subject: [PATCH 28/69] Bindings works with Update, Insert and Where --- src/Binding.php | 18 +++- src/QueryBuilder/QueryBuilderHandler.php | 5 +- src/QueryBuilder/WPDBAdapter.php | 121 ++++++++++++++++------- tests/TestBinding.php | 90 +++++++++++++++++ tests/TestQueryBuilderSQLGeneration.php | 9 -- 5 files changed, 194 insertions(+), 49 deletions(-) diff --git a/src/Binding.php b/src/Binding.php index cfe8377..c4cc498 100644 --- a/src/Binding.php +++ b/src/Binding.php @@ -36,6 +36,10 @@ class Binding */ protected $isRaw = false; + /** + * @param mixed $value + * @param string|null $type + */ public function __construct($value, ?string $type = null) { $this->verifyType($type); @@ -152,10 +156,20 @@ public function getValue() /** * Gets the types format Conversion Specifier * - * @return string + * @return string|null */ - public function getType(): string + public function getType(): ?string { return $this->type; } + + /** + * Get denotes if the field is a RAW value + * + * @return bool + */ + public function isRaw(): bool + { + return $this->isRaw; + } } diff --git a/src/QueryBuilder/QueryBuilderHandler.php b/src/QueryBuilder/QueryBuilderHandler.php index a451f13..ece96f1 100644 --- a/src/QueryBuilder/QueryBuilderHandler.php +++ b/src/QueryBuilder/QueryBuilderHandler.php @@ -7,7 +7,9 @@ use Throwable; use Pixie\Exception; use Pixie\Connection; + use function mb_strlen; + use Pixie\QueryBuilder\Raw; use Pixie\Hydration\Hydrator; use Pixie\QueryBuilder\JoinBuilder; @@ -183,7 +185,6 @@ public function query($sql, $bindings = []): self */ public function statement(string $sql, $bindings = []): array { - dump($sql); $start = microtime(true); $sqlStatement = empty($bindings) ? $sql : $this->dbInstance->prepare($sql, $bindings); @@ -547,7 +548,6 @@ public function update($data) if (!is_null($eventResult)) { return $eventResult; } - $queryObject = $this->getQuery('update', $data); list($preparedQuery, $executionTime) = $this->statement($queryObject->getSql(), $queryObject->getBindings()); @@ -1188,7 +1188,6 @@ protected function whereHandler($key, $operator = null, $value = null, $joiner = { $key = $this->addTablePrefix($key); $this->statements['wheres'][] = compact('key', 'operator', 'value', 'joiner'); - return $this; } diff --git a/src/QueryBuilder/WPDBAdapter.php b/src/QueryBuilder/WPDBAdapter.php index ae2154b..dcc1c36 100644 --- a/src/QueryBuilder/WPDBAdapter.php +++ b/src/QueryBuilder/WPDBAdapter.php @@ -3,10 +3,15 @@ namespace Pixie\QueryBuilder; use Closure; +use Pixie\Binding; use Pixie\Exception; + use function is_bool; + use Pixie\Connection; + use function is_float; + use Pixie\QueryBuilder\Raw; use Pixie\QueryBuilder\NestedCriteria; @@ -91,17 +96,17 @@ public function select(array $statements): array /** @var string[] */ $sqlArray = [ - 'SELECT' . (isset($statements['distinct']) ? ' DISTINCT' : ''), - $selects, - 'FROM', - $tables, - $joinString, - $whereCriteria, - $groupBys, - $havingCriteria, - $orderBys, - $limit, - $offset, + 'SELECT' . (isset($statements['distinct']) ? ' DISTINCT' : ''), + $selects, + 'FROM', + $tables, + $joinString, + $whereCriteria, + $groupBys, + $havingCriteria, + $orderBys, + $limit, + $offset, ]; $sql = $this->concatenateQuery($sqlArray); @@ -157,8 +162,19 @@ private function doInsert(array $statements, array $data, string $type): array foreach ($data as $key => $value) { $keys[] = $key; + + // Handle value as bindings + $isBindings = $value instanceof Binding; + // If this is a raw binding, extract the Raw and replace value. + if ($isBindings && $value->isRaw()) { + $value = $value->getValue(); + } + if ($value instanceof Raw) { $values[] = (string) $value; + } elseif ($isBindings) { + $values[] = $value->getType(); + $bindings[] = $value->getValue(); } else { $values[] = $this->inferType($value); $bindings[] = $value; @@ -166,11 +182,11 @@ private function doInsert(array $statements, array $data, string $type): array } $sqlArray = [ - $type . ' INTO', - $this->wrapSanitizer($table), - '(' . $this->arrayStr($keys, ',') . ')', - 'VALUES', - '(' . $this->arrayStr($values, ',') . ')', + $type . ' INTO', + $this->wrapSanitizer($table), + '(' . $this->arrayStr($keys, ',') . ')', + 'VALUES', + '(' . $this->arrayStr($values, ',') . ')', ]; if (isset($statements['onduplicate'])) { @@ -279,8 +295,17 @@ private function getUpdateStatement(array $data): array $statement = ''; foreach ($data as $key => $value) { + $isBindings = $value instanceof Binding; + // If this is a raw binding, extract the Raw and replace value. + if ($isBindings && $value->isRaw()) { + $value = $value->getValue(); + } + if ($value instanceof Raw) { $statement .= $this->stringifyValue($this->wrapSanitizer($key)) . '=' . $value . ','; + } elseif ($isBindings) { + $statement .= $this->stringifyValue($this->wrapSanitizer($key)) . sprintf('=%s,', $value->getType()); + $bindings[] = $value->getValue(); } else { $statement .= $this->stringifyValue($this->wrapSanitizer($key)) . sprintf('=%s,', $this->inferType($value)); $bindings[] = $value; @@ -322,11 +347,11 @@ public function update($statements, array $data) $limit = isset($statements['limit']) ? 'LIMIT ' . $statements['limit'] : ''; $sqlArray = [ - 'UPDATE', - $this->wrapSanitizer($table), - 'SET ' . $updateStatement, - $whereCriteria, - $limit, + 'UPDATE', + $this->wrapSanitizer($table), + 'SET ' . $updateStatement, + $whereCriteria, + $limit, ]; $sql = $this->concatenateQuery($this->stringifyValues($sqlArray)); @@ -411,6 +436,20 @@ protected function concatenateQuery(array $pieces): string return trim($str); } + /** + * Gets the type of a value + * + * @param mixed $value + * @return string + */ + public function getType($value): string + { + if ($value instanceof Binding) { + return '%G'; + } + return $this->inferType($value); + } + /** * Build generic criteria string and bindings from statements, like "a = b and c = ?" * @@ -426,7 +465,6 @@ protected function buildCriteria(array $statements, bool $bindValues = true): ar foreach ($statements as $statement) { $key = $statement['key']; $value = $statement['value']; - if (is_null($value) && $key instanceof Closure) { // We have a closure, a nested criteria @@ -452,8 +490,8 @@ protected function buildCriteria(array $statements, bool $bindValues = true): ar $bindings = array_merge($bindings, $statement['value']); $criteria .= sprintf( ' %s AND %s ', - $this->inferType($statement['value'][0]), - $this->inferType($statement['value'][1]) + $this->getType($statement['value'][0]), + $this->getType($statement['value'][1]) ); break; default: @@ -484,10 +522,23 @@ protected function buildCriteria(array $statements, bool $bindValues = true): ar $bindings = array_merge($bindings, $statement['key']->getBindings()); } else { // For wheres - $valuePlaceholder = $this->inferType($value); - $bindings[] = $value; + // CHECK HERE IF BINDING THEN USE OBJECTS VALS + // If we have a binding, either get the type and value + if ($value instanceof Binding) { + // If returns a raw, treat as a raw and skip. + if ($value->getValue() instanceof Raw) { + $criteria .= "{$statement['joiner']} {$key} {$statement['operator']} {$value->getValue()} "; + continue; + } + $valuePlaceholder = $value->getType(); + $bindings[] = $value->getValue(); + } else { + $valuePlaceholder = $this->inferType($value); + $bindings[] = $value; + } + $criteria .= $statement['joiner'] . ' ' . $key . ' ' . $statement['operator'] . ' ' - . $valuePlaceholder . ' '; + . $valuePlaceholder . ' '; } } } @@ -598,19 +649,19 @@ protected function buildJoin(array $statements): string $table = $mainTable . ' AS ' . $aliasTable; } else { $table = $joinArr['table'] instanceof Raw ? - (string) $joinArr['table'] : - $this->wrapSanitizer($joinArr['table']); + (string) $joinArr['table'] : + $this->wrapSanitizer($joinArr['table']); } $joinBuilder = $joinArr['joinBuilder']; /** @var string[] */ $sqlArr = [ - $sql, - strtoupper($joinArr['type']), - 'JOIN', - $table, - 'ON', - $joinBuilder->getQuery('criteriaOnly', false)->getSql(), + $sql, + strtoupper($joinArr['type']), + 'JOIN', + $table, + 'ON', + $joinBuilder->getQuery('criteriaOnly', false)->getSql(), ]; $sql = $this->concatenateQuery($sqlArr); diff --git a/tests/TestBinding.php b/tests/TestBinding.php index 6bb5bc6..761c6ce 100644 --- a/tests/TestBinding.php +++ b/tests/TestBinding.php @@ -14,11 +14,37 @@ use Pixie\Binding; use Pixie\Exception; use WP_UnitTestCase; +use Pixie\Connection; use Pixie\QueryBuilder\Raw; +use Pixie\QueryBuilder\QueryBuilderHandler; class TestBinding extends WP_UnitTestCase { + /** Mocked WPDB instance. + * @var Logable_WPDB + */ + private $wpdb; + + public function setUp(): void + { + $this->wpdb = new Logable_WPDB(); + parent::setUp(); + } + + /** + * Generates a query builder helper. + * + * @param string|null $prefix + * @return \Pixie\QueryBuilder\QueryBuilderHandler + */ + public function queryBuilderProvider(?string $prefix = null, ?string $alias = null): QueryBuilderHandler + { + $config = $prefix ? ['prefix' => $prefix] : []; + $connection = new Connection($this->wpdb, $config, $alias); + return new QueryBuilderHandler($connection); + } + /** @testdox It should be possible to create a bindings using the Value and its Type. */ public function testCanCreateValidBinding() { @@ -127,4 +153,68 @@ public function testAsRaw(): void $this->assertEquals('Raw', $binding->getValue()); $this->assertEquals(Binding::RAW, $binding->getType()); } + + /** USING BINDING OBJECT */ + + /** @testdox It should be possible to define both the value and its expected type, when creating a query using a Binding object. */ + public function testUsingBindingOnWhere(): void + { + $this->queryBuilderProvider() + ->table('foo') + ->where('raw', '=', Binding::asRaw("'value'")) + ->where('string', '=', Binding::asString('value')) + ->where('int', '=', Binding::asInt(7)) + ->where('bool', '=', Binding::asBool(7 === 8)) + ->where('float', '=', Binding::asFloat(3.14)) + ->where('json', '>', '["something"]') + ->get(); + + $queryWithPlaceholders = $this->wpdb->usage_log['prepare'][0]['query']; + + $this->assertStringContainsString("raw = 'value'", $queryWithPlaceholders); + $this->assertStringContainsString("string = %s", $queryWithPlaceholders); + $this->assertStringContainsString("int = %d", $queryWithPlaceholders); + $this->assertStringContainsString("bool = %d", $queryWithPlaceholders); + $this->assertStringContainsString("float = %f", $queryWithPlaceholders); + $this->assertStringContainsString("json > %s", $queryWithPlaceholders); + } + + /** @testdox It should be possible to create an update query with the use of binding objects for the value to define the format, regardless of value type. */ + public function testUsingBindingOnUpdate(): void + { + $this->queryBuilderProvider() + ->table('foo') + ->update([ + 'string' => Binding::asString('some string value'), + 'int' => Binding::asInt('7'), + 'float' => Binding::asFloat((1 / 3)), + 'bool' => Binding::asBool('1'), + 'raw' => Binding::asRaw("'WILD STRING'"), + ]); + + $queryWithPlaceholders = $this->wpdb->usage_log['prepare'][0]['query']; + $this->assertCount(4, $this->wpdb->usage_log['prepare'][0]['args']); + $this->assertStringContainsString("raw='WILD STRING'", $queryWithPlaceholders); + $this->assertStringContainsString("string=%s", $queryWithPlaceholders); + $this->assertStringContainsString("int=%d", $queryWithPlaceholders); + $this->assertStringContainsString("float=%f", $queryWithPlaceholders); + $this->assertStringContainsString("bool=%d", $queryWithPlaceholders); + } + + /** @testdox It should be possible to create an insert query with the use of binding objects for the value to define the format, regardless of value type. */ + public function testInertBindingOnUpdate(): void + { + $this->queryBuilderProvider() + ->table('foo') + ->insert([ + 'string' => Binding::asString('some string value'), + 'int' => Binding::asInt('7'), + 'float' => Binding::asFloat((1 / 3)), + 'bool' => Binding::asBool('1'), + 'raw' => Binding::asRaw("'WILD STRING'"), + ]); + + $queryWithPlaceholders = $this->wpdb->usage_log['prepare'][0]['query']; + $this->assertEquals("INSERT INTO foo (string,int,float,bool,raw) VALUES (%s,%d,%f,%d,'WILD STRING')", $queryWithPlaceholders); + } } diff --git a/tests/TestQueryBuilderSQLGeneration.php b/tests/TestQueryBuilderSQLGeneration.php index bf5fdfb..52997bf 100644 --- a/tests/TestQueryBuilderSQLGeneration.php +++ b/tests/TestQueryBuilderSQLGeneration.php @@ -806,14 +806,5 @@ public function testUseRawValueForUnescapedMysqlConstants(): void ); } - /** USING BINDING OBJECT */ - public function testUsingBindingOnWhere(): void - { - $builderWhere = $this->queryBuilderProvider() - ->table('foo') - ->where('key', '=', Binding::asString('value')) - ->where('key2', '=', 'value2')->get(); - // $this->assertEquals("SELECT * FROM foo WHERE key = 'value' AND key2 = 'value2'", $builderWhere->getQuery()->getRawSql()); - } } From 6f4a4de52ff3afefa1d5e8bdc44cb38aafc64c59 Mon Sep 17 00:00:00 2001 From: Glynn Quelch Date: Tue, 18 Jan 2022 00:40:13 +0000 Subject: [PATCH 29/69] Run CS Fixer --- src/Binding.php | 1 - src/QueryBuilder/QueryBuilderHandler.php | 4 ++-- src/QueryBuilder/WPDBAdapter.php | 10 +++++----- 3 files changed, 7 insertions(+), 8 deletions(-) diff --git a/src/Binding.php b/src/Binding.php index c4cc498..ecc3c22 100644 --- a/src/Binding.php +++ b/src/Binding.php @@ -7,7 +7,6 @@ class Binding { - public const STRING = '%s'; public const BOOL = '%d'; public const INT = '%d'; diff --git a/src/QueryBuilder/QueryBuilderHandler.php b/src/QueryBuilder/QueryBuilderHandler.php index ece96f1..7f49714 100644 --- a/src/QueryBuilder/QueryBuilderHandler.php +++ b/src/QueryBuilder/QueryBuilderHandler.php @@ -8,14 +8,14 @@ use Pixie\Exception; use Pixie\Connection; -use function mb_strlen; - use Pixie\QueryBuilder\Raw; + use Pixie\Hydration\Hydrator; use Pixie\QueryBuilder\JoinBuilder; use Pixie\QueryBuilder\QueryObject; use Pixie\QueryBuilder\Transaction; use Pixie\QueryBuilder\WPDBAdapter; +use function mb_strlen; class QueryBuilderHandler { diff --git a/src/QueryBuilder/WPDBAdapter.php b/src/QueryBuilder/WPDBAdapter.php index dcc1c36..789deed 100644 --- a/src/QueryBuilder/WPDBAdapter.php +++ b/src/QueryBuilder/WPDBAdapter.php @@ -6,15 +6,15 @@ use Pixie\Binding; use Pixie\Exception; -use function is_bool; - use Pixie\Connection; -use function is_float; - use Pixie\QueryBuilder\Raw; + use Pixie\QueryBuilder\NestedCriteria; +use function is_bool; +use function is_float; + class WPDBAdapter { /** @@ -165,7 +165,7 @@ private function doInsert(array $statements, array $data, string $type): array // Handle value as bindings $isBindings = $value instanceof Binding; - // If this is a raw binding, extract the Raw and replace value. + // If this is a raw binding, extract the Raw and replace value. if ($isBindings && $value->isRaw()) { $value = $value->getValue(); } From c285c61390b5c4231b5fc54838ee4e807d717ce2 Mon Sep 17 00:00:00 2001 From: Glynn Quelch Date: Tue, 18 Jan 2022 11:15:52 +0000 Subject: [PATCH 30/69] Extended Raw so its query can be run through WPDB::prepare() if its used as a value and not just merged into the main query. Have test on insert queries --- src/QueryBuilder/QueryBuilderHandler.php | 19 +++++- src/QueryBuilder/Raw.php | 12 +++- src/QueryBuilder/WPDBAdapter.php | 76 ++++++++++++++++++++---- tests/TestBinding.php | 20 ++++++- tests/TestQueryBuilderWPDBPrepare.php | 33 ++++++++++ 5 files changed, 142 insertions(+), 18 deletions(-) diff --git a/src/QueryBuilder/QueryBuilderHandler.php b/src/QueryBuilder/QueryBuilderHandler.php index 7f49714..8531495 100644 --- a/src/QueryBuilder/QueryBuilderHandler.php +++ b/src/QueryBuilder/QueryBuilderHandler.php @@ -5,17 +5,18 @@ use wpdb; use Closure; use Throwable; +use Pixie\Binding; use Pixie\Exception; use Pixie\Connection; -use Pixie\QueryBuilder\Raw; +use function mb_strlen; +use Pixie\QueryBuilder\Raw; use Pixie\Hydration\Hydrator; use Pixie\QueryBuilder\JoinBuilder; use Pixie\QueryBuilder\QueryObject; use Pixie\QueryBuilder\Transaction; use Pixie\QueryBuilder\WPDBAdapter; -use function mb_strlen; class QueryBuilderHandler { @@ -164,6 +165,18 @@ protected function constructCurrentBuilderClass(Connection $connection): self return new static($connection); } + /** + * Interpolates a query + * + * @param string $query + * @param array $bindings + * @return string + */ + public function interpolateQuery(string $query, array $bindings = []): string + { + return $this->adapterInstance->interpolateQuery($query, $bindings); + } + /** * @param string $sql * @param array $bindings @@ -186,7 +199,7 @@ public function query($sql, $bindings = []): self public function statement(string $sql, $bindings = []): array { $start = microtime(true); - $sqlStatement = empty($bindings) ? $sql : $this->dbInstance->prepare($sql, $bindings); + $sqlStatement = empty($bindings) ? $sql : $this->interpolateQuery($sql, $bindings); if (!is_string($sqlStatement)) { throw new Exception('Could not interpolate query', 1); diff --git a/src/QueryBuilder/Raw.php b/src/QueryBuilder/Raw.php index bef8efd..8974e10 100644 --- a/src/QueryBuilder/Raw.php +++ b/src/QueryBuilder/Raw.php @@ -15,7 +15,7 @@ class Raw protected $bindings; /** - * @param string|Raw $value + * @param string $value * @param mixed|mixed[] $bindings */ public function __construct($value, $bindings = []) @@ -34,6 +34,16 @@ public function getBindings(): array return $this->bindings; } + /** + * Returns the current value held. + * + * @return string + */ + public function getValue(): string + { + return (string) $this->value; + } + /** * @return string */ diff --git a/src/QueryBuilder/WPDBAdapter.php b/src/QueryBuilder/WPDBAdapter.php index 789deed..ae6cb58 100644 --- a/src/QueryBuilder/WPDBAdapter.php +++ b/src/QueryBuilder/WPDBAdapter.php @@ -6,15 +6,15 @@ use Pixie\Binding; use Pixie\Exception; +use function is_bool; + use Pixie\Connection; -use Pixie\QueryBuilder\Raw; +use function is_float; +use Pixie\QueryBuilder\Raw; use Pixie\QueryBuilder\NestedCriteria; -use function is_bool; -use function is_float; - class WPDBAdapter { /** @@ -171,7 +171,7 @@ private function doInsert(array $statements, array $data, string $type): array } if ($value instanceof Raw) { - $values[] = (string) $value; + $values[] = $this->parseRaw($value); } elseif ($isBindings) { $values[] = $value->getType(); $bindings[] = $value->getValue(); @@ -437,17 +437,59 @@ protected function concatenateQuery(array $pieces): string } /** - * Gets the type of a value + * Gets the type of a value, either from a binding or infered * * @param mixed $value * @return string */ public function getType($value): string { - if ($value instanceof Binding) { - return '%G'; + return $value instanceof Binding && $value->getType() !== null + ? $value->getType() : $this->inferType($value) ; + } + + /** + * Get the value from a possible Bindings object. + * + * @param mixed $value + * @return mixed + */ + public function getValue($value) + { + return $value instanceof Binding ? $value->getValue() : $value; + } + + /** + * Attempts to parse a raw query, if bindings are defined then they will be bound first. + * + * @param Raw $raw + * @requires string + */ + public function parseRaw(Raw $raw): string + { + $bindings = $raw->getBindings(); + return 0 === count($bindings) + ? (string) $raw + : $this->interpolateQuery($raw->getValue(), $bindings); + } + + /** + * Interpolates a query + * + * @param string $query + * @param array $bindings + * @return string + */ + public function interpolateQuery(string $query, array $bindings = []): string + { + if (0 === count($bindings)) { + return $query; } - return $this->inferType($value); + + + $bindings = array_map([$this, 'getValue'], $bindings); + $query = $this->connection->getDbInstance()->prepare($query, $bindings) ; + return is_string($query) ? $query : ''; } /** @@ -465,6 +507,13 @@ protected function buildCriteria(array $statements, bool $bindValues = true): ar foreach ($statements as $statement) { $key = $statement['key']; $value = $statement['value']; + + // If the value is a Raw Binding, cast to raw + if ($value instanceof Binding && Binding::RAW === $value->getType()) { + /** @var Raw */ + $value = $value->getValue(); + } + if (is_null($value) && $key instanceof Closure) { // We have a closure, a nested criteria @@ -490,9 +539,13 @@ protected function buildCriteria(array $statements, bool $bindValues = true): ar $bindings = array_merge($bindings, $statement['value']); $criteria .= sprintf( ' %s AND %s ', - $this->getType($statement['value'][0]), - $this->getType($statement['value'][1]) + $this->getType($value[0]), + $this->getType($value[1]) ); + + // Maybe cast the values bindings. + $value[0] = $this->getValue($value[0]); + $value[1] = $this->getValue($value[1]); break; default: $valuePlaceholder = ''; @@ -507,6 +560,7 @@ protected function buildCriteria(array $statements, bool $bindValues = true): ar break; } } elseif ($value instanceof Raw) { + $value = $this->parseRaw($value); $criteria .= "{$statement['joiner']} {$key} {$statement['operator']} $value "; } else { // Usual where like criteria diff --git a/tests/TestBinding.php b/tests/TestBinding.php index 761c6ce..1d84db3 100644 --- a/tests/TestBinding.php +++ b/tests/TestBinding.php @@ -202,7 +202,7 @@ public function testUsingBindingOnUpdate(): void } /** @testdox It should be possible to create an insert query with the use of binding objects for the value to define the format, regardless of value type. */ - public function testInertBindingOnUpdate(): void + public function testUsingBindingOnInsert(): void { $this->queryBuilderProvider() ->table('foo') @@ -212,9 +212,23 @@ public function testInertBindingOnUpdate(): void 'float' => Binding::asFloat((1 / 3)), 'bool' => Binding::asBool('1'), 'raw' => Binding::asRaw("'WILD STRING'"), + 'rawNative' => new Raw('[%d]', 5), ]); - $queryWithPlaceholders = $this->wpdb->usage_log['prepare'][0]['query']; - $this->assertEquals("INSERT INTO foo (string,int,float,bool,raw) VALUES (%s,%d,%f,%d,'WILD STRING')", $queryWithPlaceholders); + $queryWithPlaceholders = $this->wpdb->usage_log['prepare'][1]['query']; + $this->assertEquals("INSERT INTO foo (string,int,float,bool,raw,rawNative) VALUES (%s,%d,%f,%d,'WILD STRING',[5])", $queryWithPlaceholders); } + + /** @testdox It should be possible to use binding values on a BETWEEN query */ + public function testUsingBindingsOnBetweenCondition(): void + { + $this->queryBuilderProvider() + ->table('foo') + ->whereBetween('bar', Binding::asInt('7'), Binding::asFloat((1 / 3))) + ->get(); + + $queryWithPlaceholders = $this->wpdb->usage_log['prepare'][0]['query']; + $this->assertEquals("SELECT * FROM foo WHERE bar BETWEEN %d AND %f", $queryWithPlaceholders); + } + } diff --git a/tests/TestQueryBuilderWPDBPrepare.php b/tests/TestQueryBuilderWPDBPrepare.php index 619702d..acc2e2c 100644 --- a/tests/TestQueryBuilderWPDBPrepare.php +++ b/tests/TestQueryBuilderWPDBPrepare.php @@ -12,6 +12,7 @@ namespace Pixie\Tests; use Pixie\Connection; +use Pixie\QueryBuilder\Raw; use Pixie\Tests\Logable_WPDB; use PHPUnit\Framework\TestCase; use Pixie\QueryBuilder\QueryBuilderHandler; @@ -208,4 +209,36 @@ public function testInsertOnDuplicateKey(): void ); } + /** @testdox It should be possible to use a RAW query and have any places holders replace with prepare, before being added to the main query. */ + public function testUsingRawWithBindingsAsAValueForInsert(): void + { + $data = array( + 'name' => 'Trees', + 'something' => new Raw('(%s)', 'something') + ); + + $this->queryBuilderProvider() + ->table('foo') + ->insert($data); + + // First should be RAW bring resolved. + $this->assertEquals( + '(%s)', + $this->wpdb->usage_log['prepare'][0]['query'] + ); + $this->assertContains( + 'something', + $this->wpdb->usage_log['prepare'][0]['args'] + ); + + // Second the actual query. + $this->assertEquals( + "INSERT INTO foo (name,something) VALUES (%s,('something'))", + $this->wpdb->usage_log['prepare'][1]['query'] + ); + $this->assertContains( + 'Trees', + $this->wpdb->usage_log['prepare'][1]['args'] + ); + } } From 63329a2dd41dc8a66df71df10317b286a7ca378b Mon Sep 17 00:00:00 2001 From: Glynn Quelch Date: Wed, 19 Jan 2022 00:42:39 +0000 Subject: [PATCH 31/69] Bindings and Raw values now working in all functionality where its dicated by the type hints --- src/QueryBuilder/QueryBuilderHandler.php | 4 ++ src/QueryBuilder/WPDBAdapter.php | 88 +++++++++++------------- tests/TestQueryBuilderSQLGeneration.php | 31 +++++++++ 3 files changed, 76 insertions(+), 47 deletions(-) diff --git a/src/QueryBuilder/QueryBuilderHandler.php b/src/QueryBuilder/QueryBuilderHandler.php index 8531495..6e903c6 100644 --- a/src/QueryBuilder/QueryBuilderHandler.php +++ b/src/QueryBuilder/QueryBuilderHandler.php @@ -961,6 +961,10 @@ protected function whereNullHandler($key, string $prefix = '', $operator = ''): { $prefix = 0 === mb_strlen($prefix) ? '' : " {$prefix}"; + if ($key instanceof Raw) { + $key = $this->adapterInstance->parseRaw($key); + } + $key = $this->adapterInstance->wrapSanitizer($this->addTablePrefix($key)); if ($key instanceof Closure) { throw new Exception('Key used for whereNull condition must be a string or raw exrpession.', 1); diff --git a/src/QueryBuilder/WPDBAdapter.php b/src/QueryBuilder/WPDBAdapter.php index ae6cb58..52ec81d 100644 --- a/src/QueryBuilder/WPDBAdapter.php +++ b/src/QueryBuilder/WPDBAdapter.php @@ -96,17 +96,17 @@ public function select(array $statements): array /** @var string[] */ $sqlArray = [ - 'SELECT' . (isset($statements['distinct']) ? ' DISTINCT' : ''), - $selects, - 'FROM', - $tables, - $joinString, - $whereCriteria, - $groupBys, - $havingCriteria, - $orderBys, - $limit, - $offset, + 'SELECT' . (isset($statements['distinct']) ? ' DISTINCT' : ''), + $selects, + 'FROM', + $tables, + $joinString, + $whereCriteria, + $groupBys, + $havingCriteria, + $orderBys, + $limit, + $offset, ]; $sql = $this->concatenateQuery($sqlArray); @@ -231,7 +231,7 @@ protected function stringifyValue($value): ?string } if ($value instanceof Raw) { - return (string) $value; + return $this->parseRaw($value); } return $value; @@ -346,12 +346,12 @@ public function update($statements, array $data) // Limit $limit = isset($statements['limit']) ? 'LIMIT ' . $statements['limit'] : ''; - $sqlArray = [ - 'UPDATE', - $this->wrapSanitizer($table), - 'SET ' . $updateStatement, - $whereCriteria, - $limit, + $sqlArray = [ + 'UPDATE', + $this->wrapSanitizer($table), + 'SET ' . $updateStatement, + $whereCriteria, + $limit, ]; $sql = $this->concatenateQuery($this->stringifyValues($sqlArray)); @@ -550,9 +550,18 @@ protected function buildCriteria(array $statements, bool $bindValues = true): ar default: $valuePlaceholder = ''; foreach ($statement['value'] as $subValue) { + // Get its value. + if ($this->getValue($subValue) instanceof Raw) { + /** @var Raw $subValue */ + $subValue = $this->getValue($subValue); + $valuePlaceholder .= sprintf('%s, ', $this->parseRaw($subValue)); + continue; + } + + // Add in format placeholders. - $valuePlaceholder .= sprintf('%s, ', $this->inferType($subValue)); // glynn - $bindings[] = $subValue; + $valuePlaceholder .= sprintf('%s, ', $this->getType($subValue)); // glynn + $bindings[] = $this->getValue($subValue); } $valuePlaceholder = trim($valuePlaceholder, ', '); @@ -564,10 +573,8 @@ protected function buildCriteria(array $statements, bool $bindValues = true): ar $criteria .= "{$statement['joiner']} {$key} {$statement['operator']} $value "; } else { // Usual where like criteria - if (!$bindValues) { // Specially for joins - // We are not binding values, lets sanitize then $value = $this->stringifyValue($this->wrapSanitizer($value)) ?? ''; $criteria .= $statement['joiner'] . ' ' . $key . ' ' . $statement['operator'] . ' ' . $value . ' '; @@ -576,23 +583,10 @@ protected function buildCriteria(array $statements, bool $bindValues = true): ar $bindings = array_merge($bindings, $statement['key']->getBindings()); } else { // For wheres - // CHECK HERE IF BINDING THEN USE OBJECTS VALS - // If we have a binding, either get the type and value - if ($value instanceof Binding) { - // If returns a raw, treat as a raw and skip. - if ($value->getValue() instanceof Raw) { - $criteria .= "{$statement['joiner']} {$key} {$statement['operator']} {$value->getValue()} "; - continue; - } - $valuePlaceholder = $value->getType(); - $bindings[] = $value->getValue(); - } else { - $valuePlaceholder = $this->inferType($value); - $bindings[] = $value; - } + $bindings[] = $this->getValue($value); $criteria .= $statement['joiner'] . ' ' . $key . ' ' . $statement['operator'] . ' ' - . $valuePlaceholder . ' '; + . $this->getType($value) . ' '; } } } @@ -636,7 +630,7 @@ public function wrapSanitizer($value) { // Its a raw query, just cast as string, object has __toString() if ($value instanceof Raw) { - return (string)$value; + return $this->parseRaw($value); } elseif ($value instanceof Closure) { return $value; } @@ -702,20 +696,20 @@ protected function buildJoin(array $statements): string $aliasTable = $this->stringifyValue($this->wrapSanitizer($joinArr['table'][1])); $table = $mainTable . ' AS ' . $aliasTable; } else { - $table = $joinArr['table'] instanceof Raw ? - (string) $joinArr['table'] : - $this->wrapSanitizer($joinArr['table']); + $table = $joinArr['table'] instanceof Raw + ? $this->parseRaw($joinArr['table']) + : $this->wrapSanitizer($joinArr['table']); } $joinBuilder = $joinArr['joinBuilder']; /** @var string[] */ $sqlArr = [ - $sql, - strtoupper($joinArr['type']), - 'JOIN', - $table, - 'ON', - $joinBuilder->getQuery('criteriaOnly', false)->getSql(), + $sql, + strtoupper($joinArr['type']), + 'JOIN', + $table, + 'ON', + $joinBuilder->getQuery('criteriaOnly', false)->getSql(), ]; $sql = $this->concatenateQuery($sqlArr); diff --git a/tests/TestQueryBuilderSQLGeneration.php b/tests/TestQueryBuilderSQLGeneration.php index 52997bf..4b8ce27 100644 --- a/tests/TestQueryBuilderSQLGeneration.php +++ b/tests/TestQueryBuilderSQLGeneration.php @@ -806,5 +806,36 @@ public function testUseRawValueForUnescapedMysqlConstants(): void ); } + /** @testdox It should be possible to use a Binding value in a delete where query */ + public function testDeleteUsingBindings(): void + { + $this->queryBuilderProvider() + ->table('foo') + ->where('id', '>', Binding::asInt(5.112131564)) + ->delete(); + + $prepared = $this->wpdb->usage_log['prepare'][0]; + $query = $this->wpdb->usage_log['get_results'][0]; + + $this->assertEquals('DELETE FROM foo WHERE id > %d', $prepared['query']); + $this->assertEquals('DELETE FROM foo WHERE id > 5', $query['query']); + } + public function testWhereInUsingBindingsAndRawExpressions(): void + { + $builderWhere = $this->queryBuilderProvider() + ->table('foo') + ->whereIn('key', [Binding::asString('v1'), Binding::asRaw("'v2'")]) + ->whereIn('key2', [Binding::asInt(10 / 4), new Raw('%d', 12)]); + $this->assertEquals("SELECT * FROM foo WHERE key IN ('v1', 'v2') AND key2 IN (2, 12)", $builderWhere->getQuery()->getRawSql()); + } + + public function testWhereIsNullUsingRawForColumn(): void + { + $builderNot = $this->queryBuilderProvider() + ->table('foo') + ->whereNotNull(new Raw('key')) + ->whereNotNull('key2'); + $this->assertEquals("SELECT * FROM foo WHERE key IS NOT NULL AND key2 IS NOT NULL", $builderNot->getQuery()->getRawSql()); + } } From 8cb8b6502277f3717cf1bdd9742cf9c6809fe4d8 Mon Sep 17 00:00:00 2001 From: Glynn Quelch Date: Wed, 19 Jan 2022 00:43:04 +0000 Subject: [PATCH 32/69] House Keeping --- src/QueryBuilder/QueryBuilderHandler.php | 4 ++-- src/QueryBuilder/WPDBAdapter.php | 10 +++++----- 2 files changed, 7 insertions(+), 7 deletions(-) diff --git a/src/QueryBuilder/QueryBuilderHandler.php b/src/QueryBuilder/QueryBuilderHandler.php index 6e903c6..2d22c7d 100644 --- a/src/QueryBuilder/QueryBuilderHandler.php +++ b/src/QueryBuilder/QueryBuilderHandler.php @@ -9,14 +9,14 @@ use Pixie\Exception; use Pixie\Connection; -use function mb_strlen; - use Pixie\QueryBuilder\Raw; + use Pixie\Hydration\Hydrator; use Pixie\QueryBuilder\JoinBuilder; use Pixie\QueryBuilder\QueryObject; use Pixie\QueryBuilder\Transaction; use Pixie\QueryBuilder\WPDBAdapter; +use function mb_strlen; class QueryBuilderHandler { diff --git a/src/QueryBuilder/WPDBAdapter.php b/src/QueryBuilder/WPDBAdapter.php index 52ec81d..c16b73e 100644 --- a/src/QueryBuilder/WPDBAdapter.php +++ b/src/QueryBuilder/WPDBAdapter.php @@ -6,15 +6,15 @@ use Pixie\Binding; use Pixie\Exception; -use function is_bool; - use Pixie\Connection; -use function is_float; - use Pixie\QueryBuilder\Raw; + use Pixie\QueryBuilder\NestedCriteria; +use function is_bool; +use function is_float; + class WPDBAdapter { /** @@ -346,7 +346,7 @@ public function update($statements, array $data) // Limit $limit = isset($statements['limit']) ? 'LIMIT ' . $statements['limit'] : ''; - $sqlArray = [ + $sqlArray = [ 'UPDATE', $this->wrapSanitizer($table), 'SET ' . $updateStatement, From f7e5bdd5e3184e0259d9638ecad975c7cbe96e69 Mon Sep 17 00:00:00 2001 From: Glynn Quelch Date: Wed, 19 Jan 2022 01:25:42 +0000 Subject: [PATCH 33/69] Find or fail added, with tests and updated docs --- README.md | 6 +++++- src/QueryBuilder/QueryBuilderHandler.php | 20 ++++++++++++++++++-- tests/TestIntegrationWithWPDB.php | 18 ++++++++++++++++++ tests/TestQueryBuilderSQLGeneration.php | 2 ++ 4 files changed, 43 insertions(+), 3 deletions(-) diff --git a/README.md b/README.md index fe6cc3c..5593cb8 100644 --- a/README.md +++ b/README.md @@ -119,6 +119,7 @@ Library on [Packagist](https://packagist.org/packages/gin0115/pixie-wpdb). - [Multiple Join Criteria](#multiple-join-criteria) - [Raw Query](#raw-query) - [Raw Expressions](#raw-expressions) + - [Value Binding](#bindings) - [**Insert**](#insert) - [Batch Insert](#batch-insert) - [Insert with ON DUPLICATE KEY statement](#insert-with-on-duplicate-key-statement) @@ -191,7 +192,10 @@ The query below returns the all rows where name = 'Sana', null if no rows. ```PHP $result = QB::table('my_table')->findAll('name', 'Sana'); ``` - +The query below will either return the row or throw a `Pixie\Exception` if no result found. +```PHP +$result = QB::table('my_table')->findOrFail('name', 'Sana'); +``` ### Select ```PHP diff --git a/src/QueryBuilder/QueryBuilderHandler.php b/src/QueryBuilder/QueryBuilderHandler.php index 2d22c7d..266b6ee 100644 --- a/src/QueryBuilder/QueryBuilderHandler.php +++ b/src/QueryBuilder/QueryBuilderHandler.php @@ -9,14 +9,14 @@ use Pixie\Exception; use Pixie\Connection; -use Pixie\QueryBuilder\Raw; +use function mb_strlen; +use Pixie\QueryBuilder\Raw; use Pixie\Hydration\Hydrator; use Pixie\QueryBuilder\JoinBuilder; use Pixie\QueryBuilder\QueryObject; use Pixie\QueryBuilder\Transaction; use Pixie\QueryBuilder\WPDBAdapter; -use function mb_strlen; class QueryBuilderHandler { @@ -324,6 +324,22 @@ public function find($value, $fieldName = 'id') return $this->first(); } + /** + * @param string $fieldName + * @param mixed $value + * + * @return \stdClass\array|object Can return any object using hydrator + * @throws Exception If fails to find + */ + public function findOrFail($value, $fieldName = 'id') + { + $result = $this->find($value, $fieldName); + if (null === $result) { + throw new Exception("Failed to find {$fieldName}={$value}", 1); + } + return $result; + } + /** * Used to handle all aggregation method. * diff --git a/tests/TestIntegrationWithWPDB.php b/tests/TestIntegrationWithWPDB.php index ad7265c..cf742d4 100644 --- a/tests/TestIntegrationWithWPDB.php +++ b/tests/TestIntegrationWithWPDB.php @@ -519,4 +519,22 @@ public function testGetReturnTypes(): void $this->assertEquals('defined', $model->constructorProp); // Is set from constructor args passed, is DEFAULT if not defined. } } + + /** @testdox It should be possible to do a find or fail query. An excetpion should be thrown if no result is found. */ + public function testFindOrFail(): void + { + $this->wpdb->insert('mock_foo', ['string' => 'First', 'number' => 1], ['%s', '%d']); + $this->wpdb->insert('mock_foo', ['string' => 'Second', 'number' => 2], ['%s', '%d']); + $this->wpdb->insert('mock_foo', ['string' => 'Third', 'number' => 1], ['%s', '%d']); + + $builder = $this->queryBuilderProvider()->table('mock_foo'); + + $row = $builder->findOrFail('First', 'string'); + $this->assertEquals(1, $row->number); + + $this->expectExceptionMessage('Failed to find string=Forth'); + $this->expectException(Exception::class); + + $row = $builder->findOrFail('Forth', 'string'); + } } diff --git a/tests/TestQueryBuilderSQLGeneration.php b/tests/TestQueryBuilderSQLGeneration.php index 4b8ce27..3cdf991 100644 --- a/tests/TestQueryBuilderSQLGeneration.php +++ b/tests/TestQueryBuilderSQLGeneration.php @@ -821,6 +821,7 @@ public function testDeleteUsingBindings(): void $this->assertEquals('DELETE FROM foo WHERE id > 5', $query['query']); } + /** @testdox It should be possible to use both RAW expressions and Bindings values for doing where in queries. */ public function testWhereInUsingBindingsAndRawExpressions(): void { $builderWhere = $this->queryBuilderProvider() @@ -830,6 +831,7 @@ public function testWhereInUsingBindingsAndRawExpressions(): void $this->assertEquals("SELECT * FROM foo WHERE key IN ('v1', 'v2') AND key2 IN (2, 12)", $builderWhere->getQuery()->getRawSql()); } + /** @testdox It should be possible to use RAW expressions for the key in whereNull conditions. */ public function testWhereIsNullUsingRawForColumn(): void { $builderNot = $this->queryBuilderProvider() From e3718d62414fee99cc098fc2c3f211f69578e331 Mon Sep 17 00:00:00 2001 From: Glynn Quelch Date: Wed, 19 Jan 2022 01:26:16 +0000 Subject: [PATCH 34/69] House Keeping --- src/QueryBuilder/QueryBuilderHandler.php | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/QueryBuilder/QueryBuilderHandler.php b/src/QueryBuilder/QueryBuilderHandler.php index 266b6ee..b60bf91 100644 --- a/src/QueryBuilder/QueryBuilderHandler.php +++ b/src/QueryBuilder/QueryBuilderHandler.php @@ -9,14 +9,14 @@ use Pixie\Exception; use Pixie\Connection; -use function mb_strlen; - use Pixie\QueryBuilder\Raw; + use Pixie\Hydration\Hydrator; use Pixie\QueryBuilder\JoinBuilder; use Pixie\QueryBuilder\QueryObject; use Pixie\QueryBuilder\Transaction; use Pixie\QueryBuilder\WPDBAdapter; +use function mb_strlen; class QueryBuilderHandler { From 8818acdf29615ee60b7f93838840c8aeaf0edc53 Mon Sep 17 00:00:00 2001 From: Glynn Quelch Date: Wed, 19 Jan 2022 01:27:16 +0000 Subject: [PATCH 35/69] House Keeping --- README.md | 34 ++++++++++++++++++++++++++++++---- 1 file changed, 30 insertions(+), 4 deletions(-) diff --git a/README.md b/README.md index 5593cb8..b13d1ca 100644 --- a/README.md +++ b/README.md @@ -194,7 +194,7 @@ $result = QB::table('my_table')->findAll('name', 'Sana'); ``` The query below will either return the row or throw a `Pixie\Exception` if no result found. ```PHP -$result = QB::table('my_table')->findOrFail('name', 'Sana'); +$result = QB::table('my_table')->findOrFail('name', 'Mark'); ``` ### Select @@ -386,15 +386,41 @@ QB::query('select * from cb_my_table where age = ? and name = ?', array(10, 'usm When you wrap an expression with `raw()` method, Pixie doesn't try to sanitize these. ```PHP QB::table('my_table') - ->select(QB::raw('count(cb_my_table.id) as tot')) - ->where('value', '=', 'Ifrah') - ->where(QB::raw('DATE(?)', 'now')) + ->select(QB::raw('count(cb_my_table.id) as tot')) + ->where('value', '=', 'Ifrah') + ->where(QB::raw('DATE(?)', 'now')) ``` ___ **NOTE:** Queries that run through `query()` method are not sanitized until you pass all values through bindings. Queries that run through `raw()` method are not sanitized either, you have to do it yourself. And of course these don't add table prefix too, but you can use the `addTablePrefix()` method. +### Value Binding +As this uses WPDB under the hood, the use of `wpdb::prepare()` is required. To make it easier to define the expected type, you can use Bindings for all values used in most queries. +```php +QB::table('my_table') + ->where('id', = Binding::asInt($valueFromSomewhere)) + ->get() +``` +This will ensure the underlying statement for prepare will be passed as `WHERE id=%d`. Just passing a value, without a binding will see the values type used as the placeholder. If you wish to use values which are passed through `prepare()` please use a `Raw` value. +```php +QB::table('my_table') + ->where('col1', = Binding::asRaw('im a string, dont wrap me with quotes')) + ->where('col2', = new Raw('value')) + ->get() +``` +Neither of the above string would be automatically wrapped in `'single quotes'`. + +**Types** + +`Binding::asString($value)` +`Binding::asInt($value)` +`Binding::asFloat($value)` +`Binding::asBool($value)` +`Binding::asJson($value)` +`Binding::asRaw($value)` + + ### Insert ```PHP $data = array( From 7cc31301b9dec8c6b58691c0caa7fd9972ab1a98 Mon Sep 17 00:00:00 2001 From: Glynn Quelch Date: Wed, 19 Jan 2022 01:44:35 +0000 Subject: [PATCH 36/69] Add extra wpdb instance tables --- tests/TestIntegrationWithWPDB.php | 22 ++++++++++++++++++++++ 1 file changed, 22 insertions(+) diff --git a/tests/TestIntegrationWithWPDB.php b/tests/TestIntegrationWithWPDB.php index ad7265c..6feab30 100644 --- a/tests/TestIntegrationWithWPDB.php +++ b/tests/TestIntegrationWithWPDB.php @@ -61,9 +61,31 @@ public function createTables(): void ) COLLATE {$this->wpdb->collate}"; + $sqlJson = + "CREATE TABLE mock_json ( + id mediumint(8) unsigned NOT NULL auto_increment , + string varchar(255) NULL, + json json NULL, + PRIMARY KEY (id) + ) + COLLATE {$this->wpdb->collate}"; + + $sqlDates = + "CREATE TABLE mock_dates ( + id mediumint(8) unsigned NOT NULL auto_increment , + date DATE NULL, + datetime DATETIME NULL, + unix TIMESTAMP NULL, + time TIME NULL, + PRIMARY KEY (id) + ) + COLLATE {$this->wpdb->collate}"; + require_once(ABSPATH . 'wp-admin/includes/upgrade.php'); dbDelta($sqlFoo); dbDelta($sqlBar); + dbDelta($sqlJson); + dbDelta($sqlDates); static::$createdTables = true; } From d20b9d299ade201caf08472a8a7eedf67a26e0cd Mon Sep 17 00:00:00 2001 From: Glynn Quelch Date: Wed, 19 Jan 2022 09:21:00 +0000 Subject: [PATCH 37/69] where date formats added --- src/QueryBuilder/QueryBuilderHandler.php | 83 +++++++++++++++- src/QueryBuilder/WPDBAdapter.php | 8 +- tests/TestIntegrationWithWPDB.php | 120 +++++++++++++++++++++++ 3 files changed, 205 insertions(+), 6 deletions(-) diff --git a/src/QueryBuilder/QueryBuilderHandler.php b/src/QueryBuilder/QueryBuilderHandler.php index b60bf91..d0d7cce 100644 --- a/src/QueryBuilder/QueryBuilderHandler.php +++ b/src/QueryBuilder/QueryBuilderHandler.php @@ -9,14 +9,14 @@ use Pixie\Exception; use Pixie\Connection; -use Pixie\QueryBuilder\Raw; +use function mb_strlen; +use Pixie\QueryBuilder\Raw; use Pixie\Hydration\Hydrator; use Pixie\QueryBuilder\JoinBuilder; use Pixie\QueryBuilder\QueryObject; use Pixie\QueryBuilder\Transaction; use Pixie\QueryBuilder\WPDBAdapter; -use function mb_strlen; class QueryBuilderHandler { @@ -926,6 +926,85 @@ public function orWhereBetween($key, $valueFrom, $valueTo): self return $this->whereHandler($key, 'BETWEEN', [$valueFrom, $valueTo], 'OR'); } + /** + * Handles all function call based where conditions + * + * @param string|Raw $key + * @param string $function + * @param string|mixed|null $operator Can be used as value, if 3rd arg not passed + * @param mixed|null $value + * @return static + */ + protected function whereFunctionCallHandler($key, $function, $operator, $value): self + { + $key = \sprintf('%s(%s)', $function, $this->addTablePrefix($key)); + return $this->where($key, $operator, $value); + } + + /** + * @param string|Raw $key + * @param string|mixed|null $operator Can be used as value, if 3rd arg not passed + * @param mixed|null $value + * @return self + */ + public function whereMonth($key, $operator = null, $value = null): self + { + // If two params are given then assume operator is = + if (2 == func_num_args()) { + $value = $operator; + $operator = '='; + } + return $this->whereFunctionCallHandler($key, 'MONTH', $operator, $value); + } + + /** + * @param string|Raw $key + * @param string|mixed|null $operator Can be used as value, if 3rd arg not passed + * @param mixed|null $value + * @return self + */ + public function whereDay($key, $operator = null, $value = null): self + { + // If two params are given then assume operator is = + if (2 == func_num_args()) { + $value = $operator; + $operator = '='; + } + return $this->whereFunctionCallHandler($key, 'DAY', $operator, $value); + } + + /** + * @param string|Raw $key + * @param string|mixed|null $operator Can be used as value, if 3rd arg not passed + * @param mixed|null $value + * @return self + */ + public function whereYear($key, $operator = null, $value = null): self + { + // If two params are given then assume operator is = + if (2 == func_num_args()) { + $value = $operator; + $operator = '='; + } + return $this->whereFunctionCallHandler($key, 'YEAR', $operator, $value); + } + + /** + * @param string|Raw $key + * @param string|mixed|null $operator Can be used as value, if 3rd arg not passed + * @param mixed|null $value + * @return self + */ + public function whereDate($key, $operator = null, $value = null): self + { + // If two params are given then assume operator is = + if (2 == func_num_args()) { + $value = $operator; + $operator = '='; + } + return $this->whereFunctionCallHandler($key, 'DATE', $operator, $value); + } + /** * @param string|Raw $key * diff --git a/src/QueryBuilder/WPDBAdapter.php b/src/QueryBuilder/WPDBAdapter.php index c16b73e..9c3b341 100644 --- a/src/QueryBuilder/WPDBAdapter.php +++ b/src/QueryBuilder/WPDBAdapter.php @@ -6,15 +6,15 @@ use Pixie\Binding; use Pixie\Exception; +use function is_bool; + use Pixie\Connection; -use Pixie\QueryBuilder\Raw; +use function is_float; +use Pixie\QueryBuilder\Raw; use Pixie\QueryBuilder\NestedCriteria; -use function is_bool; -use function is_float; - class WPDBAdapter { /** diff --git a/tests/TestIntegrationWithWPDB.php b/tests/TestIntegrationWithWPDB.php index 7ed51ee..e8e3c2d 100644 --- a/tests/TestIntegrationWithWPDB.php +++ b/tests/TestIntegrationWithWPDB.php @@ -13,8 +13,10 @@ use stdClass; use Exception; +use Pixie\Binding; use WP_UnitTestCase; use Pixie\Connection; +use Pixie\QueryBuilder\Raw; use Pixie\Tests\Logable_WPDB; use Pixie\Tests\Fixtures\ModelForMockFoo; use Pixie\QueryBuilder\QueryBuilderHandler; @@ -559,4 +561,122 @@ public function testFindOrFail(): void $row = $builder->findOrFail('Forth', 'string'); } + + /** @testdox It should be possible to query a date column by month */ + public function testWhereMonth(): void + { + $this->wpdb->insert('mock_dates', ['date' => '2020-10-10', 'unix' => '2020-10-10 18:19:03', 'datetime' => '2020-10-10 18:19:03'], ['%s', '%s']); + $this->wpdb->insert('mock_dates', ['date' => '2002-10-05', 'unix' => '2002-10-10 18:19:03', 'datetime' => '2002-10-10 18:19:03'], ['%s', '%s']); + $this->wpdb->insert('mock_dates', ['date' => '2002-3-3', 'unix' => '2002-3-3 18:19:03', 'datetime' => '2002-3-3 18:19:03'], ['%s', '%s']); + + $month3 = $this->queryBuilderProvider('mock_') + ->table('dates') + ->whereMonth('unix', Binding::asString(3)) + ->get(); + + $this->assertCount(1, $month3); + $this->assertEquals('2002-03-03', $month3[0]->date); + + $month10 = $this->queryBuilderProvider('mock_') + ->table('dates') + ->whereMonth('date', '>', 9) + ->get(); + + $this->assertCount(2, $month10); + $this->assertEquals('2020-10-10', $month10[0]->date); + $this->assertEquals('2002-10-05', $month10[1]->date); + } + + /** @testdox It should be possible to query a date column by day */ + public function testWhereDay(): void + { + $this->wpdb->insert('mock_dates', ['date' => '2020-10-10', 'unix' => '2020-10-10 18:19:03', 'datetime' => '2020-10-10 18:19:03'], ['%s', '%s']); + $this->wpdb->insert('mock_dates', ['date' => '2010-10-05', 'unix' => '2010-10-05 18:19:03', 'datetime' => '2010-10-05 18:19:03'], ['%s', '%s']); + $this->wpdb->insert('mock_dates', ['date' => '2002-03-12', 'unix' => '2002-03-12 18:19:03', 'datetime' => '2002-03-12 18:19:03'], ['%s', '%s']); + + $day5 = $this->queryBuilderProvider('mock_') + ->table('dates') + ->whereDay('unix', Binding::asString(5)) + ->get(); + $this->assertCount(1, $day5); + $this->assertEquals('2010-10-05', $day5[0]->date); + + $dayAbove9 = $this->queryBuilderProvider('mock_') + ->table('dates') + ->whereDay('date', '>', 9) + ->get(); + + $this->assertCount(2, $dayAbove9); + $this->assertEquals('2020-10-10', $dayAbove9[0]->date); + $this->assertEquals('2002-03-12', $dayAbove9[1]->date); + + $day10 = $this->queryBuilderProvider('mock_') + ->table('dates') + ->whereDay('datetime', Binding::asString(10)) + ->get(); + $this->assertCount(1, $day10); + $this->assertEquals('2020-10-10', $day10[0]->date); + } + + /** @testdox It should be possible to query a date column by year */ + public function testWhereYear(): void + { + $this->wpdb->insert('mock_dates', ['date' => '2022-10-10', 'unix' => '2022-10-10 18:19:03', 'datetime' => '2022-10-10 18:19:03'], ['%s', '%s']); + $this->wpdb->insert('mock_dates', ['date' => '2010-10-05', 'unix' => '2010-10-05 18:19:03', 'datetime' => '2010-10-05 18:19:03'], ['%s', '%s']); + $this->wpdb->insert('mock_dates', ['date' => '2020-03-12', 'unix' => '2020-03-12 18:19:03', 'datetime' => '2020-03-12 18:19:03'], ['%s', '%s']); + + $only2010 = $this->queryBuilderProvider('mock_') + ->table('dates') + ->whereYear('unix', Binding::asString(2010)) + ->get(); + $this->assertCount(1, $only2010); + $this->assertEquals('2010-10-05', $only2010[0]->date); + + $after2009 = $this->queryBuilderProvider('mock_') + ->table('dates') + ->whereYear('date', '>', new Raw('%d', [2019])) + ->get(); + + $this->assertCount(2, $after2009); + $this->assertEquals('2022-10-10', $after2009[0]->date); + $this->assertEquals('2020-03-12', $after2009[1]->date); + + $only2022 = $this->queryBuilderProvider('mock_') + ->table('dates') + ->whereYear('datetime', Binding::asFloat(2022)) + ->get(); + $this->assertCount(1, $only2022); + $this->assertEquals('2022-10-10', $only2022[0]->date); + } + + /** @testdox It should be possible to query a date column by date */ + public function testWhereDate(): void + { + $this->wpdb->insert('mock_dates', ['date' => '2022-10-10', 'unix' => '2022-10-10 18:19:03', 'datetime' => '2022-10-10 18:19:03'], ['%s', '%s']); + $this->wpdb->insert('mock_dates', ['date' => '2010-10-05', 'unix' => '2010-10-05 18:19:03', 'datetime' => '2010-10-05 18:19:03'], ['%s', '%s']); + $this->wpdb->insert('mock_dates', ['date' => '2020-03-12', 'unix' => '2020-03-12 18:19:03', 'datetime' => '2020-03-12 18:19:03'], ['%s', '%s']); + + $resultA = $this->queryBuilderProvider('mock_') + ->table('dates') + ->whereDate('unix', Binding::asString('2010-10-05')) + ->get(); + $this->assertCount(1, $resultA); + $this->assertEquals('2010-10-05', $resultA[0]->date); + + $resultB = $this->queryBuilderProvider('mock_') + ->table('dates') + ->whereDate('date', '!=', new Raw('%s', ['2020-03-12'])) + ->get(); + + $this->assertCount(2, $resultB); + $this->assertEquals('2022-10-10', $resultB[0]->date); + $this->assertEquals('2010-10-05', $resultB[1]->date); + + $resultC = $this->queryBuilderProvider('mock_') + ->table('dates') + ->whereDate('datetime', date("Y-m-d", 1665425943)) // strtotime('2022-10-10 18:19:03') + ->get(); + $this->assertCount(1, $resultC); + $this->assertEquals('2022-10-10', $resultC[0]->date); + } } From 3fe79cf68b656dd08a2ab7646517d898a8773a16 Mon Sep 17 00:00:00 2001 From: Glynn Quelch Date: Wed, 19 Jan 2022 09:27:16 +0000 Subject: [PATCH 38/69] House Keeping --- README.md | 45 +++++++++++++++++++++++++++++++++++++-------- 1 file changed, 37 insertions(+), 8 deletions(-) diff --git a/README.md b/README.md index b13d1ca..098ec21 100644 --- a/README.md +++ b/README.md @@ -111,7 +111,12 @@ Library on [Packagist](https://packagist.org/packages/gin0115/pixie-wpdb). - [Where In](#where-in) - [Where Between](#where-between) - [Where Null](#where-null) + - [Where Dates](#where-dates) + - [Where Day](#where-day) + - [Where Month](#where-month) + - [Where Year](#where-year) - [Grouped Where](#grouped-where) + - [Group By and Order By](#group-by-and-order-by) - [Having](#having) - [Limit and Offset](#limit-and-offset) @@ -285,6 +290,30 @@ QB::table('my_table') ->orWhereNotNull('field4'); ``` +### Where Date +```PHP +QB::table('my_table') + ->whereDate('column', '<', '2020-12-29'); // All where date after 29 Dec 2020 +``` + +### Where Day +```PHP +QB::table('my_table') + ->whereDay('date_column', '=', '29'); // All where day is 29 in any date formats +``` + +### Where Month +```PHP +QB::table('my_table') + ->whereMonth('date_column', '=', '12'); // All where month is december in any date formats +``` + +### Where Year +```PHP +QB::table('my_table') + ->whereYear('date_column', '=', '2015'); // All where year is 2015 in any date formats +``` + #### Grouped Where Sometimes queries get complex, where you need grouped criteria, for example `WHERE age = 10 and (name like '%usman%' or description LIKE '%usman%')`. @@ -412,14 +441,14 @@ QB::table('my_table') Neither of the above string would be automatically wrapped in `'single quotes'`. **Types** - -`Binding::asString($value)` -`Binding::asInt($value)` -`Binding::asFloat($value)` -`Binding::asBool($value)` -`Binding::asJson($value)` -`Binding::asRaw($value)` - +```php +Binding::asString($value); +Binding::asInt($value); +Binding::asFloat($value); +Binding::asBool($value); +Binding::asJson($value); +Binding::asRaw($value); +``` ### Insert ```PHP From ed884901ca3e950f010f8d3bb46043ebfd3fcbd3 Mon Sep 17 00:00:00 2001 From: Glynn Quelch Date: Wed, 19 Jan 2022 09:27:26 +0000 Subject: [PATCH 39/69] House Keeping --- src/QueryBuilder/QueryBuilderHandler.php | 4 ++-- src/QueryBuilder/WPDBAdapter.php | 8 ++++---- 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/src/QueryBuilder/QueryBuilderHandler.php b/src/QueryBuilder/QueryBuilderHandler.php index d0d7cce..ca0d8a8 100644 --- a/src/QueryBuilder/QueryBuilderHandler.php +++ b/src/QueryBuilder/QueryBuilderHandler.php @@ -9,14 +9,14 @@ use Pixie\Exception; use Pixie\Connection; -use function mb_strlen; - use Pixie\QueryBuilder\Raw; + use Pixie\Hydration\Hydrator; use Pixie\QueryBuilder\JoinBuilder; use Pixie\QueryBuilder\QueryObject; use Pixie\QueryBuilder\Transaction; use Pixie\QueryBuilder\WPDBAdapter; +use function mb_strlen; class QueryBuilderHandler { diff --git a/src/QueryBuilder/WPDBAdapter.php b/src/QueryBuilder/WPDBAdapter.php index 9c3b341..c16b73e 100644 --- a/src/QueryBuilder/WPDBAdapter.php +++ b/src/QueryBuilder/WPDBAdapter.php @@ -6,15 +6,15 @@ use Pixie\Binding; use Pixie\Exception; -use function is_bool; - use Pixie\Connection; -use function is_float; - use Pixie\QueryBuilder\Raw; + use Pixie\QueryBuilder\NestedCriteria; +use function is_bool; +use function is_float; + class WPDBAdapter { /** From 16caf08cdfcef26ffa69fd8b9f970eafacb3196e Mon Sep 17 00:00:00 2001 From: Glynn Quelch Date: Wed, 19 Jan 2022 09:27:53 +0000 Subject: [PATCH 40/69] House Keeping --- README.md | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/README.md b/README.md index 098ec21..bba0f87 100644 --- a/README.md +++ b/README.md @@ -115,8 +115,7 @@ Library on [Packagist](https://packagist.org/packages/gin0115/pixie-wpdb). - [Where Day](#where-day) - [Where Month](#where-month) - [Where Year](#where-year) - - [Grouped Where](#grouped-where) - + - [Grouped Where](#grouped-where) - [Group By and Order By](#group-by-and-order-by) - [Having](#having) - [Limit and Offset](#limit-and-offset) From a3a6f05c8a3fc88d68a3bce4e1998aef2fb824c9 Mon Sep 17 00:00:00 2001 From: Glynn Quelch Date: Wed, 19 Jan 2022 09:28:47 +0000 Subject: [PATCH 41/69] House Keeping --- README.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index bba0f87..eb35aae 100644 --- a/README.md +++ b/README.md @@ -111,7 +111,7 @@ Library on [Packagist](https://packagist.org/packages/gin0115/pixie-wpdb). - [Where In](#where-in) - [Where Between](#where-between) - [Where Null](#where-null) - - [Where Dates](#where-dates) + - [Where Date](#where-date) - [Where Day](#where-day) - [Where Month](#where-month) - [Where Year](#where-year) @@ -123,7 +123,7 @@ Library on [Packagist](https://packagist.org/packages/gin0115/pixie-wpdb). - [Multiple Join Criteria](#multiple-join-criteria) - [Raw Query](#raw-query) - [Raw Expressions](#raw-expressions) - - [Value Binding](#bindings) + - [Value Binding](#value-binding) - [**Insert**](#insert) - [Batch Insert](#batch-insert) - [Insert with ON DUPLICATE KEY statement](#insert-with-on-duplicate-key-statement) From a153909a709ad5d01d98ad89611c9ddeb39420bf Mon Sep 17 00:00:00 2001 From: Glynn Quelch Date: Wed, 19 Jan 2022 09:30:16 +0000 Subject: [PATCH 42/69] House Keeping --- README.md | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/README.md b/README.md index eb35aae..6060ee6 100644 --- a/README.md +++ b/README.md @@ -120,6 +120,7 @@ Library on [Packagist](https://packagist.org/packages/gin0115/pixie-wpdb). - [Having](#having) - [Limit and Offset](#limit-and-offset) - [Join](#join) + - [Join Using](#join-using) - [Multiple Join Criteria](#multiple-join-criteria) - [Raw Query](#raw-query) - [Raw Expressions](#raw-expressions) @@ -372,7 +373,7 @@ Available methods, ### Join Using -It is possible to create a simple join statment between 2 tables, where they are matched on the same key names. +It is possible to create a simple join statement between 2 tables, where they are matched on the same key names. ```php ->table('foo')->join('bar', 'bar.id', '=', 'foo.id'); From 75fde27ba863332da769429549e1559384ebad26 Mon Sep 17 00:00:00 2001 From: Glynn Quelch Date: Wed, 19 Jan 2022 09:32:40 +0000 Subject: [PATCH 43/69] House Keeping --- README.md | 1 - 1 file changed, 1 deletion(-) diff --git a/README.md b/README.md index b13d1ca..c97111f 100644 --- a/README.md +++ b/README.md @@ -561,7 +561,6 @@ This will produce a query like this: SELECT * FROM (SELECT `cb_my_table`.*, (SELECT `details` FROM `cb_person_details` WHERE `person_id` = 3) as table_alias1 FROM `cb_my_table`) as table_alias2 -**NOTE:** Pixie doesn't use bindings for sub queries and nested queries. It quotes values with PDO's `quote()` method. ### Get wpdb Instance If you need to get the wpdb instance you can do so. From 96b84e6288b83bdff3a490c734f6d002353120bc Mon Sep 17 00:00:00 2001 From: Glynn Quelch Date: Wed, 19 Jan 2022 14:43:03 +0000 Subject: [PATCH 44/69] Basic selectJson() added with tests --- src/QueryBuilder/QueryBuilderHandler.php | 30 +++++++++++++++-- tests/TestIntegrationWithWPDB.php | 42 +++++++++++++++++++++++- tests/TestQueryBuilderSQLGeneration.php | 13 ++++++++ 3 files changed, 82 insertions(+), 3 deletions(-) diff --git a/src/QueryBuilder/QueryBuilderHandler.php b/src/QueryBuilder/QueryBuilderHandler.php index ca0d8a8..eda3111 100644 --- a/src/QueryBuilder/QueryBuilderHandler.php +++ b/src/QueryBuilder/QueryBuilderHandler.php @@ -9,14 +9,14 @@ use Pixie\Exception; use Pixie\Connection; -use Pixie\QueryBuilder\Raw; +use function mb_strlen; +use Pixie\QueryBuilder\Raw; use Pixie\Hydration\Hydrator; use Pixie\QueryBuilder\JoinBuilder; use Pixie\QueryBuilder\QueryObject; use Pixie\QueryBuilder\Transaction; use Pixie\QueryBuilder\WPDBAdapter; -use function mb_strlen; class QueryBuilderHandler { @@ -1447,4 +1447,30 @@ public function getFetchMode() ? $this->fetchMode : \OBJECT; } + + // JSON + + /** + * @param string|Raw $key The database column which holds the JSON value + * @param string|Raw $jsonKey The json key/index to search + * @param string|null $alias The alias used to define the value in results, if not defined will use json_{$jsonKey} + * @return static + */ + public function selectJson($key, $jsonKey, ?string $alias = null): self + { + // Handle potential raw values. + if ($key instanceof Raw) { + $key = $this->adapterInstance->parseRaw($key); + } + if ($jsonKey instanceof Raw) { + $jsonKey = $this->adapterInstance->parseRaw($jsonKey); + } + + // Add any possible prefixes to the key + $key = $this->addTablePrefix($key, true); + + $alias = null === $alias ? "json_{$jsonKey}" : $alias; + return $this->select(new Raw("JSON_EXTRACT({$key}, \"$.{$jsonKey}\") as {$alias}")); + } } +// 'JSON_EXTRACT(json, "$.id") as jsonID' diff --git a/tests/TestIntegrationWithWPDB.php b/tests/TestIntegrationWithWPDB.php index e8e3c2d..47e9623 100644 --- a/tests/TestIntegrationWithWPDB.php +++ b/tests/TestIntegrationWithWPDB.php @@ -67,7 +67,7 @@ public function createTables(): void "CREATE TABLE mock_json ( id mediumint(8) unsigned NOT NULL auto_increment , string varchar(255) NULL, - json json NULL, + jsonCol json NULL, PRIMARY KEY (id) ) COLLATE {$this->wpdb->collate}"; @@ -679,4 +679,44 @@ public function testWhereDate(): void $this->assertCount(1, $resultC); $this->assertEquals('2022-10-10', $resultC[0]->date); } + + + + /**************************************/ + /* JSON FUNCTIONALITY */ + /**************************************/ + + /** @testdox It should be possible to select values from inside a JSON object, held in a JSON column type. */ + public function testCanSelectFromWithingJSONColumn() + { + $this->wpdb->insert('mock_json', ['string' => 'a', 'jsonCol' => \json_encode((object)['id' => 24748, 'name' => 'Sam'])], ['%s', '%s']); + + $asRaw = $this->queryBuilderProvider('mock_') + ->table('json') + ->select('string', new Raw('JSON_EXTRACT(jsonCol, "$.id") as jsonID')) + ->get(); + + $asSelectJson = $this->queryBuilderProvider('mock_') + ->table('json') + ->select('string') + ->selectJson('jsonCol', "id", 'jsonID') + ->get(); +dump($asSelectJson, $asRaw); + $this->assertEquals($asRaw[0]->string, $asSelectJson[0]->string); + $this->assertEquals($asRaw[0]->jsonID, $asSelectJson[0]->jsonID); + + // Without Alias + $jsonWoutAlias = $this->queryBuilderProvider('mock_') + ->table('json') + ->select('string') + ->selectJson('jsonCol', "id") + ->get(); + + $this->assertEquals('a', $jsonWoutAlias[0]->string); + $this->assertEquals('24748', $jsonWoutAlias[0]->json_id); + } } + + +/* string varchar(255) NULL, + json json NULL, */ diff --git a/tests/TestQueryBuilderSQLGeneration.php b/tests/TestQueryBuilderSQLGeneration.php index 3cdf991..ab8aefa 100644 --- a/tests/TestQueryBuilderSQLGeneration.php +++ b/tests/TestQueryBuilderSQLGeneration.php @@ -840,4 +840,17 @@ public function testWhereIsNullUsingRawForColumn(): void ->whereNotNull('key2'); $this->assertEquals("SELECT * FROM foo WHERE key IS NOT NULL AND key2 IS NOT NULL", $builderNot->getQuery()->getRawSql()); } + + /** @testdox It should be possible to create a query which gets values form a JSON column, while using RAW object for both the MYSQL col key and JSON object key (1st generation) */ + public function testJsonSelectUsingRawValues(): void + { + $builder = $this->queryBuilderProvider() + ->table('jsonSelects') + ->selectJson(new Raw('column'), new Raw('foo')); + + $this->assertEquals( + 'SELECT JSON_EXTRACT(column, "$.foo") as json_foo FROM jsonSelects', + $builder->getQuery()->getRawSql() + ); + } } From ece7b4674c6472dcf5078dd364a965e06af8ad23 Mon Sep 17 00:00:00 2001 From: Glynn Quelch Date: Wed, 19 Jan 2022 14:43:23 +0000 Subject: [PATCH 45/69] House Keeping --- src/QueryBuilder/QueryBuilderHandler.php | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/QueryBuilder/QueryBuilderHandler.php b/src/QueryBuilder/QueryBuilderHandler.php index eda3111..c3be580 100644 --- a/src/QueryBuilder/QueryBuilderHandler.php +++ b/src/QueryBuilder/QueryBuilderHandler.php @@ -9,14 +9,14 @@ use Pixie\Exception; use Pixie\Connection; -use function mb_strlen; - use Pixie\QueryBuilder\Raw; + use Pixie\Hydration\Hydrator; use Pixie\QueryBuilder\JoinBuilder; use Pixie\QueryBuilder\QueryObject; use Pixie\QueryBuilder\Transaction; use Pixie\QueryBuilder\WPDBAdapter; +use function mb_strlen; class QueryBuilderHandler { From ae7aa9485448bf4ae11db27590dccd09b4144bde Mon Sep 17 00:00:00 2001 From: Glynn Quelch Date: Wed, 19 Jan 2022 15:08:07 +0000 Subject: [PATCH 46/69] Added functionality to allow traversing deeping into a JSON value --- src/QueryBuilder/QueryBuilderHandler.php | 13 ++++-- tests/TestIntegrationWithWPDB.php | 53 ++++++++++++++++++++++-- tests/TestQueryBuilderSQLGeneration.php | 2 +- 3 files changed, 60 insertions(+), 8 deletions(-) diff --git a/src/QueryBuilder/QueryBuilderHandler.php b/src/QueryBuilder/QueryBuilderHandler.php index c3be580..9fa4635 100644 --- a/src/QueryBuilder/QueryBuilderHandler.php +++ b/src/QueryBuilder/QueryBuilderHandler.php @@ -9,14 +9,14 @@ use Pixie\Exception; use Pixie\Connection; -use Pixie\QueryBuilder\Raw; +use function mb_strlen; +use Pixie\QueryBuilder\Raw; use Pixie\Hydration\Hydrator; use Pixie\QueryBuilder\JoinBuilder; use Pixie\QueryBuilder\QueryObject; use Pixie\QueryBuilder\Transaction; use Pixie\QueryBuilder\WPDBAdapter; -use function mb_strlen; class QueryBuilderHandler { @@ -1452,7 +1452,7 @@ public function getFetchMode() /** * @param string|Raw $key The database column which holds the JSON value - * @param string|Raw $jsonKey The json key/index to search + * @param string|Raw|string[] $jsonKey The json key/index to search * @param string|null $alias The alias used to define the value in results, if not defined will use json_{$jsonKey} * @return static */ @@ -1466,11 +1466,16 @@ public function selectJson($key, $jsonKey, ?string $alias = null): self $jsonKey = $this->adapterInstance->parseRaw($jsonKey); } + // If deeply nested jsonKey. + if (is_array($jsonKey)) { + $jsonKey = \implode('.', $jsonKey); + } + // Add any possible prefixes to the key $key = $this->addTablePrefix($key, true); $alias = null === $alias ? "json_{$jsonKey}" : $alias; - return $this->select(new Raw("JSON_EXTRACT({$key}, \"$.{$jsonKey}\") as {$alias}")); + return $this->select(new Raw("JSON_UNQUOTE(JSON_EXTRACT({$key}, \"$.{$jsonKey}\")) as {$alias}")); } } // 'JSON_EXTRACT(json, "$.id") as jsonID' diff --git a/tests/TestIntegrationWithWPDB.php b/tests/TestIntegrationWithWPDB.php index 47e9623..b6bbfc7 100644 --- a/tests/TestIntegrationWithWPDB.php +++ b/tests/TestIntegrationWithWPDB.php @@ -687,13 +687,13 @@ public function testWhereDate(): void /**************************************/ /** @testdox It should be possible to select values from inside a JSON object, held in a JSON column type. */ - public function testCanSelectFromWithingJSONColumn() + public function testCanSelectFromWithinJSONColumn1GenDeep() { $this->wpdb->insert('mock_json', ['string' => 'a', 'jsonCol' => \json_encode((object)['id' => 24748, 'name' => 'Sam'])], ['%s', '%s']); $asRaw = $this->queryBuilderProvider('mock_') ->table('json') - ->select('string', new Raw('JSON_EXTRACT(jsonCol, "$.id") as jsonID')) + ->select('string', new Raw('JSON_UNQUOTE(JSON_EXTRACT(jsonCol, "$.id")) as jsonID')) ->get(); $asSelectJson = $this->queryBuilderProvider('mock_') @@ -701,7 +701,7 @@ public function testCanSelectFromWithingJSONColumn() ->select('string') ->selectJson('jsonCol', "id", 'jsonID') ->get(); -dump($asSelectJson, $asRaw); + $this->assertEquals($asRaw[0]->string, $asSelectJson[0]->string); $this->assertEquals($asRaw[0]->jsonID, $asSelectJson[0]->jsonID); @@ -715,6 +715,53 @@ public function testCanSelectFromWithingJSONColumn() $this->assertEquals('a', $jsonWoutAlias[0]->string); $this->assertEquals('24748', $jsonWoutAlias[0]->json_id); } + + /** @testdox It should be possible */ + public function testCanSelectFromWithinJSONColumn3GenDeep(): void + { + $jsonData = (object)[ + 'key' => 'apple', + 'data' => (object) [ + 'array' => [1,2,3,4], + 'object' => (object) [ + 'obj1' => 'val1', + 'obj2' => 'val2', + ] + ] + ]; + $this->wpdb->insert('mock_json', ['string' => 'a', 'jsonCol' => \json_encode($jsonData)], ['%s', '%s']); + $objectVal = $this->queryBuilderProvider('mock_') + ->table('json') + ->select('string') + ->selectJson('jsonCol', ['data', 'object', 'obj1'], 'jsonVALUE') + ->first(); + + $this->assertNotNull($objectVal); + $this->assertEquals('a', $objectVal->string); + $this->assertEquals('val1', $objectVal->jsonVALUE); + + + $arrayValues = $this->queryBuilderProvider('mock_') + ->table('json') + ->select('string') + ->selectJson('jsonCol', ['data', 'array'], 'jsonVALUE') + ->first(); + + $this->assertNotNull($arrayValues); + $this->assertEquals('a', $arrayValues->string); + $this->assertEquals('[1, 2, 3, 4]', $arrayValues->jsonVALUE); + + $pluckArrayValue = $this->queryBuilderProvider('mock_') + ->table('json') + ->select('string') + ->selectJson('json.jsonCol', ['data', 'array[1]'], 'jsonVALUE') + ->first(); + + $this->assertNotNull($pluckArrayValue); + $this->assertEquals('a', $pluckArrayValue->string); + $this->assertEquals('2', $pluckArrayValue->jsonVALUE); + // dump(\json_decode($r->jsonVALUE)); + } } diff --git a/tests/TestQueryBuilderSQLGeneration.php b/tests/TestQueryBuilderSQLGeneration.php index ab8aefa..56ad2b2 100644 --- a/tests/TestQueryBuilderSQLGeneration.php +++ b/tests/TestQueryBuilderSQLGeneration.php @@ -849,7 +849,7 @@ public function testJsonSelectUsingRawValues(): void ->selectJson(new Raw('column'), new Raw('foo')); $this->assertEquals( - 'SELECT JSON_EXTRACT(column, "$.foo") as json_foo FROM jsonSelects', + 'SELECT JSON_UNQUOTE(JSON_EXTRACT(column, "$.foo")) as json_foo FROM jsonSelects', $builder->getQuery()->getRawSql() ); } From 77e415a0e874cafeb1aff75567a9b56672de2886 Mon Sep 17 00:00:00 2001 From: Glynn Quelch Date: Wed, 19 Jan 2022 15:09:44 +0000 Subject: [PATCH 47/69] House Keeping --- src/QueryBuilder/QueryBuilderHandler.php | 4 ++-- tests/TestIntegrationWithWPDB.php | 11 +++++++++-- 2 files changed, 11 insertions(+), 4 deletions(-) diff --git a/src/QueryBuilder/QueryBuilderHandler.php b/src/QueryBuilder/QueryBuilderHandler.php index 9fa4635..ec7d44c 100644 --- a/src/QueryBuilder/QueryBuilderHandler.php +++ b/src/QueryBuilder/QueryBuilderHandler.php @@ -9,14 +9,14 @@ use Pixie\Exception; use Pixie\Connection; -use function mb_strlen; - use Pixie\QueryBuilder\Raw; + use Pixie\Hydration\Hydrator; use Pixie\QueryBuilder\JoinBuilder; use Pixie\QueryBuilder\QueryObject; use Pixie\QueryBuilder\Transaction; use Pixie\QueryBuilder\WPDBAdapter; +use function mb_strlen; class QueryBuilderHandler { diff --git a/tests/TestIntegrationWithWPDB.php b/tests/TestIntegrationWithWPDB.php index b6bbfc7..d676214 100644 --- a/tests/TestIntegrationWithWPDB.php +++ b/tests/TestIntegrationWithWPDB.php @@ -730,6 +730,8 @@ public function testCanSelectFromWithinJSONColumn3GenDeep(): void ] ]; $this->wpdb->insert('mock_json', ['string' => 'a', 'jsonCol' => \json_encode($jsonData)], ['%s', '%s']); + + // Extract a value from an object $objectVal = $this->queryBuilderProvider('mock_') ->table('json') ->select('string') @@ -740,7 +742,7 @@ public function testCanSelectFromWithinJSONColumn3GenDeep(): void $this->assertEquals('a', $objectVal->string); $this->assertEquals('val1', $objectVal->jsonVALUE); - + // Extract an entire array. $arrayValues = $this->queryBuilderProvider('mock_') ->table('json') ->select('string') @@ -750,7 +752,13 @@ public function testCanSelectFromWithinJSONColumn3GenDeep(): void $this->assertNotNull($arrayValues); $this->assertEquals('a', $arrayValues->string); $this->assertEquals('[1, 2, 3, 4]', $arrayValues->jsonVALUE); + $this->assertCount(4, \json_decode($arrayValues->jsonVALUE)); + $this->assertContains('1', \json_decode($arrayValues->jsonVALUE)); + $this->assertContains('2', \json_decode($arrayValues->jsonVALUE)); + $this->assertContains('3', \json_decode($arrayValues->jsonVALUE)); + $this->assertContains('4', \json_decode($arrayValues->jsonVALUE)); + // Pluck a single item from an array using its key. $pluckArrayValue = $this->queryBuilderProvider('mock_') ->table('json') ->select('string') @@ -760,7 +768,6 @@ public function testCanSelectFromWithinJSONColumn3GenDeep(): void $this->assertNotNull($pluckArrayValue); $this->assertEquals('a', $pluckArrayValue->string); $this->assertEquals('2', $pluckArrayValue->jsonVALUE); - // dump(\json_decode($r->jsonVALUE)); } } From 1aa8eddfe0563a4099bfe3eacd5b1548ca948a0e Mon Sep 17 00:00:00 2001 From: Glynn Quelch Date: Wed, 19 Jan 2022 15:11:41 +0000 Subject: [PATCH 48/69] House Keeping --- tests/TestIntegrationWithWPDB.php | 4 ---- 1 file changed, 4 deletions(-) diff --git a/tests/TestIntegrationWithWPDB.php b/tests/TestIntegrationWithWPDB.php index d676214..7378719 100644 --- a/tests/TestIntegrationWithWPDB.php +++ b/tests/TestIntegrationWithWPDB.php @@ -770,7 +770,3 @@ public function testCanSelectFromWithinJSONColumn3GenDeep(): void $this->assertEquals('2', $pluckArrayValue->jsonVALUE); } } - - -/* string varchar(255) NULL, - json json NULL, */ From 4aca8ef2846d5025319df46ba53a36b19c000785 Mon Sep 17 00:00:00 2001 From: Glynn Quelch Date: Wed, 19 Jan 2022 16:46:49 +0000 Subject: [PATCH 49/69] House Keeping --- src/QueryBuilder/QueryBuilderHandler.php | 56 ++++++++++++++++++++++-- src/QueryBuilder/WPDBAdapter.php | 12 +++-- tests/TestIntegrationWithWPDB.php | 21 +++++++++ 3 files changed, 82 insertions(+), 7 deletions(-) diff --git a/src/QueryBuilder/QueryBuilderHandler.php b/src/QueryBuilder/QueryBuilderHandler.php index ec7d44c..c08d899 100644 --- a/src/QueryBuilder/QueryBuilderHandler.php +++ b/src/QueryBuilder/QueryBuilderHandler.php @@ -9,14 +9,14 @@ use Pixie\Exception; use Pixie\Connection; -use Pixie\QueryBuilder\Raw; +use function mb_strlen; +use Pixie\QueryBuilder\Raw; use Pixie\Hydration\Hydrator; use Pixie\QueryBuilder\JoinBuilder; use Pixie\QueryBuilder\QueryObject; use Pixie\QueryBuilder\Transaction; use Pixie\QueryBuilder\WPDBAdapter; -use function mb_strlen; class QueryBuilderHandler { @@ -1068,6 +1068,45 @@ protected function whereNullHandler($key, string $prefix = '', $operator = ''): return $this->{$operator . 'Where'}($this->raw("{$key} IS{$prefix} NULL")); } + /** + * @param string|Raw $key The database column which holds the JSON value + * @param string|Raw|string[] $jsonKey The json key/index to search + * @param string|mixed|null $operator Can be used as value, if 3rd arg not passed + * @param mixed|null $value + * @return static + */ + public function whereJson($key, $jsonKey, $operator = null, $value = null): self + { + // If two params are given then assume operator is = + if (2 == func_num_args()) { + $value = $operator; + $operator = '='; + } + + // Handle potential raw values. + if ($key instanceof Raw) { + $key = $this->adapterInstance->parseRaw($key); + } + if ($jsonKey instanceof Raw) { + $jsonKey = $this->adapterInstance->parseRaw($jsonKey); + } + + // If deeply nested jsonKey. + if (is_array($jsonKey)) { + $jsonKey = \implode('.', $jsonKey); + } + + // Add any possible prefixes to the key + $key = $this->addTablePrefix($key, true); + + // return $this->where(new Raw('JSON_EXTRACT(jsonCol,"$.thing")'), '=', 'foo') + return $this->where( + new Raw("JSON_UNQUOTE(JSON_EXTRACT({$key}, \"$.{$jsonKey}\"))"), + $operator, + $value + ); + } + /** * @param string|Raw $table * @param string|Raw|Closure $key @@ -1298,6 +1337,9 @@ public function getConnection() */ protected function whereHandler($key, $operator = null, $value = null, $joiner = 'AND') { + if ($key instanceof Raw) { + $key = $this->adapterInstance->parseRaw($key); + } $key = $this->addTablePrefix($key); $this->statements['wheres'][] = compact('key', 'operator', 'value', 'joiner'); return $this; @@ -1343,7 +1385,15 @@ public function addTablePrefix($values, bool $tableFieldMix = true) $target = &$key; } - if (!$tableFieldMix || (is_string($target) && false !== strpos($target, '.'))) { + // Do prefix if the target is an expression or function. + if ( + !$tableFieldMix + || ( + is_string($target) // Must be a string + && (bool) preg_match('/^[A-Za-z0-9_.]+$/', $target) // Can only contain letters, numbers, underscore and full stops + && 1 === \substr_count($target, '.') // Contains a single full stop ONLY. + ) + ) { $target = $this->tablePrefix . $target; } diff --git a/src/QueryBuilder/WPDBAdapter.php b/src/QueryBuilder/WPDBAdapter.php index c16b73e..0b1eade 100644 --- a/src/QueryBuilder/WPDBAdapter.php +++ b/src/QueryBuilder/WPDBAdapter.php @@ -6,15 +6,15 @@ use Pixie\Binding; use Pixie\Exception; +use function is_bool; + use Pixie\Connection; -use Pixie\QueryBuilder\Raw; +use function is_float; +use Pixie\QueryBuilder\Raw; use Pixie\QueryBuilder\NestedCriteria; -use function is_bool; -use function is_float; - class WPDBAdapter { /** @@ -62,6 +62,7 @@ public function select(array $statements): array // Wheres list($whereCriteria, $whereBindings) = $this->buildCriteriaWithType($statements, 'wheres', 'WHERE'); + // Group bys $groupBys = ''; if (isset($statements['groupBys']) && $groupBys = $this->arrayStr($statements['groupBys'], ', ')) { @@ -672,6 +673,9 @@ protected function buildCriteriaWithType(array $statements, string $key, string } } + // Remove any multiple whitespace. + $criteria = (string) preg_replace('!\s+!', ' ', $criteria); + return [$criteria, $bindings]; } diff --git a/tests/TestIntegrationWithWPDB.php b/tests/TestIntegrationWithWPDB.php index 7378719..4605a59 100644 --- a/tests/TestIntegrationWithWPDB.php +++ b/tests/TestIntegrationWithWPDB.php @@ -769,4 +769,25 @@ public function testCanSelectFromWithinJSONColumn3GenDeep(): void $this->assertEquals('a', $pluckArrayValue->string); $this->assertEquals('2', $pluckArrayValue->jsonVALUE); } + + /** @testdox It should be possible to do a where query that checks a value inside a json value. Tests only 1 level deep */ + public function testJsonWhere1GenDeep() + { + $this->wpdb->insert('mock_json', ['string' => 'a', 'jsonCol' => \json_encode((object)['id' => 24748, 'thing' => 'foo'])], ['%s', '%s']); + $this->wpdb->insert('mock_json', ['string' => 'b', 'jsonCol' => \json_encode((object)['id' => 78945, 'thing' => 'foo'])], ['%s', '%s']); + $this->wpdb->insert('mock_json', ['string' => 'b', 'jsonCol' => \json_encode((object)['id' => 78941, 'thing' => 'bar'])], ['%s', '%s']); + + $whereThingFooRaw = $this->queryBuilderProvider('mock_') + ->table('json') + ->where(new Raw('JSON_EXTRACT(jsonCol,"$.thing")'), '=', 'foo') + ->get(); + + $whereThingFoo = $this->queryBuilderProvider('mock_') + ->table('json') + ->whereJson('jsonCol', 'thing', '=', 'foo') + ->get(); + + $this->assertEquals($whereThingFooRaw[0]->string, $whereThingFoo[0]->string); + $this->assertEquals($whereThingFooRaw[0]->jsonCol, $whereThingFoo[0]->jsonCol); + } } From 1d4b4e246830fc38c5e536c1e16a0f6cd499f0d8 Mon Sep 17 00:00:00 2001 From: Glynn Quelch Date: Wed, 19 Jan 2022 16:46:56 +0000 Subject: [PATCH 50/69] House Keeping --- src/QueryBuilder/QueryBuilderHandler.php | 20 ++++++++++---------- src/QueryBuilder/WPDBAdapter.php | 10 +++++----- 2 files changed, 15 insertions(+), 15 deletions(-) diff --git a/src/QueryBuilder/QueryBuilderHandler.php b/src/QueryBuilder/QueryBuilderHandler.php index c08d899..3408ae4 100644 --- a/src/QueryBuilder/QueryBuilderHandler.php +++ b/src/QueryBuilder/QueryBuilderHandler.php @@ -9,14 +9,14 @@ use Pixie\Exception; use Pixie\Connection; -use function mb_strlen; - use Pixie\QueryBuilder\Raw; + use Pixie\Hydration\Hydrator; use Pixie\QueryBuilder\JoinBuilder; use Pixie\QueryBuilder\QueryObject; use Pixie\QueryBuilder\Transaction; use Pixie\QueryBuilder\WPDBAdapter; +use function mb_strlen; class QueryBuilderHandler { @@ -1068,16 +1068,16 @@ protected function whereNullHandler($key, string $prefix = '', $operator = ''): return $this->{$operator . 'Where'}($this->raw("{$key} IS{$prefix} NULL")); } - /** - * @param string|Raw $key The database column which holds the JSON value - * @param string|Raw|string[] $jsonKey The json key/index to search - * @param string|mixed|null $operator Can be used as value, if 3rd arg not passed - * @param mixed|null $value - * @return static - */ + /** + * @param string|Raw $key The database column which holds the JSON value + * @param string|Raw|string[] $jsonKey The json key/index to search + * @param string|mixed|null $operator Can be used as value, if 3rd arg not passed + * @param mixed|null $value + * @return static + */ public function whereJson($key, $jsonKey, $operator = null, $value = null): self { - // If two params are given then assume operator is = + // If two params are given then assume operator is = if (2 == func_num_args()) { $value = $operator; $operator = '='; diff --git a/src/QueryBuilder/WPDBAdapter.php b/src/QueryBuilder/WPDBAdapter.php index 0b1eade..7f236be 100644 --- a/src/QueryBuilder/WPDBAdapter.php +++ b/src/QueryBuilder/WPDBAdapter.php @@ -6,15 +6,15 @@ use Pixie\Binding; use Pixie\Exception; -use function is_bool; - use Pixie\Connection; -use function is_float; - use Pixie\QueryBuilder\Raw; + use Pixie\QueryBuilder\NestedCriteria; +use function is_bool; +use function is_float; + class WPDBAdapter { /** @@ -62,7 +62,7 @@ public function select(array $statements): array // Wheres list($whereCriteria, $whereBindings) = $this->buildCriteriaWithType($statements, 'wheres', 'WHERE'); - + // Group bys $groupBys = ''; if (isset($statements['groupBys']) && $groupBys = $this->arrayStr($statements['groupBys'], ', ')) { From 439caa2956658803e2e0b64e0434e01a86a6c363 Mon Sep 17 00:00:00 2001 From: Glynn Quelch Date: Wed, 19 Jan 2022 16:49:35 +0000 Subject: [PATCH 51/69] added in check to ensure where JSON takes into account table prefixes when using tableName.columnName --- tests/TestIntegrationWithWPDB.php | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/tests/TestIntegrationWithWPDB.php b/tests/TestIntegrationWithWPDB.php index 4605a59..145541b 100644 --- a/tests/TestIntegrationWithWPDB.php +++ b/tests/TestIntegrationWithWPDB.php @@ -789,5 +789,14 @@ public function testJsonWhere1GenDeep() $this->assertEquals($whereThingFooRaw[0]->string, $whereThingFoo[0]->string); $this->assertEquals($whereThingFooRaw[0]->jsonCol, $whereThingFoo[0]->jsonCol); + + // Check with prefix + $whereThingFooPrefixed = $this->queryBuilderProvider('mock_') + ->table('json') + ->whereJson('json.jsonCol', 'thing', '=', 'foo') + ->get(); + + $this->assertEquals($whereThingFooPrefixed[0]->string, $whereThingFoo[0]->string); + $this->assertEquals($whereThingFooPrefixed[0]->jsonCol, $whereThingFoo[0]->jsonCol); } } From c771d19e76e8a89c9b96504ff88cd0193375e45a Mon Sep 17 00:00:00 2001 From: Glynn Quelch Date: Wed, 19 Jan 2022 16:53:15 +0000 Subject: [PATCH 52/69] Ensure whereJson will work without passing the operator. --- src/QueryBuilder/QueryBuilderHandler.php | 22 +++++++++++----------- tests/TestIntegrationWithWPDB.php | 4 ++-- 2 files changed, 13 insertions(+), 13 deletions(-) diff --git a/src/QueryBuilder/QueryBuilderHandler.php b/src/QueryBuilder/QueryBuilderHandler.php index 3408ae4..152fbf7 100644 --- a/src/QueryBuilder/QueryBuilderHandler.php +++ b/src/QueryBuilder/QueryBuilderHandler.php @@ -9,14 +9,14 @@ use Pixie\Exception; use Pixie\Connection; -use Pixie\QueryBuilder\Raw; +use function mb_strlen; +use Pixie\QueryBuilder\Raw; use Pixie\Hydration\Hydrator; use Pixie\QueryBuilder\JoinBuilder; use Pixie\QueryBuilder\QueryObject; use Pixie\QueryBuilder\Transaction; use Pixie\QueryBuilder\WPDBAdapter; -use function mb_strlen; class QueryBuilderHandler { @@ -796,7 +796,7 @@ public function orHaving($key, $operator, $value) public function where($key, $operator = null, $value = null): self { // If two params are given then assume operator is = - if (2 == func_num_args()) { + if (2 === func_num_args()) { $value = $operator; $operator = '='; } @@ -814,7 +814,7 @@ public function where($key, $operator = null, $value = null): self public function orWhere($key, $operator = null, $value = null): self { // If two params are given then assume operator is = - if (2 == func_num_args()) { + if (2 === func_num_args()) { $value = $operator; $operator = '='; } @@ -832,7 +832,7 @@ public function orWhere($key, $operator = null, $value = null): self public function whereNot($key, $operator = null, $value = null): self { // If two params are given then assume operator is = - if (2 == func_num_args()) { + if (2 === func_num_args()) { $value = $operator; $operator = '='; } @@ -850,7 +850,7 @@ public function whereNot($key, $operator = null, $value = null): self public function orWhereNot($key, $operator = null, $value = null) { // If two params are given then assume operator is = - if (2 == func_num_args()) { + if (2 === func_num_args()) { $value = $operator; $operator = '='; } @@ -950,7 +950,7 @@ protected function whereFunctionCallHandler($key, $function, $operator, $value): public function whereMonth($key, $operator = null, $value = null): self { // If two params are given then assume operator is = - if (2 == func_num_args()) { + if (2 === func_num_args()) { $value = $operator; $operator = '='; } @@ -966,7 +966,7 @@ public function whereMonth($key, $operator = null, $value = null): self public function whereDay($key, $operator = null, $value = null): self { // If two params are given then assume operator is = - if (2 == func_num_args()) { + if (2 === func_num_args()) { $value = $operator; $operator = '='; } @@ -982,7 +982,7 @@ public function whereDay($key, $operator = null, $value = null): self public function whereYear($key, $operator = null, $value = null): self { // If two params are given then assume operator is = - if (2 == func_num_args()) { + if (2 === func_num_args()) { $value = $operator; $operator = '='; } @@ -998,7 +998,7 @@ public function whereYear($key, $operator = null, $value = null): self public function whereDate($key, $operator = null, $value = null): self { // If two params are given then assume operator is = - if (2 == func_num_args()) { + if (2 === func_num_args()) { $value = $operator; $operator = '='; } @@ -1078,7 +1078,7 @@ protected function whereNullHandler($key, string $prefix = '', $operator = ''): public function whereJson($key, $jsonKey, $operator = null, $value = null): self { // If two params are given then assume operator is = - if (2 == func_num_args()) { + if (3 === func_num_args()) { $value = $operator; $operator = '='; } diff --git a/tests/TestIntegrationWithWPDB.php b/tests/TestIntegrationWithWPDB.php index 145541b..0a63f73 100644 --- a/tests/TestIntegrationWithWPDB.php +++ b/tests/TestIntegrationWithWPDB.php @@ -784,7 +784,7 @@ public function testJsonWhere1GenDeep() $whereThingFoo = $this->queryBuilderProvider('mock_') ->table('json') - ->whereJson('jsonCol', 'thing', '=', 'foo') + ->whereJson('jsonCol', 'thing', 'foo') // Assume its '=' ->get(); $this->assertEquals($whereThingFooRaw[0]->string, $whereThingFoo[0]->string); @@ -793,7 +793,7 @@ public function testJsonWhere1GenDeep() // Check with prefix $whereThingFooPrefixed = $this->queryBuilderProvider('mock_') ->table('json') - ->whereJson('json.jsonCol', 'thing', '=', 'foo') + ->whereJson('json.jsonCol', 'thing', '<>', 'bar') // NOT BAR ->get(); $this->assertEquals($whereThingFooPrefixed[0]->string, $whereThingFoo[0]->string); From 00007e65490664da9e3483954be7aecdfbb6c2cf Mon Sep 17 00:00:00 2001 From: Glynn Quelch Date: Wed, 19 Jan 2022 16:53:23 +0000 Subject: [PATCH 53/69] House Keeping --- src/QueryBuilder/QueryBuilderHandler.php | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/QueryBuilder/QueryBuilderHandler.php b/src/QueryBuilder/QueryBuilderHandler.php index 152fbf7..86ce56f 100644 --- a/src/QueryBuilder/QueryBuilderHandler.php +++ b/src/QueryBuilder/QueryBuilderHandler.php @@ -9,14 +9,14 @@ use Pixie\Exception; use Pixie\Connection; -use function mb_strlen; - use Pixie\QueryBuilder\Raw; + use Pixie\Hydration\Hydrator; use Pixie\QueryBuilder\JoinBuilder; use Pixie\QueryBuilder\QueryObject; use Pixie\QueryBuilder\Transaction; use Pixie\QueryBuilder\WPDBAdapter; +use function mb_strlen; class QueryBuilderHandler { From c3ebfbc84dd638431d9b10e8f9906f6168620ebb Mon Sep 17 00:00:00 2001 From: Glynn Quelch Date: Thu, 20 Jan 2022 09:22:01 +0000 Subject: [PATCH 54/69] Added in simpler JSON select using LAarvel syntax --- README.md | 4 +++ src/QueryBuilder/QueryBuilderHandler.php | 43 +++++++++++++++++++++++- tests/TestQueryBuilderSQLGeneration.php | 12 +++++++ 3 files changed, 58 insertions(+), 1 deletion(-) diff --git a/README.md b/README.md index 355c514..862e270 100644 --- a/README.md +++ b/README.md @@ -103,6 +103,7 @@ Library on [Packagist](https://packagist.org/packages/gin0115/pixie-wpdb). - [**Select**](#select) - [Get Easily](#get-easily) - [Multiple Selects](#multiple-selects) + - [Select JSON]() - [Select Distinct](#select-distinct) - [Get All](#get-all) - [Get First Row](#get-first-row) @@ -116,6 +117,9 @@ Library on [Packagist](https://packagist.org/packages/gin0115/pixie-wpdb). - [Where Month](#where-month) - [Where Year](#where-year) - [Grouped Where](#grouped-where) + - [Where JSON]() + -[Where IN JSON]() + -[Where BETWEEN JSON]() - [Group By and Order By](#group-by-and-order-by) - [Having](#having) - [Limit and Offset](#limit-and-offset) diff --git a/src/QueryBuilder/QueryBuilderHandler.php b/src/QueryBuilder/QueryBuilderHandler.php index 86ce56f..84dafaf 100644 --- a/src/QueryBuilder/QueryBuilderHandler.php +++ b/src/QueryBuilder/QueryBuilderHandler.php @@ -674,12 +674,54 @@ public function select($fields): self $fields = func_get_args(); } + foreach ($fields as $field => $alias) { + // If we have a numerical string, skip. + if (is_numeric($field)) { + continue; + } + + // If we have a JSON expression + if ($this->isJsonExpression($field)) { + // Add using JSON select. + $this->castToJsonSelect($field, $alias); + unset($fields[$field]); + continue; + } + } + $fields = $this->addTablePrefix($fields); $this->addStatement('selects', $fields); return $this; } + /** + * Checks if the passed expression is for JSON + * this->denotes->json + * + * @param string $expression + * @return bool + */ + protected function isJsonExpression(string $expression): bool + { + return 2 <= count(explode('->', $expression)); + } + + /** + * Casts a select to JSON based on -> in column name. + * + * @param string $keys + * @param string|null $alias + * @return self + */ + public function castToJsonSelect(string $keys, ?string $alias): self + { + $parts = explode('->', $keys); + $field = $parts[0]; + unset($parts[0]); + return $this->selectJson($field, $parts, $alias); + } + /** * @param string|string[]|Raw[]|array $fields * @@ -1099,7 +1141,6 @@ public function whereJson($key, $jsonKey, $operator = null, $value = null): self // Add any possible prefixes to the key $key = $this->addTablePrefix($key, true); - // return $this->where(new Raw('JSON_EXTRACT(jsonCol,"$.thing")'), '=', 'foo') return $this->where( new Raw("JSON_UNQUOTE(JSON_EXTRACT({$key}, \"$.{$jsonKey}\"))"), $operator, diff --git a/tests/TestQueryBuilderSQLGeneration.php b/tests/TestQueryBuilderSQLGeneration.php index 56ad2b2..517b9e0 100644 --- a/tests/TestQueryBuilderSQLGeneration.php +++ b/tests/TestQueryBuilderSQLGeneration.php @@ -853,4 +853,16 @@ public function testJsonSelectUsingRawValues(): void $builder->getQuery()->getRawSql() ); } + + /** @testdox It should be possible to do a select from a JSON value, using column->jsonKey1->jsonKey2 */ + public function testSelectWithJSONWithAlias(): void + { + $builder = $this->queryBuilderProvider() + ->table('TableName') + ->select(['column->foo->bar' => 'alias']); + + $expected = 'SELECT JSON_UNQUOTE(JSON_EXTRACT(column, "$.foo.bar")) as alias FROM TableName'; + $this->assertEquals($expected, $builder->getQuery()->getRawSql()); + dump($builder->getQuery()->getRawSql()); + } } From b3a0b0f00d5a9639f36f5e793fced1a8c43886f2 Mon Sep 17 00:00:00 2001 From: Glynn Quelch Date: Thu, 20 Jan 2022 09:33:49 +0000 Subject: [PATCH 55/69] House Keeping --- README.md | 23 +++++++++++++++++++++++ 1 file changed, 23 insertions(+) diff --git a/README.md b/README.md index 862e270..72ca37e 100644 --- a/README.md +++ b/README.md @@ -218,6 +218,29 @@ $query = QB::table('my_table')->select('*'); Using select method multiple times `select('a')->select('b')` will also select `a` and `b`. Can be useful if you want to do conditional selects (within a PHP `if`). +### Select Alias +```php +->select(['column'=>'alias']) +``` +This would result in `SELECT column as alias` as part of the query. + +### Select JSON +There are 2 ways to express selecting a value from within a stored JSON object. +`{"someKey": "someValue","someArray":[1,2,3], "someObj":{"a":"apple","b":"banana"}}` + +#### Using Larvel style selectors. +```php +->select(['column->someObj->a' => 'jsonAlias']) +``` +This would return results with `{jsonAlias => "apple"}` +To access arrays values use `->select(['column->someArray[1]' => 'jsonAlias'])` + +> Please note using Laravel style selectors without an alias, will result in an exception being thrown. + +#### Using selectJson() helper +```php +->selectJson('column', ['someObj', 'a]'], 'jsonAlias') +``` #### Select Distinct ```PHP From 122b9c96eb14941cd2c777a7546a398208b2a478 Mon Sep 17 00:00:00 2001 From: Glynn Quelch Date: Thu, 20 Jan 2022 09:35:00 +0000 Subject: [PATCH 56/69] House Keeping --- README.md | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index 72ca37e..f268afb 100644 --- a/README.md +++ b/README.md @@ -103,7 +103,7 @@ Library on [Packagist](https://packagist.org/packages/gin0115/pixie-wpdb). - [**Select**](#select) - [Get Easily](#get-easily) - [Multiple Selects](#multiple-selects) - - [Select JSON]() + - [Select JSON](#select-json) - [Select Distinct](#select-distinct) - [Get All](#get-all) - [Get First Row](#get-first-row) @@ -239,8 +239,11 @@ To access arrays values use `->select(['column->someArray[1]' => 'jsonAlias'])` #### Using selectJson() helper ```php -->selectJson('column', ['someObj', 'a]'], 'jsonAlias') +->selectJson('column', ['someObj', 'a'], 'jsonAlias') ``` +This would return results with `{jsonAlias => "apple"}` + +> If no alias is passed, the column value will be set as `json_a`. The last selector is prepended with `json_` #### Select Distinct ```PHP From 7524576626ca72f63a3c418d78d804f31d9b7e53 Mon Sep 17 00:00:00 2001 From: Glynn Quelch Date: Thu, 20 Jan 2022 09:35:31 +0000 Subject: [PATCH 57/69] House Keeping --- README.md | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/README.md b/README.md index f268afb..9b64925 100644 --- a/README.md +++ b/README.md @@ -232,7 +232,7 @@ There are 2 ways to express selecting a value from within a stored JSON object. ```php ->select(['column->someObj->a' => 'jsonAlias']) ``` -This would return results with `{jsonAlias => "apple"}` +This would return results with `{jsonAlias => "apple"}` To access arrays values use `->select(['column->someArray[1]' => 'jsonAlias'])` > Please note using Laravel style selectors without an alias, will result in an exception being thrown. @@ -241,9 +241,9 @@ To access arrays values use `->select(['column->someArray[1]' => 'jsonAlias'])` ```php ->selectJson('column', ['someObj', 'a'], 'jsonAlias') ``` -This would return results with `{jsonAlias => "apple"}` +This would return results with `{jsonAlias => "apple"}` -> If no alias is passed, the column value will be set as `json_a`. The last selector is prepended with `json_` +> If no alias is passed, the column value will be set as `json_a`. The last selector is prepended with `json_` #### Select Distinct ```PHP From 5e6c5de0c6b5fd5cc7540f53bbb48afbd8e4cf1d Mon Sep 17 00:00:00 2001 From: Glynn Quelch Date: Thu, 20 Jan 2022 11:22:23 +0000 Subject: [PATCH 58/69] House Keeping --- README.md | 21 +++++++++++--------- src/QueryBuilder/QueryBuilderHandler.php | 25 +++++++++++++++--------- tests/TestQueryBuilderHandler.php | 11 +++++++++++ 3 files changed, 39 insertions(+), 18 deletions(-) diff --git a/README.md b/README.md index 9b64925..a8b051f 100644 --- a/README.md +++ b/README.md @@ -26,6 +26,12 @@ It has some advanced features like: - Nested Queries - Multiple Database Connections. +Additional features added to this version of Pixie + - JSON Support (Select, Where) + - Aggregation methods (Min, Max, Average & Sum) + - Custom Model Hydration + - Date based Where (Month, Day, Year & Date) + The syntax is quite similar to Laravel's query builder. ## Example @@ -81,13 +87,7 @@ There are many advanced options which are documented below. Sold? Let's install. Pixie uses [Composer](http://getcomposer.org/doc/00-intro.md#installation-nix) to make things easy. -Learn to use composer and add this to require section (in your composer.json): - - "gin0115/pixie-wpdb": "1.*@dev" - -And run: - - composer update +To install run `composer require gin0115/pixie-wpdb` Library on [Packagist](https://packagist.org/packages/gin0115/pixie-wpdb). @@ -103,6 +103,7 @@ Library on [Packagist](https://packagist.org/packages/gin0115/pixie-wpdb). - [**Select**](#select) - [Get Easily](#get-easily) - [Multiple Selects](#multiple-selects) + - [Select Alias](#select-alias) - [Select JSON](#select-json) - [Select Distinct](#select-distinct) - [Get All](#get-all) @@ -235,7 +236,7 @@ There are 2 ways to express selecting a value from within a stored JSON object. This would return results with `{jsonAlias => "apple"}` To access arrays values use `->select(['column->someArray[1]' => 'jsonAlias'])` -> Please note using Laravel style selectors without an alias, will result in an exception being thrown. +> Please note using Laravel style selectors without an alias, will result in an exception being thrown. example `->select('column->someObj->a')` #### Using selectJson() helper ```php @@ -243,7 +244,9 @@ To access arrays values use `->select(['column->someArray[1]' => 'jsonAlias'])` ``` This would return results with `{jsonAlias => "apple"}` -> If no alias is passed, the column value will be set as `json_a`. The last selector is prepended with `json_` +> If no alias is passed, the column value will be set as `json_a`. The last selector is prepended with `json_` +**Example ** +`->selectJson('column', ['someObj', 'a'])` would return `{json_a => "apple"}` #### Select Distinct ```PHP diff --git a/src/QueryBuilder/QueryBuilderHandler.php b/src/QueryBuilder/QueryBuilderHandler.php index 84dafaf..d4c6698 100644 --- a/src/QueryBuilder/QueryBuilderHandler.php +++ b/src/QueryBuilder/QueryBuilderHandler.php @@ -9,14 +9,14 @@ use Pixie\Exception; use Pixie\Connection; -use Pixie\QueryBuilder\Raw; +use function mb_strlen; +use Pixie\QueryBuilder\Raw; use Pixie\Hydration\Hydrator; use Pixie\QueryBuilder\JoinBuilder; use Pixie\QueryBuilder\QueryObject; use Pixie\QueryBuilder\Transaction; use Pixie\QueryBuilder\WPDBAdapter; -use function mb_strlen; class QueryBuilderHandler { @@ -675,11 +675,6 @@ public function select($fields): self } foreach ($fields as $field => $alias) { - // If we have a numerical string, skip. - if (is_numeric($field)) { - continue; - } - // If we have a JSON expression if ($this->isJsonExpression($field)) { // Add using JSON select. @@ -687,10 +682,22 @@ public function select($fields): self unset($fields[$field]); continue; } + + // If no alias passed, but field is for JSON. thrown an exception. + if (is_numeric($field) && $this->isJsonExpression($alias)) { + throw new Exception("An alias must be used if you wish to select from JSON Object", 1); + } + + // Treat each array as a single table, to retain order added + $field = is_numeric($field) + ? $field = $alias // If single colum + : $field = [$field => $alias]; // Has alias + + $field = $this->addTablePrefix($field); + $this->addStatement('selects', $field); } - $fields = $this->addTablePrefix($fields); - $this->addStatement('selects', $fields); + return $this; } diff --git a/tests/TestQueryBuilderHandler.php b/tests/TestQueryBuilderHandler.php index c348c81..203fd94 100644 --- a/tests/TestQueryBuilderHandler.php +++ b/tests/TestQueryBuilderHandler.php @@ -255,4 +255,15 @@ public function testJoinUsingThrowsIfMultipleTableSelected(): void ->joinUsing('bar', 'id') ->get(); } + + /** @testdox When attemptning to use a JSON expression as a select, using select(). An alias must be supplied, or an exception should be thrown. */ + public function testMustUseAliasWithJsonSelect(): void + { + $this->expectExceptionMessage('An alias must be used if you wish to select from JSON Object'); + $this->expectException(Exception::class); + $this->queryBuilderProvider() + ->table('a') + ->select('a->b') + ->first(); + } } From 23abc02078eb515bd10f59b18c66ea4d32ca136b Mon Sep 17 00:00:00 2001 From: Glynn Quelch Date: Thu, 20 Jan 2022 11:22:34 +0000 Subject: [PATCH 59/69] House Keeping --- src/QueryBuilder/QueryBuilderHandler.php | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/QueryBuilder/QueryBuilderHandler.php b/src/QueryBuilder/QueryBuilderHandler.php index d4c6698..36b22cb 100644 --- a/src/QueryBuilder/QueryBuilderHandler.php +++ b/src/QueryBuilder/QueryBuilderHandler.php @@ -9,14 +9,14 @@ use Pixie\Exception; use Pixie\Connection; -use function mb_strlen; - use Pixie\QueryBuilder\Raw; + use Pixie\Hydration\Hydrator; use Pixie\QueryBuilder\JoinBuilder; use Pixie\QueryBuilder\QueryObject; use Pixie\QueryBuilder\Transaction; use Pixie\QueryBuilder\WPDBAdapter; +use function mb_strlen; class QueryBuilderHandler { From b21bb2dad18c6a4787cccef5322158aba15ea89c Mon Sep 17 00:00:00 2001 From: Glynn Quelch Date: Thu, 20 Jan 2022 11:28:39 +0000 Subject: [PATCH 60/69] Added in test to ensure a tablename is prefixed, if used in a json->select->expression --- tests/TestQueryBuilderHandler.php | 2 +- tests/TestQueryBuilderSQLGeneration.php | 12 +++++++++++- 2 files changed, 12 insertions(+), 2 deletions(-) diff --git a/tests/TestQueryBuilderHandler.php b/tests/TestQueryBuilderHandler.php index 203fd94..5312301 100644 --- a/tests/TestQueryBuilderHandler.php +++ b/tests/TestQueryBuilderHandler.php @@ -256,7 +256,7 @@ public function testJoinUsingThrowsIfMultipleTableSelected(): void ->get(); } - /** @testdox When attemptning to use a JSON expression as a select, using select(). An alias must be supplied, or an exception should be thrown. */ + /** @testdox When attempting to use a JSON expression as a select, using select(). An alias must be supplied, or an exception should be thrown. */ public function testMustUseAliasWithJsonSelect(): void { $this->expectExceptionMessage('An alias must be used if you wish to select from JSON Object'); diff --git a/tests/TestQueryBuilderSQLGeneration.php b/tests/TestQueryBuilderSQLGeneration.php index 517b9e0..76547e4 100644 --- a/tests/TestQueryBuilderSQLGeneration.php +++ b/tests/TestQueryBuilderSQLGeneration.php @@ -863,6 +863,16 @@ public function testSelectWithJSONWithAlias(): void $expected = 'SELECT JSON_UNQUOTE(JSON_EXTRACT(column, "$.foo.bar")) as alias FROM TableName'; $this->assertEquals($expected, $builder->getQuery()->getRawSql()); - dump($builder->getQuery()->getRawSql()); + } + + /** @testdox It should be possible to use table.column and have the prefix added to the table, even if used as JSON Select query */ + public function testAllColumnsInJSONSelectWithTableDotColumnShouldHavePrefixAdded() + { + $builder = $this->queryBuilderProvider('pr_') + ->table('table') + ->select(['table.column->foo->bar' => 'alias']); + + $expected = 'SELECT JSON_UNQUOTE(JSON_EXTRACT(pr_table.column, "$.foo.bar")) as alias FROM pr_table'; + $this->assertEquals($expected, $builder->getQuery()->getRawSql()); } } From fec7f74edfb3f282d829dbc7c7544ea06bd06e30 Mon Sep 17 00:00:00 2001 From: Glynn Quelch Date: Thu, 20 Jan 2022 11:32:14 +0000 Subject: [PATCH 61/69] House Keeping --- README.md | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/README.md b/README.md index a8b051f..5eeea67 100644 --- a/README.md +++ b/README.md @@ -118,15 +118,15 @@ Library on [Packagist](https://packagist.org/packages/gin0115/pixie-wpdb). - [Where Month](#where-month) - [Where Year](#where-year) - [Grouped Where](#grouped-where) - - [Where JSON]() - -[Where IN JSON]() - -[Where BETWEEN JSON]() + - [Where JSON]() + - [Where IN JSON]() + - [Where BETWEEN JSON]() - [Group By and Order By](#group-by-and-order-by) - [Having](#having) - [Limit and Offset](#limit-and-offset) - [Join](#join) - - [Join Using](#join-using) - - [Multiple Join Criteria](#multiple-join-criteria) + - [Join Using](#join-using) + - [Multiple Join Criteria](#multiple-join-criteria) - [Raw Query](#raw-query) - [Raw Expressions](#raw-expressions) - [Value Binding](#value-binding) From 831fa9b21e1213fd466aa7754a0b983d570babed Mon Sep 17 00:00:00 2001 From: Glynn Quelch Date: Thu, 20 Jan 2022 12:53:30 +0000 Subject: [PATCH 62/69] added config helper and docs --- README.md | 22 ++++++++++++++ src/Connection.php | 52 +++++++++++++++++++++++++++++-- tests/TestConnection.php | 66 ++++++++++++++++++++++++++++++++++++++++ 3 files changed, 137 insertions(+), 3 deletions(-) diff --git a/README.md b/README.md index 5eeea67..c596558 100644 --- a/README.md +++ b/README.md @@ -163,6 +163,28 @@ new \Pixie\Connection($wpdb, $config, 'QB'); $query = QB::table('my_table')->where('name', '=', 'Sana'); ``` +### Config + +It is possible to conigure the connection used by your instance of the query builder. + +Values + +| Key | Constant | Value | Description | +| ----------- | ----------- |----------- |----------- | +| prefix | Connection::PREFIX | STRING | Custom table prefix (will ignore WPDB prefix)| +| use_wpdb_prefix | Connection::USE_WPDB_PREFIX | BOOL | If true will use WPDB prefix and ignore custom prefix +| clone_wpdb | Connection::CLONE_WPDB | BOOL | If true, will clone WPDB to not use GLOBAL instance| +| show_errors | Connection::SHOW_ERRORS | BOOL | If set to true will configure WPDB to show/hide errors | + +```php +$config = [ + Connection::USE_WPDB_PREFIX => true, + Connection::CLONE_WPDB => true, + Connection::SHOW_ERRORS => false, +]; +``` +> It is advise to use the class constants over string keys, to avoid BC breakages later on + ### Alias When you create a connection: ```PHP diff --git a/src/Connection.php b/src/Connection.php index 946cd7a..b892af7 100644 --- a/src/Connection.php +++ b/src/Connection.php @@ -2,15 +2,21 @@ namespace Pixie; +use wpdb; use Exception; +use Viocon\Container; use Pixie\AliasFacade; use Pixie\EventHandler; use Pixie\QueryBuilder\QueryBuilderHandler; -use Viocon\Container; -use wpdb; class Connection { + /** Config keys */ + public const CLONE_WPDB = 'clone_wpdb'; + public const PREFIX = 'prefix'; + public const SHOW_ERRORS = 'show_errors'; + public const USE_WPDB_PREFIX = 'use_wpdb_prefix'; + /** * @var Container */ @@ -53,8 +59,9 @@ public function __construct( ?string $alias = null, ?Container $container = null ) { - $this->dbInstance = $wpdb; + $this->setAdapterConfig($adapterConfig); + $this->dbInstance = $this->configureWpdb($wpdb); $this->container = $container ?? new Container(); $this->eventHandler = $this->container->build(EventHandler::class); @@ -69,6 +76,45 @@ public function __construct( } } + /** + * Configures the instance of WPDB based on adaptor config values. + * + * @param \wpdb $wpdb + * @return \wpdb + */ + protected function configureWpdb(wpdb $wpdb): wpdb + { + // Maybe clone instance. + if ( + array_key_exists(self::CLONE_WPDB, $this->adapterConfig) + && true === $this->adapterConfig[self::CLONE_WPDB] + ) { + $wpdb = clone $wpdb; + } + + // Maybe set the prefix to WPDB's. + if ( + array_key_exists(self::USE_WPDB_PREFIX, $this->adapterConfig) + && 0 < \mb_strlen($this->adapterConfig[self::USE_WPDB_PREFIX]) + ) { + $this->adapterConfig[self::PREFIX] = $wpdb->prefix; + } + + // Maybe configure errors + if (array_key_exists(self::SHOW_ERRORS, $this->adapterConfig)) { + // Based in its value. + if (true === (bool) $this->adapterConfig[self::SHOW_ERRORS]) { + $wpdb->show_errors(true); + $wpdb->suppress_errors(false); + } else { + $wpdb->show_errors(false); + $wpdb->suppress_errors(true); + } + } + + return $wpdb; + } + /** * Create an easily accessible query builder alias * diff --git a/tests/TestConnection.php b/tests/TestConnection.php index c880b23..92ec9f7 100644 --- a/tests/TestConnection.php +++ b/tests/TestConnection.php @@ -76,4 +76,70 @@ public function testGetQueryBuilder(): void $this->assertInstanceOf(QueryBuilderHandler::class, $builder); $this->assertSame($builder->getConnection(), $connection); } + + /** @testdox It should be possible to configure the connection to use a cloned instance of WPDB and have all options added */ + public function testConnectionConfigClonedWPDBInstance(): void + { + $wpdb = new Logable_WPDB(); + + // As true + $connection = new Connection($wpdb, [Connection::CLONE_WPDB => true]); + $connectionInstance = $connection->getDbInstance(); + $this->assertNotSame($wpdb, $connectionInstance); + + $connection2 = new Connection($wpdb, [Connection::CLONE_WPDB => false]); + $connection2Instance = $connection2->getDbInstance(); + $this->assertSame($wpdb, $connection2Instance); + } + + /** @testdox It should be possible to set the connection to use the prefix from WPDB and should a custom prefix also be added, WPDB will take priority. */ + public function testCanUseWPDBPrefixWithConnection(): void + { + // use WPDB Prefix TRUE, to set form WPDB. + $wpdb = new Logable_WPDB(); + $wpdb->prefix = 'TEST_'; + $connection = new Connection($wpdb, [Connection::USE_WPDB_PREFIX => true]); + $this->assertEquals('TEST_', $connection->getAdapterConfig()['prefix']); + + // do not use WPDB Prefix FALSE to not set. + $connection2 = new Connection($wpdb, [Connection::USE_WPDB_PREFIX => false]); + $this->assertArrayNotHasKey('prefix', $connection2->getAdapterConfig()); + + // using WPDB instance should overrule custom prefix + $connection3 = new Connection($wpdb, [ + Connection::USE_WPDB_PREFIX => true, + Connection::PREFIX => 'bar_' + ]); + $this->assertEquals('TEST_', $connection3->getAdapterConfig()['prefix']); + $this->assertNotEquals('bar_', $connection3->getAdapterConfig()['prefix']); + } + + /** @testdox It should be possible to configure if the connection should show or hide errors */ + public function testShowHideWPDBErrorsConfig(): void + { + // As is defined in WPDB, even with clone + $wpdb1 = new Logable_WPDB(); + $wpdb1->show_errors(true); + $wpdb1->suppress_errors(false); + $connection1 = new Connection($wpdb1, [Connection::CLONE_WPDB => true]); + $connection1WPDBInstance = $connection1->getDbInstance(); + $this->assertTrue($connection1WPDBInstance->show_errors); + $this->assertFalse($connection1WPDBInstance->suppress_errors); + + // Defined to hide errors. + $wpdb2 = new Logable_WPDB(); + $wpdb2->show_errors(); + $connection2 = new Connection($wpdb2, [Connection::SHOW_ERRORS => false]); + $connection2WPDBInstance = $connection2->getDbInstance(); + $this->assertFalse($connection2WPDBInstance->show_errors); + $this->assertTrue($connection2WPDBInstance->suppress_errors); + + // Defined to show errors + $wpdb3 = new Logable_WPDB(); + $wpdb3->show_errors(); + $connection3 = new Connection($wpdb3, [Connection::SHOW_ERRORS => true]); + $connection3WPDBInstance = $connection3->getDbInstance(); + $this->assertTrue($connection3WPDBInstance->show_errors); + $this->assertFalse($connection3WPDBInstance->suppress_errors); + } } From 530188fa52af06eb53c4dbf508ce6d6b5a6618b8 Mon Sep 17 00:00:00 2001 From: Glynn Quelch Date: Thu, 20 Jan 2022 12:54:11 +0000 Subject: [PATCH 63/69] House Keeping --- README.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index c596558..abd5c2f 100644 --- a/README.md +++ b/README.md @@ -41,7 +41,7 @@ require 'vendor/autoload.php'; // Create a connection, once only. $config = [ - 'prefix' => 'cb_', // Table prefix, optional + Connection::PREFIX => 'cb_', // Table prefix, optional ]; // Get the current (gloabl) WPDB instance, or create a custom one @@ -155,7 +155,7 @@ Pixie supports three database drivers, MySQL, SQLite and PostgreSQL. You can spe // Make sure you have Composer's autoload file included require 'vendor/autoload.php'; -$config = array( 'prefix' => 'cb_'); // Table prefix, optional +$config = [Connection::PREFIX => 'cb_']; // Table prefix, optional new \Pixie\Connection($wpdb, $config, 'QB'); From 7d946e843e4cb3ddcf5ed1dc63680c0578286375 Mon Sep 17 00:00:00 2001 From: Glynn Quelch Date: Thu, 20 Jan 2022 12:54:46 +0000 Subject: [PATCH 64/69] House Keeping --- src/Connection.php | 1 - 1 file changed, 1 deletion(-) diff --git a/src/Connection.php b/src/Connection.php index b892af7..1d4ef4b 100644 --- a/src/Connection.php +++ b/src/Connection.php @@ -59,7 +59,6 @@ public function __construct( ?string $alias = null, ?Container $container = null ) { - $this->setAdapterConfig($adapterConfig); $this->dbInstance = $this->configureWpdb($wpdb); From abe4532a4420ddabdf2c3b279182302fc8348507 Mon Sep 17 00:00:00 2001 From: Glynn Quelch Date: Thu, 20 Jan 2022 12:59:33 +0000 Subject: [PATCH 65/69] House Keeping --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index abd5c2f..bfe2cd7 100644 --- a/README.md +++ b/README.md @@ -96,9 +96,9 @@ Library on [Packagist](https://packagist.org/packages/gin0115/pixie-wpdb). ### Table of Contents - [Connection](#connection) + - [Connection Config](#config) - [Alias](#alias) - [Multiple Connection](#alias) - - [SQLite and PostgreSQL Config Sample](#sqlite-and-postgresql-config-sample) - [Query](#query) - [**Select**](#select) - [Get Easily](#get-easily) From ef16f7727380c885e965ae1a3ea94fddda3a1dc5 Mon Sep 17 00:00:00 2001 From: Glynn Quelch Date: Thu, 20 Jan 2022 13:01:23 +0000 Subject: [PATCH 66/69] House Keeping --- README.md | 22 +++++++++++----------- 1 file changed, 11 insertions(+), 11 deletions(-) diff --git a/README.md b/README.md index bfe2cd7..5cbcf92 100644 --- a/README.md +++ b/README.md @@ -150,18 +150,8 @@ Library on [Packagist](https://packagist.org/packages/gin0115/pixie-wpdb). ___ ## Connection -Pixie supports three database drivers, MySQL, SQLite and PostgreSQL. You can specify the driver during connection and the associated configuration when creating a new connection. You can also create multiple connections, but you can use alias for only one connection at a time.; -```PHP -// Make sure you have Composer's autoload file included -require 'vendor/autoload.php'; - -$config = [Connection::PREFIX => 'cb_']; // Table prefix, optional - -new \Pixie\Connection($wpdb, $config, 'QB'); +Pixie WPDB supports only WPDB (WordPress DataBase). You can also create multiple connections, but you can use alias for only one connection at a time.; -// Run query -$query = QB::table('my_table')->where('name', '=', 'Sana'); -``` ### Config @@ -183,6 +173,16 @@ $config = [ Connection::SHOW_ERRORS => false, ]; ``` + +```PHP +// Make sure you have Composer's autoload file included +require 'vendor/autoload.php'; + +new \Pixie\Connection($wpdb, $config, 'QB'); + +// Run query +$query = QB::table('my_table')->where('name', '=', 'Sana'); +``` > It is advise to use the class constants over string keys, to avoid BC breakages later on ### Alias From 2f83b92a68c24c3e059472ff3561201150a37c50 Mon Sep 17 00:00:00 2001 From: Glynn Quelch Date: Thu, 20 Jan 2022 13:02:13 +0000 Subject: [PATCH 67/69] House Keeping --- README.md | 178 ++++++++++++++++++++++++++++++++++++++---------------- 1 file changed, 125 insertions(+), 53 deletions(-) diff --git a/README.md b/README.md index 5cbcf92..b134eb6 100644 --- a/README.md +++ b/README.md @@ -2,20 +2,18 @@ ## WIP! - **This project is Not Actively Maintained but most of the features are fully working and there are no major security issues, I'm just not giving it much time.** - # Pixie Query Builder +![alt text](https://img.shields.io/badge/Current_Version-0.1.0-yellow.svg?style=flat " ") -![alt text](https://img.shields.io/badge/Current_Version-0.1.0-yellow.svg?style=flat " ") + [![Open Source Love](https://badges.frapsoft.com/os/mit/mit.svg?v=102)]() ![](https://github.com/gin0115/pixie-wpdb/workflows/GitHub_CI/badge.svg " ") [![codecov](https://codecov.io/gh/gin0115/pixie-wpdb/branch/master/graph/badge.svg?token=4yEceIaSFP)](https://codecov.io/gh/gin0115/pixie-wpdb) [![Scrutinizer Code Quality](https://scrutinizer-ci.com/g/gin0115/pixie-wpdb/badges/quality-score.png?b=master)](https://scrutinizer-ci.com/g/gin0115/pixie-wpdb/?branch=master) - A lightweight, expressive, framework agnostic query builder for PHP it can also be referred as a Database Abstraction Layer. Pixie supports MySQL, SQLite and PostgreSQL and it takes care of query sanitization, table prefixing and many other things with a unified API. It has some advanced features like: @@ -35,6 +33,7 @@ Additional features added to this version of Pixie The syntax is quite similar to Laravel's query builder. ## Example + ```PHP // Make sure you have Composer's autoload file included require 'vendor/autoload.php'; @@ -56,6 +55,7 @@ new \Pixie\Connection($wpdb, $config, $alias); **Simple Query:** The query below returns the row where id = 3, null if no rows. + ```PHP $row = QB::table('my_table')->find(3); ``` @@ -80,7 +80,6 @@ QB::registerEvent('before-select', 'users', function($qb) }); ``` - There are many advanced options which are documented below. Sold? Let's install. ## Installation @@ -117,10 +116,12 @@ Library on [Packagist](https://packagist.org/packages/gin0115/pixie-wpdb). - [Where Day](#where-day) - [Where Month](#where-month) - [Where Year](#where-year) + - [Grouped Where](#grouped-where) - [Where JSON]() - [Where IN JSON]() - [Where BETWEEN JSON]() + - [Group By and Order By](#group-by-and-order-by) - [Having](#having) - [Limit and Offset](#limit-and-offset) @@ -150,8 +151,20 @@ Library on [Packagist](https://packagist.org/packages/gin0115/pixie-wpdb). ___ ## Connection -Pixie WPDB supports only WPDB (WordPress DataBase). You can also create multiple connections, but you can use alias for only one connection at a time.; +Pixie WPDB supports only WPDB (WordPress DataBase). You can also create multiple connections, but you can use alias for only one connection at a time.; + +```PHP +// Make sure you have Composer's autoload file included +require 'vendor/autoload.php'; + +$config = [Connection::PREFIX => 'cb_']; // full config options below. + +new \Pixie\Connection($wpdb, $config, 'QB'); + +// Run query +$query = QB::table('my_table')->where('name', '=', 'Sana'); +``` ### Config @@ -161,11 +174,12 @@ Values | Key | Constant | Value | Description | | ----------- | ----------- |----------- |----------- | -| prefix | Connection::PREFIX | STRING | Custom table prefix (will ignore WPDB prefix)| -| use_wpdb_prefix | Connection::USE_WPDB_PREFIX | BOOL | If true will use WPDB prefix and ignore custom prefix -| clone_wpdb | Connection::CLONE_WPDB | BOOL | If true, will clone WPDB to not use GLOBAL instance| -| show_errors | Connection::SHOW_ERRORS | BOOL | If set to true will configure WPDB to show/hide errors | +| prefix | Connection:: PREFIX | STRING | Custom table prefix (will ignore WPDB prefix)| +| use_wpdb_prefix | Connection:: USE_WPDB_PREFIX | BOOL | If true will use WPDB prefix and ignore custom prefix +| clone_wpdb | Connection:: CLONE_WPDB | BOOL | If true, will clone WPDB to not use GLOBAL instance| +| show_errors | Connection:: SHOW_ERRORS | BOOL | If set to true will configure WPDB to show/hide errors | + ```php $config = [ Connection::USE_WPDB_PREFIX => true, @@ -174,23 +188,17 @@ $config = [ ]; ``` -```PHP -// Make sure you have Composer's autoload file included -require 'vendor/autoload.php'; - -new \Pixie\Connection($wpdb, $config, 'QB'); - -// Run query -$query = QB::table('my_table')->where('name', '=', 'Sana'); -``` > It is advise to use the class constants over string keys, to avoid BC breakages later on ### Alias + When you create a connection: + ```PHP new \Pixie\Connection($wpdb, $config, 'MyAlias'); ``` -`MyAlias` is the name for the class alias you want to use (like `MyAlias::table(...)`), you can use whatever name (with Namespace also, `MyNamespace\\MyClass`) you like or you may skip it if you don't need an alias. Alias gives you the ability to easily access the QueryBuilder class across your application. + +`MyAlias` is the name for the class alias you want to use (like `MyAlias::table(...)` ), you can use whatever name (with Namespace also, `MyNamespace\\MyClass` ) you like or you may skip it if you don't need an alias. Alias gives you the ability to easily access the QueryBuilder class across your application. When not using an alias you can instantiate the QueryBuilder handler separately, helpful for Dependency Injection and Testing. @@ -206,83 +214,105 @@ var_dump($query->get()); `$connection` here is optional, if not given it will always associate itself to the first connection, but it can be useful when you have multiple database connections. ## Query -You **must** use `table()` method before every query, except raw `query()`. + +You **must** use `table()` method before every query, except raw `query()` . To select from multiple tables you can use the variadic nature of the method + ```PHP QB::table('mytable1', 'mytable2'); ``` - ### Get Easily + The query below returns the (first) row where id = 3, null if no rows. + ```PHP $row = QB::table('my_table')->find(3); ``` -Access your row like, `echo $row->name`. If your field name is not `id` then pass the field name as second parameter `QB::table('my_table')->find(3, 'person_id');`. + +Access your row like, `echo $row->name` . If your field name is not `id` then pass the field name as second parameter `QB::table('my_table')->find(3, 'person_id');` . The query below returns the all rows where name = 'Sana', null if no rows. + ```PHP $result = QB::table('my_table')->findAll('name', 'Sana'); ``` + The query below will either return the row or throw a `Pixie\Exception` if no result found. + ```PHP $result = QB::table('my_table')->findOrFail('name', 'Mark'); ``` ### Select + ```PHP $query = QB::table('my_table')->select('*'); ``` #### Multiple Selects + ```PHP ->select('mytable.myfield1', 'mytable.myfield2', 'another_table.myfield3'); ``` -Using select method multiple times `select('a')->select('b')` will also select `a` and `b`. Can be useful if you want to do conditional selects (within a PHP `if`). +Using select method multiple times `select('a')->select('b')` will also select `a` and `b` . Can be useful if you want to do conditional selects (within a PHP `if` ). ### Select Alias + ```php ->select(['column'=>'alias']) ``` + This would result in `SELECT column as alias` as part of the query. ### Select JSON + There are 2 ways to express selecting a value from within a stored JSON object. -`{"someKey": "someValue","someArray":[1,2,3], "someObj":{"a":"apple","b":"banana"}}` + `{"someKey": "someValue","someArray":[1,2,3], "someObj":{"a":"apple","b":"banana"}}` #### Using Larvel style selectors. + ```php ->select(['column->someObj->a' => 'jsonAlias']) ``` -This would return results with `{jsonAlias => "apple"}` + +This would return results with `{jsonAlias => "apple"}` + To access arrays values use `->select(['column->someArray[1]' => 'jsonAlias'])` > Please note using Laravel style selectors without an alias, will result in an exception being thrown. example `->select('column->someObj->a')` #### Using selectJson() helper + ```php ->selectJson('column', ['someObj', 'a'], 'jsonAlias') ``` -This would return results with `{jsonAlias => "apple"}` -> If no alias is passed, the column value will be set as `json_a`. The last selector is prepended with `json_` +This would return results with `{jsonAlias => "apple"}` + +> If no alias is passed, the column value will be set as `json_a` . The last selector is prepended with `json_` + **Example ** `->selectJson('column', ['someObj', 'a'])` would return `{json_a => "apple"}` #### Select Distinct + ```PHP ->selectDistinct(array('mytable.myfield1', 'mytable.myfield2')); ``` - #### Get All + Return an array. + ```PHP $query = QB::table('my_table')->where('name', '=', 'Sana'); $result = $query->get(); ``` + You can loop through it like: + ```PHP foreach ($result as $row) { echo $row->name; @@ -290,21 +320,24 @@ foreach ($result as $row) { ``` #### Get First Row + ```PHP $query = QB::table('my_table')->where('name', '=', 'Sana'); $row = $query->first(); ``` -Returns the first row, or null if there is no record. Using this method you can also make sure if a record exists. Access these like `echo $row->name`. +Returns the first row, or null if there is no record. Using this method you can also make sure if a record exists. Access these like `echo $row->name` . #### Get Rows Count + ```PHP $query = QB::table('my_table')->where('name', '=', 'Sana'); $query->count(); ``` ### Where -Basic syntax is `(fieldname, operator, value)`, if you give two parameters then `=` operator is assumed. So `where('name', 'usman')` and `where('name', '=', 'usman')` is the same. + +Basic syntax is `(fieldname, operator, value)` , if you give two parameters then `=` operator is assumed. So `where('name', 'usman')` and `where('name', '=', 'usman')` is the same. ```PHP QB::table('my_table') @@ -315,8 +348,8 @@ QB::table('my_table') ; ``` - #### Where In + ```PHP QB::table('my_table') ->whereIn('name', array('usman', 'sana')) @@ -330,6 +363,7 @@ QB::table('my_table') ``` #### Where Between + ```PHP QB::table('my_table') ->whereBetween('id', 10, 100) @@ -337,6 +371,7 @@ QB::table('my_table') ``` #### Where Null + ```PHP QB::table('my_table') ->whereNull('modified') @@ -346,33 +381,39 @@ QB::table('my_table') ``` ### Where Date + ```PHP QB::table('my_table') ->whereDate('column', '<', '2020-12-29'); // All where date after 29 Dec 2020 ``` ### Where Day + ```PHP QB::table('my_table') ->whereDay('date_column', '=', '29'); // All where day is 29 in any date formats ``` ### Where Month + ```PHP QB::table('my_table') ->whereMonth('date_column', '=', '12'); // All where month is december in any date formats ``` ### Where Year + ```PHP QB::table('my_table') ->whereYear('date_column', '=', '2015'); // All where year is 2015 in any date formats ``` #### Grouped Where -Sometimes queries get complex, where you need grouped criteria, for example `WHERE age = 10 and (name like '%usman%' or description LIKE '%usman%')`. + +Sometimes queries get complex, where you need grouped criteria, for example `WHERE age = 10 and (name like '%usman%' or description LIKE '%usman%')` . Pixie allows you to do so, you can nest as many closures as you need, like below. + ```PHP QB::table('my_table') ->where('my_table.age', 10) @@ -385,26 +426,30 @@ QB::table('my_table') ``` ### Group By and Order By + ```PHP $query = QB::table('my_table')->groupBy('age')->orderBy('created_at', 'ASC'); ``` #### Multiple Group By + ```PHP ->groupBy(array('mytable.myfield1', 'mytable.myfield2', 'another_table.myfield3')); ->orderBy(array('mytable.myfield1', 'mytable.myfield2', 'another_table.myfield3')); ``` -Using `groupBy()` or `orderBy()` methods multiple times `groupBy('a')->groupBy('b')` will also group by first `a` and than `b`. Can be useful if you want to do conditional grouping (within a PHP `if`). Same applies to `orderBy()`. +Using `groupBy()` or `orderBy()` methods multiple times `groupBy('a')->groupBy('b')` will also group by first `a` and than `b` . Can be useful if you want to do conditional grouping (within a PHP `if` ). Same applies to `orderBy()` . ### Having + ```PHP ->having('total_count', '>', 2) ->orHaving('type', '=', 'admin'); ``` ### Limit and Offset + ```PHP ->limit(30); @@ -412,13 +457,14 @@ Using `groupBy()` or `orderBy()` methods multiple times `groupBy('a')->groupBy(' ``` ### Join + ```PHP QB::table('my_table') ->join('another_table', 'another_table.person_id', '=', 'my_table.id') ``` -Available methods, +Available methods, - join() or innerJoin - leftJoin() @@ -436,9 +482,11 @@ It is possible to create a simple join statement between 2 tables, where they ar // Would become ->table('foo')->joinUsing('bar', 'id'); ``` + > Please note this only works with a single base table defined. #### Multiple Join Criteria + If you need more than one criterion to join a table then pass a closure as second parameter. ```PHP @@ -453,7 +501,9 @@ If you need more than one criterion to join a table then pass a closure as secon > Closures can be used as for the $key ### Raw Query -You can always use raw queries if you need, + +You can always use raw queries if you need, + ```PHP $query = QB::query('select * from cb_my_table where age = 12'); @@ -461,6 +511,7 @@ var_dump($query->get()); ``` You can also pass your bindings + ```PHP QB::query('select * from cb_my_table where age = ? and name = ?', array(10, 'usman')); ``` @@ -468,6 +519,7 @@ QB::query('select * from cb_my_table where age = ? and name = ?', array(10, 'usm #### Raw Expressions When you wrap an expression with `raw()` method, Pixie doesn't try to sanitize these. + ```PHP QB::table('my_table') ->select(QB::raw('count(cb_my_table.id) as tot')) @@ -475,27 +527,32 @@ QB::table('my_table') ->where(QB::raw('DATE(?)', 'now')) ``` - ___ **NOTE:** Queries that run through `query()` method are not sanitized until you pass all values through bindings. Queries that run through `raw()` method are not sanitized either, you have to do it yourself. And of course these don't add table prefix too, but you can use the `addTablePrefix()` method. ### Value Binding + As this uses WPDB under the hood, the use of `wpdb::prepare()` is required. To make it easier to define the expected type, you can use Bindings for all values used in most queries. + ```php QB::table('my_table') ->where('id', = Binding::asInt($valueFromSomewhere)) ->get() ``` -This will ensure the underlying statement for prepare will be passed as `WHERE id=%d`. Just passing a value, without a binding will see the values type used as the placeholder. If you wish to use values which are passed through `prepare()` please use a `Raw` value. + +This will ensure the underlying statement for prepare will be passed as `WHERE id=%d` . Just passing a value, without a binding will see the values type used as the placeholder. If you wish to use values which are passed through `prepare()` please use a `Raw` value. + ```php QB::table('my_table') ->where('col1', = Binding::asRaw('im a string, dont wrap me with quotes')) ->where('col2', = new Raw('value')) ->get() ``` -Neither of the above string would be automatically wrapped in `'single quotes'`. + +Neither of the above string would be automatically wrapped in `'single quotes'` . **Types** + ```php Binding::asString($value); Binding::asInt($value); @@ -506,6 +563,7 @@ Binding::asRaw($value); ``` ### Insert + ```PHP $data = array( 'name' => 'Sana', @@ -517,6 +575,7 @@ $insertId = QB::table('my_table')->insert($data); `insert()` method returns the insert id. #### Batch Insert + ```PHP $data = array( array( @@ -534,6 +593,7 @@ $insertIds = QB::table('my_table')->insert($data); In case of batch insert, it will return an array of insert ids. #### Insert with ON DUPLICATE KEY statement + ```PHP $data = array( 'name' => 'Sana', @@ -547,6 +607,7 @@ $insertId = QB::table('my_table')->onDuplicateKeyUpdate($dataUpdate)->insert($da ``` ### Update + ```PHP $data = array( 'name' => 'Sana', @@ -559,9 +620,11 @@ QB::table('my_table')->where('id', 5)->update($data); Will update the name field to Sana and description field to Blah where id = 5. ### Delete + ```PHP QB::table('my_table')->where('id', '>', 5)->delete(); ``` + Will delete all the rows where id is greater than 5. ### Transactions @@ -604,18 +667,21 @@ QB::transaction(function ($qb) { ``` ### Get Built Query + Sometimes you may need to get the query string, its possible. + ```PHP $query = QB::table('my_table')->where('id', '=', 3); $queryObj = $query->getQuery(); ``` -`getQuery()` will return a query object, from this you can get sql, bindings or raw sql. +`getQuery()` will return a query object, from this you can get sql, bindings or raw sql. ```PHP $queryObj->getSql(); // Returns: SELECT * FROM my_table where `id` = ? ``` + ```PHP $queryObj->getBindings(); // Returns: array(3) @@ -627,12 +693,12 @@ $queryObj->getRawSql(); ``` ### Sub Queries and Nested Queries + Rarely but you may need to do sub queries or nested queries. Pixie is powerful enough to do this for you. You can create different query objects and use the `QB::subQuery()` method. ```PHP $subQuery = QB::table('person_details')->select('details')->where('person_id', '=', 3); - $query = QB::table('my_table') ->select('my_table.*') ->select(QB::subQuery($subQuery, 'table_alias1')); @@ -643,10 +709,10 @@ $nestedQuery->get(); This will produce a query like this: - SELECT * FROM (SELECT `cb_my_table`.*, (SELECT `details` FROM `cb_person_details` WHERE `person_id` = 3) as table_alias1 FROM `cb_my_table`) as table_alias2 - + SELECT * FROM (SELECT `cb_my_table` .*, (SELECT `details` FROM `cb_person_details` WHERE `person_id` = 3) as table_alias1 FROM `cb_my_table` ) as table_alias2 ### Get wpdb Instance + If you need to get the wpdb instance you can do so. ```PHP @@ -654,6 +720,7 @@ QB::dbInstance(); ``` ### Fetch results as objects of specified class + Simply call `asObject` query's method. ```PHP @@ -667,6 +734,7 @@ QB::table('my_table')->setFetchMode(PDO::FETCH_COLUMN|PDO::FETCH_UNIQUE)->get(); ``` ### Query Events + Pixie comes with powerful query events to supercharge your application. These events are like database triggers, you can perform some actions when an event occurs, for example you can hook `after-delete` event of a table and delete related data from another table. #### Available Events @@ -688,15 +756,17 @@ QB::registerEvent('before-select', 'users', function($qb) $qb->where('status', '!=', 'banned'); }); ``` + Now every time a select query occurs on `users` table, it will add this where criteria, so banned users don't get access. -The syntax is `registerEvent('event type', 'table name', action in a closure)`. +The syntax is `registerEvent('event type', 'table name', action in a closure)` . If you want the event to be performed when **any table is being queried**, provide `':any'` as table name. **Other examples:** -After inserting data into `my_table`, details will be inserted into another table +After inserting data into `my_table` , details will be inserted into another table + ```PHP QB::registerEvent('after-insert', 'my_table', function($queryBuilder, $insertId) { @@ -705,7 +775,8 @@ QB::registerEvent('after-insert', 'my_table', function($queryBuilder, $insertId) }); ``` -Whenever data is inserted into `person_details` table, set the timestamp field `created_at`, so we don't have to specify it everywhere: +Whenever data is inserted into `person_details` table, set the timestamp field `created_at` , so we don't have to specify it everywhere: + ```PHP QB::registerEvent('after-insert', 'person_details', function($queryBuilder, $insertId) { @@ -714,6 +785,7 @@ QB::registerEvent('after-insert', 'person_details', function($queryBuilder, $ins ``` After deleting from `my_table` delete the relations: + ```PHP QB::registerEvent('after-delete', 'my_table', function($queryBuilder, $queryObject) { @@ -722,20 +794,19 @@ QB::registerEvent('after-delete', 'my_table', function($queryBuilder, $queryObje }); ``` - - -Pixie passes the current instance of query builder as first parameter of your closure so you can build queries with this object, you can do anything like usual query builder (`QB`). +Pixie passes the current instance of query builder as first parameter of your closure so you can build queries with this object, you can do anything like usual query builder ( `QB` ). If something other than `null` is returned from the `before-*` query handler, the value will be result of execution and DB will not be actually queried (and thus, corresponding `after-*` handler will not be called either). Only on `after-*` events you get three parameters: **first** is the query builder, **third** is the execution time as float and **the second** varies: - - On `after-select` you get the `results` obtained from `select`. + - On `after-select` you get the `results` obtained from `select` . - On `after-insert` you get the insert id (or array of ids in case of batch insert) - - On `after-delete` you get the [query object](#get-built-query) (same as what you get from `getQuery()`), from it you can get SQL and Bindings. - - On `after-update` you get the [query object](#get-built-query) like `after-delete`. + - On `after-delete` you get the [query object](#get-built-query) (same as what you get from `getQuery()` ), from it you can get SQL and Bindings. + - On `after-update` you get the [query object](#get-built-query) like `after-delete` . #### Removing Events + ```PHP QB::removeEvent('event-name', 'table-name'); ``` @@ -754,8 +825,9 @@ Here are some cases where Query Events can be extremely helpful: - Add/edit created_at and updated _at data after each entry. #### Notes + - Query Events are set as per connection basis so multiple database connection don't create any problem, and creating new query builder instance preserves your events. - - Query Events go recursively, for example after inserting into `table_a` your event inserts into `table_b`, now you can have another event registered with `table_b` which inserts into `table_c`. + - Query Events go recursively, for example after inserting into `table_a` your event inserts into `table_b` , now you can have another event registered with `table_b` which inserts into `table_c` . - Of course Query Events don't work with raw queries. ___ From 0424a034c6aea997ba0f34ae544039c05d18f77b Mon Sep 17 00:00:00 2001 From: Glynn Quelch Date: Thu, 20 Jan 2022 13:02:18 +0000 Subject: [PATCH 68/69] House Keeping --- README.md | 157 +++++++++++++++--------------------------------------- 1 file changed, 44 insertions(+), 113 deletions(-) diff --git a/README.md b/README.md index b134eb6..fb87698 100644 --- a/README.md +++ b/README.md @@ -2,18 +2,20 @@ ## WIP! + **This project is Not Actively Maintained but most of the features are fully working and there are no major security issues, I'm just not giving it much time.** + # Pixie Query Builder -![alt text](https://img.shields.io/badge/Current_Version-0.1.0-yellow.svg?style=flat " ") - +![alt text](https://img.shields.io/badge/Current_Version-0.1.0-yellow.svg?style=flat " ") [![Open Source Love](https://badges.frapsoft.com/os/mit/mit.svg?v=102)]() ![](https://github.com/gin0115/pixie-wpdb/workflows/GitHub_CI/badge.svg " ") [![codecov](https://codecov.io/gh/gin0115/pixie-wpdb/branch/master/graph/badge.svg?token=4yEceIaSFP)](https://codecov.io/gh/gin0115/pixie-wpdb) [![Scrutinizer Code Quality](https://scrutinizer-ci.com/g/gin0115/pixie-wpdb/badges/quality-score.png?b=master)](https://scrutinizer-ci.com/g/gin0115/pixie-wpdb/?branch=master) + A lightweight, expressive, framework agnostic query builder for PHP it can also be referred as a Database Abstraction Layer. Pixie supports MySQL, SQLite and PostgreSQL and it takes care of query sanitization, table prefixing and many other things with a unified API. It has some advanced features like: @@ -33,7 +35,6 @@ Additional features added to this version of Pixie The syntax is quite similar to Laravel's query builder. ## Example - ```PHP // Make sure you have Composer's autoload file included require 'vendor/autoload.php'; @@ -55,7 +56,6 @@ new \Pixie\Connection($wpdb, $config, $alias); **Simple Query:** The query below returns the row where id = 3, null if no rows. - ```PHP $row = QB::table('my_table')->find(3); ``` @@ -80,6 +80,7 @@ QB::registerEvent('before-select', 'users', function($qb) }); ``` + There are many advanced options which are documented below. Sold? Let's install. ## Installation @@ -116,12 +117,10 @@ Library on [Packagist](https://packagist.org/packages/gin0115/pixie-wpdb). - [Where Day](#where-day) - [Where Month](#where-month) - [Where Year](#where-year) - - [Grouped Where](#grouped-where) - [Where JSON]() - [Where IN JSON]() - [Where BETWEEN JSON]() - - [Group By and Order By](#group-by-and-order-by) - [Having](#having) - [Limit and Offset](#limit-and-offset) @@ -151,8 +150,8 @@ Library on [Packagist](https://packagist.org/packages/gin0115/pixie-wpdb). ___ ## Connection +Pixie WPDB supports only WPDB (WordPress DataBase). You can also create multiple connections, but you can use alias for only one connection at a time.; -Pixie WPDB supports only WPDB (WordPress DataBase). You can also create multiple connections, but you can use alias for only one connection at a time.; ```PHP // Make sure you have Composer's autoload file included @@ -174,12 +173,11 @@ Values | Key | Constant | Value | Description | | ----------- | ----------- |----------- |----------- | -| prefix | Connection:: PREFIX | STRING | Custom table prefix (will ignore WPDB prefix)| -| use_wpdb_prefix | Connection:: USE_WPDB_PREFIX | BOOL | If true will use WPDB prefix and ignore custom prefix -| clone_wpdb | Connection:: CLONE_WPDB | BOOL | If true, will clone WPDB to not use GLOBAL instance| -| show_errors | Connection:: SHOW_ERRORS | BOOL | If set to true will configure WPDB to show/hide errors | +| prefix | Connection::PREFIX | STRING | Custom table prefix (will ignore WPDB prefix)| +| use_wpdb_prefix | Connection::USE_WPDB_PREFIX | BOOL | If true will use WPDB prefix and ignore custom prefix +| clone_wpdb | Connection::CLONE_WPDB | BOOL | If true, will clone WPDB to not use GLOBAL instance| +| show_errors | Connection::SHOW_ERRORS | BOOL | If set to true will configure WPDB to show/hide errors | - ```php $config = [ Connection::USE_WPDB_PREFIX => true, @@ -191,14 +189,11 @@ $config = [ > It is advise to use the class constants over string keys, to avoid BC breakages later on ### Alias - When you create a connection: - ```PHP new \Pixie\Connection($wpdb, $config, 'MyAlias'); ``` - -`MyAlias` is the name for the class alias you want to use (like `MyAlias::table(...)` ), you can use whatever name (with Namespace also, `MyNamespace\\MyClass` ) you like or you may skip it if you don't need an alias. Alias gives you the ability to easily access the QueryBuilder class across your application. +`MyAlias` is the name for the class alias you want to use (like `MyAlias::table(...)`), you can use whatever name (with Namespace also, `MyNamespace\\MyClass`) you like or you may skip it if you don't need an alias. Alias gives you the ability to easily access the QueryBuilder class across your application. When not using an alias you can instantiate the QueryBuilder handler separately, helpful for Dependency Injection and Testing. @@ -214,105 +209,83 @@ var_dump($query->get()); `$connection` here is optional, if not given it will always associate itself to the first connection, but it can be useful when you have multiple database connections. ## Query - -You **must** use `table()` method before every query, except raw `query()` . +You **must** use `table()` method before every query, except raw `query()`. To select from multiple tables you can use the variadic nature of the method - ```PHP QB::table('mytable1', 'mytable2'); ``` -### Get Easily +### Get Easily The query below returns the (first) row where id = 3, null if no rows. - ```PHP $row = QB::table('my_table')->find(3); ``` - -Access your row like, `echo $row->name` . If your field name is not `id` then pass the field name as second parameter `QB::table('my_table')->find(3, 'person_id');` . +Access your row like, `echo $row->name`. If your field name is not `id` then pass the field name as second parameter `QB::table('my_table')->find(3, 'person_id');`. The query below returns the all rows where name = 'Sana', null if no rows. - ```PHP $result = QB::table('my_table')->findAll('name', 'Sana'); ``` - The query below will either return the row or throw a `Pixie\Exception` if no result found. - ```PHP $result = QB::table('my_table')->findOrFail('name', 'Mark'); ``` ### Select - ```PHP $query = QB::table('my_table')->select('*'); ``` #### Multiple Selects - ```PHP ->select('mytable.myfield1', 'mytable.myfield2', 'another_table.myfield3'); ``` -Using select method multiple times `select('a')->select('b')` will also select `a` and `b` . Can be useful if you want to do conditional selects (within a PHP `if` ). +Using select method multiple times `select('a')->select('b')` will also select `a` and `b`. Can be useful if you want to do conditional selects (within a PHP `if`). ### Select Alias - ```php ->select(['column'=>'alias']) ``` - This would result in `SELECT column as alias` as part of the query. ### Select JSON - There are 2 ways to express selecting a value from within a stored JSON object. - `{"someKey": "someValue","someArray":[1,2,3], "someObj":{"a":"apple","b":"banana"}}` +`{"someKey": "someValue","someArray":[1,2,3], "someObj":{"a":"apple","b":"banana"}}` #### Using Larvel style selectors. - ```php ->select(['column->someObj->a' => 'jsonAlias']) ``` - -This would return results with `{jsonAlias => "apple"}` - +This would return results with `{jsonAlias => "apple"}` To access arrays values use `->select(['column->someArray[1]' => 'jsonAlias'])` > Please note using Laravel style selectors without an alias, will result in an exception being thrown. example `->select('column->someObj->a')` #### Using selectJson() helper - ```php ->selectJson('column', ['someObj', 'a'], 'jsonAlias') ``` +This would return results with `{jsonAlias => "apple"}` -This would return results with `{jsonAlias => "apple"}` - -> If no alias is passed, the column value will be set as `json_a` . The last selector is prepended with `json_` - +> If no alias is passed, the column value will be set as `json_a`. The last selector is prepended with `json_` **Example ** `->selectJson('column', ['someObj', 'a'])` would return `{json_a => "apple"}` #### Select Distinct - ```PHP ->selectDistinct(array('mytable.myfield1', 'mytable.myfield2')); ``` -#### Get All +#### Get All Return an array. - ```PHP $query = QB::table('my_table')->where('name', '=', 'Sana'); $result = $query->get(); ``` - You can loop through it like: - ```PHP foreach ($result as $row) { echo $row->name; @@ -320,24 +293,21 @@ foreach ($result as $row) { ``` #### Get First Row - ```PHP $query = QB::table('my_table')->where('name', '=', 'Sana'); $row = $query->first(); ``` +Returns the first row, or null if there is no record. Using this method you can also make sure if a record exists. Access these like `echo $row->name`. -Returns the first row, or null if there is no record. Using this method you can also make sure if a record exists. Access these like `echo $row->name` . #### Get Rows Count - ```PHP $query = QB::table('my_table')->where('name', '=', 'Sana'); $query->count(); ``` ### Where - -Basic syntax is `(fieldname, operator, value)` , if you give two parameters then `=` operator is assumed. So `where('name', 'usman')` and `where('name', '=', 'usman')` is the same. +Basic syntax is `(fieldname, operator, value)`, if you give two parameters then `=` operator is assumed. So `where('name', 'usman')` and `where('name', '=', 'usman')` is the same. ```PHP QB::table('my_table') @@ -348,8 +318,8 @@ QB::table('my_table') ; ``` -#### Where In +#### Where In ```PHP QB::table('my_table') ->whereIn('name', array('usman', 'sana')) @@ -363,7 +333,6 @@ QB::table('my_table') ``` #### Where Between - ```PHP QB::table('my_table') ->whereBetween('id', 10, 100) @@ -371,7 +340,6 @@ QB::table('my_table') ``` #### Where Null - ```PHP QB::table('my_table') ->whereNull('modified') @@ -381,39 +349,33 @@ QB::table('my_table') ``` ### Where Date - ```PHP QB::table('my_table') ->whereDate('column', '<', '2020-12-29'); // All where date after 29 Dec 2020 ``` ### Where Day - ```PHP QB::table('my_table') ->whereDay('date_column', '=', '29'); // All where day is 29 in any date formats ``` ### Where Month - ```PHP QB::table('my_table') ->whereMonth('date_column', '=', '12'); // All where month is december in any date formats ``` ### Where Year - ```PHP QB::table('my_table') ->whereYear('date_column', '=', '2015'); // All where year is 2015 in any date formats ``` #### Grouped Where - -Sometimes queries get complex, where you need grouped criteria, for example `WHERE age = 10 and (name like '%usman%' or description LIKE '%usman%')` . +Sometimes queries get complex, where you need grouped criteria, for example `WHERE age = 10 and (name like '%usman%' or description LIKE '%usman%')`. Pixie allows you to do so, you can nest as many closures as you need, like below. - ```PHP QB::table('my_table') ->where('my_table.age', 10) @@ -426,30 +388,26 @@ QB::table('my_table') ``` ### Group By and Order By - ```PHP $query = QB::table('my_table')->groupBy('age')->orderBy('created_at', 'ASC'); ``` #### Multiple Group By - ```PHP ->groupBy(array('mytable.myfield1', 'mytable.myfield2', 'another_table.myfield3')); ->orderBy(array('mytable.myfield1', 'mytable.myfield2', 'another_table.myfield3')); ``` -Using `groupBy()` or `orderBy()` methods multiple times `groupBy('a')->groupBy('b')` will also group by first `a` and than `b` . Can be useful if you want to do conditional grouping (within a PHP `if` ). Same applies to `orderBy()` . +Using `groupBy()` or `orderBy()` methods multiple times `groupBy('a')->groupBy('b')` will also group by first `a` and than `b`. Can be useful if you want to do conditional grouping (within a PHP `if`). Same applies to `orderBy()`. ### Having - ```PHP ->having('total_count', '>', 2) ->orHaving('type', '=', 'admin'); ``` ### Limit and Offset - ```PHP ->limit(30); @@ -457,14 +415,13 @@ Using `groupBy()` or `orderBy()` methods multiple times `groupBy('a')->groupBy(' ``` ### Join - ```PHP QB::table('my_table') ->join('another_table', 'another_table.person_id', '=', 'my_table.id') ``` -Available methods, +Available methods, - join() or innerJoin - leftJoin() @@ -482,11 +439,9 @@ It is possible to create a simple join statement between 2 tables, where they ar // Would become ->table('foo')->joinUsing('bar', 'id'); ``` - > Please note this only works with a single base table defined. #### Multiple Join Criteria - If you need more than one criterion to join a table then pass a closure as second parameter. ```PHP @@ -501,9 +456,7 @@ If you need more than one criterion to join a table then pass a closure as secon > Closures can be used as for the $key ### Raw Query - -You can always use raw queries if you need, - +You can always use raw queries if you need, ```PHP $query = QB::query('select * from cb_my_table where age = 12'); @@ -511,7 +464,6 @@ var_dump($query->get()); ``` You can also pass your bindings - ```PHP QB::query('select * from cb_my_table where age = ? and name = ?', array(10, 'usman')); ``` @@ -519,7 +471,6 @@ QB::query('select * from cb_my_table where age = ? and name = ?', array(10, 'usm #### Raw Expressions When you wrap an expression with `raw()` method, Pixie doesn't try to sanitize these. - ```PHP QB::table('my_table') ->select(QB::raw('count(cb_my_table.id) as tot')) @@ -527,32 +478,27 @@ QB::table('my_table') ->where(QB::raw('DATE(?)', 'now')) ``` + ___ **NOTE:** Queries that run through `query()` method are not sanitized until you pass all values through bindings. Queries that run through `raw()` method are not sanitized either, you have to do it yourself. And of course these don't add table prefix too, but you can use the `addTablePrefix()` method. ### Value Binding - As this uses WPDB under the hood, the use of `wpdb::prepare()` is required. To make it easier to define the expected type, you can use Bindings for all values used in most queries. - ```php QB::table('my_table') ->where('id', = Binding::asInt($valueFromSomewhere)) ->get() ``` - -This will ensure the underlying statement for prepare will be passed as `WHERE id=%d` . Just passing a value, without a binding will see the values type used as the placeholder. If you wish to use values which are passed through `prepare()` please use a `Raw` value. - +This will ensure the underlying statement for prepare will be passed as `WHERE id=%d`. Just passing a value, without a binding will see the values type used as the placeholder. If you wish to use values which are passed through `prepare()` please use a `Raw` value. ```php QB::table('my_table') ->where('col1', = Binding::asRaw('im a string, dont wrap me with quotes')) ->where('col2', = new Raw('value')) ->get() ``` - -Neither of the above string would be automatically wrapped in `'single quotes'` . +Neither of the above string would be automatically wrapped in `'single quotes'`. **Types** - ```php Binding::asString($value); Binding::asInt($value); @@ -563,7 +509,6 @@ Binding::asRaw($value); ``` ### Insert - ```PHP $data = array( 'name' => 'Sana', @@ -575,7 +520,6 @@ $insertId = QB::table('my_table')->insert($data); `insert()` method returns the insert id. #### Batch Insert - ```PHP $data = array( array( @@ -593,7 +537,6 @@ $insertIds = QB::table('my_table')->insert($data); In case of batch insert, it will return an array of insert ids. #### Insert with ON DUPLICATE KEY statement - ```PHP $data = array( 'name' => 'Sana', @@ -607,7 +550,6 @@ $insertId = QB::table('my_table')->onDuplicateKeyUpdate($dataUpdate)->insert($da ``` ### Update - ```PHP $data = array( 'name' => 'Sana', @@ -620,11 +562,9 @@ QB::table('my_table')->where('id', 5)->update($data); Will update the name field to Sana and description field to Blah where id = 5. ### Delete - ```PHP QB::table('my_table')->where('id', '>', 5)->delete(); ``` - Will delete all the rows where id is greater than 5. ### Transactions @@ -667,21 +607,18 @@ QB::transaction(function ($qb) { ``` ### Get Built Query - Sometimes you may need to get the query string, its possible. - ```PHP $query = QB::table('my_table')->where('id', '=', 3); $queryObj = $query->getQuery(); ``` - `getQuery()` will return a query object, from this you can get sql, bindings or raw sql. + ```PHP $queryObj->getSql(); // Returns: SELECT * FROM my_table where `id` = ? ``` - ```PHP $queryObj->getBindings(); // Returns: array(3) @@ -693,12 +630,12 @@ $queryObj->getRawSql(); ``` ### Sub Queries and Nested Queries - Rarely but you may need to do sub queries or nested queries. Pixie is powerful enough to do this for you. You can create different query objects and use the `QB::subQuery()` method. ```PHP $subQuery = QB::table('person_details')->select('details')->where('person_id', '=', 3); + $query = QB::table('my_table') ->select('my_table.*') ->select(QB::subQuery($subQuery, 'table_alias1')); @@ -709,10 +646,10 @@ $nestedQuery->get(); This will produce a query like this: - SELECT * FROM (SELECT `cb_my_table` .*, (SELECT `details` FROM `cb_person_details` WHERE `person_id` = 3) as table_alias1 FROM `cb_my_table` ) as table_alias2 + SELECT * FROM (SELECT `cb_my_table`.*, (SELECT `details` FROM `cb_person_details` WHERE `person_id` = 3) as table_alias1 FROM `cb_my_table`) as table_alias2 -### Get wpdb Instance +### Get wpdb Instance If you need to get the wpdb instance you can do so. ```PHP @@ -720,7 +657,6 @@ QB::dbInstance(); ``` ### Fetch results as objects of specified class - Simply call `asObject` query's method. ```PHP @@ -734,7 +670,6 @@ QB::table('my_table')->setFetchMode(PDO::FETCH_COLUMN|PDO::FETCH_UNIQUE)->get(); ``` ### Query Events - Pixie comes with powerful query events to supercharge your application. These events are like database triggers, you can perform some actions when an event occurs, for example you can hook `after-delete` event of a table and delete related data from another table. #### Available Events @@ -756,17 +691,15 @@ QB::registerEvent('before-select', 'users', function($qb) $qb->where('status', '!=', 'banned'); }); ``` - Now every time a select query occurs on `users` table, it will add this where criteria, so banned users don't get access. -The syntax is `registerEvent('event type', 'table name', action in a closure)` . +The syntax is `registerEvent('event type', 'table name', action in a closure)`. If you want the event to be performed when **any table is being queried**, provide `':any'` as table name. **Other examples:** -After inserting data into `my_table` , details will be inserted into another table - +After inserting data into `my_table`, details will be inserted into another table ```PHP QB::registerEvent('after-insert', 'my_table', function($queryBuilder, $insertId) { @@ -775,8 +708,7 @@ QB::registerEvent('after-insert', 'my_table', function($queryBuilder, $insertId) }); ``` -Whenever data is inserted into `person_details` table, set the timestamp field `created_at` , so we don't have to specify it everywhere: - +Whenever data is inserted into `person_details` table, set the timestamp field `created_at`, so we don't have to specify it everywhere: ```PHP QB::registerEvent('after-insert', 'person_details', function($queryBuilder, $insertId) { @@ -785,7 +717,6 @@ QB::registerEvent('after-insert', 'person_details', function($queryBuilder, $ins ``` After deleting from `my_table` delete the relations: - ```PHP QB::registerEvent('after-delete', 'my_table', function($queryBuilder, $queryObject) { @@ -794,19 +725,20 @@ QB::registerEvent('after-delete', 'my_table', function($queryBuilder, $queryObje }); ``` -Pixie passes the current instance of query builder as first parameter of your closure so you can build queries with this object, you can do anything like usual query builder ( `QB` ). + + +Pixie passes the current instance of query builder as first parameter of your closure so you can build queries with this object, you can do anything like usual query builder (`QB`). If something other than `null` is returned from the `before-*` query handler, the value will be result of execution and DB will not be actually queried (and thus, corresponding `after-*` handler will not be called either). Only on `after-*` events you get three parameters: **first** is the query builder, **third** is the execution time as float and **the second** varies: - - On `after-select` you get the `results` obtained from `select` . + - On `after-select` you get the `results` obtained from `select`. - On `after-insert` you get the insert id (or array of ids in case of batch insert) - - On `after-delete` you get the [query object](#get-built-query) (same as what you get from `getQuery()` ), from it you can get SQL and Bindings. - - On `after-update` you get the [query object](#get-built-query) like `after-delete` . + - On `after-delete` you get the [query object](#get-built-query) (same as what you get from `getQuery()`), from it you can get SQL and Bindings. + - On `after-update` you get the [query object](#get-built-query) like `after-delete`. #### Removing Events - ```PHP QB::removeEvent('event-name', 'table-name'); ``` @@ -825,9 +757,8 @@ Here are some cases where Query Events can be extremely helpful: - Add/edit created_at and updated _at data after each entry. #### Notes - - Query Events are set as per connection basis so multiple database connection don't create any problem, and creating new query builder instance preserves your events. - - Query Events go recursively, for example after inserting into `table_a` your event inserts into `table_b` , now you can have another event registered with `table_b` which inserts into `table_c` . + - Query Events go recursively, for example after inserting into `table_a` your event inserts into `table_b`, now you can have another event registered with `table_b` which inserts into `table_c`. - Of course Query Events don't work with raw queries. ___ From 9e0965c9b0ab460c180155f775fe007c80039119 Mon Sep 17 00:00:00 2001 From: Glynn Quelch Date: Thu, 20 Jan 2022 13:07:16 +0000 Subject: [PATCH 69/69] House Keeping --- README.md | 17 ++++++----------- 1 file changed, 6 insertions(+), 11 deletions(-) diff --git a/README.md b/README.md index fb87698..49613f6 100644 --- a/README.md +++ b/README.md @@ -1,22 +1,17 @@ -# Modified version of Pixie for use with WordPress and WPDB over PDO. +# Pixie WPDB Query Builder for WordPress -## WIP! - -**This project is Not Actively Maintained but most of the features are fully working and there are no major security issues, I'm just not giving it much time.** - - -# Pixie Query Builder - - -![alt text](https://img.shields.io/badge/Current_Version-0.1.0-yellow.svg?style=flat " ") +![alt text](https://img.shields.io/badge/Current_Version-0.0.1-yellow.svg?style=flat " ") [![Open Source Love](https://badges.frapsoft.com/os/mit/mit.svg?v=102)]() ![](https://github.com/gin0115/pixie-wpdb/workflows/GitHub_CI/badge.svg " ") [![codecov](https://codecov.io/gh/gin0115/pixie-wpdb/branch/master/graph/badge.svg?token=4yEceIaSFP)](https://codecov.io/gh/gin0115/pixie-wpdb) [![Scrutinizer Code Quality](https://scrutinizer-ci.com/g/gin0115/pixie-wpdb/badges/quality-score.png?b=master)](https://scrutinizer-ci.com/g/gin0115/pixie-wpdb/?branch=master) -A lightweight, expressive, framework agnostic query builder for PHP it can also be referred as a Database Abstraction Layer. Pixie supports MySQL, SQLite and PostgreSQL and it takes care of query sanitization, table prefixing and many other things with a unified API. +A lightweight, expressive, query builder for WordPRess it can also be referred as a Database Abstraction Layer. Pixie WPDB supports WPDB ONLY and it takes care of query sanitization, table prefixing and many other things with a unified API. + +> **Pixie WPDB** is an adaption of `pixie` originally written by [usmanhalalit](https://github.com/usmanhalalit). [Pixie is not longer under active development ](https://github.com/usmanhalalit/pixie) + It has some advanced features like: