├── .gitignore ├── .stickler.yml ├── .travis.yml ├── LICENSE ├── README.md ├── composer.json ├── config └── bootstrap.php ├── phpstan.neon ├── phpunit.xml.dist ├── src ├── Controller │ └── Action.php ├── Event │ └── DispatcherListener.php ├── Http │ ├── ActionFactory.php │ └── Exception │ │ └── MissingActionClassException.php ├── Shell │ └── Task │ │ └── ActionTask.php └── Template │ └── Bake │ ├── action.ctp │ └── test.ctp └── tests ├── PHPStan ├── BakeTemplatePropertyReflection.php └── ShellPropertiesClassReflectionExtension.php ├── TestCase ├── Controller │ └── ActionTest.php ├── Event │ └── DispatcherListenerTest.php ├── Http │ └── ActionFactoryTest.php └── Shell │ └── Task │ └── ActionTaskTest.php ├── bootstrap.php ├── comparisons └── Controller │ ├── Admin │ └── Posts │ │ └── IndexAction.php │ ├── Plugin │ ├── Admin │ │ └── Posts │ │ │ └── IndexAction.php │ └── Posts │ │ └── IndexAction.php │ └── Posts │ └── IndexAction.php └── test_app ├── Plugin ├── Company │ └── TestPluginThree │ │ └── src │ │ └── Controller │ │ └── Ovens │ │ └── IndexAction.php └── TestPlugin │ └── src │ └── Controller │ ├── Admin │ └── Comments │ │ └── IndexAction.php │ └── TestPlugin │ └── IndexAction.php └── TestApp └── Controller ├── Admin ├── Posts │ └── IndexAction.php └── Sub │ └── Posts │ └── IndexAction.php ├── Cakes ├── AddAction.php └── IndexAction.php └── Invalid ├── AbstractAction.php └── InterfaceAction.php /.gitignore: -------------------------------------------------------------------------------- 1 | # User specific & automatically generated files # 2 | ################################################# 3 | /build 4 | /dist 5 | /tags 6 | /composer.lock 7 | /phpunit.xml 8 | /vendor 9 | *.mo 10 | 11 | # IDE and editor specific files # 12 | ################################# 13 | /nbproject 14 | .idea 15 | 16 | # OS generated files # 17 | ###################### 18 | .DS_Store 19 | .DS_Store? 20 | ._* 21 | .Spotlight-V100 22 | .Trashes 23 | Icon? 24 | ehthumbs.db 25 | Thumbs.db -------------------------------------------------------------------------------- /.stickler.yml: -------------------------------------------------------------------------------- 1 | linters: 2 | phpcs: 3 | standard: PSR2 4 | extensions: '.php' -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: php 2 | 3 | php: 4 | - 7.0 5 | - 7.1 6 | - nightly 7 | 8 | sudo: false 9 | 10 | env: 11 | global: 12 | - DEFAULT=1 13 | 14 | matrix: 15 | allow_failures: 16 | - env: CODECOVERAGE=1 DEFAULT=0 17 | - php: nightly 18 | 19 | fast_finish: true 20 | 21 | include: 22 | - php: 7.0 23 | env: DEFAULT=1 24 | 25 | - php: 7.1 26 | env: DEFAULT=1 27 | 28 | - php: 7.1 29 | env: CODECOVERAGE=1 DEFAULT=0 30 | 31 | - php: 7.1 32 | env: PHPSTAN=1 DEFAULT=0 33 | 34 | before_script: 35 | - composer self-update 36 | - composer install --prefer-dist --no-interaction 37 | 38 | - phpenv rehash 39 | - set +H 40 | 41 | script: 42 | - sh -c "if [ '$DEFAULT' = '1' ]; then vendor/bin/phpunit --stderr; fi" 43 | - sh -c "if [ '$PHPSTAN' = '1' ]; then composer require --dev phpstan/phpstan:^0.8 && vendor/bin/phpstan analyse -c phpstan.neon -l 5 src; fi" 44 | - sh -c "if [ '$CODECOVERAGE' = '1' ]; then vendor/bin/phpunit --coverage-clover=clover.xml || true; fi" 45 | 46 | after_success: 47 | - sh -c "if [ '$CODECOVERAGE' = '1' ]; then curl -s https://codecov.io/bash | bash || true; fi" 48 | 49 | notifications: 50 | email: false -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | CakePHP 3.X Actions class plugin. 4 | Copyright (c) 2017, Yves Piquel. (http://www.havokinspiration.fr) 5 | 6 | Permission is hereby granted, free of charge, to any person obtaining a copy 7 | of this software and associated documentation files (the "Software"), to deal 8 | in the Software without restriction, including without limitation the rights 9 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 10 | copies of the Software, and to permit persons to whom the Software is 11 | furnished to do so, subject to the following conditions: 12 | 13 | The above copyright notice and this permission notice shall be included in all 14 | copies or substantial portions of the Software. 15 | 16 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 17 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 18 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 19 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 20 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 21 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 22 | SOFTWARE. -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # CakePHP ActionsClass plugin 2 | 3 | [![Software License](https://img.shields.io/badge/license-MIT-brightgreen.svg?style=flat-square)](LICENSE.txt) 4 | [![Build Status](https://travis-ci.org/HavokInspiration/cakephp-actions-class.svg?branch=master)](https://travis-ci.org/HavokInspiration/cakephp-actions-class) 5 | [![codecov.io](https://codecov.io/github/HavokInspiration/cakephp-actions-class/coverage.svg?branch=master)](https://codecov.io/github/HavokInspiration/cakephp-actions-class?branch=master) 6 | 7 | This plugin gives you the ability to manage your CakePHP Controller actions as single classes. Each action of your Controllers will be managed in its own object. 8 | 9 | ## Requirements 10 | 11 | - PHP >= 7.0.0 12 | - CakePHP 3.4.X 13 | 14 | ## Installation 15 | 16 | You can install this plugin into your CakePHP application using [Composer](https://getcomposer.org). 17 | 18 | The recommended way to install it is: 19 | 20 | ``` 21 | composer require havokinspiration/cakephp-actions-class 22 | ``` 23 | 24 | ## Loading the plugin 25 | 26 | It is recommanded to load this plugin using the `bootstrap()` method of your application's `Application` class. This is needed if you want to be able to use [Integration testing](https://book.cakephp.org/3.0/en/development/testing.html#controller-integration-testing): 27 | 28 | ```php 29 | // in src/Application.php 30 | namespace App; 31 | 32 | use Cake\Core\Plugin; 33 | use Cake\Http\BaseApplication; 34 | 35 | class Application extends BaseApplication 36 | { 37 | public function bootstrap() 38 | { 39 | parent::bootstrap(); 40 | 41 | Plugin::load('HavokInspiration/ActionsClass', ['bootstrap' => true]); 42 | } 43 | } 44 | ``` 45 | 46 | **Loading the plugin bootstrap file is mandatory.** 47 | 48 | ## Usage 49 | 50 | By default, CakePHP controller management is based around one controller (which represents one part of your application, for instance "Posts") which is a single class containing one public method per actions needed to be exposed in your application: 51 | 52 | ```php 53 | // in src/Controller/PostsController.php 54 | namespace App\Controller; 55 | 56 | use Cake\Controller; 57 | 58 | class PostsController extends Controller 59 | { 60 | 61 | public function index() {} 62 | public function add() {} 63 | public function edit($id) {} 64 | } 65 | ``` 66 | 67 | As your application grows, your controllers grow as well. In the end, on large applications, you can end up with big controller files with lots of content, making it hard to read through. You might even be in the case where you need to have methods specific to some actions in the middle of other methods dedicated to other actions. 68 | 69 | This is where the **cakephp-actions-class** plugin is useful. When enabled, **you can have your controllers actions as single classes**. 70 | 71 | So the `PostsController` example given above would become: 72 | 73 | ```php 74 | // in src/Controller/Posts/IndexAction.php 75 | namespace App\Controller\Posts; 76 | 77 | use HavokInspiration\ActionsClass\Controller\Action; 78 | 79 | class IndexAction extends Action 80 | { 81 | public function execute() {} 82 | } 83 | ``` 84 | 85 | ```php 86 | // in src/Controller/Posts/AddAction.php 87 | namespace App\Controller\Posts; 88 | 89 | use HavokInspiration\ActionsClass\Controller\Action; 90 | 91 | class AddAction extends Action 92 | { 93 | public function execute() {} 94 | } 95 | ``` 96 | 97 | ```php 98 | // in src/Controller/Posts/EditAction.php 99 | namespace App\Controller\Posts; 100 | 101 | use HavokInspiration\ActionsClass\Controller\Action; 102 | 103 | class EditAction extends Action 104 | { 105 | public function execute($id) {} 106 | } 107 | ``` 108 | 109 | Living in the following directory structure : 110 | 111 | ``` 112 | src/ 113 | Controller/ 114 | Posts/ 115 | AddAction.php 116 | EditAction.php 117 | IndexAction.php 118 | ``` 119 | 120 | Your `Action` classes are only expected to hold an `execute()` method. It can receive passed parameters as in regular controller actions (meaning the URL `/posts/edit/5` will pass `5` to the argument `$id` in the `execute()` method of the `EditAction` class in our previous example). 121 | 122 | #### Using the `bake` command-line to create Action classes 123 | 124 | You first need to load the plugin in your **config/bootstrap_cli.php** : 125 | 126 | ```php 127 | Plugin::load('HavokInspiration/ActionsClass'); 128 | ``` 129 | 130 | You can then create an Action class file with the following command : 131 | 132 | ``` 133 | bin/cake bake action Posts/Index 134 | ``` 135 | 136 | The command expects the name to get the controller name and the action name separated by a forward slash. For instance, the above example would create a `IndexAction` file for the `Posts` controller. 137 | 138 | You can also specify the routing prefix your controller action lives under by using the `--prefix` option : 139 | 140 | ``` 141 | bin/cake bake action Posts/Index --prefix Admin 142 | ``` 143 | 144 | If you want to create an action file for a plugin, you can use the `--plugin` option : 145 | 146 | ``` 147 | bin/cake bake action Posts/Index --plugin MyPlugin 148 | ``` 149 | 150 | You can of course use both together : 151 | 152 | ``` 153 | bin/cake bake action Posts/Index --plugin MyPlugin --prefix Admin 154 | ``` 155 | 156 | By default, baking an action class will generate the corresponding test file. You can skip the test file generation by using the `--no-test` boolean flag : 157 | 158 | ``` 159 | bin/cake bake action Posts/Index --no-test 160 | ``` 161 | 162 | ## Compatibility 163 | 164 | This plugin was designed to have a maximum compatibility with the regular CakePHP behavior. 165 | 166 | ### Fallback to CakePHP regular behavior 167 | 168 | If you wish to use this plugin in an existing application, it will first try to provide a response using an `Action` class. If an action class matching the routing parameters can not be found, it will let CakePHP fallback to its regular behavior (meaning looking for a `Controller` class). 169 | 170 | This also means that you can develop a plugin with controllers implementing this behavior without breaking the base application (since, when the **cakephp-actions-class** plugin is loaded, the Dispatcher will first try to load an `Action` class and fallback to the regular `Controller` dispatching behavior if it can not find a proper `Action` class to load). 171 | 172 | ### Everything you do in Controller can be done in an Action class 173 | 174 | Under the hood, `Action` classes are instances of `\Cake\Controller\Controller`, meaning that **everything you do in a regular `Controller` class can be done in an `Action` class**. 175 | Every events (like `beforeFilter` or `beforeRender`) a controller fires are also fired by `Action` classes. 176 | 177 | #### Loading Components 178 | 179 | ```php 180 | // in src/Controller/Posts/EditAction.php 181 | namespace App\Controller\Posts; 182 | 183 | use HavokInspiration\ActionsClass\Controller\Action; 184 | 185 | class EditAction extends Action 186 | { 187 | public function initialize() 188 | { 189 | $this->loadComponent('Flash'); 190 | } 191 | 192 | public function execute($id) 193 | { 194 | // some logic 195 | $this->Flash->success('Post updated !'); 196 | } 197 | } 198 | ``` 199 | 200 | #### Actions in Controllers under a routing prefix 201 | 202 | `Action` classes can live under a routing prefix or a plugin : 203 | 204 | ```php 205 | // in src/Controller/Posts/EditAction.php 206 | 207 | // Assuming that `Admin` is a routing prefix 208 | namespace App\Controller\Admin\Posts; 209 | 210 | use HavokInspiration\ActionsClass\Controller\Action; 211 | 212 | class EditAction extends Action 213 | { 214 | public function execute($id) 215 | { 216 | } 217 | } 218 | ``` 219 | 220 | ### Integration testing 221 | 222 | The plugin is compatible with the [Integration testing](https://book.cakephp.org/3.0/en/development/testing.html#controller-integration-testing) feature of CakePHP. 223 | You just need to follow the same directory pattern as you'd do for you application: 224 | 225 | ``` 226 | tests 227 | /TestCase 228 | /Controller 229 | /Posts 230 | /IndexActionTest.php 231 | ``` 232 | 233 | And a basic test file would look like: 234 | 235 | ```php 236 | // in tests/TestCase/Controller/Posts/IndexActionTest.php 237 | namespace App\Test\TestCase\Controller; 238 | 239 | use Cake\TestSuite\IntegrationTestCase; 240 | 241 | class IndexActionTest extends IntegrationTestCase 242 | { 243 | public function testIndexAction() 244 | { 245 | $this->get('/posts'); 246 | $this->assertResponseOk(); 247 | } 248 | } 249 | ``` 250 | 251 | Make sure you load the plugin in the `App\Application::bootstrap()` method, otherwise, Integration tests will not work. See the "Loading the plugin" section of this README to know how to. 252 | 253 | ### No-op methods 254 | 255 | As seen above, `Action` classes are instance of `\Cake\Controller\Controller`. Some methods in this class are related to actions. But since we are now having objects that represent actions, two methods had to be made "no-op" : `\Cake\Controller\Controller::isAction()` and `\Cake\Controller\Controller::setAction()`. Using these methods in an `Action` subclass will have no effect. 256 | 257 | ## Configuration 258 | 259 | ### Strict Mode 260 | 261 | As seen above, the plugin will let CakePHP handle the request in its regular dispatching cycle if an action can not be found. However, if you wish to only use `Action` classes, you can enable the strict mode. With strict mode enabled, the plugin will throw an exception if it can not find an `Action` class matching the request currently being resolved. 262 | 263 | To enable the strict mode, set it using the `Configure` object in the **bootstrap.php** file of your application: 264 | 265 | ```php 266 | Configure::write('ActionsClass.strictMode', true); 267 | ``` 268 | 269 | ## Contributing 270 | 271 | If you find a bug or would like to ask for a feature, please use the [GitHub issue tracker](https://github.com/HavokInspiration/cakephp-actions-class/issues). 272 | If you would like to submit a fix or a feature, please fork the repository and [submit a pull request](https://github.com/HavokInspiration/cakephp-actions-class/pulls). 273 | 274 | ### Coding standards 275 | 276 | This repository follows the PSR-2 standard. Some methods might be prefixed with an underscore because they are an overload from existing methods inside the CakePHP framework. 277 | Coding standards are checked when a pull request is made using the [Stickler CI](https://stickler-ci.com/) bot. 278 | 279 | ## License 280 | 281 | Copyright (c) 2015 - 2017, Yves Piquel and licensed under [The MIT License](http://opensource.org/licenses/mit-license.php). 282 | Please refer to the LICENSE.txt file. 283 | -------------------------------------------------------------------------------- /composer.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "havokinspiration/cakephp-actions-class", 3 | "description": "Manage your Controllers actions as single classes in CakePHP", 4 | "type": "cakephp-plugin", 5 | "license": "MIT", 6 | "authors": [ 7 | { 8 | "name": "Yves P.", 9 | "email": "havokinspiration@gmail.com" 10 | } 11 | ], 12 | "require": { 13 | "php": ">=7.0.0", 14 | "cakephp/cakephp": "^3.4" 15 | }, 16 | "require-dev": { 17 | "phpunit/phpunit": "^6.0", 18 | "cakephp/bake": "@stable" 19 | }, 20 | "autoload": { 21 | "psr-4": { 22 | "HavokInspiration\\ActionsClass\\": "src" 23 | } 24 | }, 25 | "autoload-dev": { 26 | "psr-4": { 27 | "HavokInspiration\\ActionsClass\\Test\\": "tests", 28 | "HavokInspiration\\ActionsClass\\PHPStan\\": "tests/PHPStan", 29 | "Cake\\Test\\": "./vendor/cakephp/cakephp/tests", 30 | "TestApp\\": "tests/test_app/TestApp", 31 | "TestPlugin\\": "tests/test_app/Plugin/TestPlugin/src", 32 | "Company\\TestPluginThree\\": "tests/test_app/Plugin/Company/TestPluginThree/src" 33 | } 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /config/bootstrap.php: -------------------------------------------------------------------------------- 1 | false 23 | ]); 24 | 25 | /** 26 | * Bind an event listener that will react to the "Dispatcher.beforeDispatch" triggered when the CakePHP 27 | * dispatchers dispatches the request 28 | */ 29 | EventManager::instance()->on(new HavokInspiration\ActionsClass\Event\DispatcherListener()); -------------------------------------------------------------------------------- /phpstan.neon: -------------------------------------------------------------------------------- 1 | services: 2 | - 3 | class: HavokInspiration\ActionsClass\PHPStan\ShellPropertiesClassReflectionExtension 4 | tags: 5 | - phpstan.broker.propertiesClassReflectionExtension -------------------------------------------------------------------------------- /phpunit.xml.dist: -------------------------------------------------------------------------------- 1 | 2 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | ./tests/TestCase 18 | 19 | 20 | 21 | 22 | 23 | 24 | ./config/ 25 | ./vendor/ 26 | ./tests/ 27 | 28 | 29 | 30 | 31 | 32 | 33 | ./src/ 34 | 35 | 36 | 37 | 38 | -------------------------------------------------------------------------------- /src/Controller/Action.php: -------------------------------------------------------------------------------- 1 | controllerName; 44 | } 45 | 46 | /** 47 | * Sets the controller name this action is binded to. 48 | * 49 | * @param string $controllerName 50 | * @return \HavokInspiration\ActionsClass\Controller\Action 51 | */ 52 | public function setControllerName(string $controllerName) : Action 53 | { 54 | $this->controllerName = $controllerName; 55 | $this->modelClass = $controllerName; 56 | 57 | return $this; 58 | } 59 | 60 | /** 61 | * {@inheritDoc} 62 | * 63 | * Override in order for the method to use the `controllerName` property of the object instead of `name` (which 64 | * is used to store the action name) to append to the view path returned. 65 | */ 66 | protected function _viewPath() 67 | { 68 | $viewPath = $this->controllerName; 69 | if ($this->request->getParam('prefix')) { 70 | $prefixes = array_map( 71 | 'Cake\Utility\Inflector::camelize', 72 | explode('/', $this->request->getParam('prefix')) 73 | ); 74 | $viewPath = implode(DIRECTORY_SEPARATOR, $prefixes) . DIRECTORY_SEPARATOR . $viewPath; 75 | } 76 | 77 | return $viewPath; 78 | } 79 | 80 | /** 81 | * {@inheritDoc} 82 | * 83 | * Rewrite it as we only expect a method 'execute' and not an action name that is auto-resolved. 84 | */ 85 | public function invokeAction() 86 | { 87 | $request = $this->request; 88 | if (!isset($request)) { 89 | throw new LogicException('No Request object configured. Cannot invoke action'); 90 | } 91 | 92 | if (!method_exists($this, 'execute')) { 93 | throw new LogicException( 94 | sprintf( 95 | 'Your class `%s` should implement an `execute()` method', 96 | get_class($this) 97 | ) 98 | ); 99 | } 100 | 101 | /* @var callable $callable */ 102 | $callable = [$this, 'execute']; 103 | 104 | return $callable(...array_values($request->getParam('pass'))); 105 | } 106 | 107 | /** 108 | * This method should be no-op as an action can not redirect to another action. 109 | * 110 | * @param string $action The new action to be 'redirected' to. 111 | * Any other parameters passed to this method will be passed as parameters to the new action. 112 | * @param array ...$args Arguments passed to the action 113 | * @return void 114 | */ 115 | public function setAction($action, ...$args) 116 | { 117 | } 118 | 119 | /** 120 | * This method should be no-op as we already are in an action. 121 | * 122 | * @param string $action The action to check. 123 | * @return void 124 | */ 125 | public function isAction($action) 126 | { 127 | } 128 | } 129 | -------------------------------------------------------------------------------- /src/Event/DispatcherListener.php: -------------------------------------------------------------------------------- 1 | 'beforeDispatch' 40 | ]; 41 | } 42 | 43 | /** 44 | * Hook method called when a beforeDispatch event is triggered by the CakePHP Dispatcher. 45 | * 46 | * @param \Cake\Event\Event $event Instance of the Event being dispatched. 47 | * @param \Cake\Http\ServerRequest $request The request to build an action for. 48 | * @param \Cake\Http\Response $response The response to use. 49 | * @return \Cake\Event\Event 50 | */ 51 | public function beforeDispatch(Event $event, ServerRequest $request, Response $response) 52 | { 53 | $action = null; 54 | 55 | try { 56 | $action = $this->createActionFactory($request, $response); 57 | } catch (MissingActionClassException $e) { 58 | if (Configure::read('ActionsClass.strictMode') === true) { 59 | throw $e; 60 | } 61 | } 62 | 63 | $event->setData('controller', $action); 64 | 65 | return $event; 66 | } 67 | 68 | /** 69 | * Create the Action object that will be used by the Dispatcher. 70 | * 71 | * @param \Cake\Http\ServerRequest $request The request to build an action for. 72 | * @param \Cake\Http\Response $response The response to use. 73 | * @return \HavokInspiration\ActionsClass\Controller\Action 74 | * @throws \HavokInspiration\ActionsClass\Http\Exception\MissingActionClassException In case the action can not be 75 | * found 76 | */ 77 | protected function createActionFactory(ServerRequest $request, Response $response) : Action 78 | { 79 | $factory = new ActionFactory(); 80 | $action = $factory->create($request, $response); 81 | 82 | return $action; 83 | } 84 | } 85 | -------------------------------------------------------------------------------- /src/Http/ActionFactory.php: -------------------------------------------------------------------------------- 1 | getParam('plugin')) { 42 | $pluginPath = $request->getParam('plugin') . '.'; 43 | } 44 | if ($request->getParam('prefix')) { 45 | if (strpos($request->getParam('prefix'), '/') === false) { 46 | $namespace .= '\\' . Inflector::camelize($request->getParam('prefix')); 47 | } else { 48 | $prefixes = array_map( 49 | 'Cake\Utility\Inflector::camelize', 50 | explode('/', $request->getParam('prefix')) 51 | ); 52 | $namespace .= '\\' . implode('\\', $prefixes); 53 | } 54 | } 55 | 56 | $this->failureIfForbiddenCharacters($request->getParam('controller'), $namespace); 57 | 58 | if ($request->getParam('controller')) { 59 | $namespace .= '\\' . $request->getParam('controller'); 60 | } 61 | 62 | $action = 'Index'; 63 | if ($request->getParam('action')) { 64 | $action = Inflector::camelize($request->getParam('action')); 65 | } 66 | 67 | // Disallow plugin short forms, / and \\ from 68 | // controller names as they allow direct references to 69 | // be created. 70 | $this->failureIfForbiddenCharacters($action, $namespace); 71 | 72 | $className = App::className($pluginPath . $action, $namespace, 'Action'); 73 | if (!$className) { 74 | $this->missingAction($namespace, $action); 75 | } 76 | $reflection = new ReflectionClass($className); 77 | if ($reflection->isAbstract() || $reflection->isInterface()) { 78 | $this->missingAction($namespace, $action); 79 | } 80 | 81 | $instance = $reflection->newInstance($request, $response, $action); 82 | $instance->setControllerName($request->getParam('controller')); 83 | return $instance; 84 | } 85 | 86 | /** 87 | * Throws an exception when an action is missing. 88 | * 89 | * @param string $namespace Namespace where the action was looked for 90 | * @param string $action Name of the action looked for 91 | * @return void 92 | * @throws \HavokInspiration\ActionsClass\Http\Exception\MissingActionClassException 93 | */ 94 | protected function missingAction(string $namespace, string $action) 95 | { 96 | throw new MissingActionClassException([ 97 | $namespace . '\\' . $action . 'Action' 98 | ]); 99 | } 100 | 101 | /** 102 | * Checks an entity's name (an action or a controller) for forbidden characters. If some are find, an exception 103 | * will be thrown. 104 | * 105 | * @param string $name Name of the entity being checked 106 | * @param string $namespace Namespace where the `$name` argument should belong to. 107 | * @return void 108 | * @throws \HavokInspiration\ActionsClass\Http\Exception\MissingActionClassException 109 | */ 110 | protected function failureIfForbiddenCharacters(string $name, string $namespace) 111 | { 112 | if (is_string($name) && 113 | ( 114 | strpos($name, '\\') !== false || 115 | strpos($name, '/') !== false || 116 | strpos($name, '.') !== false || 117 | substr($name, 0, 1) === strtolower(substr($name, 0, 1)) 118 | ) 119 | ) { 120 | $this->missingAction($namespace, $name); 121 | } 122 | } 123 | } 124 | -------------------------------------------------------------------------------- /src/Http/Exception/MissingActionClassException.php: -------------------------------------------------------------------------------- 1 | err('You must pass a Controller name for your action in the format `ControllerName/ActionName`'); 84 | 85 | return (string)Shell::CODE_ERROR; 86 | } 87 | 88 | $this->out("\n" . sprintf('Baking action class for %s...', $name), 1, Shell::QUIET); 89 | 90 | list($controller, $action) = $this->getName($name); 91 | 92 | $namespace = Configure::read('App.namespace'); 93 | if ($this->plugin) { 94 | $namespace = $this->_pluginNamespace($this->plugin); 95 | } 96 | 97 | $prefix = $this->_getPrefix(); 98 | if ($prefix) { 99 | $prefix = '\\' . str_replace('/', '\\', $prefix); 100 | } 101 | 102 | $data = [ 103 | 'action' => $action, 104 | 'controller' => $controller, 105 | 'namespace' => $namespace, 106 | 'prefix' => $prefix 107 | ]; 108 | 109 | $out = $this->bakeAction($action, $data); 110 | 111 | if (!isset($this->params['no-test']) || $this->params['no-test'] !== true) { 112 | $this->bakeActionTest($action, $data); 113 | } 114 | 115 | return $out; 116 | } 117 | 118 | /** 119 | * Generate the action code 120 | * 121 | * @param string $actionName The name of the action. 122 | * @param array $data The data to turn into code. 123 | * @return string The generated action file. 124 | */ 125 | public function bakeAction($actionName, array $data) 126 | { 127 | $data += [ 128 | 'namespace' => null, 129 | 'controller' => null, 130 | 'prefix' => null, 131 | 'actions' => null, 132 | ]; 133 | $this->BakeTemplate->set($data); 134 | $contents = $this->BakeTemplate->generate($this->template()); 135 | $path = $this->getPath(); 136 | $filename = $path . $data['controller'] . DS . $this->fileName($actionName); 137 | $this->createFile($filename, $contents); 138 | 139 | return $contents; 140 | } 141 | 142 | /** 143 | * Assembles and writes a unit test file 144 | * 145 | * @param string $className Controller class name 146 | * @return string|null Baked test 147 | */ 148 | public function bakeActionTest($actionName, $data) 149 | { 150 | $data += [ 151 | 'namespace' => null, 152 | 'controller' => null, 153 | 'prefix' => null, 154 | 'actions' => null, 155 | ]; 156 | $this->Test->plugin = $this->plugin; 157 | $this->BakeTemplate->set($data); 158 | 159 | $prefix = $this->_getPrefix(); 160 | $contents = $this->BakeTemplate->generate($this->templateTest()); 161 | 162 | $path = $this->Test->getPath() . 'Controller' . DS; 163 | if ($prefix) { 164 | $path .= $prefix . DS; 165 | } 166 | $path .= $data['controller']; 167 | 168 | $filename = $path . DS . $this->fileName($actionName, true); 169 | $this->createFile($filename, $contents); 170 | 171 | return $contents; 172 | } 173 | 174 | /** 175 | * Transform the name parameter into Controller & Action name. 176 | * 177 | * @param string $name Name passed to the CLI. 178 | * @return array First key is the controller name, second key the action name. 179 | */ 180 | protected function getName($name) 181 | { 182 | list($controller, $action) = explode('/', $name); 183 | 184 | $controller = $this->_camelize($controller); 185 | $action = $this->_camelize($action); 186 | 187 | return [$controller, $action]; 188 | } 189 | 190 | /** 191 | * {@inheritDoc} 192 | */ 193 | public function getOptionParser() 194 | { 195 | $parser = parent::getOptionParser(); 196 | 197 | $parser 198 | ->setDescription( 199 | 'Bake an Action class file skeleton' 200 | ) 201 | ->addOption('prefix', [ 202 | 'help' => 'The namespace/routing prefix to use.' 203 | ]) 204 | ->addOption('no-test', [ 205 | 'boolean' => true, 206 | 'help' => 'Do not generate a test skeleton.' 207 | ]); 208 | 209 | return $parser; 210 | } 211 | } 212 | -------------------------------------------------------------------------------- /src/Template/Bake/action.ctp: -------------------------------------------------------------------------------- 1 | \Controller<%= $prefix %>\<%= $controller %>; 3 | 4 | use HavokInspiration\ActionsClass\Controller\Action; 5 | 6 | /** 7 | * Controller : <%= $controller %> 8 | * Action : <%= $action %> 9 | * 10 | * @package <%= $namespace %>\Controller 11 | */ 12 | class <%= $action %>Action extends Action 13 | { 14 | /** 15 | * This method will be executed when the `<%= $controller %>` Controller action `<%= $action %>` will be invoked. 16 | * It is the equivalent of the `<%= $controller %>Controller::<%= $action %>()` method. 17 | * 18 | * @return void|\Cake\Network\Response 19 | */ 20 | public function execute() 21 | { 22 | 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /src/Template/Bake/test.ctp: -------------------------------------------------------------------------------- 1 | \Test\TestCase\Controller<%= $prefix %>\<%= $controller %>; 3 | 4 | use Cake\TestSuite\IntegrationTestCase; 5 | 6 | /** 7 | * Controller <%= $controller %> 8 | * Action <%= $action %> 9 | * 10 | * @package <%= $namespace %>\Controller 11 | */ 12 | class <%= $action %>ActionTest extends IntegrationTestCase 13 | { 14 | /** 15 | * TestCase for \<%= $namespace %>\Controller\<%= $controller %>\<%= $action %>Action 16 | */ 17 | public function test<%= $action %>Action() 18 | { 19 | $this->get('<%= str_replace('\\', '/', strtolower($prefix)) %>/<%= strtolower($controller) %>/<%= strtolower($action) %>'); 20 | $this->assertResponseOk(); 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /tests/PHPStan/BakeTemplatePropertyReflection.php: -------------------------------------------------------------------------------- 1 | declaringClass = $declaringClass; 21 | $this->type = $type; 22 | } 23 | public function getDeclaringClass(): ClassReflection 24 | { 25 | return $this->declaringClass; 26 | } 27 | 28 | public function isStatic(): bool 29 | { 30 | return false; 31 | } 32 | 33 | public function isPrivate(): bool 34 | { 35 | return false; 36 | } 37 | 38 | public function isPublic(): bool 39 | { 40 | return true; 41 | } 42 | 43 | public function getType(): Type 44 | { 45 | return $this->type; 46 | } 47 | 48 | public function isReadable(): bool 49 | { 50 | return true; 51 | } 52 | 53 | public function isWritable(): bool 54 | { 55 | return true; 56 | } 57 | } 58 | -------------------------------------------------------------------------------- /tests/PHPStan/ShellPropertiesClassReflectionExtension.php: -------------------------------------------------------------------------------- 1 | isSubclassOf(SimpleBakeTask::class) && ($propertyName === 'BakeTemplate' || $propertyName === 'Test'); 22 | } 23 | 24 | /** 25 | * @param ClassReflection $classReflection Class reflection 26 | * @param string $propertyName Method name 27 | * @return PropertyReflection 28 | */ 29 | public function getProperty(ClassReflection $classReflection, string $propertyName): PropertyReflection 30 | { 31 | $object = 'Bake\Shell\Task\BakeTemplateTask'; 32 | 33 | if ($propertyName === 'Test') { 34 | $object = 'Bake\Shell\Task\TestTask'; 35 | } 36 | 37 | return new BakeTemplatePropertyReflection( 38 | $classReflection, 39 | new ObjectType($object, false) 40 | ); 41 | } 42 | } 43 | -------------------------------------------------------------------------------- /tests/TestCase/Controller/ActionTest.php: -------------------------------------------------------------------------------- 1 | 'cakes/add', 36 | 'params' => [ 37 | 'controller' => 'Cakes', 38 | 'action' => 'add', 39 | ] 40 | ]); 41 | $this->response = $this->getMockBuilder('Cake\Http\Response')->getMock(); 42 | $this->action = (new ActionFactory())->create($request, $this->response); 43 | } 44 | 45 | /** 46 | * Test that an action without an execute method will throw an exception. 47 | * 48 | * @expectedException \LogicException 49 | * @expectedExceptionMessage Your class `TestApp\Controller\Cakes\AddAction` should implement an `execute()` method 50 | * @return void 51 | */ 52 | public function testInvokeActionMissingExecuteMethod() 53 | { 54 | $this->action->invokeAction(); 55 | } 56 | 57 | /** 58 | * Test that an action without a request object will throw an exception. 59 | * 60 | * @expectedException \LogicException 61 | * @expectedExceptionMessage No Request object configured. Cannot invoke action 62 | * @return void 63 | */ 64 | public function testInvokeActionMissingRequest() 65 | { 66 | $this->action->request = null; 67 | $this->action->invokeAction(); 68 | } 69 | 70 | /** 71 | * Test that an action will run its `execute` method. 72 | * 73 | * @return void 74 | */ 75 | public function testInvokeAction() 76 | { 77 | $request = new ServerRequest([ 78 | 'url' => 'cakes/index', 79 | 'params' => [ 80 | 'controller' => 'Cakes', 81 | 'action' => 'index', 82 | 'pass' => [] 83 | ] 84 | ]); 85 | $this->response = $this->getMockBuilder('Cake\Http\Response')->getMock(); 86 | $this->action = (new ActionFactory())->create($request, $this->response); 87 | $this->action->invokeAction(); 88 | 89 | $this->assertEquals('executed', $this->action->someProperty); 90 | $this->assertEquals('Cakes', $this->action->viewPath); 91 | } 92 | 93 | /** 94 | * Test that a prefixed action will have its prefix in the viewPath. 95 | * 96 | * @return void 97 | */ 98 | public function testInvokePrefixedAction() 99 | { 100 | $request = new ServerRequest([ 101 | 'url' => 'admin/sub/posts/index', 102 | 'params' => [ 103 | 'prefix' => 'admin/sub', 104 | 'controller' => 'Posts', 105 | 'action' => 'index', 106 | 'pass' => [ 107 | 'order' => 'desc', 108 | 'limit' => 500 109 | ] 110 | ] 111 | ]); 112 | $this->response = $this->getMockBuilder('Cake\Http\Response')->getMock(); 113 | $this->action = (new ActionFactory())->create($request, $this->response); 114 | $this->action->invokeAction(); 115 | 116 | $this->assertEquals('executed', $this->action->someProperty); 117 | $this->assertEquals('Admin/Sub/Posts', $this->action->viewPath); 118 | 119 | $expected = [ 120 | 'order' => 'desc', 121 | 'limit' => 500 122 | ]; 123 | $this->assertEquals($expected, $this->action->passed); 124 | } 125 | } 126 | -------------------------------------------------------------------------------- /tests/TestCase/Event/DispatcherListenerTest.php: -------------------------------------------------------------------------------- 1 | response = $this->getMockBuilder('Cake\Http\Response')->getMock(); 38 | } 39 | 40 | /** 41 | * Tests that dispatching an event with an existing action will return the correct instance in the `controller` key 42 | * of the returned data. 43 | * 44 | * @return void 45 | */ 46 | public function testDispatchingEventWithExistingAction() 47 | { 48 | $request = new ServerRequest([ 49 | 'url' => 'cakes/index', 50 | 'params' => [ 51 | 'controller' => 'Cakes', 52 | 'action' => 'index', 53 | ] 54 | ]); 55 | 56 | EventManager::instance()->on(new DispatcherListener()); 57 | $beforeEvent = $this->dispatchEvent( 58 | 'Dispatcher.beforeDispatch', 59 | [ 60 | 'request' => $request, 61 | 'response' => $this->response 62 | ] 63 | ); 64 | 65 | $this->assertInstanceOf('TestApp\Controller\Cakes\IndexAction', $beforeEvent->getData('controller')); 66 | } 67 | 68 | /** 69 | * Tests that dispatching an event with a missing action will return `null` in the `controller` key of the returned 70 | * data if strict mode is disabled (meaning CakePHP will try its regular routine to load a "regular controller" 71 | * object). 72 | * 73 | * @return void 74 | */ 75 | public function testDispatchingEventWithMissingActionNoStrictMode() 76 | { 77 | $request = new ServerRequest([ 78 | 'url' => 'cakes/index', 79 | 'params' => [ 80 | 'controller' => 'Cakes', 81 | 'action' => 'notHere', 82 | ] 83 | ]); 84 | 85 | EventManager::instance()->on(new DispatcherListener()); 86 | $beforeEvent = $this->dispatchEvent( 87 | 'Dispatcher.beforeDispatch', 88 | [ 89 | 'request' => $request, 90 | 'response' => $this->response 91 | ] 92 | ); 93 | 94 | $this->assertNull($beforeEvent->getData('controller')); 95 | } 96 | 97 | /** 98 | * Tests that dispatching an event with a missing action will throw an exception if strict mode is on. 99 | * 100 | * @expectedException \HavokInspiration\ActionsClass\Http\Exception\MissingActionClassException 101 | * @expectedExceptionMessage Action class Controller\Cakes\NotHereAction could not be found. 102 | * @return void 103 | */ 104 | public function testDispatchingEventWithMissingActionWithStrictMode() 105 | { 106 | $request = new ServerRequest([ 107 | 'url' => 'cakes/index', 108 | 'params' => [ 109 | 'controller' => 'Cakes', 110 | 'action' => 'notHere', 111 | ] 112 | ]); 113 | 114 | Configure::write('ActionsClass.strictMode', true); 115 | EventManager::instance()->on(new DispatcherListener()); 116 | $beforeEvent = $this->dispatchEvent( 117 | 'Dispatcher.beforeDispatch', 118 | [ 119 | 'request' => $request, 120 | 'response' => $this->response 121 | ] 122 | ); 123 | } 124 | } 125 | -------------------------------------------------------------------------------- /tests/TestCase/Http/ActionFactoryTest.php: -------------------------------------------------------------------------------- 1 | factory = new ActionFactory(); 36 | $this->response = $this->getMockBuilder('Cake\Http\Response')->getMock(); 37 | } 38 | 39 | /** 40 | * Test building an application action 41 | * 42 | * @return void 43 | */ 44 | public function testApplicationAction() 45 | { 46 | $request = new ServerRequest([ 47 | 'url' => 'cakes/index', 48 | 'params' => [ 49 | 'controller' => 'Cakes', 50 | 'action' => 'index', 51 | ] 52 | ]); 53 | $result = $this->factory->create($request, $this->response); 54 | $this->assertInstanceOf('TestApp\Controller\Cakes\IndexAction', $result); 55 | $this->assertEquals('Index', $result->name); 56 | $this->assertEquals('Cakes', $result->getControllerName()); 57 | $this->assertEquals('Cakes', $result->modelClass); 58 | $this->assertSame($request, $result->request); 59 | $this->assertSame($this->response, $result->response); 60 | } 61 | 62 | /** 63 | * Test building a prefixed app action. 64 | * 65 | * @return void 66 | */ 67 | public function testPrefixedAppAction() 68 | { 69 | $request = new ServerRequest([ 70 | 'url' => 'admin/posts/index', 71 | 'params' => [ 72 | 'prefix' => 'admin', 73 | 'controller' => 'Posts', 74 | 'action' => 'index', 75 | ] 76 | ]); 77 | $result = $this->factory->create($request, $this->response); 78 | $this->assertInstanceOf( 79 | 'TestApp\Controller\Admin\Posts\IndexAction', 80 | $result 81 | ); 82 | $this->assertEquals('Posts', $result->getControllerName()); 83 | $this->assertSame($request, $result->request); 84 | $this->assertSame($this->response, $result->response); 85 | } 86 | 87 | /** 88 | * Test building a nested prefix app action 89 | * 90 | * @return void 91 | */ 92 | public function testNestedPrefixedAppAction() 93 | { 94 | $request = new ServerRequest([ 95 | 'url' => 'admin/sub/posts/index', 96 | 'params' => [ 97 | 'prefix' => 'admin/sub', 98 | 'controller' => 'Posts', 99 | 'action' => 'index', 100 | ] 101 | ]); 102 | $result = $this->factory->create($request, $this->response); 103 | $this->assertInstanceOf( 104 | 'TestApp\Controller\Admin\Sub\Posts\IndexAction', 105 | $result 106 | ); 107 | $this->assertEquals('Posts', $result->getControllerName()); 108 | $this->assertSame($request, $result->request); 109 | $this->assertSame($this->response, $result->response); 110 | } 111 | 112 | /** 113 | * Test building a plugin action 114 | * 115 | * @return void 116 | */ 117 | public function testPluginAction() 118 | { 119 | $request = new ServerRequest([ 120 | 'url' => 'test_plugin/test_plugin/index', 121 | 'params' => [ 122 | 'plugin' => 'TestPlugin', 123 | 'controller' => 'TestPlugin', 124 | 'action' => 'index', 125 | ] 126 | ]); 127 | $result = $this->factory->create($request, $this->response); 128 | $this->assertInstanceOf( 129 | 'TestPlugin\Controller\TestPlugin\IndexAction', 130 | $result 131 | ); 132 | $this->assertSame($request, $result->request); 133 | $this->assertSame($this->response, $result->response); 134 | } 135 | 136 | /** 137 | * Test building a vendored plugin action. 138 | * 139 | * @return void 140 | */ 141 | public function testVendorPluginAction() 142 | { 143 | $request = new ServerRequest([ 144 | 'url' => 'test_plugin_three/ovens/index', 145 | 'params' => [ 146 | 'plugin' => 'Company/TestPluginThree', 147 | 'controller' => 'Ovens', 148 | 'action' => 'index', 149 | ] 150 | ]); 151 | $result = $this->factory->create($request, $this->response); 152 | $this->assertInstanceOf( 153 | 'Company\TestPluginThree\Controller\Ovens\IndexAction', 154 | $result 155 | ); 156 | $this->assertSame($request, $result->request); 157 | $this->assertSame($this->response, $result->response); 158 | } 159 | 160 | /** 161 | * Test building a prefixed plugin action 162 | * 163 | * @return void 164 | */ 165 | public function testPrefixedPluginAction() 166 | { 167 | $request = new ServerRequest([ 168 | 'url' => 'test_plugin/admin/comments', 169 | 'params' => [ 170 | 'prefix' => 'admin', 171 | 'plugin' => 'TestPlugin', 172 | 'controller' => 'Comments', 173 | 'action' => 'index', 174 | ] 175 | ]); 176 | $result = $this->factory->create($request, $this->response); 177 | $this->assertInstanceOf( 178 | 'TestPlugin\Controller\Admin\Comments\IndexAction', 179 | $result 180 | ); 181 | $this->assertSame($request, $result->request); 182 | $this->assertSame($this->response, $result->response); 183 | } 184 | 185 | /** 186 | * Test that trying to load an existing action that is abstract will throw an exception. 187 | * 188 | * @expectedException \HavokInspiration\ActionsClass\Http\Exception\MissingActionClassException 189 | * @expectedExceptionMessage Action class Controller\Invalid\AbstractAction could not be found. 190 | * @return void 191 | */ 192 | public function testAbstractClassFailure() 193 | { 194 | $request = new ServerRequest([ 195 | 'url' => 'invalid/abstract', 196 | 'params' => [ 197 | 'controller' => 'Invalid', 198 | 'action' => 'Abstract', 199 | ] 200 | ]); 201 | $this->factory->create($request, $this->response); 202 | } 203 | 204 | /** 205 | * Test that trying to load an existing action that is an interface will throw an exception. 206 | * 207 | * @expectedException \HavokInspiration\ActionsClass\Http\Exception\MissingActionClassException 208 | * @expectedExceptionMessage Action class Controller\Invalid\InterfaceAction could not be found. 209 | * @return void 210 | */ 211 | public function testInterfaceFailure() 212 | { 213 | $request = new ServerRequest([ 214 | 'url' => 'invalid/interface', 215 | 'params' => [ 216 | 'controller' => 'Invalid', 217 | 'action' => 'Interface', 218 | ] 219 | ]); 220 | $this->factory->create($request, $this->response); 221 | } 222 | 223 | /** 224 | * That that trying to load a missing class will throw an exception. 225 | * 226 | * @expectedException \HavokInspiration\ActionsClass\Http\Exception\MissingActionClassException 227 | * @expectedExceptionMessage Action class Controller\Invisible\IndexAction could not be found. 228 | * @return void 229 | */ 230 | public function testMissingClassFailure() 231 | { 232 | $request = new ServerRequest([ 233 | 'url' => 'interface/index', 234 | 'params' => [ 235 | 'controller' => 'Invisible', 236 | 'action' => 'index', 237 | ] 238 | ]); 239 | $this->factory->create($request, $this->response); 240 | } 241 | 242 | /** 243 | * Test that having a slash in the controller name will throw an exception. 244 | * 245 | * @expectedException \HavokInspiration\ActionsClass\Http\Exception\MissingActionClassException 246 | * @expectedExceptionMessage Action class Controller\Admin/PostsAction could not be found. 247 | * @return void 248 | */ 249 | public function testSlashedControllerFailure() 250 | { 251 | $request = new ServerRequest([ 252 | 'url' => 'admin/posts/index', 253 | 'params' => [ 254 | 'controller' => 'Admin/Posts', 255 | 'action' => 'index', 256 | ] 257 | ]); 258 | $this->factory->create($request, $this->response); 259 | } 260 | 261 | /** 262 | * Test that trying to load an absolute namespace path in the controller will throw an exception. 263 | * 264 | * @expectedException \HavokInspiration\ActionsClass\Http\Exception\MissingActionClassException 265 | * @expectedExceptionMessage Action class Controller\TestApp\Controller\CakesAction could not be found. 266 | * @return void 267 | */ 268 | public function testAbsoluteReferenceFailure() 269 | { 270 | $request = new ServerRequest([ 271 | 'url' => 'interface/index', 272 | 'params' => [ 273 | 'controller' => 'TestApp\Controller\Cakes', 274 | 'action' => 'index', 275 | ] 276 | ]); 277 | $this->factory->create($request, $this->response); 278 | } 279 | 280 | /** 281 | * Test that trying to load an absolute namespace path in the action will throw an exception. 282 | * 283 | * @expectedException \HavokInspiration\ActionsClass\Http\Exception\MissingActionClassException 284 | * @expectedExceptionMessage Action class Controller\Admin\Posts\IndexAction could not be found. 285 | * @return void 286 | */ 287 | public function testAbsoluteReferenceInActionFailure() 288 | { 289 | $request = new ServerRequest([ 290 | 'url' => 'interface/index', 291 | 'params' => [ 292 | 'controller' => 'Admin', 293 | 'action' => 'Posts\Index', 294 | ] 295 | ]); 296 | $this->factory->create($request, $this->response); 297 | } 298 | } 299 | -------------------------------------------------------------------------------- /tests/TestCase/Shell/Task/ActionTaskTest.php: -------------------------------------------------------------------------------- 1 | _compareBasePath = Plugin::path('HavokInspiration/ActionsClass') . 'tests' . DS . 'comparisons' . DS . 'Controller' . DS; 35 | 36 | $io = $this->getMockBuilder('Cake\Console\ConsoleIo') 37 | ->disableOriginalConstructor() 38 | ->getMock(); 39 | 40 | $this->Task = $this->getMockBuilder('HavokInspiration\ActionsClass\Shell\Task\ActionTask') 41 | ->setMethods(['in', 'err', 'createFile', '_stop']) 42 | ->setConstructorArgs([$io]) 43 | ->getMock(); 44 | 45 | $this->Task->Test = $this->getMockBuilder('Bake\Shell\Task\TestTask') 46 | ->setMethods(['in', 'err', 'createFile', '_stop']) 47 | ->setConstructorArgs([$io]) 48 | ->getMock(); 49 | 50 | $this->Task->BakeTemplate = new BakeTemplateTask($io); 51 | $this->Task->BakeTemplate->initialize(); 52 | $this->Task->BakeTemplate->interactive = false; 53 | $this->Task->Test->BakeTemplate = new BakeTemplateTask($io); 54 | $this->Task->Test->BakeTemplate->initialize(); 55 | $this->Task->Test->BakeTemplate->interactive = false; 56 | } 57 | 58 | /** 59 | * Load a plugin from the tests folder, and add to the autoloader 60 | * 61 | * @param string $name plugin name to load 62 | * @return void 63 | */ 64 | protected function _loadTestPlugin($name) 65 | { 66 | $path = TESTS . 'test_app' . DS . 'Plugin' . DS . $name . DS; 67 | 68 | Plugin::load($name, [ 69 | 'path' => $path, 70 | 'autoload' => true 71 | ]); 72 | } 73 | 74 | /** 75 | * Test the main method with a wrong name passed (does not follow the ControllerName/ActionName pattern). 76 | * 77 | * @return void 78 | */ 79 | public function testWrongName() 80 | { 81 | $this->Task->expects($this->once()) 82 | ->method('err') 83 | ->with( 84 | 'You must pass a Controller name for your action in the format `ControllerName/ActionName`' 85 | ); 86 | 87 | $this->Task->main('Posts'); 88 | } 89 | 90 | /** 91 | * Test the main method. 92 | * 93 | * @return void 94 | */ 95 | public function testMain() 96 | { 97 | $this->Task->expects($this->at(0)) 98 | ->method('createFile') 99 | ->with( 100 | $this->_normalizePath(APP . 'Controller/Posts/IndexAction.php'), 101 | $this->stringContains('class IndexAction extends Action') 102 | ); 103 | 104 | $this->Task->expects($this->at(1)) 105 | ->method('createFile') 106 | ->with( 107 | $this->_normalizePath(TESTS . 'TestCase/Controller/Posts/IndexActionTest.php'), 108 | $this->stringContains('class IndexActionTest extends IntegrationTestCase') 109 | ); 110 | 111 | $this->Task->main('Posts/Index'); 112 | } 113 | 114 | /** 115 | * Test the main method with the no-test parameter will only call createFile once. 116 | * 117 | * @return void 118 | */ 119 | public function testMainNoTest() 120 | { 121 | $this->Task->expects($this->once()) 122 | ->method('createFile'); 123 | 124 | $this->Task->params['no-test'] = true; 125 | $this->Task->main('Posts/Index'); 126 | } 127 | 128 | /** 129 | * Test the main method with a plugin. 130 | * 131 | * @return void 132 | */ 133 | public function testMainPlugin() 134 | { 135 | $this->_loadTestPlugin('MaintenanceTest'); 136 | $path = Plugin::path('MaintenanceTest'); 137 | 138 | $this->Task->expects($this->at(0)) 139 | ->method('createFile') 140 | ->with( 141 | $this->_normalizePath($path . 'src/Controller/Posts/IndexAction.php'), 142 | $this->stringContains('class IndexAction extends Action') 143 | ); 144 | 145 | $this->Task->expects($this->at(1)) 146 | ->method('createFile') 147 | ->with( 148 | $this->_normalizePath($path . 'tests/TestCase/Controller/Posts/IndexActionTest.php'), 149 | $this->stringContains('class IndexActionTest extends IntegrationTestCase') 150 | ); 151 | 152 | $this->Task->main('MaintenanceTest.Posts/Index'); 153 | } 154 | 155 | /** 156 | * Test bake. 157 | * 158 | * @return void 159 | */ 160 | public function testBake() 161 | { 162 | $this->Task->expects($this->at(0)) 163 | ->method('createFile') 164 | ->with( 165 | $this->_normalizePath(APP . 'Controller/Posts/IndexAction.php') 166 | ); 167 | 168 | $result = $this->Task->bake('Posts/Index'); 169 | $this->assertSameAsFile('Posts/IndexAction.php', $result); 170 | } 171 | 172 | /** 173 | * Test bake with a plugin. 174 | * 175 | * @return void 176 | */ 177 | public function testBakePlugin() 178 | { 179 | $this->_loadTestPlugin('MaintenanceTest'); 180 | $path = Plugin::path('MaintenanceTest'); 181 | 182 | $this->Task->expects($this->at(0)) 183 | ->method('createFile') 184 | ->with( 185 | $this->_normalizePath($path . 'src/Controller/Posts/IndexAction.php') 186 | ); 187 | 188 | $this->Task->plugin = 'MaintenanceTest'; 189 | $result = $this->Task->bake('Posts/Index'); 190 | $this->assertSameAsFile('Plugin/Posts/IndexAction.php', $result); 191 | } 192 | 193 | /** 194 | * Test bake with a routing prefix. 195 | * 196 | * @return void 197 | */ 198 | public function testBakePrefix() 199 | { 200 | $this->Task->expects($this->at(0)) 201 | ->method('createFile') 202 | ->with( 203 | $this->_normalizePath(APP . 'Controller/Admin/Posts/IndexAction.php') 204 | ); 205 | 206 | $this->Task->params['prefix'] = 'Admin'; 207 | $result = $this->Task->bake('Posts/Index'); 208 | $this->assertSameAsFile('Admin/Posts/IndexAction.php', $result); 209 | } 210 | 211 | /** 212 | * Test bake with a plugin and a routing prefix. 213 | * 214 | * @return void 215 | */ 216 | public function testBakePluginPrefix() 217 | { 218 | $this->_loadTestPlugin('MaintenanceTest'); 219 | $path = Plugin::path('MaintenanceTest'); 220 | 221 | $this->Task->expects($this->at(0)) 222 | ->method('createFile') 223 | ->with( 224 | $this->_normalizePath($path . 'src/Controller/Admin/Posts/IndexAction.php') 225 | ); 226 | 227 | $this->Task->params['prefix'] = 'Admin'; 228 | $this->Task->plugin = 'MaintenanceTest'; 229 | $result = $this->Task->bake('Posts/Index'); 230 | $this->assertSameAsFile('Plugin/Admin/Posts/IndexAction.php', $result); 231 | } 232 | } 233 | -------------------------------------------------------------------------------- /tests/bootstrap.php: -------------------------------------------------------------------------------- 1 | 'App', 38 | 'paths' => [ 39 | 'plugins' => [APP . 'Plugin' . DS], 40 | 'templates' => [APP . 'Template' . DS] 41 | ] 42 | ]); 43 | 44 | Plugin::load('HavokInspiration/ActionsClass', [ 45 | 'path' => dirname(dirname(__FILE__)) . DS, 46 | ]); -------------------------------------------------------------------------------- /tests/comparisons/Controller/Admin/Posts/IndexAction.php: -------------------------------------------------------------------------------- 1 | someProperty = 'executed'; 28 | $this->viewPath = $this->_viewPath(); 29 | 30 | $this->passed = compact('order', 'limit'); 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /tests/test_app/TestApp/Controller/Cakes/AddAction.php: -------------------------------------------------------------------------------- 1 | someProperty = 'executed'; 26 | $this->viewPath = $this->_viewPath(); 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /tests/test_app/TestApp/Controller/Invalid/AbstractAction.php: -------------------------------------------------------------------------------- 1 |