Skip to content
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
wants to merge 8 commits into from
Closed
Show file tree
Hide file tree
Changes from 6 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
13 changes: 13 additions & 0 deletions src/Illuminate/Contracts/Support/Transformable.php
@@ -0,0 +1,13 @@
<?php

namespace Illuminate\Contracts\Support;

interface Transformable
{
/**
* Get data to apply transformations.
*
* @return array
*/
public function getTransformableData();
}
382 changes: 382 additions & 0 deletions src/Illuminate/Http/TransformableResponse.php
@@ -0,0 +1,382 @@
<?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
*/
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

missing return void annotation

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I copied the constructor from JsonResponse, should I apply equally?

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Looks like a mistake there then. Laravel always includes return annotations, on all methods, without exception.

public function __construct($data = null, $status = 200, $headers = [], $options = 0)
{
if (is_array($data)) {
Copy link
Contributor

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?

Copy link
Contributor

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.

Copy link
Contributor Author

@Aferz Aferz Mar 28, 2017

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 a Collection of Transformables ? Should Transformable prevail against Arrayable ?

Copy link
Contributor

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 ??

Have a look at \Illuminate\Routing\Router::prepareResponse

How do we should treat a Collection of Transformables

If data is a Collection, map all its items

Should Transformable prevail against Arrayable

I believe so

$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 Collection $data
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

fqnc required please

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Oops! Fixed.

* @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':
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

lots of code duplication here with eloquent

Copy link
Contributor Author

@Aferz Aferz Mar 27, 2017

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yep, thats something I wanted to review here.

I didn't want to refactor Eloquent to use the switch that it uses to define the casting. What should I do here ?

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
* @throws Exception
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Please follow our cs regarding throws docs. :)

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Sorry but what do you eaxctly mean ??

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

* @throws \Illuminate\Auth\Access\AuthorizationException

* @return mixed
*/
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Missing @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;
},
[]);
}
}