adriengras / guard-php
A tiny guard() helper for PHP (Symfony/Laravel/plain).
Requires
- php: ^8.2
Requires (Dev)
- pestphp/pest: ^2
- phpunit/phpunit: ^10
README
A tiny PHP library bringing the guard
concept (inspired by Kotlin, Swift, Dart…) to PHP.
Available globally via Composer — works in plain PHP, Symfony, and Laravel.
✨ Features
- Global
guard()
function available everywhere - Throws an exception if a condition is not met
- Supports:
- Simple error message
- Existing exception instance
- Lazy exception factory (
callable
)
- Caller blame mode: the error is shown as if it occurred where
guard()
was called, not inside the package - PHP 8.1+ compatible
- Zero runtime dependencies
❓ Why this package?
Defensive programming is a key practice to make your code more reliable and easier to debug.
Languages like Kotlin and Swift provide a native guard
statement to quickly validate assumptions and fail fast when something is wrong.
In PHP, similar checks often look like this:
if ($price < 0) { throw new InvalidArgumentException('Price must be non-negative'); }
This package brings a concise, expressive, and consistent way to write those checks:
guard($price >= 0, 'Price must be non-negative');
Benefits:
- Readability – One-line, expressive intent
- Consistency – Same syntax across all projects and frameworks
- Less boilerplate – No repetitive if + throw blocks
- Framework-agnostic – Works in plain PHP, Symfony, Laravel…
⚙️ How it works — Caller blame mode
By default, guard() is in caller blame mode ($blameCaller = true
):
- It scans the debug backtrace to find the first frame outside
vendor/
and outside theguard.php
file itself. - It then tries to rewrite the
$file
and$line
properties of the exception viaReflectionProperty
so the error appears to originate from your code. - If PHP forbids modifying these properties (some runtimes mark them internally as readonly),
guard()
falls back to wrapping the original exception in anErrorException
:- The new exception has
file
andline
set to the caller’s location - The original exception is preserved as
$previous
- The new exception has
Example — without blame:
In vendor/your-vendor/guard-php/src/functions.php line 47: Price must be non-negative
Example — with blame (default):
In src/Service/CheckoutService.php line 123: Price must be non-negative
You can disable caller blame explicitly:
guard($condition, 'message', null, null, false);
📦 Installation
composer require adriengras/guard-php
The global function is automatically loaded via autoload.files — no extra configuration needed.
🚀 Usage
Basic example
guard($price >= 0, 'Price must be non-negative');
With a custom exception
guard($user !== null, new DomainException('User not found'));
Lazy exception (callable)
guard($stock >= $qty, fn() => new OutOfRangeException("Not enough stock for SKU {$sku}"));
🧩 Framework compatibility
- Symfony – works out-of-the-box in controllers, services, commands…
- Laravel – works directly in controllers, jobs, events…
- Plain PHP – just require 'vendor/autoload.php';
🧪 Tests
Tests are written with Pest.
Run tests:
composer test
Run tests with coverage:
XDEBUG_MODE=coverage composer test:cov
🤝 Contributing
Contributions are welcome!
If you’d like to contribute:
- Fork the repository
- Create a new branch (
git checkout -b feature/my-feature
) - Make your changes
- Run the tests (
composer test
) - Submit a Pull Request
📄 License
MIT - You can find the licence in the LICENSE file.