API CRUD Generator

Version 1.5.0 introduced the make:crud Artisan command, a full API CRUD scaffold generator. It offers a code generation approach alongside the abstract CrudController base class, giving you production-ready Model, Controller, and Migration files in a single command, fully customisable through publishable stubs.

make:crud — Artisan CRUD Generator

php artisan make:crud {name} [options]

Run the command with your model name and a set of options to instantly scaffold a complete API resource.

Command Options

Option Type Description
name Argument Model name in any case — e.g. Post, blog_post, UserProfile
--fields String Comma-separated field definitions: column:type:validation
--belongs-to Multiple BelongsTo relationship — e.g. --belongs-to=User
--has-many Multiple HasMany relationship — e.g. --has-many=Comment
--has-one Multiple HasOne relationship — e.g. --has-one=Profile
--belongs-to-many Multiple BelongsToMany relationship — e.g. --belongs-to-many=Tag
--soft-deletes Flag Adds SoftDeletes trait to model and softDeletes() column to migration
--searchable String Comma-separated fields to enable ?search= on in the index endpoint
--sortable String Comma-separated fields to allow ?sort_by= on. Defaults to id, the declared --fields, and created_at
--per-page Integer Default pagination size (default: 15)
--register-routes Flag Automatically appends Route::apiResource(...) to routes/api.php
--migrate Flag Runs php artisan migrate immediately after generating the migration
--force Flag Overwrites existing model and controller (migration is never duplicated)

Field Definition Format

Fields are defined as a comma-separated string using the format column:type:validation:

--fields="title:string:required|max:255,body:text:nullable,price:decimal:required|min:0"

Supported Column Types

Input Type Migration Column Model Cast
string, varchar string()
text, longtext, mediumtext text() / longText() / mediumText()
integer, int integer() integer
biginteger, bigint bigInteger() integer
tinyinteger, smallinteger tinyInteger() / smallInteger() integer
float float() float
double double() float
decimal decimal(10, 2) decimal:2
boolean, bool boolean()->default(false) boolean
date date() date
datetime dateTime() datetime
timestamp timestamp() datetime
json json() array
uuid uuid()
enum enum([], [])

The nullable validation rule automatically appends ->nullable() to the migration column.

What Gets Generated

Migration

  • All typed columns from --fields
  • ->nullable() applied when field rule contains nullable
  • ->unique() applied when field rule contains standalone unique
  • foreignId('relation_id')->constrained('table')->cascadeOnDelete() for every --belongs-to
  • $table->softDeletes() when --soft-deletes is set
  • Standard $table->id() and $table->timestamps()

Model

  • Correct namespace, class, and extends Model
  • $fillable populated from --fields plus all foreign key columns from --belongs-to
  • $casts automatically populated for typed fields (boolean, decimal, date, json, etc.)
  • use SoftDeletes trait and import when --soft-deletes is set
  • Typed relationship methods for all four relationship types:
public function user(): \Illuminate\Database\Eloquent\Relations\BelongsTo
public function comments(): \Illuminate\Database\Eloquent\Relations\HasMany
public function profile(): \Illuminate\Database\Eloquent\Relations\HasOne
public function tags(): \Illuminate\Database\Eloquent\Relations\BelongsToMany

Controller

  • Full apiResource controller with index, show, store, update, destroy
  • Route model binding on show, update, and destroy
  • store returns HTTP 201
  • Pagination with data + meta (current_page, last_page, per_page, total)
  • Search across --searchable fields using ?search=term
  • Sort via ?sort_by=field&sort_direction=asc, restricted to an allow-list
  • Configurable ?per_page=N (default from --per-page)
  • Eager loading of all defined relationships on every endpoint
  • Validation rules generated for all --fields and --belongs-to foreign keys
  • unique rules automatically modified on update to ignore the current record's ID

Sorting

As of v1.5.3 the generated index() carries an explicit allow-list, so ?sort_by is honoured only for columns you permit:

$sortable = ['id', 'title', 'body', 'created_at'];

if ($request->filled('sort_by') && in_array($request->input('sort_by'), $sortable, true)) {
    $direction = strtolower($request->input('sort_direction', 'asc')) === 'desc' ? 'desc' : 'asc';
    $query->orderBy($request->input('sort_by'), $direction);
}

A sort_by outside the list is ignored rather than rejected, and sort_direction is normalised to asc or desc. This keeps a caller from ordering by a column they should not be able to observe, such as a password hash, and inferring its value from the row order.

The list defaults to id, your declared --fields, and created_at, with credential-looking columns such as password, api_token, and remember_token left out. Set it yourself with --sortable:

php artisan make:crud Post --fields="title:string,body:text" --sortable="title,created_at"

Because the array is written into the method rather than stored as a class property, you can edit it directly in the generated controller at any time.

Controllers generated before v1.5.3 are unaffected, since the fix applies at generation time. Add an allow-list to their index() methods, or regenerate them with --force.

Smart Validation

Custom Error Messages

For every exists: and unique: rule the generated controller includes a human-readable message:

$request->validate([
    'user_id' => 'required|integer|exists:users,id',
    'slug'    => 'required|unique:posts',
], [
    'user_id.exists'   => 'The selected user does not exist in the users table.',
    'user_id.required' => 'The user field is required.',
    'slug.unique'      => 'This slug has already been taken.',
]);

Update — Unique Ignore

On update, unique rules are automatically adjusted so the current record is excluded from the uniqueness check:

// store
'slug' => 'required|unique:posts'

// update (auto-generated)
'slug' => 'required|unique:posts,slug,' . $post->id

FK Validation Rules

Context Generated Rule
store required\|integer\|exists:table,id
update sometimes\|integer\|exists:table,id

Relationship Table Warnings

After generation, the command checks the database for every related table. If any are missing it tells you exactly which ones and how to fix them — at generation time, not at runtime:

WARN  The following related tables do not exist yet.
      Eager loading and FK validation will fail until their migrations are run:

  Comment → table comments not found
  Run: php artisan make:model Comment -m

  Tag → table tags not found
  Run: php artisan make:model Tag -m

Stub-Based Generation

All three generated files are driven by publishable stubs. Users can override them to match their own code style without touching the package.

Stubs

Stub File Controls
stubs/crud.migration.stub Migration structure
stubs/crud.model.stub Model structure
stubs/crud.controller.stub Controller structure

Publishing Stubs

php artisan vendor:publish --tag=lara-util-x-stubs

Stubs are published to stubs/vendor/lara-util-x/ and are automatically picked up by the command. The package defaults are used as a fallback if no published stubs are found.

--force Behaviour

File Without --force With --force
Migration Skipped if any migration for the table exists Still skipped — migrations are never duplicated
Model Skipped with warning Overwritten
Controller Skipped with warning Overwritten

Full Example

php artisan make:crud Post \
  --fields="title:string:required|max:255,slug:string:required|unique:posts,body:text:required,excerpt:string:nullable,price:decimal:nullable|min:0,is_published:boolean:nullable,published_at:datetime:nullable" \
  --belongs-to=User \
  --has-many=Comment \
  --belongs-to-many=Tag \
  --soft-deletes \
  --searchable="title,body,excerpt" \
  --per-page=10 \
  --register-routes \
  --migrate \
  --force

This single command produces:

  • A migration with 7 typed columns, a user_id foreign key, soft deletes, and timestamps
  • A model with $fillable, $casts, SoftDeletes, and 3 relationship methods
  • A controller with full CRUD, search, sort, pagination, eager loading, and smart validation
  • The Route::apiResource('posts', PostController::class) entry appended to routes/api.php
  • The migration executed immediately

Manual CrudController

The abstract CrudController base class is retained for manual use. Extend it when you would rather configure a controller than generate one:

use LaraUtilX\Http\Controllers\CrudController;

class PostController extends CrudController
{
    protected array $validationRules  = ['title' => 'required|string'];
    protected array $searchableFields = ['title', 'body'];
    protected array $sortableFields   = ['title', 'created_at'];
    protected array $relationships    = ['author'];
    protected int   $perPage          = 15;

    public function __construct(Post $post)
    {
        parent::__construct($post);
    }
}

It exposes getAllRecords, getRecordById, storeRecord, updateRecord, and deleteRecord.

Sorting

?sort_by is honoured only for columns listed in $sortableFields. A value outside that list is ignored rather than rejected, and when the list is empty no sorting is applied at all. This keeps a caller from ordering results by a column they should not be able to observe, such as a password hash, and inferring its value from the row order.

?sort_direction accepts asc or desc; anything else falls back to asc.

Changes in v1.5.2

Change Detail
Sorting restricted sort_by now requires the column to be declared in $sortableFields. Previously any column was accepted.
Delete status code deleteRecord() returns HTTP 200 with a message body. It previously returned 204, which is not permitted to carry a body.

If you relied on sorting by an arbitrary column, add those columns to $sortableFields. If a client special-cased the 204 response, update it to expect 200.