Chapter 7. Collecting and Handling User Data

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.

Injecting a Request Object

The most common tool for accessing user data in Laravel is injecting an instance of the IlluminateHttpRequest object. It offers easy access to all of the ways users can provide input to your site: POSTed form data or JSON, GET requests (query parameters), and URL segments.

Other Options for Accessing Request Data

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 (IlluminateHttpRequest $request) {
    // $request->etc()
});

$request->all()

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—for example, 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.)

Example 7-1. $request->all()
<!-- GET route form view at /get-route -->
<form method="post" action="/signup?utm=12345">
    @csrf
    <input type="text" name="first_name">
    <input type="submit">
</form>
// routes/web.php
Route::post('signup', function (Request $request) {
    var_dump($request->all());
});

// Outputs:
/**
 * [
 *     '_token' => 'CSRF token here',
 *     'first_name' => 'value',
 *     'utm' => 12345,
 * ]
 */

$request->except() and $request->only()

$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.

Example 7-2. $request->except()
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.

Example 7-3. $request->only()
Route::post('post-route', function (Request $request) {
    var_dump($request->only(['firstName', 'utm']));
});

// Outputs:
/**
 * [
 *     'firstName' => 'value',
 *     'utm' => 12345
 * ]
 */

$request->has()

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.

Example 7-4. $request->has()
// POST route at /post-route
if ($request->has('utm')) {
    // Do some analytics work
}

$request->input()

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.

Example 7-5. $request->input()
Route::post('post-route', function (Request $request) {
    $userName = $request->input('name', 'Matt');
});

$request->method() and ->isMethod()

$request->method() returns the HTTP verb for the request, and $request->isMethod() checks whether it matches the specified verb. Example 7-6 illustrates their use.

Example 7-6. $request->method() and $request->isMethod()
$method = $request->method();

if ($request->isMethod('patch')) {
    // Do something if request method is PATCH
}

Array Input

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.

Example 7-7. Dot notation to access array values in user data
<!-- GET route form view at /employees/create -->
<form method="post" action="/employees/">
    @csrf
    <input type="text" name="employees[0][firstName]">
    <input type="text" name="employees[0][lastName]">
    <input type="text" name="employees[1][firstName]">
    <input type="text" name="employees[1][lastName]">
    <input type="submit">
</form>
// POST route at /employees
Route::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'];

JSON Input (and $request->json())

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 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).

Example 7-8. Getting data from JSON with $request->input()
POST /post-route HTTP/1.1
Content-Type: application/json

{
    "firstName": "Joe",
    "lastName": "Schmoe",
    "spouse": {
        "firstName": "Jill",
        "lastName":"Schmoe"
    }
}
// Post-route
Route::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 working 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.

Route Data

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 and via route parameters.

From Request

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 focus on at getting information about the URL segments.

If you’re not familiar with the idea, each group of characters after the domain in a URL 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⁠->​s⁠e⁠g⁠m⁠e⁠n⁠t⁠($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⁠->​s⁠e⁠g⁠m⁠e⁠n⁠t⁠(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.

From Route Parameters

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.

Example 7-9. Getting URL details from route parameters
// routes/web.php
Route::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.

Uploaded Files

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 SymfonyComponentHttpFoundationFileUploadedFile. Let’s walk through an example. First, our form, in Example 7-10.

Example 7-10. A form to upload files
<form method="post" enctype="multipart/form-data">
    @csrf
    <input type="text" name="name">
    <input type="file" name="profile_picture">
    <input type="submit">
</form>

Now let’s take a look at what we get from running $request->all(), as shown in Example 7-11. Note that $request->input('profile_picture') will return null; we need to use $request->file('profile_picture') instead.

Example 7-11. The output from submitting the form in Example 7-10
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.

Example 7-12. Common file upload workflow
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.

Proper Form Encoding for File Uploads

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:

<form method="post" enctype="multipart/form-data">

Validation

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().

validate() on the Request Object

The Request object has a validate() method that provides a convenient shortcut for the most common validation workflow. Take a look at Example 7-13.

Example 7-13. Basic usage of request validation
// routes/web.php
Route::get('recipes/create', 'RecipeController@create');
Route::post('recipes', 'RecipeController@store');
// app/Http/Controllers/RecipeController.php
class RecipeController extends Controller
{
    public function create()
    {
        return view('recipes.create');
    }

    public function store(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 explicitly define the fields we expect and apply 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.

Calling the validate() Method on the Controller Prior to Laravel 5.5

In projects running versions of Laravel prior to 5.5, this validation shortcut is called on the controller (running $this->validate()) instead of on the request.

Manual Validation

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.

Example 7-14. Manual validation
Route::get('recipes/create', function () {
    return view('recipes.create');
});

Route::post('recipes', function (IlluminateHttpRequest $request) {
    $validator = Validator::make($request->all(), [
        'title' => 'required|unique:recipes|max:125',
        'body' => 'required'
    ]);

    if ($validator->fails()) {
        return redirect('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.

Custom Rule Objects

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 indicating 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.

Example 7-15. A sample custom rule
class WhitelistedEmailDomain implements Rule
{
    public function passes($attribute, $value)
    {
        return in_array(Str::after($value, '@'), ['tighten.co']);
    }

    public function message()
    {
        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' => new WhitelistedEmailDomain,
]);
Note

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 this in the docs.

Displaying Validation Error Messages

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.

Example 7-16. Echo validation errors
@if ($errors->any())
    <ul id="errors">
        @foreach ($errors->all() as $error)
            <li>{{ $error }}</li>
        @endforeach
    </ul>
@endif

You can also conditionally echo a single field’s error message. In Laravel 5.8+ you’ll use the @error Blade directive to check for whether there’s an error on a given field; prior to 5.8 you can use $errors->has('fieldName').

// Before Laravel 5.8
@if ($errors->has('first_name'))
    <span>{{ $errors->first('first_name') }}</span>
@endif

// Laravel 5.8+
@error('first_name')
    <span>{{ $message }}</span>
@enderror

Form Requests

As 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 responsibility 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—for example, “Create Comment.”

Creating a Form Request

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.

Example 7-17. Sample form request
<?php

namespace AppHttpRequests;

use AppBlogPost;
use IlluminateFoundationHttpFormRequest;

class CreateCommentRequest extends FormRequest
{
    public function authorize()
    {
        $blogPostId = $this->route('blogPost');

        return auth()->check() && BlogPost::where('id', $blogPostId)
            ->where('user_id', auth()->id())->exists();
    }

    public function rules()
    {
        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.

Requests Extend Userland Request Prior to Laravel 5.3

In projects running versions of Laravel prior to 5.3, form requests extended AppHttpRequestsRequest instead of IlluminateFoundationHttpFormRequest.

Using a Form Request

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 definition of that form request.

Let’s try it out, in Example 7-18.

Example 7-18. Using a form request
Route::post('comments', function (AppHttpRequestsCreateCommentRequest $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’s validate() method, 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.

Eloquent Model Mass Assignment

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.

Example 7-19. Passing the entirety of a form to an Eloquent model
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 them 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 they used their browser tools to add an author_id field and set the ID to be someone else’s ID, and impersonated the other person by creating fake blog posts attributed to them?

Eloquent has a concept called “mass assignment” that allows you to either whitelist fields that should be fillable (using the model’s $fillable property) or blacklist fields that shouldn’t be fillable (using the model’s $guarded property) by passing them in an array to create() or update(). See “Mass assignment” for more information.

In our example, we might want to fill out the model like in Example 7-20 to keep our app safe.

Example 7-20. Guarding an Eloquent model from mischievous mass assignment
<?php

namespace App;

use IlluminateDatabaseEloquentModel;

class Post extends Model
{
    // Disable mass assignment on the author_id field
    protected $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.

Double Protection Using $request->only()

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 using $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',
    ]));
});

{{ Versus {!!

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.

Testing

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.

Requiring BrowserKit After Laravel 5.4

If you want to work with test specific user interactions on the page and with your forms, and you’re working in a project running 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 LaravelBrowserKitTestingTestCase instead of IlluminateFoundationTestingTestCase.

Let’s start with an invalid route that we expect to be rejected, as in Example 7-21.

Example 7-21. Testing that invalid input is rejected
public function test_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.

Different Names for Testing Methods Prior to Laravel 5.4

Prior to Laravel 5.4, the assertRedirect() assertion was named assertRedirectedTo().

So, how do we test our route’s success? Check out Example 7-22.

Example 7-22. Testing that valid input is processed
public function test_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.

Different Names for Testing Methods Prior to Laravel 5.4

In projects that are running versions of Laravel prior to 5.4, assertDatabaseHas() should be replaced by seeInDatabase().

TL;DR

There are a lot of ways to get the same data: using the Request facade, using the request() global helper, and injecting an instance of IlluminateHttpRequest. Each exposes the ability to get all input, some input, or specific pieces of data, and there can be some special considerations for files and JSON input.

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.

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

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