Tangible Bytes

A Web Developer’s Blog

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.

generating migrations

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();
  });
}

See available column types

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

Why $request->all() is insecure

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.