From 58840e6b39ceeba8e60b9be0c306b67bb43536fa Mon Sep 17 00:00:00 2001 From: Greg Bowler Date: Wed, 7 May 2025 16:02:13 +0100 Subject: [PATCH 1/9] ci: upgrade workflow --- .github/workflows/ci.yml | 16 ++++++++++++---- 1 file changed, 12 insertions(+), 4 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 2a04e16..7de7c9d 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -1,6 +1,11 @@ name: CI -on: [push] +on: [push, pull_request] + +permissions: + contents: read + actions: read + id-token: none jobs: composer: @@ -24,7 +29,7 @@ jobs: php_version: ${{ matrix.php }} - name: Archive build - run: mkdir /tmp/github-actions/ && tar -cvf /tmp/github-actions/build.tar ./ + run: mkdir /tmp/github-actions/ && tar --exclude=".git" -cvf /tmp/github-actions/build.tar ./ - name: Upload build archive for test runners uses: actions/upload-artifact@v4 @@ -127,7 +132,7 @@ jobs: run: tar -xvf /tmp/github-actions/build.tar ./ - name: PHP Mess Detector - uses: php-actions/phpmd@v1 + uses: php-actions/phpmd@v2 with: php_version: ${{ matrix.php }} path: src/ @@ -160,12 +165,15 @@ jobs: remove_old_artifacts: runs-on: ubuntu-latest + permissions: + actions: write + steps: - name: Remove old artifacts for prior workflow runs on this repository env: GH_TOKEN: ${{ github.token }} run: | - gh api "/repos/${{ github.repository }}/actions/artifacts?name=build-artifact" | jq ".artifacts[] | select(.name | startswith(\"build-artifact\")) | .id" > artifact-id-list.txt + gh api "/repos/${{ github.repository }}/actions/artifacts" | jq ".artifacts[] | select(.name | startswith(\"build-artifact\")) | .id" > artifact-id-list.txt while read id do echo -n "Deleting artifact ID $id ... " From 67f2e46c7aa6cbc8ad9fa60dba618a22114a72b1 Mon Sep 17 00:00:00 2001 From: Greg Bowler Date: Thu, 8 May 2025 14:20:30 +0100 Subject: [PATCH 2/9] feature: split query collection into directory/class subclasses for #84 --- src/Query/QueryCollection.php | 4 +- src/Query/QueryCollectionClass.php | 7 +++ src/Query/QueryCollectionDirectory.php | 5 +++ src/Query/QueryCollectionFactory.php | 45 ++++++++++++++----- test/phpunit/DatabaseTest.php | 26 ++++++++++- test/phpunit/Helper/Helper.php | 6 ++- .../Query/QueryCollectionCRUDsTest.php | 3 +- test/phpunit/Query/QueryCollectionTest.php | 5 ++- test/phpunit/Query/QueryFactoryTest.php | 2 +- 9 files changed, 83 insertions(+), 20 deletions(-) create mode 100644 src/Query/QueryCollectionClass.php create mode 100644 src/Query/QueryCollectionDirectory.php diff --git a/src/Query/QueryCollection.php b/src/Query/QueryCollection.php index 0678883..5fbd9ad 100644 --- a/src/Query/QueryCollection.php +++ b/src/Query/QueryCollection.php @@ -5,7 +5,7 @@ use Gt\Database\Fetchable; use Gt\Database\Result\ResultSet; -class QueryCollection { +abstract class QueryCollection { use Fetchable; protected string $directoryPath; @@ -14,7 +14,7 @@ class QueryCollection { public function __construct( string $directoryPath, Driver $driver, - ?QueryFactory $queryFactory = null + ?QueryFactory $queryFactory = null, ) { $this->directoryPath = $directoryPath; $this->queryFactory = $queryFactory ?? new QueryFactory( diff --git a/src/Query/QueryCollectionClass.php b/src/Query/QueryCollectionClass.php new file mode 100644 index 0000000..16bcead --- /dev/null +++ b/src/Query/QueryCollectionClass.php @@ -0,0 +1,7 @@ +queryCollectionCache[$name])) { - $directoryPath = $this->locateDirectory($name); - - if(is_null($directoryPath)) { - throw new QueryCollectionNotFoundException($name); - } - - $this->queryCollectionCache[$name] = new QueryCollection( - $directoryPath, - $this->driver + $this->queryCollectionCache[$name] = $this->findQueryCollection( + $name, + $this->driver, ); } @@ -74,13 +69,13 @@ protected function recurseLocateDirectory( throw new BaseQueryPathDoesNotExistException($basePath); } + /** @var SplFileInfo $fileInfo */ foreach(new DirectoryIterator($basePath) as $fileInfo) { - if($fileInfo->isDot() - || !$fileInfo->isDir()) { + if($fileInfo->isDot()) { continue; } - $basename = $fileInfo->getBasename(); + $basename = $fileInfo->getBasename(".php"); if(strtolower($part) === strtolower($basename)) { $realPath = $fileInfo->getRealPath(); @@ -101,4 +96,30 @@ protected function recurseLocateDirectory( protected function getDefaultBasePath():string { return getcwd(); } + + private function findQueryCollection( + string $name, + Driver $driver, + ):QueryCollection { + $path = $this->locateDirectory($name); + + if($path && is_dir($path)) { + $this->queryCollectionCache[$name] = new QueryCollectionDirectory( + $path, + $driver, + ); + } + elseif($path && is_file($path)) { + $this->queryCollectionCache[$name] = new QueryCollectionClass( + $path, + $driver, + ); + } + else { + throw new QueryCollectionNotFoundException($name); + } + + return $this->queryCollectionCache[$name]; + } + } diff --git a/test/phpunit/DatabaseTest.php b/test/phpunit/DatabaseTest.php index 8fe8cc1..c91007e 100644 --- a/test/phpunit/DatabaseTest.php +++ b/test/phpunit/DatabaseTest.php @@ -4,6 +4,7 @@ use Gt\Database\Connection\Settings; use Gt\Database\Database; use Gt\Database\Query\QueryCollection; +use Gt\Database\Query\QueryCollectionClass; use Gt\Database\Query\QueryCollectionNotFoundException; use PHPUnit\Framework\TestCase; @@ -60,4 +61,27 @@ public function testQueryCollectionDots( $queryCollection = $db->queryCollection($dotName); self::assertInstanceOf(QueryCollection::class, $queryCollection); } -} \ No newline at end of file + + /** @dataProvider \Gt\Database\Test\Helper\Helper::queryCollectionPathNotExistsProvider() */ + public function testQueryCollectionPhp( + string $name, + string $path, + ) { + $path = "$path.php"; + $baseQueryDirectory = dirname($path); + if(!is_dir($baseQueryDirectory)) { + mkdir($baseQueryDirectory, recursive: true); + } + touch($path); + + $settings = new Settings( + $baseQueryDirectory, + Settings::DRIVER_SQLITE, + Settings::SCHEMA_IN_MEMORY, + ); + $sut = new Database($settings); + $queryCollection = $sut->queryCollection($name); + + self::assertInstanceOf(QueryCollectionClass::class, $queryCollection); + } +} diff --git a/test/phpunit/Helper/Helper.php b/test/phpunit/Helper/Helper.php index 6521d76..ae3a781 100644 --- a/test/phpunit/Helper/Helper.php +++ b/test/phpunit/Helper/Helper.php @@ -141,7 +141,11 @@ private static function queryCollectionPathProvider( $nameParts = []; for($n = 0; $n < $nested; $n++) { - $nameParts []= uniqid(); + $uniqid = uniqid(); + if(is_numeric($uniqid[0])) { + $uniqid = chr(rand(97, 102)) . $uniqid; // Add a random lowercase letter (a-f) to the beginning + } + $nameParts []= $uniqid; } $name = implode(DIRECTORY_SEPARATOR, $nameParts); diff --git a/test/phpunit/Query/QueryCollectionCRUDsTest.php b/test/phpunit/Query/QueryCollectionCRUDsTest.php index 380e197..e5f39da 100644 --- a/test/phpunit/Query/QueryCollectionCRUDsTest.php +++ b/test/phpunit/Query/QueryCollectionCRUDsTest.php @@ -5,6 +5,7 @@ use Gt\Database\Connection\Driver; use Gt\Database\Query\Query; use Gt\Database\Query\QueryCollection; +use Gt\Database\Query\QueryCollectionDirectory; use Gt\Database\Query\QueryFactory; use Gt\Database\Result\ResultSet; use Gt\Database\Result\Row; @@ -27,7 +28,7 @@ protected function setUp():void { ->willReturn($this->mockQuery); /** @var QueryFactory $mockQueryFactory */ - $this->queryCollection = new QueryCollection( + $this->queryCollection = new QueryCollectionDirectory( __DIR__, new Driver(new DefaultSettings()), $mockQueryFactory diff --git a/test/phpunit/Query/QueryCollectionTest.php b/test/phpunit/Query/QueryCollectionTest.php index d40d1ab..fabdd59 100644 --- a/test/phpunit/Query/QueryCollectionTest.php +++ b/test/phpunit/Query/QueryCollectionTest.php @@ -5,6 +5,7 @@ use Gt\Database\Connection\Driver; use Gt\Database\Query\Query; use Gt\Database\Query\QueryCollection; +use Gt\Database\Query\QueryCollectionDirectory; use Gt\Database\Query\QueryFactory; use Gt\Database\Result\ResultSet; use PHPUnit\Framework\MockObject\MockObject; @@ -26,7 +27,7 @@ protected function setUp():void { ->with("something") ->willReturn($this->mockQuery); - $this->queryCollection = new QueryCollection( + $this->queryCollection = new QueryCollectionDirectory( __DIR__, new Driver(new DefaultSettings()), $mockQueryFactory @@ -83,4 +84,4 @@ public function testQueryShorthandNoParams() { $this->queryCollection->something() ); } -} \ No newline at end of file +} diff --git a/test/phpunit/Query/QueryFactoryTest.php b/test/phpunit/Query/QueryFactoryTest.php index 0dabbd5..2e8388b 100644 --- a/test/phpunit/Query/QueryFactoryTest.php +++ b/test/phpunit/Query/QueryFactoryTest.php @@ -92,4 +92,4 @@ public function testSelectsCorrectFile() { $queryFileList[$queryName] = $query->getFilePath(); } } -} \ No newline at end of file +} From 0422a5c5cdaa204da450f94bdbc9fd9c6296a69c Mon Sep 17 00:00:00 2001 From: Greg Bowler Date: Thu, 8 May 2025 17:37:48 +0100 Subject: [PATCH 3/9] wip: break Query into SqlQuery and PhpQuery --- src/Query/PhpQuery.php | 25 ++ src/Query/Query.php | 367 +++++++++++++++++++- src/Query/QueryFactory.php | 36 +- src/Query/SqlQuery.php | 369 +-------------------- test/phpunit/Query/QueryCollectionTest.php | 28 ++ test/phpunit/Query/QueryFactoryTest.php | 17 + 6 files changed, 458 insertions(+), 384 deletions(-) create mode 100644 src/Query/PhpQuery.php diff --git a/src/Query/PhpQuery.php b/src/Query/PhpQuery.php new file mode 100644 index 0000000..fd58824 --- /dev/null +++ b/src/Query/PhpQuery.php @@ -0,0 +1,25 @@ +filePath = $filePath; + $this->functionName = $functionName; + $this->connection = $driver->getConnection(); + } + + /** @param array|array $bindings */ + public function getSql(array &$bindings = []):string { +// TODO: Include similarly to page logic files, with optional namespacing (I think...) + } +} diff --git a/src/Query/Query.php b/src/Query/Query.php index 94bfe52..0fb8a96 100644 --- a/src/Query/Query.php +++ b/src/Query/Query.php @@ -2,28 +2,269 @@ namespace Gt\Database\Query; use Gt\Database\Connection\Connection; -use Gt\Database\Connection\Driver; use Gt\Database\Result\ResultSet; +use PDO; +use PDOException; +use PDOStatement; +use PHPSQLParser\lexer\PHPSQLLexer; abstract class Query { + const SPECIAL_BINDINGS = [ + "field" => ["groupBy", "orderBy"], + "int" => ["limit", "offset"], + "string" => ["infileName"], + ]; + protected string $filePath; protected Connection $connection; - public function __construct(string $filePath, Driver $driver) { - if(!is_file($filePath)) { - throw new QueryNotFoundException($filePath); - } - $this->filePath = $filePath; - $this->connection = $driver->getConnection(); - } public function getFilePath():string { return $this->filePath; } /** @param array|array $bindings */ - abstract public function execute(array $bindings = []):ResultSet; + abstract public function getSql(array &$bindings = []):string; + + /** @param array|array $bindings */ + public function execute(array $bindings = []):ResultSet { + $bindings = $this->flattenBindings($bindings); + + $pdo = $this->preparePdo(); + $totalSql = $this->getSql($bindings); + + $lexer = new PHPSQLLexer(); + $splitSqlQueryList = []; + $currentQuery = ""; + foreach($lexer->split($totalSql) as $token) { + if($token === ";") { + array_push($splitSqlQueryList, $currentQuery); + $currentQuery = ""; + continue; + } + + $currentQuery .= $token; + } + if($currentQuery) { + array_push($splitSqlQueryList, $currentQuery); + } + + $statement = $lastInsertId = null; + foreach($splitSqlQueryList as $sql) { + $sql = trim($sql); + if(!$sql) { + continue; + } + $statement = $this->prepareStatement($pdo, $sql); + $preparedBindings = $this->prepareBindings($bindings); + $preparedBindings = $this->ensureParameterCharacter($preparedBindings); + $preparedBindings = $this->removeUnusedBindings($preparedBindings, $sql); + + try { + $statement->execute($preparedBindings); + $lastInsertId = $pdo->lastInsertId(); + } + catch(PDOException $exception) { + throw new PreparedStatementException( + $exception->getMessage() . " (" . $exception->getCode(), + 0, + $exception + ); + } + } + + + return new ResultSet($statement, $lastInsertId); + } + + public function prepareStatement(PDO $pdo, string $sql):PDOStatement { + try { + return $pdo->prepare($sql); + } + catch(PDOException $exception) { + throw new PreparedStatementException( + $exception->getMessage(), + (int)$exception->getCode(), + $exception + ); + } + } + + /** + * Certain words are reserved for use by different SQL engines, such as "limit" + * and "offset", and can't be used by the driver as bound parameters. This + * function returns the SQL for the query after replacing the bound parameters + * manually using string replacement. + * + * @param array|array $bindings + */ + public function injectSpecialBindings( + string $sql, + array $bindings + ):string { + foreach(self::SPECIAL_BINDINGS as $type => $specialList) { + foreach($specialList as $special) { + $specialPlaceholder = ":" . $special; + + if(!array_key_exists($special, $bindings)) { + continue; + } + + $replacement = ""; + if($type !== "string") { + $replacement = $this->escapeSpecialBinding( + $bindings[$special], + $special + ); + } + + if($type === "field") { + $words = explode(" ", $bindings[$special]); + $words[0] = "`" . $words[0] . "`"; + $replacement = implode(" ", $words); + } + elseif($type === "string") { + $replacement = "'" . $bindings[$special] . "'"; + } + + $sql = str_replace( + $specialPlaceholder, + $replacement, + $sql + ); + unset($bindings[$special]); + } + } + + foreach($bindings as $key => $value) { + if(is_array($value)) { + $inString = ""; + + foreach(array_keys($value) as $innerKey) { + $newKey = $key . "__" . $innerKey; + $keyParamString = ":$newKey"; + $inString .= "$keyParamString, "; + } + + $inString = rtrim($inString, " ,"); + $sql = str_replace( + ":$key", + $inString, + $sql + ); + } + } + + return $sql; + } + + /** @param array> $data */ + public function injectDynamicBindings(string $sql, array &$data):string { + $sql = $this->injectDynamicBindingsValueSet($sql, $data); + $sql = $this->injectDynamicIn($sql, $data); + $sql = $this->injectDynamicOr($sql, $data); + return trim($sql); + } + + /** @param array>> $data */ + private function injectDynamicBindingsValueSet(string $sql, array &$data):string { + $pattern = '/\(\s*:__dynamicValueSet\s\)/'; + if(!preg_match($pattern, $sql, $matches)) { + return $sql; + } + if(!isset($data["__dynamicValueSet"])) { + return $sql; + } + + $replacementRowList = []; + foreach($data["__dynamicValueSet"] as $i => $kvp) { + $indexedRow = []; + foreach($kvp as $key => $value) { + $indexedKey = $key . "_" . str_pad($i, 5, "0", STR_PAD_LEFT); + array_push($indexedRow, $indexedKey); + + $data[$indexedKey] = $value; + } + unset($data[$i]); + array_push($replacementRowList, $indexedRow); + } + unset($data["__dynamicValueSet"]); + + $replacementString = ""; + foreach($replacementRowList as $i => $indexedKeyList) { + if($i > 0) { + $replacementString .= ",\n"; + } + $replacementString .= "("; + foreach($indexedKeyList as $j => $key) { + if($j > 0) { + $replacementString .= ","; + } + $replacementString .= "\n\t:$key"; + } + $replacementString .= "\n)"; + } + + return str_replace($matches[0], $replacementString, $sql); + } + + /** @param array> $data */ + private function injectDynamicIn(string $sql, array &$data):string { + $pattern = '/\(\s*:__dynamicIn\s\)/'; + if(!preg_match($pattern, $sql, $matches)) { + return $sql; + } + if(!isset($data["__dynamicIn"])) { + return $sql; + } + + foreach($data["__dynamicIn"] as $i => $value) { + if(is_string($value)) { + $value = str_replace("'", "''", $value); + $data["__dynamicIn"][$i] = "'$value'"; + } + } + + $replacementString = implode(", ", $data["__dynamicIn"]); + unset($data["__dynamicIn"]); + return str_replace($matches[0], "( $replacementString )", $sql); + } + + /** @param array>> $data */ + private function injectDynamicOr(string $sql, array &$data):string { + $pattern = '/:__dynamicOr/'; + if(!preg_match($pattern, $sql, $matches)) { + return $sql; + } + if(!isset($data["__dynamicOr"])) { + return $sql; + } + + $replacementString = ""; + foreach($data["__dynamicOr"] as $kvp) { + $conditionString = ""; + foreach($kvp as $key => $value) { + if(is_string($value)) { + $value = str_replace("'", "''", $value); + $value = "'$value'"; + } + + if($conditionString) { + $conditionString .= " and "; + } + $conditionString .= "`$key` = $value"; + } + + if($replacementString) { + $replacementString .= " or\n"; + } + $replacementString .= "\t($conditionString)"; + } + + $replacementString = "\n(\n$replacementString\n)\n"; + return str_replace($matches[0], $replacementString, $sql); + } /** * $bindings can either be : @@ -78,4 +319,112 @@ protected function flattenBindings(array $bindings):array { return $flatArray; } + + /** + * @param array|array $bindings + * @return array|array + */ + public function prepareBindings(array $bindings):array { + foreach($bindings as $key => $value) { + if(is_bool($value)) { + $bindings[$key] = (int)$value; + } + if($value instanceof DateTimeInterface) { + $bindings[$key] = $value->format("Y-m-d H:i:s"); + } + if(is_array($value)) { + foreach($value as $i => $innerValue) { + $newKey = $key . "__" . $i; + $bindings[$newKey] = $innerValue; + } + unset($bindings[$key]); + } + } + + return $bindings; + } + + /** + * @param array|array $bindings + * @return array|array + */ + public function ensureParameterCharacter(array $bindings):array { + if($this->bindingsEmptyOrNonAssociative($bindings)) { + return $bindings; + } + + foreach($bindings as $key => $value) { + if(substr($key, 0, 1) !== ":") { + $bindings[":" . $key] = $value; + unset($bindings[$key]); + } + } + + return $bindings; + } + + /** + * @param array|array $bindings + * @return array|array + */ + public function removeUnusedBindings(array $bindings, string $sql):array { + if($this->bindingsEmptyOrNonAssociative($bindings)) { + return $bindings; + } + + foreach(array_keys($bindings) as $key) { + if(!preg_match("/$key(\W|\$)/", $sql)) { + unset($bindings[$key]); + } + } + + return $bindings; + } + + /** @param array|array $bindings */ + public function bindingsEmptyOrNonAssociative(array $bindings):bool { + return $bindings === [] + || array_keys($bindings) === range( + 0, + count($bindings) - 1); + } + + protected function preparePdo():PDO { + $this->connection->setAttribute(PDO::ATTR_ERRMODE, PDO::ERRMODE_EXCEPTION); + return $this->connection; + } + + /** @noinspection PhpUnusedParameterInspection */ + protected function escapeSpecialBinding( + string $value, + string $type, + ):string { + $value = preg_replace( + "/[^0-9a-z,'\"`\s]/i", + "", + $value + ); + +// TODO: In v2 we will properly parse the different parts of the special bindings. +// See https://github.com/PhpGt/Database/issues/117 +// switch($type) { +// [GROUP BY {col_name | expr | position}, ... [WITH ROLLUP]] +// case "groupBy": +// break; +// +// [ORDER BY {col_name | expr | position} +// case "orderBy": +// break; +// +// [LIMIT {[offset,] row_count | row_count OFFSET offset}] +// case "limit": +// break; +// +// [LIMIT {[offset,] row_count | row_count OFFSET offset}] +// case "offset": +// break; +// } + + return (string)$value; + } } diff --git a/src/Query/QueryFactory.php b/src/Query/QueryFactory.php index 82c48ca..dcfb193 100644 --- a/src/Query/QueryFactory.php +++ b/src/Query/QueryFactory.php @@ -11,31 +11,36 @@ class QueryFactory { const CLASS_FOR_EXTENSION = [ "sql" => SqlQuery::class, - "php" => "NOT_YET_IMPLEMENTED", + "php" => PhpQuery::class, ]; public function __construct( - protected string $directoryOfQueries, + protected string $queryHolder, protected Driver $driver ) {} public function findQueryFilePath(string $name):string { - foreach(new DirectoryIterator($this->directoryOfQueries) as $fileInfo) { - if($fileInfo->isDot() - || $fileInfo->isDir()) { - continue; + if(is_dir($this->queryHolder)) { + foreach(new DirectoryIterator($this->queryHolder) as $fileInfo) { + if($fileInfo->isDot() + || $fileInfo->isDir()) { + continue; + } + + $this->getExtensionIfValid($fileInfo); + $fileNameNoExtension = strtok($fileInfo->getFilename(), "."); + if($fileNameNoExtension !== $name) { + continue; + } + + return $fileInfo->getRealPath(); } - - $this->getExtensionIfValid($fileInfo); - $fileNameNoExtension = strtok($fileInfo->getFilename(), "."); - if($fileNameNoExtension !== $name) { - continue; - } - - return $fileInfo->getRealPath(); + } + elseif(is_file($this->queryHolder)) { + return "$this->queryHolder::$name"; } - throw new QueryNotFoundException($this->directoryOfQueries . ", " . $name); + throw new QueryNotFoundException($this->queryHolder . ", " . $name); } public function create(string $name):Query { @@ -62,6 +67,7 @@ public function getQueryClassForFilePath(string $filePath):string { protected function getExtensionIfValid(SplFileInfo $fileInfo):string { $ext = strtolower($fileInfo->getExtension()); + $ext = strstr($ext, ":", true) ?: $ext; if(!array_key_exists($ext, self::CLASS_FOR_EXTENSION)) { throw new QueryFileExtensionException($ext); diff --git a/src/Query/SqlQuery.php b/src/Query/SqlQuery.php index a61c592..ab30020 100644 --- a/src/Query/SqlQuery.php +++ b/src/Query/SqlQuery.php @@ -1,21 +1,17 @@ ["groupBy", "orderBy"], - "int" => ["limit", "offset"], - "string" => ["infileName"], - ]; + public function __construct(string $filePath, Driver $driver) { + if(!is_file($filePath)) { + throw new QueryNotFoundException($filePath); + } + + $this->filePath = $filePath; + $this->connection = $driver->getConnection(); + } /** @param array|array $bindings */ public function getSql(array &$bindings = []):string { @@ -30,351 +26,4 @@ public function getSql(array &$bindings = []):string { ); return $sql; } - - /** @param array|array $bindings */ - public function execute(array $bindings = []):ResultSet { - $bindings = $this->flattenBindings($bindings); - - $pdo = $this->preparePdo(); - $totalSql = $this->getSql($bindings); - - $lexer = new PHPSQLLexer(); - $splitSqlQueryList = []; - $currentQuery = ""; - foreach($lexer->split($totalSql) as $token) { - if($token === ";") { - array_push($splitSqlQueryList, $currentQuery); - $currentQuery = ""; - continue; - } - - $currentQuery .= $token; - } - if($currentQuery) { - array_push($splitSqlQueryList, $currentQuery); - } - - $statement = $lastInsertId = null; - foreach($splitSqlQueryList as $sql) { - $sql = trim($sql); - if(!$sql) { - continue; - } - $statement = $this->prepareStatement($pdo, $sql); - $preparedBindings = $this->prepareBindings($bindings); - $preparedBindings = $this->ensureParameterCharacter($preparedBindings); - $preparedBindings = $this->removeUnusedBindings($preparedBindings, $sql); - - try { - $statement->execute($preparedBindings); - $lastInsertId = $pdo->lastInsertId(); - } - catch(PDOException $exception) { - throw new PreparedStatementException( - $exception->getMessage() . " (" . $exception->getCode(), - 0, - $exception - ); - } - } - - - return new ResultSet($statement, $lastInsertId); - } - - public function prepareStatement(PDO $pdo, string $sql):PDOStatement { - try { - return $pdo->prepare($sql); - } - catch(PDOException $exception) { - throw new PreparedStatementException( - $exception->getMessage(), - (int)$exception->getCode(), - $exception - ); - } - } - - /** - * Certain words are reserved for use by different SQL engines, such as "limit" - * and "offset", and can't be used by the driver as bound parameters. This - * function returns the SQL for the query after replacing the bound parameters - * manually using string replacement. - * - * @param array|array $bindings - */ - public function injectSpecialBindings( - string $sql, - array $bindings - ):string { - foreach(self::SPECIAL_BINDINGS as $type => $specialList) { - foreach($specialList as $special) { - $specialPlaceholder = ":" . $special; - - if(!array_key_exists($special, $bindings)) { - continue; - } - - $replacement = ""; - if($type !== "string") { - $replacement = $this->escapeSpecialBinding( - $bindings[$special], - $special - ); - } - - if($type === "field") { - $words = explode(" ", $bindings[$special]); - $words[0] = "`" . $words[0] . "`"; - $replacement = implode(" ", $words); - } - elseif($type === "string") { - $replacement = "'" . $bindings[$special] . "'"; - } - - $sql = str_replace( - $specialPlaceholder, - $replacement, - $sql - ); - unset($bindings[$special]); - } - } - - foreach($bindings as $key => $value) { - if(is_array($value)) { - $inString = ""; - - foreach(array_keys($value) as $innerKey) { - $newKey = $key . "__" . $innerKey; - $keyParamString = ":$newKey"; - $inString .= "$keyParamString, "; - } - - $inString = rtrim($inString, " ,"); - $sql = str_replace( - ":$key", - $inString, - $sql - ); - } - } - - return $sql; - } - - /** @param array> $data */ - public function injectDynamicBindings(string $sql, array &$data):string { - $sql = $this->injectDynamicBindingsValueSet($sql, $data); - $sql = $this->injectDynamicIn($sql, $data); - $sql = $this->injectDynamicOr($sql, $data); - return trim($sql); - } - - /** @param array>> $data */ - private function injectDynamicBindingsValueSet(string $sql, array &$data):string { - $pattern = '/\(\s*:__dynamicValueSet\s\)/'; - if(!preg_match($pattern, $sql, $matches)) { - return $sql; - } - if(!isset($data["__dynamicValueSet"])) { - return $sql; - } - - $replacementRowList = []; - foreach($data["__dynamicValueSet"] as $i => $kvp) { - $indexedRow = []; - foreach($kvp as $key => $value) { - $indexedKey = $key . "_" . str_pad($i, 5, "0", STR_PAD_LEFT); - array_push($indexedRow, $indexedKey); - - $data[$indexedKey] = $value; - } - unset($data[$i]); - array_push($replacementRowList, $indexedRow); - } - unset($data["__dynamicValueSet"]); - - $replacementString = ""; - foreach($replacementRowList as $i => $indexedKeyList) { - if($i > 0) { - $replacementString .= ",\n"; - } - $replacementString .= "("; - foreach($indexedKeyList as $j => $key) { - if($j > 0) { - $replacementString .= ","; - } - $replacementString .= "\n\t:$key"; - } - $replacementString .= "\n)"; - } - - return str_replace($matches[0], $replacementString, $sql); - } - - /** @param array> $data */ - private function injectDynamicIn(string $sql, array &$data):string { - $pattern = '/\(\s*:__dynamicIn\s\)/'; - if(!preg_match($pattern, $sql, $matches)) { - return $sql; - } - if(!isset($data["__dynamicIn"])) { - return $sql; - } - - foreach($data["__dynamicIn"] as $i => $value) { - if(is_string($value)) { - $value = str_replace("'", "''", $value); - $data["__dynamicIn"][$i] = "'$value'"; - } - } - - $replacementString = implode(", ", $data["__dynamicIn"]); - unset($data["__dynamicIn"]); - return str_replace($matches[0], "( $replacementString )", $sql); - } - - /** @param array>> $data */ - private function injectDynamicOr(string $sql, array &$data):string { - $pattern = '/:__dynamicOr/'; - if(!preg_match($pattern, $sql, $matches)) { - return $sql; - } - if(!isset($data["__dynamicOr"])) { - return $sql; - } - - $replacementString = ""; - foreach($data["__dynamicOr"] as $kvp) { - $conditionString = ""; - foreach($kvp as $key => $value) { - if(is_string($value)) { - $value = str_replace("'", "''", $value); - $value = "'$value'"; - } - - if($conditionString) { - $conditionString .= " and "; - } - $conditionString .= "`$key` = $value"; - } - - if($replacementString) { - $replacementString .= " or\n"; - } - $replacementString .= "\t($conditionString)"; - } - - $replacementString = "\n(\n$replacementString\n)\n"; - return str_replace($matches[0], $replacementString, $sql); - } - - /** - * @param array|array $bindings - * @return array|array - */ - public function prepareBindings(array $bindings):array { - foreach($bindings as $key => $value) { - if(is_bool($value)) { - $bindings[$key] = (int)$value; - } - if($value instanceof DateTimeInterface) { - $bindings[$key] = $value->format("Y-m-d H:i:s"); - } - if(is_array($value)) { - foreach($value as $i => $innerValue) { - $newKey = $key . "__" . $i; - $bindings[$newKey] = $innerValue; - } - unset($bindings[$key]); - } - } - - return $bindings; - } - - /** - * @param array|array $bindings - * @return array|array - */ - public function ensureParameterCharacter(array $bindings):array { - if($this->bindingsEmptyOrNonAssociative($bindings)) { - return $bindings; - } - - foreach($bindings as $key => $value) { - if(substr($key, 0, 1) !== ":") { - $bindings[":" . $key] = $value; - unset($bindings[$key]); - } - } - - return $bindings; - } - - /** - * @param array|array $bindings - * @return array|array - */ - public function removeUnusedBindings(array $bindings, string $sql):array { - if($this->bindingsEmptyOrNonAssociative($bindings)) { - return $bindings; - } - - foreach(array_keys($bindings) as $key) { - if(!preg_match("/$key(\W|\$)/", $sql)) { - unset($bindings[$key]); - } - } - - return $bindings; - } - - /** @param array|array $bindings */ - public function bindingsEmptyOrNonAssociative(array $bindings):bool { - return $bindings === [] - || array_keys($bindings) === range( - 0, - count($bindings) - 1); - } - - protected function preparePdo():PDO { - $this->connection->setAttribute(PDO::ATTR_ERRMODE, PDO::ERRMODE_EXCEPTION); - return $this->connection; - } - - /** @noinspection PhpUnusedParameterInspection */ - protected function escapeSpecialBinding( - string $value, - string $type - ):string { - $value = preg_replace( - "/[^0-9a-z,'\"`\s]/i", - "", - $value - ); - -// TODO: In v2 we will properly parse the different parts of the special bindings. -// See https://github.com/PhpGt/Database/issues/117 -// switch($type) { -// [GROUP BY {col_name | expr | position}, ... [WITH ROLLUP]] -// case "groupBy": -// break; -// -// [ORDER BY {col_name | expr | position} -// case "orderBy": -// break; -// -// [LIMIT {[offset,] row_count | row_count OFFSET offset}] -// case "limit": -// break; -// -// [LIMIT {[offset,] row_count | row_count OFFSET offset}] -// case "offset": -// break; -// } - - return (string)$value; - } } diff --git a/test/phpunit/Query/QueryCollectionTest.php b/test/phpunit/Query/QueryCollectionTest.php index fabdd59..80f3eb8 100644 --- a/test/phpunit/Query/QueryCollectionTest.php +++ b/test/phpunit/Query/QueryCollectionTest.php @@ -3,8 +3,10 @@ use Gt\Database\Connection\DefaultSettings; use Gt\Database\Connection\Driver; +use Gt\Database\Query\PhpQuery; use Gt\Database\Query\Query; use Gt\Database\Query\QueryCollection; +use Gt\Database\Query\QueryCollectionClass; use Gt\Database\Query\QueryCollectionDirectory; use Gt\Database\Query\QueryFactory; use Gt\Database\Result\ResultSet; @@ -84,4 +86,30 @@ public function testQueryShorthandNoParams() { $this->queryCollection->something() ); } + + public function testQueryCollectionClass() { + $projectDir = implode(DIRECTORY_SEPARATOR, [ + sys_get_temp_dir(), + "phpgt", + "database", + uniqid(), + ]); + $baseQueryDirectory = implode(DIRECTORY_SEPARATOR, [ + $projectDir, + "query", + ]); + $queryCollectionClassPath = "$baseQueryDirectory/Example.php"; + if(!is_dir($baseQueryDirectory)) { + mkdir($baseQueryDirectory, recursive: true); + } + touch($queryCollectionClassPath); + + $sut = new QueryCollectionClass( + $queryCollectionClassPath, + new Driver(new DefaultSettings()), + ); + + $query = $sut->query("getTimestamp"); + self::assertInstanceOf(PhpQuery::class, $query); + } } diff --git a/test/phpunit/Query/QueryFactoryTest.php b/test/phpunit/Query/QueryFactoryTest.php index 2e8388b..9404c47 100644 --- a/test/phpunit/Query/QueryFactoryTest.php +++ b/test/phpunit/Query/QueryFactoryTest.php @@ -3,6 +3,7 @@ use Gt\Database\Connection\DefaultSettings; use Gt\Database\Connection\Driver; +use Gt\Database\Query\PhpQuery; use Gt\Database\Query\Query; use Gt\Database\Query\QueryFactory; use Gt\Database\Query\QueryFileExtensionException; @@ -92,4 +93,20 @@ public function testSelectsCorrectFile() { $queryFileList[$queryName] = $query->getFilePath(); } } + + /** @dataProvider \Gt\Database\Test\Helper\Helper::queryPathNotExistsProvider */ + public function testCreatePhp( + string $queryName, + string $directoryOfQueries, + ) { + $classPath = "$directoryOfQueries.php"; + if(!is_dir($directoryOfQueries)) { + mkdir($directoryOfQueries, recursive: true); + } + touch($classPath); + + $sut = new QueryFactory($classPath, new Driver(new DefaultSettings())); + $query = $sut->create("getTimestamp"); + self::assertInstanceOf(PhpQuery::class, $query); + } } From 4f479267d374badf97112ad9a17bb6d1701b1536 Mon Sep 17 00:00:00 2001 From: Greg Bowler Date: Sun, 11 May 2025 23:15:48 +0100 Subject: [PATCH 4/9] feature: php object query for #84 --- src/Database.php | 9 ++ src/Query/PhpQuery.php | 25 ++- src/Query/PhpQueryClassNotFoundException.php | 7 + src/Query/Query.php | 3 +- src/Query/QueryCollection.php | 13 ++ src/Query/QueryCollectionClass.php | 2 - test/phpunit/Query/QueryCollectionTest.php | 154 +++++++++++++++---- 7 files changed, 180 insertions(+), 33 deletions(-) create mode 100644 src/Query/PhpQueryClassNotFoundException.php diff --git a/src/Database.php b/src/Database.php index 3014802..6a3f343 100644 --- a/src/Database.php +++ b/src/Database.php @@ -25,6 +25,7 @@ class Database { /** @var array */ protected array $driverArray; protected Connection $currentConnectionName; + protected string $appNamespace = "\\App"; public function __construct(SettingsInterface...$connectionSettings) { if(empty($connectionSettings)) { @@ -36,6 +37,14 @@ public function __construct(SettingsInterface...$connectionSettings) { $this->storeQueryCollectionFactoryFromSettings($connectionSettings); } + public function setAppNameSpace(string $namespace):void { + if(!str_starts_with($namespace, "\\")) { + $namespace = "\\$namespace"; + } + + $this->appNamespace = $namespace; + } + public function insert(string $queryName, mixed...$bindings):string { $result = $this->query($queryName, $bindings); return $result->lastInsertId(); diff --git a/src/Query/PhpQuery.php b/src/Query/PhpQuery.php index fd58824..42570a9 100644 --- a/src/Query/PhpQuery.php +++ b/src/Query/PhpQuery.php @@ -4,7 +4,10 @@ use Gt\Database\Connection\Driver; class PhpQuery extends Query { + private string $className; private string $functionName; + private string $appNamespace = "\\App\\Query"; + private mixed $instance; public function __construct(string $filePathWithFunction, Driver $driver) { [$filePath, $functionName] = explode("::", $filePathWithFunction); @@ -14,12 +17,32 @@ public function __construct(string $filePathWithFunction, Driver $driver) { } $this->filePath = $filePath; + $this->className = pathinfo($filePath, PATHINFO_FILENAME); $this->functionName = $functionName; $this->connection = $driver->getConnection(); + + require_once($filePath); + } + + public function setAppNamespace(string $namespace):void { + if(!str_starts_with($namespace, "\\")) { + $namespace = "\\$namespace"; + } + + $this->appNamespace = $namespace; } /** @param array|array $bindings */ public function getSql(array &$bindings = []):string { -// TODO: Include similarly to page logic files, with optional namespacing (I think...) + $fqClassName = $this->appNamespace . "\\" . $this->className; + if(!class_exists($fqClassName)) { + throw new PhpQueryClassNotFoundException($fqClassName); + } + + if(!isset($this->instance)) { + $this->instance = new $fqClassName(); + } + + return $this->instance->{$this->functionName}(); } } diff --git a/src/Query/PhpQueryClassNotFoundException.php b/src/Query/PhpQueryClassNotFoundException.php new file mode 100644 index 0000000..7cffbc2 --- /dev/null +++ b/src/Query/PhpQueryClassNotFoundException.php @@ -0,0 +1,7 @@ +filePath; diff --git a/src/Query/QueryCollection.php b/src/Query/QueryCollection.php index 5fbd9ad..28e89ca 100644 --- a/src/Query/QueryCollection.php +++ b/src/Query/QueryCollection.php @@ -10,6 +10,7 @@ abstract class QueryCollection { protected string $directoryPath; protected QueryFactory $queryFactory; + protected string $appNamespace = "\\App\\Query"; public function __construct( string $directoryPath, @@ -23,6 +24,14 @@ public function __construct( ); } + public function setAppNamespace(string $namespace):void { + if(!str_starts_with($namespace, "\\")) { + $namespace = "\\$namespace"; + } + + $this->appNamespace = $namespace; + } + /** @param array $args */ public function __call(string $name, array $args):ResultSet { if(isset($args[0]) && is_array($args[0])) { @@ -40,6 +49,10 @@ public function query( mixed...$placeholderMap ):ResultSet { $query = $this->queryFactory->create($name); + if($query instanceof PhpQuery) { + $query->setAppNamespace($this->appNamespace); + } + return $query->execute($placeholderMap); } diff --git a/src/Query/QueryCollectionClass.php b/src/Query/QueryCollectionClass.php index 16bcead..0d4254d 100644 --- a/src/Query/QueryCollectionClass.php +++ b/src/Query/QueryCollectionClass.php @@ -1,7 +1,5 @@ createMock(QueryFactory::class); - $this->mockQuery = $this->createMock(Query::class); - $mockQueryFactory + public function testQueryCollectionQuery() { + $queryFactory = $this->createMock(QueryFactory::class); + $query = $this->createMock(Query::class); + $queryFactory ->expects(static::once()) ->method("create") ->with("something") - ->willReturn($this->mockQuery); + ->willReturn($query); - $this->queryCollection = new QueryCollectionDirectory( + $queryCollection = new QueryCollectionDirectory( __DIR__, new Driver(new DefaultSettings()), - $mockQueryFactory + $queryFactory ); - } - public function testQueryCollectionQuery() { $placeholderVars = ["nombre" => "hombre"]; - $this->mockQuery + $query ->expects(static::once()) ->method("execute") ->with([$placeholderVars]); - $resultSet = $this->queryCollection->query( + $resultSet = $queryCollection->query( "something", $placeholderVars ); @@ -55,9 +48,23 @@ public function testQueryCollectionQuery() { } public function testQueryCollectionQueryNoParams() { - $this->mockQuery->expects(static::once())->method("execute")->with(); + $queryFactory = $this->createMock(QueryFactory::class); + $query = $this->createMock(Query::class); + $queryFactory + ->expects(static::once()) + ->method("create") + ->with("something") + ->willReturn($query); + + $queryCollection = new QueryCollectionDirectory( + __DIR__, + new Driver(new DefaultSettings()), + $queryFactory + ); + + $query->expects(static::once())->method("execute")->with(); - $resultSet = $this->queryCollection->query("something"); + $resultSet = $queryCollection->query("something"); static::assertInstanceOf( ResultSet::class, @@ -66,28 +73,56 @@ public function testQueryCollectionQueryNoParams() { } public function testQueryShorthand() { + $queryFactory = $this->createMock(QueryFactory::class); + $query = $this->createMock(Query::class); + $queryFactory + ->expects(static::once()) + ->method("create") + ->with("something") + ->willReturn($query); + + $queryCollection = new QueryCollectionDirectory( + __DIR__, + new Driver(new DefaultSettings()), + $queryFactory + ); + $placeholderVars = ["nombre" => "hombre"]; - $this->mockQuery + $query ->expects(static::once()) ->method("execute") ->with([$placeholderVars]); static::assertInstanceOf( ResultSet::class, - $this->queryCollection->something($placeholderVars) + $queryCollection->something($placeholderVars) ); } public function testQueryShorthandNoParams() { - $this->mockQuery->expects(static::once())->method("execute")->with(); + $queryFactory = $this->createMock(QueryFactory::class); + $query = $this->createMock(Query::class); + $queryFactory + ->expects(static::once()) + ->method("create") + ->with("something") + ->willReturn($query); + + $queryCollection = new QueryCollectionDirectory( + __DIR__, + new Driver(new DefaultSettings()), + $queryFactory + ); + + $query->expects(static::once())->method("execute")->with(); static::assertInstanceOf( ResultSet::class, - $this->queryCollection->something() + $queryCollection->something() ); } - public function testQueryCollectionClass() { + public function testQueryCollectionClass_defaultNamespace() { $projectDir = implode(DIRECTORY_SEPARATOR, [ sys_get_temp_dir(), "phpgt", @@ -96,20 +131,83 @@ public function testQueryCollectionClass() { ]); $baseQueryDirectory = implode(DIRECTORY_SEPARATOR, [ $projectDir, - "query", + "resultSet", ]); $queryCollectionClassPath = "$baseQueryDirectory/Example.php"; if(!is_dir($baseQueryDirectory)) { mkdir($baseQueryDirectory, recursive: true); } - touch($queryCollectionClassPath); + $php = <<query("getTimestamp"); - self::assertInstanceOf(PhpQuery::class, $query); + $resultSet = $sut->query("getTimestamp"); + self::assertInstanceOf(ResultSet::class, $resultSet); + $row = $resultSet->fetch(); + $actualDateTime = $row->getDateTime("current_timestamp"); + $expectedDateTime = new DateTime(); + self::assertSame( + $expectedDateTime->format("Y-m-d H:i"), + $actualDateTime->format("Y-m-d H:i"), + ); + } + + public function testQueryCollectionClass_namespace() { + $projectDir = implode(DIRECTORY_SEPARATOR, [ + sys_get_temp_dir(), + "phpgt", + "database", + uniqid(), + ]); + $baseQueryDirectory = implode(DIRECTORY_SEPARATOR, [ + $projectDir, + "resultSet", + ]); + $queryCollectionClassPath = "$baseQueryDirectory/Example.php"; + if(!is_dir($baseQueryDirectory)) { + mkdir($baseQueryDirectory, recursive: true); + } + $php = <<setAppNamespace("GtTest\\DatabaseExample"); + + $resultSet = $sut->query("getTimestamp"); + self::assertInstanceOf(ResultSet::class, $resultSet); + $row = $resultSet->fetch(); + $actualDateTime = $row->getDateTime("current_timestamp"); + $expectedDateTime = new DateTime(); + self::assertSame( + $expectedDateTime->format("Y-m-d H:i"), + $actualDateTime->format("Y-m-d H:i"), + ); } } From 9d55c8f60654bbe7b3c3745959b1fef0787779ac Mon Sep 17 00:00:00 2001 From: Greg Bowler Date: Mon, 12 May 2025 14:40:39 +0100 Subject: [PATCH 5/9] build: php 8.1 compatibility --- composer.lock | 97 ++++++++++++++++++++++++++++----------------------- 1 file changed, 53 insertions(+), 44 deletions(-) diff --git a/composer.lock b/composer.lock index 6410f21..29d955f 100644 --- a/composer.lock +++ b/composer.lock @@ -473,16 +473,16 @@ }, { "name": "myclabs/deep-copy", - "version": "1.13.0", + "version": "1.13.1", "source": { "type": "git", "url": "https://github.com/myclabs/DeepCopy.git", - "reference": "024473a478be9df5fdaca2c793f2232fe788e414" + "reference": "1720ddd719e16cf0db4eb1c6eca108031636d46c" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/myclabs/DeepCopy/zipball/024473a478be9df5fdaca2c793f2232fe788e414", - "reference": "024473a478be9df5fdaca2c793f2232fe788e414", + "url": "https://api.github.com/repos/myclabs/DeepCopy/zipball/1720ddd719e16cf0db4eb1c6eca108031636d46c", + "reference": "1720ddd719e16cf0db4eb1c6eca108031636d46c", "shasum": "" }, "require": { @@ -521,7 +521,7 @@ ], "support": { "issues": "https://github.com/myclabs/DeepCopy/issues", - "source": "https://github.com/myclabs/DeepCopy/tree/1.13.0" + "source": "https://github.com/myclabs/DeepCopy/tree/1.13.1" }, "funding": [ { @@ -529,7 +529,7 @@ "type": "tidelift" } ], - "time": "2025-02-12T12:17:51+00:00" + "time": "2025-04-29T12:36:36+00:00" }, { "name": "nikic/php-parser", @@ -855,16 +855,16 @@ }, { "name": "phpstan/phpstan", - "version": "1.12.23", + "version": "1.12.25", "source": { "type": "git", "url": "https://github.com/phpstan/phpstan.git", - "reference": "29201e7a743a6ab36f91394eab51889a82631428" + "reference": "e310849a19e02b8bfcbb63147f495d8f872dd96f" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/phpstan/phpstan/zipball/29201e7a743a6ab36f91394eab51889a82631428", - "reference": "29201e7a743a6ab36f91394eab51889a82631428", + "url": "https://api.github.com/repos/phpstan/phpstan/zipball/e310849a19e02b8bfcbb63147f495d8f872dd96f", + "reference": "e310849a19e02b8bfcbb63147f495d8f872dd96f", "shasum": "" }, "require": { @@ -909,7 +909,7 @@ "type": "github" } ], - "time": "2025-03-23T14:57:32+00:00" + "time": "2025-04-27T12:20:45+00:00" }, { "name": "phpunit/php-code-coverage", @@ -1234,16 +1234,16 @@ }, { "name": "phpunit/phpunit", - "version": "10.5.45", + "version": "10.5.46", "source": { "type": "git", "url": "https://github.com/sebastianbergmann/phpunit.git", - "reference": "bd68a781d8e30348bc297449f5234b3458267ae8" + "reference": "8080be387a5be380dda48c6f41cee4a13aadab3d" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/sebastianbergmann/phpunit/zipball/bd68a781d8e30348bc297449f5234b3458267ae8", - "reference": "bd68a781d8e30348bc297449f5234b3458267ae8", + "url": "https://api.github.com/repos/sebastianbergmann/phpunit/zipball/8080be387a5be380dda48c6f41cee4a13aadab3d", + "reference": "8080be387a5be380dda48c6f41cee4a13aadab3d", "shasum": "" }, "require": { @@ -1253,7 +1253,7 @@ "ext-mbstring": "*", "ext-xml": "*", "ext-xmlwriter": "*", - "myclabs/deep-copy": "^1.12.1", + "myclabs/deep-copy": "^1.13.1", "phar-io/manifest": "^2.0.4", "phar-io/version": "^3.2.1", "php": ">=8.1", @@ -1315,7 +1315,7 @@ "support": { "issues": "https://github.com/sebastianbergmann/phpunit/issues", "security": "https://github.com/sebastianbergmann/phpunit/security/policy", - "source": "https://github.com/sebastianbergmann/phpunit/tree/10.5.45" + "source": "https://github.com/sebastianbergmann/phpunit/tree/10.5.46" }, "funding": [ { @@ -1326,12 +1326,20 @@ "url": "https://github.com/sebastianbergmann", "type": "github" }, + { + "url": "https://liberapay.com/sebastianbergmann", + "type": "liberapay" + }, + { + "url": "https://thanks.dev/u/gh/sebastianbergmann", + "type": "thanks_dev" + }, { "url": "https://tidelift.com/funding/github/packagist/phpunit/phpunit", "type": "tidelift" } ], - "time": "2025-02-06T16:08:12+00:00" + "time": "2025-05-02T06:46:24+00:00" }, { "name": "psr/container", @@ -2354,16 +2362,16 @@ }, { "name": "squizlabs/php_codesniffer", - "version": "3.12.0", + "version": "3.13.0", "source": { "type": "git", "url": "https://github.com/PHPCSStandards/PHP_CodeSniffer.git", - "reference": "2d1b63db139c3c6ea0c927698e5160f8b3b8d630" + "reference": "65ff2489553b83b4597e89c3b8b721487011d186" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/PHPCSStandards/PHP_CodeSniffer/zipball/2d1b63db139c3c6ea0c927698e5160f8b3b8d630", - "reference": "2d1b63db139c3c6ea0c927698e5160f8b3b8d630", + "url": "https://api.github.com/repos/PHPCSStandards/PHP_CodeSniffer/zipball/65ff2489553b83b4597e89c3b8b721487011d186", + "reference": "65ff2489553b83b4597e89c3b8b721487011d186", "shasum": "" }, "require": { @@ -2434,7 +2442,7 @@ "type": "thanks_dev" } ], - "time": "2025-03-18T05:04:51+00:00" + "time": "2025-05-11T03:36:00+00:00" }, { "name": "symfony/config", @@ -2513,16 +2521,16 @@ }, { "name": "symfony/dependency-injection", - "version": "v6.4.19", + "version": "v6.4.20", "source": { "type": "git", "url": "https://github.com/symfony/dependency-injection.git", - "reference": "b343c3b2f1539fe41331657b37d5c96c1d1ea842" + "reference": "c49796a9184a532843e78e50df9e55708b92543a" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/dependency-injection/zipball/b343c3b2f1539fe41331657b37d5c96c1d1ea842", - "reference": "b343c3b2f1539fe41331657b37d5c96c1d1ea842", + "url": "https://api.github.com/repos/symfony/dependency-injection/zipball/c49796a9184a532843e78e50df9e55708b92543a", + "reference": "c49796a9184a532843e78e50df9e55708b92543a", "shasum": "" }, "require": { @@ -2530,7 +2538,7 @@ "psr/container": "^1.1|^2.0", "symfony/deprecation-contracts": "^2.5|^3", "symfony/service-contracts": "^2.5|^3.0", - "symfony/var-exporter": "^6.2.10|^7.0" + "symfony/var-exporter": "^6.4.20|^7.2.5" }, "conflict": { "ext-psr": "<1.1|>=2", @@ -2574,7 +2582,7 @@ "description": "Allows you to standardize and centralize the way objects are constructed in your application", "homepage": "https://symfony.com", "support": { - "source": "https://github.com/symfony/dependency-injection/tree/v6.4.19" + "source": "https://github.com/symfony/dependency-injection/tree/v6.4.20" }, "funding": [ { @@ -2590,7 +2598,7 @@ "type": "tidelift" } ], - "time": "2025-02-20T10:02:49+00:00" + "time": "2025-03-13T09:55:08+00:00" }, { "name": "symfony/deprecation-contracts", @@ -2727,7 +2735,7 @@ }, { "name": "symfony/polyfill-ctype", - "version": "v1.31.0", + "version": "v1.32.0", "source": { "type": "git", "url": "https://github.com/symfony/polyfill-ctype.git", @@ -2786,7 +2794,7 @@ "portable" ], "support": { - "source": "https://github.com/symfony/polyfill-ctype/tree/v1.31.0" + "source": "https://github.com/symfony/polyfill-ctype/tree/v1.32.0" }, "funding": [ { @@ -2806,19 +2814,20 @@ }, { "name": "symfony/polyfill-mbstring", - "version": "v1.31.0", + "version": "v1.32.0", "source": { "type": "git", "url": "https://github.com/symfony/polyfill-mbstring.git", - "reference": "85181ba99b2345b0ef10ce42ecac37612d9fd341" + "reference": "6d857f4d76bd4b343eac26d6b539585d2bc56493" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/polyfill-mbstring/zipball/85181ba99b2345b0ef10ce42ecac37612d9fd341", - "reference": "85181ba99b2345b0ef10ce42ecac37612d9fd341", + "url": "https://api.github.com/repos/symfony/polyfill-mbstring/zipball/6d857f4d76bd4b343eac26d6b539585d2bc56493", + "reference": "6d857f4d76bd4b343eac26d6b539585d2bc56493", "shasum": "" }, "require": { + "ext-iconv": "*", "php": ">=7.2" }, "provide": { @@ -2866,7 +2875,7 @@ "shim" ], "support": { - "source": "https://github.com/symfony/polyfill-mbstring/tree/v1.31.0" + "source": "https://github.com/symfony/polyfill-mbstring/tree/v1.32.0" }, "funding": [ { @@ -2882,7 +2891,7 @@ "type": "tidelift" } ], - "time": "2024-09-09T11:45:10+00:00" + "time": "2024-12-23T08:48:59+00:00" }, { "name": "symfony/service-contracts", @@ -2969,16 +2978,16 @@ }, { "name": "symfony/var-exporter", - "version": "v6.4.19", + "version": "v6.4.21", "source": { "type": "git", "url": "https://github.com/symfony/var-exporter.git", - "reference": "be6e71b0c257884c1107313de5d247741cfea172" + "reference": "717e7544aa99752c54ecba5c0e17459c48317472" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/var-exporter/zipball/be6e71b0c257884c1107313de5d247741cfea172", - "reference": "be6e71b0c257884c1107313de5d247741cfea172", + "url": "https://api.github.com/repos/symfony/var-exporter/zipball/717e7544aa99752c54ecba5c0e17459c48317472", + "reference": "717e7544aa99752c54ecba5c0e17459c48317472", "shasum": "" }, "require": { @@ -3026,7 +3035,7 @@ "serialize" ], "support": { - "source": "https://github.com/symfony/var-exporter/tree/v6.4.19" + "source": "https://github.com/symfony/var-exporter/tree/v6.4.21" }, "funding": [ { @@ -3042,7 +3051,7 @@ "type": "tidelift" } ], - "time": "2025-02-13T09:33:32+00:00" + "time": "2025-04-27T21:06:26+00:00" }, { "name": "theseer/tokenizer", From c14cd35abc7884dda384af9ca66ffdc48e0d6c5b Mon Sep 17 00:00:00 2001 From: Greg Bowler Date: Mon, 12 May 2025 14:49:01 +0100 Subject: [PATCH 6/9] tweak: remove non-existant test --- phpcs.xml | 1 - 1 file changed, 1 deletion(-) diff --git a/phpcs.xml b/phpcs.xml index bff466c..ce2a486 100644 --- a/phpcs.xml +++ b/phpcs.xml @@ -30,7 +30,6 @@ - From 3b46f5b96a272c1cc327fd7424662085e3775904 Mon Sep 17 00:00:00 2001 From: Greg Bowler Date: Mon, 12 May 2025 15:19:28 +0100 Subject: [PATCH 7/9] tweak: import DateTimeInterface --- src/Query/Query.php | 1 + 1 file changed, 1 insertion(+) diff --git a/src/Query/Query.php b/src/Query/Query.php index dd36c8d..110efab 100644 --- a/src/Query/Query.php +++ b/src/Query/Query.php @@ -1,6 +1,7 @@ Date: Mon, 12 May 2025 15:21:24 +0100 Subject: [PATCH 8/9] tweak: remove superfluous typehint --- src/Query/QueryCollectionFactory.php | 1 - 1 file changed, 1 deletion(-) diff --git a/src/Query/QueryCollectionFactory.php b/src/Query/QueryCollectionFactory.php index e812f9f..c66a7cb 100644 --- a/src/Query/QueryCollectionFactory.php +++ b/src/Query/QueryCollectionFactory.php @@ -69,7 +69,6 @@ protected function recurseLocateDirectory( throw new BaseQueryPathDoesNotExistException($basePath); } - /** @var SplFileInfo $fileInfo */ foreach(new DirectoryIterator($basePath) as $fileInfo) { if($fileInfo->isDot()) { continue; From ff1c2cf3db91c1032bf45fd3aa795b18f87cc55f Mon Sep 17 00:00:00 2001 From: Greg Bowler Date: Mon, 12 May 2025 15:33:02 +0100 Subject: [PATCH 9/9] tweak: ignore complexity of abstract query class --- src/Query/Query.php | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/src/Query/Query.php b/src/Query/Query.php index 110efab..e4c2230 100644 --- a/src/Query/Query.php +++ b/src/Query/Query.php @@ -9,6 +9,10 @@ use PDOStatement; use PHPSQLParser\lexer\PHPSQLLexer; + +/** + * @SuppressWarnings(PHPMD.ExcessiveClassComplexity) + */ abstract class Query { const SPECIAL_BINDINGS = [ "field" => ["groupBy", "orderBy"],