Actions
Motivation
Built in CRUD operations and filtering, Restify allows you to define extra actions for your repositories.
Let's say you have a list of posts and you have to publish them. Usually, for these kind of operations, you have to define a custom route like:
$router->post('posts/publish', PublishPostsController::class);
// PublishPostsController.php
public function __invoke(RestifyRequest $request)
{
//...
}
The classic
approach is good. However, it has a few limitations. First, you have to manually take care of the middleware
route, as the testability for these endpoints should be done separately, which might be hard to maintain. Ultimately, the endpoint is disconnected from the repository, which makes it feel out of context so it has a bad readability.
On that wise, code readability, testability and maintainability may become hard.
Invokable Action Format
The simplest way to define an action is to use the invokable
class format.
Here's an example:
namespace App\Restify\Actions;
use Illuminate\Http\Request;
class PublishPostAction
{
public function __invoke(Request $request)
{
// $request->input(...)
return response()->json([
'message' => 'Post published successfully',
]);
}
}
Then add the action
instance to the repository actions
method:
...
public function actions(RestifyRequest $request): array
{
return [
new PublishPostAction,
];
}
...
Bellow we will see how to define actions in a more advanced way.
Action definition
The action is nothing more than a class that extends the Binaryk\LaravelRestify\Actions\Action
abstract class.
It could be generated by using the following command:
php artisan restify:action PublishPostsAction
This will generate the action class:
namespace App\Restify\Actions;
use Binaryk\LaravelRestify\Actions\Action;
use Binaryk\LaravelRestify\Http\Requests\ActionRequest;
use Illuminate\Http\JsonResponse;
use Illuminate\Support\Collection;
class PublishPostAction extends Action
{
public function handle(ActionRequest $request, Collection $models): JsonResponse
{
return response()->json();
}
}
The $models
argument represents a collection of all the models for this query.
Register action
Then add the action instance to the repository actions
method:
// PostRepository.php
public function actions(RestifyRequest $request): array
{
return [
PublishPostAction::new();
];
}
Authorize action
You can authorize certain actions to be active for specific users:
public function actions(RestifyRequest $request): array
{
return [
PublishPostAction::new()->canSee(function (Request $request) {
return $request->user()->can('publishAnyPost', Post::class);
}),
];
}
Call actions
To call an action, you simply access:
POST: api/restify/posts/actions?action=publish-posts-action
The action
query param value is the ke-bab
form of the filter class name by default, or a custom $uriKey
defined in the action
The payload could be any type of json data. However, if you're using an index-action, you are required to pass the repositories
key, which represents the list of model keys that we apply to this action:
{
"repositories": [1, 2]
}
Handle action
As soon the action is called, the handled method will be invoked with the $request
and a list of models matching the keys passed via repositories
:
public function handle(ActionRequest $request, Collection $models)
{
$models->each->publish();
return ok();
}
Action customizations
Actions can be easily customized.
Action index query
Similarly to repository index query, we can do the same by adding the indexQuery
method on the action:
class PublishPostAction extends Action
{
public static function indexQuery(RestifyRequest $request, $query)
{
$query->whereNotNull('published_at');
}
...
}
This method will be called right before items are retrieved from the database, so you can filter out or eager load using your custom statements.
Custom uri key
Since your class names might change along the way, you can define a $uriKey
property to your actions, so the frontend will always use the same action
query when applying an action:
class PublishPostAction extends Action
{
public static $uriKey = 'publish-posts';
//...
};
Rules
Similarly to advanced filters rules, you could define rules for the action so the payload will get validated before the handle method is fired.
public function rules(): array
{
return [
'active' => ['required', 'bool'],
];
}
Always validate the payload as early as possible in the handle
method:
public function handle(ActionRequest $request, Collection $models)
{
$request->validate($this->rules());
...
}
Actions scope
By default, any action could be used on index as well as on show. However, you can choose to instruct your action to be displayed to a specific scope.
Show actions
Show actions are used when you have to apply them for a single item.
Show action definition
The show action definition is different, in a way it receives arguments for the handle
method.
Restify automatically resolves Eloquent models defined in the route id and passes them to the action's handle method:
// PublishPostAction.php
public function handle(ActionRequest $request, Post $post): JsonResponse
{
}
Show action registration
To register a show action, we have to use the ->onlyOnShow()
accessor:
public function actions(RestifyRequest $request)
{
return [
PublishPostAction::new()->onlyOnShow(),
];
}
Show action call
The post URL should include the key of the model we want Restify to resolve:
POST: api/restfiy/posts/1/actions?action=publish-post-action
The payload could be empty:
{}
List show actions
To get the list of available actions only for a specific model key:
GET: api/api/restify/posts/1/actions
See get available actions for more details.
Index actions
Index actions are used when you have to apply them for a many items.
Index action definition
The index action definition is different in the way it receives arguments for the handle
method.
Restify automatically resolves Eloquent models sent via the repositories
key into the call payload. Then, it passes it to the action's handle method as a collection of items:
// PublishPostAction.php
use Illuminate\Support\Collection;
public function handle(ActionRequest $request, Collection $posts): JsonResponse
{
//
}
Index action registration
To register an index action, we have to use the ->onlyOnIndex()
accessor:
// PostRepository.php
public function actions(RestifyRequest $request)
{
return [
PublishPostsAction::new()->onlyOnIndex(),
];
}
Index action call
The post URL:
POST: api/restfiy/posts/actions?action=publish-posts-action
The payload should always include a key called repositories
, which is an array of model keys or the all
keyword if you want to get them all:
{
"repositories": [1, 2, 3]
}
So Restify will resolve posts with ids in the list of [1, 2, 3]
.
Apply index action for all
You can apply the index action for all the models from the database if you send the payload:
{
"repositories": "all"
}
Restify will get chunks of 200 and send them into the Collection
argument for the handle
method.
You can customize the chunk number by customizing the chunkCount
action property:
// PublishPostAction.php
public static int $chunkCount = 500;
List index actions
To get the list of available actions:
GET: api/api/restify/posts/actions
See get available actions for more details.
Standalone actions
Sometimes, you don't need to have an action with models. Let's say for example the authenticated user wants to disable his/her account.
Standalone action definition:
The index action definition is different, in a way it doesn't require the second argument for the handle
.
// DisableProfileAction.php
public function handle(ActionRequest $request): JsonResponse
{
//
}
Standalone action registration
There are two ways to register the standalone action:
// UserRepository
public function actions(RestifyRequest $request)
{
return [
DisableProfileAction::new()->standalone(),
];
}
Using the ->standalone()
mutator or by overriding the $standalone
action property directly into the action:
class DisableProfileAction extends Action
{
public bool $standalone = true;
//...
}
Standalone action call
To call a standalone action you're using a similar URL as for the index action
POST: api/restfiy/users/actions?action=disable-profile-action
However, you are not required to pass the repositories
payload key.
List standalone actions
Standalone actions will be displayed on both listing show actions or listing index actions.
Filters
You can apply any search, match, filter or eager loadings as for a usual request:
POST: api/api/restify/posts/actions?action=publish-posts-action&id=1&filters=
This will apply the match for the id = 1
and filter
along with the match for the repositories
payload you're
sending.
Action Log
Oftentimes, it is quite useful to view a log of the actions that have been run against a model, or see when the model was updated, deleted or created (and by whom).
Thankfully, Restify makes it a breeze to add an action log to a model by attaching the Binaryk\LaravelRestify\Models\Concerns\HasActionLogs
trait to the repository's corresponding Eloquent model.
Activate logs
By simply adding the HasActionLogs
trait to your model, it will log all actions and CRUD operations into the database into the action_logs
table:
// Post.php
class Post extends Model
{
use \Binaryk\LaravelRestify\Models\Concerns\HasActionLogs;
}
Display logs
You can display them by attaching them to the related repository for example:
// PostRepository.php
use Binaryk\LaravelRestify\Fields\MorphToMany;
use Binaryk\LaravelRestify\Repositories\ActionLogRepository;
public static function related(): array
{
return [
'logs' => MorphToMany::make('actionLogs', ActionLogRepository::class),
];
}
Now you can call the posts with logs api/restify/posts/1?related=logs
, and it will return you the list of actions
performed for posts:
[
{
"id": "1",
"type": "action_logs",
"attributes": {
"user_id": "1",
"name": "Stored",
"actionable_type": "App\\Models\\Post",
"actionable_id": "1",
"status": "finished",
"original": [],
"changes": [],
"exception": ""
}
}
]
Custom logs repository
You can definitely use your own ActionLogRepository
. Just make sure you have it defined into the config:
// config/restify.php
...
'logs' => [
'repository' => MyCustomLogsRepository::class,
],
Get available actions
The frontend that consumes your API could check available actions by using this exposed endpoint:
GET: api/api/restify/posts/actions
This will answer with a json like:
{
"data": {
"name": "Publish Posts Action",
"destructive": false,
"uriKey": "publish-posts-action",
"payload": []
}
}
name
- humanized name of the action
destructive
- you may extend the Binaryk\LaravelRestify\Actions\DestructiveAction
to indicate to the frontend that
this action is destructive (could be used for deletions)
uriKey
- is the key of the action and it will be used to perform the action
payload
- a key / value object indicating required payload defined in the rules
Action class