+17

Laravel requests... DEADLY flexible

Request class is very flexible and gives a lot of ways to get the data. It extends Symfony Request class, so it does not only provide Laravel-specific methods.

The class is highly flexible, but despite common opinion, very hard to use. And misuse of a single method may cause any kind of issues.

Getter methods:

The most common operation is getting data from request parameters. There are 13 different ways to do this depending on data source, and controller flexibility. And there is no best way to implement in any code.

TL;DR: if you are lazy to read long text... here is a short recommendation.

  • $request->query($key) — for URL query parameters
  • $request->input($key) — for default POST requests (both form-data and JSON)
  • $request->json($key) — for JSON API
  • $request->file($key) — for multipart form-data requests with files.
  • Never: $request->$key.
  • Not recommended: $request->get($key), $request[$key].
  • And proceed to security section.

Strongly recommended to read all descriptions and remember their differences.

Methods list

1. Request method get()

/** @var string|array|null $value */
$value = $request->get($key, $default = null);

Uses data from 3 or 4 sources following the order below.

Not recommended to use according to Symfony docs except implicit need to have flexible request parsing. Much slower and less safe than using direct usage of bags.

Framework Data sources
Symfony * 1. Custom attributes
2. URL query parameters ($_GET)
3. Request form data ($_POST)
Laravel 4. JSON request body

* Hereafter "Symfony" assumes availability both in Symfony and Laravel.

2. Query parameter bag

/** @var string|array|null $value */
$value = $request->query->get($key, $default = null);

/** @var array $all */
$all = $request->query->all();

Returns one or all fields from URL query parameter bag.

Framework Data sources
Symfony URL query parameters

3. Request method query()

/** @var string|array|null $value */
$value = $request->query($key, $default = null);

/** @var array $all */
$all = $request->query();

Returns one or all fields from URL query parameter bag. Wrapper for query parameter bag as Laravel-style overloaded method.

Highly recommended to use with GET requests where no request body is provided.

Framework Data sources
Laravel URL query parameters

4. Request parameter bag

/** @var string|array|null $value */
$value = $request->request->get($key, $default = null);

/** @var array $all */
$all = $request->request->all();

Returns one or all fields from POST request sent with form data or from decoded JSON body (Laravel only).

Framework Data sources
Symfony 1. Request form data
Laravel 2. JSON request body

5. Files parameter bag

/** @var \Symfony\Component\HttpFoundation\File\UploadedFile|array|null $value */
$value = $request->files->get($key, $default = null);

/** @var array $all */
$all = $request->files->all();

Returns one or all files sent with multipart form-data request. This method returns instances of UploadedFile class provided with Symfony HTTP-Foundation package.

Note: works only with multipart/form-data content type.

Framework Data sources
Symfony Uploaded files

6. Request method file() and allFiles()

/** @var \Illuminate\Http\UploadedFile|array|null $value */
$value = $request->file($key, $default = null);

/** @var array $all */
$all = $request->allFiles();

Returns one or all files sent with multipart form-data request. The difference with previous method is that it returns instances of UploadedFile from Illuminate HTTP package, which has additional methods to work with Laravel file storage.

Framework Data sources
Laravel Uploaded files

7. JSON parameter bag

/** @var string|array|null $value */
$value = $request->json->get($key, $default = null);

/** @var array $all */
$all = $request->json->all();

Returns single or all fields from request JSON body. This bag is available only for valid JSON requests.

Framework Data sources
Laravel JSON request body

8. Request method json()

/** @var string|array|null $value */
$value = $request->json($key, $default = null);

/** @var array $all */
$all = $request->json();

Wrapper for json parameter bag in Laravel-style overloaded methods. Highly recommended to use with JSON API requests, which does not allow default form data.

Framework Data sources
Laravel JSON request body

9. Request method input()

/** @var string|array|null $value */
$value = $request->input($key, $default = null);

/** @var array $all */
$all = $request->input();

Generally works same as get($field) but the first priority of bag is defined by request type (JSON / Form data depending on method and content type) merged with URL query parameters.

This is the mostly recommended way if you need flexible controller (e. g. accepting both multipart and JSON requests).

Calling this method without $key attribute will return all data from priority input source merged with additional sources.

Framework Data sources
Laravel 1. Main data source (Form data / JSON)
2. URL query parameters

10. Request method all() and helpers

/** @var array $all */
$all = $request->all();

/** @var array $filtered */
$filtered = $request->only($keys); // $keys is an array.
$filtered = $request->only($key1, $key2, ..., $keyN); // Each key is a string.

/** @var array $cleaned */
$cleaned = $request->except($keys); // $keys is an array.
$cleaned = $request->except($key1, $key2, ..., $keyN); // Each key is a string.

/** @var array $common */
$common = $request->intersect($keys); // $keys is an array.
$common = $request->intersect($key1, $key2, ..., $keyN); // Each key is a string.

Recursively merges data from input() and allFiles() together.

Helpers work same as Arr helper methods applied to results of all() method. Keys can be passed either as an array attribute or as multiple single attributes.

Be careful, because input source may override keys from files array.

Framework Data sources
Laravel 1. Main data source (Form data / JSON)
2. URL query parameters
3. Uploaded files

11. Request-as-array usage

/** @var string|array|null $value */
$value = $request[$key];
$value = $request->offsetGet($key);

Array-style getter applied to results of all() method.

Important: this method uses data_get() helper, which means that calling it with dot-separated key will work to get nested parameters.

$request['foo.bar'] == $request->all()['foo']['bar'];
Framework Data sources
Laravel 1. Main data source (Form data / JSON)
2. URL query parameters
3. Uploaded files

12. Request method route()

/** @var mixed|null $value */
$value = $request->route($key, $default = null);

/** @var array $all */
$all = $request->route();

Resolves single or all parameters from URL path matched by router.

Framework Data sources
Laravel Route path named parameters

13. "Magic" getter

/** @var string|array|null $value */
$value = $request->$key;
$value = $request->__get($key);

Resolves parameter BOTH from all() method and route() method.

Important: This might be a critical mistake to confuse behavior of this method with $request[$key]. It must be used very carefully to avoid misconfigured controllers. Also will not work with reserved keys like query or json

Framework Data sources
Laravel 1. Main data source (Form data / JSON)
2. URL query parameters
3. Uploaded files
4. Route path named parameters

If data exists... or not empty... or not null?

In many cases you need to check if there is data available in Request parameters or not. And here the mix of Symfony methods and Laravel methods might confuse you a lot. Let’s take a look at available methods.

The most important part here is that there is different logic for definitions of exists and not empty, which in general complies with default PHP rules, but must be paid special attention to avoid logic issues in controllers.

Methods list

1. Method has() of parameter bags

/** @var bool $inQuery */
$inQuery = $request->query->has($key);

/** @var bool $inRequest */
$inRequest = $request->request->has($key);

/** @var bool $inFiles */
$inFiles = $request->files->has($key);

/** @var bool $inJson */
$inJson = $request->json->has($key); // Laravel-only

Checks if the key exists in the parameter bag. Important: this is the only available way for default Symfony request.

2. Request method exists()

/** @var bool $in */
$in = $request->exists($keys); // $keys is an array.
$in = $request->exists($key1, $key2, ..., $keyN); // Each key is a string.

Checks if all of the keys exist in the result of all() method.

$keys may be passed both as an array attribute or as multiple single attributes.

3. Request method has()

/** @var bool $in */
$in = $request->has($keys); // $keys is an array.
$in = $request->has($key1, $key2, ..., $keyN); // Each key is a string.

On the opposite to previous method, it checks if all of the keys are not empty in the result of all() method.

4. Request method hasFile()

Checks if the file key exists and the file is valid (not empty and not broken).

Important strongly recommended to use this method to check the files for validity before using file() method.

5. Array key check

/** @var bool $in */
$in = isset($request[$key]);
$in = array_key_exists($key, $request);
$in = $request->offsetExists($key);

Checks if the key exists in the results of all() method.

6. "Magic" property check

/** @var bool $in */
$in = isset($request->$key);
$in = $request->__isset($key);

Checks if the key exists and value is not null in the results of all() method and route() method.


Before using any of these methods, you need to make sure what exactly you want to check — data existence or emptiness, input source and possible intersections of sources.

Is it hard already? Believe me, not yet 😃 let’s continue.

Laravel == inconsistence

If you think that Laravel is following general PHP or programming logic, then you are making a deadly mistake, which may cause you just some headache in the best case and serious security issue in the worst.

Unsetting data is not simple

// URI: /path?field_a=foo&field_b=bar
// JSON: {"field_a":"not_foo"}

echo $request['field_a']; // >> not_foo — JSON input has the priority.

unset($request['field_a']); //field_a has been unset, hasn't it?

echo $request['field_a']; // >> foo (???) No, it hasn't!

unset($request['field_a']); //Let's try again!

echo $request['field_a']; // >> foo (!!!) No, please!

offsetUnset method in Laravel Request class removes data ONLY from priority input source, but offsetGet and input() methods use ALL input sources.

unset($request->field_a) WILL NOT work at all. There is simply no such method.

Overwriting is not better

The same applies to offsetSet

// URI: /path?field_a=foo&field_b=bar
// JSON: {"field_a":"not_foo"}

$request['field_a'] = 'new';
echo $request['field_a']; // >> new
echo $request->query('field_a'); // >> foo — query bag has not been overwritten.

Concerned about security? YES. You must be!

To be honest, these are not issues of Laravel itself. But with the goal to achieve maximum flexibility, there are things that you must keep in mind.

Request validation works on $request->all() — check the data twice

Let’s take a look at the following controller action.

public function store(Request $request)
{
    $this->validate($request, [
        'must_be_in_query' => 'required',
        'must_be_in_body' => 'required',
    ]);

    return [
        'must_be_in_query' => $request->query('must_be_in_query'),
        'must_be_in_body' => $request->input('must_be_in_body'),
    ];
}

If everything goes ok, it is absolutely correct.

But you must keep in mind one thing about validation request: It validates ALL request data, while it doesn’t mean you would get the data where it is needed.

What happens if we pass must_be_in_query in JSON body?

REQUEST:
POST /api/validation HTTP/1.1
Host: localhost:8000
Content-Type: application/json

{
	"must_be_in_query": "foo",
	"must_be_in_body": "bar"
}

RESPONSE:
{
  "must_be_in_query": null,
  "must_be_in_body": "bar"
}

We have no validation error triggered and possible bug with null field. Laravel validator simply doesn’t know where to look for data.

Confusing $request->input() and $request->get() is simply dangerous

Let’s take a look at the following example:

public function store(Request $request)
{
    $this->validate($request, [
        'field_a' => 'required',
        'field_b' => 'required|email',
        'field_c' => 'required',
    ]);

    return [
        'field_a' => $request->input('field_a'),
        'field_b' => $request->get('field_b'),
        'field_c' => $request->get('field_c'),
    ];
}

The following test case shows that field_b variable provided in URL query is overwritten by JSON body. This is intended behavior as $request->input() method puts preferred source as the priority.

REQUEST:
POST /api/validation?field_a=foo&field_b=not_test@example.com&field_c=baz HTTP/1.1
Host: localhost:8000
Content-Type: application/json

{
	"field_b": "test@example.com"
}

RESPONSE:
{
  "field_a": "foo",
  "field_b": "not_test@example.com",
  "field_c": "baz"
}

But this code opens one quite serious vulnerability. We can avoid email check if we put a valid email in the query, but invalid one in JSON body

REQUEST:
POST /api/validation?field_a=foo&field_b=not_an_email_at_all&field_c=baz HTTP/1.1
Host: localhost:8000
Content-Type: application/json

{
	"field_b": "test@example.com"
}

RESPONSE:
{
  "field_a": "foo",
  "field_b": "not_an_email_at_all",
  "field_c": "baz"
}

Bingo! We have possibility to store invalid email in the database. As you can guess, this applies to ANY validation rule, including exists and unique. Which troubles it might cause I leave for your imagination.

Route parameters may intervene with input sources when used incorrectly

// Route: /validation/{user}
// Here we assume that {user} is not binded to model.

public function update(Request $request, $user)
{
    return [
        'user_a' => $request->user,
        'user_b' => $request->input('user'),
    ]
}

The issue here is in very unexpected behavior for form data requests and JSON requests.

REQUEST:
PUT /api/validation/1 HTTP/1.1
Host: localhost:8000
Content-Type: multipart/form-data; boundary=----WebKitFormBoundary7MA4YWxkTrZu0gW
Cache-Control: no-cache

------WebKitFormBoundary7MA4YWxkTrZu0gW
Content-Disposition: form-data; name="user"


------WebKitFormBoundary7MA4YWxkTrZu0gW--

RESPONSE:
{
  "user_a": "1",
  "user_b": null
}

As we can see, if we pass empty value for key user in form data, $request->user returns our route key, because the result for $request->input('user') is NULL… which is quite logical.

BUT! What if we send JSON request?

REQUEST:
PUT /api/validation/1 HTTP/1.1
Host: localhost:8000
Content-Type: application/json
Cache-Control: no-cache

{
	"user": ""
}

RESPONSE:
{
  "user_a": null,
  "user_b": null
}

Our route parameter simply disappeared from $request->user result.

WHAT??? Impossible? Not really.

The same happens if we pass user as query parameter.

REQUEST:
PUT /api/validation/1?user= HTTP/1.1
Host: localhost:8000
Cache-Control: no-cache

RESPONSE:
{
  "user_a": null,
  "user_b": null
}

The reason is very simple. Symphony Request uses $_POST superglobal to fill in request bag. And by default PHP behavior empty variables are removed from $_POST, but kept as empty in $_GET… and the same applies to json_decode() function used to parse JSON requests.

Conclusion

Don’t be fooled by simplicity and flexibility of Request classes in Laravel. What is good at the first sight might play havoc with your application in the future.


All rights reserved

Viblo
Hãy đăng ký một tài khoản Viblo để nhận được nhiều bài viết thú vị hơn.
Đăng kí