How Kris Writes Symfony Apps

11,773 views
11,530 views

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, Education
1 Comment
37 Likes
Statistics
Notes
No Downloads
Views
Total views
11,773
On SlideShare
0
From Embeds
0
Number of Embeds
1,756
Actions
Shares
0
Downloads
287
Comments
1
Likes
37
Embeds 0
No embeds

No notes for slide

How Kris Writes Symfony Apps

  1. 1. How Kris Writes Symfony Apps
  2. 2. @kriswallsmith
  3. 3. father artist bowhunter hacker president widower gamer actor tapdancer lover hater singer writer founder yogi consultant archer musician architect slacker soccer player volunteer home owner scotch drinker pianist…
  4. 4. assetic
  5. 5. Buzz
  6. 6. Spork
  7. 7. Getting Started
  8. 8. composer create-project ! symfony/framework-standard-edition ! widgets-by-kris/ ! ~2.4
  9. 9. + + + + "doctrine/orm": "~2.2,>=2.2.3",! "doctrine/doctrine-bundle": "~1.2",! "doctrine/mongodb-odm-bundle": "~3.0",! "jms/di-extra-bundle": "~1.4",! "jms/security-extra-bundle": "~1.5",! "jms/serializer-bundle": "~1.0",
  10. 10. ./app/console generate:bundle ! --namespace=Kris/Bundle/MainBundle
  11. 11. 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);! }! }
  12. 12. Model
  13. 13. Treat your model like a princess.
  14. 14. She gets her own wing of the palace…
  15. 15. doctrine_mongodb:! auto_generate_hydrator_classes: %kernel.debug%! auto_generate_proxy_classes: %kernel.debug%! connections: { default: ~ }! document_managers:! default:! connection: default! database: kris! mappings:! model:! type: annotation! dir: %src%/Kris/Model! prefix: KrisModel! alias: Model
  16. 16. // repo for src/Kris/Model/Widget.php! $repo = $this->dm->getRepository('Model:User');
  17. 17. …doesn't do any work…
  18. 18. use KrisBundleMainBundleCanonicalizer;! ! public function setUsername($username)! {! $this->username = $username;! ! $canonicalizer = Canonicalizer::instance();! $this->usernameCanonical = $canonicalizer->canonicalize($username);! }
  19. 19. use KrisBundleMainBundleCanonicalizer;! ! public function setUsername($username, Canonicalizer $canonicalizer)! {! $this->username = $username;! $this->usernameCanonical = $canonicalizer->canonicalize($username);! }
  20. 20. …and is unaware of the work being done around her.
  21. 21. public function setUsername($username)! {! // a listener will update the! // canonical username! $this->username = $username;! }
  22. 22. Anemic domain model 
 is an anti-pattern?
  23. 23. Anemic???
  24. 24. “The fundamental horror of this anti-pattern is that it's so contrary to the basic idea of object-oriented design; which is to combine data and process together.” ! Martin Fowler
  25. 25. $cabinet->open();
  26. 26. Cabinets don’t open themselves.
  27. 27. $asset->getLastModified();
  28. 28. Mapping Layers
  29. 29. thin
  30. 30. thin controller fat model
  31. 31. MVC
  32. 32. Is Symfony an MVC framework?
  33. 33. HTTP
  34. 34. Application Land Controller HTTP Land
  35. 35. The controller is thin 
 because it maps from 
 HTTP-land to application-land.
  36. 36. What about the model?
  37. 37. public function registerAction()! {! // ...! $user->sendWelcomeEmail();! // ...! }
  38. 38. public function registerAction()! {! // ...! $mailer->sendWelcomeEmail($user);! // ...! }
  39. 39. Persistence Land Model Application Land
  40. 40. The model maps from application-land to persistence-land.
  41. 41. Persistence Land Model Application Land Controller HTTP Land
  42. 42. Who lives in application land?
  43. 43. Thin controller, thin model… 
 Fat service layer!
  44. 44. Application Events
  45. 45. Use them.
  46. 46. That happened.
  47. 47. /** @DIObserve("user.username_change") */! public function onUsernameChange(UserEvent $event)! {! $user = $event->getUser();! $dm = $event->getDocumentManager();! ! $dm->getRepository('Model:Widget')! ->updateDenormalizedUsernames($user);! }
  48. 48. One event class per model • • • Event name constants Accepts object manager and object as arguments Simple accessors
  49. 49. $event = new UserEvent($dm, $user);! $dispatcher->dispatch(UserEvent::CREATE, $event);
  50. 50. $event = new UserUserEvent($dm, $user, $otherUser);! $dispatcher->dispatch(UserEvent::FOLLOW, $event);
  51. 51. preFlush
  52. 52. public function preFlush(ManagerEventArgs $event)! {! $dm = $event->getObjectManager();! $uow = $dm->getUnitOfWork();! ! foreach ($uow->getIdentityMap() as $class => $docs) {! if (is_a($class, 'KrisModelUser')) {! foreach ($docs as $doc) {! $this->processUserFlush($dm, $doc);! }! } elseif (is_a($class, 'KrisModelWidget')) {! foreach ($docs as $doc) {! $this->processWidgetFlush($dm, $doc);! }! }! }! }
  53. 53. /** @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);! }
  54. 54. /** @DIObserve("user.follow_user") */! public function onFollowUser(UserUserEvent $event)! {! $event->getUser()! ->getStats()! ->incrementFollowedUsers(1);! $event->getOtherUser()! ->getStats()! ->incrementFollowers(1);! }
  55. 55. Decouple your application by delegating work to clean, concise, single-purpose event listeners.
  56. 56. Contextual Configuration
  57. 57. Save your future self a headache
  58. 58. # @MainBundle/Resources/config/widget.yml! services:! widget_twiddler:! class: KrisBundleMainBundleWidgetTwiddler! arguments:! - @event_dispatcher! - @?logger
  59. 59. JMSDiExtraBundle
  60. 60. /** @DIService("widget_twiddler") */! class Twiddler! {! /** @DIInjectParams */! public function __construct(! EventDispatcherInterface $dispatcher,! LoggerInterface $logger = null)! {! // ...! }! }
  61. 61. services:! # aliases for auto-wiring! container: @service_container! dm: @doctrine_mongodb.odm.document_manager! doctrine: @doctrine_mongodb! dispatcher: @event_dispatcher! security: @security.context
  62. 62. require.js
  63. 63. <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>
  64. 64. // web/js/model/user.js! define(! [ "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")! })! }! })! }! )
  65. 65. {% 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 %}
  66. 66. Dependencies • • • model: backbone, underscore view: backbone, jquery template: model, view
  67. 67. {% 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>
  68. 68. Unused dependencies naturally slough off
  69. 69. JMSSerializerBundle
  70. 70. {% 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 %}
  71. 71. /** @ExclusionPolicy("ALL") */! class User! {! private $id;! ! /** @Expose */! private $firstName;! ! /** @Expose */! private $lastName;! }
  72. 72. Miscellaneous
  73. 73. When to create a new bundle • • • Anything reusable Lots of classes relating to one feature Integration with a third party
  74. 74. {% include 'MainBundle:Account/Widget:sidebar.html.twig' %}
  75. 75. {% include 'AccountBundle:Widget:sidebar.html.twig' %}
  76. 76. Access Control
  77. 77. The Symfony ACL is for arbitrary permissions
  78. 78. Encapsulate access logic in custom voter classes
  79. 79. /** @DIService(public=false) @DITag("security.voter") */! class WidgetVoter implements VoterInterface! {! public function supportsAttribute($attribute)! {! return 'OWNER' === $attribute;! }! ! public function supportsClass($class)! {! return is_a($class, 'KrisModelWidget');! }! ! public function vote(TokenInterface $token, $widget, array $attributes)! {! // ...! }! }
  80. 80. public function vote(TokenInterface $token, $widget, array $attributes)! {! $result = VoterInterface::ACCESS_ABSTAIN;! if (!$this->supportsClass(get_class($widget))) {! return $result;! }! ! foreach ($attributes as $attribute) {! if (!$this->supportsAttribute($attribute)) {! continue;! }! ! $result = VoterInterface::ACCESS_DENIED;! if ($token->getUser() === $widget->getUser()) {! return VoterInterface::ACCESS_GRANTED;! }! }! ! return $result;! }
  81. 81. JMSSecurityExtraBundle
  82. 82. /** @SecureParam(name="widget", permissions="OWNER") */! public function editAction(Widget $widget)! {! // ...! }
  83. 83. {% if is_granted('OWNER', widget) %}! {# ... #}! {% endif %}
  84. 84. No query builders outside of repositories
  85. 85. 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();! }! }
  86. 86. Eager ID creation
  87. 87. public function __construct()! {! $this->id = (string) new MongoId();! }
  88. 88. public function __construct()! {! $this->id = (string) new MongoId();! $this->createdAt = new DateTime();! $this->widgets = new ArrayCollection();! }
  89. 89. Remember your clone constructor
  90. 90. $foo = new Foo();! $bar = clone $foo;
  91. 91. public function __clone()! {! $this->id = (string) new MongoId();! $this->createdAt = new DateTime();! $this->widgets = new ArrayCollection(! $this->widgets->toArray()! );! }
  92. 92. 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()! );! }
  93. 93. Save space on field names
  94. 94. /** @ODMString(name="u") */! private $username;! ! /** @ODMString(name="uc") @ODMUniqueIndex */! private $usernameCanonical;
  95. 95. Only flush from the controller
  96. 96. public function theAction(Widget $widget)! {! $this->get('widget_twiddler')! ->skeedaddle($widget);! $this->flush();! }
  97. 97. No proxy objects
  98. 98. /** @ODMReferenceOne(targetDocument="User") */! private $user;
  99. 99. public function getUser()! {! if ($this->userId && !$this->user) {! throw new UninitializedReferenceException('user');! }! ! return $this->user;! }
  100. 100. Questions?
  101. 101. @kriswallsmith.net https://joind.in/10371 Thank You!

×