Laravel Database Model
Something didn’t quite click with me about Laravel Eloquent Models.
There is nothing in the Model that defines the fields.
The Model defines which database table the data is stored in.
Whatever fields are in the table will be loaded to the Model.
Using Address as an example
In this document I’m assuming
- You already have Laravel installed
- You have Users
- You want to add an Address Model
- Users can have multiple Addresses
- The new table will be called ‘addresses’ the Model will be ‘Address’
I’m going to use a minimal form of address to keep things simple.
Take a look at the documentation for naming Eloquent Models and Tables
Start with the Schema
You can just start with a legacy table or create the schema with SQL commands or using like MysqlAdmin
But then you are forced to pull down a copy of the production database every time you want to test an upgrade - this makes things slower - is a data protection risk and means you are missing out on some great tools Laravel offers.
So I think the best place to start when making a new Model is with the Migration
./artisan make:migration create_addresses_table
Laravel will use the name of the migration to attempt to guess the name of the table and whether or not the migration will be creating a new table.
This should create a stub file that creates the table on up()
and drops it on down()
The file is timestamped - mine was
database/migrations/2022_11_20_151240_create_addresses_table.php
You need to add column definitions
public function up() {
Schema::create('addresses', function (Blueprint $table) {
$table->id();
$table->integer('user_id')->unsigned(); // has to be unsigned
$table->integer('street_number');
$table->string('house name');
$table->string('street');
$table->string('city');
$table
->foreign('user_id')
->references('id')
->on('users');
$table->timestamps();
});
}
Notes that foreign keys must exactly match the referencing key type and auto increment columns are unsigned integers.
and run the migration to create the new table
./artisan migrate:fresh
Define the Model
Start by generating a stub
./artisan make:model Address
which generates the file app/Models/Address.php
NB when you create the model you can make related files like migrations, seeders, tests - and it’s probably best to do iut that way - I’m going one at a time here as I think it makes it clearer what is happening.
and we can check the model
./artisan model:show Address
App\Models\Address ...........................
Database .............................. sqlite
Table .............................. addresses
Attributes ....................... type / cast
id increments, unique .......... integer / int
user_id .............................. integer
street_number ........................ integer
house_name ............................ string
street ................................ string
city .................................. string
created_at nullable ...... datetime / datetime
updated_at nullable ...... datetime / datetime
Relations ....................................
Observers ....................................
Note that there are no fields defined in Models/Address.php - Laravel is picking them up from the database.
Generate Some Data
We have a Schema and a Model - both are in code so we can share this easily with our team and don’t need to share database dumps.
This makes it easy to get in the habit of frequent database refreshes which is awesome for thorough testing and the delivery of a solid product.
What we need next is some data.
Create an address factory so we can get as many addresses as we want.
I’ve seen lot’s of projects get into difficulty because people forgot silly things like implementing pagers which aren’t needed with small amounts of test data - or worse forgetting indexes on database tables - which you only notice with significant amounts of data.
php artisan make:factory Address
Add some fake data with faker
public function definition() {
return [
'user_id' => 1,
'street_number' => $this->faker->randomNumber(2, true),
'house_name' => "",
'street' => $this->faker->streetName(),
'city' => $this->faker->city(),
];
}
Now we can use this in database/seeders/DatabaseSeeder.php
First uncomment the lines to generate test Users
Then add Address generation
The run method should look something like
public function run() {
\App\Models\User::factory(10)->create();
\App\Models\User::factory()->create([
'name' => 'Test User',
'email' => 'test@example.com',
]);
\App\Models\Address::factory()
->count(30)
->create();
}
This creates 10 random users - plus one with a known name - and 30 addresses (all for user 1).
This is the most basic case - I’ll leave Sequences and Relationships another time.
But now I have more than enough data to play with.
Test
Make a test to try out our Model
./artisan make:test Models/AddressTest
public function test_example() {}
// the table exists and has a record with id 1
$this->assertDatabaseHas('addresses', [
'id' => 1,
]);
// Address with ID 1 loads
$address = Address::find(1);
$address->street = 'new street';
$address->save();
$address->refresh();
// can change values in the database
$this->assertEquals($address->street, 'new street');
// Note that it is possible to set invalid fields
$address->fake_field = 2;
// but saving the model would throw an error
// $address->save();
}
And that’s it - we have a new Model with data which we can load and save.
Conclusion
I haven’t touched on relationships or Mass Assignment (which is what fillable
fields are about).
But I hope I’ve explained how to setup a new Model in Laravel - using it and setting up routes is the same as for any other model.
I like the migrations, seeding and test tools in Laravel.
I worry that with such loose field definitions bugs or security issues could easily creep in
When too much magic happens in web development it’s just too easy to overlook things and for the bad guys to use the magic against us.
But it’s great to be so easy to setup with loads of test data - easily shared and refreshed for all the team.