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 ... "
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",
diff --git a/phpcs.xml b/phpcs.xml
index bff466c..ce2a486 100644
--- a/phpcs.xml
+++ b/phpcs.xml
@@ -30,7 +30,6 @@
-
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
new file mode 100644
index 0000000..42570a9
--- /dev/null
+++ b/src/Query/PhpQuery.php
@@ -0,0 +1,48 @@
+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 {
+ $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 @@
+ ["groupBy", "orderBy"],
+ "int" => ["limit", "offset"],
+ "string" => ["infileName"],
+ ];
+
protected string $filePath;
protected Connection $connection;
+ protected ?string $namespace = null;
- public function __construct(string $filePath, Driver $driver) {
- if(!is_file($filePath)) {
- throw new QueryNotFoundException($filePath);
+ public function getFilePath():string {
+ return $this->filePath;
+ }
+
+ /** @param array|array $bindings */
+ 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);
- $this->filePath = $filePath;
- $this->connection = $driver->getConnection();
+ 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 getFilePath():string {
- return $this->filePath;
+ 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
+ );
+ }
}
- /** @param array|array $bindings */
- abstract public function execute(array $bindings = []):ResultSet;
+ /**
+ * 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 +323,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/QueryCollection.php b/src/Query/QueryCollection.php
index 0678883..28e89ca 100644
--- a/src/Query/QueryCollection.php
+++ b/src/Query/QueryCollection.php
@@ -5,16 +5,17 @@
use Gt\Database\Fetchable;
use Gt\Database\Result\ResultSet;
-class QueryCollection {
+abstract class QueryCollection {
use Fetchable;
protected string $directoryPath;
protected QueryFactory $queryFactory;
+ protected string $appNamespace = "\\App\\Query";
public function __construct(
string $directoryPath,
Driver $driver,
- ?QueryFactory $queryFactory = null
+ ?QueryFactory $queryFactory = null,
) {
$this->directoryPath = $directoryPath;
$this->queryFactory = $queryFactory ?? new QueryFactory(
@@ -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
new file mode 100644
index 0000000..0d4254d
--- /dev/null
+++ b/src/Query/QueryCollectionClass.php
@@ -0,0 +1,5 @@
+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,
);
}
@@ -75,12 +70,11 @@ protected function recurseLocateDirectory(
}
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 +95,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/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/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..e3c3b3e 100644
--- a/test/phpunit/Query/QueryCollectionTest.php
+++ b/test/phpunit/Query/QueryCollectionTest.php
@@ -1,46 +1,42 @@
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 QueryCollection(
+ $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
);
@@ -52,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);
- $resultSet = $this->queryCollection->query("something");
+ $queryCollection = new QueryCollectionDirectory(
+ __DIR__,
+ new Driver(new DefaultSettings()),
+ $queryFactory
+ );
+
+ $query->expects(static::once())->method("execute")->with();
+
+ $resultSet = $queryCollection->query("something");
static::assertInstanceOf(
ResultSet::class,
@@ -63,24 +73,141 @@ 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_defaultNamespace() {
+ $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 = <<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"),
);
}
-}
\ No newline at end of file
+}
diff --git a/test/phpunit/Query/QueryFactoryTest.php b/test/phpunit/Query/QueryFactoryTest.php
index 0dabbd5..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();
}
}
-}
\ No newline at end of file
+
+ /** @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);
+ }
+}