lucidarch/lucid
Lucid is a Laravel package for building scalable, maintainable apps with Domain-Driven Design and Clean Architecture. It provides Artisan commands to generate domains, actions, services, repositories, and tests, keeping business logic organized.
Lucid is not a package you "add" to an existing app so much as an application shell. It scaffolds a feature-oriented src/ tree and gives you generators that place every class in the right layer of the right feature.
Install and scaffold:
composer require lucidarch/lucid
php artisan lucid:install # publishes config/lucid.php and scaffolds src/
composer dump-autoload -o # required: install adds the PSR-4 mapping for src/
lucid:install is the entry point. It creates the directory structure, publishes config/lucid.php, and (depending on your version) wires the PSR-4 autoload entry so Lucid\Components\Foo\... resolves to src/Components/Foo/.... If you are merging into a hand-built composer.json, confirm the autoload block manually — a missing entry here is the single most common "class not found" cause with this package.
Where to look first:
config/lucid.php — namespace root, test paths, and generator defaults. Read this before writing any code.src/Components/ — one folder per feature ("unit"). This is the primary navigation surface.src/Foundation/ — cross-cutting providers, exceptions, helpers, and anything that does not belong to a single feature.Console/, Database/, Domain/, Http/, Tests/) is the contract you are agreeing to; learn it once and every feature looks the same.First use case — a CRUD feature end-to-end:
php artisan make:feature Blog
php artisan make:model Blog/Post
php artisan make:service Blog/CreatePost
php artisan make:controller Blog/PostController
php artisan make:request Blog/StorePostRequest
php artisan make:resource Blog/PostResource
Then wire a route in the feature's route file (normally under src/Components/Blog/Http/routes.php or registered from the feature's service provider), inject CreatePost into the controller, and delegate. The point of the scaffolding is that "where does this class go?" is answered for you every time — fight that and you lose the benefit.
Feature as the unit of change. Group code by what the app does, not by what kind of file it is. Blog, Billing, Authentication, Notifications — each owns its models, migrations, controllers, tests, and routes. When you cannot name a feature without "and", it is probably two features. New work almost always starts with make:feature or an existing feature folder, never with a folder under app/.
Thin controller → request → service → model. Controllers validate via a Form Request, hand off to a single service, and return a Resource. Keep controllers to routing, authorization, and response shaping. All decision logic lives in the service:
final class CreatePost
{
public function __construct(private Post $post) {}
public function execute(array $attributes): Post
{
return $this->post->create($attributes);
}
}
One service per operation, not per entity. CreatePost, PublishPost, ArchivePost — not PostService with ten methods. This is the pattern that makes Lucid hold up as the app grows: a service name is a verb you can search for, and a diff on a single file tells you exactly what changed in a workflow. It also gives you a natural seam to wrap in a transaction, event, or job later.
Jobs wrap services. A queued job should do nothing but resolve its service and call execute(). This keeps retry semantics and side effects identical between sync and async paths, and it means the service can be unit-tested without touching the queue.
Domain models stay anemic. Models carry relationships, casts, accessors, and scopes — not business rules. If a model needs an if, that decision probably belongs in a service or a value object under Domain/Values. Value objects (money, slugs, statuses) are the right home for invariants.
Repositories are opt-in. Lucid supports them, but the useful rule is: add a repository when a query is complex or reused across services, not by default. An unnecessary repository layer is pure indirection and one of the fastest ways to make a clean architecture feel heavy.
Policies, requests, resources live with the feature. Authorization for Blog belongs in Blog/Policies, not in a global Policies/ folder. Anything genuinely shared — base exceptions, common traits, cross-feature contracts — moves up to src/Foundation/.
Monolith now, services later. Features are shaped like service boundaries: each one has its own models, migrations, and routes. Extraction is mechanical (move the folder, move the migrations, point the gateway at it) rather than archaeological. If you are targeting microservices, keep features talking to each other only through their services or events — never reach into another feature's models.
Incremental adoption. In a legacy Laravel app, do not rewrite. Add src/, pick one feature, build it entirely in Lucid, and route traffic to it from the existing app. Over time the app/ layer shrinks. Doing a big-bang migration to a new structure is how teams abandon architectures.
lucid:install, run composer dump-autoload. If you change the namespace in config/lucid.php, dump again and clear the config cache. Class "Lucid\Components\Foo\..." not found is almost always a missing or stale PSR-4 entry, not a missing file.php artisan config:cache can bake in stale values after a deploy that changed config. Standard fix: run config:clear then config:cache in your deploy script, in that order.Feature/Class — make:service Blog/PublishPost writes to Components/Blog/Domain/Services/PublishPost.php. A typo in the feature segment creates a new feature folder silently rather than failing. Always git status after generating a batch.src/ namespace collision. If you also have an src/ (or app/) PSR-4 root, you can end up with two mappings for the same prefix. Keep Lucid's root distinct and confirmed in composer.json.phpunit.xml must include src/Components (or wherever your root is) in its testsuite. New tests silently not running is a common half-hour debug.php artisan about confirms the app boots cleanly. composer dump-autoload -o surfaces namespace mistakes immediately (optimized autoload validates class maps). php artisan route:list confirms per-feature routes are registered. Reach for these three before reading source.config/lucid.php before you fork anything. Feature-level service providers are the clean place to bind interfaces to implementations — bind cross-feature contracts there, not in a global service provider.How can I help you explore Laravel packages today?