strategy-cookbook-introduction.rst 8.5 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254
  1. Strategy-Pattern
  2. ================
  3. This recipe will give you a short introduction on how to design
  4. similar entities without using expensive (i.e. slow) inheritance
  5. but with not more than \* the well-known strategy pattern \* event
  6. listeners
  7. Scenario / Problem
  8. ------------------
  9. Given a Content-Management-System, we probably want to add / edit
  10. some so-called "blocks" and "panels". What are they for?
  11. - A block might be a registration form, some text content, a table
  12. with information. A good example might also be a small calendar.
  13. - A panel is by definition a block that can itself contain blocks.
  14. A good example for a panel might be a sidebar box: You could easily
  15. add a small calendar into it.
  16. So, in this scenario, when building your CMS, you will surely add
  17. lots of blocks and panels to your pages and you will find yourself
  18. highly uncomfortable because of the following:
  19. - Every existing page needs to know about the panels it contains -
  20. therefore, you'll have an association to your panels. But if you've
  21. got several types of panels - what do you do? Add an association to
  22. every panel-type? This wouldn't be flexible. You might be tempted
  23. to add an AbstractPanelEntity and an AbstractBlockEntity that use
  24. class inheritance. Your page could then only confer to the
  25. AbstractPanelType and Doctrine 2 would do the rest for you, i.e.
  26. load the right entities. But - you'll for sure have lots of panels
  27. and blocks, and even worse, you'd have to edit the discriminator
  28. map *manually* every time you or another developer implements a new
  29. block / entity. This would tear down any effort of modular
  30. programming.
  31. Therefore, we need something that's far more flexible.
  32. Solution
  33. --------
  34. The solution itself is pretty easy. We will have one base class
  35. that will be loaded via the page and that has specific behaviour -
  36. a Block class might render the front-end and even the backend, for
  37. example. Now, every block that you'll write might look different or
  38. need different data - therefore, we'll offer an API to these
  39. methods but internally, we use a strategy that exactly knows what
  40. to do.
  41. First of all, we need to make sure that we have an interface that
  42. contains every needed action. Such actions would be rendering the
  43. front-end or the backend, solving dependencies (blocks that are
  44. supposed to be placed in the sidebar could refuse to be placed in
  45. the middle of your page, for example).
  46. Such an interface could look like this:
  47. .. code-block:: php
  48. <?php
  49. /**
  50. * This interface defines the basic actions that a block / panel needs to support.
  51. *
  52. * Every blockstrategy is *only* responsible for rendering a block and declaring some basic
  53. * support, but *not* for updating its configuration etc. For this purpose, use controllers
  54. * and models.
  55. */
  56. interface BlockStrategyInterface {
  57. /**
  58. * This could configure your entity
  59. */
  60. public function setConfig(Config\EntityConfig $config);
  61. /**
  62. * Returns the config this strategy is configured with.
  63. * @return Core\Model\Config\EntityConfig
  64. */
  65. public function getConfig();
  66. /**
  67. * Set the view object.
  68. * @param \Zend_View_Interface $view
  69. * @return \Zend_View_Helper_Interface
  70. */
  71. public function setView(\Zend_View_Interface $view);
  72. /**
  73. * @return \Zend_View_Interface
  74. */
  75. public function getView();
  76. /**
  77. * Renders this strategy. This method will be called when the user
  78. * displays the site.
  79. *
  80. * @return string
  81. */
  82. public function renderFrontend();
  83. /**
  84. * Renders the backend of this block. This method will be called when
  85. * a user tries to reconfigure this block instance.
  86. *
  87. * Most of the time, this method will return / output a simple form which in turn
  88. * calls some controllers.
  89. *
  90. * @return string
  91. */
  92. public function renderBackend();
  93. /**
  94. * Returns all possible types of panels this block can be stacked onto
  95. *
  96. * @return array
  97. */
  98. public function getRequiredPanelTypes();
  99. /**
  100. * Determines whether a Block is able to use a given type or not
  101. * @param string $typeName The typename
  102. * @return boolean
  103. */
  104. public function canUsePanelType($typeName);
  105. public function setBlockEntity(AbstractBlock $block);
  106. public function getBlockEntity();
  107. }
  108. As you can see, we have a method "setBlockEntity" which ties a potential strategy to an object of type AbstractBlock. This type will simply define the basic behaviour of our blocks and could potentially look something like this:
  109. .. code-block:: php
  110. <?php
  111. /**
  112. * This is the base class for both Panels and Blocks.
  113. * It shouldn't be extended by your own blocks - simply write a strategy!
  114. */
  115. abstract class AbstractBlock {
  116. /**
  117. * The id of the block item instance
  118. * This is a doctrine field, so you need to setup generation for it
  119. * @var integer
  120. */
  121. private $id;
  122. // Add code for relation to the parent panel, configuration objects, ....
  123. /**
  124. * This var contains the classname of the strategy
  125. * that is used for this blockitem. (This string (!) value will be persisted by Doctrine 2)
  126. *
  127. * This is a doctrine field, so make sure that you use an @column annotation or setup your
  128. * yaml or xml files correctly
  129. * @var string
  130. */
  131. protected $strategyClassName;
  132. /**
  133. * This var contains an instance of $this->blockStrategy. Will not be persisted by Doctrine 2.
  134. *
  135. * @var BlockStrategyInterface
  136. */
  137. protected $strategyInstance;
  138. /**
  139. * Returns the strategy that is used for this blockitem.
  140. *
  141. * The strategy itself defines how this block can be rendered etc.
  142. *
  143. * @return string
  144. */
  145. public function getStrategyClassName() {
  146. return $this->strategyClassName;
  147. }
  148. /**
  149. * Returns the instantiated strategy
  150. *
  151. * @return BlockStrategyInterface
  152. */
  153. public function getStrategyInstance() {
  154. return $this->strategyInstance;
  155. }
  156. /**
  157. * Sets the strategy this block / panel should work as. Make sure that you've used
  158. * this method before persisting the block!
  159. *
  160. * @param BlockStrategyInterface $strategy
  161. */
  162. public function setStrategy(BlockStrategyInterface $strategy) {
  163. $this->strategyInstance = $strategy;
  164. $this->strategyClassName = get_class($strategy);
  165. $strategy->setBlockEntity($this);
  166. }
  167. Now, the important point is that $strategyClassName is a Doctrine 2
  168. field, i.e. Doctrine will persist this value. This is only the
  169. class name of your strategy and not an instance!
  170. Finishing your strategy pattern, we hook into the Doctrine postLoad
  171. event and check whether a block has been loaded. If so, you will
  172. initialize it - i.e. get the strategies classname, create an
  173. instance of it and set it via setStrategyBlock().
  174. This might look like this:
  175. .. code-block:: php
  176. <?php
  177. use \Doctrine\ORM,
  178. \Doctrine\Common;
  179. /**
  180. * The BlockStrategyEventListener will initialize a strategy after the
  181. * block itself was loaded.
  182. */
  183. class BlockStrategyEventListener implements Common\EventSubscriber {
  184. protected $view;
  185. public function __construct(\Zend_View_Interface $view) {
  186. $this->view = $view;
  187. }
  188. public function getSubscribedEvents() {
  189. return array(ORM\Events::postLoad);
  190. }
  191. public function postLoad(ORM\Event\LifecycleEventArgs $args) {
  192. $blockItem = $args->getEntity();
  193. // Both blocks and panels are instances of Block\AbstractBlock
  194. if ($blockItem instanceof Block\AbstractBlock) {
  195. $strategy = $blockItem->getStrategyClassName();
  196. $strategyInstance = new $strategy();
  197. if (null !== $blockItem->getConfig()) {
  198. $strategyInstance->setConfig($blockItem->getConfig());
  199. }
  200. $strategyInstance->setView($this->view);
  201. $blockItem->setStrategy($strategyInstance);
  202. }
  203. }
  204. }
  205. In this example, even some variables are set - like a view object
  206. or a specific configuration object.