patterns / guard
Guard pattern - validate inputs and preconditions early, returning a Result instead of throwing.
Requires
- php: >=8.1
- patterns/result: ^1.0
Requires (Dev)
- phpunit/phpunit: ^9.5 || ^10.0 || ^11.0
Suggests
None
Provides
None
Conflicts
None
Replaces
None
README
Validate inputs and preconditions early — returning a Result, never throwing.
Part of the Patterns collection: small, rock-solid, dependency-free building blocks. One pattern, one package.
- Package:
patterns/guard - Namespace:
Patterns\Guard - Depends on:
patterns/result - PHP:
>=8.1
Install
composer require patterns/guard
Why
Validation scattered through constructors is noisy, repetitive, and inconsistent:
// Before — mixed with assignment, ad-hoc messages, throws public static function create(array $data): self { if (!isset($data['name'])) { throw new \Exception("Required:name"); } if (!isset($data['price']) || $data['price'] <= 0) { throw new \Exception("Price must be positive"); } // ... }
// After — declarative, consistent messages, one Result use Patterns\Guard\Guard; use Patterns\Guard\Numbers; use Patterns\Guard\Strings; use Patterns\Result; public static function create(array $data): Result { $check = Guard::combine([ Strings::nonEmpty($data['name'] ?? null, 'name'), Numbers::positive($data['price'] ?? null, 'price'), ]); if ($check->isFailure()) { return Result::fail($check->errorMessage()); // "name cannot be empty; price must be positive" } return Result::success(new self(/* ... */)); }
Every guard returns a Result — success carries the validated value, failure
carries a consistent message. Guard::combine() merges messages with "; ".
API
All guards accept mixed and return a failed Result for the wrong type
(they never throw a TypeError), so they're safe on raw request/DB data.
Common — Patterns\Guard\Guard
| Method | Fails when |
|---|---|
Guard::defined($value, $name) |
value is null |
Guard::assert($condition, $message) |
condition is false |
Guard::combine(array $results) |
any result is a failure (messages joined with "; ") |
Guard::isTrue($value, $name) |
not the boolean true |
Strings — Patterns\Guard\Strings
nonEmpty($v, $name) · pattern($v, $regex, $name, $message?) · email($v, $name)
· minLength($v, $min, $name) · maxLength($v, $max, $name) · length($v, $min, $max, $name)
· letters($v, $name) · alphanumeric($v, $name)
Numbers — Patterns\Guard\Numbers
positive($v, $name) · nonNegative($v, $name) · range($v, $min, $max, $name)
· min($v, $min, $name) · max($v, $max, $name) · integer($v, $name)
Arrays — Patterns\Guard\Arrays
nonEmpty($v, $name) · minLength($v, $min, $name) · maxLength($v, $max, $name)
· includes($v, $item, $name) · every($v, $predicate, $name, $message)
Objects — Patterns\Guard\Objects
hasProperty($v, $property, $name) · nonEmpty($v, $name)
(accepts objects and associative arrays)
Dates — Patterns\Guard\Dates
inPast($v, $name) · inFuture($v, $name) · before($v, $threshold, $name) · after($v, $threshold, $name)
Usage
use Patterns\Guard\Guard; use Patterns\Guard\Numbers; use Patterns\Guard\Strings; $check = Guard::combine([ Strings::email($email, 'email'), Strings::minLength($password, 8, 'password'), Numbers::range($age, 18, 120, 'age'), ]); if ($check->isSuccess()) { // proceed with validated values }
PHP notes (vs the TypeScript original)
- Grouped static classes mirror
Guard.String.nonEmpty→Strings::nonEmpty,Guard.Number.positive→Numbers::positive, etc. - Guards validate the type too (
mixedin, failedResultout) instead of relying on declared types — soStrings::nonEmpty(123, 'name')fails with"name must be a string"rather than a fatalTypeError. Guard.isTrue()is static (in the TS source it was accidentally an instance method, soGuard.isTrue(...)was unreachable).Objectsaccepts arrays as well as objects.
Tests
composer install composer test # or vendor/bin/phpunit
License
MIT.