Test Setup Failed
Push — v2 ( 74186a...c94b75 )
by Alexander
06:56
created

TransformBuilder::eagerLoadIfApplicable()   A

Complexity

Conditions 3
Paths 2

Size

Total Lines 6
Code Lines 3

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 3
eloc 3
nc 2
nop 1
dl 0
loc 6
rs 9.4285
c 0
b 0
f 0
1
<?php
2
3
namespace Flugg\Responder;
4
5
use Flugg\Responder\Contracts\Pagination\PaginatorFactory;
6
use Flugg\Responder\Contracts\Resources\ResourceFactory;
7
use Flugg\Responder\Contracts\TransformFactory;
8
use Flugg\Responder\Exceptions\InvalidSerializerException;
9
use Flugg\Responder\Pagination\CursorPaginator;
10
use Flugg\Responder\Transformers\Transformer;
0 ignored issues
show
Bug introduced by
This use statement conflicts with another class in this namespace, Flugg\Responder\Transformer.

Let’s assume that you have a directory layout like this:

.
|-- OtherDir
|   |-- Bar.php
|   `-- Foo.php
`-- SomeDir
    `-- Foo.php

and let’s assume the following content of Bar.php:

// Bar.php
namespace OtherDir;

use SomeDir\Foo; // This now conflicts the class OtherDir\Foo

If both files OtherDir/Foo.php and SomeDir/Foo.php are loaded in the same runtime, you will see a PHP error such as the following:

PHP Fatal error:  Cannot use SomeDir\Foo as Foo because the name is already in use in OtherDir/Foo.php

However, as OtherDir/Foo.php does not necessarily have to be loaded and the error is only triggered if it is loaded before OtherDir/Bar.php, this problem might go unnoticed for a while. In order to prevent this error from surfacing, you must import the namespace with a different alias:

// Bar.php
namespace OtherDir;

use SomeDir\Foo as SomeDirFoo; // There is no conflict anymore.
Loading history...
11
use Illuminate\Contracts\Pagination\LengthAwarePaginator;
12
use Illuminate\Database\Eloquent\Collection;
13
use Illuminate\Database\Eloquent\Model;
14
use League\Fractal\Pagination\Cursor;
15
use League\Fractal\Pagination\IlluminatePaginatorAdapter;
16
use League\Fractal\Serializer\SerializerAbstract;
17
18
/**
19
 * A builder class responsible for building transformed arrays.
20
 *
21
 * @package flugger/laravel-responder
22
 * @author  Alexander Tømmerås <[email protected]>
23
 * @license The MIT License
24
 */
25
class TransformBuilder
26
{
27
    /**
28
     * A factory class for making Fractal resources.
29
     *
30
     * @var \Flugg\Responder\Contracts\Resources\ResourceFactory
31
     */
32
    protected $resourceFactory;
33
34
    /**
35
     * A factory for making transformed arrays.
36
     *
37
     * @var \Flugg\Responder\Contracts\TransformFactory
38
     */
39
    private $transformFactory;
40
41
    /**
42
     * A factory used to build Fractal paginator adapters.
43
     *
44
     * @var \Flugg\Responder\Contracts\Pagination\PaginatorFactory
45
     */
46
    protected $paginatorFactory;
47
48
    /**
49
     * The resource that's being built.
50
     *
51
     * @var \League\Fractal\Resource\ResourceInterface
52
     */
53
    protected $resource;
54
55
    /**
56
     * A serializer for formatting data after transforming.
57
     *
58
     * @var \League\Fractal\Serializer\SerializerAbstract
59
     */
60
    protected $serializer;
61
62
    /**
63
     * A list of included relations.
64
     *
65
     * @var array
66
     */
67
    protected $with = [];
68
69
    /**
70
     * A list of excluded relations.
71
     *
72
     * @var array
73
     */
74
    protected $without = [];
75
76
    /**
77
     * A list of sparse fieldsets.
78
     *
79
     * @var array
80
     */
81
    protected $only = [];
82
83
    /**
84
     * Construct the builder class.
85
     *
86
     * @param \Flugg\Responder\Contracts\Resources\ResourceFactory   $resourceFactory
87
     * @param \Flugg\Responder\Contracts\TransformFactory            $transformFactory
88
     * @param \Flugg\Responder\Contracts\Pagination\PaginatorFactory $paginatorFactory
89
     */
90
    public function __construct(ResourceFactory $resourceFactory, TransformFactory $transformFactory, PaginatorFactory $paginatorFactory)
91
    {
92
        $this->resourceFactory = $resourceFactory;
93
        $this->transformFactory = $transformFactory;
94
        $this->paginatorFactory = $paginatorFactory;
95
        $this->resource = $this->resourceFactory->make();
96
    }
97
98
    /**
99
     * Make a resource from the given data and transformer and set the resource key.
100
     *
101
     * @param  mixed                                                          $data
102
     * @param  \Flugg\Responder\Transformers\Transformer|callable|string|null $transformer
103
     * @param  string|null                                                    $resourceKey
104
     * @return self
105
     */
106
    public function resource($data = null, $transformer = null, string $resourceKey = null): TransformBuilder
107
    {
108
        $this->resource = $this->resourceFactory->make($data, $transformer, $resourceKey);
109
110
        if ($data instanceof CursorPaginator) {
111
            $this->cursor($this->paginatorFactory->makeCursor($data));
112
        } elseif ($data instanceof LengthAwarePaginator) {
113
            $this->paginator($this->paginatorFactory->make($data));
0 ignored issues
show
Compatibility introduced by
$this->paginatorFactory->make($data) of type object<League\Fractal\Pa...ion\PaginatorInterface> is not a sub-type of object<League\Fractal\Pa...minatePaginatorAdapter>. It seems like you assume a concrete implementation of the interface League\Fractal\Pagination\PaginatorInterface to be always present.

This check looks for parameters that are defined as one type in their type hint or doc comment but seem to be used as a narrower type, i.e an implementation of an interface or a subclass.

Consider changing the type of the parameter or doing an instanceof check before assuming your parameter is of the expected type.

Loading history...
114
        }
115
116
        return $this;
117
    }
118
119
    /**
120
     * Manually set the cursor on the resource.
121
     *
122
     * @param  \League\Fractal\Pagination\Cursor $cursor
123
     * @return self
124
     */
125
    public function cursor(Cursor $cursor): TransformBuilder
126
    {
127
        $this->resource->setCursor($cursor);
0 ignored issues
show
Bug introduced by
It seems like you code against a concrete implementation and not the interface League\Fractal\Resource\ResourceInterface as the method setCursor() does only exist in the following implementations of said interface: League\Fractal\Resource\Collection.

Let’s take a look at an example:

interface User
{
    /** @return string */
    public function getPassword();
}

class MyUser implements User
{
    public function getPassword()
    {
        // return something
    }

    public function getDisplayName()
    {
        // return some name.
    }
}

class AuthSystem
{
    public function authenticate(User $user)
    {
        $this->logger->info(sprintf('Authenticating %s.', $user->getDisplayName()));
        // do something.
    }
}

In the above example, the authenticate() method works fine as long as you just pass instances of MyUser. However, if you now also want to pass a different implementation of User which does not have a getDisplayName() method, the code will break.

Available Fixes

  1. Change the type-hint for the parameter:

    class AuthSystem
    {
        public function authenticate(MyUser $user) { /* ... */ }
    }
    
  2. Add an additional type-check:

    class AuthSystem
    {
        public function authenticate(User $user)
        {
            if ($user instanceof MyUser) {
                $this->logger->info(/** ... */);
            }
    
            // or alternatively
            if ( ! $user instanceof MyUser) {
                throw new \LogicException(
                    '$user must be an instance of MyUser, '
                   .'other instances are not supported.'
                );
            }
    
        }
    }
    
Note: PHP Analyzer uses reverse abstract interpretation to narrow down the types inside the if block in such a case.
  1. Add the method to the interface:

    interface User
    {
        /** @return string */
        public function getPassword();
    
        /** @return string */
        public function getDisplayName();
    }
    
Loading history...
128
129
        return $this;
130
    }
131
132
    /**
133
     * Manually set the paginator on the resource.
134
     *
135
     * @param  \League\Fractal\Pagination\IlluminatePaginatorAdapter $paginator
136
     * @return self
137
     */
138
    public function paginator(IlluminatePaginatorAdapter $paginator): TransformBuilder
139
    {
140
        $this->resource->setPaginator($paginator);
0 ignored issues
show
Bug introduced by
It seems like you code against a concrete implementation and not the interface League\Fractal\Resource\ResourceInterface as the method setPaginator() does only exist in the following implementations of said interface: League\Fractal\Resource\Collection.

Let’s take a look at an example:

interface User
{
    /** @return string */
    public function getPassword();
}

class MyUser implements User
{
    public function getPassword()
    {
        // return something
    }

    public function getDisplayName()
    {
        // return some name.
    }
}

class AuthSystem
{
    public function authenticate(User $user)
    {
        $this->logger->info(sprintf('Authenticating %s.', $user->getDisplayName()));
        // do something.
    }
}

In the above example, the authenticate() method works fine as long as you just pass instances of MyUser. However, if you now also want to pass a different implementation of User which does not have a getDisplayName() method, the code will break.

Available Fixes

  1. Change the type-hint for the parameter:

    class AuthSystem
    {
        public function authenticate(MyUser $user) { /* ... */ }
    }
    
  2. Add an additional type-check:

    class AuthSystem
    {
        public function authenticate(User $user)
        {
            if ($user instanceof MyUser) {
                $this->logger->info(/** ... */);
            }
    
            // or alternatively
            if ( ! $user instanceof MyUser) {
                throw new \LogicException(
                    '$user must be an instance of MyUser, '
                   .'other instances are not supported.'
                );
            }
    
        }
    }
    
Note: PHP Analyzer uses reverse abstract interpretation to narrow down the types inside the if block in such a case.
  1. Add the method to the interface:

    interface User
    {
        /** @return string */
        public function getPassword();
    
        /** @return string */
        public function getDisplayName();
    }
    
Loading history...
141
142
        return $this;
143
    }
144
145
    /**
146
     * Add meta data appended to the response data.
147
     *
148
     * @param  array $meta
149
     * @return self
150
     */
151
    public function meta(array $meta): TransformBuilder
152
    {
153
        $this->resource->setMeta($meta);
154
155
        return $this;
156
    }
157
158
    /**
159
     * Include relations to the transform.
160
     *
161
     * @param  string[]|string $relations
162
     * @return self
163
     */
164
    public function with($relations): TransformBuilder
165
    {
166
        $this->with = array_merge($this->with, is_array($relations) ? $relations : func_get_args());
167
168
        return $this;
169
    }
170
171
    /**
172
     * Exclude relations from the transform.
173
     *
174
     * @param  string[]|string $relations
175
     * @return self
176
     */
177
    public function without($relations): TransformBuilder
178
    {
179
        $this->without = array_merge($this->without, is_array($relations) ? $relations : func_get_args());
180
181
        return $this;
182
    }
183
184
    /**
185
     * Filter fields to output using sparse fieldsets.
186
     *
187
     * @param  string[]|string $fields
188
     * @return self
189
     */
190
    public function only($fields): TransformBuilder
191
    {
192
        $this->only = array_merge($this->only, is_array($fields) ? $fields : func_get_args());
193
194
        return $this;
195
    }
196
197
    /**
198
     * Set the serializer.
199
     *
200
     * @param  \League\Fractal\Serializer\SerializerAbstract|string $serializer
201
     * @return self
202
     * @throws \Flugg\Responder\Exceptions\InvalidSerializerException
203
     */
204
    public function serializer($serializer): TransformBuilder
205
    {
206
        if (is_string($serializer)) {
207
            $serializer = new $serializer;
208
        }
209
210
        if (! $serializer instanceof SerializerAbstract) {
211
            throw new InvalidSerializerException;
212
        }
213
214
        $this->serializer = $serializer;
215
216
        return $this;
217
    }
218
219
    /**
220
     * Transform and serialize the data and return the transformed array.
221
     *
222
     * @return array
223
     */
224
    public function transform(): array
225
    {
226
        $this->prepareRelations();
227
228
        return $this->transformFactory->make($this->resource, $this->serializer, [
229
            'includes' => $this->with,
230
            'excludes' => $this->without,
231
            'fields' => $this->only,
232
        ]);
233
    }
234
235
    /**
236
     * Prepare requested relations for the transformation.
237
     *
238
     * @return void
239
     */
240
    protected function prepareRelations()
241
    {
242
        $this->setDefaultIncludes($this->resource->getTransformer());
243
        $this->eagerLoadIfApplicable($this->resource->getData());
244
245
        $this->with = $this->trimEagerLoadFunctions($this->with);
246
    }
247
248
    /**
249
     * Set default includes extracted from the transformer.
250
     *
251
     * @param \Flugg\Responder\Transformers\Transformer|callable $transformer
252
     * @return void
253
     */
254
    protected function setDefaultIncludes($transformer)
255
    {
256
        if ($transformer instanceof Transformer) {
257
            $this->with($transformer->extractDefaultRelations());
258
        }
259
    }
260
261
    /**
262
     * Eager load relations on the given data, if it's an Eloquent model or collection.
263
     *
264
     * @param  mixed $data
265
     * @return void
266
     */
267
    protected function eagerLoadIfApplicable($data)
268
    {
269
        if ($data instanceof Model || $data instanceof Collection) {
270
            $data->load($this->with);
271
        }
272
    }
273
274
    /**
275
     * Remove eager load constraint functions from the given array.
276
     *
277
     * @param  array $relations
278
     * @return void
0 ignored issues
show
Documentation introduced by
Should the return type not be array?

This check compares the return type specified in the @return annotation of a function or method doc comment with the types returned by the function and raises an issue if they mismatch.

Loading history...
279
     */
280
    protected function trimEagerLoadFunctions(array $relations)
281
    {
282
        return collect($relations)->map(function ($value, $key) {
283
            return is_numeric($key) ? $value : $key;
284
        })->values()->all();
285
    }
286
}