Date post: | 06-May-2015 |
Category: |
Technology |
Upload: | kris-wallsmith |
View: | 14,406 times |
Download: | 0 times |
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