Getting to Know the Laravel HTTP Client

Laravel 7 has a new HTTP client that provides a minimal API around the popular Guzzle HTTP client. If you missed the news, let’s walk through some of the basics. You can find everything you need to know in the Laravel 7 HTTP Client Documentation, but lets look at some of those features at a glance.

Guzzle is a powerful HTTP client, but the 80% use-case feels complicated when trying to make a simple HTTP GET or getting data back from a JSON API.

Highlight Features

At a glance, here are my favorite features the Laravel Http client provides:

  • Chain-able Request setup
  • Easy access to JSON response data
  • No boilerplate setup to make a simple request
  • Retrying failed requests
  • Convenience methods around authentication headers (basic, digest, bearer)
  • Test fakes and test inspections

For more complicated HTTP client work, you may have to use Guzzle directly, but I suspect that Laravel HTTP has everything that you’d need for most of your projects.

Basic Usage

Here’s the most basic usage you can expect using Laravel’s HTTP client:

use Illuminate\Support\Facades\Http;

$response = Http::get('https://laravel.com');

// Get the response body
$response->body();

$response = Http::get( 'https://api.github.com/users/paulredmond/gists' );

// Array of data from the JSON response
$data = $response->json();

Accessing Data

There are a few ways you can access data from JSON responses: the json() method and ArrayAccess:

// Access the response data via `json()`
$data = $response->json();
$data['username']; 

// Array access from the Response object
Http::get( 'https://api.github.com/users/paulredmond/gists' )['username'];

// Return the response object and access directly
$response = Http::get('...');
$response['username'];

Error Handling

One thing that can get confusing and complicated with Guzzle is continually having to wrap calls in a try/catch block, or creating a mini-abstraction on every project to handle this for you.

Laravel catches 400 and 500 level responses and provides methods you can use to determine if anything bad happened during the response:

$response = Http::get( 'https://api.github.com/users/paulredmond/gists' );

// Boolean checks on the response
$response->ok() : bool;
$response->clientError(): bool;
$response->successful() : bool;
$response->serverError() : bool;
$response->clientError() : bool;

Timeouts

After reading that Laravel handles client and server exceptions, it wasn’t obvious to me at first that timeouts will cause an exception. The default timeout is rather long; however, Laravel HTTP allows you to define it yourself (in seconds) with the convenient timeout() method:

$response = Http::timeout(5)
    ->get( 'https://ffdsafdsafdsafas.com' );

// Illuminate\Http\Client\ConnectionException:
// cURL error 28: Connection timed out after 5005 milliseconds

Learn More

You can learn most everything you need to know from the Laravel 7 HTTP Client Documentation. I’d also encourage you to check out the classes in Illuminate/Http—its quite a simple, elegant API which feels intuitive to use.

5 Ways of Battling Form Spam

Once you’ve created an application sooner or later you are going to get hit with a spam bot. It’s inevitable.

On one of my projects the registration form got hit and I ended up with 17,000+ fake users and it was a pain to clean up. As part of the process, I asked on Twitter how other people are fighting bots and I had a huge response with a lot of great ideas. I wanted to share all the ones mentioned so that if you ever get hit you’ll know how to fight back.

1. Cloudflare

If you are using a service like Cloudflare you can utilize their “page rules” set up to lock the URL with form submissions down.

I do not believe this will work as well as the others, but it’s quick to implement if you are already using the service and it shouldn’t hurt anything.

2. Honeypot

Honeypots are a first-line defense and are pretty easy to setup. The way they work in this context is you add a hidden form input that should never have a value, then when the form is submitted if that hidden field has a value you know it’s probably from a bot.

You can implement this yourself but if packages like spatie/laravel-honeypot exist to make integration a breeze.

Just remember if you are making your own name the input fields something that seems legitimate, that way the bots think it’s a field that should be filled out.

3. Validate Emails

If your form requires an email, like for new user registration then you could verify that the email actually exists and that they click a button in the email to confirm registration. This option is how a lot of newsletters work including our weekly Laravel Newsletter.

You enter your email. The service we use sends you an email. You click confirm. Then you are a subscriber.

This helps ensure they really wanted to signup and are not a bot, or someone signing up an enemy.

Another option that goes along with this is a service like identibyte that will verify an email through their API. This saves the hassle of making your users go through extra steps.

4. Captcha

This is my least favorite option because I really hate captchas. Google has invisible reCaptcha which is probably the best captcha option.

5. Dedicated Spam Services

The two primary spam services are Akismet by WordPress and Stop Forum Spam. Akismet is paid and Stop Forum Spam is free.

I’ve used Akismet extensively on every WordPress site I run, but you can use it with any app including Laravel. You’ll just need to tap into their API or use an existing page like nickurt/laravel-akismet.

Stop Forum Spam is a free service that sounds similar to Akismet and Laravel packages like nickurt/laravel-stopforumspam exist to help with the integration.

***

Out of all these options I’d recommend start with the honeypot, then if that fails, continue to the other options until you’ve stopped the bots from attacking your forms. Just remember what works today, may not work tomorrow. So it’s always going to be a battle.

Testing API Validation Errors With Different Locales

Have you ever wondered how to provide API validation errors and translations for different locales? This beginner post will show you how to get started with setting a locale in a stateless Laravel API and testing various locale validation messages.

On a related note, if you want a quick way to pull translations into your project for validation, we’ve covered a package called Laravel-lang – Translations for Laravel. Larvel-lang has translations for over 68 languages and including translations for things like authentication and validation (among other things).

For now we’ll skip installing this package and get started with the built-in features that Laravel provides for Locale support.

Getting Started

First, we’re going to create a new Laravel project to get started. This tutorial isn’t going to leverage any Locale packages.

laravel new locale-api
cd locale-api

Next, let’s create the files we’ll use to demonstrate working with Locale in stateless APIs:

php artisan make:middleware DetectLocale
php artisan make:controller ProfileController
php artisan make:test LocaleDetectionTest

mkdir -p resources/lang/es/
touch resources/lang/es/validation.php

We will set the API Locale through a middleware, which will try to set a preferred locale. We create a controller and test so that we can demonstrate some validation tests we can perform with our API.

Last, we are creating a new validation file for Spanish to demonstrate the Localization of our API validation errors.

We’re not going to install the full Laravel-lang package, but for this tutorial, grab the contents of es/validation.php and add it to the resources/lang/es/validation.php file we created above.

The Middleware

Though I typically use TDD, we’re going to create the middleware and controller first, and then create a test as a means of using our middleware. The DetectLocale middleware is simple for our purposes since APIs are stateless; we will rely on the user sending an Accept-Locale header along with the request. You could also check for a query string param to set the locale, but we’ll keep it simple:

<?php

namespace App\Http\Middleware;

use Closure;

class DetectLocale
{
    /**
     * Handle an incoming request.
     *
     * @param \Illuminate\Http\Request $request
     * @param \Closure $next
     * @param mixed ...$locales
     * @return mixed
     */
    public function handle($request, Closure $next, ...$locales)
    {
        $locales = $locales ?: ['en'];

        if ($language = $request->getPreferredLanguage($locales)) {
            app()->setLocale($language);
        }

        return $next($request);
    }
}

Our middleware takes a $locales configuration, which is an array of supported languages. We then call the super-convenient Request::getPreferredLanguage() method provided by the Symfony HTTP Foundation component, which takes an ordered array of available locales and returns the preferred locale.

If the method returns a preferred locale, we set it via Laravel’s App::setLocale() method.

Next, we configure the middleware in the app/Http/Kernel.php file:

protected $routeMiddleware = [
    // ...
    'locale' => \App\Http\Middleware\DetectLocale::class,
];

Last, in the same file we add the locale middleware to the api group with the preferred locales:

protected $middlewareGroups = [
    // ...
    'api' => [
        'throttle:60,1',
        'bindings',
        'locale:en,es',
    ],
];

The Controller

We’re not going to write out a full controller; we are only going to create a simple validation check to trigger some validation rules:

<?php

namespace App\Http\Controllers;

use Illuminate\Http\Request;

class ProfileController extends Controller
{
    public function store(Request $request)
    {
        $request->validate([
            'name' => 'required',
            'email' => 'required|email'
        ]);

        return response()->json(['status' => 'OK']);
    }
}

This controller is going to validate name and email inputs, which will throw an exception if validation fails, returning a 422 Unprocessable Entity statue code and JSON validation errors.

Next, we need to define the route in routes/api.php:

<?php

Route::post('/profile', 'ProfileController@store');

Next, we’ll use the example test case in the feature test we created in LocaleDetectionTest:

<?php

namespace Tests\Feature;

use Illuminate\Http\Response;
use Tests\TestCase;

class LocaleDetectionTest extends TestCase
{
    /**
     * A basic feature test example.
     *
     * @return void
     */
    public function testExample()
    {
        $response = $this
            ->withHeaders([
                'Accept-Language' => 'es',
            ])
            ->postJson('/api/profile', [
                'email' => 'invalid.email',
            ]);

        $response
            ->assertStatus(Response::HTTP_UNPROCESSABLE_ENTITY)
            ->assertJsonPath('errors.email', ['correo electrónico no es un correo válido'])
            ->assertJsonPath('errors.name', ['El campo nombre es obligatorio.']);
    }
}

Our test makes a JSON post request to our profile endpoint and sends the Accept-Language header with es for Spanish. As you can see by running the tests, they pass when we assert the JSON returned using the newly contributed assertJsonPath() added in Laravel 6.0.4.

If you dump out the JSON as an array, you can see that the message is in English:

dd($response->decodeResponseJson());
..array:2 [
  "message" => "The given data was invalid."
  "errors" => array:2 [
    "name" => array:1 [
      0 => "El campo nombre es obligatorio."
    ]
    "email" => array:1 [
      0 => "correo electrónico no es un correo válido"
    ]
  ]
]

We won’t dive into solving this in this tutorial, but perhaps we will write a follow-up going more into detail in translating other parts of an API. A simple solution might be to catch ValidationException messages and use a translation for the message.

Another thing to consider is experimenting with what happens when you send a language that you don’t support. I’ll give you a hint: Symonfy’s getPreferredLanguage() will return the first preferred language that you pass it (in our case, en, which is first in the array). Play around with changing the order of preferred locales and testing with the use of an unsupported locale.

Well, that’s it for our brief walk-through of a simple way to set locale in a Laravel API. I’d like to credit Yaz Jallad (@ninjaparade), who was discussing this topic with me yesterday. From our discussion I was curious about how to add validation translations to an API—which I’ve never had to do before—and figured I’d write a quick post after digging in for a bit.

To learn more about Localization in Laravel, check out the Localization documentation.

Eloquent Subquery Enhancements in Laravel 6.0

If you’ve been following my work for any length of time, you know that I am a big fan of pushing more work in our Laravel applications to the database layer. By doing more work in the database we can often reduce the number of database queries we make, reduce the amount of memory our applications use, and reduce the amount of time required by Eloquent to process our models. This can result in some pretty significant performance wins.

One excellent way to push more work to the database is by using subqueries. Subqueries allow you to run nested queries within another database query. This can be a powerful way to retrieve ancillary model data, without making any additional database queries, when it’s not possible to do via a relationship. You can also use subqueries in order by statements, where statements, and other database clauses.

During my Laracon US 2019 talk, I made reference to a couple of query builder macros I’ve been using that make it easier to use subqueries in Laravel. I’ve since submitted three pull requests to Laravel to add these to the core framework.

Here’s an overview of each:

“Select” subqueries

Pull request #29567 adds support for subqueries to both the select() and addSelect() query builder methods.

For example, let’s imagine that we have a table of flight destinations and a table of flights to destinations. The flights table contains an arrived_at column which indicates when the flight arrived at the destination.

Using the new subquery select functionality in Laravel 6.0, we can select all of the destinations and the name of the flight that most recently arrived at that destination using a single query:

return Destination::addSelect(['last_flight' => Flight::select('name')
    ->whereColumn('destination_id', 'destinations.id')
    ->orderBy('arrived_at', 'desc')
    ->limit(1)
])->get();

Notice how we’re using Eloquent to generate the subquery here. This makes for a nice, expressive syntax. That said, you can also do this using the query builder:

return Destination::addSelect(['last_flight' => function ($query) {
    $query->select('name')
        ->from('flights')
        ->whereColumn('destination_id', 'destinations.id')
        ->orderBy('arrived_at', 'desc')
        ->limit(1);
}])->get();

“Order by” subqueries

In addition, pull request #29563 makes it possible to use subqueries in the query builder’s orderBy() method. Continuing our example above, we can use this to sort the destinations based on when the last flight arrived at that destination.

return Destination::orderByDesc(
    Flight::select('arrived_at')
        ->whereColumn('destination_id', 'destinations.id')
        ->orderBy('arrived_at', 'desc')
        ->limit(1)
)->get();

As with selects, you can also use the query builder directly to create the subquery. For example, maybe you want to order users based on their last login date:

return User::orderBy(function ($query) {
    $query->select('created_at')
        ->from('logins')
        ->whereColumn('user_id', 'users.id')
        ->latest()
        ->limit(1);
})->get();

“From” subqueries

Finally, pull request #29602 makes it possible to use subqueries within the query builder’s from() method. These are sometimes called derived tables.

For example, maybe you want to calculate the average total donations made by users in your application. However, in SQL it’s not possible to nest aggregate functions:

AVG(SUM(amount))

Instead, we can use a from subquery to calculate this:

return DB::table(function ($query) {
    $query->selectRaw('sum(amount) as total')
        ->from('donations')
        ->groupBy('user_id');
}, 'donations')->avg('total');

You probably won’t need to use this every day, but when you do need it, it’s indispensable.

One breaking change to be aware of if you’re using Eloquent outside of Laravel is a signature change to the table() method on the Illuminate/Database/Capsule/Manager object. It’s been changed from table($table, $connection = null) to table($table, $as = null, $connection = null).

Learn more

If you’re interested in learning more about subqueries and other advanced database techniques, be sure to follow my blog, and also watch my Laracon US 2019 talk.

At Laracon I also announced a new video course I’m working on called Eloquent Performance Patterns. My goal with this course is to teach Laravel developers how to drastically improve the performance of their Laravel applications by pushing more work to the database layer, all while still using the Eloquent ORM. Be sure to join the mailing list for that if you’re interested!

Testing Streamed Responses in Laravel

Laravel provides a convenient API to generate an HTTP response that forces the user’s browser to download the file for a given URL. When writing a feature that includes file downloads in your application, Laravel provides a pleasant testing experience to make writing and testing downloadable files a breeze.

Let’s go over a hands-on example of creating and testing a downloadable URL in a Laravel app.

Introduction

In this tutorial, we are going to build a quick users export to CSV feature that allows a user to download a CSV export of all users in the database. To force a user download, the Laravel has a download method that accepts a path to a file (as noted in the response documentation):

// Download response for a filesystem file
return response()->download($pathToFile, $name, $headers);

If you are providing an export of customers, users, or another database record the response()->streamDownload() is really awesome:

return response()->streamDownload(function () {
    echo GitHub::api('repo')
                ->contents()
                ->readme('laravel', 'laravel')['contents'];
}, 'laravel-readme.md');

This tutorial is a quick demo of how to write and test controllers responding with a streamed file response. I want to note, that in a real application you should provide some security mechanism around exporting users based on your application’s business rules.

Setting Up the Application

First, we need to create a new Laravel application and scaffold the authentication:

laravel new testing-stream-response
cd testing-stream-response
php artisan make:auth

Next, let’s configure a SQLite database for our testing environment. This tutorial will use tests to drive out the feature and you are free to configure any type of database to try out the application in the browser.

Open the phpunit.xml file in the root of the project and add the following environment variables:

<php>
    <!-- ... -->
    <server name="DB_CONNECTION" value="sqlite"/>
    <server name="DB_DATABASE" value=":memory:"/>
</php>

We scaffolded a quick Laravel application and generated the authentication files needed to protect our user export route. We’re ready to start testing our export and writing the controller.

Writing the Test

We’ll drive this feature out with a PHPUnit feature test and build the controller logic as we go.

First, we’ll create a new feature test:

php artisan make:test UsersExportTest

Inside this file, we’ll scaffold out the first test, which is the expectation that guests are redirected to the login URL when trying to access /users/export.

<?php

namespace Tests\Feature;

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

class UsersExportTest extends TestCase
{
    /** @test */
    public function guests_cannot_download_users_export()
    {
        $this->get('/users/export')
            ->assertStatus(302)
            ->assertLocation('/login');
    }
}

We are requesting the export endpoint and expecting a redirect response to the login page.

We haven’t even defined the route, so this test will fail when we run the test:

$ phpunit --filter=guests_cannot_download_users_export
PHPUnit 7.5.9 by Sebastian Bergmann and contributors.

F                                                                   1 / 1 (100%)

Time: 113 ms, Memory: 14.00 MB

There was 1 failure:

1) Tests\Feature\UsersExportTest::guests_cannot_download_users_export
Expected status code 302 but received 404.
Failed asserting that false is true.

We haven’t defined a route, so let’s do so now in the routes/web.php file:

Route::get('/users/export', 'UsersExportController');

Rerunning the test brings us to the next failure that we need to fix:

phpunit --filter=guests_cannot_download_users_export
PHPUnit 7.5.9 by Sebastian Bergmann and contributors.

E                                                                   1 / 1 (100%)

Time: 96 ms, Memory: 12.00 MB

There was 1 error:

1) Tests\Feature\UsersExportTest::guests_cannot_download_users_export
UnexpectedValueException: Invalid route action: [App\Http\Controllers\UsersExportController].

Next, generate the Controller as an invokable action. I find that for file exports I tend to reach for an invokable controller because I want to be explicit with the intention of this controller outside my typical RESTful routes.

Before running this command, you need to comment out the route we added to the routes/web.php file:

// Route::get('/users/export', 'UsersExportController');

Now you can create a new invokable controller with the artisan command:

php artisan make:controller -i UsersExportController

Uncomment the route we added and rerun the tests to get the next test failure:

phpunit --filter=guests_cannot_download_users_export
PHPUnit 7.5.9 by Sebastian Bergmann and contributors.

F                                                                   1 / 1 (100%)

Time: 122 ms, Memory: 14.00 MB

There was 1 failure:

1) Tests\Feature\UsersExportTest::guests_cannot_download_users_export
Expected status code 302 but received 200.

The last code change we need to add is the auth middleware to protect the route from guests. We don’t assume any permissions for this tutorial—any user can download the export of users.

Route::get('/users/export', 'UsersExportController')
    ->middleware('auth');

Finally, our test should pass:

phpunit --filter=guests_cannot_download_users_export
PHPUnit 7.5.9 by Sebastian Bergmann and contributors.

.                                                                   1 / 1 (100%)

Time: 118 ms, Memory: 14.00 MB

OK (1 test, 2 assertions)

Starting the Export Feature

We have our user export test and controller in place, and we are ready to start testing the actual streamed download feature. Our first step is creating the next test case in the UsersExportTest test class which requests the export endpoint as an authenticated user.

// Make sure to import the RefreshDatabase trait
use RefreshDatabase;

/** @test */
public function authenticated_users_can_export_all_users()
{
    $users = factory('App\User', 5)->create();

    $response = $this->actingAs($users->first())->get('/users/export');
    $content = $response->streamedContent();
    dd($content);
}

Notice the streamedContent() method on the TestResponse instance. This is an excellent helper method for getting the content as string from a StreamResponse instance—check out the Symfony HttpFoundation Component) for more information about the StreamResponse class.

Our test scaffolds five users and uses the first user in the collection to make an authenticated request to the export endpoint. Since we haven’t written any controller code, our test will fail to assert that we don’t have a StreamResponse instance:

$ phpunit --filter=authenticated_users_can_export_all_users
PHPUnit 7.5.9 by Sebastian Bergmann and contributors.

F                                                                   1 / 1 (100%)

Time: 183 ms, Memory: 20.00 MB

There was 1 failure:

1) Tests\Feature\UsersExportTest::authenticated_users_can_export_all_users
The response is not a streamed response.

Let’s get this test passing with as little code as needed. Change the invokable UsersExportController class to the following:

/**
 * Handle the incoming request.
 *
 * @param  \Illuminate\Http\Request  $request
 * @return \Illuminate\Http\Response
 */
public function __invoke(Request $request)
{
    return response()->streamDownload(function () {
        echo "hello world";
    }, 'users.txt');
}

If we rerun the PHPUnit test we should have a string representation of our StreamResponse instance:

$ phpunit --filter=authenticated_users_can_export_all_users
PHPUnit 7.5.9 by Sebastian Bergmann and contributors.

"hello world"

Further, if you want to experiment with the content-disposition header and assert that the response is indeed forcing a download you can add the following lines:

/** @test */
public function authenticated_users_can_export_all_users()
{
    $users = factory('App\User', 5)->create();

    $response = $this->actingAs($users->first())->get('/users/export');
    $response->assertHeader('Content-Disposition', 'attachment; filename=users.txt');
    $content = $response->streamedContent();
    dd($content);
}

Laravel has tests for the streamDownload feature, but it doesn’t hurt to check the header to make sure we are forcing a download for the export endpoint to ensure we are sending the appropriate response headers from our controller.

Testing a CSV Export

We have a basic test in place for testing the StreamedResponse instance our controller returns, and now it’s time to move on to generating and testing a CSV export.

We are going to rely on the PHP League CSV package to generate and test our endpoint:

composer require league/csv

Next, let’s continue writing our test, first asserting that the CSV row count matches the count of users in the database:

// Import the CSV Reader instance at the top...
use League\Csv\Reader as CsvReader;

// ...

/** @test */
public function authenticated_users_can_export_all_users()
{
    $users = factory('App\User', 5)->create();

    $response = $this->actingAs($users->first())->get('/users/export');
    $response->assertHeader('Content-Disposition', 'attachment; filename=users.txt');

    $reader = CsvReader::createFromString($response->streamedContent());
    $reader->setHeaderOffset(0);

    $this->assertCount(User::count(), $reader);
}

We first create a new CSV reader instance and set the header offset as the first row (we haven’t written the CSV file in the controller yet) which will match our CSV columns shortly. Setting the header offset means that the Countable CSV reader instance will ignore the header row as part of the row count.

To make this pass, we need to create a CSV writer instance and add our users:

<?php

namespace App\Http\Controllers;

use App\User;
use Illuminate\Http\Request;
use League\Csv\Writer as CsvWriter;

class UsersExportController extends Controller
{
    /**
     * @var  \League\Csv\Writer
     */
    private $writer;

    /**
     * Handle the incoming request.
     *
     * @param  \Illuminate\Http\Request  $request
     * @return \Illuminate\Http\Response
     */
    public function __invoke(Request $request)
    {
        $this->writer = CsvWriter::createFromString('');
        $this->writer->insertOne([
            'Name', 'Email', 'Email Verified', 'Created', 'Updated'
        ]);

        User::all()->each(function ($user) {
            $this->addUserToCsv($user);
        });

        return response()->streamDownload(function () {
            echo $this->writer->getContent();
        }, 'users.csv');
    }

    private function addUserToCsv(User $user)
    {
        $this->writer->insertOne([
            $user->name,
            $user->email,
            $user->email_verified_at->format('Y-m-d'),
            $user->created_at->format('Y-m-d'),
            $user->updated_at->format('Y-m-d'),
        ]);
    }
}

There’s a lot to unpack here, but most importantly we create a CSV writer instance and add the users from the database to the writer. Finally, we output the content of the CSV file in the streamDownload closure.

Also note that we changed the filename to users.csv and need to adjust our test to match. Let’s also check out the streamed content to see our raw CSV file in action:

$response->assertHeader('Content-Disposition', 'attachment; filename=users.csv');
dd($response->streamedContent());

You should see something like the following with the five factory users we added in the test:

Finally, if you remove the call to dd() the test should now pass:

phpunit --filter=authenticated_users_can_export_all_users
PHPUnit 7.5.9 by Sebastian Bergmann and contributors.

.                                                                   1 / 1 (100%)

Time: 180 ms, Memory: 22.00 MB

OK (1 test, 3 assertions)

Here’s what our test case looks like right now:

/** @test */
public function authenticated_users_can_export_all_users()
{
    $this->withoutExceptionHandling();
    $users = factory('App\User', 5)->create();

    $response = $this->actingAs($users->first())->get('/users/export');
    $response->assertHeader('Content-Disposition', 'attachment; filename=users.csv');

    $reader = CsvReader::createFromString($response->streamedContent());
    $reader->setHeaderOffset(0);

    $this->assertCount(User::count(), $reader);
}

Checking the Records

We can verify that the records contain the expected values from each of our users now by reverse engineering the CSV writer with our CSV reader in the test:

/** @test */
public function authenticated_users_can_export_all_users()
{
    $this->withoutExceptionHandling();
    $users = factory('App\User', 5)->create();

    $response = $this->actingAs($users->first())->get('/users/export');
    $response->assertHeader('Content-Disposition', 'attachment; filename=users.csv');

    $reader = CsvReader::createFromString($response->streamedContent());
    $reader->setHeaderOffset(0);

    $allUsers = User::all();
    $this->assertCount($allUsers->count(), $reader);

    foreach ($reader->getRecords() as $record) {
        $index = $allUsers->search(function ($user) use ($record) {
            return $user->email === $record['Email'];
        });

        $this->assertNotFalse($index);

        $found = $allUsers->get($index);

        $this->assertEquals($found->name, $record['Name']);
        $this->assertEquals($found->email, $record['Email']);
        $this->assertEquals($found->email_verified_at->format('Y-m-d'), $record['Email Verified']);
        $this->assertEquals($found->created_at->format('Y-m-d'), $record['Created']);
        $this->assertEquals($found->updated_at->format('Y-m-d'), $record['Updated']);

        $allUsers->forget($index);
    }

    $this->assertCount(0, $allUsers, 'All users should be accounted for in the CSV file.');
}

Our last test case has a lot of new lines, but they’re not super complicated. We are first querying the database for all records separately from our $users variable from the factory. We want to get a fresh collection of all users straight from the database.

Next, we verify that the number of rows in the CSV file matches the database collection. Using the CSV rows, we search for users in the $allUsers collection to make sure we account for each user. We assert the format of columns and finally remove the user from the $allUsers collection at the bottom of our loop.

The final assertion guarantees that all users are removed from the temporary collection by being represented as a row in the CSV file.

Conclusion

While we got into the details of writing and testing this feature, the big takeaway from this tutorial is the TestResponse::streamedContent() method to get the streamed file to verify the content. One of the neatest parts of this tutorial is the realization that we can generate plaintext files from a stream without saving a file to disk first! The possibility of representing model data as a streamed download without exporting a file is a fantastic feature in my opinion!

Building a Vue SPA with Laravel Part 5

We left off in Part 4 with the ability to edit users and learned how to use v-model to track changes to the view component user property. Now we’re ready to look at deleting users and how to handle the UI after the delete action succeeds.

Along the way, we’re going to look at building an Axios client instance to enable greater flexibility in how we configure API clients.

Updating the API to Handle Deleting Users

The first thing we are going to work on is defining the API route for deleting an individual user. Thanks to route model binding this is only a couple of lines in the UsersController:

public function destroy(User $user)
{
    $user->delete();

    return response(null, 204);
}

Next, define the new route at the bottom of the Api group in the routes/api.php file:

Route::namespace('Api')->group(function () {
    Route::get('/users', 'UsersController@index');
    Route::get('/users/{user}', 'UsersController@show');
    Route::put('/users/{user}', 'UsersController@update');
    Route::delete('/users/{user}', 'UsersController@destroy');
});

Deleting Users on the Frontend

We’re going to add the delete functionality to our /users/:id/edit view component, by adding a delete button to the UsersEdit.vue component under the “Update” button:

<div class="form-group">
    <button type="submit" :disabled="saving">Update</button>
    <button :disabled="saving" @click.prevent="onDelete($event)">Delete</button>
</div>

We copied the :disabled attribute from the update button that we can use to prevent an inadvertent update or delete when an action is taking place.

Next, we need to hook up the onDelete() callback to handle deleting the user:

onDelete() {
  this.saving = true;

  api.delete(this.user.id)
     .then((response) => {
        console.log(response);
     });
}

We call the delete() function on our API client and then chain a callback to log out the response object in the console. The update and delete buttons are disabled if click “delete” because we’re setting this.saving = true —we will come back to this point in a second. If you have the console open, you will see a 204 No Content response object indicating the deletion worked.

How to React to a Successfully Deleted User

One thing that’s a little different from updating a user is that we don’t have a user in the database once we delete the record. In a traditional web application, we would likely delete the record and then redirect the user back to the list of all users.

We can do this very thing in our SPA by programmatically navigating the user back to the /users page:

this.$router.push({ name: 'users.index' })

Applying the this.$router.push() call to our event, the most basic version would look like this:

onDelete() {
  this.saving = true;
  api.delete(this.user.id)
     .then((response) => {
        this.$router.push({ name: 'users.index' });
     });
}

If you refresh the application and delete a user, you will notice a brief flash of disabled buttons, and then the browser navigates to the /users page without any feedback.

We could handle notifying the user with a dedicated toast/notification mechanism. I’ll leave the approach up to you, but here’s a basic idea of what I’m talking about:

onDelete() {
  this.saving = true;
  api.delete(this.user.id)
     .then((response) => {
        this.message = 'User Deleted';
        setTimeout(() => this.$router.push({ name: 'users.index' }), 2000);
     });
}

The above code sets the this.message data property we set up in Part 4 and waits two seconds before navigating to the /users index page.

You could also use something like portal-vue or a component in your layout that flashes the message temporarily (or with a mandatory close button) to indicate an action has succeeded (or failed) to give the user some feedback.

Four Oh Four

You might have noticed that if our Vue route matches the pattern /users/:id/edit, we still might have a 404 response from the API if the user id is not found:

With a server-side Laravel application, we could render a 404.blade.php from a ModelNotFoundException easily. A SPA is a little bit different though. The above route is valid, so we need our component to either render the error component instead of or redirect the user to a dedicated 404 route.

We will add a few new routes to the Vue router configuration in resources/assets/js/app.js with a dedicated 404 view and a catch-all component that redirects routes that don’t match to the 404 route:

{ path: '/404', name: '404', component: NotFound },
{ path: '*', redirect: '/404' },

We’ll create a simple NotFound component at resources/assets/js/views/NotFound.vue:

<template>
  <div>
    <h2>Not Found</h2>
    <p>Woops! Looks like the page you requested cannot be found.</p>
  </div>
</template>

Because we have a catch-all route on the backend in Laravel, that means that the frontend also needs a catch-all route to respond with a 404 page if the path doesn’t match a defined route. Here’s the backend route as a refresher that catches all routes and sends renders the SPA template:

Route::get('/{any}', 'SpaController@index')
    ->where('any', '.*');

If you enter an invalid URL like /does-not-exist, you will see something like the following:

The Vue router hits the wildcard route which redirects the browser to /404.

Our previous example with an invalid user id still isn’t working yet, because technically the route is valid. We need to update the UsersEdit component to catch failed requests in the create() callback and send the user to the 404 route:

created() {
  api.find(this.$route.params.id)
     .then((response) => {
         this.loaded = true;
         this.user = response.data.data;
     })
     .catch((err) => {
       this.$router.push({ name: '404' });
     });
}

Now if you make a request directly to a URI like /users/2000/edit you should see the app redirect to the 404 page instead of hanging on the “Loading…” UI in the UsersEdit component.

API Client Options

Although our dedicated users.js HTTP client might be considered overkill in a small application, I think the separation has already served us well as we use the API module in multiple components. I discuss this idea at great length in my article Building Flexible Axios Clients if you want to learn all the details of what a flexible client affords.

Without changing the external API of our client, we can change how the client works under the hood. For example, we can create an Axios client instance with customizable configuration and defaults:

import axios from 'axios';

const client = axios.create({
  baseURL: '/api',
});

export default {
  all(params) {
    return client.get('users', params);
  },
  find(id) {
    return client.get(`users/${id}`);
  },
  update(id, data) {
    return client.put(`users/${id}`, data);
  },
  delete(id) {
    return client.delete(`users/${id}`);
  },
};

Now I can swap out the baseURL with some configuration later if I want to customize the way the entire module works without affecting the methods.

What’s Next

We learned how to delete users and respond to a successful deletion on the frontend via Vue router. We’ve introduced programmatic navigation via the this.$router property by registering the Vue router with Vue.use(VueRouter) in our main app.js file.

Next, we will turn to build the user creation to wrap up learning how to perform basic create, read, update, and delete (CRUD) actions. At this point you should have all the tools you need to complete creating new users on your own, so feel free to try building this functionality before the publication of the next article in this series.

When you’re ready, check out Part 6 – creating new users

Building a Vue SPA with Laravel Part 4

We left off building a real users endpoint and learned about a new way to fetch component data with Vue router in part 3. Now we’re ready to move our attention to creating CRUD functionality for our users—this tutorial will focus on editing existing users.

Along with working through our first form, we will get a chance to look at defining a dynamic Vue route. The dynamic part of our route will be the user’s ID which matches his or her database record. For editing a user, the Vue route will look like this:

/users/:id/edit

The dynamic part of this route is the :id parameter, which will depend on the user’s ID. We are going to use the id field from the database, but you could also use a UUID or something else.

The Setup

Before we focus on the Vue component, we need to define a new API endpoint to fetch an individual user, and then later we’ll need to specify another endpoint to perform the update.

Open the routes/api.php routes file and add the following route below the index route that fetches all users:

Route::namespace('Api')->group(function () {
    Route::get('/users', 'UsersController@index');
    Route::get('/users/{user}', 'UsersController@show');
});

Using Laravel’s implicit route model binding, our controller method is straightforward. Add the following method to the app/Http/Controllers/Api/UsersController.php file:

// app/Http/Controllers/Api/UsersController

public function show(User $user)
{
    return new UserResource($user);
}

Requesting a user at something like /api/users/1 will return the following JSON response:

{
    "data": {
        "name": "Antonetta Zemlak",
        "email":"znikolaus@example.org"
    }
}

Our UserResource from Part 3 needs updated to include the id column, so you should update the app/Http/Resources/UserResource.php file to include the id array key. I’ll paste the entire file from Part 3 here:

<?php

namespace App\Http\Resources;

use Illuminate\Http\Resources\Json\Resource;

class UserResource extends Resource
{
    /**
     * Transform the resource into an array.
     *
     * @param  \Illuminate\Http\Request  $request
     * @return array
     */
    public function toArray($request)
    {
        return [
            'id' => $this->id,
            'name' => $this->name,
            'email' => $this->email,
        ];
    }
}

Now our /api/users and /api/users/{user} routes will respond with the id field, which we need to identify the users in our routes.

Defining the UsersEdit Vue Component

With the show route in place, we can turn our attention to defining the frontend Vue route and the accompanying component. Add the following route definition to the resources/js/app.js routes. Here’s a snippet of importing the UsersEdit component—which we have yet to create—along with the entire route instance:

import UsersEdit from './views/UsersEdit';

// ...

const router = new VueRouter({
    mode: 'history',
    routes: [
        {
            path: '/',
            name: 'home',
            component: Home
        },
        {
            path: '/hello',
            name: 'hello',
            component: Hello,
        },
        {
            path: '/users',
            name: 'users.index',
            component: UsersIndex,
        },
        {
            path: '/users/:id/edit',
            name: 'users.edit',
            component: UsersEdit,
        },
    ],
});

We’ve added the users.edit route to the end of the routes configuration.

Next, we need to create the UsersEdit component at resources/assets/js/views/UsersEdit.vue with the following component code:

<template>
  <div>
      <form @submit.prevent="onSubmit($event)">
        <div class="form-group">
            <label for="user_name">Name</label>
            <input id="user_name" v-model="user.name" />
        </div>
        <div class="form-group">
            <label for="user_email">Email</label>
            <input id="user_email" type="email" v-model="user.email" />
        </div>
        <div class="form-group">
            <button type="submit">Update</button>
        </div>
    </form>
  </div>
</template>
<script>
export default {
  data() {
    return {
      user: {
        id: null,
        name: "",
        email: ""
      }
    };
  },
  methods: {
    onSubmit(event) {
        // @todo form submit event
    }
  },
  created() {
      // @todo load user details
  }
};
</script>

Let’s focus on the template portion first: we render a <form> around a closing div because soon we’ll need to conditionally show the form after loading the user’s data.

The <form> tag has a placeholder @submit event, and we’ve defined an onSubmit() method handler that takes an event object. The last thing I’ll mention is the v-model attributes on the <input> elements, which maps to accompanying data.users Object literal. We’ve stubbed out the default values for id, name, and email.

At this point if you load up /users/1/edit you’ll see an empty form rendered:

We intend on editing existing users, so our next step is figuring out how to grab the dynamic :id property from the route and loading the user’s data from the UsersEdit.vue component.

Loading User Details with a Dedicated Client

Before we load the user data in the component, we’re going to go on a side-quest to extract the /api/users resource into a dedicated API module that we can use to query all users, individual users, and update users.

First, we’re going to create a new folder and file to house the API modules for our backend. You can create these files in any way you please. We’ll demonstrate from the command line on a `Nix command line:

mkdir -p resources/assets/js/api/
touch resources/assets/js/api/users.js

The users.js component is going to expose some functions we can call to do operations on the /api/users resource. This module is going to be relatively simple, but later can allow you to do any mapping, data manipulation, etc. before or after the API request. This file serves as a repository of reusable API operations:

import axios from 'axios';

export default {
    all() {
        return axios.get('/api/users');
    },
    find(id) {
        return axios.get(`/api/users/${id}`);
    },
    update(id, data) {
        return axios.put(`/api/users/${id}`, data);
    },
};

Now we can use the same module to get all users, as well as find and update individual users:

// Get all users
client.all().then((data) => mapData);

// Find a user
client.find(userId);

For now, the all() method doesn’t accept any pagination query params, but I’ll leave it up to you to implement pagination and replace what we have on the UsersIndex.vue component with our new all() client function.

Loading the User from the UsersEdit Component

Now that we have a reusable—albeit very basic—API client, we can put it to work to load the user data when the edit page is rendered.

We originally stubbed out a created() function on our component, which is where we’ll request the user’s data now:

// UsersEdit.vue Component
<script>
import api from '../api/users';

export default {
  // ...
  created() {
      api.find(this.$route.params.id).then((response) => {
        this.loaded = true;
        this.user = response.data.data;
      });
  }
}
</script>

Our created() callback calls the users.js client find() function which returns a promise. In the Promise callback, we set a loaded data property (which we haven’t created yet) and set the this.user data property.

Let’s add the loaded property to our data key and set it to false by default:

data() {
  return {
    loaded: false,
    user: {
      id: null,
      name: "",
      email: ""
    }
  };
},

Since our component loads up the data inside of created() we’ll show a conditional “loading” message on the component initially:

<div v-if="! loaded">Loading...</div>
<form @submit.prevent="onSubmit($event)" v-else>
<!-- ... -->
</form>

At this point if you refresh the page, the component will briefly flash a Loading... message:

And then the user’s data should populate the form:

The API is very quick, so if you want to verify that the condition is working, you can call setTimeout to delay the setting of the this.user data property:

api.find(this.$route.params.id).then((response) => {
    setTimeout(() => {
      this.loaded = true;
      this.user = response.data.data;
    }, 5000);
});

The above timeout will show the loading message for five seconds and then set the loaded and user data properties.

Updating the User

We’re ready to hook up the onSubmit() event handler and update the user via the PUT /api/users/{user} API endpoint.

First, let’s add the onSubmit() code and then we’ll move to the Laravel backend to make the backend perform the update on the database:

onSubmit(event) {
  this.saving = true;

  api.update(this.user.id, {
      name: this.user.name,
      email: this.user.email,
  }).then((response) => {
      this.message = 'User updated';
      setTimeout(() => this.message = null, 2000);
      this.user = response.data.data;
  }).catch(error => {
      console.log(error)
  }).then(_ => this.saving = false);
},

We’ve called the api.update() function with the current user’s ID, and passed the name and email values from the bound form inputs.

We then chain a callback on the Promise object to set the success message and set the updated user data after the API succeeds. After 2000 milliseconds we clear the message which will effectively hide the message in the template.

For now, we are catching any errors and logging them to the console. In the future, we may go back and cover handling errors such as server failure or validation errors, but for now, we’ll skip over this to focus on the success state.

We use this.saving to determine if our component is in the process of updating the user. Our template ensures the submit button is disabled when a save is in progress to avoid double submissions with a bound :disabled property:

<div class="form-group">
  <button type="submit" :disabled="saving">Update</button>
</div>

Once the API request is finished, the last thing we’re doing here is setting the this.saving to false by chaining on another then() callback after catch. We need to reset this property to false so the component can submit the form again. Our last then() chain uses the _ underscore variable as a convention you’ll find in some languages indicating that there’s an argument here, but we don’t need to use it. You could also define the short arrow function with empty parenthesis:

.then(() => this.saving = false);

We’ve introduced two new data properties that we need to add to our data() call:

data() {
  return {
    message: null,
    loaded: false,
    saving: false,
    user: {
      id: null,
      name: "",
      email: ""
    }
  };
},

Next, let’s update our <template> to show the message when it’s set:

<template>
  <div>
      <div v-if="message" class="alert">{{ message }}</div>
      <div v-if="! loaded">Loading...</div>
      <form @submit.prevent="onSubmit($event)" v-else>
        <div class="form-group">
            <label for="user_name">Name</label>
            <input id="user_name" v-model="user.name" />
        </div>
        <div class="form-group">
            <label for="user_email">Email</label>
            <input id="user_email" type="email" v-model="user.email" />
        </div>
        <div class="form-group">
            <button type="submit" :disabled="saving">Update</button>
        </div>
    </form>
  </div>
</template>

Finally, let’s add a few styles for the alert message at the bottom of the UsersEdit.vue file:

<style lang="scss" scoped>
$red: lighten(red, 30%);
$darkRed: darken($red, 50%);
.form-group label {
  display: block;
}
.alert {
    background: $red;
    color: $darkRed;
    padding: 1rem;
    margin-bottom: 1rem;
    width: 50%;
    border: 1px solid $darkRed;
    border-radius: 5px;
}
</style>

We’ve finished updating our frontend component to handle a submitted form and update the template accordingly after the API request succeeds. We now need to turn our attention back to the API to wire it all up.

Updating Users in the API Backend

We’re ready to connect all the dots by defining an update method on our User resource controller. We are going to define necessary validation on the server side. However, we aren’t going to wire it up on the frontend yet.

First, we will define a new route in the routes/api.php file for a PUT /api/users/{user} request:

Route::namespace('Api')->group(function () {
    Route::get('/users', 'UsersController@index');
    Route::get('/users/{user}', 'UsersController@show');
    Route::put('/users/{user}', 'UsersController@update');
});

Next, the UsersController@update method will use the request object to validate the data and return the fields we intend to update. Add the following method to the app/Http/Controllers/Api/UsersController.php file:

public function update(User $user, Request $request)
{
    $data = $request->validate([
        'name' => 'required',
        'email' => 'required|email',
    ]);

    $user->update($data);

    return new UserResource($user);
}

Just like the show() method, we are using the implicit request model binding to load the user from the database. After validating the required fields, we update the user model and return the updated model by creating a new instance of the UserResource class.

A successful request to the backend will return the user’s updated JSON data, which we then, in turn, use to update the this.user property in the Vue component.

{
  "data": {
    "id": 1,
    "name":"Miguel Boyle",
    "email":"hirthe.joel@example.org"
  }
}

Navigating to the Edit Page

We’ve been requesting the /users/:id/edit page directly, however, we haven’t added it anywhere in the interface. Feel free to try to figure out how to dynamically navigate to the edit page on your own before seeing how I did it.

Here’s how I added the edit link for each user listed on the /users index page in the UsersIndex.vue template we created back in Part 2:

<ul v-if="users">
    <li v-for="{ id, name, email } in users">
        <strong>Name:</strong> {{ name }},
        <strong>Email:</strong> {{ email }} |
        <router-link :to="{ name: 'users.edit', params: { id } }">Edit</router-link>
    </li>
</ul>

We restructure the user object in our loop to give us the id, name and email properties. We use the <router-link/> component to reference our users.edit named route with the id parameter passed in the params key.

To better visualize the <router-link> properties, here’s the route definition from the app.js file we added earlier:

{
  path: '/users/:id/edit',
  name: 'users.edit',
  component: UsersEdit,
},

If you refresh the app or visit the /users endpoint, you’ll see something like the following:

Putting it All Together

If you edit a user now, the backend should save it and respond with a 200 success if all went well. After the PUT request succeeds you should see the following for two seconds:

Here’s the final UsersEdit.vue component in full for your reference:

<template>
  <div>
      <div v-if="message" class="alert">{{ message }}</div>
      <div v-if="! loaded">Loading...</div>
      <form @submit.prevent="onSubmit($event)" v-else>
        <div class="form-group">
            <label for="user_name">Name</label>
            <input id="user_name" v-model="user.name" />
        </div>
        <div class="form-group">
            <label for="user_email">Email</label>
            <input id="user_email" type="email" v-model="user.email" />
        </div>
        <div class="form-group">
            <button type="submit" :disabled="saving">Update</button>
        </div>
    </form>
  </div>
</template>
<script>
import api from '../api/users';

export default {
  data() {
    return {
      message: null,
      loaded: false,
      saving: false,
      user: {
        id: null,
        name: "",
        email: ""
      }
    };
  },
  methods: {
    onSubmit(event) {
        this.saving = true;

        api.update(this.user.id, {
            name: this.user.name,
            email: this.user.email,
        }).then((response) => {
            this.message = 'User updated';
            setTimeout(() => this.message = null, 10000);
            this.user = response.data.data;
        }).catch(error => {
            console.log(error)
        }).then(_ => this.saving = false);
    }
  },
  created() {
      api.find(this.$route.params.id).then((response) => {
          setTimeout(() => {
            this.loaded = true;
            this.user = response.data.data;
          }, 5000);
      });
  }
};
</script>
<style lang="scss" scoped>
$red: lighten(red, 30%);
$darkRed: darken($red, 50%);
.form-group label {
  display: block;
}
.alert {
    background: $red;
    color: $darkRed;
    padding: 1rem;
    margin-bottom: 1rem;
    width: 50%;
    border: 1px solid $darkRed;
    border-radius: 5px;
}
</style>

Homework

After the user update succeeds, we just reset the message after two seconds. Change the behavior to set the message and then redirect the user back to the previous location (i.e., the /users index page).

Second, add a “Back” or “Cancel” button to the bottom of the form that discards the for updates and navigates back to the previous page.

If you are feeling adventurous, display validation errors when the UsersEdit component sends an invalid request to the API. Clear the error messages after successfully submitting the form.

What’s Next

With updating users out of the way, we will move our attention to deleting users. Deleting a user will be helpful to demonstrate programmatically navigating after successful deletion. We will also look at defining a global 404 page now that we have dynamic routing for editing users.

If you’re ready, move on to Part 5.

Tips for Using Laravel’s Scheduler

Laravel’s task scheduling features are well documented and give you the full power of cron in a fluent API. The documentation covers most of what you need to get up and running with the scheduler quickly, however, there are a few underlying concepts I’d like to cover related to cron that will help solidify your understanding of how Laravel determines which scheduled tasks should run.

Understanding Cron

At the foundation of Laravel’s scheduler, you need to understand how to schedule tasks on a server through Cron’s somewhat confusing syntax.

Before we dive into understanding cron better and resources you can use to familiarize yourself with cron, let’s look at the essential pieces of the scheduler.

First, you define scheduled tasks through your Laravel application’s App\Console\Kernel::schedule() method:

/**
 * Define the application's command schedule.
 *
 * @param  \Illuminate\Console\Scheduling\Schedule  $schedule
 * @return void
 */
protected function schedule(Schedule $schedule)
{
    // $schedule->command('inspire')
    //         ->hourly();
}

You can use this method to define all of the scheduled tasks that need to run. The Schedule instance method command() returns an instance of the Illuminate\Console\Scheduling\Event class.

If you want to tinker/debug with an instance of the event class, you can dump like the following example:

$event = $schedule->command('inspire')
                  ->hourly();

dd($event->expression); // "0 * * * *"

To trigger this method, run artisan:

> php artisan
"0 * * * *"

The event instance has an expression property that stores the cron representation of the task after the fluent API calls.

Keep this example’s value in mind while we talk about cron.

Laravel shields you from cron with the scheduler’s fluent API—in our example the hourly() method—but understanding cron will help you understand better how to troubleshoot what is going on under the hood.

Here’s a text representation that should clarify how cron works if you’re not familiar (even if you are I bet this is still useful):

# Use the hash sign to prefix a comment
# +---------------- minute (0 - 59)
# |  +------------- hour (0 - 23)
# |  |  +---------- day of month (1 - 31)
# |  |  |  +------- month (1 - 12)
# |  |  |  |  +---- day of week (0 - 7) (Sunday=0 or 7)
# |  |  |  |  |
# *  *  *  *  *  command to be executed
#-----------------------------------------------------------

Using the above example of “0 * * * *” this task will run at the zero minute mark every single hour of every day of every month on every day of the week.

Cron also has some other formats that might feel weird, such as the expression generated using Laravel’s quarterly() method:

0 0 1 1-12/3 *

Running a task quarterly means it will run at 00:00 on the first day of the month in every third month from January to December. The weird 1-12/3 syntax is called a “step value” which can be used in conjunction with ranges. The crontab – Linux manual page describes step values as follows:

Step values can be used in conjunction with ranges. Following a range with “” specifies skips of the number’s value through the range. For example, “0-23/2” can be used in the hours’ field to specify command execution every other hour (the alternative in the V7 standard is “0,2,4,6,8,10,12,14,16,18,20,22”). Steps are also permitted after an asterisk, so if you want to say “every two hours”, just use “*/2”.

I’d encourage you to read through the man-page, or at least keep it handy if you run into a situation where you need to understand the underlying cron schedule for a task better.

Understanding the Scheduling Event API

Laravel has some excellent fluent APIs that allow you to chain multiple method calls together. The scheduling Event class is no different. However, there are some nuances with some of the combinations you might use.

Take the following as an example to better illustrate: let’s say that we want a command to run hourly, but only on Monday, Wednesday, and Friday:

$schedule->command('inspire')
         ->hourly()
         ->mondays()
         ->wednesdays()
         ->fridays();

You might think the above command achieves the correct cron, but that’s not how it works. In the above example, the last “day” method called is fridays(), thus, here’s what the cron looks like:

0 * * * 5

The above task will run hourly, but only on Friday.

Before I show you the correct method call to achieve what we want, let’s look at the Event::fridays() method. The fridays() method (and many others) come from Laravel’s ManagesFrequencies trait:

/**
 * Schedule the event to run only on Fridays.
 *
 * @return $this
 */
public function fridays()
{
    return $this->days(5);
}

The method calls another method on the same trait days() which looks like the following at the time of writing:

/**
 * Set the days of the week the command should run on.
 *
 * @param  array|mixed  $days
 * @return $this
 */
public function days($days)
{
    $days = is_array($days) ? $days : func_get_args();

    return $this->spliceIntoPosition(5, implode(',', $days));
}

You can look at the details of how spliceIntoPosition() works, but all of the “day” methods overwrite each other, so the last one called sticks.

Here’s how you’d write the correct schedule using Laravel’s fluent API:

$schedule->command('inspire')
         ->hourly()
         ->days([1, 3, 5]);

Debugging this Task instance yields the following expression:

0 * * * 1,3,5

Bingo!

Using Cron Directly

Most of the time I think most people prefer to use Laravel’s fluent API. However, the Event task includes a cron() method to set the expression directly:

$schedule->command('inspire')
         ->cron('0 * * * 1,3,5');

I’d argue that Laravel’s fluent API is a more readable way to define the command, but you can get the full power of cron directly with this method if you’d rather use cron syntax.

Crontab Guru

For advanced use-cases and better understanding how your scheduled tasks are going to run, consider debugging the underlying cron expression and using a tool like crontab.guru – the cron schedule expression editor.

Building a Laravel Translation Package –Launching the Package

With the pre-launch checklist completed, it’s time to go ahead and make our package available for others to use.

Chances are, the consumers of the package will be using Composer to manage the dependencies in their project. To make the package compatible with composer, there are a few steps we need to follow.

Tagging a Release

To allow our users to manage their dependencies effectively, it’s important to properly release new versions of the package.

The most common approach to versioning code is to follow Semantic Versioning. This defines a set of ‘rules and requirements that dictate how version numbers are assigned and incremented’. From the website, these are defined as:

  1. MAJOR version when you make incompatible API changes,
  2. MINOR version when you add functionality in a backwards-compatible manner, and
  3. PATCH version when you make backwards-compatible bug fixes.

Additional labels for pre-release and build metadata are available as extensions to the MAJOR.MINOR.PATCH format.

If you are interested, the full definition can be found on the website.

Deciding which version to tag your initial release can be tricky and I recently saw an interesting thread on Twitter discussing the issue.

Semantic Versioning suggest if you are are using the package in production, you should you go straight to 1.0.0, but if not and the package is still in development, the initial release should be 0.1.0.

There is more than one way to tag a release. For the purposes of this article, I’m going to show you how to do so on GitHub.

From the root of your repository, click on ‘Releases’ followed by ‘Draft a new release’.

There, enter your desired version number in the ‘Tag version’ field and select the target you want to reference. This can be a branch or an individual commit. If you wish, you can also provide an appropriate title for which, typically, I use the version number.

You can also provide release notes, which can be a nice way to let you users know exactly what has changed and maybe even thank your contributors.

Submitting to Packagist

Now, to allow users to easily install the package using Composer, it’s common to publish it to Packagist.

To do this, login to your Packagist account and click ‘Submit’ in the main navigation. Enter the URL of your git repository when prompted.

Packagist will pull in all the relevant information from the composer.json file and publish the package to the repository, ready for people to use. The package will now have its own page on the site providing users with details such as the number of installations, versions and latest activity.

Summary

With the package published and ready for people to use, we are at the end of this series of articles.

We now move in to business as usual, releasing new versions of the package and dealing with issues and pull requests submitted from the users.

I really hoped you enjoyed this series and have picked up some useful tips along the way. As usual, should you have any questions or comments, please send them across on Twitter.

Building a Tool for Laravel Nova

Recently, I took advantage of the Black Friday sale and, although I didn’t have a use for it, I purchased a Laravel Nova license. I had been impressed with what I had seen and was desperate to give it a try.

I decided it would be fun to port one of my open source packages, Laravel Translation to a Nova Tool.

Related: Building a Laravel Translation Package.

If you were unaware, a Tool is Nova’s method of allowing developers to add additional custom functionality to what ships out of the box.

This article will walk you through the process I took when building it.

Documentation

Not knowing where to start, I headed for the documentation. As you would expect from a Laravel product, the documentation is fantastic. There is a section dedicated to building tools as well as all the other ways of customizing Nova.

Within a few minutes, I had a grasp of what I needed to do.

Scaffolding

Nova ships with a CLI command which generates the scaffolding of the tool for you, php artisan nova:tool. When running this command, you pass in the tool’s name in Packagist vendor/name format which it uses when generating the composer.json file. This means it will be ready to publish as soon as development has completed.

The full scaffolding of the tool includes everything needed to get started with development including service providers, starter JavaScript, and SASS files and its class which is used to register the tool in the application.


Service Provider

A tool has its service provider where it can be bootstrapped. This acts much like a Laravel service provider. It extends Illuminate\Support\ServiceProvider used in a standard Laravel application. This means all the methods like loading routes, views, migrations, etc. are available to you.

Unlike a standard Laravel service provider, all the necessary wiring up of views and routes is provided out of the box.

// Views
$this->loadViewsFrom(__DIR__.'/../resources/views', 'nova-translation');

// Routes
Route::middleware(['nova', Authorize::class])
    ->prefix('nova-vendor/nova-translation')
    ->group(__DIR__.'/../routes/api.php');

As you can see, there is some middleware automatically applied to the routes. The nova middleware group which applies any middleware defined in the nova.php configuration file and the Authorize::class which applies any custom authorization set in the tool class. More on this later.

The routes are also registered under your vendor prefix, so they don’t conflict with any other routes.

Finally, your routes are loaded from the routes/api.php file that is automatically generated with the tool.

Tool Class

As previously mentioned, the tool class is created automatically for you. It is named using the studly-cased version of the name you provided when running php artisan nova:tool vendor/name.

You don’t have to do anything in here, but it is possible to modify the way you render your navigation or which assets should be loaded.

For my package, I didn’t need to make any changes to this file.

Navigation

Nova makes it incredibly easy to add a link to your tool in the main sidebar of the application. Once again, this is generated automatically by providing a navigation.blade.php file containing a default navigation link and loading it from the tool class.

public function renderNavigation()
{
    return view('nova-translation::navigation');
}

You are free to edit this file to suit your needs. Typically, this will involve updating the SVG icon rendered in the navigation. Conveniently, Nova ships with a set of icons you are free to use.

Frontend

The Nova frontend is a Vue.js application. A new tool is initialized with a tool.js file where you can use Vue’s router to define any routes needed. From there, you are free to build the application as you wish. As you would expect, the route to the index of your app is defined for you, but you are free to update it if required.

Nova.booting((Vue, router) => {
    router.addRoutes([
        {
            name: 'nova-translation',
            path: '/nova-translation',
            component: require('./views/LanguagesIndex'),
        },

        ...
    ])
})

Tools are compiled using Laravel Mix, and a configuration file is included as part of the scaffolding. This allows you to run development and production builds, and even trigger a rebuild when files change using commands you are likely familiar with such as npm run watch.

One of the things I liked when developing the frontend was the ability to utilize all the Vue components from the core of Nova.

It’s incredibly useful to be able to pull in things like loading views which automatically handle the loading state of your component or dropdown components for building out the user interface.

<template>
    <loading-view :loading="initialLoading"> // ships with Nova
        <loading-card :loading="loading" class="card"> // ships with Nova
            // show something custom in here when loading state changes     
        </loading-card>
    </loading-view>
</template>

<script>
export default {    
    data() {
        return {
            initialLoading: true,
            loading: false,
            languages: {}
        }
    },

    methods: {
        listLanguages() {
            Nova.request().get('/nova-vendor/nova-translation/languages')
                .then((response) => {
                    this.languages = response.data;
                    this.initialLoading = false;
                    this.loading = false;
                })
        }
    },

    created() {
        this.listLanguages()
    },
}
</script>

As you can see above, I use the LoadingView and LoadingCard components that ship with Nova. I then pass the loading state of my component as a prop and the loading state transition is handled for me. The result is the loading animation being rendered while my data is prepared.


Registering a Tool

With the tool built, the last stage of the process is to let Nova know about its existence by registering it to the application. This is done by adding the tool class to the registerTools method of the NovaServiceProvider.

protected function registerTools()
{
    Nova::tools([
        new Dashboard,
        new ResourceManager,
        new NovaTranslation,
    ]);
}

Laravel Community

Taking a step away from the technical implementation, I wanted to highlight something that happened during the development of this tool.

During the build, I wanted to implement the blue loading bar at the top of the page when carrying out an ajax-powered search. If you have used Nova before, it’s the same loader that appears when you search through any of your resources.

I was tired and burning the midnight oil trying to get it finished and could not figure it out. I decided to contact David on Twitter for some advice. Ten minutes later, I had a reply with exactly what I needed to know.

A big thank you to David for helping me through this issue, but also to the broader Laravel community. This is not, and certainly won’t be the last time I have been offered assistance from members of the community when in need.

Conclusion

The developer experience for Nova was fantastic. From not knowing where to start to have my package ported over only took a couple of evenings which is a testament to, not only the documentation but also the planning and execution of Nova itself. As with all things in the Laravel ecosystem, it’s been built with extreme care and attention to detail, making the onboarding experience for developers quick and painless. I, for one, can’t wait to start the next one!

If you want to look at the tool I built, check it out on Nova Packages and as always, if you have any questions, you can find me on Twitter.