├── sanbox └── .gitignore ├── .gitignore ├── src ├── Response │ ├── Objects │ │ └── Contracts │ │ │ ├── ActionMessage.php │ │ │ ├── ResourceDetail.php │ │ │ └── ResourceCollection.php │ ├── Exceptions │ │ ├── Contracts │ │ │ └── ApplicationExceptionInterface.php │ │ ├── ConflictException.php │ │ ├── NotFoundException.php │ │ ├── BadRequestException.php │ │ ├── ForbiddenException.php │ │ ├── UnauthorizedException.php │ │ ├── MethodNotAllowedException.php │ │ └── InternalServerErrorException.php │ ├── Compose │ │ ├── ResourceResponsible.php │ │ └── Handlers │ │ │ └── ResponseCompose.php │ └── ResourceResponseInterface.php ├── FieldSelector │ ├── Objects │ │ ├── Structure │ │ │ ├── CollectionStructure.php │ │ │ ├── ObjectStructure.php │ │ │ └── Abstracts │ │ │ │ └── FieldStructure.php │ │ └── FieldObject.php │ ├── Exceptions │ │ └── FieldSelectorSyntaxErrorException.php │ ├── FieldSelectable.php │ └── Notifications │ │ └── FieldSelectorErrorBag.php ├── ResourceFilter │ ├── Exceptions │ │ ├── FilteringException.php │ │ ├── FilteringInvalidException.php │ │ ├── FilteringRequiredException.php │ │ ├── FilteringInvalidRuleException.php │ │ ├── FilteringRuleAlreadyDefinedException.php │ │ ├── FilteringInvalidValueException.php │ │ ├── FilteringRequiredWithException.php │ │ ├── FilteringInvalidRuleOperatorException.php │ │ ├── FilteringRequiredIfException.php │ │ ├── FilteringInvalidValueTypeException.php │ │ └── FilteringInvalidValueSyntaxException.php │ ├── Objects │ │ ├── FilteringOptions.php │ │ ├── FilteringObject.php │ │ ├── InvalidFilteringValue.php │ │ └── FilteringRules.php │ ├── ResourceFilterable.php │ └── Notifications │ │ └── ResourceFilterErrorBag.php ├── Common │ ├── Objects │ │ ├── BaseAlchemistComponent.php │ │ ├── BaseAlchemistErrorBag.php │ │ └── ParamBag.php │ ├── Helpers │ │ ├── Numerics.php │ │ ├── Strings.php │ │ ├── DotArrays.php │ │ └── Arrays.php │ ├── Notification │ │ ├── ErrorBag.php │ │ └── CompoundErrors.php │ ├── Integrations │ │ ├── StatefulAlchemistQueryable.php │ │ ├── AlchemistQueryable.php │ │ └── Adapters │ │ │ └── AlchemistAdapter.php │ └── Exceptions │ │ └── AlchemistRestfulApiException.php ├── ResourceSort │ ├── Objects │ │ └── ResourceSortObject.php │ ├── Notifications │ │ └── ResourceSortErrorBag.php │ ├── ResourceSortable.php │ └── Handlers │ │ └── ResourceSort.php ├── ResourceSearch │ ├── Objects │ │ └── ResourceSearchObject.php │ ├── Handlers │ │ └── ResourceSearch.php │ ├── Notifications │ │ └── ResourceSearchErrorBag.php │ └── ResourceSearchable.php ├── ResourcePaginations │ └── OffsetPaginator │ │ ├── Objects │ │ └── OffsetPaginateObject.php │ │ ├── Handlers │ │ └── ResourceOffsetPaginator.php │ │ ├── ResourceOffsetPaginate.php │ │ └── Notifications │ │ └── ResourceOffsetPaginationErrorBag.php └── AlchemistRestfulApi.php ├── phpunit.xml ├── LICENSE ├── CONTRIBUTING.md ├── composer.json ├── docs └── changelogs │ └── 2_0_1_Add_support_for_Adapter_to_change_parameter_name_in_request_input.md ├── STRUCTURE.md └── tests └── Feature ├── AlchemistAdapter ├── AlchemistAdapter_FieldSelectorConfiguration_Test.php ├── AlchemistAdapter_ResourceSearchConfiguration_Test.php ├── AlchemistAdapter_ResourceSortConfiguration_Test.php ├── AlchemistAdapter_ResourceOffsetPaginator_Test.php └── AlchemistAdapter_ResourceFilterConfiguration_Test.php ├── ResourceSearchTest.php ├── FieldSelectorTest.php └── ResourceSortTest.php /sanbox/.gitignore: -------------------------------------------------------------------------------- 1 | * 2 | !.gitignore 3 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .idea/ 2 | /vendor/ 3 | composer.lock 4 | composer.phar 5 | .phpunit.result.cache -------------------------------------------------------------------------------- /src/Response/Objects/Contracts/ActionMessage.php: -------------------------------------------------------------------------------- 1 | 2 | 7 | 8 | 9 | ./tests/Feature 10 | 11 | 12 | 13 | -------------------------------------------------------------------------------- /src/ResourceFilter/Exceptions/FilteringInvalidException.php: -------------------------------------------------------------------------------- 1 | responseCompose = new ResponseCompose($alchemist); 22 | } 23 | 24 | /** 25 | * @return ResponseCompose 26 | */ 27 | public function responseCompose(): ResponseCompose 28 | { 29 | return $this->responseCompose; 30 | } 31 | } -------------------------------------------------------------------------------- /src/ResourceSort/Objects/ResourceSortObject.php: -------------------------------------------------------------------------------- 1 | sortField = $sortField; 23 | $this->direction = $direction; 24 | } 25 | 26 | /** 27 | * @return string 28 | */ 29 | public function getSortField(): string 30 | { 31 | return $this->sortField; 32 | } 33 | 34 | /** 35 | * @return string 36 | */ 37 | public function getDirection(): string 38 | { 39 | return $this->direction; 40 | } 41 | } -------------------------------------------------------------------------------- /src/ResourceSearch/Objects/ResourceSearchObject.php: -------------------------------------------------------------------------------- 1 | searchValue = $searchValue; 23 | $this->searchCondition = $searchCondition; 24 | } 25 | 26 | /** 27 | * @return string 28 | */ 29 | public function getSearchValue(): string 30 | { 31 | return $this->searchValue; 32 | } 33 | 34 | /** 35 | * @return string 36 | */ 37 | public function getSearchCondition(): string 38 | { 39 | return $this->searchCondition; 40 | } 41 | } -------------------------------------------------------------------------------- /src/FieldSelector/Objects/Structure/Abstracts/FieldStructure.php: -------------------------------------------------------------------------------- 1 | name = $name; 15 | $this->substitute = $substitute; 16 | $this->fields = $fields; 17 | $this->defaultFields = $defaultFields; 18 | } 19 | 20 | public function getName(): string 21 | { 22 | return $this->name; 23 | } 24 | 25 | public function getSubstitute(): ?string 26 | { 27 | return $this->substitute; 28 | } 29 | 30 | public function getFields(): array 31 | { 32 | return $this->fields; 33 | } 34 | 35 | public function getDefaultFields(): array 36 | { 37 | return $this->defaultFields; 38 | } 39 | } -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2023 nvmcommunity.io 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /src/Common/Notification/ErrorBag.php: -------------------------------------------------------------------------------- 1 | passes = $passes; 26 | $this->errors = $errors; 27 | } 28 | 29 | /** 30 | * @return bool 31 | */ 32 | public function passes(): bool 33 | { 34 | return $this->passes; 35 | } 36 | 37 | /** 38 | * @param Closure|null $closure 39 | * @return void 40 | */ 41 | public function setErrorHandler(?Closure $closure): void 42 | { 43 | $this->errorHandler = $closure; 44 | } 45 | 46 | /** 47 | * @return mixed 48 | */ 49 | public function getErrors() 50 | { 51 | return $this->errorHandler ? call_user_func($this->errorHandler, $this->errors) : $this->errors; 52 | } 53 | } -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | # How to Contribute 2 | 3 | ## Pull Requests 4 | 5 | 1. Fork the Alchemist Restful API repository 6 | 2. Create a new branch for each feature or improvement 7 | 3. Send a pull request from each feature branch to the **master** branch 8 | 9 | It is very important to separate new features or improvements into separate feature branches, and to send a 10 | pull request for each branch. This allows me to review and pull in new features or improvements individually. 11 | 12 | ## Style Guide 13 | 14 | All pull requests must adhere to the [PSR-12 standard](https://github.com/php-fig/fig-standards/blob/master/accepted/PSR-12-extended-coding-style-guide.md). 15 | 16 | ## Dependencies 17 | 18 | The Alchemist Restful API tries to keep dependencies to zero. If you feel that a dependency is necessary, please discuss it with me before adding it to the project. 19 | 20 | ## Framework-agnostic 21 | 22 | The Alchemist Restful API is a framework-agnostic project. Please do not add any framework specific code to the project. 23 | 24 | ## Testing 25 | 26 | All pull requests must be accompanied by passing feature tests and complete code coverage. The Alchemist Restful API uses phpunit for testing. 27 | 28 | [Learn about PHPUnit](https://github.com/sebastianbergmann/phpunit/) 29 | -------------------------------------------------------------------------------- /src/Common/Notification/CompoundErrors.php: -------------------------------------------------------------------------------- 1 | offset = $offset; 34 | $this->limit = $limit; 35 | $this->maxLimit = $maxLimit; 36 | } 37 | 38 | /** 39 | * @return int 40 | */ 41 | public function getMaxLimit(): int 42 | { 43 | return $this->maxLimit; 44 | } 45 | 46 | /** 47 | * @return int 48 | */ 49 | public function getOffset(): int 50 | { 51 | return $this->offset; 52 | } 53 | 54 | /** 55 | * @return int 56 | */ 57 | public function getLimit(): int 58 | { 59 | return $this->limit; 60 | } 61 | } -------------------------------------------------------------------------------- /src/ResourceFilter/Exceptions/FilteringInvalidValueTypeException.php: -------------------------------------------------------------------------------- 1 | filteringTitle = $filteringTitle; 24 | $this->validValueTypes = $validValueTypes; 25 | 26 | $message = "Filtering [{$filteringTitle}] just only support value type: " . implode(', ', $validValueTypes) . '.'; 27 | 28 | parent::__construct($message, $code, $previous); 29 | } 30 | 31 | /** 32 | * @return string 33 | */ 34 | public function getFilteringTitle(): string 35 | { 36 | return $this->filteringTitle; 37 | } 38 | 39 | /** 40 | * @return array 41 | */ 42 | public function getValidValueTypes(): array 43 | { 44 | return $this->validValueTypes; 45 | } 46 | } -------------------------------------------------------------------------------- /src/ResourceFilter/Exceptions/FilteringInvalidValueSyntaxException.php: -------------------------------------------------------------------------------- 1 | filteringTitle = $filteringTitle; 16 | $this->validSyntax = $validSyntax; 17 | $this->filteringOperator = $filteringOperator; 18 | 19 | $message = "{$filteringTitle} is invalid value syntax. Syntax must be is {$validSyntax}. Example: {$example}"; 20 | 21 | parent::__construct($message, $code, $previous); 22 | } 23 | 24 | /** 25 | * @return string 26 | */ 27 | public function getFilteringOperator(): string 28 | { 29 | return $this->filteringOperator; 30 | } 31 | 32 | /** 33 | * @return string 34 | */ 35 | public function getFilteringTitle(): string 36 | { 37 | return $this->filteringTitle; 38 | } 39 | 40 | /** 41 | * @return string 42 | */ 43 | public function getValidSyntax(): string 44 | { 45 | return $this->validSyntax; 46 | } 47 | } -------------------------------------------------------------------------------- /composer.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "nvmcommunity/alchemist-restful-api", 3 | "description": "A library that helps you quickly get a rigorous and flexible RESTful-based API interface for your application.", 4 | "keywords": [ 5 | "api", 6 | "json", 7 | "rest", 8 | "restful", 9 | "rest-api", 10 | "restful-api", 11 | "swagger", 12 | "openapi", 13 | "crud", 14 | "service-discovery", 15 | "nvmcommunity.io", 16 | "Người-Viết-Mã" 17 | ], 18 | "type": "library", 19 | "license": "MIT", 20 | "autoload": { 21 | "psr-4": { 22 | "Nvmcommunity\\Alchemist\\RestfulApi\\": "src/" 23 | } 24 | }, 25 | "support": { 26 | "issues": "https://github.com/nvmcommunity/alchemist-restful-api/issues", 27 | "source": "https://github.com/nvmcommunity/alchemist-restful-api" 28 | }, 29 | "authors": [ 30 | { 31 | "name": "NVM Community (Người Viết Mã)", 32 | "email": "nvmcommunity.io@gmail.com", 33 | "homepage": "https://nvmcommunity.io" 34 | } 35 | ], 36 | "require": { 37 | "php": ">=7.4", 38 | "nette/utils": "*", 39 | "psr/http-message": "^2.0" 40 | }, 41 | "minimum-stability": "dev", 42 | "prefer-stable": true, 43 | "require-dev": { 44 | "phpstan/phpstan": "^1.10", 45 | "symfony/var-dumper": "^6.2", 46 | "phpunit/phpunit": "9.6.15" 47 | } 48 | } 49 | -------------------------------------------------------------------------------- /src/FieldSelector/Objects/FieldObject.php: -------------------------------------------------------------------------------- 1 | name = $fieldName; 36 | $this->limit = $limit; 37 | $this->subFields = $subFields; 38 | } 39 | 40 | /** 41 | * @return string 42 | */ 43 | public function getName(): string 44 | { 45 | return $this->name; 46 | } 47 | 48 | /** 49 | * @return int 50 | */ 51 | public function getLimit(): int 52 | { 53 | return $this->limit; 54 | } 55 | 56 | /** 57 | * @return string[] 58 | */ 59 | public function getSubFields(): array 60 | { 61 | return $this->subFields; 62 | } 63 | 64 | /** 65 | * @return string 66 | */ 67 | public function __toString(): string 68 | { 69 | return $this->name; 70 | } 71 | } -------------------------------------------------------------------------------- /src/ResourceFilter/Objects/FilteringOptions.php: -------------------------------------------------------------------------------- 1 | ['v_collection.valid:in' => [1]]] 17 | * 18 | * @var array 19 | */ 20 | private array $required_if = []; 21 | /** 22 | * Example: ['required_with' => ['s_string:any' => ['v_enum:eq', 'v_collection.valid:not_in']]] 23 | * 24 | * @var array 25 | */ 26 | private array $required_with = []; 27 | 28 | /** 29 | * @param array $params 30 | */ 31 | public function __construct(array $params) 32 | { 33 | foreach ($params as $field => $value) { 34 | $this->{$field} = $value; 35 | } 36 | } 37 | 38 | /** 39 | * @return array 40 | */ 41 | public function getRequired(): array 42 | { 43 | return $this->required; 44 | } 45 | 46 | /** 47 | * @return array 48 | */ 49 | public function getRequiredIf(): array 50 | { 51 | return $this->required_if; 52 | } 53 | 54 | /** 55 | * @return array 56 | */ 57 | public function getRequiredWith(): array 58 | { 59 | return $this->required_with; 60 | } 61 | } -------------------------------------------------------------------------------- /src/ResourceFilter/Objects/FilteringObject.php: -------------------------------------------------------------------------------- 1 | filtering = $filtering; 23 | $this->operator = $operator; 24 | $this->filteringValue = $filteringValue; 25 | } 26 | 27 | /** 28 | * @return string 29 | */ 30 | public function getFiltering(): string 31 | { 32 | return $this->filtering; 33 | } 34 | 35 | /** 36 | * @return string 37 | */ 38 | public function getOperator(): string 39 | { 40 | return $this->operator; 41 | } 42 | 43 | /** 44 | * @return mixed 45 | */ 46 | public function getFilteringValue() 47 | { 48 | return $this->filteringValue; 49 | } 50 | 51 | /** 52 | * @return array 53 | */ 54 | public function toArray(): array 55 | { 56 | return [ 57 | 'filtering' => $this->filtering, 58 | 'operator' => $this->operator, 59 | 'filteringValue' => $this->filteringValue, 60 | ]; 61 | } 62 | 63 | /** 64 | * @return array 65 | */ 66 | public function flatArray(): array 67 | { 68 | return [$this->filtering, $this->operator, $this->filteringValue]; 69 | } 70 | } -------------------------------------------------------------------------------- /src/ResourceSearch/Handlers/ResourceSearch.php: -------------------------------------------------------------------------------- 1 | originalInput = $searchValue; 30 | 31 | $this->searchValue = is_string($searchValue) ? $searchValue : ''; 32 | } 33 | 34 | /** 35 | * @param string $condition 36 | * @return ResourceSearch 37 | */ 38 | public function defineSearchCondition(string $condition): self 39 | { 40 | $this->searchCondition = $condition; 41 | 42 | return $this; 43 | } 44 | 45 | /** 46 | * @return ResourceSearchObject 47 | */ 48 | public function search(): ResourceSearchObject 49 | { 50 | return new ResourceSearchObject($this->searchValue, $this->searchCondition); 51 | } 52 | 53 | /** 54 | * @param $notification 55 | * @return ResourceSearchErrorBag 56 | */ 57 | public function validate(&$notification = null): ResourceSearchErrorBag 58 | { 59 | if (! is_null($this->originalInput) && ! is_string($this->originalInput)) { 60 | return $notification = new ResourceSearchErrorBag(false, true); 61 | } 62 | 63 | return $notification = new ResourceSearchErrorBag(true); 64 | } 65 | } -------------------------------------------------------------------------------- /src/Response/ResourceResponseInterface.php: -------------------------------------------------------------------------------- 1 | filtering = $filtering; 36 | $this->supportedType = $supportedType; 37 | $this->supportedValue = $supportedValue; 38 | $this->supportedFormats = $supportedFormats; 39 | } 40 | 41 | /** 42 | * @return string 43 | */ 44 | public function getFiltering(): string 45 | { 46 | return $this->filtering; 47 | } 48 | 49 | /** 50 | * @return string[] 51 | */ 52 | public function getSupportedType(): array 53 | { 54 | return $this->supportedType; 55 | } 56 | 57 | /** 58 | * @return array|null 59 | */ 60 | public function getSupportedValue(): ?array 61 | { 62 | return $this->supportedValue; 63 | } 64 | 65 | /** 66 | * @return array|null 67 | */ 68 | public function getSupportedFormats(): ?array 69 | { 70 | return $this->supportedFormats; 71 | } 72 | 73 | /** 74 | * @return array 75 | */ 76 | public function toArray(): array 77 | { 78 | return [ 79 | 'filtering' => $this->filtering, 80 | 'supported_type' => $this->supportedType, 81 | 'supported_value' => $this->supportedValue, 82 | 'supported_format' => $this->supportedFormats, 83 | ]; 84 | } 85 | } -------------------------------------------------------------------------------- /src/ResourceSearch/Notifications/ResourceSearchErrorBag.php: -------------------------------------------------------------------------------- 1 | passes = $passes; 31 | $this->invalidInputType = $invalidInputType; 32 | } 33 | 34 | /** 35 | * @return bool[] 36 | */ 37 | public function toArray(): array 38 | { 39 | return [ 40 | 'passes' => $this->passes, 41 | 'invalid_input_type' => $this->invalidInputType, 42 | ]; 43 | } 44 | 45 | /** 46 | * @return bool 47 | */ 48 | public function passes(): bool 49 | { 50 | return $this->passes; 51 | } 52 | 53 | /** 54 | * @return string 55 | */ 56 | public function errorKey(): string 57 | { 58 | return 'search'; 59 | } 60 | 61 | /** 62 | * @return array 63 | */ 64 | public function getMessages(): array 65 | { 66 | if ($this->passes()) { 67 | return []; 68 | } 69 | 70 | $messages = []; 71 | 72 | if ($this->isInvalidInputType()) { 73 | $messages[] = [ 74 | 'error_code' => static::INVALID_INPUT_TYPE, 75 | 'error_message' => "The input type is invalid. It must be a string.", 76 | ]; 77 | } 78 | 79 | return $messages; 80 | } 81 | 82 | /** 83 | * @return bool 84 | */ 85 | public function isInvalidInputType(): bool 86 | { 87 | return $this->invalidInputType; 88 | } 89 | } -------------------------------------------------------------------------------- /src/Common/Objects/ParamBag.php: -------------------------------------------------------------------------------- 1 | fields = $fields; 43 | $this->filtering = $filtering; 44 | $this->offsetPaginate = $offsetPaginate; 45 | $this->search = $search; 46 | $this->sort = $sort; 47 | } 48 | 49 | /** 50 | * @return array 51 | */ 52 | public function getFields(): array 53 | { 54 | return $this->fields; 55 | } 56 | 57 | /** 58 | * @return array 59 | */ 60 | public function getFiltering(): array 61 | { 62 | return $this->filtering; 63 | } 64 | 65 | /** 66 | * @return OffsetPaginateObject 67 | */ 68 | public function getOffsetPaginate(): OffsetPaginateObject 69 | { 70 | return $this->offsetPaginate; 71 | } 72 | 73 | /** 74 | * @return ResourceSearchObject 75 | */ 76 | public function getSearch(): ResourceSearchObject 77 | { 78 | return $this->search; 79 | } 80 | 81 | /** 82 | * @return ResourceSortObject 83 | */ 84 | public function getSort(): ResourceSortObject 85 | { 86 | return $this->sort; 87 | } 88 | } -------------------------------------------------------------------------------- /src/FieldSelector/FieldSelectable.php: -------------------------------------------------------------------------------- 1 | adapter->componentConfigs(); 22 | 23 | if (! isset($componentConfig[FieldSelector::class])) { 24 | throw new AlchemistRestfulApiException( 25 | "The `FieldSelector` component is not configured! 26 | Add below config into componentConfigs in your Adapter (inheritance of AlchemistAdapter): 27 | 28 | ``` 29 | FieldSelector::class => [ 30 | 'request_params' => [ 31 | 'fields_param' => 'fields', 32 | ] 33 | ], 34 | ``` 35 | ", AlchemistRestfulApiException::FIELD_SELECTOR_CONFIGURATION_INCORRECT 36 | ); 37 | } 38 | 39 | if (! isset($componentConfig[FieldSelector::class]['request_params'])) { 40 | throw new AlchemistRestfulApiException("Missing `request_params` for `FieldSelector` component configuration! 41 | Here is an example of the correct configuration of componentConfigs in your Adapter (inheritance of AlchemistAdapter): 42 | 43 | ``` 44 | FieldSelector::class => [ 45 | 'request_params' => [ 46 | 'fields_param' => 'fields', 47 | ] 48 | ], 49 | ``` 50 | ", AlchemistRestfulApiException::FIELD_SELECTOR_CONFIGURATION_INCORRECT 51 | ); 52 | } 53 | 54 | if (! isset($componentConfig[FieldSelector::class]['request_params']['fields_param'])) { 55 | throw new AlchemistRestfulApiException("Missing `fields_param` for `request_params` in `FieldSelector` component configuration! 56 | Here is an example of the correct configuration of componentConfigs in your Adapter (inheritance of AlchemistAdapter): 57 | 58 | ``` 59 | FieldSelector::class => [ 60 | 'request_params' => [ 61 | 'fields_param' => 'fields', 62 | ] 63 | ], 64 | ``` 65 | ", AlchemistRestfulApiException::FIELD_SELECTOR_CONFIGURATION_INCORRECT); 66 | } 67 | 68 | $fieldsParam = $componentConfig[FieldSelector::class]['request_params']['fields_param']; 69 | 70 | $this->fieldSelector = new FieldSelector($requestInput[$fieldsParam] ?? null); 71 | } 72 | 73 | /** 74 | * @return FieldSelector 75 | */ 76 | public function fieldSelector(): FieldSelector 77 | { 78 | return $this->fieldSelector; 79 | } 80 | } -------------------------------------------------------------------------------- /src/ResourceSearch/ResourceSearchable.php: -------------------------------------------------------------------------------- 1 | adapter->componentConfigs(); 22 | 23 | if (! isset($componentConfig[ResourceSearch::class])) { 24 | throw new AlchemistRestfulApiException("The `ResourceSearch` component is not configured! 25 | Here is an example of the correct configuration of componentConfigs in your Adapter (inheritance of AlchemistAdapter): 26 | 27 | ``` 28 | ResourceSearch::class => [ 29 | 'request_params' => [ 30 | 'search_param' => 'search', 31 | ] 32 | ], 33 | ``` 34 | ", AlchemistRestfulApiException::RESOURCE_SEARCH_CONFIGURATION_INCORRECT); 35 | } 36 | 37 | if (! isset($componentConfig[ResourceSearch::class]['request_params'])) { 38 | throw new AlchemistRestfulApiException("Missing `request_params` configuration for `ResourceSearch` component! 39 | Here is an example of the correct configuration of componentConfigs in your Adapter (inheritance of AlchemistAdapter): 40 | 41 | ``` 42 | ResourceSearch::class => [ 43 | 'request_params' => [ 44 | 'search_param' => 'search', 45 | ] 46 | ], 47 | ``` 48 | ", AlchemistRestfulApiException::RESOURCE_SEARCH_CONFIGURATION_INCORRECT); 49 | } 50 | 51 | if (! isset($componentConfig[ResourceSearch::class]['request_params']['search_param'])) { 52 | throw new AlchemistRestfulApiException("Missing `search_param` configuration for request_params in `ResourceSearch` component configuration! 53 | Here is an example of the correct configuration of componentConfigs in your Adapter (inheritance of AlchemistAdapter): 54 | 55 | ``` 56 | ResourceSearch::class => [ 57 | 'request_params' => [ 58 | 'search_param' => 'search', 59 | ] 60 | ], 61 | ``` 62 | ", AlchemistRestfulApiException::RESOURCE_SEARCH_CONFIGURATION_INCORRECT); 63 | } 64 | 65 | $searchParam = $componentConfig[ResourceSearch::class]['request_params']['search_param']; 66 | 67 | $this->resourceSearch = new ResourceSearch($requestInput[$searchParam] ?? null); 68 | } 69 | 70 | /** 71 | * @return ResourceSearch 72 | */ 73 | public function resourceSearch(): ResourceSearch 74 | { 75 | return $this->resourceSearch; 76 | } 77 | } -------------------------------------------------------------------------------- /src/ResourceFilter/ResourceFilterable.php: -------------------------------------------------------------------------------- 1 | adapter->componentConfigs(); 23 | 24 | if (! isset($componentConfig[ResourceFilter::class])) { 25 | throw new AlchemistRestfulApiException("The `ResourceFilter` component is not configured! 26 | Here is an example of the correct configuration of componentConfigs in your Adapter (inheritance of AlchemistAdapter): 27 | 28 | ``` 29 | ResourceFilter::class => [ 30 | 'request_params' => [ 31 | 'filtering_param' => 'filter', 32 | ] 33 | ], 34 | ``` 35 | ", AlchemistRestfulApiException::RESOURCE_FILTER_CONFIGURATION_INCORRECT); 36 | } 37 | 38 | if (! isset($componentConfig[ResourceFilter::class]['request_params'])) { 39 | throw new AlchemistRestfulApiException("Missing `request_params` configuration for `ResourceFilter` component! 40 | Here is an example of the correct configuration of componentConfigs in your Adapter (inheritance of AlchemistAdapter): 41 | 42 | ``` 43 | ResourceFilter::class => [ 44 | 'request_params' => [ 45 | 'filtering_param' => 'filter', 46 | ] 47 | ], 48 | ``` 49 | ", AlchemistRestfulApiException::RESOURCE_FILTER_CONFIGURATION_INCORRECT); 50 | } 51 | 52 | if (! isset($componentConfig[ResourceFilter::class]['request_params']['filtering_param'])) { 53 | throw new AlchemistRestfulApiException("Missing `filtering_param` configuration for request_params in `ResourceFilter` component configuration! 54 | Here is an example of the correct configuration of componentConfigs in your Adapter (inheritance of AlchemistAdapter): 55 | 56 | ``` 57 | ResourceFilter::class => [ 58 | 'request_params' => [ 59 | 'filtering_param' => 'filter', 60 | ] 61 | ], 62 | ``` 63 | ", AlchemistRestfulApiException::RESOURCE_FILTER_CONFIGURATION_INCORRECT); 64 | } 65 | 66 | $filteringParam = $componentConfig[ResourceFilter::class]['request_params']['filtering_param']; 67 | 68 | $this->resourceFilter = new ResourceFilter($requestInput[$filteringParam] ?? null); 69 | } 70 | 71 | /** 72 | * @return ResourceFilter 73 | */ 74 | public function resourceFilter(): ResourceFilter 75 | { 76 | return $this->resourceFilter; 77 | } 78 | } -------------------------------------------------------------------------------- /docs/changelogs/2_0_1_Add_support_for_Adapter_to_change_parameter_name_in_request_input.md: -------------------------------------------------------------------------------- 1 | # Change Log 2 | 3 | ## [2.0.1] - 2024-04-15 4 | 5 | ## Add support for AlchemistAdapter to change parameter name in request input of default components 6 | 7 | ### Problem 8 | 9 | Currently, all default components of the Alchemist Restful API use hardcoded parameter names in the request input. This makes it difficult for users to customize the parameter names to suit their needs. 10 | 11 | ### Solution 12 | 13 | Add support for the `AlchemistAdapter` to change the parameter name in the request input of the default components. This will allow users to customize the parameter names to suit their needs. 14 | 15 | ### Implementation 16 | 17 | Create your own `MyAdapter` class extending `AlchemistAdapter` and override the `componentConfigs` method to change the overall configuration of the default components that. Also, change the parameter name in the request input of those components. 18 | 19 | ```php 20 | 21 | use Nvmcommunity\Alchemist\RestfulApi\FieldSelector\Handlers\FieldSelector; 22 | use Nvmcommunity\Alchemist\RestfulApi\ResourceFilter\Handlers\ResourceFilter; 23 | use Nvmcommunity\Alchemist\RestfulApi\ResourcePaginations\OffsetPaginator\Handlers\ResourceOffsetPaginator; 24 | use Nvmcommunity\Alchemist\RestfulApi\ResourceSearch\Handlers\ResourceSearch; 25 | use Nvmcommunity\Alchemist\RestfulApi\ResourceSort\Handlers\ResourceSort; 26 | 27 | class MyAdapter extends AlchemistAdapter 28 | { 29 | /** 30 | * @return array 31 | */ 32 | public function componentConfigs(): array 33 | { 34 | return [ 35 | FieldSelector::class => [ 36 | 'request_params' => [ 37 | // change the fields parameter to whatever you want 38 | 'fields_param' => 'fields', 39 | ] 40 | ], 41 | ResourceFilter::class => [ 42 | 'request_params' => [ 43 | // change the filtering parameter to whatever you want 44 | 'filtering_param' => 'filtering', 45 | ] 46 | ], 47 | ResourceOffsetPaginator::class => [ 48 | 'request_params' => [ 49 | // change the limit and offset parameter to whatever you want 50 | 'limit_param' => 'limit', 51 | 'offset_param' => 'offset', 52 | ] 53 | ], 54 | ResourceSort::class => [ 55 | 'request_params' => [ 56 | // change the sort and direction parameter to whatever you want 57 | 'sort_param' => 'sort', 58 | 'direction_param' => 'direction', 59 | ] 60 | ], 61 | ResourceSearch::class => [ 62 | 'request_params' => [ 63 | // change the search parameter to whatever you want 64 | 'search_param' => 'search', 65 | ] 66 | ] 67 | ]; 68 | } 69 | } 70 | ``` 71 | 72 | Then, use the `MyAdapter` class when creating the `AlchemistRestfulApi` instance. 73 | 74 | ```php 75 | $restfulApi = AlchemistRestfulApi::for(ExampleOrderApiQuery::class, $requestInput, new MyAdapter()); 76 | ``` 77 | 78 | [Go back to home](../../README.md) -------------------------------------------------------------------------------- /STRUCTURE.md: -------------------------------------------------------------------------------- 1 | # Library Structure 2 | 3 | ## Overview 4 | 5 | Folder structure of the library is as follows: 6 | 7 | ``` 8 | library 9 | ├── src 10 | │ ├── Common (1) 11 | │ │ ├── Exceptions (1.1) 12 | │ │ │ ├── AlchemistRestfulApiException.php (1.1.1) 13 | │ │ ├── Helpers (1.2) 14 | │ │ │ ├── Arrays.php (1.2.1) 15 | │ │ │ ├── Strings.php (1.2.2) 16 | │ │ │ ├── Numerics.php (1.2.3) 17 | │ │ ├── Integrations (1.3) 18 | │ │ │ ├── Adapters (1.3.1) 19 | │ │ │ │ ├── AlchemistAdapter.php (1.3.1.1) 20 | │ │ │ ├── AlchemistQueryable.php (1.3.2) 21 | │ │ │ ├── StatefulAlchemistQueryable (1.3.3) 22 | │ │ ├── Notification (1.4) 23 | │ │ │ ├── ErrorBag.php (1.4.1) 24 | │ │ │ ├── CompoundErrors.php (1.4.2) 25 | │ ├── ModuleA (2) 26 | │ │ ├── Handlers (2.1) 27 | │ │ │ ├── ModuleA.php (2.1.1) 28 | │ │ ├── Notifications (2.2) 29 | │ │ │ ├── ModuleA[ErrorBag].php (2.2.1) 30 | │ │ ├── Objects (2.3) 31 | │ │ ├── ModuleA[able].php (2.4) 32 | │ ├── ModuleB 33 | │ │ ├── ... 34 | │ ├── ModuleC 35 | │ │ ├── ... 36 | │ ├── AlchemistRestfulApi.php (3) 37 | ``` 38 | 39 | ## Description 40 | 41 | ### 1. Common 42 | 43 | #### 1.1. Exceptions 44 | 45 | - **1.1.1. AlchemistRestfulApiException.php** 46 | - Custom exception class for the library. 47 | - Extends `Exception` class. 48 | - Used for throwing exceptions in the library. 49 | 50 | #### 1.2. Helpers 51 | 52 | - **1.2.1. Arrays.php** 53 | - Static class for array helper functions. 54 | - Contains functions for array manipulation. 55 | - Used for array operations in the library. 56 | - **1.2.2. Strings.php** 57 | - Static class for string helper functions. 58 | - Contains functions for string manipulation. 59 | - Used for string operations in the library. 60 | - **1.2.3. Numerics.php** 61 | - Static class for numeric helper functions. 62 | - Contains functions for numeric manipulation. 63 | - Used for numeric operations in the library. 64 | 65 | #### 1.3. Integrations 66 | 67 | - **1.3.1. Adapters** 68 | - **1.3.1.1. AlchemistAdapter.php** 69 | - Adapter class for integrating with external services. 70 | - Custom error messages. 71 | - Custom which components to use. 72 | - Custom request parameters used. 73 | - **1.3.2. AlchemistQueryable.php** 74 | - Abstract class for defining rules for querying data. 75 | - **1.3.3. StatefulAlchemistQueryable** 76 | - same as `AlchemistQueryable` but non-static. 77 | 78 | #### 1.4. Notification 79 | 80 | - **1.4.1. ErrorBag.php** 81 | - Used for error handling in the library. 82 | - **1.4.2. CompoundErrors.php** 83 | - Used for containing multiple errors of all components. 84 | 85 | ### 2. ModuleA 86 | 87 | - **2.1. Handlers** 88 | - **2.1.1. ModuleA.php** 89 | - Main classes for handling ModuleA. 90 | - **2.2. Notifications** 91 | - **2.2.1. ModuleA[ErrorBag].php** 92 | - ErrorBag class for ModuleA. 93 | - Contains error messages for ModuleA. 94 | - **2.3. Objects** 95 | - This folder contains objects for ModuleA. 96 | - **2.4. ModuleA[able].php** 97 | - An entry point for ModuleA. 98 | - Contains methods for init ModuleA. 99 | - Contains access method for ModuleA Handlers. 100 | 101 | ### 3. AlchemistRestfulApi.php 102 | - Main class for the library. -------------------------------------------------------------------------------- /src/Common/Exceptions/AlchemistRestfulApiException.php: -------------------------------------------------------------------------------- 1 | [ 19 | * 'request_params' => [ 20 | * 'fields_param' => 'fields', 21 | * ] 22 | * ], 23 | * ``` 24 | */ 25 | public const FIELD_SELECTOR_CONFIGURATION_INCORRECT = 356; 26 | 27 | /** 28 | * This exception is thrown when the configuration of the ResourceFilter component is incorrect or missing. 29 | * 30 | * You can fix this by adding the correct configuration to the componentConfigs method in your Adapter class. 31 | * (inheritance of Nvmcommunity\Alchemist\RestfulApi\Common\Integrations\Adapters\AlchemistAdapter) 32 | * 33 | * For example: 34 | * 35 | * ``` 36 | * ResourceFilter::class => [ 37 | * 'request_params' => [ 38 | * 'filter_param' => 'filter', 39 | * ] 40 | * ], 41 | * ``` 42 | */ 43 | public const RESOURCE_FILTER_CONFIGURATION_INCORRECT = 934; 44 | 45 | /** 46 | * This exception is thrown when the configuration of the ResourceOffsetPaginator component is incorrect or missing. 47 | * (inheritance of Nvmcommunity\Alchemist\RestfulApi\Common\Integrations\Adapters\AlchemistAdapter) 48 | * 49 | * You can fix this by adding the correct configuration to the componentConfigs method in your Adapter class. 50 | * For example: 51 | * 52 | * ``` 53 | * ResourceOffsetPaginator::class => [ 54 | * 'request_params' => [ 55 | * 'offset_param' => 'offset', 56 | * 'limit_param' => 'limit', 57 | * ] 58 | * ], 59 | * ``` 60 | */ 61 | public const RESOURCE_OFFSET_PAGINATOR_CONFIGURATION_INCORRECT = 768; 62 | 63 | /** 64 | * This exception is thrown when the configuration of the ResourceSort component is incorrect or missing. 65 | * 66 | * You can fix this by adding the correct configuration to the componentConfigs method in your Adapter class. 67 | * (inheritance of Nvmcommunity\Alchemist\RestfulApi\Common\Integrations\Adapters\AlchemistAdapter) 68 | * 69 | * For example: 70 | * 71 | * ``` 72 | * ResourceSort::class => [ 73 | * 'request_params' => [ 74 | * 'sort_param' => 'sort', 75 | * ] 76 | * ], 77 | * ``` 78 | */ 79 | public const RESOURCE_SORT_CONFIGURATION_INCORRECT = 824; 80 | 81 | /** 82 | * This exception is thrown when the configuration of the ResourceSearch component is incorrect or missing. 83 | * 84 | * You can fix this by adding the correct configuration to the componentConfigs method in your Adapter class. 85 | * (inheritance of Nvmcommunity\Alchemist\RestfulApi\Common\Integrations\Adapters\AlchemistAdapter) 86 | * 87 | * For example: 88 | * 89 | * ``` 90 | * ResourceSearch::class => [ 91 | * 'request_params' => [ 92 | * 'search_param' => 'search', 93 | * ] 94 | * ], 95 | * ``` 96 | */ 97 | public const RESOURCE_SEARCH_CONFIGURATION_INCORRECT = 198; 98 | } -------------------------------------------------------------------------------- /src/ResourceSort/Notifications/ResourceSortErrorBag.php: -------------------------------------------------------------------------------- 1 | passes = $passes; 41 | $this->invalidDirection = $invalidDirection; 42 | $this->invalidSortField = $invalidSortField; 43 | $this->invalidInputTypes = $invalidInputTypes; 44 | } 45 | 46 | /** 47 | * @return bool[] 48 | */ 49 | public function toArray(): array 50 | { 51 | return [ 52 | 'passes' => $this->passes, 53 | 'invalid_direction' => $this->invalidDirection, 54 | 'invalid_sort_field' => $this->invalidSortField, 55 | 'invalid_input_parameters' => $this->invalidInputTypes, 56 | ]; 57 | } 58 | 59 | /** 60 | * @return bool 61 | */ 62 | public function passes(): bool 63 | { 64 | return $this->passes; 65 | } 66 | 67 | /** 68 | * @return string 69 | */ 70 | public function errorKey(): string 71 | { 72 | return 'sort'; 73 | } 74 | 75 | /** 76 | * @return array 77 | */ 78 | public function getMessages(): array 79 | { 80 | if ($this->passes()) { 81 | return []; 82 | } 83 | 84 | $messages = []; 85 | 86 | if ($this->hasInvalidInputTypes()) { 87 | $messages[] = [ 88 | 'error_code' => static::INVALID_INPUT_TYPE, 89 | 'error_message' => "The input type is invalid.", 90 | 'invalid_input_parameters' => $this->invalidInputTypes, 91 | ]; 92 | } 93 | 94 | if ($this->isInvalidDirection()) { 95 | $messages[] = [ 96 | 'error_code' => static::INVALID_DIRECTION, 97 | 'error_message' => "You are trying to sort by an invalid direction.", 98 | ]; 99 | } 100 | 101 | if ($this->isInvalidSortField()) { 102 | $messages[] = [ 103 | 'error_code' => static::INVALID_SORT_FIELD, 104 | 'error_message' => "You are trying to sort by a field that is not supported.", 105 | ]; 106 | } 107 | 108 | return $messages; 109 | } 110 | 111 | /** 112 | * @return bool 113 | */ 114 | public function isInvalidDirection(): bool 115 | { 116 | return $this->invalidDirection; 117 | } 118 | 119 | /** 120 | * @return bool 121 | */ 122 | public function isInvalidSortField(): bool 123 | { 124 | return $this->invalidSortField; 125 | } 126 | 127 | /** 128 | * @return bool 129 | */ 130 | public function hasInvalidInputTypes(): bool 131 | { 132 | return ! empty($this->invalidInputTypes); 133 | } 134 | } -------------------------------------------------------------------------------- /src/FieldSelector/Notifications/FieldSelectorErrorBag.php: -------------------------------------------------------------------------------- 1 | passes = $passes; 53 | $this->namespace = $namespace; 54 | $this->unselectableFields = $unselectableFields; 55 | $this->invalidInputType = $invalidInputType; 56 | } 57 | 58 | /** 59 | * @return array> 60 | */ 61 | public function toArray(): array 62 | { 63 | return [ 64 | 'passes' => $this->passes, 65 | 'namespace' => $this->namespace, 66 | 'unselectable_fields' => $this->unselectableFields, 67 | 'invalid_input_type' => $this->invalidInputType, 68 | ]; 69 | } 70 | 71 | /** 72 | * @return bool 73 | */ 74 | public function passes(): bool 75 | { 76 | return $this->passes; 77 | } 78 | 79 | /** 80 | * @return string 81 | */ 82 | public function errorKey(): string 83 | { 84 | return 'fields'; 85 | } 86 | 87 | /** 88 | * @return array 89 | */ 90 | public function getMessages(): array 91 | { 92 | if ($this->passes()) { 93 | return []; 94 | } 95 | 96 | $messages = []; 97 | 98 | if ($this->isInvalidInputType()) { 99 | $messages[] = [ 100 | 'error_code' => static::INVALID_INPUT_TYPE, 101 | 'error_message' => "The input type is invalid. It must be a string.", 102 | ]; 103 | } 104 | 105 | if ($this->hasUnselectableFields()) { 106 | $messages[] = [ 107 | 'error_code' => static::UNSELECTABLE_FIELD, 108 | 'error_message' => "You are trying to select fields that are not selectable.", 109 | 'error_namespace' => $this->getNamespace(), 110 | 'error_fields' => $this->getUnselectableFields(), 111 | ]; 112 | } 113 | 114 | return $messages; 115 | } 116 | 117 | /** 118 | * @return string 119 | */ 120 | public function getNamespace(): string 121 | { 122 | return $this->namespace; 123 | } 124 | 125 | /** 126 | * @return string[] 127 | */ 128 | public function getUnselectableFields(): array 129 | { 130 | return $this->unselectableFields; 131 | } 132 | 133 | /** 134 | * @return bool 135 | */ 136 | public function hasUnselectableFields(): bool 137 | { 138 | return ! empty($this->unselectableFields); 139 | } 140 | 141 | /** 142 | * @return bool 143 | */ 144 | public function isInvalidInputType(): bool 145 | { 146 | return $this->invalidInputType; 147 | } 148 | } -------------------------------------------------------------------------------- /src/ResourcePaginations/OffsetPaginator/Handlers/ResourceOffsetPaginator.php: -------------------------------------------------------------------------------- 1 | limitParam = $limitParam; 48 | $this->offsetParam = $offsetParam; 49 | $this->originalInput = $input; 50 | 51 | $this->limit = ! is_bool($input[$limitParam]) && (filter_var($input[$limitParam], FILTER_VALIDATE_INT) !== false) ? $input[$limitParam] : 0; 52 | $this->offset = ! is_bool($input[$offsetParam]) && (filter_var($input[$offsetParam], FILTER_VALIDATE_INT) !== false) ? $input[$offsetParam] : 0; 53 | } 54 | 55 | /** 56 | * @param int $defaultLimit 57 | * @return ResourceOffsetPaginator 58 | */ 59 | public function defineDefaultLimit(int $defaultLimit): self 60 | { 61 | $this->defaultLimit = $defaultLimit; 62 | 63 | return $this; 64 | } 65 | 66 | /** 67 | * @param int $maxLimit 68 | * @return ResourceOffsetPaginator 69 | */ 70 | public function defineMaxLimit(int $maxLimit): self 71 | { 72 | $this->maxLimit = $maxLimit; 73 | 74 | return $this; 75 | } 76 | 77 | /** 78 | * @return OffsetPaginateObject 79 | */ 80 | public function offsetPaginate(): OffsetPaginateObject 81 | { 82 | return new OffsetPaginateObject($this->limit ?: $this->defaultLimit ?: $this->maxLimit, $this->offset, $this->maxLimit); 83 | } 84 | 85 | /** 86 | * @param $notification 87 | * @return ResourceOffsetPaginationErrorBag 88 | */ 89 | public function validate(&$notification = null): ResourceOffsetPaginationErrorBag 90 | { 91 | $passes = true; 92 | $isNegativeOffset = false; 93 | $isNegativeLimit = false; 94 | $maxLimitReached = false; 95 | $invalidInputTypes = []; 96 | 97 | if (! is_null($this->originalInput[$this->limitParam]) 98 | && (is_bool($this->originalInput[$this->limitParam]) || (filter_var($this->originalInput[$this->limitParam], FILTER_VALIDATE_INT) === false)) 99 | ) { 100 | $invalidInputTypes[] = $this->limitParam; 101 | } 102 | 103 | if (! is_null($this->originalInput[$this->offsetParam]) 104 | && (is_bool($this->originalInput[$this->offsetParam]) || (filter_var($this->originalInput[$this->offsetParam], FILTER_VALIDATE_INT) === false)) 105 | ) { 106 | $invalidInputTypes[] = $this->offsetParam; 107 | } 108 | 109 | if (! empty($invalidInputTypes)) { 110 | $passes = false; 111 | } 112 | 113 | if ($this->maxLimit > 0 && $this->maxLimit < $this->limit) { 114 | $passes = false; 115 | $maxLimitReached = true; 116 | } 117 | 118 | if ($this->offset < 0) { 119 | $passes = false; 120 | $isNegativeOffset = true; 121 | } 122 | 123 | if ($this->limit < 0) { 124 | $passes = false; 125 | $isNegativeLimit = true; 126 | } 127 | 128 | return $notification = new ResourceOffsetPaginationErrorBag($passes, $maxLimitReached, $isNegativeOffset, $isNegativeLimit, $invalidInputTypes); 129 | } 130 | } -------------------------------------------------------------------------------- /src/ResourceSort/ResourceSortable.php: -------------------------------------------------------------------------------- 1 | adapter->componentConfigs(); 22 | 23 | if (! isset($componentConfig[ResourceSort::class])) { 24 | throw new AlchemistRestfulApiException("The `ResourceSort` component is not configured! 25 | Here is an example of the correct configuration of componentConfigs in your Adapter (inheritance of AlchemistAdapter): 26 | 27 | ``` 28 | ResourceSort::class => [ 29 | 'request_params' => [ 30 | 'sort_param' => 'sort', 31 | 'direction_param' => 'direction', 32 | ] 33 | ], 34 | ``` 35 | ", AlchemistRestfulApiException::RESOURCE_SORT_CONFIGURATION_INCORRECT); 36 | } 37 | 38 | if (! isset($componentConfig[ResourceSort::class]['request_params'])) { 39 | throw new AlchemistRestfulApiException("Missing `request_params` configuration for `ResourceSort` component! 40 | Here is an example of the correct configuration of componentConfigs in your Adapter (inheritance of AlchemistAdapter): 41 | 42 | ``` 43 | ResourceSort::class => [ 44 | 'request_params' => [ 45 | 'sort_param' => 'sort', 46 | 'direction_param' => 'direction', 47 | ] 48 | ], 49 | ``` 50 | ", AlchemistRestfulApiException::RESOURCE_SORT_CONFIGURATION_INCORRECT); 51 | } 52 | 53 | if (! isset($componentConfig[ResourceSort::class]['request_params']['sort_param'])) { 54 | throw new AlchemistRestfulApiException("Missing `sort_param` configuration for request_params in `ResourceSort` component configuration! 55 | Here is an example of the correct configuration of componentConfigs in your Adapter (inheritance of AlchemistAdapter): 56 | 57 | ``` 58 | ResourceSort::class => [ 59 | 'request_params' => [ 60 | 'sort_param' => 'sort', 61 | 'direction_param' => 'direction', 62 | ] 63 | ], 64 | ``` 65 | ", AlchemistRestfulApiException::RESOURCE_SORT_CONFIGURATION_INCORRECT); 66 | } 67 | 68 | if (! isset($componentConfig[ResourceSort::class]['request_params']['sort_param'])) { 69 | throw new AlchemistRestfulApiException("Missing `direction_param` configuration for request_params in `ResourceSort` component configuration! 70 | Here is an example of the correct configuration of componentConfigs in your Adapter (inheritance of AlchemistAdapter): 71 | 72 | ``` 73 | ResourceSort::class => [ 74 | 'request_params' => [ 75 | 'sort_param' => 'sort', 76 | 'direction_param' => 'direction', 77 | ] 78 | ], 79 | ``` 80 | ", AlchemistRestfulApiException::RESOURCE_SORT_CONFIGURATION_INCORRECT); 81 | } 82 | 83 | $sortParam = $componentConfig[ResourceSort::class]['request_params']['sort_param']; 84 | $directionParam = $componentConfig[ResourceSort::class]['request_params']['direction_param']; 85 | 86 | $this->resourceSort = new ResourceSort($sortParam, $directionParam, [ 87 | $sortParam => $requestInput[$sortParam] ?? null, 88 | $directionParam => $requestInput[$directionParam] ?? null, 89 | ]); 90 | } 91 | 92 | /** 93 | * @return ResourceSort 94 | */ 95 | public function resourceSort(): ResourceSort 96 | { 97 | return $this->resourceSort; 98 | } 99 | } -------------------------------------------------------------------------------- /tests/Feature/AlchemistAdapter/AlchemistAdapter_FieldSelectorConfiguration_Test.php: -------------------------------------------------------------------------------- 1 | createMock(AlchemistAdapter::class); 19 | $adapter->method('componentUses') 20 | ->willReturn([ 21 | FieldSelector::class, 22 | ]); 23 | 24 | $adapter->method('componentConfigs') 25 | ->willReturn([ 26 | FieldSelector::class => [ 27 | 'request_params' => [ 28 | 'fields_param' => 'fields', 29 | ] 30 | ], 31 | ]); 32 | 33 | $restfulApi = new AlchemistRestfulApi([ 34 | 'fields' => 'id,order_date' 35 | ], $adapter); 36 | 37 | $restfulApi->fieldSelector() 38 | ->defineFieldStructure([ 39 | 'id', 40 | 'order_date', 41 | ]); 42 | 43 | $this->assertSame(['id', 'order_date'], $restfulApi->fieldSelector()->flatFields()); 44 | } 45 | 46 | /** 47 | * @throws AlchemistRestfulApiException 48 | */ 49 | public function test_adapter_with_field_selector_enable_but_missing_configuration_must_throw_exception(): void 50 | { 51 | $adapter = $this->createMock(AlchemistAdapter::class); 52 | $adapter->method('componentUses') 53 | ->willReturn([ 54 | FieldSelector::class, 55 | ]); 56 | 57 | $adapter->method('componentConfigs') 58 | ->willReturn([ 59 | // >Missing below config: 60 | // FieldSelector::class => [ 61 | // ... 62 | // ], 63 | ]); 64 | 65 | $this->expectExceptionCode(AlchemistRestfulApiException::FIELD_SELECTOR_CONFIGURATION_INCORRECT); 66 | 67 | new AlchemistRestfulApi([], $adapter); 68 | } 69 | 70 | /** 71 | * @throws AlchemistRestfulApiException 72 | */ 73 | public function test_adapter_with_field_selector_enable_but_missing_request_params_of_configuration_must_throw_exception(): void 74 | { 75 | $adapter = $this->createMock(AlchemistAdapter::class); 76 | $adapter->method('componentUses') 77 | ->willReturn([ 78 | FieldSelector::class, 79 | ]); 80 | 81 | $adapter->method('componentConfigs') 82 | ->willReturn([ 83 | FieldSelector::class => [ 84 | // >Missing below config: 85 | // 'request_params' => [ 86 | // 'fields_param' => 'fields', 87 | // ], 88 | // ] 89 | // ], 90 | ], 91 | ]); 92 | 93 | $this->expectExceptionCode(AlchemistRestfulApiException::FIELD_SELECTOR_CONFIGURATION_INCORRECT); 94 | 95 | new AlchemistRestfulApi([], $adapter); 96 | } 97 | 98 | /** 99 | * @throws AlchemistRestfulApiException 100 | */ 101 | public function test_adapter_with_field_selector_enable_but_missing_fields_param_inside_request_params_of_configuration_must_throw_exception(): void 102 | { 103 | $adapter = $this->createMock(AlchemistAdapter::class); 104 | $adapter->method('componentUses') 105 | ->willReturn([ 106 | FieldSelector::class, 107 | ]); 108 | 109 | $adapter->method('componentConfigs') 110 | ->willReturn([ 111 | FieldSelector::class => [ 112 | 'request_params' => [ 113 | // >Missing below config: 114 | // 'fields_param' => 'fields', 115 | ], 116 | ], 117 | ]); 118 | 119 | $this->expectExceptionCode(AlchemistRestfulApiException::FIELD_SELECTOR_CONFIGURATION_INCORRECT); 120 | 121 | new AlchemistRestfulApi([], $adapter); 122 | } 123 | } 124 | -------------------------------------------------------------------------------- /tests/Feature/AlchemistAdapter/AlchemistAdapter_ResourceSearchConfiguration_Test.php: -------------------------------------------------------------------------------- 1 | createMock(AlchemistAdapter::class); 19 | $adapter->method('componentUses') 20 | ->willReturn([ 21 | ResourceSearch::class, 22 | ]); 23 | 24 | $adapter->method('componentConfigs') 25 | ->willReturn([ 26 | ResourceSearch::class => [ 27 | 'request_params' => [ 28 | 'search_param' => 'search', 29 | ] 30 | ], 31 | ]); 32 | 33 | $restfulApi = new AlchemistRestfulApi([ 34 | 'search' => 'abc' 35 | ], $adapter); 36 | 37 | $restfulApi->resourceSearch()->defineSearchCondition('title'); 38 | 39 | $this->assertSame('title', $restfulApi->resourceSearch()->search()->getSearchCondition()); 40 | $this->assertSame('abc', $restfulApi->resourceSearch()->search()->getSearchValue()); 41 | } 42 | 43 | /** 44 | * @throws AlchemistRestfulApiException 45 | */ 46 | public function test_adapter_with_resource_search_enable_but_missing_configuration_must_throw_exception(): void 47 | { 48 | $adapter = $this->createMock(AlchemistAdapter::class); 49 | $adapter->method('componentUses') 50 | ->willReturn([ 51 | ResourceSearch::class, 52 | ]); 53 | 54 | $adapter->method('componentConfigs') 55 | ->willReturn([ 56 | // >Missing below config: 57 | // ResourceSearch::class => [ 58 | // ... 59 | // ], 60 | ]); 61 | 62 | $this->expectExceptionCode(AlchemistRestfulApiException::RESOURCE_SEARCH_CONFIGURATION_INCORRECT); 63 | 64 | new AlchemistRestfulApi([], $adapter); 65 | } 66 | 67 | /** 68 | * @throws AlchemistRestfulApiException 69 | */ 70 | public function test_adapter_with_resource_search_enable_but_missing_request_params_of_configuration_must_throw_exception(): void 71 | { 72 | $adapter = $this->createMock(AlchemistAdapter::class); 73 | $adapter->method('componentUses') 74 | ->willReturn([ 75 | ResourceSearch::class, 76 | ]); 77 | 78 | $adapter->method('componentConfigs') 79 | ->willReturn([ 80 | ResourceSearch::class => [ 81 | // >Missing below config: 82 | // 'request_params' => [ 83 | // 'filtering_param' => 'filtering', 84 | // ], 85 | // ] 86 | // ], 87 | ], 88 | ]); 89 | 90 | $this->expectExceptionCode(AlchemistRestfulApiException::RESOURCE_SEARCH_CONFIGURATION_INCORRECT); 91 | 92 | new AlchemistRestfulApi([], $adapter); 93 | } 94 | 95 | /** 96 | * @throws AlchemistRestfulApiException 97 | */ 98 | public function test_adapter_with_resource_search_enable_but_missing_filtering_param_inside_request_params_of_configuration_must_throw_exception(): void 99 | { 100 | $adapter = $this->createMock(AlchemistAdapter::class); 101 | $adapter->method('componentUses') 102 | ->willReturn([ 103 | ResourceSearch::class, 104 | ]); 105 | 106 | $adapter->method('componentConfigs') 107 | ->willReturn([ 108 | ResourceSearch::class => [ 109 | 'request_params' => [ 110 | // >Missing below config: 111 | // 'search_param' => 'search', 112 | ], 113 | ], 114 | ]); 115 | 116 | $this->expectExceptionCode(AlchemistRestfulApiException::RESOURCE_SEARCH_CONFIGURATION_INCORRECT); 117 | 118 | new AlchemistRestfulApi([], $adapter); 119 | } 120 | } 121 | -------------------------------------------------------------------------------- /src/ResourceSort/Handlers/ResourceSort.php: -------------------------------------------------------------------------------- 1 | sortFieldParam = $sortParam; 55 | $this->directionParam = $directionParam; 56 | $this->originalInput = $input; 57 | 58 | $this->sortField = is_string($input[$sortParam]) ? $input[$sortParam] : ''; 59 | $this->direction = is_string($input[$directionParam]) ? $input[$directionParam] : ''; 60 | } 61 | 62 | /** 63 | * @param string[] $sortableFields 64 | * @return ResourceSort 65 | */ 66 | public function defineSortableFields(array $sortableFields): self 67 | { 68 | $this->sortableFields = $sortableFields; 69 | 70 | return $this; 71 | } 72 | 73 | /** 74 | * @param string $sortField 75 | * @return ResourceSort 76 | */ 77 | public function defineDefaultSort(string $sortField): self 78 | { 79 | $this->defaultSortField = $sortField; 80 | 81 | return $this; 82 | } 83 | 84 | /** 85 | * @param string $direction 86 | * @return ResourceSort 87 | * @throws AlchemistRestfulApiException 88 | */ 89 | public function defineDefaultDirection(string $direction): self 90 | { 91 | if (! in_array($direction, ['asc', 'desc'], true)) { 92 | throw new AlchemistRestfulApiException('Invalid default sort direction, it must be either `asc` or `desc`.'); 93 | } 94 | 95 | $this->defaultSortDirection = $direction; 96 | 97 | return $this; 98 | } 99 | 100 | /** 101 | * @param $notification 102 | * @return ResourceSortErrorBag 103 | */ 104 | public function validate(&$notification = null): ResourceSortErrorBag 105 | { 106 | $passes = true; 107 | 108 | $invalidDirection = false; 109 | $invalidSortField = false; 110 | $invalidInputTypes = []; 111 | 112 | if (! is_null($this->originalInput[$this->sortFieldParam]) && ! is_string($this->originalInput[$this->sortFieldParam])) { 113 | $invalidInputTypes[] = $this->sortFieldParam; 114 | } 115 | 116 | if (! is_null($this->originalInput[$this->directionParam]) && ! is_string($this->originalInput[$this->directionParam])) { 117 | $invalidInputTypes[] = $this->directionParam; 118 | } 119 | 120 | if (! empty($invalidInputTypes)) { 121 | $passes = false; 122 | } 123 | 124 | if (! empty($this->direction) && ! in_array($this->direction, ['asc', 'desc'], true)) { 125 | $passes = false; 126 | $invalidDirection = true; 127 | } 128 | 129 | if (! empty($this->sortField) && (empty($this->sortableFields) || ! in_array($this->sortField, $this->sortableFields, true))) { 130 | $passes = false; 131 | $invalidSortField = true; 132 | } 133 | 134 | return $notification = new ResourceSortErrorBag($passes, $invalidDirection, $invalidSortField, $invalidInputTypes); 135 | } 136 | 137 | /** 138 | * @return ResourceSortObject 139 | */ 140 | public function sort(): ResourceSortObject 141 | { 142 | return new ResourceSortObject( 143 | $this->sortField ?: $this->defaultSortField, 144 | $this->direction ?: $this->defaultSortDirection 145 | ); 146 | } 147 | } -------------------------------------------------------------------------------- /tests/Feature/AlchemistAdapter/AlchemistAdapter_ResourceSortConfiguration_Test.php: -------------------------------------------------------------------------------- 1 | createMock(AlchemistAdapter::class); 19 | $adapter->method('componentUses') 20 | ->willReturn([ 21 | ResourceSort::class, 22 | ]); 23 | 24 | $adapter->method('componentConfigs') 25 | ->willReturn([ 26 | ResourceSort::class => [ 27 | 'request_params' => [ 28 | 'sort_param' => 'sort', 29 | 'direction_param' => 'direction', 30 | ] 31 | ], 32 | ]); 33 | 34 | $restfulApi = new AlchemistRestfulApi([ 35 | 'sort' => 'title', 36 | 'direction' => 'asc', 37 | ], $adapter); 38 | 39 | $restfulApi->resourceSort()->defineSortableFields(['title']); 40 | 41 | $this->assertSame('title', $restfulApi->resourceSort()->sort()->getSortField()); 42 | $this->assertSame('asc', $restfulApi->resourceSort()->sort()->getDirection()); 43 | } 44 | 45 | /** 46 | * @throws AlchemistRestfulApiException 47 | */ 48 | public function test_adapter_with_resource_sort_enable_but_missing_configuration_must_throw_exception(): void 49 | { 50 | $adapter = $this->createMock(AlchemistAdapter::class); 51 | $adapter->method('componentUses') 52 | ->willReturn([ 53 | ResourceSort::class, 54 | ]); 55 | 56 | $adapter->method('componentConfigs') 57 | ->willReturn([ 58 | // >Missing below config: 59 | // ResourceSort::class => [ 60 | // ... 61 | // ], 62 | ]); 63 | 64 | $this->expectExceptionCode(AlchemistRestfulApiException::RESOURCE_SORT_CONFIGURATION_INCORRECT); 65 | 66 | new AlchemistRestfulApi([], $adapter); 67 | } 68 | 69 | /** 70 | * @throws AlchemistRestfulApiException 71 | */ 72 | public function test_adapter_with_resource_sort_enable_but_missing_request_params_of_configuration_must_throw_exception(): void 73 | { 74 | $adapter = $this->createMock(AlchemistAdapter::class); 75 | $adapter->method('componentUses') 76 | ->willReturn([ 77 | ResourceSort::class, 78 | ]); 79 | 80 | $adapter->method('componentConfigs') 81 | ->willReturn([ 82 | ResourceSort::class => [ 83 | // >Missing below config: 84 | // 'request_params' => [ 85 | // 'filtering_param' => 'filtering', 86 | // ], 87 | // ] 88 | // ], 89 | ], 90 | ]); 91 | 92 | $this->expectExceptionCode(AlchemistRestfulApiException::RESOURCE_SORT_CONFIGURATION_INCORRECT); 93 | 94 | new AlchemistRestfulApi([], $adapter); 95 | } 96 | 97 | /** 98 | * @throws AlchemistRestfulApiException 99 | */ 100 | public function test_adapter_with_resource_sort_enable_but_missing_sort_and_direction_param_inside_request_params_of_configuration_must_throw_exception(): void 101 | { 102 | $adapter = $this->createMock(AlchemistAdapter::class); 103 | $adapter->method('componentUses') 104 | ->willReturn([ 105 | ResourceSort::class, 106 | ]); 107 | 108 | $adapter->method('componentConfigs') 109 | ->willReturn([ 110 | ResourceSort::class => [ 111 | 'request_params' => [ 112 | // >Missing below config: 113 | // 'sort_param' => 'sort', 114 | // 'direction_param' => 'direction', 115 | ], 116 | ], 117 | ]); 118 | 119 | $this->expectExceptionCode(AlchemistRestfulApiException::RESOURCE_SORT_CONFIGURATION_INCORRECT); 120 | 121 | new AlchemistRestfulApi([], $adapter); 122 | } 123 | } 124 | -------------------------------------------------------------------------------- /tests/Feature/AlchemistAdapter/AlchemistAdapter_ResourceOffsetPaginator_Test.php: -------------------------------------------------------------------------------- 1 | createMock(AlchemistAdapter::class); 19 | $adapter->method('componentUses') 20 | ->willReturn([ 21 | ResourceOffsetPaginator::class, 22 | ]); 23 | 24 | $adapter->method('componentConfigs') 25 | ->willReturn([ 26 | ResourceOffsetPaginator::class => [ 27 | 'request_params' => [ 28 | 'limit_param' => 'limit', 29 | 'offset_param' => 'offset', 30 | ] 31 | ], 32 | ]); 33 | 34 | $restfulApi = new AlchemistRestfulApi([ 35 | 'limit' => 10, 36 | 'offset' => 0, 37 | ], $adapter); 38 | 39 | $this->assertSame(10, $restfulApi->resourceOffsetPaginator()->offsetPaginate()->getLimit()); 40 | $this->assertSame(0, $restfulApi->resourceOffsetPaginator()->offsetPaginate()->getOffset()); 41 | } 42 | 43 | /** 44 | * @throws AlchemistRestfulApiException 45 | */ 46 | public function test_Adapter_with_resource_offset_paginator_enable_but_missing_configuration_must_throw_exception(): void 47 | { 48 | $adapter = $this->createMock(AlchemistAdapter::class); 49 | $adapter->method('componentUses') 50 | ->willReturn([ 51 | ResourceOffsetPaginator::class, 52 | ]); 53 | 54 | $adapter->method('componentConfigs') 55 | ->willReturn([ 56 | // >Missing below config: 57 | // ResourceOffsetPaginator::class => [ 58 | // ... 59 | // ], 60 | ]); 61 | 62 | $this->expectExceptionCode(AlchemistRestfulApiException::RESOURCE_OFFSET_PAGINATOR_CONFIGURATION_INCORRECT); 63 | 64 | new AlchemistRestfulApi([], $adapter); 65 | } 66 | 67 | /** 68 | * @throws AlchemistRestfulApiException 69 | */ 70 | public function test_adapter_with_resource_offset_paginator_enable_but_missing_request_params_of_configuration_must_throw_exception(): void 71 | { 72 | $adapter = $this->createMock(AlchemistAdapter::class); 73 | $adapter->method('componentUses') 74 | ->willReturn([ 75 | ResourceOffsetPaginator::class, 76 | ]); 77 | 78 | $adapter->method('componentConfigs') 79 | ->willReturn([ 80 | ResourceOffsetPaginator::class => [ 81 | // >Missing below config: 82 | // 'request_params' => [ 83 | // 'fields_param' => 'fields', 84 | // ], 85 | // ] 86 | // ], 87 | ], 88 | ]); 89 | 90 | $this->expectExceptionCode(AlchemistRestfulApiException::RESOURCE_OFFSET_PAGINATOR_CONFIGURATION_INCORRECT); 91 | 92 | new AlchemistRestfulApi([], $adapter); 93 | } 94 | 95 | /** 96 | * @throws AlchemistRestfulApiException 97 | */ 98 | public function test_adapter_with_resource_offset_paginator_enable_but_missing_offset_and_limit_param_inside_request_params_of_configuration_must_throw_exception(): void 99 | { 100 | $adapter = $this->createMock(AlchemistAdapter::class); 101 | $adapter->method('componentUses') 102 | ->willReturn([ 103 | ResourceOffsetPaginator::class, 104 | ]); 105 | 106 | $adapter->method('componentConfigs') 107 | ->willReturn([ 108 | ResourceOffsetPaginator::class => [ 109 | 'request_params' => [ 110 | // >Missing below config: 111 | // 'limit_param' => 'fields', 112 | // 'offset_param' => 'fields', 113 | ], 114 | ], 115 | ]); 116 | 117 | $this->expectExceptionCode(AlchemistRestfulApiException::RESOURCE_OFFSET_PAGINATOR_CONFIGURATION_INCORRECT); 118 | 119 | new AlchemistRestfulApi([], $adapter); 120 | } 121 | } 122 | -------------------------------------------------------------------------------- /src/ResourcePaginations/OffsetPaginator/ResourceOffsetPaginate.php: -------------------------------------------------------------------------------- 1 | adapter->componentConfigs(); 23 | 24 | if (! isset($componentConfig[ResourceOffsetPaginator::class])) { 25 | throw new AlchemistRestfulApiException("The `ResourceOffsetPaginator` component is not configured! 26 | Here is an example of the correct configuration of componentConfigs in your Adapter (inheritance of AlchemistAdapter): 27 | 28 | ``` 29 | ResourceOffsetPaginator::class => [ 30 | 'request_params' => [ 31 | 'limit_param' => 'limit', 32 | 'offset_param' => 'offset', 33 | ] 34 | ], 35 | ``` 36 | ", AlchemistRestfulApiException::RESOURCE_OFFSET_PAGINATOR_CONFIGURATION_INCORRECT); 37 | } 38 | 39 | if (! isset($componentConfig[ResourceOffsetPaginator::class]['request_params'])) { 40 | throw new AlchemistRestfulApiException("Missing `request_params` configuration for `ResourceOffsetPaginator` component! 41 | Here is an example of the correct configuration of componentConfigs in your Adapter (inheritance of AlchemistAdapter): 42 | 43 | ``` 44 | ResourceOffsetPaginator::class => [ 45 | 'request_params' => [ 46 | 'limit_param' => 'limit', 47 | 'offset_param' => 'offset', 48 | ] 49 | ], 50 | ``` 51 | ", AlchemistRestfulApiException::RESOURCE_OFFSET_PAGINATOR_CONFIGURATION_INCORRECT); 52 | } 53 | 54 | if (! isset($componentConfig[ResourceOffsetPaginator::class]['request_params']['limit_param'])) { 55 | throw new AlchemistRestfulApiException("Missing `offset_param` configuration for request_params in `ResourceOffsetPaginator` component configuration! 56 | Here is an example of the correct configuration of componentConfigs in your Adapter (inheritance of AlchemistAdapter): 57 | 58 | ``` 59 | ResourceOffsetPaginator::class => [ 60 | 'request_params' => [ 61 | 'limit_param' => 'limit', 62 | 'offset_param' => 'offset', 63 | ] 64 | ], 65 | ``` 66 | ", AlchemistRestfulApiException::RESOURCE_OFFSET_PAGINATOR_CONFIGURATION_INCORRECT); 67 | } 68 | 69 | if (! isset($componentConfig[ResourceOffsetPaginator::class]['request_params']['offset_param'])) { 70 | throw new AlchemistRestfulApiException("Missing `limit_param` configuration for request_params in `ResourceOffsetPaginator` component configuration! 71 | Here is an example of the correct configuration of componentConfigs in your Adapter (inheritance of AlchemistAdapter): 72 | 73 | ``` 74 | ResourceOffsetPaginator::class => [ 75 | 'request_params' => [ 76 | 'limit_param' => 'limit', 77 | 'offset_param' => 'offset', 78 | ] 79 | ], 80 | ``` 81 | ", AlchemistRestfulApiException::RESOURCE_OFFSET_PAGINATOR_CONFIGURATION_INCORRECT); 82 | } 83 | 84 | $limitParam = $componentConfig[ResourceOffsetPaginator::class]['request_params']['limit_param']; 85 | $offsetParam = $componentConfig[ResourceOffsetPaginator::class]['request_params']['offset_param']; 86 | 87 | $this->resourceOffsetPaginator = new ResourceOffsetPaginator($limitParam, $offsetParam, [ 88 | $limitParam => $requestInput[$limitParam] ?? null, 89 | $offsetParam => $requestInput[$offsetParam] ?? null, 90 | ]); 91 | } 92 | 93 | /** 94 | * @return ResourceOffsetPaginator 95 | */ 96 | public function resourceOffsetPaginator(): ResourceOffsetPaginator 97 | { 98 | return $this->resourceOffsetPaginator; 99 | } 100 | } -------------------------------------------------------------------------------- /src/ResourcePaginations/OffsetPaginator/Notifications/ResourceOffsetPaginationErrorBag.php: -------------------------------------------------------------------------------- 1 | passes = $passes; 60 | $this->maxLimitReached = $maxLimitReached; 61 | $this->negativeOffset = $negativeOffset; 62 | $this->negativeLimit = $negativeLimit; 63 | $this->invalidInputTypes = $invalidInputTypes; 64 | } 65 | 66 | /** 67 | * @return bool[] 68 | */ 69 | public function toArray(): array 70 | { 71 | return [ 72 | 'passes' => $this->passes, 73 | 'max_limit_reached' => $this->maxLimitReached, 74 | 'negative_offset' => $this->negativeOffset, 75 | 'negative_limit' => $this->negativeLimit, 76 | 'invalid_input_parameters' => $this->invalidInputTypes, 77 | ]; 78 | } 79 | 80 | /** 81 | * @return bool 82 | */ 83 | public function passes(): bool 84 | { 85 | return $this->passes; 86 | } 87 | 88 | /** 89 | * @return string 90 | */ 91 | public function errorKey(): string 92 | { 93 | return 'paginator'; 94 | } 95 | 96 | /** 97 | * @return array 98 | */ 99 | public function getMessages(): array 100 | { 101 | if ($this->passes()) { 102 | return []; 103 | } 104 | 105 | $messages = []; 106 | 107 | if ($this->hasInvalidInputTypes()) { 108 | $messages[] = [ 109 | 'error_code' => static::INVALID_INPUT_TYPE, 110 | 'error_message' => "The input type is invalid.", 111 | 'invalid_input_parameters' => $this->invalidInputTypes, 112 | ]; 113 | } 114 | 115 | if ($this->isMaxLimitReached()) { 116 | $messages[] = [ 117 | 'error_code' => static::MAX_LIMIT_REACHED, 118 | 'error_message' => "You have passed in a limit that exceeds the maximum limit.", 119 | ]; 120 | } 121 | 122 | if ($this->isNegativeOffset()) { 123 | $messages[] = [ 124 | 'error_code' => static::NEGATIVE_OFFSET, 125 | 'error_message' => "You have passed in a negative offset. Offset must be a positive integer.", 126 | ]; 127 | } 128 | 129 | if ($this->isNegativeLimit()) { 130 | $messages[] = [ 131 | 'error_code' => static::NEGATIVE_LIMIT, 132 | 'error_message' => "You have passed in a negative limit. Limit must be a positive integer.", 133 | ]; 134 | } 135 | 136 | return $messages; 137 | } 138 | 139 | /** 140 | * @return bool 141 | */ 142 | public function isMaxLimitReached(): bool 143 | { 144 | return $this->maxLimitReached; 145 | } 146 | 147 | /** 148 | * @return bool 149 | */ 150 | public function isNegativeOffset(): bool 151 | { 152 | return $this->negativeOffset; 153 | } 154 | 155 | /** 156 | * @return bool 157 | */ 158 | public function isNegativeLimit(): bool 159 | { 160 | return $this->negativeLimit; 161 | } 162 | 163 | /** 164 | * @return bool 165 | */ 166 | public function hasInvalidInputTypes(): bool 167 | { 168 | return count($this->invalidInputTypes) > 0; 169 | } 170 | } -------------------------------------------------------------------------------- /tests/Feature/AlchemistAdapter/AlchemistAdapter_ResourceFilterConfiguration_Test.php: -------------------------------------------------------------------------------- 1 | createMock(AlchemistAdapter::class); 20 | $adapter->method('componentUses') 21 | ->willReturn([ 22 | ResourceFilter::class, 23 | ]); 24 | 25 | $adapter->method('componentConfigs') 26 | ->willReturn([ 27 | ResourceFilter::class => [ 28 | 'request_params' => [ 29 | 'filtering_param' => 'filtering', 30 | ] 31 | ], 32 | ]); 33 | 34 | $restfulApi = new AlchemistRestfulApi([ 35 | 'filtering' => [ 36 | 'order_date:lte' => '2023-02-26', 37 | 'product_name:contains' => 'clothes hanger' 38 | ] 39 | ], $adapter); 40 | 41 | $restfulApi->resourceFilter() 42 | ->defineFilteringRules([ 43 | FilteringRules::String('product_name', ['eq', 'contains']), 44 | FilteringRules::Date('order_date', ['eq', 'lte', 'gte'], ['Y-m-d']), 45 | 46 | ]); 47 | 48 | $this->assertTrue($restfulApi->resourceFilter()->hasFiltering('product_name')); 49 | $this->assertTrue($restfulApi->resourceFilter()->hasFiltering('order_date')); 50 | 51 | $this->assertNotTrue($restfulApi->resourceFilter()->hasFiltering('abc')); 52 | } 53 | 54 | /** 55 | * @throws AlchemistRestfulApiException 56 | */ 57 | public function test_adapter_with_resource_filter_enable_but_missing_configuration_must_throw_exception(): void 58 | { 59 | $adapter = $this->createMock(AlchemistAdapter::class); 60 | $adapter->method('componentUses') 61 | ->willReturn([ 62 | ResourceFilter::class, 63 | ]); 64 | 65 | $adapter->method('componentConfigs') 66 | ->willReturn([ 67 | // >Missing below config: 68 | // ResourceFilter::class => [ 69 | // ... 70 | // ], 71 | ]); 72 | 73 | $this->expectExceptionCode(AlchemistRestfulApiException::RESOURCE_FILTER_CONFIGURATION_INCORRECT); 74 | 75 | new AlchemistRestfulApi([], $adapter); 76 | } 77 | 78 | /** 79 | * @throws AlchemistRestfulApiException 80 | */ 81 | public function test_adapter_with_resource_filter_enable_but_missing_request_params_of_configuration_must_throw_exception(): void 82 | { 83 | $adapter = $this->createMock(AlchemistAdapter::class); 84 | $adapter->method('componentUses') 85 | ->willReturn([ 86 | ResourceFilter::class, 87 | ]); 88 | 89 | $adapter->method('componentConfigs') 90 | ->willReturn([ 91 | ResourceFilter::class => [ 92 | // >Missing below config: 93 | // 'request_params' => [ 94 | // 'filtering_param' => 'filtering', 95 | // ], 96 | // ] 97 | // ], 98 | ], 99 | ]); 100 | 101 | $this->expectExceptionCode(AlchemistRestfulApiException::RESOURCE_FILTER_CONFIGURATION_INCORRECT); 102 | 103 | new AlchemistRestfulApi([], $adapter); 104 | } 105 | 106 | /** 107 | * @throws AlchemistRestfulApiException 108 | */ 109 | public function test_adapter_with_resource_filter_enable_but_missing_filtering_param_inside_request_params_of_configuration_must_throw_exception(): void 110 | { 111 | $adapter = $this->createMock(AlchemistAdapter::class); 112 | $adapter->method('componentUses') 113 | ->willReturn([ 114 | ResourceFilter::class, 115 | ]); 116 | 117 | $adapter->method('componentConfigs') 118 | ->willReturn([ 119 | ResourceFilter::class => [ 120 | 'request_params' => [ 121 | // >Missing below config: 122 | // 'filtering_param' => 'filtering', 123 | ], 124 | ], 125 | ]); 126 | 127 | $this->expectExceptionCode(AlchemistRestfulApiException::RESOURCE_FILTER_CONFIGURATION_INCORRECT); 128 | 129 | new AlchemistRestfulApi([], $adapter); 130 | } 131 | } 132 | -------------------------------------------------------------------------------- /src/Common/Integrations/Adapters/AlchemistAdapter.php: -------------------------------------------------------------------------------- 1 | alchemistRestfulApi = $alchemistRestfulApi; 31 | } 32 | 33 | /** 34 | * @param AlchemistRestfulApi $alchemistRestfulApi 35 | * @return void 36 | */ 37 | public function setAlchemistRestfulApi(AlchemistRestfulApi $alchemistRestfulApi): void 38 | { 39 | $this->alchemistRestfulApi = $alchemistRestfulApi; 40 | } 41 | 42 | /** 43 | * @return AlchemistRestfulApi 44 | */ 45 | public function getAlchemistRestfulApi(): AlchemistRestfulApi 46 | { 47 | return $this->alchemistRestfulApi; 48 | } 49 | 50 | /** 51 | * @param ErrorBag $errorBag 52 | * @return void 53 | */ 54 | public function errorHandler(ErrorBag $errorBag): void { 55 | $errorBag->setErrorHandler(function (CompoundErrors $errors) { 56 | $messages = []; 57 | 58 | foreach ($this->componentUses() as $componentClass) { 59 | $componentPropertyName = $this->alchemistRestfulApi->componentPropertyName($componentClass); 60 | 61 | if ($errors->{$componentPropertyName} === null) { 62 | continue; 63 | } 64 | 65 | if ($componentClass === FieldSelector::class) { 66 | $messages[$errors->fieldSelector->errorKey()] = $errors->fieldSelector->getMessages(); 67 | 68 | foreach ($messages[$errors->fieldSelector->errorKey()] as &$error) { 69 | if ($error['error_code'] === FieldSelectorErrorBag::UNSELECTABLE_FIELD) { 70 | $fieldStruct = $this->alchemistRestfulApi->fieldSelector()->getFieldStructure($error['error_namespace']); 71 | 72 | if ($fieldStruct) { 73 | $error['selectable'] = array_keys($fieldStruct['sub']); 74 | } 75 | } 76 | } 77 | 78 | unset($error); 79 | } else { 80 | $messages[$errors->{$componentPropertyName}->errorKey()] = $errors->{$componentPropertyName}->getMessages(); 81 | } 82 | } 83 | 84 | return $messages; 85 | }); 86 | } 87 | 88 | /** 89 | * @return string[] 90 | */ 91 | public function componentUses(): array 92 | { 93 | return [ 94 | FieldSelector::class, 95 | ResourceFilter::class, 96 | ResourceOffsetPaginator::class, 97 | ResourceSort::class, 98 | ResourceSearch::class 99 | ]; 100 | } 101 | 102 | /** 103 | * @return array 104 | */ 105 | public function componentConfigs(): array 106 | { 107 | return [ 108 | FieldSelector::class => [ 109 | 'request_params' => [ 110 | 'fields_param' => 'fields', 111 | ] 112 | ], 113 | ResourceFilter::class => [ 114 | 'request_params' => [ 115 | 'filtering_param' => 'filtering', 116 | ] 117 | ], 118 | ResourceOffsetPaginator::class => [ 119 | 'request_params' => [ 120 | 'limit_param' => 'limit', 121 | 'offset_param' => 'offset', 122 | ] 123 | ], 124 | ResourceSort::class => [ 125 | 'request_params' => [ 126 | 'sort_param' => 'sort', 127 | 'direction_param' => 'direction', 128 | ] 129 | ], 130 | ResourceSearch::class => [ 131 | 'request_params' => [ 132 | 'search_param' => 'search', 133 | ] 134 | ] 135 | ]; 136 | } 137 | } -------------------------------------------------------------------------------- /src/Common/Helpers/DotArrays.php: -------------------------------------------------------------------------------- 1 | $value) { 140 | self::array_dot_set($unflatten, $dotKey, $value); 141 | } 142 | 143 | return $unflatten; 144 | } 145 | 146 | #---------------------------------------------------------------------------# 147 | # Private Functions # 148 | #---------------------------------------------------------------------------# 149 | 150 | /** 151 | * @param array $array 152 | * @param bool $withSlashOnDotKey 153 | * @param bool $withEmptyArray 154 | * 155 | * @return array 156 | */ 157 | private static function array_dot_flatten_recursive( 158 | array $array, 159 | bool $withSlashOnDotKey = true, 160 | bool $withEmptyArray = true, 161 | array $ignoreFlatKeys = [], 162 | string $parentDotKey = '' 163 | ): array 164 | { 165 | $flatten = []; 166 | 167 | foreach ($array as $key => $value) { 168 | if ($withSlashOnDotKey) { 169 | $key = str_replace('.', '\.', $key); 170 | } 171 | 172 | if (! is_array($value)) { 173 | $flatten[$key] = $value; 174 | 175 | continue; 176 | } 177 | 178 | if (empty($value)) { 179 | if ($withEmptyArray) { 180 | $flatten[$key] = $value; 181 | } 182 | 183 | continue; 184 | } 185 | 186 | $path = ($parentDotKey !== '') ? "{$parentDotKey}.{$key}" : $key; 187 | 188 | $hasMatch = false; 189 | 190 | foreach ($ignoreFlatKeys as $ignorePattern) { 191 | $ignoreRegexPattern = str_replace('.', '\.', $ignorePattern); 192 | $ignoreRegexPattern = str_replace('*', '\d+', $ignoreRegexPattern); 193 | 194 | if (preg_match("/{$ignoreRegexPattern}/", $path)) { 195 | $nestedArray = $value; 196 | $flatten["{$key}"] = $value; 197 | $hasMatch = true; 198 | 199 | break; 200 | } 201 | } 202 | 203 | if (! $hasMatch) { 204 | $nestedArray = DotArrays::array_dot_flatten_recursive($value, $withSlashOnDotKey, $withEmptyArray, $ignoreFlatKeys, $path); 205 | 206 | foreach ($nestedArray as $nestedKey => $nestedValue) { 207 | $flatten["{$key}.{$nestedKey}"] = $nestedValue; 208 | } 209 | } 210 | } 211 | 212 | return $flatten; 213 | } 214 | } -------------------------------------------------------------------------------- /src/ResourceFilter/Notifications/ResourceFilterErrorBag.php: -------------------------------------------------------------------------------- 1 | passes = $passes; 64 | $this->missingRequiredFiltering = $missingRequiredFiltering; 65 | $this->invalidFiltering = $invalidFiltering; 66 | $this->invalidFilteringValue = $invalidFilteringValue; 67 | $this->invalidInputType = $invalidInputType; 68 | } 69 | 70 | /** 71 | * @return array 72 | */ 73 | public function toArray(): array 74 | { 75 | return [ 76 | 'passes' => $this->passes, 77 | 'missing_required_filtering' => $this->missingRequiredFiltering, 78 | 'invalid_filtering' => $this->invalidFiltering, 79 | 'invalid_filtering_value' => $this->invalidFilteringValue, 80 | 'invalid_input_type' => $this->invalidInputType, 81 | ]; 82 | } 83 | 84 | /** 85 | * @return bool 86 | */ 87 | public function passes(): bool 88 | { 89 | return $this->passes; 90 | } 91 | 92 | /** 93 | * @return string 94 | */ 95 | public function errorKey(): string 96 | { 97 | return 'filtering'; 98 | } 99 | 100 | /** 101 | * @return array 102 | */ 103 | public function getMessages(): array 104 | { 105 | if ($this->passes()) { 106 | return []; 107 | } 108 | 109 | $messages = []; 110 | 111 | if ($this->isInvalidInputType()) { 112 | $messages[] = [ 113 | 'error_code' => static::INVALID_INPUT_TYPE, 114 | 'error_message' => "The input type is invalid. It must be an array.", 115 | ]; 116 | } 117 | 118 | if ($this->hasInvalidFiltering()) { 119 | $messages[] = [ 120 | 'error_code' => static::INVALID_FILTERING, 121 | 'error_message' => "You have passed in invalid filtering.", 122 | 'error_filtering' => $this->getInvalidFiltering(), 123 | ]; 124 | } 125 | 126 | if ($this->hasInvalidFilteringValue()) { 127 | 128 | $invalidFilteringValue = array_map(static function($e) { 129 | $data = []; 130 | 131 | $data[] = $e->getFiltering(); 132 | 133 | if (! empty($e->getSupportedFormats())) { 134 | $data[] = implode('|', $e->getSupportedFormats()); 135 | } elseif (! empty($e->getSupportedValue())) { 136 | $data[] = implode('|', $e->getSupportedValue()); 137 | } elseif (! empty($e->getSupportedType())) { 138 | $data[] = implode('|', $e->getSupportedType()); 139 | } 140 | 141 | return implode('^', $data); 142 | 143 | } , $this->getInvalidFilteringValue()); 144 | 145 | $messages[] = [ 146 | 'error_code' => static::INVALID_FILTERING_VALUE, 147 | 'error_message' => "You have passed in invalid filtering value.", 148 | 'error_filtering' => $invalidFilteringValue, 149 | ]; 150 | } 151 | 152 | if ($this->hasMissingRequiredFiltering()) { 153 | $messages[] = [ 154 | 'error_code' => static::MISSING_REQUIRED_FILTERING, 155 | 'error_message' => "You have missed required filtering.", 156 | 'error_filtering' => $this->getMissingRequiredFiltering(), 157 | ]; 158 | } 159 | 160 | return $messages; 161 | } 162 | 163 | /** 164 | * @return string[] 165 | */ 166 | public function getMissingRequiredFiltering(): array 167 | { 168 | return $this->missingRequiredFiltering; 169 | } 170 | 171 | /** 172 | * @return string[] 173 | */ 174 | public function getInvalidFiltering(): array 175 | { 176 | return $this->invalidFiltering; 177 | } 178 | 179 | /** 180 | * @return array 181 | */ 182 | public function getInvalidFilteringValue(): array 183 | { 184 | return $this->invalidFilteringValue; 185 | } 186 | 187 | /** 188 | * @return bool 189 | */ 190 | public function hasMissingRequiredFiltering(): bool 191 | { 192 | return ! empty($this->missingRequiredFiltering); 193 | } 194 | 195 | /** 196 | * @return bool 197 | */ 198 | public function hasInvalidFiltering(): bool 199 | { 200 | return ! empty($this->invalidFiltering); 201 | } 202 | 203 | /** 204 | * @return bool 205 | */ 206 | public function hasInvalidFilteringValue(): bool 207 | { 208 | return ! empty($this->invalidFilteringValue); 209 | } 210 | 211 | /** 212 | * @return bool 213 | */ 214 | public function isInvalidInputType(): bool 215 | { 216 | return $this->invalidInputType; 217 | } 218 | } -------------------------------------------------------------------------------- /tests/Feature/ResourceSearchTest.php: -------------------------------------------------------------------------------- 1 | 'qwerty', 18 | ]); 19 | 20 | $restfulApi->resourceSearch()->defineSearchCondition('title'); 21 | 22 | $this->assertTrue($restfulApi->validate()->passes()); 23 | 24 | $this->assertSame('title', $restfulApi->resourceSearch()->search()->getSearchCondition()); 25 | $this->assertSame('qwerty', $restfulApi->resourceSearch()->search()->getSearchValue()); 26 | } 27 | 28 | /** 29 | * @throws AlchemistRestfulApiException 30 | */ 31 | public function test_ResourceSearch_with_SearchConditionDefined_must_pass(): void 32 | { 33 | $restfulApi = new AlchemistRestfulApi([ 34 | 'search' => 'qwerty', 35 | ]); 36 | 37 | $restfulApi->resourceSearch()->defineSearchCondition('title'); 38 | 39 | $this->assertTrue($restfulApi->validate()->passes()); 40 | 41 | $this->assertSame('title', $restfulApi->resourceSearch()->search()->getSearchCondition()); 42 | $this->assertSame('qwerty', $restfulApi->resourceSearch()->search()->getSearchValue()); 43 | } 44 | 45 | /** 46 | * @throws AlchemistRestfulApiException 47 | */ 48 | public function test_ResourceSearch_without_SearchCondition_must_pass(): void 49 | { 50 | $restfulApi = new AlchemistRestfulApi([ 51 | 'search' => 'qwerty', 52 | ]); 53 | 54 | $this->assertTrue($restfulApi->validate()->passes()); 55 | 56 | $this->assertSame('', $restfulApi->resourceSearch()->search()->getSearchCondition()); 57 | $this->assertSame('qwerty', $restfulApi->resourceSearch()->search()->getSearchValue()); 58 | } 59 | 60 | /** 61 | * @throws AlchemistRestfulApiException 62 | */ 63 | public function test_ResourceSearch_with_NullSearchValue_must_pass(): void 64 | { 65 | $restfulApi = new AlchemistRestfulApi([ 66 | 'search' => null, 67 | ]); 68 | 69 | $this->assertTrue($restfulApi->validate()->passes()); 70 | 71 | $this->assertSame('', $restfulApi->resourceSearch()->search()->getSearchCondition()); 72 | $this->assertSame('', $restfulApi->resourceSearch()->search()->getSearchValue()); 73 | } 74 | 75 | /** 76 | * @throws AlchemistRestfulApiException 77 | */ 78 | public function test_ResourceSearch_with_ArraySortParamValue_must_FALSE_validation(): void 79 | { 80 | $restfulApi = new AlchemistRestfulApi([ 81 | 'search' => ['invalid_type'], 82 | ]); 83 | 84 | $this->assertFalse($restfulApi->validate()->passes()); 85 | 86 | $this->assertNotNull($restfulApi->validate()->getErrors()['search']); 87 | $this->assertTrue($restfulApi->resourceSearch()->validate()->isInvalidInputType()); 88 | 89 | $this->assertSame('', $restfulApi->resourceSearch()->search()->getSearchCondition()); 90 | $this->assertSame('', $restfulApi->resourceSearch()->search()->getSearchValue()); 91 | } 92 | 93 | /** 94 | * @throws AlchemistRestfulApiException 95 | */ 96 | public function test_ResourceSearch_with_ObjectSortParamValue_must_FALSE_validation(): void 97 | { 98 | $restfulApi = new AlchemistRestfulApi([ 99 | 'search' => new \stdClass(), 100 | ]); 101 | 102 | $this->assertFalse($restfulApi->validate()->passes()); 103 | 104 | $this->assertNotNull($restfulApi->validate()->getErrors()['search']); 105 | $this->assertTrue($restfulApi->resourceSearch()->validate()->isInvalidInputType()); 106 | 107 | $this->assertSame('', $restfulApi->resourceSearch()->search()->getSearchCondition()); 108 | $this->assertSame('', $restfulApi->resourceSearch()->search()->getSearchValue()); 109 | } 110 | 111 | /** 112 | * @throws AlchemistRestfulApiException 113 | */ 114 | public function test_ResourceSearch_with_TrueBooleanSortParamValue_must_FALSE_validation(): void 115 | { 116 | $restfulApi = new AlchemistRestfulApi([ 117 | 'search' => true, 118 | ]); 119 | 120 | $this->assertFalse($restfulApi->validate()->passes()); 121 | 122 | $this->assertNotNull($restfulApi->validate()->getErrors()['search']); 123 | $this->assertTrue($restfulApi->resourceSearch()->validate()->isInvalidInputType()); 124 | 125 | $this->assertSame('', $restfulApi->resourceSearch()->search()->getSearchCondition()); 126 | $this->assertSame('', $restfulApi->resourceSearch()->search()->getSearchValue()); 127 | } 128 | 129 | /** 130 | * @throws AlchemistRestfulApiException 131 | */ 132 | public function test_ResourceSearch_with_FalseBooleanSortParamValue_must_FALSE_validation(): void 133 | { 134 | $restfulApi = new AlchemistRestfulApi([ 135 | 'search' => false, 136 | ]); 137 | 138 | $this->assertFalse($restfulApi->validate()->passes()); 139 | 140 | $this->assertNotNull($restfulApi->validate()->getErrors()['search']); 141 | $this->assertTrue($restfulApi->resourceSearch()->validate()->isInvalidInputType()); 142 | 143 | $this->assertSame('', $restfulApi->resourceSearch()->search()->getSearchCondition()); 144 | $this->assertSame('', $restfulApi->resourceSearch()->search()->getSearchValue()); 145 | } 146 | 147 | /** 148 | * @throws AlchemistRestfulApiException 149 | */ 150 | public function test_ResourceSearch_with_IntegerBooleanSortParamValue_must_FALSE_validation(): void 151 | { 152 | $restfulApi = new AlchemistRestfulApi([ 153 | 'search' => 123, 154 | ]); 155 | 156 | $this->assertFalse($restfulApi->validate()->passes()); 157 | 158 | $this->assertNotNull($restfulApi->validate()->getErrors()['search']); 159 | $this->assertTrue($restfulApi->resourceSearch()->validate()->isInvalidInputType()); 160 | 161 | $this->assertSame('', $restfulApi->resourceSearch()->search()->getSearchCondition()); 162 | $this->assertSame('', $restfulApi->resourceSearch()->search()->getSearchValue()); 163 | } 164 | 165 | /** 166 | * @throws AlchemistRestfulApiException 167 | */ 168 | public function test_ResourceSearch_withNumericBooleanSortParamValue_must_FALSE_validation(): void 169 | { 170 | $restfulApi = new AlchemistRestfulApi([ 171 | 'search' => 123.1, 172 | ]); 173 | 174 | $this->assertFalse($restfulApi->validate()->passes()); 175 | 176 | $this->assertNotNull($restfulApi->validate()->getErrors()['search']); 177 | $this->assertTrue($restfulApi->resourceSearch()->validate()->isInvalidInputType()); 178 | 179 | $this->assertSame('', $restfulApi->resourceSearch()->search()->getSearchCondition()); 180 | $this->assertSame('', $restfulApi->resourceSearch()->search()->getSearchValue()); 181 | } 182 | } 183 | -------------------------------------------------------------------------------- /src/ResourceFilter/Objects/FilteringRules.php: -------------------------------------------------------------------------------- 1 | $value) { 32 | $this->{$field} = $value; 33 | } 34 | 35 | $this->nameMappedItems = []; 36 | 37 | foreach ($this->items as $item) { 38 | $this->nameMappedItems[$item->getName()] = $item; 39 | } 40 | 41 | $this->flippedOperators = array_flip($this->operators); 42 | $this->flippedEnums = array_flip($this->enums); 43 | } 44 | 45 | #---------------------------------------------------------------------------# 46 | # Define Instances # 47 | #---------------------------------------------------------------------------# 48 | 49 | /** 50 | * @param string $name 51 | * @param array $operators 52 | * 53 | * @return FilteringRules 54 | */ 55 | public static function String(string $name, array $operators): FilteringRules 56 | { 57 | return new FilteringRules(['type' => self::TYPE_STRING, 'name' => $name, 'operators' => $operators]); 58 | } 59 | 60 | /** 61 | * @param string $name 62 | * @param array $operators 63 | * 64 | * @return FilteringRules 65 | */ 66 | public static function Integer(string $name, array $operators): FilteringRules 67 | { 68 | return new FilteringRules(['type' => self::TYPE_INTEGER, 'name' => $name, 'operators' => $operators]); 69 | } 70 | 71 | /** 72 | * @param string $name 73 | * @param array $operators 74 | * 75 | * @return FilteringRules 76 | */ 77 | public static function Number(string $name, array $operators): FilteringRules 78 | { 79 | return new FilteringRules(['type' => self::TYPE_NUMBER, 'name' => $name, 'operators' => $operators]); 80 | } 81 | 82 | /** 83 | * @param string $name 84 | * @param array $operators 85 | * @param array $formats 86 | * 87 | * @return FilteringRules 88 | */ 89 | public static function Date(string $name, array $operators, array $formats = ['Y-m-d']): FilteringRules 90 | { 91 | return new FilteringRules(['type' => self::TYPE_DATE, 'name' => $name, 'operators' => $operators, 'formats' => $formats]); 92 | } 93 | 94 | /** 95 | * @param string $name 96 | * @param array $operators 97 | * @param array $formats 98 | * 99 | * @return FilteringRules 100 | */ 101 | public static function Datetime(string $name, array $operators, array $formats = ['Y-m-d H:i:s']): FilteringRules 102 | { 103 | return new FilteringRules(['type' => self::TYPE_DATETIME, 'name' => $name, 'operators' => $operators, 'formats' => $formats]); 104 | } 105 | 106 | /** 107 | * @param string $name 108 | * @param array $operators 109 | * @param array $enums 110 | * 111 | * @return FilteringRules 112 | */ 113 | public static function Enum(string $name, array $operators, array $enums): FilteringRules 114 | { 115 | return new FilteringRules(['type' => self::TYPE_ENUM, 'name' => $name, 'operators' => $operators, 'enums' => $enums]); 116 | } 117 | 118 | /** 119 | * @param string $name 120 | * @param array $extraOperators [empty, duplicate] 121 | * 122 | * @return FilteringRules 123 | */ 124 | public static function Boolean(string $name, array $extraOperators = []): FilteringRules 125 | { 126 | return new FilteringRules(['type' => self::TYPE_BOOLEAN, 'name' => $name, 'operators' => array_unique(array_merge(['is', 'eq'], $extraOperators))]); 127 | } 128 | 129 | /** 130 | * @param string $name 131 | * @param array $items 132 | * 133 | * @return FilteringRules 134 | */ 135 | public static function Group(string $name, array $items): FilteringRules 136 | { 137 | return new FilteringRules(['type' => self::TYPE_GROUP, 'name' => $name, 'operators' => ['eq'], 'items' => $items]); 138 | } 139 | 140 | #---------------------------------------------------------------------------# 141 | # Getter Methods # 142 | #---------------------------------------------------------------------------# 143 | 144 | /** 145 | * @return string|null 146 | */ 147 | public function getName(): ?string 148 | { 149 | return $this->name; 150 | } 151 | 152 | /** 153 | * @return array 154 | */ 155 | public function getOperators(): array 156 | { 157 | return $this->operators; 158 | } 159 | 160 | /** 161 | * @return string|null 162 | */ 163 | public function getType(): ?string 164 | { 165 | return $this->type; 166 | } 167 | 168 | /** 169 | * @return array 170 | */ 171 | public function getEnums(): array 172 | { 173 | return $this->enums; 174 | } 175 | 176 | /** 177 | * @return FilteringRules[] 178 | */ 179 | public function getItems(): array 180 | { 181 | return $this->items; 182 | } 183 | 184 | /** 185 | * @return array 186 | */ 187 | public function getFormats(): array 188 | { 189 | return $this->formats; 190 | } 191 | 192 | /** 193 | * @return array 194 | */ 195 | public function getNameMappedItems(): array 196 | { 197 | return $this->nameMappedItems; 198 | } 199 | 200 | #---------------------------------------------------------------------------# 201 | # Helpers # 202 | #---------------------------------------------------------------------------# 203 | 204 | /** 205 | * @param string $itemName 206 | * 207 | * @return FilteringRules|null 208 | */ 209 | public function collectItemByName(string $itemName): ?FilteringRules 210 | { 211 | return $this->nameMappedItems[$itemName] ?? null; 212 | } 213 | 214 | /** 215 | * @param string $operator 216 | * 217 | * @return bool 218 | */ 219 | public function hasOperator(string $operator): bool 220 | { 221 | return isset($this->flippedOperators[$operator]); 222 | } 223 | 224 | /** 225 | * @param $enum 226 | * 227 | * @return bool 228 | */ 229 | public function hasEnum($enum): bool 230 | { 231 | return isset($this->flippedEnums[$enum]); 232 | } 233 | 234 | /** 235 | * @param string $itemName 236 | * 237 | * @return bool 238 | */ 239 | public function hasItem(string $itemName): bool 240 | { 241 | return isset($this->nameMappedItems[$itemName]); 242 | } 243 | } -------------------------------------------------------------------------------- /src/Common/Helpers/Arrays.php: -------------------------------------------------------------------------------- 1 | $value) { 82 | foreach ($values as $oneValue) { 83 | if ($value === $oneValue) { 84 | unset($array[$key]); 85 | 86 | break; 87 | } 88 | } 89 | } 90 | 91 | return $array; 92 | } 93 | 94 | /** 95 | * @param array $array 96 | * 97 | * @return array 98 | */ 99 | public static function removeUndefined(array $array): array 100 | { 101 | return Arrays::removeValues($array, ['@UNDEFINED!']); 102 | } 103 | 104 | /** 105 | * @param array $array 106 | * @param array $sortBy 107 | * 108 | * @return array 109 | */ 110 | public static function sort2D(array $array, array $sortBy): array 111 | { 112 | $mapSortDirections = [ 113 | 'desc' => SORT_DESC, 114 | 'asc' => SORT_ASC, 115 | ]; 116 | 117 | $sortParams = []; 118 | 119 | foreach ($sortBy as $sort => $direction) { 120 | $sortColumns = array_column($array, $sort); 121 | $direction = $mapSortDirections[strtolower($direction)]; 122 | 123 | $sortParams[] = $sortColumns; 124 | $sortParams[] = $direction; 125 | } 126 | 127 | $sortParams[] = $array; 128 | 129 | array_multisort(...$sortParams); 130 | 131 | return $sortParams[array_key_last($sortParams)]; 132 | } 133 | 134 | 135 | /** 136 | * @param array $array 137 | * @param string $dotKey 138 | * @param mixed $value 139 | * @param bool $force 140 | * 141 | * @return void 142 | */ 143 | public static function dotSet(array &$array, string $dotKey, $value, bool $force = false): void 144 | { 145 | $matches = []; 146 | preg_match_all("/(?:(?:\\\\\\.)|(?!\.).|\s)+/", $dotKey, $matches); 147 | $keys = str_replace('\.', '.', $matches[0]); 148 | 149 | foreach ($keys as $key) { 150 | if ($force && ! is_array($array)) { 151 | $array = []; 152 | } 153 | 154 | $array = &$array[$key]; 155 | } 156 | 157 | $array = $value; 158 | } 159 | 160 | /** 161 | * @param array $array 162 | * @param string $dotKey 163 | * 164 | * @return bool 165 | */ 166 | public static function dotUnset(array &$array, string $dotKey): bool 167 | { 168 | if (! Arrays::dotKeyExists($array, $dotKey)) { 169 | return false; 170 | } 171 | 172 | if (array_key_exists($dotKey, $array)) { 173 | unset($array[$dotKey]); 174 | 175 | return true; 176 | } 177 | 178 | $matches = []; 179 | preg_match_all("/(?:(?:\\\\\\.)|(?!\.).|\s)+/", $dotKey, $matches); 180 | $keys = str_replace('\.', '.', $matches[0]); 181 | $lastKey = array_pop($keys); 182 | 183 | foreach ($keys as $key) { 184 | if (! isset($array[$key])) { 185 | continue; 186 | } 187 | 188 | $array = &$array[$key]; 189 | } 190 | 191 | unset($array[$lastKey]); 192 | 193 | return true; 194 | } 195 | 196 | /** 197 | * @param array $array 198 | * @param string $dotKey 199 | * 200 | * @return bool 201 | */ 202 | public static function dotKeyExists(array $array, string $dotKey): bool 203 | { 204 | $matches = []; 205 | preg_match_all("/(?:(?:\\\\\\.)|(?!\.).|\s)+/", $dotKey, $matches); 206 | $keys = str_replace('\.', '.', $matches[0]); 207 | 208 | foreach ($keys as $key) { 209 | if (! is_array($array) || ! array_key_exists($key, $array)) { 210 | return false; 211 | } 212 | 213 | $array = &$array[$key]; 214 | } 215 | 216 | return true; 217 | } 218 | 219 | /** 220 | * @param array $array 221 | * @param string $dotKey 222 | * 223 | * @return mixed 224 | */ 225 | public static function dotValue(array $array, string $dotKey) 226 | { 227 | $matches = []; 228 | preg_match_all("/(?:(?:\\\\\\.)|(?!\.).|\s)+/", $dotKey, $matches); 229 | $keys = str_replace('\.', '.', $matches[0]); 230 | 231 | foreach ($keys as $key) { 232 | if (! is_array($array) || ! array_key_exists($key, $array)) { 233 | return null; 234 | } 235 | 236 | $array = &$array[$key]; 237 | } 238 | 239 | return $array; 240 | } 241 | 242 | /** 243 | * @param array $array 244 | * @param bool $slashOnDotKey 245 | * 246 | * @return array 247 | */ 248 | public static function dotFlatten(array $array, bool $slashOnDotKey = true): array 249 | { 250 | $arrayRecursiveIterator = new RecursiveIteratorIterator(new RecursiveArrayIterator($array)); 251 | 252 | $flatten = []; 253 | 254 | foreach ($arrayRecursiveIterator as $leafValue) { 255 | $keys = []; 256 | 257 | foreach (range(0, $arrayRecursiveIterator->getDepth()) as $depth) { 258 | $keys[] = $arrayRecursiveIterator->getSubIterator($depth)->key(); 259 | } 260 | 261 | if ($slashOnDotKey) { 262 | $keys = str_replace('.', '\.', $keys); 263 | } 264 | 265 | $flatten[implode('.', $keys)] = $leafValue; 266 | } 267 | 268 | return $flatten; 269 | } 270 | 271 | /** 272 | * @param array $flatten 273 | * @return array 274 | */ 275 | public static function dotUnflatten(array $flatten): array 276 | { 277 | $unflatten = []; 278 | 279 | foreach ($flatten as $dotKey => $value) { 280 | Arrays::dotSet($unflatten, $dotKey, $value); 281 | } 282 | 283 | return $unflatten; 284 | } 285 | } -------------------------------------------------------------------------------- /src/Response/Compose/Handlers/ResponseCompose.php: -------------------------------------------------------------------------------- 1 | alchemist = $alchemist; 31 | } 32 | 33 | /** 34 | * @return array 35 | */ 36 | public function responseData(): array 37 | { 38 | return $this->responseData; 39 | } 40 | 41 | /** 42 | * @throws AlchemistRestfulApiException 43 | */ 44 | public function collection(array $data): void 45 | { 46 | $this->deepValidateFieldStructure('collection', '$', $data); 47 | 48 | $this->responseData = $data; 49 | 50 | $this->responseDataType = 'collection'; 51 | } 52 | 53 | /** 54 | * @throws AlchemistRestfulApiException 55 | */ 56 | public function object(array $data): void 57 | { 58 | $this->deepValidateFieldStructure('object', '$', $data); 59 | 60 | $this->responseData = $data; 61 | 62 | $this->responseDataType = 'object'; 63 | } 64 | 65 | /** 66 | * @param array $dataCollection 67 | * @return $this 68 | */ 69 | public function fromCollection(array $dataCollection): self 70 | { 71 | $this->responseData = $dataCollection; 72 | 73 | $this->responseDataType = 'collection'; 74 | 75 | return $this; 76 | } 77 | 78 | /** 79 | * @param array $data 80 | * @return $this 81 | */ 82 | public function fromObject(array $data): self 83 | { 84 | $this->responseData = $data; 85 | 86 | $this->responseDataType = 'object'; 87 | 88 | return $this; 89 | } 90 | 91 | public function compose( 92 | string $namespace, string $composeFieldName, string $compareField, array $incomingDataMap): self 93 | { 94 | $dotFlatKey = $this->calculateDotFlatKey($namespace, $compareField); 95 | $pattern = $this->calculateRegexPatternFromDotFlatKey($dotFlatKey); 96 | 97 | $data = $this->responseData; 98 | 99 | $flatData = DotArrays::array_dot_flatten($data, true, true, [$dotFlatKey]); 100 | 101 | $composeFieldStruct = $this->alchemist->fieldSelector()->getFieldStructure($namespace.'.'.$composeFieldName); 102 | $composeFieldStructType = $composeFieldStruct['type']; 103 | 104 | foreach ($flatData as $dotKey => $value) { 105 | $hasMatched = (bool) preg_match("/^{$pattern}$/", $dotKey, $matches); 106 | 107 | if ($hasMatched) { 108 | $dotComposeKey = str_replace($compareField, $composeFieldName, $dotKey); 109 | 110 | if (! is_array($value)) { 111 | if (array_key_exists($value, $incomingDataMap)) { 112 | $flatData[$dotComposeKey] = $incomingDataMap[$value]; 113 | } else { 114 | $flatData[$dotComposeKey] = ($composeFieldStructType === 'collection') ? [] : null; 115 | } 116 | } else { 117 | $flatData[$dotComposeKey] = array_reduce($value, function ($res, $identifier) use ($incomingDataMap) { 118 | if (isset($incomingDataMap[$identifier])) { 119 | $res[] = $incomingDataMap[$identifier]; 120 | } 121 | 122 | return $res; 123 | }, []); 124 | } 125 | } 126 | } 127 | 128 | $this->responseData = DotArrays::array_dot_unflatten($flatData); 129 | 130 | return $this; 131 | } 132 | 133 | /** 134 | * Clean redundant data from response data 135 | * 136 | * @return $this 137 | */ 138 | public function cleanRedundantData(): self 139 | { 140 | if ($this->responseDataType === 'collection') { 141 | $this->responseData = $this->cleanRedundantDataCollection($this->responseData, '$'); 142 | } else { 143 | $this->responseData = $this->cleanRedundantDataObject($this->responseData, '$'); 144 | } 145 | 146 | return $this; 147 | } 148 | 149 | /** 150 | * @param array $data 151 | * @param string $namespace 152 | * 153 | * @return array 154 | */ 155 | private function cleanRedundantDataObject(array $data, string $namespace): array 156 | { 157 | if (empty($data)) { 158 | return $data; 159 | } 160 | 161 | $fieldStructure = $this->alchemist->fieldSelector()->getFieldStructure($namespace); 162 | $inputFields = $this->alchemist->fieldSelector()->flatFields($namespace, false); 163 | 164 | $data = Arrays::array_with_keys($data, $inputFields); 165 | 166 | foreach ($data as $subField => $subData) { 167 | $subDataType = $fieldStructure['sub'][$subField]; 168 | 169 | if (! empty($subData)) { 170 | switch ($subDataType) { 171 | case 'collection': 172 | $subData = $this->cleanRedundantDataCollection($subData, "{$namespace}.{$subField}"); 173 | break; 174 | case 'object': 175 | $subData = $this->cleanRedundantDataObject($subData, "{$namespace}.{$subField}"); 176 | break; 177 | } 178 | } 179 | 180 | $data[$subField] = $subData; 181 | } 182 | 183 | return $data; 184 | } 185 | 186 | /** 187 | * @param array $dataCollection 188 | * @param string $namespace 189 | * 190 | * @return array 191 | */ 192 | private function cleanRedundantDataCollection(array $dataCollection, string $namespace): array 193 | { 194 | foreach ($dataCollection as $index => $data) { 195 | $dataCollection[$index] = $this->cleanRedundantDataObject($data, $namespace); 196 | } 197 | 198 | return $dataCollection; 199 | } 200 | 201 | /** 202 | * @param string|null $type 203 | * @param string $namespace 204 | * @param array $data 205 | * @return void 206 | * @throws AlchemistRestfulApiException 207 | */ 208 | private function deepValidateFieldStructure(?string $type, string $namespace, array $data): void 209 | { 210 | $structure = $this->alchemist->fieldSelector()->getFieldStructure($namespace); 211 | 212 | if ($structure['type'] === 'root') { 213 | $structure['type'] = $type; 214 | } 215 | 216 | if ($structure['type'] === 'collection') { 217 | if (! array_is_list($data)) { 218 | throw new AlchemistRestfulApiException( 219 | sprintf("Data with `%s` namespace must be a list", $namespace) 220 | ); 221 | } 222 | 223 | foreach ($data as $object) { 224 | foreach ($object as $field => $value) { 225 | if (! array_key_exists($field, $structure['sub'])) { 226 | throw new AlchemistRestfulApiException( 227 | sprintf("`%s` field doesn't exist at `%s` namespace collection", $field, $namespace) 228 | ); 229 | } 230 | 231 | if (in_array($structure['sub'][$field], ['collection', 'object'])) { 232 | $this->deepValidateFieldStructure(null, $namespace.".$field", $value); 233 | } 234 | } 235 | 236 | break; 237 | } 238 | } else { // type object 239 | foreach ($data as $field => $value) { 240 | if (! array_key_exists($field, $structure['sub'])) { 241 | throw new AlchemistRestfulApiException( 242 | sprintf("`%s` field doesn't exist at `%s` namespace collection", $field, $namespace) 243 | ); 244 | } 245 | if (in_array($structure['sub'][$field], ['collection', 'object'])) { 246 | $this->deepValidateFieldStructure(null, $namespace.".$field", $value); 247 | } 248 | } 249 | } 250 | } 251 | 252 | /** 253 | * @param string $namespace 254 | * @param string $compareField 255 | * @param string $mapType 256 | * 257 | * @return string 258 | */ 259 | private function calculateDotFlatKey(string $namespace, string $compareField): string 260 | { 261 | $namespaceFragments = explode('.', $namespace); 262 | 263 | $pattern = ''; 264 | $subNamespace = ''; 265 | foreach ($namespaceFragments as $namespaceFragment) { 266 | $subNamespace .= $namespaceFragment; 267 | 268 | if ($namespaceFragment === '$') { 269 | $subNamespace .= '.'; 270 | 271 | if ($this->responseDataType === 'collection') { 272 | $pattern = '*.'; 273 | } 274 | 275 | continue; 276 | } 277 | 278 | $fieldStructure = $this->alchemist->fieldSelector()->getFieldStructure($subNamespace); 279 | 280 | $subNamespace .= '.'; 281 | 282 | $pattern .= "{$namespaceFragment}."; 283 | 284 | if ($fieldStructure['type'] === 'collection') { 285 | $pattern .= '*.'; 286 | } 287 | } 288 | 289 | $pattern .= $compareField; 290 | 291 | return $pattern; 292 | } 293 | 294 | /** 295 | * @param string $dotFlatKey 296 | * @return string 297 | */ 298 | public function calculateRegexPatternFromDotFlatKey(string $dotFlatKey): string 299 | { 300 | $pattern = str_replace('.', '\.', $dotFlatKey); 301 | $pattern = str_replace('*', '\d+', $pattern); 302 | 303 | return $pattern; 304 | } 305 | } -------------------------------------------------------------------------------- /src/AlchemistRestfulApi.php: -------------------------------------------------------------------------------- 1 | linkAdapter($adapter); 45 | 46 | foreach ($adapter->componentUses() as $componentClass) { 47 | switch ($componentClass) { 48 | case FieldSelector::class: 49 | $this->initFieldSelector($requestInput); 50 | break; 51 | case ResourceFilter::class: 52 | $this->initResourceFilter($requestInput); 53 | break; 54 | case ResourceOffsetPaginator::class: 55 | $this->initResourceOffsetPaginator($requestInput); 56 | break; 57 | case ResourceSort::class: 58 | $this->initResourceSort($requestInput); 59 | break; 60 | case ResourceSearch::class: 61 | $this->initResourceSearch($requestInput); 62 | break; 63 | } 64 | } 65 | 66 | $this->initResponseCompose($this); 67 | } 68 | 69 | /** 70 | * @param $apiClass 71 | * @param array $requestInput 72 | * @param AlchemistAdapter|null $adapter 73 | * @return AlchemistRestfulApi 74 | * @throws AlchemistRestfulApiException 75 | */ 76 | public static function for($apiClass, array $requestInput, ?AlchemistAdapter $adapter = null): self 77 | { 78 | /** @var AlchemistQueryable|StatefulAlchemistQueryable $apiClass */ 79 | 80 | if (! is_object($apiClass)) { 81 | $apiClass = new $apiClass; 82 | } 83 | 84 | $hasValidApiInstance = $apiClass instanceof AlchemistQueryable 85 | || $apiClass instanceof StatefulAlchemistQueryable; 86 | 87 | if (! $hasValidApiInstance) { 88 | throw new \RuntimeException("Api Class must be instance of AlchemistQueryable or StatefulAlchemistQueryable"); 89 | } 90 | 91 | if ($adapter === null) { 92 | if ($apiClass instanceof AlchemistQueryable) { 93 | $adapter = $apiClass::getAdapter(); 94 | } elseif ($apiClass instanceof StatefulAlchemistQueryable) { 95 | $adapter = $apiClass->getAdapter(); 96 | } 97 | } 98 | 99 | $instance = new self($requestInput, $adapter); 100 | 101 | foreach ($instance->adapter->componentUses() as $componentClass) { 102 | if (! $instance->isComponentUses($componentClass)) { 103 | continue; 104 | } 105 | 106 | switch ($componentClass) { 107 | case FieldSelector::class: 108 | if ($apiClass instanceof AlchemistQueryable) { 109 | $apiClass::fieldSelector($instance->fieldSelector()); 110 | } elseif ($apiClass instanceof StatefulAlchemistQueryable) { 111 | $apiClass->fieldSelector($instance->fieldSelector()); 112 | } 113 | break; 114 | case ResourceFilter::class: 115 | if ($apiClass instanceof AlchemistQueryable) { 116 | $apiClass::resourceFilter($instance->resourceFilter()); 117 | } elseif ($apiClass instanceof StatefulAlchemistQueryable) { 118 | $apiClass->resourceFilter($instance->resourceFilter()); 119 | } 120 | break; 121 | case ResourceOffsetPaginator::class: 122 | if ($apiClass instanceof AlchemistQueryable) { 123 | $apiClass::resourceOffsetPaginator($instance->resourceOffsetPaginator()); 124 | } elseif ($apiClass instanceof StatefulAlchemistQueryable) { 125 | $apiClass->resourceOffsetPaginator($instance->resourceOffsetPaginator()); 126 | } 127 | break; 128 | case ResourceSort::class: 129 | if ($apiClass instanceof AlchemistQueryable) { 130 | $apiClass::resourceSort($instance->resourceSort()); 131 | } elseif ($apiClass instanceof StatefulAlchemistQueryable) { 132 | $apiClass->resourceSort($instance->resourceSort()); 133 | } 134 | break; 135 | case ResourceSearch::class: 136 | if ($apiClass instanceof AlchemistQueryable) { 137 | $apiClass::resourceSearch($instance->resourceSearch()); 138 | } elseif ($apiClass instanceof StatefulAlchemistQueryable) { 139 | $apiClass->resourceSearch($instance->resourceSearch()); 140 | } 141 | break; 142 | default: 143 | throw new \RuntimeException("Missing handle for component '{$componentClass}'"); 144 | } 145 | } 146 | 147 | return $instance; 148 | } 149 | 150 | /** 151 | * @deprecated Use linkAdapter() instead. 152 | * 153 | * @param AlchemistAdapter $adapter 154 | */ 155 | public function setAdapter(AlchemistAdapter $adapter): void 156 | { 157 | $this->linkAdapter($adapter); 158 | } 159 | 160 | /** 161 | * @param AlchemistAdapter $adapter 162 | */ 163 | public function linkAdapter(AlchemistAdapter $adapter): void 164 | { 165 | $this->adapter = $adapter; 166 | $this->adapter->setAlchemistRestfulApi($this); 167 | } 168 | 169 | /** 170 | * @return AlchemistAdapter 171 | */ 172 | public function getAdapter(): AlchemistAdapter 173 | { 174 | return $this->adapter; 175 | } 176 | 177 | /** 178 | * @param ErrorBag|null $errorBag 179 | * @return ErrorBag 180 | */ 181 | public function validate(?ErrorBag &$errorBag = null): ErrorBag 182 | { 183 | $errors = new CompoundErrors; 184 | 185 | $passes = true; 186 | 187 | foreach ($this->adapter->componentUses() as $componentClass) { 188 | switch ($componentClass) { 189 | case FieldSelector::class: 190 | if (! $this->fieldSelector->validate($componentErrorBag)->passes()) { 191 | $passes = false; 192 | $errors->fieldSelector = $componentErrorBag; 193 | } 194 | break; 195 | case ResourceFilter::class: 196 | if (! $this->resourceFilter->validate($componentErrorBag)->passes()) { 197 | $passes = false; 198 | $errors->resourceFilter = $componentErrorBag; 199 | } 200 | break; 201 | case ResourceOffsetPaginator::class: 202 | if (! $this->resourceOffsetPaginator->validate($componentErrorBag)->passes()) { 203 | $passes = false; 204 | $errors->resourceOffsetPaginator = $componentErrorBag; 205 | } 206 | break; 207 | case ResourceSort::class: 208 | if (! $this->resourceSort->validate($componentErrorBag)->passes()) { 209 | $passes = false; 210 | $errors->resourceSort = $componentErrorBag; 211 | } 212 | break; 213 | case ResourceSearch::class: 214 | if (! $this->resourceSearch->validate($componentErrorBag)->passes()) { 215 | $passes = false; 216 | $errors->resourceSearch = $componentErrorBag; 217 | } 218 | break; 219 | default: 220 | throw new \RuntimeException("Missing validate for component '{$componentClass}'"); 221 | } 222 | } 223 | 224 | $errorBag = new ErrorBag($passes, $errors); 225 | 226 | if (method_exists($this->adapter, 'errorHandler')) { 227 | $this->adapter->errorHandler($errorBag); 228 | } 229 | 230 | return $errorBag; 231 | } 232 | 233 | /** 234 | * @param string $componentClass 235 | * @return string 236 | */ 237 | public function componentName(string $componentClass): string 238 | { 239 | return ucfirst(Strings::end($componentClass, '\\')); 240 | } 241 | 242 | /** 243 | * @param string $componentClass 244 | * @return string 245 | */ 246 | public function componentPropertyName(string $componentClass): string 247 | { 248 | return lcfirst(Strings::end($componentClass, '\\')); 249 | } 250 | 251 | /** 252 | * @param string $componentClass 253 | * @return bool 254 | */ 255 | public function isComponentUses(string $componentClass): bool 256 | { 257 | return in_array($componentClass, $this->adapter->componentUses()); 258 | } 259 | 260 | /** 261 | * @param string $componentClass 262 | * @return array 263 | */ 264 | public function getComponentRequestParams(string $componentClass): array 265 | { 266 | return $this->adapter->componentConfigs()[$componentClass]['request_params'] ?? []; 267 | } 268 | } -------------------------------------------------------------------------------- /tests/Feature/FieldSelectorTest.php: -------------------------------------------------------------------------------- 1 | 'id,order_date,product{product_id,product_name,attributes{attribute_id,attribute_name}},gifts{gift_id,gift_name}' 21 | ]); 22 | 23 | $restfulApi->fieldSelector() 24 | ->defineFieldStructure([ 25 | 'id', 26 | 'order_date', 27 | new ObjectStructure('product', null, [ 28 | 'product_id', 29 | 'product_name', 30 | new CollectionStructure('attributes', null, [ 31 | 'attribute_id', 32 | 'attribute_name', 33 | ]), 34 | ]), 35 | new ObjectStructure('gifts', null, [ 36 | 'gift_id', 37 | 'gift_name', 38 | ]) 39 | ]); 40 | 41 | $this->assertTrue($restfulApi->validate()->passes()); 42 | 43 | $this->assertIsArray($restfulApi->fieldSelector()->fields()); 44 | 45 | $this->assertCount(4, $restfulApi->fieldSelector()->fields()); 46 | 47 | $this->assertContainsOnlyInstancesOf(FieldObject::class, $restfulApi->fieldSelector()->fields()); 48 | 49 | $this->assertSame(['id', 'order_date', 'product', 'gifts'], $restfulApi->fieldSelector()->flatFields()); 50 | 51 | $this->assertSame(['product_id', 'product_name', 'attributes'], $restfulApi->fieldSelector()->flatFields('$.product')); 52 | 53 | $this->assertSame(['gift_id', 'gift_name'], $restfulApi->fieldSelector()->flatFields('$.gifts')); 54 | 55 | $this->assertSame(['attribute_id', 'attribute_name'], $restfulApi->fieldSelector()->flatFields('$.product.attributes')); 56 | } 57 | 58 | /** 59 | * @throws AlchemistRestfulApiException 60 | */ 61 | public function test_FieldSelector_in_NormalCase_WithNullParamValue_must_pass(): void 62 | { 63 | $restfulApi = new AlchemistRestfulApi([ 64 | 'fields' => null 65 | ]); 66 | 67 | $this->assertTrue($restfulApi->validate()->passes()); 68 | 69 | $this->assertSame([], $restfulApi->fieldSelector()->fields()); 70 | } 71 | 72 | /** 73 | * @throws AlchemistRestfulApiException 74 | */ 75 | public function test_FieldSelector_when_FieldsParameterIsEmpty_then_DefaultFields_returned(): void 76 | { 77 | $restfulApi = new AlchemistRestfulApi([ 78 | 'fields' => '' 79 | ]); 80 | 81 | $restfulApi->fieldSelector() 82 | ->defineDefaultFields(['id']) 83 | ->defineFieldStructure([ 84 | 'id', 85 | ]); 86 | 87 | $this->assertTrue($restfulApi->validate()->passes()); 88 | 89 | $this->assertContains('id', $restfulApi->fieldSelector()->flatFields()); 90 | } 91 | 92 | /** 93 | * @throws AlchemistRestfulApiException 94 | */ 95 | public function test_FieldSelector_in_NormalCase_with_ObjectFieldParameter_must_pass(): void 96 | { 97 | $restfulApi = new AlchemistRestfulApi([ 98 | 'fields' => 'product{id,name}' 99 | ]); 100 | 101 | $restfulApi->fieldSelector() 102 | ->defineDefaultFields(['id']) 103 | ->defineFieldStructure([ 104 | 'id', 105 | new ObjectStructure('product', null, [ 106 | 'id', 107 | 'name', 108 | ]), 109 | ]); 110 | 111 | $this->assertTrue($restfulApi->validate()->passes()); 112 | 113 | $this->assertSame(['product'], $restfulApi->fieldSelector()->flatFields()); 114 | $this->assertSame(['id', 'name'], $restfulApi->fieldSelector()->flatFields('$.product')); 115 | } 116 | 117 | /** 118 | * @throws AlchemistRestfulApiException 119 | */ 120 | public function test_FieldSelector_when_SelectFieldHasSpace_must_pass(): void 121 | { 122 | $restfulApi = new AlchemistRestfulApi([ 123 | 'fields' => 'id, order_date, product{id, name}' 124 | ]); 125 | 126 | $restfulApi->fieldSelector() 127 | ->defineFieldStructure([ 128 | 'id', 129 | 'order_date', 130 | new ObjectStructure('product', null, [ 131 | 'id', 132 | 'name', 133 | ]), 134 | ]); 135 | 136 | $this->assertTrue($restfulApi->validate()->passes()); 137 | 138 | $this->assertSame(['id', 'order_date', 'product'], $restfulApi->fieldSelector()->flatFields()); 139 | $this->assertSame(['id', 'name'], $restfulApi->fieldSelector()->flatFields('$.product')); 140 | } 141 | 142 | /** 143 | * @throws AlchemistRestfulApiException 144 | */ 145 | public function test_FieldSelector_when_ObjectFieldWithEmptySubFields_then_DefaultFields_must_be_returned(): void 146 | { 147 | $restfulApi = new AlchemistRestfulApi([ 148 | 'fields' => 'product{}' 149 | ]); 150 | 151 | $restfulApi->fieldSelector() 152 | ->defineFieldStructure([ 153 | 'id', 154 | new ObjectStructure('product', null, [ 155 | 'id', 156 | 'name', 157 | ], ['id']), 158 | ]); 159 | 160 | $this->assertTrue($restfulApi->validate()->passes()); 161 | 162 | $this->assertContains('product', $restfulApi->fieldSelector()->flatFields()); 163 | 164 | $this->assertEquals(['id'], $restfulApi->fieldSelector()->flatFields('$.product')); 165 | } 166 | 167 | /** 168 | * @throws AlchemistRestfulApiException 169 | */ 170 | public function test_FieldSelector_when_NestedObjectFieldWithEmptySubFields_then_DefaultFields_must_be_returned(): void 171 | { 172 | $restfulApi = new AlchemistRestfulApi([ 173 | 'fields' => 'product{category}' 174 | ]); 175 | 176 | $restfulApi->fieldSelector() 177 | ->defineFieldStructure([ 178 | 'id', 179 | new ObjectStructure('product', null, [ 180 | 'id', 181 | 'name', 182 | new ObjectStructure('category', null, [ 183 | 'id', 184 | 'name', 185 | ], ['id']), 186 | ]), 187 | ]); 188 | 189 | $this->assertTrue($restfulApi->validate()->passes()); 190 | 191 | $this->assertContains('product', $restfulApi->fieldSelector()->flatFields()); 192 | $this->assertEquals(['id'], $restfulApi->fieldSelector()->flatFields('$.product.category')); 193 | } 194 | 195 | /** 196 | * @throws AlchemistRestfulApiException 197 | */ 198 | public function test_FieldSelector_when_SelectNotDefinedField_must_False_validation(): void 199 | { 200 | $restfulApi = new AlchemistRestfulApi([ 201 | 'fields' => 'not_exist_field' 202 | ]); 203 | 204 | $restfulApi->fieldSelector() 205 | ->defineFieldStructure([ 206 | 'id', 207 | 'order_date', 208 | ]); 209 | 210 | $this->assertFalse($restfulApi->validate()->passes()); 211 | 212 | $this->assertContains('not_exist_field', $restfulApi->fieldSelector()->validate()->getUnselectableFields()); 213 | 214 | $this->assertSame(['not_exist_field'], $restfulApi->fieldSelector()->flatFields()); 215 | } 216 | 217 | /** 218 | * @throws AlchemistRestfulApiException 219 | */ 220 | public function test_FieldSelector_when_SelectNestedFieldNotInStructure_must_False_validation(): void 221 | { 222 | $restfulApi = new AlchemistRestfulApi([ 223 | 'fields' => 'id,order_date,product{id,name,not_exist_field}' 224 | ]); 225 | 226 | $restfulApi->fieldSelector() 227 | ->defineFieldStructure([ 228 | 'id', 229 | 'order_date', 230 | new ObjectStructure('product', null, [ 231 | 'id', 232 | 'name', 233 | ]), 234 | ]); 235 | 236 | $this->assertFalse($restfulApi->validate()->passes()); 237 | 238 | $this->assertSame(['id', 'order_date', 'product'], $restfulApi->fieldSelector()->flatFields()); 239 | $this->assertSame(['id', 'name', 'not_exist_field'], $restfulApi->fieldSelector()->flatFields('$.product')); 240 | } 241 | 242 | /** 243 | * @throws AlchemistRestfulApiException 244 | */ 245 | public function test_FieldSelector_when_SelectNestedFieldNotInStructureOfSecondObject_must_False_validation(): void 246 | { 247 | $restfulApi = new AlchemistRestfulApi([ 248 | 'fields' => 'id,order_date,product{id,name},gift{id,name,not_exist_field}' 249 | ]); 250 | 251 | $restfulApi->fieldSelector() 252 | ->defineFieldStructure([ 253 | 'id', 254 | 'order_date', 255 | new ObjectStructure('product', null, [ 256 | 'id', 257 | 'name', 258 | ]), 259 | new ObjectStructure('gift', null, [ 260 | 'id', 261 | 'name', 262 | ]), 263 | ]); 264 | 265 | $this->assertFalse($restfulApi->validate()->passes()); 266 | 267 | $this->assertSame(['id', 'order_date', 'product', 'gift'], $restfulApi->fieldSelector()->flatFields()); 268 | $this->assertSame(['id', 'name'], $restfulApi->fieldSelector()->flatFields('$.product')); 269 | $this->assertSame(['id', 'name', 'not_exist_field'], $restfulApi->fieldSelector()->flatFields('$.gift')); 270 | } 271 | 272 | /** 273 | * @throws AlchemistRestfulApiException 274 | */ 275 | public function test_FieldSelector_WithObjectParamValue_must_False_validation(): void 276 | { 277 | $restfulApi = new AlchemistRestfulApi([ 278 | 'fields' => new \stdClass() 279 | ]); 280 | 281 | $this->assertFalse($restfulApi->validate()->passes()); 282 | $this->assertNotNull($restfulApi->validate()->getErrors()['fields']); 283 | $this->assertTrue($restfulApi->fieldSelector()->validate()->isInvalidInputType()); 284 | 285 | $this->assertSame([], $restfulApi->fieldSelector()->fields()); 286 | } 287 | 288 | /** 289 | * @throws AlchemistRestfulApiException 290 | */ 291 | public function test_FieldSelector_WithArrayParamValue_must_False_validation(): void 292 | { 293 | $restfulApi = new AlchemistRestfulApi([ 294 | 'fields' => [] 295 | ]); 296 | 297 | $this->assertFalse($restfulApi->validate()->passes()); 298 | $this->assertNotNull($restfulApi->validate()->getErrors()['fields']); 299 | $this->assertTrue($restfulApi->fieldSelector()->validate()->isInvalidInputType()); 300 | 301 | $this->assertSame([], $restfulApi->fieldSelector()->fields()); 302 | } 303 | 304 | /** 305 | * @throws AlchemistRestfulApiException 306 | */ 307 | public function test_FieldSelector_WithIntegerParamValue_must_False_validation(): void 308 | { 309 | $restfulApi = new AlchemistRestfulApi([ 310 | 'fields' => 123 311 | ]); 312 | 313 | $this->assertFalse($restfulApi->validate()->passes()); 314 | $this->assertNotNull($restfulApi->validate()->getErrors()['fields']); 315 | $this->assertTrue($restfulApi->fieldSelector()->validate()->isInvalidInputType()); 316 | 317 | $this->assertSame([], $restfulApi->fieldSelector()->fields()); 318 | } 319 | 320 | /** 321 | * @throws AlchemistRestfulApiException 322 | */ 323 | public function test_FieldSelector_WithNumericParamValue_must_False_validation(): void 324 | { 325 | $restfulApi = new AlchemistRestfulApi([ 326 | 'fields' => 123.1 327 | ]); 328 | 329 | $this->assertFalse($restfulApi->validate()->passes()); 330 | $this->assertNotNull($restfulApi->validate()->getErrors()['fields']); 331 | $this->assertTrue($restfulApi->fieldSelector()->validate()->isInvalidInputType()); 332 | 333 | $this->assertSame([], $restfulApi->fieldSelector()->fields()); 334 | } 335 | 336 | /** 337 | * @throws AlchemistRestfulApiException 338 | */ 339 | public function test_FieldSelector_WithTrueBooleanParamValue_must_False_validation(): void 340 | { 341 | $restfulApi = new AlchemistRestfulApi([ 342 | 'fields' => true 343 | ]); 344 | 345 | $this->assertFalse($restfulApi->validate()->passes()); 346 | $this->assertNotNull($restfulApi->validate()->getErrors()['fields']); 347 | $this->assertTrue($restfulApi->fieldSelector()->validate()->isInvalidInputType()); 348 | 349 | $this->assertSame([], $restfulApi->fieldSelector()->fields()); 350 | } 351 | 352 | /** 353 | * @throws AlchemistRestfulApiException 354 | */ 355 | public function test_FieldSelector_WithFalseBooleanParamValue_must_False_validation(): void 356 | { 357 | $restfulApi = new AlchemistRestfulApi([ 358 | 'fields' => false 359 | ]); 360 | 361 | $this->assertFalse($restfulApi->validate()->passes()); 362 | $this->assertNotNull($restfulApi->validate()->getErrors()['fields']); 363 | $this->assertTrue($restfulApi->fieldSelector()->validate()->isInvalidInputType()); 364 | 365 | $this->assertSame([], $restfulApi->fieldSelector()->fields()); 366 | } 367 | 368 | /** 369 | * @throws AlchemistRestfulApiException 370 | */ 371 | public function test_FieldSelector_when_SelectSubFieldsOnAtomicFields_must_False_validation(): void 372 | { 373 | $restfulApi = new AlchemistRestfulApi([ 374 | 'fields' => 'id{category}' 375 | ]); 376 | 377 | $restfulApi->fieldSelector() 378 | ->defineFieldStructure([ 379 | 'id', 380 | ]); 381 | 382 | $this->assertFalse($restfulApi->validate()->passes()); 383 | $this->assertNotNull($restfulApi->validate()->getErrors()['fields']); 384 | $this->assertTrue($restfulApi->fieldSelector()->validate()->hasUnselectableFields()); 385 | 386 | $this->assertSame(['id'], $restfulApi->fieldSelector()->flatFields()); 387 | 388 | //$this->expectException(AlchemistRestfulApiException::class); 389 | //$restfulApi->fieldSelector()->flatFields('$.id'); 390 | } 391 | } 392 | -------------------------------------------------------------------------------- /tests/Feature/ResourceSortTest.php: -------------------------------------------------------------------------------- 1 | 'id', 18 | 'direction' => 'asc', 19 | ]); 20 | 21 | $restfulApi->resourceSort()->defineSortableFields(['id']); 22 | 23 | $this->assertTrue($restfulApi->validate()->passes()); 24 | 25 | $this->assertSame('id', $restfulApi->resourceSort()->sort()->getSortField()); 26 | $this->assertSame('asc', $restfulApi->resourceSort()->sort()->getDirection()); 27 | } 28 | 29 | /** 30 | * @throws AlchemistRestfulApiException 31 | */ 32 | public function test_ResourceSort_in_NormalCase_without_DirectionValueParam_then_DefaultDirection_used(): void 33 | { 34 | $restfulApi = new AlchemistRestfulApi([ 35 | 'sort' => 'id', 36 | ]); 37 | 38 | $restfulApi->resourceSort()->defineSortableFields(['id']); 39 | 40 | $this->assertTrue($restfulApi->validate()->passes()); 41 | 42 | $this->assertSame('id', $restfulApi->resourceSort()->sort()->getSortField()); 43 | $this->assertSame('asc', $restfulApi->resourceSort()->sort()->getDirection()); 44 | } 45 | 46 | /** 47 | * @throws AlchemistRestfulApiException 48 | */ 49 | public function test_ResourceSort_with_DefaultSortField_then_DefaultSort_used(): void 50 | { 51 | $restfulApi = new AlchemistRestfulApi([]); 52 | 53 | $restfulApi->resourceSort()->defineDefaultSort('id'); 54 | 55 | $this->assertTrue($restfulApi->validate()->passes()); 56 | 57 | $this->assertSame('id', $restfulApi->resourceSort()->sort()->getSortField()); 58 | } 59 | 60 | /** 61 | * @throws AlchemistRestfulApiException 62 | */ 63 | public function test_ResourceSort_with_DefaultDirection_must_pass(): void 64 | { 65 | $restfulApi = new AlchemistRestfulApi([]); 66 | 67 | $restfulApi->resourceSort()->defineDefaultDirection('asc'); 68 | 69 | $this->assertTrue($restfulApi->validate()->passes()); 70 | 71 | $this->assertSame('', $restfulApi->resourceSort()->sort()->getSortField()); 72 | $this->assertSame('asc', $restfulApi->resourceSort()->sort()->getDirection()); 73 | } 74 | 75 | /** 76 | * @throws AlchemistRestfulApiException 77 | */ 78 | public function test_ResourceSort_with_NullSortParamValue_must_pass(): void 79 | { 80 | $restfulApi = new AlchemistRestfulApi([ 81 | 'sort' => null, 82 | 'direction' => null, 83 | ]); 84 | 85 | $restfulApi->resourceSort()->defineSortableFields(['id']); 86 | 87 | $this->assertTrue($restfulApi->validate()->passes()); 88 | 89 | $this->assertSame('', $restfulApi->resourceSort()->sort()->getSortField()); 90 | $this->assertSame('asc', $restfulApi->resourceSort()->sort()->getDirection()); 91 | } 92 | 93 | /** 94 | * @throws AlchemistRestfulApiException 95 | */ 96 | public function test_ResourceSort_with_ArraySortParamValue_must_FALSE_validation(): void 97 | { 98 | $restfulApi = new AlchemistRestfulApi([ 99 | 'sort' => ['id'], 100 | ]); 101 | 102 | $restfulApi->resourceSort()->defineSortableFields(['id']); 103 | 104 | $this->assertFalse($restfulApi->validate()->passes()); 105 | 106 | $this->assertNotNull($restfulApi->validate()->getErrors()['sort']); 107 | $this->assertTrue($restfulApi->resourceSort()->validate()->hasInvalidInputTypes()); 108 | 109 | $this->assertSame('', $restfulApi->resourceSort()->sort()->getSortField()); 110 | $this->assertSame('asc', $restfulApi->resourceSort()->sort()->getDirection()); 111 | } 112 | 113 | /** 114 | * @throws AlchemistRestfulApiException 115 | */ 116 | public function test_ResourceSort_with_ObjectSortParamValue_must_FALSE_validation(): void 117 | { 118 | $restfulApi = new AlchemistRestfulApi([ 119 | 'sort' => new \stdClass() 120 | ]); 121 | 122 | $this->assertFalse($restfulApi->validate()->passes()); 123 | 124 | $this->assertNotNull($restfulApi->validate()->getErrors()['sort']); 125 | $this->assertTrue($restfulApi->resourceSort()->validate()->hasInvalidInputTypes()); 126 | 127 | $this->assertSame('', $restfulApi->resourceSort()->sort()->getSortField()); 128 | $this->assertSame('asc', $restfulApi->resourceSort()->sort()->getDirection()); 129 | } 130 | 131 | /** 132 | * @throws AlchemistRestfulApiException 133 | */ 134 | public function test_ResourceSort_with_TrueBooleanSortParamValue_must_FALSE_validation(): void 135 | { 136 | $restfulApi = new AlchemistRestfulApi([ 137 | 'sort' => true 138 | ]); 139 | 140 | $restfulApi->resourceSort()->defineSortableFields(['id']); 141 | 142 | $this->assertFalse($restfulApi->validate()->passes()); 143 | 144 | $this->assertNotNull($restfulApi->validate()->getErrors()['sort']); 145 | $this->assertTrue($restfulApi->resourceSort()->validate()->hasInvalidInputTypes()); 146 | 147 | $this->assertSame('', $restfulApi->resourceSort()->sort()->getSortField()); 148 | $this->assertSame('asc', $restfulApi->resourceSort()->sort()->getDirection()); 149 | } 150 | 151 | /** 152 | * @throws AlchemistRestfulApiException 153 | */ 154 | public function test_ResourceSort_with_FalseBooleanSortParamValue_must_FALSE_validation(): void 155 | { 156 | $restfulApi = new AlchemistRestfulApi([ 157 | 'sort' => false 158 | ]); 159 | 160 | $restfulApi->resourceSort()->defineSortableFields(['id']); 161 | 162 | $this->assertFalse($restfulApi->validate()->passes()); 163 | 164 | $this->assertNotNull($restfulApi->validate()->getErrors()['sort']); 165 | $this->assertTrue($restfulApi->resourceSort()->validate()->hasInvalidInputTypes()); 166 | 167 | $this->assertSame('', $restfulApi->resourceSort()->sort()->getSortField()); 168 | $this->assertSame('asc', $restfulApi->resourceSort()->sort()->getDirection()); 169 | } 170 | 171 | /** 172 | * @throws AlchemistRestfulApiException 173 | */ 174 | public function test_ResourceSort_with_IntegerSortParamValue_must_FALSE_validation(): void 175 | { 176 | $restfulApi = new AlchemistRestfulApi([ 177 | 'sort' => 123 178 | ]); 179 | 180 | $this->assertFalse($restfulApi->validate()->passes()); 181 | 182 | $this->assertNotNull($restfulApi->validate()->getErrors()['sort']); 183 | $this->assertTrue($restfulApi->resourceSort()->validate()->hasInvalidInputTypes()); 184 | 185 | $this->assertSame('', $restfulApi->resourceSort()->sort()->getSortField()); 186 | $this->assertSame('asc', $restfulApi->resourceSort()->sort()->getDirection()); 187 | } 188 | 189 | /** 190 | * @throws AlchemistRestfulApiException 191 | */ 192 | public function test_ResourceSort_with_NumberSortParamValue_must_FALSE_validation(): void 193 | { 194 | $restfulApi = new AlchemistRestfulApi([ 195 | 'sort' => 123.1 196 | ]); 197 | 198 | $this->assertFalse($restfulApi->validate()->passes()); 199 | 200 | $this->assertNotNull($restfulApi->validate()->getErrors()['sort']); 201 | $this->assertTrue($restfulApi->resourceSort()->validate()->hasInvalidInputTypes()); 202 | 203 | $this->assertSame('', $restfulApi->resourceSort()->sort()->getSortField()); 204 | $this->assertSame('asc', $restfulApi->resourceSort()->sort()->getDirection()); 205 | } 206 | 207 | /** 208 | * @throws AlchemistRestfulApiException 209 | */ 210 | public function test_ResourceSort_with_ArrayDirectionParamValue_must_FALSE_validation(): void 211 | { 212 | $restfulApi = new AlchemistRestfulApi([ 213 | 'direction' => ['id'], 214 | ]); 215 | 216 | $restfulApi->resourceSort()->defineSortableFields(['id']); 217 | 218 | $this->assertFalse($restfulApi->validate()->passes()); 219 | $this->assertNotNull($restfulApi->validate()->getErrors()['sort']); 220 | $this->assertTrue($restfulApi->resourceSort()->validate()->hasInvalidInputTypes()); 221 | 222 | $this->assertSame('', $restfulApi->resourceSort()->sort()->getSortField()); 223 | $this->assertSame('asc', $restfulApi->resourceSort()->sort()->getDirection()); 224 | } 225 | 226 | /** 227 | * @throws AlchemistRestfulApiException 228 | */ 229 | public function test_ResourceSort_with_ObjectDirectionParamValue_must_FALSE_validation(): void 230 | { 231 | $restfulApi = new AlchemistRestfulApi([ 232 | 'direction' => new \stdClass() 233 | ]); 234 | 235 | $this->assertFalse($restfulApi->validate()->passes()); 236 | 237 | $this->assertNotNull($restfulApi->validate()->getErrors()['sort']); 238 | $this->assertTrue($restfulApi->resourceSort()->validate()->hasInvalidInputTypes()); 239 | 240 | $this->assertSame('', $restfulApi->resourceSort()->sort()->getSortField()); 241 | $this->assertSame('asc', $restfulApi->resourceSort()->sort()->getDirection()); 242 | } 243 | 244 | /** 245 | * @throws AlchemistRestfulApiException 246 | */ 247 | public function test_ResourceSort_with_TrueBooleanDirectionParamValue_must_FALSE_validation(): void 248 | { 249 | $restfulApi = new AlchemistRestfulApi([ 250 | 'direction' => true 251 | ]); 252 | 253 | $restfulApi->resourceSort()->defineSortableFields(['id']); 254 | 255 | $this->assertFalse($restfulApi->validate()->passes()); 256 | 257 | $this->assertNotNull($restfulApi->validate()->getErrors()['sort']); 258 | $this->assertTrue($restfulApi->resourceSort()->validate()->hasInvalidInputTypes()); 259 | 260 | $this->assertSame('', $restfulApi->resourceSort()->sort()->getSortField()); 261 | $this->assertSame('asc', $restfulApi->resourceSort()->sort()->getDirection()); 262 | } 263 | 264 | /** 265 | * @throws AlchemistRestfulApiException 266 | */ 267 | public function test_ResourceSort_with_FalseBooleanDirectionParamValue_must_FALSE_validation(): void 268 | { 269 | $restfulApi = new AlchemistRestfulApi([ 270 | 'direction' => false 271 | ]); 272 | 273 | $restfulApi->resourceSort()->defineSortableFields(['id']); 274 | 275 | $this->assertFalse($restfulApi->validate()->passes()); 276 | 277 | $this->assertNotNull($restfulApi->validate()->getErrors()['sort']); 278 | $this->assertTrue($restfulApi->resourceSort()->validate()->hasInvalidInputTypes()); 279 | 280 | $this->assertSame('', $restfulApi->resourceSort()->sort()->getSortField()); 281 | $this->assertSame('asc', $restfulApi->resourceSort()->sort()->getDirection()); 282 | } 283 | 284 | /** 285 | * @throws AlchemistRestfulApiException 286 | */ 287 | public function test_ResourceSort_with_IntegerDirectionParamValue_must_FALSE_validation(): void 288 | { 289 | $restfulApi = new AlchemistRestfulApi([ 290 | 'direction' => 123 291 | ]); 292 | 293 | $this->assertFalse($restfulApi->validate()->passes()); 294 | 295 | $this->assertNotNull($restfulApi->validate()->getErrors()['sort']); 296 | $this->assertTrue($restfulApi->resourceSort()->validate()->hasInvalidInputTypes()); 297 | 298 | $this->assertSame('', $restfulApi->resourceSort()->sort()->getSortField()); 299 | $this->assertSame('asc', $restfulApi->resourceSort()->sort()->getDirection()); 300 | } 301 | 302 | /** 303 | * @throws AlchemistRestfulApiException 304 | */ 305 | public function test_ResourceSort_with_NumericDirectionParamValue_must_FALSE_validation(): void 306 | { 307 | $restfulApi = new AlchemistRestfulApi([ 308 | 'direction' => 123.1 309 | ]); 310 | 311 | $this->assertFalse($restfulApi->validate()->passes()); 312 | 313 | $this->assertNotNull($restfulApi->validate()->getErrors()['sort']); 314 | $this->assertTrue($restfulApi->resourceSort()->validate()->hasInvalidInputTypes()); 315 | 316 | $this->assertSame('', $restfulApi->resourceSort()->sort()->getSortField()); 317 | $this->assertSame('asc', $restfulApi->resourceSort()->sort()->getDirection()); 318 | } 319 | 320 | /** 321 | * @throws AlchemistRestfulApiException 322 | */ 323 | public function test_ResourceSort_with_InvalidDirectionParamValue_must_FALSE_validation(): void 324 | { 325 | $restfulApi = new AlchemistRestfulApi([ 326 | 'direction' => 'not_asc_nor_desc', 327 | ]); 328 | 329 | $this->assertFalse($restfulApi->validate()->passes()); 330 | 331 | $this->assertNotNull($restfulApi->validate()->getErrors()['sort']); 332 | $this->assertTrue($restfulApi->resourceSort()->validate()->isInvalidDirection()); 333 | 334 | $this->assertSame('', $restfulApi->resourceSort()->sort()->getSortField()); 335 | $this->assertSame('not_asc_nor_desc', $restfulApi->resourceSort()->sort()->getDirection()); 336 | } 337 | 338 | /** 339 | * @throws AlchemistRestfulApiException 340 | */ 341 | public function test_ResourceSort_with_UndefinedSortField_must_FALSE_validation(): void 342 | { 343 | $restfulApi = new AlchemistRestfulApi([ 344 | 'sort' => 'qwerty', 345 | ]); 346 | 347 | $this->assertFalse($restfulApi->validate()->passes()); 348 | $this->assertNotNull($restfulApi->validate()->getErrors()['sort']); 349 | $this->assertTrue($restfulApi->resourceSort()->validate()->isInvalidSortField()); 350 | 351 | $this->assertSame('qwerty', $restfulApi->resourceSort()->sort()->getSortField()); 352 | $this->assertSame('asc', $restfulApi->resourceSort()->sort()->getDirection()); 353 | } 354 | 355 | /** 356 | * @throws AlchemistRestfulApiException 357 | */ 358 | public function test_ResourceSort_with_FieldThatIsNotInSortFields_must_FALSE_validation(): void 359 | { 360 | $restfulApi = new AlchemistRestfulApi([ 361 | 'sort' => 'not_in_sort_fields', 362 | ]); 363 | 364 | $restfulApi->resourceSort()->defineSortableFields([ 365 | 'id', 366 | 'name', 367 | ]); 368 | 369 | $this->assertFalse($restfulApi->validate()->passes()); 370 | $this->assertNotNull($restfulApi->validate()->getErrors()['sort']); 371 | $this->assertTrue($restfulApi->resourceSort()->validate()->isInvalidSortField()); 372 | 373 | $this->assertSame('not_in_sort_fields', $restfulApi->resourceSort()->sort()->getSortField()); 374 | $this->assertSame('asc', $restfulApi->resourceSort()->sort()->getDirection()); 375 | } 376 | } 377 | --------------------------------------------------------------------------------