The spark: from architecture to ecosystems 🔌
In my first post, I shared how explicit module boundaries (imports/exports) make architecture visible. In my second post, I realized that same foundation quietly unlocked an ecosystem pattern that goes beyond web apps.
This post shows the first concrete piece of that ecosystem story: a dependency‑injection‑native plugin system with static analysis support (Layer 1), a real extension point in the dependency graph module (Layer 2), and a third‑party implementation you can use today—the Mermaid renderers (Layer 3).
You'll see how plugins are discovered automatically, resolved with DI, and used to visualize your architecture with one command. This foundation enables everything from content filter pipelines to payment gateway adapters—patterns we'll explore in Part 2.
Ecosystem layers, now practical
- Layer 1: Foundation for plugins (generic, domain‑agnostic)
- Layer 2: Domain‑specific cores exposing plugin points (e.g., dependency‑graph → Renderer)
- Layer 3: Third‑party plugins (e.g., Mermaid renderers)
We'll walk through all three, end‑to‑end.
Layer 1 — The plugin foundation (generic but powerful) 🧩
The goals:
- Plugins are first‑class citizens with metadata
- Registries provide IDE/static analysis support (via PHPDoc generics) and lazy loading (instantiate on demand)
- Discovery is automatic at app boot, integrated with module isolation
Key pieces (power-modules/plugin
):
-
Plugin
: every plugin returnsPluginMetadata
-
PluginRegistry<TPlugin>
: register, list, and instantiate plugins (generic annotation for IDE support) -
GenericPluginRegistry
: default, DI‑native implementation -
ProvidesPlugins<TPlugin>
: a module declares its plugins for one or more registries - Setups:
-
GenericPluginRegistrySetup
: provides a default registry in root -
PluginRegistryModuleConvenienceSetup
: makes the registry injectable in modules -
PluginRegistrySetup
: discovers modules that implementProvidesPlugins
and registers their plugins
-
A tiny example:
use Modular\Framework\App\ModularAppBuilder;
use Modular\Framework\PowerModule\Contract\PowerModule;
use Modular\Framework\Container\ConfigurableContainerInterface;
use Modular\Plugin\Contract\Plugin;
use Modular\Plugin\PluginMetadata;
use Modular\Plugin\Contract\PluginRegistry;
use Modular\Plugin\Contract\ProvidesPlugins;
use Modular\Plugin\PowerModule\Setup\PluginRegistrySetup;
interface TextProcessor extends Plugin { public function process(string $in): string; }
final class UppercasePlugin implements TextProcessor
{
public static function getPluginMetadata(): PluginMetadata
{
return new PluginMetadata('Uppercase', '1.0.0', 'Converts text to UPPERCASE');
}
public function process(string $in): string { return strtoupper($in); }
}
final class TextPluginModule implements PowerModule, ProvidesPlugins
{
public static function getPlugins(): array
{
return [PluginRegistry::class => [UppercasePlugin::class]]; // declare plugins
}
public function register(ConfigurableContainerInterface $c): void
{
$c->set(UppercasePlugin::class, UppercasePlugin::class); // DI binding
}
}
$app = (new ModularAppBuilder(__DIR__))
->withPowerSetup(...PluginRegistrySetup::withDefaults())
->withModules(TextPluginModule::class)
->build();
/** @var PluginRegistry<TextProcessor> $registry */
$registry = $app->get(PluginRegistry::class);
$plugin = $registry->makePlugin(UppercasePlugin::class);
echo $plugin->process('hello'); // HELLO
What's happening:
- At boot,
PluginRegistrySetup
scans modules that implementProvidesPlugins
- For each plugin class, it registers a (class → owning container) mapping in the root registry
- At runtime,
makePlugin()
resolves the instance from that module's container (full DI)
Layer 2 — The dependency graph exposes a plugin point đź§
The power-modules/dependency-graph
module collects your modules and their relationships during app bootstrap.
-
DependencyGraphSetup
builds aDependencyGraph
fromModuleNode
s and their imports -
Renderer
is a plugin contract for turning that graph into outputs (text, JSON, Mermaid, etc.) -
RendererPluginRegistry
(extendsGenericPluginRegistry<Renderer>
) is exported byRendererModule
Interface:
interface Renderer extends Plugin
{
public function render(DependencyGraph $graph): string;
public function getFileExtension(): string; // e.g. mmd, json
public function getMimeType(): string;
public function getDescription(): string;
}
This gives us a clean "slot" where anyone can add a new renderer.
Layer 3 — Third‑party Mermaid renderers 🧪
Now here's where it gets interesting. This repository (power-modules/dependency-graph-mermaid
) is a third‑party plugin package that extends the dependency-graph module with visual renderers.
The MermaidRendererModule
registers three plugins implementing Renderer
:
-
Flowchart:
MermaidGraph
(LR direction, shows imports as labeled arrows) -
Class diagram:
MermaidClassDiagram
(TB direction + YAML frontmatter, exports as class members) -
Timeline:
MermaidTimeline
(dependency levels as phases, Infrastructure/Domain sections)
They're regular plugins declared via ProvidesPlugins<Renderer>
and discovered automatically—no special wiring needed.
Visual tour: your architecture, rendered
The best part? You get immediate visual feedback. Below are small, self‑contained Mermaid snippets inspired by the ecommerce example. You can paste them into Mermaid Live or VS Code's Mermaid preview to see them rendered.
1) Flowchart (modules and imports)
2) Class diagram (exports as members, imports as dashed deps)
3) Timeline (boot phases by dependency levels)
Tip: in code, MermaidTimeline
computes phases Kahn‑style and separates Infrastructure/Domain with a simple classifier.
Complete setup pattern 🚀
Here's the complete setup pattern—replace the example modules with your own:
Add the necessary setups and modules:
-
DependencyGraphSetup()
to collect the graph during app bootstrap -
...PluginRegistrySetup::withDefaults()
to enable plugin discovery -
RendererModule
to expose theRendererPluginRegistry
-
MermaidRendererModule
to register the three Mermaid plugins
use Modular\Framework\App\ModularAppBuilder;
use Modular\DependencyGraph\PowerModule\Setup\DependencyGraphSetup;
use Modular\DependencyGraph\Renderer\RendererModule;
use Modular\DependencyGraph\Renderer\Mermaid\MermaidRendererModule;
use Modular\DependencyGraph\Graph\DependencyGraph;
use Modular\DependencyGraph\Renderer\RendererPluginRegistry;
use Modular\Plugin\PowerModule\Setup\PluginRegistrySetup;
$app = (new ModularAppBuilder(__DIR__))
->withPowerSetup(new DependencyGraphSetup())
->withPowerSetup(...PluginRegistrySetup::withDefaults())
->withModules(
// your app modules
App\User\UserModule::class,
App\Product\ProductModule::class,
App\Order\OrderModule::class,
App\Payment\PaymentModule::class,
App\Notification\NotificationModule::class,
App\Database\DatabaseModule::class,
// plugin point + plugins
RendererModule::class,
MermaidRendererModule::class,
)
->build();
$graph = $app->get(DependencyGraph::class);
$renderers = $app->get(RendererPluginRegistry::class);
foreach ($renderers->getRegisteredPlugins() as $rendererClass) {
$renderer = $renderers->makePlugin($rendererClass);
$code = $renderer->render($graph);
file_put_contents(__DIR__ . '/mermaid/out.' . $renderer->getFileExtension(), $code);
}
Want ready‑made scripts? Check out examples
.
Why this matters (and how teams use it) 📌
For refactoring: Spot central "god" modules that import from everyone, or unused modules that export but no one imports.
For onboarding: Give new teammates a real map—not just "here's the codebase," but "here's how modules connect and what they export."
For architecture reviews: Validate intended boundaries before they calcify. Catch cycles early.
For documentation: Check in generated .mmd
files alongside code. PRs that change module relationships show visual diffs.
For evolution planning: Identify natural seams for service extraction—modules with low in‑degree and clear export contracts are good candidates.
For CI guardrails: Add analyzers to fail builds on cycles or unexpected coupling.
Try it yourself
- Foundation:
composer require power-modules/plugin
- Graph + plugin point:
composer require power-modules/dependency-graph
- Mermaid renderers:
composer require power-modules/dependency-graph-mermaid
Then add the setups and modules as shown above, render, and paste the .mmd
into Mermaid Live.
What's next đź”®
Part 2: Building your own plugin ecosystem
- Custom registries with domain-specific validation (payment gateways, content filters)
- Config-driven plugin pipelines (tenant-specific processing steps)
- Metadata-driven admin UIs (plugin catalogs without instantiation)
- Real patterns: CMS filters, discount strategies, ETL steps, gateway adapters
Resources
- Framework: power-modules/framework
- Plugin system: power-modules/plugin
- Dependency graph: power-modules/dependency-graph
- Mermaid renderers: power-modules/dependency-graph-mermaid
-
Try the examples: Clone the repository and run
php examples/ecommerce/generate.php
I'm building this ecosystem in the open. If you try it, build a renderer, or have ideas for Part 2—drop a comment or connect with me!
What architectural challenges keep you up at night? Have you tried similar visualization approaches? đź’¬
Top comments (0)