What are Layers?

Layer Service Provider

Extend LayerServiceProvider to register migrations, seeders, and config from your layer.

Every layer should have a service provider that extends Xefi\LaravelOSDD\LayerServiceProvider instead of Laravel's base Illuminate\Support\ServiceProvider.

src/Providers/OrdersServiceProvider.php
<?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');
Call 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.

MethodWho winsWhen it runs
mergeConfigFrom()Existing config winsDuring register()
overrideConfigFrom()Layer config winsDuring 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',
);
ArgumentBehavior
webWrapped in the web middleware group. Accepts a path or array of paths.
apiWrapped in the api middleware group and prefixed with apiPrefix (default api). Accepts a path or array of paths.
commandsrequired when running in console — define Artisan closures with Artisan::command(...).
channelsrequired on every request — define Broadcast::channel(...) mappings.
healthRegisters GET {health} returning {"status":"ok"}.
apiPrefixPrefix 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.

The whole call is a no-op when 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

src/Providers/OrdersServiceProvider.php
<?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.

Tinker
# 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.

If two layers define classes with the same short name (e.g. 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:

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.