How kris-writes-symfony-apps-london

Preview:

DESCRIPTION

You've seen Kris' open source libraries, but how does he tackle coding out an application? Walk through green fields with a Symfony expert as he takes his latest “next big thing” idea from the first line of code to a functional prototype. Learn design patterns and principles to guide your way in organizing your own code and take home some practical examples to kickstart your next project.

Citation preview

How Kris Writes Symfony Apps

Mapping Layers

thin

thin controller fat model

MVC

Is Symfony an MVC framework?

HTTP

Symfony is an HTTP framework

HT

TP Land

Application Land

Controller

The controller is thin because it maps from

HTTP-land to application-land.

What about the model?

Application Land

Persistence Land

Model

The model maps from application-land to persistence-land.

Model

Application Land

Persistence Land

HT

TP Land

Controller

Who lives in application land?

Thin controller, thin model… Fat service layer

Should there be managers?

Application Events

Listen

/** @DI\Observe(UserEvent::CREATE) */public function onUserCreate(UserEvent $event){ $user = $event->getUser();

$activity = new Activity(); $activity->setActor($user); $activity->setVerb('register'); $activity->setCreatedAt($user->getCreatedAt());

$this->dm->persist($activity);}

/** @DI\Observe(UserEvent::USERNAME_CHANGE) */public function onUsernameChange(UserEvent $event){ $user = $event->getUser(); $dm = $event->getDocumentManager();

$dm->getRepository('Model:Widget') ->updateDenormalizedUsernames($user);}

/** @DI\Observe(UserEvent::FOLLOW) */public function onFollow(UserUserEvent $event){ $event->getUser() ->getStats() ->incrementFollowedUsers(1); $event->getOtherUser() ->getStats() ->incrementFollowers(1);}

Dispatch

$event = new UserEvent($dm, $user);$dispatcher->dispatch(UserEvent::CREATE, $event);

$event = new UserEvent($dm, $user);$dispatcher->dispatch(UserEvent::UPDATE, $event);

$event = new UserUserEvent($dm, $user, $otherUser);$dispatcher->dispatch(UserEvent::FOLLOW, $event);

preFlush

public function preFlush(ManagerEventArgs $event){ $dm = $event->getObjectManager(); $uow = $dm->getUnitOfWork();

foreach ($uow->getIdentityMap() as $class => $docs) { if (is_a($class, 'Kris\Model\User')) { foreach ($docs as $doc) { $this->processUserFlush($dm, $doc); } } elseif (is_a($class, 'Kris\Model\Widget')) { foreach ($docs as $doc) { $this->processWidgetFlush($dm, $doc); } } }}

Decouple your application by delegating work to clean, concise,

single-purpose event listeners.

Model

Treat your model like a princess.

She gets her own wing of the palace…

doctrine_mongodb: auto_generate_hydrator_classes: %kernel.debug% auto_generate_proxy_classes: %kernel.debug% connections: { default: ~ } document_managers: default: connection: default database: kris mappings: model: type: annotation dir: %src%/Kris/Model prefix: Kris\Model alias: Model

// repo for src/Kris/Model/User.php$repo = $this->dm->getRepository('Model:User');

…doesn't do any work…

use Kris\Bundle\MainBundle\Canonicalizer;

public function setUsername($username){ $this->username = $username;

$canonicalizer = Canonicalizer::instance(); $this->usernameCanonical = $canonicalizer->canonicalize($username);}

use Kris\Bundle\MainBundle\Canonicalizer;

public function setUsername($username, Canonicalizer $canonicalizer){ $this->username = $username; $this->usernameCanonical = $canonicalizer->canonicalize($username);}

…and is unaware of the work being done around her.

public function setUsername($username){ // a listener will update the // canonical username $this->username = $username;}

Cabinets don’t open themselves.

Contextual Configuration

Save your future self a headache.

# @MainBundle/Resources/config/widget.ymlservices: widget_twiddler: class: Kris\Bundle\MainBundle\Widget\Twiddler arguments: - @event_dispatcher - @?logger

JMSDiExtraBundle

/** @DI\Service("widget_twiddler") */class Twiddler{ /** @DI\InjectParams() */ public function __construct( EventDispatcherInterface $dispatcher, LoggerInterface $logger = null) { // ... }}

services: # aliases for auto-wiring container: @service_container dm: @doctrine_mongodb.odm.document_manager doctrine: @doctrine_mongodb dispatcher: @event_dispatcher security: @security.context

require.js

{% block head %}<script>require( [ "view/user", "model/user" ], function(UserView, User) { var view = new UserView({ model: new User({{ user|serialize|raw }}), el: document.getElementById("user") }) })</script>{% endblock %}

JMSSerializerBundle

{% block head %}<script>require( [ "view/user", "model/user" ], function(UserView, User) { var view = new UserView({ model: new User({{ user|serialize|raw }}), el: document.getElementById("user") }) })</script>{% endblock %}

/** @ExclusionPolicy("ALL") */class User{ private $id;

/** @Expose() */ private $firstName;

/** @Expose() */ private $lastName;}

Five more things…

When to create a new bundle

• Anything reusable

• A new feature

• Lots of classes relating to one feature

• Integration with a third party

{% include 'MainBundle:Account/Widget:sidebar.html.twig' %}

{% include 'AccountBundle:Widget:sidebar.html.twig' %}

Access Control

The Symfony ACL is for arbitrary permissions

Encapsulate access logic in custom voter classes

public function vote(TokenInterface $token, $widget, array $attributes){ $result = VoterInterface::ACCESS_ABSTAIN; if (!$this->supportsClass(get_class($widget))) { return $result; }

foreach ($attributes as $attribute) { if (!$this->supportsAttribute($attribute)) { continue; }

$result = VoterInterface::ACCESS_DENIED; if ($token->getUser() === $widget->getUser()) { return VoterInterface::ACCESS_GRANTED; } }

return $result;}

JMSSecurityExtraBundle

/** @SecureParam(name="widget", permissions="OWNER") */public function editAction(Widget $widget){ // ...}

{% if is_granted('OWNER', widget) %}{# ... #}{% endif %}

No query builders outside of repositories

class WidgetRepository extends DocumentRepository{ public function findByUser(User $user) { return $this->createQueryBuilder() ->field('userId')->equals($user->getId()) ->getQuery() ->execute(); }

public function updateDenormalizedUsernames(User $user) { $this->createQueryBuilder() ->update() ->multiple() ->field('userId')->equals($user->getId()) ->field('userName')->set($user->getUsername()) ->getQuery() ->execute(); }}

Eager ID creation

public function __construct(){ $this->id = (string) new \MongoId();}

public function __construct(){ $this->id = (string) new \MongoId(); $this->createdAt = new \DateTime(); $this->widgets = new ArrayCollection();}

Remember your clone constructor

$foo = new Foo();$bar = clone $foo;

public function __clone(){ $this->id = (string) new \MongoId(); $this->createdAt = new \DateTime(); $this->widgets = new ArrayCollection( $this->widgets->toArray() );}

public function __construct(){ $this->id = (string) new \MongoId(); $this->createdAt = new \DateTime(); $this->widgets = new ArrayCollection();}

public function __clone(){ $this->id = (string) new \MongoId(); $this->createdAt = new \DateTime(); $this->widgets = new ArrayCollection( $this->widgets->toArray() );}

Only flush from the controller

public function theAction(Widget $widget){ $this->get('widget_twiddler') ->skeedaddle($widget);

$this->flush();}

Questions?

Recommended