Skip to content

Invokable constraints #6354

@linepogl

Description

@linepogl

PHP 8.5 is coming soon. I believe that PHPUnit's constraints is a very good use case for the new pipe operator.

The new pipe operator forwards a value to any kind of callable, including invokable objects. If PHPUnit's constraints were invokable, then they could be used with the new pipe operator to directly test some value against a constraint.

I have created a proof-of-concept here: https://github.com/linepogl/should

Example of an invocable constraint:

final class IsEqual extends Constraint
{
    ...

    /**
     * @template A
     * @param A $actual
     * @return A
     */
    #[Override]
    public function __invoke(mixed $actual): mixed
    {
        Assert::assertThat($actual, $this);
        return $actual;
    }

    ...
}

Usage:

$actual |> new IsEqual('test')

Benefits

Readability

Invokable constraints give a syntax similar to expect( $actual )->to... for free.

// (current)
Assert::assertThat(new IsEqual('test'), $actual);

// (proposed)
$actual |> new IsEqual('test');

Static type checking

Since every __invoke method has its own signature, the acceptable types of the $actual value can be statically defined and checked:

$actual = 5;

// (current) 
// The generic signature of `assertThat` does not help static type checkers
// resulting in a run-time TypeError: expected string, got int
Assert::assertThat(new RegularExpression('/test/'), $actual); 

// (proposed)
// A tailored signature for `__invoke` of the constraint `RegularExpression`
// makes it possible to detect the error statically
$actual |> new RegularExpression('/test/');

Static type narrowing

Again the signature of __invoke can be used to pass type narrowing instructions to the static type checker.

// (current) 
// impossible to statically determine what's the type of $actual
Assert::assertThat(new IsString(), $actual); 

// (proposed)
// thanks to the signature of `__invoke`, it's possible to narrow the type of $actual to string, 
$actual |> new IsString();

Simple chaining

Testing a value against multiple constraints can be easy with the proposed syntax

// (proposed)
$actual
|> new IsString()
|> new RegularExpression('/test/');

Minor expectation parameters placement

The minor parameters of an assertion, like delta in the example below, are conceptually part of the expectation. However, the order of the arguments separates them from the expected value:

// (current)
Assert::assertEqualsWithDelta(5, $actual, 0.05);

With the proposed syntax, the minor expectation parameters are nicely grouped with the expected value:

// (proposed)
$actual |> new IsEqualWithDelta(5, 0.05);

Metadata

Metadata

Assignees

No one assigned

    Labels

    type/enhancementA new idea that should be implemented

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions