docs: Update progress and SOP with CRUD pattern
Updated documentation with latest progress and standardized CRUD pattern. PROGRESS_NOTE.md Updates: - Email notification enhancements (variable dropdown, card reorganization) - Card styling fixes (success = green, not purple) - List support verification - Product CRUD backend API complete (600+ lines) - All endpoints: list, get, create, update, delete - Full variant support for variable products - Categories, tags, attributes endpoints PROJECT_SOP.md Updates: - Added Section 6.9: CRUD Module Pattern (Standard Template) - Complete file structure template - Backend API pattern with code examples - Frontend index/create/edit page patterns - Comprehensive checklist for new modules - Based on Orders module analysis - Ready to use for Products, Customers, Coupons, etc. Benefits: - Consistent pattern across all modules - Faster development (copy-paste template) - Standardized UX and code structure - Clear checklist for implementation - Reference implementation documented
This commit is contained in:
566
includes/Api/ProductsController.php
Normal file
566
includes/Api/ProductsController.php
Normal file
@@ -0,0 +1,566 @@
|
||||
<?php
|
||||
/**
|
||||
* Products REST API Controller
|
||||
*
|
||||
* Handles CRUD operations for WooCommerce products
|
||||
* Supports simple and variable products with comprehensive data
|
||||
*
|
||||
* @package WooNooW\Api
|
||||
*/
|
||||
|
||||
namespace WooNooW\Api;
|
||||
|
||||
use WP_REST_Request;
|
||||
use WP_REST_Response;
|
||||
use WP_Error;
|
||||
use WC_Product;
|
||||
use WC_Product_Simple;
|
||||
use WC_Product_Variable;
|
||||
use WC_Product_Variation;
|
||||
|
||||
class ProductsController {
|
||||
|
||||
/**
|
||||
* Register REST API routes
|
||||
*/
|
||||
public static function register_routes() {
|
||||
// List products
|
||||
register_rest_route('woonoow/v1', '/products', [
|
||||
'methods' => 'GET',
|
||||
'callback' => [__CLASS__, 'get_products'],
|
||||
'permission_callback' => [Permissions::class, 'check_admin'],
|
||||
]);
|
||||
|
||||
// Get single product
|
||||
register_rest_route('woonoow/v1', '/products/(?P<id>\d+)', [
|
||||
'methods' => 'GET',
|
||||
'callback' => [__CLASS__, 'get_product'],
|
||||
'permission_callback' => [Permissions::class, 'check_admin'],
|
||||
]);
|
||||
|
||||
// Create product
|
||||
register_rest_route('woonoow/v1', '/products', [
|
||||
'methods' => 'POST',
|
||||
'callback' => [__CLASS__, 'create_product'],
|
||||
'permission_callback' => [Permissions::class, 'check_admin'],
|
||||
]);
|
||||
|
||||
// Update product
|
||||
register_rest_route('woonoow/v1', '/products/(?P<id>\d+)', [
|
||||
'methods' => 'PUT',
|
||||
'callback' => [__CLASS__, 'update_product'],
|
||||
'permission_callback' => [Permissions::class, 'check_admin'],
|
||||
]);
|
||||
|
||||
// Delete product
|
||||
register_rest_route('woonoow/v1', '/products/(?P<id>\d+)', [
|
||||
'methods' => 'DELETE',
|
||||
'callback' => [__CLASS__, 'delete_product'],
|
||||
'permission_callback' => [Permissions::class, 'check_admin'],
|
||||
]);
|
||||
|
||||
// Get product categories
|
||||
register_rest_route('woonoow/v1', '/products/categories', [
|
||||
'methods' => 'GET',
|
||||
'callback' => [__CLASS__, 'get_categories'],
|
||||
'permission_callback' => [Permissions::class, 'check_admin'],
|
||||
]);
|
||||
|
||||
// Get product tags
|
||||
register_rest_route('woonoow/v1', '/products/tags', [
|
||||
'methods' => 'GET',
|
||||
'callback' => [__CLASS__, 'get_tags'],
|
||||
'permission_callback' => [Permissions::class, 'check_admin'],
|
||||
]);
|
||||
|
||||
// Get product attributes
|
||||
register_rest_route('woonoow/v1', '/products/attributes', [
|
||||
'methods' => 'GET',
|
||||
'callback' => [__CLASS__, 'get_attributes'],
|
||||
'permission_callback' => [Permissions::class, 'check_admin'],
|
||||
]);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get products list with filters
|
||||
*/
|
||||
public static function get_products(WP_REST_Request $request) {
|
||||
$page = max(1, (int) $request->get_param('page'));
|
||||
$per_page = min(100, max(1, (int) ($request->get_param('per_page') ?: 20)));
|
||||
$search = $request->get_param('search');
|
||||
$status = $request->get_param('status');
|
||||
$category = $request->get_param('category');
|
||||
$type = $request->get_param('type');
|
||||
$stock_status = $request->get_param('stock_status');
|
||||
$orderby = $request->get_param('orderby') ?: 'date';
|
||||
$order = $request->get_param('order') ?: 'DESC';
|
||||
|
||||
$args = [
|
||||
'post_type' => 'product',
|
||||
'posts_per_page' => $per_page,
|
||||
'paged' => $page,
|
||||
'orderby' => $orderby,
|
||||
'order' => $order,
|
||||
];
|
||||
|
||||
// Search
|
||||
if ($search) {
|
||||
$args['s'] = sanitize_text_field($search);
|
||||
}
|
||||
|
||||
// Status filter
|
||||
if ($status) {
|
||||
$args['post_status'] = $status;
|
||||
} else {
|
||||
$args['post_status'] = ['publish', 'draft', 'pending', 'private'];
|
||||
}
|
||||
|
||||
// Category filter
|
||||
if ($category) {
|
||||
$args['tax_query'] = [
|
||||
[
|
||||
'taxonomy' => 'product_cat',
|
||||
'field' => 'term_id',
|
||||
'terms' => (int) $category,
|
||||
],
|
||||
];
|
||||
}
|
||||
|
||||
// Type filter
|
||||
if ($type) {
|
||||
$args['tax_query'] = $args['tax_query'] ?? [];
|
||||
$args['tax_query'][] = [
|
||||
'taxonomy' => 'product_type',
|
||||
'field' => 'slug',
|
||||
'terms' => $type,
|
||||
];
|
||||
}
|
||||
|
||||
// Stock status filter
|
||||
if ($stock_status) {
|
||||
$args['meta_query'] = [
|
||||
[
|
||||
'key' => '_stock_status',
|
||||
'value' => $stock_status,
|
||||
],
|
||||
];
|
||||
}
|
||||
|
||||
$query = new \WP_Query($args);
|
||||
$products = [];
|
||||
|
||||
foreach ($query->posts as $post) {
|
||||
$product = wc_get_product($post->ID);
|
||||
if ($product) {
|
||||
$products[] = self::format_product_list_item($product);
|
||||
}
|
||||
}
|
||||
|
||||
return new WP_REST_Response([
|
||||
'rows' => $products,
|
||||
'total' => $query->found_posts,
|
||||
'page' => $page,
|
||||
'per_page' => $per_page,
|
||||
'pages' => $query->max_num_pages,
|
||||
], 200);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get single product
|
||||
*/
|
||||
public static function get_product(WP_REST_Request $request) {
|
||||
$id = (int) $request->get_param('id');
|
||||
$product = wc_get_product($id);
|
||||
|
||||
if (!$product) {
|
||||
return new WP_Error('product_not_found', __('Product not found', 'woonoow'), ['status' => 404]);
|
||||
}
|
||||
|
||||
return new WP_REST_Response(self::format_product_full($product), 200);
|
||||
}
|
||||
|
||||
/**
|
||||
* Create new product
|
||||
*/
|
||||
public static function create_product(WP_REST_Request $request) {
|
||||
$data = $request->get_json_params();
|
||||
|
||||
// Determine product type
|
||||
$type = $data['type'] ?? 'simple';
|
||||
|
||||
if ($type === 'variable') {
|
||||
$product = new WC_Product_Variable();
|
||||
} else {
|
||||
$product = new WC_Product_Simple();
|
||||
}
|
||||
|
||||
// Set basic data
|
||||
$product->set_name($data['name'] ?? '');
|
||||
$product->set_slug($data['slug'] ?? '');
|
||||
$product->set_status($data['status'] ?? 'publish');
|
||||
$product->set_description($data['description'] ?? '');
|
||||
$product->set_short_description($data['short_description'] ?? '');
|
||||
$product->set_sku($data['sku'] ?? '');
|
||||
$product->set_regular_price($data['regular_price'] ?? '');
|
||||
$product->set_sale_price($data['sale_price'] ?? '');
|
||||
$product->set_manage_stock($data['manage_stock'] ?? false);
|
||||
|
||||
if ($data['manage_stock']) {
|
||||
$product->set_stock_quantity($data['stock_quantity'] ?? 0);
|
||||
$product->set_stock_status($data['stock_status'] ?? 'instock');
|
||||
} else {
|
||||
$product->set_stock_status($data['stock_status'] ?? 'instock');
|
||||
}
|
||||
|
||||
$product->set_weight($data['weight'] ?? '');
|
||||
$product->set_length($data['length'] ?? '');
|
||||
$product->set_width($data['width'] ?? '');
|
||||
$product->set_height($data['height'] ?? '');
|
||||
|
||||
// Categories
|
||||
if (!empty($data['categories'])) {
|
||||
$product->set_category_ids($data['categories']);
|
||||
}
|
||||
|
||||
// Tags
|
||||
if (!empty($data['tags'])) {
|
||||
$product->set_tag_ids($data['tags']);
|
||||
}
|
||||
|
||||
// Images
|
||||
if (!empty($data['image_id'])) {
|
||||
$product->set_image_id($data['image_id']);
|
||||
}
|
||||
if (!empty($data['gallery_image_ids'])) {
|
||||
$product->set_gallery_image_ids($data['gallery_image_ids']);
|
||||
}
|
||||
|
||||
$product->save();
|
||||
|
||||
// Handle variations for variable products
|
||||
if ($type === 'variable' && !empty($data['attributes'])) {
|
||||
self::save_product_attributes($product, $data['attributes']);
|
||||
|
||||
if (!empty($data['variations'])) {
|
||||
self::save_product_variations($product, $data['variations']);
|
||||
}
|
||||
}
|
||||
|
||||
return new WP_REST_Response(self::format_product_full($product), 201);
|
||||
}
|
||||
|
||||
/**
|
||||
* Update product
|
||||
*/
|
||||
public static function update_product(WP_REST_Request $request) {
|
||||
$id = (int) $request->get_param('id');
|
||||
$data = $request->get_json_params();
|
||||
|
||||
$product = wc_get_product($id);
|
||||
if (!$product) {
|
||||
return new WP_Error('product_not_found', __('Product not found', 'woonoow'), ['status' => 404]);
|
||||
}
|
||||
|
||||
// Update basic data
|
||||
if (isset($data['name'])) $product->set_name($data['name']);
|
||||
if (isset($data['slug'])) $product->set_slug($data['slug']);
|
||||
if (isset($data['status'])) $product->set_status($data['status']);
|
||||
if (isset($data['description'])) $product->set_description($data['description']);
|
||||
if (isset($data['short_description'])) $product->set_short_description($data['short_description']);
|
||||
if (isset($data['sku'])) $product->set_sku($data['sku']);
|
||||
if (isset($data['regular_price'])) $product->set_regular_price($data['regular_price']);
|
||||
if (isset($data['sale_price'])) $product->set_sale_price($data['sale_price']);
|
||||
|
||||
if (isset($data['manage_stock'])) {
|
||||
$product->set_manage_stock($data['manage_stock']);
|
||||
if ($data['manage_stock']) {
|
||||
if (isset($data['stock_quantity'])) $product->set_stock_quantity($data['stock_quantity']);
|
||||
}
|
||||
}
|
||||
|
||||
if (isset($data['stock_status'])) $product->set_stock_status($data['stock_status']);
|
||||
if (isset($data['weight'])) $product->set_weight($data['weight']);
|
||||
if (isset($data['length'])) $product->set_length($data['length']);
|
||||
if (isset($data['width'])) $product->set_width($data['width']);
|
||||
if (isset($data['height'])) $product->set_height($data['height']);
|
||||
|
||||
// Categories
|
||||
if (isset($data['categories'])) {
|
||||
$product->set_category_ids($data['categories']);
|
||||
}
|
||||
|
||||
// Tags
|
||||
if (isset($data['tags'])) {
|
||||
$product->set_tag_ids($data['tags']);
|
||||
}
|
||||
|
||||
// Images
|
||||
if (isset($data['image_id'])) {
|
||||
$product->set_image_id($data['image_id']);
|
||||
}
|
||||
if (isset($data['gallery_image_ids'])) {
|
||||
$product->set_gallery_image_ids($data['gallery_image_ids']);
|
||||
}
|
||||
|
||||
$product->save();
|
||||
|
||||
// Handle variations for variable products
|
||||
if ($product->is_type('variable')) {
|
||||
if (isset($data['attributes'])) {
|
||||
self::save_product_attributes($product, $data['attributes']);
|
||||
}
|
||||
if (isset($data['variations'])) {
|
||||
self::save_product_variations($product, $data['variations']);
|
||||
}
|
||||
}
|
||||
|
||||
return new WP_REST_Response(self::format_product_full($product), 200);
|
||||
}
|
||||
|
||||
/**
|
||||
* Delete product
|
||||
*/
|
||||
public static function delete_product(WP_REST_Request $request) {
|
||||
$id = (int) $request->get_param('id');
|
||||
$force = $request->get_param('force') === 'true';
|
||||
|
||||
$product = wc_get_product($id);
|
||||
if (!$product) {
|
||||
return new WP_Error('product_not_found', __('Product not found', 'woonoow'), ['status' => 404]);
|
||||
}
|
||||
|
||||
$result = $product->delete($force);
|
||||
|
||||
if (!$result) {
|
||||
return new WP_Error('delete_failed', __('Failed to delete product', 'woonoow'), ['status' => 500]);
|
||||
}
|
||||
|
||||
return new WP_REST_Response(['success' => true, 'id' => $id], 200);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get product categories
|
||||
*/
|
||||
public static function get_categories(WP_REST_Request $request) {
|
||||
$terms = get_terms([
|
||||
'taxonomy' => 'product_cat',
|
||||
'hide_empty' => false,
|
||||
]);
|
||||
|
||||
$categories = [];
|
||||
foreach ($terms as $term) {
|
||||
$categories[] = [
|
||||
'id' => $term->term_id,
|
||||
'name' => $term->name,
|
||||
'slug' => $term->slug,
|
||||
'parent' => $term->parent,
|
||||
'count' => $term->count,
|
||||
];
|
||||
}
|
||||
|
||||
return new WP_REST_Response($categories, 200);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get product tags
|
||||
*/
|
||||
public static function get_tags(WP_REST_Request $request) {
|
||||
$terms = get_terms([
|
||||
'taxonomy' => 'product_tag',
|
||||
'hide_empty' => false,
|
||||
]);
|
||||
|
||||
$tags = [];
|
||||
foreach ($terms as $term) {
|
||||
$tags[] = [
|
||||
'id' => $term->term_id,
|
||||
'name' => $term->name,
|
||||
'slug' => $term->slug,
|
||||
'count' => $term->count,
|
||||
];
|
||||
}
|
||||
|
||||
return new WP_REST_Response($tags, 200);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get product attributes
|
||||
*/
|
||||
public static function get_attributes(WP_REST_Request $request) {
|
||||
$attributes = wc_get_attribute_taxonomies();
|
||||
$result = [];
|
||||
|
||||
foreach ($attributes as $attribute) {
|
||||
$result[] = [
|
||||
'id' => $attribute->attribute_id,
|
||||
'name' => $attribute->attribute_name,
|
||||
'label' => $attribute->attribute_label,
|
||||
'type' => $attribute->attribute_type,
|
||||
'orderby' => $attribute->attribute_orderby,
|
||||
];
|
||||
}
|
||||
|
||||
return new WP_REST_Response($result, 200);
|
||||
}
|
||||
|
||||
/**
|
||||
* Format product for list view
|
||||
*/
|
||||
private static function format_product_list_item($product) {
|
||||
$image = wp_get_attachment_image_src($product->get_image_id(), 'thumbnail');
|
||||
|
||||
return [
|
||||
'id' => $product->get_id(),
|
||||
'name' => $product->get_name(),
|
||||
'sku' => $product->get_sku(),
|
||||
'type' => $product->get_type(),
|
||||
'status' => $product->get_status(),
|
||||
'price' => $product->get_price(),
|
||||
'regular_price' => $product->get_regular_price(),
|
||||
'sale_price' => $product->get_sale_price(),
|
||||
'price_html' => $product->get_price_html(),
|
||||
'stock_status' => $product->get_stock_status(),
|
||||
'stock_quantity' => $product->get_stock_quantity(),
|
||||
'manage_stock' => $product->get_manage_stock(),
|
||||
'image_url' => $image ? $image[0] : '',
|
||||
'permalink' => get_permalink($product->get_id()),
|
||||
'date_created' => $product->get_date_created() ? $product->get_date_created()->date('Y-m-d H:i:s') : '',
|
||||
'date_modified' => $product->get_date_modified() ? $product->get_date_modified()->date('Y-m-d H:i:s') : '',
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* Format product with full details
|
||||
*/
|
||||
private static function format_product_full($product) {
|
||||
$data = self::format_product_list_item($product);
|
||||
|
||||
// Add full details
|
||||
$data['description'] = $product->get_description();
|
||||
$data['short_description'] = $product->get_short_description();
|
||||
$data['slug'] = $product->get_slug();
|
||||
$data['weight'] = $product->get_weight();
|
||||
$data['length'] = $product->get_length();
|
||||
$data['width'] = $product->get_width();
|
||||
$data['height'] = $product->get_height();
|
||||
$data['categories'] = $product->get_category_ids();
|
||||
$data['tags'] = $product->get_tag_ids();
|
||||
$data['image_id'] = $product->get_image_id();
|
||||
$data['gallery_image_ids'] = $product->get_gallery_image_ids();
|
||||
|
||||
// Gallery images
|
||||
$gallery = [];
|
||||
foreach ($product->get_gallery_image_ids() as $image_id) {
|
||||
$image = wp_get_attachment_image_src($image_id, 'full');
|
||||
if ($image) {
|
||||
$gallery[] = [
|
||||
'id' => $image_id,
|
||||
'url' => $image[0],
|
||||
'width' => $image[1],
|
||||
'height' => $image[2],
|
||||
];
|
||||
}
|
||||
}
|
||||
$data['gallery'] = $gallery;
|
||||
|
||||
// Variable product specifics
|
||||
if ($product->is_type('variable')) {
|
||||
$data['attributes'] = self::get_product_attributes($product);
|
||||
$data['variations'] = self::get_product_variations($product);
|
||||
}
|
||||
|
||||
return $data;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get product attributes
|
||||
*/
|
||||
private static function get_product_attributes($product) {
|
||||
$attributes = [];
|
||||
foreach ($product->get_attributes() as $attribute) {
|
||||
$attributes[] = [
|
||||
'id' => $attribute->get_id(),
|
||||
'name' => $attribute->get_name(),
|
||||
'options' => $attribute->get_options(),
|
||||
'position' => $attribute->get_position(),
|
||||
'visible' => $attribute->get_visible(),
|
||||
'variation' => $attribute->get_variation(),
|
||||
];
|
||||
}
|
||||
return $attributes;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get product variations
|
||||
*/
|
||||
private static function get_product_variations($product) {
|
||||
$variations = [];
|
||||
foreach ($product->get_children() as $variation_id) {
|
||||
$variation = wc_get_product($variation_id);
|
||||
if ($variation) {
|
||||
$image = wp_get_attachment_image_src($variation->get_image_id(), 'thumbnail');
|
||||
$variations[] = [
|
||||
'id' => $variation->get_id(),
|
||||
'sku' => $variation->get_sku(),
|
||||
'price' => $variation->get_price(),
|
||||
'regular_price' => $variation->get_regular_price(),
|
||||
'sale_price' => $variation->get_sale_price(),
|
||||
'stock_status' => $variation->get_stock_status(),
|
||||
'stock_quantity' => $variation->get_stock_quantity(),
|
||||
'manage_stock' => $variation->get_manage_stock(),
|
||||
'attributes' => $variation->get_attributes(),
|
||||
'image_id' => $variation->get_image_id(),
|
||||
'image_url' => $image ? $image[0] : '',
|
||||
];
|
||||
}
|
||||
}
|
||||
return $variations;
|
||||
}
|
||||
|
||||
/**
|
||||
* Save product attributes
|
||||
*/
|
||||
private static function save_product_attributes($product, $attributes_data) {
|
||||
$attributes = [];
|
||||
foreach ($attributes_data as $attr_data) {
|
||||
$attribute = new \WC_Product_Attribute();
|
||||
$attribute->set_name($attr_data['name']);
|
||||
$attribute->set_options($attr_data['options']);
|
||||
$attribute->set_position($attr_data['position'] ?? 0);
|
||||
$attribute->set_visible($attr_data['visible'] ?? true);
|
||||
$attribute->set_variation($attr_data['variation'] ?? true);
|
||||
$attributes[] = $attribute;
|
||||
}
|
||||
$product->set_attributes($attributes);
|
||||
$product->save();
|
||||
}
|
||||
|
||||
/**
|
||||
* Save product variations
|
||||
*/
|
||||
private static function save_product_variations($product, $variations_data) {
|
||||
foreach ($variations_data as $var_data) {
|
||||
if (isset($var_data['id']) && $var_data['id']) {
|
||||
// Update existing variation
|
||||
$variation = wc_get_product($var_data['id']);
|
||||
} else {
|
||||
// Create new variation
|
||||
$variation = new WC_Product_Variation();
|
||||
$variation->set_parent_id($product->get_id());
|
||||
}
|
||||
|
||||
if ($variation) {
|
||||
if (isset($var_data['sku'])) $variation->set_sku($var_data['sku']);
|
||||
if (isset($var_data['regular_price'])) $variation->set_regular_price($var_data['regular_price']);
|
||||
if (isset($var_data['sale_price'])) $variation->set_sale_price($var_data['sale_price']);
|
||||
if (isset($var_data['stock_status'])) $variation->set_stock_status($var_data['stock_status']);
|
||||
if (isset($var_data['manage_stock'])) $variation->set_manage_stock($var_data['manage_stock']);
|
||||
if (isset($var_data['stock_quantity'])) $variation->set_stock_quantity($var_data['stock_quantity']);
|
||||
if (isset($var_data['attributes'])) $variation->set_attributes($var_data['attributes']);
|
||||
if (isset($var_data['image_id'])) $variation->set_image_id($var_data['image_id']);
|
||||
|
||||
$variation->save();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -17,6 +17,7 @@ use WooNooW\Api\DeveloperController;
|
||||
use WooNooW\Api\SystemController;
|
||||
use WooNooW\Api\NotificationsController;
|
||||
use WooNooW\Api\ActivityLogController;
|
||||
use WooNooW\Api\ProductsController;
|
||||
|
||||
class Routes {
|
||||
public static function init() {
|
||||
@@ -89,6 +90,9 @@ class Routes {
|
||||
// Activity Log controller
|
||||
$activity_log_controller = new ActivityLogController();
|
||||
$activity_log_controller->register_routes();
|
||||
|
||||
// Products controller
|
||||
ProductsController::register_routes();
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user