laravel

Live reloading with Laravel mix and BrowserSync

I was about to record a TailwindCSS video, and this kind of screencast works better if the browser shows you the changes in real-time; otherwise, you have to refresh the page with every change you make – and that’s just annoying.

The first thing that popped into my mind was tailwind play, an online playground where you can try out tailwind stuff. Any change you make will instantly appear on the right.

Then I remembered Laravel mix can do pretty much the same thing with BrowserSync.

Here’s how you can use it when serving a Laravel app with artisan:

mix.browserSync({
  proxy: 'http://127.0.0.1:8000/'
});

And here’s how to do it when using Laravel valet to serve a custom domain:

mix.browserSync({
  proxy: 'https://myapp.test',
  host: 'myapp.test',
  open: 'external',
  https: {
    key: '/Users/yourUser/.config/valet/Certificates/myapp.test.key',
    cert: '/Users/yourUser/.config/valet/Certificates/myapp.test.crt',
  },
});

It’s nowhere near as fast as tailwind play, but it’s good enough when working on entire projects.

The only reasonable, cost-effective way to test validation in Laravel apps

Every time I tell someone how I test validation in Laravel, their reaction is somewhere in the lines of “wait, what? this is so much better. I wish I knew it existed”.

So, yeah, here’s how I test validation in Laravel.

Bellow, we have a bunch of tests asserting that different validation rules are in place when registering a new user. We have required name, required email, email must be a valid email, email must be unique, and so on.

namespace Tests\Feature\Auth;

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

class RegistrationTest extends TestCase
{
    use RefreshDatabase;

    /** @test */
    public function name_is_required()
    {
        $response = $this->postJson(route('register'), [
            'name' => '',

            'email' => 'druc@pinsmile.com',
            'password' => 'password',
            'password_confirmation' => 'password',
            'device_name' => 'iphone'
        ]);

        $response->assertStatus(422);
        $response->assertJsonValidationErrors('name');
    }

    /** @test */
    public function email_is_required()
    {
        $response = $this->postJson(route('register'), [
            'email' => '',

            'name' => 'Constantin Druc',
            'password' => 'password',
            'password_confirmation' => 'password',
            'device_name' => 'iphone'
        ]);

        $response->assertStatus(422);
        $response->assertJsonValidationErrors('email');
    }

    /** @test */
    public function email_must_be_a_valid_email()
    {
        // code
    }

    /** @test */
    public function email_must_unique()
    {
        // code
    }

    // more similar tests
}

What we are doing in every test is sending the correct values except for the field we are testing the validation. This way, we can assert that we receive a 422 response and validation errors for that specific field.

And this is how most people I know test validation – some optimize this further by extracting methods to reduce duplication, but the general idea is to write one test per validation rule.

The thing is, just as production code, test code requires maintenance, and the more tests you have, the slower your test suite becomes.

The slower your test suite becomes, the less often you’ll run it, and the less often you’ll be refactoring and improving code. Not only that, but you will also end up avoiding writing more tests knowing it will slow you down even more.

So while writing tests is crucial, having too many of them can also become a problem. Ideally, you want to have as few tests as possible that run as fast as possible while still being confident enough that everything works.

We pay for confidence with tests

We write tests so we can be confident that our changes don’t break the app. We pay for confidence with tests. The more tests we write, the more confident we are things are working.

But sometimes, we happen to overpay for that confidence. Sometimes we write more tests than we actually need to.

In our example, those tests take about 300ms to run. Let’s say we have an app where 20 requests require validation – that will add up to a cost of about 6s of waiting time and 160 tests – assuming each request has about 8 validation rules. This is the price we pay for the confidence that our requests are validated. 6s and 160 tests.

But there’s a cheaper way to do it. It doesn’t yield as much confidence as what we are doing now, but it is close enough, and it’s much, much cheaper.

Laravel has thorough and exhaustive tests for every validation rule. If I set required as a validation rule, I’m confident that it will work; it will let me know if the field is missing. I don’t need to test that.

What I do need to test is that my request is validated with whatever rules I set in place. But this approach comes with some costs.

The first is, we need to install an additional package: jasonmccreary/laravel-test-assertions – this will provide us with the assertions we need to test that our controller action is validated using the correct form request object.

The second thing is, with this approach, you can no longer use inline validation – it only works with form request objects.

Here’s how it looks:

namespace Tests\Feature\Auth;

use App\Http\Controllers\Auth\RegistrationController;
use App\Http\Requests\RegistrationRequest;
use JMac\Testing\Traits\AdditionalAssertions;
use Tests\TestCase;

class RegistrationRequestTest extends TestCase
{
    use AdditionalAssertions;

    /** @test */
    public function registration_uses_the_correct_form_request()
    {
        $this->assertActionUsesFormRequest(
            RegistrationController::class, 
            'register', 
            RegistrationRequest::class
        );
    }

    /** @test */
    public function registration_request_has_the_correct_rules()
    {
        $this->assertValidationRules([
            'name' => ['required'],
            'email' => ['required', 'email', 'unique:users,email'],
            'password' => ['required', 'min:8', 'confirmed'],
            'device_name' => ['required']
        ], (new RegistrationRequest())->rules());
    }
}

The first test asserts that the RegistrationController@register action uses the RegistrationRequest form request object.

The second test asserts that the RegistrationRequest has the rules we want it to have.

Before, we had to write 8 tests to ensure our register action is validated; now, we only need 2.

Before, our tests needed 300ms to run; now, they only take 80ms. And we can speed this up even more by replacing Laravel’s TestCase with the PHPUnit/Framework/TestCase class. The first one loads the entire Laravel application, and we don’t need that to run these two tests.

namespace Tests\Feature\Auth;

use App\Http\Controllers\Auth\RegistrationController;
use App\Http\Requests\RegistrationRequest;
use JMac\Testing\Traits\AdditionalAssertions;
use PHPUnit\Framework\TestCase; // previously: use Tests\TestCase;

class RegistrationRequestTest extends TestCase
{
    use AdditionalAssertions;

    /** @test */
    public function registration_uses_the_correct_form_request()
    {
		// code
    }

	  // second test
}

Now, these tests only take 9ms to run. That’s over 30 times faster than what we had before.

So while we need to install an additional package and we are limited to only using form request objects for validation, this second approach is much faster. On top of that, it only requires 2 tests instead of one for each validation rule.

That’s how I test validation in Laravel. 2 tests. And they are fast tests.

If you liked this article, consider subscribing to my youtube channel.

4 ways to reduce complexity in your eloquent models

I think everyone loves to work on completely greenfield applications. You get to plan your own course, chose your current favourite technologies, structures, and patterns to follow. There is no legacy code, no technical debt, nothing that stands in your way. You can do whatever you want and building features is a breeze.

But you know the story. You know what happens next.

Your application grows. New requirements come in, and old features need to be changed.

You do your best to keep customers happy, but after a while complexity creeps in and you find yourself in a position where you start doing every possible hack and take every crappy decision to fight your own code into submission.

One of the places our application tends to grow is in our model layer. The usual suspects are the User class and whatever models are part of the core of our application. If you’re building a content management system, Post would be one of the suspects. Selling stuff? Take a look at the Order class.

We’re going to look at ways to deal with complexity in our eloquent models.

Use traits

Traits are the easiest way to slim down an eloquent model. Create a new file, copy and paste a bunch of methods, and BAM! – your model is 100 lines thiner.

The problem with traits is that most of the time you end up sweeping dirt under the rug. You want to cleanup the model but instead of thinking really hard at the problem, you take the trash and sweep it under a trait.

The biggest problem is not the accumulated dirt. Everyone has dirt on their floor at some point. The problem is that, with traits, you won’t really notice it. You’ll look at the room, thinking it’s clean and tidy.

Having your model spread over multiple files makes it a lot harder to identify new concepts and behaviour that can be given representations in your system. You clean and organise things you can see. Code that is not seen is not refactored.

Nevertheless, especially in programming, things are never black and white, and traits are sometimes a good option.

Query builders

One of Eloquent’s nicest features are query scopes. They allow you to pick a common set of constraints, name it, and re-use it through out your application. Let’s take the following example of a Video model:

class Video extends Model
{
    public function scopeDraft($query)
    {
        return $query->where('published', false);
    }

    public function scopePublished($query)
    {
        return $query->where('published', true);
    }

    public function scopeLive($query)
    {
        return $query->published()
            ->where('published_at', '<=', Carbon::now());
    }
}

Once the list of scope methods starts to get in our way we can move them into a dedicated query builder class like bellow. Notice that we no longer need the scope prefix, nor to pass in the $query parameter as we are now in a query builder context.

class VideoQueryBuilder extends Builder
{
    public function draft()
    {
        return $this->where('published', false);
    }

    public function published()
    {
        return $this->where('published', true);
    }

    public function live()
    {
        return $this->published()
            ->where('published_at', '<=', Carbon::now());
    }
}

To replace the default query builder with our enhanced one, we override the newEloquentBuilder method in our Video model like bellow:

class Video extends Model
{
    public function newEloquentBuilder($query)
    {
        return new VideoQueryBuilder($query);
    }
}

Move event listeners to observer classes

When models are short and easy to go through, I like to keep the event listeners right in the boot method so I don’t need to switch to a second file to figure out what happens when. But, when the model starts growing and growing, moving the event listeners into their own observer class is a good trade-off.

protected static function boot()
{
    parent::boot();

    self::saved(function(BodyState $bodyState) {
        $bodyState->update([
            'macros_id' => $this->user->latestMacros->id
        ]);
    });

    self::created(function(BodyState $bodyState) {
        if ($bodyState->user->logBodyStateNotification) {
            $bodyState->user->logBodyStateNotification->markAsRead();
        }
    });
}

Bonus tip 1: Instead of a having a bunch of CRUD calls, when possible, make your code as expressive as possible.

class BodyStateObserver
{
    public function saved(BodyState $bodyState)
    {
        $bodyState->associateWithLatestMacros();
    }

    public function created(BodyState $bodyState)
    {
        $bodyState->user->markLogBodyStateNotificationsAsRead();
    }
}

Bonus tip 2: Instead of hiding your observers in a provider class, register them in the model’s boot method. Not only you’ll know that observer exists, but you’ll also be able to quickly navigate to it from your model.

class BodyState extends Model
{
    protected static function boot()
    {
        parent::boot();
        self::observe(BodyStateObserver::class);
    }
}

Value Objects

When you notice two or more things that seem to always go together, for example street name and street number, start date and end date, you have an opportunity to extract and represent them into a single concept. In our case Address and DateRange.

Another way to detect this kind of objects is to look for methods that “play” a lot with one of your model’s attributes. In the example bellow it seems there are quite a few things we do with price (in cents) of the product.

class Product
{
    public function getPrice()
    {
       return $this->price;
    }

    public function getPriceInDollars()
    {
       return $this->price / 100;
    }

    public function getPriceDisplay()
    {
       return (new NumberFormatter( 'en_US', NumberFormatter::CURRENCY ))
          ->formatCurrency($this->getPriceInDollars(), "USD");
    }
}

We can extract these methods into a Price class.

class Price
{
    public function __constructor($cents)
    {
        $this->cents = $cents;
    }

    public function inDollars()
    {
        $this->cents / 100;
    }

    public function getDisplay()
    {
       return (new NumberFormatter('en_US', NumberFormatter::CURRENCY))
          ->formatCurrency($this->getDollars(), "USD");
    }
}

Remove the previous methods and return the Price class.

class Product
{
    public function getPrice()
    {
        return new Price($this->price);
    }
}

There are many other ways of putting your models on a diet (service objects, form objects, decorators, view objects, policies and others), but the ones I shared above are the ones I tend to reach out the most when my models need to lose some weight. I hope it makes a good diet for your models too 🙂

InertiaJS infinite scrolling example

I just published a new video on how to do infinite scrolling in an InertiaJS and Laravel application – using a twitter-like feed as an example.

Infinite scrolling with InertiaJs and Laravel

The gist of it is:

  1. Setup a listener for the scroll event
  2. Inside the listener calculate the remaining pixels until the bottom of the page so you can make additional requests to load new items before the user hits the bottom of the page.
  3. Make sure you use lodash’s debounce method to avoid making the same request multiple times.
  4. Use axios or fetch or anything else to make a regular xhr request to the server.
  5. Make sure the server endpoint knows when to return an inertia response and when to return a regular json response.
  6. Put your items in a local state property so you can add the new ones when you make the additional requests. The reason for this is because VueJs props shouldn’t be mutated.

Backend snippet:

public function index(User $user, Request $request)
{
    $tweets = $user->tweets()->with('user')->paginate();

    if ($request->wantsJson()) {
        return $tweets;
    }

    return Inertia::render('UserTweets', [
        'user' => $user,
        'tweets' => $tweets
    ]);
}

Scroll event listener snippet:

import AppLayout from './../Layouts/AppLayout'
import {debounce} from "lodash/function";

export default {
  props: {
    user: Object,
    tweets: Object
  },
  data() {
    return {
      userTweets: this.tweets
    }
  },
  components: {
    AppLayout,
  },
  mounted() {
    window.addEventListener('scroll', debounce((e) => {
      let pixelsFromBottom = document.documentElement.offsetHeight - document.documentElement.scrollTop - window.innerHeight;

      if (pixelsFromBottom < 200) {
        axios.get(this.userTweets.next_page_url).then(response => {
          this.userTweets = {
            ...response.data,
            data: [...this.userTweets.data, ...response.data.data]
          }
        });
      }
    }, 100));
  }
}