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 the Route::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 replaced
App::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 because addDependencyForCallParameter()
resolves the parameters in the order they are needed for the Procedure method. So the first parameter is a FormRequest
(or in fact any class) that implements the new BindsParameters
Interface. During the resolution of the subsequent parameters, addDependencyForCallParameter()
first checks each previously resolved parameters if they implement the BindsParameters
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 to parent::addDependencyForCallParameter()
ensuring the implementation stays compatible with the original behaviour, if someone does not use custom FormRequests
or the BindsParameters
Interface.
Now the BindsParameters
Interface defines two methods to be used for the resolution.
resolveParameter()
is the "replacement" for Route::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, while Route::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: the getBindings()
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 the getBindings()
, and getBindings()
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 separate Request
or FormRequest
and another dependency of a BindsParameters
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 different FormRequest
casses with the same BindsParametersClass
or using the same FormRequest
with a different BindsParametersClass
for different methods.
Examples
You can find the tests in the PR, which indicates how to use the bindings:
FixtureRequest
demonstrates the implementation of BindsParameters::getBindings()
and BindsParameters::resolveParameter()
.
FixtureProcedure
has been extended with few new getUserName...
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!