exeque/fluent-assert

Fluent interface for Webmozart Assert

0.1.1 2025-04-05 13:07 UTC

This package is auto-updated.

Last update: 2025-04-05 13:19:18 UTC


README

This library is built upon webmozart/assert and provides a fluent interface for assertions, which allows for a more compact set of assertions on the same value.

It is a useful tool for those who want to use webmozart/assert but prefer a more fluent interface.

Built with love ❤️

Installation

Use Composer to install the library.

composer require exeque/fluent-assert

Differences

There are a few differences between this library and webmozart/assert:

  • The all* methods don't use the source methods in webmozart/assert.
    • They work as you would expect, but they use each() internally to apply the assertions to each value. See Iterative Assertions.
  • The exception throw on failures are different.
    • \ExeQue\FluentAssert\Exceptions\InvalidArgumentException is thrown on failures.
      • It extends \Webmozart\Assert\InvalidArgumentException.
    • \ExeQue\FluentAssert\Exceptions\BulkInvalidArgumentException is thrown on grouped assertions
      • It extends \ExeQue\FluentAssert\Exceptions\InvalidArgumentException.
      • It provides multiple exceptions in the getExceptions() method.
      • See Grouped Assertions.
    • \ExeQue\FluentAssert\Exceptions\IndexedInvalidArgumentException is thrown on positional or iterative assertions
      • It extends \ExeQue\FluentAssert\Exceptions\InvalidArgumentException.
      • It provides the index of the failing assertion in the getIndex() method.
      • It provides the original message in the getOriginalMessage() method.
      • See Positional Assertions or Iterative Assertions.

Example

use Exeque\FluentAssert\Assert;

$assert = Assert::for('foo bar');

// All methods from webmozart/assert are available
// The arguments are the same except for the first one (the value to assert).
$assert
    ->string()          // Webmozart/Assert::string(..., $message = '')
    ->startsWith('foo') // Webmozart/Assert::startsWith(..., $prefix, $message = '')
    ->endsWith('bar');  // Webmozart/Assert::endsWith(..., $suffix, $message = '')

// You can retrieve via the `value()` method.
$value = $assert->value(); // 'foo bar'

The example from webmozart/assert can be rewritten as:

use Exeque\FluentAssert\Assert;

class Employee
{
    public function __construct($id)
    {
        Assert::for($id)
            ->integer('The employee ID must be an integer. Got: %s')
            ->greaterThan(0, 'The employee ID must be a positive integer. Got: %s');
    }
}

new Employee('foo bar');
// -> ExeQue\FluentAssert\Exceptions\InvalidArgumentException:
//    The employee ID must be an integer. Got: string

new Employee(-10);
// -> ExeQue\FluentAssert\Exceptions\InvalidArgumentException:
//    The employee ID must be a positive integer. Got: -10

Grouped Assertions

The fluent interface provides a and() and or() method to group assertions.

This is useful when you want to make multiple assertions at once and a singular combined error.

use Exeque\FluentAssert\Assert;

$assert = Assert::for('fizz buzz');

// Using `and()` will throw an exception if any of the assertions fail.
// The errors will be combined into a single exception.
$assert->and(
    fn (Assert $assert) => $assert->startsWith('foo'),
    fn (Assert $assert) => $assert->endsWith('bar'),
);
// -> ExeQue\FluentAssert\Exceptions\BulkInvalidArgumentException:
//    Expected a value to start with "foo" (Got: "fizz buzz"), or expected a value to end with "bar" (Got: "fizz buzz").

// Using `or()` will only throw an exception if all the assertions fail.
// The errors will be combined into a single exception.
$assert->or(
    fn (Assert $assert) => $assert->startsWith('foo'),
    fn (Assert $assert) => $assert->startsWith('fizz'),
):
// -> Does not fail

$assert->or(
    fn (Assert $assert) => $assert->startsWith('foo'),
    fn (Assert $assert) => $assert->startsWith('bar'),
):
// -> ExeQue\FluentAssert\Exceptions\BulkInvalidArgumentException:
//    Expected a value to start with "foo" (Got: "fizz buzz"), or expected a value to start with "bar" (Got: "fizz buzz").

Iterative Assertions

The each() method allows you to iterate over an array or an ArrayAccess object and apply assertions to each value.

The errors thrown will have a prefix of failing index applied.

use Exeque\FluentAssert\Assert;

$assert = Assert::for(['foo', 'bar', 'baz']);

$assert->each(
    fn (Assert $assert) => $assert->string()->length(3)
);

$assert->each(
    fn (Assert $assert) => $assert->inArray(['foo', 'bar'])
);
// -> ExeQue\FluentAssert\Exceptions\IndexedInvalidArgumentException:
//    [2]: Expected one of: "foo", "bar". Got: "baz"

The IndexedInvalidArgumentException can provide the index that failed use getIndex() and the original message using getOriginalMessage().

Positional Assertions

The at() method allows you to assert a value at a specific index in an array or an ArrayAccess object.

It automatically calls keyExists() on the index before applying the assertion.

The errors thrown will have a prefix of failing index applied.

use Exeque\FluentAssert\Assert;

// Works with integer indices
$assert = Assert::for(['foo', 'bar', 'baz']);
$assert->at(0, fn (Assert $assert) => $assert->eq('foo'));

// Works with string indices
$assert = Assert::for(['foo' => 'bar', 'baz' => 'qux']);
$assert->at('baz', fn (Assert $assert) => $assert->eq('qux'));

$assert = Assert::for(['foo' => 'bar']);
$assert->at('foo', fn (Assert $assert) => $assert->eq('fizz'));
// -> ExeQue\FluentAssert\Exceptions\IndexedInvalidArgumentException:
//    [foo]: Expected a value equal to "fizz". Got: "bar"

Conditional Assertions

Sometimes you only want to apply certain assertions if a condition is met or not. The when() method allows you to do this.

use Exeque\FluentAssert\Assert;
use ExeQue\FluentAssert\ConditionAssert;

$assert = Assert::for(['foo', 'bar', 'baz']);

// The `when()` method takes a callable that may return a boolean value
// or a `ConditionAssert` object.
$assert->when(
    // Any truthy value will be considered true
    condition: fn (Assert $assert) => true,
    // [Required] Assertions to apply if the condition is true 
    then: fn (Assert $assert) => null,
    // [Optional] Assertions to apply if the condition is false
    otherwise: fn (Assert $assert) => null,
);

// If the condition uses the `Assert` provided as an argument and
// returns null (or nothing) and the inner assertion did not fail,
// then that is considered true.

$assert = Assert::for('foo bar');

$assert->when(
    function (Assert $assert) {
        $assert->string(); // Returns nothing
    },
    fn(Assert $assert) => null, // Is called
);

$assert->when(
    function (Assert $assert) {
        $assert->string();
        
        return null;
    },
    fn(Assert $assert) => null, // Is called
);

$assert->when(
    function (Assert $assert) {
        $assert->isArray();
    },
    fn(Assert $assert) => null, // Is not called
    fn(Assert $assert) => null, // Is called
);

Inverted Assertions

The not() method allows you to invert the assertion. This is useful when the assertions MUST fail.

use Exeque\FluentAssert\Assert;

$assert = Assert::for([1, 2, 3, 4]);

$assert->not(
    fn (Assert $assert) => $assert->arrayContains(3),
    'Input cannot contain the number 3'
);

Assertions

All assertions from webmozart/assert are available. See here

An extended implementation of webmozart/assert is available in the ExeQue\FluentAssert\Base class.

The following additional assertions are available:

Method Description
hasIndices($value, string $message = '') Check that a value has indices (array or ArrayAccess)
arrayContains($array, mixed $value, string $message = '') Check that a value contains another value
type($value, string $type, string $message = '') Check that a value is of a certain type (using get_debug_type())

Development

The library uses code generation to create definitions for all methods present in webmozart/assert.

To trigger the generation run:

composer build

Testing

You can run the tests using the following command:

composer test

License

This library is open-sourced software licensed under the MIT license.

Appreciation

Big thanks to Bernard Schussek (and community) for the webmozart/assert.

It is one of the first dependencies I include in any project and I use it extensively.