diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 70a1405..3e1def9 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -9,7 +9,7 @@ jobs: runs-on: ubuntu-latest strategy: matrix: - php: [ 8.2, 8.3, 8.4, 8.5 ] + php: [ 8.4, 8.5 ] steps: - uses: actions/checkout@v6 @@ -41,7 +41,7 @@ jobs: needs: [ composer ] strategy: matrix: - php: [ 8.2, 8.3, 8.4, 8.5 ] + php: [ 8.4, 8.5 ] outputs: coverage: ${{ steps.store-coverage.outputs.coverage_text }} @@ -76,7 +76,7 @@ jobs: needs: [ phpunit ] strategy: matrix: - php: [ 8.2, 8.3, 8.4, 8.5 ] + php: [ 8.4, 8.5 ] steps: - uses: actions/checkout@v4 @@ -100,7 +100,7 @@ jobs: needs: [ composer ] strategy: matrix: - php: [ 8.2, 8.3, 8.4, 8.5 ] + php: [ 8.4, 8.5 ] steps: - uses: actions/download-artifact@v4 @@ -124,7 +124,7 @@ jobs: needs: [ composer ] strategy: matrix: - php: [ 8.2, 8.3, 8.4, 8.5 ] + php: [ 8.4, 8.5 ] steps: - uses: actions/download-artifact@v4 @@ -148,7 +148,7 @@ jobs: needs: [ composer ] strategy: matrix: - php: [ 8.2, 8.3, 8.4, 8.5 ] + php: [ 8.4, 8.5 ] steps: - uses: actions/download-artifact@v4 diff --git a/composer.json b/composer.json index 8237eaa..fea0d45 100644 --- a/composer.json +++ b/composer.json @@ -3,7 +3,7 @@ "description": "Object oriented representation of SQL queries.", "require": { - "php": ">=8.0" + "php": ">=8.4" }, "require-dev": { @@ -24,10 +24,14 @@ "Gt\\SqlBuilder\\Test\\": "./test/phpunit" } }, + "config": { + "platform": { + "php": "8.4.0" + } + }, "scripts": { "phpunit": "vendor/bin/phpunit --configuration phpunit.xml", - "phpunit:coverage": "XDEBUG_MODE=coverage vendor/bin/phpunit --configuration phpunit.xml --coverage-text", "phpstan": "vendor/bin/phpstan analyse --level 6 src", "phpcs": "vendor/bin/phpcs src --standard=phpcs.xml", "phpmd": "vendor/bin/phpmd src/ text phpmd.xml", @@ -42,7 +46,7 @@ "funding": [ { "type": "github", - "url": "https://github.com/sponsors/PhpGt" + "url": "https://github.com/sponsors/phpgt" } ] } diff --git a/composer.lock b/composer.lock index e3161f2..fc60f35 100644 --- a/composer.lock +++ b/composer.lock @@ -4,7 +4,7 @@ "Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies", "This file is @generated automatically" ], - "content-hash": "d9259536d8965d0a25abc276e36bf574", + "content-hash": "c07248f1d453fc34b5737a1d558ba458", "packages": [], "packages-dev": [ { @@ -2154,16 +2154,16 @@ }, { "name": "symfony/config", - "version": "v7.4.7", + "version": "v7.4.8", "source": { "type": "git", "url": "https://github.com/symfony/config.git", - "reference": "6c17162555bfb58957a55bb0e43e00035b6ae3d5" + "reference": "2d19dde43fa2ff720b9a40763ace7226594f503b" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/config/zipball/6c17162555bfb58957a55bb0e43e00035b6ae3d5", - "reference": "6c17162555bfb58957a55bb0e43e00035b6ae3d5", + "url": "https://api.github.com/repos/symfony/config/zipball/2d19dde43fa2ff720b9a40763ace7226594f503b", + "reference": "2d19dde43fa2ff720b9a40763ace7226594f503b", "shasum": "" }, "require": { @@ -2209,7 +2209,7 @@ "description": "Helps you find, load, combine, autofill and validate configuration values of any kind", "homepage": "https://symfony.com", "support": { - "source": "https://github.com/symfony/config/tree/v7.4.7" + "source": "https://github.com/symfony/config/tree/v7.4.8" }, "funding": [ { @@ -2229,20 +2229,20 @@ "type": "tidelift" } ], - "time": "2026-03-06T10:41:14+00:00" + "time": "2026-03-24T13:12:05+00:00" }, { "name": "symfony/dependency-injection", - "version": "v7.4.7", + "version": "v7.4.8", "source": { "type": "git", "url": "https://github.com/symfony/dependency-injection.git", - "reference": "0f651e58f4917fb0e2cd261ccbfe3d71e6e0f5db" + "reference": "f7025fd7b687c240426562f86ada06a93b1e771d" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/dependency-injection/zipball/0f651e58f4917fb0e2cd261ccbfe3d71e6e0f5db", - "reference": "0f651e58f4917fb0e2cd261ccbfe3d71e6e0f5db", + "url": "https://api.github.com/repos/symfony/dependency-injection/zipball/f7025fd7b687c240426562f86ada06a93b1e771d", + "reference": "f7025fd7b687c240426562f86ada06a93b1e771d", "shasum": "" }, "require": { @@ -2293,7 +2293,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/v7.4.7" + "source": "https://github.com/symfony/dependency-injection/tree/v7.4.8" }, "funding": [ { @@ -2313,7 +2313,7 @@ "type": "tidelift" } ], - "time": "2026-03-03T07:48:48+00:00" + "time": "2026-03-31T06:50:29+00:00" }, { "name": "symfony/deprecation-contracts", @@ -2384,16 +2384,16 @@ }, { "name": "symfony/filesystem", - "version": "v7.4.6", + "version": "v7.4.8", "source": { "type": "git", "url": "https://github.com/symfony/filesystem.git", - "reference": "3ebc794fa5315e59fd122561623c2e2e4280538e" + "reference": "58b9790d12f9670b7f53a1c1738febd3108970a5" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/filesystem/zipball/3ebc794fa5315e59fd122561623c2e2e4280538e", - "reference": "3ebc794fa5315e59fd122561623c2e2e4280538e", + "url": "https://api.github.com/repos/symfony/filesystem/zipball/58b9790d12f9670b7f53a1c1738febd3108970a5", + "reference": "58b9790d12f9670b7f53a1c1738febd3108970a5", "shasum": "" }, "require": { @@ -2430,7 +2430,7 @@ "description": "Provides basic utilities for the filesystem", "homepage": "https://symfony.com", "support": { - "source": "https://github.com/symfony/filesystem/tree/v7.4.6" + "source": "https://github.com/symfony/filesystem/tree/v7.4.8" }, "funding": [ { @@ -2450,7 +2450,7 @@ "type": "tidelift" } ], - "time": "2026-02-25T16:50:00+00:00" + "time": "2026-03-24T13:12:05+00:00" }, { "name": "symfony/polyfill-ctype", @@ -2709,26 +2709,25 @@ }, { "name": "symfony/var-exporter", - "version": "v7.4.0", + "version": "v8.0.8", "source": { "type": "git", "url": "https://github.com/symfony/var-exporter.git", - "reference": "03a60f169c79a28513a78c967316fbc8bf17816f" + "reference": "15776bb07a91b089037da89f8832fa41d5fa6ec6" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/var-exporter/zipball/03a60f169c79a28513a78c967316fbc8bf17816f", - "reference": "03a60f169c79a28513a78c967316fbc8bf17816f", + "url": "https://api.github.com/repos/symfony/var-exporter/zipball/15776bb07a91b089037da89f8832fa41d5fa6ec6", + "reference": "15776bb07a91b089037da89f8832fa41d5fa6ec6", "shasum": "" }, "require": { - "php": ">=8.2", - "symfony/deprecation-contracts": "^2.5|^3" + "php": ">=8.4" }, "require-dev": { - "symfony/property-access": "^6.4|^7.0|^8.0", - "symfony/serializer": "^6.4|^7.0|^8.0", - "symfony/var-dumper": "^6.4|^7.0|^8.0" + "symfony/property-access": "^7.4|^8.0", + "symfony/serializer": "^7.4|^8.0", + "symfony/var-dumper": "^7.4|^8.0" }, "type": "library", "autoload": { @@ -2766,7 +2765,7 @@ "serialize" ], "support": { - "source": "https://github.com/symfony/var-exporter/tree/v7.4.0" + "source": "https://github.com/symfony/var-exporter/tree/v8.0.8" }, "funding": [ { @@ -2786,7 +2785,7 @@ "type": "tidelift" } ], - "time": "2025-09-11T10:15:23+00:00" + "time": "2026-03-30T15:14:47+00:00" }, { "name": "theseer/tokenizer", @@ -2845,8 +2844,11 @@ "prefer-stable": false, "prefer-lowest": false, "platform": { - "php": ">=8.0" + "php": ">=8.4" }, "platform-dev": {}, + "platform-overrides": { + "php": "8.4.0" + }, "plugin-api-version": "2.9.0" } diff --git a/src/Condition/Between.php b/src/Condition/Between.php new file mode 100644 index 0000000..a01eff0 --- /dev/null +++ b/src/Condition/Between.php @@ -0,0 +1,30 @@ +column, + $this->formatValue($this->from), + $this->formatValue($this->upperBound), + ); + } +} diff --git a/src/Condition/ConditionMatcher.php b/src/Condition/ConditionMatcher.php new file mode 100644 index 0000000..4e48d49 --- /dev/null +++ b/src/Condition/ConditionMatcher.php @@ -0,0 +1,6 @@ +value === null) { + return $this->column . " is null"; + } + + return $this->column . " = " . $this->formatValue($this->value); + } +} diff --git a/src/Condition/GreaterThan.php b/src/Condition/GreaterThan.php new file mode 100644 index 0000000..feec092 --- /dev/null +++ b/src/Condition/GreaterThan.php @@ -0,0 +1,24 @@ +column . " > " . $this->formatValue($this->value); + } +} diff --git a/src/Condition/LessThan.php b/src/Condition/LessThan.php new file mode 100644 index 0000000..4770578 --- /dev/null +++ b/src/Condition/LessThan.php @@ -0,0 +1,24 @@ +column . " < " . $this->formatValue($this->value); + } +} diff --git a/src/Condition/MatchAll.php b/src/Condition/MatchAll.php new file mode 100644 index 0000000..3ad1f95 --- /dev/null +++ b/src/Condition/MatchAll.php @@ -0,0 +1,8 @@ +matchLogic = $matcher->getLogic(); + $this->parts = $parts; + } + + public function getCondition( + ?string $subLogic = null, + string $separator = "\n\t", + ):string { + unset($subLogic); + $condition = ""; + $wrap = false; + + foreach($this->parts as $index => $part) { + if($index > 0) { + $condition .= $separator . $this->matchLogic . " "; + } + + if($part instanceof Condition) { + $rendered = $part->getCondition(); + $wrap = true; + } + else { + $rendered = $part; + } + + $condition .= $rendered; + } + + if($wrap) { + return "( $condition )"; + } + + return $condition; + } +} diff --git a/src/Condition/SqlValueFormatter.php b/src/Condition/SqlValueFormatter.php new file mode 100644 index 0000000..824ef56 --- /dev/null +++ b/src/Condition/SqlValueFormatter.php @@ -0,0 +1,28 @@ +format("Y-m-d H:i:s") . "'"; + } + + if(is_bool($value)) { + return $value ? "1" : "0"; + } + + if(is_int($value) || is_float($value)) { + return (string)$value; + } + + return "'" . str_replace("'", "''", $value) . "'"; + } +} diff --git a/test/phpunit/Condition/ComparisonConditionTest.php b/test/phpunit/Condition/ComparisonConditionTest.php new file mode 100644 index 0000000..bbbddf6 --- /dev/null +++ b/test/phpunit/Condition/ComparisonConditionTest.php @@ -0,0 +1,71 @@ +getCondition()); + } + + public function testEqualsNull():void { + $sut = new Equals("deletedAt", null); + self::assertSame("deletedAt is null", $sut->getCondition()); + } + + public function testEqualsString():void { + $sut = new Equals("status", "active"); + self::assertSame("status = 'active'", $sut->getCondition()); + } + + public function testEqualsEscapesSingleQuotes():void { + $sut = new Equals("surname", "O'Reilly"); + self::assertSame("surname = 'O''Reilly'", $sut->getCondition()); + } + + public function testEqualsFormatsBooleans():void { + $sut = new Equals("enabled", true); + self::assertSame("enabled = 1", $sut->getCondition()); + } + + public function testGreaterThan():void { + $sut = new GreaterThan("score", 100); + self::assertSame("score > 100", $sut->getCondition()); + } + + public function testGreaterThanDateTime():void { + $sut = new GreaterThan("createdAt", new DateTime("2026-01-02 03:04:05")); + self::assertSame("createdAt > '2026-01-02 03:04:05'", $sut->getCondition()); + } + + public function testLessThan():void { + $sut = new LessThan("score", 10); + self::assertSame("score < 10", $sut->getCondition()); + } + + public function testLessThanFormatsFalseAsZero():void { + $sut = new LessThan("isArchived", false); + self::assertSame("isArchived < 0", $sut->getCondition()); + } + + public function testBetweenScalars():void { + $sut = new Between("score", 10, 20); + self::assertSame("score between 10 and 20", $sut->getCondition()); + } + + public function testBetweenDateTime():void { + $from = new DateTime("2026-01-02 03:04:05"); + $to = new DateTime("2026-06-07 08:09:10"); + $sut = new Between("createdAt", $from, $to); + self::assertSame( + "createdAt between '2026-01-02 03:04:05' and '2026-06-07 08:09:10'", + $sut->getCondition() + ); + } +} diff --git a/test/phpunit/Condition/MultipleConditionTest.php b/test/phpunit/Condition/MultipleConditionTest.php new file mode 100644 index 0000000..18dee1d --- /dev/null +++ b/test/phpunit/Condition/MultipleConditionTest.php @@ -0,0 +1,64 @@ +getCondition() + ); + } + + public function testMatchAnyWithStrings():void { + $sut = new MultipleCondition( + new MatchAny(), + "status = 'active'", + "status = 'pending'", + ); + + self::assertSame( + "status = 'active'\n\tor status = 'pending'", + $sut->getCondition() + ); + } + + public function testMultipleConditionWrapsNestedConditions():void { + $sut = new MultipleCondition( + new MatchAll(), + new GreaterThan("score", 10), + new MultipleCondition( + new MatchAny(), + new Equals("status", "active"), + new LessThan("score", 100), + ), + ); + + self::assertSame( + "( score > 10\n\tand ( status = 'active'\n\tor score < 100 ) )", + $sut->getCondition() + ); + } + + public function testMultipleConditionRejectsNonMatcher():void { + $this->expectException(\TypeError::class); + new MultipleCondition( + new class extends Condition {}, + "test = 1", + ); + } +} diff --git a/test/phpunit/Helper/Query/SelectExampleExtendNewConditions.php b/test/phpunit/Helper/Query/SelectExampleExtendNewConditions.php new file mode 100644 index 0000000..b66ea40 --- /dev/null +++ b/test/phpunit/Helper/Query/SelectExampleExtendNewConditions.php @@ -0,0 +1,27 @@ + 50 and ( status = 'active' or createdAt between '2026-01-01 00:00:00' and '2026-12-31 23:59:59' )", $sql); + } + public function testSelectInnerJoin() { $sut = new SelectExampleInnerJoin(); $sql = self::normalise($sut); @@ -61,4 +67,4 @@ public function testSelectSubquery() { $sql = self::normalise($sut); self::assertEquals("select id, name, dateOfBirth from student where deletedAt is null and age > ( select minAge from consent where consent.district = student.district )", $sql); } -} \ No newline at end of file +}