<?php
|
namespace JVBase\rest;
|
|
use WP_REST_Response;
|
use WP_Error;
|
use DateTime;
|
use DateTimeZone;
|
use Exception;
|
|
if (!defined('ABSPATH')) {
|
exit;
|
}
|
|
/**
|
* Fluent Response Builder for REST API
|
*
|
* Provides consistent response formatting, caching headers, and error handling.
|
*
|
* Usage:
|
* return Response::success(['user' => $userData]);
|
* return Response::error('Not found', 'not_found', 404);
|
* return Response::paginated($items, $total, $page, $perPage);
|
*/
|
class Response
|
{
|
private array $data = [];
|
private int $status = 200;
|
private array $headers = [];
|
private ?WP_REST_Response $response = null;
|
|
/**
|
* Create a success response
|
*/
|
public static function success(array $data = [], int $status = 200): WP_REST_Response
|
{
|
$response = new self();
|
$response->data = array_merge(['success' => true], $data);
|
$response->status = $status;
|
return $response->build();
|
}
|
|
/**
|
* Create an error response
|
*/
|
public static function error(
|
string $message,
|
string $code = 'error',
|
int $status = 400,
|
?string $field = null,
|
array $extra = []
|
): WP_REST_Response {
|
$data = [
|
'success' => false,
|
'code' => $code,
|
'message' => $message,
|
];
|
|
if ($field !== null) {
|
$data['field'] = $field;
|
}
|
|
if (!empty($extra)) {
|
$data = array_merge($data, $extra);
|
}
|
|
return new WP_REST_Response($data, $status);
|
}
|
|
/**
|
* Create a validation error response (422)
|
*/
|
public static function validationError(array $errors, string $message = 'Validation failed'): WP_REST_Response
|
{
|
return new WP_REST_Response([
|
'success' => false,
|
'code' => 'validation_error',
|
'message' => $message,
|
'errors' => $errors,
|
], 422);
|
}
|
|
/**
|
* Create an unauthorized response (401)
|
*/
|
public static function unauthorized(string $message = 'Authentication required'): WP_REST_Response
|
{
|
return self::error($message, 'unauthorized', 401);
|
}
|
|
/**
|
* Create a forbidden response (403)
|
*/
|
public static function forbidden(string $message = 'Access denied'): WP_REST_Response
|
{
|
return self::error($message, 'forbidden', 403);
|
}
|
|
/**
|
* Create a not found response (404)
|
*/
|
public static function notFound(string $message = 'Resource not found'): WP_REST_Response
|
{
|
return self::error($message, 'not_found', 404);
|
}
|
|
/**
|
* Create a rate limit response (429)
|
*/
|
public static function rateLimited(int $retryAfter = 60, string $message = 'Too many requests'): WP_REST_Response
|
{
|
$response = self::error($message, 'rate_limit', 429);
|
$response->header('Retry-After', (string) $retryAfter);
|
return $response;
|
}
|
|
/**
|
* Create a server error response (500)
|
*/
|
public static function serverError(string $message = 'An unexpected error occurred'): WP_REST_Response
|
{
|
return self::error($message, 'server_error', 500);
|
}
|
|
/**
|
* Create a paginated response
|
*/
|
public static function paginated(
|
array $items,
|
int $total,
|
int $page = 1,
|
int $perPage = 20,
|
array $extra = []
|
): WP_REST_Response {
|
$totalPages = (int) ceil($total / $perPage);
|
|
$data = array_merge([
|
'success' => true,
|
'items' => $items,
|
'pagination' => [
|
'total' => $total,
|
'per_page' => $perPage,
|
'current_page' => $page,
|
'total_pages' => $totalPages,
|
'has_more' => $page < $totalPages,
|
],
|
], $extra);
|
|
$response = new WP_REST_Response($data, 200);
|
|
// Add pagination headers
|
$response->header('X-Total-Count', (string) $total);
|
$response->header('X-Total-Pages', (string) $totalPages);
|
$response->header('X-Current-Page', (string) $page);
|
$response->header('X-Per-Page', (string) $perPage);
|
|
return $response;
|
}
|
|
/**
|
* Create a collection response (list without pagination)
|
*/
|
public static function collection(array $items, array $extra = []): WP_REST_Response
|
{
|
$data = array_merge([
|
'success' => true,
|
'items' => $items,
|
'total' => count($items),
|
], $extra);
|
|
return new WP_REST_Response($data, 200);
|
}
|
|
/**
|
* Create a single item response
|
*/
|
public static function item(array $item, string $key = 'item'): WP_REST_Response
|
{
|
return new WP_REST_Response([
|
'success' => true,
|
$key => $item,
|
], 200);
|
}
|
|
/**
|
* Create a created response (201)
|
*/
|
public static function created(array $data = [], ?string $location = null): WP_REST_Response
|
{
|
$response = new WP_REST_Response(
|
array_merge(['success' => true], $data),
|
201
|
);
|
|
if ($location) {
|
$response->header('Location', $location);
|
}
|
|
return $response;
|
}
|
|
/**
|
* Create a no content response (204)
|
*/
|
public static function noContent(): WP_REST_Response
|
{
|
return new WP_REST_Response(null, 204);
|
}
|
|
/**
|
* Create a response from WP_Error
|
*/
|
public static function fromError(WP_Error $error): WP_REST_Response
|
{
|
$data = $error->get_error_data();
|
$status = is_array($data) && isset($data['status']) ? $data['status'] : 400;
|
|
return self::error(
|
$error->get_error_message(),
|
$error->get_error_code(),
|
$status
|
);
|
}
|
|
/**
|
* Build a response with fluent interface
|
*/
|
public static function make(array $data = []): self
|
{
|
$instance = new self();
|
$instance->data = $data;
|
return $instance;
|
}
|
|
/**
|
* Set response data
|
*/
|
public function data(array $data): self
|
{
|
$this->data = array_merge($this->data, $data);
|
return $this;
|
}
|
|
/**
|
* Set HTTP status code
|
*/
|
public function status(int $status): self
|
{
|
$this->status = $status;
|
return $this;
|
}
|
|
/**
|
* Add a response header
|
*/
|
public function header(string $name, string $value): self
|
{
|
$this->headers[$name] = $value;
|
return $this;
|
}
|
|
/**
|
* Add multiple headers
|
*/
|
public function headers(array $headers): self
|
{
|
$this->headers = array_merge($this->headers, $headers);
|
return $this;
|
}
|
|
/**
|
* Add cache control headers
|
*/
|
public function cache(int $maxAge = 300, bool $private = true): self
|
{
|
$directive = $private ? 'private' : 'public';
|
$this->headers['Cache-Control'] = "{$directive}, max-age={$maxAge}";
|
$this->headers['Vary'] = 'Cookie';
|
return $this;
|
}
|
|
/**
|
* Add no-cache headers
|
*/
|
public function noCache(): self
|
{
|
$this->headers['Cache-Control'] = 'no-store, no-cache, must-revalidate, max-age=0';
|
$this->headers['Pragma'] = 'no-cache';
|
return $this;
|
}
|
|
/**
|
* Add ETag header for conditional requests
|
*/
|
public function etag(string $data): self
|
{
|
$this->headers['ETag'] = '"' . md5($data) . '"';
|
return $this;
|
}
|
|
/**
|
* Add Last-Modified header
|
*/
|
public function lastModified(string|int $timestamp): self
|
{
|
if (is_string($timestamp)) {
|
$timestamp = strtotime($timestamp);
|
}
|
$this->headers['Last-Modified'] = gmdate('D, d M Y H:i:s', $timestamp) . ' GMT';
|
return $this;
|
}
|
|
/**
|
* Build and return the response
|
*/
|
public function build(): WP_REST_Response
|
{
|
$response = new WP_REST_Response($this->data, $this->status);
|
|
foreach ($this->headers as $name => $value) {
|
$response->header($name, $value);
|
}
|
|
return $response;
|
}
|
|
/**
|
* Convert to WP_REST_Response (alias for build)
|
*/
|
public function toResponse(): WP_REST_Response
|
{
|
return $this->build();
|
}
|
|
// =========================================================================
|
// UTILITY METHODS
|
// =========================================================================
|
|
/**
|
* Format MySQL datetime to ISO 8601 timestamp
|
*/
|
public static function formatTimestamp(?string $mysqlDatetime): ?string
|
{
|
if (empty($mysqlDatetime)) {
|
return null;
|
}
|
|
try {
|
$wpTimezone = wp_timezone();
|
$date = new DateTime($mysqlDatetime, $wpTimezone);
|
$date->setTimezone(new DateTimeZone('UTC'));
|
return $date->format('c');
|
} catch (Exception $e) {
|
return null;
|
}
|
}
|
|
/**
|
* Format an array of items with a callback
|
*/
|
public static function formatItems(array $items, callable $formatter): array
|
{
|
return array_map($formatter, $items);
|
}
|
|
/**
|
* Pluck specific fields from items
|
*/
|
public static function pluck(array $items, array $fields): array
|
{
|
return array_map(function($item) use ($fields) {
|
$result = [];
|
foreach ($fields as $field) {
|
if (is_array($item) && array_key_exists($field, $item)) {
|
$result[$field] = $item[$field];
|
} elseif (is_object($item) && property_exists($item, $field)) {
|
$result[$field] = $item->$field;
|
}
|
}
|
return $result;
|
}, $items);
|
}
|
|
/**
|
* Add server timestamp to response data
|
*/
|
public static function withTimestamp(array $data): array
|
{
|
$data['timestamp'] = date('c');
|
$data['server_time'] = date('c');
|
return $data;
|
}
|
|
/**
|
* Create response for queued operation
|
*/
|
public static function queued(string $operationId, string $message = 'Queued for processing', array $extra = []): WP_REST_Response
|
{
|
return self::success(array_merge([
|
'message' => $message,
|
'operation_id' => $operationId,
|
'status' => 'queued',
|
], $extra), 202);
|
}
|
|
/**
|
* Create response for operation status
|
*/
|
public static function operationStatus(
|
string $id,
|
string $status,
|
int $progress = 0,
|
int $total = 0,
|
?string $message = null
|
): WP_REST_Response {
|
$data = [
|
'operation_id' => $id,
|
'status' => $status,
|
'progress' => $progress,
|
'total' => $total,
|
];
|
|
if ($total > 0) {
|
$data['progress_percentage'] = round(($progress / $total) * 100);
|
}
|
|
if ($message) {
|
$data['message'] = $message;
|
}
|
|
return self::success($data);
|
}
|
}
|