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
--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
  • 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

A note on sorting. The generated controller passes ?sort_by straight to orderBy(), so any column on the table can be used to order results. If the table holds sensitive columns such as password hashes or tokens, restrict sorting after generating by checking the requested column against an allow-list before applying it. The abstract CrudController does this for you as of v1.5.2.

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.