Your SlideShare is downloading. ×
0
How Kris Writes Symfony Apps
How Kris Writes Symfony Apps
How Kris Writes Symfony Apps
How Kris Writes Symfony Apps
How Kris Writes Symfony Apps
How Kris Writes Symfony Apps
How Kris Writes Symfony Apps
How Kris Writes Symfony Apps
How Kris Writes Symfony Apps
How Kris Writes Symfony Apps
How Kris Writes Symfony Apps
How Kris Writes Symfony Apps
How Kris Writes Symfony Apps
How Kris Writes Symfony Apps
How Kris Writes Symfony Apps
How Kris Writes Symfony Apps
How Kris Writes Symfony Apps
How Kris Writes Symfony Apps
How Kris Writes Symfony Apps
How Kris Writes Symfony Apps
How Kris Writes Symfony Apps
How Kris Writes Symfony Apps
How Kris Writes Symfony Apps
How Kris Writes Symfony Apps
How Kris Writes Symfony Apps
How Kris Writes Symfony Apps
How Kris Writes Symfony Apps
How Kris Writes Symfony Apps
How Kris Writes Symfony Apps
How Kris Writes Symfony Apps
How Kris Writes Symfony Apps
How Kris Writes Symfony Apps
How Kris Writes Symfony Apps
How Kris Writes Symfony Apps
How Kris Writes Symfony Apps
How Kris Writes Symfony Apps
How Kris Writes Symfony Apps
How Kris Writes Symfony Apps
How Kris Writes Symfony Apps
How Kris Writes Symfony Apps
How Kris Writes Symfony Apps
How Kris Writes Symfony Apps
How Kris Writes Symfony Apps
How Kris Writes Symfony Apps
How Kris Writes Symfony Apps
How Kris Writes Symfony Apps
How Kris Writes Symfony Apps
How Kris Writes Symfony Apps
How Kris Writes Symfony Apps
How Kris Writes Symfony Apps
How Kris Writes Symfony Apps
How Kris Writes Symfony Apps
How Kris Writes Symfony Apps
How Kris Writes Symfony Apps
How Kris Writes Symfony Apps
How Kris Writes Symfony Apps
How Kris Writes Symfony Apps
How Kris Writes Symfony Apps
How Kris Writes Symfony Apps
How Kris Writes Symfony Apps
How Kris Writes Symfony Apps
How Kris Writes Symfony Apps
How Kris Writes Symfony Apps
How Kris Writes Symfony Apps
How Kris Writes Symfony Apps
How Kris Writes Symfony Apps
How Kris Writes Symfony Apps
How Kris Writes Symfony Apps
How Kris Writes Symfony Apps
How Kris Writes Symfony Apps
How Kris Writes Symfony Apps
How Kris Writes Symfony Apps
How Kris Writes Symfony Apps
How Kris Writes Symfony Apps
How Kris Writes Symfony Apps
How Kris Writes Symfony Apps
How Kris Writes Symfony Apps
How Kris Writes Symfony Apps
How Kris Writes Symfony Apps
How Kris Writes Symfony Apps
How Kris Writes Symfony Apps
How Kris Writes Symfony Apps
How Kris Writes Symfony Apps
How Kris Writes Symfony Apps
How Kris Writes Symfony Apps
How Kris Writes Symfony Apps
How Kris Writes Symfony Apps
How Kris Writes Symfony Apps
How Kris Writes Symfony Apps
How Kris Writes Symfony Apps
How Kris Writes Symfony Apps
How Kris Writes Symfony Apps
How Kris Writes Symfony Apps
How Kris Writes Symfony Apps
How Kris Writes Symfony Apps
How Kris Writes Symfony Apps
How Kris Writes Symfony Apps
How Kris Writes Symfony Apps
How Kris Writes Symfony Apps
How Kris Writes Symfony Apps
How Kris Writes Symfony Apps
How Kris Writes Symfony Apps
How Kris Writes Symfony Apps
How Kris Writes Symfony Apps
How Kris Writes Symfony Apps
How Kris Writes Symfony Apps
How Kris Writes Symfony Apps
How Kris Writes Symfony Apps
How Kris Writes Symfony Apps
How Kris Writes Symfony Apps
How Kris Writes Symfony Apps
How Kris Writes Symfony Apps
How Kris Writes Symfony Apps
How Kris Writes Symfony Apps
How Kris Writes Symfony Apps
How Kris Writes Symfony Apps
Upcoming SlideShare
Loading in...5
×

Thanks for flagging this SlideShare!

Oops! An error has occurred.

×
Saving this for later? Get the SlideShare app to save on your phone or tablet. Read anywhere, anytime – even offline.
Text the download link to your phone
Standard text messaging rates apply

How Kris Writes Symfony Apps

14,776

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

Published in: Technology
7 Comments
66 Likes
Statistics
Notes
No Downloads
Views
Total Views
14,776
On Slideshare
0
From Embeds
0
Number of Embeds
15
Actions
Shares
0
Downloads
440
Comments
7
Likes
66
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!

×