Generators
Every make:* generator of Laravel has an osdd:* twin that takes the same arguments and options. The difference: the file lands inside a layer, under the layer's namespace, and the layer provider is updated when the file needs it.
php artisan osdd:model Order --layer=functional/orders
# functional/orders/src/Models/Order.php
The --layer option
--layer takes the layer's name, as written in its composer.json, or its package name alone when no other layer has it:
php artisan osdd:controller OrderController --layer=functional/orders
php artisan osdd:controller OrderController --layer=orders
| Case | Result |
|---|---|
| Full name | --layer=functional/orders |
| Package name, unique | --layer=orders finds functional/orders |
| Package name shared by two layers | Exit 1: Layer [orders] is ambiguous: it matches functional/orders, technical/orders. Pass the full layer name. |
| Unknown name | Exit 1: Layer [functional/oders] not found. Available layers: functional/orders, functional/users. Did you mean [functional/orders]? |
| Omitted, in a terminal | The command asks you to search and pick a layer |
Omitted, without a terminal or with -n | Exit 1: The --layer option is required when the command is not interactive. Available layers: functional/orders, functional/users. |
The error is printed once and nothing is written. Always pass --layer in scripts, CI and when a coding agent runs the command.
Where files go
Classes go under the layer's source directory: the directory of its shortest psr-4 prefix, src/ for a layer created by osdd:layer. For the layer functional/orders, namespace Functional\Orders:
| Command | Folder | Namespace |
|---|---|---|
osdd:cast | src/Casts | Functional\Orders\Casts |
osdd:channel | src/Broadcasting | Functional\Orders\Broadcasting |
osdd:class | src | Functional\Orders |
osdd:command | src/Console/Commands | Functional\Orders\Console\Commands |
osdd:component | src/View/Components, view in resources/views/components | Functional\Orders\View\Components |
osdd:config | config | |
osdd:controller | src/Http/Controllers | Functional\Orders\Http\Controllers |
osdd:enum | src/Enums | Functional\Orders\Enums |
osdd:event | src/Events | Functional\Orders\Events |
osdd:exception | src/Exceptions | Functional\Orders\Exceptions |
osdd:factory | database/factories | Functional\Orders\Database\Factories |
osdd:interface | src/Contracts | Functional\Orders\Contracts |
osdd:job | src/Jobs | Functional\Orders\Jobs |
osdd:job-middleware | src/Jobs/Middleware | Functional\Orders\Jobs\Middleware |
osdd:listener | src/Listeners | Functional\Orders\Listeners |
osdd:mail | src/Mail | Functional\Orders\Mail |
osdd:middleware | src/Http/Middleware | Functional\Orders\Http\Middleware |
osdd:migration | database/migrations | |
osdd:model | src/Models | Functional\Orders\Models |
osdd:notification | src/Notifications | Functional\Orders\Notifications |
osdd:observer | src/Observers | Functional\Orders\Observers |
osdd:policy | src/Policies | Functional\Orders\Policies |
osdd:provider | src/Providers | Functional\Orders\Providers |
osdd:request | src/Http/Requests | Functional\Orders\Http\Requests |
osdd:resource | src/Http/Resources | Functional\Orders\Http\Resources |
osdd:rule | src/Rules | Functional\Orders\Rules |
osdd:scope | src/Models/Scopes | Functional\Orders\Models\Scopes |
osdd:seeder | database/seeders | Functional\Orders\Database\Seeders |
osdd:test | tests/Feature, or tests/Unit with --unit | Functional\Orders\Tests\Feature / Unit |
osdd:trait | src/Concerns | Functional\Orders\Concerns |
osdd:view | resources/views | |
osdd:*-table | database/migrations |
Sub-folders work like with make:*: osdd:model Admin/Role writes src/Models/Admin/Role.php in the Functional\Orders\Models\Admin namespace.
Generators that wire the provider
These generators also update the layer, so the new file is loaded without editing anything:
| Command | Adds |
|---|---|
osdd:migration, osdd:*-table | $this->loadMigrationsFrom(__DIR__ . '/../../database/migrations'); to boot() |
osdd:seeder | $this->loadSeeders([\Functional\Orders\Database\Seeders\OrdersSeeder::class]); to boot() |
osdd:config | $this->overrideConfigFrom(__DIR__ . '/../../config/orders.php', 'orders'); to register() |
osdd:command | $this->commands([\Functional\Orders\Console\Commands\SyncOrders::class]); to boot() |
osdd:view | $this->withViews(__DIR__ . '/../../resources/views'); to boot() |
osdd:component | withViews() (unless --inline) and $this->withComponents('Functional\\Orders\\View\\Components'); (unless --view) to boot() |
osdd:provider | The new provider to extra.laravel.providers in the layer's composer.json |
They edit the first class of the layer's extra.laravel.providers found on disk, else src/Providers/<Package>ServiceProvider.php, with the package name in StudlyCase as in the layer namespace: OrdersServiceProvider, OrderLinesServiceProvider for functional/order-lines. The __DIR__ path is computed from the provider's folder. They print what they did:
Registered [database/migrations] in [functional/orders/src/Providers/OrdersServiceProvider.php].
The injections are idempotent:
- A call the provider already makes is not added again, whatever its layout: spacing (Pint writes
__DIR__.'/../../database/migrations'), line breaks, comments, a trailing comma, or the short name of an imported class (loadSeeders([OrdersSeeder::class])). withViews()andwithComponents()are added only when the provider does not call them at all.- A commented-out call does not count.
- When the provider imports the class, the added line uses its short name:
$this->loadSeeders([AdminSeeder::class]);.
When the layer has no provider, or the provider has no boot() or register() method, the file is still generated and the command prints the line to add by hand:
Layer [functional/orders] has no service provider. Add this line to the boot() method of its provider:
Examples
Model and its companions
osdd:model accepts the options of make:model and creates every companion in the same layer:
php artisan osdd:model Order --layer=functional/orders --factory --migration --seed --policy --resource --requests
functional/orders/
├── src/Models/Order.php
├── src/Policies/OrderPolicy.php
├── src/Http/Controllers/OrderController.php
├── src/Http/Requests/StoreOrderRequest.php
├── src/Http/Requests/UpdateOrderRequest.php
└── database/
├── factories/OrderFactory.php
├── migrations/<timestamp>_create_orders_table.php
└── seeders/OrderSeeder.php
The controller is built like make:model builds it:
| Option | Controller |
|---|---|
--controller | A plain controller |
--resource | A resource controller for the model |
--api | An API controller for the model |
--requests | Also the Store and Update form requests |
--test, --pest | Passed on to the controller |
--all creates the factory, migration, seeder, policy, resource controller and form requests at once. With --factory, the model gets use HasFactory; and a #[UseFactory(OrderFactory::class)] attribute pointing to the layer's factory.
When the model already exists, osdd:model prints Model already exists. and, in a terminal, asks Do you want to generate additional components for the model? before creating the components you passed. Without a terminal or with -n, it creates them without asking. --force overwrites the model.
Migration
php artisan osdd:migration add_status_to_orders_table --table=orders --layer=functional/orders
# functional/orders/database/migrations/<timestamp>_add_status_to_orders_table.php
Framework tables
osdd:cache-table, osdd:session-table, osdd:queue-table, osdd:queue-failed-table, osdd:queue-batches-table and osdd:notifications-table write the framework migration into the layer's database/migrations/ and add loadMigrationsFrom():
php artisan osdd:notifications-table --layer=technical/osdd
They fail with exit code 1 and Migration already exists. when a layer, the root database/migrations/ or a registered migration path already creates the table. After osdd:start, the cache, jobs, batches, failed jobs and sessions tables already exist: only osdd:notifications-table is usually needed. The queue:table-style aliases stay on Laravel's commands.
Factory
php artisan osdd:factory Order --layer=functional/orders
# functional/orders/database/factories/OrderFactory.php, class OrderFactory
osdd:factory Order and osdd:factory OrderFactory both create OrderFactory.
Event and listener
php artisan osdd:event OrderPlaced --layer=functional/orders
php artisan osdd:listener SendOrderConfirmation --event=OrderPlaced --layer=functional/orders
--event=OrderPlaced is taken from the layer's Events namespace. Start the name with a backslash for an event of another layer: --event='\Functional\Orders\Events\OrderPlaced'. To register the listener, turn on event discovery or call Event::listen() in the layer provider.
Command
php artisan osdd:command PruneAbandonedOrders --layer=functional/orders
# functional/orders/src/Console/Commands/PruneAbandonedOrders.php
The command adds $this->commands([...]) to the layer provider, so php artisan list shows it.
View and component
php artisan osdd:view orders.index --layer=functional/orders
php artisan osdd:component Alert --layer=functional/orders
Render them as view('orders::orders.index') and <x-orders::alert />. See Views and components.
Job middleware
php artisan osdd:job-middleware RateLimitedOrders --layer=functional/orders
# functional/orders/src/Jobs/Middleware/RateLimitedOrders.php
Provider
php artisan osdd:provider OrderEventsServiceProvider --layer=functional/orders
# functional/orders/src/Providers/OrderEventsServiceProvider.php, listed in the layer's composer.json
The provider is registered in the layer's composer.json only, never in bootstrap/providers.php. Run composer update functional/orders so Laravel discovers it.
Test
php artisan osdd:test PlaceOrderTest --layer=functional/orders
# functional/orders/tests/Feature/PlaceOrderTest.php
Referencing another layer
--model on osdd:policy, osdd:controller, osdd:observer and osdd:factory resolves a short name inside the current layer: --model=Order means Functional\Orders\Models\Order. A fully qualified class of another layer, or of a package, is kept as is:
php artisan osdd:policy UserOrderPolicy --model='Functional\Users\Models\User' --layer=functional/orders
The layer then uses functional/users: require it, or osdd:check reports it.
Calling a generator from code
use Illuminate\Support\Facades\Artisan;
Artisan::call('osdd:model', ['name' => 'Order', '--layer' => 'functional/orders']);
Inside a command, $this->call(\Lomkit\Osdd\Console\Commands\Make\ModelMakeCommand::class, [...]) works too. Each call writes into the layer passed to it, even when one process calls the generators for several layers. Check the exit code: an unresolved --layer returns 1, it does not throw.
The generator classes are final. To change a generator, write your own command that extends the Laravel make:* command, or call the osdd:* one.