| name | create-plugin |
| description | Create a Marko plugin — a class that intercepts methods on another class to modify behavior without subclassing. **Use this skill whenever the user wants to add a plugin, intercept a method, or extend a class's behavior.** Concrete triggers: "add a plugin for the OrderService", "intercept the save method on User", "extend the behavior of X without changing it", "modify Y's return value before it's returned", "I need a plugin", "create a Before/After plugin". Plugins use the #[Plugin], #[Before], #[After] attributes.
|
Create a Marko Plugin
This skill is the canonical specification for a Marko plugin. Do not inspect existing plugins in this project to infer structure — siblings may have drifted from spec. Copy the templates from assets/ verbatim, substitute placeholders, and stop.
A plugin is a class that intercepts the input or output of a public method on any
other class — without replacing that class. Plugins are Marko's fine-grained
extensibility primitive. They are auto-discovered from any module's src/
directory; no manual registration is needed.
When to use
Use a plugin when the user wants to modify arguments to, or the result from, a
public method on a class without rewriting or subclassing it. Common cases:
- Enrich a return value
- Validate or transform inputs before the method runs
- Short-circuit (cache hit, guard, redirect)
- Log or observe calls
- Chain transformations across modules
For total replacement of a class, use a Preference instead — that is a separate
skill (marko-create-preference). Plugins and Preferences are complementary; only
Preferences swap entire implementations.
Plugin model — two timings only
Marko supports exactly two plugin types. A third type that wraps the call is
intentionally absent — anything it could do is expressed as a Before
(short-circuit) or After (result transformation), keeping the call stack
debuggable.
| Attribute | When | What it does |
|---|
#[Before] | Before the target method | Modify args, short-circuit, or pass through |
#[After] | After the target method | Receive and modify the return value |
Before return semantics
| Return value | Effect |
|---|
null | Pass-through — original method runs with original args |
array | Replace arguments — original method runs with these args |
| Any other non-null value | Short-circuit — original method is NOT called |
After return semantics
After plugins receive $result (the return value of the original method or a
prior After plugin) as their first parameter, followed by the (possibly modified)
original arguments. Each After plugin's return value feeds the next After plugin
in sort order — always return the (possibly modified) result.
sortOrder
sortOrder is defined on the method-level attribute, not the class. Lower
numbers run first; negatives are valid; default is 0.
#[Before(sortOrder: -10)]
#[After(sortOrder: 100)]
Runbook
Step 1 — Identify the target
Determine which class and public method to intercept. Plugins cannot intercept
protected or private methods, and the target class must not be final.
Step 2 — Copy the plugin class template
Copy assets/PluginClass.php.tmpl verbatim. Substitute:
| Placeholder | Value |
|---|
{{Vendor}} | The host module's namespace vendor segment, StudlyCase (e.g. App, Acme) |
{{Name}} | The host module's name segment, StudlyCase (e.g., Blog) |
{{TargetClass}} | Unqualified class name (e.g., PostRepository) |
{{Vendor}}\{{Name}} is inherited from the module that hosts the plugin — read it, do not re-derive it. A plugin lives inside an existing module, so its namespace must match that module exactly. Read the host module's composer.json autoload.psr-4 key and use its prefix verbatim:
- App-local module (
app/{name}/) → namespace App\{Name} (the vendor segment is always the literal App, never the project directory name).
- Distributable module (
modules/{vendor}/{name}/) → namespace {Vendor}\{Name} from that module's psr-4.
- Framework monorepo package (
packages/{name}/, root contains packages/core/) → namespace Marko\{Name}.
Never use Marko as the vendor outside the framework monorepo. Do not infer the vendor from the project's root composer.json name: a skeleton-derived project still carries marko/skeleton there. The host module's own psr-4 mapping is the source of truth.
Place the file at src/Plugins/{{TargetClass}}Plugin.php inside the module.
Add the correct use statement for the fully-qualified target class.
Remove any #[Before] or #[After] methods that are not needed for this plugin —
the template shows both for illustration.
Step 3 — Implement the interceptor methods
Name each plugin method identically to the target method it intercepts (or use the
method: argument on the attribute to override when names would collide).
Apply the correct return semantics from the table above.
Step 4 — Copy the test template
Copy assets/PluginTest.php.tmpl verbatim. Substitute the same placeholders.
Adjust test cases to reflect the actual behavior being intercepted (pass-through,
argument modification, short-circuit, result enrichment).
Step 5 — Verify placement
- The plugin class lives anywhere under
src/ — src/Plugins/ is conventional.
- Do not register it in
module.php — discovery is automatic.
Step 6 — LSP and MCP verification gate
After writing files, expect LSP diagnostics from marko-lsp. Resolve all
diagnostics before declaring the plugin complete — diagnostics are the verification
gate. Then call the find_plugins_targeting MCP tool with the target class to
confirm the new plugin is discovered.
Constraints
- Targeted methods must be
public on the target class
- Target class must not be
final (Marko avoids final for this reason)
- Plugin classes should be
readonly when they have no mutable state
- Constructor property promotion always
declare(strict_types=1) always
- No magic methods
- No traits
What this skill does not cover
- Creating a new module to host the plugin — see
marko-create-module
- Replacing an entire class — that is a Preference, not a Plugin (use
marko-create-preference)
- Listening to events — that is
#[Observer], a different mechanism
See also