Digging Deeper

Artisan Commands

Generate resources from your tables, controllers, policies, actions, metrics and responses, and check perimeters and policies with rest:check.

The package ships generators for each class it uses. The classes they write pass Pint with the laravel preset and Larastan at level 10: they import the classes they use and mark each overriding method with #[\Override].

CommandWrites
rest:quick-startthe resources, controllers, policies and routes of the models you choose
rest:resource PostResource --model=Postapp/Rest/Resources/PostResource.php
rest:controller PostsController --resource=PostResourceapp/Rest/Controllers/PostsController.php
rest:policy PostPolicy --model=Postapp/Policies/PostPolicy.php
rest:action ExtendSubscriptionsapp/Rest/Actions/ExtendSubscriptions.php
rest:instruction OddEvenIdapp/Rest/Instructions/OddEvenId.php
rest:metric OrdersByStatus --type=partition --group-by=statusapp/Rest/Metrics/OrdersByStatus.php
rest:response ArticleResponseapp/Rest/Responses/ArticleResponse.php
rest:base-resource, rest:base-controllerthe abstract base classes app/Rest/Resources/Resource.php and app/Rest/Controllers/Controller.php

rest:check writes nothing: it checks the application.

Every generator keeps an existing class: it prints <Type> already exists. and exits with 0. Pass --force (-f) to replace it.

Quick start

rest:quick-start asks which models to expose, then writes their resources from their tables, their controllers, a policy that allows every ability when Gate finds none, and one route per model behind the auth middleware of the guard, auth:sanctum by default. It checks the result in a new process before it reports success.

php artisan rest:quick-start
php artisan rest:quick-start --no-interaction --model=Post --model=Tag --install-api

See Quick start for every option.

Checking the application

rest:check checks the routes, policies, perimeters and action row checks of your resources. Run it in CI:

php artisan rest:check
php artisan rest:check "App\Rest\Controllers\PostsController" --json
routes and policies ... 14 routes authenticated, 2 policies found
rows ... 1 resource with a perimeter, 1 marked #[WithoutPerimeter]
App\Rest\Resources\CountryResource ... #[WithoutPerimeter]
actions ... 1 action checks each row

Without arguments, it checks every controller that a route uses. It exits with 1 when:

  • a route of a controller has no authentication middleware, or a model has no policy;
  • no route uses a controller of the package;
  • a resource that the routes reach, through their relations too, does not override perimeterQuery(), so every row is visible to users who pass viewAny;
  • an action on the Search or Resources target returns null from authorizeEach(), so no policy ability is checked on each row.
rows ... FAILED
⇂ App\Rest\Resources\JournalResource does not override perimeterQuery(): every row is visible to users who pass viewAny. Override perimeterQuery() to limit the rows, or mark the resource with #[WithoutPerimeter] when every row is meant for these users
  • Override perimeterQuery(), or mark a resource whose rows are meant for all these users, such as countries, with #[Lomkit\Rest\Attributes\WithoutPerimeter]. A subclass inherits the attribute.
  • An action that only reads returns 'view' from authorizeEach().
  • --json prints the report as an object: routes, policies, problems, search, then rows (perimeter, without_perimeter, problems) and actions (checked, problems).
  • rest:quick-start checks the routes and the policies only.

Resources

php artisan rest:resource PostResource --model=Post --generate
  • Without --model, the model is guessed from the class name: PostResource gives App\Models\Post when that class exists.
  • The key field is ID::make() with the key name of the model.
  • rest:resource creates the base resource when it is missing.

Generating the fields and relations

--generate (-G) reads the table of the model and declares one field per column, in the order of the table, then one relation per relation method whose related model has a resource. Run it after the migrations.

PostResource ... 8 fields, 1 relation
Skipped: author: no resource for App\Models\User. Generate one first with php artisan rest:resource UserResource --model=User --generate, then run this command again with --force.
  • A cast gives the field type when one matches: a backed enum gives Enum, the JSON casts give Json, boolean, integer, decimal, float, date and datetime give Boolean, Integer, Number, Date and DateTime. Otherwise the column type decides.
  • The columns that the authentication flow writes are left out, with a note: remember_token (or the getRememberTokenName() of an Authenticatable model), two_factor_secret, two_factor_recovery_codes and two_factor_confirmed_at. password keeps its field. A hidden column that the model does not let fill, when the model declares $fillable or $guarded, is left out with a note too.
  • A nullable column gets nullable(). The timestamps, auto-increment and generated columns get readonly(). A hidden column gets writeOnly().
  • A string column whose length the database reports gets rules('max:<length>'). A column that is not nullable and has no default gets creationRules('required'), and a column with a unique index gets Rule::unique().
  • A generated BelongsTo relation makes its foreign key readonly(), so clients set it through the relation.
  • A MorphTo relation is skipped, with a note that prints the declaration to write by hand. The note names the resources of the morph map models that point back to the relation, through a morphOne(), a morphMany() or a morphToMany() of the same morph name:
Skipped: commentable: MorphTo relations are not generated, declare it in relations() by hand with its targets, for example MorphTo::make('commentable', [PostResource::class, VideoResource::class]).
  • A many-to-many relation with withPivot() columns gets pivotFields([...]).

When the command runs interactively without --generate and the table exists, it asks Generate the fields and relations from the <table> table?. Read the generated fields before you expose the resource.

Controllers

php artisan rest:controller PostsController --resource=PostResource

Without --resource, the resource is guessed: PostsController gives App\Rest\Resources\PostResource. When nothing is found, the $resource line is left out and the command warns: set it before Rest::resource() registers the controller.

Policies

php artisan rest:policy PostPolicy --model=Post

The policy allows every ability to a signed-in user: viewAny(), view(), create(), update() and delete(), plus restore() and forceDelete() for a model that uses SoftDeletes. The user parameter has the type of the user model of the default guard.

When Gate already finds another policy for the model, the command prints The model [App\Models\Post] already has the policy [App\Policies\ContentPolicy]. and writes nothing.

Edit the generated policy before production. See Policies.

Metrics

php artisan rest:metric OrdersByStatus --type=partition --group-by=status
--typecalculate() returns
value$this->count($query), without ranges
trend$this->countByDays($query), with the ranges 7, 30 and 90 days
partition$this->count($query, '<group-by>')
progress$this->count($query, target: 100)
table$this->rows($query, '<group-by>', ['count' => TableColumn::count()])
customa ValueResult, in a class that extends Metric

--group-by is required for partition and table. With --no-interaction, a metric without --type is a value metric. See Metrics.

Actions, instructions and responses

  • rest:action writes an action with handle(ActionFields $fields, Collection $models) and a fields() method that returns []. It has the Search target. See Actions.
  • rest:instruction writes an instruction with handle(InstructionFields $fields, Builder $query). See Scopes and instructions.
  • rest:response writes a response with its map() method. See Custom responses.

Published stubs

The generators read the stubs that php artisan stub:publish copies to stubs/rest.<name>.stub, and fall back to the package stubs. A published stub keeps the placeholders that the generator fills, such as {{ namespace }} and {{ class }}. A missing placeholder throws Lomkit\Rest\Exceptions\InvalidStubException, which names the stub and the placeholder.

rest:metric reads one stub per type: rest.metric-value.stub, rest.metric-trend.stub, rest.metric-partition.stub, rest.metric-progress.stub, rest.metric-table.stub, and rest.metric.stub for custom.