From 45f0e6be6732cae4642a7992a3366f9ef1a4472c Mon Sep 17 00:00:00 2001 From: BenceSzalai Date: Thu, 8 Apr 2021 19:52:04 +0200 Subject: [PATCH] Cover the new binding features --- docs/api-docs-generator/index.html | 11 +- docs/authentication/index.html | 11 +- docs/batch/index.html | 11 +- docs/binding/index.html | 571 +++++++++++++++++++++++++++++ docs/compression/index.html | 11 +- docs/index.html | 11 +- docs/installation/index.html | 11 +- docs/notifications/index.html | 11 +- docs/quickstart/index.html | 11 +- docs/requests/index.html | 74 ++-- docs/specification/index.html | 11 +- docs/testing/index.html | 11 +- 12 files changed, 716 insertions(+), 39 deletions(-) create mode 100644 docs/binding/index.html diff --git a/docs/api-docs-generator/index.html b/docs/api-docs-generator/index.html index 38fc821..d94cf02 100644 --- a/docs/api-docs-generator/index.html +++ b/docs/api-docs-generator/index.html @@ -167,7 +167,16 @@

Application

  • - + + + Parameter binding + + +
  • +
  • + diff --git a/docs/authentication/index.html b/docs/authentication/index.html index b984673..2e68f75 100644 --- a/docs/authentication/index.html +++ b/docs/authentication/index.html @@ -167,7 +167,16 @@

    Application

  • - + + + Parameter binding + + +
  • +
  • + diff --git a/docs/batch/index.html b/docs/batch/index.html index 6d180bb..359942b 100644 --- a/docs/batch/index.html +++ b/docs/batch/index.html @@ -167,7 +167,16 @@

    Application

  • - + + + Parameter binding + + +
  • +
  • + diff --git a/docs/binding/index.html b/docs/binding/index.html new file mode 100644 index 0000000..fafb16d --- /dev/null +++ b/docs/binding/index.html @@ -0,0 +1,571 @@ + + + + + + + + + + + + + + + + + + + + + Requests binding for JSON-RPC | JSON-RPC API server for Laravel framework + + + + + + + + + + + + + + + + + +
    +
    +
    + +
    + Logo +
    + +
    +
    + + +
    +
    +
    +
    + +
    +
    + +
    +

    Binding

    +

    Sometimes you may miss the automatic binding of models in the route. There are multiple possible solutions to access objects based on request parameters depending on your needs:

    +
      +
    • +

      Access using custom methods

      +
        +
      • use custom methods in a custom Request class
      • +
      • bind instances inside a custom Request class
      • +
      +
    • +
    • +

      Access using dependency injection

      +
        +
      • use a custom Request class to automatically resolve and inject Eloquent models
      • +
      • use a custom Request class to apply a custom resolution of objects to be injected
      • +
      • use the global binding features of the library
      • +
      +
    • +
    +

    Access using custom methods

    +

    Use custom methods in a custom Request class

    +

    Once you are using a custom FormRequest it is easy to implement additional methods to facilitate with resolution of object instances.

    +

    Access instances using dedicated methods

    +

    A simple approach is to create a method in the request, that returns the required instance based on the request parameters.

    +
    declare(strict_types=1);
    +
    +namespace App\Http\Requests;
    +
    +use App\User;
    +use Illuminate\Foundation\Http\FormRequest;
    +
    +class ExampleRequest extends FormRequest
    +{
    +    /**
    +     * @return \Illuminate\Database\Eloquent\Model|null
    +     */
    +    public function user()
    +    {
    +        $user = new User();
    +
    +        return $user->resolveRouteBinding($this->user);
    +    }
    +}
    +

    This user() method resolves an Eloquent model based on the user_id parameter. Then you can quickly and conveniently get the models in the methods of procedures:

    +
    /**
    + * Execute the procedure.
    + *
    + * @param  ExampleRequest  $request
    + * @return string
    + */
    +public function ping(ExampleRequest $request)
    +{
    +    $request->user();
    +    //...
    +}
    +

    Bind instances inside the request

    +

    It is also possible to replace or add the parameters directly inside the request.

    +
    declare(strict_types=1);
    +
    +namespace App\Http\Requests;
    +
    +use App\User;
    +use Illuminate\Foundation\Http\FormRequest;
    +
    +class ExampleRequest extends FormRequest
    +{
    +    /**
    +     * Bind instances into the Request after validation has passed.
    +     */
    +    public function passedValidation()
    +    {
    +        $user = new User();
    +        $user = $user->resolveRouteBinding($this->user);
    +        $this->merge(
    +            [
    +                'user_original' => $this->user,
    +                'user' => $user,
    +            ]
    +        );
    +    }
    +}
    +

    After this the resolved User model can be accessed from the Request, while the original user parameter value is also kept under the new user_original parameter.

    +
    /**
    + * Execute the procedure.
    + *
    + * @param  ExampleRequest  $request
    + * @return string
    + */
    +public function ping(ExampleRequest $request)
    +{
    +    $userInstance = $request->user;
    +    $originalUserParameter = $request->user_original;
    +    //...
    +}
    +

    Access using dependency injection

    +

    Sajya comes with three different built in ways to handle dependency injection needs. This means that by applying one (or more) of the below listed methods, class instances can be automatically injected into the Procedure methods as needed. The first two methods require the use of a special FormRequest while the last one can be used with any Request.

    +

    Use a custom Request class to automatically resolve and inject Eloquent models

    +

    If you want to get Eloquent model instances in the Procedure methods using dependency injection use a custom FormRequest that implements the Sajya\Server\Binding\BindsParameters interface.

    +
    declare(strict_types=1);
    +
    +namespace App\Http\Requests;
    +
    +use App\User;
    +use Illuminate\Foundation\Http\FormRequest;
    +use Sajya\Server\Binding\BindsParameters;
    +
    +class ExampleRequest extends FormRequest implements BindsParameters
    +{
    +    public function getBindings(): array
    +    {
    +        return [
    +            'userById'       => 'user_id',
    +            'userByEmail'    => 'user_email:email',
    +            'userByNestedId' => ['user','id']
    +        ];
    +    }
    +
    +    public function resolveParameter(string $parameterName)
    +    {
    +        return false;
    +    }
    +}
    +

    In the getBindings() method you can define the mapping between the Procedure method parameters and the request parameters. The meaning of the definitions are:

    +
      +
    • 'userById' => 'user': the procedure method parameter $userById will get the type-hinted Eloquent model instance with the id matching the user request parameter
    • +
    • 'userByEmail' => 'user:email': the procedure method parameter $userByEmail will get the type-hinted Eloquent model instance with the email attribute matching the user_email request parameter
    • +
    • 'userByNestedId' => ['user','id']: the procedure method parameter $userByNestedId will get the type-hinted Eloquent model instance with the id attribute matching the id request parameter nested inside the user request parameter
    • +
    +

    The actual model type to be injected depends on the Procedure method signature, so it is mandatory to typehint those parameters correctly. Note that it is only possible to use this resolution logic for classes that implement the Illuminate\Contracts\Routing\UrlRoutable interface, e.g.: Eloquent models.

    +
    /**
    + * Execute the procedure.
    + *
    + * @param  ExampleRequest  $request
    + * @return string
    + */
    +public function ping(ExampleRequest $request, User $userById, User $userByEmail, User $userByNestedId)
    +{
    +    $userInstance = userById;
    +    $originalUserParameter = $request->user_id;
    +    //...
    +}
    +

    Note: because the procedure method parameters are resolved in order, it is mandatory to always put the Request before the type-hinted parameter(s) of the handling method. Therefore the same setup with the following method signature like would fail.

    +
    public function ping(User $userById, User $userByEmail, User $userByNestedId, ExampleRequest $request) { //...
    + +

    Use a custom Request class to apply a custom resolution of objects to be injected

    +

    Similarly to the previous case, it is possible to implement any custom resolution logic inside a FormRequest that implements the Sajya\Server\Binding\BindsParameters interface. This is most usefull to resolve instances other than Eloquent models. For this, use the resolveParameter() method.

    +
    declare(strict_types=1);
    +
    +namespace App\Http\Requests;
    +
    +use App\User;
    +use Illuminate\Foundation\Http\FormRequest;
    +use Sajya\Server\Binding\BindsParameters;
    +
    +class ExampleRequest extends FormRequest implements BindsParameters
    +{
    +    public function getBindings(): array
    +    {
    +        return [];
    +    }
    +
    +    public function resolveParameter(string $parameterName)
    +    {
    +        if ( 'userByHash' === $parameterName ) {
    +            return User::getUserInstanceBasedOnASecretHash( $this->input('user_hash') );
    +        }
    +        return false; // Allow the service container to proceed with default resolution.
    +    }
    +}
    +

    In this case the procedure method parameter called $userByHash will be resolved by the User::getUserInstanceBasedOnASecretHash() method, and injected accordingly.

    +
    /**
    + * Execute the procedure.
    + *
    + * @param  ExampleRequest  $request
    + * @return string
    + */
    +public function ping(ExampleRequest $request, User $userByHash)
    +{
    +    $userInstance = $userByHash;
    +    $userHash = $request->user_hash;
    +    //...
    +}
    +

    It is possible to use both the getBindings() and the resolveParameter() methods together. If both are configured, resolution by resolveParameter() takes precedence over the resolution by getBindings().

    + + +

    Use the global binding features of the library

    +

    Defining the parameter bindings in a custom FormRequest class is convenient, because the connection between the binding and the Procedure method becomes implicit, by type-hinting the right FormRequest class on the method itself. However sometimes you may need to define the bindings in other places in the code, perhaps without using a custom FormRequest. For that case Sajya provides the RPC facade, which allows injection bindings to be defined in a global context.

    +

    It mirrors the logic of Laravel's Route::bind() and Route::model() calls, but with an extended call signature to address various needs.

    + +

    RPC::model()

    +

    The simplest case is to bind an Eloquent model based on a request attribute:

    +
    RPC::model('user', User::class);
    +

    With this on the Procedure method that handles the RPC call an User $user parameter will be injected with the resolved User model:

    +
    /**
    + * @param User $user The user resolved by global bindings.
    + */
    +public function getUserName(User $user): string
    +{
    +    return $user->getAttribute('name');
    +}
    + +
    Scoping
    +

    One may need to apply different models for different Procedure methods, so the binding can be scoped with the third argument:

    +
    RPC::model('user', RegularUser::class, 'myNamespace\MyProcedure@handleUser');
    +RPC::model('user', AdminUser::class,   'myNamespace\MyProcedure@handleAdminUser');
    +RPC::model('user', SpecialUser::class, 'myNamespace\special');
    +RPC::model('user', User::class, ''); // = RPC::model('user', User::class);
    +

    These lines mean:

    +
      +
    • Resolve a RegularUser model for the myNamespace\MyProcedure@handleUser method
    • +
    • Resolve an AdminUser model for the myNamespace\MyProcedure@handleAdminUser method
    • +
    • Resolve a SpecialUser model for all Procedures and methods under the myNamespace\special namespace
    • +
    • Resolve a User model for every other Procedure and method
    • +
    +

    The resolution happens in the order the binders are registered, so start with the more specific ones and progress with more generic towards the unscoped global binding.

    +

    It is also possible to bind multiple scopes in one call:

    +
    RPC::model('user', RegularUser::class, [
    +    'myNamespace\MyProcedure@handleUser',
    +    'myNamespace\MyOtherProcedure@handleUser'
    +]);
    +

    It is also possible to use the PHP callable array syntax for the scopes which are defined dow to the method level, e.g.:

    +
    RPC::model('user', RegularUser::class, ['myNamespace\MyProcedure', 'handleUser']);
    +RPC::model('user', AdminUser::class,   ['myNamespace\MyProcedure', 'handleAdminUser']);
    + +
    Method parameter mapping
    +

    In some cases the method parameter may have a different name than the request parameter. In that case the 4th argument can be used to declare which method parameter should the binding apply to:

    +
    RPC::model('customer', User::class, '', 'user');
    +

    In this case the User model resolved based on the customer request parameter will be injected into the Procedure method argument $user, instead of the default $customer.
    + This can be particularly useful with nested parameters, as their names may even collide. Consider these two bindings:

    +
    RPC::model(['seller','id'], User::class, '', 'seller');
    +RPC::model(['buyer','id'], User::class, '', 'buyer');
    +

    Without the 4th parameter both the seller and the buyer User would be attempted to be injected for a parameter called $id, however that is not possible to have two method parameters with the same name, therefore the first line will resolve a User model based on the parameter id nested under the parameter seller and inject the resulting User instance under $seller, while the second will inject another User resolved based on the id of the buyer and inject that for the parameter called $buyer.

    + +
    Error handling
    +

    Similar to Laravel's Route::model() the last optional parameter is an error handler callback. It is called if the automatic resolution of the model fails. Anything returned from this method will be injected instead.

    +
    RPC::model('user', User::class, '', null, static function () {
    +    return auth()->user();
    +});
    +

    This code will attempt automatic Eloquent model resolution, but if it fails to find the User it will inject the currently logged in user instead.

    + +

    RPC::bind()

    +

    It is the same for Route::bind() what RPC::model() is for Route::model(). The 1st, 3rd and 4th parameters are the same.
    + The 2nd parameter is instead of $class is $binder, which is a callback that performs the binding. It receives the value of the request parameter configured by the 1st argument and is expected to return the instance to be injected into the Procedure method. E.g.:

    +
    RPC::bind(
    +    'user_hash',
    +    /**
    +     * @param  string  $parameter
    +     * @return User
    +     */
    +    static function (string $parameter) {
    +        return User::getUserByHash($parameter);
    +    },
    +    '', // global scope
    +    'user' // Inject for the method parameter called $user
    +);
    +

    Nested parameters

    +

    Nested parameters are supported the same way for the global bindings using the RPC Facade as for the BindsParameters::getBindings() method. It is possible to bind to a nested request parameter, for example if the request has a customer which has an id, the binding can be configured with ['customer', 'id']. Custom fields are supported too when the binding is for Eloquent models, e.g.: ['customer', 'address:email'].

    + +

    Resolution order

    +

    Bindings configured in a FormRequest that implement the Sajya\Server\Binding\BindsParameters interface take precedence over bindings using the RPC Facade. However FormRequest based resolution only works for parameters that come after the type-hinted FormRequest parameter. For any parameter before the FormRequest parameter only the RPC Facade defined bindings apply. If none of the configured bindings can resolve the type-hinted parameters, the resolution is left for Laravel's Service container.

    +
    +
    +
    +
    +
    + + +
    + + + + + + + + + + + + + + + + + + + + diff --git a/docs/compression/index.html b/docs/compression/index.html index 8183fc5..6e90a5d 100644 --- a/docs/compression/index.html +++ b/docs/compression/index.html @@ -167,7 +167,16 @@

    Application

  • - + + + Parameter binding + + +
  • +
  • + diff --git a/docs/index.html b/docs/index.html index ef0e720..78bb152 100644 --- a/docs/index.html +++ b/docs/index.html @@ -167,7 +167,16 @@

    Application

  • - + + + Parameter binding + + +
  • +
  • + diff --git a/docs/installation/index.html b/docs/installation/index.html index 52e97d7..03ca252 100644 --- a/docs/installation/index.html +++ b/docs/installation/index.html @@ -167,7 +167,16 @@

    Application

  • - + + + Parameter binding + + +
  • +
  • + diff --git a/docs/notifications/index.html b/docs/notifications/index.html index 021d7c7..7874c60 100644 --- a/docs/notifications/index.html +++ b/docs/notifications/index.html @@ -167,7 +167,16 @@

    Application

  • - + + + Parameter binding + + +
  • +
  • + diff --git a/docs/quickstart/index.html b/docs/quickstart/index.html index 9fe1c65..9459298 100644 --- a/docs/quickstart/index.html +++ b/docs/quickstart/index.html @@ -167,7 +167,16 @@

    Application

  • - + + + Parameter binding + + +
  • +
  • + diff --git a/docs/requests/index.html b/docs/requests/index.html index 8d87419..68d9e67 100644 --- a/docs/requests/index.html +++ b/docs/requests/index.html @@ -18,8 +18,8 @@ - - Batch/Notification requests for JSON-RPC | JSON-RPC API server for Laravel framework + + Requests data and validation for JSON-RPC | JSON-RPC API server for Laravel framework @@ -165,6 +165,24 @@

    Application

    Validation and Data
    +
  • +
  • + + + Parameter binding + + +
  • +
  • + + + Parameter binding + +
  • @@ -231,10 +249,12 @@

    Requests

    Requests

    -
    -

    Accessing The Request

    -

    To obtain an instance of the current HTTP request via dependency injection, you should type-hint the Illuminate\Http\Request class on your controller method. The incoming request instance will automatically be injected.

    -
    declare(strict_types=1);
    +                    
    +

    Accessing the Data

    +

    The parameters of the incoming RPC request are automatically made available under the standard Laravel request.

    +
    curl 'http://127.0.0.1:8000/api/v1/endpoint' --data-binary '{"jsonrpc":"2.0","method":"tennis@ping","params":{"innings": "out"},"id" : 1}'
    +

    To obtain an instance of the current HTTP request via dependency injection, you should type-hint the Illuminate\Http\Request class on your controller method. The incoming request instance will automatically be injected.

    +
    declare(strict_types=1);
     
     namespace App\Http\Procedures;
     
    @@ -257,13 +277,14 @@ 

    Accessing The Request

    */ public function ping(Request $request) { - return $request->input('innings'); + return $request->input('innings'); // will return 'out' } }
    -

    The transferred parameters will be automatically written to the object:

    -
    curl 'http://127.0.0.1:8000/api/v1/endpoint' --data-binary '{"jsonrpc":"2.0","method":"tennis@ping","params":{"innings": "out"},"id" : 1}'
    -

    Since this is a regular Laravel object, you can perform all available operations on it, for example, validation:

    -
    /**
    +                    

    To obtain all parameters as an array, use this syntax on the injected Request:

    +
    $request->request->all()
    +}
    +

    Since this is a regular Laravel object, you can perform all available operations on it, for example, validation:

    +
    /**
      * Execute the procedure.
      *
      * @param  Request  $request
    @@ -275,10 +296,12 @@ 

    Accessing The Request

    'innings' => 'required|string|max:255', ]); }
    -

    Sometimes you may miss the automatic binding of models in the route. But you can extend the request class. Let's execute the artisan command:

    -
    php artisan make:request ExampleRequest
    -

    The generated class will be placed in the app/Http/Requests directory.

    -
    declare(strict_types=1);
    +                    

    Using FormRequests

    +

    Just like in Laravel controllers, FormRequests can be used to provide validation and authentication using a simple syntax.

    +

    The first step is to create a child class of the Illuminate\Foundation\Http\FormRequest class. Let's execute the artisan command:

    +
    php artisan make:request ExampleRequest
    +

    The generated class will be placed in the app/Http/Requests directory.

    +
    declare(strict_types=1);
     
     namespace App\Http\Requests;
     
    @@ -304,21 +327,13 @@ 

    Accessing The Request

    public function rules() { return [ - 'user' => 'bail|required|unique:user|max:255', + 'user_id' => 'bail|required|unique:user|max:255', ]; } - - /** - * @return \Illuminate\Database\Eloquent\Model|null - */ - public function user() - { - $user = new User(); - - return $user->resolveRouteBinding($this->user); - }
    -

    Then you can quickly and conveniently get values in the methods of procedures:

    -
    /**
    +}
    +

    The authorize() and rules() methods behave the same as in standard Laravel.

    +

    All you need to do is to type-hint this new Request class instead of Illuminate\Http\Request on the procedure method. Then you can quickly and conveniently get values in the methods of procedures:

    +
    /**
      * Execute the procedure.
      *
      * @param  ExampleRequest  $request
    @@ -326,7 +341,8 @@ 

    Accessing The Request

    */ public function ping(ExampleRequest $request) { - $request->user(); + // Do stuff with the request already authenticated and validated, e.g.: + $user_id = $request->get('user_id'); //... }
    diff --git a/docs/specification/index.html b/docs/specification/index.html index 78fa81e..36e5d38 100644 --- a/docs/specification/index.html +++ b/docs/specification/index.html @@ -167,7 +167,16 @@

    Application

  • - + + + Parameter binding + + +
  • +
  • + diff --git a/docs/testing/index.html b/docs/testing/index.html index 8facc1d..e118056 100644 --- a/docs/testing/index.html +++ b/docs/testing/index.html @@ -167,7 +167,16 @@

    Application

  • - + + + Parameter binding + + +
  • +
  • +