Successfully reported this slideshow.
We use your LinkedIn profile and activity data to personalize ads and to show you more relevant ads. You can change your ad preferences anytime.
How Kris Writes Symfony Apps
Mapping Layers
thin
thin controller fat model
MVC
Is Symfony an MVC framework?
HTTP
Symfony is an HTTP framework
Application Land 
Controller 
HTTP Land
The controller is thin 
because it maps from 
HTTP-land to application-land.
What about the model?
Persistence Land 
Model 
Application Land
The model maps from 
application-land to persistence-land.
Persistence Land 
Model 
Application Land 
Controller 
HTTP Land
Who lives in application land?
Thin controller , thin model… 
Fat service layer
Should there be managers?
Application Events
Listen
/** @DIObserve(UserEvent::CREATE) */ 
public function onUserCreate(UserEvent $event) 
{ 
$user = $event->getUser(); 
$acti...
/** @DIObserve(UserEvent::USERNAME_CHANGE) */ 
public function onUsernameChange(UserEvent $event) 
{ 
$user = $event->getU...
/** @DIObserve(UserEvent::FOLLOW) */ 
public function onFollow(UserUserEvent $event) 
{ 
$event->getUser() 
->getStats() 
...
Dispatch
$event = new UserEvent($dm, $user); 
$dispatcher->dispatch(UserEvent::CREATE, $event);
$event = new UserEvent($dm, $user); 
$dispatcher->dispatch(UserEvent::UPDATE, $event);
$event = new UserUserEvent($dm, $user, $otherUser); 
$dispatcher->dispatch(UserEvent::FOLLOW, $event);
preFlush
public function preFlush(ManagerEventArgs $event) 
{ 
$dm = $event->getObjectManager(); 
$uow = $dm->getUnitOfWork(); 
for...
Decouple your application by 
delegating work to clean, concise, 
single-purpose event listeners.
Model
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% 
connection...
// repo for src/Kris/Model/User.php 
$repo = $this->dm->getRepository('Model:User');
…doesn't do any work…
use KrisBundleMainBundleCanonicalizer; 
public function setUsername($username) 
{ 
$this->username = $username; 
$canonica...
use KrisBundleMainBundleCanonicalizer; 
public function setUsername($username, Canonicalizer $canonicalizer) 
{ 
$this->us...
…and is unaware of the work 
being done around her.
public function setUsername($username) 
{ 
// a listener will update the 
// canonical username 
$this->username = $userna...
Cabinets don’t open themselves.
Contextual Configuration
Save your future self a headache.
# @MainBundle/Resources/config/widget.yml 
services: 
widget_twiddler: 
class: KrisBundleMainBundleWidgetTwiddler 
argumen...
JMSDiExtraBundle
/** @DIService("widget_twiddler") */ 
class Twiddler 
{ 
/** @DIInjectParams() */ 
public function __construct( 
EventDisp...
services: 
# aliases for auto-wiring 
container: @service_container 
dm: @doctrine_mongodb.odm.document_manager 
doctrine:...
require.js
{% block head %} 
<script> 
require( 
[ "view/user", "model/user" ], 
function(UserView, User) { 
var view = new UserView(...
JMSSerializerBundle
{% block head %} 
<script> 
require( 
[ "view/user", "model/user" ], 
function(UserView, User) { 
var view = new UserView(...
/** @ExclusionPolicy("ALL") */ 
class User 
{ 
private $id; 
/** @Expose() */ 
private $firstName; 
/** @Expose() */ 
priv...
Five more things…
When to create a new bundle 
• Anything reusable 
• A new feature 
• Lots of classes relating to one feature 
• Integratio...
{% 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
public function vote(TokenInterface $token, $widget, array $attributes) 
{ 
$result = VoterInterface::ACCESS_ABSTAIN; 
if ...
JMSSecurityExtraBundle
/** @SecureParam(name="widget", permissions="OWNER") */ 
public function editAction(Widget $widget) 
{ 
// ... 
}
{% if is_granted('OWNER', widget) %} 
{# ... #} 
{% endif %}
No query builders 
outside of repositories
class WidgetRepository extends DocumentRepository 
{ 
public function findByUser(User $user) 
{ 
return $this->createQuery...
Eager ID creation
public function __construct() 
{ 
$this->id = (string) new MongoId(); 
}
public function __construct() 
{ 
$this->id = (string) new MongoId(); 
$this->createdAt = new DateTime(); 
$this->widgets ...
Remember your 
clone constructor
$foo = new Foo(); 
$bar = clone $foo;
public function __clone() 
{ 
$this->id = (string) new MongoId(); 
$this->createdAt = new DateTime(); 
$this->widgets = ne...
public function __construct() 
{ 
$this->id = (string) new MongoId(); 
$this->createdAt = new DateTime(); 
$this->widgets ...
Only flush from the controller
public function theAction(Widget $widget) 
{ 
$this->get('widget_twiddler') 
->skeedaddle($widget); 
$this->flush(); 
}
Questions?
How kris-writes-symfony-apps-london
Upcoming SlideShare
Loading in …5
×

How kris-writes-symfony-apps-london

3,330 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
  • Be the first to comment

How kris-writes-symfony-apps-london

  1. 1. How Kris Writes Symfony Apps
  2. 2. Mapping Layers
  3. 3. thin
  4. 4. thin controller fat model
  5. 5. MVC
  6. 6. Is Symfony an MVC framework?
  7. 7. HTTP
  8. 8. Symfony is an HTTP framework
  9. 9. Application Land Controller HTTP Land
  10. 10. The controller is thin because it maps from HTTP-land to application-land.
  11. 11. What about the model?
  12. 12. Persistence Land Model Application Land
  13. 13. The model maps from application-land to persistence-land.
  14. 14. Persistence Land Model Application Land Controller HTTP Land
  15. 15. Who lives in application land?
  16. 16. Thin controller , thin model… Fat service layer
  17. 17. Should there be managers?
  18. 18. Application Events
  19. 19. Listen
  20. 20. /** @DIObserve(UserEvent::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); }
  21. 21. /** @DIObserve(UserEvent::USERNAME_CHANGE) */ public function onUsernameChange(UserEvent $event) { $user = $event->getUser(); $dm = $event->getDocumentManager(); $dm->getRepository('Model:Widget') ->updateDenormalizedUsernames($user); }
  22. 22. /** @DIObserve(UserEvent::FOLLOW) */ public function onFollow(UserUserEvent $event) { $event->getUser() ->getStats() ->incrementFollowedUsers(1); $event->getOtherUser() ->getStats() ->incrementFollowers(1); }
  23. 23. Dispatch
  24. 24. $event = new UserEvent($dm, $user); $dispatcher->dispatch(UserEvent::CREATE, $event);
  25. 25. $event = new UserEvent($dm, $user); $dispatcher->dispatch(UserEvent::UPDATE, $event);
  26. 26. $event = new UserUserEvent($dm, $user, $otherUser); $dispatcher->dispatch(UserEvent::FOLLOW, $event);
  27. 27. preFlush
  28. 28. 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); } } } }
  29. 29. Decouple your application by delegating work to clean, concise, single-purpose event listeners.
  30. 30. Model
  31. 31. Treat your model like a princess.
  32. 32. She gets her own wing of the palace…
  33. 33. 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
  34. 34. // repo for src/Kris/Model/User.php $repo = $this->dm->getRepository('Model:User');
  35. 35. …doesn't do any work…
  36. 36. use KrisBundleMainBundleCanonicalizer; public function setUsername($username) { $this->username = $username; $canonicalizer = Canonicalizer::instance(); $this->usernameCanonical = $canonicalizer->canonicalize($username); }
  37. 37. use KrisBundleMainBundleCanonicalizer; public function setUsername($username, Canonicalizer $canonicalizer) { $this->username = $username; $this->usernameCanonical = $canonicalizer->canonicalize($username); }
  38. 38. …and is unaware of the work being done around her.
  39. 39. public function setUsername($username) { // a listener will update the // canonical username $this->username = $username; }
  40. 40. Cabinets don’t open themselves.
  41. 41. Contextual Configuration
  42. 42. Save your future self a headache.
  43. 43. # @MainBundle/Resources/config/widget.yml services: widget_twiddler: class: KrisBundleMainBundleWidgetTwiddler arguments: - @event_dispatcher - @?logger
  44. 44. JMSDiExtraBundle
  45. 45. /** @DIService("widget_twiddler") */ class Twiddler { /** @DIInjectParams() */ public function __construct( EventDispatcherInterface $dispatcher, LoggerInterface $logger = null) { // ... } }
  46. 46. services: # aliases for auto-wiring container: @service_container dm: @doctrine_mongodb.odm.document_manager doctrine: @doctrine_mongodb dispatcher: @event_dispatcher security: @security.context
  47. 47. require.js
  48. 48. {% 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 %}
  49. 49. JMSSerializerBundle
  50. 50. {% 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 %}
  51. 51. /** @ExclusionPolicy("ALL") */ class User { private $id; /** @Expose() */ private $firstName; /** @Expose() */ private $lastName; }
  52. 52. Five more things…
  53. 53. When to create a new bundle • Anything reusable • A new feature • Lots of classes relating to one feature • Integration with a third party
  54. 54. {% include 'MainBundle:Account/Widget:sidebar.html.twig' %}
  55. 55. {% include 'AccountBundle:Widget:sidebar.html.twig' %}
  56. 56. Access Control
  57. 57. The Symfony ACL is for arbitrary permissions
  58. 58. Encapsulate access logic in custom voter classes
  59. 59. 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; }
  60. 60. JMSSecurityExtraBundle
  61. 61. /** @SecureParam(name="widget", permissions="OWNER") */ public function editAction(Widget $widget) { // ... }
  62. 62. {% if is_granted('OWNER', widget) %} {# ... #} {% endif %}
  63. 63. No query builders outside of repositories
  64. 64. 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(); } }
  65. 65. Eager ID creation
  66. 66. public function __construct() { $this->id = (string) new MongoId(); }
  67. 67. public function __construct() { $this->id = (string) new MongoId(); $this->createdAt = new DateTime(); $this->widgets = new ArrayCollection(); }
  68. 68. Remember your clone constructor
  69. 69. $foo = new Foo(); $bar = clone $foo;
  70. 70. public function __clone() { $this->id = (string) new MongoId(); $this->createdAt = new DateTime(); $this->widgets = new ArrayCollection( $this->widgets->toArray() ); }
  71. 71. 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() ); }
  72. 72. Only flush from the controller
  73. 73. public function theAction(Widget $widget) { $this->get('widget_twiddler') ->skeedaddle($widget); $this->flush(); }
  74. 74. Questions?

×