Layer Service Provider
Every layer should have a service provider that extends Xefi\LaravelOSDD\LayerServiceProvider instead of Laravel's base Illuminate\Support\ServiceProvider.
<?php
namespace Functional\Orders\Providers;
use Xefi\LaravelOSDD\LayerServiceProvider;
use Functional\Orders\Database\Seeders\OrdersSeeder;
class OrdersServiceProvider extends LayerServiceProvider
{
public function boot(): void
{
$this->loadMigrationsFrom(__DIR__ . '/../../database/migrations');
$this->loadSeeders([OrdersSeeder::class]);
$this->withRouting(
web: __DIR__ . '/../../routes/web.php',
api: __DIR__ . '/../../routes/api.php',
commands: __DIR__ . '/../../routes/console.php',
channels: __DIR__ . '/../../routes/channels.php',
);
}
public function register(): void
{
$this->overrideConfigFrom(__DIR__ . '/../../config/orders.php', 'orders');
}
}
Methods
loadMigrationsFrom()
Inherited from Laravel's base ServiceProvider. Registers the layer's migration directory so php artisan migrate picks up the layer's migrations.
$this->loadMigrationsFrom(__DIR__ . '/../../database/migrations');
loadSeeders(array $seeders, int $priority = 0): void
Pushes one or more seeder class-strings into the global SeederRegistry singleton. Registered seeders are discovered by php artisan osdd:seed. The optional $priority parameter controls execution order — lower numbers run first (default: 0).
// Default priority
$this->loadSeeders([
OrdersSeeder::class,
OrderStatusSeeder::class,
]);
// Run this layer's seeders before others (e.g. foundational reference data)
$this->loadSeeders([RolesSeeder::class], priority: -10);
loadSeeders is the OSDD equivalent of adding seeders to DatabaseSeeder::call(). The difference is that each layer manages its own seeders — no central file to edit.overrideConfigFrom(string $path, string $key): void
Deep-merges the config file at $path over the already-loaded config key $key. Layer values win over whatever was loaded first. Skipped when config:cache has produced a cached config.
$this->overrideConfigFrom(__DIR__ . '/../../config/orders.php', 'orders');
overrideConfigFrom() from register(), not boot(). Other packages may read their config during their own boot() (for example, Horizon reads horizon.path and horizon.middleware while registering its routes), and the override needs to be in place by then.Why this matters: Laravel's built-in mergeConfigFrom() gives the already-loaded config priority — layer values are ignored if the key already exists. overrideConfigFrom() runs immediately during register() using array_replace_recursive, so the layer's values always take precedence — and they're in place before any other package's boot() runs.
| Method | Who wins | When it runs |
|---|---|---|
mergeConfigFrom() | Existing config wins | During register() |
overrideConfigFrom() | Layer config wins | During register() (skipped if config is cached) |
withRouting(web:, api:, commands:, channels:, health:, apiPrefix:): void
Mounts a layer's route files using Laravel's standard conventions — mirrors Application::configure()->withRouting(...) at the layer level.
$this->withRouting(
web: __DIR__ . '/../../routes/web.php',
api: __DIR__ . '/../../routes/api.php',
commands: __DIR__ . '/../../routes/console.php',
channels: __DIR__ . '/../../routes/channels.php',
);
| Argument | Behavior |
|---|---|
web | Wrapped in the web middleware group. Accepts a path or array of paths. |
api | Wrapped in the api middleware group and prefixed with apiPrefix (default api). Accepts a path or array of paths. |
commands | required when running in console — define Artisan closures with Artisan::command(...). |
channels | required on every request — define Broadcast::channel(...) mappings. |
health | Registers GET {health} returning {"status":"ok"}. |
apiPrefix | Prefix used by api routes. Defaults to 'api'. |
Each argument is optional. Missing files are skipped silently — so a generated layer can keep the full withRouting(...) call even when no route files exist yet.
php artisan route:cache has produced a cached route file, so it plays nicely with production caching.For anything beyond these conventions (custom prefixes, domains, named groups), use the Route facade directly in boot():
Route::middleware('web')
->prefix('admin')
->name('admin.')
->group(__DIR__ . '/../../routes/admin.php');
Full Example
<?php
namespace Functional\Orders\Providers;
use Xefi\LaravelOSDD\LayerServiceProvider;
use Functional\Orders\Database\Seeders\OrdersSeeder;
use Functional\Orders\Database\Seeders\OrderStatusSeeder;
use Functional\Orders\Contracts\PaymentGatewayInterface;
use Functional\Orders\Gateways\StripePaymentGateway;
class OrdersServiceProvider extends LayerServiceProvider
{
public function register(): void
{
$this->app->bind(
PaymentGatewayInterface::class,
StripePaymentGateway::class
);
// Override the 'orders' config key with this layer's config file.
// Called in register() so other packages see the override during their own boot().
$this->overrideConfigFrom(__DIR__ . '/../../config/orders.php', 'orders');
}
public function boot(): void
{
// Load this layer's migrations
$this->loadMigrationsFrom(__DIR__ . '/../../database/migrations');
// Register seeders with the global registry
$this->loadSeeders([
OrdersSeeder::class,
OrderStatusSeeder::class,
]);
// Mount route files using Laravel's standard conventions
$this->withRouting(
web: __DIR__ . '/../../routes/web.php',
api: __DIR__ . '/../../routes/api.php',
commands: __DIR__ . '/../../routes/console.php',
channels: __DIR__ . '/../../routes/channels.php',
);
}
}
Tinker Short-Name Aliases
When running inside Laravel Tinker, OSDD automatically registers an SPL autoloader that resolves layer classes by their short name (without the full namespace). This means you can type Invoice instead of Functional\Billing\Models\Invoice directly in the Tinker REPL.
# Instead of:
>>> Functional\Orders\Models\Order::first()
# You can write:
>>> Order::first()
This alias resolution is only active inside Tinker — it has no effect in normal application code. The autoloader scans all configured layer directories at boot time using the class map built by LaravelOSDDServiceProvider.
Functional\Users\Models\User and Functional\Admin\Models\User), the first one registered wins. Use fully-qualified names in Tinker to disambiguate.Auto-discovery
For the service provider to be booted automatically by Laravel, it must be listed in the layer's composer.json:
{
"extra": {
"laravel": {
"providers": [
"Functional\\Orders\\Providers\\OrdersServiceProvider"
]
}
}
}
osdd:layer injects this entry automatically when the service-provider generator is selected. After running composer update functional/orders, Laravel discovers and boots the provider without any changes to bootstrap/providers.php.