-
Notifications
You must be signed in to change notification settings - Fork 22
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
Parameter model binding to resolve method dependencies #17
base: master
Are you sure you want to change the base?
Conversation
Hey @BenceSzalai! Thanks for your suggestion. I, just like you, pondered on this topic. At first glance, this makes sense: public function getBindings(): array
{
return [
'user' => 'user:email',
];
} Against: public function user(): User
{
return User::where('email', $this->user)->firstOrFail();
} But this has several disadvantages, for example:
I am very glad that you are also interested in this issue. Let's try to discuss a little.I tend to lean more towards the general notation of bindings for a point. For example: PRC::bind('user', function ($value) {
return User::where('email', $value)->firstOrFail();
}); This avoids multiple descriptions for each action, regardless of the procedure. // Any multidimensional array
$params = [
'terminal' => '127.0.0.0',
'command' => [
'patch' => '/',
'user' => 1,
'execute' => 'ps -a'
],
];
// After binding
$params = [
'terminal' => '127.0.0.0',
'command' => [
'patch' => '/',
'user' => EloquentModel,
'execute' => 'ps -a'
],
]; In addition to eliminating duplication, this also expands the possibilities since we can form any pre-built classes (That is, the ability to use not only with eloquent models). But that doesn't cover many of the cons, like the IDE hints. This shouldn't greatly increase the responsibilities of the package, what do you think of such a solution? About the questions:
|
|
Hi @BenceSzalai, I've seen the latest changes. |
Nested parametersNow it is possible to bind to a request parameter, for example if the request has a Global bindingI've added a facade which can be used to define bindings globally, without using a special
|
…sing custom FormRequest classes
…sing custom FormRequest classes
Completely eliminates the potential issue covered in f3f6ea0.
…error code. Also removed the BindingResolutionException, which not correctly linked into the `BoundMethod::addDependencyForCallParameter()` method and can be replaced by adding the message and code parameters to the `throw_if()` call as additional arguments.
Ahh, sorry, than for my push again to the branch. Ignore them please! I'll stop then for now looking at it. And thanks for looking into it! I can learn from your changes I think! |
Hi! Is there anything I can do to move this forward? Is there something missing or wrong in the solution, or is it just the lack of free capacity it is staled for? |
@BenceSzalai Hi. Yes, I remember this. I have no time at all now. I will try to free myself as quickly as possible. @Mamau Can you please do this? |
Sure, don't get me wrong, I was just wondering, no rushing or so! :) |
Hi @BenceSzalai. I came back a month after our last productive conversation and looked at what we had to offer. It seems we overcomplicated this thing. It may bring some practices that I don't really like. Therefore, I have prepared a small alternative that should cover almost all our expectations from binding: Make it possible to receive parameters directly in the method: For example, let's take the following simple procedure, where we pass two values for which we want to get the subtract: {
"jsonrpc": "2.0",
"method": "math@subtract",
"params": {
"a": 4,
"b": 2
},
"id": 1
} In this case, we need to write something like the following in the handler: public function subtract(Request $request) {
return $request->get('a') - $request->get('b');
} It is rather strange when we operate with a small number of parameters and without specifying their type. So let us make it possible to pass these arguments on-demand automatically. That could be written as follows: public function subtract(int $a, int $b): int
{
return $a - $b;
} It will also work for deep things, via {
"jsonrpc": "2.0",
"method": "....",
"params": {
"user": {
"name": "Alex"
}
},
"id": 1
} for argument: public function handler(string $userName): string
{
return $userName;
} Specifying the same names when working with code and documentation We can use different variable names in the code and expected Binding declaration I think we should rely on how laravel does it. Take our substract example: RPC::bind('a', function () {
return 100;
}); This will automatically replace the substituted value in our method: public function subtract(int $a, int $b): int
{
return $a - $b; // $a = 100
} But at the same time, the request will contain the original value that was passed. As for the binding to the models, here I am on the side of the sequence, as I mentioned earlier. However, it is extraordinary that the API would accept different values for the same value in other places. For example, the Therefore, you should rely on what is indicated in the model. By default, it looks like this. (Every /**
* Retrieve the model for a bound value.
*
* @param mixed $value
* @param string|null $field
* @return \Illuminate\Database\Eloquent\Model|null
*/
public function resolveRouteBinding($value, $field = null)
{
return $this->where($field ?? $this->getRouteKeyName(), $value)->first();
} If this needs to be changed, and accept more email or something else. You can change this condition by adding And this is how we declare the model binding in a straightforward way: RPC::model('user', User::class); It will work for anyone where the As with automatic substitution, bindings RPC::model('user.order', ...);
RPC::bind('user.order', ...; In turn, this will expect public function handler($userOrder) I regret that I did not have a long time to get to this, and, of course, in the end, I distorted everything. Still, I sincerely believe that the proposed alternative is more practical. I respect and appreciate your work. |
* refs #17 Added alternative for binding params * Update config for phpunit * Update cs-fixer * Fix styling
Introduction
I've been a bit disappointed to loose the ability to automatically bind method parameters based on the requests. With a REST-ish interface Laravel provides the Route Model Binding feature, which makes it convenient to just type-hint models on the controller methods, and let them be automatically resolved and injected to be used.
I could see an explicit mention of a workaround in the documentation, but I wanted to achieve something which provides a more elegant syntax to do it so.
Considerations
I've been looking at how the default Route Model Binding works and tried to achieve a similar level of convenience, however since with the RPC route definition all Procedure classes are listed together, defining the parameter bindings in the route files would create convoluted and hard to read syntax.
Also while the default Route Model Binding provides the
Route::model()
and theRoute::bind()
methods, they require the registration of a new Service and Facade accessors and I wanted to keep the solution to minimal and to implement with adding the least overhead to the library.Solution
Since the official recommentation to work around the problem was already to use custom
FormRequest
instances on the Procedure methods, I think it is kind of easy to add the additional binding definitions there too. This way the implementer gets the full control over how to resolve the custom parameters, but can rely on the service container to help with the binding and after all simply just type hint what they need on the Procedure methods.The entry point of the solution is the
HandleProcedure::handle()
method, where I've replacedApp::call($this->procedure);
with
BoundMethod::call(app(), $this->procedure);
which is pretty much the same, however by this change we can extend the
\Illuminate\Container\BoundMethod::addDependencyForCallParameter()
with the custom logic needed to inject the parameters.The only catch is that the
FormRequest
parameter must come before any of the type hinted parameters that need custom resolution logic to apply. This is becauseaddDependencyForCallParameter()
resolves the parameters in the order they are needed for the Procedure method. So the first parameter is aFormRequest
(or in fact any class) that implements the newBindsParameters
Interface. During the resolution of the subsequent parameters,addDependencyForCallParameter()
first checks each previously resolved parameters if they implement theBindsParameters
Interface, and if so, uses them for the resolution. If there is no such parameters or no matches to be resolved, it passes the resolution back toparent::addDependencyForCallParameter()
ensuring the implementation stays compatible with the original behaviour, if someone does not use customFormRequests
or theBindsParameters
Interface.Now the
BindsParameters
Interface defines two methods to be used for the resolution.resolveParameter()
is the "replacement" forRoute::bind()
. It receives the name of the argument of the Procedure method, and expects the dependency instance to be returned. False can be returned to indicate to proceed with the rest of the resolution methods.null
can be also used, if the parameter is optional.getBindings()
is the "replacement" for the default Implicit Binding. Since the Route files do not define how request parameters corresponds to Model parameters, "implicit" binding is not possible: it must be explicit. On the other hand, whileRoute::model()
really matches the route parameters to Model types, in our case this is not needed, since the Model type can always be determined by the type hint on the Procedure method. What we need instead is a way to explicitly specify which parameters of the RPC request correspond to which parameter of the Processor method: thegetBindings()
therefore should return an array mapping the Processor method parameters to the RPC request parameters.The resolution follows the same logic as for route parameters. By default the request parameter is expected to contain a primary key (id or whatever is defined as the key of the given Model), or may contain other keys, in the same fashion as for route model binding using custom keys; the parameter and the key separated using a colon, e.g.:
post:slug
.Resolution by
resolveParameter()
takes precedence over resolution based on thegetBindings()
, andgetBindings()
takes precedence over the default resolution by the service container.Note
Since any parameter that implements the
BindsParameters
interface can resolve subsequent dependencies, it is also possible to use a separateRequest
orFormRequest
and another dependency of aBindsParameters
class, effectively separating the Request from the Resolution, but this is more like a sideefect than a feature, tho someone may find it useful to use differentFormRequest
casses with the sameBindsParametersClass
or using the sameFormRequest
with a differentBindsParametersClass
for different methods.Examples
You can find the tests in the PR, which indicates how to use the bindings:
FixtureRequest
demonstrates the implementation ofBindsParameters::getBindings()
andBindsParameters::resolveParameter()
.FixtureProcedure
has been extended with few newgetUserName...
methods, which utilise the parameter resolution (and suit the test cases).In the Test cases I've user the
Illuminate\Foundation\Auth\User
as the type to be resolved to avoid adding an additional fixture class to the library just for the tests' sake.Questions
Error codes
I've read the error code section of the JSON:RPC documentation, but couldn't truly figure out what the ranges really mean. I came to the conclusion that the library implementing the standard may use the codes between -32000 to -32099 so I've assigned few of those in the new
BoundMethod
class to various resolution issues, but please advice if this usage is correct or should it use different codes?Documentation
The contribution guide says documentation is expected with patches, but would that mean to submit a separate PR against the https://github.com/sajya/sajya.github.io repo with the documentation?
Please let me know what do you think. I hope you find the addition useful! Let me know if something should be changed!