mleczek/laravel-rest

Laravel package with a set of helpful tools for building REST API.

v1.2.2 2020-08-28 20:54 UTC

This package is auto-updated.

Last update: 2024-10-29 04:59:14 UTC


README

Latest Stable Version Build Status License

Laravel package with a set of tools helpful for building REST API.

Installation

To install this package you will need:

  • Laravel 5.3+
  • PHP 5.6.4+

Require this package with composer:

composer require mleczek/laravel-rest

In config/app.php add the RestServiceProvider:

'providers' => [
    Mleczek\Rest\RestServiceProvider::class,
]

Configuration

Publish the package configuration and ContextServiceProvider:

php artisan vendor:publish --provider="Mleczek\Rest\RestServiceProvider"

This command will create 2 files for you:

  • config/rest.php
  • app/Providers/ContextServiceProvider.php

Register new local copy of the ContextServiceProvider in the config/app.php file:

'providers' => [
    App\Providers\ContextServiceProvider::class,
]

Usage

Query params

Supplied by the client. They control the format of the response most often by narrowing result.

With

Include related data in response:

users?with=messages,permissions

In the background the library will attach related models using Eloquent defined relations, which is quite similar to calling $quer->with(['messages', 'permissions']).

By default all relations are disabled, which means that you have to explicitly define which relations can be used in API call. You can set up this in the previously published ContextServiceProvider:

$with = [
    User::class => 'messages',
    Message::class => ['author', 'recipient'],
]

If you'd like to do some policy checks then you can define context class. In this class you can create methods which name is equal to the relation name. Whenever you try to access this relation the new instance of this class will be created and the result of the method will determine if the relation can be used or not.

class UserWithContext
{
    public function messages()
    {
        return Auth::check() && Auth::user()->is_root;
    }
}

Of course you have to register that context in the ContextServiceProvider:

$with = [
    User::class => UserWithContext::class,
]

Now if someone without root access call the with=messages then nothing will happen. If you'd like you can throw 401 or 403 response code from the context class.

In UserWithContext class and other context classes you can inject your dependencies in the constructor, because class is resolved using service container.

Offset

Skip n first items:

users?offset=3

You can use this as well for the related data:

users?with=messages&offset=3,messages.5

Limit

Limit results to n items:

users?limit=5

You can use this as well for the related data:

users?with=messages&limit=messages.1

Fields

Get only specified fields:

users?fields=first_name,last_name

You can use this as well for the related data:

users?with=messages&fields=first_name,last_name,messages.id

If you specify fields only for the primary model then all fields will be retrieved for the related one:

users?with=messages&fields=first_name,last_name

Above example will return first_name, last_name and all columns for the messages model (eq. id, author_id, recipient_id, content). This helps in finding a sub-optimal query - if you don't want any field from the related model then just simply remove redundant values from the with query param.

Sort

Return results in specified order:

users?sort=score,last_name_desc

You can use this as well for the related data:

users?with=messages&sort=messages.latest

By default no sort methods are available, which means that you have to explicitly define sort that can be used for specific model. You can set up this in the previously published ContextServiceProvider:

$sort = [
    Message::class => MessageSortContext::class,
]

Then you have to create context class. Sort name will be converted to method name using camelCase style (eq. last_name_desc will call lastNameDesc method). As a first argument you will receive the Illuminate\Database\Query\Builder.

class MessageSortContext
{
    public function latest($query)
    {
        $query->latest();
    }
}

Now you can sort messages using latest method in any context:

users?with=messages&sort=messages.latest
messages?with=author&sort=latest

In MessageSortContext class and other context classes you can inject your dependencies in the constructor, because class is resolved using service container.

Filter

Put constraint on request:

users?filter=score_above:30,last_name_in:[Smith,Bloggs]

Unlike sort param the filter query param can also accept arguments:

users?filter=without_args
users?filter=one_arg:5
users?filter=special_chars:"O'X\" []],,"
users?filter=two_or_more_args:[12,"Lorem lipsum"]

You can use this as well for the related data:

users?with=messages&filter=messages.recipient_id:5

By default no filter methods are available, which means that you have to explicitly define filters that can be used for specific model. You can set up this in the previously published ContextServiceProvider:

$filter = [
    User::class => UserFilterContext::class,
]

Then you have to create context class. Filter name will be converted to method name using camelCase style (eq. last_name_in will call lastNameIn method). As a first argument you will receive the Illuminate\Database\Query\Builder.

class UserFilterContext
{
    public function scoreAbove($query, $value)
    {
        // Validation of the $value argument...
        
        $query->where('score', '>', $value);
    }
}

Now you can filter users using scoreAbove method in any context:

users?filter=score_above:15
groups?with=users&filter=users.score_above:5

In UserFilterContext class and other context classes you can inject your dependencies in the constructor, because class is resolved using service container.

Responses

Library extends the Response class using some helpful macros.

Item

response()->item($query);

Response single model item with response code 200 OK:

return response()->item(User::query());

This macro will use the fields and with query param.

This is not recommended to use with, select and addSelect on the $query parameter passed to the method. After all if you would like to do this the behavior it is as follows:

  • if someone pass the same relation name in with query param the one you created will be overridden
  • if someone pass the fields query parameter then only fields specified in this parameter will be returned

Often you will need to do some operations using retrieved model, in this case use rest() helper funtion:

public function show()
{
    $user = rest()->item(User::query());
    $this->authorize('show', $user); // bad usage, see below solution

    return response()->item($user);
}

The above example has one major defect, the second argument passed to the authorize can contain only fields specified in the fields query param. The better solution is to retrieve the whole model, call authorize method and then apply the query param transformations:

public function show()
{
    $user = User::first();
    $this->authorize('show', $user);
    
    return response()->item($user);
}

Summarizing, the response()->item() macro can accept the Illuminate\Database\Eloquent\Model or Illuminate\Database\Eloquent\Builder object.

Collection

response()->collection($query);

Response collection of models with response code 206 Partial Content:

return response()->collection(User::query());

This macro will use the fields, sort, filter, offset, limit and with query param. Again, using orderBy, select, addSelect, limit/take, offset/skip methods on the $query argument is not recommended, but fell free to add some constraints using where method.

return response()->collection(User::where('is_root', false));

If you will need to make some operations before returning response you can use rest() helper function:

public function show()
{
    $users = rest()->collection(User::query());
    // Some operations goes here...
    // $users->count  - number of retrieved models [0,limit]
    // $users->limit  - max number of retrieved models
    // $users->offset - number of skipped models
    // $users->data   - retrieved models

    return response()->collections($users);
}

Accepted

response()->accepted();

Empty response with status code 202 Accepted.

No Content

response()->noContent();

Empty response with status code 204 No Content.

Created

response()->created($model[, $location]);

Response created model with status code 201 Created. If $location is specified then appropriate Location header will be added to the response.

Updated

response()->updated([$model]);

Response updated model (if provided) with status code 200 OK.

Patched

response()->patched([$model]);

Response part of updated model (if provided) with status code 200 OK.

Deleted

response()->deleted();

Empty response with status code 204 No Content.

Tips and tricks

Default Context

By default library implements 2 context classes:

protected $sort = [
    // Your context classes...
    User::class => \Mleczek\Rest\Context\DefaultSortContext::class,
];

protected $filter = [
    // Your context classes...
    User::class => \Mleczek\Rest\Context\DefaultFilterContext::class,
];

These context can be used with any class and allow sorting and filtering using fillable attributes. Example usage for the default User model:

// ?filter=<attribute_name>:<expected_value>
users?filter=email:"rest@example.com"
users?filter=password:some_string // side effect

// ?sort=<attribute_name> or ?sort=<attribute_name>_desc
users?sort=name,email_desc
users?sort=name_desc

It's recommended to use only for the dev purposes. In future releases implementation will change in order to prevent accidental security vulnerabilities (like the above one with password column). Any ideas are welcome.

Contributing

Thank you for considering contributing! If you would like to fix a bug or propose a new feature, you can submit a Pull Request.

License

The library is licensed under the MIT license.