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
|
|
|
* @param array|null $meta |
34
|
|
|
* @return JsonResponse |
35
|
|
|
*/ |
36
|
|
|
public function success( $data = null, $statusCode = 200, $meta = null ):JsonResponse |
37
|
|
|
{ |
38
|
|
|
if ( is_integer( $data ) ) { |
39
|
|
|
list( $statusCode, $data ) = [ $data, null ]; |
40
|
|
|
} elseif ( is_array( $statusCode ) ) { |
41
|
|
|
list( $statusCode, $meta ) = [ 200, $statusCode ]; |
42
|
|
|
} elseif ( is_array( $data ) ) { |
43
|
|
|
list( $data, $statusCode, $meta ) = [ null, 200, $data ]; |
44
|
|
|
} |
45
|
|
|
|
46
|
|
|
$resource = $this->transform( $data ); |
47
|
|
|
|
48
|
|
|
if ( is_array( $meta ) ) { |
49
|
|
|
$resource->setMeta( $meta ); |
50
|
|
|
} |
51
|
|
|
|
52
|
|
|
$data = $this->serialize( $resource ); |
53
|
|
|
$data = $this->includeStatusCode( $statusCode, $data ); |
54
|
|
|
|
55
|
|
|
return response()->json( $data, $statusCode ); |
56
|
|
|
} |
57
|
|
|
|
58
|
|
|
/** |
59
|
|
|
* Generate an unsuccessful JSON response. |
60
|
|
|
* |
61
|
|
|
* @param string $errorCode |
62
|
|
|
* @param int $statusCode |
63
|
|
|
* @param mixed $message |
64
|
|
|
* @return JsonResponse |
65
|
|
|
*/ |
66
|
|
|
public function error( string $errorCode, int $statusCode = 500, $message = null ):JsonResponse |
67
|
|
|
{ |
68
|
|
|
$response = $this->getErrorResponse( $errorCode, $statusCode ); |
69
|
|
|
$messages = $this->getErrorMessages( $errorCode, $message ); |
70
|
|
|
|
71
|
|
|
if ( count( $messages ) === 1 ) { |
72
|
|
|
$response[ 'error' ][ 'message' ] = $messages[ 0 ]; |
73
|
|
|
} else if ( count( $messages ) > 1 ) { |
74
|
|
|
$response[ 'error' ][ 'messages' ] = $messages; |
75
|
|
|
} |
76
|
|
|
|
77
|
|
|
return response()->json( $response, $statusCode ); |
78
|
|
|
} |
79
|
|
|
|
80
|
|
|
/** |
81
|
|
|
* Transforms the data. |
82
|
|
|
* |
83
|
|
|
* @param mixed $data |
84
|
|
|
* @param Transformer|null $transformer |
85
|
|
|
* @return ResourceInterface |
86
|
|
|
*/ |
87
|
|
|
public function transform( $data = null, Transformer $transformer = null ):ResourceInterface |
88
|
|
|
{ |
89
|
|
|
if ( is_null( $data ) ) { |
90
|
|
|
return new FractalNull(); |
91
|
|
|
} elseif ( $data instanceof Transformable ) { |
92
|
|
|
return $this->transformModel( $data, $transformer ); |
93
|
|
|
} elseif ( $data instanceof Collection ) { |
94
|
|
|
return $this->transformCollection( $data, $transformer ); |
95
|
|
|
} elseif ( $data instanceof LengthAwarePaginator ) { |
96
|
|
|
return $this->transformPaginator( $data, $transformer ); |
97
|
|
|
} |
98
|
|
|
|
99
|
|
|
throw new InvalidArgumentException( 'Data must be one or multiple models implementing the Transformable contract.' ); |
100
|
|
|
} |
101
|
|
|
|
102
|
|
|
/** |
103
|
|
|
* Serializes the data. |
104
|
|
|
* |
105
|
|
|
* @param ResourceInterface $resource |
106
|
|
|
* @return array |
107
|
|
|
*/ |
108
|
|
|
public function serialize( ResourceInterface $resource ):array |
109
|
|
|
{ |
110
|
|
|
$manager = app( Manager::class ); |
111
|
|
|
|
112
|
|
|
$data = $resource->getData(); |
113
|
|
|
$model = $data instanceof Collection ? $this->resolveModel( $data ) : $data; |
114
|
|
|
|
115
|
|
|
if ( ! is_null( $data ) ) { |
116
|
|
|
$transformer = $model::transformer(); |
117
|
|
|
$includes = ( new $transformer( $model ) )->getAvailableIncludes(); |
118
|
|
|
$manager = $manager->parseIncludes( $includes ); |
119
|
|
|
} |
120
|
|
|
|
121
|
|
|
return $manager->createData( $resource )->toArray(); |
122
|
|
|
} |
123
|
|
|
|
124
|
|
|
/** |
125
|
|
|
* Transform a transformable Eloquent model. |
126
|
|
|
* |
127
|
|
|
* @param Transformable $model |
128
|
|
|
* @param Transformer|null $transformer |
129
|
|
|
* @return ResourceInterface |
130
|
|
|
*/ |
131
|
|
View Code Duplication |
protected function transformModel( Transformable $model, Transformer $transformer = null ):ResourceInterface |
|
|
|
|
132
|
|
|
{ |
133
|
|
|
$transformer = $transformer ?: $model::transformer(); |
134
|
|
|
|
135
|
|
|
if ( is_null( $transformer ) ) { |
136
|
|
|
return new FractalNull(); |
137
|
|
|
} |
138
|
|
|
|
139
|
|
|
return $this->transformData( $model, new $transformer( $model ), $model->getTable() ); |
140
|
|
|
} |
141
|
|
|
|
142
|
|
|
/** |
143
|
|
|
* Transform a collection of Eloquent models. |
144
|
|
|
* |
145
|
|
|
* @param Collection $collection |
146
|
|
|
* @param Transformer|null $transformer |
147
|
|
|
* @return ResourceInterface |
148
|
|
|
*/ |
149
|
|
View Code Duplication |
protected function transformCollection( Collection $collection, Transformer $transformer = null ):ResourceInterface |
|
|
|
|
150
|
|
|
{ |
151
|
|
|
$model = $this->resolveModel( $collection ); |
152
|
|
|
$transformer = $transformer ?: $model::transformer(); |
153
|
|
|
|
154
|
|
|
if ( is_null( $transformer ) ) { |
155
|
|
|
return new FractalNull(); |
156
|
|
|
} |
157
|
|
|
|
158
|
|
|
return $this->transformData( $collection, new $transformer( $model ), $model->getTable() ); |
159
|
|
|
} |
160
|
|
|
|
161
|
|
|
/** |
162
|
|
|
* Transform paginated data using Laravel's paginator. |
163
|
|
|
* |
164
|
|
|
* @param LengthAwarePaginator $paginator |
165
|
|
|
* @param Transformer|null $transformer |
166
|
|
|
* @return ResourceInterface |
167
|
|
|
*/ |
168
|
|
|
protected function transformPaginator( LengthAwarePaginator $paginator, Transformer $transformer = null ):ResourceInterface |
169
|
|
|
{ |
170
|
|
|
$resource = $this->transformCollection( $paginator->getCollection(), $transformer ); |
171
|
|
|
$resource->setPaginator( new IlluminatePaginatorAdapter( $paginator ) ); |
172
|
|
|
|
173
|
|
|
return $resource; |
174
|
|
|
} |
175
|
|
|
|
176
|
|
|
/** |
177
|
|
|
* Transform the data using the given transformer. |
178
|
|
|
* |
179
|
|
|
* @param Transformable|Collection $data |
180
|
|
|
* @param Transformer|null $transformer |
181
|
|
|
* @param string $resourceKey |
182
|
|
|
* @return ResourceInterface |
183
|
|
|
*/ |
184
|
|
|
protected function transformData( $data, Transformer $transformer, string $resourceKey ):ResourceInterface |
185
|
|
|
{ |
186
|
|
|
$class = $data instanceof Transformable ? FractalItem::class : FractalCollection::class; |
187
|
|
|
$resource = new $class( $data, $transformer ); |
188
|
|
|
$resource->setResourceKey( $resourceKey ); |
189
|
|
|
|
190
|
|
|
return $resource; |
191
|
|
|
} |
192
|
|
|
|
193
|
|
|
/** |
194
|
|
|
* Resolves model class path from a collection of models. |
195
|
|
|
* |
196
|
|
|
* @param Collection $collection |
197
|
|
|
* @return Transformable |
198
|
|
|
* @throws InvalidArgumentException |
199
|
|
|
*/ |
200
|
|
|
protected function resolveModel( Collection $collection ):Transformable |
201
|
|
|
{ |
202
|
|
|
$class = $collection->first(); |
203
|
|
|
|
204
|
|
|
if ( ! $class instanceof Transformable ) { |
205
|
|
|
throw new InvalidArgumentException( 'Data must only contain models implementing the Transformable contract.' ); |
206
|
|
|
} |
207
|
|
|
|
208
|
|
|
$collection->each( function ( $model ) use ( $class ) { |
209
|
|
|
if ( get_class( $model ) !== get_class( $class ) ) { |
210
|
|
|
throw new InvalidArgumentException( 'You cannot transform arrays or collections with multiple model types.' ); |
211
|
|
|
} |
212
|
|
|
} ); |
213
|
|
|
|
214
|
|
|
return $class; |
215
|
|
|
} |
216
|
|
|
|
217
|
|
|
/** |
218
|
|
|
* Here we prepend a status code to the response data, if status code is enabled in |
219
|
|
|
* the configuration file. |
220
|
|
|
* |
221
|
|
|
* @param int $statusCode |
222
|
|
|
* @param array $data |
223
|
|
|
* @return array |
224
|
|
|
*/ |
225
|
|
|
protected function includeStatusCode( int $statusCode, array $data ):array |
226
|
|
|
{ |
227
|
|
|
if ( ! config( 'responder.status_code' ) ) { |
228
|
|
|
return $data; |
229
|
|
|
} |
230
|
|
|
|
231
|
|
|
return array_merge( [ |
232
|
|
|
'status' => $statusCode |
233
|
|
|
], $data ); |
234
|
|
|
} |
235
|
|
|
|
236
|
|
|
/** |
237
|
|
|
* Get the skeleton for an error response. |
238
|
|
|
* |
239
|
|
|
* @param string $errorCode |
240
|
|
|
* @param int $statusCode |
241
|
|
|
* @return array |
242
|
|
|
*/ |
243
|
|
|
protected function getErrorResponse( string $errorCode, int $statusCode ):array |
244
|
|
|
{ |
245
|
|
|
$response = [ |
246
|
|
|
'success' => false, |
247
|
|
|
'error' => [ |
248
|
|
|
'code' => $errorCode |
249
|
|
|
] |
250
|
|
|
]; |
251
|
|
|
|
252
|
|
|
return $this->includeStatusCode( $statusCode, $response ); |
253
|
|
|
} |
254
|
|
|
|
255
|
|
|
/** |
256
|
|
|
* Get any error messages for the response. If no message can be found it will |
257
|
|
|
* try to resolve a set message from the translator. |
258
|
|
|
* |
259
|
|
|
* @param string $errorCode |
260
|
|
|
* @param mixed $message |
261
|
|
|
* @return array |
262
|
|
|
*/ |
263
|
|
|
protected function getErrorMessages( string $errorCode, $message ):array |
264
|
|
|
{ |
265
|
|
|
if ( is_array( $message ) ) { |
266
|
|
|
return $message; |
267
|
|
|
|
268
|
|
|
} elseif ( is_string( $message ) ) { |
269
|
|
|
if ( strlen( $message ) === 0 ) { |
270
|
|
|
return [ ]; |
271
|
|
|
} |
272
|
|
|
|
273
|
|
|
return [ $message ]; |
274
|
|
|
} |
275
|
|
|
|
276
|
|
|
return [ app( 'translator' )->trans( 'errors.' . $errorCode ) ]; |
277
|
|
|
} |
278
|
|
|
} |
Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.
You can also find more detailed suggestions in the “Code” section of your repository.