calcinai/strut
Strut is a Laravel package by calcinai that provides a structured, schema-oriented foundation for building data-driven applications, making it easier to define resources, relationships, and management workflows with less boilerplate.
Strut is a plain PHP object model for OpenAPI/Swagger documents — there is no service provider, no facade, and no config file. Install it with Composer and start using the classes directly:
composer require calcinai/strut
Where to look first, in order:
Calcinai\Strut\OpenApi — the OpenAPI 3.0 root document (what you'll use most).Calcinai\Strut\Swagger — the Swagger/OpenAPI 2.0 root document.Calcinai\Strut\Factory — parses an existing spec and picks the right version class for you.src/ (Info, PathItem, Operation, Response, Schema, Components, …) — they mirror the OpenAPI schema tree almost 1:1.The first useful thing to try is building a small spec by hand in php artisan tinker (or a throwaway command) to see how the object tree behaves:
use Calcinai\Strut\OpenApi;
$spec = new OpenApi();
$spec->info->title = 'My API';
$spec->info->version = '1.0.0';
$spec->paths['/users']->get->responses['200']->description = 'OK';
echo $spec->toJson(JSON_PRETTY_PRINT);
Once that round-trips into something a validator accepts, move on to loading an existing spec with Factory::getSpecFromJson($json) (or the array equivalent) and mutating it — that load → modify → dump cycle is the real day-to-day use case.
Generate specs from your Laravel routes. This is the highest-leverage pattern. Walk Route::getRoutes(), and for each route create a path/operation on a shared OpenApi instance:
$spec = new OpenApi();
$spec->info->title = config('app.name');
$spec->info->version = '1.0.0';
foreach (Route::getRoutes() as $route) {
$path = '/' . ltrim($route->uri(), '/');
$method = strtolower($route->methods()[0]);
$op = $spec->paths[$path]->{$method};
$op->operationId = $route->getName();
$op->responses['200']->description = 'OK';
}
Wrap it in an Artisan command (openapi:generate) that writes storage/app/openapi.json or public/openapi.json. Commit the output or serve it — either way, keep generation out of the request lifecycle.
Load, patch, and re-emit. For teams that hand-author a spec, Strut is best used as a post-processor: read the source file, inject environment-specific servers, stamp version/build metadata, splice in generated schemas, then write it back. This keeps the hand-authored file reviewable while allowing automation.
Use components for reuse, not duplication. Define reusable schemas once under $spec->components->schemas['User'] and reference them as strings ('$ref' => '#/components/schemas/User'). Strut does not resolve or inline $ref, so you get exactly what you wrote — which is usually what you want.
Cross-version conversion. The package models Swagger 2.0 and OpenAPI 3.0 as separate trees. Convert via the root classes (toSwagger() / toOpenApi() where available in your pinned version) or the Factory, then diff the output against the original before trusting it. Treat conversion as a one-way migration aid rather than a lossless transform.
Merge multiple specs. If you split docs per module, build each as its own OpenApi, then merge paths/components into a root document by iterating toArray() and assigning into the target. Doing it at the array level is often clearer than fighting object identity.
Snapshot tests. In Pest/PHPUnit, assert against $spec->toArray() rather than the JSON string — array comparisons give you readable diffs and avoid key-ordering noise. Pair this with an external validator (see Gotchas) in CI.
webhooks, jsonSchemaDialect, sibling $ref keywords). Do not point it at a 3.1 document and expect a faithful round-trip.$ref resolution. You can happily produce an invalid document (missing info.title, dangling $ref, malformed status codes). Validate the output with swagger-cli validate or cebe/php-openapi in CI; resolve refs with a separate tool if you need inlining.$spec->paths['/x']->get tends to instantiate intermediate objects. Always guard reads with isset() / array_key_exists before inspecting, or you'll silently mutate the spec while "reading" it.$responses['200'], not $responses[200]. Mixing int and string keys produces duplicate-looking entries in JSON.responses or components can serialize as [] where the spec requires {}. Check the raw toJson() output, not just the PHP structure, and force object encoding where needed.x-*), ordering, and some 3.0-only constructs may not survive a Swagger ↔ OpenAPI conversion. Diff before and after, and keep the original under version control.$spec->{'x-internal-id'} = '...' directly on the relevant node.toJson(JSON_PRETTY_PRINT | JSON_UNESCAPED_SLASHES) produces far more readable output for review; toArray() is the right input for diffing and merging.json_encode($spec, JSON_PRETTY_PRINT) at each mutation step. The object graph is deep and it's easy to set a property on the wrong node (e.g. paths['/x']->get->responses vs. paths['/x']->responses).How can I help you explore Laravel packages today?