How Kris Writes Symfony Apps
Upcoming SlideShare
Loading in...5
×
 

Like this? Share it with your network

Share

How Kris Writes Symfony Apps

on

  • 13,691 views

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

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.

Statistics

Views

Total Views
13,691
Views on SlideShare
10,294
Embed Views
3,397

Actions

Likes
60
Downloads
428
Comments
7

16 Embeds 3,397

http://blog.mmx3.pl 1574
http://mrzepinski.pl 1175
https://twitter.com 473
http://yosymfony.com 133
http://www.rockto.com 8
http://www.twylah.com 7
https://www.google.pl 6
http://feeds.feedburner.com 5
http://devi-vm.local 4
http://symfony2developer.com 3
https://sendtoinc.com 3
http://librosweb.es 2
https://fr.twitter.com 1
https://si0.twimg.com 1
http://translate.googleusercontent.com 1
http://www.365dailyjournal.com 1
More...

Accessibility

Categories

Upload Details

Uploaded via as Adobe PDF

Usage Rights

© All Rights Reserved

Report content

Flagged as inappropriate Flag as inappropriate
Flag as inappropriate

Select your reason for flagging this presentation as inappropriate.

Cancel

15 of 7 Post a comment

  • Full Name Full Name Comment goes here.
    Are you sure you want to
    Your message goes here
    Processing…
  • Is there video link?
    Are you sure you want to
    Your message goes here
    Processing…
  • yeah..................
    Are you sure you want to
    Your message goes here
    Processing…
  • 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
    Processing…
  • 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
    Processing…
  • Is there a video of this talk anywhere?
    Are you sure you want to
    Your message goes here
    Processing…
Post Comment
Edit your comment

How Kris Writes Symfony Apps Presentation 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!