Skip to content

Commit

Permalink
Update HelperCollection & Helpers for events.
Browse files Browse the repository at this point in the history
A number of things have changed here:

* Helpers now directly bound to the EventManager.
* View now directly triggers events.
* HelperCollection is a basic factory/registry now.
* Helper uses reflection to determine the implementedEvents. In
  a subsequent commit the base methods will be removed, making fewer
  events bound per helper.

Eventually, this will help increase performance around helpers and help
streamline the events overall.
  • Loading branch information
markstory committed Jul 3, 2013
1 parent 8f82ff5 commit d51dd4b
Show file tree
Hide file tree
Showing 4 changed files with 96 additions and 102 deletions.
57 changes: 27 additions & 30 deletions lib/Cake/Test/TestCase/View/HelperCollectionTest.php
Expand Up @@ -30,6 +30,9 @@
* Extended HtmlHelper
*/
class HtmlAliasHelper extends HtmlHelper {

public function afterRender($viewFile) {
}
}

/**
Expand All @@ -46,7 +49,8 @@ class HelperCollectionTest extends TestCase {
*/
public function setUp() {
parent::setUp();
$this->View = $this->getMock('Cake\View\View', array(), array(null));
$this->View = new View(null);
$this->Events = $this->View->getEventManager();
$this->Helpers = new HelperCollection($this->View);
}

Expand All @@ -73,8 +77,6 @@ public function testLoad() {

$result = $this->Helpers->loaded();
$this->assertEquals(array('Html'), $result, 'loaded() results are wrong.');

$this->assertTrue($this->Helpers->enabled('Html'));
}

/**
Expand Down Expand Up @@ -106,6 +108,17 @@ public function testLazyLoadException() {
$this->Helpers->NotAHelper;
}

/**
* Test that loading helpers subscribes to events.
*
* @return void
*/
public function testLoadSubscribeEvents() {
$this->Helpers->load('Html', array('className' => __NAMESPACE__ . '\HtmlAliasHelper'));
$result = $this->Events->listeners('View.afterRender');
$this->assertCount(1, $result);
}

/**
* Tests loading as an alias
*
Expand All @@ -119,20 +132,24 @@ public function testLoadWithAlias() {
$result = $this->Helpers->loaded();
$this->assertEquals(array('Html'), $result, 'loaded() results are wrong.');

$this->assertTrue($this->Helpers->enabled('Html'));

$result = $this->Helpers->load('Html');
$this->assertInstanceOf(__NAMESPACE__ . '\HtmlAliasHelper', $result);
}

App::build(array('Plugin' => array(CAKE . 'Test/TestApp/Plugin/')));
Plugin::load(array('TestPlugin'));
/**
* Test loading helpers with aliases and plugins.
*
* @return void
*/
public function testLoadWithAliasAndPlugin() {
App::build(['Plugin' => [CAKE . 'Test/TestApp/Plugin/']]);
Plugin::load('TestPlugin');
$result = $this->Helpers->load('SomeOther', array('className' => 'TestPlugin.OtherHelper'));
$this->assertInstanceOf('TestPlugin\View\Helper\OtherHelperHelper', $result);
$this->assertInstanceOf('TestPlugin\View\Helper\OtherHelperHelper', $this->Helpers->SomeOther);

$result = $this->Helpers->loaded();
$this->assertEquals(array('Html', 'SomeOther'), $result, 'loaded() results are wrong.');
App::build();
$this->assertEquals(['SomeOther'], $result, 'loaded() results are wrong.');
}

/**
Expand All @@ -145,7 +162,7 @@ public function testLoadWithEnabledFalse() {
$this->assertInstanceOf('Cake\View\Helper\HtmlHelper', $result);
$this->assertInstanceOf('Cake\View\Helper\HtmlHelper', $this->Helpers->Html);

$this->assertFalse($this->Helpers->enabled('Html'), 'Html should be disabled');
$this->assertEmpty($this->Events->listeners('View.beforeRender'));
}

/**
Expand Down Expand Up @@ -176,24 +193,4 @@ public function testLoadPluginHelper() {
App::build();
}

/**
* test unload()
*
* @return void
*/
public function testUnload() {
$this->Helpers->load('Form');
$this->Helpers->load('Html');

$result = $this->Helpers->loaded();
$this->assertEquals(array('Form', 'Html'), $result, 'loaded helpers is wrong');

$this->Helpers->unload('Html');
$this->assertNotContains('Html', $this->Helpers->loaded());
$this->assertContains('Form', $this->Helpers->loaded());

$result = $this->Helpers->loaded();
$this->assertEquals(array('Form'), $result, 'loaded helpers is wrong');
}

}
32 changes: 31 additions & 1 deletion lib/Cake/View/Helper.php
Expand Up @@ -19,6 +19,7 @@
use Cake\Core\Configure;
use Cake\Core\Object;
use Cake\Core\Plugin;
use Cake\Event\EventListener;
use Cake\Routing\Router;
use Cake\Utility\ClassRegistry;
use Cake\Utility\Hash;
Expand All @@ -31,7 +32,7 @@
*
* @package Cake.View
*/
class Helper extends Object {
class Helper extends Object implements EventListener {

/**
* Settings for this helper.
Expand Down Expand Up @@ -863,6 +864,35 @@ public function beforeRenderFile($viewfile) {
public function afterRenderFile($viewfile, $content) {
}

/**
* Get the View callbacks this helper is interested in.
*
* By defining one of the callback methods a helper is assumed
* to be interested in the related event.
*
* Override this method if you need to add non-conventional event listeners.
* Or if you want helpers to listen to non-standard events.
*
* @return array
*/
public function implementedEvents() {
$eventMap = [
'View.beforeRenderFile' => 'beforeRenderFile',
'View.afterRenderFile' => 'afterRenderFile',
'View.beforeRender' => 'beforeRender',
'View.afterRender' => 'afterRender',
'View.beforeLayout' => 'beforeLayout',
'View.afterLayout' => 'afterLayout'
];
$events = [];
foreach ($eventMap as $event => $method) {
if (method_exists($this, $method)) {
$events[$event] = $method;
}
}
return $events;
}

/**
* Transforms a recordset from a hasAndBelongsToMany association to a list of selected
* options for a multiple select element
Expand Down
98 changes: 38 additions & 60 deletions lib/Cake/View/HelperCollection.php
Expand Up @@ -20,9 +20,7 @@

use Cake\Core\App;
use Cake\Error;
use Cake\Event\Event;
use Cake\Event\EventListener;
use Cake\Utility\ObjectCollection;
use Cake\Event\EventManager;
use Cake\View\View;

/**
Expand All @@ -31,7 +29,14 @@
*
* @package Cake.View
*/
class HelperCollection extends ObjectCollection implements EventListener {
class HelperCollection {

/**
* Hash of already loaded helpers.
*
* @var array
*/
protected $_loaded = [];

/**
* View object to use when making helpers.
Expand All @@ -40,13 +45,25 @@ class HelperCollection extends ObjectCollection implements EventListener {
*/
protected $_View;


/**
* EventManager instance.
*
* Helpers constructed by this object will be subscribed to this manager.
*
* @var Cake\Event\EventManager
*/
protected $_eventManager;

/**
* Constructor
*
* @param View $view
*/
public function __construct(View $view) {
$this->_View = $view;
$eventManager = $view->getEventManager();
$this->_eventManager = $eventManager;
}

/**
Expand All @@ -60,7 +77,7 @@ public function __construct(View $view) {
* App helpers are searched, and then plugin helpers.
*/
public function __isset($helper) {
if (parent::__isset($helper)) {
if (isset($this->_loaded[$helper])) {
return true;
}

Expand All @@ -87,8 +104,8 @@ public function __isset($helper) {
* @return mixed
*/
public function __get($name) {
if ($result = parent::__get($name)) {
return $result;
if (isset($this->_loaded[$name])) {
return $this->_loaded[$name];
}
if ($this->__isset($name)) {
return $this->_loaded[$name];
Expand Down Expand Up @@ -134,72 +151,33 @@ public function load($helper, $settings = array()) {
'plugin' => substr($plugin, 0, -1)
));
}
$this->_loaded[$name] = new $helperClass($this->_View, $settings);
$helperObject = new $helperClass($this->_View, $settings);

$vars = array('request', 'theme', 'plugin');
foreach ($vars as $var) {
$this->_loaded[$name]->{$var} = $this->_View->{$var};
$helperObject->{$var} = $this->_View->{$var};
}

$this->_loaded[$name] = $helperObject;

$enable = isset($settings['enabled']) ? $settings['enabled'] : true;
if ($enable) {
$this->enable($name);
$this->_eventManager->attach($helperObject);
}
return $this->_loaded[$name];
}

/**
* Returns a list of all events that will fire in the View during it's lifecycle.
*
* @return array
*/
public function implementedEvents() {
return array(
'View.beforeRenderFile' => 'trigger',
'View.afterRenderFile' => 'trigger',
'View.beforeRender' => 'trigger',
'View.afterRender' => 'trigger',
'View.beforeLayout' => 'trigger',
'View.afterLayout' => 'trigger'
);
return $helperObject;
}

/**
* Trigger a callback method on every object in the collection.
* Used to trigger methods on objects in the collection. Will fire the methods in the
* order they were attached.
*
* ### Options
*
* - `breakOn` Set to the value or values you want the callback propagation to stop on.
* Can either be a scalar value, or an array of values to break on. Defaults to `false`.
*
* - `break` Set to true to enabled breaking. When a trigger is broken, the last returned value
* will be returned. If used in combination with `collectReturn` the collected results will be returned.
* Defaults to `false`.
*
* - `collectReturn` Set to true to collect the return of each object into an array.
* This array of return values will be returned from the trigger() call. Defaults to `false`.
*
* - `modParams` Allows each object the callback gets called on to modify the parameters to the next object.
* Setting modParams to an integer value will allow you to modify the parameter with that index.
* Any non-null value will modify the parameter index indicated.
* Defaults to false.
*
* Get the loaded helpers list, or get the helper instance at a given name.
*
* @param string|Cake\Event\Event $callback Method to fire on all the objects. Its assumed all the objects implement
* the method you are calling. If an instance of Cake\Event\Event is provided, then then Event name will parsed to
* get the callback name. This is done by getting the last word after any dot in the event name
* (eg. `Model.afterSave` event will trigger the `afterSave` callback)
* @param array $params Array of parameters for the triggered callback.
* @param array $options Array of options.
* @return mixed Either the last result or all results if collectReturn is on.
* @throws CakeException when modParams is used with an index that does not exist.
* @param null|string $name The helper name to get or null.
* @return array|Helper Either a list of helper names, or a loaded helper.
*/
public function trigger($callback, $params = array(), $options = array()) {
if ($callback instanceof Event) {
$callback->omitSubject = true;
public function loaded($name = null) {
if (!empty($name)) {
return isset($this->_loaded[$name]);
}
return parent::trigger($callback, $params, $options);
return array_keys($this->_loaded);
}

}
11 changes: 0 additions & 11 deletions lib/Cake/View/View.php
Expand Up @@ -304,13 +304,6 @@ class View extends Object {
*/
protected $_eventManager = null;

/**
* Whether the event manager was already configured for this object
*
* @var boolean
*/
protected $_eventManagerConfigured = false;

/**
* Constant for view file type 'view'
*/
Expand Down Expand Up @@ -366,10 +359,6 @@ public function getEventManager() {
if (empty($this->_eventManager)) {
$this->_eventManager = new EventManager();
}
if (!$this->_eventManagerConfigured) {
$this->_eventManager->attach($this->Helpers);
$this->_eventManagerConfigured = true;
}
return $this->_eventManager;
}

Expand Down

0 comments on commit d51dd4b

Please sign in to comment.