-
Notifications
You must be signed in to change notification settings - Fork 11.5k
[12.x] Colocate Container build functions with the SelfBuilding
interface
#56731
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
Conversation
Like the idea, don't like the name so much. One doubt: In your sample code, the Wouldn't this trigger an infinite loop? As the EDIT: I guess, from the test case, the If so, the code sample could be something like this: class AolInstantMessengerConfig implements WithFactory
{
public function __construct(
public string $apiKey,
public string $userName,
public int $awayMessageDuration,
public string $awayMessage,
) {}
public static function buildWithFactory(): self
{
$validated = Validator::make(config('aim'), [
'api-key' => 'string',
'user_name' => 'string',
'away_message' => 'array',
'away_message.duration' => ['integer', 'min:60', 'max:3600'],
'away_message.body' => ['string', 'min:1'],
])->validate();
// the config was already fetched for validation,
// so let's use the validation's result
return new self(
apiKey: $validated['api-key'],
userName: $validated['user_name'],
awayMessageDuration: $validated['duration'],
awayMessage: $validated['body'],
);
}
} |
Same 💀 I figured no matter what I named it, Taylor would have a better suggestion so I didn't try very hard.
Ooof. I forgot to push a commit! Thank you! I went back and factored this a bit differently. The class is now named |
WithFactory
interfaceBuildable
interface
Much better name! Also I see that you guard against an infinite loop using the container's What do you think of adding a test case with this recursive behavior? To future-proof it? |
Indeed! 👍🏻 |
I do have a test written locally for this, will try to push up a |
Co-authored-by: Rodrigo Pedra Brum <[email protected]>
c7e5bb2
to
fc0a0ef
Compare
I think I may go with |
Makes good sense to me. Do you see any obvious next steps for this? Happy to jump on anything obvious. Otherwise, I plan to take a run at typed form request objects. Thanks for considering this @taylorotwell! |
My main question would be why not just use |
We can and do! But I think that it would pay dividends to live inside the framework. |
Buildable
interfaceSelfBuilding
interface
@rodrigopedra I'm not sure I'm following (and I don't have the framework code pulled up at the moment): how exactly is the
Is this a typo? There's no Singleton class, we would have to reflect and search through the attributes , right? |
The Vibe
To me, it feels like Laravel devs are demanding greater degrees of type-safety. There have been numerous PRs to improve static analysis within the framework, Wayfinder 2 creates TypeScript definitions from request and response objects, Laravel Idea has added the ability to create a DTO from a Request object, things like spatie's Laravel Data package are popular, et cetera.
The Problem
Requests are untyped objects, essentially functioning as a wrapper around an InputBag. There are ways to retrieve input as a specific type, but they require magic strings and often will have me looking back and forth between the request
rules()
to make sure I have the correct name and type. Creating a separate class for request DTOs feels very cluttered.What if our request object was instead a strictly typed DTO? Names and types are immediately clear, and the request has already been validated and authorized. Something perhaps like
This is not the problem that this PR solves, but instead, clears the way for the above. What this PR introduces is a way to validate and configure a class before it's built, colocated inside of the implementation. To my knowledge, the framework probably doesn't support any way of doing this cleanly. For the validating before resolving concern, you could register a
Container@beforeResolving()
callback. But for something like a strictly typed form request, I'm actually not sure I can think of a way that doesn't involve creating two separate files (one for the DTO, one for the FormRequest) and doing something funky in the ServiceProvider (that would amount to ValidatesWhenResolved's functionality).A Path Forward
I drew some inspiration from how the
ValidatesWhenResolved
interface works, but took it a step further. The shortcoming with the existing interface is that it runs AFTER the object has been built, meaning we lose the ability to build objects with constructor property promotion (as well as have to deal with collisions of existing properties from theFormRequest
class).What if devs (and framework code) could specify how to construct an object directly on the class itself? Without the need for a separate class, the colocation makes it easier to follow. The
SelfBuilding
interface is used by the container to indicate how an object is constructed.An Example
Let's say I want to build a config class to avoid having to call
Config::string('aim.away_message')
in my code, but keep it centralized inside of an AolInstantMessengerConfig class. I might build something like this:This code validates that the config values are all valid and if so, uses the container to leverage the #[Config] attributes.
Possible Next Steps
Create a new FormRequest class that moves all of the
ValidatesWhenResolvedTrait
functionality to live inside of anewInstance()
method.Closing Thoughts
I realize this is a pretty long-shot PR and the follow ups would be too. Having two separate FormRequests would be a mess for people just getting up to speed with the framework. I do think that, even without moving in that direction, there's still a reason to merge this PR. Outside of the example above, we could make the instantiation of DTOs from FormRequests even easier.