├── .gitignore ├── README.md ├── README_en.md ├── README_ru.md ├── composer.json ├── phpunit.xml.dist ├── src ├── Extension │ ├── AnonymousResourceCollection.php │ ├── ErrorInspectionHelpers.php │ ├── Exception │ │ ├── InvalidArgumentException.php │ │ └── ResourceMappingException.php │ ├── ExtendableResourceCollection.php │ ├── HasActiveFlag.php │ └── RestrictableResource.php ├── Pipeline │ ├── ExtensionPipeline.php │ └── UsesExtensionPipeline.php ├── Policy │ ├── ResourcePolicy.php │ └── UsesPolicy.php └── Transformer │ ├── ResourceTransformer.php │ └── UsesTransformer.php └── tests ├── ExtendableResourceCollectionTest.php ├── Mocks ├── MockExtensionPipeline.php ├── MockModel.php ├── MockResource.php ├── MockResourceCollection.php ├── PolicyStub.php └── TransformerStub.php ├── RestrictableResourceTest.php └── Utils └── HeadsOrTails.php /.gitignore: -------------------------------------------------------------------------------- 1 | .idea 2 | vendor 3 | composer.lock -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | ### [1. English](README_en.md) :point_left: 2 | ### [2. Русский](README_ru.md) :point_left: 3 | -------------------------------------------------------------------------------- /README_en.md: -------------------------------------------------------------------------------- 1 | ## Contents 2 | 1) [Installation](#installation) 3 | 2) [Testing](#testing) 4 | 3) [API Resource Collection underlying resource dependency injection](#api-resource-collection-underlying-resource-dependency-injection) 5 | 4) [Anonymous extendable resource collections](#anonymous-extendable-resource-collections) 6 | 5) [Resource policies](#resource-policies) 7 | 6) [Resource transformers](#resource-transformers) 8 | 7) [Resource class error inspection helpers](#resource-class-error-inspection-helpers) 9 | 8) [Extension pipelines](#extension-pipelines) 10 | 9) [Security](#security) 11 | 12 | ## Installation 13 | 14 | To install the package you need to run next command in your terminal: 15 | ``` bash 16 | composer require moofik/laravel-resources-extensions 17 | ``` 18 | ### Testing 19 | ```shell script 20 | composer test 21 | ``` 22 | 23 | ### Features 24 | #### API Resource Collection underlying resource dependency injection 25 | While using API Resource collection (ResourceCollection and its subclasses), extendable resource collections is used to pass arbitrary arguments to ResourceCollection underlying resources ```__construct()``` method. By default, using ResourceCollection prohibit to have any arbitrary arguments in the underlying resources, otherwise it will throw an error (because it does not know what arguments it should pass to every instance of underlying resource). 26 | 27 | Image we have resource, corresponded resource collection and controller which uses that resource collection. Here is example how we can use it: 28 |

Given we have a resource that needs some arguments besides default $resource argument into ```__construct()```: 29 | ``` php 30 | class RepairRequestResource extends JsonResource 31 | { 32 | private $repairOfferRepository; 33 | 34 | public function __construct( 35 | RepairRequest $resource, 36 | RepairOffersRepository $repairOfferRepository 37 | ) { 38 | parent::__construct($resource); 39 | $this->repairOfferRepository = $repairOfferRepository; 40 | } 41 | 42 | public function toArray($request) 43 | { 44 | $result = parent::toArray($request); 45 | $result['offers_count'] = $this->repairOfferRepository->countOffersByRequest($this->resource); 46 | 47 | return $result; 48 | } 49 | } 50 | ``` 51 | 52 |

53 | Imagine, we want to use the collection of this resource type in the controller. By default, we are not able to do this, because our RepairRequestResource waits for custom second argument in the __construct method, which RepairRequestResourceCollection knows nothing about. To pass that argument all we need is our resource collection class to extends 54 | 55 | ```php 56 | Moofik\LaravelResourceExtenstion\Extension\ExtendableResourceCollection 57 | ``` 58 | class; 59 |

60 | 61 | ```php 62 | use Moofik\LaravelResourceExtenstion\Extension\ExtendableResourceCollection; 63 | 64 | class RepairRequestResourceCollection extends ExtendableResourceCollection 65 | { 66 | public function toArray($request) 67 | { 68 | $result = parent::toArray($request); 69 | 70 | /* 71 | * Maybe some arbitrary but needful actions on $result here 72 | */ 73 | 74 | return $result; 75 | } 76 | } 77 | ``` 78 | 79 |

80 | Now arguments that we pass into our RepairRequestResourceCollection constructor will automatically be passed to constructor method of every underlying RepairRequestResource inside the collection. 81 |

82 | 83 | ```php 84 | class RepairRequestController 85 | { 86 | public function repairRequests(RepairOffersRepository $repairOfferRepository) 87 | { 88 | $repairRequests = RepairRequest::all(); 89 | 90 | return new RepairRequestResourceCollection($repairRequests, $repairOfferRepository); 91 | } 92 | } 93 | ``` 94 | 95 | Note: if we pass some another arguments to the RepairRequestResourceCollection constructor after the ones we needed, this arguments could be accessible inside our resource collection via 96 | ```php 97 | $this->args; 98 | ``` 99 | 100 | #### Anonymous extendable resource collections 101 |

If we don't want to create subclass for our API resource collection, because either we don't want to use custom logic inside ```toArray()``` resource or collection method, or we decided to move all the logic to resource policies and transformers - for these purposes you can use static method ```\Moofik\LaravelResourceExtenstion\Extension\ExtendableResourceCollection::extendableCollection($collection)". It return an instance of ```Moofik\LaravelResourceExtenstion\Extension\ExtendableResourceCollection``` class. As a second argument we can pass class name which will be used for creating items of this resource collection. By default, anonymous extendable collection uses ```Moofik\LaravelResourceExtenstion\Extension\RestrictableResource``` class. Every underlying resource in collection will be created based on this class. 102 |

103 | 104 | #### Resource policies 105 |

If you need somehow to force underlying resource model to hide or show some of its fields depending on some sophisticated behaviour we can use resource policies. For example we want to show some resource fields if user has specific role (it may be any condition, you are not somehow restricted in policy logic): 106 |

107 |

First, we need to create ResourcePolicy subclass that will define our policy behaviour.

108 | 109 | ```php 110 | use App\User; 111 | use App\RepairRequest; 112 | use Moofik\LaravelResourceExtenstion\Policy\ResourcePolicy; 113 | 114 | class RepairRequestPolicy extends ResourcePolicy 115 | { 116 | /** 117 | * @var User 118 | */ 119 | private $user; 120 | 121 | public function __construct(User $user) 122 | { 123 | $this->user = $user; 124 | } 125 | 126 | /** 127 | * @param RepairRequest $repairRequest 128 | * @return array 129 | */ 130 | public function getHiddenFields($repairRequest): array 131 | { 132 | if (!$this->user->hasRole(User::USER_ROLE_WORKSHOP)) { 133 | return ['description', 'city', 'details']; 134 | } 135 | 136 | return []; 137 | } 138 | 139 | /** 140 | * @param RepairRequest $repairRequest 141 | * @return array 142 | */ 143 | public function getVisibleFields($repairRequest): array 144 | { 145 | return []; 146 | } 147 | } 148 | ``` 149 | Now, if we use this policy, it will hide description, city and details fields if user has no "Workshop" role. Let's use it. To use it we need our resource class to extend 150 | ```php 151 | Moofik\LaravelResourceExtenstion\Extension\RestrictableResource; 152 | ``` 153 | Here our new API resource: 154 | 155 | ```php 156 | use App\RepairRequest; 157 | use Moofik\LaravelResourceExtenstion\Extension\RestrictableResource; 158 | 159 | class RepairRequestResource extends RestrictableResource 160 | { 161 | /** 162 | * RepairRequest constructor. 163 | * @param RepairRequest $resource 164 | */ 165 | public function __construct(RepairRequest $resource) 166 | { 167 | parent::__construct($resource); 168 | } 169 | 170 | /** 171 | * Transform the resource into an array. 172 | * 173 | * @param Request $request 174 | * @return array 175 | */ 176 | public function toArray($request) 177 | { 178 | /* If you want policies to work you should use either parent::toArray() or directly call $this->resolvePolicies() */ 179 | return parent::toArray($request); 180 | } 181 | } 182 | ``` 183 | 184 | After that, our resource will obtain three new methods. First (and we need it now) is "applyPolicy" which return resource itself, so that we can use chaining or return its result directly from controller method - it will be serialized to correct response. 185 | We also can use policies with ExtendableResourceCollection and its subclasses. In that case policy will be applied to every underlying resource of ```Moofik\LaravelResourceExtenstion\Extension\RestrictableResource``` class or its subclasses. Below is how we can use it inside controller methods: 186 | ```php 187 | class RepairRequestController 188 | { 189 | public function allRequests(Guard $guard, RepairOffersRepository $repairOfferRepository) 190 | { 191 | /** @var User $user */ 192 | $user = $guard->user(); 193 | $repairRequests = RepairRequest::paginate(); 194 | 195 | $resource = new RepairRequestResourceCollection($repairRequests, $repairOfferRepository); 196 | $resourcePolicy = new RepairRequestPolicy($user); 197 | 198 | return $resource->applyPolicy($resourcePolicy); 199 | } 200 | 201 | public function oneRequest(int $id, Guard $guard, RepairOffersRepository $repairOfferRepository) 202 | { 203 | /** @var User $user */ 204 | $user = $guard->user(); 205 | $repairRequest = RepairRequest::find($id); 206 | 207 | $resource = new RepairRequestResource($repairRequest, $repairOfferRepository); 208 | $resourcePolicy = new RepairRequestPolicy($user); 209 | 210 | return $resource->applyPolicy($resourcePolicy); 211 | } 212 | } 213 | ``` 214 | 215 | By the way, you MAY use ```Moofik\LaravelResourceExtenstion\Extension\RestrictableResource``` instances as class for your resources if you don't want to place custom logic in resource toArray() method 216 | and prefer to write it inside of Resource Policies and Resource Transformers (this topics will be discussed in this documentation below) 217 | 218 | Notes: 219 | 1) You can apply multiple policies to one API resource, but be careful. Order matters. Order of running policies on API resource is the same as an order of applying that policies to that resource. 220 | 2) You MUST either call ```parent::toArray($request)``` or directly call ```$this->resolvePolicies()``` inside toArray() method of your resource, or create anonymous collection with default second argument (most important thing is that second argument must use ```parent::toArray()``` 221 | or directly call ```$this->resolvePolicies()``` inside) to resource policies to work. 222 | #### Resource transformers 223 |

If you need somehow to postprocess resulting array of data either for resource or resource collection you can use resource transformers. Imagine we want to attach "is_offer_made" flag to the resulting resource array, based on simple idea "whether user make offer for given repair request": if offer have been made we set flag to true, and false otherwise. 224 |

225 |

First, we need to create ResourceTransformer subclass that will define our transformer behaviour.

226 | 227 | ```php 228 | use App\User; 229 | use App\RepairRequest; 230 | use Moofik\LaravelResourceExtenstion\Transformer\ResourceTransformer; 231 | 232 | class RepairRequestWorkshopTransformer extends ResourceTransformer 233 | { 234 | /** 235 | * @var RepairRequestOffersRepository 236 | */ 237 | private $repairRequestOffersRepository; 238 | 239 | /** 240 | * @var User 241 | */ 242 | private $user; 243 | 244 | public function __construct(User $user, RepairRequestOffersRepository $repairRequestOffersRepository) 245 | { 246 | $this->user = $user; 247 | $this->repairRequestOffersRepository = $repairRequestOffersRepository; 248 | } 249 | 250 | /** 251 | * @param RepairRequest $resource 252 | * @param array $data 253 | * @return array 254 | */ 255 | public function transform($resource, array $data): array 256 | { 257 | if (!$resource instanceof RepairRequest) { 258 | throw new InvalidArgumentException(sprintf('Invalid argument passed into %s::transform()', __CLASS__)); 259 | } 260 | 261 | $offer = $this->repairRequestOffersRepository->findOneByRepairRequestAndWorkshop($resource, $this->user); 262 | $data['is_offer_make'] = (null === $offer) ? false : true; 263 | 264 | return $data; 265 | } 266 | } 267 | ``` 268 | Now, if we use this transformer, it will postprocess our resource data (it will happen after Laravel calls toArray method of resource). Let's use it. To use it we need our resource class to extends 269 | ```php 270 | Moofik\LaravelResourceExtenstion\Extension\RestrictableResource; 271 | ``` 272 | class. 273 |
274 | As we said earlier, our API resource obtain several new methods. Method that we will use now is called ```applyTransformer()``` and it returns resource itself, so that we can use chaining or return it directly from controller method. Here our "old friend", the resource, which as earlier extends RestrictableResource in order to use transformers feature: 275 | 276 | ```php 277 | use Moofik\LaravelResourceExtenstion\Extension\RestrictableResource; 278 | 279 | class RepairRequest extends RestrictableResource 280 | { 281 | /** 282 | * RepairRequest constructor. 283 | * @param RepairRequest $resource 284 | */ 285 | public function __construct(RepairRequest $resource) 286 | { 287 | parent::__construct($resource); 288 | } 289 | 290 | /** 291 | * Transform the resource into an array. 292 | * 293 | * @param Request $request 294 | * @return array 295 | */ 296 | public function toArray($request) 297 | { 298 | return parent::toArray($request); 299 | } 300 | } 301 | ``` 302 | 303 | By the way, we also can use transformers with ```Moofik\LaravelResourceExtenstion\Extension\ExtendableResourceCollection``` subclasses or ```Moofik\LaravelResourceExtenstion\Extension\ExtendableResourceCollection``` itself. In that case transformer will be applied to every underlying resource of ```Moofik\LaravelResourceExtenstion\Extension\RestrictableResource``` subclass. Below is how we use it inside controller methods: 304 | ```php 305 | class SomeController 306 | { 307 | public function allRequests(Guard $guard, RepairOffersRepository $repairOfferRepository) 308 | { 309 | /** @var User $user */ 310 | $user = $guard->user(); 311 | $repairRequests = RepairRequest::paginate(); 312 | 313 | $resource = new RepairRequestResourceCollection($repairRequests, $repairOfferRepository); 314 | $resourceTransformer = new RepairRequestTransformer($user); 315 | 316 | return $resource->applyTransformer($resourceTransformer); 317 | } 318 | 319 | public function oneRequest(int $id, Guard $guard, RepairOffersRepository $repairOfferRepository) 320 | { 321 | /** @var User $user */ 322 | $user = $guard->user(); 323 | $repairRequest = RepairRequest::find($id); 324 | 325 | $resource = new RepairRequestResource($repairRequest, $repairOfferRepository); 326 | $resourceTransformer = new RepairRequestTransformer($user); 327 | 328 | return $resource->applyTransformer($resourceTransformer); 329 | } 330 | } 331 | ``` 332 | Notes: 333 | 1) You also can apply multiple transformers to one API resource, but be careful. Order matters. Order of running transformers on API resource is the same as an order of applying that transformers to that resource. 334 | 2) You MUST either call ```parent::toArray($request)``` or directly call ```$this->resolveTransformation($data)``` (where data is array that you want to pass to your transformers ```transform($resource, array $data)``` method) inside toArray() method of your resource or create anonymous collection with default second argument 335 | (most important thing is that second argument must use ```parent::toArray()``` or directly call ```$this->resolveTransformation($data)``` inside. Default ```Moofik\LaravelResourceExtenstion\Extension\RestrictableResource``` class of choice for anonymous collections can handle these things automatically.) to resource transformers to work 336 | #### 4) Resource class error inspection helpers 337 |

This small package has a couple of error inspection helper methods to use inside your resources which are convenient in some cases. To use them just add next trait to your class 338 | 339 | ```php 340 | use Moofik\LaravelResourceExtenstions\Extension\ErrorInspectionHelpers; 341 | ``` 342 |
343 | The ErrorInspectionHelpers trait methods are listed below: 344 |

345 | 346 | ```php 347 | public function throwIfResourceIsNot(string $expectedClass) // it will throw InvalidArgumentException if resource is not of class $expectedClass 348 | 349 | public function throwIfNot($instance, string $expectedClass) // it will throw InvalidArgumentException if passed instance object is not of class $expectedClass 350 | 351 | public function throwIfNotAnyOf($instance, array $expectedClasses) // it will throw InvalidArgumentException if passed instance object is not any of classes presented in $expectedClassed array 352 | ``` 353 |
354 | 355 |

356 | 357 | #### Extension pipelines 358 | ExtensionPipeline serves as a configuration class that defines which policies and transformers have to be applied to the API Resource or API Resource Collection. 359 | It can be useful if you are want to apply the same set of policies and transformers to different resources or in multiple places. 360 | Here is the short example: 361 | 362 | ```php 363 | namespace App\Http\Resources\Pipeline; 364 | 365 | use App\User; 366 | use App\Repository\RepairRequestOfferRepository; 367 | use App\Repository\RepairRequestViewerRepository; 368 | use App\Http\Resources\Policy\RepairRequestPolicy; 369 | use App\Http\Resources\Transformer\RepairRequestViewersTransformer; 370 | use App\Http\Resources\Transformer\RepairRequestOffersTransformer; 371 | use Moofik\LaravelResourceExtenstion\Pipeline\ExtensionPipeline; 372 | 373 | class RepairRequestExtensionPipeline extends ExtensionPipeline 374 | { 375 | public function __construct(User $user, RepairRequestOfferRepository $offerRepository, RepairRequestViewerRepository $viewerRepository) 376 | { 377 | $this 378 | ->addPolicy(new RepairRequestPolicy($user, $offerRepository)) 379 | ->addTransformer(new RepairRequestOffersTransformer($user, $offerRepository)) 380 | ->addTransformer(new RepairRequestViewersTransformer($user, $viewerRepository)); 381 | } 382 | } 383 | ``` 384 | 385 | Now we can reuse it in multiple places and with multiple resources: 386 | 387 | ```php 388 | use App\User; 389 | use App\RepairRequest; 390 | use App\Repository\RepairRequestOfferRepository; 391 | use App\Repository\RepairRequestViewerRepository; 392 | use App\Http\Resources\Pipeline\RepairRequestExtensionPipeline; 393 | use App\Http\Resources\RepairRequestResource; 394 | use App\Http\Resources\RepairRequestResourceCollection; 395 | use Illuminate\Contracts\Auth\Guard; 396 | 397 | class SomeController 398 | { 399 | public function repairRequests( 400 | Guard $guard, 401 | RepairRequestOfferRepository $offerRepository, 402 | RepairRequestViewerRepository $viewerRepository 403 | ) { 404 | $repairRequests = RepairRequest::all(); 405 | $pipeline = new RepairRequestExtensionPipeline($guard->user(), $offerRepository, $viewerRepository); 406 | $resource = new RepairRequestResourceCollection($repairRequests, $offerRepository); 407 | 408 | return $resource->applyPipeline($pipeline); 409 | } 410 | 411 | public function repairRequest( 412 | int $id, 413 | Guard $guard, 414 | RepairRequestOfferRepository $offerRepository, 415 | RepairRequestViewerRepository $viewerRepository 416 | ) { 417 | $repairRequest = RepairRequest::find($id); 418 | $pipeline = new RepairRequestExtensionPipeline($guard->user(), $offerRepository, $viewerRepository); 419 | $resource = new RepairRequestResource($repairRequest, $offerRepository); 420 | 421 | return $resource->applyPipeline($pipeline); 422 | } 423 | } 424 | ``` 425 | 426 | ### Security 427 | 428 | If you discover any security-related issues, please email [moofik12@gmail.com](mailto:moofik12@gmail.com) instead of using the issue tracker. -------------------------------------------------------------------------------- /README_ru.md: -------------------------------------------------------------------------------- 1 | ## Содержание 2 | 1) [Установка](#инструкции-по-установке-и-использованию-пакета) 3 | 2) [Тесты](#тесты) 4 | 3) [Инъекция зависимостей в ресурс на базе которого строится коллекция ресурсов](#инъекция-зависимостей-в-ресурс-на-базе-которого-строится-коллекция-ресурсов) 5 | 4) [Анонимные расширенные коллекции ресурсов](#анонимные-расширенные-коллекции-ресурсов) 6 | 5) [Политики ресурсов](#политики-ресурсов) 7 | 6) [Преобразователи ресурсов](#преобразователи-ресурсов) 8 | 7) [Хелперы для инспекции некоторых ошибок ресурса](#хелперы-для-инспекции-некоторых-ошибок-ресурса) 9 | 8) [Пайплайны](#пайплайны) 10 | 9) [Безопасность](#безопасность) 11 | 12 | ## Инструкции по установке и использованию пакета 13 | Чтобы установить пакет, запустите в терминале следующую команду: 14 | ``` bash 15 | composer require moofik/laravel-resources-extensions 16 | ``` 17 | ### Тесты 18 | Для запуска тестов, находясь в корне пакета выполните в терминале комманду 19 | ```shell script 20 | composer test 21 | ``` 22 | 23 | ### Возможности 24 | #### Расширенные коллекции ресурсов 25 | ##### Инъекция зависимостей в ресурс на базе которого строится коллекция ресурсов 26 | При использовании стандартного ResourceCollection в Laravel существуют некоторые ограничения. 27 | Одним тиз аких ограничений является отсутствие возможности передавать произвольные аргументы в ресурс 28 | на базе которого строится наша коллекция ресурсов (это актуально, до тех пор, пока мы используем 29 | в коллекциях ресурсов метод ```parent::toArray()``` или пытаемся создать инстанс ResourceCollection, передав внутрь его конструктора какую-то коллекцию). По умолчанию, если ресурс на базе которого строится ваша коллекция принимает какие-то произвольные аргументы в конструктор - Laravel пробросит исключение. 30 | 31 | Предположим, что у нас есть API Resource, API Resource Collection которая предполагает 32 | его использование, и контроллер который использует эту коллекцию. Рассмотрим на их примере 33 | использование расширенных коллекций ресурсов. 34 |

Дадим возможность конструтору ```__construct()``` у ресурса RepairRequestResource, помимо модели, 35 | принимать второй аргумент типа RepairOffersRepository: 36 | ``` php 37 | class RepairRequestResource extends JsonResource 38 | { 39 | private $repairOfferRepository; 40 | 41 | public function __construct( 42 | RepairRequest $resource, 43 | RepairOffersRepository $repairOfferRepository 44 | ) { 45 | parent::__construct($resource); 46 | $this->repairOfferRepository = $repairOfferRepository; 47 | } 48 | 49 | public function toArray($request) 50 | { 51 | $result = parent::toArray($request); 52 | $result['offers_count'] = $this->repairOfferRepository->countOffersByRequest($this->resource); 53 | 54 | return $result; 55 | } 56 | } 57 | ``` 58 | 59 |

60 | Пусть, мы хотим использовать коллекцию на базе этого ресурса в контроллере (наша конкретная реализация 61 | класса коллекции будет полагаться на вызов parent::toArray() , а следовательно, каждый объект нашей коллекции будет преобразован в RepairRequestResource). 62 | По умолчанию, наследуя класс своей коллекции от дефолтного ResourceCollection, 63 | наша коллекция не будет знать какой аргумент передавать в конструктор RepairRequestResource. 64 | Эту проблему можно решить отнаследовав метод коллекции от класса: 65 | 66 | ```php 67 | Moofik\LaravelResourceExtenstion\Extension\ExtendableResourceCollection 68 | ``` 69 | class; 70 |

71 | 72 | ```php 73 | use Moofik\LaravelResourceExtenstion\Extension\ExtendableResourceCollection; 74 | 75 | class RepairRequestResourceCollection extends ExtendableResourceCollection 76 | { 77 | public function toArray($request) 78 | { 79 | $result = parent::toArray($request); 80 | 81 | /* 82 | * Предположительно, здесь мы делаем какие-то действия с нашим $result 83 | */ 84 | 85 | return $result; 86 | } 87 | } 88 | ``` 89 | 90 |

91 | Теперь конструктор коллекции RepairRequestResourceCollection будет автоматически передавать нужное число аргументов (идущих после первого обязательного аргумента $resource) в конструктор ресурса RepairRequestResource. 92 |

93 | 94 | ```php 95 | class RepairRequestController 96 | { 97 | public function repairRequests(RepairOffersRepository $repairOfferRepository) 98 | { 99 | $repairRequests = RepairRequest::all(); 100 | 101 | return new RepairRequestResourceCollection($repairRequests, $repairOfferRepository); 102 | } 103 | } 104 | ``` 105 | 106 | Примечание: Если бы мы передали в конструктор RepairRequestResourceCollection дополнительные аргументы, кроме тех, которые нужны для конструктора ресурса RepairRequestResource, эти аргументы были бы доступны внутри нашей коллекции. Мы могли бы обратиться к массиву этих аргументов следующим образом: 107 | ```php 108 | $this->args; 109 | ``` 110 | ##### Анонимные расширенные коллекции ресурсов 111 | Чтобы не создавать отдельный класс коллекции ресурсов (например, если нам не нужна кастомная логика в методе ```toArray()```, или всю кастомную логику было решено вынести в политики и преобразователи ресурсов (эти темы будут рассмотрены разделы 2 и 3 документации)) можно воспользоваться статическим методом "extendableCollection". Он возвращает экземпляр класса ```Moofik\LaravelResourceExtenstion\Extension\ExtendableResourceCollection```, дополнительно мы можем передать в него класс используемый для создания коллекции ресурса и набор аргументов для конструктора этого ресурса. По умолчанию, анонимная коллекция использует класс ```Moofik\LaravelResourceExtenstion\Extension\RestrictableResource```, на основе которого создается каждый отдельный ресурс в коллекции. 112 | 113 | #### Политики ресурсов 114 |

Если вам требуется каким-либо образом заставить ваш ресурс принудительно скрывать или делать видимыми поля используемых в ресурсе моделей, особенно в случае если эта логика является достаточно сложной и опирается на какие-то внешние зависимости - вы можете использовать политики ресурсов. Например, пусть нам необходимо отображать в конечном результате, в который сериалиуется ресурс, некоторые поля модели, которые зависят от роли пользователя (это условие приведено лишь для примера, логика отображения или скрытия полей может быть абсолютно любой): 115 |

116 |

Первое, что нам нужно - отнаследовать нашу будущую политику от класса ResourcePolicy. Назовем этот класс RepairRequestPolicy. Внутри него будет располагаться логика отображения/скрытия полей модели используемой ресурсом.

117 | 118 | ```php 119 | use App\User; 120 | use App\RepairRequest; 121 | use Moofik\LaravelResourceExtenstion\Policy\ResourcePolicy; 122 | 123 | class RepairRequestPolicy extends ResourcePolicy 124 | { 125 | /** 126 | * @var User 127 | */ 128 | private $user; 129 | 130 | public function __construct(User $user) 131 | { 132 | $this->user = $user; 133 | } 134 | 135 | /** 136 | * @param RepairRequest $repairRequest 137 | * @return array 138 | */ 139 | public function getHiddenFields($repairRequest): array 140 | { 141 | if (!$this->user->hasRole(User::USER_ROLE_WORKSHOP)) { 142 | return ['description', 'city', 'details']; 143 | } 144 | 145 | return []; 146 | } 147 | 148 | /** 149 | * @param RepairRequest $repairRequest 150 | * @return array 151 | */ 152 | public function getVisibleFields($repairRequest): array 153 | { 154 | return []; 155 | } 156 | } 157 | ``` 158 | Теперь, если мы применим эту политику к нашему ресурсу, то поля description, city and details будут скрыты если у пользователя нет роли "Workshop". Чтобы применить политику к ресурсу, нужно чтобы ресурс наследовал следующий класс 159 | ```php 160 | Moofik\LaravelResourceExtenstion\Extension\RestrictableResource; 161 | ``` 162 | Создадим следующий API Resource: 163 | 164 | ```php 165 | use App\RepairRequest; 166 | use Moofik\LaravelResourceExtenstion\Extension\RestrictableResource; 167 | 168 | class RepairRequestResource extends RestrictableResource 169 | { 170 | /** 171 | * RepairRequest constructor. 172 | * @param RepairRequest $resource 173 | */ 174 | public function __construct(RepairRequest $resource) 175 | { 176 | parent::__construct($resource); 177 | } 178 | 179 | /** 180 | * Transform the resource into an array. 181 | * 182 | * @param Request $request 183 | * @return array 184 | */ 185 | public function toArray($request) 186 | { 187 | /* Если вы хотите чтобы политики работали корректно, вы должны либо полагаться на вызов 188 | * parent::toArray() либо вызывать метод $this->resolvePolicies() напрямую, в начале метода toArray вашего ресурса 189 | */ 190 | return parent::toArray($request); 191 | } 192 | } 193 | ``` 194 | 195 | После этого у ресурса появится несколько новых методов. 196 | Первый (как раз тот, что нам нужен сейчас) - метод ```applyPolicy```, который применяет политику к ресурсу, 197 | и возвращает сам ресурс (и поэтому мы можем использовать чейнинг - цепочки методов - или возвращать результат метода ```applyPolicy``` прямо из методов контроллера) 198 | Политики ресурсов так же могут быть использованы с ExtendableResourceCollection и его наследниками 199 | (смотри пункт 1. документации). В этом случае политики будут применены к каждому подклассу 200 | RestrictableResource, на который полагается наша коллекция. Ниже приведен пример использования политик в методах контроллера вместе с обычными ресурсами и коллекциями ресурсов: 201 | ```php 202 | class RepairRequestController 203 | { 204 | public function allRequests(Guard $guard, RepairOffersRepository $repairOfferRepository) 205 | { 206 | /** @var User $user */ 207 | $user = $guard->user(); 208 | $repairRequests = RepairRequest::paginate(); 209 | 210 | $resource = new RepairRequestResourceCollection($repairRequests, $repairOfferRepository); 211 | $resourcePolicy = new RepairRequestPolicy($user); 212 | 213 | return $resource->applyPolicy($resourcePolicy); 214 | } 215 | 216 | public function oneRequest(int $id, Guard $guard, RepairOffersRepository $repairOfferRepository) 217 | { 218 | /** @var User $user */ 219 | $user = $guard->user(); 220 | $repairRequest = RepairRequest::find($id); 221 | 222 | $resource = new RepairRequestResource($repairRequest, $repairOfferRepository); 223 | $resourcePolicy = new RepairRequestPolicy($user); 224 | 225 | return $resource->applyPolicy($resourcePolicy); 226 | } 227 | } 228 | ``` 229 | 230 | Примечания: 231 | 1) Вы можете применять множество политик к одному ресурсу единовременно, но необходимо делать это осторожно. Порядок применяемых политик имеет значение. Политики будут применены последовательно, в таком же порядке, в котором вы применили их на ресурсе. 232 | 2) Внутри метода toArray() вашего ресурса вы ДОЛЖНЫ либо вызывать ```parent::toArray($request)``` либо же, если вы не хотите полагаться на использование родительского toArray(), напрямую вызывать метод ```$this->resolvePolicies()``` для того чтобы политики работали должным образом. 233 | 234 | #### Преобразователи ресурсов 235 | Если вам нужна какая-то пост-обработка массива который формируется внутри метода ```toArray()``` вашего ресурса или коллекции ресурсов вы можете использовать преобразователи ресурсов. Представим, что нам нужно прикрепить флаг "is_offer_made" к результату метода ```toArray()``` нашего ресурса. Логика прикрепления флага будет основана на простой идее: "принадлежит ли данному пользователю оффер". Если оффер принадлежит данному пользователю, будем устанавливать данный флаг в true, в противном случае значение флага должно быть false. 236 | Первое что нам нужно - создать преобразователь ресурса, отнаследовав его от класса ```Moofik\LaravelResourceExtenstion\Transformer\ResourceTransformer```. 237 | Внутри него будет описана описанных выше преобразований. 238 | 239 | ```php 240 | use App\User; 241 | use App\RepairRequest; 242 | use Moofik\LaravelResourceExtenstion\Transformer\ResourceTransformer; 243 | 244 | class RepairRequestWorkshopTransformer extends ResourceTransformer 245 | { 246 | /** 247 | * @var RepairRequestOffersRepository 248 | */ 249 | private $repairRequestOffersRepository; 250 | 251 | /** 252 | * @var User 253 | */ 254 | private $user; 255 | 256 | public function __construct(User $user, RepairRequestOffersRepository $repairRequestOffersRepository) 257 | { 258 | $this->user = $user; 259 | $this->repairRequestOffersRepository = $repairRequestOffersRepository; 260 | } 261 | 262 | /** 263 | * @param RepairRequest $resource 264 | * @param array $data 265 | * @return array 266 | */ 267 | public function transform($resource, array $data): array 268 | { 269 | if (!$resource instanceof RepairRequest) { 270 | throw new InvalidArgumentException(sprintf('Invalid argument passed into %s::transform()', __CLASS__)); 271 | } 272 | 273 | $offer = $this->repairRequestOffersRepository->findOneByRepairRequestAndWorkshop($resource, $this->user); 274 | $data['is_offer_make'] = (null === $offer) ? false : true; 275 | 276 | return $data; 277 | } 278 | } 279 | ``` 280 | 281 | Чтобы мы могли применить преобразователь на ресурсе, ресурс должен наследовать класс ```Moofik\LaravelResourceExtenstion\Extension\RestrictableResource``` или быть экземляром класса ```Moofik\LaravelResourceExtenstion\Extension\RestrictableResource```. После того, как мы используем преобразователь на ресурсе, ресурс запустит его автоматически, по окончании преобразования данных при вызове ```parent::toArray()```. 282 | В случае полностью кастомной логики метода ```toArray()``` вашего ресурса вы должны вручную вызвать метод ```$this->resolveTransformation($data)``` для запуска преобразователей ресурсов в требуемом месте. 283 | 284 |
285 | 286 | На этот раз, для того, чтобы применить коллекцию на ресурсе воспользуемся другим методом унаследованным от класса ```Moofik\LaravelResourceExtenstion\Extension\RestrictableResource```. Нужный нам метод называется ```applyTransformer```. В качестве результата выполнения этот метод возвращает инстанс текущего ресурса или коллекции ресурсов, благодаря этому мы можем использовать цепчки методов (например применить несколько преобразователей подряд) или вернуть результат выполнения из метода контроллера. Он будет преобразован в корректный Response. Рассмотрим на примере: 287 | 288 | ```php 289 | use Moofik\LaravelResourceExtenstion\Extension\RestrictableResource; 290 | 291 | class RepairRequest extends RestrictableResource 292 | { 293 | /** 294 | * RepairRequest constructor. 295 | * @param RepairRequest $resource 296 | */ 297 | public function __construct(RepairRequest $resource) 298 | { 299 | parent::__construct($resource); 300 | } 301 | 302 | /** 303 | * Transform the resource into an array. 304 | * 305 | * @param Request $request 306 | * @return array 307 | */ 308 | public function toArray($request) 309 | { 310 | return parent::toArray($request); 311 | } 312 | } 313 | ``` 314 | 315 | Кстати, мы также можем использовать преобразователи ресурсов вместе с классом ExtendableResourceCollection или его потомками. В этом случае каждый заданный преобразователь ресурса будет последовательно применен к каждому отдельному ресурсу коллекции. 316 | Повторю, что это будет актуально, только в нескольких случаях: в случае создания коллекции с помощью метода ```ExtendableResourceCollection::extendableCollection($collection)``` с дефолтными параметрами - 317 | в этом случае, для представления ресурсов внутри коллекции используется класс RestrictableResource, который "из коробки" позволяет применять к нему политики, преобразователи и цепочки - следовательно коллекция которая была создана этим методом также 318 | будет корректно работать с политиками, преобразователями и цепочками; 319 | в случае вызова ```parent::toArray()``` внутри метода ```toArray()``` РЕСУРСА (да, речь идет именно об одиночном ресурсе, а не о коллекции ресурсов) к которому мы хотим применять преобразователи; 320 | в случае ручного вызова метода ```$this->resolveTransformation($data)``` в том месте метода ```toArray()``` вашего ресурса, где вы хотите применить преобразователи ресурса). 321 | Ниже приведен пример использования преобразователей ресурса в методах контроллера: 322 | ```php 323 | class RepairRequestController 324 | { 325 | public function all(Guard $guard, RepairOffersRepository $repairOfferRepository) 326 | { 327 | /** @var User $user */ 328 | $user = $guard->user(); 329 | $repairRequests = RepairRequest::paginate(); 330 | 331 | $resource = new RepairRequestResourceCollection($repairRequests, $repairOfferRepository); 332 | $resourceTransformer = new RepairRequestTransformer($user); 333 | 334 | return $resource->applyTransformer($resourceTransformer); 335 | } 336 | 337 | public function one(int $id, Guard $guard, RepairOffersRepository $repairOfferRepository) 338 | { 339 | /** @var User $user */ 340 | $user = $guard->user(); 341 | $repairRequest = RepairRequest::find($id); 342 | 343 | $resource = new RepairRequestResource($repairRequest, $repairOfferRepository); 344 | $resourceTransformer = new RepairRequestTransformer($user); 345 | 346 | return $resource->applyTransformer($resourceTransformer); 347 | } 348 | } 349 | ``` 350 | 351 | Примечания: 352 | 1) Вы можете применить несколько преобразователей ресурса к одному и тому же ресурсы, но будьте внимательны, поскольку порядок их применения имеет значение. Порядок запуска преобразователей ресурса на каждом отдельном ресурсе коллекции будет таким же, как порядок применения преобразователей ресурса к коллекции ресурса. 353 | 2) Вы должны либо полагаться на вызов ```parent::toArray($request)```, либо же вызывать напрямую метод ```$this->resolveTransformation($data)``` внутри метода ```toArray()``` вашего ресурса, либо создавать анонимную коллекцию с помощью 354 | метода ```ExtendableResourceCollection::extendableCollection()``` с дефолтными параметрами, для того чтобы преобразователи ресурсов могли работать. Преобразователи ресурсов будут так же работать если в качестве второго аргумета метода ```ExtendableResourceCollection::extendableCollection()``` будет передано имя класса, 355 | метод ```toArray()``` которого использует ```parent::toArray($request)```, либо ```$this->resolveTransformation($data)```. 356 | #### Хелперы для инспекции некоторых ошибок ресурса 357 |

Этот пакет также имеет несколько полезных трейтов с хелперами, которые можно использовать внутри JsonResource, ResourceCollection и всех их подклассов, включая ExtendableResourceCollection и RestrictableResource.This small package has a couple of error inspection helper methods to use inside your resources which are convenient in some cases. To use them just add next trait to your class 358 | 359 | ```php 360 | use Moofik\LaravelResourceExtenstions\Extension\ErrorInspectionHelpers; 361 | ``` 362 |
363 | Методы-хелперы трейта ErrorInspectionHelpers перечислены ниже: 364 |

365 | 366 | ```php 367 | public function throwIfResourceIsNot(string $expectedClass) // выбросит ошибку, в случае если переданный в API Resource объект используемый для создания ресурса (это может быть модель, пагинатор, коллекция или какой-то другой класс) имеет класс с именем не идентичным параметру $expectedClass 368 | 369 | public function throwIfNot($instance, string $expectedClass) // // выбросит ошибку, в случае если переданный в метод аргумент $instance имеет класс с именем не идентичным параметру $expectedClass 370 | 371 | public function throwIfNotAnyOf($instance, array $expectedClasses) // выбросит ошибку, в случае если переданный в метод аргумент $instance имеет класс с именем не совпадающим ни с одним значением из массива $expectedClass 372 | ``` 373 |
374 | 375 |

376 | 377 | #### Пайплайны 378 | Цепочка ресурсов и преобразователей (пайплайн) представляет собой конфигурационный класс, который определяет какие политики и преобразователи будут применены к ресурсу, на котором будет использованная данная цепочка. 379 | Пайплайны могут быть полезны, если вы хотите использовать один и тот же набор политик и преобразователей для ресурсов в разных местах программы (самый очевидный пример - в разных методах одного контроллера). 380 | Каждый пайплайн должен наследовать класс ```use Moofik\LaravelResourceExtenstion\Pipeline\ExtensionPipeline```. 381 | Ниже приведен простой пример пайплайна и его использования: 382 | 383 | ```php 384 | namespace App\Http\Resources\Pipeline; 385 | 386 | use App\User; 387 | use App\Repository\RepairRequestOfferRepository; 388 | use App\Repository\RepairRequestViewerRepository; 389 | use App\Http\Resources\Policy\RepairRequestPolicy; 390 | use App\Http\Resources\Transformer\RepairRequestViewersTransformer; 391 | use App\Http\Resources\Transformer\RepairRequestOffersTransformer; 392 | use Moofik\LaravelResourceExtenstion\Pipeline\ExtensionPipeline; 393 | 394 | class RepairRequestExtensionPipeline extends ExtensionPipeline 395 | { 396 | public function __construct(User $user, RepairRequestOfferRepository $offerRepository, RepairRequestViewerRepository $viewerRepository) 397 | { 398 | $this 399 | ->addPolicy(new RepairRequestPolicy($user, $offerRepository)) 400 | ->addTransformer(new RepairRequestOffersTransformer($user, $offerRepository)) 401 | ->addTransformer(new RepairRequestViewersTransformer($user, $viewerRepository)); 402 | } 403 | } 404 | ``` 405 | 406 | Теперь мы можем использовать этот пайплайн с разными ресурсами в разных местах приложения (с одинаковым успехом вместе с ExtendableResourceCollection, RestrictableResource и их наследниками): 407 | 408 | ```php 409 | use App\User; 410 | use App\RepairRequest; 411 | use App\Repository\RepairRequestOfferRepository; 412 | use App\Repository\RepairRequestViewerRepository; 413 | use App\Http\Resources\Pipeline\RepairRequestExtensionPipeline; 414 | use App\Http\Resources\RepairRequestResource; 415 | use App\Http\Resources\RepairRequestResourceCollection; 416 | use Illuminate\Contracts\Auth\Guard; 417 | 418 | class RepairRequestController 419 | { 420 | public function repairRequests( 421 | Guard $guard, 422 | RepairRequestOfferRepository $offerRepository, 423 | RepairRequestViewerRepository $viewerRepository 424 | ) { 425 | $repairRequests = RepairRequest::all(); 426 | $pipeline = new RepairRequestExtensionPipeline($guard->user(), $offerRepository, $viewerRepository); 427 | $resource = new RepairRequestResourceCollection($repairRequests, $offerRepository); 428 | 429 | return $resource->applyPipeline($pipeline); 430 | } 431 | 432 | public function repairRequest( 433 | int $id, 434 | Guard $guard, 435 | RepairRequestOfferRepository $offerRepository, 436 | RepairRequestViewerRepository $viewerRepository 437 | ) { 438 | $repairRequest = RepairRequest::find($id); 439 | $pipeline = new RepairRequestExtensionPipeline($guard->user(), $offerRepository, $viewerRepository); 440 | $resource = new RepairRequestResource($repairRequest, $offerRepository); 441 | 442 | return $resource->applyPipeline($pipeline); 443 | } 444 | } 445 | ``` 446 | 447 | ### Безопасность 448 | 449 | Если вы обнаружите какие-то проблемы связанные с безопасностью в коде пакета, пожалуйста напишите об этом на электронную почту [moofik12@gmail.com](mailto:moofik12@gmail.com). Не используйте issue tracker для обсуждения проблем с безопасностью. -------------------------------------------------------------------------------- /composer.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "moofik/laravel-resources-extensions", 3 | "description": "This package provide new resource classes and helper traits which will extent standard abilities of Laravel resources", 4 | "keywords": [ 5 | "moofik", 6 | "laravel", 7 | "resource", 8 | "resources", 9 | "api", 10 | "api-resources" 11 | ], 12 | "homepage": "https://github.com/moofik/laravel-resource-extensions", 13 | "license": "MIT", 14 | "authors": [ 15 | { 16 | "name": "Alexander Orlovsky", 17 | "email": "moofik12@gmail.com" 18 | } 19 | ], 20 | "require": { 21 | "php": ">=7.2", 22 | "illuminate/container": ">=7.0", 23 | "illuminate/contracts": ">=7.0", 24 | "illuminate/database": ">=7.0", 25 | "illuminate/pagination": ">=7.0", 26 | "illuminate/http": ">=7.0", 27 | "illuminate/support": ">=7.0" 28 | }, 29 | "autoload": { 30 | "psr-4": { 31 | "Moofik\\LaravelResourceExtenstion\\": "src", 32 | "Moofik\\LaravelResourceExtenstion\\Tests\\": "tests" 33 | } 34 | }, 35 | "require-dev": { 36 | "phpunit/phpunit": ">=8.5" 37 | }, 38 | "scripts": { 39 | "test": "phpunit" 40 | } 41 | } 42 | -------------------------------------------------------------------------------- /phpunit.xml.dist: -------------------------------------------------------------------------------- 1 | 2 | 12 | 13 | 14 | tests 15 | 16 | 17 | 18 | 19 | src/ 20 | 21 | 22 | 23 | 24 | 25 | -------------------------------------------------------------------------------- /src/Extension/AnonymousResourceCollection.php: -------------------------------------------------------------------------------- 1 | resource = $resource; 18 | $this->args = $args; 19 | } 20 | 21 | /** 22 | * @param string $class 23 | * @return $this 24 | */ 25 | public function setUnderlyingResource(string $class) 26 | { 27 | $this->underlyingResourceClass = $class; 28 | parent::__construct($this->resource, $this->args); 29 | 30 | return $this; 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /src/Extension/ErrorInspectionHelpers.php: -------------------------------------------------------------------------------- 1 | resource instanceof $expectedClass) { 23 | if (!isset($this->resource)) { 24 | $actual = 'NULL'; 25 | } else { 26 | $actual = get_class($this->resource); 27 | } 28 | 29 | throw new InvalidArgumentException( 30 | get_class($this), 31 | $actual, 32 | $expectedClass 33 | ); 34 | } 35 | } 36 | 37 | /** 38 | * @param mixed $instance 39 | * @param string $expectedClass 40 | */ 41 | public function throwIfNot($instance, string $expectedClass) 42 | { 43 | if (!isset($this->resource)) { 44 | $actual = 'NULL'; 45 | } else { 46 | $actual = get_class($this->resource); 47 | } 48 | 49 | if (!$instance instanceof $expectedClass) { 50 | throw new InvalidArgumentException( 51 | get_class($this), 52 | $actual, 53 | $expectedClass 54 | ); 55 | } 56 | } 57 | 58 | /** 59 | * @param $instance 60 | * @param array $expectedClasses 61 | */ 62 | public function throwIfNotAnyOf($instance, array $expectedClasses) 63 | { 64 | $expectedClassesList = ''; 65 | $times = 0; 66 | 67 | foreach ($expectedClasses as $expectedClass) { 68 | try { 69 | $this->throwIfNot($instance, $expectedClass); 70 | } catch (Throwable $exception) { 71 | $expectedClassesList = ' nor ' . $expectedClass; 72 | $times++; 73 | } 74 | } 75 | 76 | if ($times === count($expectedClasses)) { 77 | $this->throwIfNot($instance, $expectedClassesList); 78 | } 79 | } 80 | } 81 | -------------------------------------------------------------------------------- /src/Extension/Exception/InvalidArgumentException.php: -------------------------------------------------------------------------------- 1 | args = $args; 47 | parent::__construct($resource); 48 | } 49 | 50 | /** 51 | * @param mixed $resource 52 | * @return AbstractPaginator|Collection|mixed 53 | */ 54 | public function collectResource($resource) 55 | { 56 | if ($resource instanceof MissingValue) { 57 | return $resource; 58 | } 59 | 60 | if (is_array($resource)) { 61 | $resource = new Collection($resource); 62 | } 63 | 64 | if (isset($this->underlyingResourceClass)) { 65 | $classToCollect = $this->underlyingResourceClass; 66 | } else { 67 | $classToCollect = $this->collects(); 68 | } 69 | 70 | $this->collection = $classToCollect && !$resource->first() instanceof $classToCollect 71 | ? $this->mapIntoResourceCollection($classToCollect, $resource) 72 | : $resource->toBase(); 73 | 74 | return $resource instanceof AbstractPaginator 75 | ? $resource->setCollection($this->collection) 76 | : $this->collection; 77 | } 78 | 79 | /** 80 | * @param string $classToCollect 81 | * @param Collection|LengthAwarePaginator $resource 82 | * @return Collection 83 | */ 84 | protected function mapIntoResourceCollection(string $classToCollect, $resource): Collection 85 | { 86 | try { 87 | $class = new ReflectionClass($classToCollect); 88 | } catch (ReflectionException $e) { 89 | throw new ResourceMappingException(sprintf('Class %s does not exist.', $classToCollect)); 90 | } 91 | 92 | $additionalParametersCount = $class->getConstructor()->getNumberOfParameters() - 1; 93 | 94 | if ($additionalParametersCount > count($this->args)) { 95 | throw new ResourceMappingException(sprintf('Invalid arguments count passed to %s constructor.', __CLASS__)); 96 | } 97 | 98 | $this->resourceConstructorArguments = array_splice($this->args, 0, $additionalParametersCount); 99 | 100 | return $resource->map(function ($value, $key) use ($classToCollect) { 101 | $instance = new $classToCollect($value, ...$this->resourceConstructorArguments); 102 | 103 | return $instance; 104 | }); 105 | } 106 | 107 | /** 108 | * @param Request $request 109 | * @return array 110 | */ 111 | public function toArray($request) 112 | { 113 | $hasPolices = isset($this->policies) && !empty($this->policies); 114 | $hasTransformers = isset($this->transformers) && !empty($this->transformers); 115 | 116 | foreach ($this->collection as $instance) { 117 | if ($hasPolices && ($instance instanceof RestrictableResource)) { 118 | foreach ($this->policies as $policy) { 119 | $instance->applyPolicy($policy); 120 | } 121 | } 122 | 123 | if ($hasTransformers && ($instance instanceof RestrictableResource)) { 124 | foreach ($this->transformers as $transformer) { 125 | $instance->applyTransformer($transformer); 126 | } 127 | } 128 | } 129 | 130 | return parent::toArray($request); 131 | } 132 | 133 | /** 134 | * @param $resource 135 | * @param string $class 136 | * @param array $args 137 | * @return $this 138 | */ 139 | public static function extendableCollection($resource, string $class = RestrictableResource::class, ...$args): ExtendableResourceCollection 140 | { 141 | $collection = new AnonymousResourceCollection($resource, $args); 142 | $collection->setUnderlyingResource($class); 143 | 144 | return $collection; 145 | } 146 | } 147 | -------------------------------------------------------------------------------- /src/Extension/HasActiveFlag.php: -------------------------------------------------------------------------------- 1 | intersect($allItems); 51 | $falseFlagged = $allItems->diff($activeItems); 52 | $this->flagName = $flagName; 53 | 54 | $trueFlagged->each(function (Model $item) use ($flagName) { 55 | $item[$flagName] = true; 56 | }); 57 | 58 | $falseFlagged->each(function (Model $item) use ($flagName) { 59 | $item[$flagName] = false; 60 | }); 61 | 62 | $this->flagCollection = true; 63 | $this->flaggedResult = $trueFlagged->merge($falseFlagged); 64 | 65 | if ($this->onlyTrueFlaggedResources) { 66 | return $this->flaggedResult->where($flagName, true); 67 | } 68 | 69 | if ($this->onlyFalseFlaggedResources) { 70 | return $this->flaggedResult->where($flagName, false); 71 | } 72 | 73 | return $this->flaggedResult; 74 | } 75 | 76 | /** 77 | * Restricts result collection to resource models with true flag. 78 | * It can be used either before or after "attachActiveFlag" method - it doesn't affect final result. 79 | * 80 | * @return static 81 | */ 82 | public function onlyTrueFlagged(): self 83 | { 84 | if ($this->flagCollection) { 85 | $data = $this->flaggedResult->where($this->flagName, true); 86 | $this->collection = new Collection($data); 87 | } else { 88 | $this->onlyTrueFlaggedResources = true; 89 | } 90 | 91 | 92 | return $this; 93 | } 94 | 95 | /** 96 | * Restricts result collection to resource models with false flag 97 | * It can be used either before or after "flagCollection" method - it doesn't affect final result. 98 | * 99 | * @return static 100 | */ 101 | public function onlyFalseFlagged(): self 102 | { 103 | if ($this->flagCollection) { 104 | $data = $this->flaggedResult->where($this->flagName, false); 105 | $this->collection = new Collection($data); 106 | } else { 107 | $this->onlyFalseFlaggedResources = true; 108 | } 109 | 110 | 111 | return $this; 112 | } 113 | } 114 | -------------------------------------------------------------------------------- /src/Extension/RestrictableResource.php: -------------------------------------------------------------------------------- 1 | resolvePolicy(); 27 | 28 | if (is_null($this->resource)) { 29 | return []; 30 | } 31 | 32 | $result = is_array($this->resource) 33 | ? $this->resource 34 | : $this->resource->toArray(); 35 | 36 | return $this->resolveTransformation($result); 37 | } 38 | 39 | /** 40 | * Resolve resource policy 41 | */ 42 | protected function resolvePolicy() 43 | { 44 | if (empty($this->policies)) { 45 | return; 46 | } 47 | 48 | foreach ($this->policies as $policy) { 49 | if (is_array($this->resource)) { 50 | foreach ($this->resource as $singleResource) { 51 | if ($singleResource instanceof Model) { 52 | $singleResource->makeHidden($policy->getHiddenFields($singleResource)); 53 | $singleResource->makeVisible($policy->getVisibleFields($singleResource)); 54 | } 55 | } 56 | } elseif ($this->resource instanceof Model) { 57 | $this->resource->makeHidden($policy->getHiddenFields($this->resource)); 58 | $this->resource->makeVisible($policy->getVisibleFields($this->resource)); 59 | } 60 | } 61 | } 62 | 63 | /** 64 | * Resolve resource transformations 65 | * 66 | * @param array $data 67 | * @return array 68 | */ 69 | protected function resolveTransformation(array $data): array 70 | { 71 | foreach ($this->transformers as $transformer) { 72 | $data = $transformer->transform($this->resource, $data); 73 | } 74 | 75 | return $data; 76 | } 77 | } 78 | -------------------------------------------------------------------------------- /src/Pipeline/ExtensionPipeline.php: -------------------------------------------------------------------------------- 1 | resourcePolicies[] = $resourcePolicy; 29 | 30 | return $this; 31 | } 32 | 33 | /** 34 | * @param ResourceTransformer $resourceTransformer 35 | * @return ExtensionPipeline 36 | */ 37 | public function addTransformer(ResourceTransformer $resourceTransformer): ExtensionPipeline 38 | { 39 | $this->resourceTransformers[] = $resourceTransformer; 40 | 41 | return $this; 42 | } 43 | 44 | /** 45 | * @return ResourcePolicy[] 46 | */ 47 | public function getResourcePolicies(): array 48 | { 49 | return $this->resourcePolicies; 50 | } 51 | 52 | /** 53 | * @return ResourceTransformer[] 54 | */ 55 | public function getResourceTransformers(): array 56 | { 57 | return $this->resourceTransformers; 58 | } 59 | } 60 | -------------------------------------------------------------------------------- /src/Pipeline/UsesExtensionPipeline.php: -------------------------------------------------------------------------------- 1 | policies) && is_array($this->policies)) { 16 | $this->policies = $extensionPipeline->getResourcePolicies(); 17 | } 18 | 19 | if (isset($this->transformers) && is_array($this->transformers)) { 20 | $this->transformers = $extensionPipeline->getResourceTransformers(); 21 | } 22 | 23 | return $this; 24 | } 25 | } -------------------------------------------------------------------------------- /src/Policy/ResourcePolicy.php: -------------------------------------------------------------------------------- 1 | policies[] = $policy; 22 | 23 | return $this; 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /src/Transformer/ResourceTransformer.php: -------------------------------------------------------------------------------- 1 | transformers[] = $transformer; 21 | 22 | return $this; 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /tests/ExtendableResourceCollectionTest.php: -------------------------------------------------------------------------------- 1 | model = new MockModel(); 38 | $this->model->setAttrs([ 39 | 'first_name' => 'John', 40 | 'last_name' => 'Doe', 41 | ]); 42 | 43 | $this->model_2 = new MockModel(); 44 | $this->model_2->setAttrs([ 45 | 'first_name' => 'Jane', 46 | 'last_name' => 'Doe', 47 | ]); 48 | 49 | $headsOrTails = new HeadsOrTails(); 50 | $collection = new Collection(); 51 | $collection 52 | ->add($this->model) 53 | ->add($this->model_2); 54 | 55 | $this->resource = new MockResourceCollection($collection, $headsOrTails); 56 | } 57 | 58 | public function testExtendableResourceCollectionPassingArgsToUnderlyingResources() 59 | { 60 | $result = $this 61 | ->resource 62 | ->toArray(Request::createFromGlobals()); 63 | 64 | $this->assertEquals('John', $result[0]['first_name']); 65 | $this->assertEquals('Doe', $result[0]['last_name']); 66 | $this->assertArrayHasKey('coin_side', $result[0]); 67 | $this->assertEquals('Jane', $result[1]['first_name']); 68 | $this->assertEquals('Doe', $result[1]['last_name']); 69 | $this->assertArrayHasKey('coin_side', $result[1]); 70 | } 71 | 72 | public function testExtendableResourceCollectionPolicies() 73 | { 74 | $this->model->setHidden(['last_name']); 75 | 76 | $policy_1 = new PolicyStub(['first_name'], []); 77 | $policy_2 = new PolicyStub([], ['last_name']); 78 | 79 | $result = $this 80 | ->resource 81 | ->applyPolicy($policy_1) 82 | ->applyPolicy($policy_2) 83 | ->toArray(Request::createFromGlobals()); 84 | 85 | $this->assertEquals('Doe', $result[0]['last_name']); 86 | $this->assertArrayHasKey('coin_side', $result[0]); 87 | $this->assertEquals('Doe', $result[1]['last_name']); 88 | $this->assertArrayHasKey('coin_side', $result[1]); 89 | } 90 | 91 | public function testTransformers() 92 | { 93 | $transformer_1 = new TransformerStub(function ($resource, array $data) { 94 | $data['test'] = 'test'; 95 | return $data; 96 | }); 97 | 98 | $transformer_2 = new TransformerStub(function ($resource, array $data) { 99 | $data['test_2'] = 'test_2'; 100 | return $data; 101 | }); 102 | 103 | $result = $this 104 | ->resource 105 | ->applyTransformer($transformer_1) 106 | ->applyTransformer($transformer_2) 107 | ->toArray(Request::createFromGlobals()); 108 | 109 | $this->assertEquals('Doe', $result[0]['last_name']); 110 | $this->assertEquals('test', $result[0]['test']); 111 | $this->assertEquals('test_2', $result[0]['test_2']); 112 | $this->assertArrayHasKey('coin_side', $result[0]); 113 | $this->assertEquals('Doe', $result[1]['last_name']); 114 | $this->assertEquals('test', $result[1]['test']); 115 | $this->assertEquals('test_2', $result[1]['test_2']); 116 | $this->assertArrayHasKey('coin_side', $result[1]); 117 | } 118 | 119 | public function testMultiplePoliciesAndTransformers() 120 | { 121 | $this->model->setHidden(['last_name']); 122 | $this->model_2->setHidden(['last_name']); 123 | 124 | $policy_1 = new PolicyStub(['first_name'], []); 125 | $policy_2 = new PolicyStub([], ['last_name']); 126 | 127 | $transformer_1 = new TransformerStub(function ($resource, array $data) { 128 | $data['test'] = 'test'; 129 | return $data; 130 | }); 131 | 132 | $transformer_2 = new TransformerStub(function ($resource, array $data) { 133 | $data['test_2'] = 'test_2'; 134 | return $data; 135 | }); 136 | 137 | $result = $this 138 | ->resource 139 | ->applyPolicy($policy_1) 140 | ->applyPolicy($policy_2) 141 | ->applyTransformer($transformer_1) 142 | ->applyTransformer($transformer_2) 143 | ->toArray(Request::createFromGlobals()); 144 | 145 | $this->assertEquals('Doe', $result[0]['last_name']); 146 | $this->assertEquals('test', $result[0]['test']); 147 | $this->assertEquals('test_2', $result[0]['test_2']); 148 | $this->assertArrayHasKey('coin_side', $result[0]); 149 | $this->assertEquals('Doe', $result[0]['last_name']); 150 | $this->assertEquals('test', $result[0]['test']); 151 | $this->assertEquals('test_2', $result[0]['test_2']); 152 | $this->assertArrayHasKey('coin_side', $result[0]); 153 | } 154 | 155 | 156 | public function testMultiplePoliciesAndTransformersOrderMatters() 157 | { 158 | $this->model->setHidden(['last_name']); 159 | $this->model_2->setHidden(['last_name']); 160 | 161 | $policy_1 = new PolicyStub(['first_name'], []); 162 | $policy_2 = new PolicyStub([], ['first_name']); 163 | 164 | $transformer_1 = new TransformerStub(function ($resource, array $data) { 165 | $data['test'] = 'test'; 166 | return $data; 167 | }); 168 | 169 | $transformer_2 = new TransformerStub(function ($resource, array $data) { 170 | $data['test'] = 'test_2'; 171 | return $data; 172 | }); 173 | 174 | $result_1 = $this 175 | ->resource 176 | ->applyPolicy($policy_1) 177 | ->applyPolicy($policy_2) 178 | ->applyTransformer($transformer_2) 179 | ->applyTransformer($transformer_1) 180 | ->toArray(Request::createFromGlobals()); 181 | 182 | $result_2 = $this 183 | ->resource 184 | ->applyPolicy($policy_2) 185 | ->applyPolicy($policy_1) 186 | ->applyTransformer($transformer_2) 187 | ->applyTransformer($transformer_1) 188 | ->toArray(Request::createFromGlobals()); 189 | 190 | $this->assertNotEquals($result_1, $result_2); 191 | } 192 | 193 | public function testExtendablePipeline() 194 | { 195 | $this->model->setHidden(['last_name']); 196 | $this->model_2->setHidden(['last_name']); 197 | 198 | $policy_1 = new PolicyStub(['first_name'], []); 199 | $policy_2 = new PolicyStub([], ['last_name']); 200 | 201 | $transformer_1 = new TransformerStub(function ($resource, array $data) { 202 | $data['test'] = 'test'; 203 | return $data; 204 | }); 205 | 206 | $transformer_2 = new TransformerStub(function ($resource, array $data) { 207 | $data['test_2'] = 'test_2'; 208 | return $data; 209 | }); 210 | 211 | $extensionPipeline = new MockExtensionPipeline(); 212 | $extensionPipeline 213 | ->addPolicy($policy_1) 214 | ->addPolicy($policy_2) 215 | ->addTransformer($transformer_1) 216 | ->addTransformer($transformer_2); 217 | 218 | $result = $this 219 | ->resource 220 | ->applyPipeline($extensionPipeline) 221 | ->toArray(Request::createFromGlobals()); 222 | 223 | $this->assertEquals('Doe', $result[0]['last_name']); 224 | $this->assertEquals('test', $result[0]['test']); 225 | $this->assertEquals('test_2', $result[0]['test_2']); 226 | $this->assertArrayHasKey('coin_side', $result[0]); 227 | $this->assertEquals('Doe', $result[0]['last_name']); 228 | $this->assertEquals('test', $result[0]['test']); 229 | $this->assertEquals('test_2', $result[0]['test_2']); 230 | $this->assertArrayHasKey('coin_side', $result[0]); 231 | } 232 | } -------------------------------------------------------------------------------- /tests/Mocks/MockExtensionPipeline.php: -------------------------------------------------------------------------------- 1 | attributes = $attrs; 17 | } 18 | } -------------------------------------------------------------------------------- /tests/Mocks/MockResource.php: -------------------------------------------------------------------------------- 1 | headsOrTails = $headsOrTails; 27 | } 28 | 29 | /** 30 | * @param Request $request 31 | * @return array 32 | */ 33 | public function toArray($request) 34 | { 35 | $result = parent::toArray($request); 36 | $result['coin_side'] = $this->headsOrTails->head(); 37 | 38 | return $result; 39 | } 40 | } -------------------------------------------------------------------------------- /tests/Mocks/MockResourceCollection.php: -------------------------------------------------------------------------------- 1 | hidden = $hidden; 29 | $this->visible = $visible; 30 | } 31 | 32 | /** 33 | * @param mixed $resource 34 | * @return array 35 | */ 36 | public function getHiddenFields($resource): array 37 | { 38 | return $this->hidden; 39 | } 40 | 41 | /** 42 | * @param mixed $resource 43 | * @return array 44 | */ 45 | public function getVisibleFields($resource): array 46 | { 47 | return $this->visible; 48 | } 49 | } -------------------------------------------------------------------------------- /tests/Mocks/TransformerStub.php: -------------------------------------------------------------------------------- 1 | closure = $closure; 24 | } 25 | 26 | public function transform($resource, array $data): array 27 | { 28 | return $this->closure->call($this, $resource, $data); 29 | } 30 | } -------------------------------------------------------------------------------- /tests/RestrictableResourceTest.php: -------------------------------------------------------------------------------- 1 | model = new MockModel(); 31 | $this->model->setAttrs([ 32 | 'first_name' => 'John', 33 | 'last_name' => 'Doe', 34 | 'city' => 'New Jersey', 35 | ]); 36 | 37 | $headsOrTails = new HeadsOrTails(); 38 | $this->resource = new MockResource($this->model, $headsOrTails); 39 | } 40 | 41 | public function testPolicies() 42 | { 43 | $this->model->setHidden(['last_name']); 44 | 45 | $policy_1 = new PolicyStub(['first_name'], []); 46 | $policy_2 = new PolicyStub([], ['last_name']); 47 | 48 | $result = $this 49 | ->resource 50 | ->applyPolicy($policy_1) 51 | ->applyPolicy($policy_2) 52 | ->toArray(Request::createFromGlobals()); 53 | 54 | $this->assertArrayHasKey('last_name', $result); 55 | $this->assertArrayNotHasKey('first_name', $result); 56 | $this->assertArrayHasKey('coin_side', $result); 57 | $this->assertArrayHasKey('city', $result); 58 | } 59 | 60 | public function testTransformers() 61 | { 62 | $transformer_1 = new TransformerStub(function ($resource, array $data) { 63 | $data['test'] = 'test'; 64 | return $data; 65 | }); 66 | 67 | $transformer_2 = new TransformerStub(function ($resource, array $data) { 68 | $data['test_2'] = 'test_2'; 69 | return $data; 70 | }); 71 | 72 | $result = $this 73 | ->resource 74 | ->applyTransformer($transformer_1) 75 | ->applyTransformer($transformer_2) 76 | ->toArray(Request::createFromGlobals()); 77 | 78 | $this->assertArrayHasKey('last_name', $result); 79 | $this->assertArrayHasKey('first_name', $result); 80 | $this->assertArrayHasKey('coin_side', $result); 81 | $this->assertArrayHasKey('city', $result); 82 | $this->assertEquals('test', $result['test']); 83 | $this->assertEquals('test_2', $result['test_2']); 84 | } 85 | 86 | public function testMultipleTransformersAndPolicies() 87 | { 88 | $this->model->setHidden(['last_name']); 89 | 90 | $policy_1 = new PolicyStub(['first_name'], []); 91 | $policy_2 = new PolicyStub([], ['last_name']); 92 | 93 | $transformer_1 = new TransformerStub(function ($resource, array $data) { 94 | $data['test'] = 'test'; 95 | return $data; 96 | }); 97 | 98 | $transformer_2 = new TransformerStub(function ($resource, array $data) { 99 | $data['test_2'] = 'test_2'; 100 | return $data; 101 | }); 102 | 103 | $result = $this 104 | ->resource 105 | ->applyPolicy($policy_1) 106 | ->applyPolicy($policy_2) 107 | ->applyTransformer($transformer_1) 108 | ->applyTransformer($transformer_2) 109 | ->toArray(Request::createFromGlobals()); 110 | 111 | $this->assertArrayHasKey('last_name', $result); 112 | $this->assertArrayNotHasKey('first_name', $result); 113 | $this->assertArrayHasKey('coin_side', $result); 114 | $this->assertArrayHasKey('city', $result); 115 | $this->assertEquals('test', $result['test']); 116 | $this->assertEquals('test_2', $result['test_2']); 117 | } 118 | 119 | public function testMultipleTransformersAndPoliciesOrderMatters() 120 | { 121 | $this->model->setHidden(['last_name']); 122 | 123 | $policy_1 = new PolicyStub(['first_name'], []); 124 | $policy_2 = new PolicyStub([], ['last_name']); 125 | 126 | $transformer_1 = new TransformerStub(function ($resource, array $data) { 127 | $data['test'] = 'test'; 128 | return $data; 129 | }); 130 | 131 | $transformer_2 = new TransformerStub(function ($resource, array $data) { 132 | $data['test'] = 'test_2'; 133 | return $data; 134 | }); 135 | 136 | $result = $this 137 | ->resource 138 | ->applyPolicy($policy_1) 139 | ->applyPolicy($policy_2) 140 | ->applyTransformer($transformer_1) 141 | ->applyTransformer($transformer_2) 142 | ->toArray(Request::createFromGlobals()); 143 | 144 | $result_2 = $this 145 | ->resource 146 | ->applyTransformer($transformer_2) 147 | ->applyTransformer($transformer_1) 148 | ->applyPolicy($policy_1) 149 | ->applyPolicy($policy_2) 150 | ->toArray(Request::createFromGlobals()); 151 | 152 | $this->assertNotEquals($result, $result_2); 153 | } 154 | 155 | public function testExtensionPipeline() 156 | { 157 | $this->model->setHidden(['last_name']); 158 | 159 | $policy_1 = new PolicyStub(['first_name'], []); 160 | $policy_2 = new PolicyStub([], ['last_name']); 161 | 162 | $transformer_1 = new TransformerStub(function ($resource, array $data) { 163 | $data['test'] = 'test'; 164 | return $data; 165 | }); 166 | 167 | $transformer_2 = new TransformerStub(function ($resource, array $data) { 168 | $data['test_2'] = 'test_2'; 169 | return $data; 170 | }); 171 | 172 | $extensionPipeline = new MockExtensionPipeline(); 173 | $extensionPipeline 174 | ->addPolicy($policy_1) 175 | ->addPolicy($policy_2) 176 | ->addTransformer($transformer_1) 177 | ->addTransformer($transformer_2); 178 | 179 | $result = $this 180 | ->resource 181 | ->applyPipeline($extensionPipeline) 182 | ->toArray(Request::createFromGlobals()); 183 | 184 | $this->assertArrayHasKey('last_name', $result); 185 | $this->assertArrayNotHasKey('first_name', $result); 186 | $this->assertArrayHasKey('coin_side', $result); 187 | $this->assertArrayHasKey('city', $result); 188 | $this->assertEquals('test', $result['test']); 189 | $this->assertEquals('test_2', $result['test_2']); 190 | } 191 | } -------------------------------------------------------------------------------- /tests/Utils/HeadsOrTails.php: -------------------------------------------------------------------------------- 1 |