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