Skip to content

Commit a9a0683

Browse files
committed
Merge branch 'master' into attributes-per-spec
2 parents d403dc3 + e6c3012 commit a9a0683

File tree

5 files changed

+260
-13
lines changed

5 files changed

+260
-13
lines changed

composer.json

Lines changed: 4 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -12,11 +12,10 @@
1212
}
1313
],
1414
"require": {
15-
"php": ">=5.4.0",
16-
"illuminate/database": "5.0.*",
17-
"illuminate/http": "5.0.*",
18-
"illuminate/support": "5.0.*",
19-
"illuminate/pagination" : "5.0.*"
15+
"illuminate/database": "5.1.*",
16+
"illuminate/http": "5.1.*",
17+
"illuminate/support": "5.1.*",
18+
"illuminate/pagination" : "5.1.*"
2019
},
2120
"require-dev": {
2221
"phpunit/phpunit": "4.*"
Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,42 @@
1+
<?php namespace EchoIt\JsonApi\Exception;
2+
3+
use EchoIt\JsonApi\Exception;
4+
use EchoIt\JsonApi\MultiErrorResponse;
5+
use Illuminate\Support\MessageBag as ValidationMessages;
6+
7+
/**
8+
* Validation represents an Exception that can be thrown in the event of a validation failure where a JSON response may be expected.
9+
*
10+
* @author Matt <[email protected]>
11+
*/
12+
class Validation extends Exception
13+
{
14+
protected $httpStatusCode;
15+
protected $validationMessages;
16+
17+
/**
18+
* Constructor.
19+
*
20+
* @param string $message The Exception message to throw
21+
* @param int $code The Exception code
22+
* @param int $httpStatusCode HTTP status code which can be used for broken request
23+
* @param Illuminate\Support\MessageBag $messages Validation errors
24+
*/
25+
public function __construct($message = '', $code = 0, $httpStatusCode = 500, ValidationMessages $messages = NULL)
26+
{
27+
parent::__construct($message, $code);
28+
29+
$this->httpStatusCode = $httpStatusCode;
30+
$this->validationMessages = $messages;
31+
}
32+
33+
/**
34+
* This method returns a HTTP response representation of the Exception
35+
*
36+
* @return JsonApi\MultiErrorResponse
37+
*/
38+
public function response()
39+
{
40+
return new MultiErrorResponse($this->httpStatusCode, $this->code, $this->message, $this->validationMessages);
41+
}
42+
}

src/EchoIt/JsonApi/Handler.php

Lines changed: 76 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -85,7 +85,7 @@ public function fulfillRequest()
8585
} elseif ($models instanceof LengthAwarePaginator) {
8686
$items = new Collection($models->items());
8787
foreach ($items as $model) {
88-
$model->load($this->exposedRelationsFromRequest());
88+
$this->loadRelatedModels($model);
8989
}
9090

9191
$response = new Response($items, static::successfulHttpStatusCode($this->request->method));
@@ -96,10 +96,10 @@ public function fulfillRequest()
9696
} else {
9797
if ($models instanceof Collection) {
9898
foreach ($models as $model) {
99-
$model->load($this->exposedRelationsFromRequest());
99+
$this->loadRelatedModels($model);
100100
}
101101
} else {
102-
$models->load($this->exposedRelationsFromRequest());
102+
$this->loadRelatedModels($models);
103103
}
104104

105105
$response = new Response($models, static::successfulHttpStatusCode($this->request->method, $models));
@@ -111,14 +111,54 @@ public function fulfillRequest()
111111
return $response;
112112
}
113113

114+
/**
115+
* Load a model's relations
116+
*
117+
* @param Model $model the model to load relations of
118+
* @return void
119+
*/
120+
protected function loadRelatedModels(Model $model) {
121+
// get the relations to load
122+
$relations = $this->exposedRelationsFromRequest($model);
123+
124+
foreach ($relations as $relation) {
125+
// if this relation is loaded via a method, then call said method
126+
if (in_array($relation, $model->relationsFromMethod())) {
127+
$model->$relation = $model->$relation();
128+
continue;
129+
}
130+
131+
$model->load($relation);
132+
}
133+
}
134+
114135
/**
115136
* Returns which requested resources are available to include.
116137
*
117138
* @return array
118139
*/
119-
protected function exposedRelationsFromRequest()
140+
protected function exposedRelationsFromRequest($model = null)
120141
{
121-
return array_intersect(static::$exposedRelations, $this->request->include);
142+
$exposedRelations = static::$exposedRelations;
143+
144+
// if no relations are to be included by request
145+
if (count($this->request->include) == 0) {
146+
// and if we have a model
147+
if ($model !== null && $model instanceof Model) {
148+
// then use the relations exposed by default
149+
$exposedRelations = array_intersect($exposedRelations, $model->defaultExposedRelations());
150+
$model->setExposedRelations($exposedRelations);
151+
return $exposedRelations;
152+
}
153+
154+
}
155+
156+
$exposedRelations = array_intersect($exposedRelations, $this->request->include);
157+
if ($model !== null && $model instanceof Model) {
158+
$model->setExposedRelations($exposedRelations);
159+
}
160+
161+
return $exposedRelations;
122162
}
123163

124164
/**
@@ -143,7 +183,7 @@ protected function getIncludedModels($models)
143183
$models = $models instanceof Collection ? $models : [$models];
144184

145185
foreach ($models as $model) {
146-
foreach ($this->exposedRelationsFromRequest() as $relationName) {
186+
foreach ($this->exposedRelationsFromRequest($model) as $relationName) {
147187
$value = static::getModelsForRelation($model, $relationName);
148188

149189
if (is_null($value)) {
@@ -287,7 +327,6 @@ protected static function getModelsForRelation($model, $relationKey)
287327
BaseResponse::HTTP_INTERNAL_SERVER_ERROR
288328
);
289329
}
290-
291330
$relationModels = $model->{$relationKey};
292331
if (is_null($relationModels)) {
293332
return null;
@@ -296,6 +335,7 @@ protected static function getModelsForRelation($model, $relationKey)
296335
if (! $relationModels instanceof Collection) {
297336
return [ $relationModels ];
298337
}
338+
299339
return $relationModels;
300340
}
301341

@@ -486,6 +526,33 @@ protected function handleGetDefault(Request $request, $model)
486526
return $results;
487527
}
488528

529+
/**
530+
* Validates passed data against a model
531+
* Validation performed safely and only if model provides rules
532+
*
533+
* @param EchoIt\JsonApi\Model $model model to validate against
534+
* @param Array $values passed array of values
535+
*
536+
* @throws Exception\Validation Exception thrown when validation fails
537+
*
538+
* @return Bool true if validation successful
539+
*/
540+
protected function validateModelData(Model $model, Array $values)
541+
{
542+
$validationResponse = $model->validateArray($values);
543+
544+
if ($validationResponse === true) {
545+
return true;
546+
}
547+
548+
throw new Exception\Validation(
549+
'Bad Request',
550+
static::ERROR_SCOPE | static::ERROR_HTTP_METHOD_NOT_ALLOWED,
551+
BaseResponse::HTTP_BAD_REQUEST,
552+
$validationResponse
553+
);
554+
}
555+
489556
/**
490557
* Default handling of POST request.
491558
* Must be called explicitly in handlePost function.
@@ -497,6 +564,8 @@ protected function handleGetDefault(Request $request, $model)
497564
public function handlePostDefault(Request $request, $model)
498565
{
499566
$values = $this->parseRequestContent($request->content, $model->getResourceType());
567+
$this->validateModelData($model, $values);
568+
500569
$model->fill($values);
501570

502571
if (!$model->save()) {

src/EchoIt/JsonApi/Model.php

Lines changed: 99 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
<?php namespace EchoIt\JsonApi;
22

3+
use Validator;
34
use Illuminate\Database\Eloquent\Collection;
45
use Illuminate\Database\Eloquent\Model as BaseModel;
56
use Illuminate\Database\Eloquent\Relations\Pivot as Pivot;
@@ -43,8 +44,53 @@ class Model extends \Eloquent
4344
*
4445
* @var array
4546
*/
47+
protected $defaultExposedRelations = [];
4648
protected $exposedRelations = [];
4749

50+
/**
51+
* An array of relation names of relations who
52+
* simply return a collection, and not a Relation instance
53+
*
54+
* @var array
55+
*/
56+
protected $relationsFromMethod = [];
57+
58+
/**
59+
* Get the model's default exposed relations
60+
*
61+
* @return Array
62+
*/
63+
public function defaultExposedRelations() {
64+
return $this->defaultExposedRelations;
65+
}
66+
67+
/**
68+
* Get the model's exposed relations
69+
*
70+
* @return Array
71+
*/
72+
public function exposedRelations() {
73+
return $this->exposedRelations;
74+
}
75+
76+
/**
77+
* Set this model's exposed relations
78+
*
79+
* @param Array $relations
80+
*/
81+
public function setExposedRelations(Array $relations) {
82+
$this->exposedRelations = $relations;
83+
}
84+
85+
/**
86+
* Get the model's relations that are from methods
87+
*
88+
* @return Array
89+
*/
90+
public function relationsFromMethod() {
91+
return $this->relationsFromMethod;
92+
}
93+
4894
/**
4995
* mark this model as changed
5096
*
@@ -76,6 +122,37 @@ public function getResourceType()
76122
return ($this->resourceType ?: $this->getTable());
77123
}
78124

125+
/**
126+
* Validate passed values
127+
*
128+
* @param Array $values user passed values (request data)
129+
*
130+
* @return bool|Illuminate\Support\MessageBag True on pass, MessageBag of errors on fail
131+
*/
132+
public function validateArray(Array $values)
133+
{
134+
if (count($this->getValidationRules())) {
135+
$validator = Validator::make($values, $this->getValidationRules());
136+
137+
if ($validator->fails()) {
138+
return $validator->errors();
139+
}
140+
}
141+
142+
return True;
143+
}
144+
145+
/**
146+
* Return model validation rules
147+
* Models should overload this to provide their validation rules
148+
*
149+
* @return Array validation rules
150+
*/
151+
public function getValidationRules()
152+
{
153+
return [];
154+
}
155+
79156
/**
80157
* Convert the model instance to an array. This method overrides that of
81158
* Eloquent to prevent relations to be serialize into output array.
@@ -85,13 +162,29 @@ public function getResourceType()
85162
public function toArray()
86163
{
87164
$relations = [];
165+
$arrayableRelations = [];
88166

89167
// include any relations exposed by default
90168
foreach ($this->exposedRelations as $relation) {
169+
// skip loading a relation if it is from a method
170+
if (in_array($relation, $this->relationsFromMethod)) {
171+
// if the relation hasnt been loaded, then load it
172+
if (!isset($this->$relation)) {
173+
$this->$relation = $this->$relation();
174+
}
175+
176+
$arrayableRelations[$relation] = $this->$relation;
177+
continue;
178+
}
179+
91180
$this->load($relation);
92181
}
93182

94-
foreach ($this->getArrayableRelations() as $relation => $value) {
183+
// fetch the relations that can be represented as an array
184+
$arrayableRelations = array_merge($this->getArrayableRelations(), $arrayableRelations);
185+
186+
// add the relations to the linked array
187+
foreach ($arrayableRelations as $relation => $value) {
95188
if (in_array($relation, $this->hidden)) {
96189
continue;
97190
}
@@ -110,6 +203,11 @@ public function toArray()
110203
}
111204
$relations[$relation] = $items;
112205
}
206+
207+
// remove models / collections that we loaded from a method
208+
if (in_array($relation, $this->relationsFromMethod)) {
209+
unset($this->$relation);
210+
}
113211
}
114212

115213
//add type parameter
Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,39 @@
1+
<?php namespace EchoIt\JsonApi;
2+
3+
use Illuminate\Http\JsonResponse;
4+
use Illuminate\Support\MessageBag as ValidationMessages;
5+
6+
/**
7+
* MultiErrorResponse represents a HTTP error response containing multiple errors with a JSON API compliant payload.
8+
*
9+
* @author Matt <[email protected]>
10+
*/
11+
class MultiErrorResponse extends JsonResponse
12+
{
13+
/**
14+
* Constructor.
15+
*
16+
* @param int $httpStatusCode HTTP status code
17+
* @param mixed $errorCode Internal error code
18+
* @param string $errorTitle Error description
19+
* @param Illuminate\Support\MessageBag $errors Validation errors
20+
*/
21+
public function __construct($httpStatusCode, $errorCode, $errorTitle, ValidationMessages $errors = NULL)
22+
{
23+
$data = [ 'errors' => [] ];
24+
25+
if ($errors) {
26+
foreach ($errors->keys() as $field) {
27+
28+
foreach ($errors->get($field) as $message) {
29+
30+
$data['errors'][] = [ 'status' => $httpStatusCode, 'code' => $errorCode, 'title' => 'Validation Fail', 'detail' => $message, 'meta' => ['field' => $field] ];
31+
32+
}
33+
34+
}
35+
}
36+
37+
parent::__construct($data, $httpStatusCode);
38+
}
39+
}

0 commit comments

Comments
 (0)