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.
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.
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.
...
'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 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.
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')
put('file.jpg', $contentsOrStream)
putFile('myDir', $file)
Puts the contents of a provided file (in the form of an instance of either Illuminate
HttpFile
or IlluminateHttpUploadedFile
) to the myDir
directory, but with Laravel managing the entire streaming process and naming the file
exists('file.jpg')
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')
move('file.jpg', 'newfile.jpg')
prepend('my.log', 'log text')
append('my.log', 'log text')
delete('file.jpg')
size('file.jpg')
lastModified('file.jpg')
files('myDir')
allFiles('myDir')
Returns an array of filenames in the directory myDir
and all subdirectories
directories('myDir')
allDirectories('myDir')
Returns an array of directory names in the directory myDir
and all subdirectories
makeDirectory('myDir')
deleteDirectory('myDir')
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.
// 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
));
});
}
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.
...
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.
...
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.
Laravel 5.3 introduced the ability to store an uploaded file using the file itself. Learn more in Example 7-12.
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.
public
function
downloadMyFile
()
{
return
Storage
::
download
(
'my-file.pdf'
);
}
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.
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.
Route
::
get
(
'dashboard'
,
function
(
Request
$request
)
{
$request
->
session
()
->
get
(
'user_id'
);
});
Or you can inject an instance of IlluminateSessionStore
, as in Example 14-7.
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.
// 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.
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.
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.
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.
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.
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.
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.
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.
// 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.
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.
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 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.
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 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
.
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
);
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 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
);
You can also inject an instance of IlluminateCookieCookieJar
anywhere in the app, but you’ll have the same limitations discussed here.
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.
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.
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.
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.
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.
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" })" }
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 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:
'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 splits out a new file for each day. You can see its default config here in Example 14-14.
'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 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.
'slack'
=>
[
'driver'
=>
'slack'
,
'url'
=>
env
(
'LOG_SLACK_WEBHOOK_URL'
),
'username'
=>
'Laravel Log'
,
'emoji'
=>
':boom:'
,
'level'
=>
'critical'
,
],
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.
'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.
'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'
,
],
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."
);
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.
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.
First, pull in the package in any Laravel 5.3+ app:
composer require laravel/scout
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
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.
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.
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.
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
();
});
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
();
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.
Testing file uploads can be a bit of a pain, but follow these steps and it will be clear.
First, let’s look at how to manually create an IlluminateHttpUploadedFile
object for use in our application testing (Example 14-18).
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.
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.
$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.
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.
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>'
);
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.
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.
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.
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.
// 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.
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.