├── 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 |
--------------------------------------------------------------------------------