Minnow

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):

  • HookListenerTemplate maps 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 MailLogger class with onAfterSend and onSendFailed methods using Minnow's Message and Connection APIs
  • Always emits fully-qualified class names in hook callbacks (e.g., GravitySMTP\Hook\MailLogger::onAfterSend)
  • Searches both hooks.actions and hooks.filters from the analyzer manifest
  • Auto-discovers log/event table from manifest for the logEvent() method

Analyzer Output Fixes:

  • Blueprint.php now emits name/version/description at YAML root level (not under plugin: wrapper)
  • Settings output is now a flat list with group keys (not nested under options:)
  • Option.php now includes group property (default: general) in toArray() output

Recent Improvements (2026-02-05)

Phase 1 Complete - 3 New Templates:

  • MigrationTemplate - Generates database migrations using Minnow Core's Schema/Blueprint
  • EmailTemplate - Generates Mailable classes and HTML email templates
  • ApiClientTemplate - Generates HTTP API clients with bearer, basic, and api_key auth support
  • Added core/Mail/Mailable.php base class for plugin email classes
  • Generator.php updated with extractEmails() and extractApiClients() 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 metaboxes section and nested post_types[*].meta_boxes
  • WordPress media library integration for image fields

Gutenberg Block Templates:

  • Generates block classes with register() and render() methods
  • PHP render templates in blocks/{block-name}/template.php
  • BlockRegistrar for centralized block registration
  • Full attribute support with type inference and defaults
  • WordPress supports configuration (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 taxonomy automatically
  • 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 postTypes but no entities
  • Generated entities extend Minnow\Core\Entity\Post
  • Includes friendly accessors (getTitle(), getContent(), getStatus())
  • Scoped queries filter by post_type automatically
  • 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 with register() method
  • src/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.php
  • src/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.php
  • src/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 registration
  • src/Block/HeroSection.php - Block class with register/render
  • blocks/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.php
  • src/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 settings
  • phpstan.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 @param and @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, *_datedatetime type, nullable
  • *_idint type, foreign key relationship
  • is_*, has_*bool type
  • *_count, *_totalint type
  • *_url, *_linkstring type with URL validation
  • *_emailstring type 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_NAME class 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:

  1. Standardize response format in generator (use data to match existing convention)
  2. 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:

  1. ✅ Reads the analyzer's extracted WordPress hooks (both actions and filters)
  2. ✅ Maps them to Minnow equivalents (wp_mailminnow.mail.after_send, etc.)
  3. ✅ Generates listener classes in src/Hook/ with proper method signatures
  4. ✅ 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