Commands

Generators

The osdd:* twins of Laravel's make:* commands write inside a layer.

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
CaseResult
Full name--layer=functional/orders
Package name, unique--layer=orders finds functional/orders
Package name shared by two layersExit 1: Layer [orders] is ambiguous: it matches functional/orders, technical/orders. Pass the full layer name.
Unknown nameExit 1: Layer [functional/oders] not found. Available layers: functional/orders, functional/users. Did you mean [functional/orders]?
Omitted, in a terminalThe command asks you to search and pick a layer
Omitted, without a terminal or with -nExit 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:

CommandFolderNamespace
osdd:castsrc/CastsFunctional\Orders\Casts
osdd:channelsrc/BroadcastingFunctional\Orders\Broadcasting
osdd:classsrcFunctional\Orders
osdd:commandsrc/Console/CommandsFunctional\Orders\Console\Commands
osdd:componentsrc/View/Components, view in resources/views/componentsFunctional\Orders\View\Components
osdd:configconfig
osdd:controllersrc/Http/ControllersFunctional\Orders\Http\Controllers
osdd:enumsrc/EnumsFunctional\Orders\Enums
osdd:eventsrc/EventsFunctional\Orders\Events
osdd:exceptionsrc/ExceptionsFunctional\Orders\Exceptions
osdd:factorydatabase/factoriesFunctional\Orders\Database\Factories
osdd:interfacesrc/ContractsFunctional\Orders\Contracts
osdd:jobsrc/JobsFunctional\Orders\Jobs
osdd:job-middlewaresrc/Jobs/MiddlewareFunctional\Orders\Jobs\Middleware
osdd:listenersrc/ListenersFunctional\Orders\Listeners
osdd:mailsrc/MailFunctional\Orders\Mail
osdd:middlewaresrc/Http/MiddlewareFunctional\Orders\Http\Middleware
osdd:migrationdatabase/migrations
osdd:modelsrc/ModelsFunctional\Orders\Models
osdd:notificationsrc/NotificationsFunctional\Orders\Notifications
osdd:observersrc/ObserversFunctional\Orders\Observers
osdd:policysrc/PoliciesFunctional\Orders\Policies
osdd:providersrc/ProvidersFunctional\Orders\Providers
osdd:requestsrc/Http/RequestsFunctional\Orders\Http\Requests
osdd:resourcesrc/Http/ResourcesFunctional\Orders\Http\Resources
osdd:rulesrc/RulesFunctional\Orders\Rules
osdd:scopesrc/Models/ScopesFunctional\Orders\Models\Scopes
osdd:seederdatabase/seedersFunctional\Orders\Database\Seeders
osdd:testtests/Feature, or tests/Unit with --unitFunctional\Orders\Tests\Feature / Unit
osdd:traitsrc/ConcernsFunctional\Orders\Concerns
osdd:viewresources/views
osdd:*-tabledatabase/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:

CommandAdds
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:componentwithViews() (unless --inline) and $this->withComponents('Functional\\Orders\\View\\Components'); (unless --view) to boot()
osdd:providerThe 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() and withComponents() 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:

OptionController
--controllerA plain controller
--resourceA resource controller for the model
--apiAn API controller for the model
--requestsAlso the Store and Update form requests
--test, --pestPassed 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.