diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 7a7903e3..e732e1a8 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -10,15 +10,15 @@ on: jobs: testsuite: - runs-on: ubuntu-22.04 + runs-on: ubuntu-24.04 strategy: fail-fast: false matrix: - php-version: [ '7.4', '8.0', '8.1' ] + php-version: [ '8.1', '8.2', '8.3', '8.4' ] db-type: [ mysql ] prefer-lowest: [''] include: - - php-version: '7.4' + - php-version: '8.1' db-type: 'mysql' prefer-lowest: 'prefer-lowest' @@ -49,7 +49,7 @@ jobs: run: echo "::set-output name=date::$(date +'%Y-%m')" - name: Cache composer dependencies - uses: actions/cache@v3 + uses: actions/cache@v4 with: path: ${{ steps.composer-cache.outputs.dir }} key: ${{ runner.os }}-composer-${{ steps.key-date.outputs.date }}-${{ hashFiles('composer.json') }} @@ -62,27 +62,14 @@ jobs: composer update fi - - name: Configure PHPUnit matcher - if: matrix.php-version == '7.4' && matrix.db-type == 'mysql' - uses: mheap/phpunit-matcher-action@v1 - - name: Run PHPUnit run: | - if [[ ${{ matrix.db-type }} == 'mysql' && ${{ matrix.php-version }} != '7.2' ]]; then export DB_URL='mysql://root:root@127.0.0.1/cakephp?init[]=SET sql_mode = "STRICT_TRANS_TABLES,NO_ZERO_IN_DATE,NO_ZERO_DATE,ERROR_FOR_DIVISION_BY_ZERO,NO_ENGINE_SUBSTITUTION"'; fi - if [[ ${{ matrix.db-type }} == 'mysql' && ${{ matrix.php-version }} == '7.2' ]]; then export DB_URL='mysql://root:root@127.0.0.1/cakephp?encoding=utf8'; fi - if [[ ${{ matrix.db-type }} == 'mysql' && ${{ matrix.php-version }} == '7.4' ]]; then - vendor/bin/phpunit --coverage-clover=coverage.xml --verbose - else - vendor/bin/phpunit - fi - - - name: Code Coverage Report - if: success() && matrix.php-version == '7.4' && matrix.db-type == 'mysql' - uses: codecov/codecov-action@v3 + export DB_URL='mysql://root:root@127.0.0.1/cakephp?init[]=SET sql_mode = "STRICT_TRANS_TABLES,NO_ZERO_IN_DATE,NO_ZERO_DATE,ERROR_FOR_DIVISION_BY_ZERO,NO_ENGINE_SUBSTITUTION"' + vendor/bin/phpunit --display-incomplete --display-skipped --display-warnings --display-deprecations coding-standard: name: Coding Standard - runs-on: ubuntu-22.04 + runs-on: ubuntu-24.04 steps: - uses: actions/checkout@v3 @@ -90,7 +77,7 @@ jobs: - name: Setup PHP uses: shivammathur/setup-php@v2 with: - php-version: '7.4' + php-version: '8.1' extensions: mbstring, intl, apcu coverage: none @@ -103,7 +90,7 @@ jobs: run: echo "::set-output name=date::$(date +'%Y-%m')" - name: Cache composer dependencies - uses: actions/cache@v3 + uses: actions/cache@v4 with: path: ${{ steps.composer-cache.outputs.dir }} key: ${{ runner.os }}-composer-${{ steps.key-date.outputs.date }}-${{ hashFiles('composer.json') }} @@ -116,7 +103,7 @@ jobs: static-analysis: name: Static Analysis - runs-on: ubuntu-22.04 + runs-on: ubuntu-24.04 steps: - uses: actions/checkout@v3 @@ -124,7 +111,7 @@ jobs: - name: Setup PHP uses: shivammathur/setup-php@v2 with: - php-version: '7.4' + php-version: '8.1' extensions: mbstring, intl, apcu coverage: none tools: cs2pr @@ -138,7 +125,7 @@ jobs: run: echo "::set-output name=date::$(date +'%Y-%m')" - name: Cache composer dependencies - uses: actions/cache@v1 + uses: actions/cache@v4 with: path: ${{ steps.composer-cache.outputs.dir }} key: ${{ runner.os }}-composer-${{ steps.key-date.outputs.date }}-${{ hashFiles('composer.json') }} @@ -146,8 +133,5 @@ jobs: - name: composer install run: composer stan-setup - - name: Run psalm - run: vendor/bin/psalm.phar --output-format=github - - name: Run phpstan run: vendor/bin/phpstan.phar analyse --error-format=checkstyle ./src | cs2pr diff --git a/.gitignore b/.gitignore index 5209c916..ad935574 100644 --- a/.gitignore +++ b/.gitignore @@ -5,3 +5,4 @@ vendor/ composer.lock tmp .phpunit.result.cache +.phpunit.cache diff --git a/composer.json b/composer.json index 5fc999b4..cb6df793 100644 --- a/composer.json +++ b/composer.json @@ -29,17 +29,17 @@ } ], "require": { - "php": ">=7.4.0", - "cakephp/cakephp": "^4.4.1", - "friendsofcake/crud": "^6.0", - "laravel-json-api/neomerx-json-api": "^5.0" + "php": ">=8.1.0", + "cakephp/cakephp": "^5.0.0", + "friendsofcake/crud": "^7.2.0", + "laravel-json-api/neomerx-json-api": "^5.0.2" }, "require-dev": { - "phpunit/phpunit": "~8.5 || ^9.3", - "friendsofcake/cakephp-test-utilities": "^2.0.1", - "friendsofcake/search": "^6.2.2", - "cakephp/cakephp-codesniffer": "^4.0", - "dms/phpunit-arraysubset-asserts": "^0.4.0" + "phpunit/phpunit": "^10.5.5 || ^11.1.3 || ^12.0.9", + "friendsofcake/cakephp-test-utilities": "^3.0.0", + "friendsofcake/search": "^7.0.0", + "cakephp/cakephp-codesniffer": "^5.1", + "dms/phpunit-arraysubset-asserts": "^0.5.0" }, "autoload": { "psr-4": { @@ -65,13 +65,8 @@ "scripts": { "cs-check": "phpcs -p --standard=vendor/cakephp/cakephp-codesniffer/CakePHP src/ tests/", "cs-fix": "phpcbf --standard=vendor/cakephp/cakephp-codesniffer/CakePHP src/ tests/", - "stan-setup": "cp composer.json composer.backup && composer require --dev phpstan/phpstan:~1.8.0 psalm/phar:~4.26.0 && mv composer.backup composer.json", + "stan-setup": "cp composer.json composer.backup && composer require --dev phpstan/phpstan:~2.1.17 && mv composer.backup composer.json", "phpstan": "phpstan analyse --memory-limit=3G src/", - "psalm": "psalm.phar --show-info=false", - "stan": [ - "@phpstan", - "@psalm" - ], "test": "phpunit" }, "config": { diff --git a/phpstan-baseline.neon b/phpstan-baseline.neon index c8716fa2..8c2e6a5a 100644 --- a/phpstan-baseline.neon +++ b/phpstan-baseline.neon @@ -9,8 +9,3 @@ parameters: message: "#^Access to an undefined property Cake\\\\Controller\\\\Controller\\:\\:\\$Crud\\.$#" count: 1 path: src/Listener/PaginationListener.php - - - - message: "#^Call to an undefined method Cake\\\\Datasource\\\\EntityInterface\\:\\:visibleProperties\\(\\)\\.$#" - count: 1 - path: src/Schema/JsonApi/DynamicEntitySchema.php diff --git a/phpstan.neon b/phpstan.neon index e03ea496..458597eb 100644 --- a/phpstan.neon +++ b/phpstan.neon @@ -5,9 +5,10 @@ parameters: level: 6 paths: - src - checkMissingIterableValueType: false - checkGenericClassInNonGenericObjectType: false universalObjectCratesClasses: - Crud\Event\Subject bootstrapFiles: - vendor/cakephp/cakephp/src/Core/Exception/CakeException.php + ignoreErrors: + - identifier: missingType.iterableValue + - identifier: missingType.generics diff --git a/phpunit.xml.dist b/phpunit.xml.dist index 69dcc090..bea061dc 100644 --- a/phpunit.xml.dist +++ b/phpunit.xml.dist @@ -1,28 +1,24 @@ + - - - - ./tests/ - - - - - - - - - - - - - - ./src/ - ./src/ - - - + xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + colors="true" + processIsolation="false" + stopOnFailure="false" + bootstrap="./tests/bootstrap.php" + xsi:noNamespaceSchemaLocation="https://schema.phpunit.de/10.5/phpunit.xsd" + cacheDirectory=".phpunit.cache" +> + + + + + + ./tests/ + + + + + ./src/ + + diff --git a/psalm.xml b/psalm.xml deleted file mode 100644 index a889113f..00000000 --- a/psalm.xml +++ /dev/null @@ -1,19 +0,0 @@ - - - - - - - - - - - - - diff --git a/src/Action/RelationshipsAction.php b/src/Action/RelationshipsAction.php index 93a9d6d9..6e435223 100644 --- a/src/Action/RelationshipsAction.php +++ b/src/Action/RelationshipsAction.php @@ -21,6 +21,8 @@ use Crud\Traits\SerializeTrait; use Crud\Traits\ViewTrait; use Crud\Traits\ViewVarTrait; +use function Cake\Core\pluginSplit; +use function Cake\I18n\__; /** * Class RelationshipViewAction @@ -47,7 +49,7 @@ class RelationshipsAction extends BaseAction * * @var array */ - protected $_defaultConfig = [ + protected array $_defaultConfig = [ 'enabled' => true, 'scope' => 'entity', 'findMethod' => 'all', @@ -150,7 +152,7 @@ public function checkAllowed(): void protected function _findRelations(Subject $subject): EntityInterface { $relationName = $this->_request()->getParam('type'); - $table = $this->_table(); + $table = $this->_controller()->fetchTable(); $association = $table->getAssociation($relationName); $targetTable = $association->getTarget(); @@ -188,7 +190,7 @@ protected function _findRelations(Subject $subject): EntityInterface ->where( [ $table->aliasField($primaryKey) => $foreignKeyParam, - ] + ], ) ->contain([ $relationName => [ @@ -202,7 +204,7 @@ protected function _findRelations(Subject $subject): EntityInterface 'association' => $association, 'repository' => $table, 'query' => $primaryQuery, - ] + ], ); $this->_trigger('beforeFind', $subject); $entity = $subject->query->first(); @@ -239,7 +241,7 @@ protected function _get(): void * * @return void */ - protected function _delete() + protected function _delete(): void { $subject = $this->_subject(); $request = $this->_request(); @@ -272,7 +274,7 @@ protected function _delete() $idsToDelete = (array)Hash::extract($data, '{n}.id'); $foreignRecords = $entity->$property; $entity->$property = []; - foreach ($foreignRecords as $key => $foreignRecord) { + foreach ($foreignRecords as $foreignRecord) { if (!in_array($foreignRecord->id, $idsToDelete, false)) { $entity->{$property}[] = $foreignRecord; } @@ -284,7 +286,7 @@ protected function _delete() $association->setSaveStrategy('replace'); } $saveMethod = $this->saveMethod(); - if ($this->_table()->$saveMethod($entity, $this->saveOptions())) { + if ($this->_controller()->fetchTable()->$saveMethod($entity, $this->saveOptions())) { $this->_success($subject); return; @@ -298,7 +300,7 @@ protected function _delete() * * @return void */ - protected function _post() + protected function _post(): void { $subject = $this->_subject(); $request = $this->_request(); @@ -333,7 +335,7 @@ protected function _post() $this->_trigger('beforeSave', $subject); $saveMethod = $this->saveMethod(); - if ($this->_table()->$saveMethod($entity, $this->saveOptions())) { + if ($this->_controller()->fetchTable()->$saveMethod($entity, $this->saveOptions())) { $this->_success($subject); return; @@ -347,7 +349,7 @@ protected function _post() * * @return void */ - protected function _patch() + protected function _patch(): void { $subject = $this->_subject(); $request = $this->_request(); @@ -363,7 +365,6 @@ protected function _patch() if (in_array($association->type(), [Association::MANY_TO_ONE, Association::ONE_TO_ONE], true)) { //Set the relationship to the corresponding entity - /** @psalm-suppress TypeDoesNotContainNull */ if (array_key_exists('id', $data)) { $entity->{$property} = $foreignTable->get($data['id']); } elseif ($data === null) { @@ -382,7 +383,7 @@ protected function _patch() $association->setSaveStrategy('replace'); } $saveMethod = $this->saveMethod(); - if ($this->_table()->$saveMethod($entity, $this->saveOptions())) { + if ($this->_controller()->fetchTable()->$saveMethod($entity, $this->saveOptions())) { $this->_success($subject); return; @@ -442,7 +443,7 @@ protected function getForeignRecords(array $data, Association $association): arr ->where( [ $association->aliasField($associationPrimaryKey) . ' in' => $idsToAdd, - ] + ], ) ->all(); @@ -450,13 +451,13 @@ protected function getForeignRecords(array $data, Association $association): arr $foundIds = $foreignRecords->extract( static function ($record) { return $record->id; - } + }, ) ->toArray(); $missingIds = array_diff($idsToAdd, $foundIds); throw new RecordNotFoundException( - __('Not all requested records could be found. Missing IDs are {0}', implode(', ', $missingIds)) + __('Not all requested records could be found. Missing IDs are {0}', implode(', ', $missingIds)), ); } diff --git a/src/Action/ViewAction.php b/src/Action/ViewAction.php index 316d3c46..b9f726d2 100644 --- a/src/Action/ViewAction.php +++ b/src/Action/ViewAction.php @@ -17,7 +17,7 @@ class ViewAction extends BaseViewAction * @return void * @throws \Exception */ - protected function _handle(?string $id = null): void + protected function _handle(string|int|null $id = null): void { $request = $this->_request(); $from = $request->getParam('from'); @@ -44,16 +44,16 @@ protected function _handle(?string $id = null): void */ protected function _findRecordViaRelated(Subject $subject): EntityInterface { - $repository = $this->_table(); + $repository = $this->_controller()->fetchTable(); - [$finder, $options] = $this->_extractFinder(); - $query = $repository->find($finder, $options); + [$finder] = $this->_extractFinder(); + $query = $repository->find($finder); $subject->set( [ 'repository' => $repository, 'query' => $query, - ] + ], ); $this->_trigger('beforeFind', $subject); diff --git a/src/Error/JsonApiExceptionRenderer.php b/src/Error/JsonApiExceptionRenderer.php index 0986cdc8..7ced2810 100644 --- a/src/Error/JsonApiExceptionRenderer.php +++ b/src/Error/JsonApiExceptionRenderer.php @@ -5,13 +5,15 @@ use Cake\Controller\Controller; use Cake\Core\Configure; -use Cake\Core\Exception\Exception; +use Cake\Core\Exception\CakeException; use Cake\Error\Debugger; use Cake\Http\Response; +use Cake\Http\ServerRequest; use Cake\Utility\Inflector; use Crud\Error\Exception\ValidationException; use Crud\Error\ExceptionRenderer; use Crud\Listener\ApiQueryLogListener; +use Exception; use Laminas\Diactoros\Stream; use Neomerx\JsonApi\Encoder\Encoder; use Neomerx\JsonApi\Schema\Error; @@ -28,10 +30,11 @@ class JsonApiExceptionRenderer extends ExceptionRenderer /** * Method used for all non-validation errors. * - * @param string $template Name of template to use (ignored for jsonapi) + * @param string $template + * @param bool $skipControllerCheck * @return \Cake\Http\Response */ - protected function _outputMessage(string $template): Response + protected function _outputMessage(string $template, bool $skipControllerCheck = false): Response { if (!$this->controller->getRequest()->accepts('application/vnd.api+json')) { return parent::_outputMessage($template); @@ -57,8 +60,8 @@ protected function _outputMessage(string $template): Response $status, $code = null, $title, - $detail - ) + $detail, + ), ); $encoder = Encoder::instance(); @@ -85,7 +88,7 @@ protected function _outputMessage(string $template): Response * Method used for rendering 422 validation used for both CakePHP entity * validation errors and JSON API (request data) documents. * - * @param \Crud\Error\Exception\ValidationException $error Exception + * @param \Crud\Error\Exception\ValidationException $error Exception * @return \Cake\Http\Response */ public function validation(ValidationException $error): Response @@ -98,7 +101,7 @@ public function validation(ValidationException $error): Response try { $this->controller->setResponse($this->controller->getResponse()->withStatus($status)); - } catch (\Exception $e) { + } catch (Exception $e) { $status = 422; $this->controller->setResponse($this->controller->getResponse()->withStatus($status)); } @@ -131,7 +134,7 @@ public function validation(ValidationException $error): Response * - returning cloaked collection as passed down from the Listener * - creating a new collection from CakePHP validation errors * - * @param array $validationErrors CakePHP validation errors + * @param array $validationErrors CakePHP validation errors * @return \Neomerx\JsonApi\Schema\ErrorCollection */ protected function _getNeoMerxErrorCollection(array $validationErrors): ErrorCollection @@ -157,7 +160,7 @@ protected function _getNeoMerxErrorCollection(array $validationErrors): ErrorCol $idx = null, $aboutLink = null, $code = null, - $meta = null + $meta = null, ); } @@ -167,7 +170,7 @@ protected function _getNeoMerxErrorCollection(array $validationErrors): ErrorCol /** * Adds top-level `debug` node to a json encoded string * - * @param string $json Json encoded string + * @param string $json Json encoded string * @return string Json encoded string with added debug node */ protected function _addDebugNode(string $json): string @@ -185,7 +188,7 @@ protected function _addDebugNode(string $json): string [ 'format' => 'array', 'args' => false, - ] + ], ); $result = json_decode($json, true); @@ -193,7 +196,7 @@ protected function _addDebugNode(string $json): string try { return (string)json_encode($result, JSON_PRETTY_PRINT); - } catch (Exception $e) { + } catch (CakeException $e) { $result['debug']['message'] = $e->getMessage(); $result['debug']['trace'] = [ 'error' => 'Unable to encode stack trace', @@ -206,7 +209,7 @@ protected function _addDebugNode(string $json): string /** * Add top-level `query` node if ApiQueryLogListener is loaded. * - * @param string $json Json encoded string + * @param string $json Json encoded string * @return string Json encoded string */ protected function _addQueryLogsNode(string $json): string @@ -231,7 +234,7 @@ protected function _addQueryLogsNode(string $json): string */ protected function _getApiQueryLogListenerObject(): ApiQueryLogListener { - return new ApiQueryLogListener(new Controller()); + return new ApiQueryLogListener(new Controller(new ServerRequest())); } /** @@ -242,7 +245,7 @@ protected function _getApiQueryLogListenerObject(): ApiQueryLogListener * Note: we need this function because Cake's built-in rules don't pass * through `_processRules()` function in the Validator. * - * @param array $errors CakePHP validation errors + * @param array $errors CakePHP validation errors * @return array Standardized array */ protected function _standardizeValidationErrors(array $errors = []): array diff --git a/src/Listener/JsonApi/DocumentRelationshipValidator.php b/src/Listener/JsonApi/DocumentRelationshipValidator.php index 37b156de..d58b4f7f 100644 --- a/src/Listener/JsonApi/DocumentRelationshipValidator.php +++ b/src/Listener/JsonApi/DocumentRelationshipValidator.php @@ -67,7 +67,7 @@ protected function _primaryDataMayBeNullEmptyArrayObjectOrArray(): bool $detail = "Related records are missing member 'type' or 'id'", $status = null, $idx = null, - $aboutLink = $this->_getAboutLink('http://jsonapi.org/format/' . $about) + $aboutLink = $this->_getAboutLink('http://jsonapi.org/format/' . $about), ); return false; diff --git a/src/Listener/JsonApi/DocumentValidator.php b/src/Listener/JsonApi/DocumentValidator.php index 16ce452c..7e1d789b 100644 --- a/src/Listener/JsonApi/DocumentValidator.php +++ b/src/Listener/JsonApi/DocumentValidator.php @@ -28,25 +28,25 @@ class DocumentValidator extends stdClass * * @var array $_document */ - protected $_document; + protected array $_document; /** * @var \Neomerx\JsonApi\Schema\ErrorCollection */ - protected $_errorCollection; + protected ErrorCollection $_errorCollection; /** * JsonApiListener config() options * * @var array */ - protected $_config; + protected array $_config; /** * Constructor * - * @param array $documentArray Decoded JSON API document - * @param array $listenerConfig JsonApiListener config() options + * @param array $documentArray Decoded JSON API document + * @param array $listenerConfig JsonApiListener config() options * @return void */ public function __construct(array $documentArray, array $listenerConfig) @@ -127,8 +127,8 @@ protected function _documentMustHavePrimaryData(): bool $detail = "Document does not contain top-level member 'data'", $source = [ 'pointer' => '', - ] - ) + ], + ), ); throw new ValidationException($this->_getErrorCollectionEntity()); @@ -139,7 +139,7 @@ protected function _documentMustHavePrimaryData(): bool * * @return bool */ - protected function _primaryDataMustHaveType() + protected function _primaryDataMustHaveType(): bool { $path = $this->_getPathObject('data.type'); @@ -149,7 +149,7 @@ protected function _primaryDataMustHaveType() $detail = "Primary data does not contain member 'type'", $status = null, $idx = null, - $aboutLink = $this->_getAboutLink('http://jsonapi.org/format/#crud-creating') + $aboutLink = $this->_getAboutLink('http://jsonapi.org/format/#crud-creating'), ); return false; @@ -166,7 +166,7 @@ protected function _primaryDataMustHaveType() $details = "Primary data member 'type' is not a string", $status = null, $idx = null, - $aboutLink = $this->_getAboutLink('http://jsonapi.org/format/#document-resource-object-identification') + $aboutLink = $this->_getAboutLink('http://jsonapi.org/format/#document-resource-object-identification'), ); return false; @@ -187,7 +187,7 @@ protected function _primaryDataMustHaveId(): bool $detail = "Primary data does not contain member 'id'", $status = null, $idx = null, - $aboutLink = $this->_getAboutLink('http://jsonapi.org/format/#crud-updating') + $aboutLink = $this->_getAboutLink('http://jsonapi.org/format/#crud-updating'), ); return false; @@ -204,7 +204,7 @@ protected function _primaryDataMustHaveId(): bool $details = "Primary data member 'id' is not a string", $status = null, $idx = null, - $aboutLink = $this->_getAboutLink('http://jsonapi.org/format/#document-resource-object-identification') + $aboutLink = $this->_getAboutLink('http://jsonapi.org/format/#document-resource-object-identification'), ); return false; @@ -234,7 +234,7 @@ protected function _primaryDataMayHaveUuid(): bool $details = "Primary data member 'id' is not a valid UUID", $status = null, $idx = null, - $aboutLink = $this->_getAboutLink('http://jsonapi.org/format/#crud-creating-client-ids') + $aboutLink = $this->_getAboutLink('http://jsonapi.org/format/#crud-creating-client-ids'), ); return false; @@ -262,7 +262,7 @@ protected function _primaryDataMayHaveRelationships(): bool $detail = 'Relationships object does not contain any members', $status = null, $idx = null, - $aboutLink = $this->_getAboutLink('http://jsonapi.org/format/#crud-creating') + $aboutLink = $this->_getAboutLink('http://jsonapi.org/format/#crud-creating'), ); return false; @@ -304,10 +304,10 @@ protected function _primaryDataMayHaveRelationships(): bool /** * Ensures a relationship object has a 'data' member. * - * @param string|\stdClass $path Dot separated path of relationship object or path object + * @param \stdClass|string $path Dot separated path of relationship object or path object * @return bool */ - protected function _relationshipMustHaveData($path): bool + protected function _relationshipMustHaveData(string|stdClass $path): bool { $path = $this->_getPathObject($path); @@ -321,7 +321,7 @@ protected function _relationshipMustHaveData($path): bool $detail = "Relationships object does not contain member 'data'", $status = null, $idx = null, - $aboutLink = $this->_getAboutLink('http://jsonapi.org/format/#crud-creating') + $aboutLink = $this->_getAboutLink('http://jsonapi.org/format/#crud-creating'), ); return false; @@ -331,10 +331,10 @@ protected function _relationshipMustHaveData($path): bool * Checks if relationship object has 'data' member set to null which is * allowed by the JSON API spec. * - * @param string|\stdClass $path Dot separated path of relationship object or path object + * @param \stdClass|string $path Dot separated path of relationship object or path object * @return bool */ - protected function _relationshipDataIsNull($path): bool + protected function _relationshipDataIsNull(string|stdClass $path): bool { $path = $this->_getPathObject($path); @@ -344,11 +344,11 @@ protected function _relationshipDataIsNull($path): bool /** * Ensures a relationship data has a 'type' member. * - * @param string $relationship Singular or plural relationship name - * @param string|\stdClass $path Dot separated path of relationship object or path object + * @param string $relationship Singular or plural relationship name + * @param \stdClass|string $path Dot separated path of relationship object or path object * @return bool */ - protected function _relationshipDataMustHaveType($relationship, $path): bool + protected function _relationshipDataMustHaveType(string $relationship, string|stdClass $path): bool { $path = $this->_getPathObject($path); @@ -372,7 +372,7 @@ protected function _relationshipDataMustHaveType($relationship, $path): bool $detail = "Relationship data does not contain member 'type'", $status = null, $idx = null, - $aboutLink = $this->_getAboutLink('http://jsonapi.org/format/#crud-creating') + $aboutLink = $this->_getAboutLink('http://jsonapi.org/format/#crud-creating'), ); return false; @@ -388,7 +388,7 @@ protected function _relationshipDataMustHaveType($relationship, $path): bool $detail = "Relationship data member 'type' is not a string", $status = null, $idx = null, - $aboutLink = $this->_getAboutLink('http://jsonapi.org/format/#crud-creating') + $aboutLink = $this->_getAboutLink('http://jsonapi.org/format/#crud-creating'), ); return false; @@ -400,11 +400,11 @@ protected function _relationshipDataMustHaveType($relationship, $path): bool /** * Ensures relationship data has an 'id' member. * - * @param string $relationship Singular or plural relationship name - * @param string|\stdClass $path Dot separated path of relationship object or path object + * @param string $relationship Singular or plural relationship name + * @param \stdClass|string $path Dot separated path of relationship object or path object * @return bool */ - protected function _relationshipDataMustHaveId(string $relationship, $path): bool + protected function _relationshipDataMustHaveId(string $relationship, string|stdClass $path): bool { $path = $this->_getPathObject($path); @@ -428,7 +428,7 @@ protected function _relationshipDataMustHaveId(string $relationship, $path): boo $detail = "Relationship data does not contain member 'id'", $status = null, $idx = null, - $aboutLink = $this->_getAboutLink('http://jsonapi.org/format/#crud-creating') + $aboutLink = $this->_getAboutLink('http://jsonapi.org/format/#crud-creating'), ); return false; @@ -444,7 +444,7 @@ protected function _relationshipDataMustHaveId(string $relationship, $path): boo $detail = "Relationship data member 'type' is not a string", $status = null, $idx = null, - $aboutLink = $this->_getAboutLink('http://jsonapi.org/format/#crud-creating') + $aboutLink = $this->_getAboutLink('http://jsonapi.org/format/#crud-creating'), ); return false; @@ -456,10 +456,10 @@ protected function _relationshipDataMustHaveId(string $relationship, $path): boo /** * Checks if a document property is a string. * - * @param string|\stdClass $path Dot separated path of the property + * @param \stdClass|string $path Dot separated path of the property * @return bool */ - protected function _isString($path): bool + protected function _isString(string|stdClass $path): bool { $path = $this->_getPathObject($path); @@ -479,11 +479,11 @@ protected function _isString($path): bool /** * Checks if a document property is a valid UUID. * - * @param string|\stdClass $path Dot separated path of the property + * @param \stdClass|string $path Dot separated path of the property * @return bool * @throws \Crud\Error\Exception\CrudException */ - protected function _isUuid($path): bool + protected function _isUuid(string|stdClass $path): bool { $path = $this->_getPathObject($path); @@ -504,10 +504,10 @@ protected function _isUuid($path): bool * Checks if document contains a given property (even when value * is `false` or `null`). * - * @param string|\stdClass $path Dot separated path of the property or a path object + * @param \stdClass|string $path Dot separated path of the property or a path object * @return bool */ - protected function _hasProperty($path): bool + protected function _hasProperty(string|stdClass $path): bool { if (is_a($path, 'stdClass')) { $path = $path->dotted; @@ -530,11 +530,11 @@ protected function _hasProperty($path): bool /** * Returns the value for a given document property. * - * @param string|\stdClass $path Dot separated path of the property or path object + * @param \stdClass|string $path Dot separated path of the property or path object * @throws \Crud\Error\Exception\CrudException * @return mixed */ - protected function _getProperty($path) + protected function _getProperty(string|stdClass $path): mixed { if (is_a($path, 'stdClass')) { $path = $path->dotted; @@ -560,10 +560,10 @@ protected function _getProperty($path) * Helper method to create an object with consistent path strings from * given dot separated path. * - * @param string|\stdClass $path Dot separated path or stdClass $path object + * @param \stdClass|string $path Dot separated path or stdClass $path object * @return \stdClass */ - protected function _getPathObject($path): \stdClass + protected function _getPathObject(string|stdClass $path): stdClass { // return as-is if parameter is if (is_a($path, 'stdClass')) { @@ -594,7 +594,7 @@ protected function _getPathObject($path): \stdClass /** * Helper method that displays aboutLink only if enabled in Listener config. * - * @param string $url URL + * @param string $url URL * @return \Neomerx\JsonApi\Schema\Link|null */ protected function _getAboutLink(string $url): ?Link @@ -623,7 +623,7 @@ protected function _getErrorCollectionEntity(): Entity 'CrudJsonApiListener' => [ 'NeoMerxErrorCollection' => $this->_errorCollection, ], - ] + ], ); return $entity; @@ -632,10 +632,10 @@ protected function _getErrorCollectionEntity(): Entity /** * Helper function to determine if string is singular or plural. * - * @param string $string Preferably a CakePHP generated name. + * @param string $string Preferably a CakePHP generated name. * @return bool */ - protected function _stringIsSingular($string): bool + protected function _stringIsSingular(string $string): bool { return Inflector::singularize($string) === $string; } diff --git a/src/Listener/JsonApiListener.php b/src/Listener/JsonApiListener.php index 1609c4bd..e994e296 100644 --- a/src/Listener/JsonApiListener.php +++ b/src/Listener/JsonApiListener.php @@ -6,7 +6,6 @@ use Cake\Collection\CollectionInterface; 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; @@ -15,7 +14,7 @@ use Cake\Http\Response; use Cake\ORM\Association; use Cake\ORM\Locator\LocatorAwareTrait; -use Cake\ORM\Query; +use Cake\ORM\Query\SelectQuery; use Cake\ORM\ResultSet; use Cake\ORM\Table; use Cake\Utility\Hash; @@ -27,6 +26,7 @@ use CrudJsonApi\Listener\JsonApi\DocumentRelationshipValidator; use CrudJsonApi\Listener\JsonApi\DocumentValidator; use InvalidArgumentException; +use function Cake\Core\pluginSplit; /** * Extends Crud ApiListener to respond in JSON API format. @@ -46,7 +46,7 @@ class JsonApiListener extends ApiListener * * @var array */ - protected $_defaultConfig = [ + protected array $_defaultConfig = [ 'detectors' => [ 'jsonapi' => ['ext' => false, 'accept' => [self::MIME_TYPE]], ], @@ -81,7 +81,7 @@ class JsonApiListener extends ApiListener * * @var bool */ - protected $_ControllerHasSetContain; + protected bool $_ControllerHasSetContain = false; /** * Returns a list of all events that will fire in the controller during its lifecycle. @@ -121,7 +121,7 @@ public function implementedEvents(): array * * Called before the crud action is executed. * - * @param \Cake\Event\EventInterface $event Event + * @param \Cake\Event\EventInterface $event Event * @return void */ public function beforeHandle(EventInterface $event): void @@ -136,27 +136,19 @@ public function beforeHandle(EventInterface $event): void * a single primary resource. Does NOT execute when either a Controller has set `contain` or the * `?include=` query parameter was passed because that would override/break previously generated data. * - * @param \Cake\Event\EventInterface $event Event - * @return null + * @param \Cake\Event\EventInterface $event Event + * @return void */ - public function afterFind(EventInterface $event) + public function afterFind(EventInterface $event): void { if (!$this->_request()->is('get')) { - return null; + return; } // set property so we can check inside `_renderWithResources()` if (!empty($event->getSubject()->query->getContain())) { $this->_ControllerHasSetContain = true; - - return null; - } - - if ($this->getConfig('include')) { - return null; } - - return null; } /** @@ -164,7 +156,7 @@ public function afterFind(EventInterface $event) * to prevent them from sending `hasMany` relationships not belonging to this primary resource * when PATCHing. * - * @param \Cake\Event\EventInterface $event Event + * @param \Cake\Event\EventInterface $event Event * @return void * @throws \Cake\Http\Exception\BadRequestException */ @@ -195,23 +187,19 @@ public function beforeSave(EventInterface $event): void if ($this->_request()->getMethod() === 'POST') { throw new BadRequestException( 'JSON API 1.1 does not support sideposting ' . - '(hasMany relationships detected in the request body)' + '(hasMany relationships detected in the request body)', ); } // hasMany found in the entity, extract ids from the request data $primaryResourceId = $this->_controller()->getRequest()->getData('id'); - /** - * @var array $hasManyIds -*/ + /** @var array $hasManyIds */ $hasManyIds = Hash::extract($this->_controller()->getRequest()->getData($key), '{n}.id'); $hasManyTable = $this->getTableLocator()->get($associationName); // query database only for hasMany that match both passed id and the id of the primary resource - /** - * @var string $entityForeignKey -*/ + /** @var string $entityForeignKey */ $entityForeignKey = $hasManyTable->getAssociation($entity->getSource())->getForeignKey(); $primaryKey = current((array)$hasManyTable->getPrimaryKey()); $query = $hasManyTable->find() @@ -220,14 +208,14 @@ public function beforeSave(EventInterface $event): void [ $entityForeignKey => $primaryResourceId, $primaryKey . ' IN' => $hasManyIds, - ] + ], ); // throw an exception if number of database records does not exactly matches passed ids if (count($hasManyIds) !== $query->count()) { throw new BadRequestException( "One or more of the provided relationship ids for - $associationName do not exist in the database" + $associationName do not exist in the database", ); } @@ -243,18 +231,22 @@ public function beforeSave(EventInterface $event): void /** * afterSave() event. * - * @param \Cake\Event\EventInterface $event Event - * @return bool|null + * @param \Cake\Event\EventInterface $event Event + * @return void */ - public function afterSave(EventInterface $event): ?bool + public function afterSave(EventInterface $event): void { if (!$event->getSubject()->success) { - return false; + $event->setResult(false); + + return; } // `created` will be set for add actions, `id` for edit actions if (!$event->getSubject()->created && !$event->getSubject()->id) { - return false; + $event->setResult(false); + + return; } // The `add`action (new Resource) MUST respond with HTTP Status Code 201, @@ -264,12 +256,10 @@ public function afterSave(EventInterface $event): ?bool } /** - * @var \Crud\Event\Subject $subject -*/ + * @var \Crud\Event\Subject $subject + */ $subject = $event->getSubject(); $this->render($subject); - - return null; } /** @@ -279,27 +269,27 @@ public function afterSave(EventInterface $event): ?bool * only meta node after a successful delete as well but this has not * been implemented here yet. http://jsonapi.org/format/#crud-deleting * - * @param \Cake\Event\EventInterface $event Event - * @return bool|null + * @param \Cake\Event\EventInterface $event Event + * @return void */ - public function afterDelete(EventInterface $event) + public function afterDelete(EventInterface $event): void { if (!$event->getSubject()->success) { - return false; + $event->setResult(false); + + return; } $this->_controller()->setResponse($this->_controller()->getResponse()->withStatus(204)); - - return null; } /** * beforeRedirect() event used to stop the event and thus redirection. * - * @param \Cake\Event\EventInterface $event Event + * @param \Cake\Event\EventInterface $event Event * @return void */ - public function beforeRedirect(EventInterface $event) + public function beforeRedirect(EventInterface $event): void { $event->stopPropagation(); } @@ -318,11 +308,11 @@ protected function _checkIsRelationshipsRequest(): bool } /** - * @param \Cake\ORM\Table $repository Repository - * @param string $include The association include path + * @param \Cake\ORM\Table $repository Repository + * @param string $include The association include path * @return \Cake\ORM\Association|null */ - protected function _getAssociation(Table $repository, $include): ?Association + protected function _getAssociation(Table $repository, string $include): ?Association { //We refer to associations by their property names, so we try that first $propertyName = Inflector::underscore($include); @@ -355,16 +345,21 @@ protected function _getAssociation(Table $repository, $include): ?Association /** * Takes a "include" string and converts it into a correct CakePHP ORM association alias * - * @param array $includes The relationships to include - * @param array|bool $denyList Denied includes - * @param array|bool $allowList Allowed includes - * @param \Cake\ORM\Table|null $repository The repository - * @param array $path Include path + * @param array $includes The relationships to include + * @param array|bool $denyList Denied includes + * @param array|bool $allowList Allowed includes + * @param \Cake\ORM\Table|null $repository The repository + * @param array $path Include path * @return array * @throws \Cake\Http\Exception\BadRequestException */ - protected function _parseIncludes($includes, $denyList, $allowList, ?Table $repository = null, $path = []): array - { + protected function _parseIncludes( + array $includes, + array|bool $denyList, + array|bool $allowList, + ?Table $repository = null, + array $path = [], + ): array { $wildcard = implode('.', array_merge($path, ['*'])); $wildcardAllowList = Hash::get((array)$allowList, $wildcard); $wildcardDenyList = Hash::get((array)$denyList, $wildcard); @@ -396,7 +391,7 @@ protected function _parseIncludes($includes, $denyList, $allowList, ?Table $repo $association = $this->_getAssociation($repository, $include); if ($association === null) { throw new BadRequestException( - "Invalid relationship path '{$includeDotPath}' supplied in include parameter" + "Invalid relationship path '{$includeDotPath}' supplied in include parameter", ); } } @@ -407,7 +402,7 @@ protected function _parseIncludes($includes, $denyList, $allowList, ?Table $repo $denyList, $allowList, $association ? $association->getTarget() : null, - $includePath + $includePath, ); } @@ -428,12 +423,12 @@ protected function _parseIncludes($includes, $denyList, $allowList, ?Table $repo * * Supported options is "allowList" and "Blacklist" * - * @param string|array $includes The query data - * @param \Crud\Event\Subject $subject The subject - * @param array $options Array of options for includes. + * @param array|string $includes The query data + * @param \Crud\Event\Subject $subject The subject + * @param array $options Array of options for includes. * @return void */ - protected function _includeParameter($includes, Subject $subject, $options): void + protected function _includeParameter(string|array $includes, Subject $subject, array $options): void { if (is_string($includes)) { $includes = explode(',', $includes); @@ -471,15 +466,15 @@ protected function _includeParameter($includes, Subject $subject, $options): voi /** * Parses out fields query parameter and apply it to the query * - * @param string|array|null $fieldSets The query data - * @param \Crud\Event\Subject $subject The subject - * @param array $options Array of options for includes. + * @param array|string|null $fieldSets The query data + * @param \Crud\Event\Subject $subject The subject + * @param array $options Array of options for includes. * @return void */ - protected function _fieldSetsParameter($fieldSets, Subject $subject, $options): void + protected function _fieldSetsParameter(string|array|null $fieldSets, Subject $subject, array $options): void { // could be null for e.g. using integration tests - if ($fieldSets === null) { + if (empty($fieldSets)) { return; } @@ -489,7 +484,7 @@ protected function _fieldSetsParameter($fieldSets, Subject $subject, $options): static function ($val) { return explode(',', $val); }, - (array)$fieldSets + (array)$fieldSets, ); $repository = $subject->query->getRepository(); @@ -518,7 +513,7 @@ static function ($val) use ($repository, $columns) { return $repository->aliasField($val); }, - $fields + $fields, ); $selectFields[] = array_filter($aliasFields); } @@ -543,7 +538,7 @@ static function ($val) use ($repository, $columns) { /** * BeforeFind event listener to parse any supplied query parameters * - * @param \Cake\Event\EventInterface $event Event + * @param \Cake\Event\EventInterface $event Event * @return void */ public function beforeFind(EventInterface $event): void @@ -552,16 +547,16 @@ public function beforeFind(EventInterface $event): void $queryParameters = Hash::merge( $this->getConfig('queryParameters'), [ - 'sort' => [ - 'callable' => [$this, '_sortParameter'], - ], - 'include' => [ - 'callable' => [$this, '_includeParameter'], - ], - 'fields' => [ - 'callable' => [$this, '_fieldSetsParameter'], + 'sort' => [ + 'callable' => [$this, '_sortParameter'], + ], + 'include' => [ + 'callable' => [$this, '_includeParameter'], + ], + 'fields' => [ + 'callable' => [$this, '_fieldSetsParameter'], + ], ], - ] ); /** @var \Crud\Event\Subject $subject */ @@ -578,7 +573,7 @@ public function beforeFind(EventInterface $event): void throw new InvalidArgumentException('Invalid callable supplied for query parameter ' . $parameter); } - $options['callable']($this->_request()->getQuery($parameter), $subject, $options); + $options['callable']($this->_request()->getQuery($parameter, ''), $subject, $options); } $this->_fetchRelated($subject); @@ -591,7 +586,7 @@ public function beforeFind(EventInterface $event): void */ protected function checkValidReverseAssociation( Association $forwardAssociation, - Association $reverseAssociation + Association $reverseAssociation, ): bool { $reverseAssociationTarget = $reverseAssociation->getTarget(); $forwardAssociationSource = $forwardAssociation->getSource(); @@ -704,14 +699,14 @@ protected function _fetchRelated(Subject $subject): void $associationKeys = $repository->associations()->keys(); $subject->query - ->matching($reverseAssociation->getName(), static function (Query $query) use ( + ->matching($reverseAssociation->getName(), static function (SelectQuery $query) use ( $reverseAssociation, - $foreignKeyValue + $foreignKeyValue, ) { return $query ->where([ $reverseAssociation->aliasField( - current((array)$reverseAssociation->getPrimaryKey()) + current((array)$reverseAssociation->getPrimaryKey()), ) => $foreignKeyValue, ]); }) @@ -730,13 +725,13 @@ protected function _fetchRelated(Subject $subject): void /** * Add 'sort' capability * - * @see http://jsonapi.org/format/#fetching-sorting - * @param string|array $sortFields Field sort request - * @param \Crud\Event\Subject $subject The subject - * @param array $options Array of options for includes. + * @see http://jsonapi.org/format/#fetching-sorting + * @param array|string $sortFields Field sort request + * @param \Crud\Event\Subject $subject The subject + * @param array $options Array of options for includes. * @return void */ - protected function _sortParameter($sortFields, Subject $subject, $options): void + protected function _sortParameter(string|array $sortFields, Subject $subject, array $options): void { if (is_string($sortFields)) { $sortFields = explode(',', $sortFields); @@ -783,7 +778,7 @@ protected function _sortParameter($sortFields, Subject $subject, $options): void ], 'strategy' => 'select', ], - ] + ], ); $subject->query->leftJoinWith($association->getAlias()); @@ -794,13 +789,13 @@ protected function _sortParameter($sortFields, Subject $subject, $options): void $order[$repository->aliasField($sortField)] = $direction; } } - $subject->query->order($order); + $subject->query->orderBy($order); } /** * Set required viewVars before rendering the JsonApiView. * - * @param \Crud\Event\Subject $subject Subject + * @param \Crud\Event\Subject $subject Subject * @return \Cake\Http\Response */ public function render(Subject $subject): Response @@ -866,7 +861,7 @@ protected function _renderWithIdentifiers(Subject $subject): Response 'serialize' => true, 'association' => $association, 'inflect' => $this->getConfig('inflect'), - ] + ], ); return $this->_controller() @@ -876,12 +871,12 @@ protected function _renderWithIdentifiers(Subject $subject): Response /** * Renders a JSON API response with top-level data node holding resource(s). * - * @param \Crud\Event\Subject $subject Subject + * @param \Crud\Event\Subject $subject Subject * @return \Cake\Http\Response */ protected function _renderWithResources(Subject $subject): Response { - $repository = $this->_controller()->loadModel(); // Default model class + $repository = $this->_controller()->fetchTable(); // Default model class $usedAssociations = []; if (isset($subject->query)) { @@ -921,7 +916,7 @@ protected function _renderWithResources(Subject $subject): Response 'include' => $include, 'repositories' => $this->_getRepositoryList( $repository, - $usedAssociations + $usedAssociations, ), ]); $this->_controller()->set([ @@ -945,7 +940,7 @@ protected function _validateConfigOptions(): void !is_array($this->getConfig('withJsonApiVersion')) ) { throw new CrudException( - 'JsonApiListener configuration option `withJsonApiVersion` only accepts a boolean or an array' + 'JsonApiListener configuration option `withJsonApiVersion` only accepts a boolean or an array', ); } } @@ -956,7 +951,7 @@ protected function _validateConfigOptions(): void if (!is_bool($this->getConfig('absoluteLinks'))) { throw new CrudException( - 'JsonApiListener configuration option `absoluteLinks` only accepts a boolean' + 'JsonApiListener configuration option `absoluteLinks` only accepts a boolean', ); } @@ -999,7 +994,7 @@ protected function _checkRequestMethods(): void if ($this->_request()->contentType() !== self::MIME_TYPE) { throw new BadRequestException( - 'JSON API requests with data require the "' . self::MIME_TYPE . '" Content-Type header' + 'JSON API requests with data require the "' . self::MIME_TYPE . '" Content-Type header', ); } } @@ -1007,10 +1002,10 @@ protected function _checkRequestMethods(): void /** * Deduplicate resultset from rows that might have come from joins * - * @param \Crud\Event\Subject $subject Subject + * @param \Crud\Event\Subject $subject Subject * @return \Cake\Datasource\ResultSetInterface */ - protected function _deduplicateResultSet($subject): ResultSetInterface + protected function _deduplicateResultSet(Subject $subject): ResultSetInterface { $ids = []; $entities = []; @@ -1023,24 +1018,17 @@ protected function _deduplicateResultSet($subject): ResultSetInterface } } - if ($subject->entities instanceof ResultSet) { - $resultSet = clone $subject->entities; - $resultSet->unserialize(serialize($entities)); - } else { - $resultSet = new ResultSetDecorator($entities); - } - - return $resultSet; + return new ResultSet($entities); } /** * Helper function to easily retrieve `find()` result from Crud subject * regardless of current action. * - * @param \Crud\Event\Subject $subject Subject - * @return \Cake\Datasource\EntityInterface|\Cake\Datasource\EntityInterface[]|\Cake\ORM\ResultSet Single Entity or ORM\ResultSet + * @param \Crud\Event\Subject $subject Subject + * @return \Cake\Datasource\EntityInterface|\Cake\Datasource\ResultSetInterface|array<\Cake\Datasource\EntityInterface>|string Single Entity or ORM\ResultSet */ - protected function _getFindResult(Subject $subject) + protected function _getFindResult(Subject $subject): EntityInterface|ResultSetInterface|array|string { if (!empty($subject->entities)) { if (isset($subject->query)) { @@ -1057,13 +1045,12 @@ protected function _getFindResult(Subject $subject) * Helper function to easily retrieve a single entity from Crud subject * find result regardless of current action. * - * @param \Crud\Event\Subject $subject Subject + * @param \Crud\Event\Subject $subject Subject * @return \Cake\Datasource\EntityInterface|null */ protected function _getSingleEntity(Subject $subject): ?EntityInterface { - if (!empty($subject->entities) && $subject->entities instanceof Query) { - /** @psalm-suppress InvalidReturnStatement */ + if (!empty($subject->entities) && $subject->entities instanceof SelectQuery) { return (clone $subject->entities)->first(); } @@ -1078,14 +1065,14 @@ protected function _getSingleEntity(Subject $subject): ?EntityInterface return $subject->entities->first(); } - return $subject->entity; + return $subject->entity ?? null; } /** * Creates a nested array of all associations used in the query * - * @param \Cake\Datasource\RepositoryInterface $repository Repository - * @param array $contains Array of contained associations + * @param \Cake\Datasource\RepositoryInterface $repository Repository + * @param array $contains Array of contained associations * @return array Array with \Cake\ORM\AssociationCollection */ protected function _getContainedAssociations(RepositoryInterface $repository, array $contains): array @@ -1118,7 +1105,7 @@ protected function _getContainedAssociations(RepositoryInterface $repository, ar if (!empty($nestedContains)) { $associations[$associationKey]['children'] = $this->_getContainedAssociations( $association->getTarget(), - $nestedContains + $nestedContains, ); } } @@ -1131,8 +1118,8 @@ protected function _getContainedAssociations(RepositoryInterface $repository, ar * query) in the find result from the entity's AssociationCollection to * prevent `null` entries appearing in the json api `relationships` node. * - * @param \Cake\Datasource\RepositoryInterface $repository Repository - * @param \Cake\Datasource\EntityInterface $entity Entity + * @param \Cake\Datasource\RepositoryInterface $repository Repository + * @param \Cake\Datasource\EntityInterface $entity Entity * @return array */ protected function _extractEntityAssociations(RepositoryInterface $repository, EntityInterface $entity): array @@ -1163,8 +1150,8 @@ protected function _extractEntityAssociations(RepositoryInterface $repository, E * Get a flat list of all repositories indexed by their registry alias. * * @param \Cake\Datasource\RepositoryInterface $repository Current repository - * @param array $associations Nested associations to get repository from - * @return array Used repositories indexed by registry alias + * @param array $associations Nested associations to get repository from + * @return array Used repositories indexed by registry alias * @internal */ protected function _getRepositoryList(RepositoryInterface $repository, array $associations): array @@ -1197,8 +1184,8 @@ protected function _getRepositoryList(RepositoryInterface $repository, array $as * `included` node in the json response UNLESS user has specified listener * config option 'include'. * - * @param array $associations Array with \Cake\ORM\AssociationCollection(s) - * @param bool $last Is this the "top-level"/entry point for the recursive function + * @param array $associations Array with \Cake\ORM\AssociationCollection(s) + * @param bool $last Is this the "top-level"/entry point for the recursive function * @return array * @throws \InvalidArgumentException */ @@ -1257,7 +1244,7 @@ protected function _checkRequestData(): void throw new BadRequestException( 'Missing request data required for POST and PATCH methods, ' . 'as well as DELETE methods to relationship endpoints. ' . - 'Make sure that you are sending a request body and that it is valid JSON.' + 'Make sure that you are sending a request body and that it is valid JSON.', ); } @@ -1271,7 +1258,6 @@ protected function _checkRequestData(): void $validator->validateCreateDocument(); } - /** @psalm-suppress TypeDoesNotContainType */ if ($requestMethod === 'PATCH' || $requestMethod === 'DELETE') { $validator->validateUpdateDocument(); } @@ -1289,7 +1275,7 @@ protected function _checkRequestData(): void if ($exception) { throw new BadRequestException( - 'URL id does not match request data id as required for JSON API PATCH actions' + 'URL id does not match request data id as required for JSON API PATCH actions', ); } @@ -1300,16 +1286,13 @@ protected function _checkRequestData(): void * Returns a flat array list with the names of all associations for the given * repository (Or the default for the controller), optionally limited to only matching associationTypes. * - * @param \Cake\ORM\Table|null $table Table - * @param array $associationTypes Array with any combination of Cake\ORM\Association types + * @param \Cake\ORM\Table|null $table Table + * @param array $associationTypes Array with any combination of Cake\ORM\Association types * @return array */ protected function _getAssociationsList(?Table $table, array $associationTypes = []): array { - $table = $table ?: $this->_table(); - if (!$table instanceof Table) { - return []; - } + $table = $table ?: $this->_controller()->fetchTable(); $associations = $table->associations(); @@ -1335,7 +1318,7 @@ protected function _getAssociationsList(?Table $table, array $associationTypes = * * Please note that decoding hasMany relationships has not yet been implemented. * - * @param array $document Request data document array + * @param array $document Request data document array * @return array */ protected function _convertJsonApiDocumentArray(array $document): array diff --git a/src/Listener/PaginationListener.php b/src/Listener/PaginationListener.php index 1028150e..b9c1fd94 100644 --- a/src/Listener/PaginationListener.php +++ b/src/Listener/PaginationListener.php @@ -3,6 +3,7 @@ namespace CrudJsonApi\Listener; +use Cake\Datasource\Paging\PaginatedInterface; use Cake\Event\EventInterface; use Cake\Routing\Router; use Crud\Listener\ApiPaginationListener as BaseListener; @@ -38,29 +39,37 @@ public function implementedEvents(): array /** * Appends the pagination information to the JSON or XML output * - * @param \Cake\Event\EventInterface $event Event + * @param \Cake\Event\EventInterface $event Event * @return void */ public function beforeRender(EventInterface $event): void { - $paging = $this->_request()->getAttribute('paging'); + $viewVar = 'data'; + $action = $this->_action(); - if (empty($paging)) { - return; + if (method_exists($action, 'viewVar')) { + $viewVar = $action->viewVar(); } - $pagination = current($paging); - if (empty($pagination)) { + $paginatedResultset = $this + ->_controller() + ->viewBuilder() + ->getVar($viewVar); + + if (!$paginatedResultset instanceof PaginatedInterface) { return; } - $this->_controller->viewBuilder()->setOption('pagination', $this->_getJsonApiPaginationResponse($pagination)); + $this + ->_controller() + ->viewBuilder() + ->setOption('pagination', $this->_getJsonApiPaginationResponse($paginatedResultset->pagingParams())); } /** * Generates pagination viewVars with JSON API compatible hyperlinks. * - * @param array $pagination CakePHP pagination result + * @param array $pagination CakePHP pagination result * @return array */ protected function _getJsonApiPaginationResponse(array $pagination): array @@ -72,7 +81,7 @@ protected function _getJsonApiPaginationResponse(array $pagination): array 'page' => null, 'limit' => null, ], - $pagination + $pagination, ); $request = $this->_request(); @@ -86,7 +95,6 @@ protected function _getJsonApiPaginationResponse(array $pagination): array $query['sort'] = $request->getQuery('sort'); } - /** @psalm-suppress UndefinedMagicPropertyFetch */ $fullBase = (bool)$this->_controller()->Crud->getConfig('listeners.jsonApi.absoluteLinks'); $baseUrl = $request->getAttributes()['params']; @@ -94,42 +102,42 @@ protected function _getJsonApiPaginationResponse(array $pagination): array $self = Router::url( $baseUrl + [ - '?' => ['page' => $pagination['page']] + $query, + '?' => ['page' => $pagination['currentPage']] + $query, ], - $fullBase + $fullBase, ); $first = Router::url( $baseUrl + [ '?' => ['page' => 1] + $query, ], - $fullBase + $fullBase, ); $last = Router::url( $baseUrl + [ '?' => ['page' => $pagination['pageCount']] + $query, ], - $fullBase + $fullBase, ); $prev = null; - if ($pagination['prevPage']) { + if ($pagination['hasPrevPage']) { $prev = Router::url( $baseUrl + [ - '?' => ['page' => $pagination['page'] - 1] + $query, + '?' => ['page' => $pagination['currentPage'] - 1] + $query, ], - $fullBase + $fullBase, ); } $next = null; - if ($pagination['nextPage']) { + if ($pagination['hasNextPage']) { $next = Router::url( $baseUrl + [ - '?' => ['page' => $pagination['page'] + 1] + $query, + '?' => ['page' => $pagination['currentPage'] + 1] + $query, ], - $fullBase + $fullBase, ); } @@ -139,7 +147,7 @@ protected function _getJsonApiPaginationResponse(array $pagination): array 'last' => $last, 'prev' => $prev, 'next' => $next, - 'record_count' => $pagination['count'], + 'record_count' => $pagination['totalCount'], 'page_count' => $pagination['pageCount'], 'page_limit' => $pagination['limit'], ]; diff --git a/src/Listener/SearchListener.php b/src/Listener/SearchListener.php index a91c001d..fd6f1b41 100644 --- a/src/Listener/SearchListener.php +++ b/src/Listener/SearchListener.php @@ -4,7 +4,6 @@ namespace CrudJsonApi\Listener; use Cake\Event\EventInterface; -use Cake\ORM\Table; use Crud\Listener\BaseListener; use RuntimeException; @@ -15,7 +14,7 @@ class SearchListener extends BaseListener * * @var array */ - protected $_defaultConfig = [ + protected array $_defaultConfig = [ 'enabled' => [ 'Crud.beforeLookup', 'Crud.beforePaginate', @@ -40,7 +39,7 @@ public function implementedEvents(): array /** * Inject search conditions into the query object. * - * @param \Cake\Event\EventInterface $event Event + * @param \Cake\Event\EventInterface $event Event * @return void */ public function injectSearch(EventInterface $event): void @@ -49,13 +48,13 @@ public function injectSearch(EventInterface $event): void return; } - $repository = $this->_table(); - if ($repository instanceof Table && !$repository->behaviors()->has('Search')) { + $repository = $this->_controller()->fetchTable(); + if (!$repository->behaviors()->has('Search')) { throw new RuntimeException( sprintf( 'Missing Search.Search behavior on %s', - get_class($repository) - ) + get_class($repository), + ), ); } diff --git a/src/Plugin.php b/src/Plugin.php index 036dff65..dd08c99f 100644 --- a/src/Plugin.php +++ b/src/Plugin.php @@ -13,7 +13,7 @@ class Plugin extends BasePlugin /** * Plugin name * - * @var string + * @var ?string */ - protected $name = 'CrudJsonApi'; + protected ?string $name = 'CrudJsonApiPlugin'; } diff --git a/src/Route/JsonApiRoutes.php b/src/Route/JsonApiRoutes.php index 6a1c4e76..274e5bce 100644 --- a/src/Route/JsonApiRoutes.php +++ b/src/Route/JsonApiRoutes.php @@ -10,6 +10,8 @@ use Cake\Routing\RouteBuilder; use Cake\Utility\Hash; use Cake\Utility\Inflector; +use function Cake\Core\pluginSplit; +use function in_array; /** * Class RouteBuilder @@ -38,7 +40,7 @@ private static function inflect(string $string): string private static function buildRelationshipLink( RouteBuilder $routeBuilder, Association $association, - array $options + array $options, ): void { $type = $association->getName(); @@ -85,7 +87,7 @@ private static function buildRelationshipLink( $routeBuilder->connect( '/relationships/' . $path, - $base + ['_method' => $method] + $base + ['_method' => $method], ); } } @@ -96,8 +98,11 @@ private static function buildRelationshipLink( * @param array $options Array of options * @return void */ - private static function buildAssociationLinks(RouteBuilder $routeBuilder, Association $association, array $options) - { + private static function buildAssociationLinks( + RouteBuilder $routeBuilder, + Association $association, + array $options, + ): void { $name = $association->getName(); if (in_array($name, $options['ignoredAssociations'], true)) { @@ -111,10 +116,10 @@ private static function buildAssociationLinks(RouteBuilder $routeBuilder, Associ $from = $association->getSource()->getRegistryAlias(); $plugin = $routeBuilder->params()['plugin'] ?? null; - $isOne = \in_array( + $isOne = in_array( $association->type(), [Association::MANY_TO_ONE, Association::ONE_TO_ONE], - true + true, ); $pathName = self::inflect($association->getProperty()); @@ -133,8 +138,8 @@ static function (RouteBuilder $routeBuilder) use ( $name, $isOne, $from, - $controller - ) { + $controller, + ): void { $routeBuilder->connect( '/' . $pathName, [ @@ -146,9 +151,9 @@ static function (RouteBuilder $routeBuilder) use ( ], [ '_name' => "CrudJsonApi.{$from}:{$name}", - ] + ], ); - } + }, ); return; @@ -165,7 +170,7 @@ static function (RouteBuilder $routeBuilder) use ( ], [ '_name' => "CrudJsonApi.{$from}:{$name}", - ] + ], ); } @@ -181,7 +186,7 @@ public static function mapModels(array $models, RouteBuilder $routeBuilder): voi $plugin = $routeBuilder->params()['plugin'] ?? null; - $routeBuilder->scope('/', function (RouteBuilder $routeBuilder) use ($models, $plugin, $locator) { + $routeBuilder->scope('/', function (RouteBuilder $routeBuilder) use ($models, $plugin, $locator): void { $routeBuilder->namePrefix(''); foreach ($models as $model => $options) { @@ -210,7 +215,7 @@ public static function mapModels(array $models, RouteBuilder $routeBuilder): voi $associations = $tableObject->associations(); if ($options['allowedAssociations'] !== false) { - $callback = function (RouteBuilder $routeBuilder) use ($associations, $options) { + $callback = function (RouteBuilder $routeBuilder) use ($associations, $options): void { /** @var \Cake\ORM\Association $association */ foreach ($associations as $association) { self::buildAssociationLinks($routeBuilder, $association, $options); @@ -223,7 +228,7 @@ public static function mapModels(array $models, RouteBuilder $routeBuilder): voi $routeBuilder->resources( $model, $options, - $callback + $callback, ); } }); diff --git a/src/Schema/JsonApi/DynamicEntitySchema.php b/src/Schema/JsonApi/DynamicEntitySchema.php index 1064f8c5..623564de 100644 --- a/src/Schema/JsonApi/DynamicEntitySchema.php +++ b/src/Schema/JsonApi/DynamicEntitySchema.php @@ -5,6 +5,7 @@ use Cake\Core\App; use Cake\Datasource\EntityInterface; +use Cake\Datasource\RepositoryInterface; use Cake\ORM\Association; use Cake\ORM\Locator\LocatorAwareTrait; use Cake\ORM\Table; @@ -20,6 +21,9 @@ use Neomerx\JsonApi\Contracts\Schema\LinkInterface; use Neomerx\JsonApi\Schema\BaseSchema; use Neomerx\JsonApi\Schema\Identifier; +use RuntimeException; +use function Cake\Core\pluginSplit; +use function in_array; /** * Licensed under The MIT License @@ -35,11 +39,11 @@ class DynamicEntitySchema extends BaseSchema * * @var \Cake\View\View */ - protected $view; + protected View $view; /** * @var \Cake\ORM\Table */ - protected $repository; + protected Table $repository; /** * Class constructor @@ -51,7 +55,7 @@ class DynamicEntitySchema extends BaseSchema public function __construct( FactoryInterface $factory, View $view, - Table $repository + Table $repository, ) { $this->view = $view; $this->repository = $repository; @@ -63,7 +67,7 @@ public function __construct( * @param \Cake\ORM\Table $repository The repository object * @return mixed */ - protected function getTypeFromRepository(Table $repository) + protected function getTypeFromRepository(Table $repository): mixed { $repositoryName = App::shortName(get_class($repository), 'Model/Table', 'Table'); [, $entityName] = pluginSplit($repositoryName); @@ -82,26 +86,26 @@ public function getType(): string /** * Get resource id. * - * @param \Cake\ORM\Entity $resource Entity + * @param \Cake\ORM\Entity $resource Entity * @return string - * @psalm-suppress MoreSpecificImplementedParamType + * @phpcs:disable SlevomatCodingStandard.TypeHints.ParameterTypeHint.MissingNativeTypeHint */ public function getId($resource): ?string { $primaryKey = $this->repository->getPrimaryKey(); if (is_array($primaryKey)) { - throw new \RuntimeException('Crud-Json-Api does not support composite keys out of the box.'); + throw new RuntimeException('Crud-Json-Api does not support composite keys out of the box.'); } return (string)$resource->get($primaryKey); } /** - * @param \Cake\Datasource\EntityInterface $entity Entity + * @param \Cake\Datasource\EntityInterface $entity Entity * @return \Cake\ORM\Table */ - protected function getRepository($entity = null): Table + protected function getRepository(?EntityInterface $entity = null): Table { if (!$entity) { return $this->repository; @@ -118,16 +122,13 @@ protected function getRepository($entity = null): Table * * This method will ignore any properties that are entities. * - * @param \Cake\Datasource\EntityInterface $entity Entity + * @param \Cake\Datasource\EntityInterface $entity Entity * @return array */ - protected function entityToShallowArray(EntityInterface $entity) + protected function entityToShallowArray(EntityInterface $entity): array { $result = []; - /** @psalm-suppress UndefinedInterfaceMethod */ - $properties = method_exists($entity, 'getVisible') - ? $entity->getVisible() - : $entity->visibleProperties(); + $properties = $entity->getVisible(); foreach ($properties as $property) { if ($property[0] === '_') { continue; @@ -156,7 +157,7 @@ protected function entityToShallowArray(EntityInterface $entity) * @param \Cake\Datasource\EntityInterface $resource Entity * @param \Neomerx\JsonApi\Contracts\Schema\ContextInterface $context The Context * @return array - * @psalm-suppress MoreSpecificImplementedParamType + * @phpcs:disable SlevomatCodingStandard.TypeHints.ParameterTypeHint.MissingNativeTypeHint */ public function getAttributes($resource, ContextInterface $context): iterable { @@ -197,10 +198,10 @@ public function getAttributes($resource, ContextInterface $context): iterable * * JSON API optional `related` links not implemented yet. * - * @param \Cake\Datasource\EntityInterface $resource Entity object + * @param \Cake\Datasource\EntityInterface $resource Entity object * @param \Neomerx\JsonApi\Contracts\Schema\ContextInterface $context The Context * @return array - * @psalm-suppress MoreSpecificImplementedParamType + * @phpcs:disable SlevomatCodingStandard.TypeHints.ParameterTypeHint.MissingNativeTypeHint */ public function getRelationships($resource, ContextInterface $context): iterable { @@ -258,7 +259,7 @@ public function getRelationships($resource, ContextInterface $context): iterable if (!$data && !is_array($foreignKey)) { $data = new Identifier( (string)$resource->get($foreignKey), - $this->getTypeFromRepository($association->getTarget()) + $this->getTypeFromRepository($association->getTarget()), ); } @@ -275,9 +276,9 @@ public function getRelationships($resource, ContextInterface $context): iterable /** * NeoMerx override used to generate `self` links * - * @param \Cake\ORM\Entity|null $resource Entity, null only to be compatible with the Neomerx method + * @param \Cake\ORM\Entity|null $resource Entity, null only to be compatible with the Neomerx method * @return string - * @psalm-suppress MoreSpecificImplementedParamType + * @phpcs:disable SlevomatCodingStandard.TypeHints.ParameterTypeHint.MissingNativeTypeHint */ public function getSelfSubUrl($resource = null): string { @@ -292,7 +293,7 @@ public function getSelfSubUrl($resource = null): string '_method' => 'GET', 'action' => 'view', ], - $this->view->getConfig('absoluteLinks', false) + $this->view->getConfig('absoluteLinks', false), ); } @@ -321,14 +322,14 @@ protected function getAssociationByProperty(string $name): ?Association * @param \Cake\Datasource\EntityInterface $resource Entity * @param string $name Relationship name in lowercase singular or plural * @return \Neomerx\JsonApi\Contracts\Schema\LinkInterface - * @psalm-suppress MoreSpecificImplementedParamType + * @phpcs:disable SlevomatCodingStandard.TypeHints.ParameterTypeHint.MissingNativeTypeHint */ public function getRelationshipSelfLink($resource, string $name): LinkInterface { $association = $this->getAssociationByProperty($name); if (!$association) { throw new InvalidArgumentException( - sprintf('Invalid association for resource %s: %s', get_class($resource), $name) + sprintf('Invalid association for resource %s: %s', get_class($resource), $name), ); } @@ -346,7 +347,7 @@ public function getRelationshipSelfLink($resource, string $name): LinkInterface 'from' => $from, 'type' => $type, ], - $this->view->getConfig('absoluteLinks', false) + $this->view->getConfig('absoluteLinks', false), ); return $this->getFactory()->createLink(false, $url, false); @@ -361,7 +362,7 @@ public function getRelationshipSelfLink($resource, string $name): LinkInterface * @param \Cake\Datasource\EntityInterface $resource Entity * @param string $name Relationship name in lowercase singular or plural * @return \Neomerx\JsonApi\Contracts\Schema\LinkInterface - * @psalm-suppress MoreSpecificImplementedParamType + * @phpcs:disable SlevomatCodingStandard.TypeHints.ParameterTypeHint.MissingNativeTypeHint */ public function getRelationshipRelatedLink($resource, string $name): LinkInterface { @@ -375,10 +376,10 @@ public function getRelationshipRelatedLink($resource, string $name): LinkInterfa ['controller' => $controllerName] = $this->_getRepositoryRoutingParameters($this->getRepository()); $sourceName = Inflector::underscore(Inflector::singularize($controllerName)); - $isOne = \in_array( + $isOne = in_array( $association->type(), [Association::MANY_TO_ONE, Association::ONE_TO_ONE], - true + true, ); $baseRoute = $this->_getRepositoryRoutingParameters($relatedRepository) + [ @@ -399,7 +400,7 @@ public function getRelationshipRelatedLink($resource, string $name): LinkInterfa try { $url = Router::url( $route, - $this->view->getConfig('absoluteLinks', false) + $this->view->getConfig('absoluteLinks', false), ); } catch (MissingRouteException $e) { //This means that the JSON:API recommended route is missing. We need to try something else. @@ -424,7 +425,7 @@ public function getRelationshipRelatedLink($resource, string $name): LinkInterfa $url = Router::url( $baseRoute + $keys, - $this->view->getConfig('absoluteLinks', false) + $this->view->getConfig('absoluteLinks', false), ); } @@ -436,10 +437,10 @@ public function getRelationshipRelatedLink($resource, string $name): LinkInterfa * Parses the name of an Entity class to build a lowercase plural * controller name to be used in links. * - * @param \Cake\Datasource\RepositoryInterface $repository Repository + * @param \Cake\Datasource\RepositoryInterface $repository Repository * @return array Array holding lowercase controller name as the value */ - protected function _getRepositoryRoutingParameters($repository) + protected function _getRepositoryRoutingParameters(RepositoryInterface $repository): array { $repositoryName = App::shortName(get_class($repository), 'Model/Table', 'Table'); [$pluginName, $controllerName] = pluginSplit($repositoryName); diff --git a/src/View/JsonApiView.php b/src/View/JsonApiView.php index c81d56ad..0e66bbc1 100644 --- a/src/View/JsonApiView.php +++ b/src/View/JsonApiView.php @@ -5,6 +5,7 @@ use Cake\Core\App; use Cake\Core\Configure; +use Cake\Core\InstanceConfigTrait; use Cake\Event\EventManager; use Cake\Http\Response; use Cake\Http\ServerRequest; @@ -17,11 +18,16 @@ use Neomerx\JsonApi\Contracts\Schema\LinkInterface; use Neomerx\JsonApi\Encoder\Encoder; use Neomerx\JsonApi\Schema\Link; +use RuntimeException; +use function Cake\Core\pluginSplit; class JsonApiView extends View { use InflectTrait; + # For BC, re-use trait since Cake 5.0.0 declares `getConfig()` protected in its `View::class`. + use InstanceConfigTrait; + /** * Constructor * @@ -34,7 +40,7 @@ public function __construct( ?ServerRequest $request = null, ?Response $response = null, ?EventManager $eventManager = null, - array $viewOptions = [] + array $viewOptions = [], ) { parent::__construct($request, $response, $eventManager, $viewOptions); @@ -73,11 +79,11 @@ protected function _getSpecialVars(): array * - with empty body * - with body containing only the meta node * - * @param string|null $template Name of view file to use - * @param string|false|null $layout Layout to use. + * @param string|null $template Name of view file to use + * @param string|false|null $layout Layout to use. * @return string */ - public function render(?string $template = null, $layout = null): string + public function render(?string $template = null, string|false|null $layout = null): string { if ($this->getConfig('association')) { $json = $this->_encodeWithIdentifiers(); @@ -130,8 +136,8 @@ function ($link) { return new Link(false, $link, false); }, - $links - ) + $links, + ), ); } @@ -141,7 +147,7 @@ function ($link) { /** * Generates a JSON API string without resource(s). * - * @return null|string + * @return string|null */ protected function _encodeWithoutSchemas(): ?string { @@ -260,7 +266,7 @@ protected function _encodeWithSchemas(): string * 2. custom dynamic schema * 3. Crud's dynamic schema * - * @param \Cake\ORM\Table[] $repositories List holding repositories used to map entities to schema classes + * @param array<\Cake\ORM\Table> $repositories List holding repositories used to map entities to schema classes * @throws \Crud\Error\Exception\CrudException * @return array A list with Entity class names as key holding NeoMerx Closure object */ @@ -278,7 +284,7 @@ protected function _entitiesToNeoMerxSchema(array $repositories): array throw new CrudException(sprintf( 'Entity classes must not be the generic "%s" class for repository "%s"', $entityClass, - $repositoryName + $repositoryName, )); } @@ -313,7 +319,7 @@ protected function _entitiesToNeoMerxSchema(array $repositories): array //Otherwise something is horribly wrong if (!$schemaClass) { - throw new \RuntimeException('No valid schema classes found'); + throw new RuntimeException('No valid schema classes found'); } // Uses NeoMerx createSchemaFromClosure()` to generate Closure @@ -332,10 +338,10 @@ protected function _entitiesToNeoMerxSchema(array $repositories): array /** * Returns an array with NeoMerx Link objects to be used for pagination. * - * @param array $pagination ApiPaginationListener pagination response + * @param array $pagination ApiPaginationListener pagination response * @return array */ - protected function _getPaginationLinks($pagination): array + protected function _getPaginationLinks(array $pagination): array { $links = []; @@ -365,23 +371,23 @@ protected function _getPaginationLinks($pagination): array /** * Returns data to be serialized. * - * @param array|string|bool|object $serialize The name(s) of the view variable(s) that + * @param object|array|string|bool $serialize The name(s) of the view variable(s) that * need(s) to be serialized. If true all available view variables will be used. * @return mixed The data to serialize. */ - protected function _getDataToSerializeFromViewVars($serialize = true) + protected function _getDataToSerializeFromViewVars(array|string|bool|object $serialize = true): mixed { if (is_object($serialize)) { throw new CrudException( 'Assigning an object to JsonApiListener "serialize" is deprecated, ' . - 'assign the object to its own variable and assign "serialize" = true instead.' + 'assign the object to its own variable and assign "serialize" = true instead.', ); } if ($serialize === true) { $viewVars = array_diff( $this->getVars(), - $this->_getSpecialVars() + $this->_getSpecialVars(), ); if (empty($viewVars)) { @@ -404,7 +410,7 @@ protected function _getDataToSerializeFromViewVars($serialize = true) * * @return int Flag holding json options */ - protected function _jsonOptions() + protected function _jsonOptions(): int { $jsonOptions = 0; @@ -434,7 +440,7 @@ protected function _jsonOptions() * * @return void */ - protected function _inflectIncludesViewVar() + protected function _inflectIncludesViewVar(): void { $inflect = $this->getConfig('inflect'); $include = $this->getConfig('include'); diff --git a/tests/Fixture/CountriesFixture.php b/tests/Fixture/CountriesFixture.php index 11a5da08..3c047dc9 100644 --- a/tests/Fixture/CountriesFixture.php +++ b/tests/Fixture/CountriesFixture.php @@ -5,18 +5,7 @@ class CountriesFixture extends TestFixture { - public $fields = [ - 'id' => ['type' => 'integer'], - 'code' => ['type' => 'string', 'length' => 2, 'null' => false], - 'name' => ['type' => 'string', 'length' => 255, 'null' => false], - 'dummy_counter' => ['type' => 'integer'], - 'currency_id' => ['type' => 'integer', 'null' => true], - 'national_capital_id' => ['type' => 'integer', 'null' => true], - 'supercountry_id' => ['type' => 'integer', 'null' => true], - '_constraints' => ['primary' => ['type' => 'primary', 'columns' => ['id']]], - ]; - - public $records = [ + public array $records = [ ['code' => 'NL', 'name' => 'The Netherlands', 'dummy_counter' => 11111, 'currency_id' => 1, 'national_capital_id' => 1], ['code' => 'BG', 'name' => 'Bulgaria', 'dummy_counter' => 22222, 'currency_id' => 1, 'national_capital_id' => 2], ['code' => 'IT', 'name' => 'Italy', 'dummy_counter' => 33333, 'currency_id' => 1, 'national_capital_id' => 4], diff --git a/tests/Fixture/CountriesLanguagesFixture.php b/tests/Fixture/CountriesLanguagesFixture.php index 3e80c03e..cde5e67f 100644 --- a/tests/Fixture/CountriesLanguagesFixture.php +++ b/tests/Fixture/CountriesLanguagesFixture.php @@ -5,14 +5,7 @@ class CountriesLanguagesFixture extends TestFixture { - public $fields = [ - 'id' => ['type' => 'integer'], - 'country_id' => ['type' => 'integer', 'length' => 3, 'null' => false], - 'language_id' => ['type' => 'integer', 'length' => 100, 'null' => false], - '_constraints' => ['primary' => ['type' => 'primary', 'columns' => ['id']]], - ]; - - public $records = [ + public array $records = [ ['country_id' => 1, 'language_id' => 1], ['country_id' => 1, 'language_id' => 2], ['country_id' => 2, 'language_id' => 4], diff --git a/tests/Fixture/CulturesFixture.php b/tests/Fixture/CulturesFixture.php index 4258d5e9..857f0ba2 100644 --- a/tests/Fixture/CulturesFixture.php +++ b/tests/Fixture/CulturesFixture.php @@ -5,16 +5,7 @@ class CulturesFixture extends TestFixture { - public $fields = [ - 'id' => ['type' => 'integer'], - 'code' => ['type' => 'string', 'length' => 5, 'null' => false], - 'name' => ['type' => 'string', 'length' => 100, 'null' => false], - 'another_dummy_counter' => ['type' => 'integer'], - 'country_id' => ['type' => 'integer', 'null' => false], - '_constraints' => ['primary' => ['type' => 'primary', 'columns' => ['id']]], - ]; - - public $records = [ + public array $records = [ ['code' => 'nl-NL', 'name' => 'Dutch', 'another_dummy_counter' => 11111, 'country_id' => 1], ['code' => 'bg-BG', 'name' => 'Bulgarian', 'another_dummy_counter' => 22222, 'country_id' => 2], ['code' => 'tr-BG', 'name' => 'Turkish (Bulgarian)', 'another_dummy_counter' => 22222, 'country_id' => 2], diff --git a/tests/Fixture/CurrenciesFixture.php b/tests/Fixture/CurrenciesFixture.php index 1f720cf5..dfbeaf54 100644 --- a/tests/Fixture/CurrenciesFixture.php +++ b/tests/Fixture/CurrenciesFixture.php @@ -5,14 +5,7 @@ class CurrenciesFixture extends TestFixture { - public $fields = [ - 'id' => ['type' => 'integer'], - 'code' => ['type' => 'string', 'length' => 3, 'null' => false], - 'name' => ['type' => 'string', 'length' => 100, 'null' => false], - '_constraints' => ['primary' => ['type' => 'primary', 'columns' => ['id']]], - ]; - - public $records = [ + public array $records = [ ['code' => 'EUR', 'name' => 'Euro'], ['code' => 'USD', 'name' => 'US Dollar'], ]; diff --git a/tests/Fixture/JsonApiResponseBodies/Errors/404-error-for-collection-in-debug-mode.json b/tests/Fixture/JsonApiResponseBodies/Errors/404-error-for-collection-in-debug-mode.json index 3bbe68bf..35a306fa 100644 --- a/tests/Fixture/JsonApiResponseBodies/Errors/404-error-for-collection-in-debug-mode.json +++ b/tests/Fixture/JsonApiResponseBodies/Errors/404-error-for-collection-in-debug-mode.json @@ -3,7 +3,7 @@ { "status": "404", "title": "Not Found", - "detail": "A route matching \"\/nonexistents\" could not be found." + "detail": "A route matching `\/nonexistents` could not be found." } ], "debug": {} diff --git a/tests/Fixture/LanguagesFixture.php b/tests/Fixture/LanguagesFixture.php index d3a72b4e..38806110 100644 --- a/tests/Fixture/LanguagesFixture.php +++ b/tests/Fixture/LanguagesFixture.php @@ -5,14 +5,7 @@ class LanguagesFixture extends TestFixture { - public $fields = [ - 'id' => ['type' => 'integer'], - 'code' => ['type' => 'string', 'length' => 2, 'null' => false], - 'name' => ['type' => 'string', 'length' => 100, 'null' => false], - '_constraints' => ['primary' => ['type' => 'primary', 'columns' => ['id']]], - ]; - - public $records = [ + public array $records = [ ['id' => 1, 'code' => 'en', 'name' => 'English'], ['id' => 2, 'code' => 'nl', 'name' => 'Dutch'], ['id' => 3, 'code' => 'it', 'name' => 'Italian'], diff --git a/tests/Fixture/NationalCapitalsFixture.php b/tests/Fixture/NationalCapitalsFixture.php index 98549c15..27d34868 100644 --- a/tests/Fixture/NationalCapitalsFixture.php +++ b/tests/Fixture/NationalCapitalsFixture.php @@ -5,14 +5,7 @@ class NationalCapitalsFixture extends TestFixture { - public $fields = [ - 'id' => ['type' => 'integer'], - 'name' => ['type' => 'string', 'length' => 100, 'null' => false], - 'description' => ['type' => 'string', 'length' => 255, 'null' => false], - '_constraints' => ['primary' => ['type' => 'primary', 'columns' => ['id']]], - ]; - - public $records = [ + public array $records = [ ['name' => 'Amsterdam', 'description' => 'National capital of the Netherlands'], ['name' => 'Sofia', 'description' => 'National capital of Bulgaria'], ['name' => 'Wellington', 'description' => 'National capital of New Zealand'], diff --git a/tests/Fixture/NationalCitiesFixture.php b/tests/Fixture/NationalCitiesFixture.php index f476fb68..29bfc9ef 100644 --- a/tests/Fixture/NationalCitiesFixture.php +++ b/tests/Fixture/NationalCitiesFixture.php @@ -5,14 +5,7 @@ class NationalCitiesFixture extends TestFixture { - public $fields = [ - 'id' => ['type' => 'integer'], - 'name' => ['type' => 'string', 'length' => 100, 'null' => false], - 'country_id' => ['type' => 'integer', 'null' => false], - '_constraints' => ['primary' => ['type' => 'primary', 'columns' => ['id']]], - ]; - - public $records = [ + public array $records = [ ['name' => 'Amsterdam', 'country_id' => 1], ['name' => 'Rotterdam', 'country_id' => 1], ['name' => 'Sofia', 'country_id' => 2], diff --git a/tests/Fixture/schema.php b/tests/Fixture/schema.php new file mode 100644 index 00000000..2cb3b8a9 --- /dev/null +++ b/tests/Fixture/schema.php @@ -0,0 +1,85 @@ + [ + 'columns' => [ + 'id' => ['type' => 'integer'], + 'code' => ['type' => 'string', 'length' => 2, 'null' => false], + 'name' => ['type' => 'string', 'length' => 255, 'null' => false], + 'dummy_counter' => ['type' => 'integer'], + 'currency_id' => ['type' => 'integer', 'null' => true], + 'national_capital_id' => ['type' => 'integer', 'null' => true], + 'supercountry_id' => ['type' => 'integer', 'null' => true], + ], + 'constraints' => [ + 'primary' => ['type' => 'primary', 'columns' => ['id']], + ], + ], + 'countries_languages' => [ + 'columns' => [ + 'id' => ['type' => 'integer'], + 'country_id' => ['type' => 'integer', 'length' => 3, 'null' => false], + 'language_id' => ['type' => 'integer', 'length' => 100, 'null' => false], + ], + 'constraints' => [ + 'primary' => ['type' => 'primary', 'columns' => ['id']], + ], + ], + 'cultures' => [ + 'columns' => [ + 'id' => ['type' => 'integer'], + 'code' => ['type' => 'string', 'length' => 5, 'null' => false], + 'name' => ['type' => 'string', 'length' => 100, 'null' => false], + 'another_dummy_counter' => ['type' => 'integer'], + 'country_id' => ['type' => 'integer', 'null' => false], + ], + 'constraints' => [ + 'primary' => ['type' => 'primary', 'columns' => ['id']], + ], + ], + 'currencies' => [ + 'columns' => [ + 'id' => ['type' => 'integer'], + 'code' => ['type' => 'string', 'length' => 3, 'null' => false], + 'name' => ['type' => 'string', 'length' => 100, 'null' => false], + ], + 'constraints' => [ + 'primary' => ['type' => 'primary', 'columns' => ['id']], + ], + ], + 'languages' => [ + 'columns' => [ + 'id' => ['type' => 'integer'], + 'code' => ['type' => 'string', 'length' => 2, 'null' => false], + 'name' => ['type' => 'string', 'length' => 100, 'null' => false], + ], + 'constraints' => [ + 'primary' => ['type' => 'primary', 'columns' => ['id']], + ], + ], + 'national_capitals' => [ + 'columns' => [ + 'id' => ['type' => 'integer'], + 'name' => ['type' => 'string', 'length' => 100, 'null' => false], + 'description' => ['type' => 'string', 'length' => 255, 'null' => false], + ], + 'constraints' => [ + 'primary' => ['type' => 'primary', 'columns' => ['id']], + ], + ], + 'national_cities' => [ + 'columns' => [ + 'id' => ['type' => 'integer'], + 'name' => ['type' => 'string', 'length' => 100, 'null' => false], + 'country_id' => ['type' => 'integer', 'null' => false], + ], + 'constraints' => [ + 'primary' => ['type' => 'primary', 'columns' => ['id']], + ], + ], +]; diff --git a/tests/TestCase/Error/JsonApiExceptionRendererTest.php b/tests/TestCase/Error/JsonApiExceptionRendererTest.php index 4bb6aecb..84ce22a1 100644 --- a/tests/TestCase/Error/JsonApiExceptionRendererTest.php +++ b/tests/TestCase/Error/JsonApiExceptionRendererTest.php @@ -5,15 +5,15 @@ use Cake\Controller\Controller; use Cake\Core\Configure; -use Cake\Core\Exception\Exception; +use Cake\Core\Exception\CakeException; use Cake\Core\Plugin; use Cake\Http\Response; use Cake\Http\ServerRequest; -use Cake\ORM\TableRegistry; use Crud\Error\Exception\ValidationException; use Crud\TestSuite\TestCase; use CrudJsonApi\Error\JsonApiExceptionRenderer; use Neomerx\JsonApi\Schema\ErrorCollection; +use PHPUnit\Framework\MockObject\MockObject; class JsonApiExceptionRendererTest extends TestCase { @@ -22,14 +22,14 @@ class JsonApiExceptionRendererTest extends TestCase * * @var */ - protected $_JsonApiResponseBodyFixtures; + protected string $_JsonApiResponseBodyFixtures; /** * fixtures property * * @var array */ - public $fixtures = [ + public array $fixtures = [ 'plugin.CrudJsonApi.Countries', ]; @@ -52,10 +52,11 @@ public function setUp(): void */ public function testRenderWithNonValidationError() { - $exception = new Exception('Hello World'); + $exception = new CakeException('Hello World'); $controller = $this->getMockBuilder(Controller::class) ->onlyMethods(['render']) + ->setConstructorArgs([new ServerRequest()]) ->getMock(); $controller->setRequest(new ServerRequest([ 'environment' => [ @@ -102,7 +103,7 @@ public function testRenderWithNonValidationError() */ public function testRenderWithValidationError() { - $countries = TableRegistry::get('Countries'); + $countries = $this->fetchTable('Countries'); $invalidCountry = $countries->newEntity([ 'code' => 'not-all-uppercase', @@ -111,17 +112,22 @@ public function testRenderWithValidationError() $exception = new ValidationException($invalidCountry); $controller = $this->getMockBuilder('Cake\Controller\Controller') - ->setMethods(['render']) + ->onlyMethods(['render']) + ->setConstructorArgs([new ServerRequest()]) ->getMock(); - $controller->request = new ServerRequest([ + + $request = new ServerRequest([ 'environment' => [ 'HTTP_ACCEPT' => 'application/vnd.api+json', ], ]); - $controller->response = new Response(); + + $controller + ->setRequest($request) + ->setResponse(new Response()); $renderer = $this->getMockBuilder('CrudJsonApi\Error\JsonApiExceptionRenderer') - ->setMethods(['_getController']) + ->onlyMethods(['_getController']) ->disableOriginalConstructor() ->getMock(); $renderer @@ -151,49 +157,68 @@ public function testRenderWithValidationError() */ public function testValidationExceptionsFallBackToStatusCode422() { - $countries = TableRegistry::get('Countries'); + $countries = $this->fetchTable('Countries'); $invalidCountry = $countries->newEntity([]); $exception = new ValidationException($invalidCountry); $controller = $this->getMockBuilder('Cake\Controller\Controller') - ->setMethods(['render']) + ->onlyMethods(['render']) + ->setConstructorArgs([new ServerRequest()]) ->getMock(); - $controller->request = new ServerRequest([ + $request = new ServerRequest([ 'environment' => [ 'HTTP_ACCEPT' => 'application/vnd.api+json', ], ]); - $res = new Response(); + $controller->setRequest($request); $response = $this->getMockBuilder('Cake\Http\Response') - ->setMethods(['withStatus']) + ->onlyMethods(['withStatus', 'getStatusCode', 'withType', 'withBody']) ->getMock(); $response - ->expects($this->at(0)) + ->expects($this->exactly(2)) ->method('withStatus') - ->will($this->throwException(new Exception('woot'))); + ->willReturnCallback(function () use (&$callCount, $response): MockObject { + $callCount++; + + // First call should throw an exception. + if ($callCount === 1) { + throw new CakeException('woot'); + } + + // Second call should succeed and return response with status. + return $response; + }); + + // Mock getStatusCode to return 422 after the successful withStatus call. $response - ->expects($this->at(1)) - ->method('withStatus') - ->will($this->returnCallback(function ($input) use ($res) { - return $res->withStatus($input); - })); + ->method('getStatusCode') + ->willReturn(422); + + // Mock the other methods that are called in the validation method. + $response + ->method('withType') + ->willReturn($response); + + $response + ->method('withBody') + ->willReturn($response); - $controller->response = $response; + $controller->setResponse($response); $renderer = $this->getMockBuilder('CrudJsonApi\Error\JsonApiExceptionRenderer') - ->setMethods(['_getController']) + ->onlyMethods(['_getController']) ->disableOriginalConstructor() ->getMock(); $renderer ->expects($this->once()) ->method('_getController') ->with() - ->will($this->returnValue($controller)); + ->willReturn($controller); $renderer->__construct($exception); $result = $renderer->render(); @@ -227,7 +252,7 @@ public function testStandardizeValidationErrors() ]; $renderer = $this->getMockBuilder('CrudJsonApi\Error\JsonApiExceptionRenderer') - ->setMethods(null) + ->onlyMethods([]) ->disableOriginalConstructor() ->getMock(); @@ -271,7 +296,7 @@ public function testStandardizeValidationErrors() public function testGetNeoMerxErrorCollection() { $renderer = $this->getMockBuilder('CrudJsonApi\Error\JsonApiExceptionRenderer') - ->setMethods(null) + ->onlyMethods([]) ->disableOriginalConstructor() ->getMock(); @@ -334,19 +359,14 @@ public function testAddQueryLogs() ->disableOriginalConstructor() ->getMock(); $apiQueryLogListener - ->expects($this->at(0)) - ->method('getQueryLogs') - ->with() - ->willReturn([]); - $apiQueryLogListener - ->expects($this->at(1)) + ->expects($this->exactly(2)) ->method('getQueryLogs') ->with() - ->willReturn( - [ - 'dummy' => 'log-entry', - ] - ); + ->willReturnCallback(function () use (&$callCount): array { + $callCount++; + + return $callCount === 1 ? [] : ['dummy' => 'log-entry']; + }); $renderer = $this->getMockBuilder('CrudJsonApi\Error\JsonApiExceptionRenderer') ->onlyMethods(['_getApiQueryLogListenerObject']) diff --git a/tests/TestCase/Integration/JsonApi/CreatingResourcesIntegrationTest.php b/tests/TestCase/Integration/JsonApi/CreatingResourcesIntegrationTest.php index 63525316..d8e0b407 100644 --- a/tests/TestCase/Integration/JsonApi/CreatingResourcesIntegrationTest.php +++ b/tests/TestCase/Integration/JsonApi/CreatingResourcesIntegrationTest.php @@ -27,8 +27,8 @@ public function testSidePostingException() $this->post( '/countries', $this->_getJsonApiRequestBody( - 'CreatingResources' . DS . 'create-country-throw-side-posting-exception.json' - ) + 'CreatingResources' . DS . 'create-country-throw-side-posting-exception.json', + ), ); $this->assertResponseCode(400); // bad request $responseBodyArray = json_decode((string)$this->_response->getBody(), true); @@ -52,7 +52,7 @@ public function testSidePostingException() * * @return array */ - public function createResourceProvider() + public static function createResourceProvider(): array { return [ 'create-single-word-resource-no-relationships' => [ diff --git a/tests/TestCase/Integration/JsonApi/FetchingCollectionsIntegrationTest.php b/tests/TestCase/Integration/JsonApi/FetchingCollectionsIntegrationTest.php index 5d722b36..bcc55d61 100644 --- a/tests/TestCase/Integration/JsonApi/FetchingCollectionsIntegrationTest.php +++ b/tests/TestCase/Integration/JsonApi/FetchingCollectionsIntegrationTest.php @@ -12,7 +12,7 @@ class FetchingCollectionsIntegrationTest extends JsonApiBaseTestCase * * @return array */ - public function getProvider() + public static function getProvider(): array { return [ # Test fetching a single-word collection. diff --git a/tests/TestCase/Integration/JsonApi/FetchingResourcesIntegrationTest.php b/tests/TestCase/Integration/JsonApi/FetchingResourcesIntegrationTest.php index 46fb991c..852be0bc 100644 --- a/tests/TestCase/Integration/JsonApi/FetchingResourcesIntegrationTest.php +++ b/tests/TestCase/Integration/JsonApi/FetchingResourcesIntegrationTest.php @@ -13,7 +13,7 @@ class FetchingResourcesIntegrationTest extends JsonApiBaseTestCase * * @return array */ - public function fetchResourceProvider() + public static function fetchResourceProvider(): array { return [ 'fetch-single-word-resource-with-no-relationships' => [ @@ -67,7 +67,7 @@ public function testFetchResource($url, $expectedResponseFile) * * @return array */ - public function fetchNestedResourceProvider() + public static function fetchNestedResourceProvider(): array { return [ 'fetch-one-to-many-relation' => [ @@ -100,7 +100,7 @@ public function testFetchNestedResource($url, $expectedResponseFile) 'headers' => [ 'Accept' => 'application/vnd.api+json', ], - ] + ], ); # execute the GET request diff --git a/tests/TestCase/Integration/JsonApi/FilteringIntegrationTest.php b/tests/TestCase/Integration/JsonApi/FilteringIntegrationTest.php index bb4f125b..d982f308 100644 --- a/tests/TestCase/Integration/JsonApi/FilteringIntegrationTest.php +++ b/tests/TestCase/Integration/JsonApi/FilteringIntegrationTest.php @@ -10,7 +10,7 @@ class FilteringIntegrationTest extends JsonApiBaseTestCase /** * @return array */ - public function filterProvider() + public static function filterProvider(): array { return [ // assert single-field searches (case sensitive for now or diff --git a/tests/TestCase/Integration/JsonApi/InclusionIntegrationTest.php b/tests/TestCase/Integration/JsonApi/InclusionIntegrationTest.php index 2ef789ef..55cf91fc 100644 --- a/tests/TestCase/Integration/JsonApi/InclusionIntegrationTest.php +++ b/tests/TestCase/Integration/JsonApi/InclusionIntegrationTest.php @@ -12,7 +12,7 @@ class InclusionIntegrationTest extends JsonApiBaseTestCase /** * @return array */ - public function inclusionProvider() + public static function inclusionProvider(): array { return [ // assert single-word associations diff --git a/tests/TestCase/Integration/JsonApi/RelationshipsIntegrationTest.php b/tests/TestCase/Integration/JsonApi/RelationshipsIntegrationTest.php index b60219f3..f4205e27 100644 --- a/tests/TestCase/Integration/JsonApi/RelationshipsIntegrationTest.php +++ b/tests/TestCase/Integration/JsonApi/RelationshipsIntegrationTest.php @@ -13,7 +13,7 @@ class RelationshipsIntegrationTest extends JsonApiBaseTestCase /** * @return array */ - public function getProvider() + public static function getProvider(): array { return [ // 'one-to-many: get cultures for country' => [ @@ -54,7 +54,7 @@ public function testGet($url, $expectedFile): void /** * @return array */ - public function postProvider() + public static function postProvider(): array { return [ 'one-to-many: add culture relationship for country' => [ @@ -91,7 +91,7 @@ public function testPost($url, $requestBodyFile, $expectedResponseFile) 'Content-Type' => 'application/vnd.api+json', ], 'input' => $this->_getJsonApiRequestBody('Relationships' . DS . $requestBodyFile), - ] + ], ); // execute the POST request @@ -107,7 +107,7 @@ public function testPost($url, $requestBodyFile, $expectedResponseFile) /** * @return \string[][] */ - public function toOne() + public static function toOne(): array { return [ 'POST' => ['post'], @@ -134,9 +134,9 @@ public function testNoToOne($method) 'type' => 'currencies', 'id' => 1, ], - ] + ], ), - ] + ], ); // execute the POST request @@ -151,7 +151,7 @@ public function testNoToOne($method) /** * @return \string[][] */ - public function missingRecordProvider() + public static function missingRecordProvider(): array { return [ 'POST' => ['post'], @@ -183,9 +183,9 @@ public function testMissingRecords($method) 'id' => 10, ], ], - ] + ], ), - ] + ], ); // execute the POST request @@ -201,7 +201,7 @@ public function testMissingRecords($method) /** * @return array */ - public function patchProvider() + public static function patchProvider(): array { return [ 'one-to-many: replace culture relationship for country' => [ @@ -243,7 +243,7 @@ public function testPatch($url, $requestBodyFile, $expectedResponseFile) 'Content-Type' => 'application/vnd.api+json', ], 'input' => $this->_getJsonApiRequestBody('Relationships' . DS . $requestBodyFile), - ] + ], ); $this->disableErrorHandlerMiddleware(); @@ -260,7 +260,7 @@ public function testPatch($url, $requestBodyFile, $expectedResponseFile) /** * @return array */ - public function deleteProvider() + public static function deleteProvider(): array { return [ 'one-to-many: delete culture relationship for country' => [ @@ -297,7 +297,7 @@ public function testDelete($url, $requestBodyFile, $expectedResponseFile) 'Content-Type' => 'application/vnd.api+json', ], 'input' => $this->_getJsonApiRequestBody('Relationships' . DS . $requestBodyFile), - ] + ], ); // execute the DELETE request diff --git a/tests/TestCase/Integration/JsonApi/SelfReferencedAssociationIntegrationTest.php b/tests/TestCase/Integration/JsonApi/SelfReferencedAssociationIntegrationTest.php index f8ccedaf..46c5eca5 100644 --- a/tests/TestCase/Integration/JsonApi/SelfReferencedAssociationIntegrationTest.php +++ b/tests/TestCase/Integration/JsonApi/SelfReferencedAssociationIntegrationTest.php @@ -10,7 +10,7 @@ class SelfReferencedAssociationIntegrationTest extends JsonApiBaseTestCase /** * @return array */ - public function viewProvider() + public static function viewProvider(): array { return [ 'get supercountry with subcountries' => [ diff --git a/tests/TestCase/Integration/JsonApi/SortingIntegrationTest.php b/tests/TestCase/Integration/JsonApi/SortingIntegrationTest.php index 12a8e507..037979d7 100644 --- a/tests/TestCase/Integration/JsonApi/SortingIntegrationTest.php +++ b/tests/TestCase/Integration/JsonApi/SortingIntegrationTest.php @@ -11,7 +11,7 @@ class SortingIntegrationTest extends JsonApiBaseTestCase /** * @return array */ - public function sortProvider() + public static function sortProvider(): array { return [ 'unsorted' => [ @@ -92,7 +92,7 @@ public function sortProvider() /** * @return array */ - public function paginationUrlProvider() + public static function paginationUrlProvider(): array { return [ 'pagination' => [ diff --git a/tests/TestCase/Integration/JsonApi/SparseFieldsetsIntegrationTest.php b/tests/TestCase/Integration/JsonApi/SparseFieldsetsIntegrationTest.php index 7de306d0..596aff51 100644 --- a/tests/TestCase/Integration/JsonApi/SparseFieldsetsIntegrationTest.php +++ b/tests/TestCase/Integration/JsonApi/SparseFieldsetsIntegrationTest.php @@ -11,7 +11,7 @@ class SparseFieldsetsIntegrationTest extends JsonApiBaseTestCase /** * @return array */ - public function viewProvider() + public static function viewProvider(): array { return [ // assert "single-field" sparse for index actions @@ -101,7 +101,7 @@ public function viewProvider() /** * @return array */ - public function paginationUrlProvider() + public static function paginationUrlProvider(): array { return [ 'pagination' => [ diff --git a/tests/TestCase/Integration/JsonApi/UpdatingResourcesIntegrationTest.php b/tests/TestCase/Integration/JsonApi/UpdatingResourcesIntegrationTest.php index 6a3328a8..e8d08ace 100644 --- a/tests/TestCase/Integration/JsonApi/UpdatingResourcesIntegrationTest.php +++ b/tests/TestCase/Integration/JsonApi/UpdatingResourcesIntegrationTest.php @@ -3,7 +3,6 @@ namespace CrudJsonApi\Test\TestCase\Integration\JsonApi; -use Cake\ORM\TableRegistry; use Cake\Utility\Inflector; use CrudJsonApi\Test\TestCase\Integration\JsonApiBaseTestCase; use DMS\PHPUnitExtensions\ArraySubset\ArraySubsetAsserts; @@ -20,7 +19,7 @@ class UpdatingResourcesIntegrationTest extends JsonApiBaseTestCase * * @return array */ - public function updateResourceProvider() + public static function updateResourceProvider(): array { return [ # changing USD to RUB @@ -179,7 +178,7 @@ public function testUpdateResource($url, $requestBodyFile, $expectedResponseFile $recordId = $matches[2]; # assert the database record got updated like expected - $table = TableRegistry::get($tableName); + $table = $this->fetchTable($tableName); $record = $table->get($recordId)->toArray(); $this->assertArraySubset($expectedRecordSubset, $record); diff --git a/tests/TestCase/Integration/JsonApiBaseTestCase.php b/tests/TestCase/Integration/JsonApiBaseTestCase.php index 151ea8d2..06e9ea23 100644 --- a/tests/TestCase/Integration/JsonApiBaseTestCase.php +++ b/tests/TestCase/Integration/JsonApiBaseTestCase.php @@ -37,7 +37,7 @@ abstract class JsonApiBaseTestCase extends TestCase * * @var array */ - public $fixtures = [ + public array $fixtures = [ 'plugin.CrudJsonApi.Countries', 'plugin.CrudJsonApi.Currencies', 'plugin.CrudJsonApi.Cultures', diff --git a/tests/TestCase/Listener/JsonApi/DocumentValidatorTest.php b/tests/TestCase/Listener/JsonApi/DocumentValidatorTest.php index ab0acf14..10d84322 100644 --- a/tests/TestCase/Listener/JsonApi/DocumentValidatorTest.php +++ b/tests/TestCase/Listener/JsonApi/DocumentValidatorTest.php @@ -580,9 +580,9 @@ public function testGetPathObject() $obj = $this->callProtectedMethod('_getPathObject', ['data'], $this->_validator); $this->assertTrue(is_a($obj, 'stdClass')); - $this->assertObjectHasAttribute('dotted', $obj); - $this->assertObjectHasAttribute('toKey', $obj); - $this->assertObjectHasAttribute('key', $obj); + $this->assertObjectHasProperty('dotted', $obj); + $this->assertObjectHasProperty('toKey', $obj); + $this->assertObjectHasProperty('key', $obj); // assert single-level path $this->assertEquals('data', $obj->dotted); diff --git a/tests/TestCase/Listener/JsonApiListenerTest.php b/tests/TestCase/Listener/JsonApiListenerTest.php index 382f4387..9f263a72 100644 --- a/tests/TestCase/Listener/JsonApiListenerTest.php +++ b/tests/TestCase/Listener/JsonApiListenerTest.php @@ -7,7 +7,6 @@ use Cake\Core\Plugin; use Cake\Datasource\ResultSetDecorator; use Cake\Event\Event; -use Cake\Filesystem\File; use Cake\Http\Exception\BadRequestException; use Cake\Http\Exception\MethodNotAllowedException; use Cake\Http\Response; @@ -15,13 +14,13 @@ use Cake\ORM\Entity; use Cake\ORM\Query; use Cake\ORM\ResultSet; -use Cake\ORM\TableRegistry; use Cake\Routing\RouteBuilder; use Cake\Routing\Router; use Crud\Event\Subject; use Crud\TestSuite\TestCase; use CrudJsonApi\Listener\JsonApiListener; use CrudJsonApi\Test\App\Model\Entity\Country; +use function file_get_contents; /** * Licensed under The MIT License @@ -41,7 +40,7 @@ class JsonApiListenerTest extends TestCase * * @var array */ - public $fixtures = [ + public array $fixtures = [ 'plugin.CrudJsonApi.Countries', 'plugin.CrudJsonApi.Cultures', 'plugin.CrudJsonApi.Currencies', @@ -60,7 +59,7 @@ public function setUp(): void '/', static function (RouteBuilder $routeBuilder) { $routeBuilder->fallbacks(); - } + }, ); $this->_JsonApiDecoderFixtures = Plugin::path('CrudJsonApi') . 'tests' . DS . 'Fixture' . DS . 'JsonApiDecoder'; @@ -71,7 +70,7 @@ static function (RouteBuilder $routeBuilder) { */ public function testDefaultConfig() { - $listener = new JsonApiListener(new Controller()); + $listener = new JsonApiListener(new Controller(new ServerRequest())); $expected = [ 'detectors' => [ @@ -120,14 +119,14 @@ public function testImplementedEvents() ->getMock(); $listener - ->expects($this->at(1)) + ->expects($this->exactly(2)) ->method('_checkRequestType') - ->willReturn(false); // for asserting missing JSON API Accept header + ->willReturnCallback(function () use (&$callCount): bool { + $callCount++; - $listener - ->expects($this->at(3)) - ->method('_checkRequestType') - ->willReturn(true); // for asserting valid JSON API Accept header + // First call asserts missing JSON API Accept header; second call asserts valid JSON API Accept header. + return $callCount === 2; + }); // assert that listener does nothing if JSON API Accept header is missing $result = $listener->implementedEvents(); @@ -212,6 +211,7 @@ public function testAfterSave() $controller = $this ->getMockBuilder(Controller::class) ->onlyMethods([]) + ->setConstructorArgs([new ServerRequest()]) ->getMock(); $response = $this @@ -252,16 +252,18 @@ public function testAfterSave() // assert nothing happens if `success` is false $event->getSubject()->success = false; - $this->assertFalse($this->callProtectedMethod('afterSave', [$event], $listener)); + $this->callProtectedMethod('afterSave', [$event], $listener); + $this->assertFalse($event->getResult()); // assert nothing happens if `success` is true but both `created` and `id` are false $event->getSubject()->success = true; $event->getSubject()->created = false; $event->getSubject()->id = false; - $this->assertFalse($this->callProtectedMethod('afterSave', [$event], $listener)); + $this->callProtectedMethod('afterSave', [$event], $listener); + $this->assertFalse($event->getResult()); // assert success - $table = TableRegistry::get('Countries'); + $table = $this->fetchTable('Countries'); $entity = $table->find()->first(); $subject->entity = $entity; @@ -301,7 +303,7 @@ public function testAfterDelete() ->onlyMethods([]) ->getMock(); - $controller->response = $response; + $controller->setResponse($response); $listener ->method('_response') @@ -331,7 +333,8 @@ public function testAfterDelete() // assert nothing happens if `success` is false $event->getSubject()->success = false; - $this->assertFalse($this->callProtectedMethod('afterDelete', [$event], $listener)); + $this->callProtectedMethod('afterDelete', [$event], $listener); + $this->assertFalse($event->getResult()); $event->getSubject()->success = true; $this->assertNull($this->callProtectedMethod('afterDelete', [$event], $listener)); @@ -362,7 +365,7 @@ public function testRenderWithResources() ->getMockBuilder(Controller::class) ->onlyMethods([]) ->enableOriginalConstructor() - ->setConstructorArgs([null, null, 'Countries']) + ->setConstructorArgs([new ServerRequest(), 'Countries']) ->getMock(); $listener = $this @@ -395,6 +398,7 @@ public function testRenderWithoutResources() ->getMockBuilder(Controller::class) ->onlyMethods([]) ->enableOriginalConstructor() + ->setConstructorArgs([new ServerRequest()]) ->getMock(); $listener = $this @@ -695,8 +699,7 @@ public function testCheckRequestMethodsSuccess() { $request = new ServerRequest(); $request = $request->withEnv('HTTP_ACCEPT', 'application/vnd.api+json'); - $response = new Response(); - $controller = new Controller($request, $response); + $controller = new Controller($request); $listener = new JsonApiListener($controller); $listener->setupDetectors(); @@ -706,8 +709,7 @@ public function testCheckRequestMethodsSuccess() $request = new ServerRequest(); $request = $request->withEnv('HTTP_ACCEPT', 'application/vnd.api+json') ->withEnv('CONTENT_TYPE', 'application/vnd.api+json'); - $response = new Response(); - $controller = new Controller($request, $response); + $controller = new Controller($request); $listener = new JsonApiListener($controller); $listener->setupDetectors(); @@ -726,8 +728,7 @@ public function testCheckRequestMethodsFailContentHeader() $request = new ServerRequest(); $request = $request->withEnv('HTTP_ACCEPT', 'application/vnd.api+json') ->withEnv('CONTENT_TYPE', 'application/json'); - $response = new Response(); - $controller = new Controller($request, $response); + $controller = new Controller($request); $listener = new JsonApiListener($controller); $listener->setupDetectors(); @@ -746,8 +747,7 @@ public function testCheckRequestMethodsFailOnPutMethod() $request = new ServerRequest(); $request = $request->withEnv('HTTP_ACCEPT', 'application/vnd.api+json') ->withEnv('REQUEST_METHOD', 'PUT'); - $response = new Response(); - $controller = new Controller($request, $response); + $controller = new Controller($request); $listener = new JsonApiListener($controller); $listener->setupDetectors(); @@ -793,6 +793,7 @@ public function testGetSingleEntity() ->getMockBuilder(Controller::class) ->onlyMethods([]) ->enableOriginalConstructor() + ->setConstructorArgs([new ServerRequest()]) ->getMock(); $listener = $this @@ -851,6 +852,7 @@ public function testGetSingleEntityForEmptyResultSet() ->getMockBuilder(Controller::class) ->onlyMethods([]) ->enableOriginalConstructor() + ->setConstructorArgs([new ServerRequest()]) ->getMock(); $listener = $this @@ -864,8 +866,6 @@ public function testGetSingleEntityForEmptyResultSet() ->method('_controller') ->willReturn($controller); - $entity = new Entity(); - $subject = $this ->getMockBuilder(Subject::class) ->getMock(); @@ -888,7 +888,7 @@ public function testGetSingleEntityForEmptyResultSet() $subject->query = $query; $subject->query ->method('getRepository') - ->willReturn(TableRegistry::get('Countries')); + ->willReturn($this->fetchTable('Countries')); $this->setReflectionClassInstance($listener); $result = $this->callProtectedMethod('_getSingleEntity', [$subject], $listener); @@ -905,9 +905,7 @@ public function testGetSingleEntityForEmptyResultSet() */ public function testGetContainedAssociations() { - $table = TableRegistry::get('Countries'); - $table->belongsTo('Currencies'); - $table->hasMany('Cultures'); + $table = $this->fetchTable('Countries'); // make sure expected associations are there $associationsBefore = $table->associations(); @@ -924,7 +922,7 @@ public function testGetContainedAssociations() $this->assertNull($entity->cultures); // make sure cultures are removed from AssociationCollection - $listener = new JsonApiListener(new Controller()); + $listener = new JsonApiListener(new Controller(new ServerRequest())); $this->setReflectionClassInstance($listener); $associationsAfter = $this->callProtectedMethod('_getContainedAssociations', [$table, $query->getContain()], $listener); @@ -940,21 +938,7 @@ public function testGetContainedAssociations() */ public function testGetRepositoryList() { - $table = TableRegistry::get('Countries'); - $table->belongsTo('Currencies'); - $table->belongsTo('NationalCapitals'); - $table->hasMany('Cultures'); - $table->hasMany('NationalCities'); - - $table->hasMany('SubCountries', [ - 'className' => 'Countries', - 'propertyName' => 'subcountry', - ]); - - $table->belongsTo('SuperCountries', [ - 'className' => 'Countries', - 'propertyName' => 'supercountry', - ]); + $table = $this->fetchTable('Countries'); $associations = []; foreach ($table->associations() as $association) { @@ -973,7 +957,7 @@ public function testGetRepositoryList() $this->assertArrayHasKey('currencies', $associations); $this->assertArrayHasKey('cultures', $associations); - $listener = new JsonApiListener(new Controller()); + $listener = new JsonApiListener(new Controller(new ServerRequest())); $this->setReflectionClassInstance($listener); $result = $this->callProtectedMethod('_getRepositoryList', [$table, $associations], $listener); @@ -1011,7 +995,7 @@ public function testGetIncludeList() // hasMany relations (if listener config option `include` is not set) $this->assertEmpty($listener->getConfig('include')); - $table = TableRegistry::get('Countries'); + $table = $this->fetchTable('Countries'); $associations = []; foreach ($table->associations() as $association) { $associations[strtolower($association->getName())] = [ @@ -1076,7 +1060,7 @@ public function testCheckRequestData() $this->expectException(BadRequestException::class); $this->expectExceptionMessage( 'Missing request data required for POST and PATCH methods, as well as DELETE methods to relationship endpoints. ' . - 'Make sure that you are sending a request body and that it is valid JSON.' + 'Make sure that you are sending a request body and that it is valid JSON.', ); $controller = $this ->getMockBuilder(Controller::class) @@ -1091,24 +1075,17 @@ public function testCheckRequestData() ->getMock(); $request - ->expects($this->at(0)) - ->method('getMethod') - ->willReturn('GET'); - - $request - ->expects($this->at(1)) - ->method('getMethod') - ->willReturn('POST'); - - $request - ->expects($this->at(2)) + ->expects($this->exactly(4)) ->method('getMethod') - ->will($this->returnValue('POST')); + ->willReturnCallback(function () use (&$callCount): string { + $callCount++; - $request - ->expects($this->at(3)) - ->method('getMethod') - ->willReturn('PATCH'); + return match ($callCount) { + 1 => 'GET', + 2, 3 => 'POST', + default => 'PATCH', + }; + }); $controller->setRequest($request); @@ -1164,7 +1141,7 @@ public function testCheckRequestData() */ public function testConvertJsonApiDataArray() { - $listener = new JsonApiListener(new Controller()); + $listener = new JsonApiListener(new Controller(new ServerRequest())); $this->setReflectionClassInstance($listener); // assert posted id attribute gets processed as expected @@ -1181,8 +1158,11 @@ public function testConvertJsonApiDataArray() $this->assertSame($expected, $result); // assert success (single entity, no relationships) - $jsonApiFixture = new File($this->_JsonApiDecoderFixtures . DS . 'incoming-country-no-relationships.json'); - $jsonApiArray = json_decode($jsonApiFixture->read(), true); + $jsonApiFixture = file_get_contents( + $this->_JsonApiDecoderFixtures . DS . 'incoming-country-no-relationships.json', + ); + + $jsonApiArray = json_decode($jsonApiFixture, true); $expected = [ 'code' => 'NL', 'name' => 'The Netherlands', @@ -1192,8 +1172,11 @@ public function testConvertJsonApiDataArray() $this->assertSame($expected, $result); // assert success (single entity, multiple relationships, hasMany ignored for now) - $jsonApiFixture = new File($this->_JsonApiDecoderFixtures . DS . 'incoming-country-mixed-relationships.json'); - $jsonApiArray = json_decode($jsonApiFixture->read(), true); + $jsonApiFixture = file_get_contents( + $this->_JsonApiDecoderFixtures . DS . 'incoming-country-mixed-relationships.json', + ); + + $jsonApiArray = json_decode($jsonApiFixture, true); $expected = [ 'code' => 'NL', 'name' => 'The Netherlands', @@ -1211,8 +1194,11 @@ public function testConvertJsonApiDataArray() $this->assertSame($expected, $result); // assert success for relationships with null/empty data - $jsonApiFixture = new File($this->_JsonApiDecoderFixtures . DS . 'incoming-country-mixed-relationships.json'); - $jsonApiArray = json_decode($jsonApiFixture->read(), true); + $jsonApiFixture = file_get_contents( + $this->_JsonApiDecoderFixtures . DS . 'incoming-country-mixed-relationships.json', + ); + + $jsonApiArray = json_decode($jsonApiFixture, true); $jsonApiArray['data']['relationships']['cultures']['data'] = null; $jsonApiArray['data']['relationships']['currency']['data'] = null; @@ -1225,7 +1211,7 @@ public function testConvertJsonApiDataArray() $this->assertSame($expected, $result); } - public function includeQueryProvider() + public static function includeQueryProvider(): array { return [ 'standard' => [ @@ -1337,7 +1323,7 @@ public function includeQueryProvider() */ public function testIncludeQuery($include, $options, $expectedContain, $expectedInclude) { - $listener = new JsonApiListener(new Controller()); + $listener = new JsonApiListener(new Controller(new ServerRequest())); $this->setReflectionClassInstance($listener); $subject = new Subject(); @@ -1350,13 +1336,13 @@ public function testIncludeQuery($include, $options, $expectedContain, $expected $subject->query = $query; $subject->query ->method('getRepository') - ->willReturn(TableRegistry::get('Countries')); + ->willReturn($this->fetchTable('Countries')); $this->callProtectedMethod('_includeParameter', [$include, $subject, $options], $listener); $this->assertSame($expectedInclude, $listener->getConfig('include')); } - public function includeQueryBadRequestProvider() + public static function includeQueryBadRequestProvider(): array { return [ 'denyList everything' => [ @@ -1384,7 +1370,7 @@ public function includeQueryBadRequestProvider() public function testIncludeQueryBadRequest($include, $options, $expectedContain, $expectedInclude) { $this->expectException('Cake\Http\Exception\BadRequestException'); - $listener = new JsonApiListener(new Controller()); + $listener = new JsonApiListener(new Controller(new ServerRequest())); $this->setReflectionClassInstance($listener); $subject = new Subject(); @@ -1400,8 +1386,8 @@ public function testIncludeQueryBadRequest($include, $options, $expectedContain, ->method('contain') ->with($expectedContain); $subject->query - ->method('repository') - ->willReturn(TableRegistry::get('Countries')); + ->method('getRepository') + ->willReturn($this->fetchTable('Countries')); $this->callProtectedMethod('_includeParameter', [$include, $subject, $options], $listener); $this->assertSame($expectedInclude, $listener->getConfig('include')); @@ -1416,7 +1402,7 @@ public function testIncludeQueryBadRequest($include, $options, $expectedContain, */ public function testSortingNotAppliedToAllTables() { - $listener = new JsonApiListener(new Controller()); + $listener = new JsonApiListener(new Controller(new ServerRequest())); $this->setReflectionClassInstance($listener); $subject = new Subject(); @@ -1432,7 +1418,7 @@ public function testSortingNotAppliedToAllTables() ->method('contain'); $subject->query ->method('getRepository') - ->willReturn(TableRegistry::get('Countries')); + ->willReturn($this->fetchTable('Countries')); $sort = 'code,currency.code'; $listener->setConfig('include', ['currency', 'national_capitals']); diff --git a/tests/TestCase/Schema/DynamicEntitySchemaTest.php b/tests/TestCase/Schema/DynamicEntitySchemaTest.php index 88c66229..2fbd0e7b 100644 --- a/tests/TestCase/Schema/DynamicEntitySchemaTest.php +++ b/tests/TestCase/Schema/DynamicEntitySchemaTest.php @@ -4,10 +4,11 @@ namespace CrudJsonApi\Test\TestCase\Schema\JsonApi; use Cake\Controller\Controller; -use Cake\View\View; +use Cake\Http\ServerRequest; use Crud\TestSuite\TestCase; use CrudJsonApi\Listener\JsonApiListener; use CrudJsonApi\Schema\JsonApi\DynamicEntitySchema; +use CrudJsonApi\View\JsonApiView; use Neomerx\JsonApi\Contracts\Factories\FactoryInterface; use Neomerx\JsonApi\Contracts\Schema\ContextInterface; use Neomerx\JsonApi\Contracts\Schema\SchemaInterface; @@ -25,7 +26,7 @@ class DynamicEntitySchemaTest extends TestCase * * @var array */ - public $fixtures = [ + public array $fixtures = [ 'plugin.CrudJsonApi.Countries', 'plugin.CrudJsonApi.Cultures', 'plugin.CrudJsonApi.Currencies', @@ -75,14 +76,14 @@ public function testGetAttributes() $this->assertSame($expectedSecondCultureId, $entity['cultures'][1]['id']); // get required AssociationsCollection - $listener = new JsonApiListener(new Controller()); + $listener = new JsonApiListener(new Controller(new ServerRequest())); $this->setReflectionClassInstance($listener); $associations = $this->callProtectedMethod('_getContainedAssociations', [$table, $query->getContain()], $listener); $repositories = $this->callProtectedMethod('_getRepositoryList', [$table, $associations], $listener); // make view return associations on get('_associations') call $view = $this - ->getMockBuilder(View::class) + ->getMockBuilder(JsonApiView::class) ->onlyMethods(['get']) ->disableOriginalConstructor() ->getMock(); @@ -155,13 +156,13 @@ public function testRelationships() $this->assertSame($expectedSecondCultureId, $entity['cultures'][1]['id']); // get required AssociationsCollection - $listener = new JsonApiListener(new Controller()); + $listener = new JsonApiListener(new Controller(new ServerRequest())); $this->setReflectionClassInstance($listener); $associations = $this->callProtectedMethod('_getContainedAssociations', [$table, $query->getContain()], $listener); $repositories = $this->callProtectedMethod('_getRepositoryList', [$table, $associations], $listener); // make view return associations on get('_associations') call - $view = new View(); + $view = new JsonApiView(); $view->setConfig('repositories', $repositories); $view->setConfig('absoluteLinks', false); // test relative links (listener default) diff --git a/tests/TestCase/View/JsonApiViewTest.php b/tests/TestCase/View/JsonApiViewTest.php index 51d67ea3..f984d485 100644 --- a/tests/TestCase/View/JsonApiViewTest.php +++ b/tests/TestCase/View/JsonApiViewTest.php @@ -7,7 +7,6 @@ use Cake\Core\Configure; use Cake\Core\Plugin; use Cake\Event\Event; -use Cake\Http\Response; use Cake\Http\ServerRequest; use Cake\ORM\Entity; use Cake\ORM\ResultSet; @@ -34,7 +33,7 @@ class JsonApiViewTest extends TestCase * * @var array */ - public $fixtures = [ + public array $fixtures = [ 'plugin.CrudJsonApi.Countries', 'plugin.CrudJsonApi.Currencies', 'plugin.CrudJsonApi.Cultures', @@ -63,7 +62,7 @@ public function setUp(): void require CONFIG . 'routes.php'; - $listener = new JsonApiListener(new Controller()); + $listener = new JsonApiListener(new Controller(new ServerRequest())); $this->_defaultOptions = [ 'urlPrefix' => $listener->getConfig('urlPrefix'), @@ -130,8 +129,7 @@ protected function _getView(?string $tableName, array $viewVars = [], array $opt // create required (but non user configurable) viewVars next $request = new ServerRequest(); - $response = new Response(); - $controller = new Controller($request, $response, $tableName); + $controller = new Controller($request); $builder = $controller->viewBuilder(); $builder @@ -147,7 +145,7 @@ protected function _getView(?string $tableName, array $viewVars = [], array $opt // still here, create view with viewVars for response with resource(s) $controller->setName($tableName); // e.g. Countries - $table = $controller->loadModel(); // table object + $table = $controller->fetchTable($tableName); // table object // fetch data from test viewVar normally found in subject $subject = new Subject(['event' => new Event('Crud.beforeHandle')]); @@ -157,7 +155,7 @@ protected function _getView(?string $tableName, array $viewVars = [], array $opt } else { $subject->entity = $findResult; } - $subject->query = $table->query(); + $subject->query = $table->selectQuery(); // create required '_entities' and '_associations' viewVars normally // produced and set by the JsonApiListener @@ -242,7 +240,7 @@ public function testEncodeWithDynamicSchemas(): void $this->assertSameAsFile( $this->_JsonApiResponseBodyFixtures . DS . 'FetchingCollections' . DS . 'get-countries-without-pagination.json', - $view->render() + $view->render(), ); // test single entity without relationships @@ -253,7 +251,7 @@ public function testEncodeWithDynamicSchemas(): void $this->assertSameAsFile( $this->_JsonApiResponseBodyFixtures . DS . 'FetchingResources' . DS . 'get-country-no-relationships.json', - $view->render() + $view->render(), ); } @@ -279,7 +277,7 @@ public function testEncodeWithoutSchemas(): void $this->assertSameAsFile( $this->_JsonApiResponseBodyFixtures . DS . 'MetaInformation' . DS . 'meta-only.json', - $view->render() + $view->render(), ); } @@ -297,11 +295,7 @@ public function testOptionalWithJsonApiVersion(): void ], [ 'withJsonApiVersion' => true, ]); - $expectedVersionArray = [ - 'jsonapi' => [ - 'version' => '1.1', - ], - ]; + $jsonApi = json_decode($view->render(), true); $this->assertArrayHasKey('jsonapi', $jsonApi); $this->assertSame(['version' => '1.1'], $jsonApi['jsonapi']); @@ -316,15 +310,7 @@ public function testOptionalWithJsonApiVersion(): void 'meta-key-2' => 'meta-val-2', ], ]); - $expectedVersionArray = [ - 'jsonapi' => [ - 'version' => '1.1', - 'meta' => [ - 'meta-key-1' => 'meta-val-1', - 'meta-key-2' => 'meta-val-2', - ], - ], - ]; + $this->assertArrayHasKey('jsonapi', json_decode($view->render(), true)); $this->assertSame(['version' => '1.1', 'meta' => ['meta-key-1' => 'meta-val-1', 'meta-key-2' => 'meta-val-2']], json_decode($view->render(), true)['jsonapi']); @@ -352,11 +338,7 @@ public function testOptionalMeta(): void 'author' => 'bravo-kernel', ], ]); - $expectedMetaArray = [ - 'meta' => [ - 'author' => 'bravo-kernel', - ], - ]; + $this->assertArrayHasKey('meta', json_decode($view->render(), true)); $this->assertSame(['author' => 'bravo-kernel'], json_decode($view->render(), true)['meta']); @@ -405,7 +387,7 @@ public function testOptionalDebugPrettyPrint(): void $this->assertSameAsFile( $this->_JsonApiResponseBodyFixtures . DS . 'FetchingResources' . DS . 'get-country-no-relationships.json', - $view->render() + $view->render(), ); // make sure we can produce non-pretty in debug mode as well @@ -422,7 +404,7 @@ public function testOptionalDebugPrettyPrint(): void $this->assertSame( '{"data":{"type":"countries","id":"1","attributes":{"code":"NL","dummyCounter":11111,"name":"The Netherlands"},"relationships":{"currency":{"links":{"self":"\/countries\/1\/relationships\/currency","related":"\/countries\/1\/currency"},"data":{"type":"currencies","id":"1"}},"nationalCapital":{"links":{"self":"\/countries\/1\/relationships\/nationalCapital","related":"\/countries\/1\/nationalCapital"},"data":{"type":"nationalCapitals","id":"1"}},"cultures":{"links":{"self":"\/countries\/1\/relationships\/cultures","related":"\/countries\/1\/cultures"}},"nationalCities":{"links":{"self":"\/countries\/1\/relationships\/nationalCities","related":"\/countries\/1\/nationalCities"}},"subcountries":{"links":{"self":"\/countries\/1\/relationships\/subcountries","related":"\/countries\/1\/subcountries"}},"supercountry":{"links":{"self":"\/countries\/1\/relationships\/supercountry","related":"\/countries\/1\/supercountry"}},"languages":{"links":{"self":"\/countries\/1\/relationships\/languages","related":"\/countries\/1\/languages"}}},"links":{"self":"\/countries\/1"}}}', - $view->render() + $view->render(), ); } @@ -453,7 +435,7 @@ public function testGetDataToSerializeFromViewVarsSuccess(): void $view = $this ->getMockBuilder(JsonApiView::class) ->disableOriginalConstructor() - ->setMethods(null) + ->onlyMethods([]) ->getMock(); $this->setReflectionClassInstance($view); @@ -465,7 +447,7 @@ public function testGetDataToSerializeFromViewVarsSuccess(): void $this->assertSame( 'dummy-would-normally-be-an-entity-or-resultset', - $this->callProtectedMethod('_getDataToSerializeFromViewVars', [], $view) + $this->callProtectedMethod('_getDataToSerializeFromViewVars', [], $view), ); // make sure null is returned when no data is found (which would mean @@ -489,7 +471,7 @@ public function testGetDataToSerializeFromViewVarsSuccess(): void $this->assertSame( 'dummy-country-would-normally-be-an-entity-or-resultset', - $this->callProtectedMethod('_getDataToSerializeFromViewVars', [$parameters], $view) + $this->callProtectedMethod('_getDataToSerializeFromViewVars', [$parameters], $view), ); // In this case the first entity in the _serialize array does not have @@ -515,7 +497,7 @@ public function testGetDataToSerializeFromViewVarsObjectExcecption(): void $this->expectException(CrudException::class); $this->expectExceptionMessage( 'Assigning an object to JsonApiListener "serialize" is deprecated, ' . - 'assign the object to its own variable and assign "serialize" = true instead.' + 'assign the object to its own variable and assign "serialize" = true instead.', ); $view = $this ->getMockBuilder(JsonApiView::class) @@ -536,7 +518,7 @@ public function testJsonOptions(): void $view = $this ->getMockBuilder(JsonApiView::class) ->disableOriginalConstructor() - ->setMethods(null) + ->onlyMethods([]) ->getMock(); $this->setReflectionClassInstance($view); @@ -550,7 +532,7 @@ public function testJsonOptions(): void [ JSON_HEX_AMP, // 2 JSON_HEX_QUOT, // 8 - ] + ], ); $this->assertEquals(10, $this->callProtectedMethod('_jsonOptions', [], $view)); @@ -563,7 +545,7 @@ public function testJsonOptions(): void [ JSON_HEX_AMP, // 2 JSON_HEX_QUOT, // 8 - ] + ], ); $this->assertEquals(138, $this->callProtectedMethod('_jsonOptions', [], $view)); @@ -577,7 +559,7 @@ public function testJsonOptions(): void [ JSON_HEX_AMP, // 2 JSON_HEX_QUOT, // 8 - ] + ], ); $this->assertEquals(10, $this->callProtectedMethod('_jsonOptions', [], $view)); @@ -590,7 +572,7 @@ public function testJsonOptions(): void [ JSON_HEX_AMP, // 2 JSON_HEX_QUOT, // 8 - ] + ], ); $this->assertEquals(10, $this->callProtectedMethod('_jsonOptions', [], $view)); } @@ -663,7 +645,7 @@ public function testGetPaginationLinks(): void $view = $this ->getMockBuilder(JsonApiView::class) ->disableOriginalConstructor() - ->setMethods(null) + ->onlyMethods([]) ->getMock(); $this->setReflectionClassInstance($view); diff --git a/tests/bootstrap.php b/tests/bootstrap.php index 3880bec9..4974583f 100644 --- a/tests/bootstrap.php +++ b/tests/bootstrap.php @@ -1,7 +1,8 @@ create(TMP . 'cache/models', 0777); -$TMP->create(TMP . 'cache/persistent', 0777); -$TMP->create(TMP . 'cache/views', 0777); +$tmps = ['models', 'persistent', 'views']; +foreach ($tmps as $tmp) { + if (!is_dir(sprintf('%scache/%s', TMP, $tmp))) { + mkdir(sprintf('%scache/%s', TMP, $tmp), 0777, true); + } +} $cache = [ 'default' => [ 'engine' => 'File' ], + # This config key wasn't deprecated until CakePHP 5.1. + # @link https://book.cakephp.org/5/en/core-libraries/caching.html#configuring-cache-engines '_cake_core_' => [ 'className' => 'File', 'prefix' => 'crud_myapp_cake_core_', @@ -68,6 +73,13 @@ 'serialize' => true, 'duration' => '+10 seconds' ], + '_cake_translations_' => [ + 'className' => 'File', + 'prefix' => 'crud_myapp_cake_translations_', + 'path' => CACHE . 'persistent/', + 'serialize' => true, + 'duration' => '+10 seconds' + ], '_cake_model_' => [ 'className' => 'File', 'prefix' => 'crud_my_app_cake_model_', @@ -90,17 +102,18 @@ putenv('DB_URL=sqlite:///:memory:'); } -Cake\Datasource\ConnectionManager::setConfig( - 'test', - [ - 'url' => getenv('DB_URL'), - 'timezone' => 'UTC' - ] -); +// Configure both 'default' and 'test' datasources. +$dbConfig = [ + 'url' => getenv('DB_URL'), + 'timezone' => 'UTC' +]; -Plugin::getCollection()->add(new \Crud\Plugin()); -Plugin::getCollection()->add(new \CrudJsonApi\Plugin()); +ConnectionManager::setConfig('default', $dbConfig); +ConnectionManager::setConfig('test', $dbConfig); +ConnectionManager::alias('test', 'default'); + +$loader = new SchemaLoader(); +$loader->loadInternalFile(ROOT . '/tests/Fixture/schema.php'); -Configure::write('Error.ignoredDeprecationPaths', [ - 'vendor/cakephp/cakephp/src/TestSuite/Fixture/FixtureInjector.php', -]); +Plugin::getCollection()->add(new \Crud\CrudPlugin()); +Plugin::getCollection()->add(new \CrudJsonApi\Plugin()); diff --git a/tests/test_app/config/routes.php b/tests/test_app/config/routes.php index 035ae314..cbf5ff30 100644 --- a/tests/test_app/config/routes.php +++ b/tests/test_app/config/routes.php @@ -17,5 +17,8 @@ 'Countries', 'Currencies', 'Cultures', + 'Languages', + 'NationalCapitals', + 'NationalCities', ], $routes); }); diff --git a/tests/test_app/src/Controller/CountriesController.php b/tests/test_app/src/Controller/CountriesController.php index 23d3db75..c3075a38 100644 --- a/tests/test_app/src/Controller/CountriesController.php +++ b/tests/test_app/src/Controller/CountriesController.php @@ -13,13 +13,12 @@ class CountriesController extends Controller { use ControllerTrait; - public $paginate = ['limit' => 3]; + public array $paginate = ['limit' => 3]; public function initialize(): void { parent::initialize(); - $this->loadComponent('RequestHandler'); $this->loadComponent('Flash'); $this->loadComponent( 'Crud.Crud', @@ -37,7 +36,7 @@ public function initialize(): void 'CrudJsonApi.Pagination', 'Crud.Search', ], - ] + ], ); } } diff --git a/tests/test_app/src/Controller/CurrenciesController.php b/tests/test_app/src/Controller/CurrenciesController.php index e7c108a5..f4bb2137 100644 --- a/tests/test_app/src/Controller/CurrenciesController.php +++ b/tests/test_app/src/Controller/CurrenciesController.php @@ -13,13 +13,12 @@ class CurrenciesController extends Controller { use ControllerTrait; - public $paginate = ['limit' => 3]; + public array $paginate = ['limit' => 3]; public function initialize(): void { parent::initialize(); - $this->loadComponent('RequestHandler'); $this->loadComponent('Flash'); $this->loadComponent( 'Crud.Crud', @@ -35,7 +34,7 @@ public function initialize(): void 'listeners' => [ 'CrudJsonApi.JsonApi', ], - ] + ], ); } } diff --git a/tests/test_app/src/Controller/LanguagesController.php b/tests/test_app/src/Controller/LanguagesController.php index 506f180b..1d2b0a4c 100644 --- a/tests/test_app/src/Controller/LanguagesController.php +++ b/tests/test_app/src/Controller/LanguagesController.php @@ -13,13 +13,12 @@ class LanguagesController extends Controller { use ControllerTrait; - public $paginate = ['limit' => 3]; + public array $paginate = ['limit' => 3]; public function initialize(): void { parent::initialize(); - $this->loadComponent('RequestHandler'); $this->loadComponent('Flash'); $this->loadComponent( 'Crud.Crud', @@ -35,7 +34,7 @@ public function initialize(): void 'listeners' => [ 'CrudJsonApi.JsonApi', ], - ] + ], ); } } diff --git a/tests/test_app/src/Controller/NationalCapitalsController.php b/tests/test_app/src/Controller/NationalCapitalsController.php index cd2a3378..25a5aa35 100644 --- a/tests/test_app/src/Controller/NationalCapitalsController.php +++ b/tests/test_app/src/Controller/NationalCapitalsController.php @@ -17,7 +17,6 @@ public function initialize(): void { parent::initialize(); - $this->loadComponent('RequestHandler'); $this->loadComponent('Flash'); $this->loadComponent( 'Crud.Crud', @@ -34,7 +33,7 @@ public function initialize(): void 'CrudJsonApi.JsonApi', 'CrudJsonApi.Pagination', ], - ] + ], ); } } diff --git a/tests/test_app/src/Controller/NationalCitiesController.php b/tests/test_app/src/Controller/NationalCitiesController.php index 479bac22..4cd22227 100644 --- a/tests/test_app/src/Controller/NationalCitiesController.php +++ b/tests/test_app/src/Controller/NationalCitiesController.php @@ -18,7 +18,6 @@ public function initialize(): void { parent::initialize(); - $this->loadComponent('RequestHandler'); $this->loadComponent('Flash'); $this->loadComponent( 'Crud.Crud', @@ -35,7 +34,7 @@ public function initialize(): void 'CrudJsonApi.JsonApi', 'CrudJsonApi.Pagination', ], - ] + ], ); } diff --git a/tests/test_app/src/Model/Table/CountriesTable.php b/tests/test_app/src/Model/Table/CountriesTable.php index 00090708..dfa223af 100644 --- a/tests/test_app/src/Model/Table/CountriesTable.php +++ b/tests/test_app/src/Model/Table/CountriesTable.php @@ -32,7 +32,9 @@ public function initialize(array $config): void 'propertyName' => 'supercountry', ]); - $this->belongsToMany('Languages'); + $this + ->belongsToMany('Languages') + ->setThrough('CountriesLanguages'); } public function validationDefault(Validator $validator): Validator diff --git a/tests/test_app/src/Model/Table/LanguagesTable.php b/tests/test_app/src/Model/Table/LanguagesTable.php index 7467c6df..768b7f53 100644 --- a/tests/test_app/src/Model/Table/LanguagesTable.php +++ b/tests/test_app/src/Model/Table/LanguagesTable.php @@ -12,6 +12,8 @@ class LanguagesTable extends Table { public function initialize(array $config): void { - $this->belongsToMany('Countries'); + $this + ->belongsToMany('Countries') + ->setThrough('CountriesLanguages'); } }