Passed
Push — v2 ( 8c624d...f3844b )
by Alexander
02:24
created

TransformBuilder   A

Complexity

Total Complexity 24

Size/Duplication

Total Lines 242
Duplicated Lines 0 %

Coupling/Cohesion

Components 1
Dependencies 8

Test Coverage

Coverage 98.11%

Importance

Changes 0
Metric Value
dl 0
loc 242
ccs 52
cts 53
cp 0.9811
rs 10
c 0
b 0
f 0
wmc 24
lcom 1
cbo 8

12 Methods

Rating   Name   Duplication   Size   Complexity  
A resource() 0 12 3
A cursor() 0 6 1
A paginator() 0 6 1
A meta() 0 6 1
A with() 0 6 2
A without() 0 6 2
A only() 0 6 2
A serializer() 0 14 3
A __construct() 0 6 1
A transform() 0 10 2
A prepareRelations() 0 12 4
A stripEagerLoadConstraints() 0 6 2
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\Resource\NullResource;
17
use League\Fractal\Serializer\SerializerAbstract;
18
19
/**
20
 * A builder class responsible for building transformed arrays.
21
 *
22
 * @package flugger/laravel-responder
23
 * @author  Alexander Tømmerås <[email protected]>
24
 * @license The MIT License
25
 */
26
class TransformBuilder
27
{
28
    /**
29
     * A factory class for making Fractal resources.
30
     *
31
     * @var \Flugg\Responder\Contracts\Resources\ResourceFactory
32
     */
33
    protected $resourceFactory;
34
35
    /**
36
     * A factory for making transformed arrays.
37
     *
38
     * @var \Flugg\Responder\Contracts\TransformFactory
39
     */
40
    private $transformFactory;
41
42
    /**
43
     * A factory used to build Fractal paginator adapters.
44
     *
45
     * @var \Flugg\Responder\Contracts\Pagination\PaginatorFactory
46
     */
47
    protected $paginatorFactory;
48
49
    /**
50
     * The resource that's being built.
51
     *
52
     * @var \League\Fractal\Resource\ResourceInterface
53
     */
54
    protected $resource;
55
56
    /**
57
     * A serializer for formatting data after transforming.
58
     *
59
     * @var \League\Fractal\Serializer\SerializerAbstract
60
     */
61
    protected $serializer;
62
63
    /**
64
     * A list of included relations.
65
     *
66
     * @var array
67
     */
68
    protected $with = [];
69
70
    /**
71
     * A list of excluded relations.
72
     *
73
     * @var array
74
     */
75
    protected $without = [];
76
77
    /**
78
     * A list of sparse fieldsets.
79
     *
80
     * @var array
81
     */
82
    protected $only = [];
83
84
    /**
85
     * Construct the builder class.
86
     *
87
     * @param \Flugg\Responder\Contracts\Resources\ResourceFactory   $resourceFactory
88
     * @param \Flugg\Responder\Contracts\TransformFactory            $transformFactory
89
     * @param \Flugg\Responder\Contracts\Pagination\PaginatorFactory $paginatorFactory
90
     */
91 17
    public function __construct(ResourceFactory $resourceFactory, TransformFactory $transformFactory, PaginatorFactory $paginatorFactory)
92
    {
93 17
        $this->resourceFactory = $resourceFactory;
94 17
        $this->transformFactory = $transformFactory;
95 17
        $this->paginatorFactory = $paginatorFactory;
96 17
    }
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 $this
105
     */
106 16
    public function resource($data = null, $transformer = null, string $resourceKey = null)
107
    {
108 16
        $this->resource = $this->resourceFactory->make($data, $transformer, $resourceKey);
109
110 16
        if ($data instanceof CursorPaginator) {
111 1
            $this->cursor($this->paginatorFactory->makeCursor($data));
112
        } elseif ($data instanceof LengthAwarePaginator) {
113 1
            $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 16
        return $this;
117
    }
118
119
    /**
120
     * Manually set the cursor on the resource.
121
     *
122
     * @param  \League\Fractal\Pagination\Cursor $cursor
123
     * @return $this
124
     */
125 2
    public function cursor(Cursor $cursor)
126
    {
127 2
        $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 2
        return $this;
130
    }
131
132
    /**
133
     * Manually set the paginator on the resource.
134
     *
135
     * @param  \League\Fractal\Pagination\IlluminatePaginatorAdapter $paginator
136
     * @return $this
137
     */
138 2
    public function paginator(IlluminatePaginatorAdapter $paginator)
139
    {
140 2
        $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 2
        return $this;
143
    }
144
145
    /**
146
     * Add meta data appended to the response data.
147
     *
148
     * @param  array $data
149
     * @return $this
150
     */
151 1
    public function meta(array $data)
152
    {
153 1
        $this->resource->setMeta($data);
154
155 1
        return $this;
156
    }
157
158
    /**
159
     * Include relations to the transform.
160
     *
161
     * @param  string[]|string $relations
162
     * @return $this
163
     */
164 3
    public function with($relations)
165
    {
166 3
        $this->with = array_merge($this->with, is_array($relations) ? $relations : func_get_args());
167
168 3
        return $this;
169
    }
170
171
    /**
172
     * Exclude relations from the transform.
173
     *
174
     * @param  string[]|string $relations
175
     * @return $this
176
     */
177 2
    public function without($relations)
178
    {
179 2
        $this->without = array_merge($this->without, is_array($relations) ? $relations : func_get_args());
180
181 2
        return $this;
182
    }
183
184
    /**
185
     * Filter fields to output using sparse fieldsets.
186
     *
187
     * @param  string[]|string $fields
188
     * @return $this
189
     */
190 2
    public function only($fields)
191
    {
192 2
        $this->only = array_merge($this->only, is_array($fields) ? $fields : func_get_args());
193
194 2
        return $this;
195
    }
196
197
    /**
198
     * Set the serializer.
199
     *
200
     * @param  \League\Fractal\Serializer\SerializerAbstract|string $serializer
201
     * @return $this
202
     * @throws \Flugg\Responder\Exceptions\InvalidSerializerException
203
     */
204 17
    public function serializer($serializer)
205
    {
206 17
        if (is_string($serializer)) {
207 2
            $serializer = new $serializer;
208
        }
209
210 17
        if (! $serializer instanceof SerializerAbstract) {
211 1
            throw new InvalidSerializerException;
212
        }
213
214 17
        $this->serializer = $serializer;
215
216 17
        return $this;
217
    }
218
219
    /**
220
     * Transform and serialize the data and return the transformed array.
221
     *
222
     * @return array
223
     */
224 10
    public function transform(): array
225
    {
226 10
        $this->prepareRelations($this->resource->getData(), $this->resource->getTransformer());
227
228 10
        return $this->transformFactory->make($this->resource ?: new NullResource, $this->serializer, [
229 10
            'includes' => $this->with,
230 10
            'excludes' => $this->without,
231 10
            'fieldsets' => $this->only,
232
        ]);
233
    }
234
235
    /**
236
     * Prepare requested relations for the transformation.
237
     *
238
     * @param  mixed                                                          $data
239
     * @param  \Flugg\Responder\Transformers\Transformer|callable|string|null $transformer
240
     * @return void
241
     */
242 10
    protected function prepareRelations($data, $transformer)
243
    {
244 10
        if ($transformer instanceof Transformer) {
245 1
            $this->with($transformer->extractDefaultRelations());
246
        }
247
248 10
        if ($data instanceof Model || $data instanceof Collection) {
249 1
            $data->load($this->with);
250
        }
251
252 10
        $this->with = $this->stripEagerLoadConstraints($this->with);
253 10
    }
254
255
    /**
256
     * Remove eager load constraint functions from the given relations.
257
     *
258
     * @param  array $relations
259
     * @return array
260
     */
261
    protected function stripEagerLoadConstraints(array $relations): array
262
    {
263 10
        return collect($relations)->map(function ($value, $key) {
264 3
            return is_numeric($key) ? $value : $key;
265 10
        })->values()->all();
266
    }
267
}