hoa/praspel
Hoa\Praspel brings Design by Contract to PHP: write behavioral specifications with preconditions, postconditions and invariants, then automatically generate test data to validate your code. A concise specification and testing toolkit for robust PHP libraries.
Praspel is the PHP Realistic Annotation and Specification Language: a docblock dialect (@requires, @ensures, @invariant, @description, @throwable) plus a parser that turns those clauses into a traversable model. It is a design-by-contract language and model library, not a runtime enforcer — the enforcement/test-generation halves live in companion packages (Hoa\Test, Hoa\Realdom, the atoum extensions) which are equally archived.
Minimal install (dev-only — never ship this in require):
composer require --dev hoa/praspel
# on PHP 8.x you will almost certainly need:
composer require --dev hoa/praspel --ignore-platform-reqs
First steps, in order:
Test/Unit/ directory before the README — the README is a stub, the tests are the actual API documentation.Hoa/Praspel/Praspel.php (the entry point) and Hoa/Praspel/Model/Specification.php, Model/Clause.php, Model/Property.php (the object graph you'll actually navigate).Grammar.pp / the Hoa\Compiler grammar) to see exactly what syntax is legal — it is not PHP.First use case — parse a contract and inspect it:
use Hoa\Praspel\Praspel;
$praspel = new Praspel();
$praspel->interpret(
'@requires ( x >= 0 and y >= 0 );' . "\n" .
'@ensures \result >= x;'
);
$spec = $praspel->getRoot(); // Hoa\Praspel\Model\Specification
$requires = $spec->getClause('requires'); // Hoa\Praspel\Model\Clause
$property = $requires->getProperty('x'); // Hoa\Praspel\Model\Property
$body = $property->getBody(); // Hoa\Ruler\Model\Model
The realistic Laravel use case is specification linting in CI: walk app/, pull ReflectionMethod::getDocComment(), feed the @requires/@ensures blocks to Praspel::interpret(), and fail the build on a clause that doesn't parse. That gets you a documented, machine-checked contract layer without betting production traffic on a 2017 library.
1. A thin, swappable parsing service. Wrap Praspel behind your own interface so the archived dependency stays a single replaceable class:
namespace App\Support\Contracts;
interface SpecParser
{
/** @return \Hoa\Praspel\Model\Specification */
public function parse(string $declaration): object;
}
final class PraspelSpecParser implements SpecParser
{
private array $cache = [];
public function parse(string $declaration): object
{
$key = md5($declaration);
if (! isset($this->cache[$key])) {
$praspel = new \Hoa\Praspel\Praspel();
$praspel->interpret($declaration);
$this->cache[$key] = $praspel->getRoot();
}
return $this->cache[$key];
}
}
Bind it in a service provider, and when the package finally breaks you swap the binding for a nikic/php-parser + webmozart/assert implementation without touching call sites.
2. Docblock extraction via reflection. Contracts live on methods, so pull them with ReflectionMethod:
$doc = (new \ReflectionMethod(TransferMoney::class, 'handle'))->getDocComment();
preg_match_all('/@(requires|ensures|invariant)\s+(.+?);/s', $doc, $m, PREG_SET_ORDER);
$declaration = implode("\n", array_map(fn ($c) => "@{$c[1]} {$c[2]};", $m));
Cache the resulting Specification per Class::method in the container or a static map — parsing goes through Hoa\Compiler, which is not something you want running on every request.
3. Spec-driven data generation for property tests. The original workflow is Praspel → Hoa\Realdom → sample values satisfying @requires, then assert @ensures holds. In Laravel terms: generate realistic inputs for actions/jobs (including boundary values implied by the predicates) and run them through an in-memory database. You'll be wiring Realdom by hand; there is no Laravel glue to inherit.
4. Contract decorator for critical domain services. If you do want runtime checking, Praspel only gives you the parsed model — you evaluate it. Build a decorator around a service that maps arguments into a Hoa\Ruler\Context, evaluates @requires, runs the method, then evaluates @ensures with \result and \old(...) bound:
$context = new \Hoa\Ruler\Context([
'x' => $amount,
'result' => $returnValue, // \result
'old' => $before, // \old(...)
]);
$ruler = new \Hoa\Ruler\Ruler();
$ruler->assert($property->getBody(), $context);
Keep this to a handful of money/state-transition services — it's expensive and throws on violations, which you do not want in a hot HTTP path.
5. Round-trip for documentation. Hoa\Praspel\Visitor\* compiles a model back into Praspel source. Use it to normalize specs (consistent formatting) or emit contract tables into generated docs — handy for proving the docblocks and the code haven't drifted.
6. CI gate, not runtime gate. The pragmatic integration: php artisan contracts:lint (a simple Artisan command) run in the pipeline over app/Domain only. Zero production risk, real value in catching stale/garbled contracts.
require-dev at most, and pin it exactly. Also: Dependents: 0 on Packagist is a strong signal there's no community to lean on.hoa/* packages declare legacy PHP constraints and pull in hoa/consistency (autoloader + the hoa:// stream wrapper), hoa/ruler, hoa/compiler, hoa/ustring, hoa/protocol, hoa/zformat. On modern PHP you'll need --ignore-platform-reqs, and if hoa:// isn't initialized you'll get confusing "stream not found" errors deep inside the compiler. Verify with a one-line file_get_contents('hoa://Library/Praspel/.') smoke test.and / or / xor / not — not &&, ||, !. No strlen(), no in_array(), no null-coalescing. Anything exotic must be registered as a custom operator/function on Hoa\Ruler\Ruler. This is the single biggest surprise for PHP devs.\result, \old(...), \this, \not_this, \trigger_error are Praspel-level, not PHP-level. You must bind them yourself when evaluating — and in a double-quoted PHP string you need "\\result", while inside a docblock it's a literal \result.;. A missing semicolon silently swallows the next annotation or yields an empty clause. Multi-line clauses are legal but easy to break with auto-formatters (Pint/php-cs-fixer can reflow docblocks). Exclude spec-bearing files from docblock reflow, or lint after formatting in CI.Hoa\Compiler builds/uses a memoized LL(k) parser; do not call interpret() inside a request loop. Cache by Class::method plus a hash of the docblock.Hoa\Compiler\Exception — catch it in your linter and report the offending Class::method with the clause text, otherwise your CI step just dies with a stack trace.@requires to be honored automatically anywhere.Model objects are mutable. You can add clauses programmatically before compiling — useful for building specs from config rather than docblocks, but also a footgun if you cache and then mutate a shared Specification.Ruler::registerFunction() / registerOperator() for domain predicates; the Hoa\Praspel\Visitor classes if you want custom output; the Hoa\Praspel\Model\* tree if you need structured queries over clauses.webmozart/assert + phpstan/phpstan (invariants), Pest/PHPUnit datasets + fakerphp/faker (property-style input generation), and nikic/php-parser if you need real annotation parsing. Keep Praspel isolated behind an interface so replacing it is a one-file change.How can I help you explore Laravel packages today?