visifo / smack-clause
SmackClause is a punchy, no-nonsense guard layer: it hits bad input fast, exits early, and leaves a clean trace of what failed, where, and why. The vibe is direct and a little aggressive, but the behavior is precise: stack small, composable rules, get strong default messages, and keep the happy path
Requires
- php: ^8.5
Requires (Dev)
- carthage-software/mago: ^1.16.0
- pestphp/pest: ^4.4.3
- phpstan/phpstan: ^2.1.44
- phpstan/phpstan-strict-rules: ^2.0.10
- rector/rector: ^2.3.9
- spatie/ray: ^1.47
This package is not auto-updated.
Last update: 2026-05-11 11:18:33 UTC
README
Fail fast. Speak clear. Stop bad input early.
SmackClause is a no-nonsense guard layer for PHP 8.5+. It hits bad input before it reaches your business logic, leaving a clean, structured trace of exactly what went wrong.
Why Smack?
- Direct Assertions: No "should be" or "must be" fluff. Just
isInt(),isEmail(),min(). - Auto-Discovery: It knows your variable names. No more passing string labels manually.
- PHP 8.5 Native: Built for the future with property hooks, asymmetric visibility, and high-performance tokenization.
- Smart Nullability: Choose between a hard hit (
that()) or a quiet pass (maybe()).
Installation
composer require visifo/smack-clause
Usage
The Hard Hit (Mandatory)
Smack::that() expects a value. If it's null or fails a rule, it smacks back.
Smack::that($email)->isString()->isEmail(); Smack::that($age)->isInt()->min(18);
The Quiet Pass (Optional)
Smack::maybe() allows nulls. If the value is there, it better be right.
Smack::maybe($bio)->isString()->max(200);
Hitting Collections
Smack::that($userIDs)->each()->isInt()->isPositive();
Allow Zero for Sign Checks
By default, isPositive() and isNegative() are strict (> 0 and < 0).
Use allowZero() when you want zero to pass sign checks.
Smack::that($quantity)->isInt()->allowZero()->isPositive(); // allows 0 and positive ints Smack::that($balance)->isInt()->allowZero()->isNegative(); // allows 0 and negative ints Smack::that($price)->isFloat()->allowZero()->isPositive(); // allows 0.0 and positive floats Smack::that($delta)->isFloat()->allowZero()->isNegative(); // allows 0.0 and negative floats
Custom Smacks
Extend the library with your own logic while keeping Smack::that(...)->... syntax.
use App\Smack\PlayerSmack; use Visifo\SmackClause\Smack; Smack::register(PlayerSmack::class); Smack::that($player) ->isPlayer() ->isNotUn() ->isInPlayState();
You can register as many project-specific root methods as you need, one class at a time:
Smack::register(PlayerSmack::class); Smack::register(VatSmack::class);
Use #[SmackMethod('...')] on the class and implement screenInto(...).
Dynamic smack root methods do not accept arguments.
Option 1: Implement Smackable directly
use Visifo\SmackClause\Exceptions\SmackException; use Visifo\SmackClause\Exceptions\Trace; use Visifo\SmackClause\Extensions\SmackMethod; use Visifo\SmackClause\Smackable; #[SmackMethod('isVat')] final readonly class VatSmack implements Smackable { private function __construct( private VatId $vat, private Trace $trace, ) {} public static function screenInto( mixed $value, Trace $trace, ): self { if ($value instanceof VatId) { return new self($value, $trace); } throw SmackException::forExpectedType(VatId::class, $value, $trace); } public function isEu(): self { if ($this->vat->isEu()) { return $this; } throw SmackException::forConstraint('EU VAT ID', $this->vat, $this->trace); } }
Option 2: Extend a typed smack (for method reuse)
If you extend ObjectSmack, always override screenInto(...) and call parent::__construct(...) to keep inherited object methods safe.
use Visifo\SmackClause\Exceptions\SmackException; use Visifo\SmackClause\Exceptions\Trace; use Visifo\SmackClause\Extensions\SmackMethod; use Visifo\SmackClause\Types\ObjectSmack; #[SmackMethod('isPlayer')] final readonly class PlayerSmack extends ObjectSmack { public function __construct( private GamePlayer $player, private Trace $trace, ) { parent::__construct($player, $trace); } public static function screenInto(mixed $value, Trace $trace): self { if ($value instanceof GamePlayer) { return new self($value, $trace); } throw SmackException::forExpectedType(GamePlayer::class, $value, $trace); } public function isNotUn(): self { if (! $this->player->isUn()) { return $this; } throw SmackException::forConstraint('not UN', $this->player, $this->trace); } }
See tests/Fixtures/Smacks/PlayerSmack.php for a complete example.
IDE Helper
For dynamic custom methods (isPlayer(), isVat(), ...), generate a typed helper class for IDE/static tooling:
vendor/bin/smack-ide-helper
Available parameters:
--root=<path>- Default: current working directory (
getcwd()). - Purpose: project root used for
composer.json, autoload, and output file location.
- Default: current working directory (
--scan=<path>- Default: all
autoload.psr-4directories from<root>/composer.json. - Purpose: limit scanning to specific directory.
- Notes: can be passed only once; when provided, it must point to a directory inside one of the root package's
autoload.psr-4directories.
- Default: all
This command generates _smack_ide_helper.php in your project root with a Visifo\\SmackClause\\IdeHelperSmack class that contains @method annotations for dynamic smack methods. Smack is linked to it via @mixin.
After generating, static tooling can understand calls like:
use Visifo\SmackClause\Smack; Smack::that($player) ->isPlayer() ->isNotUn();
The generator is strict and fails if any registered smack class is invalid (missing #[SmackMethod('...')], duplicate method name, missing screenInto(...), inherited screenInto(...) without override, invalid class, etc.).
The Violation Report
When a check fails, SmackViolation (extends InvalidArgumentException) gives you the forensics:
- Path: The variable or nested key name.
- Value: What was actually received.
- Rule: Which assertion failed.
Changelog
Please see CHANGELOG for more information on what has changed recently.
Contributing
Please see CONTRIBUTING for details.
Branding & Tone Guide
TONE.md file ensures that anyone contributing to the project maintains the "Smack" vibe.
Security Vulnerabilities
Please review our security policy on how to report security vulnerabilities.
Credits
License
The MIT License (MIT). Please see License File for more information.