happenv-com/laravel-true-modular

Maintainers

Package info

github.com/happenv-com/laravel-true-modular

pkg:composer/happenv-com/laravel-true-modular

Statistics

Installs: 68

Dependents: 0

Suggesters: 0

Stars: 0

Open Issues: 0

v0.1.1 2026-04-29 18:36 UTC

This package is auto-updated.

Last update: 2026-04-29 18:36:36 UTC


README

Extended Laravel Application with topological service provider ordering and enhanced lifecycle.

Overview

The Kernel module provides a custom Application class that extends Laravel's foundation application with two key enhancements:

  1. Topological Provider Ordering - Service providers from app modules are automatically sorted based on their composer.json dependencies
  2. Enhanced Lifecycle - A new initialize() method between register() and boot()

Application Lifecycle

Laravel Default

flowchart LR
    A[register] --> B[boot]
Loading

True Modular Extended

flowchart LR
    A[register] --> B[initialize] --> C[boot]
    style B fill:#22c55e,color:#fff
Loading

Lifecycle Phases

Phase Purpose Use Cases
register() Bind services to container Singletons, bindings, interfaces
initialize() Configure cross-cutting concerns Morph maps, permissions, hooks, drivers
boot() Bootstrap services Routes, views, commands, event listeners

Why initialize()?

In a modular monolith, modules often need to register things that depend on other modules being registered first, but before the full boot phase:

  • Morph Maps - Relation::morphMap() needs all models registered
  • Filament Hooks - Schema hooks need Filament resources registered
  • Permissions - Permission registration needs auth models ready
  • Drivers - Custom drivers for other modules' managers

The initialize() phase runs after all providers are registered and before any provider boots, ensuring cross-module dependencies are satisfied.

Usage

Implementing initialize() in a Service Provider

<?php

namespace Myapp\Sale;

use Illuminate\Database\Eloquent\Relations\Relation;
use Illuminate\Support\ServiceProvider;

class SaleServiceProvider extends ServiceProvider
{
    public function register(): void
    {
        // Bind services to container
        $this->app->singleton(OrderService::class);
    }

    public function initialize(): void
    {
        // Register morph maps (all models are now available)
        Relation::morphMap([
            'order' => Order::class,
            'payment' => Payment::class,
        ]);

        // Register permissions
        app(PermissionRegistrar::class)->register([
            'orders.view',
            'orders.create',
        ]);

        // Register Filament hooks
        ProductResource::registerSchemaHook('sale', fn () => [
            TextColumn::make('orders_count'),
        ]);
    }

    public function boot(): void
    {
        // Load routes, views, commands
        $this->loadRoutesFrom(__DIR__ . '/../routes/web.php');
    }
}

Execution Order

Providers are executed in topological order based on module dependencies:

myapp/kernel      (no dependencies)
myapp/core        (depends on kernel)
myapp/pim         (depends on core)
myapp/inventory   (depends on core)
myapp/sale        (depends on core, pim, inventory)
myapp/amazon      (depends on sale, pim)

This ensures that when SaleServiceProvider::initialize() runs, all its dependencies (core, pim, inventory) have already completed their initialize() phase.

Architecture

ServiceProviderSorter

Sorts service providers according to module dependency order:

use Myapp\Kernel\ServiceProviderSorter;

$sorter = app(ServiceProviderSorter::class);

// Sort providers - non-modules first, then modules in topological order
$sorted = $sorter->sort($providers);

// Get module name for a provider
$module = $sorter->getModuleName($provider);
// Returns: 'myapp/sale' or null

// Group providers by module
$grouped = $sorter->groupByModule($providers);
// Returns: ['myapp/core' => [...], 'myapp/sale' => [...]]

How Sorting Works

  1. Identify modules by composer.json type field (true-module by default)
  2. Separate providers into modules vs others (Laravel, Spatie, etc.)
  3. Preserve original order for non-module providers
  4. Sort module providers using Kahn's topological sort algorithm
  5. Merge - other providers first, then sorted module providers
Input:  [Laravel, Filament, Sale, Core, Spatie, Amazon, Pim]
Output: [Laravel, Filament, Spatie, Core, Pim, Sale, Amazon]
                                    ↑─── Module sorted ───↑

Configuration

The custom Application is configured in bootstrap/app.php:

<?php

use Myapp\Kernel\Application;

return Application::configure(basePath: dirname(__DIR__))
    ->withRouting(...)
    ->withMiddleware(...)
    ->create();

Module Detection

Modules are identified by their composer.json type field. By default, the package looks for packages with type: "true-module".

Module composer.json

Each module must have the correct type in its composer.json:

{
    "name": "myapp/sale",
    "type": "true-module",
    "require": {
        "myapp/core": "*",
        "myapp/pim": "*"
    },
    "autoload": {
        "psr-4": {
            "Myapp\\Sale\\": "src/"
        }
    }
}

Custom Module Type

If you prefer a different type identifier (e.g., for organization-specific naming), you can customize it in bootstrap/app.php:

<?php

use Myapp\Kernel\Application;

return Application::moduleComposerType('acme-module')
    ::configure(basePath: dirname(__DIR__))
    ->withRouting(...)
    ->withMiddleware(...)
    ->create();

Then your modules would use:

{
    "name": "acme/billing",
    "type": "acme-module"
}

Retrieving the Current Type

You can programmatically check the configured module type:

$type = Application::getModuleComposerType();
// Returns: 'true-module' (default) or your custom type

Best Practices

DO use initialize() for:

  • Relation::morphMap() registration
  • Gate::define() and permission registration
  • Filament schema hooks and customizations
  • Driver registration for managers
  • Cross-module event listener registration

DON'T use initialize() for:

  • Container bindings → use register()
  • Loading routes/views/commands → use boot()
  • Anything that doesn't need cross-module coordination

Migration from existing code

If you have code in boot() that registers morph maps, permissions, or hooks, move it to initialize():

// Before (in boot)
public function boot(): void
{
    Relation::morphMap(['order' => Order::class]); // ❌ Too late
    $this->loadRoutesFrom(...);
}

// After (split between initialize and boot)
public function initialize(): void
{
    Relation::morphMap(['order' => Order::class]); // ✅ Perfect timing
}

public function boot(): void
{
    $this->loadRoutesFrom(...);
}

Module Setup Checklist

  • Set "type": "true-module" in each module's composer.json
  • Define dependencies in require section
  • Configure PSR-4 autoload namespace
  • (Optional) Customize type via Application::moduleComposerType()

Config Extension (extendConfigs)

Modules can extend existing config files using configsToExtend. The merge follows these rules:

Original value Extending value Result
scalar scalar keeps original (overwrite: false) or replaces (overwrite: true)
list [a, b] list [b, c] merged + deduplicated → [a, b, c]
assoc array assoc array recursively merged with the same rules
new key any always added

Lists (array_is_list) are always extended regardless of the overwrite flag. Scalars and type mismatches (e.g. scalar vs array) respect overwrite.