├── .editorconfig ├── .github ├── stale.yml └── workflows │ ├── .editorconfig │ ├── run-tests.yml │ ├── static.yml │ └── style-checker.yml ├── LICENSE ├── composer.json ├── phpcs.xml.dist ├── phpstan-baseline.neon ├── phpstan.neon.dist ├── psalm.baseline.xml ├── psalm.xml └── src ├── Manager.php ├── Pagination ├── Cursor.php ├── CursorInterface.php ├── DoctrinePaginatorAdapter.php ├── IlluminatePaginatorAdapter.php ├── LaminasPaginatorAdapter.php ├── PagerfantaPaginatorAdapter.php ├── PaginatorCountTrait.php ├── PaginatorInterface.php ├── PhalconFrameworkPaginatorAdapter.php └── ZendFrameworkPaginatorAdapter.php ├── ParamBag.php ├── Resource ├── Collection.php ├── Item.php ├── NullResource.php ├── Primitive.php ├── ResourceAbstract.php └── ResourceInterface.php ├── Scope.php ├── ScopeFactory.php ├── ScopeFactoryInterface.php ├── Serializer ├── ArraySerializer.php ├── DataArraySerializer.php ├── JsonApiSerializer.php ├── Serializer.php └── SerializerAbstract.php └── TransformerAbstract.php /.editorconfig: -------------------------------------------------------------------------------- 1 | root = true 2 | 3 | [*] 4 | charset = utf-8 5 | end_of_line = lf 6 | indent_size = 4 7 | indent_style = space 8 | insert_final_newline = true 9 | trim_trailing_whitespace = true 10 | -------------------------------------------------------------------------------- /.github/stale.yml: -------------------------------------------------------------------------------- 1 | daysUntilStale: 180 2 | daysUntilClose: 28 3 | exemptLabels: 4 | - keep-open 5 | # Label to use when marking an issue as stale 6 | staleLabel: stale 7 | # Comment to post when marking an issue as stale. Set to `false` to disable 8 | markComment: > 9 | This issue has been automatically marked as stale because it has not had 10 | recent activity. It will be closed after 4 weeks if no further activity occurs. Thank you 11 | for your contributions. 12 | # Comment to post when closing a stale issue. Set to `false` to disable 13 | closeComment: false 14 | -------------------------------------------------------------------------------- /.github/workflows/.editorconfig: -------------------------------------------------------------------------------- 1 | [*.yml] 2 | indent_size = 2 3 | -------------------------------------------------------------------------------- /.github/workflows/run-tests.yml: -------------------------------------------------------------------------------- 1 | name: The PHP League Tests 2 | 3 | on: [push, pull_request] 4 | 5 | jobs: 6 | ci: 7 | runs-on: ${{ matrix.os }} 8 | strategy: 9 | fail-fast: true 10 | matrix: 11 | os: [ubuntu-20.04] 12 | php: [7.4, 8.0, 8.1, 8.2, 8.3, 8.4] 13 | 14 | name: League - PHP ${{ matrix.php }} on ${{ matrix.os }} 15 | 16 | steps: 17 | 18 | - name: Checkout code 19 | uses: actions/checkout@v2 20 | 21 | - name: Setup PHP 22 | uses: shivammathur/setup-php@v2 23 | with: 24 | php-version: ${{ matrix.php }} 25 | tools: phpcs, phpunit 26 | 27 | - name: Download dependencies 28 | uses: ramsey/composer-install@v2 29 | 30 | - name: Run Tests 31 | run: vendor/bin/phpunit 32 | -------------------------------------------------------------------------------- /.github/workflows/static.yml: -------------------------------------------------------------------------------- 1 | on: [ pull_request ] 2 | name: Static analysis 3 | 4 | jobs: 5 | phpstan: 6 | name: PHPStan 7 | runs-on: ubuntu-20.04 8 | 9 | steps: 10 | - name: Checkout code 11 | uses: actions/checkout@v2 12 | 13 | - name: Setup PHP 14 | uses: shivammathur/setup-php@v2 15 | with: 16 | php-version: 8.1 17 | extensions: apcu, redis 18 | coverage: none 19 | tools: phpstan:1.4.6, cs2pr 20 | 21 | - name: Download dependencies 22 | uses: ramsey/composer-install@v1 23 | 24 | - name: PHPStan 25 | run: phpstan analyze --no-progress --error-format=checkstyle | cs2pr 26 | 27 | psalm: 28 | name: Psalm 29 | runs-on: ubuntu-20.04 30 | steps: 31 | - name: Checkout code 32 | uses: actions/checkout@v2 33 | 34 | - name: Setup PHP 35 | uses: shivammathur/setup-php@v2 36 | with: 37 | php-version: 8.1 38 | extensions: apcu, redis 39 | coverage: none 40 | tools: vimeo/psalm:4.30.0 41 | 42 | - name: Download dependencies 43 | uses: ramsey/composer-install@v1 44 | 45 | - name: Psalm 46 | run: psalm --no-progress --output-format=github 47 | -------------------------------------------------------------------------------- /.github/workflows/style-checker.yml: -------------------------------------------------------------------------------- 1 | name: The PHP League Style Checks 2 | 3 | on: [push, pull_request] 4 | 5 | jobs: 6 | style: 7 | runs-on: ubuntu-latest 8 | name: The PHP League Style Checks 9 | steps: 10 | 11 | - name: Checkout Code 12 | uses: actions/checkout@v2 13 | 14 | - name: Setup PHP 15 | uses: shivammathur/setup-php@v2 16 | with: 17 | php-version: '7.4' 18 | 19 | - name: Cache Dependencies 20 | id: composer-cache-style 21 | uses: actions/cache@v2 22 | with: 23 | path: vendor 24 | key: ubuntu-composer-cache-style-${{ hashFiles('**/composer.lock') }} 25 | restore-keys: ubuntu-php-style 26 | 27 | - name: Install Dependencies 28 | if: steps.composer-cache.outputs.cache-hit != 'true' 29 | run: composer install --prefer-dist --no-progress --no-suggest 30 | 31 | - name: Check Coding Style 32 | run: vendor/bin/phpcs src/ -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2013 Phil Sturgeon 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 13 | all 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 21 | THE SOFTWARE. 22 | -------------------------------------------------------------------------------- /composer.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "league/fractal", 3 | "description": "Handle the output of complex data structures ready for API output.", 4 | "keywords": [ 5 | "league", 6 | "api", 7 | "json", 8 | "rest" 9 | ], 10 | "homepage": "http://fractal.thephpleague.com/", 11 | "license": "MIT", 12 | "authors": [ 13 | { 14 | "name": "Phil Sturgeon", 15 | "email": "me@philsturgeon.uk", 16 | "homepage": "http://philsturgeon.uk/", 17 | "role": "Developer" 18 | } 19 | ], 20 | "config": { 21 | "sort-packages": true 22 | }, 23 | "require": { 24 | "php": ">=7.4" 25 | }, 26 | "require-dev": { 27 | "doctrine/orm": "^2.5", 28 | "illuminate/contracts": "~5.0", 29 | "laminas/laminas-paginator": "~2.12", 30 | "mockery/mockery": "^1.3", 31 | "pagerfanta/pagerfanta": "~1.0.0|~4.0.0", 32 | "phpstan/phpstan": "^1.4", 33 | "phpunit/phpunit": "^9.5", 34 | "squizlabs/php_codesniffer": "~3.4", 35 | "vimeo/psalm": "^4.30" 36 | }, 37 | "suggest": { 38 | "illuminate/pagination": "The Illuminate Pagination component.", 39 | "pagerfanta/pagerfanta": "Pagerfanta Paginator", 40 | "laminas/laminas-paginator": "Laminas Framework Paginator" 41 | }, 42 | "autoload": { 43 | "psr-4": { 44 | "League\\Fractal\\": "src" 45 | } 46 | }, 47 | "autoload-dev": { 48 | "psr-4": { 49 | "League\\Fractal\\Test\\": "test" 50 | } 51 | }, 52 | "scripts": { 53 | "check": "vendor/bin/phpcs src/", 54 | "test": "vendor/bin/phpunit --testdox --colors=always", 55 | "test:coverage": "vendor/bin/phpunit --coverage-html build/coverage" 56 | }, 57 | "extra": { 58 | "branch-alias": { 59 | "dev-master": "0.20.x-dev" 60 | } 61 | } 62 | } 63 | -------------------------------------------------------------------------------- /phpcs.xml.dist: -------------------------------------------------------------------------------- 1 | 2 | 3 | Fractal Coding Standards 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | -------------------------------------------------------------------------------- /phpstan-baseline.neon: -------------------------------------------------------------------------------- 1 | parameters: 2 | ignoreErrors: 3 | - 4 | message: "#^Parameter \\#1 \\$separator of function explode expects non\\-empty\\-string, string given\\.$#" 5 | count: 1 6 | path: src/Manager.php 7 | 8 | - 9 | message: "#^Call to an undefined method Illuminate\\\\Contracts\\\\Pagination\\\\LengthAwarePaginator\\:\\:count\\(\\)\\.$#" 10 | count: 1 11 | path: src/Pagination/IlluminatePaginatorAdapter.php 12 | 13 | - 14 | message: "#^Call to an undefined method League\\\\Fractal\\\\Resource\\\\ResourceInterface\\:\\:getMeta\\(\\)\\.$#" 15 | count: 1 16 | path: src/Scope.php 17 | 18 | - 19 | message: "#^Call to an undefined method League\\\\Fractal\\\\TransformerAbstract\\:\\:transform\\(\\)\\.$#" 20 | count: 2 21 | path: src/Scope.php 22 | 23 | - 24 | message: "#^Call to function is_null\\(\\) with array will always evaluate to false\\.$#" 25 | count: 1 26 | path: src/Scope.php 27 | 28 | - 29 | message: "#^PHPDoc tag @param has invalid value \\(\\$includeKey\\)\\: Unexpected token \"\\$includeKey\", expected type at offset 18$#" 30 | count: 2 31 | path: src/Serializer/JsonApiSerializer.php 32 | 33 | - 34 | message: "#^PHPDoc tag @param has invalid value \\(\\$includeObject\\)\\: Unexpected token \"\\$includeObject\", expected type at offset 18$#" 35 | count: 1 36 | path: src/Serializer/JsonApiSerializer.php 37 | 38 | - 39 | message: "#^PHPDoc tag @param has invalid value \\(\\$includeObject\\)\\: Unexpected token \"\\$includeObject\", expected type at offset 73$#" 40 | count: 1 41 | path: src/Serializer/JsonApiSerializer.php 42 | 43 | - 44 | message: "#^PHPDoc tag @param has invalid value \\(\\$includeObjects\\)\\: Unexpected token \"\\$includeObjects\", expected type at offset 18$#" 45 | count: 1 46 | path: src/Serializer/JsonApiSerializer.php 47 | 48 | - 49 | message: "#^PHPDoc tag @param has invalid value \\(\\$key\\)\\: Unexpected token \"\\$key\", expected type at offset 102$#" 50 | count: 1 51 | path: src/Serializer/JsonApiSerializer.php 52 | 53 | - 54 | message: "#^PHPDoc tag @param has invalid value \\(\\$linkedIds\\)\\: Unexpected token \"\\$linkedIds\", expected type at offset 48$#" 55 | count: 1 56 | path: src/Serializer/JsonApiSerializer.php 57 | 58 | - 59 | message: "#^PHPDoc tag @param has invalid value \\(\\$relationship\\)\\: Unexpected token \"\\$relationship\", expected type at offset 47$#" 60 | count: 1 61 | path: src/Serializer/JsonApiSerializer.php 62 | 63 | - 64 | message: "#^PHPDoc tag @param has invalid value \\(\\$relationships\\)\\: Unexpected token \"\\$relationships\", expected type at offset 44$#" 65 | count: 2 66 | path: src/Serializer/JsonApiSerializer.php 67 | 68 | - 69 | message: "#^PHPDoc tag @param has invalid value \\(\\$serializedData\\)\\: Unexpected token \"\\$serializedData\", expected type at offset 73$#" 70 | count: 1 71 | path: src/Serializer/JsonApiSerializer.php 72 | 73 | -------------------------------------------------------------------------------- /phpstan.neon.dist: -------------------------------------------------------------------------------- 1 | includes: 2 | - ./phpstan-baseline.neon 3 | 4 | parameters: 5 | level: 5 6 | reportUnmatchedIgnoredErrors: false 7 | paths: 8 | - src 9 | bootstrapFiles: 10 | - test/phpstan.php 11 | -------------------------------------------------------------------------------- /psalm.baseline.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | int 6 | 7 | 8 | $this->paginator->getQuery()->getMaxResults() 9 | 10 | 11 | 12 | 13 | count 14 | 15 | 16 | 17 | 18 | getMeta 19 | 20 | 21 | transform 22 | 23 | 24 | 25 | -------------------------------------------------------------------------------- /psalm.xml: -------------------------------------------------------------------------------- 1 | 2 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | -------------------------------------------------------------------------------- /src/Manager.php: -------------------------------------------------------------------------------- 1 | 7 | * 8 | * For the full copyright and license information, please view the LICENSE 9 | * file that was distributed with this source code. 10 | */ 11 | 12 | namespace League\Fractal; 13 | 14 | use League\Fractal\Resource\ResourceInterface; 15 | use League\Fractal\Serializer\DataArraySerializer; 16 | use League\Fractal\Serializer\Serializer; 17 | 18 | /** 19 | * Manager 20 | * 21 | * Not a wildly creative name, but the manager is what a Fractal user will interact 22 | * with the most. The manager has various configurable options, and allows users 23 | * to create the "root scope" easily. 24 | */ 25 | class Manager 26 | { 27 | /** 28 | * Array of scope identifiers for resources to include. 29 | */ 30 | protected array $requestedIncludes = []; 31 | 32 | /** 33 | * Array of scope identifiers for resources to exclude. 34 | */ 35 | protected array $requestedExcludes = []; 36 | 37 | /** 38 | * Array of requested fieldsets. 39 | */ 40 | protected array $requestedFieldsets = []; 41 | 42 | /** 43 | * Array containing modifiers as keys and an array value of params. 44 | */ 45 | protected array $includeParams = []; 46 | 47 | /** 48 | * The character used to separate modifier parameters. 49 | */ 50 | protected string $paramDelimiter = '|'; 51 | 52 | /** 53 | * Upper limit to how many levels of included data are allowed. 54 | */ 55 | protected int $recursionLimit = 10; 56 | 57 | protected ?Serializer $serializer = null; 58 | 59 | /** 60 | * Factory used to create new configured scopes. 61 | */ 62 | private ScopeFactoryInterface $scopeFactory; 63 | 64 | public function __construct(?ScopeFactoryInterface $scopeFactory = null) 65 | { 66 | $this->scopeFactory = $scopeFactory ?: new ScopeFactory(); 67 | } 68 | 69 | /** 70 | * Main method to kick this all off. Make a resource then pass it over, and use toArray() 71 | */ 72 | public function createData( 73 | ResourceInterface $resource, 74 | ?string $scopeIdentifier = null, 75 | ?Scope $parentScopeInstance = null 76 | ): Scope { 77 | if ($parentScopeInstance !== null) { 78 | return $this->scopeFactory->createChildScopeFor($this, $parentScopeInstance, $resource, $scopeIdentifier); 79 | } 80 | 81 | return $this->scopeFactory->createScopeFor($this, $resource, $scopeIdentifier); 82 | } 83 | 84 | public function getIncludeParams(string $include): ParamBag 85 | { 86 | $params = isset($this->includeParams[$include]) ? $this->includeParams[$include] : []; 87 | 88 | return new ParamBag($params); 89 | } 90 | 91 | public function getRequestedIncludes(): array 92 | { 93 | return $this->requestedIncludes; 94 | } 95 | 96 | public function getRequestedExcludes(): array 97 | { 98 | return $this->requestedExcludes; 99 | } 100 | 101 | public function getSerializer(): Serializer 102 | { 103 | if (! $this->serializer) { 104 | $this->serializer = new DataArraySerializer(); 105 | } 106 | 107 | return $this->serializer; 108 | } 109 | 110 | /** 111 | * @param array|string $includes Array or csv string of resources to include 112 | */ 113 | public function parseIncludes($includes): self 114 | { 115 | // Wipe these before we go again 116 | $this->requestedIncludes = $this->includeParams = []; 117 | $subRelations = ''; 118 | 119 | if (is_string($includes)) { 120 | $includes = explode(',', $includes); 121 | } 122 | 123 | if (! is_array($includes)) { 124 | throw new \InvalidArgumentException( 125 | 'The parseIncludes() method expects a string or an array. '.gettype($includes).' given' 126 | ); 127 | } 128 | 129 | foreach ($includes as $include) { 130 | list($includeName, $allModifiersStr) = array_pad(explode(':', $include, 2), 2, ''); 131 | $a = $allModifiersStr ? explode('.', $allModifiersStr, 2) : ['']; 132 | list($allModifiersStr, $subRelations) = array_pad($a, 2, null); 133 | 134 | // Trim it down to a cool level of recursion 135 | $includeName = $this->trimToAcceptableRecursionLevel($includeName); 136 | 137 | if (in_array($includeName, $this->requestedIncludes)) { 138 | continue; 139 | } 140 | $this->requestedIncludes[] = $includeName; 141 | 142 | // No Params? Bored 143 | if ($allModifiersStr === null) { 144 | continue; 145 | } 146 | 147 | // Matches multiple instances of 'something(foo|bar|baz)' in the string 148 | // I guess it ignores : so you could use anything, but probably don't do that 149 | preg_match_all('/([\w]+)(\(([^\)]+)\))?/', $allModifiersStr, $allModifiersArr); 150 | 151 | // [0] is full matched strings... 152 | $modifierCount = count($allModifiersArr[0]); 153 | 154 | $modifierArr = []; 155 | 156 | for ($modifierIt = 0; $modifierIt < $modifierCount; $modifierIt++) { 157 | // [1] is the modifier 158 | $modifierName = $allModifiersArr[1][$modifierIt]; 159 | 160 | // and [3] is delimited params 161 | $modifierParamStr = $allModifiersArr[3][$modifierIt]; 162 | 163 | // Make modifier array key with an array of params as the value 164 | $modifierArr[$modifierName] = explode($this->paramDelimiter, $modifierParamStr); 165 | } 166 | 167 | $this->includeParams[$includeName] = $modifierArr; 168 | 169 | if ($subRelations) { 170 | $this->requestedIncludes[] = $this->trimToAcceptableRecursionLevel($includeName . '.' . $subRelations); 171 | } 172 | } 173 | 174 | // This should be optional and public someday, but without it includes would never show up 175 | $this->autoIncludeParents(); 176 | 177 | return $this; 178 | } 179 | 180 | /** 181 | * Parse field parameter. 182 | * 183 | * @param array $fieldsets Array of fields to include. It must be an array whose keys 184 | * are resource types and values an array or a string 185 | * of the fields to return, separated by a comma 186 | */ 187 | public function parseFieldsets(array $fieldsets): self 188 | { 189 | $this->requestedFieldsets = []; 190 | foreach ($fieldsets as $type => $fields) { 191 | if (is_string($fields)) { 192 | $fields = explode(',', $fields); 193 | } 194 | 195 | //Remove empty and repeated fields 196 | $this->requestedFieldsets[$type] = array_unique(array_filter($fields)); 197 | } 198 | return $this; 199 | } 200 | public function getRequestedFieldsets(): array 201 | { 202 | return $this->requestedFieldsets; 203 | } 204 | 205 | /** 206 | * Get fieldset params for the specified type. 207 | */ 208 | public function getFieldset(string $type): ?ParamBag 209 | { 210 | return !isset($this->requestedFieldsets[$type]) ? 211 | null : 212 | new ParamBag($this->requestedFieldsets[$type]); 213 | } 214 | 215 | /** 216 | * @param array|string $excludes Array or csv string of resources to exclude 217 | */ 218 | public function parseExcludes($excludes): self 219 | { 220 | $this->requestedExcludes = []; 221 | 222 | if (is_string($excludes)) { 223 | $excludes = explode(',', $excludes); 224 | } 225 | 226 | if (! is_array($excludes)) { 227 | throw new \InvalidArgumentException( 228 | 'The parseExcludes() method expects a string or an array. '.gettype($excludes).' given' 229 | ); 230 | } 231 | 232 | foreach ($excludes as $excludeName) { 233 | $excludeName = $this->trimToAcceptableRecursionLevel($excludeName); 234 | 235 | if (in_array($excludeName, $this->requestedExcludes)) { 236 | continue; 237 | } 238 | 239 | $this->requestedExcludes[] = $excludeName; 240 | } 241 | 242 | return $this; 243 | } 244 | 245 | public function setRecursionLimit(int $recursionLimit): self 246 | { 247 | $this->recursionLimit = $recursionLimit; 248 | 249 | return $this; 250 | } 251 | 252 | public function setSerializer(Serializer $serializer): self 253 | { 254 | $this->serializer = $serializer; 255 | 256 | return $this; 257 | } 258 | 259 | /** 260 | * Look at the requested includes and automatically include the parents if they 261 | * are not explicitly requested. E.g: [foo, bar.baz] becomes [foo, bar, bar.baz] 262 | * 263 | * @internal 264 | */ 265 | protected function autoIncludeParents(): void 266 | { 267 | $parsed = []; 268 | 269 | foreach ($this->requestedIncludes as $include) { 270 | $nested = explode('.', $include); 271 | 272 | $part = array_shift($nested); 273 | $parsed[] = $part; 274 | 275 | while (count($nested) > 0) { 276 | $part .= '.'.array_shift($nested); 277 | $parsed[] = $part; 278 | } 279 | } 280 | 281 | $this->requestedIncludes = array_values(array_unique($parsed)); 282 | } 283 | 284 | /** 285 | * Strip off any requested resources that are too many levels deep, to avoid DiCaprio being chased 286 | * by trains or whatever the hell that movie was about. 287 | * 288 | * @internal 289 | */ 290 | protected function trimToAcceptableRecursionLevel(string $includeName): string 291 | { 292 | return implode('.', array_slice(explode('.', $includeName), 0, $this->recursionLimit)); 293 | } 294 | } 295 | -------------------------------------------------------------------------------- /src/Pagination/Cursor.php: -------------------------------------------------------------------------------- 1 | 7 | * 8 | * For the full copyright and license information, please view the LICENSE 9 | * file that was distributed with this source code. 10 | */ 11 | 12 | namespace League\Fractal\Pagination; 13 | 14 | /** 15 | * A generic cursor adapter. 16 | * 17 | * @author Isern Palaus 18 | * @author Michele Massari 19 | */ 20 | class Cursor implements CursorInterface 21 | { 22 | /** 23 | * Current cursor value. 24 | * 25 | * @var mixed 26 | */ 27 | protected $current; 28 | 29 | /** 30 | * Previous cursor value. 31 | * 32 | * @var mixed 33 | */ 34 | protected $prev; 35 | 36 | /** 37 | * Next cursor value. 38 | * 39 | * @var mixed 40 | */ 41 | protected $next; 42 | 43 | /** 44 | * Items being held for the current cursor position. 45 | */ 46 | protected ?int $count; 47 | 48 | /** 49 | * Create a new Cursor instance. 50 | * 51 | * @param mixed $current 52 | * @param mixed $prev 53 | * @param mixed $next 54 | */ 55 | public function __construct($current = null, $prev = null, $next = null, ?int $count = null) 56 | { 57 | $this->current = $current; 58 | $this->prev = $prev; 59 | $this->next = $next; 60 | $this->count = $count; 61 | } 62 | 63 | /** 64 | * {@inheritDoc} 65 | */ 66 | #[\ReturnTypeWillChange] 67 | public function getCurrent() 68 | { 69 | return $this->current; 70 | } 71 | 72 | /** 73 | * Set the current cursor value. 74 | * 75 | * @param mixed $current 76 | */ 77 | public function setCurrent($current): self 78 | { 79 | $this->current = $current; 80 | 81 | return $this; 82 | } 83 | 84 | /** 85 | * {@inheritDoc} 86 | */ 87 | #[\ReturnTypeWillChange] 88 | public function getPrev() 89 | { 90 | return $this->prev; 91 | } 92 | 93 | /** 94 | * Set the prev cursor value. 95 | * 96 | * @param mixed $prev 97 | */ 98 | public function setPrev($prev): self 99 | { 100 | $this->prev = $prev; 101 | 102 | return $this; 103 | } 104 | 105 | /** 106 | * {@inheritDoc} 107 | */ 108 | #[\ReturnTypeWillChange] 109 | public function getNext() 110 | { 111 | return $this->next; 112 | } 113 | 114 | /** 115 | * Set the next cursor value. 116 | * 117 | * @param mixed $next 118 | */ 119 | public function setNext($next): self 120 | { 121 | $this->next = $next; 122 | 123 | return $this; 124 | } 125 | 126 | /** 127 | * {@inheritDoc} 128 | */ 129 | public function getCount(): ?int 130 | { 131 | return $this->count; 132 | } 133 | 134 | /** 135 | * Set the total items in the current cursor. 136 | */ 137 | public function setCount(int $count): self 138 | { 139 | $this->count = $count; 140 | 141 | return $this; 142 | } 143 | } 144 | -------------------------------------------------------------------------------- /src/Pagination/CursorInterface.php: -------------------------------------------------------------------------------- 1 | 7 | * 8 | * For the full copyright and license information, please view the LICENSE 9 | * file that was distributed with this source code. 10 | */ 11 | 12 | namespace League\Fractal\Pagination; 13 | 14 | /** 15 | * A common interface for cursors to use. 16 | * 17 | * @author Isern Palaus 18 | */ 19 | interface CursorInterface 20 | { 21 | /** 22 | * Get the current cursor value. 23 | * 24 | * @return mixed 25 | */ 26 | #[\ReturnTypeWillChange] 27 | public function getCurrent(); 28 | 29 | /** 30 | * Get the prev cursor value. 31 | * 32 | * @return mixed 33 | */ 34 | #[\ReturnTypeWillChange] 35 | public function getPrev(); 36 | 37 | /** 38 | * Get the next cursor value. 39 | * 40 | * @return mixed 41 | */ 42 | #[\ReturnTypeWillChange] 43 | public function getNext(); 44 | 45 | /** 46 | * Returns the total items in the current cursor. 47 | */ 48 | public function getCount(): ?int; 49 | } 50 | -------------------------------------------------------------------------------- /src/Pagination/DoctrinePaginatorAdapter.php: -------------------------------------------------------------------------------- 1 | 6 | * 7 | * For the full copyright and license information, please view the LICENSE 8 | * file that was distributed with this source code. 9 | */ 10 | 11 | namespace League\Fractal\Pagination; 12 | 13 | use Doctrine\ORM\Tools\Pagination\Paginator; 14 | 15 | /** 16 | * A paginator adapter for doctrine pagination. 17 | * 18 | * @author Fraser Stockley 19 | */ 20 | class DoctrinePaginatorAdapter implements PaginatorInterface 21 | { 22 | use PaginatorCountTrait; 23 | 24 | /** 25 | * The paginator instance. 26 | * @var Paginator 27 | */ 28 | private $paginator; 29 | 30 | /** 31 | * The route generator. 32 | * 33 | * @var callable 34 | */ 35 | private $routeGenerator; 36 | 37 | /** 38 | * Create a new DoctrinePaginatorAdapter. 39 | * @param Paginator $paginator 40 | * @param callable $routeGenerator 41 | * 42 | */ 43 | public function __construct(Paginator $paginator, callable $routeGenerator) 44 | { 45 | $this->paginator = $paginator; 46 | $this->routeGenerator = $routeGenerator; 47 | } 48 | 49 | /** 50 | * {@inheritDoc} 51 | */ 52 | public function getCurrentPage(): int 53 | { 54 | return (int) ($this->paginator->getQuery()->getFirstResult() / $this->paginator->getQuery()->getMaxResults()) + 1; 55 | } 56 | 57 | /** 58 | * {@inheritDoc} 59 | */ 60 | public function getLastPage(): int 61 | { 62 | return (int) ceil($this->getTotal() / $this->paginator->getQuery()->getMaxResults()); 63 | } 64 | 65 | /** 66 | * {@inheritDoc} 67 | */ 68 | public function getTotal(): int 69 | { 70 | return count($this->paginator); 71 | } 72 | 73 | /** 74 | * {@inheritDoc} 75 | */ 76 | public function getCount(): int 77 | { 78 | return $this->getTraversableCount($this->paginator->getIterator()); 79 | } 80 | 81 | /** 82 | * {@inheritDoc} 83 | */ 84 | public function getPerPage(): int 85 | { 86 | return $this->paginator->getQuery()->getMaxResults(); 87 | } 88 | 89 | /** 90 | * {@inheritDoc} 91 | */ 92 | public function getUrl(int $page): string 93 | { 94 | return call_user_func($this->getRouteGenerator(), $page); 95 | } 96 | 97 | /** 98 | * Get the route generator. 99 | */ 100 | private function getRouteGenerator(): callable 101 | { 102 | return $this->routeGenerator; 103 | } 104 | } 105 | -------------------------------------------------------------------------------- /src/Pagination/IlluminatePaginatorAdapter.php: -------------------------------------------------------------------------------- 1 | 7 | * 8 | * For the full copyright and license information, please view the LICENSE 9 | * file that was distributed with this source code. 10 | */ 11 | 12 | namespace League\Fractal\Pagination; 13 | 14 | use Illuminate\Contracts\Pagination\LengthAwarePaginator; 15 | 16 | /** 17 | * A paginator adapter for illuminate/pagination. 18 | * 19 | * @author Maxime Beaudoin 20 | * @author Marc Addeo 21 | */ 22 | class IlluminatePaginatorAdapter implements PaginatorInterface 23 | { 24 | protected LengthAwarePaginator $paginator; 25 | 26 | /** 27 | * Create a new illuminate pagination adapter. 28 | */ 29 | public function __construct(LengthAwarePaginator $paginator) 30 | { 31 | $this->paginator = $paginator; 32 | } 33 | 34 | /** 35 | * {@inheritDoc} 36 | */ 37 | public function getCurrentPage(): int 38 | { 39 | return $this->paginator->currentPage(); 40 | } 41 | 42 | /** 43 | * {@inheritDoc} 44 | */ 45 | public function getLastPage(): int 46 | { 47 | return $this->paginator->lastPage(); 48 | } 49 | 50 | /** 51 | * {@inheritDoc} 52 | */ 53 | public function getTotal(): int 54 | { 55 | return $this->paginator->total(); 56 | } 57 | 58 | /** 59 | * {@inheritDoc} 60 | */ 61 | public function getCount(): int 62 | { 63 | return $this->paginator->count(); 64 | } 65 | 66 | /** 67 | * {@inheritDoc} 68 | */ 69 | public function getPerPage(): int 70 | { 71 | return $this->paginator->perPage(); 72 | } 73 | 74 | /** 75 | * {@inheritDoc} 76 | */ 77 | public function getUrl(int $page): string 78 | { 79 | return $this->paginator->url($page); 80 | } 81 | 82 | /** 83 | * Get the paginator instance. 84 | */ 85 | public function getPaginator(): LengthAwarePaginator 86 | { 87 | return $this->paginator; 88 | } 89 | } 90 | -------------------------------------------------------------------------------- /src/Pagination/LaminasPaginatorAdapter.php: -------------------------------------------------------------------------------- 1 | 7 | * 8 | * For the full copyright and license information, please view the LICENSE 9 | * file that was distributed with this source code. 10 | */ 11 | 12 | namespace League\Fractal\Pagination; 13 | 14 | use Laminas\Paginator\Paginator; 15 | 16 | /** 17 | * A paginator adapter for laminas/laminas-paginator. 18 | * 19 | * @author Abdul Malik Ikhsan 20 | */ 21 | class LaminasPaginatorAdapter implements PaginatorInterface 22 | { 23 | protected Paginator $paginator; 24 | 25 | /** 26 | * The route generator. 27 | * 28 | * @var callable 29 | */ 30 | protected $routeGenerator; 31 | 32 | public function __construct(Paginator $paginator, callable $routeGenerator) 33 | { 34 | $this->paginator = $paginator; 35 | $this->routeGenerator = $routeGenerator; 36 | } 37 | 38 | /** 39 | * {@inheritDoc} 40 | */ 41 | public function getCurrentPage(): int 42 | { 43 | return $this->paginator->getCurrentPageNumber(); 44 | } 45 | 46 | /** 47 | * {@inheritDoc} 48 | */ 49 | public function getLastPage(): int 50 | { 51 | return $this->paginator->count(); 52 | } 53 | 54 | /** 55 | * {@inheritDoc} 56 | */ 57 | public function getTotal(): int 58 | { 59 | return $this->paginator->getTotalItemCount(); 60 | } 61 | 62 | /** 63 | * {@inheritDoc} 64 | */ 65 | public function getCount(): int 66 | { 67 | return $this->paginator->getCurrentItemCount(); 68 | } 69 | 70 | /** 71 | * {@inheritDoc} 72 | */ 73 | public function getPerPage(): int 74 | { 75 | return $this->paginator->getItemCountPerPage(); 76 | } 77 | 78 | /** 79 | * {@inheritDoc} 80 | */ 81 | public function getUrl(int $page): string 82 | { 83 | return call_user_func($this->routeGenerator, $page); 84 | } 85 | 86 | public function getPaginator(): Paginator 87 | { 88 | return $this->paginator; 89 | } 90 | 91 | public function getRouteGenerator(): callable 92 | { 93 | return $this->routeGenerator; 94 | } 95 | } 96 | -------------------------------------------------------------------------------- /src/Pagination/PagerfantaPaginatorAdapter.php: -------------------------------------------------------------------------------- 1 | 7 | * 8 | * For the full copyright and license information, please view the LICENSE 9 | * file that was distributed with this source code. 10 | */ 11 | 12 | namespace League\Fractal\Pagination; 13 | 14 | use Pagerfanta\Pagerfanta; 15 | 16 | /** 17 | * A paginator adapter for pagerfanta/pagerfanta. 18 | * 19 | * @author Márk Sági-Kazár 20 | */ 21 | class PagerfantaPaginatorAdapter implements PaginatorInterface 22 | { 23 | use PaginatorCountTrait; 24 | 25 | protected Pagerfanta $paginator; 26 | 27 | /** 28 | * The route generator. 29 | * 30 | * @var callable 31 | */ 32 | protected $routeGenerator; 33 | 34 | public function __construct(Pagerfanta $paginator, callable $routeGenerator) 35 | { 36 | $this->paginator = $paginator; 37 | $this->routeGenerator = $routeGenerator; 38 | } 39 | 40 | /** 41 | * {@inheritDoc} 42 | */ 43 | public function getCurrentPage(): int 44 | { 45 | return $this->paginator->getCurrentPage(); 46 | } 47 | 48 | /** 49 | * {@inheritDoc} 50 | */ 51 | public function getLastPage(): int 52 | { 53 | return $this->paginator->getNbPages(); 54 | } 55 | 56 | /** 57 | * {@inheritDoc} 58 | */ 59 | public function getTotal(): int 60 | { 61 | return count($this->paginator); 62 | } 63 | 64 | /** 65 | * {@inheritDoc} 66 | */ 67 | public function getCount(): int 68 | { 69 | return $this->getIterableCount($this->paginator->getCurrentPageResults()); 70 | } 71 | 72 | /** 73 | * {@inheritDoc} 74 | */ 75 | public function getPerPage(): int 76 | { 77 | return $this->paginator->getMaxPerPage(); 78 | } 79 | 80 | /** 81 | * {@inheritDoc} 82 | */ 83 | public function getUrl(int $page): string 84 | { 85 | return call_user_func($this->routeGenerator, $page); 86 | } 87 | 88 | public function getPaginator(): Pagerfanta 89 | { 90 | return $this->paginator; 91 | } 92 | 93 | public function getRouteGenerator(): callable 94 | { 95 | return $this->routeGenerator; 96 | } 97 | } 98 | -------------------------------------------------------------------------------- /src/Pagination/PaginatorCountTrait.php: -------------------------------------------------------------------------------- 1 | getTraversableCount($iterable); 14 | } 15 | 16 | return count($iterable); 17 | } 18 | 19 | /** 20 | * Safely get the count from a traversable 21 | */ 22 | private function getTraversableCount(\Traversable $traversable): int 23 | { 24 | if ($traversable instanceof \Countable) { 25 | return count($traversable); 26 | } 27 | 28 | // Call the "count" method if it exists 29 | if (method_exists($traversable, 'count')) { 30 | return $traversable->count(); 31 | } 32 | 33 | // If not, fall back to iterator_count and rewind if possible 34 | $count = iterator_count($traversable); 35 | if ($traversable instanceof \Iterator || $traversable instanceof \IteratorAggregate) { 36 | $traversable->rewind(); 37 | } 38 | 39 | return $count; 40 | } 41 | 42 | } 43 | -------------------------------------------------------------------------------- /src/Pagination/PaginatorInterface.php: -------------------------------------------------------------------------------- 1 | 7 | * 8 | * For the full copyright and license information, please view the LICENSE 9 | * file that was distributed with this source code. 10 | */ 11 | 12 | namespace League\Fractal\Pagination; 13 | 14 | /** 15 | * A common interface for paginators to use 16 | * 17 | * @author Marc Addeo 18 | */ 19 | interface PaginatorInterface 20 | { 21 | /** 22 | * Get the current page. 23 | */ 24 | public function getCurrentPage(): int; 25 | 26 | /** 27 | * Get the last page. 28 | */ 29 | public function getLastPage(): int; 30 | 31 | /** 32 | * Get the total. 33 | */ 34 | public function getTotal(): int; 35 | 36 | /** 37 | * Get the count. 38 | */ 39 | public function getCount(): int; 40 | 41 | /** 42 | * Get the number per page. 43 | */ 44 | public function getPerPage(): int; 45 | 46 | /** 47 | * Get the url for the given page. 48 | */ 49 | public function getUrl(int $page): string; 50 | } 51 | -------------------------------------------------------------------------------- /src/Pagination/PhalconFrameworkPaginatorAdapter.php: -------------------------------------------------------------------------------- 1 | 7 | * 8 | * For the full copyright and license information, please view the LICENSE 9 | * file that was distributed with this source code. 10 | */ 11 | 12 | namespace League\Fractal\Pagination; 13 | 14 | /** 15 | * A paginator adapter for PhalconPHP/pagination. 16 | * 17 | * @author Thien Tran 18 | * @author Nikolaos Dimopoulos 19 | */ 20 | class PhalconFrameworkPaginatorAdapter implements PaginatorInterface 21 | { 22 | /** 23 | * A slice of the result set to show in the pagination 24 | */ 25 | private \stdClass $paginator; 26 | 27 | public function __construct(\stdClass $paginator) 28 | { 29 | $this->paginator = $paginator; 30 | } 31 | 32 | /** 33 | * {@inheritDoc} 34 | */ 35 | public function getCurrentPage(): int 36 | { 37 | return $this->paginator->current; 38 | } 39 | 40 | /** 41 | * {@inheritDoc} 42 | */ 43 | public function getLastPage(): int 44 | { 45 | return $this->paginator->last; 46 | } 47 | 48 | /** 49 | * {@inheritDoc} 50 | */ 51 | public function getTotal(): int 52 | { 53 | return $this->paginator->total_items; 54 | } 55 | 56 | /** 57 | * {@inheritDoc} 58 | */ 59 | public function getCount(): int 60 | { 61 | return $this->paginator->total_pages; 62 | } 63 | 64 | /** 65 | * {@inheritDoc} 66 | */ 67 | public function getPerPage(): int 68 | { 69 | // $this->paginator->items->count() 70 | // Because when we use raw sql have not this method 71 | return count($this->paginator->items); 72 | } 73 | 74 | /** 75 | * {@inheritDoc} 76 | */ 77 | public function getNext(): int 78 | { 79 | return $this->paginator->next; 80 | } 81 | 82 | /** 83 | * {@inheritDoc} 84 | */ 85 | public function getUrl(int $page): string 86 | { 87 | return (string) $page; 88 | } 89 | } 90 | -------------------------------------------------------------------------------- /src/Pagination/ZendFrameworkPaginatorAdapter.php: -------------------------------------------------------------------------------- 1 | 7 | * 8 | * For the full copyright and license information, please view the LICENSE 9 | * file that was distributed with this source code. 10 | */ 11 | 12 | namespace League\Fractal\Pagination; 13 | 14 | use Zend\Paginator\Paginator; 15 | 16 | /** 17 | * A paginator adapter for zendframework/zend-paginator. 18 | * 19 | * @author Abdul Malik Ikhsan 20 | */ 21 | class ZendFrameworkPaginatorAdapter implements PaginatorInterface 22 | { 23 | protected Paginator $paginator; 24 | 25 | /** 26 | * The route generator. 27 | * 28 | * @var callable 29 | */ 30 | protected $routeGenerator; 31 | 32 | public function __construct(Paginator $paginator, callable $routeGenerator) 33 | { 34 | $this->paginator = $paginator; 35 | $this->routeGenerator = $routeGenerator; 36 | } 37 | 38 | /** 39 | * {@inheritDoc} 40 | */ 41 | public function getCurrentPage(): int 42 | { 43 | return $this->paginator->getCurrentPageNumber(); 44 | } 45 | 46 | /** 47 | * {@inheritDoc} 48 | */ 49 | public function getLastPage(): int 50 | { 51 | return $this->paginator->count(); 52 | } 53 | 54 | /** 55 | * {@inheritDoc} 56 | */ 57 | public function getTotal(): int 58 | { 59 | return $this->paginator->getTotalItemCount(); 60 | } 61 | 62 | /** 63 | * {@inheritDoc} 64 | */ 65 | public function getCount(): int 66 | { 67 | return $this->paginator->getCurrentItemCount(); 68 | } 69 | 70 | /** 71 | * {@inheritDoc} 72 | */ 73 | public function getPerPage(): int 74 | { 75 | return $this->paginator->getItemCountPerPage(); 76 | } 77 | 78 | /** 79 | * {@inheritDoc} 80 | */ 81 | public function getUrl(int $page): string 82 | { 83 | return call_user_func($this->routeGenerator, $page); 84 | } 85 | 86 | public function getPaginator(): Paginator 87 | { 88 | return $this->paginator; 89 | } 90 | 91 | public function getRouteGenerator(): callable 92 | { 93 | return $this->routeGenerator; 94 | } 95 | } 96 | -------------------------------------------------------------------------------- /src/ParamBag.php: -------------------------------------------------------------------------------- 1 | 7 | * 8 | * For the full copyright and license information, please view the LICENSE 9 | * file that was distributed with this source code. 10 | */ 11 | 12 | namespace League\Fractal; 13 | 14 | /** 15 | * A handy interface for getting at include parameters. 16 | */ 17 | class ParamBag implements \ArrayAccess, \IteratorAggregate 18 | { 19 | protected array $params = []; 20 | 21 | /** 22 | * Create a new parameter bag instance. 23 | */ 24 | public function __construct(array $params) 25 | { 26 | $this->params = $params; 27 | } 28 | 29 | /** 30 | * Get parameter values out of the bag. 31 | * 32 | * @return mixed 33 | */ 34 | #[\ReturnTypeWillChange] 35 | public function get(string $key) 36 | { 37 | return $this->__get($key); 38 | } 39 | 40 | /** 41 | * Get parameter values out of the bag via the property access magic method. 42 | * 43 | * @return mixed 44 | */ 45 | #[\ReturnTypeWillChange] 46 | public function __get(string $key) 47 | { 48 | return isset($this->params[$key]) ? $this->params[$key] : null; 49 | } 50 | 51 | /** 52 | * Check if a param exists in the bag via an isset() check on the property. 53 | */ 54 | public function __isset(string $key): bool 55 | { 56 | return isset($this->params[$key]); 57 | } 58 | 59 | /** 60 | * Disallow changing the value of params in the data bag via property access. 61 | * 62 | * @param mixed $value 63 | * 64 | * @throws \LogicException 65 | */ 66 | public function __set(string $key, $value): void 67 | { 68 | throw new \LogicException('Modifying parameters is not permitted'); 69 | } 70 | 71 | /** 72 | * Disallow unsetting params in the data bag via property access. 73 | * 74 | * @throws \LogicException 75 | * 76 | * @return void 77 | */ 78 | public function __unset(string $key): void 79 | { 80 | throw new \LogicException('Modifying parameters is not permitted'); 81 | } 82 | 83 | /** 84 | * Check if a param exists in the bag via an isset() and array access. 85 | * 86 | * @param string $key 87 | */ 88 | public function offsetExists($key): bool 89 | { 90 | return $this->__isset($key); 91 | } 92 | 93 | /** 94 | * Get parameter values out of the bag via array access. 95 | * 96 | * @param string $key 97 | * 98 | * @return mixed 99 | */ 100 | #[\ReturnTypeWillChange] 101 | public function offsetGet($key) 102 | { 103 | return $this->__get($key); 104 | } 105 | 106 | /** 107 | * Disallow changing the value of params in the data bag via array access. 108 | * 109 | * @param string $key 110 | * @param mixed $value 111 | * 112 | * @throws \LogicException 113 | */ 114 | public function offsetSet($key, $value): void 115 | { 116 | throw new \LogicException('Modifying parameters is not permitted'); 117 | } 118 | 119 | /** 120 | * Disallow unsetting params in the data bag via array access. 121 | * 122 | * @param string $key 123 | * 124 | * @throws \LogicException 125 | */ 126 | public function offsetUnset($key): void 127 | { 128 | throw new \LogicException('Modifying parameters is not permitted'); 129 | } 130 | 131 | /** 132 | * IteratorAggregate for iterating over the object like an array. 133 | */ 134 | public function getIterator(): \ArrayIterator 135 | { 136 | return new \ArrayIterator($this->params); 137 | } 138 | } 139 | -------------------------------------------------------------------------------- /src/Resource/Collection.php: -------------------------------------------------------------------------------- 1 | 7 | * 8 | * For the full copyright and license information, please view the LICENSE 9 | * file that was distributed with this source code. 10 | */ 11 | 12 | namespace League\Fractal\Resource; 13 | 14 | use ArrayIterator; 15 | use League\Fractal\Pagination\CursorInterface; 16 | use League\Fractal\Pagination\PaginatorInterface; 17 | 18 | /** 19 | * Resource Collection 20 | * 21 | * The data can be a collection of any sort of data, as long as the 22 | * "collection" is either array or an object implementing ArrayIterator. 23 | */ 24 | class Collection extends ResourceAbstract 25 | { 26 | /** 27 | * A collection of data. 28 | * 29 | * @var array|ArrayIterator 30 | */ 31 | protected $data; 32 | 33 | protected ?PaginatorInterface $paginator = null; 34 | 35 | protected ?CursorInterface $cursor = null; 36 | 37 | public function getPaginator(): ?PaginatorInterface 38 | { 39 | return $this->paginator; 40 | } 41 | 42 | /** 43 | * Determine if the resource has a paginator implementation. 44 | */ 45 | public function hasPaginator(): bool 46 | { 47 | return $this->paginator !== null; 48 | } 49 | 50 | /** 51 | * Get the cursor instance. 52 | */ 53 | public function getCursor(): ?CursorInterface 54 | { 55 | return $this->cursor; 56 | } 57 | 58 | /** 59 | * Determine if the resource has a cursor implementation. 60 | */ 61 | public function hasCursor(): bool 62 | { 63 | return $this->cursor !== null; 64 | } 65 | 66 | public function setPaginator(PaginatorInterface $paginator): self 67 | { 68 | $this->paginator = $paginator; 69 | 70 | return $this; 71 | } 72 | 73 | public function setCursor(CursorInterface $cursor): self 74 | { 75 | $this->cursor = $cursor; 76 | 77 | return $this; 78 | } 79 | } 80 | -------------------------------------------------------------------------------- /src/Resource/Item.php: -------------------------------------------------------------------------------- 1 | 7 | * 8 | * For the full copyright and license information, please view the LICENSE 9 | * file that was distributed with this source code. 10 | */ 11 | 12 | namespace League\Fractal\Resource; 13 | 14 | /** 15 | * The Item Resource can store any mixed data, usually an ORM, ODM or 16 | * other sort of intelligent result, DataMapper model, etc but could 17 | * be a basic array, object, or whatever you like. 18 | */ 19 | class Item extends ResourceAbstract 20 | { 21 | } 22 | -------------------------------------------------------------------------------- /src/Resource/NullResource.php: -------------------------------------------------------------------------------- 1 | 7 | * 8 | * For the full copyright and license information, please view the LICENSE 9 | * file that was distributed with this source code. 10 | */ 11 | 12 | namespace League\Fractal\Resource; 13 | 14 | /** 15 | * The Null Resource represents a resource that doesn't exist. This can be 16 | * useful to indicate that a certain relationship is null in some output 17 | * formats (e.g. JSON API), which require even a relationship that is null at 18 | * the moment to be listed. 19 | */ 20 | class NullResource extends ResourceAbstract 21 | { 22 | /** 23 | * Get the data. 24 | * 25 | * @return mixed 26 | */ 27 | #[\ReturnTypeWillChange] 28 | public function getData() 29 | { 30 | // Null has no data associated with it. 31 | return null; 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /src/Resource/Primitive.php: -------------------------------------------------------------------------------- 1 | 7 | * 8 | * For the full copyright and license information, please view the LICENSE 9 | * file that was distributed with this source code. 10 | */ 11 | 12 | namespace League\Fractal\Resource; 13 | 14 | /** 15 | * Primitive Resource 16 | * 17 | * The Primitive Resource can store any primitive data, like a string, integer, 18 | * float, double etc. 19 | */ 20 | class Primitive extends ResourceAbstract 21 | { 22 | } 23 | -------------------------------------------------------------------------------- /src/Resource/ResourceAbstract.php: -------------------------------------------------------------------------------- 1 | 7 | * 8 | * For the full copyright and license information, please view the LICENSE 9 | * file that was distributed with this source code. 10 | */ 11 | 12 | namespace League\Fractal\Resource; 13 | 14 | use League\Fractal\TransformerAbstract; 15 | 16 | abstract class ResourceAbstract implements ResourceInterface 17 | { 18 | /** 19 | * Any item to process. 20 | * 21 | * @var mixed 22 | */ 23 | protected $data; 24 | 25 | /** 26 | * Array of meta data. 27 | */ 28 | protected array $meta = []; 29 | 30 | /** 31 | * The resource key. 32 | */ 33 | protected ?string $resourceKey; 34 | 35 | /** 36 | * A callable to process the data attached to this resource. 37 | * 38 | * @var callable|TransformerAbstract|null 39 | */ 40 | protected $transformer; 41 | 42 | /** 43 | * @param mixed $data 44 | * @param callable|TransformerAbstract|null $transformer 45 | */ 46 | public function __construct($data = null, $transformer = null, ?string $resourceKey = null) 47 | { 48 | $this->data = $data; 49 | $this->transformer = $transformer; 50 | $this->resourceKey = $resourceKey; 51 | } 52 | 53 | /** 54 | * Get the data. 55 | * 56 | * @return mixed 57 | */ 58 | #[\ReturnTypeWillChange] 59 | public function getData() 60 | { 61 | return $this->data; 62 | } 63 | 64 | /** 65 | * Set the data. 66 | * 67 | * @param mixed $data 68 | */ 69 | public function setData($data): self 70 | { 71 | $this->data = $data; 72 | 73 | return $this; 74 | } 75 | 76 | /** 77 | * Get the meta data. 78 | */ 79 | public function getMeta(): array 80 | { 81 | return $this->meta; 82 | } 83 | 84 | /** 85 | * Get the meta data. 86 | * 87 | * @return mixed 88 | */ 89 | #[\ReturnTypeWillChange] 90 | public function getMetaValue(string $metaKey) 91 | { 92 | return $this->meta[$metaKey]; 93 | } 94 | 95 | public function getResourceKey(): string 96 | { 97 | return $this->resourceKey ?? ''; 98 | } 99 | 100 | /** 101 | * Get the transformer. 102 | * 103 | * @return callable|TransformerAbstract|null 104 | */ 105 | public function getTransformer() 106 | { 107 | return $this->transformer; 108 | } 109 | 110 | /** 111 | * Set the transformer. 112 | * 113 | * @param callable|TransformerAbstract $transformer 114 | */ 115 | public function setTransformer($transformer): self 116 | { 117 | $this->transformer = $transformer; 118 | 119 | return $this; 120 | } 121 | 122 | /** 123 | * Set the meta data. 124 | */ 125 | public function setMeta(array $meta): self 126 | { 127 | $this->meta = $meta; 128 | 129 | return $this; 130 | } 131 | 132 | /** 133 | * Set one meta data value. 134 | * 135 | * @param mixed $metaValue 136 | */ 137 | public function setMetaValue(string $metaKey, $metaValue): self 138 | { 139 | $this->meta[$metaKey] = $metaValue; 140 | 141 | return $this; 142 | } 143 | 144 | public function setResourceKey(string $resourceKey): self 145 | { 146 | $this->resourceKey = $resourceKey; 147 | 148 | return $this; 149 | } 150 | } 151 | -------------------------------------------------------------------------------- /src/Resource/ResourceInterface.php: -------------------------------------------------------------------------------- 1 | 7 | * 8 | * For the full copyright and license information, please view the LICENSE 9 | * file that was distributed with this source code. 10 | */ 11 | 12 | namespace League\Fractal\Resource; 13 | 14 | interface ResourceInterface 15 | { 16 | /** 17 | * Get the resource key. 18 | */ 19 | public function getResourceKey(): string; 20 | 21 | /** 22 | * Get the data. 23 | * 24 | * @return mixed 25 | */ 26 | #[\ReturnTypeWillChange] 27 | public function getData(); 28 | 29 | /** 30 | * Get the transformer. 31 | * 32 | * @return callable|\League\Fractal\TransformerAbstract|null 33 | */ 34 | public function getTransformer(); 35 | 36 | /** 37 | * Set the data. 38 | * 39 | * @param mixed $data 40 | */ 41 | public function setData($data): self; 42 | 43 | /** 44 | * Set the transformer. 45 | * 46 | * @param callable|\League\Fractal\TransformerAbstract $transformer 47 | */ 48 | public function setTransformer($transformer): self; 49 | 50 | /** 51 | * Get the meta data. 52 | */ 53 | public function getMeta(): array; 54 | } 55 | -------------------------------------------------------------------------------- /src/Scope.php: -------------------------------------------------------------------------------- 1 | 7 | * 8 | * For the full copyright and license information, please view the LICENSE 9 | * file that was distributed with this source code. 10 | **/ 11 | 12 | namespace League\Fractal; 13 | 14 | use InvalidArgumentException; 15 | use League\Fractal\Resource\Collection; 16 | use League\Fractal\Resource\Item; 17 | use League\Fractal\Resource\Primitive; 18 | use League\Fractal\Resource\NullResource; 19 | use League\Fractal\Resource\ResourceInterface; 20 | use League\Fractal\Serializer\Serializer; 21 | 22 | /** 23 | * Scope 24 | * 25 | * The scope class acts as a tracker, relating a specific resource in a specific 26 | * context. For example, the same resource could be attached to multiple scopes. 27 | * There are root scopes, parent scopes and child scopes. 28 | */ 29 | class Scope implements \JsonSerializable 30 | { 31 | protected array $availableIncludes = []; 32 | 33 | protected ?string $scopeIdentifier; 34 | 35 | protected Manager $manager; 36 | 37 | protected ResourceInterface $resource; 38 | 39 | protected array $parentScopes = []; 40 | 41 | public function __construct(Manager $manager, ResourceInterface $resource, ?string $scopeIdentifier = null) 42 | { 43 | $this->manager = $manager; 44 | $this->resource = $resource; 45 | $this->scopeIdentifier = $scopeIdentifier; 46 | } 47 | 48 | /** 49 | * Embed a scope as a child of the current scope. 50 | * 51 | * @internal 52 | */ 53 | public function embedChildScope(string $scopeIdentifier, ResourceInterface $resource): Scope 54 | { 55 | return $this->manager->createData($resource, $scopeIdentifier, $this); 56 | } 57 | 58 | /** 59 | * Get the current identifier. 60 | */ 61 | public function getScopeIdentifier(): ?string 62 | { 63 | return $this->scopeIdentifier; 64 | } 65 | 66 | /** 67 | * Get the unique identifier for this scope. 68 | */ 69 | public function getIdentifier(?string $appendIdentifier = null): string 70 | { 71 | $identifierParts = array_merge($this->parentScopes, [$this->scopeIdentifier, $appendIdentifier]); 72 | 73 | return implode('.', array_filter($identifierParts)); 74 | } 75 | 76 | /** 77 | * @return mixed 78 | */ 79 | #[\ReturnTypeWillChange] 80 | public function getParentScopes() 81 | { 82 | return $this->parentScopes; 83 | } 84 | 85 | public function getResource(): ResourceInterface 86 | { 87 | return $this->resource; 88 | } 89 | 90 | public function getManager(): Manager 91 | { 92 | return $this->manager; 93 | } 94 | 95 | /** 96 | * Check if - in relation to the current scope - this specific segment is allowed. 97 | * That means, if a.b.c is requested and the current scope is a.b, then c is allowed. If the current 98 | * scope is a then c is not allowed, even if it is there and potentially transformable. 99 | * 100 | * @internal 101 | * 102 | * @return bool Returns the new number of elements in the array. 103 | */ 104 | public function isRequested(string $checkScopeSegment): bool 105 | { 106 | if ($this->parentScopes) { 107 | $scopeArray = array_slice($this->parentScopes, 1); 108 | array_push($scopeArray, $this->scopeIdentifier, $checkScopeSegment); 109 | } else { 110 | $scopeArray = [$checkScopeSegment]; 111 | } 112 | 113 | $scopeString = implode('.', $scopeArray); 114 | 115 | return in_array($scopeString, $this->manager->getRequestedIncludes()); 116 | } 117 | 118 | /** 119 | * Check if - in relation to the current scope - this specific segment should 120 | * be excluded. That means, if a.b.c is excluded and the current scope is a.b, 121 | * then c will not be allowed in the transformation whether it appears in 122 | * the list of default or available, requested includes. 123 | * 124 | * @internal 125 | */ 126 | public function isExcluded(string $checkScopeSegment): bool 127 | { 128 | if ($this->parentScopes) { 129 | $scopeArray = array_slice($this->parentScopes, 1); 130 | array_push($scopeArray, $this->scopeIdentifier, $checkScopeSegment); 131 | } else { 132 | $scopeArray = [$checkScopeSegment]; 133 | } 134 | 135 | $scopeString = implode('.', $scopeArray); 136 | 137 | return in_array($scopeString, $this->manager->getRequestedExcludes()); 138 | } 139 | 140 | /** 141 | * Push Parent Scope. 142 | * 143 | * Push a scope identifier into parentScopes 144 | * 145 | * @internal 146 | * 147 | * @return int Returns the new number of elements in the array. 148 | */ 149 | public function pushParentScope(string $identifierSegment): int 150 | { 151 | return array_push($this->parentScopes, $identifierSegment); 152 | } 153 | 154 | /** 155 | * Set parent scopes. 156 | * 157 | * @internal 158 | * 159 | * @param string[] $parentScopes Value to set. 160 | */ 161 | public function setParentScopes(array $parentScopes): self 162 | { 163 | $this->parentScopes = $parentScopes; 164 | 165 | return $this; 166 | } 167 | 168 | /** 169 | * Convert the current data for this scope to an array. 170 | */ 171 | public function toArray(): ?array 172 | { 173 | list($rawData, $rawIncludedData) = $this->executeResourceTransformers(); 174 | 175 | $serializer = $this->manager->getSerializer(); 176 | 177 | $data = $this->serializeResource($serializer, $rawData); 178 | 179 | // If the serializer wants the includes to be side-loaded then we'll 180 | // serialize the included data and merge it with the data. 181 | if ($serializer->sideloadIncludes()) { 182 | //Filter out any relation that wasn't requested 183 | $rawIncludedData = array_map(array($this, 'filterFieldsets'), $rawIncludedData); 184 | 185 | $includedData = $serializer->includedData($this->resource, $rawIncludedData); 186 | 187 | // If the serializer wants to inject additional information 188 | // about the included resources, it can do so now. 189 | $data = $serializer->injectData($data, $rawIncludedData); 190 | 191 | if ($this->isRootScope()) { 192 | // If the serializer wants to have a final word about all 193 | // the objects that are sideloaded, it can do so now. 194 | $includedData = $serializer->filterIncludes( 195 | $includedData, 196 | $data 197 | ); 198 | } 199 | 200 | $data = $data + $includedData; 201 | } 202 | 203 | if (!empty($this->availableIncludes)) { 204 | $data = $serializer->injectAvailableIncludeData($data, $this->availableIncludes); 205 | } 206 | 207 | if ($this->resource instanceof Collection) { 208 | if ($this->resource->hasCursor()) { 209 | $pagination = $serializer->cursor($this->resource->getCursor()); 210 | } elseif ($this->resource->hasPaginator()) { 211 | $pagination = $serializer->paginator($this->resource->getPaginator()); 212 | } 213 | 214 | if (! empty($pagination)) { 215 | $this->resource->setMetaValue(key($pagination), current($pagination)); 216 | } 217 | } 218 | 219 | // Pull out all of OUR metadata and any custom meta data to merge with the main level data 220 | $meta = $serializer->meta($this->resource->getMeta()); 221 | 222 | // in case of returning NullResource we should return null and not to go with array_merge 223 | if (is_null($data)) { 224 | if (!empty($meta)) { 225 | return $meta; 226 | } 227 | return null; 228 | } 229 | 230 | return $data + $meta; 231 | } 232 | 233 | /** 234 | * @return mixed 235 | */ 236 | #[\ReturnTypeWillChange] 237 | public function jsonSerialize() 238 | { 239 | return $this->toArray(); 240 | } 241 | 242 | /** 243 | * Convert the current data for this scope to JSON. 244 | */ 245 | public function toJson(int $options = 0): string 246 | { 247 | return \json_encode($this, $options); 248 | } 249 | 250 | /** 251 | * Transformer a primitive resource 252 | * 253 | * @return mixed 254 | */ 255 | #[\ReturnTypeWillChange] 256 | public function transformPrimitiveResource() 257 | { 258 | if (! ($this->resource instanceof Primitive)) { 259 | throw new InvalidArgumentException( 260 | 'Argument $resource should be an instance of League\Fractal\Resource\Primitive' 261 | ); 262 | } 263 | 264 | $transformer = $this->resource->getTransformer(); 265 | $data = $this->resource->getData(); 266 | 267 | if (null === $transformer) { 268 | $transformedData = $data; 269 | } elseif (is_callable($transformer)) { 270 | $transformedData = call_user_func($transformer, $data); 271 | } else { 272 | $transformer->setCurrentScope($this); 273 | $transformedData = $transformer->transform($data); 274 | } 275 | 276 | return $transformedData; 277 | } 278 | 279 | /** 280 | * Execute the resources transformer and return the data and included data. 281 | * 282 | * @internal 283 | */ 284 | protected function executeResourceTransformers(): array 285 | { 286 | $transformer = $this->resource->getTransformer(); 287 | $data = $this->resource->getData(); 288 | 289 | $transformedData = $includedData = []; 290 | 291 | if ($this->resource instanceof Item) { 292 | list($transformedData, $includedData[]) = $this->fireTransformer($transformer, $data); 293 | } elseif ($this->resource instanceof Collection) { 294 | foreach ($data as $value) { 295 | list($transformedData[], $includedData[]) = $this->fireTransformer($transformer, $value); 296 | } 297 | } elseif ($this->resource instanceof NullResource) { 298 | $transformedData = null; 299 | $includedData = []; 300 | } else { 301 | throw new InvalidArgumentException( 302 | 'Argument $resource should be an instance of League\Fractal\Resource\Item' 303 | .' or League\Fractal\Resource\Collection' 304 | ); 305 | } 306 | 307 | return [$transformedData, $includedData]; 308 | } 309 | 310 | /** 311 | * Serialize a resource 312 | * 313 | * @internal 314 | * 315 | * @param mixed $data 316 | */ 317 | protected function serializeResource(Serializer $serializer, $data): ?array 318 | { 319 | $resourceKey = $this->resource->getResourceKey(); 320 | 321 | if ($this->resource instanceof Collection) { 322 | if (!empty($resourceKey)) { 323 | return $serializer->collection($resourceKey, $data); 324 | } 325 | 326 | return $serializer->collection(null, $data); 327 | } 328 | 329 | if ($this->resource instanceof Item) { 330 | // this is where it breaks now. 331 | if (!empty($resourceKey)) { 332 | return $serializer->item($resourceKey, $data); 333 | } 334 | 335 | return $serializer->item(null, $data); 336 | } 337 | 338 | return $serializer->null(); 339 | } 340 | 341 | /** 342 | * Fire the main transformer. 343 | * 344 | * @internal 345 | * 346 | * @param TransformerAbstract|callable $transformer 347 | * @param mixed $data 348 | */ 349 | protected function fireTransformer($transformer, $data): array 350 | { 351 | $includedData = []; 352 | 353 | if (is_callable($transformer)) { 354 | $transformedData = call_user_func($transformer, $data); 355 | } else { 356 | $transformer->setCurrentScope($this); 357 | $transformedData = $transformer->transform($data); 358 | } 359 | 360 | if ($this->transformerHasIncludes($transformer)) { 361 | $includedData = $this->fireIncludedTransformers($transformer, $data); 362 | $transformedData = $this->manager->getSerializer()->mergeIncludes($transformedData, $includedData); 363 | } 364 | 365 | //Stick only with requested fields 366 | $transformedData = $this->filterFieldsets($transformedData); 367 | 368 | return [$transformedData, $includedData]; 369 | } 370 | 371 | /** 372 | * Fire the included transformers. 373 | * 374 | * @internal 375 | * 376 | * @param \League\Fractal\TransformerAbstract $transformer 377 | * @param mixed $data 378 | */ 379 | protected function fireIncludedTransformers($transformer, $data): array 380 | { 381 | $this->availableIncludes = $transformer->getAvailableIncludes(); 382 | 383 | return $transformer->processIncludedResources($this, $data) ?: []; 384 | } 385 | 386 | /** 387 | * Determine if a transformer has any available includes. 388 | * 389 | * @internal 390 | * 391 | * @param TransformerAbstract|callable $transformer 392 | */ 393 | protected function transformerHasIncludes($transformer): bool 394 | { 395 | if (! $transformer instanceof TransformerAbstract) { 396 | return false; 397 | } 398 | 399 | $defaultIncludes = $transformer->getDefaultIncludes(); 400 | $availableIncludes = $transformer->getAvailableIncludes(); 401 | 402 | return ! empty($defaultIncludes) || ! empty($availableIncludes); 403 | } 404 | 405 | /** 406 | * Check, if this is the root scope. 407 | */ 408 | protected function isRootScope(): bool 409 | { 410 | return empty($this->parentScopes); 411 | } 412 | 413 | /** 414 | * Filter the provided data with the requested filter fieldset for 415 | * the scope resource 416 | * 417 | * @internal 418 | */ 419 | protected function filterFieldsets(array $data): array 420 | { 421 | if (!$this->hasFilterFieldset()) { 422 | return $data; 423 | } 424 | $serializer = $this->manager->getSerializer(); 425 | $requestedFieldset = iterator_to_array($this->getFilterFieldset()); 426 | //Build the array of requested fieldsets with the mandatory serializer fields 427 | $filterFieldset = array_flip( 428 | array_merge( 429 | $serializer->getMandatoryFields(), 430 | $requestedFieldset 431 | ) 432 | ); 433 | return array_intersect_key($data, $filterFieldset); 434 | } 435 | 436 | /** 437 | * Return the requested filter fieldset for the scope resource 438 | * 439 | * @internal 440 | */ 441 | protected function getFilterFieldset(): ?ParamBag 442 | { 443 | return $this->manager->getFieldset($this->getResourceType()); 444 | } 445 | 446 | /** 447 | * Check if there are requested filter fieldsets for the scope resource. 448 | * 449 | * @internal 450 | */ 451 | protected function hasFilterFieldset(): bool 452 | { 453 | return $this->getFilterFieldset() !== null; 454 | } 455 | 456 | /** 457 | * Return the scope resource type. 458 | * 459 | * @internal 460 | */ 461 | protected function getResourceType(): string 462 | { 463 | return $this->resource->getResourceKey(); 464 | } 465 | } 466 | -------------------------------------------------------------------------------- /src/ScopeFactory.php: -------------------------------------------------------------------------------- 1 | 7 | * 8 | * For the full copyright and license information, please view the LICENSE 9 | * file that was distributed with this source code. 10 | */ 11 | 12 | namespace League\Fractal; 13 | 14 | use League\Fractal\Resource\ResourceInterface; 15 | 16 | class ScopeFactory implements ScopeFactoryInterface 17 | { 18 | public function createScopeFor( 19 | Manager $manager, 20 | ResourceInterface $resource, 21 | ?string $scopeIdentifier = null 22 | ): Scope { 23 | return new Scope($manager, $resource, $scopeIdentifier); 24 | } 25 | 26 | public function createChildScopeFor( 27 | Manager $manager, 28 | Scope $parentScope, 29 | ResourceInterface $resource, 30 | ?string $scopeIdentifier = null 31 | ): Scope { 32 | $scopeInstance = $this->createScopeFor($manager, $resource, $scopeIdentifier); 33 | 34 | // This will be the new children list of parents (parents parents, plus the parent) 35 | $scopeArray = $parentScope->getParentScopes(); 36 | $scopeArray[] = $parentScope->getScopeIdentifier(); 37 | 38 | $scopeInstance->setParentScopes($scopeArray); 39 | 40 | return $scopeInstance; 41 | } 42 | } 43 | -------------------------------------------------------------------------------- /src/ScopeFactoryInterface.php: -------------------------------------------------------------------------------- 1 | 7 | * 8 | * For the full copyright and license information, please view the LICENSE 9 | * file that was distributed with this source code. 10 | */ 11 | 12 | namespace League\Fractal; 13 | 14 | use League\Fractal\Resource\ResourceInterface; 15 | 16 | /** 17 | * Creates Scope Instances for resources 18 | */ 19 | interface ScopeFactoryInterface 20 | { 21 | public function createScopeFor( 22 | Manager $manager, 23 | ResourceInterface $resource, 24 | ?string $scopeIdentifier = null 25 | ): Scope; 26 | 27 | public function createChildScopeFor( 28 | Manager $manager, 29 | Scope $parentScope, 30 | ResourceInterface $resource, 31 | ?string $scopeIdentifier = null 32 | ): Scope; 33 | } 34 | -------------------------------------------------------------------------------- /src/Serializer/ArraySerializer.php: -------------------------------------------------------------------------------- 1 | 7 | * 8 | * For the full copyright and license information, please view the LICENSE 9 | * file that was distributed with this source code. 10 | */ 11 | 12 | namespace League\Fractal\Serializer; 13 | 14 | use League\Fractal\Pagination\CursorInterface; 15 | use League\Fractal\Pagination\PaginatorInterface; 16 | use League\Fractal\Resource\ResourceInterface; 17 | 18 | class ArraySerializer extends SerializerAbstract 19 | { 20 | /** 21 | * {@inheritDoc} 22 | */ 23 | public function collection(?string $resourceKey, array $data): array 24 | { 25 | return [$resourceKey ?: 'data' => $data]; 26 | } 27 | 28 | /** 29 | * {@inheritDoc} 30 | */ 31 | public function item(?string $resourceKey, array $data): array 32 | { 33 | return $data; 34 | } 35 | 36 | /** 37 | * {@inheritDoc} 38 | */ 39 | public function null(): ?array 40 | { 41 | return []; 42 | } 43 | 44 | /** 45 | * {@inheritDoc} 46 | */ 47 | public function includedData(ResourceInterface $resource, array $data): array 48 | { 49 | return $data; 50 | } 51 | 52 | /** 53 | * {@inheritDoc} 54 | */ 55 | public function meta(array $meta): array 56 | { 57 | if (empty($meta)) { 58 | return []; 59 | } 60 | 61 | return ['meta' => $meta]; 62 | } 63 | 64 | /** 65 | * {@inheritDoc} 66 | */ 67 | public function paginator(PaginatorInterface $paginator): array 68 | { 69 | $currentPage = $paginator->getCurrentPage(); 70 | $lastPage = $paginator->getLastPage(); 71 | 72 | $pagination = [ 73 | 'total' => $paginator->getTotal(), 74 | 'count' => $paginator->getCount(), 75 | 'per_page' => $paginator->getPerPage(), 76 | 'current_page' => $currentPage, 77 | 'total_pages' => $lastPage, 78 | ]; 79 | 80 | $pagination['links'] = []; 81 | 82 | if ($currentPage > 1) { 83 | $pagination['links']['previous'] = $paginator->getUrl($currentPage - 1); 84 | } 85 | 86 | if ($currentPage < $lastPage) { 87 | $pagination['links']['next'] = $paginator->getUrl($currentPage + 1); 88 | } 89 | 90 | if (empty($pagination['links'])) { 91 | $pagination['links'] = (object) []; 92 | } 93 | 94 | return ['pagination' => $pagination]; 95 | } 96 | 97 | /** 98 | * {@inheritDoc} 99 | */ 100 | public function cursor(CursorInterface $cursor): array 101 | { 102 | $cursor = [ 103 | 'current' => $cursor->getCurrent(), 104 | 'prev' => $cursor->getPrev(), 105 | 'next' => $cursor->getNext(), 106 | 'count' => $cursor->getCount(), 107 | ]; 108 | 109 | return ['cursor' => $cursor]; 110 | } 111 | } 112 | -------------------------------------------------------------------------------- /src/Serializer/DataArraySerializer.php: -------------------------------------------------------------------------------- 1 | 7 | * 8 | * For the full copyright and license information, please view the LICENSE 9 | * file that was distributed with this source code. 10 | */ 11 | 12 | namespace League\Fractal\Serializer; 13 | 14 | class DataArraySerializer extends ArraySerializer 15 | { 16 | /** 17 | * {@inheritDoc} 18 | */ 19 | public function collection(?string $resourceKey, array $data): array 20 | { 21 | return ['data' => $data]; 22 | } 23 | 24 | /** 25 | * {@inheritDoc} 26 | */ 27 | public function item(?string $resourceKey, array $data): array 28 | { 29 | return ['data' => $data]; 30 | } 31 | 32 | /** 33 | * {@inheritDoc} 34 | */ 35 | public function null(): ?array 36 | { 37 | return ['data' => []]; 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /src/Serializer/JsonApiSerializer.php: -------------------------------------------------------------------------------- 1 | 7 | * 8 | * For the full copyright and license information, please view the LICENSE 9 | * file that was distributed with this source code. 10 | */ 11 | 12 | namespace League\Fractal\Serializer; 13 | 14 | use InvalidArgumentException; 15 | use League\Fractal\Pagination\PaginatorInterface; 16 | use League\Fractal\Resource\ResourceInterface; 17 | 18 | class JsonApiSerializer extends ArraySerializer 19 | { 20 | protected ?string $baseUrl = null; 21 | protected array $rootObjects = []; 22 | 23 | public function __construct(?string $baseUrl = null) 24 | { 25 | $this->baseUrl = $baseUrl; 26 | } 27 | 28 | /** 29 | * {@inheritDoc} 30 | */ 31 | public function collection(?string $resourceKey, array $data): array 32 | { 33 | $resources = []; 34 | 35 | foreach ($data as $resource) { 36 | $resources[] = $this->item($resourceKey, $resource)['data']; 37 | } 38 | 39 | return ['data' => $resources]; 40 | } 41 | 42 | /** 43 | * {@inheritDoc} 44 | */ 45 | public function item(?string $resourceKey, array $data): array 46 | { 47 | $id = $this->getIdFromData($data); 48 | 49 | $resource = [ 50 | 'data' => [ 51 | 'type' => $resourceKey, 52 | 'id' => "$id", 53 | 'attributes' => $data, 54 | ], 55 | ]; 56 | 57 | unset($resource['data']['attributes']['id']); 58 | 59 | if (isset($resource['data']['attributes']['links'])) { 60 | $custom_links = $data['links']; 61 | unset($resource['data']['attributes']['links']); 62 | } 63 | 64 | if (isset($resource['data']['attributes']['meta'])) { 65 | $resource['data']['meta'] = $data['meta']; 66 | unset($resource['data']['attributes']['meta']); 67 | } 68 | 69 | if (empty($resource['data']['attributes'])) { 70 | $resource['data']['attributes'] = (object) []; 71 | } 72 | 73 | if ($this->shouldIncludeLinks()) { 74 | $resource['data']['links'] = [ 75 | 'self' => "{$this->baseUrl}/$resourceKey/$id", 76 | ]; 77 | if (isset($custom_links)) { 78 | $resource['data']['links'] = array_merge($resource['data']['links'], $custom_links); 79 | } 80 | } 81 | 82 | return $resource; 83 | } 84 | 85 | /** 86 | * {@inheritDoc} 87 | */ 88 | public function paginator(PaginatorInterface $paginator): array 89 | { 90 | $currentPage = $paginator->getCurrentPage(); 91 | $lastPage = $paginator->getLastPage(); 92 | 93 | $pagination = [ 94 | 'total' => $paginator->getTotal(), 95 | 'count' => $paginator->getCount(), 96 | 'per_page' => $paginator->getPerPage(), 97 | 'current_page' => $currentPage, 98 | 'total_pages' => $lastPage, 99 | ]; 100 | 101 | $pagination['links'] = []; 102 | 103 | $pagination['links']['self'] = $paginator->getUrl($currentPage); 104 | $pagination['links']['first'] = $paginator->getUrl(1); 105 | 106 | if ($currentPage > 1) { 107 | $pagination['links']['prev'] = $paginator->getUrl($currentPage - 1); 108 | } 109 | 110 | if ($currentPage < $lastPage) { 111 | $pagination['links']['next'] = $paginator->getUrl($currentPage + 1); 112 | } 113 | 114 | $pagination['links']['last'] = $paginator->getUrl($lastPage); 115 | 116 | return ['pagination' => $pagination]; 117 | } 118 | 119 | /** 120 | * {@inheritDoc} 121 | */ 122 | public function meta(array $meta): array 123 | { 124 | if (empty($meta)) { 125 | return []; 126 | } 127 | 128 | $result['meta'] = $meta; 129 | 130 | if (array_key_exists('pagination', $result['meta'])) { 131 | $result['links'] = $result['meta']['pagination']['links']; 132 | unset($result['meta']['pagination']['links']); 133 | } 134 | 135 | return $result; 136 | } 137 | 138 | /** 139 | * {@inheritDoc} 140 | */ 141 | public function null(): ?array 142 | { 143 | return [ 144 | 'data' => null, 145 | ]; 146 | } 147 | 148 | /** 149 | * {@inheritDoc} 150 | */ 151 | public function includedData(ResourceInterface $resource, array $data): array 152 | { 153 | list($serializedData, $linkedIds) = $this->pullOutNestedIncludedData($data); 154 | 155 | foreach ($data as $value) { 156 | foreach ($value as $includeObject) { 157 | if ($this->isNull($includeObject) || $this->isEmpty($includeObject)) { 158 | continue; 159 | } 160 | 161 | $includeObjects = $this->createIncludeObjects($includeObject); 162 | 163 | list($serializedData, $linkedIds) = $this->serializeIncludedObjectsWithCacheKey( 164 | $includeObjects, 165 | $linkedIds, 166 | $serializedData 167 | ); 168 | } 169 | } 170 | 171 | return empty($serializedData) ? [] : ['included' => $serializedData]; 172 | } 173 | 174 | /** 175 | * {@inheritDoc} 176 | */ 177 | public function sideloadIncludes(): bool 178 | { 179 | return true; 180 | } 181 | 182 | /** 183 | * {@inheritDoc} 184 | */ 185 | public function injectData(array $data, array $rawIncludedData): array 186 | { 187 | $relationships = $this->parseRelationships($rawIncludedData); 188 | 189 | if (!empty($relationships)) { 190 | $data = $this->fillRelationships($data, $relationships); 191 | } 192 | 193 | return $data; 194 | } 195 | 196 | /** 197 | * {@inheritDoc} 198 | * 199 | * Hook to manipulate the final sideloaded includes. 200 | * The JSON API specification does not allow the root object to be included 201 | * into the sideloaded `included`-array. We have to make sure it is 202 | * filtered out, in case some object links to the root object in a 203 | * relationship. 204 | */ 205 | public function filterIncludes(array $includedData, array $data): array 206 | { 207 | if (!isset($includedData['included'])) { 208 | return $includedData; 209 | } 210 | 211 | // Create the RootObjects 212 | $this->createRootObjects($data); 213 | 214 | // Filter out the root objects 215 | $filteredIncludes = array_filter($includedData['included'], [$this, 'filterRootObject']); 216 | 217 | // Reset array indizes 218 | $includedData['included'] = array_merge([], $filteredIncludes); 219 | 220 | return $includedData; 221 | } 222 | 223 | /** 224 | * {@inheritDoc} 225 | */ 226 | public function getMandatoryFields(): array 227 | { 228 | return ['id']; 229 | } 230 | 231 | /** 232 | * Filter function to delete root objects from array. 233 | */ 234 | protected function filterRootObject(array $object): bool 235 | { 236 | return !$this->isRootObject($object); 237 | } 238 | 239 | /** 240 | * Set the root objects of the JSON API tree. 241 | */ 242 | protected function setRootObjects(array $objects = []): void 243 | { 244 | $this->rootObjects = array_map(function ($object) { 245 | return "{$object['type']}:{$object['id']}"; 246 | }, $objects); 247 | } 248 | 249 | /** 250 | * Determines whether an object is a root object of the JSON API tree. 251 | */ 252 | protected function isRootObject(array $object): bool 253 | { 254 | $objectKey = "{$object['type']}:{$object['id']}"; 255 | 256 | return in_array($objectKey, $this->rootObjects); 257 | } 258 | 259 | protected function isCollection(array $data): bool 260 | { 261 | return array_key_exists('data', $data) && 262 | array_key_exists(0, $data['data']); 263 | } 264 | 265 | protected function isNull(array $data): bool 266 | { 267 | return array_key_exists('data', $data) && $data['data'] === null; 268 | } 269 | 270 | protected function isEmpty(array $data): bool 271 | { 272 | return array_key_exists('data', $data) && $data['data'] === []; 273 | } 274 | 275 | protected function fillRelationships(array $data, array $relationships): array 276 | { 277 | if ($this->isCollection($data)) { 278 | foreach ($relationships as $key => $relationship) { 279 | $data = $this->fillRelationshipAsCollection($data, $relationship, $key); 280 | } 281 | } else { // Single resource 282 | foreach ($relationships as $key => $relationship) { 283 | $data = $this->fillRelationshipAsSingleResource($data, $relationship, $key); 284 | } 285 | } 286 | 287 | return $data; 288 | } 289 | 290 | protected function parseRelationships(array $includedData): array 291 | { 292 | $relationships = []; 293 | 294 | foreach ($includedData as $key => $inclusion) { 295 | foreach ($inclusion as $includeKey => $includeObject) { 296 | $relationships = $this->buildRelationships($includeKey, $relationships, $includeObject, $key); 297 | if (isset($includedData[0][$includeKey]['meta'])) { 298 | $relationships[$includeKey][0]['meta'] = $includedData[0][$includeKey]['meta']; 299 | } 300 | } 301 | } 302 | 303 | return $relationships; 304 | } 305 | 306 | /** 307 | * @return mixed 308 | */ 309 | #[\ReturnTypeWillChange] 310 | protected function getIdFromData(array $data) 311 | { 312 | if (!array_key_exists('id', $data)) { 313 | throw new InvalidArgumentException( 314 | 'JSON API resource objects MUST have a valid id' 315 | ); 316 | } 317 | 318 | return $data['id']; 319 | } 320 | 321 | /** 322 | * Keep all sideloaded inclusion data on the top level. 323 | */ 324 | protected function pullOutNestedIncludedData(array $data): array 325 | { 326 | $includedData = []; 327 | $linkedIds = []; 328 | 329 | foreach ($data as $value) { 330 | foreach ($value as $includeObject) { 331 | if (isset($includeObject['included'])) { 332 | list($includedData, $linkedIds) = $this->serializeIncludedObjectsWithCacheKey( 333 | $includeObject['included'], 334 | $linkedIds, 335 | $includedData 336 | ); 337 | } 338 | } 339 | } 340 | 341 | return [$includedData, $linkedIds]; 342 | } 343 | 344 | /** 345 | * Whether or not the serializer should include `links` for resource objects. 346 | */ 347 | protected function shouldIncludeLinks(): bool 348 | { 349 | return $this->baseUrl !== null; 350 | } 351 | 352 | /** 353 | * Check if the objects are part of a collection or not 354 | * 355 | * @param array|object $includeObject 356 | */ 357 | private function createIncludeObjects($includeObject): array 358 | { 359 | if ($this->isCollection($includeObject)) { 360 | $includeObjects = $includeObject['data']; 361 | 362 | return $includeObjects; 363 | } else { 364 | $includeObjects = [$includeObject['data']]; 365 | 366 | return $includeObjects; 367 | } 368 | } 369 | 370 | /** 371 | * Sets the RootObjects, either as collection or not. 372 | */ 373 | private function createRootObjects(array $data): void 374 | { 375 | if ($this->isCollection($data)) { 376 | $this->setRootObjects($data['data']); 377 | } else { 378 | $this->setRootObjects([$data['data']]); 379 | } 380 | } 381 | 382 | 383 | /** 384 | * Loops over the relationships of the provided data and formats it 385 | */ 386 | private function fillRelationshipAsCollection(array $data, array $relationship, string $key): array 387 | { 388 | foreach ($relationship as $index => $relationshipData) { 389 | $data['data'][$index]['relationships'][$key] = $relationshipData; 390 | } 391 | 392 | return $data; 393 | } 394 | 395 | private function fillRelationshipAsSingleResource(array $data, array $relationship, string $key): array 396 | { 397 | $data['data']['relationships'][$key] = $relationship[0]; 398 | 399 | return $data; 400 | } 401 | 402 | private function buildRelationships( 403 | string $includeKey, 404 | array $relationships, 405 | array $includeObject, 406 | string $key 407 | ): array { 408 | $relationships = $this->addIncludekeyToRelationsIfNotSet($includeKey, $relationships); 409 | 410 | if ($this->isNull($includeObject)) { 411 | $relationship = $this->null(); 412 | } elseif ($this->isEmpty($includeObject)) { 413 | $relationship = [ 414 | 'data' => [], 415 | ]; 416 | } elseif ($this->isCollection($includeObject)) { 417 | $relationship = ['data' => []]; 418 | 419 | $relationship = $this->addIncludedDataToRelationship($includeObject, $relationship); 420 | } else { 421 | $relationship = [ 422 | 'data' => [ 423 | 'type' => $includeObject['data']['type'], 424 | 'id' => $includeObject['data']['id'], 425 | ], 426 | ]; 427 | } 428 | 429 | $relationships[$includeKey][$key] = $relationship; 430 | 431 | return $relationships; 432 | } 433 | 434 | private function addIncludekeyToRelationsIfNotSet(string $includeKey, array $relationships): array 435 | { 436 | if (!array_key_exists($includeKey, $relationships)) { 437 | $relationships[$includeKey] = []; 438 | return $relationships; 439 | } 440 | 441 | return $relationships; 442 | } 443 | 444 | private function addIncludedDataToRelationship(array $includeObject, array $relationship): array 445 | { 446 | foreach ($includeObject['data'] as $object) { 447 | $relationship['data'][] = [ 448 | 'type' => $object['type'], 449 | 'id' => $object['id'], 450 | ]; 451 | } 452 | 453 | return $relationship; 454 | } 455 | 456 | /** 457 | * {@inheritdoc} 458 | */ 459 | public function injectAvailableIncludeData(array $data, array $availableIncludes): array 460 | { 461 | if (!$this->shouldIncludeLinks()) { 462 | return $data; 463 | } 464 | 465 | if ($this->isCollection($data)) { 466 | $data['data'] = array_map(function ($resource) use ($availableIncludes) { 467 | foreach ($availableIncludes as $relationshipKey) { 468 | $resource = $this->addRelationshipLinks($resource, $relationshipKey); 469 | } 470 | return $resource; 471 | }, $data['data']); 472 | } else { 473 | foreach ($availableIncludes as $relationshipKey) { 474 | $data['data'] = $this->addRelationshipLinks($data['data'], $relationshipKey); 475 | } 476 | } 477 | 478 | return $data; 479 | } 480 | 481 | /** 482 | * Adds links for all available includes to a single resource. 483 | * 484 | * @param array $resource The resource to add relationship links to 485 | * @param string $relationshipKey The resource key of the relationship 486 | */ 487 | private function addRelationshipLinks(array $resource, string $relationshipKey): array 488 | { 489 | if (!isset($resource['relationships']) || !isset($resource['relationships'][$relationshipKey])) { 490 | $resource['relationships'][$relationshipKey] = []; 491 | } 492 | 493 | $resource['relationships'][$relationshipKey] = array_merge( 494 | [ 495 | 'links' => [ 496 | 'self' => "{$this->baseUrl}/{$resource['type']}/{$resource['id']}/relationships/{$relationshipKey}", 497 | 'related' => "{$this->baseUrl}/{$resource['type']}/{$resource['id']}/{$relationshipKey}", 498 | ] 499 | ], 500 | $resource['relationships'][$relationshipKey] 501 | ); 502 | 503 | return $resource; 504 | } 505 | 506 | private function serializeIncludedObjectsWithCacheKey( 507 | array $includeObjects, 508 | array $linkedIds, 509 | array $serializedData 510 | ): array { 511 | foreach ($includeObjects as $object) { 512 | $includeType = $object['type']; 513 | $includeId = $object['id']; 514 | $cacheKey = "$includeType:$includeId"; 515 | if (!array_key_exists($cacheKey, $linkedIds)) { 516 | $serializedData[] = $object; 517 | $linkedIds[$cacheKey] = $object; 518 | } 519 | } 520 | return [$serializedData, $linkedIds]; 521 | } 522 | } 523 | -------------------------------------------------------------------------------- /src/Serializer/Serializer.php: -------------------------------------------------------------------------------- 1 | 7 | * 8 | * For the full copyright and license information, please view the LICENSE 9 | * file that was distributed with this source code. 10 | */ 11 | 12 | namespace League\Fractal\Serializer; 13 | 14 | use League\Fractal\Pagination\CursorInterface; 15 | use League\Fractal\Pagination\PaginatorInterface; 16 | use League\Fractal\Resource\ResourceInterface; 17 | 18 | abstract class SerializerAbstract implements Serializer 19 | { 20 | /** 21 | * {@inheritDoc} 22 | */ 23 | public function mergeIncludes(array $transformedData, array $includedData): array 24 | { 25 | // If the serializer does not want the includes to be side-loaded then 26 | // the included data must be merged with the transformed data. 27 | if (! $this->sideloadIncludes()) { 28 | return array_merge($transformedData, $includedData); 29 | } 30 | 31 | return $transformedData; 32 | } 33 | 34 | /** 35 | * {@inheritDoc} 36 | */ 37 | public function sideloadIncludes(): bool 38 | { 39 | return false; 40 | } 41 | 42 | /** 43 | * {@inheritDoc} 44 | */ 45 | public function injectData(array $data, array $rawIncludedData): array 46 | { 47 | return $data; 48 | } 49 | 50 | /** 51 | * {@inheritDoc} 52 | */ 53 | public function injectAvailableIncludeData(array $data, array $availableIncludes): array 54 | { 55 | return $data; 56 | } 57 | 58 | /** 59 | * {@inheritDoc} 60 | */ 61 | public function filterIncludes(array $includedData, array $data): array 62 | { 63 | return $includedData; 64 | } 65 | 66 | /** 67 | * {@inheritDoc} 68 | */ 69 | public function getMandatoryFields(): array 70 | { 71 | return []; 72 | } 73 | } 74 | -------------------------------------------------------------------------------- /src/TransformerAbstract.php: -------------------------------------------------------------------------------- 1 | 7 | * 8 | * For the full copyright and license information, please view the LICENSE 9 | * file that was distributed with this source code. 10 | */ 11 | 12 | namespace League\Fractal; 13 | 14 | use League\Fractal\Resource\Collection; 15 | use League\Fractal\Resource\Item; 16 | use League\Fractal\Resource\NullResource; 17 | use League\Fractal\Resource\Primitive; 18 | use League\Fractal\Resource\ResourceInterface; 19 | 20 | /** 21 | * All Transformer classes should extend this to utilize the convenience methods 22 | * collection() and item(), and make the self::$availableIncludes property available. 23 | * Extend it and add a `transform()` method to transform any default or included data 24 | * into a basic array. 25 | */ 26 | abstract class TransformerAbstract 27 | { 28 | /** 29 | * Resources that can be included if requested. 30 | */ 31 | protected array $availableIncludes = []; 32 | 33 | /** 34 | * Include resources without needing it to be requested. 35 | */ 36 | protected array $defaultIncludes = []; 37 | 38 | /** 39 | * The transformer should know about the current scope, so we can fetch relevant params. 40 | */ 41 | protected ?Scope $currentScope = null; 42 | 43 | /** 44 | * Getter for availableIncludes. 45 | */ 46 | public function getAvailableIncludes(): array 47 | { 48 | return $this->availableIncludes; 49 | } 50 | 51 | /** 52 | * Getter for defaultIncludes. 53 | */ 54 | public function getDefaultIncludes(): array 55 | { 56 | return $this->defaultIncludes; 57 | } 58 | 59 | /** 60 | * Getter for currentScope. 61 | */ 62 | public function getCurrentScope(): ?Scope 63 | { 64 | return $this->currentScope; 65 | } 66 | 67 | /** 68 | * Figure out which includes we need. 69 | */ 70 | private function figureOutWhichIncludes(Scope $scope): array 71 | { 72 | $includes = $this->getDefaultIncludes(); 73 | 74 | foreach ($this->getAvailableIncludes() as $include) { 75 | if ($scope->isRequested($include)) { 76 | $includes[] = $include; 77 | } 78 | } 79 | 80 | foreach ($includes as $include) { 81 | if ($scope->isExcluded($include)) { 82 | $includes = array_diff($includes, [$include]); 83 | } 84 | } 85 | 86 | return $includes; 87 | } 88 | 89 | /** 90 | * This method is fired to loop through available includes, see if any of 91 | * them are requested and permitted for this scope. 92 | * 93 | * @internal 94 | * 95 | * @param mixed $data 96 | * 97 | * @return array|false 98 | */ 99 | public function processIncludedResources(Scope $scope, $data) 100 | { 101 | $includedData = []; 102 | 103 | $includes = $this->figureOutWhichIncludes($scope); 104 | 105 | foreach ($includes as $include) { 106 | $includedData = $this->includeResourceIfAvailable( 107 | $scope, 108 | $data, 109 | $includedData, 110 | $include 111 | ); 112 | } 113 | 114 | return $includedData === [] ? false : $includedData; 115 | } 116 | 117 | /** 118 | * Include a resource only if it is available on the method. 119 | * 120 | * @param mixed $data 121 | */ 122 | private function includeResourceIfAvailable( 123 | Scope $scope, 124 | $data, 125 | array $includedData, 126 | string $include 127 | ): array { 128 | if ($resource = $this->callIncludeMethod($scope, $include, $data)) { 129 | $childScope = $scope->embedChildScope($include, $resource); 130 | 131 | if ($childScope->getResource() instanceof Primitive) { 132 | $includedData[$include] = $childScope->transformPrimitiveResource(); 133 | } else { 134 | $includedData[$include] = $childScope->toArray(); 135 | } 136 | } 137 | 138 | return $includedData; 139 | } 140 | 141 | /** 142 | * Call Include Method. 143 | * 144 | * @internal 145 | * 146 | * @param mixed $data 147 | * 148 | * @throws \Exception 149 | * 150 | * @return \League\Fractal\Resource\ResourceInterface|false 151 | */ 152 | protected function callIncludeMethod(Scope $scope, string $includeName, $data) 153 | { 154 | $scopeIdentifier = $scope->getIdentifier($includeName); 155 | 156 | $params = $scope->getManager()->getIncludeParams($scopeIdentifier); 157 | 158 | // Check if the method name actually exists 159 | $methodName = 'include'.str_replace( 160 | ' ', 161 | '', 162 | ucwords(str_replace( 163 | '_', 164 | ' ', 165 | str_replace( 166 | '-', 167 | ' ', 168 | $includeName 169 | ) 170 | )) 171 | ); 172 | 173 | $resource = call_user_func([$this, $methodName], $data, $params); 174 | 175 | if ($resource === null) { 176 | return false; 177 | } 178 | 179 | if (! $resource instanceof ResourceInterface) { 180 | throw new \Exception(sprintf( 181 | 'Invalid return value from %s::%s(). Expected %s, received %s.', 182 | __CLASS__, 183 | $methodName, 184 | 'League\Fractal\Resource\ResourceInterface', 185 | is_object($resource) ? get_class($resource) : gettype($resource) 186 | )); 187 | } 188 | 189 | return $resource; 190 | } 191 | 192 | /** 193 | * Setter for availableIncludes. 194 | */ 195 | public function setAvailableIncludes(array $availableIncludes): self 196 | { 197 | $this->availableIncludes = $availableIncludes; 198 | 199 | return $this; 200 | } 201 | 202 | /** 203 | * Setter for defaultIncludes. 204 | */ 205 | public function setDefaultIncludes(array $defaultIncludes): self 206 | { 207 | $this->defaultIncludes = $defaultIncludes; 208 | 209 | return $this; 210 | } 211 | 212 | /** 213 | * Setter for currentScope. 214 | */ 215 | public function setCurrentScope(Scope $currentScope): self 216 | { 217 | $this->currentScope = $currentScope; 218 | 219 | return $this; 220 | } 221 | 222 | /** 223 | * Create a new primitive resource object. 224 | * 225 | * @param mixed $data 226 | * @param callable|null $transformer 227 | */ 228 | protected function primitive($data, ?callable $transformer = null, ?string $resourceKey = null): Primitive 229 | { 230 | return new Primitive($data, $transformer, $resourceKey); 231 | } 232 | 233 | /** 234 | * Create a new item resource object. 235 | * 236 | * @param mixed $data 237 | * @param TransformerAbstract|callable $transformer 238 | */ 239 | protected function item($data, $transformer, ?string $resourceKey = null): Item 240 | { 241 | return new Item($data, $transformer, $resourceKey); 242 | } 243 | 244 | /** 245 | * Create a new collection resource object. 246 | * 247 | * @param mixed $data 248 | * @param TransformerAbstract|callable $transformer 249 | */ 250 | protected function collection($data, $transformer, ?string $resourceKey = null): Collection 251 | { 252 | return new Collection($data, $transformer, $resourceKey); 253 | } 254 | 255 | /** 256 | * Create a new null resource object. 257 | */ 258 | protected function null(): NullResource 259 | { 260 | return new NullResource(); 261 | } 262 | } 263 | --------------------------------------------------------------------------------