Need help architecting or implementing a REST API? Book a call!

Guide

Models

Learn how to expose models via API.

Setting up Controller

To expose a model through API, first you need to create a controller for it. As you may have seen in the Getting Started - Simple CRUD section, defining a model controller is pretty straightforward.

<?php

namespace App\Http\Controllers\Api;

use App\Models\Post;
use Orion\Http\Controllers\Controller;

class PostsController extends Controller
{
    /**
     * Fully-qualified model class name
     */
    protected $model = Post::class; // or "App\Models\Post"
}

Disabling Pagination

By default, resources returned by the index endpoint are paginated. To disable the pagination and return all resources, you can use DisablePagination trait.

<?php

namespace App\Http\Controllers\Api;

use App\Models\Post;
use Orion\Http\Controllers\Controller;
use Orion\Concerns\DisablePagination;

class PostsController extends Controller
{
    use DisablePagination;

    /**
     * Fully-qualified model class name
     */
    protected $model = Post::class; // or "App\Models\Post"
}
Key takeaways
  • Model controllers always extend Orion\Http\Controllers\Controller
  • $model property is set to a fully qualified model class name
  • Pagination can be disabled for both model and relation resources using DisablePagination trait

Setting up Routes

Once controller is created, it is the time to register routes.

<?php

use Illuminate\Support\Facades\Route;
use Orion\Facades\Orion;
use App\Http\Controllers\PostsController;

Route::group(['as' => 'api.'], function() {
    ...
    Orion::resource('posts', PostsController::class);
    ...
});

Essentially, Orion::resource method is the same as Laravel's default Route::apiResource - it will create multiple routes to handle a variety of actions on the resource.

+--------+-----------+-------------------------------------------------+----------------------------------------+---------------------------------------------------------------------------+-------------------------------------------------+
| Domain | Method    | URI                                             | Name                                   | Action                                                                    | Middleware                                      |
+--------+-----------+-------------------------------------------------+----------------------------------------+---------------------------------------------------------------------------+-------------------------------------------------+
...
|        | GET|HEAD  | api/posts                                       | api.posts.index                        | App\Http\Controllers\Api\PostsController@index                            | api                                             |
|        | POST      | api/posts/search                                | api.posts.search                       | App\Http\Controllers\Api\PostsController@index                            | api                                             |
|        | POST      | api/posts                                       | api.posts.store                        | App\Http\Controllers\Api\PostsController@store                            | api                                             |
|        | GET|HEAD  | api/posts/{post}                                | api.posts.show                         | App\Http\Controllers\Api\PostsController@show                             | api                                             |  
|        | PUT|PATCH | api/posts/{post}                                | api.posts.update                       | App\Http\Controllers\Api\PostsController@update                           | api                                             |
|        | DELETE    | api/posts/{post}                                | api.posts.destroy                      | App\Http\Controllers\Api\PostsController@destroy                          | api                                             |
|        | POST      | api/posts/batch                                 | api.posts.batchStore                   | App\Http\Controllers\Api\PostsController@batchStore                       | api                                             |
|        | PATCH     | api/posts/batch                                 | api.posts.batchUpdate                  | App\Http\Controllers\Api\PostsController@batchUpdate                      | api                                             |
|        | DELETE    | api/posts/batch                                 | api.posts.batchDestroy                 | App\Http\Controllers\Api\PostsController@batchDestroy                     | api                                             |

Soft Deletes

If your model uses SoftDeletes trait and you would like to expose the same functionality via API, call withSoftDeletes method upon resource registration.

<?php

use Illuminate\Support\Facades\Route;
use Orion\Facades\Orion;

Route::group(['as' => 'api.'], function() {
    ...
    Orion::resource('posts', 'Api\PostsController')->withSoftDeletes();
    ...
});

This will introduce restore and batchRestore endpoints. To learn how to permanently delete a resource via API (force delete), take a look at the related Query Parameters section.

+--------+-----------+-------------------------------------------------+----------------------------------------+---------------------------------------------------------------------------+-------------------------------------------------+
| Domain | Method    | URI                                             | Name                                   | Action                                                                    | Middleware                                      |
+--------+-----------+-------------------------------------------------+----------------------------------------+---------------------------------------------------------------------------+-------------------------------------------------+
...
|        | POST      | api/posts/{post}/restore                        | api.posts.restore                      | App\Http\Controllers\Api\PostsController@restore                          | api                                             |
|        | POST      | api/posts/batch/restore                         | api.posts.batchRestore                 | App\Http\Controllers\Api\PostsController@batchRestore                     | api                                             |

Customizing Keys

By default, endpoints use primary key (usually id) to retrieve models from the database. However, in some cases you would want to use a different field to fetch the models while maintaining the primary key as it is. To do that, override the keyName method on the controller:

<?php

namespace App\Http\Controllers\Api;

use App\Models\Post;
use Orion\Http\Controllers\Controller;

class PostsController extends Controller
{
    /**
     * Fully-qualified model class name
     */
    protected $model = Post::class; // or "App\Models\Post"

    /**
     * The name of the field used to fetch a resource from the database.
     *
     * @return string
     */
    protected function keyName(): string
    {
        return 'slug';
    }
}

Customizing Queries

Orion is quite flexible and allows you to redefine how Eloquent queries are build and run for each endpoint.

For Individual Endpoints

Building queries

Let's say you would like the index endpoint to return only published blog posts. To do so, override the buildIndexFetchQuery method on the controller:

<?php

namespace App\Http\Controllers\Api;

use App\Models\Post;
use Orion\Http\Controllers\Controller;

class PostsController extends Controller
{
    /**
     * Fully-qualified model class name
     */
    protected $model = Post::class; // or "App\Models\Post"

    /**
     * Builds Eloquent query for fetching entities in index method.
     *
     * @param Request $request
     * @param array $requestedRelations
     * @return Builder
     */
    protected function buildIndexFetchQuery(Request $request, array $requestedRelations): Builder
    {
        $query = parent::buildIndexFetchQuery($request, $requestedRelations);

        $query->whereNotNull('published_at');

        return $query;
    }
}

Running queries

A common example is selecting only specific columns when fetching a list of models. To do so, override the runIndexFetchQuery method:

<?php

namespace App\Http\Controllers\Api;

use App\Models\Post;
use Orion\Http\Controllers\Controller;

class PostsController extends Controller
{
    /**
     * Fully-qualified model class name
     */
    protected $model = Post::class; // or "App\Models\Post"

    ...

    /**
     * Runs the given query for fetching entities in index method.
     *
     * @param Request $request
     * @param Builder $query
     * @param int $paginationLimit
     * @return LengthAwarePaginator
     */
    protected function runIndexFetchQuery(Request $request, Builder $query, int $paginationLimit): LengthAwarePaginator
    {
        return $query->paginate($paginationLimit, ['id', 'title' 'published_at']);
    }
}

Performing operations

The main purpose of endpoints like index or show is to retrieve data from the database, not change it. But endpoints like store, update, etc. make changes to the database. It is also possible to customize how certain operations, like storing a model, are performed.

In the given example we would force fill the attributes on the post, if the currenthly authenticated user is admin (roles implementation here is imaginary):

<?php

namespace App\Http\Controllers\Api;

use App\Models\Post;
use Orion\Http\Controllers\Controller;

class PostsController extends Controller
{
    /**
     * Fully-qualified model class name
     */
    protected $model = Post::class; // or "App\Models\Post"

    ...

    /**
     * Fills attributes on the given entity and stores it in database.
     *
     * @param Request $request
     * @param Model $entity
     * @param array $attributes
     */
    protected function performStore(Request $request, Model $entity, array $attributes): void
    {
        if ($this->resolveUser()->hasRole('admin')) {
            $entity->forceFill($attributes);
        } else {
            $entity->fill($attributes);
        }
        $entity->save();
    }
}

For a Group of Endpoints

Cool, now index endpoint returns only published posts. But what if you would like to apply the same constraint to show endpoint as well? Sure enough, we could duplicate it and override the buildShowFetchQuery method, but there is a better way.

Building queries

If you take a look at how buildIndexFetchQuery method is implemented, you will notice that it uses buildFetchQuery method. In fact, this method is used by index, show, update, destroy, and restore endpoints to build the query for fetching model(s), and you can override it as well!

Here is how it would look like, if you would like to apply the same constraint to index and show endpoints (and update, destroy, and restore) at once:

<?php

namespace App\Http\Controllers\Api;

use App\Models\Post;
use Orion\Http\Controllers\Controller;

class PostsController extends Controller
{
    /**
     * Fully-qualified model class name
     */
    protected $model = Post::class; // or "App\Models\Post"

    /**
     * Builds Eloquent query for fetching entity(-ies).
     *
     * @param Request $request
     * @param array $requestedRelations
     * @return Builder
     */
    protected function buildFetchQuery(Request $request, array $requestedRelations): Builder
    {
        $query = parent::buildFetchQuery($request, $requestedRelations);

        $query->whereNotNull('published_at');

        return $query;
    }

    ...
}

Running queries

While index endpoint is sort of an exception when it comes to running queries (since we fetch a list of models, not a single model), show, update, destroy, and restore endpoints share the same logic and use runFetchQuery under the hood.

With a bit of refactor we get the controller where only published blog posts are fetched and only id, title, and published_at columns (attributes) are retrieved from the database.

<?php

namespace App\Http\Controllers\Api;

use App\Models\Post;
use Orion\Http\Controllers\Controller;

class PostsController extends Controller
{
    /**
     * Fully-qualified model class name
     */
    protected $model = Post::class; // or "App\Models\Post"

    /**
     * The list of attributes to select from db
     */
    protected $attributes = ['id', 'title' 'published_at'];

    /**
     * Builds Eloquent query for fetching entity(-ies).
     *
     * @param Request $request
     * @param array $requestedRelations
     * @return Builder
     */
    protected function buildFetchQuery(Request $request, array $requestedRelations): Builder
    {
        $query = parent::buildFetchQuery($request, $requestedRelations);

        $query->whereNotNull('published_at');

        return $query;
    }

    /**
     * Runs the given query for fetching entity.
     *
     * @param Request $request
     * @param Builder $query
     * @param int|string $key
     * @return Model
     */
    protected function runFetchQuery(Request $request, Builder $query, $key): Model
    {
        return $query->select($this->attributes)->findOrFail($key);
    }

    /**
     * Runs the given query for fetching entities in index method.
     *
     * @param Request $request
     * @param Builder $query
     * @param int $paginationLimit
     * @return LengthAwarePaginator
     */
    protected function runIndexFetchQuery(Request $request, Builder $query, int $paginationLimit): LengthAwarePaginator
    {
        return $query->paginate($paginationLimit, $this->attributes);
    }

    /**
     * Fills attributes on the given entity and stores it in database.
     *
     * @param Request $request
     * @param Model $post
     * @param array $attributes
     */
    protected function performStore(Request $request, Model $post, array $attributes): void
    {
        if ($this->resolveUser()->hasRole('admin')) {
            $post->forceFill($attributes);
        } else {
            $post->fill($attributes);
        }
        $post->save();
    }
}

Standard Operations Methods

MethodBuildRunPerform
indexbuildIndexFetchQueryrunIndexFetchQuery-
store--performStore
showbuildShowFetchQueryrunShowFetchQuery-
updatebuildUpdateFetchQueryrunUpdateFetchQueryperformUpdate
destroybuildDestroyFetchQueryrunDestroyFetchQueryperformDestroy
restorebuildRestoreFetchQueryrunRestoreFetchQueryperformRestore

Copyright © 2019-2024 Aleksei Zarubin