ModelTask.php 19 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707
  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\Shell;
  17. use Cake\Core\App;
  18. use Cake\Core\Configure;
  19. use Cake\Datasource\ConnectionManager;
  20. use Cake\ORM\Table;
  21. use Cake\ORM\TableRegistry;
  22. use Cake\Utility\ClassRegistry;
  23. use Cake\Utility\Inflector;
  24. /**
  25. * Task class for generating model files.
  26. *
  27. * @codingStandardsIgnoreFile
  28. */
  29. class ModelTask extends BakeTask {
  30. /**
  31. * path to Model directory
  32. *
  33. * @var string
  34. */
  35. public $path = null;
  36. /**
  37. * tasks
  38. *
  39. * @var array
  40. */
  41. public $tasks = ['DbConfig', 'Fixture', 'Test', 'Template'];
  42. /**
  43. * Tables to skip when running all()
  44. *
  45. * @var array
  46. */
  47. public $skipTables = ['i18n'];
  48. /**
  49. * Holds tables found on connection.
  50. *
  51. * @var array
  52. */
  53. protected $_tables = [];
  54. /**
  55. * Holds the model names
  56. *
  57. * @var array
  58. */
  59. protected $_modelNames = [];
  60. /**
  61. * Holds validation method map.
  62. *
  63. * @var array
  64. */
  65. protected $_validations = [];
  66. /**
  67. * Override initialize
  68. *
  69. * @return void
  70. */
  71. public function initialize() {
  72. $this->path = APP . '/Model/';
  73. }
  74. /**
  75. * Execution method always used for tasks
  76. *
  77. * @return void
  78. */
  79. public function execute() {
  80. parent::execute();
  81. if (!isset($this->connection)) {
  82. $this->connection = 'default';
  83. }
  84. if (empty($this->args)) {
  85. $this->out(__d('cake_console', 'Choose a model to bake from the following:'));
  86. foreach ($this->listAll() as $table) {
  87. $this->out('- ' . $this->_modelName($table));
  88. }
  89. return true;
  90. }
  91. if (strtolower($this->args[0]) === 'all') {
  92. return $this->all();
  93. }
  94. $this->generate($this->_modelName($this->args[0]));
  95. }
  96. /**
  97. * Generate code for the given model name.
  98. *
  99. * @param string $name The model name to generate.
  100. * @return void
  101. */
  102. public function generate($name) {
  103. $table = $this->getTable($name);
  104. $model = $this->getTableObject($name, $table);
  105. $associations = $this->getAssociations($model);
  106. $primaryKey = $this->getPrimaryKey($model);
  107. $displayField = $this->getDisplayField($model);
  108. $fields = $this->getFields($model);
  109. $validation = $this->getValidation($model);
  110. $behaviors = $this->getBehaviors($model);
  111. $data = compact(
  112. 'associations', 'primaryKey', 'displayField',
  113. 'table', 'fields', 'validation', 'behaviors');
  114. $this->bakeTable($model, $data);
  115. $this->bakeEntity($model, $data);
  116. $this->bakeFixture($model, $table);
  117. $this->bakeTest($model);
  118. }
  119. /**
  120. * Bake all models at once.
  121. *
  122. * @return void
  123. */
  124. public function all() {
  125. $this->listAll($this->connection, false);
  126. foreach ($this->_tables as $table) {
  127. if (in_array($table, $this->skipTables)) {
  128. continue;
  129. }
  130. $modelClass = $this->_modelName($table);
  131. $this->out(__d('cake_console', 'Baking %s', $modelClass));
  132. $this->generate($modelClass);
  133. }
  134. }
  135. /**
  136. * Get a model object for a class name.
  137. *
  138. * @param string $className Name of class you want model to be.
  139. * @param string $table Table name
  140. * @return Cake\ORM\Table Table instance
  141. */
  142. public function getTableObject($className, $table) {
  143. if (TableRegistry::exists($className)) {
  144. return TableRegistry::get($className);
  145. }
  146. return TableRegistry::get($className, [
  147. 'name' => $className,
  148. 'table' => $table,
  149. 'connection' => ConnectionManager::get($this->connection)
  150. ]);
  151. }
  152. /**
  153. * Get the array of associations to generate.
  154. *
  155. * @return array
  156. */
  157. public function getAssociations(Table $table) {
  158. if (!empty($this->params['no-associations'])) {
  159. return [];
  160. }
  161. $assocs = [];
  162. $this->out(__d('cake_console', 'One moment while associations are detected.'));
  163. $this->listAll();
  164. $associations = [
  165. 'belongsTo' => [],
  166. 'hasMany' => [],
  167. 'belongsToMany' => []
  168. ];
  169. $associations = $this->findBelongsTo($table, $associations);
  170. $associations = $this->findHasMany($table, $associations);
  171. $associations = $this->findBelongsToMany($table, $associations);
  172. return $associations;
  173. }
  174. /**
  175. * Find belongsTo relations and add them to the associations list.
  176. *
  177. * @param ORM\Table $table Database\Table instance of table being generated.
  178. * @param array $associations Array of in progress associations
  179. * @return array Associations with belongsTo added in.
  180. */
  181. public function findBelongsTo($model, $associations) {
  182. $schema = $model->schema();
  183. $primary = (array)$schema->primaryKey();
  184. foreach ($schema->columns() as $fieldName) {
  185. $offset = strpos($fieldName, '_id');
  186. if (!in_array($fieldName, $primary) && $fieldName !== 'parent_id' && $offset !== false) {
  187. $tmpModelName = $this->_modelNameFromKey($fieldName);
  188. $associations['belongsTo'][] = [
  189. 'alias' => $tmpModelName,
  190. 'className' => $tmpModelName,
  191. 'foreignKey' => $fieldName,
  192. ];
  193. } elseif ($fieldName === 'parent_id') {
  194. $associations['belongsTo'][] = [
  195. 'alias' => 'Parent' . $model->alias(),
  196. 'className' => $model->alias(),
  197. 'foreignKey' => $fieldName,
  198. ];
  199. }
  200. }
  201. return $associations;
  202. }
  203. /**
  204. * Find the hasMany relations and add them to associations list
  205. *
  206. * @param Model $model Model instance being generated
  207. * @param array $associations Array of in progress associations
  208. * @return array Associations with hasMany added in.
  209. */
  210. public function findHasMany($model, $associations) {
  211. $schema = $model->schema();
  212. $primaryKey = (array)$schema->primaryKey();
  213. $tableName = $schema->name();
  214. $foreignKey = $this->_modelKey($tableName);
  215. foreach ($this->listAll() as $otherTable) {
  216. $otherModel = $this->getTableObject($this->_modelName($otherTable), $otherTable);
  217. $otherSchema = $otherModel->schema();
  218. // Exclude habtm join tables.
  219. $pattern = '/_' . preg_quote($tableName, '/') . '|' . preg_quote($tableName, '/') . '_/';
  220. $possibleJoinTable = preg_match($pattern, $otherTable);
  221. if ($possibleJoinTable) {
  222. continue;
  223. }
  224. foreach ($otherSchema->columns() as $fieldName) {
  225. $assoc = false;
  226. if (!in_array($fieldName, $primaryKey) && $fieldName == $foreignKey) {
  227. $assoc = [
  228. 'alias' => $otherModel->alias(),
  229. 'className' => $otherModel->alias(),
  230. 'foreignKey' => $fieldName
  231. ];
  232. } elseif ($otherTable == $tableName && $fieldName === 'parent_id') {
  233. $assoc = [
  234. 'alias' => 'Child' . $model->alias(),
  235. 'className' => $model->alias(),
  236. 'foreignKey' => $fieldName
  237. ];
  238. }
  239. if ($assoc) {
  240. $associations['hasMany'][] = $assoc;
  241. }
  242. }
  243. }
  244. return $associations;
  245. }
  246. /**
  247. * Find the BelongsToMany relations and add them to associations list
  248. *
  249. * @param Model $model Model instance being generated
  250. * @param array $associations Array of in-progress associations
  251. * @return array Associations with belongsToMany added in.
  252. */
  253. public function findBelongsToMany($model, $associations) {
  254. $schema = $model->schema();
  255. $primaryKey = (array)$schema->primaryKey();
  256. $tableName = $schema->name();
  257. $foreignKey = $this->_modelKey($tableName);
  258. $tables = $this->listAll();
  259. foreach ($tables as $otherTable) {
  260. $assocTable = null;
  261. $offset = strpos($otherTable, $tableName . '_');
  262. $otherOffset = strpos($otherTable, '_' . $tableName);
  263. if ($offset !== false) {
  264. $assocTable = substr($otherTable, strlen($tableName . '_'));
  265. } elseif ($otherOffset !== false) {
  266. $assocTable = substr($otherTable, 0, $otherOffset);
  267. }
  268. if ($assocTable && in_array($assocTable, $tables)) {
  269. $habtmName = $this->_modelName($assocTable);
  270. $associations['belongsToMany'][] = [
  271. 'alias' => $habtmName,
  272. 'className' => $habtmName,
  273. 'foreignKey' => $foreignKey,
  274. 'targetForeignKey' => $this->_modelKey($habtmName),
  275. 'joinTable' => $otherTable
  276. ];
  277. }
  278. }
  279. return $associations;
  280. }
  281. /**
  282. * Get the display field from the model or parameters
  283. *
  284. * @param Cake\ORM\Table $model The model to introspect.
  285. * @return string
  286. */
  287. public function getDisplayField($model) {
  288. if (!empty($this->params['display-field'])) {
  289. return $this->params['display-field'];
  290. }
  291. return $model->displayField();
  292. }
  293. /**
  294. * Get the primary key field from the model or parameters
  295. *
  296. * @param Cake\ORM\Table $model The model to introspect.
  297. * @return array The columns in the primary key
  298. */
  299. public function getPrimaryKey($model) {
  300. if (!empty($this->params['primary-key'])) {
  301. $fields = explode(',', $this->params['primary-key']);
  302. return array_values(array_filter(array_map('trim', $fields)));
  303. }
  304. return (array)$model->primaryKey();
  305. }
  306. /**
  307. * Get the fields from a model.
  308. *
  309. * Uses the fields and no-fields options.
  310. *
  311. * @param Cake\ORM\Table $model The model to introspect.
  312. * @return array The columns to make accessible
  313. */
  314. public function getFields($model) {
  315. if (!empty($this->params['no-fields'])) {
  316. return [];
  317. }
  318. if (!empty($this->params['fields'])) {
  319. $fields = explode(',', $this->params['fields']);
  320. return array_values(array_filter(array_map('trim', $fields)));
  321. }
  322. $schema = $model->schema();
  323. $columns = $schema->columns();
  324. $exclude = ['created', 'modified', 'updated', 'password', 'passwd'];
  325. return array_diff($columns, $exclude);
  326. }
  327. /**
  328. * Generate default validation rules.
  329. *
  330. * @param Cake\ORM\Table $model The model to introspect.
  331. * @return array The validation rules.
  332. */
  333. public function getValidation($model) {
  334. if (!empty($this->params['no-validation'])) {
  335. return [];
  336. }
  337. $schema = $model->schema();
  338. $fields = $schema->columns();
  339. if (empty($fields)) {
  340. return false;
  341. }
  342. $skipFields = false;
  343. $validate = [];
  344. $primaryKey = (array)$schema->primaryKey();
  345. foreach ($fields as $fieldName) {
  346. $field = $schema->column($fieldName);
  347. $validation = $this->fieldValidation($fieldName, $field, $primaryKey);
  348. if (!empty($validation)) {
  349. $validate[$fieldName] = $validation;
  350. }
  351. }
  352. return $validate;
  353. }
  354. /**
  355. * Does individual field validation handling.
  356. *
  357. * @param string $fieldName Name of field to be validated.
  358. * @param array $metaData metadata for field
  359. * @param string $primaryKey
  360. * @return array Array of validation for the field.
  361. */
  362. public function fieldValidation($fieldName, $metaData, $primaryKey) {
  363. $ignoreFields = array_merge($primaryKey, ['created', 'modified', 'updated']);
  364. if ($metaData['null'] == true && in_array($fieldName, $ignoreFields)) {
  365. return false;
  366. }
  367. if ($fieldName === 'email') {
  368. $rule = 'email';
  369. } elseif ($metaData['type'] === 'uuid') {
  370. $rule = 'uuid';
  371. } elseif ($metaData['type'] === 'string') {
  372. $rule = 'notEmpty';
  373. } elseif ($metaData['type'] === 'text') {
  374. $rule = 'notEmpty';
  375. } elseif ($metaData['type'] === 'integer') {
  376. $rule = 'numeric';
  377. } elseif ($metaData['type'] === 'float') {
  378. $rule = 'numeric';
  379. } elseif ($metaData['type'] === 'decimal') {
  380. $rule = 'decimal';
  381. } elseif ($metaData['type'] === 'boolean') {
  382. $rule = 'boolean';
  383. } elseif ($metaData['type'] === 'date') {
  384. $rule = 'date';
  385. } elseif ($metaData['type'] === 'time') {
  386. $rule = 'time';
  387. } elseif ($metaData['type'] === 'datetime') {
  388. $rule = 'datetime';
  389. } elseif ($metaData['type'] === 'inet') {
  390. $rule = 'ip';
  391. }
  392. $allowEmpty = false;
  393. if ($rule !== 'notEmpty' && $metaData['null'] === false) {
  394. $allowEmpty = true;
  395. }
  396. return [
  397. 'rule' => $rule,
  398. 'allowEmpty' => $allowEmpty,
  399. ];
  400. }
  401. /**
  402. * Get behaviors
  403. *
  404. * @param Cake\ORM\Table $model
  405. * @return array Behaviors
  406. */
  407. public function getBehaviors($model) {
  408. $behaviors = [];
  409. $schema = $model->schema();
  410. $fields = $schema->columns();
  411. if (empty($fields)) {
  412. return [];
  413. }
  414. if (in_array('created', $fields) || in_array('modified', $fields)) {
  415. $behaviors[] = 'Timestamp';
  416. }
  417. if (in_array('lft', $fields) && $schema->columnType('lft') === 'integer' &&
  418. in_array('rght', $fields) && $schema->columnType('rght') === 'integer' &&
  419. in_array('parent_id', $fields)
  420. ) {
  421. $behaviors[] = 'Tree';
  422. }
  423. return $behaviors;
  424. }
  425. /**
  426. * Bake an entity class.
  427. *
  428. * @param Cake\ORM\Table $model Model name or object
  429. * @param array $data An array to use to generate the Table
  430. * @return string
  431. */
  432. public function bakeEntity($model, $data = []) {
  433. if (!empty($this->params['no-entity'])) {
  434. return;
  435. }
  436. $name = Inflector::singularize($model->alias());
  437. $ns = Configure::read('App.namespace');
  438. $pluginPath = '';
  439. if ($this->plugin) {
  440. $ns = $this->plugin;
  441. $pluginPath = $this->plugin . '.';
  442. }
  443. $data += [
  444. 'name' => $name,
  445. 'namespace' => $ns,
  446. 'plugin' => $this->plugin,
  447. 'pluginPath' => $pluginPath,
  448. 'fields' => [],
  449. ];
  450. $this->Template->set($data);
  451. $out = $this->Template->generate('classes', 'entity');
  452. $path = $this->getPath();
  453. $filename = $path . 'Entity/' . $name . '.php';
  454. $this->out("\n" . __d('cake_console', 'Baking entity class for %s...', $name), 1, Shell::QUIET);
  455. $this->createFile($filename, $out);
  456. return $out;
  457. }
  458. /**
  459. * Bake a table class.
  460. *
  461. * @param Cake\ORM\Table $model Model name or object
  462. * @param array $data An array to use to generate the Table
  463. * @return string
  464. */
  465. public function bakeTable($model, $data = []) {
  466. if (!empty($this->params['no-table'])) {
  467. return;
  468. }
  469. $ns = Configure::read('App.namespace');
  470. $pluginPath = '';
  471. if ($this->plugin) {
  472. $ns = $this->plugin;
  473. $pluginPath = $this->plugin . '.';
  474. }
  475. $name = $model->alias();
  476. $data += [
  477. 'plugin' => $this->plugin,
  478. 'pluginPath' => $pluginPath,
  479. 'namespace' => $ns,
  480. 'name' => $name,
  481. 'associations' => [],
  482. 'primaryKey' => 'id',
  483. 'displayField' => null,
  484. 'table' => null,
  485. 'validation' => [],
  486. 'behaviors' => [],
  487. ];
  488. $this->Template->set($data);
  489. $out = $this->Template->generate('classes', 'table');
  490. $path = $this->getPath();
  491. $filename = $path . 'Table/' . $name . 'Table.php';
  492. $this->out("\n" . __d('cake_console', 'Baking table class for %s...', $name), 1, Shell::QUIET);
  493. $this->createFile($filename, $out);
  494. TableRegistry::clear();
  495. return $out;
  496. }
  497. /**
  498. * Outputs the a list of possible models or controllers from database
  499. *
  500. * @param string $useDbConfig Database configuration name
  501. * @return array
  502. */
  503. public function listAll() {
  504. if (!empty($this->_tables)) {
  505. return $this->_tables;
  506. }
  507. $this->_modelNames = [];
  508. $this->_tables = $this->_getAllTables();
  509. foreach ($this->_tables as $table) {
  510. $this->_modelNames[] = $this->_modelName($table);
  511. }
  512. return $this->_tables;
  513. }
  514. /**
  515. * Get an Array of all the tables in the supplied connection
  516. * will halt the script if no tables are found.
  517. *
  518. * @return array Array of tables in the database.
  519. * @throws InvalidArgumentException When connection class
  520. * has a schemaCollection method.
  521. */
  522. protected function _getAllTables() {
  523. $tables = [];
  524. $db = ConnectionManager::get($this->connection);
  525. if (!method_exists($db, 'schemaCollection')) {
  526. $this->err(__d(
  527. 'cake_console',
  528. 'Connections need to implement schemaCollection() to be used with bake.'
  529. ));
  530. return $this->_stop();
  531. }
  532. $schema = $db->schemaCollection();
  533. $tables = $schema->listTables();
  534. if (empty($tables)) {
  535. $this->err(__d('cake_console', 'Your database does not have any tables.'));
  536. return $this->_stop();
  537. }
  538. sort($tables);
  539. return $tables;
  540. }
  541. /**
  542. * Get the table name for the model being baked.
  543. *
  544. * Uses the `table` option if it is set.
  545. *
  546. * @return string.
  547. */
  548. public function getTable($name) {
  549. if (isset($this->params['table'])) {
  550. return $this->params['table'];
  551. }
  552. return Inflector::tableize($name);
  553. }
  554. /**
  555. * Gets the option parser instance and configures it.
  556. *
  557. * @return ConsoleOptionParser
  558. */
  559. public function getOptionParser() {
  560. $parser = parent::getOptionParser();
  561. $parser->description(
  562. __d('cake_console', 'Bake models.')
  563. )->addArgument('name', [
  564. 'help' => __d('cake_console', 'Name of the model to bake. Can use Plugin.name to bake plugin models.')
  565. ])->addSubcommand('all', [
  566. 'help' => __d('cake_console', 'Bake all model files with associations and validation.')
  567. ])->addOption('plugin', [
  568. 'short' => 'p',
  569. 'help' => __d('cake_console', 'Plugin to bake the model into.')
  570. ])->addOption('theme', [
  571. 'short' => 't',
  572. 'help' => __d('cake_console', 'Theme to use when baking code.')
  573. ])->addOption('connection', [
  574. 'short' => 'c',
  575. 'help' => __d('cake_console', 'The connection the model table is on.')
  576. ])->addOption('force', [
  577. 'short' => 'f',
  578. 'help' => __d('cake_console', 'Force overwriting existing files without prompting.')
  579. ])->addOption('table', [
  580. 'help' => __d('cake_console', 'The table name to use if you have non-conventional table names.')
  581. ])->addOption('no-entity', [
  582. 'boolean' => true,
  583. 'help' => __d('cake_console', 'Disable generating an entity class.')
  584. ])->addOption('no-table', [
  585. 'boolean' => true,
  586. 'help' => __d('cake_console', 'Disable generating a table class.')
  587. ])->addOption('no-validation', [
  588. 'boolean' => true,
  589. 'help' => __d('cake_console', 'Disable generating validation rules.')
  590. ])->addOption('no-associations', [
  591. 'boolean' => true,
  592. 'help' => __d('cake_console', 'Disable generating associations.')
  593. ])->addOption('no-fields', [
  594. 'boolean' => true,
  595. 'help' => __d('cake_console', 'Disable generating accessible fields in the entity.')
  596. ])->addOption('fields', [
  597. 'help' => __d('cake_console', 'A comma separated list of fields to make accessible.')
  598. ])->addOption('primary-key', [
  599. 'help' => __d('cake_console', 'The primary key if you would like to manually set one.')
  600. ])->addOption('display-field', [
  601. 'help' => __d('cake_console', 'The displayField if you would like to choose one.')
  602. ])->addOption('no-test', [
  603. 'boolean' => true,
  604. 'help' => __d('cake_console', 'Do not generate a test case skeleton.')
  605. ])->addOption('no-fixture', [
  606. 'boolean' => true,
  607. 'help' => __d('cake_console', 'Do not generate a test fixture skeleton.')
  608. ])->epilog(
  609. __d('cake_console', 'Omitting all arguments and options will list ' .
  610. 'the table names you can generate models for')
  611. );
  612. return $parser;
  613. }
  614. /**
  615. * Interact with FixtureTask to automatically bake fixtures when baking models.
  616. *
  617. * @param string $className Name of class to bake fixture for
  618. * @param string $useTable Optional table name for fixture to use.
  619. * @return void
  620. * @see FixtureTask::bake
  621. */
  622. public function bakeFixture($className, $useTable = null) {
  623. if (!empty($this->params['no-fixture'])) {
  624. return;
  625. }
  626. $this->Fixture->connection = $this->connection;
  627. $this->Fixture->plugin = $this->plugin;
  628. $this->Fixture->bake($className, $useTable);
  629. }
  630. /**
  631. * Assembles and writes a unit test file
  632. *
  633. * @param string $className Model class name
  634. * @return string
  635. */
  636. public function bakeTest($className) {
  637. if (!empty($this->params['no-test'])) {
  638. return;
  639. }
  640. $this->Test->plugin = $this->plugin;
  641. $this->Test->connection = $this->connection;
  642. return $this->Test->bake('Model', $className);
  643. }
  644. }