From 9deb64598736fb0a082eca8d261f2c053e9115a8 Mon Sep 17 00:00:00 2001 From: Simon Mundy Date: Mon, 5 Oct 2026 11:01:56 +1100 Subject: [PATCH] Narrow Predicate's fluent identifiers to what each predicate accepts between(), equalTo(), in(), isNull(), like() and their siblings advertised int, float or array identifiers that the Between, Operator, In, IsNull and Like constructors reject, so such calls raised a TypeError one frame deeper. The fluent signatures now match, a test compares each one against its predicate's constructor, and the API listing in where-having.md shows the narrowed types. Signed-off-by: Simon Mundy --- analyzer-baseline.toml | 60 ------------------- docs/book/sql/where-having.md | 45 ++++++++------ src/Sql/Predicate/Predicate.php | 36 ++++++------ test/unit/Sql/Predicate/PredicateTest.php | 71 +++++++++++++++++++++++ 4 files changed, 116 insertions(+), 96 deletions(-) diff --git a/analyzer-baseline.toml b/analyzer-baseline.toml index 6a0f82f4..f61f092a 100644 --- a/analyzer-baseline.toml +++ b/analyzer-baseline.toml @@ -3804,24 +3804,6 @@ code = "impossible-condition" message = "This condition (type `false`) will always evaluate to false." count = 1 -[[issues]] -file = "src/Sql/Predicate/Predicate.php" -code = "imprecise-type" -message = "Type `array` in parameter `$identifier` is imprecise, equivalent to `array`." -count = 2 - -[[issues]] -file = "src/Sql/Predicate/Predicate.php" -code = "imprecise-type" -message = "Type `array` in parameter `$maxValue` is imprecise, equivalent to `array`." -count = 2 - -[[issues]] -file = "src/Sql/Predicate/Predicate.php" -code = "imprecise-type" -message = "Type `array` in parameter `$minValue` is imprecise, equivalent to `array`." -count = 2 - [[issues]] file = "src/Sql/Predicate/Predicate.php" code = "imprecise-type" @@ -3846,48 +3828,6 @@ code = "invalid-property-assignment-value" message = 'Invalid type for property `$unnest`: expected `PhpDb\Sql\Predicate\Predicate`, but got `null`.' count = 1 -[[issues]] -file = "src/Sql/Predicate/Predicate.php" -code = "possibly-invalid-argument" -message = 'Possible argument type mismatch for argument #1 of `PhpDb\Sql\Predicate\Between::__construct`: expected `PhpDb\Sql\ArgumentInterface|null|string`, but possibly received `PhpDb\Sql\ArgumentInterface|array|float|int|null|string`.' -count = 2 - -[[issues]] -file = "src/Sql/Predicate/Predicate.php" -code = "possibly-invalid-argument" -message = 'Possible argument type mismatch for argument #1 of `PhpDb\Sql\Predicate\In::__construct`: expected `PhpDb\Sql\ArgumentInterface|null|string`, but possibly received `PhpDb\Sql\ArgumentInterface|float|int|string`.' -count = 2 - -[[issues]] -file = "src/Sql/Predicate/Predicate.php" -code = "possibly-invalid-argument" -message = 'Possible argument type mismatch for argument #1 of `PhpDb\Sql\Predicate\IsNull::__construct`: expected `PhpDb\Sql\ArgumentInterface|null|string`, but possibly received `PhpDb\Sql\ArgumentInterface|float|int|string`.' -count = 2 - -[[issues]] -file = "src/Sql/Predicate/Predicate.php" -code = "possibly-invalid-argument" -message = 'Possible argument type mismatch for argument #1 of `PhpDb\Sql\Predicate\Like::__construct`: expected `PhpDb\Sql\ArgumentInterface|null|string`, but possibly received `PhpDb\Sql\ArgumentInterface|float|int|null|string`.' -count = 2 - -[[issues]] -file = "src/Sql/Predicate/Predicate.php" -code = "possibly-invalid-argument" -message = 'Possible argument type mismatch for argument #1 of `PhpDb\Sql\Predicate\Operator::__construct`: expected `PhpDb\Sql\ArgumentInterface|PhpDb\Sql\ExpressionInterface|PhpDb\Sql\SqlInterface|null|string`, but possibly received `PhpDb\Sql\ArgumentInterface|float|int|null|string`.' -count = 6 - -[[issues]] -file = "src/Sql/Predicate/Predicate.php" -code = "possibly-invalid-argument" -message = 'Possible argument type mismatch for argument #2 of `PhpDb\Sql\Predicate\Between::__construct`: expected `PhpDb\Sql\ArgumentInterface|float|int|null|string`, but possibly received `PhpDb\Sql\ArgumentInterface|array|float|int|null|string`.' -count = 2 - -[[issues]] -file = "src/Sql/Predicate/Predicate.php" -code = "possibly-invalid-argument" -message = 'Possible argument type mismatch for argument #3 of `PhpDb\Sql\Predicate\Between::__construct`: expected `PhpDb\Sql\ArgumentInterface|float|int|null|string`, but possibly received `PhpDb\Sql\ArgumentInterface|array|float|int|null|string`.' -count = 2 - [[issues]] file = "src/Sql/Predicate/PredicateInterface.php" code = "missing-api-or-internal" diff --git a/docs/book/sql/where-having.md b/docs/book/sql/where-having.md index ecb24bf9..02ab5303 100644 --- a/docs/book/sql/where-having.md +++ b/docs/book/sql/where-having.md @@ -132,35 +132,35 @@ class Predicate extends PredicateSet public function setUnnest(?Predicate $predicate = null) : void; public function unnest() : Predicate; public function equalTo( - null|float|int|string|ArgumentInterface $left, + null|string|ArgumentInterface $left, null|float|int|string|ArgumentInterface $right ) : static; public function notEqualTo( - null|float|int|string|ArgumentInterface $left, + null|string|ArgumentInterface $left, null|float|int|string|ArgumentInterface $right ) : static; public function lessThan( - null|float|int|string|ArgumentInterface $left, + null|string|ArgumentInterface $left, null|float|int|string|ArgumentInterface $right ) : static; public function greaterThan( - null|float|int|string|ArgumentInterface $left, + null|string|ArgumentInterface $left, null|float|int|string|ArgumentInterface $right ) : static; public function lessThanOrEqualTo( - null|float|int|string|ArgumentInterface $left, + null|string|ArgumentInterface $left, null|float|int|string|ArgumentInterface $right ) : static; public function greaterThanOrEqualTo( - null|float|int|string|ArgumentInterface $left, + null|string|ArgumentInterface $left, null|float|int|string|ArgumentInterface $right ) : static; public function like( - null|float|int|string|ArgumentInterface $identifier, + null|string|ArgumentInterface $identifier, null|float|int|string|ArgumentInterface $like ) : static; public function notLike( - null|float|int|string|ArgumentInterface $identifier, + null|string|ArgumentInterface $identifier, null|float|int|string|ArgumentInterface $notLike ) : static; public function literal(string $literal) : static; @@ -170,28 +170,28 @@ class Predicate extends PredicateSet |ExpressionInterface $parameters = [] ) : static; public function isNull( - float|int|string|ArgumentInterface $identifier + string|ArgumentInterface $identifier ) : static; public function isNotNull( - float|int|string|ArgumentInterface $identifier + string|ArgumentInterface $identifier ) : static; public function in( - float|int|string|ArgumentInterface $identifier, + string|ArgumentInterface $identifier, array|ArgumentInterface $valueSet ) : static; public function notIn( - float|int|string|ArgumentInterface $identifier, + string|ArgumentInterface $identifier, array|ArgumentInterface $valueSet ) : static; public function between( - null|float|int|string|array|ArgumentInterface $identifier, - null|float|int|string|array|ArgumentInterface $minValue, - null|float|int|string|array|ArgumentInterface $maxValue + null|string|ArgumentInterface $identifier, + null|float|int|string|ArgumentInterface $minValue, + null|float|int|string|ArgumentInterface $maxValue ) : static; public function notBetween( - null|float|int|string|array|ArgumentInterface $identifier, - null|float|int|string|array|ArgumentInterface $minValue, - null|float|int|string|array|ArgumentInterface $maxValue + null|string|ArgumentInterface $identifier, + null|float|int|string|ArgumentInterface $minValue, + null|float|int|string|ArgumentInterface $maxValue ) : static; public function predicate(PredicateInterface $predicate) : static; @@ -222,6 +222,15 @@ class Predicate extends PredicateSet > `ArgumentInterface` implementations. Pass an `Argument\Identifier` for > column names, `Argument\Value` for values, or `Argument\Literal` for raw > SQL fragments directly to control how values are treated. +> +> The left-hand side and identifier parameters accept a string (treated as an +> identifier) or an `ArgumentInterface`. To put a value on the left, wrap it in +> `Argument\Value`: +> +> ```php +> $where->equalTo(new Argument\Value(5), new Argument\Identifier('quantity')); +> // WHERE '5' = "quantity" +> ``` Each method in the API will produce a corresponding `Predicate` object of a similarly named type, as described below. diff --git a/src/Sql/Predicate/Predicate.php b/src/Sql/Predicate/Predicate.php index 9704d52f..e0e5f13e 100644 --- a/src/Sql/Predicate/Predicate.php +++ b/src/Sql/Predicate/Predicate.php @@ -31,9 +31,9 @@ class Predicate extends PredicateSet * @return $this Provides a fluent interface */ public function between( - float|int|string|array|ArgumentInterface|null $identifier, - float|int|string|array|ArgumentInterface|null $minValue, - float|int|string|array|ArgumentInterface|null $maxValue, + string|ArgumentInterface|null $identifier, + float|int|string|ArgumentInterface|null $minValue, + float|int|string|ArgumentInterface|null $maxValue, ): static { $this->addPredicate( new Between($identifier, $minValue, $maxValue), @@ -48,7 +48,7 @@ public function between( * Utilizes Operator predicate */ public function equalTo( - float|int|string|ArgumentInterface|null $left, + string|ArgumentInterface|null $left, float|int|string|ArgumentInterface|null $right, ): static { $this->addPredicate( @@ -90,7 +90,7 @@ public function expression( * @return $this Provides a fluent interface */ public function greaterThan( - float|int|string|ArgumentInterface|null $left, + string|ArgumentInterface|null $left, float|int|string|ArgumentInterface|null $right, ): static { $this->addPredicate( @@ -108,7 +108,7 @@ public function greaterThan( * @return $this Provides a fluent interface */ public function greaterThanOrEqualTo( - float|int|string|ArgumentInterface|null $left, + string|ArgumentInterface|null $left, float|int|string|ArgumentInterface|null $right, ): static { $this->addPredicate( @@ -125,7 +125,7 @@ public function greaterThanOrEqualTo( * * @return $this Provides a fluent interface */ - public function in(float|int|string|ArgumentInterface $identifier, array|ArgumentInterface $valueSet): static + public function in(string|ArgumentInterface $identifier, array|ArgumentInterface $valueSet): static { $this->addPredicate( new In($identifier, $valueSet), @@ -141,7 +141,7 @@ public function in(float|int|string|ArgumentInterface $identifier, array|Argumen * * @return $this Provides a fluent interface */ - public function isNotNull(float|int|string|ArgumentInterface $identifier): static + public function isNotNull(string|ArgumentInterface $identifier): static { $this->addPredicate( new IsNotNull($identifier), @@ -157,7 +157,7 @@ public function isNotNull(float|int|string|ArgumentInterface $identifier): stati * * @return $this Provides a fluent interface */ - public function isNull(float|int|string|ArgumentInterface $identifier): static + public function isNull(string|ArgumentInterface $identifier): static { $this->addPredicate( new IsNull($identifier), @@ -172,7 +172,7 @@ public function isNull(float|int|string|ArgumentInterface $identifier): static * Utilizes Operator predicate */ public function lessThan( - float|int|string|ArgumentInterface|null $left, + string|ArgumentInterface|null $left, float|int|string|ArgumentInterface|null $right, ): static { $this->addPredicate( @@ -190,7 +190,7 @@ public function lessThan( * @return $this Provides a fluent interface */ public function lessThanOrEqualTo( - float|int|string|ArgumentInterface|null $left, + string|ArgumentInterface|null $left, float|int|string|ArgumentInterface|null $right, ): static { $this->addPredicate( @@ -208,7 +208,7 @@ public function lessThanOrEqualTo( * @return $this Provides a fluent interface */ public function like( - float|int|string|ArgumentInterface|null $identifier, + string|ArgumentInterface|null $identifier, float|int|string|ArgumentInterface|null $like, ): static { $this->addPredicate( @@ -255,9 +255,9 @@ public function nest(): self * @return $this Provides a fluent interface */ public function notBetween( - float|int|string|array|ArgumentInterface|null $identifier, - float|int|string|array|ArgumentInterface|null $minValue, - float|int|string|array|ArgumentInterface|null $maxValue, + string|ArgumentInterface|null $identifier, + float|int|string|ArgumentInterface|null $minValue, + float|int|string|ArgumentInterface|null $maxValue, ): static { $this->addPredicate( new NotBetween($identifier, $minValue, $maxValue), @@ -272,7 +272,7 @@ public function notBetween( * Utilizes Operator predicate */ public function notEqualTo( - float|int|string|ArgumentInterface|null $left, + string|ArgumentInterface|null $left, float|int|string|ArgumentInterface|null $right, ): static { $this->addPredicate( @@ -289,7 +289,7 @@ public function notEqualTo( * * @return $this Provides a fluent interface */ - public function notIn(float|int|string|ArgumentInterface $identifier, array|ArgumentInterface $valueSet): static + public function notIn(string|ArgumentInterface $identifier, array|ArgumentInterface $valueSet): static { $this->addPredicate( new NotIn($identifier, $valueSet), @@ -306,7 +306,7 @@ public function notIn(float|int|string|ArgumentInterface $identifier, array|Argu * @return $this Provides a fluent interface */ public function notLike( - float|int|string|ArgumentInterface|null $identifier, + string|ArgumentInterface|null $identifier, float|int|string|ArgumentInterface|null $notLike, ): static { $this->addPredicate( diff --git a/test/unit/Sql/Predicate/PredicateTest.php b/test/unit/Sql/Predicate/PredicateTest.php index 98c261fb..2d668c55 100644 --- a/test/unit/Sql/Predicate/PredicateTest.php +++ b/test/unit/Sql/Predicate/PredicateTest.php @@ -9,18 +9,72 @@ use PhpDb\Adapter\Platform\Sql92; use PhpDb\Sql\Argument; use PhpDb\Sql\Expression; +use PhpDb\Sql\Predicate\Between; use PhpDb\Sql\Predicate\Exception\RuntimeException; +use PhpDb\Sql\Predicate\In; +use PhpDb\Sql\Predicate\IsNull; +use PhpDb\Sql\Predicate\Like; +use PhpDb\Sql\Predicate\Operator; use PhpDb\Sql\Predicate\Predicate; use PhpDb\Sql\Predicate\PredicateInterface; use PhpDb\Sql\Select; +use PHPUnit\Framework\Attributes\DataProvider; use PHPUnit\Framework\Attributes\Group; use PHPUnit\Framework\Attributes\Test; use PHPUnit\Framework\Attributes\TestDox; use PHPUnit\Framework\TestCase; +use ReflectionMethod; +use ReflectionNamedType; +use ReflectionParameter; +use ReflectionUnionType; + +use function array_diff; +use function array_map; #[Group('unit')] final class PredicateTest extends TestCase { + /** @return array */ + public static function fluentParameterProvider(): array + { + return [ + 'between identifier' => ['between', 0, Between::class, 0], + 'between minimum' => ['between', 1, Between::class, 1], + 'between maximum' => ['between', 2, Between::class, 2], + 'notBetween identifier' => ['notBetween', 0, Between::class, 0], + 'equalTo left' => ['equalTo', 0, Operator::class, 0], + 'equalTo right' => ['equalTo', 1, Operator::class, 2], + 'notEqualTo left' => ['notEqualTo', 0, Operator::class, 0], + 'greaterThan left' => ['greaterThan', 0, Operator::class, 0], + 'greaterThanOrEqualTo left' => ['greaterThanOrEqualTo', 0, Operator::class, 0], + 'lessThan left' => ['lessThan', 0, Operator::class, 0], + 'lessThanOrEqualTo left' => ['lessThanOrEqualTo', 0, Operator::class, 0], + 'in identifier' => ['in', 0, In::class, 0], + 'in value set' => ['in', 1, In::class, 1], + 'notIn identifier' => ['notIn', 0, In::class, 0], + 'isNull identifier' => ['isNull', 0, IsNull::class, 0], + 'isNotNull identifier' => ['isNotNull', 0, IsNull::class, 0], + 'like identifier' => ['like', 0, Like::class, 0], + 'like pattern' => ['like', 1, Like::class, 1], + 'notLike identifier' => ['notLike', 0, Like::class, 0], + ]; + } + + /** + * @throws ErrorException + */ + /** @return list */ + private static function typeNames(ReflectionParameter $parameter): array + { + $type = $parameter->getType(); + $types = $type instanceof ReflectionUnionType ? $type->getTypes() : [$type]; + + return array_map( + static fn(?ReflectionNamedType $named): string => $named?->getName() ?? 'mixed', + $types, + ); + } + #[Test] public function betweenCreatesBetweenPredicate(): void { @@ -162,6 +216,23 @@ public function expressionNullParameters(): void } } + /** + * @param class-string $predicateClass + */ + #[Test] + #[DataProvider('fluentParameterProvider')] + public function fluentMethodAcceptsOnlyTypesItsPredicateAccepts( + string $method, + int $position, + string $predicateClass, + int $constructorPosition, + ): void { + $fluent = (new ReflectionMethod(Predicate::class, $method))->getParameters()[$position]; + $constructor = (new ReflectionMethod($predicateClass, '__construct'))->getParameters()[$constructorPosition]; + + static::assertSame([], array_diff(self::typeNames($fluent), self::typeNames($constructor))); + } + #[Test] public function greaterThanCreatesOperatorPredicate(): void {