PUT and PATCH for Accounts

This video is available to view for members only.

Click here to Join!

Already a member?

Login


In this video we are going to cover off the PUT and PATCH methods of our Account's endpoint.

We can cover both off as we have already covered PATCH for our User entity. PUT is very similar, with the main change being in how Symfony's form component will handle the data submission step.

To recap, a PATCH will allow our API consumers to send in only the changes that they wish to make on a per field basis. Assuming we have an entity that has fields A, B, and C, then a PATCH might only send in changes for field B, leaving all the other data alone.

A PUT would require that an API consumer send in all three fields (A, B, and C) even if some of them remain the same.

You don't have to implement both. Interestingly, you may choose not to implement PATCH at all, as the implementation could potentially lead to lost data - if your API is highly reliant on data integrity being absolutely critical then, honestly, this probably isn't the best implementation. Feel free to read more on this subject at your leasure.

Data Transfer Objects And Symfony

The main difference between how User and every other entity added to the system (File, and anything else you add) will behave is that every other entity will go from form submission to database update via a Data Transfer Object.

You are completely free to rip out or bypass this stage entirely.

The reasoning behind this step is that I generally try to avoid having setters on my entities. My life became much easier when I made my entities (and objects in general) harder to change. Yes, it seems counter intuitive, but bear with me as there is some method to this madness.

The problem I encountered was that the language I wanted to use, and the language being forced upon me following the classic getter / setter (technically, accessors) paradigm were at odds with each other.

An example might be:

$person = new Person();
$person->setName('Tom');
$person->setDateOfBirth(new \DateTime('1st January 1980'));

My issue with this sort of thing is that it's very, very unlikely that a persons date of birth will ever change. I know mine hasn't.

Instead, it would make sense - in my opinion - to move these things to the constructor:

class Person {
    private $name;
    private $dateOfBirth;

    public function __construct($name, \DateTime $dateOfBirth) {
        $this->name = $name;
        $this->dateOfBirth = $dateOfBirth;
    }
}

$person = new Person('Tom', new \DateTime('1st January 1980'));

This makes it much harder for someone to accidentally change a property they are not supposed to.

You may be wondering how you might change a Person's name in this instance. I would argue this depends entirely on your business rules. Sometimes it would be fine to entirely throw away the old Person, create a new one, and start fresh.

Other situations may call for a changeName($newName) method. For me, this language change makes the system easier to reason about.

Think about a conversation with your sales team where the customer has recently changed their business name from Bob's Widgets to Bob and Shirley's Finer Widget Co. The sales guy would never say to you - hey, can we just setName('Bob and Shirley's Finer Widget Co.') on getId(47)?

They speak human :)

Hey, can you just change the name of 'Bob's Widgets' to 'Bob and Shirley's Finer Widget Co.'? Thanks! Is it done yet? Is it done yet? Can you give me an estimated time scale... Oh wait yeah, sorry, I said sales, not project manager ;)

So that's the direction I'm trying to take my entities (and objects in general).

If you're interested in knowing more about this subject, I recommend the following blog posts:

The Symfony Form Component Side Effect

Unfortunately, as good as Symfony's form component is, it makes using objects without getters and setters a real pain.

This kinda rains on our parade, somewhat.

To get around this originally, I followed the guidance of a couple of interesting blog posts:

And generally, things have been pretty good.

There is, however, quite a lot of extra boiler plate code to write.

For every entity that might change, an associated DTO is needed. As such, I tend to create an interface for the object inside my Model dir:

<?php

// src/AppBundle/Model/AccountInterface.php

namespace AppBundle\Model;

/**
 * Interface AccountInterface
 * @package AppBundle\Model
 */
interface AccountInterface
{
    /**
     * @return string
     */
    public function getId();

    /**
     * @return string
     */
    public function getName();

    /**
     * @return AccountInterface
     */
    public function changeName($newName);

    /**
     * @return \Doctrine\Common\Collections\Collection
     */
    public function getUsers();

    /**
     * @param UserInterface $user
     * @return AccountInterface
     */
    public function addUser(UserInterface $user);

    /**
     * @param UserInterface $user
     * @return bool
     */
    public function isManagedBy(UserInterface $user);

    /**
     * @param UserInterface $user
     * @return 
     */
    public function removeUser(UserInterface $user);

    /**
     * @return void
     */
    public function removeAllUsers();
}

So long as my Account entity and DTO both implement this interface, then for all intents and purposes, they will co-operate swimmingly.

Of course, some of these methods make no sense on a DTO. After all, my Symfony form won't ever need to call isManagedBy, so for those methods I throw a \RuntimeException:

<?php

// src/AppBundle/DTO/AccountDTO.php

namespace AppBundle\DTO;

use AppBundle\Model\AccountInterface;
use AppBundle\Model\UserInterface;
use Doctrine\Common\Collections\ArrayCollection;
use Symfony\Component\Validator\Constraints as Assert;

/**
 * Class AccountDTO
 * @package AppBundle\DTO
 */
class AccountDTO implements AccountInterface
{
    /**
     * @var string
     */
    private $id;
    /**
     * @var string
     * @Assert\NotBlank()
     */
    private $name;
    /**
     * @var ArrayCollection
     * @Assert\Count(min="1", minMessage="This Account needs to be associated with at least one User ID")
     */
    private $users;

    /**
     * AccountDTO constructor.
     */
    public function __construct()
    {
        $this->users = new ArrayCollection();
    }

    /**
     * @return mixed
     */
    public function getDataClass()
    {
        return self::class;
    }

    /**
     * @return string
     */
    public function jsonSerialize()
    {
        return [
            'name'                  => $this->name,
            'users'                 => $this->users,
        ];
    }

    /**
     * @return ArrayCollection<UserInterface>
     */
    public function getUsers()
    {
        return $this->users;
    }

    /**
     * @param UserInterface $user
     * @return mixed
     */
    public function addUser(UserInterface $user)
    {
        if ( ! $this->users->contains($user)) {
            $this->users->add($user);
        }

        return $this;
    }

    /**
     * @param UserInterface $user
     * @return mixed
     */
    public function removeUser(UserInterface $user)
    {
        if ($this->users->contains($user)) {
            $this->users->removeElement($user);
        }

        return $this;
    }

    /**
     * @param Array<UserInterface> $users
     * @return $this
     */
    public function setUsers($users)
    {
        $this->users = $users;

        return $this;
    }

    /**
     * @return mixed
     */
    public function getName()
    {
        return $this->name;
    }

    /**
     * @param mixed $name
     * @return $this
     */
    public function setName($name)
    {
        $this->name = $name;

        return $this;
    }

    /**
     * @return mixed
     */
    public function getId()
    {
        throw new \RuntimeException(sprintf('Should never be calling %s on an Account DTO', __METHOD__));
    }

    /**
     * @param $newName
     * @return mixed
     */
    public function changeName($newName)
    {
        throw new \RuntimeException(sprintf('Should never be calling %s on an Account DTO', __METHOD__));
    }

    /**
     * @param UserInterface $user
     * @return mixed
     */
    public function isManagedBy(UserInterface $user)
    {
        throw new \RuntimeException(sprintf('Should never be calling %s on an Account DTO', __METHOD__));
    }

    /**
     * @return mixed
     */
    public function removeAllUsers()
    {
        throw new \RuntimeException(sprintf('Should never be calling %s on an Account DTO', __METHOD__));
    }
}

Notice also, the inclusion of Symfony validation constraints on some properties. This is for the purposes of form validation. Very useful :)

Just because the interface doesn't have setters doesn't mean I can't add them in to the DTO. This is the right place for the setters to live, as these objects are created, used once, and thrown away.

They are used by a DataTransformer:

<?php

// src/AppBundle/DataTransformer/AccountDataTransformer.php

namespace AppBundle\DataTransformer;

use AppBundle\DTO\AccountDTO;
use AppBundle\Model\AccountInterface;
use AppBundle\Model\UserInterface;

class AccountDataTransformer
{
    public function convertToDTO(AccountInterface $account)
    {
        $dto = new AccountDTO();

        $dto->setName($account->getName());
        $dto->setUsers($account->getUsers());

        return $dto;
    }

    public function updateFromDTO(AccountInterface $account, AccountDTO $dto)
    {
        if ($account->getName() !== $dto->getName()) {
            $account->changeName($dto->getName());
        }

        $account->removeAllUsers();

        foreach ($dto->getUsers() as $user) { /** @var UserInterface $user */
            $user->addAccount($account);
        }

        return $account;
    }
}

And all this is put together (no pun intended) in the AccountHandler::put method:

// src/AppBundle/Handler/AccountHandler.php

    /**
     * @param  AccountInterface     $account
     * @param  array                $parameters
     * @param  array                $options
     * @return mixed
     */
    public function put($account, array $parameters, array $options = [])
    {
        $this->guardAccountImplementsInterface($account);

        /** @var AccountInterface $account */
        $accountDTO = $this->dataTransformer->convertToDTO($account);

        $accountDTO = $this->formHandler->handle(
            $accountDTO,
            $parameters,
            Request::METHOD_PUT,
            $options
        );

        $this->repository->refresh($account);

        $account = $this->dataTransformer->updateFromDTO($account, $accountDTO);

        $this->repository->save($account);

        return $account;
    }

The PATCH method is very similar, only differing in sending in Request::METHOD_PATCH as the method to the form handler handle method. Please watch this video if unsure on why this is.

As mentioned though, you may find this entire process completely overkill for your needs. It's up to you, but I find this methodology has improved the modularity of my code so far.


Code For This Course

Get the code for this course.

Share This Episode

If you have found this video helpful, please consider sharing. I really appreciate it.


Episodes in this series

# Title Duration
1 Project Introduction 17:13
2 Setting Up Our Development Environment 05:08
3 Installing Symfony 3, Behat, and more 13:53
4 User Feature - Part 1 17:47
5 User Feature - Part 2 07:51
6 Talking English To Your Computer 11:05
7 Teaching Your Database To Forget 07:42
8 Creating User Data From Behat Background - Part 1 14:44
9 Creating User Data From Behat Background - Part 2 11:33
10 Creating A Custom RestApiContext 17:44
11 Our First Passing Behat User Scenario 12:01
12 Our Next Passing Step 13:10
13 Securing Our User Endpoint - Part 1 17:17
14 Securing Our User Endpoint - Part 2 24:27
15 Securing Our User Endpoint - Part 3 24:47
16 Log In To A Symfony API With JWTs (LexikJWTAuthenticationBundle) 11:02
17 Implementing PATCH for Users 18:17
18 Improving our API User Experience 13:59
19 GET a Collection of Accounts 12:15
20 POSTing in New Accounts 14:34
21 PUT and PATCH for Accounts 12:14
22 How To DELETE Existing Accounts 05:11
23 File Feature Overview 11:40
24 File - Using Existing Resources as Boilerplate 15:17
25 File POST 14:53
26 Fixing A Bug In POST Guided By Behat 12:50
27 Wrapping Up With File DELETE 07:47