Skip to content

Package layout and boot ​

How racerfish/evolve is put together and what happens between composer install and the first request.

One package, many providers ​

composer.json declares the package racerfish/evolve (PHP 8.3, tested against Laravel 12 through orchestra/testbench). Its PSR-4 map points Racerfish\Evolve\ at core/src/ and one namespace per module at <module>/src/ (Racerfish\Blog\, Racerfish\Shopify\, Racerfish\Mcp\, Racerfish\AiChatbot\, Racerfish\Email\, and so on). core/global/helpers.php is autoloaded as a file and holds the global helpers such as module_index_url().

extra.laravel.providers lists fourteen providers for package discovery: the twelve module providers, Racerfish\Theme\ThemeServiceProvider and Racerfish\Evolve\EvolveServiceProvider. A host application therefore needs no provider registration of its own. Optional packages are suggested rather than required: laravel/mcp for the MCP module and laravel/ai for the chatbot (both since 2.21, in require-dev so the suite covers the modules), and sentry/sentry-laravel for error tracking.

Directory roles:

PathRole
core/the CMS: src/, config/, database/, routes/, views/, lang/, assets/ (panel build), bin/ (Envoy, deploy_exclude.txt), app-publish/ (blueprints published into projects), stubs/claude/ (CLAUDE.md, skill and .mcp.json for client projects), docs/, tests/
<module>/src/, config/<module>.php, config/components/*.yaml, database/migrations, views/, lang/, optionally app-publish/ and tests/
theme/ThemeServiceProvider, the theme:: views the package renders (head partials, structured data, mail), and theme/theme/, the publishable starter theme
workbench/Testbench host app for development and tests, not shipped
.github/CI, Release Drafter, RC and semver-label workflows, the docs release entry script

Boot sequence ​

What Laravel runs between autoload and the first request, and where Evolve hooks in:

Core provider: register ​

EvolveServiceProvider::register() (core/src/EvolveServiceProvider.php) runs before any provider boots:

  1. Merges the package configs: evolve, image, translatable, tracking, activitylog, llm, health, and services recursively (mergeConfigRecursivelyFrom), so a project config/services.php that defines services.google does not wipe the package keys it leaves out (2.11). Values a project config empties anyway are reported by the shadowed-config health check.
  2. Resolves the module switches (Support\ModuleSwitches::resolve()): every key under evolve.modules, every module provider registered so far and the core switch region get one boolean. An EVOLVE_MODULE_<KEY> variable wins, then the config key, then the module's defaultEnabled(). The source of each lands in evolve.module-sources (env, config, default). A booting callback resolves again once project modules registered, which register after the package. Both runs end up in a cached config.
  3. Binds singletons: Deployment\Environments, Tracking\TrackingManager.
  4. Registers the settings store: the SettingsOverlay singleton and a booting callback that lays stored values over the config after every provider merged and before any provider boots. See Settings overlay.
  5. Defines filesystem disks the package relies on: public rooted at public_path(), uploads, logs, evolveRoot, evolveModules, theme.

Core provider: boot ​

boot() calls bootEvolve(), which loads routes (core/routes/routes.php), views (evolve::), translations, migrations and Livewire components, registers Blade components and the Route::evolveModule() macro, the panel commands for Cmd-K, the health checks and the schedule (health checks and heartbeats every minute, history pruning daily). Then it registers Gate::before with Support\Permissions::before, excludes the consent and tracking cookies from cookie encryption (frontend JavaScript writes evolve_consent, PHP reads it back, and _ga, _fbp, _fbc are forwarded to the conversion APIs), and wires Fortify's views and actions to Evolve's auth screens.

Route::evolveModule($name, $group) wraps a module's panel routes in web, auth and backend.locale middleware, the prefix evolve/<name>, the name prefix <name>., and the role middleware resolved from evolve.roles.modules.

Module providers ​

Every module extends core/src/ModuleServiceProvider.php and sets $moduleName and $modulePath. The base class:

  • answers isEnabled() from config("evolve.modules.{$moduleName}", $defaultEnabled); after the switches resolved, the key always exists;
  • returns early from register() and boot() when the module is off or dependenciesMet() is false, so nothing of the module exists at runtime. A module with optional packages declares them in requiredPackages() as package name to a class or interface the package ships (MCP: laravel/mcp to Laravel\Mcp\Server, the chatbot: laravel/ai to Laravel\Ai\Contracts\Agent); missingPackages() lists the ones not installed, and dependenciesMet() is true when that list is empty (2.21, before each module overrode dependenciesMet() and logged a warning on every request);
  • when on, merges config/<module>.php, loads lang, views, database/migrations, src/Http/routes.php, and the module's Livewire components (Services\LivewireComponentLoader), publishes <module>.config and <module>.components, then calls bootModule().

bootModule() is where a module calls Evolve::registerModule($name, [...]) with name, path, optional label, commands (Cmd-K entries built with Support\PanelCommand), settings (keys, booleans, secrets the Settings screen may write; config maps each key to the config path the screen renders it from so an untouched default is not written to the .env; required maps the env keys the module needs to their config path for the API keys health check), searchable models and frontend.translations. Evolve::registerModule() ignores a module whose switch is off. The registry feeds the sidebar, the command palette, the Settings "Packages" tab, global search and the MCP list-modules tool.

The Evolve singleton ​

core/src/Evolve.php (facade Evolve) is the registry: modules, block composers (registerBlockComposer), discovered blueprints (evolve.blueprints.paths plus legacy YAML), roles per area (rolesFor('media'), rolesForModule('blog'), roleMiddleware()), the environment type (environmentType()) and emitsTracking().

Publishing ​

Core tags: evolve.config (evolve, image, fortify, translatable), evolve.blueprints and evolve.components, evolve.assets (the built panel to public/vendor/evolve), evolve.mail-views (the mail document layout and block views to theme/views/vendor/evolve/email, 2.22), evolve.llm, evolve.deploy-exclude, evolve.claude.starter and evolve.claude.skill. Modules add <module>.config, <module>.components and, where they ship them, <module>.blueprints, <module>.views or <module>.assets. The health config is deliberately not publishable.

Reading order ​

For a change in the core, start at the provider method that wires the area, then the service under core/src/Services, Support, Tracking, Settings, Health or Deployment, then the Livewire component or controller under core/src/Http, then the view under core/views. The other pages of this track follow that path per system.