File - Using Existing Resources as Boilerplate
In this video we cover off all the elements of the File
resource that stay roughly the same (in terms of implementation) as the resources we have already covered before (User
and Account
).
The reasoning behind this is that once you have an understanding of how the pieces of this system fit together, it is relatively straightforward - dare I say, copy and paste - to add in new resources as needed.
As mentioned in the previous video, File
is a nested resource, which means that a File
will always belong to an Account
, and this can be seen clearly from the URL structure:
/accounts/{accountId}/files/{fileId}
To configure this, we need to add in an extra line to our routing definition. To make this a little easier to manage, I split my routing into two files. The first is the standard routing.yml
file that comes out of the box with Symfony, and the second is a routing_api.yml
file which I create and then 'link to' using the resource
option:
# app/config/routing.yml
api_login_check:
path: /login
NelmioApiDocBundle:
resource: "@NelmioApiDocBundle/Resources/config/routing.yml"
prefix: "/doc"
api:
type: rest
resource: "routing_api.yml"
And:
# app/config/routing_api.yml
# -- ROOT RESOURCES --
accounts:
type: rest
resource: AppBundle\Controller\AccountsController
users:
type: rest
resource: AppBundle\Controller\UsersController
# -- ACCOUNT CHILDREN --
files:
type: rest
parent: accounts
resource: AppBundle\Controller\FilesController
Whilst I don't tend to mix API and 'normal' Symfony builds into the same project, it should be less cumbersome to do so if your routing structure (and other config) is separated out.
Service Definitions
Whilst I said that in this video we talk about what stays the same, that doesn't mean there isn't a lot of new stuff to cover. There is new code and config here, but it is largely very similar to what we have already configured.
The best example of this is in services.yml
, which sees almost every 'section' have something new added in. Let's take a closer look:
# app/config/services.yml
# -- DATA TRANSFORMERS --
crv.data_transformer.account_data_transformer:
class: AppBundle\DataTransformer\AccountDataTransformer
crv.data_transformer.file_data_transformer:
class: AppBundle\DataTransformer\FileDataTransformer
We covered off the reasoning and implementation behind Data Transformers in an earlier video, so I won't go into the specifics here.
Generally for each new resource, the existing service definitions (and associated classes) will need creating for the new resource also. We are going to allow files to be created and updated, so we need a way of converting between the data sent in, and the entities which Doctrine can persist. This is handled by our Data Transformers, so we need to add in a Data Transformer for File
, just like we did for Account
.
This repeats itself for each of the other type of services we have already defined:
# app/config/services.yml
# -- FORM HANDLER --
crv.form.handler.account_form_handler:
class: AppBundle\Form\Handler\FormHandler
arguments:
- "@form.factory"
- "@crv.form.type.account"
crv.form.handler.file_form_handler:
class: AppBundle\Form\Handler\FormHandler
arguments:
- "@form.factory"
- "@crv.form.type.file"
# -- REPOSITORY --
crv.repository.doctrine_account_repository:
class: AppBundle\Repository\Doctrine\DoctrineAccountRepository
arguments:
- "@crv.repository.common_doctrine_repository"
- "@crv.doctrine_entity_repository.account"
crv.repository.doctrine_file_repository:
class: AppBundle\Repository\Doctrine\DoctrineFileRepository
arguments:
- "@crv.repository.common_doctrine_repository"
- "@crv.doctrine_entity_repository.file"
# etc
And as mentioned, this means each of the existing classes - e.g. DoctrineAccountRepository
- can easily be copy / pasted, have the methods re-written, and be rather quickly re-used. This effectively becomes your boilerplate. Personally I find this is a huge help to me when working on a project. If the file patterns repeat themselves, you can focus on writing the application rather than structuring the system.
An example of this can be illustrated with the Data Transformers from earlier. If we look at the 'shape' of these files without their implementations, you will hopefully see what I am aiming for:
<?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)
{
}
public function updateFromDTO(AccountInterface $account, AccountDTO $dto)
{
}
}
Creating an equivalent FileDataTransformer
becomes trivial:
<?php
// src/AppBundle/DataTransformer/FileDataTransformer.php
namespace AppBundle\DataTransformer;
use AppBundle\DTO\FileDTO;
use AppBundle\Model\FileInterface;
class FileDataTransformer
{
/**
* @param FileInterface $file
* @return FileDTO
*/
public function convertToDTO(FileInterface $file)
{
}
/**
* @param FileInterface $file
* @param FileDTO $dto
* @return FileInterface
*/
public function updateFromDTO(FileInterface $file, FileDTO $dto)
{
}
}
The implementations differ, but the 'shape' stays the same.
Unfortunately PHP interfaces won't allow generics so we can't create a single common interface here. It would be good practice to create an interface per implementation anyway, which I haven't done here. It's unlikely at this stage that I would create multiple implementations of FileDataTransformer
, but even so, other code that relies on this will ultimately end up relying on a concrete implementation - which is not so good, Al.