Skip to content
On this page

Backend Integration with Laravel

Introduction

Spire seamlessly integrates with a default (Laravel 9+) Installation, but there's additional strategies you can use to further benefit from the symbiosis of Spire and Laravel.

On this Page we will go into further Steps you can take.

Form Requests

Each App needs a functioning Validation Layer - especially in the Backend. Spire makes it easy to bring that Backend Validation to your Frontend. The only Prerequisite to this is using Form Requests.

Form Requests have the following benefits when being used:

  • A single, encapsulated place and way to handle all Validation in your App
  • Less bloat in your Controllers / more concise validations
  • Reusability
  • Searchability (You can search the same Term in the Back- and Frontend)

Another Validation Layer

We can actually create a Static Helper Class to aid in re-usable Validations since we need to probably validate something like an email or a zipcode at multiple Points in our Application. We want to make sure that we don't have any issues in our Validation or missed an occurence.

So instead of manually specifying them in multiple places like this:

php
public function rules(): array
{
    return [
        'first_name' => ['required', 'string'],
        'last_name' => ['required', 'string'],
        'email' => ['required', 'string', 'email:rfc,dns'],
    ];
}

we could do this:

php
public function rules(): array
{
    return [
        'first_name' => AppRules::FirstName(),
        'last_name' => AppRules::LastName(),
        'email' => AppRules::Email(),
    ];
}
php
<?php

namespace App\Rules;

class AppRules
{
    public static function Street(): array
    {
        return ['required', 'string', new FirstLetterUppercaseRule];
    }

    public static function StreetNumber(): array
    {
        return ['required', 'string', 'alpha_num'];
    }

    public static function Email(bool $unique = true): array
    {
        $validations = ['required', 'string', 'email:rfc,dns', 'lowercase'];

        if ($unique) {
            $validations[] = 'unique:App\Models\User,email';
        }

        return $validations;
    }
}

By using this methology we can be sure that we never forget to change that one validation at that one endpoint, which ends up mangling the database and propagate.

It seems like a tiny thing, but has big repercussions.

Resources / Collections

Resources represent an additional Layer in the Presentation Logic of your App.

They are incredibly helpful in many ways:

  • Concise, per-action, responses
  • Reusability
  • Can have logic in them, e.g. conditionals based upon certain states.
  • Correct types for your Controllers, which is helpful in lots of ways.
  • Can be extended by their Parent Classes for additional, global logic (like object-based permissions)

You should think about Resources the same way you think about Frontend-Components. They can be nested, they can be reused, and they're normally quite specific.

Why add another Layer?

Multiple Reasons, one of them being the ability to really control what goes out (apart from a Models hidden property). Another being the ability of adding presentational logic to this request only, like a computed.

Examples

You can have multiple Representations of a User:

php
class PublicUserResource extends Resource
{
    public function toArray(Request $request): array
    {
        return [
            'name' => $this->name,
            'zipcode' => $this->zipcode
        ];
    }
}
php
class ProtectedUserResource extends Resource
{
    public function toArray(Request $request): array
    {
        return [
            'name' => $this->name,
            'street' => $this->street . " " . $this->street_nr,
            'ahv_nr' => $this->ahv_nr,
            'zipcode' => $this->zipcode
        ];
    }
}

You can see how this can be helpful, especially if you think about that those Resources can be nested. If your Resource returns a User relation, you can decide in that other Resource that you want to display it as the full-version, or the public version.

INFO

Note how we returned a concatenated version of the street in our ProtectedUserResource. While this is a pretty bad example, it should show you that you can process the data that should be returned quite gracefully.

Resources also have helpful methods for checking the existance of a relation (like an optional) - check the Laravel Documentation for more on this.

Exception Handling

Spire provides an Exception Handler you can use in your Laravel codebase. This Handler (which also integrates with sentry) will render any exception in a format that Spire will understand and can act upon.

INFO

In the future this handler might also add more information so that the Frontend can show Ignition like error pages.

A note on Errors and HTTP Responses

The biggest takeaway is that you should really, really generalize certain things about your API Responses. An AuthorizationException should behave the same way every time it gets thrown. The data should be in the same structure everytime.

This will ensure that you can act on those states gracefully and generally.

Typehinting Controllers

This is an example of a full, restful, entity controller:

php
class TemplateTaskController extends Controller
{
    /**
     * @throws AuthorizationException
     */
    public function all(): TemplateTaskCollection
    {
        $this->authorize('all', TemplateTask::class);

        $collection = TemplateTask::query()
            ->paginate()
            ->withQueryString();

        return new TemplateTaskCollection($collection);
    }

    /**
     * @throws AuthorizationException
     */
    public function get(TemplateTask $templateTask): TemplateTaskResource
    {
        $this->authorize('get', $templateTask);

        return new TemplateTaskResource($templateTask);
    }

    /**
     * @throws AuthorizationException
     */
    public function create(TemplateTaskCreateRequest $request): TemplateTaskResource
    {
        $this->authorize('create', TemplateTask::class);

        $templateTask = TemplateTask::create([
            'template_section_id' => $request->integer('template_section_id'),
            'title' => $request->string('title'),
        ]);

        return new TemplateTaskResource($templateTask);
    }

    /**
     * @throws AuthorizationException
     */
    public function update(TemplateTask $templateTask, TemplateTaskUpdateRequest $request): TemplateTaskResource
    {
        $this->authorize('update', $templateTask);

        $templateTask->update([
            'title' => $request->string('title'),
            'template_section_id' => $request->integer('template_section_id') ?? $templateTask->template_section_id,
        ]);

        return new TemplateTaskResource($templateTask);
    }
    
    /**
     * @throws AuthorizationException
     */
    public function delete(TemplateTask $templateTask): TemplateTaskResource
    {
        $this->authorize('delete', $templateTask);

        $templateTask->delete();

        return new TemplateTaskResource($templateTask);
    }
}

Note on how we can typehint the returntype of the controller Methods with the Resource or Collection that should be returned. This further guards against going against the convention and just adding something to the return value without adding it to the type.

It further also enables some more advanced features and use-cases (like architecture testing and programmatic documentation) and is just generally a no-lose strategy to do.

Tips & Tricks

Polymorphism

Polymorphism has many meanings, for this instance we are talking about Laravels Polymorphic Relationships.

You should definitely read up on them - they can be the easy solution for otherwise quite complex things.

foreignIdFor()

You can actually use the foreignIdFor method in a Migration to auto-generate a correct relation field for the given Class.

php
Schema::create('gardeners', function (Blueprint $table) {
    $table->id();
    $table->foreignIdFor(User::class)->constrained();
    $table->string('phone_number')->nullable();
    $table->timestamps();
});

query()

You should make each call to generate a QueryBuilder instance on a Model explicit.

Laravel uses Magic to enable something like User::where() which most IDE's don't understand without Plugins, because the where method does not exist, it gets proxied by using __callStatic().

So instead of

php
User::where('id', 1)

use this

php
User::query()->where('id', 1)

Macros

Every Facade in Laravel is "Macroable", meaning they can be extended at Runtime.

Example 1

An Example might be this Macro that unifieds restful/crud routes into a single call.

php
Route::macro('crud', function (string $model) {
    $plural = Str::of($model)->plural();
    $implicitKey = Str::of($model)->camel()->toString();

    Route::get("/{$plural}", 'all');
    Route::post("/{$model}", 'create');
    Route::get("/{$model}/{{$implicitKey}}", 'get');
    Route::put("/{$model}/{{$implicitKey}}", 'update');
    Route::delete("/{$model}/{{$implicitKey}}", 'delete');
});

which can then be used in your Route-File:

php
Route::controller(TemplateSectionController::class)->group(function () {
    Route::put('template-section/sort', 'sort');
    Route::crud('template-section');
});

Example 2

Extend a Eloquent-Collection with a Method to automatically track an impression when it's being used.

php
Collection::macro('trackImpressions', function () {
    $listingIds = $this
        ->filter(function ($listing) {
          return $listing instanceof Listing;
        })
        ->map(function ($listing) {
            return $listing->id;
        });

    DB::table('listings')
        ->whereIn('id', $listingIds)
        ->increment('impression_count');

    return $this;
});
php
$listings = Listing::query()
            ->active()
            ->get()
            ->trackImpressions()