diff --git a/src/Controller/Controller.php b/src/Controller/Controller.php index 006645dea71..ff22682435c 100644 --- a/src/Controller/Controller.php +++ b/src/Controller/Controller.php @@ -27,6 +27,7 @@ use Cake\ORM\TableRegistry; use Cake\Routing\RequestActionTrait; use Cake\Routing\Router; +use Cake\Utility\CellTrait; use Cake\Utility\Inflector; use Cake\Utility\MergeVariablesTrait; use Cake\Utility\ModelAwareTrait; @@ -78,6 +79,7 @@ */ class Controller extends Object implements EventListener { + use CellTrait; use MergeVariablesTrait; use ModelAwareTrait; use RequestActionTrait; @@ -723,23 +725,4 @@ public function beforeRedirect(Event $event, $url, $status = null, $exit = true) public function afterFilter(Event $event) { } -/** - * Constructs the view class instance based on controller properties. - * Controller stashes view variables and view configuration options in the absence of an - * instantiated view to set them on, and passes them to the View constructor here. - * - * @param string $viewClass Optional namespaced class name of the View class to instantiate. - * @return View - */ - public function createView($viewClass = null) { - if ($viewClass === null) { - $viewClass = $this->viewClass; - if ($this->viewClass !== 'View') { - list($plugin, $viewClass) = pluginSplit($viewClass, true); - $viewClass = App::classname($viewClass, 'View', 'View'); - } - } - $viewOptions = array_intersect_key(get_object_vars($this), array_flip($this->_validViewOptions)); - return new $viewClass($this->request, $this->response, $this->getEventManager(), $viewOptions); - } } diff --git a/src/Utility/CellTrait.php b/src/Utility/CellTrait.php new file mode 100644 index 00000000000..2fb7c8c756e --- /dev/null +++ b/src/Utility/CellTrait.php @@ -0,0 +1,84 @@ +cell('Taxonomy.TagCloud::smallList', ['limit' => 10]); + * + * // App\View\Cell\TagCloudCell::smallList() + * $cell = $this->cell('TagCloud::smallList', ['limit' => 10]); + * + * The `display` action will be used by default when no action is provided: + * + * // Taxonomy\View\Cell\TagCloudCell::display() + * $cell = $this->cell('Taxonomy.TagCloud'); + * + * Cells are not rendered until they are echoed. + * + * @param string $cell You must indicate both cell name, and optionally a cell action. e.g.: `TagCloud::smallList` + * will invoke `View\Cell\TagCloudCell::smallList()`, `display` action will be invoked by default when none is provided. + * @param array $data Additional arguments for cell method. e.g.: + * `cell('TagCloud::smallList', ['a1' => 'v1', 'a2' => 'v2'])` maps to `View\Cell\TagCloud::smallList(v1, v2)` + * @param array $options Options for Cell's constructor + * @return \Cake\View\Cell The cell instance + * @throws \Cake\View\Error\MissingCellException If Cell class was not found + */ + public function cell($cell, $data = [], $options = []) { + $parts = explode('::', $cell); + + if (count($parts) == 2) { + list($pluginAndCell, $action) = [$parts[0], $parts[1]]; + } else { + list($pluginAndCell, $action) = [$parts[0], 'display']; + } + + list($plugin, $cellName) = pluginSplit($pluginAndCell); + + $className = App::classname($pluginAndCell, 'View/Cell', 'Cell'); + + if (!$className) { + throw new Error\MissingCellException(array('className' => $pluginAndCell . 'Cell')); + } + + $cellInstance = new $className($this->request, $this->response, $this->getEventManager(), $options); + $cellInstance->action = Inflector::underscore($action); + $cellInstance->plugin = !empty($plugin) ? $plugin : null; + $length = count($data); + + if ($length) { + $data = array_values($data); + } + + call_user_func_array([$cellInstance, $action], $data); + + return $cellInstance; + } + +} diff --git a/src/Utility/ViewVarsTrait.php b/src/Utility/ViewVarsTrait.php index 4aa35faeadc..c5a2d841673 100644 --- a/src/Utility/ViewVarsTrait.php +++ b/src/Utility/ViewVarsTrait.php @@ -13,6 +13,8 @@ */ namespace Cake\Utility; +use Cake\Core\App; + /** * Provides the set() method for collecting template context. * @@ -29,6 +31,24 @@ trait ViewVarsTrait { */ public $viewVars = []; +/** + * Constructs the view class instance based on object properties. + * + * @param string $viewClass Optional namespaced class name of the View class to instantiate. + * @return View + */ + public function createView($viewClass = null) { + if ($viewClass === null) { + $viewClass = $this->viewClass; + if ($this->viewClass !== 'View') { + list($plugin, $viewClass) = pluginSplit($viewClass, true); + $viewClass = App::classname($viewClass, 'View', 'View'); + } + } + $viewOptions = array_intersect_key(get_object_vars($this), array_flip($this->_validViewOptions)); + return new $viewClass($this->request, $this->response, $this->getEventManager(), $viewOptions); + } + /** * Saves a variable for use inside a template. * @@ -49,4 +69,5 @@ public function set($name, $val = null) { } $this->viewVars = $data + $this->viewVars; } + } diff --git a/src/View/Cell.php b/src/View/Cell.php new file mode 100644 index 00000000000..968c79691a8 --- /dev/null +++ b/src/View/Cell.php @@ -0,0 +1,191 @@ +_eventManager = $eventManager; + $this->request = $request; + $this->response = $response; + $this->modelFactory('Table', ['Cake\ORM\TableRegistry', 'get']); + + foreach ($this->_validCellOptions as $var) { + if (isset($cellOptions[$var])) { + $this->{$var} = $cellOptions[$var]; + } + } + } + +/** + * Rendering method. + * + * @param string $action Custom template name to render. If not provided (null), the last + * value will be used. This value is automatically set by `CellTrait::cell()`. + * @return void + */ + public function render($action = null) { + if ($action !== null) { + $this->action = $action; + } + + return $this->__toString(); + } + +/** + * Magic method. + * + * Starts the rendering process when Cell is echoed. + * + * @return string Rendered cell + */ + public function __toString() { + $this->View = $this->createView(); + + $this->View->layout = false; + $className = explode('\\', get_class($this)); + $className = array_pop($className); + $this->View->subDir = 'Cell' . DS . substr($className, 0, strpos($className, 'Cell')); + + return $this->View->render(Inflector::underscore($this->action)); + } + +/** + * Debug info. + * + * @return void + */ + public function __debugInfo() { + return [ + 'plugin' => $this->plugin, + 'action' => $this->action, + 'viewClass' => $this->viewClass, + 'request' => $this->request, + 'response' => $this->response, + ]; + } + +/** + * Returns the Cake\Event\EventManager manager instance for this cell. + * + * You can use this instance to register any new listeners or callbacks to the + * cell events, or create your own events and trigger them at will. + * + * @return \Cake\Event\EventManager + */ + public function getEventManager() { + if (empty($this->_eventManager)) { + $this->_eventManager = new EventManager(); + } + return $this->_eventManager; + } + +} diff --git a/src/View/Error/MissingCellException.php b/src/View/Error/MissingCellException.php new file mode 100644 index 00000000000..a8d9133bf0a --- /dev/null +++ b/src/View/Error/MissingCellException.php @@ -0,0 +1,27 @@ + + * Copyright (c) Cake Software Foundation, Inc. (http://cakefoundation.org) + * + * Licensed under The MIT License + * For full copyright and license information, please see the LICENSE.txt + * Redistributions of files must retain the above copyright notice + * + * @copyright Copyright (c) Cake Software Foundation, Inc. (http://cakefoundation.org) + * @link http://book.cakephp.org/2.0/en/development/testing.html CakePHP(tm) Tests + * @since 3.0.0 + * @license http://www.opensource.org/licenses/mit-license.php MIT License + */ +namespace Cake\Test\TestCase\View; + +use Cake\Controller\Controller; +use Cake\Core\Configure; +use Cake\Core\Plugin; +use Cake\Event\EventManager; +use Cake\TestSuite\TestCase; +use Cake\Utility\CellTrait; +use Cake\View\Cell; + +/** + * CellTest class. + * + * For testing both View\Cell & Utility\CellTrait + */ +class CellTest extends TestCase { + +/** + * setUp method + * + * @return void + */ + public function setUp() { + parent::setUp(); + Configure::write('App.namespace', 'TestApp'); + Configure::write('debug', 2); + Plugin::load('TestPlugin'); + $request = $this->getMock('Cake\Network\Request'); + $response = $this->getMock('Cake\Network\Response'); + $this->View = new \Cake\View\View($request, $response); + } + +/** + * tearDown method + * + * @return void + */ + public function tearDown() { + parent::tearDown(); + Plugin::unload('TestPlugin'); + unset($this->View); + } + +/** + * Tests basic cell rendering. + * + * @return void + */ + public function testCellRender() { + $cell = $this->View->cell('Articles::teaserList'); + $render = "{$cell}"; + + $this->assertTrue( + strpos($render, '

Lorem ipsum

') !== false && + strpos($render, '

Usectetur adipiscing eli

') !== false && + strpos($render, '

Topis semper blandit eu non

') !== false && + strpos($render, '

Suspendisse gravida neque

') !== false + ); + } + +/** + * Tests that we are able pass multiple arguments to cell methods. + * + * @return void + */ + public function testCellWithArguments() { + $cell = $this->View->cell('Articles::doEcho', ['msg1' => 'dummy', 'msg2' => ' message']); + $render = "{$cell}"; + $this->assertTrue(strpos($render, 'dummy message') !== false); + } + +/** + * Tests that cell runs default action when none is provided. + * + * @return void + */ + public function testDefaultCellAction() { + $appCell = $this->View->cell('Articles'); + $this->assertTrue(strpos("{$appCell}", 'dummy') !== false); + + $pluginCell = $this->View->cell('TestPlugin.Dummy'); + $this->assertTrue(strpos("{$pluginCell}", 'dummy') !== false); + } + +/** + * Tests manual render() invocation. + * + * @return void + */ + public function testCellManualRender() { + $cell = $this->View->cell('Articles::doEcho', ['msg1' => 'dummy', 'msg2' => ' message']); + $this->assertTrue(strpos($cell->render(), 'dummy message') !== false); + + $cell->teaserList(); + $this->assertTrue(strpos($cell->render('teaser_list'), '

Lorem ipsum

') !== false); + } + +/** + * Tests that using plugin's cells works. + * + * @return void + */ + public function testPluginCell() { + $cell = $this->View->cell('TestPlugin.Dummy::echoThis', ['msg' => 'hello world!']); + $this->assertTrue(strpos("{$cell}", 'hello world!') !== false); + } + +/** + * Tests that using an unexisting cell throws an exception. + * + * @expectedException \Cake\View\Error\MissingCellException + * @return void + */ + public function testUnexistingCell() { + $cell = $this->View->cell('TestPlugin.Void::echoThis', ['arg1' => 'v1']); + $cell = $this->View->cell('Void::echoThis', ['arg1' => 'v1', 'arg2' => 'v2']); + } + +} diff --git a/tests/test_app/Plugin/TestPlugin/Template/Cell/Dummy/display.ctp b/tests/test_app/Plugin/TestPlugin/Template/Cell/Dummy/display.ctp new file mode 100644 index 00000000000..421376db9e8 --- /dev/null +++ b/tests/test_app/Plugin/TestPlugin/Template/Cell/Dummy/display.ctp @@ -0,0 +1 @@ +dummy diff --git a/tests/test_app/Plugin/TestPlugin/Template/Cell/Dummy/echo_this.ctp b/tests/test_app/Plugin/TestPlugin/Template/Cell/Dummy/echo_this.ctp new file mode 100644 index 00000000000..da3a59c9b5e --- /dev/null +++ b/tests/test_app/Plugin/TestPlugin/Template/Cell/Dummy/echo_this.ctp @@ -0,0 +1 @@ + diff --git a/tests/test_app/Plugin/TestPlugin/View/Cell/DummyCell.php b/tests/test_app/Plugin/TestPlugin/View/Cell/DummyCell.php new file mode 100644 index 00000000000..01c83fd218e --- /dev/null +++ b/tests/test_app/Plugin/TestPlugin/View/Cell/DummyCell.php @@ -0,0 +1,40 @@ +set('msg', $msg); + } + +} diff --git a/tests/test_app/TestApp/Template/Cell/Articles/display.ctp b/tests/test_app/TestApp/Template/Cell/Articles/display.ctp new file mode 100644 index 00000000000..421376db9e8 --- /dev/null +++ b/tests/test_app/TestApp/Template/Cell/Articles/display.ctp @@ -0,0 +1 @@ +dummy diff --git a/tests/test_app/TestApp/Template/Cell/Articles/do_echo.ctp b/tests/test_app/TestApp/Template/Cell/Articles/do_echo.ctp new file mode 100644 index 00000000000..da3a59c9b5e --- /dev/null +++ b/tests/test_app/TestApp/Template/Cell/Articles/do_echo.ctp @@ -0,0 +1 @@ + diff --git a/tests/test_app/TestApp/Template/Cell/Articles/teaser_list.ctp b/tests/test_app/TestApp/Template/Cell/Articles/teaser_list.ctp new file mode 100644 index 00000000000..ea2d902be67 --- /dev/null +++ b/tests/test_app/TestApp/Template/Cell/Articles/teaser_list.ctp @@ -0,0 +1,4 @@ + +

+

+ diff --git a/tests/test_app/TestApp/View/Cell/ArticlesCell.php b/tests/test_app/TestApp/View/Cell/ArticlesCell.php new file mode 100644 index 00000000000..cc90e32ad35 --- /dev/null +++ b/tests/test_app/TestApp/View/Cell/ArticlesCell.php @@ -0,0 +1,55 @@ +set('articles', [ + ['title' => 'Lorem ipsum', 'body' => 'dolorem sit amet'], + ['title' => 'Usectetur adipiscing eli', 'body' => 'tortor, in tincidunt sem dictum vel'], + ['title' => 'Topis semper blandit eu non', 'body' => 'alvinar diam convallis non. Nullam pu'], + ['title' => 'Suspendisse gravida neque', 'body' => 'pellentesque sed scelerisque libero'], + ]); + } + +/** + * Simple echo. + * + * @param string $msg1 + * @param string $msg2 + * @return void + */ + public function doEcho($msg1, $msg2) { + $this->set('msg', $msg1 . $msg2); + } + +}