Files
APAW/.kilo/skills/php-wordpress-patterns/SKILL.md
¨NW¨ b46a1a20a8 feat: add PHP development stack, atomic tasks, modular code rules, agent monitoring, fix target project detection
7 evolutionary tasks implemented:

1. PHP web development: php-developer agent + 6 skills (Laravel, Symfony, WordPress, security, testing, modular architecture) + 2 pipeline commands (/laravel, /wordpress)

2. Atomic task decomposition: 1 action = 1 task rule, task sizing guide, decomposition protocol for orchestrator, token budgets per complexity

3. Modular code rules: max 100 lines/file, max 30 lines/function, service/repository patterns, cross-module communication via events only

4. Gitea-centric workflow: mandatory issue creation before work, research with links, progress checkboxes, screenshots on test, git history as knowledge base

5. Fix: target project auto-detection — removed all hardcoded UniqueSoft/APAW from API calls, added get_target_repo() via git remote, GITEA_TARGET_REPO env override

6. Agent execution monitoring: agent-executions.jsonl logging, agent-stats.ts statistics script, required fields per invocation, Gitea comment includes duration/tokens

7. Token optimization: 1 action = 1 task principle, token budgets by task type, routing matrix, no scope creep, skip unnecessary pipeline steps
2026-04-18 23:43:04 +01:00

7.7 KiB

name, description
name description
php-wordpress-patterns WordPress development patterns - plugins, themes, REST API, hooks, Custom Post Types, Gutenberg blocks

PHP WordPress Patterns

Plugin Structure

my-plugin/
├── my-plugin.php              # Main plugin file
├── composer.json               # Dependencies
├── package.json                # Build tools
├── includes/
│   ├── Admin/                  # Admin settings pages
│   ├── Frontend/               # Frontend rendering
│   ├── REST/                   # REST API controllers
│   ├── PostTypes/              # Custom Post Types
│   ├── Taxonomies/             # Custom Taxonomies
│   ├── Shortcodes/             # Shortcode handlers
│   ├── Widgets/                # Widget classes
│   └── Utils/                  # Helper functions
├── src/
│   ├── blocks/                 # Gutenberg blocks (JSX)
│   └── store/                  # Block data stores
├── assets/
│   ├── css/
│   ├── js/
│   └── images/
├── templates/                  # Template files
├── languages/                  # i18n
└── tests/
    ├── Unit/
    └── Integration/

Main Plugin File

<?php
/**
 * Plugin Name: My Plugin
 * Description: Custom functionality
 * Version: 1.0.0
 * Author: Developer
 * Text Domain: my-plugin
 * Requires at least: 6.0
 * Requires PHP: 8.1
 */

declare(strict_types=1);

namespace MyPlugin;

if (!defined('ABSPATH')) exit;

require_once __DIR__ . '/vendor/autoload.php';

final class Plugin
{
    private static ?self $instance = null;

    public static function init(): self
    {
        return self::$instance ??= new self();
    }

    private function __construct()
    {
        add_action('init', [$this, 'registerPostTypes']);
        add_action('rest_api_init', [$this, 'registerRestRoutes']);
        add_action('admin_menu', [$this, 'addAdminMenu']);
        add_action('wp_enqueue_scripts', [$this, 'enqueueAssets']);
        
        register_activation_hook(__FILE__, [$this, 'activate']);
        register_deactivation_hook(__FILE__, [$this, 'deactivate']);
    }

    public function activate(): void
    {
        $db = new Database\Seeder();
        $db->createTables();
        flush_rewrite_rules();
    }

    public function deactivate(): void
    {
        flush_rewrite_rules();
    }
}

Plugin::init();

Custom Post Type

// includes/PostTypes/Product.php
namespace MyPlugin\PostTypes;

class Product
{
    public static function register(): void
    {
        register_post_type('product', [
            'labels' => [
                'name' => __('Products', 'my-plugin'),
                'singular_name' => __('Product', 'my-plugin'),
            ],
            'public' => true,
            'has_archive' => true,
            'rewrite' => ['slug' => 'products'],
            'supports' => ['title', 'editor', 'thumbnail', 'excerpt', 'custom-fields'],
            'show_in_rest' => true, // Enable REST API
            'menu_icon' => 'dashicons-cart',
        ]);

        // Custom fields via meta
        register_post_meta('product', 'price', [
            'show_in_rest' => true,
            'single' => true,
            'type' => 'number',
            'sanitize_callback' => 'absint',
        ]);
    }
}

add_action('init', [Product::class, 'register']);

REST API Controller

// includes/REST/ProductController.php
namespace MyPlugin\REST;

use WP_REST_Controller;
use WP_REST_Request;
use WP_REST_Response;
use WP_Error;

class ProductController extends WP_REST_Controller
{
    public function __construct()
    {
        $this->namespace = 'my-plugin/v1';
        $this->rest_base = 'products';
    }

    public function registerRoutes(): void
    {
        register_rest_route($this->namespace, '/' . $this->rest_base, [
            [
                'methods' => 'GET',
                'callback' => [$this, 'getItems'],
                'permission_callback' => '__return_true',
                'args' => [
                    'page' => ['default' => 1, 'sanitize_callback' => 'absint'],
                    'per_page' => ['default' => 20, 'sanitize_callback' => 'absint'],
                    'category' => ['sanitize_callback' => 'absint'],
                ],
            ],
            [
                'methods' => 'POST',
                'callback' => [$this, 'createItem'],
                'permission_callback' => function() {
                    return current_user_can('edit_posts');
                },
            ],
        ]);

        register_rest_route($this->namespace, '/' . $this->rest_base . '/(?P<id>\d+)', [
            [
                'methods' => 'GET',
                'callback' => [$this, 'getItem'],
                'permission_callback' => '__return_true',
            ],
            [
                'methods' => 'PUT',
                'callback' => [$this, 'updateItem'],
                'permission_callback' => function() {
                    return current_user_can('edit_posts');
                },
            ],
            [
                'methods' => 'DELETE',
                'callback' => [$this, 'deleteItem'],
                'permission_callback' => function() {
                    return current_user_can('delete_posts');
                },
            ],
        ]);
    }

    public function getItems(WP_REST_Request $request): WP_REST_Response
    {
        $args = [
            'post_type' => 'product',
            'posts_per_page' => $request['per_page'],
            'paged' => $request['page'],
            'post_status' => 'publish',
        ];

        if ($request['category']) {
            $args['tax_query'] = [[
                'taxonomy' => 'product_category',
                'field' => 'term_id',
                'terms' => $request['category'],
            ]];
        }

        $query = new \WP_Query($args);
        $products = array_map(fn($p) => $this->prepareItem($p), $query->posts);

        return new WP_REST_Response([
            'data' => $products,
            'total' => (int) $query->found_posts,
            'pages' => (int) $query->max_num_pages,
        ], 200);
    }

    private function prepareItem(\WP_Post $post): array
    {
        return [
            'id' => $post->ID,
            'title' => $post->post_title,
            'content' => $post->post_content,
            'price' => get_post_meta($post->ID, 'price', true),
            'thumbnail' => get_the_post_thumbnail_url($post->ID, 'medium'),
            'date' => $post->post_date,
        ];
    }
}

Security Essentials

// Nonce verification for forms
if (!wp_verify_nonce($_POST['_wpnonce'], 'my_plugin_action')) {
    wp_die('Security check failed');
}

// Data sanitization
$title = sanitize_text_field($_POST['title']);
$content = wp_kses_post($_POST['content']);
$price = floatval($_POST['price']);
$id = absint($_POST['id']);

// SQL queries - always use $wpdb->prepare()
$results = $wpdb->get_results($wpdb->prepare(
    "SELECT * FROM {$wpdb->prefix}products WHERE category_id = %d AND status = %s",
    $category_id,
    'active'
));

// Escape output
echo esc_html($title);
echo esc_url($url);
echo esc_attr($attr);

Checklist

  • Namespace all PHP code (no global functions)
  • Use declare(strict_types=1) in all files
  • Nonce verification on all forms and AJAX
  • Sanitize input, escape output, prepare SQL
  • Custom Post Types with show_in_rest
  • REST API controllers extend WP_REST_Controller
  • Capability checks (current_user_can)
  • Use wp_enqueue_script/wp_enqueue_style
  • Internationalization (__() and _e())
  • Activation/deactivation hooks
  • Options API for settings (never custom tables for settings)