PHP code example of splitstack / invariants

1. Go to this page and download the library: Download splitstack/invariants library. Choose the download type require.

2. Extract the ZIP file and open the index.php.

3. Add this code to the index.php.
    
        
<?php
require_once('vendor/autoload.php');

/* Start to develop here. Best regards https://php-download.com/ */

    

splitstack / invariants example snippets


use Splitstack\Invariants\Invariant;

// Touchless rule: the closure captures the local value, no subject needed.
$email = User::find($id)->email;
Invariant::make(
    rule: fn () => $email !== '' && str_contains($email, '@'),
    message: 'Email must be valid',
)->assert(); // throws StrictViolationException on failure (policy: HydrationPolicy::Strict by default)

// Touched rule: reads properties off a subject.
Invariant::make(
    rule: fn ($v) => $v >= 0,
    message: 'Balance cannot be negative',
    touches: ['balance'],
)->assert($account); // reads $account->balance

use Splitstack\Invariants\Concerns\AssertsInvariants;
use Splitstack\Invariants\Contracts\EnforcesInvariants;
use Splitstack\Invariants\Invariant;

class Account implements EnforcesInvariants
{
    use AssertsInvariants; // adds assertInvariants(), quarantine(), isQuarantined()

    public function __construct(public int $balance = 0) {}

    protected function balanceNonNegative(): Invariant
    {
        return Invariant::make(
            rule: fn ($v) => $v >= 0, // $v will resolve to $this->balance
            message: 'Balance cannot be negative',
            touches: ['balance'],
        );
    }
}

(new Account(balance: -5))->assertInvariants();
// throws InvariantViolationException: "Invariant [balanceNonNegative] violated: ..."
// $rule closure's `$v` resolves to `$this->balance` which is -5, so the rule fails.

use Splitstack\Invariants\HydrationPolicy;

Invariant::make(
    rule: fn ($v) => ctype_upper($v[0] ?? ''),
    message: 'Name must be capitalized',
    default: ucfirst($model->name),
    touches: ['name'],
    policy: HydrationPolicy::AutoCorrect,
)->assert($model);

use Splitstack\Invariants\Concerns\AssertsInvariants;
use Splitstack\Invariants\Contracts\EnforcesInvariants;
use Splitstack\Invariants\HydrationPolicy;
use Splitstack\Invariants\Invariant;

class Account implements EnforcesInvariants
{
    use AssertsInvariants;

    public function __construct(public int $balance = 0) {}

    protected function balanceNonNegative(): Invariant
    {
        return Invariant::make(
            rule: fn ($v) => $v >= 0,
            message: 'Balance cannot be negative',
            touches: ['balance'],
            policy: HydrationPolicy::Quarantine,
        );
    }
}

$account = new Account(balance: -5);
$account->assertInvariants(); // no throw
$account->isQuarantined();    // true

// You decide what the flag means, e.g. drop tainted rows from a batch:
$safe = array_filter($accounts, fn ($a) => ! $a->isQuarantined());

protected function balanceNonNegative(): Invariant
{
    return Invariant::make(
        rule: fn ($v) => $v >= 0,
        message: 'Balance cannot be negative',
        default: 0,
        touches: ['balance'],
        policy: HydrationPolicy::AutoCorrect,
    );
}

$account = new Account(balance: -5);
$account->assertInvariants(); // balance is now 0, no exception thrown

use Splitstack\Invariants\StateGuard;

class Account
{
    protected function balanceNonNegative(): StateGuard
    {
        return StateGuard::make(
            rule: fn () => $this->balance >= 0,
            message: 'Balance cannot be negative',
        );
    }

    public function substractFunds(int $amount): void
    {
        $this->balance -= $amount;

        // assert-throw ...
        $this->balanceNonNegative()->assert();

        //... or check without throwing
        if ($this->balanceNonNegative()->checks()) {
            // continue with the operation
        }
    }
}

// Can use $touches
$stateGuard = StateGuard::make(
    rule: fn ($v) => $v >= 0,
    message: 'Balance cannot be negative',
    touches: ['balance'],
);

// Can use hydration policies like AutoCorrect or Quarantine
function nonNegative(): StateGuard
{
    return StateGuard::make(
        rule: fn ($v) => $v >= 0,
        message: 'Balance cannot be negative',
        touches: ['balance'],
        policy: HydrationPolicy::AutoCorrect,
        default: 0,
    );
}
$this->assert($entity); // will correct instead of throwing

// Can use quarantine to isolate invalid state without throwing
$stateGuard = StateGuard::make(
    rule: fn ($v) => $v >= 0,
    message: 'Balance cannot be negative',
    touches: ['balance'],
    policy: HydrationPolicy::Quarantine,
);
$stateGuard->assert($subject); // will quarantine instead of throwing

use Splitstack\Invariants\Attributes\DispatchesEvents;
use Splitstack\Invariants\Attributes\InvariantEvent;

#[DispatchesEvents(App\EventBus::class, via: 'dispatch')] // via defaults to 'dispatch'
class ApprovalRequestEntity implements EnforcesInvariants
{
    use AssertsInvariants;

    #[InvariantEvent(['id', 'status', 'applicantUserId'])]
    protected function statusRequiresInProgress(): Invariant
    {
        return Invariant::make(
            rule: fn ($v) => $this->status === ApprovalRequestStatus::IN_PROGRESS,
            message: 'Not in progress',
            touches: ['status'],
            policy: HydrationPolicy::Quarantine,
        );
    }
}

new InvariantViolated(
    subject: ApprovalRequestEntity::class,
    invariant: 'statusRequiresInProgress',
    message: 'Not in progress',
    data: ['id' => 1, 'status' => 'draft', 'applicantUserId' => 7],
);

#[InvariantEvent(with: ['id', 'status'], event: App\Events\ApprovalStalled::class)]
// dispatches: new ApprovalStalled(id: ..., status: ...)

protected function statusRequiresInProgress(): Invariant
{
    return Invariant::make(
        rule: fn ($v) => $v === ApprovalRequestStatus::IN_PROGRESS,
        message: 'Not in progress',
        touches: ['status'],
        policy: HydrationPolicy::Quarantine,
        event: new App\Events\ApprovalStalled(id: $this->id, status: $this->status),
    );
}

use Splitstack\Invariants\Support\EventDispatcher;

// e.g. in a Laravel service provider
EventDispatcher::resolveUsing(fn (string $class) => app($class));