+ All Categories
Home > Technology > How Kris Writes Symfony Apps

How Kris Writes Symfony Apps

Date post: 06-May-2015
Category:
Upload: kris-wallsmith
View: 14,406 times
Download: 0 times
Share this document with a friend
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.
116
How Kris Writes Symfony Apps @kriswallsmith February 9, 2013
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


Recommended