Ce diaporama a bien été signalé.
Nous utilisons votre profil LinkedIn et vos données d’activité pour vous proposer des publicités personnalisées et pertinentes. Vous pouvez changer vos préférences de publicités à tout moment.
How Kris Writes Symfony Apps
@kriswallsmith
father artist bowhunter hacker
president widower gamer actor
tapdancer lover hater singer
writer founder yogi consultant
a...
assetic
Buzz
Spork
Getting Started
composer create-project !
symfony/framework-standard-edition !
widgets-by-kris/ !
~2.4
+
+
+
+

"doctrine/orm": "~2.2,>=2.2.3",!
"doctrine/doctrine-bundle": "~1.2",!
"doctrine/mongodb-odm-bundle": "~3.0",!
"jm...
./app/console generate:bundle !
--namespace=Kris/Bundle/MainBundle
public function registerContainerConfiguration(LoaderInterface $loader)!
{!
$loader->load(__DIR__.'/config/config_'.$this-...
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/Widget.php!
$repo = $this->dm->getRepository('Model:User');
…doesn't do any work…
use KrisBundleMainBundleCanonicalizer;!
!

public function setUsername($username)!
{!
$this->username = $username;!
!

$ca...
use KrisBundleMainBundleCanonicalizer;!
!

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 = $userna...
Anemic domain model 

is an anti-pattern?
Anemic???
“The fundamental horror of this anti-pattern is that it's
so contrary to the basic idea of object-oriented design;
which i...
$cabinet->open();
Cabinets don’t open themselves.
$asset->getLastModified();
Mapping Layers
thin
thin controller

fat model
MVC
Is Symfony an MVC framework?
HTTP
Application Land

Controller

HTTP Land
The controller is thin 

because it maps from 

HTTP-land to application-land.
What about the model?
public function registerAction()!
{!
// ...!
$user->sendWelcomeEmail();!
// ...!
}
public function registerAction()!
{!
// ...!
$mailer->sendWelcomeEmail($user);!
// ...!
}
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!
Application Events
Use them.
That happened.
/** @DIObserve("user.username_change") */!
public function onUsernameChange(UserEvent $event)!
{!
$user = $event->getUser(...
One event class per model
•
•
•

Event name constants	

Accepts object manager and object as arguments	

Simple accessors
$event = new UserEvent($dm, $user);!
$dispatcher->dispatch(UserEvent::CREATE, $event);
$event = new UserUserEvent($dm, $user, $otherUser);!
$dispatcher->dispatch(UserEvent::FOLLOW, $event);
preFlush
public function preFlush(ManagerEventArgs $event)!
{!
$dm = $event->getObjectManager();!
$uow = $dm->getUnitOfWork();!
!
f...
/** @DIObserve("user.create") */!
public function onUserCreate(UserEvent $event)!
{!
$user = $event->getUser();!
!

$activ...
/** @DIObserve("user.follow_user") */!
public function onFollowUser(UserUserEvent $event)!
{!
$event->getUser()!
->getStat...
Decouple your application by
delegating work to clean, concise,
single-purpose event listeners.
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(!
EventDispat...
services:!
# aliases for auto-wiring!
container: @service_container!
dm: @doctrine_mongodb.odm.document_manager!
doctrine:...
require.js
<script src="{{ asset('js/lib/require.js') }}"></script>!
<script>!
require.config({!
baseUrl: "{{ asset('js') }}",!
paths...
// web/js/model/user.js!
define(!
[ "underscore", "backbone" ],!
function(_, Backbone) {!
var tmpl = _.template("<%- first...
{% block head %}!
<script>!
require(!
[ "view/user", "model/user" ],!
function(UserView, User) {!
var view = new UserView(...
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"!
fil...
Unused dependencies
naturally slough off
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 */!
pr...
Miscellaneous
When to create a new bundle
•
•
•

Anything reusable	

Lots of classes relating to one feature	

Integration with a third ...
{% 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 ...
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 ...
Save space on field names
/** @ODMString(name="u") */!
private $username;!
!

/** @ODMString(name="uc") @ODMUniqueIndex */!
private $usernameCanonic...
Only flush from the controller
public function theAction(Widget $widget)!
{!
$this->get('widget_twiddler')!
->skeedaddle($widget);!
$this->flush();!
}
No proxy objects
/** @ODMReferenceOne(targetDocument="User") */!
private $user;
public function getUser()!
{!
if ($this->userId && !$this->user) {!
throw new UninitializedReferenceException('user');!
}!...
Questions?
@kriswallsmith.net

https://joind.in/10371

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
Prochain SlideShare
Chargement dans…5
×

How Kris Writes Symfony Apps

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.

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!

×