Fields

Field is basically the model attribute representation.

Declaration

Each Field generally extends the Binaryk\LaravelRestify\Fields\Field class from the Laravel Restify. This class ships a fluent API for a variety of mutators, interceptors and validators.

To add a field to a repository, we can simply add it to the repository's fields method. Typically, fields may be created using their static new or make method.

The first argument is always the attribute name, and usually matches the database column.

use Illuminate\Support\Facades\Hash;
use Binaryk\LaravelRestify\Fields\Field;
use Binaryk\LaravelRestify\Http\Requests\RestifyRequest;

public function fields(RestifyRequest $request)
{
    return [
        Field::make('name')->required(),
        
        Field::make('email')->required()->storingRules('unique:users')->messages([
            'required' => 'This field is required.',
        ]),
    ];
}

field helper

Instead of using the Field class, you can use the field helper. For example:

field('email')

Computed field

The second optional argument is a callback or invokable, and it represents the displayable value of the field either in show or index requests.

field('name', fn() => 'John Doe')

The field above will always return the name value as John Doe. The field is still writeable, so you can update or create an entity using it.

Readonly field

If you don't want a field to be writeable you can mark it readonly:

field('title')->readonly()

The readonly accepts a request as well as you can use:

field('title')->readonly(fn($request) => $request->user()->isGuest())

Virtual field

A virtual field, is a field that's computed and readonly.

field('name', fn() => "$this->first_name $this->last_name")->readonly()

Authorization

The Field class provides few methods to authorize certain actions. Each authorization method accept a Closure that should return true or false. The Closure will receive the incoming \Illuminate\Http\Request request.

Can see

Sometimes you may want to hide certain fields from a group of users. You may easily accomplish this by chaining the canSee:

public function fields(RestifyRequest $request)
{
   return [
       field('role_id')->canSee(fn($request) => $request->user()->isAdmin())
   ];
}

Can store

The can store closure:

public function fields(RestifyRequest $request)
{
    return [
        field('role_id')->canStore(fn($request) => $request->user()->isAdmin())
}

Can update

The can update closure:

public function fields(RestifyRequest $request)
{
    return [
        field('role_id')->canUpdate(fn($request) => $request->user()->isAdmin())
}

Validation

There is a gold rule saying - catch the exception as soon as possible on its request way.

Validations are the first bridge of your request information, it would be a good start to validate your input. So you don't have to worry about the payload anymore.

Attaching rules

Validation rules could be adding by chaining the rules method to attach validation rules to the field:

field('email')->rules('required', 'email'),

Of course, if you are leveraging Laravel's support for validation rule objects, you may attach those to resources as well:

Field::new('email')->rules('required', new CustomRule),

Additionally, you may use custom Closure rules to validate your resource fields:

Field::new('email')->rules('required', function($attribute, $value, $fail) {
    if (strtolower($value) !== $value) {
        return $fail('The '.$attribute.' field must be lowercase.');
    }
}),

Considering the required rule is very often used, Restify provides a required() validation helper: field('email')->required()

These rules will be applied for all update and store requests.

Storing Rules

If you would like to define more specific rules that only apply when a resource is being storing, you may use the storingRules method:

Field::new('email')
    ->rules('required', 'email', 'max:255')
    ->storingRules('unique:users,email');

Considering Restify concatenates rules provided by the rules() method, the entire validation for a POST request on this repository will look like this:

$request->validate([
    'email' => ['required', 'email', 'max:255', 'unique:users,email']
]);

Updating Rules

Likewise, if you would like to define rules that only apply when a resource is being updated, you may use the updatingRules method.

Field::new('email')->updatingRules('required', 'email');

Interceptors

Sometimes you may want to take the control over certain Field actions.

That's why the Field class expose a lot of chained methods you can call to configure it.

Fill callback

During the store and update requests, there are two steps before the value from the Request is attached to the model attribute.

Firstly it is retrieved from the application request, and passed to the fillCallback and secondly, the value is passed through the storeCallback or updateCallback:

You may intercept each of those with closures.

Let's start with the fillCallback. It accepts a callable (an invokable class) or a Closure. The callable will receive the Request, the repository model (an empty one for storing and filled one for updating) and the attribute name:

field('title')->fillCallback(function (RestifyRequest $request, Post $model, $attribute) {
    $model->title = strtoupper($request->input('title_from_the_request'));
})

This way you can get anything from the $request and perform any transformations with the value before storing.

Store callback

Another handy interceptor is the storeCallback, this is the step immediately before attaching the value from the request to the model attribute:

This interceptor may be useful for modifying the value passed through the $request.

Field::new('password')->storeCallback(function (RestifyRequest $request) {
    return Hash::make($request->input('password'));
});

Update callback

Likewise, works the updateCallback. Let's use an invokable this time:

Field::new('password')->updateCallback(new PasswordUpdateInvokable);

Where the PasswordUpdateInvokable could be an invokable method:

class PasswordUpdateInvokable 
{
    public function __invoke(Request $request)
    {
        return Hash::make($request->input('password'));
    }
}

Index Callback

Sometimes you may want to transform some attribute from the database right before it is returned to the frontend.

Transform the value for the index request:

Field::new('password')->indexCallback(function ($value) {
    return Hash::make($value);
});

Show callback

Transform the value for the show request:

Field::new('password')->showRequest(function ($value) {
    return Hash::make($value);
});

Fields actionable

Sometime storing attributes might require the stored model before saving it.

For example, say the Post model uses the media library and has the media relationship, that's a list of Media files:

// PostRepository

public function fields(RestifyRequest $request): array
{
    return [
        field('title'),
        
        field('files', 
            fn () => $this->model()->media()->pluck('file_name')
        )
        ->action(new AttachPostFileRestifyAction),
    ];
}

So we have a virtual files field (it's not an actual database column) that uses a computed field to display the list of Post's files names. The ->action() call, accept an instance of a class that extends Binaryk\LaravelRestify\Actions\Action:

class AttachPostFileRestifyAction extends Action
{
    public function handle(RestifyRequest $request, Post $post): void
    {
        $post->addMediaFromRequest('file');
    }
}

The action gets the $request and the current $post model. Say the frontend has to create a post with a file:

const data = new FormData;
data.append('file', blobFile);
data.append('title', 'Post title');

axios.post(`api/restify/posts`, data);

In a single request we're able to create the post and attach file using media library, otherwise it would involve 2 separate requests (post creation and file attaching).

Actionable fields handle store, put, bulk store and bulk update requests.

Fallbacks

Default Stored Value

Usually, there is necessary to store a field as Auth::id(). This field will be automatically populated by Restify if you specify the value value for it:

Field::new('user_id')->value(Auth::id());

or using a closure:

Field::new('user_id')->hidden()->value(function(RestifyRequest $request, $model, $attribute) {
    return $request->user()->id;
});

Default Displayed Value

If you have a field which has null value into the database, however, you want to return a fallback default value for the frontend:

Field::new('description')->default('N/A');

So now, for fields which don't have a description into the database, it will return N/A.

The default value is ONLY used for the READ, not for WRITE requests.

Customizations

Field label

Field label, so you can replace a field attribute spelling when it is returned to the frontend:

Field::new('created_at')->label('sent_at')

Of course if you want to populate this value from a frontend request, you can use the label as a payload key.

Hidden field

Field can be setup as hidden:

Field::new('token')->hidden(); // this will not be visible 

However, you can populate the field value when the entity is stored, by using value:

Field::new('token')->value(Str::random(32))->hidden();

Hooks

After store

You can handle the after field store callback:

Field::new('title')->afterStore(function($value) {
    dump($value);
})

After update

You can handle the after field is updated callback:

Field::new('title')->afterUpdate(function($value, $oldValue) {
    dump($value, $oldValue);
})

File fields

To illustrate the behavior of Restify file upload fields, let's assume our application's users can upload "avatar photos" to their account. So, our users database table will have an avatar column. This column will contain the path to the profile on disk, or, when using a cloud storage provider such as Amazon S3, the profile photo's path within its " bucket".

Defining the field

Next, let's attach the file field to our UserRepository. In this example, we will create the field and instruct it to store the underlying file on the public disk. This disk name should correspond to a disk name in your filesystems configuration file:

use Binaryk\LaravelRestify\Fields\File;

public function fields(RestifyRequest $request)
{
    return [
        File::make('avatar')->disk('public')
    ];
}

You can use field('avatar')->file() instead of File::make('avatar') as well.

How Files Are Stored

When a file is uploaded using this field, Restify will use Laravel's Filesystem integration to store the file on the disk of your choosing with a randomly generated filename. Once the file is stored, Restify will store the relative path to the file in the file field's underlying database column.

To illustrate the default behavior of the File field, let's take a look at an equivalent route that would store the file in the same way:

use Illuminate\Http\Request;

Route::post('/avatar', function (Request $request) {
    $path = $request->avatar->store('/', 'public');

    $request->user()->update([
        'avatar' => $path,
    ]);
});

If you are using the public disk with the local driver, you should run the php artisan storage:link Artisan command to create a symbolic link from public/storage to storage/app/public. To learn more about file storage in Laravel, check out the Laravel file storage documentation.

Image

The Image field behaves exactly like the File field; however, it will instruct Restify to only accept mimetypes of type image/* for it:

Image::make('avatar')->storeAs('avatar.jpg')

Storing Metadata

In addition to storing the path to the file within the storage system, you may also instruct Restify to store the original client filename and its size (in bytes). You may accomplish this using the storeOriginalName and storeSize methods. Each of these methods accept the name of the column you would like to store the file information:

Image::make('avatar')
    ->storeOriginalName('avatar_original')
    ->storeSize('avatar_size')
    ->storeAs('avatar.jpg')

The image above will store the file, with name avatar.jpg in the avatar column, the file original name into avatar_original column and file size in bytes under avatar_size column (only if these columns are fillable on your model).

You can use field('avatar')->image() instead of Image::make('avatar') as well.

Pruning & Deletion

File fields are deletable by default, so considering the following field definition:

File::make('avatar')

You have a request to delete the avatar of the user with the id 1:

DELETE: api/restify/users/1/field/avatar

You can override this behavior by using the deletable method:

File::make('Photo')->disk('public')->deletable(false)

So now the field will do not be deletable anymore.

Customizing File Storage

Previously we learned that, by default, Restify stores the file using the store method of the Illuminate\Http\UploadedFile class. However, you may fully customize this behavior based on your application's needs.

Customizing The Name / Path

If you only need to customize the name or path of the stored file on disk, you may use the path and storeAs methods of the File field:

use Illuminate\Http\Request;

File::make('avatar')
    ->disk('s3')
    ->path($request->user()->id.'-attachments')
    ->storeAs(function (Request $request) {
        return sha1($request->attachment->getClientOriginalName());
    }),

Customizing The Entire Storage Process

However, if you would like to take total control over the file storage logic of a field, you may use the store method. The store method accepts a callable which receives the incoming HTTP request and the model instance associated with the request:

use Illuminate\Http\Request;

File::make('avatar')
    ->store(function (Request $request, $model) {
        return [
            'attachment' => $request->attachment->store('/', 's3'),
            'attachment_name' => $request->attachment->getClientOriginalName(),
            'attachment_size' => $request->attachment->getSize(),
        ];
    }),

As you can see in the example above, the store callback is returning an array of keys and values. These key / value pairs are mapped onto your model instance before it is saved to the database, allowing you to update one or many of the model's database columns after your file is stored.

Storeables

Of course, performing all of your file storage logic within a Closure can cause your resource to become bloated. For that reason, Restify allows you to pass an "Storable" class to the store method:

File::make('avatar')->store(AvatarStore::class),

The storable class should be a simple PHP class and extends the Binaryk\LaravelRestify\Repositories\Storable contract:

<?php

namespace Binaryk\LaravelRestify\Tests\Fixtures\User;

use Binaryk\LaravelRestify\Repositories\Storable;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Http\Request;

class AvatarStore implements Storable
{
    public function handle(Request $request, Model $model, $attribute): array
    {
        return [
            'avatar' => $request->file('avatar')->storeAs('/', 'avatar.jpg', 'customDisk')
        ];
    }
}

Command

You can use the php artisan restify:store AvatarStore command to generate a store file.
Edit this page on GitHub Updated at Thu, Nov 18, 2021