Structuring my applications, Cont'd

It really irks me when I see some design/architecture decisions other developers have made but there's no technical explanation. What packages did they use? What challenges did they face? What trade-offs were made?

I'll go over some more specifics in this post.

Recap

In the last post, I described how I structure my Model directory to house framework agnostic code. I don't develop that in isolation though. I write a feature in the Model directory and then I go to my src/Acme/AwesomeProject/Infrastructure/AppBundle directory to implement what I need to in my Symfony application. This includes:

  • Writing Doctrine ORM mapping configurations
  • Implementing repositories
  • Wiring everything up in the IoC container
  • Defining routes and controllers
  • Acceptance testing with Behat.

Bundles

When I started using Symfony, I saw that you develop your code inside "bundles". I also started off using the FOSUserBundle. Seeing that set me on creating bundles for basically each of my entities. All of the routes/controller/config for an entity would be in its very own bundle. I finally came to the realization that that was moronic. They were all so coupled together that it didn't make any sense. It's not like I could use my TaskBundle in another application. It was so ingrained with classes in the other bundles. That is why I now have a single AppBundle.

This is what's inside. It looks very similar to my Model directory for a reason. This is the Symfony implementation of my application.

Model Directory


Let's go over the directories.

Command

This holds my custom console commands, not the same commands in my Model directory. I view these as controllers. They may not be dealing with http, but they're identical in purpose: take input data, create command objects, send them to the application via the command bus.

Controllers

Pretty self-explanatory but here's an example of my most complicated controller method.

namespace Acme\AwesomeProject\Infrastructure\AppBundle\Controller

use Acme\AwesomeProject\Infrastructure\AppBundle\Form\RelocatePersonCommandType;
use Acme\AwesomeProject\Model\Command\RelocatePersonCommand;

class PersonController extends ApiController
{
    /**
     * @param Request $request
     * @param string $personId
     * @return Response
     */
    public function relocatePersonAction(Request $request, $personId)
    {
        $type = new RelocatePersonCommandType($personId);
        $form = $this->createForm($type, null, ['method' => 'PUT']);

        $form->handleRequest($request);

        if ($form->isValid()) {

            /** @var RelocatePersonCommand $command */
            $command = $form->getData();
            $this->getCommandBus()->handle($command);

            return $this
                ->setData(['person' => $command->getPerson()])
                ->respond();
        }

        // creates a response with errors extracted
        // from the form object
        return $this->respondWithForm($form);
    }
}

DependencyInjection

This is for Symfony's bundle architecture. If you're not familiar with Symfony, you don't need to worry about it for this post.

Features

This holds my context and feature files for testing with Behat.

Pro tip! If you're using Symfony, use the extension docs to get up and running. I wasted a lot of time thinking everything was laid out in the Behat docs.

Form

Symfony's form component is really powerful. It maps http requests into php objects. It also triggers validation checks. The docs all show mapping data straight into entities. That simply doesn't work with my architecture because of the way I've written my entity constructors. They require arguments and Symfony's form company attempts to use setters to get the data into the entities.

I need the form component to map to command objects anyway. William Durand has a great post about bending forms to your will. I use this technique to create my command objects from POST and PUT requests.

Provider

This holds my implementation of the CurrentUserProvider interface defined in my Model directory.

namespace Acme\AwesomeProject\Infrastructure\AppBundle\Provider;

use Acme\AwesomeProject\Model\Entity\User;
use Acme\AwesomeProject\Model\Provider\CurrentUserProvider;
use Symfony\Component\Security\Core\Authentication\Token\Storage\TokenStorageInterface;

class SymfonyCurrentUserProvider implements CurrentUserProvider
{
    /**
     * @var TokenStorageInterface
     */
    private $storage;

    /**
     * @param TokenStorageInterface $storage
     */
    public function __construct(TokenStorageInterface $storage)
    {
        $this->storage = $storage;
    }

    /**
     * @return User
     */
    public function getUser()
    {
        if (null === $token = $this->storage->getToken()) {
            return null;
        }

        if (null === $user = $token->getUser()) {
            return null;
        }

        if (!is_object($user)) {
            return null;
        }

        return $user;
    }
}

Repository

This is where I put my Doctrine implementations of the repositories I've interfaced in the Model directory.

Resources

This is Symfony specific. It contains IoC container configuration, Doctrine ORM mappings, route definitions, serialization configuration, and twig templates.

By default, Symfony bundles come with a single services.yml file for you to wire up your IoC container. I delete that and make a services directory. I then break up the service definitions into multiple files, handlers.yml, infrastructure.yml, listeners.yml, repositories.yml, security.yml, etc. This avoids having a single gigantic services file. It makes finding and tweaking definitions much saner. You'll need to add these new files to the bundle's extension class.

namespace Acme\AwesomeProject\Infrastructure\AppBundle\DependencyInjection;

use Symfony\Component\DependencyInjection\ContainerBuilder;
use Symfony\Component\Config\FileLocator;
use Symfony\Component\HttpKernel\DependencyInjection\Extension;
use Symfony\Component\DependencyInjection\Loader;

class AcmeAwesomeProjectInfrastructureAppExtension extends Extension
{
    /**
     * {@inheritdoc}
     */
    public function load(array $configs, ContainerBuilder $container)
    {
        $configuration = new Configuration();
        $config = $this->processConfiguration($configuration, $configs);

        $loader = new Loader\YamlFileLoader($container, new FileLocator(__DIR__.'/../Resources/config'));
        $loader->load('services/repositories.yml');
        $loader->load('services/infrastructure.yml');
        $loader->load('services/security.yml');
        $loader->load('services/handlers.yml');
        $loader->load('services/listeners.yml');

        $env = $container->getParameter('kernel.environment');

        if (in_array($env, ['dev', 'test'])) {
            $loader->load("services/dev.yml");
        }
    }
}

The Symfony's Doctrine ORM bundle expects your entities to be in an Entity direcotry inside bundles. It also expects your mapping configurations to be in a certain place in your Resources directory. Well...my entities are defined in my Model directory, outside of my AppBundle. Here's how I configure Doctrine to know how to map my objects.

doctrine:
    dbal:
        default_connection: default
        connections:
            default:
                driver:   "%database_driver%"
                host:     "%database_host%"
                port:     "%database_port%"
                dbname:   "%database_name%"
                user:     "%database_user%"
                password: "%database_password%"
                charset:  UTF8
                logging:  true

            admin:
                driver:   "%database_driver%"
                host:     "%database_host%"
                port:     "%database_port%"
                dbname:   "%database_name%"$
                user:     "%database_admin_user%"
                password: "%database_admin_password%"
                charset:  UTF8

    orm:
        default_entity_manager: default
        entity_managers:
            default:
                connection: default
                mappings:
                    AcmeAwesomeProjectOAuthBundle: ~
                    FOSOAuthServerBundle: ~
                    models:
                        mapping:   true
                        type:      yml
                        dir:       "%kernel.root_dir%/../src/Acme/AwesomeProject/Infrastructure/AppBundle/Resources/config/entity-mappings"
                        prefix:    Acme\AwesomeProject\Model\Entity
                        is_bundle: false
                    embeddables:
                        mapping:   true
                        type:      yml
                        dir:       "%kernel.root_dir%/../src/Acme/AwesomeProject/Infrastructure/AppBundle/Resources/config/embeddable-mappings"
                        prefix:    Acme\AwesomeProject\Model\ValueObject
                        is_bundle: false

If you're using other bundles that adhere to the expected structure you can configure them with BundleName: ~ as seen above. For my entities and value objects defined in my Model directory, I have to explicitly tell Doctrine where to find the mapping files.

Inside the entity-mappings and embeddable-mappings directories I have files like User.orm.yml, Project.orm.yml, Location.orm.yml, etc.

Serializer

I'm using the JMS Serializer to turn objects into json. Most of the time you can configure how you want objects to be serialized with a configuration file(located in the Resources directory). Sometimes it's just easier to serialize objects yourself in code. This is where I put those serializer classes.

The JMS Serializer has a Symfony bundle available. It expects the classes to be serialized to be in a bundle. You can get around this by configuring it in your app/config/config.yml file.

jms_serializer:
    metadata:
        auto_detection: true
        directories:
            AppBundle:
                namespace_prefix: Acme\AwesomeProject\Model
                path: "%kernel.root_dir%/../src/Acme/AwesomeProject/Infrastructure/AppBundle/Resources/serializer"

Now, everytime the serializer encounters an object with the namespace that starts with Acme\AwesomeProject\Model it will look in my AppBundle's Resources/serializer directory. I can define how to serialize an entity by defining a Entity.Project.yml file inside the Resources/serializer directory. Directories are signifed with dots. I can define configuration for a value object with a ValueObject.CategorySummary.yml file.

Be aware! After you modify your serializer configuration files be sure to clear your app's cache! I've spent way too much time trying to figure out why my latest changes weren't being reflected.

Service

This is where I put implementations defined in my Model/Service directory, like my TwigSwiftUserMailer.

Tests

This is where my functional tests go. Mainly, this is where I use PHPUnit to ensure my entities and value objects are being persisted correctly. I actually interact with a test database in these tests.

Other bundles

There other bundles I typically pull into my projects. One such bundle is the FOSRestBundle. This bundle allows your application to accept form encoded data, json, and xml and will massage that data into the same format regardless of content type. It let's you pass Request objects straight into your forms to create command objects. Although it supports form encoded data, json, and xml, I only configure it to use json.

It also takes over how exceptions are displayed to the client. You can configure it to only show exception messages from certain classes and map exception classes to http status codes.

fos_rest:
    routing_loader:
        default_format: json
    param_fetcher_listener: true
    body_listener: true
    format_listener:
        rules:
            - { path: '^/', priorities: [ 'json'], fallback_format: json, prefer_extension: false }
    view:
        view_response_listener: 'force'
        formats:
            json: true
    service:
        serializer: jms_serializer.serializer
    exception:
        codes:
            Symfony\Component\Security\Core\Exception\AccessDeniedException: 403
            Acme\AwesomeProject\Model\Exception\UserNotFoundException: 404
            Acme\AwesomeProject\Model\Exception\AssertionFailedException: 400
            Acme\AwesomeProject\Model\Exception\AccessDeniedException: 403
            Acme\AwesomeProject\Model\Exception\DomainException: 400
        messages:
            Symfony\Component\Security\Core\Exception\AccessDeniedException: true
            Acme\AwesomeProject\Model\Exception\UserNotFoundException: true
            Acme\AwesomeProject\Model\Exception\AssertionFailedException: true
            Acme\AwesomeProject\Model\Exception\AccessDeniedException: true
            Acme\AwesomeProject\Model\Exception\DomainException: true

I also secure my applications with the FOSOauthServerBundle but you can easily create your own simpler authentication mechanism.

Heads up! If you do decide to implement your own authentication mechanism, the docs are not correct. You can see my struggle in this stackoverflow question.

Because I'm usually developing API's that have javascript frontend clients, I use the NelmioCorsBundle to get around CORS headaches with a simple configuration.

nelmio_cors:
    paths:
        '^/':
            allow_origin: ['*']
            allow_headers: ['*']
            allow_methods: ['POST', 'PUT', 'GET', 'DELETE', 'OPTIONS']
            max_age: 3600

Wrapping up

Welp, that's it. That's my glimpse into how I've started to develop apps. I'm pretty happy with it but always looking for better ways of doing things. Hopefully it gives you some ideas. I encourage you to write up a post about how you go about writing apps. I would love to read it. Stop giving my abstract ideas. I want to see the nitty-gritty details, the trade-offs, the implementation details!

Ask me questions in the comments or give me suggestions. I'm definitely open to those. If you liked what you read, you can subscribe to be notified of my future posts in the sidebar :)

Categories: PHP

Tags: Symfony, PHP