Laravel requests... DEADLY flexible
Bài đăng này đã không được cập nhật trong 7 năm
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