Weave Code
Weaver
Helps Laravel developers discover, compare, and choose open-source packages. See popularity, security, maintainers, and scores at a glance to make better decisions.
Feedback
Share your thoughts, report bugs, or suggest improvements.
Subject
Message
Plugin Design
daun/statamic-bard-mutators
cursor
plugin-design
commonmark
naming-conventions
hooks
memoization
coupling

Plugin classes follow Verb + Noun: Generate, Mark, Wrap, Insert, Normalize, Shift, LazyLoad, etc. Avoid Auto — every plugin is automatic by virtue of being a plugin, so the prefix carries no information.

Install
php artisan boost:add-skill daun/statamic-bard-mutators

Save this content to: .cursor/rules/plugin-design.md

# Plugin design conventions

## Common mistakes

- ❌ Naming a plugin `Auto*` → ✅ verb+noun (`Insert*`, `Mark*`, `Generate*`)
- ❌ Fluent setters or static factories → ✅ constructor named-args
- ❌ Walking the doc tree on every render call → ✅ memoize by `spl_object_id($info->root)`
- ❌ Duplicating config across plugins → ✅ set canonical form on the node tree, read it elsewhere

## Naming

Plugin classes follow `Verb + Noun`: `Generate*`, `Mark*`, `Wrap*`, `Insert*`, `Normalize*`, `Shift*`, `LazyLoad*`, etc. Avoid `Auto*` — every plugin is automatic by virtue of being a plugin, so the prefix carries no information.

When two plugins do related work, use the same noun and different verbs to signal a family with non-overlapping responsibilities. `ShiftHeadingLevels` (positioning) + `NormalizeHeadingLevels` (canonicalizing) is one example.

## Configuration

Constructor named-args only. No fluent setters or static factories — except where deliberately chosen by the maintainer (currently just `LazyLoadImages::usingLazysizes()`).

## Hook choice

The architecture section in `CLAUDE.md` covers the basic process-vs-render decision. Two further patterns emerged from this codebase:

**Document-wide state from a per-node hook**: when a render hook needs information computed across the whole document (e.g. "is this the first image", "what's the shallowest heading level"), walk `$info->root` once and memoize by `spl_object_id($info->root)`. See `ShiftHeadingLevels::resolveOffset()` and `NormalizeHeadingLevels::computeLevels()`.

**Process runs before render fully** — across the entire document tree, not interleaved per node. Consequence: a render-hook plugin cannot see attrs set by another render-hook plugin on a different node. A process-hook plugin running later in registration order *can* see what earlier process-hook plugins set on the same node.

## Coupling smell

If two plugins must agree on the same config value (a slug prefix, an id format, etc.) to interoperate, that's a smell. Don't duplicate the option across both APIs. Instead, surface the canonical form on the node tree by having the producing plugin set it in `process`, and have the consuming plugin read from there.

Example: `GenerateHeadingIds` sets `$item->attrs->id` in `process`. `InsertHeadingPermalinks` reads `$item->attrs->id` and skips headings without one. Neither plugin needs to know about the other's `prefix` config.

Plugin design conventions

Common mistakes

  • ❌ Naming a plugin Auto* → ✅ verb+noun (Insert*, Mark*, Generate*)
  • ❌ Fluent setters or static factories → ✅ constructor named-args
  • ❌ Walking the doc tree on every render call → ✅ memoize by spl_object_id($info->root)
  • ❌ Duplicating config across plugins → ✅ set canonical form on the node tree, read it elsewhere

Naming

Plugin classes follow Verb + Noun: Generate*, Mark*, Wrap*, Insert*, Normalize*, Shift*, LazyLoad*, etc. Avoid Auto* — every plugin is automatic by virtue of being a plugin, so the prefix carries no information.

When two plugins do related work, use the same noun and different verbs to signal a family with non-overlapping responsibilities. ShiftHeadingLevels (positioning) + NormalizeHeadingLevels (canonicalizing) is one example.

Configuration

Constructor named-args only. No fluent setters or static factories — except where deliberately chosen by the maintainer (currently just LazyLoadImages::usingLazysizes()).

Hook choice

The architecture section in CLAUDE.md covers the basic process-vs-render decision. Two further patterns emerged from this codebase:

Document-wide state from a per-node hook: when a render hook needs information computed across the whole document (e.g. "is this the first image", "what's the shallowest heading level"), walk $info->root once and memoize by spl_object_id($info->root). See ShiftHeadingLevels::resolveOffset() and NormalizeHeadingLevels::computeLevels().

Process runs before render fully — across the entire document tree, not interleaved per node. Consequence: a render-hook plugin cannot see attrs set by another render-hook plugin on a different node. A process-hook plugin running later in registration order can see what earlier process-hook plugins set on the same node.

Coupling smell

If two plugins must agree on the same config value (a slug prefix, an id format, etc.) to interoperate, that's a smell. Don't duplicate the option across both APIs. Instead, surface the canonical form on the node tree by having the producing plugin set it in process, and have the consuming plugin read from there.

Example: GenerateHeadingIds sets $item->attrs->id in process. InsertHeadingPermalinks reads $item->attrs->id and skips headings without one. Neither plugin needs to know about the other's prefix config.

Weaver

How can I help you explore Laravel packages today?

Conversation history is not saved when not logged in.
Prompt
Add packages to context
No packages found.
uni-method/symfony-lib
tacticmedia/scim-server-bundle
neika-ai/symfony-bundle
mattleads/agent-publishing-bundle
glavweb/framework-bundle
glavweb/extra-bundle
glavweb/api-bundle
glavweb/admin-bundle
adachsoft/api-version-bundle
elsayed85/laravel-api-exceptions
ouhssini/laravel-demo-mode
daworks/ncloud-outbound-mailer-driver
gp247/cms
ignibyte/aic-gates
nexa-merchant/blog
novasa/laravel-language-center
opencage/geocode
adolfoholzer/parameters
livenetworks/ln-starter
cgv/localized-resource