How Kris Writes Symfony Apps

  • 13,203 views
Uploaded on

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 …

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.

More in: Technology
  • Full Name Full Name Comment goes here.
    Are you sure you want to
    Your message goes here
  • Is there video link?
    Are you sure you want to
    Your message goes here
  • yeah..................
    Are you sure you want to
    Your message goes here
  • Where are you putting Repository classes in this application layout? I have to say I very like it :)
    Are you sure you want to
    Your message goes here
  • I'd even pay to watch a video if there were one. Judging from the sheets and the comments on other websites, this sounds like a great talk. I'm gonna go browse through the slides again :)
    Are you sure you want to
    Your message goes here
  • Is there a video of this talk anywhere?
    Are you sure you want to
    Your message goes here
No Downloads

Views

Total Views
13,203
On Slideshare
0
From Embeds
0
Number of Embeds
14

Actions

Shares
Downloads
432
Comments
7
Likes
63

Embeds 0

No embeds

Report content

Flagged as inappropriate Flag as inappropriate
Flag as inappropriate

Select your reason for flagging this presentation as inappropriate.

Cancel
    No notes for slide

Transcript

  • 1. How Kris Writes Symfony Apps @kriswallsmith • February 9, 2013
  • 2. About Me
  • 3. @kriswallsmith.net• Born, raised, & live in Portland• 10+ years of experience• Lead Architect at OpenSky• Open source fanboy
  • 4. brewcycleportland.com
  • 5. assetic
  • 6. Buzz
  • 7. Spork
  • 8. Go big or go home.
  • 9. Getting Started
  • 10. composer create-project symfony/framework-standard-edition opti-grab/ 2.2.x-dev
  • 11. - "doctrine/orm": "~2.2,>=2.2.3",- "doctrine/doctrine-bundle": "1.2.*",+ "doctrine/mongodb-odm-bundle": "3.0.*",+ "jms/serializer-bundle": "1.0.*",
  • 12. ./app/console generate:bundle --namespace=OptiGrab/Bundle/MainBundle
  • 13. assetic: debug: %kernel.debug% use_controller: false bundles: [ MainBundle ] filters: cssrewrite: ~ uglifyjs2: { compress: true, mangle: true } uglifycss: ~
  • 14. jms_di_extra: locations: bundles: - MainBundle
  • 15. 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); }}
  • 16. MongoDB
  • 17. Treat your model like a princess.
  • 18. She gets her own wing of the palace…
  • 19. 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: OptiGrabModel alias: Model
  • 20. // repo for src/OptiGrab/Model/Widget.php$repo = $this->dm->getRepository(Model:User);
  • 21. …doesnt do any work…
  • 22. use OptiGrabBundleMainBundleCanonicalizer;public function setUsername($username){ $this->username = $username; $canonicalizer = Canonicalizer::instance(); $this->usernameCanonical = $canonicalizer->canonicalize($username);}
  • 23. use OptiGrabBundleMainBundleCanonicalizer;public function setUsername($username, Canonicalizer $canonicalizer){ $this->username = $username; $this->usernameCanonical = $canonicalizer->canonicalize($username);}
  • 24. …and is unaware of the work being done around her.
  • 25. public function setUsername($username){ // a listener will update the // canonical username $this->username = $username;}
  • 26. No query buildersoutside of repositories
  • 27. 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(); }}
  • 28. Eager id creation
  • 29. public function __construct(){ $this->id = (string) new MongoId();}
  • 30. public function __construct(){ $this->id = (string) new MongoId(); $this->createdAt = new DateTime(); $this->widgets = new ArrayCollection();}
  • 31. Remember yourclone constructor
  • 32. $foo = new Foo();$bar = clone $foo;
  • 33. public function __clone(){ $this->id = (string) new MongoId(); $this->createdAt = new DateTime(); $this->widgets = new ArrayCollection( $this->widgets->toArray() );}
  • 34. 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() );}
  • 35. Only flush from the controller
  • 36. public function theAction(Widget $widget){ $this->get(widget_twiddler) ->skeedaddle($widget); $this->flush();}
  • 37. Save space on field names
  • 38. /** @ODMString(name="u") */private $username;/** @ODMString(name="uc") @ODMUniqueIndex */private $usernameCanonical;
  • 39. 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; }}
  • 40. No proxy objects
  • 41. /** @ODMReferenceOne(targetDocument="User") */private $user;
  • 42. public function getUser(){ if ($this->userId && !$this->user) { throw new UninitializedReferenceException(user); } return $this->user;}
  • 43. Mapping Layers
  • 44. What is a mapping layer?
  • 45. A mapping layer is thin
  • 46. Thin controller, fat model…
  • 47. Is Symfony an MVC framework?
  • 48. Symfony is an HTTP framework
  • 49. Application Land Controller HTTP Land
  • 50. The controller maps fromHTTP-land to application-land.
  • 51. What about the model?
  • 52. public function registerAction(){ // ... $user->sendWelcomeEmail(); // ...}
  • 53. public function registerAction(){ // ... $mailer->sendWelcomeEmail($user); // ...}
  • 54. Persistence Land ModelApplication Land
  • 55. The model maps fromapplication-land to persistence-land.
  • 56. Persistence Land ModelApplication Land Controller HTTP Land
  • 57. Who lives in application land?
  • 58. Thin controller, thin model… Fat service layer!
  • 59. Application Events
  • 60. Use lots of them
  • 61. That happened.
  • 62. /** @DIObserve("user.username_change") */public function onUsernameChange(UserEvent $event){ $user = $event->getUser(); $dm = $event->getDocumentManager(); $dm->getRepository(Model:Widget) ->updateDenormalizedUsernames($user);}
  • 63. Unit of Work
  • 64. public function onFlush(OnFlushEventArgs $event){ $dm = $event->getDocumentManager(); $uow = $dm->getUnitOfWork(); foreach ($uow->getIdentityMap() as $class => $docs) { if (self::checkClass(OptiGrabModelUser, $class)) { foreach ($docs as $doc) { $this->processUserFlush($dm, $doc); } } elseif (self::checkClass(OptiGrabModelWidget, $class)) { foreach ($docs as $doc) { $this->processWidgetFlush($dm, $doc); } } }}
  • 65. 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) { // ... } }}
  • 66. /** @DIObserve("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);}
  • 67. /** @DIObserve("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) );}
  • 68. /** @DIObserve("user.follow_user") */public function onFollowUser(UserUserEvent $event){ $event->getUser() ->getStats() ->incrementFollowedUsers(1); $event->getOtherUser() ->getStats() ->incrementFollowers(1);}
  • 69. Two event classes per model• @MainBundleUserEvents: encapsulates event name constants such as UserEvents::CREATE and UserEvents::CHANGE_USERNAME• @MainBundleEventUserEvent: base event object, accepts $dm and $user arguments• @MainBundleWidgetEvents…• @MainBundleEventWidgetEvent…
  • 70. $event = new UserEvent($dm, $user);$dispatcher->dispatch(UserEvents::CREATE, $event);
  • 71. Delegate work to clean, concise, single-purpose event listeners
  • 72. Contextual Configuration
  • 73. Save your future self a headache
  • 74. # @MainBundle/Resources/config/widget.ymlservices: widget_twiddler: class: OptiGrabBundleMainBundleWidgetTwiddler arguments: - @event_dispatcher - @?logger
  • 75. /** @DIService("widget_twiddler") */class Twiddler{ /** @DIInjectParams */ public function __construct( EventDispatcherInterface $dispatcher, LoggerInterface $logger = null) { // ... }}
  • 76. services: # aliases for auto-wiring container: @service_container dm: @doctrine_mongodb.odm.document_manager doctrine: @doctrine_mongodb dispatcher: @event_dispatcher security: @security.context
  • 77. JMSDiExtraBundle
  • 78. require.js
  • 79. <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>
  • 80. // 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") }) } }) })
  • 81. {% 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 %}
  • 82. Dependencies• model: backbone, underscore• view: backbone, jquery• template: model, view
  • 83. {% 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>
  • 84. Unused dependencies naturally slough off
  • 85. JMSSerializerBundle
  • 86. {% 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 %}
  • 87. /** @ExclusionPolicy("ALL") */class User{ private $id; /** @Expose */ private $firstName; /** @Expose */ private $lastName;}
  • 88. Miscellaneous
  • 89. When to create a new bundle
  • 90. Lots of classes pertaining to one feature
  • 91. {% include MainBundle:Account/Widget:sidebar.html.twig %}
  • 92. {% include AccountBundle:Widget:sidebar.html.twig %}
  • 93. Access Control
  • 94. The Symfony ACL is for arbitrary permissions
  • 95. Encapsulate access logic in custom voter classes
  • 96. /** @DIService(public=false) @DITag("security.voter") */class WidgetVoter implements VoterInterface{ public function supportsAttribute($attribute) { return OWNER === $attribute; } public function supportsClass($class) { return OptiGrabModelWidget === $class || is_subclass_of($class, OptiGrabModelWidget); } public function vote(TokenInterface $token, $widget, array $attributes) { // ... }}
  • 97. 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;}
  • 98. /** @SecureParam(name="widget", permissions="OWNER") */public function editAction(Widget $widget){ // ...}
  • 99. {% if is_granted(OWNER, widget) %}{# ... #}{% endif %}
  • 100. Only mock interfaces
  • 101. interface FacebookInterface{ function getUser(); function api();}/** @DIService("facebook") */class Facebook extends BaseFacebook implements FacebookInterface{ // ...}
  • 102. $facebook = $this->getMock(OptiGrabBundleMainBundleFacebookFacebookInterface);$facebook->expects($this->any()) ->method(getUser) ->will($this->returnValue(123));
  • 103. Questions?
  • 104. @kriswallsmith.net joind.in/8024 Thank You!