Plugin Generator
Plugin Generator Roadmap
The plugin-generator scaffolds Minnow-native plugins from YAML manifests produced by the plugin-analyzer or written manually.
Current State: 22 templates implemented, successfully generated working plugins for Gravity Forms, ACF Pro, and Gravity SMTP. Includes PostType-to-Entity, Taxonomy-to-Entity, Block, MetaBox, CRUD page, and Hook Listener support for comprehensive WordPress integration.
Last Updated: 2026-02-06
Implemented Templates
| Template | Generates | Status |
|---|---|---|
PluginTemplate |
src/Plugin.php - Main bootstrap class with lifecycle hooks |
✅ Complete |
EntityTemplate |
src/Entity/*.php - Entity classes with type mapping |
✅ Complete |
PostTypeEntityTemplate |
src/Entity/*.php - Entity classes from custom post types |
✅ Complete |
PostTypeTemplate |
src/PostTypeRegistrar.php - Post type registration |
✅ Complete |
TaxonomyEntityTemplate |
src/Entity/*.php - Entity classes from custom taxonomies |
✅ Complete |
TaxonomyRegistrarTemplate |
src/TaxonomyRegistrar.php - Taxonomy registration |
✅ Complete |
BlockTemplate |
src/Block/*.php - Gutenberg block classes |
✅ Complete |
BlockRegistrarTemplate |
src/BlockRegistrar.php - Block registration |
✅ Complete |
MetaBoxTemplate |
admin/components/MetaBox/*.vue - Meta box Vue components |
✅ Complete |
MetaBoxRegistrarTemplate |
src/MetaBoxRegistrar.php - Meta box registration |
✅ Complete |
ControllerTemplate |
src/*Controller.php - API controllers with smart pattern detection |
✅ Complete |
VuePageTemplate |
admin/pages/*.vue - Vue admin components |
✅ Complete |
CrudPageTemplate |
admin/pages/*List.vue, *Edit.vue - Schema-driven CRUD interfaces |
✅ Complete |
ShortcodeTemplate |
src/*Shortcode.php - Shortcode handlers |
✅ Complete |
FormRendererTemplate |
src/FormRenderer.php - Form HTML rendering |
✅ Complete |
MergeTagProcessorTemplate |
src/MergeTagProcessor.php - Template variable replacement |
✅ Complete |
NotificationProcessorTemplate |
src/NotificationProcessor.php - Email notifications |
✅ Complete |
CronTemplate |
src/CronManager.php - Scheduled task handlers |
✅ Complete |
MigrationTemplate |
migrations/*.php - Database migration files |
✅ Complete |
EmailTemplate |
src/Email/*.php + emails/*.html - Email classes and templates |
✅ Complete |
ApiClientTemplate |
src/ApiClient/*.php - HTTP API client classes |
✅ Complete |
HookListenerTemplate |
src/Hook/*.php - Hook listener classes (mail logging, etc.) |
✅ Complete |
Recent Improvements (2026-02-06)
Hook Listener Generation (NEW):
HookListenerTemplatemaps WordPress hooks to Minnow equivalents and generates listener classes- Automatic detection of mail-related hooks (
wp_mail,phpmailer_init,wp_mail_failed) from analyzer output - Generates
MailLoggerclass withonAfterSendandonSendFailedmethods using Minnow'sMessageandConnectionAPIs - Always emits fully-qualified class names in hook callbacks (e.g.,
GravitySMTP\Hook\MailLogger::onAfterSend) - Searches both
hooks.actionsandhooks.filtersfrom the analyzer manifest - Auto-discovers log/event table from manifest for the
logEvent()method
Analyzer Output Fixes:
Blueprint.phpnow emitsname/version/descriptionat YAML root level (not underplugin:wrapper)- Settings output is now a flat list with
groupkeys (not nested underoptions:) Option.phpnow includesgroupproperty (default:general) intoArray()output
Recent Improvements (2026-02-05)
Phase 1 Complete - 3 New Templates:
MigrationTemplate- Generates database migrations using Minnow Core's Schema/BlueprintEmailTemplate- Generates Mailable classes and HTML email templatesApiClientTemplate- Generates HTTP API clients with bearer, basic, and api_key auth support- Added
core/Mail/Mailable.phpbase class for plugin email classes - Generator.php updated with
extractEmails()andextractApiClients()methods
Recent Improvements (2026-02-04)
CRUD Page Template (LATEST):
- Generates complete CRUD interfaces from entity schemas instead of empty placeholders
- List page with sortable columns, search, pagination, bulk actions, row actions
- Edit page with appropriate input types derived from field types
- Smart field inference from names (email, url, code, description → appropriate inputs)
- Automatic column hiding for large text fields, secrets, and internal fields
- Read-only fields for timestamps and auto-generated values
- Delete confirmation, error handling, success messages
- Vue Router integration with proper route names
- REST API integration using configured API namespace
Analyzer/Generator Integration Fixes:
- Fixed syntax errors in generated code from 14 → 0 across all test plugins
- CustomPostTypeExtractor now skips dynamic post type names (e.g.,
{self::POSTTYPE}) - CronExtractor now skips dynamic hook names with unresolved class constants
- CronTemplate improved to handle hook names with
/and-characters - Generator now validates and sanitizes namespaces for invalid characters
- Added
isInvalidIdentifier()helper for filtering dynamic/invalid values - Baseline testing infrastructure tracks syntax error counts per plugin
Meta Box Templates (NEW):
- Generates Vue components for WordPress meta boxes with auto-save
- Supports multiple field types: text, number, textarea, checkbox, select, radio, image, date, color, etc.
- PHP MetaBoxRegistrar with
add_meta_box()registration - REST API meta registration with
register_post_meta() - Secure save handling with nonces and sanitization
- Debounced auto-save (1 second delay) for seamless editing
- Supports both direct
metaboxessection and nestedpost_types[*].meta_boxes - WordPress media library integration for image fields
Gutenberg Block Templates:
- Generates block classes with
register()andrender()methods - PHP render templates in
blocks/{block-name}/template.php - BlockRegistrar for centralized block registration
- Full attribute support with type inference and defaults
- WordPress
supportsconfiguration (align, color, etc.) - Translation-ready titles, descriptions, and keywords
- Fallback rendering if template is missing
Taxonomy-to-Entity Bridge (NEW):
- Automatically generates entity classes when manifest has
taxonomies - Generated entities extend
Minnow\Core\Entity\Term - Includes friendly accessors (
getName(),getSlug(),getDescription()) - Scoped queries filter by
taxonomyautomatically - Hierarchical support for categories (parent/children/ancestors/descendants)
- Object attachment methods (
attachToPost(),detachFromPost()) - Search, roots, and withItems convenience methods
TaxonomyRegistrar (NEW):
- Generates
register_taxonomy()calls with full WordPress args - Translation-ready labels (up to 19 label keys per taxonomy)
- Different labels for hierarchical (categories) vs flat (tags)
- Constants for taxonomy names
- Object type association
- Rewrite rules configuration
PostType-to-Entity Bridge:
- Automatically generates entity classes when manifest has
postTypesbut noentities - Generated entities extend
Minnow\Core\Entity\Post - Includes friendly accessors (
getTitle(),getContent(),getStatus()) - Scoped queries filter by
post_typeautomatically - Hierarchical support (parent/children) for hierarchical post types
- Status methods (
published(),drafts(),trashed())
PostTypeRegistrar:
- Generates
register_post_type()calls with full WordPress args - Translation-ready labels (23 label keys per post type)
- Constants for post type names
flushRewriteRules()helper for activation
ControllerTemplate Smart Pattern Detection:
- Automatically detects route patterns and generates appropriate implementations
/{entity}/{id}/meta→ GET/PUT methods for entity metadata/{entity}/{id}/full→ Combined entity + metadata response/{entity}/{id}/duplicate→ Copy entity and related metadata/{parent}/{id}/{children}→ Parent-child listing and creation
ACF Pro Test Results (2026-02-04)
What Is Generated (After Bridge Implementation)
| Output | Count | Notes |
|---|---|---|
Plugin.php |
1 | ✅ Valid PHP, correct namespace |
PostTypeRegistrar.php |
1 | ✅ 5 post types registered |
Entity/*.php |
5 | ✅ AcfField, AcfFieldGroup, AcfPostType, AcfTaxonomy, AcfUiOptionsPage |
CronManager.php |
1 | ✅ Cron hook captured |
style.css / script.js |
2 | ✅ Frontend asset stubs |
Total: 10 files generated (up from 3 before bridge)
Remaining Gaps
| Expected | Why Not Generated | Recommendation |
|---|---|---|
| Controllers | ACF uses register_rest_field() |
✅ Analyzer now detects rest_fields — generator template needed |
| Vue admin pages | ACF uses PHP-based admin | Add PHP admin page template |
Generated Entity Example
// Generated from postTypes in manifest
class AcfFieldGroup extends Post
{
public const POST_TYPE = 'acf-field-group';
public static function all(): Collection
{
return static::query()
->where('post_type', self::POST_TYPE)
->get();
}
public function getTitle(): string { ... }
public function setTitle(string $title): static { ... }
public function getChildren(): Collection { ... } // hierarchical support
}
Phase 1: Core Templates ✅ COMPLETE (8/8)
1.1 CronTemplate ✅ COMPLETE
CronTemplate has been implemented and generates CronManager.php from the cron.tasks YAML section.
YAML Input:
cron:
daily_cleanup:
schedule: daily
handler: Tasks\DailyCleanup::run
description: "Clean up expired entries"
hourly_sync:
schedule: "0 * * * *" # Cron expression
handler: Tasks\HourlySync::run
Generated Output: src/Tasks/DailyCleanup.php
<?php
namespace MyPlugin\Tasks;
use Minnow\Core\Plugin\Cron\Task;
class DailyCleanup extends Task
{
public static string $schedule = 'daily';
public static string $description = 'Clean up expired entries';
public static function run(): void
{
// TODO: Implement cleanup logic
// Suggested implementation based on entity analysis:
// Entry::where('expires_at', '<', date('Y-m-d'))->delete();
}
}
Files to create:
src/Template/CronTemplate.php
1.2 PostTypeTemplate
Generate custom post type classes from post_types YAML section.
YAML Input:
post_types:
product:
label: Product
plural: Products
icon: shopping-cart
public: true
has_archive: true
supports: [title, editor, thumbnail, excerpt]
fields:
- name: price
type: number
label: Price
- name: sku
type: text
label: SKU
Generated Output: src/PostType/Product.php
<?php
namespace MyPlugin\PostType;
use Minnow\Core\Entity\Post;
class Product extends Post
{
protected static string $postType = 'product';
public static function register(): void
{
// Register with Minnow post type system
\Minnow\Core\PostType\Registry::register('product', [
'label' => 'Product',
'plural' => 'Products',
'public' => true,
'has_archive' => true,
'supports' => ['title', 'editor', 'thumbnail', 'excerpt'],
]);
}
// Custom field accessors
public function getPrice(): float
{
return (float) $this->getMeta('_product_price');
}
public function setPrice(float $price): void
{
$this->setMeta('_product_price', $price);
}
public function getSku(): string
{
return (string) $this->getMeta('_product_sku');
}
public function setSku(string $sku): void
{
$this->setMeta('_product_sku', $sku);
}
}
Files to create:
src/Template/PostTypeTemplate.php
1.3 TaxonomyTemplate ✅ COMPLETE
TaxonomyTemplate and TaxonomyEntityTemplate have been implemented. Generates TaxonomyRegistrar.php with register_taxonomy() calls and entity classes that extend Minnow\Core\Entity\Term.
YAML Input:
taxonomies:
- name: product_category
label: Product Category
labels:
name: Product Categories
singular_name: Product Category
object_types:
- product
hierarchical: true
public: true
show_in_rest: true
Generated Output:
src/TaxonomyRegistrar.php- Registration class withregister()methodsrc/Entity/ProductCategory.php- Entity class extending Term
// TaxonomyRegistrar.php
class TaxonomyRegistrar
{
public const PRODUCT_CATEGORY = 'product_category';
public static function register(): void
{
register_taxonomy(
self::PRODUCT_CATEGORY,
'product',
self::getProductCategoryArgs()
);
}
}
// Entity/ProductCategory.php
class ProductCategory extends Term
{
public const TAXONOMY = 'product_category';
public static function all(array $options = []): Collection { ... }
public static function findBySlug(string $slug): ?static { ... }
public static function create(array $attributes = []): static { ... }
public static function roots(array $options = []): Collection { ... } // Top-level only
public function getName(): string { ... }
public function getParent(): ?static { ... } // Hierarchical
public function getChildren(): Collection { ... } // Hierarchical
public function attachToPost(int $postId): void { ... }
}
Files created:
src/Template/TaxonomyEntityTemplate.phpsrc/Template/TaxonomyRegistrarTemplate.php
1.4 MetaBoxTemplate ✅ COMPLETE
MetaBoxTemplate and MetaBoxRegistrarTemplate have been implemented. Generates Vue components for meta boxes and PHP registration class.
YAML Input:
post_types:
product:
meta_boxes:
pricing:
title: Pricing
context: side
priority: high
fields:
- name: price
type: number
label: Price
- name: sale_price
type: number
label: Sale Price
metaboxes:
- id: seo_settings
title: SEO Settings
screens: [post, page]
fields:
- name: meta_title
type: text
label: Meta Title
Generated Output:
src/MetaBoxRegistrar.php- PHP registration with add_meta_box() and register_post_meta()admin/components/MetaBox/Pricing.vue- Vue component with auto-save
Features:
- Multiple field types (text, number, textarea, checkbox, select, radio, image, date, etc.)
- Debounced auto-save via REST API
- Nonce verification and sanitization for secure saves
- WordPress media library integration for image fields
- Works with both analyzer-detected metaboxes and manually defined ones
Files created:
src/Template/MetaBoxTemplate.phpsrc/Template/MetaBoxRegistrarTemplate.php
1.5 BlockTemplate ✅ COMPLETE
BlockTemplate and BlockRegistrarTemplate have been implemented. Generates block classes with register() and render() methods, plus PHP render templates.
YAML Input:
blocks:
- name: my-plugin/hero-section
title: Hero Section
icon: cover-image
category: layout
attributes:
title: { type: string }
backgroundImage: { type: integer, default: 0 }
ctaText: { type: string, default: 'Learn More' }
supports:
align: [wide, full]
color: { background: true }
Generated Output:
src/BlockRegistrar.php- Centralized block registrationsrc/Block/HeroSection.php- Block class with register/renderblocks/hero-section/template.php- PHP render template
// BlockRegistrar.php
class BlockRegistrar
{
public static function register(): void
{
HeroSection::register();
}
}
// Block/HeroSection.php
class HeroSection
{
public const NAME = 'my-plugin/hero-section';
public static function register(): void
{
register_block_type(self::NAME, [
'title' => __('Hero Section', 'plugin-text-domain'),
'category' => 'layout',
'attributes' => [...],
'supports' => [...],
'render_callback' => [self::class, 'render'],
]);
}
public static function render(array $attributes, string $content, \WP_Block $block): string
{
$attributes = array_merge(self::getDefaults(), $attributes);
ob_start();
include dirname(__DIR__, 2) . '/blocks/hero-section/template.php';
return ob_get_clean();
}
}
Files created:
src/Template/BlockTemplate.phpsrc/Template/BlockRegistrarTemplate.php
1.6 EmailTemplate ✅ COMPLETE
Generate email template files and sender classes.
YAML Input:
emails:
welcome:
subject: "Welcome to {{ site_name }}!"
variables: [user_name, login_url]
order_confirmation:
subject: "Order #{{ order_id }} Confirmed"
variables: [order_id, items, total, shipping_address]
Generated Output: emails/welcome.html + src/Email/WelcomeEmail.php
<!DOCTYPE html>
<html>
<head>
<title>Welcome to {{ site_name }}!</title>
</head>
<body>
<h1>Welcome, {{ user_name }}!</h1>
<p>Thank you for joining us.</p>
<p><a href="{{ login_url }}">Log in to your account</a></p>
</body>
</html>
<?php
namespace MyPlugin\Email;
use Minnow\Core\Mail\Mailable;
class WelcomeEmail extends Mailable
{
public string $subject = 'Welcome to {{ site_name }}!';
public string $template = 'emails/welcome.html';
public function __construct(
public string $user_name,
public string $login_url
) {}
}
Files to create:
src/Template/EmailTemplate.php
1.7 MigrationTemplate ✅ COMPLETE
Generate database migration files for schema changes.
YAML Input:
entities:
Product:
table: products
properties:
id: { type: int, primary: true }
title: { type: string }
price: { type: decimal }
created_at: { type: datetime }
Generated Output: migrations/001_create_products_table.php
<?php
use Minnow\Core\Database\Schema;
use Minnow\Core\Database\Blueprint;
return new class {
public function up(): void
{
Schema::create('products', function (Blueprint $table) {
$table->id();
$table->string('title');
$table->decimal('price', 10, 2);
$table->timestamp('created_at')->nullable();
});
}
public function down(): void
{
Schema::dropIfExists('products');
}
};
Files to create:
src/Template/MigrationTemplate.php
1.8 ApiClientTemplate ✅ COMPLETE
Generate external API client classes using Minnow's HTTP client.
YAML Input:
api_clients:
github:
base_url: https://api.github.com
auth: bearer # or basic, api_key
endpoints:
- name: getUser
method: GET
path: /users/{username}
returns: User
- name: getRepos
method: GET
path: /users/{username}/repos
query: [per_page, page]
returns: Repository[]
- name: createIssue
method: POST
path: /repos/{owner}/{repo}/issues
body: [title, body, labels]
returns: Issue
Generated Output: src/ApiClient/GitHubClient.php
<?php
namespace MyPlugin\ApiClient;
use Minnow\Core\Http\Http;
use Minnow\Core\Http\Client;
use Minnow\Core\Http\Response;
class GitHubClient
{
private Client $client;
private string $baseUrl = 'https://api.github.com';
public function __construct(?string $token = null)
{
$this->client = Http::new()
->acceptJson()
->withHeader('User-Agent', 'MyPlugin/1.0');
if ($token) {
$this->client->withToken($token);
}
}
public function getUser(string $username): array
{
return $this->client
->get("{$this->baseUrl}/users/{$username}")
->throw()
->json();
}
public function getRepos(string $username, int $perPage = 30, int $page = 1): array
{
return $this->client
->get("{$this->baseUrl}/users/{$username}/repos", [
'per_page' => $perPage,
'page' => $page,
])
->throw()
->json();
}
public function createIssue(string $owner, string $repo, array $data): array
{
return $this->client
->asJson()
->post("{$this->baseUrl}/repos/{$owner}/{$repo}/issues", $data)
->throw()
->json();
}
}
CLI Testing:
# Test generated API clients using the HTTP CLI
minnow http GET https://api.github.com/users/octocat --json
minnow http POST https://api.example.com/endpoint -d '{"key":"value"}' -H "Authorization: Bearer token"
Files to create:
src/Template/ApiClientTemplate.php
Template Enhancements ⬜ FUTURE
Possible extensions for the Phase 1 templates. These are ideas for future improvement, not planned work.
Migration Enhancements
| Feature | Description |
|---|---|
| Index support | $table->index('column'), $table->unique('email') |
| Foreign key constraints | $table->foreign('user_id')->references('id')->on('users') |
| Composite primary keys | $table->primary(['tenant_id', 'user_id']) |
| Column modifiers | ->after('column'), ->first(), ->unsigned() |
| Table options | Engine selection, charset, collation |
| Seed data | Generate seeders alongside migrations |
Email Enhancements
| Feature | Description |
|---|---|
| Layout templates | Master layout with {{ content }} slot for consistent branding |
| Plain text fallback | Auto-generate .txt version from HTML |
| Pre-built components | Header, footer, button, card components |
| Inline CSS | CSS inliner for email client compatibility |
| Preview mode | Generate preview HTML for testing |
| Attachment support | attachments: [invoice.pdf] in YAML |
API Client Enhancements
| Feature | Description |
|---|---|
| Retry logic | retry: { attempts: 3, delay: 1000, multiplier: 2 } |
| Rate limiting | Automatic throttling with rateLimit: { requests: 100, per: 60 } |
| Response caching | cache: { ttl: 300 } per endpoint |
| Pagination helpers | paginate() method for cursor/offset pagination |
| Error mapping | Map API error codes to custom exceptions |
| Response DTOs | Generate typed response classes from returns schema |
| Webhook handlers | Generate webhook receiver classes |
Phase 2: Enhanced Generation ⬜ PENDING
2.1 Test Scaffolding
Generate PHPUnit test stubs alongside generated code.
Generated Output: tests/Entity/ProductTest.php
<?php
namespace MyPlugin\Tests\Entity;
use PHPUnit\Framework\TestCase;
use MyPlugin\Entity\Product;
class ProductTest extends TestCase
{
public function test_can_create_product(): void
{
$product = Product::create([
'title' => 'Test Product',
'price' => 19.99,
]);
$this->assertNotNull($product->id);
$this->assertEquals('Test Product', $product->title);
}
public function test_can_find_product(): void
{
// TODO: Implement
$this->markTestIncomplete();
}
}
Files to create:
src/Template/TestTemplate.php
2.2 API Documentation
Generate OpenAPI/Swagger documentation from routes.
Generated Output: docs/api.yaml
openapi: 3.0.0
info:
title: My Plugin API
version: 1.0.0
paths:
/products:
get:
summary: List all products
responses:
200:
description: Success
content:
application/json:
schema:
type: array
items:
$ref: '#/components/schemas/Product'
components:
schemas:
Product:
type: object
properties:
id:
type: integer
title:
type: string
price:
type: number
Files to create:
src/Template/ApiDocTemplate.php
2.3 Admin Route Registration
Auto-generate Vue Router configuration from admin pages.
Generated Output: admin/routes.js
export default [
{
path: '/products',
component: () => import('./pages/ProductsList.vue'),
meta: { title: 'Products', icon: 'shopping-cart' }
},
{
path: '/products/:id',
component: () => import('./pages/ProductEditor.vue'),
meta: { title: 'Edit Product' }
},
{
path: '/settings',
component: () => import('./pages/Settings.vue'),
meta: { title: 'Settings', icon: 'cog' }
}
]
Files to create:
src/Template/RouteConfigTemplate.php
Phase 3: Improved Code Quality ⬜ PENDING
3.1 Code Style Configuration
Generate code style config files.
Generated Output:
.php-cs-fixer.php- PHP CS Fixer config.editorconfig- Editor settingsphpstan.neon- Static analysis config
3.2 Type Inference
Better type inference from YAML to PHP.
Improvements:
- Infer nullable types from
nullable: true - Generate union types for polymorphic fields
- Add PHPDoc blocks with
@paramand@return - Generate strict type declarations
Before:
public function getPrice()
{
return $this->getAttribute('price');
}
After:
/**
* Get the product price.
*
* @return float|null
*/
public function getPrice(): ?float
{
return $this->getAttribute('price');
}
3.3 Intelligent Defaults
Generate sensible defaults based on field names.
Auto-detected patterns:
*_at,*_date→datetimetype, nullable*_id→inttype, foreign key relationshipis_*,has_*→booltype*_count,*_total→inttype*_url,*_link→stringtype with URL validation*_email→stringtype with email validation
Phase 4: CLI Improvements ⬜ PENDING
4.1 Interactive Mode
Prompt for missing information during generation.
$ minnow generate --interactive
? Plugin namespace: MyPlugin
? Generate tests? (Y/n) Y
? Include API documentation? (Y/n) Y
? Admin UI framework: (Vue/React/None) Vue
Generating plugin...
✓ Created src/Plugin.php
✓ Created src/Entity/Product.php
✓ Created tests/Entity/ProductTest.php
...
4.2 Selective Generation
Generate only specific component types.
# Only generate entities
minnow generate manifest.yaml --only=entities
# Generate everything except tests
minnow generate manifest.yaml --except=tests
# Regenerate a single file
minnow generate manifest.yaml --file=src/Entity/Product.php
4.3 Diff Mode
Show what would change before writing files.
$ minnow generate manifest.yaml --diff
src/Entity/Product.php:
+ public function getSku(): string
+ {
+ return (string) $this->getMeta('_sku');
+ }
Apply changes? (Y/n)
Summary
| Phase | Templates/Features | Status |
|---|---|---|
| Phase 1 | 8 core templates | ✅ Complete |
| Phase 2 | 3 enhanced features | ⬜ Pending |
| Phase 3 | 3 quality improvements | ⬜ Pending |
| Phase 4 | 3 CLI features | ⬜ Pending |
Implemented templates: 22 (PluginTemplate, EntityTemplate, PostTypeEntityTemplate, PostTypeTemplate, TaxonomyEntityTemplate, TaxonomyRegistrarTemplate, BlockTemplate, BlockRegistrarTemplate, MetaBoxTemplate, MetaBoxRegistrarTemplate, ControllerTemplate, VuePageTemplate, CrudPageTemplate, ShortcodeTemplate, FormRendererTemplate, MergeTagProcessorTemplate, NotificationProcessorTemplate, CronTemplate, MigrationTemplate, EmailTemplate, ApiClientTemplate, HookListenerTemplate) Estimated new code: ~2,000 lines
Lessons Learned (from Gravity Forms Implementation)
Issues discovered during manual testing of generated Gravity Forms plugin that should inform generator improvements:
Issue 1: Controllers Use WordPress $wpdb Instead of Minnow's Database API ✅ RESOLVED
Status: Not an issue - ControllerTemplate already uses Entity-based API correctly.
Problem: Generated controllers used WordPress's global $wpdb object for raw queries:
global $wpdb;
$table = $wpdb->prefix . 'gf_entry';
$result = $wpdb->get_var($wpdb->prepare("SELECT COUNT(*) FROM {$table}..."));
Impact: 500 errors - $wpdb is null in Minnow context.
Fix Required: ControllerTemplate should generate code using Minnow\Core\Database\Connection:
use Minnow\Core\Database\Connection;
$db = Connection::getInstance();
$table = $db->table('gf_entry');
$result = $db->fetchColumn("SELECT COUNT(*) FROM {$table} WHERE form_id = ?", [$formId]);
| Method Mappings: | WordPress ($wpdb) |
Minnow (Connection) |
|---|---|---|
$wpdb->prefix . 'table' |
$db->table('table') |
|
$wpdb->get_var() |
$db->fetchColumn() |
|
$wpdb->get_row(..., ARRAY_A) |
$db->fetchOne() |
|
$wpdb->get_results(..., ARRAY_A) |
$db->fetchAll() |
|
$wpdb->insert() |
$db->insert() |
|
$wpdb->update() |
$db->update() |
|
$wpdb->delete() |
$db->delete() |
|
$wpdb->prepare() |
Use ? placeholders with params array |
Issue 2: Entities Missing Table Prefix Flag ✅ FIXED (Updated 2026-02-05)
Problem: Generated entities didn't include $usePrefix = true:
class Form extends BaseEntity
{
protected static string $table = 'gf_form';
protected static string $primaryKey = 'id';
// Missing: protected static bool $usePrefix = true;
}
Impact: Queries executed against gf_form instead of minnow_gf_form, causing "column not found" errors.
Fix (initial): EntityTemplate hardcoded $usePrefix = true for all entities.
Fix (2026-02-05): EntityTemplate now reads uses_prefix from entity config (set by analyzer's SchemaExtractor which detects $wpdb->prefix usage). Defaults to true for backward compatibility. The analyzer tracks prefix usage via:
$wpdb->prefix . 'table_name'variable assignments$wpdb->prefix . self::TABLE_NAMEclass constant concatenations{$wpdb->prefix}patterns in CREATE TABLE SQL
Issue 3: API Response Format Inconsistency ✅ FIXED
Problem: Generated controllers return { items: [...], total: N } but the generic PluginListPage component expected { data: [...] }.
Impact: List pages showed empty even though API returned data correctly.
Fix Required: Either:
- Standardize response format in generator (use
datato match existing convention) - Or document the expected format clearly so frontend components can be updated
Recommendation: Use consistent response format across all generated controllers:
return Response::json([
'data' => $items, // Use 'data' for consistency with frontend
'total' => $total,
'page' => $page,
'per_page' => $perPage,
]);
Lessons Learned (from Gravity SMTP Implementation, 2026-02-06)
Issues discovered during manual conversion and testing of the Gravity SMTP plugin.
Issue 4: Generated Controllers Use Wrong Database Method Signatures ✅ MITIGATED (2026-02-06)
Problem: Generated controllers called $db->fetchOne($db->raw(...)) — nesting raw() inside fetchOne(). The fetchOne() method accepts (string $sql, array $params) directly; passing a PDOStatement causes a type error.
Impact: All custom controller endpoints returned 500 errors.
Mitigation: Added explicit correct/incorrect API usage guide to the conversion skill (SKILL.md Phase 5) with clear examples. The ControllerTemplate itself already generates correct code — this issue arose from manual editing. The SKILL.md now includes guardrails to prevent this pattern.
Fix needed in ControllerTemplate: Ensure generated code uses the correct method signatures:
// WRONG (generated)
$row = $db->fetchOne($db->raw("SELECT * FROM {$table} WHERE id = ?", [$id]));
// CORRECT
$row = $db->fetchOne("SELECT * FROM {$table} WHERE id = ?", [$id]);
Also ensure results are accessed as arrays, not objects:
// WRONG (generated)
$row->column_name
// CORRECT
$row['column_name']
| Method reference: | Method | Signature | Returns |
|---|---|---|---|
$db->raw($sql, $params) |
(string, array): PDOStatement |
For INSERT/UPDATE/DELETE | |
$db->fetchOne($sql, $params) |
(string, array): ?array |
Single row as associative array | |
$db->fetchAll($sql, $params) |
(string, array): array |
Multiple rows as arrays |
Issue 5: Generated Controllers Use Wrong Request API ✅ MITIGATED (2026-02-06)
Problem: Generated controllers used $request->get('param') which doesn't exist on Minnow's Request class. The method was likely carried over from the WordPress WP_REST_Request::get_param() pattern.
Impact: Fatal error: Call to undefined method Request::get().
Mitigation: Added explicit correct/incorrect Request API guide to the conversion skill (SKILL.md Phase 5). The ControllerTemplate already generates correct code — this issue arose from manual editing.
| Fix needed in ControllerTemplate: Use the correct Minnow Request methods: | Purpose | Wrong (generated) | Correct |
|---|---|---|---|
Route parameter {id} |
$request->get('id') |
$request->param('id') |
|
| Body/query input | $request->get('field') |
$request->input('field', $default) |
|
| Query string only | $request->get('page') |
$request->query('page', $default) |
|
| All body+query | — | $request->all() |
Issue 6: Generated YAML Uses plugin: Wrapper Format ✅ FIXED (2026-02-06)
Problem: The analyzer's Blueprint::toArray() wrapped name and version under a plugin: key, but PluginManifest::validate() requires these at the YAML root level.
Impact: Plugin silently fails to load — routes, hooks, entities, and admin pages are all unregistered.
Fix: Changed Blueprint::toArray() to emit name, version, description, implementation at root level (removed the plugin: wrapper). The generator's parseManifest() still supports both formats for backward compatibility.
Files modified: tools/plugin-analyzer/src/Blueprint/Blueprint.php
Emit flat YAML structure:
# WRONG (generated)
plugin:
name: 'My Plugin'
version: 1.0.0
# CORRECT
name: 'My Plugin'
namespace: MyPlugin
slug: my-plugin
version: 1.0.0
Issue 7: Generated Admin Menu Uses children: Instead of items: ✅ FIXED (2026-02-06)
Problem: Conversion skill (SKILL.md) used children: for submenu entries, but MenuRegistry reads the items: key.
Impact: Sidebar menu renders with no submenu items.
Fix: Replaced all 5 occurrences of children: with items: in .claude/skills/minnow-convert/SKILL.md.
Correct format:
# WRONG (generated)
admin:
menu:
- title: My Plugin
children:
- title: Dashboard
slug: my-plugin
# CORRECT
admin:
menu:
- title: My Plugin
items:
- title: Dashboard
slug: my-plugin
Issue 8: Generated Settings Use Nested options: Format ✅ FIXED (2026-02-06)
Problem: Analyzer's Blueprint::toArray() nested settings under an options: key, but SettingsManager expects a flat list under settings: with group: keys.
Impact: str_replace() error during settings registration — framework iterates the flat list but receives a nested object.
Fix: Changed Blueprint::toArray() to emit flat settings list. Added group property (default: 'general') to Option.php with inclusion in toArray().
Files modified: tools/plugin-analyzer/src/Blueprint/Blueprint.php, tools/plugin-analyzer/src/Blueprint/Option.php
Correct format:
# WRONG (generated)
settings:
options:
- key: my_setting
type: bool
# CORRECT
settings:
- key: my_setting
type: bool
group: general
default: false
Issue 9: Custom Admin Page Types Fall Through to PluginListPage ✅ FIXED (2026-02-06)
Problem: Generator emits type: custom for pages like dashboards and tools, but the admin app's pageComponent() method has no handler for type: custom. These pages fall through to the default PluginListPage component, which crashes when it tries to load entity data from an undefined API endpoint.
Impact: Dashboard and Tools pages show a stuck spinner with Vue errors. The browser console shows GET /api/gravitysmtp/v1/undefined 404.
Fix: Updated SKILL.md to explicitly list supported page types (list, editor, settings, placeholder) and note that type: custom is NOT supported at runtime. Rewrote Phase 6 to use type: placeholder with descriptive messages for unsupported page types.
Correct format:
- slug: my-plugin-dashboard
type: placeholder
message: 'Dashboard with statistics and charts. Custom components planned for a future release.'
Alternatively, implement runtime loading of custom Vue components in the admin app so type: custom works.
Issue 10: Hardcoded Vue Components Bypass Dynamic Plugin Page System ✅ FIXED (2026-02-06)
Problem: The generator created hardcoded Vue components (e.g., GravitySMTPDashboard, GravitySMTPSettings, GravitySMTPEmailLog) with hardcoded API URLs and registered them as static routes in the Vue Router. These bypassed the dynamic PluginPageView routing system and used wrong API endpoint patterns (/api/gravity-smtp/* instead of the correct namespaced URLs).
Impact: Settings page returned 404 because GravitySMTPSettings fetched /api/gravity-smtp/settings which doesn't exist. The generic PluginSettingsPage component (which correctly uses /api/minnow/v1/plugins/{id}/settings) was never reached.
Fix: Rewrote Phase 6 of SKILL.md to explicitly forbid hardcoded Vue components. All plugin pages should be driven by the YAML configuration and rendered through the dynamic PluginPageView component. Unsupported page types use type: placeholder with descriptive messages.
Issue 11: Hook Listener Classes Not Generated ✅ FIXED (2026-02-06)
Problem: When a WordPress plugin intercepts core hooks (like wp_mail, phpmailer_init), the generator didn't create equivalent Minnow hook listener classes or wire them into the hooks: section of plugin.yaml.
Impact: The email logging feature — a core function of Gravity SMTP — was completely missing from the generated plugin. Had to manually create Hook/MailLogger.php and add the hooks config.
Fix: Created HookListenerTemplate that:
- ✅ Reads the analyzer's extracted WordPress hooks (both actions and filters)
- ✅ Maps them to Minnow equivalents (
wp_mail→minnow.mail.after_send, etc.) - ✅ Generates listener classes in
src/Hook/with proper method signatures - ✅ Uses fully-qualified callback names (e.g.,
GravitySMTP\Hook\MailLogger::onAfterSend)
Files created: tools/plugin-generator/src/Template/HookListenerTemplate.php
Files modified: tools/plugin-generator/src/Generator.php (added extractHookListeners(), import, property, generation block)
Issue 12: Hook Callbacks Must Use Fully-Qualified Class Names ✅ FIXED (2026-02-06)
Problem: The PluginLoader::resolveCallback() method checks str_contains($class, '\\') to decide whether to prepend the plugin namespace. Callbacks with sub-namespaces like Hook\MailLogger::onAfterSend contain a backslash, so the namespace is NOT prepended — the class resolves to Hook\MailLogger instead of GravitySMTP\Hook\MailLogger.
Impact: Hooks silently fail to register because class_exists('Hook\MailLogger') returns false.
Fix: HookListenerTemplate always emits fully-qualified class names by prepending the plugin namespace from the manifest. Example output: GravitySMTP\Hook\MailLogger::onAfterSend.
Correct format:
# WRONG
hooks:
actions:
- hook: minnow.mail.after_send
callback: 'Hook\MailLogger::onAfterSend'
# CORRECT
hooks:
actions:
- hook: minnow.mail.after_send
callback: 'GravitySMTP\Hook\MailLogger::onAfterSend'
Summary of Generator Fixes (All Resolved 2026-02-06)
| # | Issue | Severity | Resolution |
|---|---|---|---|
| 4 | Wrong DB method signatures | High | ✅ SKILL.md guardrails added |
| 5 | Wrong Request API (get() → param()/input()) |
High | ✅ SKILL.md guardrails added |
| 6 | plugin: wrapper format |
High | ✅ Blueprint.php fixed |
| 7 | children: → items: in menus |
Medium | ✅ SKILL.md fixed (5 locations) |
| 8 | Nested options: in settings |
Medium | ✅ Blueprint.php + Option.php fixed |
| 9 | type: custom unsupported |
Medium | ✅ SKILL.md page type constraints added |
| 10 | Hardcoded Vue components | High | ✅ SKILL.md Phase 6 rewritten |
| 11 | No hook listener generation | High | ✅ HookListenerTemplate created |
| 12 | Hook callbacks need FQN | Medium | ✅ HookListenerTemplate uses FQN |