rapira/contract
Shared contracts and interfaces for the Rapira Laravel ecosystem. rapira/contract provides stable abstractions that keep packages decoupled and make it easier to build extensible, maintainable applications. Depend on contracts instead of concrete implementations for cleaner, more portable code.
rapira/contract is the PHP-side type contract layer for Rapira (the rapira-rs org implies a Rust core with a PHP binding). Treat it as a shared vocabulary package: interfaces, DTOs, enums, exception types, and a set of stub functions that mirror the Rust runtime's entry points. It is not a framework and not a full SDK.
Note the actual version line: the latest release is 0.9.1, not a 1.x. The package is pre-1.0, so minor bumps are permitted to break you.
Minimal steps:
composer require rapira/contract — pinned to an exact version ("0.9.1"), not ^0.9. Then immediately open vendor/rapira/contract/composer.json. With 1 star and 0 dependents there is effectively no external documentation, so the manifest is your spec: check require for ext-rapira/ext-* entries, the PHP version floor, the PSR-4 namespace root, and the autoload.files entry — the stub functions likely live in a separately included functions.php, which matters for collisions and for whether they can be swapped.Rapira\Contract\Testing\*), a Runtime/Double/Fake class with a setter or static swap(), or an env-var switch read at function-call time. Confirm whether it ships in require or require-dev, and whether it is autoloaded in production.ext-rapira installed, you have a validated testing seam on day one — this is the single most valuable thing in the package right now.If step 5 works with zero glue code, the contracts are enforced by the Rapira runtime. If it doesn't, you have a plain PHP library plus testable stubs — still useful, but plan accordingly.
Stub functions are your seam — use them only in tests. The 0.9.1 double is a testing primitive, not a production runtime. Wire it in a base TestCase:
protected function setUp(): void
{
parent::setUp();
// exact API depends on the package — inspect the double class first
ContractDouble::register(); // e.g. a global/static swap consumed by the stubs
}
If the double exposes call recording (most do), assert on it rather than on fabricated return values: assertCalled('rapira_invoke', $dto) catches argument-shape regressions, whereas a canned return value does not.
Interface-first handlers. Implement the contract interfaces on your own application classes rather than importing concrete Rapira types. The runtime binds by interface, so handlers stay free of vendor concrete classes and remain unit-testable even without the double.
Boundary adapter layer. Do not let contract types leak into your domain:
HTTP Request → FormRequest (Laravel validation)
→ Contract DTO (rapira/contract)
→ Domain object (yours)
→ Contract Response DTO
→ JSON
The mapping layer is ~20 lines but means an abandoned package costs you a day, not a rewrite.
Enum-driven error handling. Map contract error/status enums once in a single App\Exceptions\Handler::render rather than switching at every call site. Use a match with no default arm so a new enum case from a 0.x upgrade becomes a loud failure instead of a silent 500.
Laravel service provider binding. Register contract→implementation pairs in a dedicated provider:
$this->app->bind(SomeContract::class, AppSomeAdapter::class);
Keep this small and explicit. Do not auto-scan vendor/rapira/contract for implementations — it defines shapes and stubs, you supply behaviour. Register the runtime double in a test-only provider or in TestCase::setUp, never in AppServiceProvider.
Fakes over mocks, still. Where the package's own double doesn't cover something (e.g. an interface you implement that the double can't satisfy), hand-write fakes in tests/Fakes/. They double as documentation of the expected shape. Prefer the shipped double where it exists — it will track signature changes for you on upgrade.
Pin hard in CI. Commit composer.lock, use composer install --no-dev --prefer-dist in CI, and add a job running the suite against the minimum and maximum allowed PHP versions. For a package this thin the risk is not runtime bugs — it is silent signature changes. At 0.9.x that risk is structural: 0.9 → 0.10 may legally break function signatures, return types, and the double's API.
Composer provide/virtual package trick. If you need a fallback, declare "provide": {"rapira/contract": "self.version"} in an internal package so downstream code can type-hint the contract namespace regardless of which implementation is installed. Note the namespace now covers functions as well as types — a fallback implementation must ship the same function names or the stubs will fatal on include.
The double can silently mask a broken integration. Once ContractDouble::register() is in your base TestCase, every test passes whether or not Rapira is installed, whether or not ext-rapira loads, and whether or not the real runtime agrees with your DTOs. Keep at least one non-doubled integration test (or a CI job without the double registered) that exercises the real extension — otherwise 0.9.1 makes your suite greener and your confidence lower.
Guard against production registration. If the swap is a global/static setter rather than a container binding, it is trivially reachable from production code. Confirm the double class is excluded from the production autoloader (or add a class_exists() guard + an app()->environment('testing') assertion in the registration path). A stub answering from a fake in production returns fabricated data with no exception.
Stub function behaviour without the double. Without ext-rapira and without the double registered, the stubs likely throw, return null, or return an empty DTO. That is a silent-wrong-answer class of bug, not a loud one. Verify explicitly:
php -m | grep -i rapira
composer show rapira/contract
composer check-platform-reqs
Then call one stub in php artisan tinker and inspect the actual outcome before building on it.
Verify the native requirement. composer install succeeding does not mean Rapira is installed. If composer.json lists an ext-* requirement and your platform lacks it, Composer either hard-fails or — worse — your config.platform overrides mask it.
Pre-1.0 versioning is the real hazard. 0.9.1 is a minor bump containing a feature that changes how the package is consumed; expect the same again. Read the git tags and CHANGELOG.md directly rather than trusting Packagist metadata (the release metadata has looked anomalous before, so automated currency checks are unreliable). Prefer an exact constraint or a commit hash over ^0.9.
Check how the double is toggled. Three plausible designs, each with a different failure mode: a static setter (leaks across tests — reset in tearDown), a container binding (safe, but only if the stubs resolve through the container, which functions generally cannot), or a global flag read per call (cheap, but invisible in stack traces). Read the implementation before writing 200 tests against the wrong assumption.
Function stubs are not mockable by interface. You cannot bind() a namespaced function. If your handlers call stub functions directly, those call sites are hard to isolate. Wrap each stub in a thin injectable class (App\Rapira\Runtime) that implements your own interface, and call the stub from exactly one place. This also gives you a production/testing swap that doesn't depend on the package's double at all.
Autoload files collisions. autoload.files entries run on every request. Run composer dump-autoload -o and check for duplicate function names if the contract namespace is broad (e.g. Rapira\) or if you maintain a local fork. Fix with PSR-4/file exclusions in the root composer.json, not by editing vendor files. If composer.json declares "files", remember those functions cannot be conditionally autoloaded per environment without Composer tricks.
Type mismatches surface as bare TypeError. There is no runtime validation layer here — a contract violation is a PHP fatal, not a friendly 422. Mitigate with:
declare(strict_types=1); in every file touching contracts (without it, scalar coercion hides bugs until production).print_r(class_implements($obj)); and (new ReflectionClass($dto::class))->getConstructor()->getParameters().TypeErrors into a 500 with the class path logged — otherwise you debug via blank responses.Check for final, readonly, and private constructors. Many DTO packages mark classes final with private constructors plus static named constructors (::fromArray(), ::fromRequest()). You cannot extend them — decorate instead, and always go through the named constructor rather than new. The same likely applies to the double class; don't subclass it to add assertions, wrap it.
Do not serialize contract objects directly to your public API. They are internal transport shapes; a 0.x patch could rename a property and break your JSON contract with clients. Always map to your own response classes or resources.
Extension points are thin by design — with one new exception. The intended extension mechanism is implementing the interfaces plus, as of 0.9.1, swapping the runtime double in tests. Everything else belongs in the adapter layer. That is correct, not a workaround.
Escape hatch. The types are interface declarations and the behaviour is a handful of functions. If the package stalls, copy the interfaces and the function signatures into your codebase, keep the same FQCNs and function names via a classmap / provide declaration, and remove the dependency. 0.9.1 makes this slightly more attractive: you now have a documented seam where your own fake can stand in for the stubs, so the fork is testable immediately. Budget half a day.
How can I help you explore Laravel packages today?