-
-
Notifications
You must be signed in to change notification settings - Fork 114
Proposed Feature | New DTO Experience! 🪄 #488
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
base: v3
Are you sure you want to change the base?
Conversation
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.
Self review
|
Howdy @techenby - following our chat on Friday, I've been thinking about the "transformer" name we came up with and I thought that maybe it'll be better to keep this functionality as DTOs and only "outgoing" rather than incoming too, I know we were trying to figure out how to do incoming too like |
|
Taken a note, a couple of people don't like $connector->send($request)->return(Order::class); |
|
Another suggestion is to add the type support to the existing $connector->send($request)->dto(Order::class);We tried this before and couldn't get the generics right, but we might be able to try this again. |
|
Hey @Sammyjo20 very much like this idea, this whole PR! So it would be flexible and we could keep using Spatie Laravel-Data DTOs, but just flavor them with your But honestly, I am too lazy to remember the DTO classes for my hundreds of requests when tinkering, so I would end up adding a helper method to all of my Requests, e.g. Could you be so nice and also add such an (optional) In the past, I have been using |
|
about the naming: Or why not So, in my whole project, I would then use |
|
I'm just wondering how this would work if my API returns an array (e.g. when I query orders) and the JSON is maybe not quite clean but like this: |
|
@eclipse313 That's a good point I haven't thought about yet - what happens when the API returns multiple results? Do we maybe need to make this similar to Laravel's HTTP Resources/Resource collections or maybe we could have a I'm not sure on that. @onlime I agree with all your points, I don't feel super confident in a defaultDto method... Perhaps instead of replacing the old |
|
Foremost, I think it's a great initiative to see if this can be improved. I don't have a specific idea how to solve it currently, but at least I can provide some information/thoughts. For multiple resources, I tend to use a factory (as it can be used for both the public function createDtoFromResponse(Response $response): Collection
{
return $response
->collect('orders')
->map(function (array $order): Order {
return OrderFactory::buildFromResponse($order);
});
}This works fine, but the type hint is missing. The proposed solution does solve it for a single resource, but in this case, it would be preferred to even specific the type of the item in the Collection, something like: /**
* @return Collection<Order>
*/Maybe, it would be an idea to separate the functionality of building the DTO and the request? Also, because the same logic can be used for multiple requests, i.e. the |
|
Hey all many thanks for your contribution, it's much appreciated 🙏 Okay so I've come with another concept/prototype for many objects. I'm not 100% sold on it, but something is better than nothing. There's now two interfaces (IntoObject and IntoObjects) - Needs a better name. If your API response returns multiple objects then you can use IntoObjects and then use the Now one improvement I want to make is the ability to have the same interface on the class at the same time, but I need some help with naming. Is Example <?php
use Saloon\Http\Response;
use Saloon\Contracts\DataObjects\IntoObjects;
class Superhero implements IntoObjects
{
public function __construct(
public string $name,
) {
//
}
public static function fromResponse(Response $response): array
{
return $response->collect('data')
->map(function (array $item): self {
return new static($item['superhero']);
})
->toArray();
}
}Usage $superheroes = $connector->send($request)->intoMany(Superhero::class); // array<Superhero>I do wonder if there's a way I can do this automatically, like accept a method like this: function intoMany(string $class, string|callable $key, bool $throw = true)
{
$iterator = is_callable($key) ? $key() : $this->json($key);
// Iterate and call `fromResponse`?
}Then $superheroes = $connector->send($request)->intoMany(Superhero::class, 'items'); |
|
This great. Only to say I use Saloon with Spatie Laravel-Data DTOs and if you could align with their pattern of the from() method then that would be a cleaner experience than having to add an additional fromResponse method https://spatie.be/docs/laravel-data/v3/as-a-resource/from-data-to-resource |
|
I love the idea! Regarding the implementation - I use Saloon with Hyperf and other non-Laravel frameworks. And I don't use Spatie's Laravel Data package in my Laravel projects. So it'd be good to go for the best generic solution rather than tying it to how a third-party Laravel package does things. |
|
Hey 👋 Just a personal opinion I love the DX here : $superheroes = $connector->send($request)->intoMany(Superhero::class); // array<Superhero>But I think the definition in DTO could be better named, I don't have perfect ones, but here's some ideas : use Saloon\Http\Response;
use Saloon\Contracts\DataObjects\IntoDataObjects;
class Superhero implements IntoDataObjects
{
public function __construct(
public string $name,
) {
//
}
public static function fromResponseToDataObjects(Response $response): array
{
//
}
}This is more specific, I think a For this, I think it could be a great DX to reduce some code and goes with the rest of the package which is great ! $superheroes = $connector->send($request)->intoMany(Superhero::class, 'items'); |
|
Hi @Sammyjo20, I have an idea about the return type of the interface IntoObjects
{
/**
* Handle the creation of the object from Saloon
*
* @return iterable<self>
*/
public static function fromResponse(Response $response): iterable;
}use Saloon\Http\Response;
use Saloon\Contracts\DataObjects\IntoDataObjects;
Illuminate\Support\Collection;
class Superhero implements IntoDataObjects
{
public function __construct(
public string $name,
) {
//
}
public static function fromResponseToDataObjects(Response $response):Collection
{
return $response->collect('data')
->map(function (array $item): self {
return new static($item['superhero']);
});
}
} |
|
@Sammyjo20 Totally sorry I missed this, I'll take a look :) |
|
Maybe this is just me, but I don't want the place where I send the request to have to worry about what it's getting back. I like the request class being in charge of formatting the data to send and transforming the data it receives. My simplest suggestion would be to rename I don't like DTOs and would rather pass back a single value or an array. You can already do that with the DTO methods, but I feel weird about calling |
|
$order = $connector->send($request)->hydrate(Order::class); |
|
into / dto 👍 |
|
@timacdonald I love the word |
After having a fantastic conversation with @techenby, we came up with a way of improving the DTO experience with Saloon, so this is my proposed version.
Before
Previously you'd have to define a method on your connector or request called
createDtoFromResponse. This method would accept aResponseobject and returnmixed(allowing you to return any object or type of your choosing). This worked but it doesn't have very good PHPStan / type support because the response doesn't have any knowledge of the type defined in its request.Usage
This would result in your application and PHPStan not knowing what the return of
dtois resulting in having to do type-gymnastics or using instanceof checks to tell the application it's working correctly.Proposed Idea
The proposed solution is a new
intomethod on the response. This method accepts a class string.Order Class
The class must implement the
DataTransferObjectinterface provided by Saloon, which requires the class to have afromResponsemethod defined. With this, the DTO definition is moved into the DTO itself, which is great when you might construct the DTO elsewhere likefromRequest(e.g from a Laravel Request) and also makes it a lot easier to do typing. It also means that you could re-use one DTO for multiple requests write definitions based on the connector/request in the response.Questions