PluginTask.php 9.7 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328
  1. <?php
  2. /**
  3. * CakePHP(tm) : Rapid Development Framework (http://cakephp.org)
  4. * Copyright (c) Cake Software Foundation, Inc. (http://cakefoundation.org)
  5. *
  6. * Licensed under The MIT License
  7. * For full copyright and license information, please see the LICENSE.txt
  8. * Redistributions of files must retain the above copyright notice.
  9. *
  10. * @copyright Copyright (c) Cake Software Foundation, Inc. (http://cakefoundation.org)
  11. * @link http://cakephp.org CakePHP(tm) Project
  12. * @since 1.2.0
  13. * @license http://www.opensource.org/licenses/mit-license.php MIT License
  14. */
  15. namespace Cake\Console\Command\Task;
  16. use Cake\Console\Command\Task\BakeTask;
  17. use Cake\Console\Shell;
  18. use Cake\Core\App;
  19. use Cake\Utility\File;
  20. use Cake\Utility\Folder;
  21. /**
  22. * The Plugin Task handles creating an empty plugin, ready to be used
  23. *
  24. */
  25. class PluginTask extends BakeTask {
  26. /**
  27. * Path to the bootstrap file. Changed in tests.
  28. *
  29. * @var string
  30. */
  31. public $bootstrap = null;
  32. /**
  33. * Tasks this task uses.
  34. *
  35. * @var array
  36. */
  37. public $tasks = ['Template', 'Project'];
  38. /**
  39. * initialize
  40. *
  41. * @return void
  42. */
  43. public function initialize() {
  44. $this->path = current(App::path('Plugin'));
  45. $this->bootstrap = ROOT . 'config' . DS . 'bootstrap.php';
  46. }
  47. /**
  48. * Execution method always used for tasks
  49. *
  50. * @param string $name The name of the plugin to bake.
  51. * @return void
  52. */
  53. public function main($name = null) {
  54. if (empty($name)) {
  55. $this->err('<error>You must provide a plugin name in CamelCase format.</error>');
  56. $this->err('To make an "Example" plugin, run <info>Console/cake bake plugin Example</info>.');
  57. return false;
  58. }
  59. $plugin = $this->_camelize($name);
  60. $pluginPath = $this->_pluginPath($plugin);
  61. if (is_dir($pluginPath)) {
  62. $this->out(__d('cake_console', 'Plugin: %s already exists, no action taken', $plugin));
  63. $this->out(__d('cake_console', 'Path: %s', $pluginPath));
  64. return false;
  65. }
  66. if (!$this->bake($plugin)) {
  67. $this->error(__d('cake_console', "An error occurred trying to bake: %s in %s", $plugin, $this->path . $plugin));
  68. }
  69. }
  70. /**
  71. * Bake the plugin, create directories and files
  72. *
  73. * @param string $plugin Name of the plugin in CamelCased format
  74. * @return bool
  75. */
  76. public function bake($plugin) {
  77. $pathOptions = App::path('Plugin');
  78. if (count($pathOptions) > 1) {
  79. $this->findPath($pathOptions);
  80. }
  81. $this->hr();
  82. $this->out(__d('cake_console', "<info>Plugin Name:</info> %s", $plugin));
  83. $this->out(__d('cake_console', "<info>Plugin Directory:</info> %s", $this->path . $plugin));
  84. $this->hr();
  85. $classBase = 'src';
  86. $looksGood = $this->in(__d('cake_console', 'Look okay?'), ['y', 'n', 'q'], 'y');
  87. if (strtolower($looksGood) === 'y') {
  88. $Folder = new Folder($this->path . $plugin);
  89. $directories = [
  90. 'config',
  91. $classBase . DS . 'Model' . DS . 'Behavior',
  92. $classBase . DS . 'Model' . DS . 'Table',
  93. $classBase . DS . 'Model' . DS . 'Entity',
  94. $classBase . DS . 'Console' . DS . 'Command' . DS . 'Task',
  95. $classBase . DS . 'Controller' . DS . 'Component',
  96. $classBase . DS . 'View' . DS . 'Helper',
  97. $classBase . DS . 'Template',
  98. 'tests' . DS . 'TestCase' . DS . 'Controller' . DS . 'Component',
  99. 'tests' . DS . 'TestCase' . DS . 'View' . DS . 'Helper',
  100. 'tests' . DS . 'TestCase' . DS . 'Model' . DS . 'Behavior',
  101. 'tests' . DS . 'Fixture',
  102. 'webroot'
  103. ];
  104. foreach ($directories as $directory) {
  105. $dirPath = $this->path . $plugin . DS . $directory;
  106. $Folder->create($dirPath);
  107. new File($dirPath . DS . 'empty', true);
  108. }
  109. foreach ($Folder->messages() as $message) {
  110. $this->out($message, 1, Shell::VERBOSE);
  111. }
  112. $errors = $Folder->errors();
  113. if (!empty($errors)) {
  114. foreach ($errors as $message) {
  115. $this->error($message);
  116. }
  117. return false;
  118. }
  119. $controllerFileName = 'AppController.php';
  120. $out = "<?php\n\n";
  121. $out .= "namespace {$plugin}\\Controller;\n\n";
  122. $out .= "use App\\Controller\\AppController as BaseController;\n\n";
  123. $out .= "class AppController extends BaseController {\n\n";
  124. $out .= "}\n";
  125. $this->createFile($this->path . $plugin . DS . $classBase . DS . 'Controller' . DS . $controllerFileName, $out);
  126. $hasAutoloader = $this->_modifyAutoloader($plugin, $this->path);
  127. $this->_generateRoutes($plugin, $this->path);
  128. $this->_modifyBootstrap($plugin, $hasAutoloader);
  129. $this->_generatePhpunitXml($plugin, $this->path);
  130. $this->_generateTestBootstrap($plugin, $this->path);
  131. $this->hr();
  132. $this->out(__d('cake_console', '<success>Created:</success> %s in %s', $plugin, $this->path . $plugin), 2);
  133. }
  134. return true;
  135. }
  136. /**
  137. * Update the app's bootstrap.php file.
  138. *
  139. * @param string $plugin Name of plugin
  140. * @param bool $hasAutoloader Whether or not there is an autoloader configured for
  141. * the plugin
  142. * @return void
  143. */
  144. protected function _modifyBootstrap($plugin, $hasAutoloader) {
  145. $bootstrap = new File($this->bootstrap, false);
  146. $contents = $bootstrap->read();
  147. if (!preg_match("@\n\s*Plugin::loadAll@", $contents)) {
  148. $autoload = $hasAutoloader ? null : "'autoload' => true, ";
  149. $bootstrap->append(sprintf(
  150. "\nPlugin::load('%s', [%s'bootstrap' => false, 'routes' => true]);\n",
  151. $plugin,
  152. $autoload
  153. ));
  154. $this->out('');
  155. $this->out(sprintf('%s modified', $this->bootstrap));
  156. }
  157. }
  158. /**
  159. * Generate a routes file for the plugin being baked.
  160. *
  161. * @param string $plugin The plugin to generate routes for.
  162. * @param string $path The path to save the routes.php file in.
  163. * @return void
  164. */
  165. protected function _generateRoutes($plugin, $path) {
  166. $this->Template->set([
  167. 'plugin' => $plugin,
  168. ]);
  169. $this->out( __d('cake_console', 'Generating routes.php file...'));
  170. $out = $this->Template->generate('config', 'routes');
  171. $file = $path . $plugin . DS . 'config' . DS . 'routes.php';
  172. $this->createFile($file, $out);
  173. }
  174. /**
  175. * Generate a phpunit.xml stub for the plugin.
  176. *
  177. * @param string $plugin Name of plugin
  178. * @param string $path The path to save the phpunit.xml file to.
  179. * @return void
  180. */
  181. protected function _generatePhpunitXml($plugin, $path) {
  182. $this->Template->set([
  183. 'plugin' => $plugin,
  184. 'path' => $path
  185. ]);
  186. $this->out( __d('cake_console', 'Generating phpunit.xml file...'));
  187. $out = $this->Template->generate('test', 'phpunit.xml');
  188. $file = $path . $plugin . DS . 'phpunit.xml';
  189. $this->createFile($file, $out);
  190. }
  191. /**
  192. * Generate a Test/bootstrap.php stub for the plugin.
  193. *
  194. * @param string $plugin Name of plugin
  195. * @param string $path The path to save the phpunit.xml file to.
  196. * @return void
  197. */
  198. protected function _generateTestBootstrap($plugin, $path) {
  199. $this->Template->set([
  200. 'plugin' => $plugin,
  201. 'path' => $path,
  202. 'root' => ROOT
  203. ]);
  204. $this->out( __d('cake_console', 'Generating tests/bootstrap.php file...'));
  205. $out = $this->Template->generate('test', 'bootstrap');
  206. $file = $path . $plugin . DS . 'tests' . DS . 'bootstrap.php';
  207. $this->createFile($file, $out);
  208. }
  209. /**
  210. * Modifies App's composer.json to include the plugin and tries to call
  211. * composer dump-autoload to refresh the autoloader cache
  212. *
  213. * @param string $plugin Name of plugin
  214. * @param string $path The path to save the phpunit.xml file to.
  215. * @return bool True if composer could be modified correctly
  216. */
  217. protected function _modifyAutoloader($plugin, $path) {
  218. $path = dirname($path);
  219. if (!file_exists($path . DS . 'composer.json')) {
  220. return false;
  221. }
  222. $file = $path . DS . 'composer.json';
  223. $config = json_decode(file_get_contents($file), true);
  224. $config['autoload']['psr-4'][$plugin . '\\'] = "./plugins/$plugin/src";
  225. $config['autoload']['psr-4'][$plugin . '\\Test\\'] = "./plugins/$plugin/tests";
  226. $this->out(__d('cake_console', '<info>Modifying composer autoloader</info>'));
  227. file_put_contents(
  228. $file,
  229. json_encode($config, JSON_PRETTY_PRINT | JSON_UNESCAPED_SLASHES)
  230. );
  231. $composer = $this->Project->findComposer();
  232. if (!$composer) {
  233. $this->error(__d('cake_console', 'Could not locate composer, Add composer to your PATH, or use the -composer option.'));
  234. return false;
  235. }
  236. try {
  237. $command = 'cd ' . escapeshellarg($path) . '; ';
  238. $command .= 'php ' . escapeshellarg($composer) . ' dump-autoload ';
  239. $this->callProcess($command);
  240. } catch (\RuntimeException $e) {
  241. $error = $e->getMessage();
  242. $this->error(__d('cake_console', 'Could not run `composer dump-autoload`: %s', $error));
  243. return false;
  244. }
  245. return true;
  246. }
  247. /**
  248. * find and change $this->path to the user selection
  249. *
  250. * @param array $pathOptions The list of paths to look in.
  251. * @return void
  252. */
  253. public function findPath(array $pathOptions) {
  254. $valid = false;
  255. foreach ($pathOptions as $i => $path) {
  256. if (!is_dir($path)) {
  257. unset($pathOptions[$i]);
  258. }
  259. }
  260. $pathOptions = array_values($pathOptions);
  261. $max = count($pathOptions);
  262. while (!$valid) {
  263. foreach ($pathOptions as $i => $option) {
  264. $this->out($i + 1 . '. ' . $option);
  265. }
  266. $prompt = __d('cake_console', 'Choose a plugin path from the paths above.');
  267. $choice = $this->in($prompt, null, 1);
  268. if (intval($choice) > 0 && intval($choice) <= $max) {
  269. $valid = true;
  270. }
  271. }
  272. $this->path = $pathOptions[$choice - 1];
  273. }
  274. /**
  275. * Gets the option parser instance and configures it.
  276. *
  277. * @return \Cake\Console\ConsoleOptionParser
  278. */
  279. public function getOptionParser() {
  280. $parser = parent::getOptionParser();
  281. $parser->description(__d('cake_console',
  282. 'Create the directory structure, AppController class and testing setup for a new plugin. ' .
  283. 'Can create plugins in any of your bootstrapped plugin paths.'
  284. ))->addArgument('name', [
  285. 'help' => __d('cake_console', 'CamelCased name of the plugin to create.')
  286. ])->addOption('composer', [
  287. 'default' => ROOT . '/composer.phar',
  288. 'help' => __d('cake_console', 'The path to the composer executable.')
  289. ])->removeOption('plugin');
  290. return $parser;
  291. }
  292. }