PUT and PATCH for Accounts
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.