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

Maintainers

Package info

github.com/visifo/smack-clause

pkg:composer/visifo/smack-clause

Statistics

Installs: 0

Dependents: 0

Suggesters: 0

Stars: 0

dev-main 2026-04-03 08:38 UTC

This package is not auto-updated.

Last update: 2026-05-11 11:18:33 UTC


README

Latest Version on Packagist Tests Total Downloads

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.
  • --scan=<path>
    • Default: all autoload.psr-4 directories 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-4 directories.

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.