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
Conversation
Woa, really nice to see someone come to this idea. I've done something similar before, using @thephpleague/fractal on Laravel 5.4, the implementation was pretty strike forward, nothing's comlicated & it support Paginators well. IMO I'm go with fractal. |
Yeah, before make this PR I've reviewed Fractal, but IMO I think that It doesn't really fit with a nice Laravel approach. For me It was too verbose. I thought that filter through data needed to be easier, that's why I used validation rules style w\ custom classes to represent a specific response for a specific request instead of injecting multiple classes. Implement pagination is easy, I'm just waiting to an oficial response to see if this is useful. Thanks for your feedback @feryardiant !! |
Nice and elegant approach! Fractal is more confuse and verbose and this gives us the simplicity to transform the data the way it should be. Great job! |
Definitely more useful than Fractal! Good work! |
It may be overkill, but did you consider adding the ability to mutate the data? We can already define mutators and casts, but it would be nice to be able to define all of this in one custom response class. With this, I would drop Fractal completely. However, you probably don't want to add a bunch of unneeded complexity. Either way, nice work! If not accepted, I'll probably use in some of my api projects anyway. |
@devcircus What do you mean with mutate the data ? Do you mean mutate with something like If you mean mutate the schema (replace I'm glad you like it. Thanks! |
+1! very useful |
By mutate, I mean simply altering the presentation, but yes, (A) changing the reference(key) and/or (B) changing how the data is presented, similar to how Fractal does it. This is something that can already be handled by presenters, accessors, etc. I just thought, notwithstanding SRP, that it would be nice to be able to fully define the response inside a custom response class. |
Maybe a decorator outside of core would be better for that? |
Ok, now I see what you mean. I don't really mind to try to figure out how we could encapsulate that presentation logic if that does not reject this PR (because that's not the main purpose of this feature). Let's see what @taylorotwell or @GrahamCampbell think about this. |
/** | ||
* Transforms the response data. | ||
* | ||
* @param array $data | ||
* @param array $data |
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.
please revert
Hi guys, I've updated the main message to reflect some updates I made in this package:
Thanks |
* @param int $status | ||
* @param array $headers | ||
* @param int $options | ||
*/ |
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.
missing return void annotation
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 copied the constructor from JsonResponse, should I apply equally?
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.
Looks like a mistake there then. Laravel always includes return annotations, on all methods, without exception.
/** | ||
* Transforms the response data. | ||
* | ||
* @param array $data |
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.
cs
* | ||
* Example: posts.*.comments.*.title => posts.0.comments.0.title | ||
* | ||
* @param array $data |
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.
cs
* Sanitize gathered array rules removing those that don't appear into | ||
* orginal rules. | ||
* | ||
* @param string $rule |
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.
cs, etc
|
||
/** | ||
* Sanitize gathered array rules removing those that don't appear into | ||
* orginal rules. |
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.
please shorten to 80 chars including 5 spaces, star, and space
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.
80 chars length only applies to DocBlocks ?
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.
It's something we apply (ish) to the short descriptions to stop them getting too long. One rule that is strict is that they cannot overflow onto the next line.
{ | ||
switch ($this->getCastType($attribute)) { | ||
case 'int': | ||
case 'integer': |
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.
lots of code duplication here with eloquent
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.
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 ?
👍 I like the second approach, but casting and mutating should be unified. |
* Constructor. | ||
* | ||
* @param mixed $data | ||
* @param int $status |
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.
Please use exactly 2 spaces before the dollar for param annotations on new code, rather than aligning.
protected function getCastType($attribute) | ||
{ | ||
if (! $castings = $this->castingRules()) { | ||
return null; |
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.
just return;
gives the same semantics
* Get casting rule for given attribute. | ||
* | ||
* @param string $attribute | ||
* @return bool |
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.
bool|null
|
||
/** | ||
* Sanitize gathered array rules removing those that | ||
* don't appear into orginal rules. |
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.
these descriptions cannot span more than one line.
|
||
/** | ||
* Resolve array rules generating a new rule for every | ||
* item in the array specified with the '*' symbol. |
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.
same
I think you should remove abstract from those methods since most of the times we won't use the 4 methods on our child class... Making them optional would be better IMO. Also this seems pretty good so far... 👍 |
It seems reasonable. I will modify It. Thanks |
*/ | ||
public function __construct($data = null, $status = 200, $headers = [], $options = 0) | ||
{ | ||
if (is_array($data)) { |
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 a Collection
of Transformables
? Should Transformable
prevail against Arrayable
?
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
??
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 againstArrayable
I believe so
|
||
return array_reduce(array_keys($applicableRules), | ||
function ($transformedData, $rule) { | ||
if (Arr::has($transformedData, $rule)) { |
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.
\Illuminate\Support\Arr::forget
always make sure that the array key exist, so using \Illuminate\Support\Arr::has
is useless.
* @param mixed $value | ||
* @param string $mutators | ||
* @return mixed | ||
*/ |
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.
Missing @throws \Exception
if (! method_exists($this, $method)) { | ||
$classname = static::class; | ||
|
||
throw new \Exception("There is no mutator [$method] declared in [$classname]."); |
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.
Please import \Exception
|
||
return array_reduce($gatheredRules, | ||
function ($validRules, $rule) use ($pattern, $valueForValidOnes) { | ||
preg_match($pattern, $rule, $matches); |
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.
This is enough as we only want to re-use the $subject
argument.
if (preg_match($pattern, $rule)) {
$validRules[$rule] = $valueForValidOnes;
}
} | ||
|
||
/** | ||
* Sanitize rules removing those that don't appear into orginal rules. |
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.
Typo: original
} | ||
|
||
/** | ||
* Apply visbility rules to given data. |
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.
Typo: visibility
@Aferz - how do you handle So |
In constructor: if (is_array($data)) {
$transformedData = $this->transform($data);
} elseif ($data instanceof Arrayable) {
$transformedData = $this->transform($data->toArray());
} else {
$transformedData = $data;
}
parent::__construct($transformedData, $status, $headers, $options);
$this->original = $data; should do the trick.
I would like to think it a bit more because I will update this PR with your suggestions tomorrow. Thanks for the feedback! |
If this gets approved yet #14357 (simple Transformer) PR was not... that would be a shame. |
I've updated this PR with following changes:
TODO:
|
…lection handling.
/** | ||
* Handle how a collection will be transformed. | ||
* | ||
* @param Collection $data |
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.
fqnc required please
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.
Oops! Fixed.
* | ||
* @param string $mutators | ||
* @param mixed $value | ||
* @throws Exception |
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.
Please follow our cs regarding throws docs. :)
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.
Sorry but what do you eaxctly mean ??
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.
* @throws \Illuminate\Auth\Access\AuthorizationException |
I still wonder if this whole thing would be best suited as a laravel package, rather than shipping in the core. |
@GrahamCampbell - That something that we all should talk about, but I'm gonna give my 2 cents. So far, Laravel offers support/utilities for build an API (Auth & JWT, separated API routes file, Throttling, etc...) but one of the most difficult things is how you display your data and what you show and what you don't for your multiple requests. Yes, you have This could be a package? Of course. But IMO I think Laravel should provide 'something' to filter your response before sending to a client (There is no a easy/clean way so far to make this). This way you gain full control from the way your request is validated to how your response is displayed. So far, Laravel treats every Request as unique (If you want, of course), but not the Response. Conceptually, that's something that could be improved to gain granular control about your code. And that's what this PR wants to achieve. Anyways, this is my opinion. I would like to know what you all think, if you share my concept about 'unique Response' and if this PR should be accepted or rejected. I would like to know what @taylorotwell thinks about this to know if this worth it. |
I'm not ready to commit to merging this into the core at this time (or any other fractal API type package). However, I will definitely be revisiting all of this before the 5.5 release and will add this PR to my list of PRs to look over again when I get to that point. Thanks. |
Mmmh ok then. |
That's not true at all.. Whatever you return from your controller is formatted as a response. You can return anything, and easily intercept and format it however you like. I'm doing it right now. I return a class that extends a certain class, and I typecheck and format. It's pretty easy AND straight forward. |
Hi @taylorotwell , sorry for bothering you again. 5.5 release is near so... Will this PR take in consideration for 5.5 or higher versions??. Just for keep updating this PR with useful stuff and improvements or think of it as a possible package. Thanks |
Why ?
The purpose of this pull request is to give Laravel better flexibility when responding to API requests.
I have created a new
TransformableResponse
class that allows to define a few rulesets for transform the response in multiple forms that will be passed to the client.Its design is highly inspired in
FormRequests
classes. If we can defineRequests
individually, why don't extend this behaviour to responses?What is included in this PR ?
First of all, I suggest to store our
Response
classes insideapp/Http/Responses
folder(A nice artisan command will be included out of the box). Once said this ...Class structure
Dummy Data
We will use this dummy data for demo purposes:
Defining visibility rules
This ruleset define what fields will be present once the response is emitted. It's the first ruleset to be executed.
This will be the resultant response:
How does this work ?
As you can see in the rules section you define which attributes should be visible and which attributes should be hidden. So:
Easy.
Defining casting rules
This ruleset define which fields should be casted. Valid casting values are:
int, integer, real, float, double, string, bool, boolean
. It's the second ruleset to be executed.This will be the resultant response (chained from first ruleset):
Defining mutation rules
This ruleset define how the data should be mutated. You can define your own mutators in the own class, just prefix the name of your method with the word
mutator
. If you want to reuse mutators you always can inherit from a parent class 👍 . It's the third ruleset to be executed.This will be the resultant response (chained from first and second ruleset):
Defining renaming rules
This ruleset define how the keys of your data should be renamed. It's the forth ruleset to be executed.
This will be the resultant response (chained from first, second and third ruleset):
How this feature will be used ?
Simple. Look at this controller method:
Why is a WIP ?
There are a few things to be covered before merge this PR, but first I want to check if this is useful for the Laravel community.
TODO:
php artisan make:response MyCustomResponse
command.Any idea ? Any feedback ? Let me know what you all think!
Thanks !