Websites that benefit from a framework like Laravel often don’t just serve static content. Many deal with complex and mixed data sources, and one of the most common (and most complex) of these sources is user input in its myriad forms: URL paths, query parameters, POST data, and file uploads.
Laravel provides a collection of tools for gathering, validating, normalizing, and filtering user-provided data. We’ll look at those here.
The most common tool for accessing user data in Laravel is injecting an instance of the Illuminate\Http\Request object. It provides easy access to all of the ways users can provide input to your site: POST form data, posted JSON, GET (query parameters), and URL segments.
There’s also a request() global helper and a Request facade, both of which expose the same methods. Each of these options exposes the entire Illuminate Request object, but for now we’re only going to cover the methods that specifically relate to user data.
Since we’re planning on injecting a Request object, let’s take a quick look at how to get the $request object we’ll be calling all these methods on:
Route::post('form',function(Illuminate\Http\Request$request){// $request->etc()});
Just like the name suggests, $request->all() gives you an array containing all of the input the user has provided, from every source. Let’s say, for some reason, you decided to have a form POST to a URL with a query parameter—e.g., sending a POST to http://myapp.com/signup?utm=12345. Take a look at Example 7-1 to see what you’d get from $request->all(). (Note that $request->all() also contains information about any files that were uploaded, but we’ll cover that later in the chapter.)
<!-- GET route form view at /get-route --><formmethod="post"action="/signup?utm=12345">@csrf<inputtype="text"name="first_name"><inputtype="submit"></form>
// routes/web.phpRoute::post('signup',function(Request$request){var_dump($request->all());});// Outputs:/*** [* '_token' => 'CSRF token here',* 'first_name' => 'value',* 'utm' => 12345,* ]*/
$request->except() provides the same output as $request->all, but you can choose one or more fields to exclude—for example, _token. You can pass it either a string or an array of strings.
Example 7-2 shows what it looks like when we use $request->except() on the same form as in Example 7-1.
Route::post('post-route',function(Request$request){var_dump($request->except('_token'));});// Outputs:/*** [* 'firstName' => 'value',* 'utm' => 12345,* ]*/
$request->only() is the inverse of $request->except(), as you can see in Example 7-3.
Route::post('post-route',function(Request$request){var_dump($request->only(['firstName','utm']));});// Outputs:/*** [* 'firstName' => 'value',* 'utm' => 12345,* ]*/
With $request->has() you can detect whether a particular piece of user input is available to you. Check out Example 7-4 for an analytics example with our utm query string parameter from the previous examples.
// POST route at /post-routeif($request->has('utm')){// Do some analytics work}
Whereas $request->all(), $request->except(), and $request->only() operate on the full array of input provided by the user, $request->input() allows you to get the value of just a single field. Example 7-5 provides an example. Note that the second parameter is the default value, so if the user hasn’t passed in a value, you can have a sensible (and nonbreaking) fallback.
Route::post('post-route',function(Request$request){$userName=$request->input('name','Matt');});
Returns the HTTP verb, or checks against it, for the request.
$method=$request->method();if($request->isMethod('patch')){// do something if request method is PATCH}
Laravel also provides convenience helpers for accessing data from array input. Just use the “dot” notation to indicate the steps of digging into the array structure, like in Example 7-7.
<!-- GET route form view at /employees/create --><formmethod="post"action="/employees/">@csrf<inputtype="text"name="employees[0][firstName]"><inputtype="text"name="employees[0][lastName]"><inputtype="text"name="employees[1][firstName]"><inputtype="text"name="employees[1][lastName]"><inputtype="submit"></form>
// POST route at /employeesRoute::post('employees',function(Request$request){$employeeZeroFirstName=$request->input('employees.0.firstName');$allLastNames=$request->input('employees.*.lastName');$employeeOne=$request->input('employees.1');var_dump($employeeZeroFirstname,$allLastNames,$employeeOne);});// If forms filled out as "Jim" "Smith" "Bob" "Jones":// $employeeZeroFirstName = 'Jim';// $allLastNames = ['Smith', 'Jones'];// $employeeOne = ['firstName' => 'Bob', 'lastName' => 'Jones'];
So far we’ve covered input from query strings (GET) and form submissions (POST). But there’s another form of user input that’s becoming more common with the advent of JavaScript single-page apps (SPAs): the JSON request. It’s essentially just a POST request with the body set to JSON instead of a traditional form POST.
Let’s take a look at what it might look like to submit some JSON to a Laravel route, and how to use $request->input() to pull out that data (Example 7-8).
POST/post-routeHTTP/1.1Content-Type:application/json{"firstName":"Joe","lastName":"Schmoe","spouse":{"firstName":"Jill","lastName":"Schmoe"}}
// post-routeRoute::post('post-route',function(Request$request){$firstName=$request->input('firstName');$spouseFirstname=$request->input('spouse.firstName');});
Since $request->input() is smart enough to pull user data from GET, POST, or JSON, you may wonder why Laravel even offers $request->json(). There are two reasons you might prefer $request->json(). First, you might want to just be more explicit to other programmers on your project about where you’re expecting the data to come from. And second, if the POST doesn’t have the correct application/json headers, $request->input() won’t pick it up as JSON, but $request->json() will.
It might not be the first thing you think of when you imagine “user data,” but the URL is just as much user data as anything else in this chapter.
There are two primary ways you’ll get data from the URL: via Request objects an via route parameters.
Injected Request objects (and the Request facade and the request() helper) have several methods available to represent the state of the current page’s URL, but right now let’s look primarily at getting information about the URL segments.
If you’re not familiar with the idea of URL segments, each group of characters after the domain is called a segment. So, http://www.myapp.com/users/15/ has two segments: users and 15.
As you can probably guess, we have two methods available to us: $request->segments() returns an array of all segments, and $request->segment($segmentId) allows us to get the value of a single segment. Note that segments are returned on a 1-based index, so in the preceding example, $request->segment(1) would return users.
Request objects, the Request facade, and the request() global helper provide quite a few more methods to help us get data out of the URL. To learn more, check out Chapter 10.
The other primary way we get data about the URL is from route parameters, which are injected into the controller method or closure that is serving a current route as shown in Example 7-9.
// routes/web.phpRoute::get('users/{id}',function($id){// If the user visits myapp.com/users/15/, $id will equal 15});
To learn more about routes and route binding, check out Chapter 3.
We’ve talked about different ways to interact with users’ text input, but there’s also the matter of file uploads to consider. Request objects provide access to any uploaded files using the $request->file() method, which takes the file’s input name as a parameter and returns an instance of Symfony\Component\HttpFoundation\File\UploadedFile.
Let’s walk through an example. First, our form, in Example 7-10.
<formmethod="post"enctype="multipart/form-data">@csrf<inputtype="text"name="name"><inputtype="file"name="profile_picture"><inputtype="submit"></form>
Now, let’s take a look at what we get from running $request->all(), in Example 7-11. Note that $request->input('profile_picture') will return null; we need to use $request->file('profile_picture') instead.
Route::post('form',function(Request$request){var_dump($request->all());});// Output:// [// "_token" => "token here"// "name" => "asdf"// "profile_picture" => UploadedFile {}// ]Route::post('form',function(Request$request){if($request->hasFile('profile_picture')){var_dump($request->file('profile_picture'));}});// Output:// UploadedFile (details)
Symfony’s UploadedFile class extends PHP’s native SplFileInfo with methods allowing you to easily inspect and manipulate the file. This list isn’t exhaustive, but it gives you a taste of what you can do:
guessExtension()
getMimeType()
store($path, $storageDisk = default disk)
storeAs($path, $newName, $storageDisk = default disk)
storePublicly($path, $storageDisk = default disk)
storePubliclyAs($path, $newName, $storageDisk = default disk)
move($directory, $newName = null)
getClientOriginalName()
getClientOriginalExtension()
getClientMimeType()
guessClientExtension()
getClientSize()
getError()
isValid()
As you can see, most of the methods have to do with getting information about the uploaded file, but there’s one that you’ll likely use more than all the others: store() (available since Laravel 5.3), which takes the file that was uploaded with the request and stores it in a specified directory on your server. Its first parameter is the destination directory, and the optional second parameter will be the storage disk (s3, local, etc.) to use to store the file.
You can see a common workflow in Example 7-12.
if($request->hasFile('profile_picture')){$path=$request->profile_picture->store('profiles','s3');auth()->user()->profile_picture=$path;auth()->user()->save();}
If you need to specify the filename, you can use storeAs() instead of store(). The first parameter is still the path; the second is the filename, and the optional third parameter is the storage disk to use.
If you get null when you try to get the contents of a file from your request, you might’ve forgotten to set the encoding type on your form. Make sure to add the attribute enctype="multipart/form-data" on your form:
<formmethod="post"enctype="multipart/form-data">
Laravel has quite a few ways you can validate incoming data. We’ll cover form requests in the next section, so that leaves us with two primary options: validating manually or using the validate() method on the Request object. Let’s start with the simpler, and more common, validate().
The Request object has a validate() method that provides a convenient shortcut for the most common validation workflow. Let’s take a look in Example 7-13.
// routes/web.phpRoute::get('recipes/create','RecipesController@create');Route::post('recipes','RecipesController@store');
classRecipesControllerextendsController{publicfunctioncreate(){returnview('recipes.create');}publicfunctionstore(Request$request){$request->validate(['title'=>'required|unique:recipes|max:125','body'=>'required']);// Recipe is valid; proceed to save it}}
We only have four lines of code running our validation here, but they’re doing a lot.
First, we’re explicitly defining the fields we expect and applying rules (here separated by the pipe character, |) to each individually.
Next, the validate() method checks the incoming data from the $request and determines whether or not it is valid.
If the data is valid, the validate method ends and we can move on with the controller method, saving the data or whatever else.
But if the data isn’t valid, it throws a ValidationException. This contains instructions to the router about how to handle this exception. If the request is from JavaScript (or if it’s requesting JSON as a response), the exception will create a JSON response containing the validation errors. If not, the exception will return a redirect to the previous page, together with all of the user input and the validation errors—perfect for repopulating a failed form and showing some errors.
In projects running versions of Laravel prior to 5.5, this validation shortcut is on the controller (running $this->validate()) instead of on the request.
If you are not working in a controller, or if for some other reason the previously described flow is not a good fit, you can manually create a Validator instance using the Validator facade and check for success or failure like in Example 7-14.
Route::get('recipes/create',function(){returnview('recipes.create');});Route::post('recipes',function(Illuminate\Http\Request$request){$validator=Validator::make($request->all(),['title'=>'required|unique:recipes|max:125','body'=>'required']);if($validator->fails()){returnredirect('recipes/create')->withErrors($validator)->withInput();}// Recipe is valid; proceed to save it});
As you can see, we create an instance of a validator by passing it our input as the first parameter and the validation rules as the second parameter. The validator exposes a fails() method that we can check against and can be passed into the withErrors() method of the redirect.
If the validation rule you need doesn’t exist in Laravel, you can create your own. To create a custom rule, run php artisan make:rule RuleName and then edit that file in app/Rules/RuleName.php.
You’ll get two methods in your rule out of the box: passes() and message(). passes() should accept an attribute name as the first parameter and the user-provided value as the second, and then return a boolean of whether or not this input passes this validation rule. message() should return the validation error message; you can use :attribute as a placeholder in your message for the attribute name.
Take a look at Example 7-15 as an example.
classWhitelistedEmailDomainimplementsRule{publicfunctionpasses($attribute,$value){returnin_array(str_after($value,'@'),['tighten.co']);}publicfunctionmessage(){return'The :attribute field is not from a whitelisted email provider.';}}
To use this rule, just pass an instance of the rule object to your validator:
$request->validate(['email'=>newWhitelistedEmailDomain]);
In projects running versions of Laravel prior to 5.5, custom validation rules have to be written using Validator::extend. You can learn more about how in the docs: https://laravel.com/docs/5.4/validation#custom-validation-rules
We’ve already covered much of this in Chapter 6, but here’s a quick refresher on how to display errors from validation.
The validate() method on requests (and the withErrors() method on redirects that it relies on) flashes any errors to the session. These errors are made available to the view you’re being redirected to in the $errors variable. And remember that as a part of Laravel’s magic, that $errors variable will be available every time you load the view, even if it’s just empty, so you don’t have to check if it exists with isset().
That means you can do something like Example 7-16 on every page.
@if ($errors->any())
<ul id="errors">
@foreach ($errors->all() as $error)
<li>{{ $error }}</li>
@endforeach
</ul>
@endifAs you build out your applications, you might start noticing some patterns in your controller methods. There are certain patterns that are repeated—for example, input validation, user authentication and authorization, and possible redirects. If you find yourself wanting a structure to normalize and extract these common behaviors out of your controller methods, you may be interested in Laravel’s form requests.
A form request is a custom request class that is intended to map to the submission of a form, and the request takes the responsilibity for validating the request, authorizing the user, and optionally redirecting the user upon a failed validation. Each form request will usually, but not always, explicitly map to a single HTTP request—e.g., “Create Comment.”
You can create a new form request from the command line:
php artisan make:request CreateCommentRequest
You now have a form request object available at app/Http/Requests/CreateCommentRequest.php.
Every form request class provides either one or two public methods. The first is rules(), which needs to return an array of validation rules for this request. The second (optional) method is authorize(); if this returns true, the user is authorized to perform this request, and if false, the user is rejected. Take a look at Example 7-17 to see a sample form request.
<?phpnamespaceApp\Http\Requests;useApp\BlogPost;useIlluminate\Foundation\Http\FormRequest;classCreateCommentRequestextendsFormRequest{publicfunctionauthorize(){$blogPostId=$this->route('blogPost');returnauth()->check()&&BlogPost::where('id',$blogPostId)->where('user_id',auth()->id())->exists();}publicfunctionrules(){return['body'=>'required|max:1000'];}}
The rules() section of Example 7-17 is pretty self-explanatory, but let’s look at authorize() briefly.
We’re grabbing the segment from the route named blogPost. That’s implying the route definition for this route probably looks a bit like this: Route::post('blogPosts/{blogPost}', function () { // Do stuff }). As you can see, we named the route parameter blogPost, which makes it accessible in our Request using $this->route('blogPost').
We then look at whether the user is logged in and, if so, whether any blog posts exist with that identifier that are owned by the currently logged-in user. You’ve already learned some easier ways to check ownership in Chapter 5, but we’ll keep it more explicit here to keep it clean. We’ll cover what implications this has shortly, but the important thing to know is that returning true means the user is authorized to perform the specified action (in this case, creating a comment), and false means the user is not authorized.
In projects running versions of Laravel prior to 5.3, form requests extended App\Http\Requests\Request instead of Illuminate\Foundation\Http\FormRequest.
Now that we’ve created a form request object, how do we use it? It’s a little bit of Laravel magic. Any route (closure or controller method) that typehints a form request as one of its parameters will benefit from the definitions of that form request.
Let’s try it out, in Example 7-18.
Route::post('comments',function(App\Http\Requests\CreateCommentRequest$request){// Store comment});
You might be wondering where we call the form request, but Laravel does it for us. It validates the user input and authorizes the request. If the input is invalid, it’ll act just like the request object validate() method works, redirecting the user to the previous page with their input preserved and with the appropriate error messages passed along. And if the user is not authorized, Laravel will return a 403 Forbidden error and not execute the route code.
Until now, we’ve been looking at validating at the controller level, which is absolutely the best place to start. But you can also filter the incoming data at the model level.
It’s a common (but not recommended) pattern to pass the entirety of a form’s input directly to a database model. In Laravel, that might look like Example 7-19.
Route::post('posts',function(Request$request){$newPost=Post::create($request->all());});
We’re assuming here that the end user is kind and not malicious, and has kept only the fields we want him to edit—maybe the post title or body.
But what if our end user can guess, or discern, that we have an author_id field on that posts table? What if he used his browser tools to add an author_id field and set the ID to be someone else’s ID, and the other person impersonated the other person by creating fake blog posts attributed to her?
Eloquent has a concept called “mass assignment” that allows you to either whitelist fields that should be fillable by being passed as a part of an array to create() or update() methods (using the model’s $fillable property) or blacklist fields that shouldn’t be fillable (using the model’s $guarded property). Check out Chapter 5 to learn more.
In our example, we might want to fill out the model like Example 7-20 to keep our app safe.
<?phpnamespaceApp;useIlluminate\Database\Eloquent\Model;classPostextendsModel{// Disable mass assignment on the author_id fieldprotected$guarded=['author_id'];}
By setting author_id to guarded, we ensure that malicious users will no longer be able to override the value of this field by manually adding it to the contents of a form that they’re sending to our app.
While it’s important to do a good job of protecting our models from mass assignment, it’s also worth being careful on the assigning end. Rather than using $request->all(), consider $request->only() so you can specify which fields you’d like to pass into your model:
Route::post('posts',function(Request$request){$newPost=Post::create($request->only(['title','body',]));});
Any time you display content on a web page that was created by a user, you need to guard against malicious input, such as script injection.
Let’s say you allow your users to write blog posts on your site. You probably don’t want them to be able to inject malicious JavaScript that will run in your unsuspecting visitors’ browsers, right? So, you’ll want to escape any user input that you show on the page to avoid this.
Thankfully, this is almost entirely covered for you. If you use Laravel’s Blade templating engine, the default “echo” syntax ({{ $stuffToEcho }}) runs the output through htmlentities() (PHP’s best way of making user content safe to echo) automatically. You actually have to do extra work to avoid escaping the output, by using the {!! $stuffToEcho !!} syntax.
If you’re interested in testing your interactions with user input, you’re probably most interested in simulating valid and invalid user input and ensuring that if the input is invalid the user is redirected, and if the input is valid, it ends up in the proper place (e.g., the database).
Laravel’s end-to-end application testing makes this simple.
If you want to work with testing specific user interactions on the page and with your forms, and you’re working in a project with Laravel 5.4 or later, you’ll want to pull in Laravel’s BrowserKit testing package. Simply require the package:
composer require laravel/browser-kit-testing --dev
And modify your application’s base TestCase class to extend Laravel\BrowserKitTesting\TestCase instead of Illuminate\Foundation\Testing\TestCase.
Let’s start with an invalid route that we expect to be rejected, in Example 7-21.
publicfunctiontest_input_missing_a_title_is_rejected(){$response=$this->post('posts',['body'=>'This is the body of my post']);$response->assertRedirect();$response->assertSessionHasErrors();}
Here we assert that after invalid input the user is redirected, with errors attached. You can see we’re using a few custom PHPUnit assertions that Laravel adds here.
The assertRedirect assertion was named assertRedirectedTo prior to Laravel 5.4.
So, how do we test our route’s success? Check out Example 7-22.
publicfunctiontest_valid_input_should_create_a_post_in_the_database(){$this->post('posts',['title'=>'Post Title','body'=>'This is the body']);$this->assertDatabaseHas('posts',['title'=>'Post Title']);}
Note that, if you’re testing something using the database, you’ll need to learn more about database migrations and transactions. More on that in Chapter 12.
In projects running versions of Laravel prior to 5.4 assertDatabaseHas should be replaced by seeInDatabase.
There are a lot of ways to get the same data: the Request facade, the request() global helper, and injecting an instance of Illuminate\Http\Request. Each exposes the ability to get all input, some input, or specific pieces of data, and files and JSON input can have some special considerations at times.
URI path segments are also a possible source of user input, and they’re also accessible via the request tools.
Validation can be performed manually with Validator::make(), or automatically using the validate() request method or form requests. Each automatic tool, upon failed validation, redirects the user to the previous page with all old input stored and errors passed along.
Views and Eloquent models also need to be protected from nefarious user input. Protect Blade views using the double curly brace syntax ({{ }}), which escapes user input, and protect models by only passing specific fields into bulk methods using $request->only() and by defining the mass assignment rules on the model itself.