Completed
Push — master ( 2beeb5...b9673c )
by Alexander
03:30
created

Responder::transform()   B

Complexity

Conditions 5
Paths 5

Size

Total Lines 14
Code Lines 10

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
dl 0
loc 14
rs 8.8571
c 0
b 0
f 0
cc 5
eloc 10
nc 5
nop 2
1
<?php
2
3
namespace Flugg\Responder;
4
5
use Flugg\Responder\Contracts\Manager;
6
use Flugg\Responder\Contracts\Responder as ResponderContract;
7
use Flugg\Responder\Contracts\Transformable;
8
use Illuminate\Contracts\Pagination\LengthAwarePaginator;
9
use Illuminate\Http\JsonResponse;
10
use Illuminate\Support\Collection;
11
use InvalidArgumentException;
12
use League\Fractal\Pagination\IlluminatePaginatorAdapter;
13
use League\Fractal\Resource\Collection as FractalCollection;
14
use League\Fractal\Resource\Item as FractalItem;
15
use League\Fractal\Resource\NullResource as FractalNull;
16
use League\Fractal\Resource\ResourceInterface;
17
18
/**
19
 * The responder service. This class is responsible for generating JSON API responses.
20
 * It can also transform and serialize data using Fractal behind the scenes.
21
 *
22
 * @package Laravel Responder
23
 * @author  Alexander Tømmerås <[email protected]>
24
 * @license The MIT License
25
 */
26
class Responder implements ResponderContract
27
{
28
    /**
29
     * Generate a successful JSON response.
30
     *
31
     * @param  mixed $data
32
     * @param  int   $statusCode
33
     * @return JsonResponse
34
     */
35
    public function success( $data = null, int $statusCode = 200 ):JsonResponse
36
    {
37
        if ( is_integer( $data ) ) {
38
            list( $statusCode, $data ) = [ $data, null ];
39
        }
40
41
        $resource = $this->transform( $data );
42
        $data = $this->serialize( $resource );
0 ignored issues
show
Coding Style introduced by
Consider using a different name than the parameter $data. This often makes code more readable.
Loading history...
43
        $data = $this->includeStatusCode( $statusCode, $data );
0 ignored issues
show
Coding Style introduced by
Consider using a different name than the parameter $data. This often makes code more readable.
Loading history...
44
45
        return response()->json( $data, $statusCode );
46
    }
47
48
    /**
49
     * Generate an unsuccessful JSON response.
50
     *
51
     * @param  string $errorCode
52
     * @param  int    $statusCode
53
     * @param  mixed  $message
54
     * @return JsonResponse
55
     */
56
    public function error( string $errorCode, int $statusCode = 500, $message = null ):JsonResponse
57
    {
58
        $response = $this->getErrorResponse( $errorCode );
0 ignored issues
show
Bug introduced by
The call to getErrorResponse() misses a required argument $statusCode.

This check looks for function calls that miss required arguments.

Loading history...
59
        $messages = $this->getErrorMessages( $message, $errorCode );
60
61
        if ( count( $messages ) === 1 ) {
62
            $response[ 'error' ][ 'message' ] = $messages[ 0 ];
63
        } else if ( count( $messages ) > 1 ) {
64
            $response[ 'error' ][ 'messages' ] = $messages;
65
        }
66
67
        return response()->json( $response, $statusCode );
68
    }
69
70
    /**
71
     * Transforms the data.
72
     *
73
     * @param  mixed            $data
74
     * @param  Transformer|null $transformer
75
     * @return ResourceInterface
76
     */
77
    public function transform( $data = null, Transformer $transformer = null ):ResourceInterface
78
    {
79
        if ( is_null( $data ) ) {
80
            return new FractalNull();
81
        } elseif ( $data instanceof Transformable ) {
82
            return $this->transformModel( $data, $transformer );
83
        } elseif ( $data instanceof Collection ) {
84
            return $this->transformCollection( $data, $transformer );
85
        } elseif ( $data instanceof LengthAwarePaginator ) {
86
            return $this->transformPaginator( $data, $transformer );
87
        }
88
89
        throw new InvalidArgumentException( 'Data must be one or multiple models implementing the Transformable contract.' );
90
    }
91
92
    /**
93
     * Serializes the data.
94
     *
95
     * @param  ResourceInterface $resource
96
     * @return array
97
     */
98
    public function serialize( ResourceInterface $resource ):array
99
    {
100
        $manager = app( Manager::class );
101
102
        $data = $resource->getData();
103
        $model = $data instanceof Collection ? $this->resolveModel( $data ) : $data;
104
105
        if ( ! is_null( $data ) ) {
106
            $transformer = $model::transformer();
107
            $includes = ( new $transformer( $model ) )->getAvailableIncludes();
108
            $manager = $manager->parseIncludes( $includes );
109
        }
110
111
        return $manager->createData( $resource )->toArray();
112
    }
113
114
    /**
115
     * Transform a transformable Eloquent model.
116
     *
117
     * @param  Transformable    $model
118
     * @param  Transformer|null $transformer
119
     * @return FractalItem
120
     */
121
    protected function transformModel( Transformable $model, Transformer $transformer = null ):FractalItem
122
    {
123
        $transformer = $transformer ?: $model::transformer();
0 ignored issues
show
Coding Style introduced by
Consider using a different name than the parameter $transformer. This often makes code more readable.
Loading history...
124
125
        return $this->transformData( $model, new $transformer( $model ), $model->getTable() );
126
    }
127
128
    /**
129
     * Transform a collection of Eloquent models.
130
     *
131
     * @param  Collection       $collection
132
     * @param  Transformer|null $transformer
133
     * @return FractalCollection
134
     */
135
    protected function transformCollection( Collection $collection, Transformer $transformer = null ):FractalCollection
136
    {
137
        $model = $this->resolveModel( $collection );
138
        $transformer = $transformer ?: $model::transformer();
0 ignored issues
show
Coding Style introduced by
Consider using a different name than the parameter $transformer. This often makes code more readable.
Loading history...
139
140
        return $this->transformData( $collection, new $transformer( $model ), $model->getTable() );
141
    }
142
143
    /**
144
     * Transform paginated data using Laravel's paginator.
145
     *
146
     * @param LengthAwarePaginator $paginator
147
     * @param Transformer|null     $transformer
148
     * @return FractalCollection
149
     */
150
    protected function transformPaginator( LengthAwarePaginator $paginator, Transformer $transformer = null ):FractalCollection
151
    {
152
        $resource = $this->transformCollection( $paginator->getCollection(), $transformer );
153
        $resource->setPaginator( new IlluminatePaginatorAdapter( $paginator ) );
154
155
        return $resource;
156
    }
157
158
    /**
159
     * Transform the data using the given transformer.
160
     *
161
     * @param  Transformable|Collection $data
162
     * @param  Transformer|null         $transformer
163
     * @param  string                   $resourceKey
164
     * @return ResourceInterface
165
     */
166
    protected function transformData( $data, Transformer $transformer, string $resourceKey ):ResourceInterface
167
    {
168
        $class = $data instanceof Transformable ? FractalItem::class : FractalCollection::class;
169
        $resource = new $class( $data, $transformer );
170
        $resource->setResourceKey( $resourceKey );
171
172
        return $resource;
173
    }
174
175
    /**
176
     * Resolves model class path from a collection of models.
177
     *
178
     * @param  Collection $collection
179
     * @return Transformable
180
     * @throws InvalidArgumentException
181
     */
182
    protected function resolveModel( Collection $collection ):Transformable
183
    {
184
        $class = $collection->first();
185
186
        if ( ! $class instanceof Transformable ) {
187
            throw new InvalidArgumentException( 'Data must only contain models implementing the Transformable contract.' );
188
        }
189
190
        $collection->each( function ( $model ) use ( $class ) {
191
            if ( get_class( $model ) !== get_class( $class ) ) {
192
                throw new InvalidArgumentException( 'You cannot transform arrays or collections with multiple model types.' );
193
            }
194
        } );
195
196
        return $class;
197
    }
198
199
    /**
200
     * Here we prepend a status code to the response data, if status code is enabled in
201
     * the configuration file.
202
     *
203
     * @param  int   $statusCode
204
     * @param  array $data
205
     * @return array
206
     */
207
    protected function includeStatusCode( int $statusCode, array $data ):array
208
    {
209
        if ( ! config( 'responder.status_code' ) ) {
210
            return $data;
211
        }
212
213
        return array_merge( [
214
            'status' => $statusCode
215
        ], $data );
216
    }
217
218
    /**
219
     * Get the skeleton for an error response.
220
     *
221
     * @param string $errorCode
222
     * @param int    $statusCode
223
     * @return array
224
     */
225
    protected function getErrorResponse( string $errorCode, int $statusCode ):array
226
    {
227
        $response = [
228
            'success' => false,
229
            'error' => [
230
                'code' => $errorCode
231
            ]
232
        ];
233
234
        return $this->includeStatusCode( $statusCode, $response );
235
    }
236
237
    /**
238
     * Get any error messages for the response. If no message can be found it will
239
     * try to resolve a set message from the translator.
240
     *
241
     * @param  mixed  $message
242
     * @param  string $errorCode
243
     * @return array
244
     */
245
    protected function getErrorMessages( $message, string $errorCode ):array
246
    {
247
        if ( is_array( $message ) ) {
248
            return $message;
249
250
        } elseif ( is_string( $message ) ) {
251
            if ( strlen( $message ) === 0 ) {
252
                return [ ];
253
            }
254
255
            return [ $message ];
256
        }
257
258
        return [ app( 'translator' )->trans( 'errors.' . $errorCode ) ];
259
    }
260
}