Laravel – My Journey – Part 2 – Model Relationships

The ‘M’ in MVC

The ‘M’ in MVC stands for model. What is a model? In object-oriented programming, a model is a representation of a real-world object, such as a person, a product, a car, or a list. The representation takes the form of data that is related to the object in question. Not only that, but that model includes rules that relate to the data that represents it.  My nascent Laravel shopping list application will require some models … but which models?

Read Part One of the series

Important Note/Disclaimer
This is not a tutorial, nor an example of best practice. The techniques described in this article are not to be taken as efficient or secure. At the time of writing, I am a relative newcomer to Laravel, object-oriented programming, and the MVC paradigm. I won't be explaining every detail. I don't know all the theory. I'll probably get stuff to work and not know how I did it. I'll probably write things that are clearly untrue or, at best, inaccurate. This is me just finding my way ...

nouns and verbs

Nouns, verbs, models and methods

In Part One, I briefly sketched out my application’s requirements. Here they are again:

  • User registration, login and password reset capabilities
  • A user can create, view, update and delete multiple shopping lists
  • Users can create, view, update and delete multiple products
  • A user can add products to a shopping list they have created
  • Users can remove products from a shopping list they have created
  • Products can be marked as ‘essential’ i.e. to indicate a product that will always be included on a list
  • Shopping lists can be sent via email to the user

When deciding which models are required for an application, it’s useful to identify nouns (people, places, things) and verbs (what those people, places and things can do).

The nouns become the models for the application and the verbs become the methods. With that in mind, I can list my models as:

  • User
  • Shopping List
  • Product

In Laravel, models map to database tables; the properties of models map to the fields in the database table. For example, my ShoppingList model will map to a shopping_lists table in my database. Within that table will be the following properties:

  • A unique identifier (or ID) called ‘id’
  • A foreign key identifier called ‘user_id’ (foreign keys are used to identify related records in different database tables – my shopping list belongs to a user; they are related)
  • A shopping list title called ‘title’
  • A description for the list called ‘description’
  • Timestamps (date/time fields) called ‘created_at’ and ‘updated_at’

Here is a list of the database fields required for this application:

UserShoppingListProduct
ididid
nameuser_iduser_id
emailtitletitle
passworddescriptionessential
remember_tokencreated_atcreated_at
created_atupdated_atupdated_at
updated_at

There is another database table that is required. In Laravel-speak, this is called a pivot table, and rather than storing information about an application’s models, it stores information about the relationship between two models.

The pivot table is called product_shopping_list and has just two fields: product_id and shopping_list_id. A pivot table is required because products and shopping lists are related by a many-to-many relationship. A shopping list can have many products, and a product can belong to many shopping lists.

Read more about many-to-many relationships in Laravel at laravel.com.

Photo illustrating relationship

Here is a brief overview of the relationships that will exist between my application’s three models:

Model relationships:
  • A user can have many shopping lists
  • A user can have many products
  • A shopping list belongs to a single user
  • A product can belong to many shopping lists
  • A product belongs to a single user

In Laravel, these relationships are defined in the models. The actual code is quite readable. For example, here is the code from the Product model which defines it’s relationship (many-to-many) to a shopping list:

// A product belongs to many shopping lists
 public function shoppingLists()
 {
     return $this->belongsToMany(ShoppingList::class);
 }

I can use this relationship to, for example, find out how many shopping lists a product has been used in:

$product->shoppingLists()->count()

Defining the inverse of the relationship is equally straightforward. In the ShoppingList model I define the relationship to the Product model as follows:

// A shopping list belongs to many products 
 public function products()
 {
     return $this->belongsToMany(Product::class);
 }

As you might expect, this relationship can be used to tell me how many products there are in a shopping list:

$shoppingList->products->count()

Rules

Rules

In Part One, I talked about the basic rules (or logic) within which my application will operate. Let’s take a look at a couple of these rules to see how they can be implemented by Laravel.

“A user may not add a product with the same name more than once; the name must be unique… however … a user may add a product with the same name as that added by a different user”

To illustrate:

User 1 adds ‘Ham’ – validation passes as this user has not added ‘Ham’ before.
User 1 adds ‘Ham’ again – validation fails because this user has already added ‘Ham.
User 2 adds ‘Ham’ – validation passes as this user has not previously added ‘Ham’

Seems straightforward, right? I struggled A LOT with this and I still don’t fully understand how I got this to work. My confusion arose partly because the Laravel validation documentation is not especially explicit on certain validation rules … such as the unique rule.

Anyway, below is the commented code from the form request class that handles the validation when a product is added:

public function rules()
{
    // Users should be able to add a product with the same title as that added by another user
    // The validation requires a title unique to the user ...
    // Unique: table, DB column, except (ignore), idColumn, WHERE user_id = id of user (?)
    // select * from `products` where `title` = 'Squash' and `user_id` = 1
    // If this query returns a result, the product/user combination is not unique and will fail
    return [
        'title' => 'required|unique:products,title,NULL,id,user_id,' . auth()->id()
    ];
}

The second issue, one more commonly encountered and documented, is where a user attempts to update a product but doesn’t change the value of the field under unique validation, validation fails because the field being edited already exists in the database.

To illustrate:

User 1 edits the ‘Ham’ product, changing it to ‘essential’ but not changing the title of the product – validation fails because the title of ‘Ham’ already exists in the database.

In order for this to work, the validation rule has to ignore the ID of the record being edited. Here is the validation code from the form request class that handles a product update request:

public function rules()
{
    // Unique: table, DB column, except (ignore)
    // e.g. select * from `products` where `id` != 22 (?)
    return [
        'title' => 'required|unique:products,id,' . $this->product->id
    ];
}

Again, I don’t pretend to understand this completely. I would really like to see the SQL statement generated by this rule (my own example in the code comments is just a guess). If you can shed some light, why not add a comment to this article?

Before finishing off, I want to quickly look at another of my application’s rules:

“If a user deletes a product, it will be permanently removed, including from any lists to which it was associated”

To illustrate:

User 1 deletes the product ‘Ham’ – ‘Ham’ is deleted from the products table and also removed from User 1’s shopping list to which it has been previously added.

This sort of logic (maintaining referential integrity) can be handled at the database level, rather than the application level. Laravel’s migration files can be used to enforce the above logic:

$table->foreign('product_id')->references('id')->on('products')->onDelete('cascade');
$table->foreign('shopping_list_id')->references('id')->on('shopping_lists')->onDelete('cascade');

The above code (which is part of the migration file that defines the product_shopping_list pivot table) ensures that if either a product or shopping list is deleted, the corresponding records in the pivot table are also removed.

That rounds it up for my somewhat chaotic look at models and relationships in my Laravel Shopping List application. Next, I’ll be looking at Controllers – controllers handle requests for data and route it to the applications presentation layer or views .

If you are a Laravel beginner and would like to share your experience with the framework, why not leave a comment below?

Leave a Reply