Tidy up your Form Requests in Laravel
Introduction
This is part four. If you have not read the previous parts, we recommend that you go back and them here.
This will be a short article showing how we can reduce the number of form requests in our application by using HTTP verbs, and also creating an update
route, which we are missing from our UserController
.
A link to the github repository used, will be provided at the end of this article.
Understanding Laravels Route::apiResource
On default, using Laravels Route::apiResource
will create route parameters for your resource routes based on the “singularized” version of the name of the resource. With Route::apiResource
you get the help of setting up some default routes for you, and it even names them.
Route::apiResource
creates resource URIs by using English verbs. With having a Route::apiResource('users', ‘UserController');
the following default routes gets created for you by the help of the route resource. Since this is a series on APIs we will use the ::apiResource
rather than the ::resource
.
Verb Path Action Route Name
--------------------------------------------------------------
GET /users index users.index
POST /users store users.store
GET /users/{user} show users.show
PUT|PATCH /users/{user} update users.update
DELETE /users/{user} destroy users.destroy
If you feel the need to modify the naming of some the these routes, then you should re-consider as Laravel creates a standard RESTful API with ::apiResource
that should be familiar to your consumers.
Using HTTP verbs in the form requests
The most commonly used HTTP verbs, or methods as some might call them, are GET
, POST
, PATCH
, PUT
and DELETE
. These correspond to the typical CRUD
operations, to create, read, update and delete. There are numerous of other verbs too, but which are utilized less frequently than the mentioned ones, though OPTIONS
and HEAD
are used more often than other.
We make use of the HTTP verb in form requests by following $this->isMethod('post')
.
How the update functionality was implemented
In this part four, we have taken a step further and is now trying to test, whether we can update
the fields for an already existing user in the database.
The steps taken towards this, have been the following, that in api.php
we have refactored our single routes, to instead use Route::apiResource
, as this itself, as explained above, will generate the different routes for us, which would only need us, connecting to those routes, or pointing to them.
api.php
would now contain the following line, Route::apiResource('/users’, UserController::class);
instead. And by using the php artisan route:list
command in your terminal, you can see the different routes that have been generated, just as below.
This would make that we “only” need in our UserController
to point to the path, the Route::apiResource
have created for us. Inside UserController
we create a new method with the name of update
, as given by our route resource, and in the OpenAPI specs we specify that this request is of type Patch
.
...class UserController extends Controller
{
... /**
* @OA\Patch(path="/api/users/{userId}", description="Update user based on user id", operationId="",
* @OA\RequestBody(@OA\JsonContent(ref="#/components/schemas/User"), required=true,description="The updating of a user"),
* @OA\Response(response=200, description="OK",
* @OA\JsonContent(ref="#/components/schemas/User")
* ),
* @OA\Response(response=422, description="Unprocessable Entity / Validation Failed")
* )
*/
public function update(Form $request, int $id)
{
return $request->saveOrUpdate($id);
}
}
Although that PUT
and PATCH
might do the same thing of updating a resource at a location, they do it differently.
PUT
is a method that modifies a resource where the client sends data that which updates the entire resource. PUT
is used to set an entity’s information completely. This makes PUT
similar to POST
in that essence that it can create resources, but it does so when an URI is defined. PUT
overwrites the entire entity if it already exists, and creates a new resource if it doesn’t exist.
PATCH
on the other hand works slightly different, PATCH
applies a partial update to the resource. What this means is that you are only required to send the data that you actually only want to update, and it will not have an effect or change anything else. Therefore if you only want to, for example, change the name
on an existing database entry field, then you will only be required to send the first parameter, which is name
.
In our case, we want to show that we can update a database name
and/or the email
field and nothing else, therefore the use of PATCH
is more convenient than the use of PUT
.
As it can be seen, we have given the update
function one more parameter of type int
and named it $id
. This is due to when working with Route::apiResource
the specified route to the put/patch
endpoint of our userEndpoint
will require an id
specified parameter. And in the OpenAPI spec we are pointing to that parameter, named userId
, as follows @OA\Patch(path="/api/users/{userId}"
.
With this explained, we move on to our UserForm
class, in which we also have created a new function, or rather we have modified our previously named save
function to now be able to save
and update
depending on the request method we are trying to perform.
...class UserForm extends FormRequest {
... public function saveOrUpdate(?int $id = null)
{
if($this->isMethod('post'))
{
$user = new User();
$user->password = bcrypt("asd");
}
else
{
$user = User::find($id);
}
$user->name = $this->name;
$user->email = $this->email;
$user->save(); return [
'name' => $user->name,
'email' => $user->email,
];
}
}
In order to update a user field, that user needs to already be existing. We use Laravels eloquent method of interacting with the database and use User::find()
and specify that the database field it needs to match up with, is the id
field, and retrieve the data it matches up with. We use id
since this is unique and does not have multiple entries.
We use $this->isMethod()
to see what type of request we are trying to perform, as mentioned in previous articles $this
is the keyword when working with FormRequest
and the isMethod()
checks the request type.
With the introduction of PHP v.7.1
you could now provide hints to a function to only accept the given data type. In PHP you can use type hinting for objects, arrays and callable datatypes.
Since we in our function is prepending ?
to our type name int
this means that we are type hinting that this can be a nullable type. Nullable type simply means that when you prepend ?
to the type name while declaring types in the function parameters or for return values, it will be marked as nullable.
If our request method is of type post
then that must mean we have no user and that we would like to create one. Else we assume the request method is either of type patch
or put
and we would like to update the user fields in the users
database table.
Testing
With all set and “good-to-go”, we need to actually test whether the responses we get matches up with our OpenAPI specs, and if an update
actually occurs in our users
database table.
The first test we conduct is on the serverside, we need to successfully assert that an update has taken place. The test was build like the following:
...class UserTest extends TestCase
{
... public function test_that_serverside_updates_user()
{
$user = User::find(1);
$updatedMockPayloadUser = [
'name' => 'changed_name',
'email' => 'changing@email.com',
]; $response = $this->patchJson("{$this->userEndpoint}/{$user- >id}", $updatedMockPayloadUser);
$response->assertStatus(200); $this->assertDatabaseHas('users', [
'name' => $updatedMockPayloadUser['name'],
'email' => $updatedMockPayloadUser['email']
]);
}
}
We start by looking for if a given user already exists in the database, and we use the id
for this purpose.
The user we get in return gets stored inside the $user
variable, and then we create an updatedMockPayloadUser
where we set the name
field to be a different one from the current name
, and we change the email
too for the sake of changing, though either would work though for email
, same goes for name
.
When testing Json APIs, Laravel provides several helpers for testing Json APIs and their following responses. We make use of Laravels patchJson
since we specified that our update request is of type patch
.
We patchJson
to our endpoint that got created for us by using Route::apiResource
and append $user->id
to our userEndpoint
. Again this was specified as a result of Route::apiResource
, so we needed to match the endpoints to be the exact same.
Then we assertStatus
for HTTP status code 200 (OK)
, which indicates that the patch
request was successful. HTTP status code 200 (OK)
was specified in our OpenAPI specs for our UserController
update()
function. But not only that, we also assertDatabaseHas
the updated name
and email
fields by comparing it with $updatedMockPayloadUser
name
and email
property.
The next test conducted was to check whether or not our OpenAPI specs matches with Swagger, if our OpenAPI spec contains such operation. The test looks as following:
class UserTest extends TestCase
{
... public function test_that_swagger_reaches_correct_endpoint_method()
{
$updatedMockPayloadUser = [
'name' => 'changed_name',
'email' => 'changing@email.com',
];
$this->address = new OperationAddress("{$this->userEndpoint}/1", 'patch'); $request = (new ServerRequest('patch', "{$this->userEndpoint}/1"))
->withHeader('Content-Type', 'application/json')
->withBody(Utils::streamFor(json_encode($updatedMockPayloadUser))); try {
$this->validator->validate($this->address, $request);
$this->addToAssertionCount(1);
} catch (InvalidBody $e) {
$latestException = $e->getMessage();
$previousException = $e->getPrevious()->getMessage();
$exceptionLocation = implode(".", $e->getPrevious()->dataBreadCrumb()->buildChain());
$this->fail("$latestException $previousException $exceptionLocation");
}
}
}
This test is very similar to our test_that_swagger_validates_good_payload
in the way that we making a server request and trying to validate if we reach the correct endpoint. The endpoint we are trying to reach is /api/users/{userId}
where userId
is the given users id, that we are trying to update. We are mock sending the $updatedMockPayloadUser
.
Changes made to other functionalities along the way
Along the way a few refactoring, or changes, have been made, to accommodate this new update functionality.
Changes made is listed below:
- Inside
UserForm.php
the functionsave()
have been refactored tosaveOrUpdate
. - Inside
UserController
thesave
function now calls theUserForm
saveOrUpdate
function now. - Inside
api.php
the single routes have been replaced withRoute::apiResource('/users', UserController::class)
. - Inside
UserTest
inside thesetUp
method the following line have been removed:$this->address = new OperationAddress($this->userEndpoint, $requestMethod);
and placed insidewrapServerRequest
which now takes an extra parameter of type string namedrequestMethod
in which this will be the type of HTTP request we are trying to make, ie.post
,patch
and so on.