POSTing in New 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 the POST method for our Accounts endpoint. POST is used to allow our API consumers to submit brand new data to our API. The data that the user / consumer is sending in should be structurally identical (almost!) to the data that we would get back from a GET request to the same endpoint.

Why only almost identical? Well, when we GET, we should receive an id field:

    Scenario: User can GET an individual Account by ID
      When I send a "GET" request to "/accounts/a1"
      Then the response code should be 200
       And the response header "Content-Type" should be equal to "application/json; charset=utf-8"
       And the response should contain json:
        """
        {
          "id": "a1",
          "name": "account1",
          "users": [{
              "id": "u1",
              "username": "peter",
              "email": "peter@test.com"
          }]
        }
        """

But when we are initially sending in some new data, we won't have an id field, so that can be omitted:

    Scenario: User can add a new Account
      When I send a "POST" request to "/accounts" with body:
        """
        {
          "name": "a new account name",
          "users": [{
              "id": "u1",
              "username": "peter",
              "email": "peter@test.com"
          }]
        }
        """
      Then the response code should be 201

Technically you do not need to do this. You could have your GET response look entirely different to the 'shape' of data that is needed by a POST request. I'd strongly advise against this however, as anyone who has to directly interact with your API (developers... you!) will not thank you for it. Consistency is key.

Converting From ID to Data

You may have a question at this point: how does Symfony's form know how to convert the users JSON data back into a real User?

Fortunately, FOSRESTBundle comes with a solution to this problem in the form of EntityToIdObjectTransformer. Essentially this is a data transformer that can look at our submitted JSON, find the ID field, and then do a query against the repository to find the underlying entity.

What I found here with their default implementation is that this would give access to any entity that is requested. We've already covered in quite some depth that we don't want that to be the case in our system. As such, I have changed the provided implementation with one that better suits our needs in this instance:

<?php

// src/AppBundle/Form/Transformer/EntityToIdObjectTransformer.php

namespace AppBundle\Form\Transformer;

use AppBundle\Repository\RepositoryInterface;
use Symfony\Component\Form\DataTransformerInterface;
use Symfony\Component\Form\Exception\TransformationFailedException;

/**
 * Class EntityToIdObjectTransformer
 * @package AppBundle\Form\Transformer
 */
class EntityToIdObjectTransformer implements DataTransformerInterface
{
    /**
     * @var RepositoryInterface
     */
    private $repository;

    /**
     * EntityToIdObjectTransformer constructor.
     * @param RepositoryInterface $repository
     */
    public function __construct(RepositoryInterface $repository)
    {
        $this->repository = $repository;
    }

    /**
     * Do nothing.
     *
     * @param object|null $object
     *
     * @return string
     */
    public function transform($object)
    {
        return '';
    }

    /**
     * Transforms an array including an identifier to an object.
     *
     * @param array $idObject
     *
     * @return object|null
     *
     * @throws TransformationFailedException if object is not found.
     */
    public function reverseTransform($idObject)
    {
        if (!is_array($idObject)) {
            return false;
        }

        if ( ! array_key_exists('id', $idObject)) {
            throw new TransformationFailedException('Unable to find an ID key / value pair on passed in $idObject');
        }

        $object = $this->repository->findOneById($idObject['id']);

        if (null === $object) {
            throw new TransformationFailedException(sprintf(
                'A "%s" with ID "%s" does not exist!',
                get_class($object),
                $idObject['id']
            ));
        }

        return $object;
    }
}

This way, we use our repository to do the look up, rather than a direct query in the original implementation.

Another tweak I added was to allow JSON arrays to be converted back:

<?php

// src/AppBundle/Form/Transformer/ManyEntityToIdObjectTransformer.php

namespace AppBundle\Form\Transformer;

use Symfony\Component\Form\DataTransformerInterface;

class ManyEntityToIdObjectTransformer implements DataTransformerInterface
{
    /**
     * @var EntityToIdObjectTransformer
     */
    private $entityToIdObjectTransformer;

    /**
     * ManyEntityToIdObjectTransformer constructor.
     * @param EntityToIdObjectTransformer $entityToIdObjectTransformer
     */
    public function __construct(EntityToIdObjectTransformer $entityToIdObjectTransformer)
    {
        $this->entityToIdObjectTransformer = $entityToIdObjectTransformer;
    }

    /**
     * Do nothing
     *
     * @param array $array
     * @return array
     */
    public function transform($array)
    {
        $transformed = [];

        if (empty($array) || null === $array) {
            return $transformed;
        }

        foreach ($array as $k => $v) {
            $transformed[] = $this->entityToIdObjectTransformer->transform($v);
        }

        return $transformed;
    }

    /**
     * Transforms an array of arrays including an identifier to an object.
     *
     * @param array $array
     *
     * @return array
     */
    public function reverseTransform($array)
    {
        if (!is_array($array)) {
            $array = [$array];
        }

        $reverseTransformed = [];

        foreach ($array as $k => $v) {
            $reverseTransformed[] = $this->entityToIdObjectTransformer->reverseTransform($v);
        }

        return $reverseTransformed;
    }
}

This is useful as in the case of our /accounts endpoint, the users field is an array:

          "users": [{
              "id": "u1",
              "username": "peter",
              "email": "peter@test.com"
          }]

By using the ManyEntityToIdObjectTransformer, any number of passed in id fields can be converted back to the respective entity. And because the query goes via the repository, only those entities that the current User should have access to will be accessible.

Interestingly at this point, this exposes a bug in the system. I would like to be able to POST in a JSON array of Users who have access to the new Account. However, as we have already covered, we only have access to our own User object. When the id field is attempted to be converted back to an object, the repository will throw a 401 at this stage. You may not need this functionality. I have a requirement here that is not yet implemented - I'd like to email any users that are requested to join a new Account.

Let's look at how a form would look with these extra transformers added:

<?php

// src/AppBundle/Form/Type/AccountType.php

namespace AppBundle\Form\Type;

use AppBundle\Repository\UserRepositoryInterface;
use AppBundle\Form\Transformer\EntityToIdObjectTransformer;
use AppBundle\Form\Transformer\ManyEntityToIdObjectTransformer;
use Symfony\Component\Form\AbstractType;
use Symfony\Component\Form\FormBuilderInterface;
use Symfony\Component\Form\Extension\Core\Type\TextType;
use Symfony\Component\Form\Extension\Core\Type\CollectionType;
use Symfony\Component\OptionsResolver\OptionsResolver;

/**
 * Class AccountType
 * @package AppBundle\Form\Type
 */
class AccountType extends AbstractType
{
    /**
     * @var UserRepositoryInterface
     */
    private $userRepository;

    /**
     * AccountType constructor.
     * @param UserRepositoryInterface $userRepository
     */
    public function __construct(UserRepositoryInterface $userRepository)
    {
        $this->userRepository = $userRepository;
    }

    /**
     * @param FormBuilderInterface $builder
     * @param array $options
     */
    public function buildForm(FormBuilderInterface $builder, array $options)
    {
        $userTransformer = new EntityToIdObjectTransformer($this->userRepository);
        $userCollectionTransformer = new ManyEntityToIdObjectTransformer($userTransformer);

        $builder
            ->add(
                $builder->create('users', TextType::class)->addModelTransformer($userCollectionTransformer)
            )
        ;

        $builder
            ->add('name', TextType::class)
        ;
    }

    public function configureOptions(OptionsResolver $resolver)
    {
        $resolver->setDefaults([
            'data_class' => 'AppBundle\DTO\AccountDTO',
        ]);
    }

    public function getName()
    {
        return 'account';
    }
}

These steps are seperated as you may not always have a JSON array:

$userTransformer = new EntityToIdObjectTransformer($this->userRepository);
$userCollectionTransformer = new ManyEntityToIdObjectTransformer($userTransformer);

And then we come to the non-standard way you may used to be using Symfony forms:

    public function configureOptions(OptionsResolver $resolver)
    {
        $resolver->setDefaults([
            'data_class' => 'AppBundle\DTO\AccountDTO',
        ]);
    }

Rather than using entities with the form, instead each form takes a DTO, or Data Transfer Object.

Why are we doing this?

Well, the entities do not necessarily have setters for each property. This is a bit of a deal breaker for Symfony's form component. Without a setter per property, the form will throw errors when trying to update the entity passed in to the form.

An example of this may be the Account::name property:

// src/AppBundle/Entity/Account.php

* snip *

class Account implements AccountInterface, \JsonSerializable
{
    /**
     * @ORM\Column(type="string", name="name")
     * @JMSSerializer\Expose
     * @JMSSerializer\Groups({"accounts_all","accounts_summary"})
     */
    private $name;

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

    /**
     * @param string $newName
     * @return $this
     */
    public function changeName($newName)
    {
        $this->name = (string) $newName;

        return $this;
    }

This effectively breaks Symfony's form component if we try to directly use this entity.

Instead, a DTO can be created which does have a setName method available:

<?php

// src/AppBundle/DTO/AccountDTO.php

namespace AppBundle\DTO;

use AppBundle\Model\AccountInterface;
use AppBundle\Model\FileInterface;
use AppBundle\Model\ScheduleInterface;
use AppBundle\Model\SocialMediaProfileInterface;
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, DTOInterface, SymfonyFormDTOInterface
{
    /**
     * @var string
     * @Assert\NotBlank()
     */
    private $name;

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

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

        return $this;
    }

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

You may think this is totally overkill, and you are welcome to stick to the classic approach of using entities directly with your form.

Using the DTO is really no different than how you are likely very used to using the Symfony form already:

// src/AppBundle/Handler/AccountHandler.php

    /**
     * @param array                 $parameters
     * @param array                 $options
     * @return AccountInterface
     */
    public function post(array $parameters, array $options = [])
    {
        $accountDTO = $this->formHandler->handle(
            new AccountDTO(),
            $parameters,
            Request::METHOD_POST,
            $options
        );

        $account = $this->factory->createFromDTO($accountDTO);

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

        return $account;
    }

Rather than pass in a new instance of our Account entity, instead we simply pass in the AccountDTO, with all it's setters and getters, that the form component needs.

We can do a bit of validation here as required - checking that the Account name isn't blank in this case.

If we get to this line:

$account = $this->factory->createFromDTO($accountDTO);

We can assume everything went well in our form submission. Exceptions would have been thrown during the handle method if not. We've already covered these in previous videos.

It could be argued that a named constructor of Account::createFromDTO(AccountDTO $accountDTO) would be a better implementation here than using a factory:

<?php

// src/AppBundle/Factory/AccountFactory.php

namespace AppBundle\Factory;

use AppBundle\DTO\AccountDTO;
use AppBundle\Entity\Account;

class AccountFactory implements AccountFactoryInterface
{
    /**
     * @param  string       $accountName
     * @return Account
     */
    public function create($accountName)
    {
        return new Account($accountName);
    }

    /**
     * @param  AccountDTO   $accountDTO
     * @return Account
     */
    public function createFromDTO(AccountDTO $accountDTO)
    {
        $account = self::create($accountDTO->getName());

        foreach ($accountDTO->getUsers() as $user) { /** @var $user \AppBundle\Model\UserInterface */
            $user->addAccount($account);
        }

        foreach ($accountDTO->getSocialMediaProfiles() as $socialMediaProfile) {
            $account->addSocialMediaProfile($socialMediaProfile);
        }

        return $account;
    }
}

If you do decide to refactor to named constructors, there's a helpful section in the PHPSpec Cookbook on testing named constructors.

We're going to use the exact same concept in the PUT and PATCH stages, only there we will need to take the existing data and construct a DTO from the entity before we can use the form. We'll cover that in the next video.


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