how kris writes symfony apps

116
How Kris Writes Symfony Apps @kriswallsmith February 9, 2013

Upload: kris-wallsmith

Post on 06-May-2015

14.411 views

Category:

Technology


0 download

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.

TRANSCRIPT

Page 1: How Kris Writes Symfony Apps

How Kris Writes Symfony Apps@kriswallsmith • February 9, 2013

Page 2: How Kris Writes Symfony Apps

About Me

Page 3: How Kris Writes Symfony Apps

• Born, raised, & live in Portland

• 10+ years of experience

• Lead Architect at OpenSky

• Open source fanboy

@kriswallsmith.net

Page 4: How Kris Writes Symfony Apps
Page 5: How Kris Writes Symfony Apps
Page 6: How Kris Writes Symfony Apps
Page 7: How Kris Writes Symfony Apps
Page 8: How Kris Writes Symfony Apps

brewcycleportland.com

Page 9: How Kris Writes Symfony Apps
Page 10: How Kris Writes Symfony Apps
Page 11: How Kris Writes Symfony Apps
Page 12: How Kris Writes Symfony Apps

assetic

Page 13: How Kris Writes Symfony Apps

Buzz

Page 14: How Kris Writes Symfony Apps

Spork

Page 15: How Kris Writes Symfony Apps
Page 16: How Kris Writes Symfony Apps

Go big or go home.

Page 17: How Kris Writes Symfony Apps

Getting Started

Page 18: How Kris Writes Symfony Apps

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

Page 19: How Kris Writes Symfony Apps
Page 20: How Kris Writes Symfony Apps

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

Page 21: How Kris Writes Symfony Apps

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

Page 22: How Kris Writes Symfony Apps

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

Page 23: How Kris Writes Symfony Apps

jms_di_extra: locations: bundles: - MainBundle

Page 24: How Kris Writes Symfony Apps

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

Page 25: How Kris Writes Symfony Apps

MongoDB

Page 26: How Kris Writes Symfony Apps

Treat your model like a princess.

Page 27: How Kris Writes Symfony Apps

She gets her own wingof the palace…

Page 28: How Kris Writes Symfony Apps

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

Page 29: How Kris Writes Symfony Apps

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

Page 30: How Kris Writes Symfony Apps

…doesn't do any work…

Page 31: How Kris Writes Symfony Apps

use OptiGrab\Bundle\MainBundle\Canonicalizer;

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

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

Page 32: How Kris Writes Symfony Apps

use OptiGrab\Bundle\MainBundle\Canonicalizer;

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

Page 33: How Kris Writes Symfony Apps

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

Page 34: How Kris Writes Symfony Apps

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

Page 35: How Kris Writes Symfony Apps

No query buildersoutside of repositories

Page 36: How Kris Writes Symfony Apps

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

Page 37: How Kris Writes Symfony Apps

Eager id creation

Page 38: How Kris Writes Symfony Apps

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

Page 39: How Kris Writes Symfony Apps

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

Page 40: How Kris Writes Symfony Apps

Remember yourclone constructor

Page 41: How Kris Writes Symfony Apps

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

Page 42: How Kris Writes Symfony Apps

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

Page 43: How Kris Writes Symfony Apps

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

Page 44: How Kris Writes Symfony Apps

Only flush from the controller

Page 45: How Kris Writes Symfony Apps

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

Page 46: How Kris Writes Symfony Apps

Save space on field names

Page 47: How Kris Writes Symfony Apps

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

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

Page 48: How Kris Writes Symfony Apps

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

Page 49: How Kris Writes Symfony Apps

No proxy objects

Page 50: How Kris Writes Symfony Apps

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

Page 51: How Kris Writes Symfony Apps

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

return $this->user;}

Page 52: How Kris Writes Symfony Apps

Mapping Layers

Page 53: How Kris Writes Symfony Apps

What is a mapping layer?

Page 54: How Kris Writes Symfony Apps

A mapping layer is thin

Page 55: How Kris Writes Symfony Apps

Thin controller, fat model…

Page 56: How Kris Writes Symfony Apps

Is Symfony an MVC framework?

Page 57: How Kris Writes Symfony Apps

Symfony is an HTTP framework

Page 58: How Kris Writes Symfony Apps

HT

TP Land

Application Land

Controller

Page 59: How Kris Writes Symfony Apps

The controller maps fromHTTP-land to application-land.

Page 60: How Kris Writes Symfony Apps

What about the model?

Page 61: How Kris Writes Symfony Apps
Page 62: How Kris Writes Symfony Apps
Page 63: How Kris Writes Symfony Apps

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

Page 64: How Kris Writes Symfony Apps

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

Page 65: How Kris Writes Symfony Apps

Application Land

Persistence Land

Model

Page 66: How Kris Writes Symfony Apps

The model maps fromapplication-land to persistence-land.

Page 67: How Kris Writes Symfony Apps

Model

Application Land

Persistence Land

HT

TP Land

Controller

Page 68: How Kris Writes Symfony Apps

Who lives in application land?

Page 69: How Kris Writes Symfony Apps

Thin controller, thin model…Fat service layer!

Page 70: How Kris Writes Symfony Apps

Application Events

Page 71: How Kris Writes Symfony Apps

Use lots of them

Page 72: How Kris Writes Symfony Apps

That happened.

Page 73: How Kris Writes Symfony Apps

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

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

Page 74: How Kris Writes Symfony Apps

Unit of Work

Page 75: How Kris Writes Symfony Apps

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

Page 76: How Kris Writes Symfony Apps

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) { // ... } }}

Page 77: How Kris Writes Symfony Apps

/** @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);}

Page 78: How Kris Writes Symfony Apps

/** @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) );}

Page 79: How Kris Writes Symfony Apps

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

Page 80: How Kris Writes Symfony Apps

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…

Page 81: How Kris Writes Symfony Apps

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

Page 82: How Kris Writes Symfony Apps

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

Page 83: How Kris Writes Symfony Apps

Contextual Configuration

Page 84: How Kris Writes Symfony Apps

Save your future self a headache

Page 85: How Kris Writes Symfony Apps

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

Page 86: How Kris Writes Symfony Apps

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

Page 87: How Kris Writes Symfony Apps

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

Page 88: How Kris Writes Symfony Apps

JMSDiExtraBundle

Page 89: How Kris Writes Symfony Apps

require.js

Page 90: How Kris Writes Symfony Apps
Page 91: How Kris Writes Symfony Apps

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

Page 92: How Kris Writes Symfony Apps

// 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") }) } }) })

Page 93: How Kris Writes Symfony Apps

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

Page 94: How Kris Writes Symfony Apps

Dependencies

• model: backbone, underscore

• view: backbone, jquery

• template: model, view

Page 95: How Kris Writes Symfony Apps

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

Page 96: How Kris Writes Symfony Apps

Unused dependenciesnaturally slough off

Page 97: How Kris Writes Symfony Apps

JMSSerializerBundle

Page 98: How Kris Writes Symfony Apps

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

Page 99: How Kris Writes Symfony Apps

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

/** @Expose */ private $firstName;

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

Page 100: How Kris Writes Symfony Apps

Miscellaneous

Page 101: How Kris Writes Symfony Apps

When to create a new bundle

Page 102: How Kris Writes Symfony Apps

Lots of classes pertaining toone feature

Page 103: How Kris Writes Symfony Apps

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

Page 104: How Kris Writes Symfony Apps

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

Page 105: How Kris Writes Symfony Apps

Access Control

Page 106: How Kris Writes Symfony Apps

The Symfony ACL is forarbitrary permissions

Page 107: How Kris Writes Symfony Apps

Encapsulate access logic incustom voter classes

Page 108: How Kris Writes Symfony Apps

/** @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) { // ... }}

Page 109: How Kris Writes Symfony Apps

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

Page 110: How Kris Writes Symfony Apps

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

Page 111: How Kris Writes Symfony Apps

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

Page 112: How Kris Writes Symfony Apps

Only mock interfaces

Page 113: How Kris Writes Symfony Apps

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

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

Page 114: How Kris Writes Symfony Apps

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

Page 115: How Kris Writes Symfony Apps

Questions?

Page 116: How Kris Writes Symfony Apps

Thank You!

joind.in/8024

@kriswallsmith.net