Upload
kris-wallsmith
View
14.412
Download
0
Embed Size (px)
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