Rivet PHP provides contract-first PHP authoring for Rivet. Define an HTTP API with PHP interfaces, native types, and attributes; Rivet PHP turns it into the canonical Rivet contract, then Rivet emits OpenAPI.
- PHP 8.2 or later; CI currently tests PHP 8.2, 8.3, and 8.4
- Composer 2
- Rivet
0.41.0, available asrivetonPATHor configured with--binaryorRIVET_BINARY
composer require rivet/php<?php
namespace App\Contract;
use Rivet\Attribute\Contract;
use Rivet\Attribute\Get;
use Rivet\Attribute\Post;
use Rivet\Attribute\Returns;
use Rivet\Parameter\Body;
use Rivet\Parameter\Path;
// The JSON request body accepted by POST /members.
final readonly class CreateMember
{
public function __construct(public string $email) {}
}
// The successful response body returned by both endpoints.
final readonly class Member
{
public function __construct(public string $id, public string $email) {}
}
// The error response body returned when a member does not exist.
final readonly class ProblemResponse
{
public function __construct(public string $error) {}
}
// Group the endpoints under /members and name generated operations Members_*.
#[Contract('/members', name: 'Members')]
interface MembersContract
{
// Name each complete endpoint path after its operation.
public const FIND = '/members/{id}';
public const CREATE = '/members';
// Define GET /members/{id}.
#[Get(self::FIND)]
// In addition to the normal 200 Member response, document a typed 404.
#[Returns(404, ProblemResponse::class)]
// Bind {id} from the path and return a Member response body.
public function get(#[Path] string $id): Member;
// Define POST /members.
#[Post(self::CREATE)]
// Bind the JSON body to CreateMember; POST success defaults to 201.
public function create(#[Body] CreateMember $request): Member;
}At a glance:
#[Contract],#[Get], and#[Post]define the routes.#[Path]and#[Body]say where endpoint arguments come from.- Native parameter and return types define the request and response schemas.
#[Returns]adds responses outside the endpoint's normal success response.
See the compatibility profile for the complete authoring rules, including collections, nullability, defaults, security, and contract-level attributes.
Rivet PHP does not register framework routes. A Laravel controller can use the contract DTOs while the application keeps ownership of its transport:
<?php
namespace App\Http\Controllers;
use App\Contract\CreateMember;
use App\Contract\ProblemResponse;
use App\Repository\MemberRepository;
use Illuminate\Http\JsonResponse;
use Illuminate\Http\Request;
use Illuminate\Routing\Controller;
final class MembersController extends Controller
{
public function __construct(private readonly MemberRepository $members) {}
public function show(string $id): JsonResponse
{
$member = $this->members->find($id);
return $member === null
? response()->json(new ProblemResponse('Member not found'), 404)
: response()->json($member);
}
public function store(Request $request): JsonResponse
{
$input = new CreateMember($request->string('email')->toString());
return response()->json($this->members->create($input), 201);
}
}The repository represents application code here; routing, request mapping, and serialization remain Laravel's responsibility. See optional runtime enforcement if the host also wants Rivet to validate endpoint-to-handler registration and responses.
Commands run from the current directory by default. --project-root changes the
base for relative paths. Without contract roots or namespace filters, Rivet PHP
discovers Composer PSR-4 and classmap sources automatically.
vendor/bin/rivet-php inspect \
--contract-root src/Contract
vendor/bin/rivet-php generate \
--contract-root src/Contract \
--output generated
vendor/bin/rivet-php verify \
--contract-root src/Contract \
--output generatedRunning those commands against the contract above produces:
POST /members Members_create
GET /members/{id} Members_get
Published Rivet artifacts.
Rivet artifacts are current.
Generation validates the PHP contract, generated OpenAPI, and their semantic
correspondence before publishing artifacts transactionally. verify runs the
same generation configuration without mutating the target and fails when the
committed artifacts are stale. See Artifact lifecycle for
the publication and recovery guarantees.
The committed layout is:
generated/
api.contract.json
openapi.json
rivet-manifest.json