Created
August 3, 2020 10:19
-
-
Save geoidesic/f1e90c241f6758ca2f2ec2bf4e2e66e1 to your computer and use it in GitHub Desktop.
Will invoke a custom DocumentValidator class depending on the URL format.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
<?php | |
declare(strict_types=1); | |
namespace App\Listener; | |
use Cake\Core\Configure; | |
use Cake\Datasource\EntityInterface; | |
use Cake\Datasource\RepositoryInterface; | |
use Cake\Datasource\ResultSetDecorator; | |
use Cake\Datasource\ResultSetInterface; | |
use Cake\Event\EventInterface; | |
use Cake\Http\Exception\BadRequestException; | |
use Cake\Http\Response; | |
use Cake\ORM\Association; | |
use Cake\ORM\Query; | |
use Cake\ORM\ResultSet; | |
use Cake\ORM\Table; | |
use Cake\ORM\TableRegistry; | |
use Cake\Utility\Hash; | |
use Cake\Utility\Inflector; | |
use Crud\Error\Exception\CrudException; | |
use Crud\Event\Subject; | |
use Crud\Listener\ApiListener; | |
use CrudJsonApi\Listener\JsonApiListener as BaseListener; | |
use CrudJsonApi\Listener\JsonApi\DocumentValidator; | |
use App\Listener\JsonApi\DocumentRelationshipValidator; | |
use InvalidArgumentException; | |
/** | |
* Extends Crud ApiListener to respond in JSON API format. | |
* | |
* Licensed under The MIT License | |
* For full copyright and license information, please see the LICENSE.txt | |
*/ | |
class JsonApiListener extends BaseListener | |
{ | |
protected function _checkIsRelationshipsRequest(): bool | |
{ | |
// if URL matches relationship regex, then use custom validator | |
preg_match( | |
'/.*{controller}\/{id}\/relationships\/{foreignTableName}/', | |
$this->_controller()->getRequest()->getParam('_matchedRoute'), | |
$match | |
); | |
$isRelationshipURL = !empty($match); | |
return $isRelationshipURL; | |
} | |
/** | |
* Checks if data was posted to the Listener. If so then checks if the | |
* array (already converted from json) matches the expected JSON API | |
* structure for resources and if so, converts that array to CakePHP | |
* compatible format so it can be processed as usual from there. | |
* | |
* @overridden in order to provide alternate data validation for relationships (which have a different data structure from other requests) | |
* @return void | |
*/ | |
protected function _checkRequestData(): void | |
{ | |
// Controller name: $this->_controller->getName() | |
$requestMethod = $this->_controller()->getRequest()->getMethod(); | |
if ($requestMethod !== 'POST' && $requestMethod !== 'PATCH') { | |
return; | |
} | |
$requestData = $this->_controller()->getRequest()->getData(); | |
if (empty($requestData)) { | |
throw new BadRequestException( | |
'Missing request data required for POST and PATCH methods. ' . | |
'Make sure that you are sending a request body and that it is valid JSON.' | |
); | |
} | |
$validator = new DocumentValidator($requestData, $this->getConfig()); | |
$isRelationshipURL = $this->_checkIsRelationshipsRequest(); | |
if ($requestMethod === 'POST') { | |
if ($isRelationshipURL) { | |
$relationshipValidator = new DocumentRelationshipValidator($requestData, $this->getConfig()); | |
$relationshipValidator->validateUpdateDocument(); | |
} else { | |
$validator->validateCreateDocument(); | |
} | |
} | |
if ($requestMethod === 'PATCH') { | |
if ($isRelationshipURL) { | |
$relationshipValidator = new DocumentRelationshipValidator($requestData, $this->getConfig()); | |
$relationshipValidator->validateUpdateDocument(); | |
} else { | |
$validator->validateUpdateDocument(); | |
} | |
} | |
// decode JSON API to CakePHP array format, then call the action as usual | |
$decodedJsonApi = $this->_convertJsonApiDocumentArray($requestData); | |
$exception = false; | |
if ($requestMethod === 'PATCH') { | |
if (!$isRelationshipURL) { | |
// For normal PATCH operations the `id` field in the request data MUST match the URL id | |
// because JSON API considers it immutable. https://github.com/json-api/json-api/issues/481 | |
$exception = $this->_controller()->getRequest()->getParam('id') !== $decodedJsonApi['id']; | |
} else { | |
// For relationship PATCH operations, the `id` field need not be present in the data body | |
$exception = empty($this->_controller()->getRequest()->getParam('id')); | |
} | |
} | |
if ($exception) { | |
throw new BadRequestException( | |
'URL id does not match request data id as required for JSON API PATCH actions' | |
); | |
} | |
$this->_controller()->setRequest($this->_controller()->getRequest()->withParsedBody($decodedJsonApi)); | |
} | |
/** | |
* Converts (already json_decoded) request data array in JSON API document | |
* format to CakePHP format so it be processed as usual. Should only be | |
* used with already validated data/document or things will break. | |
* | |
* Please note that decoding hasMany relationships has not yet been implemented. | |
* @overriden in order to cast emtpy nodes as arrays (see @customised) | |
* | |
* @param array $document Request data document array | |
* @return array | |
*/ | |
protected function _convertJsonApiDocumentArray(array $document): array | |
{ | |
$result = []; | |
// convert primary resource | |
if (array_key_exists('id', $document['data'])) { | |
$result['id'] = $document['data']['id']; | |
} | |
if (array_key_exists('attributes', $document['data'])) { | |
$result = array_merge_recursive($result, $document['data']['attributes']); | |
// dasherize all attribute keys directly below the primary resource if need be | |
if ($this->getConfig('inflect') === 'dasherize') { | |
foreach ($result as $key => $value) { | |
$underscoredKey = Inflector::underscore($key); | |
if (!array_key_exists($underscoredKey, $result)) { | |
$result[$underscoredKey] = $value; | |
unset($result[$key]); | |
} | |
} | |
} | |
} | |
// no further action if there are no relationships | |
if (!array_key_exists('relationships', $document['data'])) { | |
return $result; | |
} | |
// translate relationships into CakePHP array format | |
foreach ($document['data']['relationships'] as $key => $details) { | |
if ($this->getConfig('inflect') === 'dasherize') { | |
$key = Inflector::underscore($key); // e.g. currency, national-capitals | |
} | |
// allow empty/null data node as per the JSON API specification | |
if (empty( | |
// @customised: cast as array to avoid any problems with parsing issues when converting between JavaScript and PHP (q.v. comments on routes.php::jsonIterator) | |
(array)$details['data']) | |
) { | |
continue; | |
} | |
// handle belongsTo relationships | |
if (!isset($details['data'][0])) { | |
$belongsToForeignKey = $key . '_id'; | |
$belongsToId = $details['data']['id']; | |
$result[$belongsToForeignKey] = $belongsToId; | |
continue; | |
} | |
// handle hasMany relationships | |
if (isset($details['data'][0])) { | |
$relationResults = []; | |
foreach ($details['data'] as $relationData) { | |
$relationResult = []; | |
if (array_key_exists('id', $relationData)) { | |
$relationResult['id'] = $relationData['id']; | |
} | |
if (array_key_exists('attributes', $relationData)) { | |
$relationResult = array_merge_recursive($relationResult, $relationData['attributes']); | |
// dasherize attribute keys if need be | |
if ($this->getConfig('inflect') === 'dasherize') { | |
foreach ($relationResult as $resultKey => $value) { | |
$underscoredKey = Inflector::underscore($resultKey); | |
if (!array_key_exists($underscoredKey, $relationResult)) { | |
$relationResult[$underscoredKey] = $value; | |
unset($relationResult[$resultKey]); | |
} | |
} | |
} | |
} | |
$relationResults[] = $relationResult; | |
} | |
$result[$key] = $relationResults; | |
} | |
} | |
return $result; | |
} | |
} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment