PHP · STEP 4 · MODELS
PHP · Laravel: models and migrations
Point Laravel 13 at Postgres (or keep the default SQLite), generate a model with its migration, seed it, then expose full CRUD through an API resource controller and Route::apiResource.
What you will learn#
- How to set the database settings
- How to create models and migrations
- How to use models to add, update, and delete records
Setup#
Create a new Laravel project:
composer create-project laravel/laravel secondlaravel(orlaravel new secondlaravel)cd secondlaravelEnable the API route file:
php artisan install:api(say yes when it offers to run the migration)Run the dev server with
php artisan serveand check http://localhost:8000 to make sure the project is working.
Setting Up the Database Settings#
Laravel reads its database settings from the .env file in the project root (config/database.php maps them onto connections). A new project is already pointed at an SQLite file, database/database.sqlite, with the built-in migrations run — to stay on SQLite today, skip to the next section.
To use Postgres instead, edit .env: change DB_CONNECTION and uncomment and fill in the DB_* lines below it.
.env
DB_CONNECTION=pgsqlDB_HOST=127.0.0.1DB_PORT=5432DB_DATABASE=secondlaravelDB_USERNAME=postgresDB_PASSWORD=secretMatch the username and password to your local Postgres, and make sure PHP has the pdo_pgsql extension (php -m | grep pgsql; the php-pgsql package on Debian/Ubuntu, included in Herd).
Laravel won't create the database for us, so open the Postgres shell with psql:
Create a new database for the project:
CREATE DATABASE secondlaravel;If you don't know your username and password, create a superuser:
CREATE USER test5 WITH SUPERUSER PASSWORD 'test5';(and put those in.env)Exit psql with
\q
Now run the migrations Laravel ships with against the new database:
php artisan migrateWHY: migrate is how every table gets created from now on; running it here proves the .env settings work before you write code of your own.
Models and Migrations#
Migrations add, change and drop tables; models are the PHP classes that read and write the rows. Much like Rails, Laravel reads the names you give it: create_turtles_table is pre-filled with Schema::create('turtles', ...), and a model called Turtle assumes a table called turtles. Generate both at once (-m is --migration):
php artisan make:model Turtle -mOpen the new file in database/migrations/:
database/migrations/XXXX_XX_XX_XXXXXX_create_turtles_table.php
<?php use Illuminate\Database\Migrations\Migration;use Illuminate\Database\Schema\Blueprint;use Illuminate\Support\Facades\Schema; return new class extends Migration{ /** * Run the migrations. */ public function up(): void { Schema::create('turtles', function (Blueprint $table) { $table->id(); $table->timestamps(); }); } /** * Reverse the migrations. */ public function down(): void { Schema::dropIfExists('turtles'); }};id() is an auto-incrementing primary key; timestamps() adds created_at and updated_at, which Eloquent maintains. Let's add fields to track turtles' names and ages:
public function up(): void { Schema::create('turtles', function (Blueprint $table) { $table->id(); $table->string('name'); $table->integer('age'); $table->timestamps(); }); }Now run the migration and create the table!
php artisan migrateConfirm it with php artisan db:table turtles, or open the database CLI with php artisan db and run \dt. php artisan migrate:rollback undoes the last batch by running down().
Creating A Model#
make:model already created the class that will create, update and delete records in our turtles table:
app/Models/Turtle.php
<?php namespace App\Models; use Illuminate\Database\Eloquent\Model; class Turtle extends Model{ //}Since the model is called Turtle (singular) it automatically connects to the table turtles (plural). Two attributes to add: #[Table] to make the link explicit, and #[Fillable] to say which columns a request may set in one go.
<?php namespace App\Models; use Illuminate\Database\Eloquent\Attributes\Fillable;use Illuminate\Database\Eloquent\Attributes\Table;use Illuminate\Database\Eloquent\Model; // Explicit link to the table (optional here — it matches the convention)#[Table('turtles')]// Columns that create() and update() may fill from an array#[Fillable(['name', 'age'])]class Turtle extends Model{ //}WHY: Eloquent blocks mass assignment by default, so Turtle::create($request->all()) ignores every column until you list the safe ones — which stops a request from setting is_admin => true just because the column exists. Older code writes protected $table = 'turtles'; and protected $fillable = ['name', 'age']; instead; both forms still work.
Seeding Our Database#
To seed the database, use database/seeders/DatabaseSeeder.php: import the model and add some records.
database/seeders/DatabaseSeeder.php
<?php namespace Database\Seeders; use App\Models\Turtle;use Illuminate\Database\Seeder; class DatabaseSeeder extends Seeder{ /** * Seed the application's database. */ public function run(): void { // Create 3 turtles Turtle::create(['name' => 'Dilbert', 'age' => 5]); Turtle::create(['name' => 'Filbert', 'age' => 5]); Turtle::create(['name' => 'Gilbert', 'age' => 5]); $this->command->info('Turtles created!'); }}Run the seed with php artisan db:seed (php artisan migrate:fresh --seed drops everything, re-migrates and seeds in one go — handy while iterating, destructive by design). Confirm in php artisan db with SELECT * FROM turtles;.
Using the Model#
Before writing routes, try the model in Tinker, Laravel's REPL with your project loaded (exit to leave):
php artisan tinkerTurtle::all(),Turtle::find(1)(nullif missing),Turtle::where('age', '>=', 5)->get()Turtle::create(['name' => 'Wilbert', 'age' => 2])$t = Turtle::find(1); $t->age = 6; $t->save();and$t->delete()
Now let's create a controller and build the turtle routes. Laravel generates an API resource controller pre-set for CRUD, with the model wired into the single-record actions:
php artisan make:controller TurtleController --api --model=TurtleThe new controller has stubs for:
index()for a GET request to/api/turtlesstore()for a POST request to/api/turtlesshow()for a GET request to/api/turtles/{turtle}update()for a PUT or PATCH request to/api/turtles/{turtle}destroy()for a DELETE request to/api/turtles/{turtle}
(--api drops the create and edit actions, which only render HTML forms.) Connect it to those routes in one line:
routes/api.php
<?php use App\Http\Controllers\TurtleController;use Illuminate\Http\Request;use Illuminate\Support\Facades\Route; // Five routes: index, store, show, update, destroyRoute::apiResource('turtles', TurtleController::class);php artisan route:list --path=turtles shows the five routes it registered.
Building out our Controller Functions#
The generated file already type-hints the model on show, update and destroy. Because the route parameter is {turtle} and the method asks for Turtle $turtle, Laravel looks the row up for you — route model binding — and answers 404 when the id doesn't exist. Add the two response imports and fill in the actions.
app/Http/Controllers/TurtleController.php
<?php namespace App\Http\Controllers; use App\Models\Turtle;use Illuminate\Http\JsonResponse;use Illuminate\Http\Request;use Illuminate\Http\Response; class TurtleController extends Controller{ // ... actions below go here}index#
public function index(): JsonResponse { return response()->json(Turtle::all()); }- test with a GET request to
/api/turtles
show#
public function show(Turtle $turtle): JsonResponse { // $turtle was loaded by route model binding; a bad id already returned 404 return response()->json($turtle); }- test with a GET request to
/api/turtles/1, then/api/turtles/999
store (create)#
public function store(Request $request): JsonResponse { // validate() returns only the fields listed, or answers 422 with the errors $validated = $request->validate([ 'name' => ['required', 'string', 'max:255'], 'age' => ['required', 'integer', 'min:0'], ]); $turtle = Turtle::create($validated); return response()->json($turtle, 201); }- test with a POST request to
/api/turtleswith a JSON body like{"name": "Wilbert", "age": 2}(headerContent-Type: application/json) - then confirm the new turtle was added with a GET request to
/api/turtles
update#
public function update(Request $request, Turtle $turtle): JsonResponse { // 'sometimes': validate the field only if it was sent, so PATCH can send a subset $validated = $request->validate([ 'name' => ['sometimes', 'required', 'string', 'max:255'], 'age' => ['sometimes', 'required', 'integer', 'min:0'], ]); $turtle->update($validated); return response()->json($turtle); }- test with a PUT request to
/api/turtles/4 - then confirm the turtle was updated with a GET request to
/api/turtles/4
destroy#
public function destroy(Turtle $turtle): Response { $turtle->delete(); // 204 No Content: the standard answer to a successful DELETE return response()->noContent(); }- test with a DELETE request to
/api/turtles/4 - then confirm the turtle was deleted: GET
/api/turtles/4is now a 404
You got it, full CRUD! Returning the affected record (and nothing on delete) is what REST clients expect. Now to the afternoon lab!
Resources to Learn More#
- Migrations — column types, modifiers, altering tables, rollback
- Eloquent: Getting Started — retrieving, inserting, updating, deleting, mass assignment
- Seeding and Eloquent: Relationships —
hasMany,belongsToand friends for tomorrow - Validation — every rule
validate()accepts
See also