Chapter 14. Storage and Retrieval

We looked at how to store data in relational databases in Chapter 5, but there’s a lot more that can be stored, both locally and remotely. In this chapter we’ll cover filesystem and in-memory storage, file uploads and manipulation, nonrelational data stores, sessions, the cache, logging, cookies, and full-text search.

Local and Cloud File Managers

Laravel provides a series of file manipulation tools through the Storage facade, and a few helper functions.

Laravel’s filesystem access tools can connect to the local filesystem as well as S3, Rackspace, and FTP. The S3 and Rackspace file drivers are provided by Flysystem, and it’s simple to add additional Flysystem providers to your Laravel app—for example, Dropbox or WebDAV.

Configuring File Access

The definitions for Laravel’s file manager live in config/filesystems.php. Each connection is called a “disk,” and Example 14-1 lists the disks that are available out of the box.

Example 14-1. Default available storage disks
...
'disks' => [
    'local' => [
        'driver' => 'local',
        'root' => storage_path('app'),
    ],

    'public' => [
        'driver' => 'local',
        'root' => storage_path('app/public'),
        'url' => env('APP_URL').'/storage',
        'visibility' => 'public',
    ],

    's3' => [
        'driver' => 's3',
        'key' => env('AWS_ACCESS_KEY_ID'),
        'secret' => env('AWS_SECRET_ACCESS_KEY'),
        'region' => env('AWS_DEFAULT_REGION'),
        'bucket' => env('AWS_BUCKET'),
        'url' => env('AWS_URL'),
    ],
],

The storage_path() Helper

The storage_path() helper used in Example 14-1 links to Laravel’s configured storage directory, storage/. Anything you pass to it is added to the end of the directory name, so storage_path('public') will return the string storage/public.

The local disk connects to your local storage system and presumes it will be interacting with the app directory of the storage path, which is storage/app.

The public disk is also a local disk (although you can change it if you’d like), which is intended for use with any files you intend to be served by your application. It defaults to the storage/app/public directory, and if you want to use this directory to serve files to the public, you’ll need to add a symbolic link (symlink) to somewhere within the public/ directory. Thankfully, there’s an Artisan command that maps public/storage to serve the files from storage/app/public:

php artisan storage:link

The s3 disk shows how Laravel connects to cloud-based file storage systems. If you’ve ever connected to S3 or any other cloud storage provider, this will be familiar; pass it your key and secret and some information defining the “folder” you’re working with, which in S3 is the region and the bucket.

Using the Storage Facade

In config/filesystem.php you can set the default disk, which is what will be used any time you call the Storage facade without specifying a disk. To specify a disk, call disk('diskname') on the facade:

Storage::disk('s3')->get('file.jpg');

The filesystems each provide the following methods:

get('file.jpg')

Retrieves the file at file.jpg

put('file.jpg', $contentsOrStream)

Puts the given file contents to file.jpg

putFile('myDir', $file)

Puts the contents of a provided file (in the form of an instance of either IlluminateHttpFile or IlluminateHttpUploadedFile) to the myDir directory, but with Laravel managing the entire streaming process and naming the file

exists('file.jpg')

Returns a Boolean indicating whether file.jpg exists

getVisibility('myPath')

Gets the visibility for the given path (“public” or “private”)

setVisibility('myPath')

Sets the visibility for the given path (“public” or “private”)

copy('file.jpg', 'newfile.jpg')

Copies file.jpg to newfile.jpg

move('file.jpg', 'newfile.jpg')

Moves file.jpg to newfile.jpg

prepend('my.log', 'log text')

Adds content at the beginning of my.log

append('my.log', 'log text')

Adds content to the end of my.log

delete('file.jpg')

Deletes file.jpg

size('file.jpg')

Returns the size in bytes of file.jpg

lastModified('file.jpg')

Returns the Unix timestamp when file.jpg was last modified

files('myDir')

Returns an array of filenames in the directory myDir

allFiles('myDir')

Returns an array of filenames in the directory myDir and all subdirectories

directories('myDir')

Returns an array of directory names in the directory myDir

allDirectories('myDir')

Returns an array of directory names in the directory myDir and all subdirectories

makeDirectory('myDir')

Creates a new directory

deleteDirectory('myDir')

Deletes myDir

Injecting an Instance

If you’d prefer injecting an instance instead of using the File facade, typehint or inject IlluminateFilesystemFilesystem and you’ll have all the same methods available to you.

Adding Additional Flysystem Providers

If you want to add an additional Flysystem provider, you’ll need to “extend” Laravel’s native storage system. In a service provider somewhere—it could be the boot() method of AppServiceProvider, but it’d be more appropriate to create a unique service provider for each new binding—use the Storage facade to add new storage systems, as seen in Example 14-2.

Example 14-2. Adding additional Flysystem providers
// Some service provider
public function boot()
{
    Storage::extend('dropbox', function ($app, $config) {
        $client = new DropboxClient(
            $config['accessToken'], $config['clientIdentifier']
        );

        return new Filesystem(new DropboxAdapter($client));
    });
}

Basic File Uploads and Manipulation

One of the more common usages for the Storage facade is accepting file uploads from your application’s users. Let’s look at a common workflow for that, in Example 14-3.

Example 14-3. Common user upload workflow
...
class DogController
{
    public function updatePicture(Request $request, Dog $dog)
    {
        Storage::put(
            "dogs/{$dog->id}",
            file_get_contents($request->file('picture')->getRealPath())
        );
    }
}

We put() to a file named dogs/id, and we grab our contents from the uploaded file. Every uploaded file is a descendant of the SplFileInfo class, which provides a getRealPath() method that returns the path to the file’s location. So, we get the temporary upload path for the user’s uploaded file, read it with file_get_contents(), and pass it into Storage::put().

Since we have this file available to us here, we can do anything we want to the file before we store it—use an image manipulation package to resize it if it’s an image, validate it and reject it if it doesn’t meet our criteria, or whatever else we like.

If we wanted to upload this same file to S3 and we had our credentials stored in config/filesystems.php, we could just adjust Example 14-3 to call Storage::disk('s3')->put(); we’ll now be uploading to S3. Take a look at Example 14-4 to see a more complex upload example.

Example 14-4. A more complex example of file uploads, using Intervention
...
class DogController
{
    public function updatePicture(Request $request, Dog $dog)
    {
        $original = $request->file('picture');

        // Resize image to max width 150
        $image = Image::make($original)->resize(150, null, function ($constraint) {
            $constraint->aspectRatio();
        })->encode('jpg', 75);

        Storage::put(
            "dogs/thumbs/{$dog->id}",
            $image->getEncoded()
        );
    }

I used an image library called Intervention in Example 14-4 just as an example; you can use any library you want. The important point is that you have the freedom to manipulate the files however you want before you store them.

Using store() and storeAs() on the Uploaded File

Laravel 5.3 introduced the ability to store an uploaded file using the file itself. Learn more in Example 7-12.

Simple File Downloads

Just like Storage makes it easy to accept uploads from users, it also simplifies the task of returning files to them. Take a look at Example 14-5 for the simplest example.

Example 14-5. Simple file downloads
public function downloadMyFile()
{
    return Storage::download('my-file.pdf');
}

Sessions

Session storage is the primary tool we use in web applications to store state between page requests. Laravel’s session manager supports session drivers using files, cookies, a database, Memcached or Redis, DynamoDB, or in-memory arrays (which expire after the page request and are only good for tests).

You can configure all of your session settings and drivers in config/session.php. You can choose whether or not to encrypt your session data, select which driver to use (file is the default), and specify more connection-specific details like the length of session storage and which files or database tables to use. Take a look at the session docs to learn about specific dependencies and settings you need to prepare for whichever driver you choose to use.

The general API of the session tools allows you to save and retrieve data based on individual keys: session()->put('user_id') and session()->get('user_id'), for example. Make sure to avoid saving anything to a flash session key, since Laravel uses that internally for flash (only available for the next page request) session storage.

Accessing the Session

The most common way to access the session is using the Session facade:

Session::get('user_id');

But you can also use the session() method on any given Illuminate Request object, as in Example 14-6.

Example 14-6. Using the session() method on a Request object
Route::get('dashboard', function (Request $request) {
    $request->session()->get('user_id');
});

Or you can inject an instance of IlluminateSessionStore, as in Example 14-7.

Example 14-7. Injecting the backing class for sessions
Route::get('dashboard', function (IlluminateSessionStore $session) {
    return $session->get('user_id');
});

Finally, you can use the global session() helper. Use it with no parameters to get a session instance, with a single string parameter to “get” from the session, or with an array to “put” to the session, as demonstrated in Example 14-8.

Example 14-8. Using the global session() helper
// Get
$value = session()->get('key');
$value = session('key');
// Put
session()->put('key', 'value');
session(['key', 'value']);

If you’re new to Laravel and not sure which to use, I’d recommend using the global helper.

Methods Available on Session Instances

The two most common methods are get() and put(), but let’s take a look at each of the available methods and their parameters:

session()->get($key, $fallbackValue)

get() pulls the value of the provided key out of the session. If there is no value attached to that key, it will return the fallback value instead (and if you don’t provide a fallback, it will return null). The fallback value can be a string or a closure, as you can see in the following examples:

$points = session()->get('points');

$points = session()->get('points', 0);

$points = session()->get('points', function () {
    return (new PointGetterService)->getPoints();
});
session()->put($key, $value)

put() stores the provided value in the session at the provided key:

session()->put('points', 45);

$points = session()->get('points');
session()->push($key, $value)

If any of your session values are arrays, you can use push() to add a value to the array:

session()->put('friends', ['Saúl', 'Quang', 'Mechteld']);

session()->push('friends', 'Javier');
session()->has($key)

has() checks whether there’s a value set at the provided key:

if (session()->has('points')) {
    // Do something
}

You can also pass an array of keys, and it only returns true if all of the keys exist.

session()->has() and Null Values

If a session value is set but the value is null, session()->has() will return false.

session()->exists($key)

exists() checks whether there’s a value set at the provided key, like has(), but unlike has(), it will return true even if the set value is null:

if (session()->exists('points')) {
    // returns true even if 'points' is set to null
}
session()->all()

all() returns an array of everything that’s in the session, including those values set by the framework. You’ll likely see values under keys like _token (CSRF tokens), _previous (previous page, for back() redirects), and flash (for flash storage).

session()->only()

only() returns an array of only the specified values in the session (introduced in 5.8.28).

session()->forget($key) and session()->flush()

forget() removes a previously set session value. flush() removes every session value, even those set by the framework:

session()->put('a', 'awesome');
session()->put('b', 'bodacious');

session()->forget('a');
// a is no longer set; b is still set
session()->flush();
// Session is now empty
session()->pull($key, $fallbackValue)

pull() is the same as get(), except that it deletes the value from the session after pulling it.

session()->regenerate()

It’s not common, but if you need to regenerate your session ID, regenerate() is there for you.

Flash Session Storage

There are three more methods we haven’t covered yet, and they all have to do with something called “flash” session storage.

One very common pattern for session storage is to set a value that you only want available for the next page load. For example, you might want to store a message like “Updated post successfully.” You could manually get that message and then wipe it on the next page load, but if you use this pattern a lot it can get wasteful. Enter flash session storage: keys that are expected to only last for a single page request.

Laravel handles the work for you, and all you need to do is use flash() instead of put(). These are the useful methods here:

session()->flash($key, $value)

flash() sets the session key to the provided value for just the next page request.

session()->reflash() and session()->keep($key)

If you need the previous page’s flash session data to stick around for one more request, you can use reflash() to restore all of it for the next request or keep($key) to just restore a single flash value for the next request. keep() can also accept an array of keys to reflash.

Cache

Caches are structured very similarly to sessions. You provide a key and Laravel stores it for you. The biggest difference is that the data in a cache is cached per application, and the data in a session is cached per user. That means caches are more commonly used for storing results from database queries, API calls, or other slow queries that can stand to get a little bit “stale.”

The cache configuration settings are available at config/cache.php. Just like with a session, you can set the specific configuration details for any of your drivers, and also choose which will be your default. Laravel uses the file cache driver by default, but you can also use Memcached or Redis, APC, DynamoDB, or a database, or write your own cache driver. Take a look at the cache docs to learn about specific dependencies and settings you need to prepare for whichever driver you choose to use.

Minutes or seconds for cache length

In versions of Laravel prior to 5.8, if you passed an integer to any cache methods to define the cache duration, it’d represent the number of minutes to cache the item. In 5.8+, as you’ll learn in the following section, it represents seconds.

Accessing the Cache

Just like with sessions, there are a few different ways to access a cache. You can use the facade:

$users = Cache::get('users');

Or you can get an instance from the container, as in Example 14-9.

Example 14-9. Injecting an instance of the cache
Route::get('users', function (IlluminateContractsCacheRepository $cache) {
    return $cache->get('users');
});

You can also use the global cache() helper (introduced in Laravel 5.3), as in Example 14-10.

Example 14-10. Using the global cache() helper
// Get from cache
$users = cache('key', 'default value');
$users = cache()->get('key', 'default value');
// Put for $seconds duration
$users = cache(['key' => 'value'], $seconds);
$users = cache()->put('key', 'value', $seconds);

If you’re new to Laravel and not sure which to use, I’d recommend using the global helper.

Methods Available on Cache Instances

Let’s take a look at the methods you can call on a Cache instance:

cache()->get($key, $fallbackValue) and
cache()->pull($key, $fallbackValue)

get() makes it easy to retrieve the value for any given key. pull() is the same as get() except it removes the cached value after retrieving it.

cache()->put($key, $value, $secondsOrExpiration)

put() sets the value of the specified key for a given number of seconds. If you’d prefer setting an expiration date/time instead of a number of seconds, you can pass a Carbon object as the third parameter:

cache()->put('key', 'value', now()->addDay());
cache()->add($key, $value)

add() is similar to put(), except if the value already exists, it won’t set it. Also, the method returns a Boolean indicating whether or not the value was actually added:

$someDate = now();
cache()->add('someDate', $someDate); // returns true
$someOtherDate = now()->addHour();
cache()->add('someDate', $someOtherDate); // returns false
cache()->forever($key, $value)

forever() saves a value to the cache for a specific key; it’s the same as put(), except the values will never expire (until they’re removed with forget()).

cache()->has($key)

has() returns a Boolean indicating whether or not there’s a value at the provided key.

cache()->remember($key, $seconds, $closure) and
cache()->rememberForever($key, $closure)

remember() provides a single method to handle a very common flow: look up whether a value exists in the cache for a certain key, and if it doesn’t, get that value somehow, save it to the cache, and return it.

remember() lets you provide a key to look up, the number of seconds it should be saved for, and a closure to define how to look it up, in case the key has no value set. rememberForever() is the same, except it doesn’t need you to set the number of seconds it should expire after. Take a look at the following example to see a common user scenario for remember():

// Either returns the value cached at "users" or gets "User::all()",
// caches it at "users", and returns it
$users = cache()->remember('users', 7200, function () {
    return User::all();
});
cache()->increment($key, $amount) and cache()->decrement($key, $amount)

increment() and decrement() allow you to increment and decrement integer values in the cache. If there is no value at the given key, it’ll be treated as if it were 0, and if you pass a second parameter to increment or decrement, it’ll increment or decrement by that amount instead of by 1.

cache()->forget($key) and cache()->flush()

forget() works just like Session’s forget() method: pass it a key and it’ll wipe that key’s value. flush() wipes the entire cache.

Cookies

You might expect cookies to work the same as sessions and the cache. A facade and a global helper are available for these too, and our mental models of all three are similar: you can get or set their values in the same way.

But because cookies are inherently attached to the requests and responses, you’ll need to interact with cookies differently. Let’s look really briefly at what makes cookies different.

Cookies in Laravel

Cookies can exist in three places in Laravel. They can come in via the request, which means the user had the cookie when they visited the page. You can read that with the Cookie facade, or you can read it off of the request object.

They can also be sent out with a response, which means the response will instruct the user’s browser to save the cookie for future visits. You can do this by adding the cookie to your response object before returning it.

And lastly, a cookie can be queued. If you use the Cookie facade to set a cookie, you have put it into a “CookieJar” queue, and it will be removed and added to the response object by the AddQueuedCookiesToResponse middleware.

Accessing the Cookie Tools

You can get and set cookies in three places: the Cookie facade, the cookie() global helper, and the request and response objects.

The Cookie facade

The Cookie facade is the most full-featured option, allowing you to not only read and make cookies, but also to queue them to be added to the response. It provides the following methods:

Cookie::get($key)

To pull the value of a cookie that came in with the request, you can just run Cookie::get('cookie-name'). This is the simplest option.

Cookie::has($key)

You can check whether a cookie came in with the request using Cookie::has('cookie-name'), which returns a Boolean.

Cookie::make(...params)

If you want to make a cookie without queueing it anywhere, you can use Cookie::make(). The most likely use for this would be to make a cookie and then manually attach it to the response object, which we’ll cover in a bit.

Here are the parameters for make(), in order:

  • $name is the name of the cookie.

  • $value is the content of the cookie.

  • $minutes specifies how many minutes the cookie should live.

  • $path is the path under which your cookie should be valid.

  • $domain lists the domains for which your cookie should work.

  • $secure indicates whether the cookie should only be transmitted over a secure (HTTPS) connection.

  • $httpOnly indicates whether the cookie will be made accessible only through the HTTP protocol.

  • $raw indicates whether the cookie should be sent without URL encoding.

  • $sameSite indicates whether the cookie should be available for cross-site requests; options are lax, strict, or null.

Cookie::make()

Returns an instance of SymfonyComponentHttpFoundationCookie.

Default Settings for Cookies

The CookieJar that the Cookie facade instance uses reads its defaults from the session config. So, if you change any of the configuration values for the session cookie in config/session.php, those same defaults will be applied to all of your cookies that you create using the Cookie facade.

Cookie::queue(Cookie || params)

If you use Cookie::make(), you’ll still need to attach the cookie to your response, which we’ll cover shortly. Cookie::queue() has the same syntax as Cookie::make(), but it enqueues the created cookie to be automatically attached to the response by middleware.

If you’d like, you can also just pass a cookie you’ve created yourself into Cookie::queue().

Here’s the simplest possible way to add a cookie to the response in Laravel:

Cookie::queue('dismissed-popup', true, 15);

When Your Queued Cookies Won’t Get Set

Cookies can only be returned as part of a response. So, if you enqueue cookies with the Cookie facade and then your response isn’t returned correctly—for example, if you use PHP’s exit() or something halts the execution of your script—your cookies won’t be set.

The cookie() global helper

The cookie() global helper will return a CookieJar instance if you call it with no parameters. However, two of the most convenient methods on the Cookie facade—has() and get()—exist only on the facade, not on the CookieJar. So, in this context, I think the global helper is actually less useful than the other options.

The one task for which the cookie() global helper is useful is creating a cookie. If you pass parameters to cookie(), they’ll be passed directly to the equivalent of Cookie::make(), so this is the fastest way to create a cookie:

$cookie = cookie('dismissed-popup', true, 15);

Injecting an Instance

You can also inject an instance of IlluminateCookieCookieJar anywhere in the app, but you’ll have the same limitations discussed here.

Cookies on Request and Response objects

Since cookies come in as a part of the request and are set as a part of the response, those Illuminate objects are the places they actually live. The Cookie facade’s get(), has(), and queue() methods are just proxies to interact with the Request and Response objects.

So, the simplest way to interact with cookies is to pull cookies from the request and set them on the response.

Reading cookies from Request objects

Once you have a copy of your Request object—if you don’t know how to get one, just try app('request')—you can use the Request object’s cookie() method to read its cookies, as shown in Example 14-11.

As you can see in this example, the cookie() method has two parameters: the cookie’s name and, optionally, the fallback value.

Setting cookies on Response objects

Whenever you have your Response object ready, you can use the cookie() method (or the withCookie() method in Laravel prior to 5.3) on it to add a cookie to the response, like in Example 14-12.

If you’re new to Laravel and not sure which option to use, I’d recommend setting cookies on the Request and Response objects. It’s a bit more work, but will lead to fewer surprises if future developers don’t understand the CookieJar queue.

Logging

We’ve seen a few really brief examples of logging so far in this book when we were talking about other concepts like the container and facades, but let’s briefly look at what options you have with logging beyond just Log::info('Message').

The purpose of logs is to increase “discoverability,” or your ability to understand what’s going on at any given moment in your application.

Logs are short messages, sometimes with some data embedded in a human-readable form, that your code will generate for the sake of understanding what was happening during the execution of the app. Each log must be captured at a specific level, which can vary from emergency (something very bad happened) to debug (something of almost no significance happened).

Without any modifications, your app will write any log statements to a file located at storage/logs/laravel.log, and each log statement will look a little bit like this:

[2018-09-22 21:34:38] local.ERROR: Something went wrong.

You can see we have the date, time, environment, error level, and message, all on one line. However, Laravel also (by default) logs any uncaught exceptions, and in that case you’ll see the entire stack trace inline.

We’ll cover how to log, why to log, and how to log elsewhere (for example, in Slack) in the following section.

When and Why to Use Logs

The most common use case for logs is to act as a semidisposable record of things that have happened that you may care about later, but to which you don’t definitively need programmatic access. The logs are more about learning what’s going on in the app and less about creating structured data your app can consume.

For example, if you want to have code that consumes a record of every user login and does something interesting with it, that’s a use case for a logins database table. However, if you have a casual interest in those logins but you’re not entirely certain whether you care or whether you need that information programmatically, you may just throw a debug- or info-level log on it and forget about it.

Logs are also common when you need to see the value of something at the moment it goes wrong, or at a certain time of day, or something else that means you want the data at a time when you’re not around. Throw a log statement in the code, get the data you need out of the logs, and either keep it in the code for later usage or just delete it again.

Writing to the Logs

The simplest way to write a log entry in Laravel is to use the Log facade, and use the method on that facade that matches the severity level you’d like to record. The levels are the same as those defined in RFC 5424:

Log::emergency($message);
Log::alert($message);
Log::critical($message);
Log::error($message);
Log::warning($message);
Log::notice($message);
Log::info($message);
Log::debug($message);

You can also, optionally, pass a second parameter that’s an array of connected data:

Log::error('Failed to upload user image.', ['user' => $user]);

This additional information may be captured differently by different log destinations, but here’s how this looks in the default local log (although it will be just a single line in the log):

[2018-09-27 20:53:31] local.ERROR: Failed to upload user image. {
    "user":"[object] (App\User: {
        "id":1,
        "name":"Matt",
        "email":"[email protected]",
        "email_verified_at":null,
        "api_token":"long-token-here",
        "created_at":"2018-09-22 21:39:55",
        "updated_at":"2018-09-22 21:40:08"
    })"
}

Log Channels

In Laravel 5.6, the way we configure and capture logs was changed pretty significantly to introduce the idea of multiple channels and drivers. If you’re working in 5.5 or earlier, you can skip on to “Full-Text Search with Laravel Scout”.

Like many other aspects of Laravel (file storage, database, mail, etc.), you can configure your logs to use one or more predefined log types, which you define in the config file. Using each type involves passing various configuration details to a specific log driver.

These log types are called channels, and out of the box you’ll have options for stack, single, daily, slack, stderr, syslog, and errorlog. Each channel is connected to a single driver; the available drivers are single, daily, slack, syslog, errorlog, monolog, custom, or stack.

We’ll cover the most common channels, here: stack, single, daily, and slack. To learn more about the drivers and the full list of channels available, take a look at the logging docs.

The single channel

The single channel writes every log entry to a single file, which you’ll define in the path key. You can see its default configuration here in Example 14-13:

Example 14-13. Default configuration for the single channel
'single' => [
    'driver' => 'single',
    'path' => storage_path('logs/laravel.log'),
    'level' => 'debug',
],

This means it’ll only log events at the debug level or higher, and it will write them all to a single file, storage/logs/laravel.log.

The daily channel

The daily channel splits out a new file for each day. You can see its default config here in Example 14-14.

Example 14-14. Default configuation for the daily channel
'daily' => [
    'driver' => 'daily',
    'path' => storage_path('logs/laravel.log'),
    'level' => 'debug',
    'days' => 7,
],

It’s similar to single, but we now can set how many days of logs to keep before they’re cleaned up, and the date will be appended to the filename we specify. For example, the preceding config will generate a file named storage/logs/laravel-{yyyy-mm-dd}.log.

The slack channel

The slack channel makes it easy to send your logs (or, more likely, only certain logs) over to Slack.

It also illustrates that you’re not limited to just the handlers that come out of the box with Laravel. We’ll cover this in a second, but this isn’t a custom Slack implementation; it’s just Laravel building a log driver that connects to the Monolog Slack handler, and if you can use any Monolog handler, you have a lot of options available to you.

The default configuration for this channel is shown in Example 14-15.

Example 14-15. Default configuation for the slack channel
'slack' => [
    'driver' => 'slack',
    'url' => env('LOG_SLACK_WEBHOOK_URL'),
    'username' => 'Laravel Log',
    'emoji' => ':boom:',
    'level' => 'critical',
],

The stack channel

The stack channel is the channel that’s enabled by default on your application. Its default configuation in 5.7+ is shown in Example 14-16.

Example 14-16. Default configuration for the stack channel
'stack' => [
    'driver' => 'stack',
    'channels' => ['daily'],
    'ignore_exceptions' => false,
],

The stack channel allows you to send all your logs to more than one channel (listed in the channels array). So, while this is the channel that’s configured by default on your Laravel apps, because its channels array is set to daily by default in 5.8+, in reality your app is just using the daily log channel.

But what if you wanted everything of the level info and above to go to the daily files, but you wanted critical and higher log messages to go to Slack? It’s easy with the stack driver, as Example 14-17 demonstrates.

Example 14-17. Customizing the stack driver
'channels' => [
    'stack' => [
        'driver' => 'stack',
        'channels' => ['daily', 'slack'],
    ],

    'daily' => [
        'driver' => 'daily',
        'path' => storage_path('logs/laravel.log'),
        'level' => 'info',
        'days' => 14,
    ],

    'slack' => [
        'driver' => 'slack',
        'url' => env('LOG_SLACK_WEBHOOK_URL'),
        'username' => 'Laravel Log',
        'emoji' => ':boom:',
        'level' => 'critical',
    ],

Writing to specific log channels

There may also be times when you want to control exactly which log messages go where. You can do that, too. Just specify the channel when you call the Log facade:

Log::channel('slack')->info("This message will go to Slack.");

Advanced Log Configuration

If you’d like to customize how each log is sent to each channel, or implement custom Monolog handlers, check out the logging docs to learn more.

Full-Text Search with Laravel Scout

Laravel Scout is a separate package that you can bring into your Laravel apps to add full-text search to your Eloquent models. Scout makes it easy to index and search the contents of your Eloquent models; it ships with an Algolia driver, but there are also community packages for other providers. I’ll assume you’re using Algolia.

Installing Scout

First, pull in the package in any Laravel 5.3+ app:

composer require laravel/scout

Manually Registering Service Providers Prior to Laravel 5.5

If you’re using a version of Laravel prior to 5.5, you will need to manually register the service provider by adding LaravelScoutScoutServiceProvider::class to the providers section of config/app.php.

Next you’ll want to set up your Scout configuration. Run this command:

php artisan vendor:publish --provider="LaravelScoutScoutServiceProvider"

and paste your Algolia credentials in config/scout.php.

Finally, install the Algolia SDK:

composer require algolia/algoliasearch-client-php

Marking Your Model for Indexing

In your model (we’ll use Review, for a book review, for this example), import the LaravelScoutSearchable trait.

You can define which properties are searchable using the toSearchableArray() method (it defaults to mirroring toArray()), and define the name of the model’s index using the searchableAs() method (it defaults to the table name).

Scout subscribes to the create/delete/update events on your marked models. When you create, update, or delete any rows, Scout will sync those changes up to Algolia. It’ll either make those changes synchronously with your updates or, if you configure Scout to use a queue, queue the updates.

Searching Your Index

Scout’s syntax is simple. For example, to find any Review with the word Llew in it:

Review::search('Llew')->get();

You can also modify your queries as you would with regular Eloquent calls:

// Get all records from the Review that match the term "Llew",
// limited to 20 per page and reading the page query parameter,
// just like Eloquent pagination
Review::search('Llew')->paginate(20);

// Get all records from the Review that match the term "Llew"
// and have the account_id field set to 2
Review::search('Llew')->where('account_id', 2)->get();

What comes back from these searches? A collection of Eloquent models, rehydrated from your database. The IDs are stored in Algolia, which returns a list of matched IDs; Scout then pulls the database records for those and returns them as Eloquent objects.

You don’t have full access to the complexity of SQL WHERE commands, but it provides a basic framework for comparison checks like you can see in the code samples here.

Queues and Scout

At this point your app will be making HTTP requests to Algolia on every request that modifies any database records. This can slow down your application quickly, which is why Scout makes it easy to push all of its actions onto a queue.

In config/scout.php, set queue to true so that these updates are set to be indexed asynchronously. Your full-text index is now operating under “eventual consistency”; your database records will receive the updates immediately, and the updates to your search indexes will be queued and updated as fast as your queue worker allows.

Performing Operations Without Indexing

If you need to perform a set of operations and avoid triggering the indexing in response, wrap the operations in the withoutSyncingToSearch() method on your model:

Review::withoutSyncingToSearch(function () {
    // Make a bunch of reviews, e.g.
    factory(Review::class, 10)->create();
});

Conditionally Indexing Models

Sometimes you might only want to index records if they meet a certain condition. You may use the shouldBeSearchable() method on the model class to achieve this:

public function shouldBeSearchable()
{
    return $this->isApproved();
}

Manually Triggering Indexing via Code

If you want to manually trigger indexing your model, you can do it using code in your app or via the command line.

To manually trigger indexing from your code, add searchable() to the end of any Eloquent query and it will index all of the records that were found in that query:

Review::all()->searchable();

You can also choose to scope the query to only those records you want to index. However, Scout is smart enough to insert new records and update old records, so you may choose to just reindex the entire contents of the model’s database table.

You can also run searchable() on relationship methods:

$user->reviews()->searchable();

If you want to unindex any records with the same sort of query chaining, just use unsearchable() instead:

Review::where('sucky', true)->unsearchable();

Manually Triggering Indexing via the CLI

You can also trigger indexing with an Artisan command:

php artisan scout:import "AppReview"

This will chunk all of the Review models and index them all.

Testing

Testing most of these features is as simple as using them in your tests; no need to mock or stub. The default configuration will already work—for example, take a look at phpunit.xml to see that your session driver and cache driver have been set to values appropriate for tests.

However, there are a few convenience methods and a few gotchas that you should know about before you attempt to test them all.

File Storage

Testing file uploads can be a bit of a pain, but follow these steps and it will be clear.

Uploading fake files

First, let’s look at how to manually create an IlluminateHttpUploadedFile object for use in our application testing (Example 14-18).

Example 14-18. Creating a fake UploadedFile for testing
public function test_file_should_be_stored()
{
    Storage::fake('public');

    $file = UploadedFile::fake()->image('avatar.jpg');

    $response = $this->postJson('/avatar', [
        'avatar' => $file,
    ]);

    // Assert the file was stored
    Storage::disk('public')->assertExists("avatars/{$file->hashName()}");

    // Assert a file does not exist
    Storage::disk('public')->assertMissing('missing.jpg');
}

We’ve created a new instance of UploadedFile that refers to our testing file, and we can now use it to test our routes.

Returning fake files

If your route is expecting a real file to exist, sometimes the best way to make it testable is to make that real file actually exist. Let’s say every user must have a profile picture.

First, let’s set up the model factory for the user to use Faker to make a copy of the picture, as in Example 14-19.

Example 14-19. Returning fake files with Faker
$factory->define(User::class, function (FakerGenerator $faker) {
    return [
        'picture' => $faker->file(
            storage_path('tests'), // Source directory
            storage_path('app'), // Target directory
            false // Return just filename, not full path
        ),
        'name' => $faker->name,
    ];
});

Faker’s file() method picks a random file from the source directory and copies it to the target directory, and then returns the filename. So, we’ve just picked a random file from the storage/tests directory, copied it to the storage/app directory, and set its filename as the picture property on our User. At this point we can use a User in tests on routes that expect the User to have a picture, as seen in Example 14-20.

Example 14-20. Asserting that an image’s URL is echoed
public function test_user_profile_picture_echoes_correctly()
{
    $user = factory(User::class)->create();

    $response = $this->get(route('users.show', $user->id));

    $response->assertSee($user->picture);
}

Of course, in many contexts you can just generate a random string there without even copying a file. But if your routes check for the file’s existence or run any operations on the file, this is your best option.

Session

If you need to assert something has been set in the session, you can use some convenience methods Laravel makes available in every test. All of these methods are available in your tests on the IlluminateFoundationTestingTestResponse object:

assertSessionHas($key, $value = null)

Asserts that the session has a value for a particular key, and, if the second parameter is passed, that that key is a particular value:

public function test_some_thing()
{
    // Do stuff that ends up with a $response object...
    $response->assertSessionHas('key', 'value');
}
assertSessionHasAll(array $bindings)

If passed an array of key/value pairs, asserts that all of the keys are equal to all of the values. If one or more of the array entries is just a value (with PHP’s default numeric key), it will just be checked for existence in the session:

$check = [
    'has',
    'hasWithThisValue' => 'thisValue',
];

$response->assertSessionHasAll($check);
assertSessionMissing($key)

Asserts that the session does not have a value for a particular key.

assertSessionHasErrors($bindings = [], $format = null)

Asserts that the session has an errors value. This is the key Laravel uses to send errors back from validation failures.

If the array contains just keys, it will check that errors are set with those keys:

$response = $this->post('test-route', ['failing' => 'data']);
$response->assertSessionHasErrors(['name', 'email']);

You can also pass values for those keys, and optionally a $format, to check that the messages for those errors came back the way you expected:

$response = $this->post('test-route', ['failing' => 'data']);
$response->assertSessionHasErrors([
    'email' => '<strong>The email field is required.</strong>',
], '<strong>:message</strong>');

Cache

There’s nothing special about testing your features that use the cache—just do it:

Cache::put('key', 'value', 900);

$this->assertEquals('value', Cache::get('key'));

Laravel uses the array cache driver by default in your testing environment, which just stores your cache values in memory.

Cookies

What if you need to set a cookie before testing a route in your application tests? You can manually pass cookies to one of the parameters of the call() method. To learn more about call(), check out Chapter 12.

Excluding Your Cookie from Encryption During Testing

Your cookies won’t work in your tests unless you exclude them from Laravel’s cookie encryption middleware. You can do this by teaching the EncryptCookies middleware to temporarily disable itself for those cookies:

use IlluminateCookieMiddlewareEncryptCookies;
...

$this->app->resolving(
    EncryptCookies::class,
    function ($object) {
        $object->disableFor('cookie-name');
    }
);

// ...run test

That means you can set and check against a cookie with something like Example 14-21.

Example 14-21. Running unit tests against cookies
public function test_cookie()
{
    $this->app->resolving(EncryptCookies::class, function ($object) {
        $object->disableFor('my-cookie');
    });

    $response = $this->call(
        'get',
        'route-echoing-my-cookie-value',
        [],
        ['my-cookie' => 'baz']
    );
    $response->assertSee('baz');
}

If you want to test that a response has a cookie set, you can use assertCookie() to test for the cookie:

$response = $this->get('cookie-setting-route');
$response->assertCookie('cookie-name');

Or you could use assertPlainCookie() to test for the cookie and to assert that it’s not encrypted.

Different Names for Testing Methods Prior to Laravel 5.4

In projects running versions of Laravel 5.4 assertCookie() should be replaced by seeCookie(), and assertPlainCookie() should be replaced by seePlainCookie().

Log

The simplest way to test that a certain log was written is by making assertions against the Log facade (learn more in “Faking Other Facades”). Example 14-22 shows how this works.

Example 14-22. Making assertions against the Log facade
// Test file
public function test_new_accounts_generate_log_entries()
{
    Log::shouldReceive('info')
        ->once()
        ->with('New account created!');

    // Create a new account
    $this->post(route('accounts.store'), ['email' => '[email protected]']);
}

// AccountController
public function store()
{
    // Create account

    Log::info('New account created!');
}

There’s also a package called Log Fake that expands on what you can do with the facade testing shown here and allows you to write more customized assertions against your logs.

Scout

If you need to test code that uses Scout data, you’re probably not going to want your tests triggering indexing actions or reading from Scout. Simply add an environment variable to your phpunit.xml to disable Scout’s connection to Algolia:

<env name="SCOUT_DRIVER" value="null"/>

TL;DR

Laravel provides simple interfaces to many common storage operations: filesystem access, sessions, cookies, the cache, and search. Each of these APIs is the same regardless of which provider you use, which Laravel enables by allowing multiple “drivers” to serve the same public interface. This makes it simple to switch providers depending on the environment, or as the needs of the application change.

..................Content has been hidden....................

You can't read the all page of ebook, please click here login for view all page.
Reset