optimus/architect
Optimus Architect is a Laravel package for structuring clean, modular applications. It offers generators and conventions for organizing domain logic, actions, and services, helping teams keep codebases consistent, testable, and maintainable as projects grow.
optimus/architect is a small, single-purpose package from the optimus/api-response ecosystem for building JSON API payloads where the set of related resources included in the response is decided at request time (?include=posts,profile). It predates Laravel's native API Resources and is essentially an "include-tree" layer on top of plain arrays.
Minimum steps:
composer require optimus/architectcomposer.json in the package for the Laravel/PHP versions it actually supports before upgrading — this package has a thin maintenance surface and its declared constraints don't always match the latest Laravel.src/Architect.php first. It is the abstract base class and the entire public contract; the README is short and the source is the real documentation.use Optimus\Architect\Architect;
class UserArchitect extends Architect
{
public function resource($user)
{
return [
'id' => $user->id,
'name' => $user->name,
'email' => $user->email,
];
}
public function relatesTo()
{
return [
'posts' => PostArchitect::class,
'profile' => ProfileArchitect::class,
];
}
}
public function index(Request $request)
{
$users = User::with(['posts', 'profile'])->paginate();
return Architect::make(UserArchitect::class, $users)
->parseIncludes($request->query('include'))
->toArray();
}
First real use case: a single GET /users?include=posts endpoint where you want the response shape to be stable for the base resource and additive for relationships, without writing a whenLoaded branch for every relation.
One architect per model, mirroring the model's public shape. Keep resource() to primitive attributes only; anything relational belongs in relatesTo(). This keeps the "always present" and "optional" parts of the payload clearly separated and makes the architect reusable across show, index, and nested contexts.
Treat relatesTo() as your whitelist. The strongest property of this package is that includes are declared in code rather than inferred from the relationship graph. A client can only ask for what you listed, so ?include=anything cannot leak or crash — it just resolves to nothing. Never bypass this by reading $request->include and calling $model->load() directly.
Parse includes once, in a base controller or trait. Centralize the parseIncludes(...) call (and any .-notation splitting for nested includes) so every endpoint has the same behavior:
trait IncludesArchitect
{
protected function respondWithArchitect(string $class, $data, Request $request)
{
return Architect::make($class, $data)
->parseIncludes($request->query('include', ''))
->toArray();
}
}
Nest via child architects, not via a single mega-architect. Each architect should only know its own relatesTo(). Nested includes (?include=posts.comments) then fall out naturally: the parent delegates to PostArchitect, which delegates to CommentArchitect. Keep the recursion depth bounded if you support dotted includes.
Pair with eager loading deliberately. The architect will resolve relations from the model, so load them up front based on the parsed includes:
$includes = array_filter(explode(',', $request->query('include', '')));
$with = array_intersect($includes, array_keys((new UserArchitect(User::make()))->relatesTo()));
$users = User::with($with)->paginate();
Alternatively load with with() unconditionally and let the architect decide what to emit — simpler, slightly more queries.
Use it with optimus/api-response if you're already there. Architect produces the data tree; api-response handles the envelope, status codes, and error shaping. Together they replace a lot of hand-rolled JsonResponse code in older Laravel apps.
Collections and single resources use the same architect. Pass an Eloquent collection or a single model; pagination metadata belongs in the response envelope, not in resource().
Test the contract, not the class. Feature tests asserting assertJsonStructure for each supported include combination are far more valuable than unit-testing the architect, because the failure mode is a missing or mis-nested key.
?include=posts and the architect will happily lazy-load it. Always intersect requested includes with your preloading list.UserArchitect -> posts -> PostArchitect -> user -> UserArchitect will recurse forever on ?include=posts.user. Either omit the back-reference from relatesTo() or enforce a max depth on the parsed include string.JsonResource in the same endpoint. Architect returns plain arrays; wrapping it in a ResourceCollection and then returning it, or vice versa, produces confusing double-serialization. Pick one per endpoint.composer.json before every Laravel upgrade, and be prepared to fork — the surface area is small enough that a fork is a one-file change if needed.?include=posts and a bare request share a cache entry and one of them is wrong.relatesTo(), a missing eager load, or a null model relation — not an architect bug.Architect class is the extension point: subclass it for shared behavior (timestamps, id casting, a common links block) and have every concrete architect extend that, rather than trying to configure the package.whenLoaded/whenIncluded cover 90% of this and are maintained by the framework. Architect is worth using when you already have an Optimus-based API, or when the include-tree with per-resource delegation is genuinely simpler than nested whenLoaded blocks — but budget for the maintenance risk.How can I help you explore Laravel packages today?