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.

Code For This Course

Get the code for this course.

Episodes