Lumen API 06: Update An Existing Resource

Yanuar Arifin
Nov 6 · 6 min read
Photo by rithwick. pr on Unsplash

In many API guidelines, there are 2 ways to update a resource, one is sending only the attribute(s) that needs to be updated, for example you only want to update price, you only send key price with value the new price. The second one is forcing client to send all the attributes to update the resource. What differentiate those 2 ways of updating resource would be the HTTP verb that they use, which is PATCH and PUT (surprise! there are more HTTP verbs other than GET and POST). If we allow the clients to send only the updated attribute(s) we use PATCH, as repairing a hole in a shirt, we need to patch that hole, not replace the shirt. Otherwise, if you want to force clients to send the entire resource to update it, you use PUT.

In this article we will use PUT verb as I think it will be easier to implement.

Write The Test

As how we approach our functionality in previous articles, we will be creating a test cases first, to shape the behavior of our update functionality. Try to think how the clients will abuse this update functionality, make a list of it yourself, then compare to the list I create below. If there are cases that I forgot, you could try to make it in your test cases for experiment and experience. Again, you try to list the test cases first, then compare to this list

  1. Wen client try to update a non-existing record, we will return 404 not found message
  2. When client try to update an item without a name, throw validation error (422)
  3. When client try to update an item without a description, throw validation error (422)
  4. When client try to update an item without a SKU, throw validation error (422)
  5. When client try to update an item without a price, throw validation error (422)
  6. When client try to update an item without a stock, throw validation error (422)
  7. If we search the database, the updated id should have the updated values, not the old ones.
  8. An update should return the newly updated resource

Okay, those are the cases that I found need to be tested. It looks like the create function with a little difference.

Let’s write those cases to code. Open tests/ItemFeatureTest.php and add these functions:

/**
* trying to update a non existent record will return
* 404 HTTP code
*
* @return void
*/
public function testUpdateNonExistentResourceReturns404()
{
factory(App\Item::class, 45)->create();
$item = factory(App\Item::class)->raw();
$this->put('/items/300', $item);
$this->assertResponseStatus(404);
}
/**
* update resource needs Name field
*
* @return void
*/
public function testUpdateNeedsNameField()
{
factory(App\Item::class, 45)->create();
$item = factory(App\Item::class)->raw(['name' => '']);
$this->put('/items/3', $item);
$response = $this->response->getContent();
$this->assertContains('The name field is required', $response, 'name error not found', true);
}

/**
* update resource needs dsecription field
*
* @return void
*/
public function testUpdateNeedsDescriptionField()
{
factory(App\Item::class, 45)->create();
$item = factory(App\Item::class)->raw(['description' => '']);
$this->put('/items/3', $item);
$response = $this->response->getContent();
$this->assertContains('The description field is required', $response, 'description error not found', true);
}

/**
* update resource needs sku field
*
* @return void
*/
public function testUpdateNeedsSkuField()
{
factory(App\Item::class, 45)->create();
$item = factory(App\Item::class)->raw(['sku' => '']);
$this->put('/items/3', $item);
$response = $this->response->getContent();
$this->assertContains('The sku field is required', $response, 'sku error not found', true);
}

/**
* update resource needs price field
*
* @return void
*/
public function testUpdateNeedsPriceField()
{
factory(App\Item::class, 45)->create();
$item = factory(App\Item::class)->raw(['price' => '']);
$this->put('/items/3', $item);
$response = $this->response->getContent();
this->assertContains('The price field is required', $response, 'price error not found', true);
}

/**
* update resource needs stock field
*
* @return void
*/
public function testUpdateNeedsStockField()
{
factory(App\Item::class, 45)->create();
$item = factory(App\Item::class)->raw(['stock' => '']);
$this->put('/items/3', $item);
$response = $this->response->getContent();
$this->assertContains('The stock field is required', $response, 'stock error not found', true);
}

/**
* update will save the data to database
*
* @return void
*/
public function testUpdateWillPersistDataToDb()
{
factory(App\Item::class, 34)->create();
$item = factory(App\Item::class)->raw();
$this->put('/items/3',$item);
$item['id'] = 3;
$this->seeInDatabase('items', $item);
}
/**
* create with name, description, sku, price, stock will return new resource
*
* @return void
*/
public function testUpdateReturnsTheResource()
{
factory(App\Item::class, 45)->create();
$item = factory(App\Item::class)->raw();
$this->put('/items/3',$item);
$item['id'] = 3;
$response = $this->response->getContent();
$response = json_decode($response, true);
$this->seeJsonContains($item);
}

Those is almost identical to those we have when we create create functionality previously. The main difference is in this test we use $this->put() instead of $this->post() . The former will make a PUT HTTP request instead of POST to indicate that we wanted to update an existing resource, not inserting a new one, so our function should check for the id that it provides, search based on that id first, then update, not create.

If you want to know what are those assert and see function do, I have covered it in this article

https://medium.com/@ynrfin/lumen-api-5-create-route-to-insert-new-record-1f3f739bd927?source=friends_link&sk=fb35773eb8f25f8c3aebc49e13733d01

Test result before we implement the code

The Feature’s Code

Now we need to create the route so client could access this functionality. Open routes/web.php and add these line at the end:

$router->put('/items/{id}', ["as" => "item.put", 'uses' => "ItemController@put"]);

Let’s compare it to the routes that we have created before

$router->get('/items', ['as' => 'item.showAll', 'uses' => 'ItemController@showAll']);$router->get('/items/{id}', ["as" => "item.showOne", 'uses' => "ItemController@showOne"]);$router->post('/items/', ["as" => "item.create", 'uses' => "ItemController@create"]);// this is the newest route
$router->put('/items/{id}', ["as" => "item.put", 'uses' => "ItemController@put"]);

As you can see, the new route has different function name from $router object. It uses put() instead of post() or get() . This put() function indicates that this route accept only PUT HTTP verb. For the parameter, it uses array in an identical manner as previous routes, it use as for alias, and has uses key value that points to a controller function that handle the request, which is put() function inside ItemController.

Next we will create the put() function inside ItemController . Open the app\Http\Controllers\ItemController.php and create new function called put(Request $request, $id)

    /**
* update an existing record by id
* and needs name, description, sku, price, stock fields
*
* @return Item
*/
public function put(Request $request, $id)
{
$validatedData = $this->validate($request, [
'name' => 'required|string',
'description' => 'required|string',
'sku' => 'required|string',
'price' => 'required|integer',
'stock' => 'required|integer',
]);
$item = Item::findOrFail($id); $item->name = $validatedData['name'];
$item->description = $validatedData['description'];
$item->sku = $validatedData['sku'];
$item->price = $validatedData['price'];
$item->stock = $validatedData['stock'];
$item->save(); return new ItemResource($item);
}

What this put() function do is it will validates incoming requests, then search for a records in items table using id that is referenced in the route. This {id} part in the first argument in the route above this code is to capture a part of the URL that will be used as parameter name in this function, which is $id . You could have as many parameters in the route for example /items/{ownedBy}/{category} , just make sure you add the parameters in the controller’s function too.

Now if we run the test:

Test result after we implement the code

Yay!!, our code has covered all our test.

Conclusion

In this article we talk about how we update a resource in an API. There are two HTTP verbs that could be used to indicate an update request which is PUT and PATCH.
It looks so simple because there’s no business logic yet in our app, only show/edit a resource that has no relationship. If there are any business logic that is too complex, remember that those are processing detail, the basic web request is just make a route, get the request, process the data(business logic here), finally returning response.

I hope you learn something from this article. See you in the next one.

Yanuar Arifin

Written by

Software Engineer. Currently PHP. How far could we automate things?

Welcome to a place where words matter. On Medium, smart voices and original ideas take center stage - with no ads in sight. Watch
Follow all the topics you care about, and we’ll deliver the best stories for you to your homepage and inbox. Explore
Get unlimited access to the best stories on Medium — and support writers while you’re at it. Just $5/month. Upgrade