├── src ├── Resource │ ├── Primitive.php │ ├── Item.php │ ├── NullResource.php │ ├── ResourceInterface.php │ ├── Collection.php │ └── ResourceAbstract.php ├── Pagination │ ├── CursorInterface.php │ ├── PaginatorInterface.php │ ├── PhalconFrameworkPaginatorAdapter.php │ ├── IlluminatePaginatorAdapter.php │ ├── DoctrinePaginatorAdapter.php │ ├── PagerfantaPaginatorAdapter.php │ ├── ZendFrameworkPaginatorAdapter.php │ └── Cursor.php ├── ScopeFactoryInterface.php ├── Serializer │ ├── DataArraySerializer.php │ ├── Serializer.php │ ├── ArraySerializer.php │ ├── SerializerAbstract.php │ └── JsonApiSerializer.php ├── ScopeFactory.php ├── ParamBag.php ├── TransformerAbstract.php ├── Manager.php └── Scope.php ├── LICENSE └── composer.json /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 | } 24 | -------------------------------------------------------------------------------- /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 | * Item Resource 16 | * 17 | * The Item Resource can store any mixed data, usually an ORM, ODM or 18 | * other sort of intelligent result, DataMapper model, etc but could 19 | * be a basic array, object, or whatever you like. 20 | */ 21 | class Item extends ResourceAbstract 22 | { 23 | // 24 | } 25 | -------------------------------------------------------------------------------- /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 | * Null Resource 16 | * 17 | * The Null Resource represents a resource that doesn't exist. This can be 18 | * useful to indicate that a certain relationship is null in some output 19 | * formats (e.g. JSON API), which require even a relationship that is null at 20 | * the moment to be listed. 21 | */ 22 | class NullResource extends ResourceAbstract 23 | { 24 | /** 25 | * Get the data. 26 | * 27 | * @return mixed 28 | */ 29 | public function getData() 30 | { 31 | // Null has no data associated with it. 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /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 | public function getCurrent(); 27 | 28 | /** 29 | * Get the prev cursor value. 30 | * 31 | * @return mixed 32 | */ 33 | public function getPrev(); 34 | 35 | /** 36 | * Get the next cursor value. 37 | * 38 | * @return mixed 39 | */ 40 | public function getNext(); 41 | 42 | /** 43 | * Returns the total items in the current cursor. 44 | * 45 | * @return int 46 | */ 47 | public function getCount(); 48 | } 49 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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 | * ScopeFactoryInterface 18 | * 19 | * Creates Scope Instances for resources 20 | */ 21 | interface ScopeFactoryInterface 22 | { 23 | /** 24 | * @param Manager $manager 25 | * @param ResourceInterface $resource 26 | * @param string|null $scopeIdentifier 27 | * @return Scope 28 | */ 29 | public function createScopeFor(Manager $manager, ResourceInterface $resource, $scopeIdentifier = null); 30 | 31 | /** 32 | * @param Manager $manager 33 | * @param Scope $parentScope 34 | * @param ResourceInterface $resource 35 | * @param string|null $scopeIdentifier 36 | * @return Scope 37 | */ 38 | public function createChildScopeFor(Manager $manager, Scope $parentScope, ResourceInterface $resource, $scopeIdentifier = null); 39 | } 40 | -------------------------------------------------------------------------------- /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 | * Serialize a collection. 18 | * 19 | * @param string $resourceKey 20 | * @param array $data 21 | * 22 | * @return array 23 | */ 24 | public function collection($resourceKey, array $data) 25 | { 26 | return ['data' => $data]; 27 | } 28 | 29 | /** 30 | * Serialize an item. 31 | * 32 | * @param string $resourceKey 33 | * @param array $data 34 | * 35 | * @return array 36 | */ 37 | public function item($resourceKey, array $data) 38 | { 39 | return ['data' => $data]; 40 | } 41 | 42 | /** 43 | * Serialize null resource. 44 | * 45 | * @return array 46 | */ 47 | public function null() 48 | { 49 | return ['data' => []]; 50 | } 51 | } 52 | -------------------------------------------------------------------------------- /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 | * @return string 20 | */ 21 | public function getResourceKey(); 22 | 23 | /** 24 | * Get the data. 25 | * 26 | * @return mixed 27 | */ 28 | public function getData(); 29 | 30 | /** 31 | * Get the transformer. 32 | * 33 | * @return callable|\League\Fractal\TransformerAbstract 34 | */ 35 | public function getTransformer(); 36 | 37 | /** 38 | * Set the data. 39 | * 40 | * @param mixed $data 41 | * 42 | * @return $this 43 | */ 44 | public function setData($data); 45 | 46 | /** 47 | * Set the transformer. 48 | * 49 | * @param callable|\League\Fractal\TransformerAbstract $transformer 50 | * 51 | * @return $this 52 | */ 53 | public function setTransformer($transformer); 54 | } 55 | -------------------------------------------------------------------------------- /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 | * @return int 25 | */ 26 | public function getCurrentPage(); 27 | 28 | /** 29 | * Get the last page. 30 | * 31 | * @return int 32 | */ 33 | public function getLastPage(); 34 | 35 | /** 36 | * Get the total. 37 | * 38 | * @return int 39 | */ 40 | public function getTotal(); 41 | 42 | /** 43 | * Get the count. 44 | * 45 | * @return int 46 | */ 47 | public function getCount(); 48 | 49 | /** 50 | * Get the number per page. 51 | * 52 | * @return int 53 | */ 54 | public function getPerPage(); 55 | 56 | /** 57 | * Get the url for the given page. 58 | * 59 | * @param int $page 60 | * 61 | * @return string 62 | */ 63 | public function getUrl($page); 64 | } 65 | -------------------------------------------------------------------------------- /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": ">=5.4" 25 | }, 26 | "require-dev": { 27 | "doctrine/orm": "^2.5", 28 | "illuminate/contracts": "~5.0", 29 | "mockery/mockery": "~0.9", 30 | "pagerfanta/pagerfanta": "~1.0.0", 31 | "phpunit/phpunit": "~4.0", 32 | "squizlabs/php_codesniffer": "~1.5", 33 | "zendframework/zend-paginator": "~2.3" 34 | }, 35 | "suggest": { 36 | "illuminate/pagination": "The Illuminate Pagination component.", 37 | "pagerfanta/pagerfanta": "Pagerfanta Paginator", 38 | "zendframework/zend-paginator": "Zend Framework Paginator" 39 | }, 40 | "autoload": { 41 | "psr-4": { 42 | "League\\Fractal\\": "src" 43 | } 44 | }, 45 | "autoload-dev": { 46 | "psr-4": { 47 | "League\\Fractal\\Test\\": "test" 48 | } 49 | }, 50 | "extra": { 51 | "branch-alias": { 52 | "dev-master": "0.13-dev" 53 | } 54 | } 55 | } 56 | -------------------------------------------------------------------------------- /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 | /** 19 | * @param Manager $manager 20 | * @param ResourceInterface $resource 21 | * @param string|null $scopeIdentifier 22 | * @return Scope 23 | */ 24 | public function createScopeFor(Manager $manager, ResourceInterface $resource, $scopeIdentifier = null) 25 | { 26 | return new Scope($manager, $resource, $scopeIdentifier); 27 | } 28 | 29 | /** 30 | * @param Manager $manager 31 | * @param Scope $parentScopeInstance 32 | * @param ResourceInterface $resource 33 | * @param string|null $scopeIdentifier 34 | * @return Scope 35 | */ 36 | public function createChildScopeFor(Manager $manager, Scope $parentScopeInstance, ResourceInterface $resource, $scopeIdentifier = null) 37 | { 38 | $scopeInstance = $this->createScopeFor($manager, $resource, $scopeIdentifier); 39 | 40 | // This will be the new children list of parents (parents parents, plus the parent) 41 | $scopeArray = $parentScopeInstance->getParentScopes(); 42 | $scopeArray[] = $parentScopeInstance->getScopeIdentifier(); 43 | 44 | $scopeInstance->setParentScopes($scopeArray); 45 | 46 | return $scopeInstance; 47 | } 48 | } 49 | -------------------------------------------------------------------------------- /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 | * 19 | */ 20 | class PhalconFrameworkPaginatorAdapter implements PaginatorInterface 21 | { 22 | /** 23 | * A slice of the result set to show in the pagination 24 | * 25 | * @var \Phalcon\Paginator\AdapterInterface 26 | */ 27 | private $paginator; 28 | 29 | 30 | public function __construct($paginator) 31 | { 32 | $this->paginator = $paginator->getPaginate(); 33 | } 34 | 35 | /** 36 | * Get the current page. 37 | * 38 | * @return int 39 | */ 40 | public function getCurrentPage() 41 | { 42 | return $this->paginator->current; 43 | } 44 | 45 | /** 46 | * Get the last page. 47 | * 48 | * @return int 49 | */ 50 | public function getLastPage() 51 | { 52 | return $this->paginator->last; 53 | } 54 | 55 | /** 56 | * Get the total. 57 | * 58 | * @return int 59 | */ 60 | public function getTotal() 61 | { 62 | return $this->paginator->total_items; 63 | } 64 | 65 | /** 66 | * Get the count. 67 | * 68 | * @return int 69 | */ 70 | public function getCount() 71 | { 72 | return $this->paginator->total_pages; 73 | } 74 | 75 | /** 76 | * Get the number per page. 77 | * 78 | * @return int 79 | */ 80 | public function getPerPage() 81 | { 82 | // $this->paginator->items->count() 83 | // Because when we use raw sql have not this method 84 | return count($this->paginator->items); 85 | } 86 | 87 | /** 88 | * Get the next. 89 | * 90 | * @return int 91 | */ 92 | public function getNext() 93 | { 94 | return $this->paginator->next; 95 | } 96 | 97 | /** 98 | * Get the url for the given page. 99 | * 100 | * @param int $page 101 | * 102 | * @return string 103 | */ 104 | public function getUrl($page) 105 | { 106 | return $page; 107 | } 108 | } 109 | -------------------------------------------------------------------------------- /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\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 | /** 34 | * The paginator instance. 35 | * 36 | * @var PaginatorInterface 37 | */ 38 | protected $paginator; 39 | 40 | /** 41 | * The cursor instance. 42 | * 43 | * @var CursorInterface 44 | */ 45 | protected $cursor; 46 | 47 | /** 48 | * Get the paginator instance. 49 | * 50 | * @return PaginatorInterface 51 | */ 52 | public function getPaginator() 53 | { 54 | return $this->paginator; 55 | } 56 | 57 | /** 58 | * Determine if the resource has a paginator implementation. 59 | * 60 | * @return bool 61 | */ 62 | public function hasPaginator() 63 | { 64 | return $this->paginator instanceof PaginatorInterface; 65 | } 66 | 67 | /** 68 | * Get the cursor instance. 69 | * 70 | * @return CursorInterface 71 | */ 72 | public function getCursor() 73 | { 74 | return $this->cursor; 75 | } 76 | 77 | /** 78 | * Determine if the resource has a cursor implementation. 79 | * 80 | * @return bool 81 | */ 82 | public function hasCursor() 83 | { 84 | return $this->cursor instanceof CursorInterface; 85 | } 86 | 87 | /** 88 | * Set the paginator instance. 89 | * 90 | * @param PaginatorInterface $paginator 91 | * 92 | * @return $this 93 | */ 94 | public function setPaginator(PaginatorInterface $paginator) 95 | { 96 | $this->paginator = $paginator; 97 | 98 | return $this; 99 | } 100 | 101 | /** 102 | * Set the cursor instance. 103 | * 104 | * @param CursorInterface $cursor 105 | * 106 | * @return $this 107 | */ 108 | public function setCursor(CursorInterface $cursor) 109 | { 110 | $this->cursor = $cursor; 111 | 112 | return $this; 113 | } 114 | } 115 | -------------------------------------------------------------------------------- /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 | /** 25 | * The paginator instance. 26 | * 27 | * @var \Illuminate\Contracts\Pagination\LengthAwarePaginator 28 | */ 29 | protected $paginator; 30 | 31 | /** 32 | * Create a new illuminate pagination adapter. 33 | * 34 | * @param \Illuminate\Contracts\Pagination\LengthAwarePaginator $paginator 35 | * 36 | * @return void 37 | */ 38 | public function __construct(LengthAwarePaginator $paginator) 39 | { 40 | $this->paginator = $paginator; 41 | } 42 | 43 | /** 44 | * Get the current page. 45 | * 46 | * @return int 47 | */ 48 | public function getCurrentPage() 49 | { 50 | return $this->paginator->currentPage(); 51 | } 52 | 53 | /** 54 | * Get the last page. 55 | * 56 | * @return int 57 | */ 58 | public function getLastPage() 59 | { 60 | return $this->paginator->lastPage(); 61 | } 62 | 63 | /** 64 | * Get the total. 65 | * 66 | * @return int 67 | */ 68 | public function getTotal() 69 | { 70 | return $this->paginator->total(); 71 | } 72 | 73 | /** 74 | * Get the count. 75 | * 76 | * @return int 77 | */ 78 | public function getCount() 79 | { 80 | return $this->paginator->count(); 81 | } 82 | 83 | /** 84 | * Get the number per page. 85 | * 86 | * @return int 87 | */ 88 | public function getPerPage() 89 | { 90 | return $this->paginator->perPage(); 91 | } 92 | 93 | /** 94 | * Get the url for the given page. 95 | * 96 | * @param int $page 97 | * 98 | * @return string 99 | */ 100 | public function getUrl($page) 101 | { 102 | return $this->paginator->url($page); 103 | } 104 | 105 | /** 106 | * Get the paginator instance. 107 | * 108 | * @return \Illuminate\Contracts\Pagination\LengthAwarePaginator 109 | */ 110 | public function getPaginator() 111 | { 112 | return $this->paginator; 113 | } 114 | } 115 | -------------------------------------------------------------------------------- /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 | /** 23 | * The paginator instance. 24 | * @var Paginator 25 | */ 26 | private $paginator; 27 | 28 | /** 29 | * The route generator. 30 | * 31 | * @var callable 32 | */ 33 | private $routeGenerator; 34 | 35 | /** 36 | * Create a new DoctrinePaginatorAdapter. 37 | * @param Paginator $paginator 38 | * @param callable $routeGenerator 39 | * 40 | */ 41 | public function __construct(Paginator $paginator, callable $routeGenerator) 42 | { 43 | $this->paginator = $paginator; 44 | $this->routeGenerator = $routeGenerator; 45 | } 46 | 47 | /** 48 | * Get the current page. 49 | * 50 | * @return int 51 | */ 52 | public function getCurrentPage() 53 | { 54 | return ($this->paginator->getQuery()->getFirstResult() / $this->paginator->getQuery()->getMaxResults()) + 1; 55 | } 56 | 57 | /** 58 | * Get the last page. 59 | * 60 | * @return int 61 | */ 62 | public function getLastPage() 63 | { 64 | return (int) ceil($this->getTotal() / $this->paginator->getQuery()->getMaxResults()); 65 | } 66 | 67 | /** 68 | * Get the total. 69 | * 70 | * @return int 71 | */ 72 | public function getTotal() 73 | { 74 | return count($this->paginator); 75 | } 76 | 77 | /** 78 | * Get the count. 79 | * 80 | * @return int 81 | */ 82 | public function getCount() 83 | { 84 | return $this->paginator->getIterator()->count(); 85 | } 86 | 87 | /** 88 | * Get the number per page. 89 | * 90 | * @return int 91 | */ 92 | public function getPerPage() 93 | { 94 | return $this->paginator->getQuery()->getMaxResults(); 95 | } 96 | 97 | /** 98 | * Get the url for the given page. 99 | * 100 | * @param int $page 101 | * 102 | * @return string 103 | */ 104 | public function getUrl($page) 105 | { 106 | return call_user_func($this->getRouteGenerator(), $page); 107 | } 108 | 109 | /** 110 | * Get the the route generator. 111 | * 112 | * @return callable 113 | */ 114 | private function getRouteGenerator() 115 | { 116 | return $this->routeGenerator; 117 | } 118 | } 119 | -------------------------------------------------------------------------------- /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 | /** 24 | * The paginator instance. 25 | * 26 | * @var \Pagerfanta\Pagerfanta 27 | */ 28 | protected $paginator; 29 | 30 | /** 31 | * The route generator. 32 | * 33 | * @var callable 34 | */ 35 | protected $routeGenerator; 36 | 37 | /** 38 | * Create a new pagerfanta pagination adapter. 39 | * 40 | * @param \Pagerfanta\Pagerfanta $paginator 41 | * @param callable $routeGenerator 42 | * 43 | * @return void 44 | */ 45 | public function __construct(Pagerfanta $paginator, $routeGenerator) 46 | { 47 | $this->paginator = $paginator; 48 | $this->routeGenerator = $routeGenerator; 49 | } 50 | 51 | /** 52 | * Get the current page. 53 | * 54 | * @return int 55 | */ 56 | public function getCurrentPage() 57 | { 58 | return $this->paginator->getCurrentPage(); 59 | } 60 | 61 | /** 62 | * Get the last page. 63 | * 64 | * @return int 65 | */ 66 | public function getLastPage() 67 | { 68 | return $this->paginator->getNbPages(); 69 | } 70 | 71 | /** 72 | * Get the total. 73 | * 74 | * @return int 75 | */ 76 | public function getTotal() 77 | { 78 | return count($this->paginator); 79 | } 80 | 81 | /** 82 | * Get the count. 83 | * 84 | * @return int 85 | */ 86 | public function getCount() 87 | { 88 | return count($this->paginator->getCurrentPageResults()); 89 | } 90 | 91 | /** 92 | * Get the number per page. 93 | * 94 | * @return int 95 | */ 96 | public function getPerPage() 97 | { 98 | return $this->paginator->getMaxPerPage(); 99 | } 100 | 101 | /** 102 | * Get the url for the given page. 103 | * 104 | * @param int $page 105 | * 106 | * @return string 107 | */ 108 | public function getUrl($page) 109 | { 110 | return call_user_func($this->routeGenerator, $page); 111 | } 112 | 113 | /** 114 | * Get the paginator instance. 115 | * 116 | * @return \Pagerfanta\Pagerfanta 117 | */ 118 | public function getPaginator() 119 | { 120 | return $this->paginator; 121 | } 122 | 123 | /** 124 | * Get the the route generator. 125 | * 126 | * @return callable 127 | */ 128 | public function getRouteGenerator() 129 | { 130 | return $this->routeGenerator; 131 | } 132 | } 133 | -------------------------------------------------------------------------------- /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 | /** 24 | * The paginator instance. 25 | * 26 | * @var \Zend\Paginator\Paginator 27 | */ 28 | protected $paginator; 29 | 30 | /** 31 | * The route generator. 32 | * 33 | * @var callable 34 | */ 35 | protected $routeGenerator; 36 | 37 | /** 38 | * Create a new zendframework pagination adapter. 39 | * 40 | * @param \Zend\Paginator\Paginator $paginator 41 | * @param callable $routeGenerator 42 | * 43 | * @return void 44 | */ 45 | public function __construct(Paginator $paginator, $routeGenerator) 46 | { 47 | $this->paginator = $paginator; 48 | $this->routeGenerator = $routeGenerator; 49 | } 50 | 51 | /** 52 | * Get the current page. 53 | * 54 | * @return int 55 | */ 56 | public function getCurrentPage() 57 | { 58 | return $this->paginator->getCurrentPageNumber(); 59 | } 60 | 61 | /** 62 | * Get the last page. 63 | * 64 | * @return int 65 | */ 66 | public function getLastPage() 67 | { 68 | return $this->paginator->count(); 69 | } 70 | 71 | /** 72 | * Get the total. 73 | * 74 | * @return int 75 | */ 76 | public function getTotal() 77 | { 78 | return $this->paginator->getTotalItemCount(); 79 | } 80 | 81 | /** 82 | * Get the count. 83 | * 84 | * @return int 85 | */ 86 | public function getCount() 87 | { 88 | return $this->paginator->getCurrentItemCount(); 89 | } 90 | 91 | /** 92 | * Get the number per page. 93 | * 94 | * @return int 95 | */ 96 | public function getPerPage() 97 | { 98 | return $this->paginator->getItemCountPerPage(); 99 | } 100 | 101 | /** 102 | * Get the url for the given page. 103 | * 104 | * @param int $page 105 | * 106 | * @return string 107 | */ 108 | public function getUrl($page) 109 | { 110 | return call_user_func($this->routeGenerator, $page); 111 | } 112 | 113 | /** 114 | * Get the paginator instance. 115 | * 116 | * @return \Zend\Paginator\Paginator 117 | */ 118 | public function getPaginator() 119 | { 120 | return $this->paginator; 121 | } 122 | 123 | /** 124 | * Get the the route generator. 125 | * 126 | * @return callable 127 | */ 128 | public function getRouteGenerator() 129 | { 130 | return $this->routeGenerator; 131 | } 132 | } 133 | -------------------------------------------------------------------------------- /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 | * @var int 47 | */ 48 | protected $count; 49 | 50 | /** 51 | * Create a new Cursor instance. 52 | * 53 | * @param int $current 54 | * @param null $prev 55 | * @param mixed $next 56 | * @param int $count 57 | * 58 | * @return void 59 | */ 60 | public function __construct($current = null, $prev = null, $next = null, $count = null) 61 | { 62 | $this->current = $current; 63 | $this->prev = $prev; 64 | $this->next = $next; 65 | $this->count = $count; 66 | } 67 | 68 | /** 69 | * Get the current cursor value. 70 | * 71 | * @return mixed 72 | */ 73 | public function getCurrent() 74 | { 75 | return $this->current; 76 | } 77 | 78 | /** 79 | * Set the current cursor value. 80 | * 81 | * @param int $current 82 | * 83 | * @return Cursor 84 | */ 85 | public function setCurrent($current) 86 | { 87 | $this->current = $current; 88 | 89 | return $this; 90 | } 91 | 92 | /** 93 | * Get the prev cursor value. 94 | * 95 | * @return mixed 96 | */ 97 | public function getPrev() 98 | { 99 | return $this->prev; 100 | } 101 | 102 | /** 103 | * Set the prev cursor value. 104 | * 105 | * @param int $prev 106 | * 107 | * @return Cursor 108 | */ 109 | public function setPrev($prev) 110 | { 111 | $this->prev = $prev; 112 | 113 | return $this; 114 | } 115 | 116 | /** 117 | * Get the next cursor value. 118 | * 119 | * @return mixed 120 | */ 121 | public function getNext() 122 | { 123 | return $this->next; 124 | } 125 | 126 | /** 127 | * Set the next cursor value. 128 | * 129 | * @param int $next 130 | * 131 | * @return Cursor 132 | */ 133 | public function setNext($next) 134 | { 135 | $this->next = $next; 136 | 137 | return $this; 138 | } 139 | 140 | /** 141 | * Returns the total items in the current cursor. 142 | * 143 | * @return int 144 | */ 145 | public function getCount() 146 | { 147 | return $this->count; 148 | } 149 | 150 | /** 151 | * Set the total items in the current cursor. 152 | * 153 | * @param int $count 154 | * 155 | * @return Cursor 156 | */ 157 | public function setCount($count) 158 | { 159 | $this->count = $count; 160 | 161 | return $this; 162 | } 163 | } 164 | -------------------------------------------------------------------------------- /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 | * Serialize a collection. 22 | * 23 | * @param string $resourceKey 24 | * @param array $data 25 | * 26 | * @return array 27 | */ 28 | public function collection($resourceKey, array $data) 29 | { 30 | return [$resourceKey ?: 'data' => $data]; 31 | } 32 | 33 | /** 34 | * Serialize an item. 35 | * 36 | * @param string $resourceKey 37 | * @param array $data 38 | * 39 | * @return array 40 | */ 41 | public function item($resourceKey, array $data) 42 | { 43 | return $data; 44 | } 45 | 46 | /** 47 | * Serialize null resource. 48 | * 49 | * @return array 50 | */ 51 | public function null() 52 | { 53 | return []; 54 | } 55 | 56 | /** 57 | * Serialize the included data. 58 | * 59 | * @param ResourceInterface $resource 60 | * @param array $data 61 | * 62 | * @return array 63 | */ 64 | public function includedData(ResourceInterface $resource, array $data) 65 | { 66 | return $data; 67 | } 68 | 69 | /** 70 | * Serialize the meta. 71 | * 72 | * @param array $meta 73 | * 74 | * @return array 75 | */ 76 | public function meta(array $meta) 77 | { 78 | if (empty($meta)) { 79 | return []; 80 | } 81 | 82 | return ['meta' => $meta]; 83 | } 84 | 85 | /** 86 | * Serialize the paginator. 87 | * 88 | * @param PaginatorInterface $paginator 89 | * 90 | * @return array 91 | */ 92 | public function paginator(PaginatorInterface $paginator) 93 | { 94 | $currentPage = (int) $paginator->getCurrentPage(); 95 | $lastPage = (int) $paginator->getLastPage(); 96 | 97 | $pagination = [ 98 | 'total' => (int) $paginator->getTotal(), 99 | 'count' => (int) $paginator->getCount(), 100 | 'per_page' => (int) $paginator->getPerPage(), 101 | 'current_page' => $currentPage, 102 | 'total_pages' => $lastPage, 103 | ]; 104 | 105 | $pagination['links'] = []; 106 | 107 | if ($currentPage > 1) { 108 | $pagination['links']['previous'] = $paginator->getUrl($currentPage - 1); 109 | } 110 | 111 | if ($currentPage < $lastPage) { 112 | $pagination['links']['next'] = $paginator->getUrl($currentPage + 1); 113 | } 114 | 115 | if (empty($pagination['links'])) { 116 | $pagination['links'] = (object) []; 117 | } 118 | 119 | return ['pagination' => $pagination]; 120 | } 121 | 122 | /** 123 | * Serialize the cursor. 124 | * 125 | * @param CursorInterface $cursor 126 | * 127 | * @return array 128 | */ 129 | public function cursor(CursorInterface $cursor) 130 | { 131 | $cursor = [ 132 | 'current' => $cursor->getCurrent(), 133 | 'prev' => $cursor->getPrev(), 134 | 'next' => $cursor->getNext(), 135 | 'count' => (int) $cursor->getCount(), 136 | ]; 137 | 138 | return ['cursor' => $cursor]; 139 | } 140 | } 141 | -------------------------------------------------------------------------------- /src/Serializer/SerializerAbstract.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 | * Serialize a collection. 22 | * 23 | * @param string $resourceKey 24 | * @param array $data 25 | * 26 | * @return array 27 | */ 28 | abstract public function collection($resourceKey, array $data); 29 | 30 | /** 31 | * Serialize an item. 32 | * 33 | * @param string $resourceKey 34 | * @param array $data 35 | * 36 | * @return array 37 | */ 38 | abstract public function item($resourceKey, array $data); 39 | 40 | /** 41 | * Serialize null resource. 42 | * 43 | * @return array 44 | */ 45 | abstract public function null(); 46 | 47 | /** 48 | * Serialize the included data. 49 | * 50 | * @param ResourceInterface $resource 51 | * @param array $data 52 | * 53 | * @return array 54 | */ 55 | abstract public function includedData(ResourceInterface $resource, array $data); 56 | 57 | /** 58 | * Serialize the meta. 59 | * 60 | * @param array $meta 61 | * 62 | * @return array 63 | */ 64 | abstract public function meta(array $meta); 65 | 66 | /** 67 | * Serialize the paginator. 68 | * 69 | * @param PaginatorInterface $paginator 70 | * 71 | * @return array 72 | */ 73 | abstract public function paginator(PaginatorInterface $paginator); 74 | 75 | /** 76 | * Serialize the cursor. 77 | * 78 | * @param CursorInterface $cursor 79 | * 80 | * @return array 81 | */ 82 | abstract public function cursor(CursorInterface $cursor); 83 | 84 | public function mergeIncludes($transformedData, $includedData) 85 | { 86 | // If the serializer does not want the includes to be side-loaded then 87 | // the included data must be merged with the transformed data. 88 | if (! $this->sideloadIncludes()) { 89 | return array_merge($transformedData, $includedData); 90 | } 91 | 92 | return $transformedData; 93 | } 94 | 95 | /** 96 | * Indicates if includes should be side-loaded. 97 | * 98 | * @return bool 99 | */ 100 | public function sideloadIncludes() 101 | { 102 | return false; 103 | } 104 | 105 | /** 106 | * Hook for the serializer to inject custom data based on the relationships of the resource. 107 | * 108 | * @param array $data 109 | * @param array $rawIncludedData 110 | * 111 | * @return array 112 | */ 113 | public function injectData($data, $rawIncludedData) 114 | { 115 | return $data; 116 | } 117 | 118 | /** 119 | * Hook for the serializer to modify the final list of includes. 120 | * 121 | * @param array $includedData 122 | * @param array $data 123 | * 124 | * @return array 125 | */ 126 | public function filterIncludes($includedData, $data) 127 | { 128 | return $includedData; 129 | } 130 | 131 | /** 132 | * Get the mandatory fields for the serializer 133 | * 134 | * @return array 135 | */ 136 | public function getMandatoryFields() 137 | { 138 | return []; 139 | } 140 | } 141 | -------------------------------------------------------------------------------- /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 | /** 20 | * @var array 21 | */ 22 | protected $params = []; 23 | 24 | /** 25 | * Create a new parameter bag instance. 26 | * 27 | * @param array $params 28 | * 29 | * @return void 30 | */ 31 | public function __construct(array $params) 32 | { 33 | $this->params = $params; 34 | } 35 | 36 | /** 37 | * Get parameter values out of the bag. 38 | * 39 | * @param string $key 40 | * 41 | * @return mixed 42 | */ 43 | public function get($key) 44 | { 45 | return $this->__get($key); 46 | } 47 | 48 | /** 49 | * Get parameter values out of the bag via the property access magic method. 50 | * 51 | * @param string $key 52 | * 53 | * @return mixed 54 | */ 55 | public function __get($key) 56 | { 57 | return isset($this->params[$key]) ? $this->params[$key] : null; 58 | } 59 | 60 | /** 61 | * Check if a param exists in the bag via an isset() check on the property. 62 | * 63 | * @param string $key 64 | * 65 | * @return bool 66 | */ 67 | public function __isset($key) 68 | { 69 | return isset($this->params[$key]); 70 | } 71 | 72 | /** 73 | * Disallow changing the value of params in the data bag via property access. 74 | * 75 | * @param string $key 76 | * @param mixed $value 77 | * 78 | * @throws \LogicException 79 | * 80 | * @return void 81 | */ 82 | public function __set($key, $value) 83 | { 84 | throw new \LogicException('Modifying parameters is not permitted'); 85 | } 86 | 87 | /** 88 | * Disallow unsetting params in the data bag via property access. 89 | * 90 | * @param string $key 91 | * 92 | * @throws \LogicException 93 | * 94 | * @return void 95 | */ 96 | public function __unset($key) 97 | { 98 | throw new \LogicException('Modifying parameters is not permitted'); 99 | } 100 | 101 | /** 102 | * Check if a param exists in the bag via an isset() and array access. 103 | * 104 | * @param string $key 105 | * 106 | * @return bool 107 | */ 108 | public function offsetExists($key) 109 | { 110 | return $this->__isset($key); 111 | } 112 | 113 | /** 114 | * Get parameter values out of the bag via array access. 115 | * 116 | * @param string $key 117 | * 118 | * @return mixed 119 | */ 120 | public function offsetGet($key) 121 | { 122 | return $this->__get($key); 123 | } 124 | 125 | /** 126 | * Disallow changing the value of params in the data bag via array access. 127 | * 128 | * @param string $key 129 | * @param mixed $value 130 | * 131 | * @throws \LogicException 132 | * 133 | * @return void 134 | */ 135 | public function offsetSet($key, $value) 136 | { 137 | throw new \LogicException('Modifying parameters is not permitted'); 138 | } 139 | 140 | /** 141 | * Disallow unsetting params in the data bag via array access. 142 | * 143 | * @param string $key 144 | * 145 | * @throws \LogicException 146 | * 147 | * @return void 148 | */ 149 | public function offsetUnset($key) 150 | { 151 | throw new \LogicException('Modifying parameters is not permitted'); 152 | } 153 | 154 | /** 155 | * IteratorAggregate for iterating over the object like an array. 156 | * 157 | * @return \ArrayIterator 158 | */ 159 | public function getIterator() 160 | { 161 | return new \ArrayIterator($this->params); 162 | } 163 | } 164 | -------------------------------------------------------------------------------- /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 | * @var array 29 | */ 30 | protected $meta = []; 31 | 32 | /** 33 | * The resource key. 34 | * 35 | * @var string 36 | */ 37 | protected $resourceKey; 38 | 39 | /** 40 | * A callable to process the data attached to this resource. 41 | * 42 | * @var callable|TransformerAbstract|null 43 | */ 44 | protected $transformer; 45 | 46 | /** 47 | * Create a new resource instance. 48 | * 49 | * @param mixed $data 50 | * @param callable|TransformerAbstract|null $transformer 51 | * @param string $resourceKey 52 | */ 53 | public function __construct($data = null, $transformer = null, $resourceKey = null) 54 | { 55 | $this->data = $data; 56 | $this->transformer = $transformer; 57 | $this->resourceKey = $resourceKey; 58 | } 59 | 60 | /** 61 | * Get the data. 62 | * 63 | * @return mixed 64 | */ 65 | public function getData() 66 | { 67 | return $this->data; 68 | } 69 | 70 | /** 71 | * Set the data. 72 | * 73 | * @param mixed $data 74 | * 75 | * @return $this 76 | */ 77 | public function setData($data) 78 | { 79 | $this->data = $data; 80 | 81 | return $this; 82 | } 83 | 84 | /** 85 | * Get the meta data. 86 | * 87 | * @return array 88 | */ 89 | public function getMeta() 90 | { 91 | return $this->meta; 92 | } 93 | 94 | /** 95 | * Get the meta data. 96 | * 97 | * @param string $metaKey 98 | * 99 | * @return array 100 | */ 101 | public function getMetaValue($metaKey) 102 | { 103 | return $this->meta[$metaKey]; 104 | } 105 | 106 | /** 107 | * Get the resource key. 108 | * 109 | * @return string 110 | */ 111 | public function getResourceKey() 112 | { 113 | return $this->resourceKey; 114 | } 115 | 116 | /** 117 | * Get the transformer. 118 | * 119 | * @return callable|TransformerAbstract 120 | */ 121 | public function getTransformer() 122 | { 123 | return $this->transformer; 124 | } 125 | 126 | /** 127 | * Set the transformer. 128 | * 129 | * @param callable|TransformerAbstract $transformer 130 | * 131 | * @return $this 132 | */ 133 | public function setTransformer($transformer) 134 | { 135 | $this->transformer = $transformer; 136 | 137 | return $this; 138 | } 139 | 140 | /** 141 | * Set the meta data. 142 | * 143 | * @param array $meta 144 | * 145 | * @return $this 146 | */ 147 | public function setMeta(array $meta) 148 | { 149 | $this->meta = $meta; 150 | 151 | return $this; 152 | } 153 | 154 | /** 155 | * Set the meta data. 156 | * 157 | * @param string $metaKey 158 | * @param mixed $metaValue 159 | * 160 | * @return $this 161 | */ 162 | public function setMetaValue($metaKey, $metaValue) 163 | { 164 | $this->meta[$metaKey] = $metaValue; 165 | 166 | return $this; 167 | } 168 | 169 | /** 170 | * Set the resource key. 171 | * 172 | * @param string $resourceKey 173 | * 174 | * @return $this 175 | */ 176 | public function setResourceKey($resourceKey) 177 | { 178 | $this->resourceKey = $resourceKey; 179 | 180 | return $this; 181 | } 182 | } 183 | -------------------------------------------------------------------------------- /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 | * Transformer Abstract 22 | * 23 | * All Transformer classes should extend this to utilize the convenience methods 24 | * collection() and item(), and make the self::$availableIncludes property available. 25 | * Extend it and add a `transform()` method to transform any default or included data 26 | * into a basic array. 27 | */ 28 | abstract class TransformerAbstract 29 | { 30 | /** 31 | * Resources that can be included if requested. 32 | * 33 | * @var array 34 | */ 35 | protected $availableIncludes = []; 36 | 37 | /** 38 | * Include resources without needing it to be requested. 39 | * 40 | * @var array 41 | */ 42 | protected $defaultIncludes = []; 43 | 44 | /** 45 | * The transformer should know about the current scope, so we can fetch relevant params. 46 | * 47 | * @var Scope 48 | */ 49 | protected $currentScope; 50 | 51 | /** 52 | * Getter for availableIncludes. 53 | * 54 | * @return array 55 | */ 56 | public function getAvailableIncludes() 57 | { 58 | return $this->availableIncludes; 59 | } 60 | 61 | /** 62 | * Getter for defaultIncludes. 63 | * 64 | * @return array 65 | */ 66 | public function getDefaultIncludes() 67 | { 68 | return $this->defaultIncludes; 69 | } 70 | 71 | /** 72 | * Getter for currentScope. 73 | * 74 | * @return \League\Fractal\Scope 75 | */ 76 | public function getCurrentScope() 77 | { 78 | return $this->currentScope; 79 | } 80 | 81 | /** 82 | * Figure out which includes we need. 83 | * 84 | * @internal 85 | * 86 | * @param Scope $scope 87 | * 88 | * @return array 89 | */ 90 | private function figureOutWhichIncludes(Scope $scope) 91 | { 92 | $includes = $this->getDefaultIncludes(); 93 | 94 | foreach ($this->getAvailableIncludes() as $include) { 95 | if ($scope->isRequested($include)) { 96 | $includes[] = $include; 97 | } 98 | } 99 | 100 | foreach ($includes as $include) { 101 | if ($scope->isExcluded($include)) { 102 | $includes = array_diff($includes, [$include]); 103 | } 104 | } 105 | 106 | return $includes; 107 | } 108 | 109 | /** 110 | * This method is fired to loop through available includes, see if any of 111 | * them are requested and permitted for this scope. 112 | * 113 | * @internal 114 | * 115 | * @param Scope $scope 116 | * @param mixed $data 117 | * 118 | * @return array 119 | */ 120 | public function processIncludedResources(Scope $scope, $data) 121 | { 122 | $includedData = []; 123 | 124 | $includes = $this->figureOutWhichIncludes($scope); 125 | 126 | foreach ($includes as $include) { 127 | $includedData = $this->includeResourceIfAvailable( 128 | $scope, 129 | $data, 130 | $includedData, 131 | $include 132 | ); 133 | } 134 | 135 | return $includedData === [] ? false : $includedData; 136 | } 137 | 138 | /** 139 | * Include a resource only if it is available on the method. 140 | * 141 | * @internal 142 | * 143 | * @param Scope $scope 144 | * @param mixed $data 145 | * @param array $includedData 146 | * @param string $include 147 | * 148 | * @return array 149 | */ 150 | private function includeResourceIfAvailable( 151 | Scope $scope, 152 | $data, 153 | $includedData, 154 | $include 155 | ) { 156 | if ($resource = $this->callIncludeMethod($scope, $include, $data)) { 157 | $childScope = $scope->embedChildScope($include, $resource); 158 | 159 | if ($childScope->getResource() instanceof Primitive) { 160 | $includedData[$include] = $childScope->transformPrimitiveResource(); 161 | } else { 162 | $includedData[$include] = $childScope->toArray(); 163 | } 164 | } 165 | 166 | return $includedData; 167 | } 168 | 169 | /** 170 | * Call Include Method. 171 | * 172 | * @internal 173 | * 174 | * @param Scope $scope 175 | * @param string $includeName 176 | * @param mixed $data 177 | * 178 | * @throws \Exception 179 | * 180 | * @return \League\Fractal\Resource\ResourceInterface 181 | */ 182 | protected function callIncludeMethod(Scope $scope, $includeName, $data) 183 | { 184 | $scopeIdentifier = $scope->getIdentifier($includeName); 185 | $params = $scope->getManager()->getIncludeParams($scopeIdentifier); 186 | 187 | // Check if the method name actually exists 188 | $methodName = 'include'.str_replace(' ', '', ucwords(str_replace('_', ' ', str_replace('-', ' ', $includeName)))); 189 | 190 | $resource = call_user_func([$this, $methodName], $data, $params); 191 | 192 | if ($resource === null) { 193 | return false; 194 | } 195 | 196 | if (! $resource instanceof ResourceInterface) { 197 | throw new \Exception(sprintf( 198 | 'Invalid return value from %s::%s(). Expected %s, received %s.', 199 | __CLASS__, 200 | $methodName, 201 | 'League\Fractal\Resource\ResourceInterface', 202 | is_object($resource) ? get_class($resource) : gettype($resource) 203 | )); 204 | } 205 | 206 | return $resource; 207 | } 208 | 209 | /** 210 | * Setter for availableIncludes. 211 | * 212 | * @param array $availableIncludes 213 | * 214 | * @return $this 215 | */ 216 | public function setAvailableIncludes($availableIncludes) 217 | { 218 | $this->availableIncludes = $availableIncludes; 219 | 220 | return $this; 221 | } 222 | 223 | /** 224 | * Setter for defaultIncludes. 225 | * 226 | * @param array $defaultIncludes 227 | * 228 | * @return $this 229 | */ 230 | public function setDefaultIncludes($defaultIncludes) 231 | { 232 | $this->defaultIncludes = $defaultIncludes; 233 | 234 | return $this; 235 | } 236 | 237 | /** 238 | * Setter for currentScope. 239 | * 240 | * @param Scope $currentScope 241 | * 242 | * @return $this 243 | */ 244 | public function setCurrentScope($currentScope) 245 | { 246 | $this->currentScope = $currentScope; 247 | 248 | return $this; 249 | } 250 | 251 | /** 252 | * Create a new primitive resource object. 253 | * 254 | * @param mixed $data 255 | * @param callable|null $transformer 256 | * @param string $resourceKey 257 | * 258 | * @return Primitive 259 | */ 260 | protected function primitive($data, $transformer = null, $resourceKey = null) 261 | { 262 | return new Primitive($data, $transformer, $resourceKey); 263 | } 264 | 265 | /** 266 | * Create a new item resource object. 267 | * 268 | * @param mixed $data 269 | * @param TransformerAbstract|callable $transformer 270 | * @param string $resourceKey 271 | * 272 | * @return Item 273 | */ 274 | protected function item($data, $transformer, $resourceKey = null) 275 | { 276 | return new Item($data, $transformer, $resourceKey); 277 | } 278 | 279 | /** 280 | * Create a new collection resource object. 281 | * 282 | * @param mixed $data 283 | * @param TransformerAbstract|callable $transformer 284 | * @param string $resourceKey 285 | * 286 | * @return Collection 287 | */ 288 | protected function collection($data, $transformer, $resourceKey = null) 289 | { 290 | return new Collection($data, $transformer, $resourceKey); 291 | } 292 | 293 | /** 294 | * Create a new null resource object. 295 | * 296 | * @return NullResource 297 | */ 298 | protected function null() 299 | { 300 | return new NullResource(); 301 | } 302 | } 303 | -------------------------------------------------------------------------------- /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\SerializerAbstract; 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 | * @var array 31 | */ 32 | protected $requestedIncludes = []; 33 | 34 | /** 35 | * Array of scope identifiers for resources to exclude. 36 | * 37 | * @var array 38 | */ 39 | protected $requestedExcludes = []; 40 | 41 | /** 42 | * Array of requested fieldsets. 43 | * 44 | * @var array 45 | */ 46 | protected $requestedFieldsets = []; 47 | 48 | /** 49 | * Array containing modifiers as keys and an array value of params. 50 | * 51 | * @var array 52 | */ 53 | protected $includeParams = []; 54 | 55 | /** 56 | * The character used to separate modifier parameters. 57 | * 58 | * @var string 59 | */ 60 | protected $paramDelimiter = '|'; 61 | 62 | /** 63 | * Upper limit to how many levels of included data are allowed. 64 | * 65 | * @var int 66 | */ 67 | protected $recursionLimit = 10; 68 | 69 | /** 70 | * Serializer. 71 | * 72 | * @var SerializerAbstract 73 | */ 74 | protected $serializer; 75 | 76 | /** 77 | * Factory used to create new configured scopes. 78 | * 79 | * @var ScopeFactoryInterface 80 | */ 81 | private $scopeFactory; 82 | 83 | public function __construct(ScopeFactoryInterface $scopeFactory = null) 84 | { 85 | $this->scopeFactory = $scopeFactory ?: new ScopeFactory(); 86 | } 87 | 88 | /** 89 | * Create Data. 90 | * 91 | * Main method to kick this all off. Make a resource then pass it over, and use toArray() 92 | * 93 | * @param ResourceInterface $resource 94 | * @param string $scopeIdentifier 95 | * @param Scope $parentScopeInstance 96 | * 97 | * @return Scope 98 | */ 99 | public function createData(ResourceInterface $resource, $scopeIdentifier = null, Scope $parentScopeInstance = null) 100 | { 101 | if ($parentScopeInstance !== null) { 102 | return $this->scopeFactory->createChildScopeFor($this, $parentScopeInstance, $resource, $scopeIdentifier); 103 | } 104 | 105 | return $this->scopeFactory->createScopeFor($this, $resource, $scopeIdentifier); 106 | } 107 | 108 | /** 109 | * Get Include Params. 110 | * 111 | * @param string $include 112 | * 113 | * @return \League\Fractal\ParamBag 114 | */ 115 | public function getIncludeParams($include) 116 | { 117 | $params = isset($this->includeParams[$include]) ? $this->includeParams[$include] : []; 118 | 119 | return new ParamBag($params); 120 | } 121 | 122 | /** 123 | * Get Requested Includes. 124 | * 125 | * @return array 126 | */ 127 | public function getRequestedIncludes() 128 | { 129 | return $this->requestedIncludes; 130 | } 131 | 132 | /** 133 | * Get Requested Excludes. 134 | * 135 | * @return array 136 | */ 137 | public function getRequestedExcludes() 138 | { 139 | return $this->requestedExcludes; 140 | } 141 | 142 | /** 143 | * Get Serializer. 144 | * 145 | * @return SerializerAbstract 146 | */ 147 | public function getSerializer() 148 | { 149 | if (! $this->serializer) { 150 | $this->setSerializer(new DataArraySerializer()); 151 | } 152 | 153 | return $this->serializer; 154 | } 155 | 156 | /** 157 | * Parse Include String. 158 | * 159 | * @param array|string $includes Array or csv string of resources to include 160 | * 161 | * @return $this 162 | */ 163 | public function parseIncludes($includes) 164 | { 165 | // Wipe these before we go again 166 | $this->requestedIncludes = $this->includeParams = []; 167 | 168 | if (is_string($includes)) { 169 | $includes = explode(',', $includes); 170 | } 171 | 172 | if (! is_array($includes)) { 173 | throw new \InvalidArgumentException( 174 | 'The parseIncludes() method expects a string or an array. '.gettype($includes).' given' 175 | ); 176 | } 177 | 178 | foreach ($includes as $include) { 179 | list($includeName, $allModifiersStr) = array_pad(explode(':', $include, 2), 2, null); 180 | 181 | // Trim it down to a cool level of recursion 182 | $includeName = $this->trimToAcceptableRecursionLevel($includeName); 183 | 184 | if (in_array($includeName, $this->requestedIncludes)) { 185 | continue; 186 | } 187 | $this->requestedIncludes[] = $includeName; 188 | 189 | // No Params? Bored 190 | if ($allModifiersStr === null) { 191 | continue; 192 | } 193 | 194 | // Matches multiple instances of 'something(foo|bar|baz)' in the string 195 | // I guess it ignores : so you could use anything, but probably don't do that 196 | preg_match_all('/([\w]+)(\(([^\)]+)\))?/', $allModifiersStr, $allModifiersArr); 197 | 198 | // [0] is full matched strings... 199 | $modifierCount = count($allModifiersArr[0]); 200 | 201 | $modifierArr = []; 202 | 203 | for ($modifierIt = 0; $modifierIt < $modifierCount; $modifierIt++) { 204 | // [1] is the modifier 205 | $modifierName = $allModifiersArr[1][$modifierIt]; 206 | 207 | // and [3] is delimited params 208 | $modifierParamStr = $allModifiersArr[3][$modifierIt]; 209 | 210 | // Make modifier array key with an array of params as the value 211 | $modifierArr[$modifierName] = explode($this->paramDelimiter, $modifierParamStr); 212 | } 213 | 214 | $this->includeParams[$includeName] = $modifierArr; 215 | } 216 | 217 | // This should be optional and public someday, but without it includes would never show up 218 | $this->autoIncludeParents(); 219 | 220 | return $this; 221 | } 222 | 223 | /** 224 | * Parse field parameter. 225 | * 226 | * @param array $fieldsets Array of fields to include. It must be an array 227 | * whose keys are resource types and values a string 228 | * of the fields to return, separated by a comma 229 | * 230 | * @return $this 231 | */ 232 | public function parseFieldsets(array $fieldsets) 233 | { 234 | $this->requestedFieldsets = []; 235 | foreach ($fieldsets as $type => $fields) { 236 | //Remove empty and repeated fields 237 | $this->requestedFieldsets[$type] = array_unique(array_filter(explode(',', $fields))); 238 | } 239 | return $this; 240 | } 241 | 242 | /** 243 | * Get requested fieldsets. 244 | * 245 | * @return array 246 | */ 247 | public function getRequestedFieldsets() 248 | { 249 | return $this->requestedFieldsets; 250 | } 251 | 252 | /** 253 | * Get fieldset params for the specified type. 254 | * 255 | * @param string $type 256 | * 257 | * @return \League\Fractal\ParamBag|null 258 | */ 259 | public function getFieldset($type) 260 | { 261 | return !isset($this->requestedFieldsets[$type]) ? 262 | null : 263 | new ParamBag($this->requestedFieldsets[$type]); 264 | } 265 | 266 | /** 267 | * Parse Exclude String. 268 | * 269 | * @param array|string $excludes Array or csv string of resources to exclude 270 | * 271 | * @return $this 272 | */ 273 | public function parseExcludes($excludes) 274 | { 275 | $this->requestedExcludes = []; 276 | 277 | if (is_string($excludes)) { 278 | $excludes = explode(',', $excludes); 279 | } 280 | 281 | if (! is_array($excludes)) { 282 | throw new \InvalidArgumentException( 283 | 'The parseExcludes() method expects a string or an array. '.gettype($excludes).' given' 284 | ); 285 | } 286 | 287 | foreach ($excludes as $excludeName) { 288 | $excludeName = $this->trimToAcceptableRecursionLevel($excludeName); 289 | 290 | if (in_array($excludeName, $this->requestedExcludes)) { 291 | continue; 292 | } 293 | 294 | $this->requestedExcludes[] = $excludeName; 295 | } 296 | 297 | return $this; 298 | } 299 | 300 | /** 301 | * Set Recursion Limit. 302 | * 303 | * @param int $recursionLimit 304 | * 305 | * @return $this 306 | */ 307 | public function setRecursionLimit($recursionLimit) 308 | { 309 | $this->recursionLimit = $recursionLimit; 310 | 311 | return $this; 312 | } 313 | 314 | /** 315 | * Set Serializer 316 | * 317 | * @param SerializerAbstract $serializer 318 | * 319 | * @return $this 320 | */ 321 | public function setSerializer(SerializerAbstract $serializer) 322 | { 323 | $this->serializer = $serializer; 324 | 325 | return $this; 326 | } 327 | 328 | /** 329 | * Auto-include Parents 330 | * 331 | * Look at the requested includes and automatically include the parents if they 332 | * are not explicitly requested. E.g: [foo, bar.baz] becomes [foo, bar, bar.baz] 333 | * 334 | * @internal 335 | * 336 | * @return void 337 | */ 338 | protected function autoIncludeParents() 339 | { 340 | $parsed = []; 341 | 342 | foreach ($this->requestedIncludes as $include) { 343 | $nested = explode('.', $include); 344 | 345 | $part = array_shift($nested); 346 | $parsed[] = $part; 347 | 348 | while (count($nested) > 0) { 349 | $part .= '.'.array_shift($nested); 350 | $parsed[] = $part; 351 | } 352 | } 353 | 354 | $this->requestedIncludes = array_values(array_unique($parsed)); 355 | } 356 | 357 | /** 358 | * Trim to Acceptable Recursion Level 359 | * 360 | * Strip off any requested resources that are too many levels deep, to avoid DiCaprio being chased 361 | * by trains or whatever the hell that movie was about. 362 | * 363 | * @internal 364 | * 365 | * @param string $includeName 366 | * 367 | * @return string 368 | */ 369 | protected function trimToAcceptableRecursionLevel($includeName) 370 | { 371 | return implode('.', array_slice(explode('.', $includeName), 0, $this->recursionLimit)); 372 | } 373 | } 374 | -------------------------------------------------------------------------------- /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\SerializerAbstract; 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 30 | { 31 | /** 32 | * @var array 33 | */ 34 | protected $availableIncludes = []; 35 | 36 | /** 37 | * @var string 38 | */ 39 | protected $scopeIdentifier; 40 | 41 | /** 42 | * @var \League\Fractal\Manager 43 | */ 44 | protected $manager; 45 | 46 | /** 47 | * @var ResourceInterface 48 | */ 49 | protected $resource; 50 | 51 | /** 52 | * @var array 53 | */ 54 | protected $parentScopes = []; 55 | 56 | /** 57 | * Create a new scope instance. 58 | * 59 | * @param Manager $manager 60 | * @param ResourceInterface $resource 61 | * @param string $scopeIdentifier 62 | * 63 | * @return void 64 | */ 65 | public function __construct(Manager $manager, ResourceInterface $resource, $scopeIdentifier = null) 66 | { 67 | $this->manager = $manager; 68 | $this->resource = $resource; 69 | $this->scopeIdentifier = $scopeIdentifier; 70 | } 71 | 72 | /** 73 | * Embed a scope as a child of the current scope. 74 | * 75 | * @internal 76 | * 77 | * @param string $scopeIdentifier 78 | * @param ResourceInterface $resource 79 | * 80 | * @return \League\Fractal\Scope 81 | */ 82 | public function embedChildScope($scopeIdentifier, $resource) 83 | { 84 | return $this->manager->createData($resource, $scopeIdentifier, $this); 85 | } 86 | 87 | /** 88 | * Get the current identifier. 89 | * 90 | * @return string 91 | */ 92 | public function getScopeIdentifier() 93 | { 94 | return $this->scopeIdentifier; 95 | } 96 | 97 | /** 98 | * Get the unique identifier for this scope. 99 | * 100 | * @param string $appendIdentifier 101 | * 102 | * @return string 103 | */ 104 | public function getIdentifier($appendIdentifier = null) 105 | { 106 | $identifierParts = array_merge($this->parentScopes, [$this->scopeIdentifier, $appendIdentifier]); 107 | 108 | return implode('.', array_filter($identifierParts)); 109 | } 110 | 111 | /** 112 | * Getter for parentScopes. 113 | * 114 | * @return mixed 115 | */ 116 | public function getParentScopes() 117 | { 118 | return $this->parentScopes; 119 | } 120 | 121 | /** 122 | * Getter for resource. 123 | * 124 | * @return ResourceInterface 125 | */ 126 | public function getResource() 127 | { 128 | return $this->resource; 129 | } 130 | 131 | /** 132 | * Getter for manager. 133 | * 134 | * @return \League\Fractal\Manager 135 | */ 136 | public function getManager() 137 | { 138 | return $this->manager; 139 | } 140 | 141 | /** 142 | * Is Requested. 143 | * 144 | * Check if - in relation to the current scope - this specific segment is allowed. 145 | * That means, if a.b.c is requested and the current scope is a.b, then c is allowed. If the current 146 | * scope is a then c is not allowed, even if it is there and potentially transformable. 147 | * 148 | * @internal 149 | * 150 | * @param string $checkScopeSegment 151 | * 152 | * @return bool Returns the new number of elements in the array. 153 | */ 154 | public function isRequested($checkScopeSegment) 155 | { 156 | if ($this->parentScopes) { 157 | $scopeArray = array_slice($this->parentScopes, 1); 158 | array_push($scopeArray, $this->scopeIdentifier, $checkScopeSegment); 159 | } else { 160 | $scopeArray = [$checkScopeSegment]; 161 | } 162 | 163 | $scopeString = implode('.', (array) $scopeArray); 164 | 165 | return in_array($scopeString, $this->manager->getRequestedIncludes()); 166 | } 167 | 168 | /** 169 | * Is Excluded. 170 | * 171 | * Check if - in relation to the current scope - this specific segment should 172 | * be excluded. That means, if a.b.c is excluded and the current scope is a.b, 173 | * then c will not be allowed in the transformation whether it appears in 174 | * the list of default or available, requested includes. 175 | * 176 | * @internal 177 | * 178 | * @param string $checkScopeSegment 179 | * 180 | * @return bool 181 | */ 182 | public function isExcluded($checkScopeSegment) 183 | { 184 | if ($this->parentScopes) { 185 | $scopeArray = array_slice($this->parentScopes, 1); 186 | array_push($scopeArray, $this->scopeIdentifier, $checkScopeSegment); 187 | } else { 188 | $scopeArray = [$checkScopeSegment]; 189 | } 190 | 191 | $scopeString = implode('.', (array) $scopeArray); 192 | 193 | return in_array($scopeString, $this->manager->getRequestedExcludes()); 194 | } 195 | 196 | /** 197 | * Push Parent Scope. 198 | * 199 | * Push a scope identifier into parentScopes 200 | * 201 | * @internal 202 | * 203 | * @param string $identifierSegment 204 | * 205 | * @return int Returns the new number of elements in the array. 206 | */ 207 | public function pushParentScope($identifierSegment) 208 | { 209 | return array_push($this->parentScopes, $identifierSegment); 210 | } 211 | 212 | /** 213 | * Set parent scopes. 214 | * 215 | * @internal 216 | * 217 | * @param string[] $parentScopes Value to set. 218 | * 219 | * @return $this 220 | */ 221 | public function setParentScopes($parentScopes) 222 | { 223 | $this->parentScopes = $parentScopes; 224 | 225 | return $this; 226 | } 227 | 228 | /** 229 | * Convert the current data for this scope to an array. 230 | * 231 | * @return array 232 | */ 233 | public function toArray() 234 | { 235 | list($rawData, $rawIncludedData) = $this->executeResourceTransformers(); 236 | 237 | $serializer = $this->manager->getSerializer(); 238 | 239 | $data = $this->serializeResource($serializer, $rawData); 240 | 241 | // If the serializer wants the includes to be side-loaded then we'll 242 | // serialize the included data and merge it with the data. 243 | if ($serializer->sideloadIncludes()) { 244 | //Filter out any relation that wasn't requested 245 | $rawIncludedData = array_map(array($this, 'filterFieldsets'), $rawIncludedData); 246 | 247 | $includedData = $serializer->includedData($this->resource, $rawIncludedData); 248 | 249 | // If the serializer wants to inject additional information 250 | // about the included resources, it can do so now. 251 | $data = $serializer->injectData($data, $rawIncludedData); 252 | 253 | if ($this->isRootScope()) { 254 | // If the serializer wants to have a final word about all 255 | // the objects that are sideloaded, it can do so now. 256 | $includedData = $serializer->filterIncludes( 257 | $includedData, 258 | $data 259 | ); 260 | } 261 | 262 | $data = array_merge($data, $includedData); 263 | } 264 | 265 | if ($this->resource instanceof Collection) { 266 | if ($this->resource->hasCursor()) { 267 | $pagination = $serializer->cursor($this->resource->getCursor()); 268 | } elseif ($this->resource->hasPaginator()) { 269 | $pagination = $serializer->paginator($this->resource->getPaginator()); 270 | } 271 | 272 | if (! empty($pagination)) { 273 | $this->resource->setMetaValue(key($pagination), current($pagination)); 274 | } 275 | } 276 | 277 | // Pull out all of OUR metadata and any custom meta data to merge with the main level data 278 | $meta = $serializer->meta($this->resource->getMeta()); 279 | 280 | // in case of returning NullResource we should return null and not to go with array_merge 281 | if (is_null($data)) { 282 | if (!empty($meta)) { 283 | return $meta; 284 | } 285 | return null; 286 | } 287 | 288 | return array_merge($data, $meta); 289 | } 290 | 291 | /** 292 | * Convert the current data for this scope to JSON. 293 | * 294 | * @param int $options 295 | * 296 | * @return string 297 | */ 298 | public function toJson($options = 0) 299 | { 300 | return json_encode($this->toArray(), $options); 301 | } 302 | 303 | /** 304 | * Transformer a primitive resource 305 | * 306 | * @return mixed 307 | */ 308 | public function transformPrimitiveResource() 309 | { 310 | if (! ($this->resource instanceof Primitive)) { 311 | throw new InvalidArgumentException( 312 | 'Argument $resource should be an instance of League\Fractal\Resource\Primitive' 313 | ); 314 | } 315 | 316 | $transformer = $this->resource->getTransformer(); 317 | $data = $this->resource->getData(); 318 | 319 | if (null === $transformer) { 320 | $transformedData = $data; 321 | } elseif (is_callable($transformer)) { 322 | $transformedData = call_user_func($transformer, $data); 323 | } else { 324 | $transformer->setCurrentScope($this); 325 | $transformedData = $transformer->transform($data); 326 | } 327 | 328 | return $transformedData; 329 | } 330 | 331 | /** 332 | * Execute the resources transformer and return the data and included data. 333 | * 334 | * @internal 335 | * 336 | * @return array 337 | */ 338 | protected function executeResourceTransformers() 339 | { 340 | $transformer = $this->resource->getTransformer(); 341 | $data = $this->resource->getData(); 342 | 343 | $transformedData = $includedData = []; 344 | 345 | if ($this->resource instanceof Item) { 346 | list($transformedData, $includedData[]) = $this->fireTransformer($transformer, $data); 347 | } elseif ($this->resource instanceof Collection) { 348 | foreach ($data as $value) { 349 | list($transformedData[], $includedData[]) = $this->fireTransformer($transformer, $value); 350 | } 351 | } elseif ($this->resource instanceof NullResource) { 352 | $transformedData = null; 353 | $includedData = []; 354 | } else { 355 | throw new InvalidArgumentException( 356 | 'Argument $resource should be an instance of League\Fractal\Resource\Item' 357 | .' or League\Fractal\Resource\Collection' 358 | ); 359 | } 360 | 361 | return [$transformedData, $includedData]; 362 | } 363 | 364 | /** 365 | * Serialize a resource 366 | * 367 | * @internal 368 | * 369 | * @param SerializerAbstract $serializer 370 | * @param mixed $data 371 | * 372 | * @return array 373 | */ 374 | protected function serializeResource(SerializerAbstract $serializer, $data) 375 | { 376 | $resourceKey = $this->resource->getResourceKey(); 377 | 378 | if ($this->resource instanceof Collection) { 379 | return $serializer->collection($resourceKey, $data); 380 | } 381 | 382 | if ($this->resource instanceof Item) { 383 | return $serializer->item($resourceKey, $data); 384 | } 385 | 386 | return $serializer->null(); 387 | } 388 | 389 | /** 390 | * Fire the main transformer. 391 | * 392 | * @internal 393 | * 394 | * @param TransformerAbstract|callable $transformer 395 | * @param mixed $data 396 | * 397 | * @return array 398 | */ 399 | protected function fireTransformer($transformer, $data) 400 | { 401 | $includedData = []; 402 | 403 | if (is_callable($transformer)) { 404 | $transformedData = call_user_func($transformer, $data); 405 | } else { 406 | $transformer->setCurrentScope($this); 407 | $transformedData = $transformer->transform($data); 408 | } 409 | 410 | if ($this->transformerHasIncludes($transformer)) { 411 | $includedData = $this->fireIncludedTransformers($transformer, $data); 412 | $transformedData = $this->manager->getSerializer()->mergeIncludes($transformedData, $includedData); 413 | } 414 | 415 | //Stick only with requested fields 416 | $transformedData = $this->filterFieldsets($transformedData); 417 | 418 | return [$transformedData, $includedData]; 419 | } 420 | 421 | /** 422 | * Fire the included transformers. 423 | * 424 | * @internal 425 | * 426 | * @param \League\Fractal\TransformerAbstract $transformer 427 | * @param mixed $data 428 | * 429 | * @return array 430 | */ 431 | protected function fireIncludedTransformers($transformer, $data) 432 | { 433 | $this->availableIncludes = $transformer->getAvailableIncludes(); 434 | 435 | return $transformer->processIncludedResources($this, $data) ?: []; 436 | } 437 | 438 | /** 439 | * Determine if a transformer has any available includes. 440 | * 441 | * @internal 442 | * 443 | * @param TransformerAbstract|callable $transformer 444 | * 445 | * @return bool 446 | */ 447 | protected function transformerHasIncludes($transformer) 448 | { 449 | if (! $transformer instanceof TransformerAbstract) { 450 | return false; 451 | } 452 | 453 | $defaultIncludes = $transformer->getDefaultIncludes(); 454 | $availableIncludes = $transformer->getAvailableIncludes(); 455 | 456 | return ! empty($defaultIncludes) || ! empty($availableIncludes); 457 | } 458 | 459 | /** 460 | * Check, if this is the root scope. 461 | * 462 | * @return bool 463 | */ 464 | protected function isRootScope() 465 | { 466 | return empty($this->parentScopes); 467 | } 468 | 469 | /** 470 | * Filter the provided data with the requested filter fieldset for 471 | * the scope resource 472 | * 473 | * @internal 474 | * 475 | * @param array $data 476 | * 477 | * @return array 478 | */ 479 | protected function filterFieldsets(array $data) 480 | { 481 | if (!$this->hasFilterFieldset()) { 482 | return $data; 483 | } 484 | $serializer = $this->manager->getSerializer(); 485 | $requestedFieldset = iterator_to_array($this->getFilterFieldset()); 486 | //Build the array of requested fieldsets with the mandatory serializer fields 487 | $filterFieldset = array_flip( 488 | array_merge( 489 | $serializer->getMandatoryFields(), 490 | $requestedFieldset 491 | ) 492 | ); 493 | return array_intersect_key($data, $filterFieldset); 494 | } 495 | 496 | /** 497 | * Return the requested filter fieldset for the scope resource 498 | * 499 | * @internal 500 | * 501 | * @return \League\Fractal\ParamBag|null 502 | */ 503 | protected function getFilterFieldset() 504 | { 505 | return $this->manager->getFieldset($this->getResourceType()); 506 | } 507 | 508 | /** 509 | * Check if there are requested filter fieldsets for the scope resource. 510 | * 511 | * @internal 512 | * 513 | * @return bool 514 | */ 515 | protected function hasFilterFieldset() 516 | { 517 | return $this->getFilterFieldset() !== null; 518 | } 519 | 520 | /** 521 | * Return the scope resource type. 522 | * 523 | * @internal 524 | * 525 | * @return string 526 | */ 527 | protected function getResourceType() 528 | { 529 | return $this->resource->getResourceKey(); 530 | } 531 | } 532 | -------------------------------------------------------------------------------- /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 $baseUrl; 21 | protected $rootObjects; 22 | 23 | /** 24 | * JsonApiSerializer constructor. 25 | * 26 | * @param string $baseUrl 27 | */ 28 | public function __construct($baseUrl = null) 29 | { 30 | $this->baseUrl = $baseUrl; 31 | $this->rootObjects = []; 32 | } 33 | 34 | /** 35 | * Serialize a collection. 36 | * 37 | * @param string $resourceKey 38 | * @param array $data 39 | * 40 | * @return array 41 | */ 42 | public function collection($resourceKey, array $data) 43 | { 44 | $resources = []; 45 | 46 | foreach ($data as $resource) { 47 | $resources[] = $this->item($resourceKey, $resource)['data']; 48 | } 49 | 50 | return ['data' => $resources]; 51 | } 52 | 53 | /** 54 | * Serialize an item. 55 | * 56 | * @param string $resourceKey 57 | * @param array $data 58 | * 59 | * @return array 60 | */ 61 | public function item($resourceKey, array $data) 62 | { 63 | $id = $this->getIdFromData($data); 64 | 65 | $resource = [ 66 | 'data' => [ 67 | 'type' => $resourceKey, 68 | 'id' => "$id", 69 | 'attributes' => $data, 70 | ], 71 | ]; 72 | 73 | unset($resource['data']['attributes']['id']); 74 | 75 | if(isset($resource['data']['attributes']['links'])) { 76 | $custom_links = $data['links']; 77 | unset($resource['data']['attributes']['links']); 78 | } 79 | 80 | if (isset($resource['data']['attributes']['meta'])){ 81 | $resource['data']['meta'] = $data['meta']; 82 | unset($resource['data']['attributes']['meta']); 83 | } 84 | 85 | if ($this->shouldIncludeLinks()) { 86 | $resource['data']['links'] = [ 87 | 'self' => "{$this->baseUrl}/$resourceKey/$id", 88 | ]; 89 | if(isset($custom_links)) { 90 | $resource['data']['links'] = array_merge($custom_links, $resource['data']['links']); 91 | } 92 | } 93 | 94 | return $resource; 95 | } 96 | 97 | /** 98 | * Serialize the paginator. 99 | * 100 | * @param PaginatorInterface $paginator 101 | * 102 | * @return array 103 | */ 104 | public function paginator(PaginatorInterface $paginator) 105 | { 106 | $currentPage = (int)$paginator->getCurrentPage(); 107 | $lastPage = (int)$paginator->getLastPage(); 108 | 109 | $pagination = [ 110 | 'total' => (int)$paginator->getTotal(), 111 | 'count' => (int)$paginator->getCount(), 112 | 'per_page' => (int)$paginator->getPerPage(), 113 | 'current_page' => $currentPage, 114 | 'total_pages' => $lastPage, 115 | ]; 116 | 117 | $pagination['links'] = []; 118 | 119 | $pagination['links']['self'] = $paginator->getUrl($currentPage); 120 | $pagination['links']['first'] = $paginator->getUrl(1); 121 | 122 | if ($currentPage > 1) { 123 | $pagination['links']['prev'] = $paginator->getUrl($currentPage - 1); 124 | } 125 | 126 | if ($currentPage < $lastPage) { 127 | $pagination['links']['next'] = $paginator->getUrl($currentPage + 1); 128 | } 129 | 130 | $pagination['links']['last'] = $paginator->getUrl($lastPage); 131 | 132 | return ['pagination' => $pagination]; 133 | } 134 | 135 | /** 136 | * Serialize the meta. 137 | * 138 | * @param array $meta 139 | * 140 | * @return array 141 | */ 142 | public function meta(array $meta) 143 | { 144 | if (empty($meta)) { 145 | return []; 146 | } 147 | 148 | $result['meta'] = $meta; 149 | 150 | if (array_key_exists('pagination', $result['meta'])) { 151 | $result['links'] = $result['meta']['pagination']['links']; 152 | unset($result['meta']['pagination']['links']); 153 | } 154 | 155 | return $result; 156 | } 157 | 158 | /** 159 | * @return array 160 | */ 161 | public function null() 162 | { 163 | return [ 164 | 'data' => null, 165 | ]; 166 | } 167 | 168 | /** 169 | * Serialize the included data. 170 | * 171 | * @param ResourceInterface $resource 172 | * @param array $data 173 | * 174 | * @return array 175 | */ 176 | public function includedData(ResourceInterface $resource, array $data) 177 | { 178 | list($serializedData, $linkedIds) = $this->pullOutNestedIncludedData($data); 179 | 180 | foreach ($data as $value) { 181 | foreach ($value as $includeObject) { 182 | if ($this->isNull($includeObject) || $this->isEmpty($includeObject)) { 183 | continue; 184 | } 185 | 186 | $includeObjects = $this->createIncludeObjects($includeObject); 187 | list($serializedData, $linkedIds) = $this->serializeIncludedObjectsWithCacheKey($includeObjects, $linkedIds, $serializedData); 188 | } 189 | } 190 | 191 | return empty($serializedData) ? [] : ['included' => $serializedData]; 192 | } 193 | 194 | /** 195 | * Indicates if includes should be side-loaded. 196 | * 197 | * @return bool 198 | */ 199 | public function sideloadIncludes() 200 | { 201 | return true; 202 | } 203 | 204 | /** 205 | * @param array $data 206 | * @param array $includedData 207 | * 208 | * @return array 209 | */ 210 | public function injectData($data, $includedData) 211 | { 212 | $relationships = $this->parseRelationships($includedData); 213 | 214 | if (!empty($relationships)) { 215 | $data = $this->fillRelationships($data, $relationships); 216 | } 217 | 218 | return $data; 219 | } 220 | 221 | /** 222 | * Hook to manipulate the final sideloaded includes. 223 | * The JSON API specification does not allow the root object to be included 224 | * into the sideloaded `included`-array. We have to make sure it is 225 | * filtered out, in case some object links to the root object in a 226 | * relationship. 227 | * 228 | * @param array $includedData 229 | * @param array $data 230 | * 231 | * @return array 232 | */ 233 | public function filterIncludes($includedData, $data) 234 | { 235 | if (!isset($includedData['included'])) { 236 | return $includedData; 237 | } 238 | 239 | // Create the RootObjects 240 | $this->createRootObjects($data); 241 | 242 | // Filter out the root objects 243 | $filteredIncludes = array_filter($includedData['included'], [$this, 'filterRootObject']); 244 | 245 | // Reset array indizes 246 | $includedData['included'] = array_merge([], $filteredIncludes); 247 | 248 | return $includedData; 249 | } 250 | 251 | /** 252 | * Get the mandatory fields for the serializer 253 | * 254 | * @return array 255 | */ 256 | public function getMandatoryFields() 257 | { 258 | return ['id']; 259 | } 260 | 261 | /** 262 | * Filter function to delete root objects from array. 263 | * 264 | * @param array $object 265 | * 266 | * @return bool 267 | */ 268 | protected function filterRootObject($object) 269 | { 270 | return !$this->isRootObject($object); 271 | } 272 | 273 | /** 274 | * Set the root objects of the JSON API tree. 275 | * 276 | * @param array $objects 277 | */ 278 | protected function setRootObjects(array $objects = []) 279 | { 280 | $this->rootObjects = array_map(function ($object) { 281 | return "{$object['type']}:{$object['id']}"; 282 | }, $objects); 283 | } 284 | 285 | /** 286 | * Determines whether an object is a root object of the JSON API tree. 287 | * 288 | * @param array $object 289 | * 290 | * @return bool 291 | */ 292 | protected function isRootObject($object) 293 | { 294 | $objectKey = "{$object['type']}:{$object['id']}"; 295 | return in_array($objectKey, $this->rootObjects); 296 | } 297 | 298 | /** 299 | * @param array $data 300 | * 301 | * @return bool 302 | */ 303 | protected function isCollection($data) 304 | { 305 | return array_key_exists('data', $data) && 306 | array_key_exists(0, $data['data']); 307 | } 308 | 309 | /** 310 | * @param array $data 311 | * 312 | * @return bool 313 | */ 314 | protected function isNull($data) 315 | { 316 | return array_key_exists('data', $data) && $data['data'] === null; 317 | } 318 | 319 | /** 320 | * @param array $data 321 | * 322 | * @return bool 323 | */ 324 | protected function isEmpty($data) 325 | { 326 | return array_key_exists('data', $data) && $data['data'] === []; 327 | } 328 | 329 | /** 330 | * @param array $data 331 | * @param array $relationships 332 | * 333 | * @return array 334 | */ 335 | protected function fillRelationships($data, $relationships) 336 | { 337 | if ($this->isCollection($data)) { 338 | foreach ($relationships as $key => $relationship) { 339 | $data = $this->fillRelationshipAsCollection($data, $relationship, $key); 340 | } 341 | } else { // Single resource 342 | foreach ($relationships as $key => $relationship) { 343 | $data = $this->fillRelationshipAsSingleResource($data, $relationship, $key); 344 | } 345 | } 346 | 347 | return $data; 348 | } 349 | 350 | /** 351 | * @param array $includedData 352 | * 353 | * @return array 354 | */ 355 | protected function parseRelationships($includedData) 356 | { 357 | $relationships = []; 358 | 359 | foreach ($includedData as $key => $inclusion) { 360 | foreach ($inclusion as $includeKey => $includeObject) { 361 | $relationships = $this->buildRelationships($includeKey, $relationships, $includeObject, $key); 362 | } 363 | } 364 | 365 | return $relationships; 366 | } 367 | 368 | /** 369 | * @param array $data 370 | * 371 | * @return integer 372 | */ 373 | protected function getIdFromData(array $data) 374 | { 375 | if (!array_key_exists('id', $data)) { 376 | throw new InvalidArgumentException( 377 | 'JSON API resource objects MUST have a valid id' 378 | ); 379 | } 380 | return $data['id']; 381 | } 382 | 383 | /** 384 | * Keep all sideloaded inclusion data on the top level. 385 | * 386 | * @param array $data 387 | * 388 | * @return array 389 | */ 390 | protected function pullOutNestedIncludedData(array $data) 391 | { 392 | $includedData = []; 393 | $linkedIds = []; 394 | 395 | foreach ($data as $value) { 396 | foreach ($value as $includeObject) { 397 | if (isset($includeObject['included'])) { 398 | list($includedData, $linkedIds) = $this->serializeIncludedObjectsWithCacheKey($includeObject['included'], $linkedIds, $includedData); 399 | } 400 | } 401 | } 402 | 403 | return [$includedData, $linkedIds]; 404 | } 405 | 406 | /** 407 | * Whether or not the serializer should include `links` for resource objects. 408 | * 409 | * @return bool 410 | */ 411 | protected function shouldIncludeLinks() 412 | { 413 | return $this->baseUrl !== null; 414 | } 415 | 416 | /** 417 | * Check if the objects are part of a collection or not 418 | * 419 | * @param $includeObject 420 | * 421 | * @return array 422 | */ 423 | private function createIncludeObjects($includeObject) 424 | { 425 | if ($this->isCollection($includeObject)) { 426 | $includeObjects = $includeObject['data']; 427 | 428 | return $includeObjects; 429 | } else { 430 | $includeObjects = [$includeObject['data']]; 431 | 432 | return $includeObjects; 433 | } 434 | } 435 | 436 | /** 437 | * Sets the RootObjects, either as collection or not. 438 | * 439 | * @param $data 440 | */ 441 | private function createRootObjects($data) 442 | { 443 | if ($this->isCollection($data)) { 444 | $this->setRootObjects($data['data']); 445 | } else { 446 | $this->setRootObjects([$data['data']]); 447 | } 448 | } 449 | 450 | 451 | /** 452 | * Loops over the relationships of the provided data and formats it 453 | * 454 | * @param $data 455 | * @param $relationship 456 | * @param $key 457 | * 458 | * @return array 459 | */ 460 | private function fillRelationshipAsCollection($data, $relationship, $key) 461 | { 462 | foreach ($relationship as $index => $relationshipData) { 463 | $data['data'][$index]['relationships'][$key] = $relationshipData; 464 | 465 | if ($this->shouldIncludeLinks()) { 466 | $data['data'][$index]['relationships'][$key] = array_merge([ 467 | 'links' => [ 468 | 'self' => "{$this->baseUrl}/{$data['data'][$index]['type']}/{$data['data'][$index]['id']}/relationships/$key", 469 | 'related' => "{$this->baseUrl}/{$data['data'][$index]['type']}/{$data['data'][$index]['id']}/$key", 470 | ], 471 | ], $data['data'][$index]['relationships'][$key]); 472 | } 473 | } 474 | 475 | return $data; 476 | } 477 | 478 | 479 | /** 480 | * @param $data 481 | * @param $relationship 482 | * @param $key 483 | * 484 | * @return array 485 | */ 486 | private function fillRelationshipAsSingleResource($data, $relationship, $key) 487 | { 488 | $data['data']['relationships'][$key] = $relationship[0]; 489 | 490 | if ($this->shouldIncludeLinks()) { 491 | $data['data']['relationships'][$key] = array_merge([ 492 | 'links' => [ 493 | 'self' => "{$this->baseUrl}/{$data['data']['type']}/{$data['data']['id']}/relationships/$key", 494 | 'related' => "{$this->baseUrl}/{$data['data']['type']}/{$data['data']['id']}/$key", 495 | ], 496 | ], $data['data']['relationships'][$key]); 497 | 498 | return $data; 499 | } 500 | return $data; 501 | } 502 | 503 | /** 504 | * @param $includeKey 505 | * @param $relationships 506 | * @param $includeObject 507 | * @param $key 508 | * 509 | * @return array 510 | */ 511 | private function buildRelationships($includeKey, $relationships, $includeObject, $key) 512 | { 513 | $relationships = $this->addIncludekeyToRelationsIfNotSet($includeKey, $relationships); 514 | 515 | if ($this->isNull($includeObject)) { 516 | $relationship = $this->null(); 517 | } elseif ($this->isEmpty($includeObject)) { 518 | $relationship = [ 519 | 'data' => [], 520 | ]; 521 | } elseif ($this->isCollection($includeObject)) { 522 | $relationship = ['data' => []]; 523 | 524 | $relationship = $this->addIncludedDataToRelationship($includeObject, $relationship); 525 | } else { 526 | $relationship = [ 527 | 'data' => [ 528 | 'type' => $includeObject['data']['type'], 529 | 'id' => $includeObject['data']['id'], 530 | ], 531 | ]; 532 | } 533 | 534 | $relationships[$includeKey][$key] = $relationship; 535 | 536 | return $relationships; 537 | } 538 | 539 | /** 540 | * @param $includeKey 541 | * @param $relationships 542 | * 543 | * @return array 544 | */ 545 | private function addIncludekeyToRelationsIfNotSet($includeKey, $relationships) 546 | { 547 | if (!array_key_exists($includeKey, $relationships)) { 548 | $relationships[$includeKey] = []; 549 | return $relationships; 550 | } 551 | 552 | return $relationships; 553 | } 554 | 555 | /** 556 | * @param $includeObject 557 | * @param $relationship 558 | * 559 | * @return array 560 | */ 561 | private function addIncludedDataToRelationship($includeObject, $relationship) 562 | { 563 | foreach ($includeObject['data'] as $object) { 564 | $relationship['data'][] = [ 565 | 'type' => $object['type'], 566 | 'id' => $object['id'], 567 | ]; 568 | } 569 | 570 | return $relationship; 571 | } 572 | 573 | /** 574 | * @param $includeObjects 575 | * @param $linkedIds 576 | * @param $serializedData 577 | * 578 | * @return array 579 | */ 580 | private function serializeIncludedObjectsWithCacheKey($includeObjects, $linkedIds, $serializedData) 581 | { 582 | foreach ($includeObjects as $object) { 583 | $includeType = $object['type']; 584 | $includeId = $object['id']; 585 | $cacheKey = "$includeType:$includeId"; 586 | if (!array_key_exists($cacheKey, $linkedIds)) { 587 | $serializedData[] = $object; 588 | $linkedIds[$cacheKey] = $object; 589 | } 590 | } 591 | return [$serializedData, $linkedIds]; 592 | } 593 | } 594 | --------------------------------------------------------------------------------