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.
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.
| 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) |
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"
| 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.
--fields->nullable() applied when field rule contains nullable->unique() applied when field rule contains standalone uniqueforeignId('relation_id')->constrained('table')->cascadeOnDelete() for every --belongs-to$table->softDeletes() when --soft-deletes is set$table->id() and $table->timestamps()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 setpublic 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
apiResource controller with index, show, store, update, destroyshow, update, and destroystore returns HTTP 201data + meta (current_page, last_page, per_page, total)--searchable fields using ?search=term?sort_by=field&sort_direction=asc?per_page=N (default from --per-page)--fields and --belongs-to foreign keysunique rules automatically modified on update to ignore the current record's IDA 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.
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.',
]);
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
| Context | Generated Rule |
|---|---|
store |
required\|integer\|exists:table,id |
update |
sometimes\|integer\|exists:table,id |
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
All three generated files are driven by publishable stubs. Users can override them to match their own code style without touching the package.
| Stub File | Controls |
|---|---|
stubs/crud.migration.stub |
Migration structure |
stubs/crud.model.stub |
Model structure |
stubs/crud.controller.stub |
Controller structure |
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.
| 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 |
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:
user_id foreign key, soft deletes, and timestamps$fillable, $casts, SoftDeletes, and 3 relationship methodsRoute::apiResource('posts', PostController::class) entry appended to routes/api.phpThe 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.
?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.
| 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.