<?php
/**
* CakePHP(tm) : Rapid Development Framework (http://cakephp.org)
* 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://cakephp.org CakePHP(tm) Project
* @since 0.1.0
* @license http://www.opensource.org/licenses/mit-license.php MIT License
*/
namespace Bake\Shell\Task;
use Bake\Utility\Model\AssociationFilter;
use Cake\Console\Shell;
use Cake\Core\App;
use Cake\Core\Configure;
use Cake\Datasource\EntityInterface;
use Cake\ORM\Table;
use Cake\ORM\TableRegistry;
use Cake\Utility\Inflector;
/**
* Task class for creating and updating view template files.
*
* @property \Bake\Shell\Task\ModelTask $Model
* @property \Bake\Shell\Task\BakeTemplateTask $BakeTemplate
*/
class TemplateTask extends BakeTask
{
/**
* Tasks to be loaded by this Task
*
* @var array
*/
public $tasks = [
'Bake.Model',
'Bake.BakeTemplate',
];
/**
* path to Template directory
*
* @var string
*/
public $pathFragment = 'Template/';
/**
* Name of the controller being used
*
* @var string
*/
public $controllerName = null;
/**
* Classname of the controller being used
*
* @var string
*/
public $controllerClass = null;
/**
* Name with plugin of the model being used
*
* @var string
*/
public $modelName = null;
/**
* Actions to use for scaffolding
*
* @var array
*/
public $scaffoldActions = ['index', 'view', 'add', 'edit'];
/**
* AssociationFilter utility
*
* @var \Bake\Utility\Model\AssociationFilter|null
*/
protected $_associationFilter = null;
/**
* Template path.
*
* @var string
*/
public $path;
/**
* Override initialize
*
* @return void
*/
public function initialize()
{
$this->path = current(App::path('Template'));
}
/**
* Execution method always used for tasks
*
* @param string|null $name The name of the controller to bake view templates for.
* @param string|null $template The template to bake with.
* @param string|null $action The output action name. Defaults to $template.
* @return mixed
*/
public function main($name = null, $template = null, $action = null)
{
parent::main();
if (empty($name)) {
$this->out('Possible tables to bake view templates for based on your current database:');
$this->Model->connection = $this->connection;
foreach ($this->Model->listUnskipped() as $table) {
$this->out('- ' . $this->_camelize($table));
}
return true;
}
$name = $this->_getName($name);
$controller = null;
if (!empty($this->params['controller'])) {
$controller = $this->params['controller'];
}
$this->controller($name, $controller);
$this->model($name);
if ($template && $action === null) {
$action = $template;
}
if ($template) {
$this->bake($template, true, $action);
return true;
}
$vars = $this->_loadController();
$methods = $this->_methodsToBake();
foreach ($methods as $method) {
$content = $this->getContent($method, $vars);
if ($content) {
$this->bake($method, $content);
}
}
}
/**
* Set the model class for the table.
*
* @param string $table The table/model that is being baked.
* @return void
*/
public function model($table)
{
$tableName = $this->_camelize($table);
$plugin = null;
if (!empty($this->params['plugin'])) {
$plugin = $this->params['plugin'] . '.';
}
$this->modelName = $plugin . $tableName;
}
/**
* Set the controller related properties.
*
* @param string $table The table/model that is being baked.
* @param string|null $controller The controller name if specified.
* @return void
*/
public function controller($table, $controller = null)
{
$tableName = $this->_camelize($table);
if (empty($controller)) {
$controller = $tableName;
}
$this->controllerName = $controller;
$plugin = $this->param('plugin');
if ($plugin) {
$plugin .= '.';
}
$prefix = $this->_getPrefix();
if ($prefix) {
$prefix .= '/';
}
$this->controllerClass = (string)App::className($plugin . $prefix . $controller, 'Controller', 'Controller');
}
/**
* Get the path base for view templates.
*
* @return string
*/
public function getPath()
{
$path = parent::getPath();
$path .= $this->controllerName . DS;
return $path;
}
/**
* Get a list of actions that can / should have view templates baked for them.
*
* @return array Array of action names that should be baked
*/
protected function _methodsToBake()
{
$base = Configure::read('App.namespace');
$methods = [];
if (class_exists($this->controllerClass)) {
$methods = array_diff(
array_map(
'Cake\Utility\Inflector::underscore',
get_class_methods($this->controllerClass)
),
array_map(
'Cake\Utility\Inflector::underscore',
get_class_methods($base . '\Controller\AppController')
)
);
}
if (empty($methods)) {
$methods = $this->scaffoldActions;
}
foreach ($methods as $i => $method) {
if ($method[0] === '_') {
unset($methods[$i]);
}
}
return $methods;
}
/**
* Bake All view templates for all controller actions.
*
* @return void
*/
public function all()
{
$this->Model->connection = $this->connection;
$tables = $this->Model->listUnskipped();
foreach ($tables as $table) {
$this->main($table);
}
}
/**
* Loads Controller and sets variables for the template
* Available template variables:
*
* - 'modelObject'
* - 'modelClass'
* - 'entityClass'
* - 'primaryKey'
* - 'displayField'
* - 'singularVar'
* - 'pluralVar'
* - 'singularHumanName'
* - 'pluralHumanName'
* - 'fields'
* - 'keyFields'
* - 'schema'
*
* @return array Returns variables to be made available to a view template
*/
protected function _loadController()
{
if (TableRegistry::getTableLocator()->exists($this->modelName)) {
$modelObject = TableRegistry::getTableLocator()->get($this->modelName);
} else {
$modelObject = TableRegistry::getTableLocator()->get($this->modelName, [
'connectionName' => $this->connection,
]);
}
$namespace = Configure::read('App.namespace');
$primaryKey = (array)$modelObject->getPrimaryKey();
$displayField = $modelObject->getDisplayField();
$singularVar = $this->_singularName($this->controllerName);
$singularHumanName = $this->_singularHumanName($this->controllerName);
$schema = $modelObject->getSchema();
$fields = $schema->columns();
$modelClass = $this->modelName;
list(, $entityClass) = namespaceSplit($this->_entityName($this->modelName));
$entityClass = sprintf('%s\Model\Entity\%s', $namespace, $entityClass);
if (!class_exists($entityClass)) {
$entityClass = EntityInterface::class;
}
$associations = $this->_filteredAssociations($modelObject);
$keyFields = [];
if (!empty($associations['BelongsTo'])) {
foreach ($associations['BelongsTo'] as $assoc) {
$keyFields[$assoc['foreignKey']] = $assoc['variable'];
}
}
$pluralVar = Inflector::variable($this->controllerName);
$pluralHumanName = $this->_pluralHumanName($this->controllerName);
return compact(
'modelObject',
'modelClass',
'entityClass',
'schema',
'primaryKey',
'displayField',
'singularVar',
'pluralVar',
'singularHumanName',
'pluralHumanName',
'fields',
'associations',
'keyFields',
'namespace'
);
}
/**
* Assembles and writes bakes the view file.
*
* @param string $template Template file to use.
* @param string|true $content Content to write.
* @param string $outputFile The output file to create. If null will use `$template`
* @return string|false Generated file content.
*/
public function bake($template, $content = '', $outputFile = null)
{
if ($outputFile === null) {
$outputFile = $template;
}
if ($content === true) {
$content = $this->getContent($template);
}
if (empty($content)) {
$this->err("<warning>No generated content for '{$template}.ctp', not generating template.</warning>");
return false;
}
$this->out("\n" . sprintf('Baking `%s` view template file...', $outputFile), 1, Shell::QUIET);
$path = $this->getPath();
$filename = $path . Inflector::underscore($outputFile) . '.ctp';
$this->createFile($filename, $content);
return $content;
}
/**
* Builds content from template and variables
*
* @param string $action name to generate content to
* @param array|null $vars passed for use in templates
* @return string|false Content from template
*/
public function getContent($action, $vars = null)
{
if (!$vars) {
$vars = $this->_loadController();
}
if (empty($vars['primaryKey'])) {
$this->abort('Cannot generate views for models with no primary key');
return false;
}
if ($action === "index" && !empty($this->params['index-columns'])) {
$this->BakeTemplate->set('indexColumns', $this->params['index-columns']);
}
$this->BakeTemplate->set('action', $action);
$this->BakeTemplate->set('plugin', $this->plugin);
$this->BakeTemplate->set($vars);
return $this->BakeTemplate->generate("Template/$action");
}
/**
* Gets the option parser instance and configures it.
*
* @return \Cake\Console\ConsoleOptionParser
*/
public function getOptionParser()
{
$parser = parent::getOptionParser();
$parser->setDescription(
'Bake views for a controller, using built-in or custom templates. '
)->addArgument('controller', [
'help' => 'Name of the controller views to bake. You can use Plugin.name as a shortcut for plugin baking.',
])->addArgument('action', [
'help' => "Will bake a single action's file. core templates are (index, add, edit, view)",
])->addArgument('alias', [
'help' => 'Will bake the template in <action> but create the filename after <alias>.',
])->addOption('controller', [
'help' => 'The controller name if you have a controller that does not follow conventions.',
])->addOption('prefix', [
'help' => 'The routing prefix to generate views for.',
])->addOption('index-columns', [
'help' => 'Limit for the number of index columns',
'default' => 0,
])->addSubcommand('all', [
'help' => '[optional] Bake all CRUD action views for all controllers. Requires models and controllers to exist.',
]);
return $parser;
}
/**
* Get filtered associations
* To be mocked...
*
* @param \Cake\ORM\Table $model Table
* @return array associations
*/
protected function _filteredAssociations(Table $model)
{
if (is_null($this->_associationFilter)) {
$this->_associationFilter = new AssociationFilter();
}
return $this->_associationFilter->filterAssociations($model);
}
}