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.
Background
Section titled “Background”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"]1. Choose the module Type and name it
Section titled “1. Choose the module Type and name it”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.
-
Pick the Type by audience. This decides the top-level folder.
Who needs it? Type folder All users, when enabled SystemOnly SuperAdmin SuperAdminDepends on the subscription plan Subscription- ✅ You can state the Type and one sentence of why.
-
Apply the naming convention to the Category, the Provider, and every class. The path is
packages/ZajModules/{Type}/{Category}/{Provider}/.Element Convention Example Category PascalCase, describes the function EmailMarketing,NotificationsProvider PascalCase, the service name Encharge,SlackNamespace ZajModules\{Type}\{Category}\{Provider}ZajModules\System\Notifications\SlackServiceProvider {Provider}ServiceProviderSlackServiceProviderService {Provider}ServiceSlackServiceListener Track{Event}orNotify{Event}NotifyUserRegisteredConfig file lowercase {provider}.phpslack.phpEnv var ZAJMODULES_{PROVIDER}_{KEY}ZAJMODULES_SLACK_ENABLED- ✅ Every name in your module follows the table — the namespace mirrors the folder path.
2. Scaffold the directory structure
Section titled “2. Scaffold the directory structure”Create the folders the module needs. A backend-only module skips the resources/ UI folders; a module with a UI keeps them.
-
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/, andresources/only if the feature has a UI.
- ✅ The module folder exists with
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.
-
Write the ServiceProvider so it publishes config, registers listeners behind an
enabledflag, 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.enabledis true — disabling the module is a single flag flip.
- ✅ Listeners register only when
-
Write the Config with an
enabledmaster switch andenv()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.
- ✅ The config path is
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).
-
Add the module’s PSR-4 autoload entry to
composer.json. Each module needs its own line, andZajCore\\(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-4incomposer.json.
- ✅ The namespace appears under
-
Register the ServiceProvider in
bootstrap/providers.php.ZajModules\System\Notifications\Slack\Providers\SlackServiceProvider::class,- ✅ The provider class is listed in
bootstrap/providers.php.
- ✅ The provider class is listed in
-
Regenerate the autoloader and confirm the class resolves.
Terminal window composer dump-autoloadphp -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 printsNOT FOUND, the autoload entry is missing or mistyped.
- ✅ The verification prints
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.
-
Clear caches, then confirm the provider and config load.
Terminal window php artisan config:clearphp artisan provider:list | grep ZajModulesphp 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.
-
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.
- ✅ Nothing under the vendor’s own folders appears in
Checklist
Section titled “Checklist”Do not mark this step done until every box below is checked.
- 👤 Type chosen —
System/SuperAdmin/Subscriptiondecided 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.jsonandclass_exists(...)printsOKaftercomposer dump-autoload. - 🔀 Isolated — the feature works and
git statusshows no vendor files changed.