How To Use And Test Traits In Laravel

Nick Basile • January 31, 2018

Heads up! This post is over a year old, so it might be out of date.

PHP Traits are a fantastic tool for sharing common functionality throughout your PHP classes. At first, this may seem a little intimidating. But, we'll be up and running in no time with a couple of examples.

When Should We Use Traits?

Before we dive into the examples, let's take a moment to discuss when we'd use a Trait. A lot of the time, we'll end up using a Trait when we add in a package. Utilizing a Trait from a package is a standard pattern because it allows package writers to encapsulate useful functionality that we can add to our classes on an as-needed basis.

An excellent example of this is the Spatie laravel-permission package. By adding their HasRoles Trait to our classes, we can then use the Trait's functionality to manage roles.

Additionally, the Laravel framework itself uses Traits to layer functionality and complexity when needed. Notable examples that you might run across include AuthenticatesUsers and Notifiable.

Lastly, while it's great to use Traits provided by others, when should we make one ourselves? Typically, it's a good idea to make one if we find ourselves repeating a lot of functionality across many classes. For example, we could make a Uuidable Trait if a lot of our models are using a UUID.

A less common use-case is to use Traits to break up a God class. But for the most part, there are better ways to do this and Traits aren't recommended unless you're going to be sharing the extracted functionality with many other classes.

Now we can wrap continue to wrap our heads around Traits by going through some examples.

Hello World Example

Let's start with a classic Hello World! example taken right from the PHP docs.

<?php
Trait Hello {
    public function sayHello() {
        echo 'Hello ';
    }
}

Trait World {
    public function sayWorld() {
        echo 'World';
    }
}

class MyHelloWorld {
    use Hello, World;
    public function sayExclamationMark() {
        echo '!';
    }
}

$o = new MyHelloWorld();
$o->sayHello();
$o->sayWorld();
$o->sayExclamationMark();
?>

As we'd expect, this will output:

Hello World!

Let's break this down a bit more. First, we're using a regular, old PHP file as usual. Then we create the Hello Trait. Inside of this Trait, we're defining a method called sayHello(), which simply echoes out 'Hello '.

Next, we've defined another very similar Trait called World. This Trait has a method called sayWorld(), which echoes out 'World'. Below this, we have our class MyHelloworld. This is where things get interesting.

You'll notice that right off the bat we're telling our class to use our Hello and World Traits. This is very important because it's how we let our class know that it should be making use of a particular Trait. Inside our class, we then define a method called sayExclamationMark() that - you guessed it - echoes out '!'.

Finally, we instantiate our class and call the methods that it is inheriting through the Traits as well as its own method. Lo and behold this echoes out 'Hello World!' just like we'd expect.

Now that we're getting the hang of this Trait thing, we can take a look at writing and testing a real-world example.

Making a UUID Trait

What is a UUID?

A Universally Unique Identifier, or UUID, is a 128-bit number that we can use to identify data that looks like this:

ceb580c4-8b8d-4c9c-85c9-5d3c39b6ed9c

Typically, we'll use a UUID if we don't want to expose the id of our data to the public. For example, let's say we're building an invoice app. We wouldn't want our users to be able to see in the route, /invoices/1, that their invoice was the very first one in the system! How secure would you feel knowing that you're this app's guinea pig?

There's also a security concern at play here. If our route were set up like that, then it would be trivial for a malicious user to increment through all of the invoices in the system! Sure, we could add some authorization protection, but it never hurts to be secure at the data-level too.

Now that we know why we'd want to use a UUID, I bet you can see that adding one to a bunch of our models make an excellent use-case for a Trait. So, let's dive in and make one.

Building Our Trait

We'll start with a fresh Laravel project, so go ahead and follow the installation guide to get set up, or run laravel new traits-demo if you have the Laravel installer already. Then you can set up your database however you'd like. If you're unsure how to do that, check out my previous post covering that.

Now, we'll be making use of the excellent ramsey/uuid package to generate our UUIDs, so we can install that now by running composer require ramsey/uuid from our terminal.

To finish our set-up, we can head over to the app directory and create a Traits directory inside of it. Inside of our Traits directory, we can add a PHP file called UuidTrait.php.

We'll be starting off our Trait by adding the namespace, referencing our ramsey/uuid package, and defining our Trait.

<?php

namespace App\Traits;

use Ramsey\Uuid\Uuid;

trait UuidTrait
{

}

Now let's take a moment to think about what our Trait should do. First, we'll need to define the key that we're setting as our UUID. Then, we'll be generating a UUID for the model when it's created. Finally, we'll need to provide a method that can be used if the model needs to override its own boot() method.

We can start by adding a method the defines the field we'll set the UUID on.

trait UuidTrait
{   
    /**
     * Defines the UUID field for the model.
     * @return string
     */
    protected static function uuidField()
    {
        return 'uuid';
    }    
}

This doesn't make a ton of sense just yet, but it'll be handy when we define our other methods because it allows us to dynamically change the field that we're adding as a UUID.

For example, we might have a model that uses a token field instead of uuid. Now, our Trait can handle that case with ease! Let's keep going by adding a boot() method.

trait UuidTrait
{
    /**
     * Defines the UUID field for the model.
     * @return string
     */
    protected static function uuidField()
    {
        return 'uuid';
    }

    /**
     * Generate UUID v4 when creating model.
     */
    protected static function boot()
    {
        parent::boot();

        static::creating(function ($model) {
            $model->{self::uuidField()} = Uuid::uuid4()->toString();
        });
    }
}

Now things are coming together. With this boot() method, we'll be overriding the boot() of all of the models that use this Trait. This is fine as long as we provide that other method I mentioned to handle that case. Back to our code, we can see that we set the model's defined UUID field to a randomly generated UUID when the creating model event is fired.

Finally, we just need a method for when the model overrides it's own boot() method.

trait UuidTrait
{
    ...

    /**
     * Use if boot() is overridden in the model.
     */
    protected static function uuid()
    {
        static::creating(function ($model) {
            $model->{self::uuidField()} = Uuid::uuid4()->toString();
        });
    }
}

As you can see, we're doing the same thing as boot() except we're not overriding the default boot() behavior. And, if we're doing the same thing then it means we can save some duplication! So, after some slight refactors, our final Trait looks like this:

<?php

namespace App\Traits;

use Ramsey\Uuid\Uuid;

trait UuidTrait
{
    /**
     * Generate UUID v4 when creating model.
     */
    protected static function boot()
    {
        parent::boot();

        self::uuid();
    }

    /**
     * Defines the UUID field for the model.
     * @return string
     */
    protected static function uuidField()
    {
        return 'uuid';
    }

    /**
     * Use if boot() is overridden in the model.
     */
    protected static function uuid()
    {
        static::creating(function ($model) {
            $model->{self::uuidField()} = Uuid::uuid4()->toString();
        });
    }
}

Bada bing bada boom, we've got ourselves a nice little UUID Trait. Now, let's put it to work in our User model. At the top of User.php, we can add UuidTrait right next to Notifiable.

<?php

namespace App;

use App\Traits\UuidTrait;
use Illuminate\Notifications\Notifiable;
use Illuminate\Foundation\Auth\User as Authenticatable;

class User extends Authenticatable
{
    use Notifiable, UuidTrait;

    ...
}

And that's all we need to do in our model! Finally, we can add a uuid field to our user migration, like so:

<?php

use Illuminate\Support\Facades\Schema;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Database\Migrations\Migration;

class CreateUsersTable extends Migration
{
    /**
     * Run the migrations.
     *
     * @return void
     */
    public function up()
    {
        Schema::create('users', function (Blueprint $table) {
            $table->increments('id');
            $table->string('name');
            $table->string('email')->unique();
            $table->string('password');
            $table->uuid('uuid');
            $table->rememberToken();
            $table->timestamps();
        });
    }

    ...
}

Now, we can "test" that this is working by running our factory in tinker. To do that let's run php artisan tinker from our command line, and then we can run factory(\App\User::class)->create() to create a user with a UUID.

And there we have it! Our User has a UUID thanks to our trait. Now, let's add in some proper tests with PHPUnit.

Testing Traits

Before we can create our test, we'll need to get set up. To start, we'll add the following two lines to the <php></php> block at the bottom our phpunit.xml file.

<env name="DB_CONNECTION" value="sqlite"/>
<env name="DB_DATABASE" value=":memory:"/>

Next, let's generate our test by running php artisan make:test UuidTraitTest --unit. We used a unit test here because testing a Trait is mostly the same as testing a model. We're interested in verifying that low-level, single-purpose methods are working as expected. So, that checks the unit test box.

Now, one could argue that testing a Trait like this is a bit overkill. I tend to agree, and think that a feature test covering the functionality that the UUID is used for would be more appropriate and easier to maintain. But, if you want to unit test everything, then who am I to stop you? With that said, we're ready to rock and roll.

Testing a Trait is a little tricky because you can't instantiate a Trait on its own. Instead, you need to pick a model that uses it and test if it's acting as expected.

In our case, this is pretty easy because we're only using our User model. But, if you're ever struggling to pick in your own project, I recommend testing it with the simplest model possible to avoid getting overwhelmed and overcomplicating the test. Now, let's write that test!

<?php

namespace Tests\Unit;

use App\User;
use Tests\TestCase;
use Illuminate\Foundation\Testing\WithFaker;
use Illuminate\Foundation\Testing\RefreshDatabase;

class UuidTraitTest extends TestCase
{
    use RefreshDatabase;

    /** @test */
    public function users_have_a_uuid()
    {
        $user = factory(User::class)->create();

        $this->assertTrue(isset($user->uuid));
    }
}

It is as simple as that. We're merely making sure that the uuid field is set when we generate a user. And with that, we're done with our test!

The Wrap-Up

We've covered a lot here today. We learned how to create and use a Trait; and, perhaps more importantly, we wrapped our minds around where and why a Trait would be useful. As we progress through our coding journeys, learning how to do something is undoubtedly valuable. But, we truly become experts when we acquire the experience and knowledge that provides us with the perspective on when and why we should use a given tool like Traits.

Today, I'd like to give a special thank you to my friend Patrick Guevara for introducing me to the UUID Trait and Traits in general. As always, feel free to ask me any questions on Twitter. And until next time, happy coding!

Nick Basile

I craft experiences that help people reach their full potential.

Interested in getting my latest content? Join my mailing list.