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.
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.
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.
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.
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