- admin.html: removed conflicting inline script, added api.js + admin.js - admin.js: dynamic section loader with fetch, init navigates to hash - api.js: credentials: 'include' for all admin requests - propertyModal: added name attributes to all form fields, saveProperty onclick handler - server/index.ts: added POST /api/analytics/event with daily aggregation - server/validation.ts: removed min(6) from password for 401 on invalid credentials - capability-index.yaml: added 11 MCP capability routes - docker-compose-mcp.yml: created for MCP servers
7.7 KiB
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)