Weave Code
Weaver
Helps Laravel developers discover, compare, and choose open-source packages. See popularity, security, maintainers, and scores at a glance to make better decisions.
Feedback
Share your thoughts, report bugs, or suggest improvements.
Subject
Message

Architect Laravel Package

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.

View on GitHub
Deep Wiki
Context7

Getting Started

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:

  1. composer require optimus/architect
  2. Check composer.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.
  3. Read 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.
  4. Define one architect per model you expose:
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,
        ];
    }
}
  1. Wire it into a controller, passing the requested includes:
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.

Implementation Patterns

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.

Gotchas and Tips

  • N+1 is the default failure mode. A client can request ?include=posts and the architect will happily lazy-load it. Always intersect requested includes with your preloading list.
  • Circular includes. 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.
  • Whitelist, don't sanitize. The package's model is "the architect decides what is includable." If you start letting clients name arbitrary methods, you've reintroduced an injection surface the package was designed to avoid.
  • Don't mix it with native 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.
  • Version compatibility is the biggest practical risk. 38 stars, a single maintainer, and zero dependents means you are effectively the QA team. Pin the exact version, read 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.
  • Caching gets include-sensitive. If you cache the architect's output, the cache key must include the normalized include list, otherwise ?include=posts and a bare request share a cache entry and one of them is wrong.
  • Debugging tip: dump the parsed includes array before building the tree; most "why is my relation missing" bugs are a typo in relatesTo(), a missing eager load, or a null model relation — not an architect bug.
  • Extension points are minimal by design. The base 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.
  • Honest assessment: for a new Laravel project, native API Resources with 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.
Weaver

How can I help you explore Laravel packages today?

Conversation history is not saved when not logged in.
Prompt
Add packages to context
No packages found.
htinlynn/genius-helper
maco-studios/hephaestus
elmogy/larafast
xeviant/laravel-paystack
gorilladash/laravel-website-sdk
martialbe/flysystem-oss
chondal/3312client
jeroen-g/flickr
opscale-co/nova-genbi
itsjustvita/laravel-retention
tequilarapido/trackit
gregsphotography/site-builder-filament
cabaoctavio/laravel-query-log
gregsphotography/site-builder-components
io-digital/clickatell
snowsoft/laravel-admin
oxygen/auth
relicsoftware/barakah-payment-gateway
tobya/work-with-composer
equipc/laravel-email-validator