????
| Current Path : /home/arabianr/www/wp-content/plugins/simplybook/app/Http/Endpoints/ |
| Current File : /home/arabianr/www/wp-content/plugins/simplybook/app/Http/Endpoints/AbstractCrudEndpoint.php |
<?php
namespace SimplyBook\Http\Endpoints;
use SimplyBook\Traits\HasRestAccess;
use SimplyBook\Support\Helpers\Storage;
use SimplyBook\Traits\HasAllowlistControl;
use SimplyBook\Http\Entities\AbstractEntity;
use SimplyBook\Exceptions\RestDataException;
use SimplyBook\Exceptions\FormException;
use SimplyBook\Interfaces\MultiEndpointInterface;
abstract class AbstractCrudEndpoint implements MultiEndpointInterface
{
use HasRestAccess;
use HasAllowlistControl;
/**
* The entity that this endpoint uses to process requests.
*/
protected AbstractEntity $entity;
public function __construct(AbstractEntity $entity)
{
$this->entity = $entity;
}
/**
* Only enable this endpoint if the user has access to the admin area
*/
public function enabled(): bool
{
return $this->adminAccessAllowed();
}
/**
* @inheritDoc
*/
public function registerRoutes(): array
{
$route = $this->entity->getInternalEndpoint();
return [
$route => [
'methods' => \WP_REST_Server::READABLE . ',' . \WP_REST_Server::CREATABLE,
'callback' => [$this, 'handleCollectionRequest'],
],
$route . '/(?P<id>[0-9]+)' => [
'methods' => \WP_REST_Server::READABLE . ',' . \WP_REST_Server::CREATABLE . ',' . \WP_REST_Server::EDITABLE . ',' . \WP_REST_Server::DELETABLE,
'callback' => [$this, 'handleSingleRequest'],
],
];
}
/**
* Handle entity collection requests.
* @internal Override this method to process the collection request.
*/
public function handleCollectionRequest(\WP_REST_Request $request): \WP_REST_Response
{
$requestStorage = new Storage($request->get_params());
switch ($request->get_method()) {
case 'GET':
return $this->getAllEntities();
case 'POST':
return $this->createItem($requestStorage);
default:
return $this->sendHttpResponse([], false, __('Method not allowed', 'simplybook'), 405);
}
}
/**
* Return all entities as a WP_REST_Response.
* @internal Override this method if you want to customize the response.
*/
protected function getAllEntities(): \WP_REST_Response
{
return $this->sendHttpResponse(
$this->entity->all()
);
}
/**
* Create a new entity based on the request parameters. It will catch any
* validation errors or exceptions thrown during the creation process.
* @internal Override this method if you want to customize the logic.
*/
protected function createItem(Storage $request): \WP_REST_Response
{
if ($request->isEmpty()) {
return $this->sendHttpResponse([], false, esc_html__('Could not create entity, no data provided.', 'simplybook'), 400);
}
try {
$this->entity->create(
$request->all()
);
} catch (\Throwable $e) {
return $this->processRequestThrowable($e, 'create');
}
// translators: %s is either 'Service' or 'Service Provider'
$successMessage = sprintf(
__('%s successfully saved!', 'simplybook'),
$this->entity->getName()
);
return $this->sendHttpResponse($this->entity->attributes(), true, $successMessage);
}
/**
* Handle single entity requests.
* @internal Override this method to handle GET, PUT, PATCH, POST, DELETE
* requests for a single entity.
*/
public function handleSingleRequest(\WP_REST_Request $request): \WP_REST_Response
{
$requestStorage = new Storage($request->get_params());
switch ($request->get_method()) {
case 'GET':
return $this->findEntity($requestStorage);
case 'PUT':
case 'PATCH':
case 'POST':
return $this->updateEntity($requestStorage);
case 'DELETE':
return $this->deleteEntity($requestStorage);
default:
return $this->sendHttpResponse([], false, __('Method not allowed', 'simplybook'), 405);
}
}
/**
* Get a single entity based on the request parameters. It will catch any
* validation errors or exceptions thrown during the retrieval process.
* @internal Override this method if you want to customize the logic.
*/
protected function findEntity(Storage $request): \WP_REST_Response
{
try {
$this->entity = $this->entity->find(
$request->getString('id')
);
} catch (\Throwable $e) {
return $this->sendHttpResponse([
'error' => $e->getMessage()
], false, __('Entity not found!', 'simplybook'), 404);
}
return $this->sendHttpResponse($this->entity->attributes());
}
/**
* Update an entity based on the request parameters. It will catch any
* validation errors or exceptions thrown during the update process.
* @internal Override this method if you want to customize the logic.
*/
protected function updateEntity(Storage $request): \WP_REST_Response
{
try {
$this->entity->fill($request->all());
$this->entity->update();
} catch (\Throwable $e) {
return $this->processRequestThrowable($e, 'update');
}
// translators: %s is either 'Service' or 'Service Provider'
$successMessage = sprintf(
__('%s successfully saved!', 'simplybook'),
$this->entity->getName()
);
return $this->sendHttpResponse($this->entity->attributes(), true, $successMessage);
}
/**
* Delete an entity based on the request parameters. It will catch any
* validation errors or exceptions thrown during the deletion process.
* @internal Override this method if you want to customize the logic.
*/
protected function deleteEntity(Storage $request): \WP_REST_Response
{
try {
$this->entity->setPrimaryValue($request->getString('id'));
$this->entity->delete();
} catch (\Throwable $e) {
return $this->sendHttpResponse([
'error' => $e->getMessage()
], false, esc_html__('Something went wrong while deleting.', 'simplybook'), 500);
}
return $this->sendHttpResponse();
}
/**
* Generically process any throwable that is caught while doing requests.
* Child classes could overwrite this method to specifically handle
* the throwable.
*/
protected function processRequestThrowable(\Throwable $exception, string $action = ''): \WP_REST_Response
{
if ($exception instanceof RestDataException) {
return $this->processRestDataException($exception, $action);
}
if ($exception instanceof FormException) {
return new \WP_REST_Response([
'message' => $exception->getMessage(),
'errors' => $exception->getErrors()
], 400);
}
return $this->sendHttpResponse([], false, esc_html__('An unknown error occurred. Please try again later.', 'simplybook'), 500);
}
/**
* Default behavior for processing {@see RestDataException}. Child classes
* should overwrite for specific handling.
*/
protected function processRestDataException(RestDataException $exception, string $action): \WP_REST_Response
{
switch ($action) {
case 'update':
case 'create':
return $this->processAttributesException($exception);
default:
return $this->sendHttpResponse($exception->getData(), false, $exception->getMessage(), $exception->getResponseCode());
}
}
/**
* Method specifically for handling an attribute exceptions. It should create
* translated, user-friendly, error messages based on the faulty attributes
* that we receive in the SimplyBook response. Errors format should be
* consistent with the entity validation method:
* {@see \SimplyBook\Http\Entities\AbstractEntity::validate}
*/
protected function processAttributesException(RestDataException $exception): \WP_REST_Response
{
$exceptionData = $exception->getData();
if (empty($exceptionData['data'])) {
return new \WP_REST_Response([
'message' => esc_html__('An unknown error occurred while saving, please try again.', 'simplybook'),
], 500);
}
$faultyFields = $exceptionData['data'];
$translatedErrors = $this->buildTranslatedErrors($faultyFields);
return new \WP_REST_Response([
'message' => __('An error occurred while saving, please try again.', 'simplybook'),
'errors' => $translatedErrors,
], 500);
}
/**
* Build a per-attribute list of translated, known, error messages from raw
* validation errors. We do this because we want to show friendly,
* translated, messages only for errors we recognize, grouped by attribute.
*
* Steps:
* 1) Iterate the faulty fields; skip attributes without a known mapping.
* 2) For each error string, scan the attribute's known
* "needle" => "translation" pairs.
* 3) If a needle appears (case-insensitive), collect its translation once
* per attribute.
* 4) Only include attributes that ended up with at least one translation.
*/
protected function buildTranslatedErrors(array $faultyFields, array $knownErrors = []): array
{
$translatedByAttribute = [];
if (empty($knownErrors)) {
$knownErrors = $this->entity->getKnownErrors();
}
foreach ($faultyFields as $attribute => $errors) {
if (!is_array($errors)) {
continue;
}
// Keep untranslated errors for unknown errors
if (!array_key_exists($attribute, $knownErrors)) {
$translatedByAttribute[$attribute] = $errors;
continue;
}
$knownAttributeErrors = $knownErrors[$attribute];
$translations = [];
$seen = [];
foreach ($errors as $key => $error) {
if (!is_string($error) || $error === '') {
continue;
}
// First add untranslated error so we won't lose it.
$translations[$key] = $error;
foreach ($knownAttributeErrors as $needle => $translation) {
if (empty($needle)) {
continue;
}
if (stripos($error, (string) $needle) !== false) {
if (!isset($seen[$translation])) {
// Override untranslated error
$translations[$key] = $translation;
$seen[$translation] = true;
} else {
// Remove untranslated error if already seen
unset($translations[$key]);
}
break;
}
}
}
if ($translations !== []) {
$translatedByAttribute[$attribute] = $translations;
}
}
return $translatedByAttribute;
}
}