How Kris Writes Symfony Apps       @kriswallsmith • February 9, 2013
About Me
@kriswallsmith.net•   Born, raised, & live in Portland•   10+ years of experience•   Lead Architect at OpenSky•   Open sou...
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.*",+   ...
./app/console generate:bundle     --namespace=OptiGrab/Bundle/MainBundle
assetic:    debug:             %kernel.debug%    use_controller:    false    bundles:           [ MainBundle ]    filters:...
jms_di_extra:    locations:        bundles:            - MainBundle
public function registerContainerConfiguration(LoaderInterface $loader){    $loader->load(__DIR__./config/config_.$this->g...
MongoDB
Treat your model like a princess.
She gets her own wing   of the palace…
doctrine_mongodb:    auto_generate_hydrator_classes: %kernel.debug%    auto_generate_proxy_classes:      %kernel.debug%   ...
// repo for src/OptiGrab/Model/Widget.php$repo = $this->dm->getRepository(Model:User);
…doesnt do any work…
use OptiGrabBundleMainBundleCanonicalizer;public function setUsername($username){    $this->username = $username;    $cano...
use OptiGrabBundleMainBundleCanonicalizer;public function setUsername($username, Canonicalizer $canonicalizer){    $this->...
…and is unaware of the work  being done around her.
public function setUsername($username){    // a listener will update the    // canonical username    $this->username = $us...
No query buildersoutside of repositories
class WidgetRepository extends DocumentRepository{    public function findByUser(User $user)    {        return $this->cre...
Eager id creation
public function __construct(){    $this->id = (string) new MongoId();}
public function __construct(){    $this->id = (string) new MongoId();    $this->createdAt = new DateTime();    $this->widg...
Remember yourclone constructor
$foo = new Foo();$bar = clone $foo;
public function __clone(){    $this->id = (string) new MongoId();    $this->createdAt = new DateTime();    $this->widgets ...
public function __construct(){    $this->id = (string) new MongoId();    $this->createdAt = new DateTime();    $this->widg...
Only flush from the controller
public function theAction(Widget $widget){    $this->get(widget_twiddler)         ->skeedaddle($widget);    $this->flush();}
Save space on field names
/** @ODMString(name="u") */private $username;/** @ODMString(name="uc") @ODMUniqueIndex */private $usernameCanonical;
public function getUsername(){    return $this->username ?: $this->usernameCanonical;}public function setUsername($usernam...
No proxy objects
/** @ODMReferenceOne(targetDocument="User") */private $user;
public function getUser(){    if ($this->userId && !$this->user) {        throw new UninitializedReferenceException(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
Application Land  Controller  HTTP Land
The controller maps fromHTTP-land to application-land.
What about the model?
public function registerAction(){    // ...    $user->sendWelcomeEmail();    // ...}
public function registerAction(){    // ...    $mailer->sendWelcomeEmail($user);    // ...}
Persistence Land    ModelApplication Land
The model maps fromapplication-land to persistence-land.
Persistence Land     ModelApplication Land  Controller  HTTP Land
Who lives in application land?
Thin controller, thin model…      Fat service layer!
Application Events
Use lots of them
That happened.
/** @DIObserve("user.username_change") */public function onUsernameChange(UserEvent $event){    $user = $event->getUser();...
Unit of Work
public function onFlush(OnFlushEventArgs $event){    $dm = $event->getDocumentManager();    $uow = $dm->getUnitOfWork();  ...
private function processUserFlush(DocumentManager $dm, User $user){    $uow     = $dm->getUnitOfWork();    $meta    = $dm-...
/** @DIObserve("user.create") */public function onUserCreate(UserEvent $event){    $user = $event->getUser();    $activity...
/** @DIObserve("user.create") */public function onUserCreate(UserEvent $event){    $dm   = $event->getDocumentManager();  ...
/** @DIObserve("user.follow_user") */public function onFollowUser(UserUserEvent $event){    $event->getUser()          ->g...
Two event classes per model• @MainBundleUserEvents: encapsulates event name constants  such as UserEvents::CREATE and  Use...
$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: OptiGrabBundleMainBundleWidgetTwiddle...
/** @DIService("widget_twiddler") */class Twiddler{    /** @DIInjectParams */    public function __construct(        Event...
services:    # aliases for auto-wiring    container: @service_container    dm: @doctrine_mongodb.odm.document_manager    d...
JMSDiExtraBundle
require.js
<script src="{{ asset(js/lib/require.js) }}"></script><script>require.config({    baseUrl: "{{ asset(js) }}",    paths: { ...
// web/js/model/user.jsdefine(    [ "underscore", "backbone" ],    function(_, Backbone) {        var tmpl = _.template("<...
{% block head %}<script>require(    [ "view/user", "model/user" ],    function(UserView, User) {         var view = new Us...
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...
Unused dependencies naturally slough off
JMSSerializerBundle
{% block head %}<script>require(    [ "view/user", "model/user" ],    function(UserView, User) {         var view = new Us...
/** @ExclusionPolicy("ALL") */class User{    private $id;    /** @Expose */    private $firstName;    /** @Expose */    pr...
Miscellaneous
When to create a new bundle
Lots of classes pertaining to        one feature
{% include MainBundle:Account/Widget:sidebar.html.twig %}
{% include AccountBundle:Widget:sidebar.html.twig %}
Access Control
The Symfony ACL is for arbitrary permissions
Encapsulate access logic in  custom voter classes
/** @DIService(public=false) @DITag("security.voter") */class WidgetVoter implements VoterInterface{    public function su...
public function vote(TokenInterface $token, $map, array $attributes){    $result = VoterInterface::ACCESS_ABSTAIN;    if (...
/** @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();}/** @DIService("facebook") */class Facebook extends...
$facebook = $this->getMock(OptiGrabBundleMainBundleFacebookFacebookInterface);$facebook->expects($this->any())    ->method...
Questions?
@kriswallsmith.net        joind.in/8024      Thank You!
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
×

How Kris Writes Symfony Apps

15,119

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 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
15,119
On Slideshare
0
From Embeds
0
Number of Embeds
15
Actions
Shares
0
Downloads
443
Comments
7
Likes
66
Embeds 0
No embeds

No notes for slide

How Kris Writes Symfony Apps

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

    Clipping is a handy way to collect important slides you want to go back to later.

×