How To DELETE Existing Accounts
In this video we are going to handle the remaining method in our Account's API - the DELETE
method.
Let's quickly take a look at the Behat feature so we know what we are trying to accomplish here:
Scenario: User can DELETE an Account they own
When I send a "DELETE" request to "/accounts/a1"
Then the response code should be 204
And the "Account" with id: a1 should have been deleted
Scenario: User cannot DELETE an Account they do not own
When I send a "DELETE" request to "/accounts/a3"
Then the response code should be 403
This follows on from the previous videos, re-using the same background steps - so if this is the first video you are viewing, be sure to go back and understand how the Background step definitions put the database into a consistent state for testing.
The deleteAction
is really quite straightforward:
/**
* Deletes a specific Account by ID
*
* @ApiDoc(
* description="Deletes an existing Account",
* statusCodes={
* 204 = "Returned when an existing Account has been successfully deleted",
* 403 = "Returned when trying to delete a non existent Account"
* }
* )
*
* @param int $accountId the account id
* @return View
*/
public function deleteAction($accountId)
{
$requestedAccount = $this->getAccountHandler()->get($accountId);
$this->getAccountHandler()->delete($requestedAccount);
return new View(null, Response::HTTP_NO_CONTENT);
}
/**
* Returns the required handler for this controller
*
* @return AccountHandlerInterface
*/
private function getAccountHandler()
{
return $this->get('crv.handler.restricted_account_handler');
}
This is a slight modification from the video, largely to move from findOneById
in the controller action, to get
, which I feel is more 'rest'-y. You don't need to do this!
The main reason I did do this is to keep things consistent. A direct query of the repository should not live in the controller. Instead, it should live in the handler. All I have done is pushed the statement further downstream, into the handler.
As we have already covered, $requestedAccount = $this->getAccountHandler()->get($accountId);
will throw
if the logged in user does not have access to the Account (resource) they are requesting. A 403
will be thrown whether the Account exists and is not accessible to this User, or the Account doesn't exist. Again, this is a security measure that may or may not be required in your architecture.
That means we can safely assume that if we get to the delete
call on the next line then we do have permission to be working with this Account object.
The delete
method on the AccountHandler
is as follows:
/**
* @param mixed $resource
* @return mixed
*/
public function delete($resource)
{
$this->guardAccountImplementsInterface($resource);
$this->repository->delete($resource);
return true;
}
Remember, we are implementing a rather generic interface - HandlerInterface
- we must check whether the passed in object actually is an instance of Account
. If you are unsure on this, please watch this video which explains this further.
The handler delete
method really doesn't do very much. Once again, the real call is delegated to the repository.
The repository that we are passing in is really the restricted repository. This means the repository itself will check again that we have access to view
this Account. You may wish to change this to vote on whether we have permission to delete
this Account:
/**
* @param AccountInterface $account
* @param array $arguments
*/
public function delete(AccountInterface $account, array $arguments = ['flush'=>true])
{
$this->denyAccessUnlessGranted('view', $account);
$this->repository->delete($account, $arguments);
}
Because a Doctrine entity removal operation is the same regardless of entity type, I have extracted the remove
call out to the CommonDoctrineRepository
.
// src/AppBundle/Repository/Doctrine/DoctrineAccountRepository.php
/**
* @param AccountInterface $account
* @param array $arguments
*/
public function delete(AccountInterface $account, array $arguments = ['flush'=>true])
{
$this->commonRepository->delete($account, $arguments);
}
Which really calls:
// src/AppBundle/Repository/Doctrine/CommonDoctrineRepository.php
/**
* @param mixed $object
* @param array $arguments
*/
public function delete($object, array $arguments = ['flush'=>true])
{
$this->em->remove($object);
if ($arguments['flush'] === true) {
$this->em->flush();
}
}
Assuming everything goes to plan, the API consumer will receive a 204
response code, and no content.
Even though there's quite a lot of code, there is very little happening at each stage. This makes it rather easy to test. Be sure to check out the code and take a look at the PHPSpec tests, and underlying Behat steps in more detail as needed.