Skip to content
prod e051e98
Browse

2 · Build a ZajModule

Objective — package any new feature as a self-contained ZajModule so the vendor app never references it and every vendor update passes through untouched.

A ZajModule is a LEGO block: a feature that works on its own and snaps into the app without the app being edited. Instead of changing the vendor’s RegisterController to add tracking, your module listens to Laravel’s Registered event and acts on it. The vendor never knows, never cares — so git pull of a new version is a non-event.

Everything below follows one structure so every module in the project looks the same, and a new developer can be told “here’s the modules folder” instead of “here are the 50 files we edited.”

flowchart LR
E["Laravel/WorkDo<br/>fires an event"] --> L["Listener<br/>in your module"]
L --> S["Service<br/>business logic"]
S --> X["External service<br/>or new feature"]

Before scaffolding, decide which Type folder the module belongs in, then name every part consistently. The naming is not cosmetic — the namespace must match the folder path exactly or autoloading fails.

  1. Pick the Type by audience. This decides the top-level folder.

    Who needs it?Type folder
    All users, when enabledSystem
    Only SuperAdminSuperAdmin
    Depends on the subscription planSubscription
    • ✅ You can state the Type and one sentence of why.
  2. Apply the naming convention to the Category, the Provider, and every class. The path is packages/ZajModules/{Type}/{Category}/{Provider}/.

    ElementConventionExample
    CategoryPascalCase, describes the functionEmailMarketing, Notifications
    ProviderPascalCase, the service nameEncharge, Slack
    NamespaceZajModules\{Type}\{Category}\{Provider}ZajModules\System\Notifications\Slack
    ServiceProvider{Provider}ServiceProviderSlackServiceProvider
    Service{Provider}ServiceSlackService
    ListenerTrack{Event} or Notify{Event}NotifyUserRegistered
    Config filelowercase {provider}.phpslack.php
    Env varZAJMODULES_{PROVIDER}_{KEY}ZAJMODULES_SLACK_ENABLED
    • ✅ Every name in your module follows the table — the namespace mirrors the folder path.

Create the folders the module needs. A backend-only module skips the resources/ UI folders; a module with a UI keeps them.

  1. Create the source and resource folders for the module.

    Terminal window
    mkdir -p packages/ZajModules/System/Notifications/Slack/src/{Providers,Services,Listeners,Console,Config}
    mkdir -p packages/ZajModules/System/Notifications/Slack/resources/{css,views}
    mkdir -p packages/ZajModules/System/Notifications/Slack/tests
    # Expected: the folder tree is created with no errors
    • ✅ The module folder exists with src/, and resources/ only if the feature has a UI.

The full shape of a module — src/Config, src/Providers, src/Services, src/Listeners, src/Console, optional resources/, tests/, a module.json metadata file, and a README.md.

3. Write the ServiceProvider, Service, and Config

Section titled “3. Write the ServiceProvider, Service, and Config”

The ServiceProvider is the module’s single entry point: it merges config, registers the service, and wires event listeners only when the module is enabled. The Service holds the actual logic.

  1. Write the ServiceProvider so it publishes config, registers listeners behind an enabled flag, and registers any console commands.

    namespace ZajModules\System\Notifications\Slack\Providers;
    use Illuminate\Support\ServiceProvider;
    class SlackServiceProvider extends ServiceProvider
    {
    public function register(): void
    {
    $this->mergeConfigFrom(__DIR__.'/../Config/slack.php', 'zajmodules.slack');
    $this->app->singleton(\ZajModules\System\Notifications\Slack\Services\SlackService::class);
    }
    public function boot(): void
    {
    if (config('zajmodules.slack.enabled', false)) {
    $this->app['events']->listen(
    \Illuminate\Auth\Events\Registered::class,
    \ZajModules\System\Notifications\Slack\Listeners\NotifyUserRegistered::class
    );
    }
    }
    }
    • ✅ Listeners register only when zajmodules.slack.enabled is true — disabling the module is a single flag flip.
  2. Write the Config with an enabled master switch and env() fallbacks, so the module is off by default and configured through .env.

    return [
    'enabled' => env('ZAJMODULES_SLACK_ENABLED', false),
    'webhook_url' => env('ZAJMODULES_SLACK_WEBHOOK_URL'),
    'channel' => env('ZAJMODULES_SLACK_CHANNEL', '#notifications'),
    ];
    • ✅ The config path is zajmodules.{provider} and every value has a safe default.

4. Register the namespace and the provider

Section titled “4. Register the namespace and the provider”

A module is invisible until two things happen: its namespace is in composer.json (so PHP can find its classes) and its ServiceProvider is registered (so Laravel boots it).

  1. Add the module’s PSR-4 autoload entry to composer.json. Each module needs its own line, and ZajCore\\ (shared interfaces and traits in _core/) must be present too.

    "ZajModules\\System\\Notifications\\Slack\\": "packages/ZajModules/System/Notifications/Slack/src/"
    • ✅ The namespace appears under autoload.psr-4 in composer.json.
  2. Register the ServiceProvider in bootstrap/providers.php.

    ZajModules\System\Notifications\Slack\Providers\SlackServiceProvider::class,
    • ✅ The provider class is listed in bootstrap/providers.php.
  3. Regenerate the autoloader and confirm the class resolves.

    Terminal window
    composer dump-autoload
    php -r "require 'vendor/autoload.php'; echo class_exists('ZajModules\\System\\Notifications\\Slack\\Services\\SlackService') ? 'OK' : 'NOT FOUND';"
    # Expected: OK
    • ✅ The verification prints OK — if it prints NOT FOUND, the autoload entry is missing or mistyped.

5. Verify the module loads and stays isolated

Section titled “5. Verify the module loads and stays isolated”

The point of a ZajModule is that the vendor app is untouched. Confirm the module boots, and confirm git status shows no vendor files changed.

  1. Clear caches, then confirm the provider and config load.

    Terminal window
    php artisan config:clear
    php artisan provider:list | grep ZajModules
    php artisan tinker --execute="dd(config('zajmodules.slack'));"
    # Expected: the provider appears in the list and the config array prints
    • ✅ The provider is listed and the config array prints its keys.
  2. Confirm no vendor files were touched.

    Terminal window
    git status --short
    # Expected: only files under packages/ZajModules/, composer.json, and bootstrap/providers.php
    • ✅ Nothing under the vendor’s own folders appears in git status — the module is fully isolated.

Do not mark this step done until every box below is checked.

  • 👤 Type chosenSystem / SuperAdmin / Subscription decided by audience.
  • 🤖 Naming consistent — namespace mirrors the folder path; provider/service/listener/config all follow the convention.
  • 🤖 Provider gated by enabled — listeners register only when the config flag is true.
  • 🤖 Autoload resolves — the namespace is in composer.json and class_exists(...) prints OK after composer dump-autoload.
  • 🔀 Isolated — the feature works and git status shows no vendor files changed.