--- 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`. ```php #[Before(sortOrder: -10)] // runs before plugins at default 0 #[After(sortOrder: 100)] // runs after lower-priority Afters ``` ## 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 - [Marko docs: plugins](https://marko.build/docs/concepts/plugins/) - [Marko docs: preferences](https://marko.build/docs/concepts/preferences/) - [`marko/core` README](https://github.com/markshust/marko/tree/develop/packages/core)