shabushabu/abseil

This package is abandoned and no longer maintained. No replacement package was suggested.

Taking the pain out of creating a JSON:API in your Laravel app

v0.3.3 2020-12-02 13:38 UTC

This package is auto-updated.

Last update: 2022-12-29 03:40:21 UTC


README

Abseil

PHPUnit Tests GitHub license

Taking some of the pain out of creating a JSON:API in your Laravel app

ToDo

  • Allow ModelQuery to query by fields other than uuid, eg by slug
  • Enjoy rock star status and live the good life

Installation

You will eventually be able to install the package via composer (‼️ once it's been published to Packagist...):

$ composer require shabushabu/abseil

Morph Map

If you don't use a morph map yet, now's the time to get your foot in the door. Just chuck all your models in there so Abseil can do it's magic. Keep the keys the same as your route parameters, btw. Strictly speaking Abseil only requires the MORPH_MAP constant, but you might as well go all in. Bet on that rope to hold your weight...

namespace App\Providers;

use Illuminate\Database\Eloquent\Relations\Relation;
use Illuminate\Support\ServiceProvider;
use [...];

class AppServiceProvider extends ServiceProvider
{
    public const MORPH_MAP = [
        'category' => Category::class,
        'page'     => Page::class,
        'user'     => User::class,
    ];

    public function boot(): void
    {
        Relation::morphMap(self::MORPH_MAP);
    }
}

The MORPH_MAP constant will then be used to add uuid patterns for your route model binding (disable in config if you use auto-incrementing ids). It'll also allow you to use the fantastic query builder package from Spatie for your single models and not just for collections.

Class Constants

Abseil expects certain class constants to be present on your models. We've already talked about MORPH_MAP, but there are also JSON_TYPE and ROUTE_PARAM. These should be set on each model.

use ShabuShabu\Abseil\Model;

class Category extends Model
{
    public const JSON_TYPE = 'categories';
    public const ROUTE_PARAM = 'category';
}

ShabuShabu\Abseil\Model is there to make things easier for you, but if you use auto-incrementing ids then you will have to create your own base model using the Abseil model as a guide.

Usage

Eloquent Resources and the JSON:API standard have a bit of a rocky relationship. "We're just way too different", JSON:API might say. "That upstart of a spec is just too opinionated", Laravel might retort. They tolerate each other and get along for the most part but won't ever be best mates, they feel. Abseil is here to make them rethink their relationship.

Controllers

Most Abseil controllers will look something like this. Just make sure that your controller extends the Abseil controller and you're all set. Please note, that Abseil controllers expect ShabuShabu Harness requests for any save operation.

namespace App\Http\Controllers;

use App\Http\Requests\PageRequest;
use App\Http\Resources\Page as PageResponse;
use App\Page;
use Illuminate\Http\{Request, Response};
use ShabuShabu\Abseil\Http\Resources\Collection;
use ShabuShabu\Abseil\Http\Controller;

class PageController extends Controller
{
    public function index(Request $request): Collection
    {
        return $this->resourceCollection(Page::class, $request);
    }

    public function store(PageRequest $request): Response
    {
        return $this->createResource($request, Page::class);
    }

    public function show(Request $request, Page $page): PageResponse
    {
        return $this->showResource($request, $page);
    }

    public function update(PageRequest $request, Page $page): Response
    {
        return $this->updateResource($request, $page);
    }

    public function destroy(Page $page): Response
    {
        return $this->deleteResource($page);
    }

    public function restore(Page $page): Response
    {
        return $this->restoreResource($page);
    }
}

Events

Abseil throws a number of events that you can hook into. Here's a full list:

  • ResourceCreated
  • ResourceUpdated
  • ResourceDeleted
  • ResourceRestored
  • ResourceRelationshipSaved

The names are kinda self-explanatory. The payload for each event is always the model in question. ResourceRelationshipSaved is fired once for each relationship model that has been saved.

Relationships

Abseil makes it easier to save any relationships via a JSON:API POST or PUT request. It does this by looping through the data.relationships array, figuring out which relation it needs and then calling a sync{relationship-name} method on the model.

Here's an example request payload:

{
    "data": {
        "id": "904754f0-7faa-4872-b7b8-2e2556d7a7bc",
        "type": "pages",
        "attributes": {
            "title": "Some title",
            "content": "Lorem what?"
        },
        "relationships": {
            "category": {
                "data": {
                    "type": "categories",
                    "id": "9041eabb-932a-4d47-a767-6c799873354a"
                }
            }
        }
    }
}

Abseil will then call the following method so you can save the category as you see fit:

$result = $page->syncCategory(
    Category::find('9041eabb-932a-4d47-a767-6c799873354a')
);

Abseil will throw an error if that method does not exist, so it's your responsibility to make sure it's there when you allow saving relationships via ShabuShabu Harness requests.

Staying with this example, the Page::syncCategory method could be as easy as the following:

public function syncCategory(Category $category): self
{
    $this->category()->associate($category)->save();
    return $this;
}

Resources

Resources have always been my biggest stumbling block when trying to create a valid JSON:API. With Abseil, though, the following is possible: Note that we're only specifying the data.attributes here. Anything else, like relationships, will be handled for you.

namespace App\Http\Resources;

use ShabuShabu\Abseil\Http\Resources\Resource; 

class Page extends Resource
{
    public function resourceAttributes($request): array
    {
        return [
            'title'       => (string)$this->title,
            'content'     => (string)$this->content,
            'createdAt'   => $this->date($this->created_at),
            'updatedAt'   => $this->date($this->updated_at),
            'deletedAt'   => $this->date($this->deleted_at),
        ];
    }
}

If, for example, there is a user relationship and it was specified via the include query parameter, then Abseil will load that relationship automatically for you and attach it to the response. ShabuShabu Belay is the perfect counter part for Abseil and will handle the JS side of things. Great if you want to create a client app for your API using Vue or Nuxt.

Resource Collections

Collection resources do not need to be specifically created, although you can if you want to. Abseil will use its own collection class by default for any index responses. One thing to note here is that the default Laravel pagination data is being transformed to camel-case:

{
    [...]
    "meta": {
        "pagination": {
            "currentPage": 1,
            "from": 1,
            "lastPage": 1,
            "path": "https:\/\/awesome-api.com\/pages",
            "perPage": 20,
            "to": 7,
            "total": 7
        }
    },
    [...]
}

All models should also implement the Queryable interface. It only requires a single method: modifyPagedQuery.

/**
 * Modify the query
 *
 * @param \Spatie\QueryBuilder\QueryBuilder $query
 * @param \Illuminate\Http\Request $request
 * @return \Spatie\QueryBuilder\QueryBuilder
 */
public static function modifyPagedQuery(QueryBuilder $query, Request $request): QueryBuilder;

Here you can then configure the query builder, add sorts, includes, filters, etc.

Routing

The only thing Abseil expects form your application as far as routing is concerned is that you name your single GET routes according to a certain convention:

'{JSON_TYPE}.show'

That way, Abseil can automatically create the links section for you.

Exceptions

Abseil ships with a couple exceptions to transform errors into valid JSON:API format. To use them, just add something like the following to your exception handler:

public function render($request, Throwable $exception): Response
{
    // ...

    if ($exception instanceof \Illuminate\Validation\ValidationException) {
        $exception = \ShabuShabu\Abseil\Exceptions\ValidationException::withMessages($exception->errors());
    }

    if ($exception instanceof \Spatie\QueryBuilder\Exceptions\InvalidQuery) {
        $exception = \ShabuShabu\Abseil\Exceptions\InvalidQueryException::from($exception);
    }

    // ...

    return parent::render($request, $exception);
}

Middleware

There's a middleware that you can use for your JSON:API enabled routes. It's already registered for you and aliased to media.type. It accepts an optional parameter for the header name. The default is conrtent-type, but can also be used for accept. If using the class directly is more your cup of tea, you can find it here: \ShabuShabu\Abseil\Http\Middleware\JsonApiMediaType

This middleware checks if the Content-Type header matches application/vnd.api+json and throws an UnsupportedMediaTypeHttpException if it doesn't.

Policies

Abseil will guess the policy names for you. By default it will assume that your policies are located here: App\Policies. It is also assumed that policy naming follows this convention: {className}Policy, so for example PagePolicy. This functionality can be completely disabled.

Testing

Abseil actually comes with a tiny and fairly useless test app. Have a look at it to see how all the pieces come together!

$ composer test

Changelog

Please see CHANGELOG for more information on what has changed recently.

Contributing

Please see CONTRIBUTING for details.

Security

If you discover any security related issues, please email boris@shabushabu.eu instead of using the issue tracker.

‼️ Caveats

Abseil is still young and while it is tested, there will probs be bugs. I will try to iron them out as I find them, but until there's a v1 release, expect things to go 💥.

Credits

License

The MIT License (MIT). Please see License File for more information.