New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
[5.5] [WIP] Transformable Responses #18502
Closed
Closed
Changes from all commits
Commits
Show all changes
8 commits
Select commit
Hold shift + click to select a range
7b20e01
Added transformable Response
Aferz 0f9ea29
Applied CI Styles Fix
Aferz 33ce58f
WIP
Aferz 753424c
Applied CI Styles Fix
Aferz bf31967
Fix typos and useless codes.
Aferz a993cb1
Added Transformable Contract. Improved renaming feature. Improved Col…
Aferz 53ff032
Fix DockBlock Full Qualified Domain Name.
Aferz fd49983
Fixed cs @throws in DocBlock.
Aferz File filter
Filter by extension
Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,13 @@ | ||
<?php | ||
|
||
namespace Illuminate\Contracts\Support; | ||
|
||
interface Transformable | ||
{ | ||
/** | ||
* Get data to apply transformations. | ||
* | ||
* @return array | ||
*/ | ||
public function getTransformableData(); | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,383 @@ | ||
<?php | ||
|
||
namespace Illuminate\Http; | ||
|
||
use Exception; | ||
use Illuminate\Support\Arr; | ||
use Illuminate\Support\Str; | ||
use Illuminate\Validation\ValidationData; | ||
use Illuminate\Contracts\Support\Arrayable; | ||
use Illuminate\Contracts\Support\Transformable; | ||
|
||
class TransformableResponse extends JsonResponse | ||
{ | ||
/** | ||
* Constructor. | ||
* | ||
* @param mixed $data | ||
* @param int $status | ||
* @param array $headers | ||
* @param int $options | ||
* @return void | ||
*/ | ||
public function __construct($data = null, $status = 200, $headers = [], $options = 0) | ||
{ | ||
if (is_array($data)) { | ||
$transformedData = $this->transform($data); | ||
} elseif ($data instanceof Transformable) { | ||
$transformedData = $this->transform($this->getTransformableData()); | ||
} elseif ($data instanceof Collection) { | ||
$transformedData = $this->handleCollectionTransformation($data); | ||
} elseif ($data instanceof Arrayable) { | ||
$transformedData = $this->transform($data->toArray()); | ||
} | ||
|
||
parent::__construct($transformedData, $status, $headers, $options); | ||
|
||
$this->original = $data; | ||
} | ||
|
||
/** | ||
* Handle how a collection will be transformed. | ||
* | ||
* @param \Illuminate\Support\Collection $data | ||
* @return mixed | ||
*/ | ||
protected function handleCollectionTransformation(Collection $data) | ||
{ | ||
return $this->transform( | ||
$data->map(function ($item) { | ||
if ($item instanceof Transformable) { | ||
return $item->getTransformableData(); | ||
} elseif ($item instanceof Arrayable) { | ||
return $item->toArray(); | ||
} | ||
|
||
return $item; | ||
}) | ||
->toArray() | ||
); | ||
} | ||
|
||
/** | ||
* Transforms the response data. | ||
* | ||
* @param array $data | ||
* @return array | ||
*/ | ||
protected function transform(array $data) | ||
{ | ||
if (! empty($data) && method_exists($this, 'visibilityRules')) { | ||
$data = $this->applyVisibilityRules( | ||
$data, | ||
$this->visibilityRules() | ||
); | ||
} | ||
|
||
if (! empty($data) && method_exists($this, 'castingRules')) { | ||
$data = $this->applyCastingRules( | ||
$data, | ||
$this->castingRules() | ||
); | ||
} | ||
|
||
if (! empty($data) && method_exists($this, 'mutationRules')) { | ||
$data = $this->applyMutationRules( | ||
$data, | ||
$this->mutationRules() | ||
); | ||
} | ||
|
||
if (! empty($data) && method_exists($this, 'renamingRules')) { | ||
$data = $this->applyRenamingRules( | ||
$data, | ||
$this->renamingRules() | ||
); | ||
} | ||
|
||
return $data; | ||
} | ||
|
||
/** | ||
* Apply visibility rules to given data. | ||
* | ||
* @param array $data | ||
* @param array $rules | ||
* @return array | ||
*/ | ||
protected function applyVisibilityRules(array $data, array $rules) | ||
{ | ||
if (empty($rules = $this->resolveWildcardRules($data, $rules))) { | ||
return $data; | ||
} | ||
|
||
$data = $this->performShowFields($data, $rules); | ||
$data = $this->performHideFields($data, $rules); | ||
|
||
return $data; | ||
} | ||
|
||
/** | ||
* Apply rules over the fields that must be displayed. | ||
* | ||
* @param array $data | ||
* @param array $rules | ||
* @return array | ||
*/ | ||
protected function performShowFields(array $data, array $rules) | ||
{ | ||
if (empty($applicableRules = array_filter($rules))) { | ||
return $data; | ||
} | ||
|
||
return array_reduce(array_keys($applicableRules), | ||
function ($transformedData, $attribute) use ($data) { | ||
if ($value = Arr::get($data, $attribute)) { | ||
Arr::set($transformedData, $attribute, $value); | ||
} | ||
|
||
return $transformedData; | ||
}, | ||
[]); | ||
} | ||
|
||
/** | ||
* Apply rules over the fields that must be hidden. | ||
* | ||
* @param array $data | ||
* @param array $rules | ||
* @return array | ||
*/ | ||
protected function performHideFields(array $data, array $rules) | ||
{ | ||
$applicableRules = array_filter($rules, function ($rule) { | ||
return ! $rule; | ||
}); | ||
|
||
return array_reduce(array_keys($applicableRules), | ||
function ($transformedData, $rule) { | ||
Arr::forget($transformedData, $rule); | ||
|
||
return $transformedData; | ||
}, | ||
$data); | ||
} | ||
|
||
/** | ||
* Apply casting rules to given data. | ||
* | ||
* @param array $data | ||
* @param array $rules | ||
* @return array | ||
*/ | ||
protected function applyCastingRules(array $data, array $rules) | ||
{ | ||
if (empty($rules = $this->resolveWildcardRules($data, $rules))) { | ||
return $data; | ||
} | ||
|
||
return array_reduce(array_keys($rules), | ||
function ($data, $rule) use ($rules) { | ||
if (Arr::has($data, $rule)) { | ||
$value = $this->performCasting( | ||
Arr::get($rules, $rule), | ||
Arr::get($data, $rule) | ||
); | ||
|
||
Arr::set($data, $rule, $value); | ||
} | ||
|
||
return $data; | ||
}, | ||
$data); | ||
} | ||
|
||
/** | ||
* Performs casting to given value based on given type. | ||
* | ||
* @param string $type | ||
* @param mixed $value | ||
* @return mixed | ||
*/ | ||
protected function performCasting($type, $value) | ||
{ | ||
switch ($type) { | ||
case 'int': | ||
case 'integer': | ||
return (int) $value; | ||
case 'real': | ||
case 'float': | ||
case 'double': | ||
return (float) $value; | ||
case 'string': | ||
return (string) $value; | ||
case 'bool': | ||
case 'boolean': | ||
return (bool) $value; | ||
default: | ||
return $value; | ||
} | ||
} | ||
|
||
/** | ||
* Apply mutation rules to given value. | ||
* | ||
* @param array $data | ||
* @param array $rules | ||
* @return mixed | ||
*/ | ||
public function applyMutationRules(array $data, array $rules) | ||
{ | ||
if (empty($rules = $this->resolveWildcardRules($data, $rules))) { | ||
return $data; | ||
} | ||
|
||
return array_reduce(array_keys($rules), | ||
function ($data, $rule) use ($rules) { | ||
if (Arr::has($data, $rule)) { | ||
$value = $this->performMutations( | ||
Arr::get($rules, $rule), | ||
Arr::get($data, $rule) | ||
); | ||
|
||
Arr::set($data, $rule, $value); | ||
} | ||
|
||
return $data; | ||
}, | ||
$data); | ||
} | ||
|
||
/** | ||
* Performs given mutators in given value. | ||
* | ||
* @param string $mutators | ||
* @param mixed $value | ||
* @return mixed | ||
* | ||
* @throws \Exception | ||
*/ | ||
public function performMutations($mutators, $value) | ||
{ | ||
$mutators = explode('|', $mutators); | ||
|
||
return array_reduce($mutators, function ($value, $mutator) { | ||
$method = 'mutator'.Str::studly($mutator); | ||
|
||
if (! method_exists($this, $method)) { | ||
$class = static::class; | ||
|
||
throw new Exception("No mutator [$method] defined in [$class]"); | ||
} | ||
|
||
return $this->$method($value); | ||
}, $value); | ||
} | ||
|
||
/** | ||
* Apply renaming rules for given attribute. | ||
* | ||
* @param array $data | ||
* @param array $rules | ||
* @return array | ||
*/ | ||
protected function applyRenamingRules(array $data, array $rules) | ||
{ | ||
if (empty($rules = $this->resolveWildcardRules($data, $rules))) { | ||
return $data; | ||
} | ||
|
||
uksort($rules, function ($a, $b) { | ||
return count(explode('.', $a)) < count(explode('.', $b)); | ||
}); | ||
|
||
$resultData = $this->performRenaming($data, $rules); | ||
|
||
return array_reduce(array_keys($resultData), | ||
function ($data, $attribute) use ($resultData) { | ||
Arr::set($data, $attribute, $resultData[$attribute]); | ||
|
||
return $data; | ||
}, | ||
[]); | ||
} | ||
|
||
/** | ||
* Performs renaming mutators in given data. | ||
* | ||
* @param array $data | ||
* @param array $rules | ||
* @return array | ||
*/ | ||
protected function performRenaming(array $data, array $rules) | ||
{ | ||
$resultData = array_reduce(array_keys($rules), | ||
function ($encodedData, $rule) use ($rules) { | ||
$replace = preg_replace( | ||
'/(\w|\s|\-)+$/', | ||
$rules[$rule], | ||
$rule | ||
); | ||
|
||
return str_replace('"'.$rule, '"'.$replace, $encodedData); | ||
}, | ||
json_encode(Arr::dot($data))); | ||
|
||
return json_decode($resultData, true); | ||
} | ||
|
||
/** | ||
* Resolve array rules generating a new rule for every '*' symbol. | ||
* | ||
* @param array $data | ||
* @param array $rules | ||
* @return array | ||
*/ | ||
protected function resolveWildcardRules(array $data, array $rules) | ||
{ | ||
return array_reduce(array_keys($rules), | ||
function ($parsedRules, $rule) use ($data, $rules) { | ||
if (Str::contains($rule, '*')) { | ||
$gatheredRules = array_keys( | ||
ValidationData::initializeAndGatherData($rule, $data) | ||
); | ||
|
||
return array_merge( | ||
$parsedRules, | ||
$this->sanitizeWildcardGatheredRules( | ||
$rule, | ||
$gatheredRules, | ||
$rules[$rule] | ||
) | ||
); | ||
} | ||
|
||
$parsedRules[$rule] = $rules[$rule]; | ||
|
||
return $parsedRules; | ||
}, []); | ||
} | ||
|
||
/** | ||
* Sanitize rules removing those that don't appear into original rules. | ||
* | ||
* @param string $rule | ||
* @param array $gatheredRules | ||
* @param bool $valueForValidOnes | ||
* @return array | ||
*/ | ||
protected function sanitizeWildcardGatheredRules($rule, array $gatheredRules, $valueForValidOnes) | ||
{ | ||
$pattern = '/'.str_replace('.*.', '\.([0-9])+\.', $rule).'/'; | ||
|
||
return array_reduce($gatheredRules, | ||
function ($validRules, $rule) use ($pattern, $valueForValidOnes) { | ||
if (preg_match($pattern, $rule)) { | ||
$validRules[$rule] = $valueForValidOnes; | ||
} | ||
|
||
return $validRules; | ||
}, | ||
[]); | ||
} | ||
} |
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
What if data implements
\Illuminate\Contracts\Support\Jsonable
?There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I would also allow any object to implement a
\Illuminate\Contracts\Support\Transformable
interface, so that the object itself can always control the way it's transformed.There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
How we should treat
\Illuminate\Contracts\Support\Jsonable
?? I mean, I have no way to determine how to transform the data if I can't get it. Any suggestion ?I like the idea of
\Illuminate\Contracts\Support\Transformable
, but how do we should treat aCollection
ofTransformables
? ShouldTransformable
prevail againstArrayable
?There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Have a look at
\Illuminate\Routing\Router::prepareResponse
If data is a
Collection
, map all its itemsI believe so