How Kris Writes Symfony Apps

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@kriswallsmith • February 9, 2013

About Me

• Born, raised, & live in Portland

• 10+ years of experience

• Lead Architect at OpenSky

• Open source fanboy

@kriswallsmith.net

brewcycleportland.com

assetic

Buzz

Spork

Go big or go home.

Getting Started

composer create-project \ symfony/framework-standard-edition \ opti-grab/ 2.2.x-dev

- "doctrine/orm": "~2.2,>=2.2.3",- "doctrine/doctrine-bundle": "1.2.*",+ "doctrine/mongodb-odm-bundle": "3.0.*",+ "jms/serializer-bundle": "1.0.*",

./app/console generate:bundle \ --namespace=OptiGrab/Bundle/MainBundle

assetic: debug: %kernel.debug% use_controller: false bundles: [ MainBundle ] filters: cssrewrite: ~ uglifyjs2: { compress: true, mangle: true } uglifycss: ~

jms_di_extra: locations: bundles: - MainBundle

public function registerContainerConfiguration(LoaderInterface $loader){ $loader->load(__DIR__.'/config/config_'.$this->getEnvironment().'.yml');

// load local_*.yml or local.yml if ( file_exists($file = __DIR__.'/config/local_'.$this->getEnvironment().'.yml') || file_exists($file = __DIR__.'/config/local.yml') ) { $loader->load($file); }}

MongoDB

Treat your model like a princess.

She gets her own wingof the palace…

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

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

…doesn't do any work…

use OptiGrab\Bundle\MainBundle\Canonicalizer;

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

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

use OptiGrab\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;}

No query buildersoutside 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 yourclone 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();}

Save space on field names

/** @ODM\String(name="u") */private $username;

/** @ODM\String(name="uc") @ODM\UniqueIndex */private $usernameCanonical;

public function getUsername(){ return $this->username ?: $this->usernameCanonical;}

public function setUsername($username){ if ($username) { $this->usernameCanonical = strtolower($username); $this->username = $username === $this->usernameCanonical ? null : $username; } else { $this->usernameCanonical = null; $this->username = null; }}

No proxy objects

/** @ODM\ReferenceOne(targetDocument="User") */private $user;

public function getUser(){ if ($this->userId && !$this->user) { throw new UninitializedReferenceException('user'); }

return $this->user;}

Mapping Layers

What is a mapping layer?

A mapping layer is thin

Thin controller, fat model…

Is Symfony an MVC framework?

Symfony is an HTTP framework

HT

TP Land

Application Land

Controller

The controller maps fromHTTP-land to application-land.

What about the model?

public function registerAction(){ // ... $user->sendWelcomeEmail(); // ...}

public function registerAction(){ // ... $mailer->sendWelcomeEmail($user); // ...}

Application Land

Persistence Land

Model

The model maps fromapplication-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!

Application Events

Use lots of them

That happened.

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

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

Unit of Work

public function onFlush(OnFlushEventArgs $event){ $dm = $event->getDocumentManager(); $uow = $dm->getUnitOfWork();

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

private function processUserFlush(DocumentManager $dm, User $user){ $uow = $dm->getUnitOfWork(); $meta = $dm->getClassMetadata('Model:User'); $changes = $uow->getDocumentChangeSet($user);

if (isset($changes['id'][1])) { $this->dispatcher->dispatch(UserEvents::CREATE, new UserEvent($dm, $user)); }

if (isset($changes['usernameCanonical'][0]) && null !== $changes['usernameCanonical'][0]) { $this->dispatcher->dispatch(UserEvents::USERNAME_CHANGE, new UserEvent($dm, $user)); }

if ($followedUsers = $meta->getFieldValue($user, 'followedUsers')) { foreach ($followedUsers->getInsertDiff() as $otherUser) { $this->dispatcher->dispatch( UserEvents::FOLLOW_USER, new UserUserEvent($dm, $user, $otherUser) ); }

foreach ($followedUsers->getDeleteDiff() as $otherUser) { // ... } }}

/** @DI\Observe("user.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("user.create") */public function onUserCreate(UserEvent $event){ $dm = $event->getDocumentManager(); $user = $event->getUser();

$widget = new Widget(); $widget->setUser($user);

$dm->persist($widget);

// manually notify the event $event->getDispatcher()->dispatch( WidgetEvents::CREATE, new WidgetEvent($dm, $widget) );}

/** @DI\Observe("user.follow_user") */public function onFollowUser(UserUserEvent $event){ $event->getUser() ->getStats() ->incrementFollowedUsers(1); $event->getOtherUser() ->getStats() ->incrementFollowers(1);}

Two event classes per model

• @MainBundle\UserEvents: encapsulates event name constants such as UserEvents::CREATE and UserEvents::CHANGE_USERNAME

• @MainBundle\Event\UserEvent: base event object, accepts $dm and $user arguments

• @MainBundle\WidgetEvents…

• @MainBundle\Event\WidgetEvent…

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

Delegate work to clean, concise, single-purpose event listeners

Contextual Configuration

Save your future self a headache

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

/** @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

JMSDiExtraBundle

require.js

<script src="{{ asset('js/lib/require.js') }}"></script><script>require.config({ baseUrl: "{{ asset('js') }}", paths: { "jquery": "//ajax.googleapis.com/.../jquery.min", "underscore": "lib/underscore", "backbone": "lib/backbone" }, shim: { "jquery": { exports: "jQuery" }, "underscore": { exports: "_" }, "backbone": { deps: [ "jquery", "underscore" ], exports: "Backbone" } }})require([ "main" ])</script>

// web/js/model/user.jsdefine( [ "underscore", "backbone" ], function(_, Backbone) { var tmpl = _.template("<%- first %> <%- last %>") return Backbone.Model.extend({ name: function() { return tmpl({ first: this.get("first_name"), last: this.get("last_name") }) } }) })

{% 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 %}

Dependencies

• model: backbone, underscore

• view: backbone, jquery

• template: model, view

{% javascripts "js/lib/jquery.js" "js/lib/underscore.js" "js/lib/backbone.js" "js/model/user.js" "js/view/user.js" filter="?uglifyjs2" output="js/packed/user.js" %}<script src="{{ asset_url }}"></script>{% endjavascripts %}

<script>var view = new UserView({ model: new User({{ user|serialize|raw }}), el: document.getElementById("user")})</script>

Unused dependenciesnaturally slough off

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;}

Miscellaneous

When to create a new bundle

Lots of classes pertaining toone feature

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

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

Access Control

The Symfony ACL is forarbitrary permissions

Encapsulate access logic incustom voter classes

/** @DI\Service(public=false) @DI\Tag("security.voter") */class WidgetVoter implements VoterInterface{ public function supportsAttribute($attribute) { return 'OWNER' === $attribute; }

public function supportsClass($class) { return 'OptiGrab\Model\Widget' === $class || is_subclass_of($class, 'OptiGrab\Model\Widget'); }

public function vote(TokenInterface $token, $widget, array $attributes) { // ... }}

public function vote(TokenInterface $token, $map, array $attributes){ $result = VoterInterface::ACCESS_ABSTAIN;

if (!$this->supportsClass(get_class($map))) { return $result; }

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

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

return $result;}

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

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

Only mock interfaces

interface FacebookInterface{ function getUser(); function api();}

/** @DI\Service("facebook") */class Facebook extends \BaseFacebook implements FacebookInterface{ // ...}

$facebook = $this->getMock('OptiGrab\Bundle\MainBundle\Facebook\FacebookInterface');$facebook->expects($this->any()) ->method('getUser') ->will($this->returnValue(123));

Questions?

Thank You!

joind.in/8024

@kriswallsmith.net

Recommended