Browse Source

Flash helper support for elements.

Mark Scherer 11 years ago
parent
commit
8493f19eb3

+ 139 - 12
Controller/Component/FlashComponent.php

@@ -14,6 +14,35 @@ class FlashComponent extends Component {
 
 
 	public $components = array('Session');
 	public $components = array('Session');
 
 
+	protected $_defaultConfig = array(
+		'headerOnAjax' => true,
+		'transformCore' => true,
+		'useElements' => false, // Set to true to use 3.x flash message rendering via Elements
+		'type' => 'info',
+		'typeToElement' => false, // Set to true to have a single type to Element matching
+		'plugin' => null, // Only for typeToElement
+		'element' => 'Tools.default',
+		'params' => [],
+		'escape' => false
+	);
+
+	/**
+	 * Constructor.
+	 *
+	 * @param ComponentCollection $collection
+	 * @param array $config
+	 */
+	public function __construct(ComponentCollection $collection, $config = array()) {
+		$defaults = (array)Configure::read('Flash') + $this->_defaultConfig;
+		//BC
+		if (Configure::read('Common.messages') !== null) {
+			$defaults['transformCore'] = Configure::read('Common.messages');
+		}
+
+		$config += $defaults;
+		parent::__construct($collection, $config);
+	}
+
 	/**
 	/**
 	 * For automatic startup
 	 * For automatic startup
 	 * for this helper the controller has to be passed as reference
 	 * for this helper the controller has to be passed as reference
@@ -30,18 +59,22 @@ class FlashComponent extends Component {
 	 * Called after the Controller::beforeRender(), after the view class is loaded, and before the
 	 * Called after the Controller::beforeRender(), after the view class is loaded, and before the
 	 * Controller::render()
 	 * Controller::render()
 	 *
 	 *
+	 * Unless Configure::read('Ajax.transformCore') is false, it will also transform any core ones to this plugin.
+	 * Unless Configure::read('Ajax.headerOnAjax') is false, it will pass the messages as header to AJAX requests.
+	 * Set it to false if other components are handling the message return in AJAX use cases already.
+	 *
 	 * @param object $Controller Controller with components to beforeRender
 	 * @param object $Controller Controller with components to beforeRender
 	 * @return void
 	 * @return void
 	 */
 	 */
 	public function beforeRender(Controller $Controller) {
 	public function beforeRender(Controller $Controller) {
-		if (Configure::read('Common.messages') !== false && $messages = $this->Session->read('Message')) {
+		if ($this->settings['transformCore'] && $messages = $this->Session->read('Message')) {
 			foreach ($messages as $message) {
 			foreach ($messages as $message) {
 				$this->message($message['message'], 'error');
 				$this->message($message['message'], 'error');
 			}
 			}
 			$this->Session->delete('Message');
 			$this->Session->delete('Message');
 		}
 		}
 
 
-		if ($this->Controller->request->is('ajax')) {
+		if ($this->settings['headerOnAjax'] && $this->Controller->request->is('ajax')) {
 			$ajaxMessages = array_merge(
 			$ajaxMessages = array_merge(
 				(array)$this->Session->read('messages'),
 				(array)$this->Session->read('messages'),
 				(array)Configure::read('messages')
 				(array)Configure::read('messages')
@@ -57,16 +90,21 @@ class FlashComponent extends Component {
 	 * Adds a flash message.
 	 * Adds a flash message.
 	 * Updates "messages" session content (to enable multiple messages of one type).
 	 * Updates "messages" session content (to enable multiple messages of one type).
 	 *
 	 *
+	 * ### Options:
+	 *
+	 * - `element` The element used to render the flash message. Default to 'default'.
+	 * - `params` An array of variables to make available when using an element.
+	 * - `escape` If content should be escaped or not in the element itself or if elements are not used in the component.
+	 *
 	 * @param string $message Message to output.
 	 * @param string $message Message to output.
-	 * @param string $type Type ('error', 'warning', 'success', 'info' or custom class).
+	 * @param array|string $options Options or Type ('error', 'warning', 'success', 'info' or custom class).
 	 * @return void
 	 * @return void
 	 */
 	 */
-	public function message($message, $type = null) {
-		if (!$type) {
-			$type = 'info';
-		}
+	public function message($message, $options = array()) {
+		$message = $this->_prepMessage($message, $options);
 
 
 		$old = (array)$this->Session->read('messages');
 		$old = (array)$this->Session->read('messages');
+		$type = $options['type'];
 		if (isset($old[$type]) && count($old[$type]) > 99) {
 		if (isset($old[$type]) && count($old[$type]) > 99) {
 			array_shift($old[$type]);
 			array_shift($old[$type]);
 		}
 		}
@@ -75,20 +113,104 @@ class FlashComponent extends Component {
 	}
 	}
 
 
 	/**
 	/**
+	 * FlashComponent::_prepMessage()
+	 *
+	 * @param string $message
+	 * @param array $options
+	 * @return array
+	 */
+	protected function _prepMessage($message, &$options) {
+		if (!is_array($options)) {
+			$type = $options ?: $this->settings['type'];
+			$options = array('type' => $type);
+		}
+		$defaults = $this->settings;
+		$options += $defaults;
+
+		$message = array(
+			'message' => $options['escape'] ? h($message) : $message,
+			'params' => $options['params'],
+			'escape' => $options['escape']
+		);
+
+		if ($this->settings['useElements']) {
+			if ($options['typeToElement'] && $options['element'] === $defaults['element']) {
+				$options['element'] = ($options['plugin'] ? $options['plugin'] . '.' : '') . $options['type'];
+			}
+			list($plugin, $element) = pluginSplit($options['element']);
+			if ($plugin) {
+				$message['element'] = $plugin . '.Flash/' . $element;
+			} else {
+				$message['element'] = 'Flash/' . $element;
+			}
+		} else {
+			// Simplify?
+			if (!$message['escape'] && !$message['params']) {
+				$message = $message['message'];
+			}
+		}
+		return $message;
+	}
+
+	/**
 	 * Adds a transient flash message.
 	 * Adds a transient flash message.
 	 * These flash messages that are not saved (only available for current view),
 	 * These flash messages that are not saved (only available for current view),
 	 * will be merged into the session flash ones prior to output.
 	 * will be merged into the session flash ones prior to output.
 	 *
 	 *
+	 * Since this method can be accessed statically, it only works with Configure configuration,
+	 * not with runtime config as the normal message() method.
+	 *
+	 * ### Options:
+	 *
+	 * - `element` The element used to render the flash message. Default to 'default'.
+	 * - `params` An array of variables to make available when using an element.
+	 * - `escape` If content should be escaped or not in the element itself or if elements are not used in the component.
+	 *
 	 * @param string $message Message to output.
 	 * @param string $message Message to output.
-	 * @param string $type Type ('error', 'warning', 'success', 'info' or custom class).
+	 * @param array|string $options Options or Type ('error', 'warning', 'success', 'info' or custom class).
 	 * @return void
 	 * @return void
 	 */
 	 */
-	public static function transientMessage($message, $type = null) {
-		if (!$type) {
-			$type = 'info';
+	public static function transientMessage($message, $options = array()) {
+		$defaults = (array)Configure::read('Flash') + array(
+			'type' => 'info',
+			'escape' => false,
+			'params' => array(),
+			'element' => 'Tools.default',
+			'typeToElement' => false,
+			'useElements' => false,
+			'plugin' => null,
+		);
+		if (!is_array($options)) {
+			$type = $options ?: $defaults['type'];
+			$options = array('type' => $type);
+		}
+		$options += $defaults;
+
+		$message = array(
+			'message' => $message, // $options['escape'] ? h($message) : $message,
+			'params' => $options['params'],
+			'escape' => $options['escape']
+		);
+
+		if ($options['useElements']) {
+			if ($options['typeToElement'] && $options['element'] === $defaults['element']) {
+				$options['element'] = ($options['plugin'] ? $options['plugin'] . '.' : '') . $options['type'];
+			}
+			list($plugin, $element) = pluginSplit($options['element']);
+			if ($plugin) {
+				$message['element'] = $plugin . '.Flash/' . $element;
+			} else {
+				$message['element'] = 'Flash/' . $element;
+			}
+		} else {
+			// Simplify?
+			if (!$message['escape'] && !$message['params']) {
+				$message = $message['message'];
+			}
 		}
 		}
 
 
 		$old = (array)Configure::read('messages');
 		$old = (array)Configure::read('messages');
+		$type = $options['type'];
 		if (isset($old[$type]) && count($old[$type]) > 99) {
 		if (isset($old[$type]) && count($old[$type]) > 99) {
 			array_shift($old[$type]);
 			array_shift($old[$type]);
 		}
 		}
@@ -111,7 +233,12 @@ class FlashComponent extends Component {
 			throw new InternalErrorException('Flash message missing.');
 			throw new InternalErrorException('Flash message missing.');
 		}
 		}
 
 
-		$this->message($args[0], $name);
+		$options = ['type' => Inflector::underscore($name)];
+		if (!empty($args[1])) {
+			$options += (array)$args[1];
+		}
+
+		$this->message($args[0], $options);
 	}
 	}
 
 
 }
 }

+ 25 - 0
Test/Case/Controller/Component/FlashComponentTest.php

@@ -59,6 +59,31 @@ class FlashComponentTest extends CakeTestCase {
 		$this->assertTrue(isset($res['info'][0]) && $res['info'][0] === 'efg');
 		$this->assertTrue(isset($res['info'][0]) && $res['info'][0] === 'efg');
 	}
 	}
 
 
+	/**
+	 * FlashComponentTest::testFlashMessage()
+	 *
+	 * @return void
+	 */
+	public function testFlashMessageComplex() {
+		$this->Controller->Flash->settings['useElements'] = true;
+		$this->Controller->Flash->message('efg', array('escape' => true, 'element' => 'PluginName.Baz', 'params' => array('foo' => 'bar')));
+
+		$res = $this->Controller->Session->read('messages');
+		$this->assertTrue(!empty($res));
+		$expected = array(
+			'message' => 'efg',
+			'params' => array('foo' => 'bar'),
+			'escape' => true,
+			'element' => 'PluginName.Flash/Baz'
+		);
+		$this->assertSame($expected, $res['info'][0]);
+	}
+
+	/**
+	 * FlashComponentTest::testMagicMessage()
+	 *
+	 * @return void
+	 */
 	public function testMagicMessage() {
 	public function testMagicMessage() {
 		$this->Controller->Flash->success('s');
 		$this->Controller->Flash->success('s');
 		$this->Controller->Flash->error('e');
 		$this->Controller->Flash->error('e');

+ 68 - 4
Test/Case/View/Helper/FlashHelperTest.php

@@ -17,8 +17,8 @@ class FlashHelperTest extends MyCakeTestCase {
 		parent::setUp();
 		parent::setUp();
 
 
 		Router::reload();
 		Router::reload();
-		$View = new View(null);
-		$this->Flash = new FlashHelper($View);
+		$this->View = new View();
+		$this->Flash = new FlashHelper($this->View);
 	}
 	}
 
 
 	/**
 	/**
@@ -28,7 +28,7 @@ class FlashHelperTest extends MyCakeTestCase {
 	 */
 	 */
 	public function testMessage() {
 	public function testMessage() {
 		$result = $this->Flash->message(h('Foo & bar'), 'success');
 		$result = $this->Flash->message(h('Foo & bar'), 'success');
-		$expected = '<div class="flash-messages flashMessages"><div class="message success">Foo &amp;amp; bar</div></div>';
+		$expected = '<div class="flash-messages flashMessages"><div class="message success">Foo &amp; bar</div></div>';
 		$this->assertEquals($expected, $result);
 		$this->assertEquals($expected, $result);
 	}
 	}
 
 
@@ -61,12 +61,76 @@ class FlashHelperTest extends MyCakeTestCase {
 	}
 	}
 
 
 	/**
 	/**
+	 * FlashComponentTest::testFlashMessage()
+	 *
+	 * @return void
+	 */
+	public function testFlashComplex() {
+		$this->Flash->settings['useElements'] = true;
+
+		FlashComponent::transientMessage('efg', array('type' => 'success', 'escape' => true,
+			'useElements' => true, 'params' => array('foo' => 'bar')));
+
+		$res = $this->Flash->flash();
+		$this->assertTrue(!empty($res));
+		$expected = '<div class="flash-messages flashMessages"><div class="message success">efg</div>
+</div>';
+		$this->assertSame($expected, $res);
+	}
+
+	/**
+	 * FlashComponentTest::testFlashMessage()
+	 *
+	 * @return void
+	 */
+	public function testFlashHtml() {
+		$this->Flash->settings['useElements'] = true;
+
+		FlashComponent::transientMessage('<b>OK<b>', array('type' => 'success', 'escape' => false,
+			'useElements' => true, 'params' => array('foo' => 'bar')));
+		FlashComponent::transientMessage('<b>Error<b>', array('type' => 'error', 'escape' => true,
+			'useElements' => true, 'params' => array('foo' => 'bar')));
+
+		$res = $this->Flash->flash();
+		$this->assertTrue(!empty($res));
+		$expected = '<div class="flash-messages flashMessages"><div class="message success"><b>OK<b></div>
+<div class="message error">&lt;b&gt;Error&lt;b&gt;</div>
+</div>';
+		$this->assertTextEquals($expected, $res);
+	}
+
+	/**
+	 * FlashComponentTest::testFlashMessage()
+	 *
+	 * @return void
+	 */
+	public function testFlashComplexMocked() {
+		$this->View = $this->getMock('View', ['element']);
+		$this->Flash = new FlashHelper($this->View);
+
+		$this->Flash->settings['useElements'] = true;
+
+		FlashComponent::transientMessage('efg', array('type' => 'success', 'escape' => true,
+			'useElements' => true, 'element' => 'PluginName.default_element', 'params' => array('foo' => 'bar')));
+
+		$this->View->expects($this->once())
+			->method('element')
+			->with('PluginName.Flash/default_element')
+			->will($this->returnValue('xyz'));
+
+		$res = $this->Flash->flash();
+		$this->assertTrue(!empty($res));
+		$expected = '<div class="flash-messages flashMessages">xyz</div>';
+		$this->assertSame($expected, $res);
+	}
+
+	/**
 	 * Test that you can define your own order or just output a subpart of
 	 * Test that you can define your own order or just output a subpart of
 	 * the types.
 	 * the types.
 	 *
 	 *
 	 * @return void
 	 * @return void
 	 */
 	 */
-	public function testFlashWithTypes() {
+	public function testFlashWithStringTypes() {
 		$this->Flash->addTransientMessage('I am an error', 'error');
 		$this->Flash->addTransientMessage('I am an error', 'error');
 		$this->Flash->addTransientMessage('I am a warning', 'warning');
 		$this->Flash->addTransientMessage('I am a warning', 'warning');
 		$this->Flash->addTransientMessage('I am some info', 'info');
 		$this->Flash->addTransientMessage('I am some info', 'info');

+ 15 - 0
View/Elements/Flash/default.ctp

@@ -0,0 +1,15 @@
+<?php
+$class = 'message';
+if (!empty($params['class'])) {
+    $class .= ' ' . $params['class'];
+}
+if (!empty($type)) {
+    $class .= ' ' . $type;
+}
+
+if (!isset($escape) || $escape) {
+	$message = h($message);
+}
+
+?>
+<div class="<?php echo h($class) ?>"><?php echo $message ?></div>

+ 40 - 15
View/Helper/FlashHelper.php

@@ -5,13 +5,25 @@ App::uses('Hash', 'Utility');
 
 
 /**
 /**
  * Flash helper
  * Flash helper
+ *
+ * Partial backport from the 3.x one to ease migration.
  */
  */
 class FlashHelper extends AppHelper {
 class FlashHelper extends AppHelper {
 
 
 	public $helpers = array('Session');
 	public $helpers = array('Session');
 
 
+	protected $_defaultConfig = array(
+		'useElements' => false, //Set to true to use 3.x flash message rendering via Elements
+	);
+
+	public function __construct(View $View, $settings = array()) {
+		$defaults = (array)Configure::read('Flash') + $this->_defaultConfig;
+		$settings += $defaults;
+		parent::__construct($View, $settings);
+	}
+
 	/**
 	/**
-	 * Display all flash messages.
+	 * Displays all flash messages.
 	 *
 	 *
 	 * TODO: export div wrapping method (for static messaging on a page)
 	 * TODO: export div wrapping method (for static messaging on a page)
 	 *
 	 *
@@ -67,16 +79,20 @@ class FlashHelper extends AppHelper {
 	 * Outputs a single flash message directly.
 	 * Outputs a single flash message directly.
 	 * Note that this does not use the Session.
 	 * Note that this does not use the Session.
 	 *
 	 *
-	 * @param string $message String to output.
+	 * $escape is deprecated as it is already part of the message
+	 *
+	 * @param array|string $message String to output.
 	 * @param string $type Type (success, warning, error, info)
 	 * @param string $type Type (success, warning, error, info)
-	 * @param bool $escape Set to false to disable escaping.
+	 * @param bool|null $escape Set to false to disable escaping.
 	 * @return string HTML
 	 * @return string HTML
 	 */
 	 */
-	public function message($msg, $type = 'info', $escape = true) {
-		$html = '<div class="flash-messages flashMessages">';
-		if ($escape) {
-			$msg = h($msg);
+	public function message($msg, $type = 'info', $escape = null) {
+		if ($escape === null && is_array($msg) && !isset($msg['escape'])) {
+			$msg['escape'] = true;
 		}
 		}
+		$escape = is_array($msg) && isset($msg['escape']) ? $msg['escape'] : true;
+
+		$html = '<div class="flash-messages flashMessages">';
 		$html .= $this->_message($msg, $type);
 		$html .= $this->_message($msg, $type);
 		$html .= '</div>';
 		$html .= '</div>';
 		return $html;
 		return $html;
@@ -90,21 +106,30 @@ class FlashHelper extends AppHelper {
 	 * @return string
 	 * @return string
 	 */
 	 */
 	protected function _message($msg, $type) {
 	protected function _message($msg, $type) {
-		if (!empty($msg)) {
-			return '<div class="message' . (!empty($type) ? ' ' . $type : '') . '">' . $msg . '</div>';
+		if (!is_array($msg)) {
+			if (!empty($msg)) {
+				return '<div class="message' . (!empty($type) ? ' ' . $type : '') . '">' . $msg . '</div>';
+			}
+			return '';
 		}
 		}
-		return '';
+		$msg['type'] = $type;
+		return $this->_View->element($msg['element'], $msg);
 	}
 	}
 
 
 	/**
 	/**
-	 * Add a message on the fly
+	 * Adds a message on the fly.
+	 *
+	 * Only works with static Configure configuration.
+	 *
+	 * This method might not be in 3.x branch anymore, since the overhead of maintaining
+	 * this static method is not worth it. Try switching to addMessage instead().
 	 *
 	 *
 	 * @param string $msg
 	 * @param string $msg
 	 * @param string $class
 	 * @param string $class
 	 * @return void
 	 * @return void
 	 */
 	 */
-	public function addTransientMessage($msg, $class = null) {
-		FlashComponent::transientMessage($msg, $class);
+	public function addTransientMessage($msg, $options = array()) {
+		FlashComponent::transientMessage($msg, $options);
 	}
 	}
 
 
 	/**
 	/**
@@ -115,8 +140,8 @@ class FlashHelper extends AppHelper {
 	 * @return void
 	 * @return void
 	 * @deprecated Use addFlashMessage() instead
 	 * @deprecated Use addFlashMessage() instead
 	 */
 	 */
-	public function transientFlashMessage($msg, $class = null) {
-		$this->addFlashMessage($msg, $class);
+	public function transientFlashMessage($msg, $options = array()) {
+		$this->addFlashMessage($msg, $options);
 	}
 	}
 
 
 }
 }