Crear comandos de consola para un framework PHP MVC propio
Una guia para automatizar la generacion de modelos, controladores y CRUD inicial en SimpleMVC desde la linea de comandos.
El problema
Cuando un framework crece, escribir controladores, modelos y acciones repetitivas a mano empieza a ser una perdida de tiempo.
En el libro PHP MVC se construye un framework propio paso a paso. Una vez que la estructura base existe, aparece una pregunta natural: como automatizar la creacion de piezas repetitivas.
La idea no es copiar Rails, Symfony o CakePHP. El objetivo es entender el patron: un punto de entrada CLI, comandos separados, plantillas y validaciones minimas para generar archivos de aplicacion.
Este recurso es educativo. El codigo generado es un punto de partida. Antes de usar algo similar en produccion, conviene reforzar validaciones, permisos, manejo de errores y pruebas.
Estructura sugerida
Una organizacion posible es crear una carpeta command dentro del framework, al mismo nivel que core y helpers.
simplemvc/
command/
console.php
CommandSupport.php
GenerateModelCommand.php
GenerateControllerCommand.php
templates/
model_template.php
controller_template.php
core/
helpers/
application/
controllers/
models/
config/
config.php
La carpeta templates contiene los archivos usados como molde para generar codigo. Los comandos se ocupan de validar nombres, leer metadatos de base de datos y escribir los archivos finales.
Punto de entrada
El archivo console.php recibe argumentos desde la linea de comandos. El primer argumento es el comando; el segundo, el nombre de la entidad.
<?php
require_once __DIR__.'/CommandSupport.php';
require_once __DIR__.'/GenerateModelCommand.php';
require_once __DIR__.'/GenerateControllerCommand.php';
$commands = [
'generate:model' => GenerateModelCommand::class,
'generate:controller' => GenerateControllerCommand::class,
];
$command = $argv[1] ?? null;
$name = $argv[2] ?? null;
if ($command === null || $name === null) {
echo "Usage: php console.php generate:model client\n";
echo "Usage: php console.php generate:controller client\n";
exit(1);
}
if (! array_key_exists($command, $commands)) {
echo "Unknown command: {$command}\n";
exit(1);
}
try {
$commands[$command]::run($name);
} catch (Throwable $exception) {
echo $exception->getMessage()."\n";
exit(1);
}
Esta version centraliza el mapa de comandos y deja que cada clase resuelva una tarea concreta.
Lectura de columnas
Para generar modelos y controladores basados en una tabla, necesitamos conocer sus columnas. Tambien necesitamos validar el nombre recibido por consola, porque no conviene usar argumentos arbitrarios para construir rutas o consultas.
<?php
final class CommandSupport
{
public static function normalizeEntityName(string $name): string
{
$name = strtolower(trim($name));
if (! preg_match('/^[a-z][a-z0-9_]*$/', $name)) {
throw new InvalidArgumentException(
'Use a lowercase entity name such as client or medical_record.'
);
}
return $name;
}
public static function classNameFromEntity(string $name): string
{
return str_replace(' ', '', ucwords(str_replace('_', ' ', $name)));
}
public static function tableNameFromEntity(string $name): string
{
return $name.'s';
}
public static function columnsForTable(PDO $pdo, string $table): array
{
$statement = $pdo->prepare(
'SELECT COLUMN_NAME
FROM information_schema.COLUMNS
WHERE TABLE_SCHEMA = DATABASE()
AND TABLE_NAME = :table
ORDER BY ORDINAL_POSITION'
);
$statement->execute(['table' => $table]);
$columns = $statement->fetchAll(PDO::FETCH_COLUMN);
if ($columns === []) {
throw new RuntimeException("Table {$table} does not exist.");
}
return $columns;
}
}
La funcion tableNameFromEntity usa una pluralizacion simple. Para casos reales, conviene permitir configurar nombres de tabla cuando la regla client a clients no alcance.
Generar un modelo
El comando de modelo calcula el nombre de la clase, detecta columnas y usa una plantilla para escribir el archivo final en application/models.
<?php
require_once dirname(__DIR__).'/../config/config.php';
class GenerateModelCommand
{
public static function run(string $rawName): void
{
$name = CommandSupport::normalizeEntityName($rawName);
$table = CommandSupport::tableNameFromEntity($name);
$modelName = CommandSupport::classNameFromEntity($name);
$modelFile = dirname(__DIR__).'/../application/models/'.$modelName.'.php';
if (file_exists($modelFile)) {
throw new RuntimeException("Model already exists: {$modelFile}");
}
$pdo = new PDO(
'mysql:host='.CONFIG['db_host'].';dbname='.CONFIG['db_name'],
CONFIG['db_user'],
CONFIG['db_password']
);
$properties = CommandSupport::columnsForTable($pdo, $table);
ob_start();
include __DIR__.'/templates/model_template.php';
$content = ob_get_clean();
file_put_contents($modelFile, $content);
echo "Model generated: {$modelFile}\n";
}
}
Plantilla del modelo
El archivo templates/model_template.php es el molde que usa GenerateModelCommand. Recibe el nombre de la clase en $modelName y la lista de columnas en $properties.
<?php
/**
* This is the template for generating a model class of a specified table
*/
echo "<?php\n";
?>
namespace App\models;
use SimpleMVC\core\Model as Model;
class <?= $modelName ?> extends Model
{
<?php foreach ($properties as $property): ?>
private $<?= $property; ?>;
<?php endforeach; ?>
public function __construct(string $table)
{
$this->table = $table;
parent::__construct($this->table, $this->db);
$this->key = 'id';
}
<?php foreach ($properties as $property): ?>
public function set<?= ucfirst($property); ?>($<?= $property; ?>)
{
$this-><?= $property ?> = $<?= $property ?>;
}
<?php endforeach; ?>
<?php foreach ($properties as $property): ?>
public function get<?= ucfirst($property) ?>()
{
return $this-><?= $property; ?>;
}
<?php endforeach; ?>
}
La primera linea escribe la apertura <?php en el archivo generado. Despues, la plantilla recorre las columnas para crear propiedades privadas y sus metodos set y get.
Generar un controlador
El controlador puede generarse con una primera version minima. La idea es crear acciones base y dejar el codigo generado preparado para ser ajustado por el desarrollador.
<?php
require_once dirname(__DIR__).'/../config/config.php';
class GenerateControllerCommand
{
public static function run(string $rawName): void
{
$name = CommandSupport::normalizeEntityName($rawName);
$table = CommandSupport::tableNameFromEntity($name);
$modelName = CommandSupport::classNameFromEntity($name);
$controllerName = $modelName.'Controller';
$controllerFile = dirname(__DIR__)
.'/../application/controllers/'
.$controllerName
.'.php';
if (file_exists($controllerFile)) {
throw new RuntimeException("Controller already exists: {$controllerFile}");
}
$pdo = new PDO(
'mysql:host='.CONFIG['db_host'].';dbname='.CONFIG['db_name'],
CONFIG['db_user'],
CONFIG['db_password']
);
$properties = CommandSupport::columnsForTable($pdo, $table);
ob_start();
include __DIR__.'/templates/controller_template.php';
$content = ob_get_clean();
file_put_contents($controllerFile, $content);
echo "Controller generated: {$controllerFile}\n";
}
}
Probar el comando
Para probar el generador, podemos usar una tabla sencilla de clientes.
CREATE TABLE clients (
id INT NOT NULL AUTO_INCREMENT,
firstname VARCHAR(30) NOT NULL,
lastname VARCHAR(30) NOT NULL,
email VARCHAR(50) DEFAULT NULL,
reg_date TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
image_path VARCHAR(200) DEFAULT NULL,
PRIMARY KEY (id)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
Luego ejecutamos:
php console.php generate:model client
php console.php generate:controller client
Si todo esta bien configurado, deberian aparecer dos archivos nuevos:
application/models/Client.php
application/controllers/ClientController.php
Mejoras futuras
Una vez que los comandos basicos funcionan, el siguiente paso natural es crear un comando de nivel superior:
php console.php generate:crud client
Ese comando podria llamar internamente a generate:model, generate:controller y a nuevos comandos para generar vistas.
- Agregar
generate:viewpara crear formularios y listados. - Permitir nombres de tabla explicitos.
- Usar una clase base para todos los comandos.
- Agregar tests para validacion de nombres y rutas.
- Crear templates mas pequenos y faciles de mantener.
La automatizacion es util cuando acelera trabajo repetitivo sin esconder decisiones importantes. Un generador debe ayudar a empezar, no reemplazar el criterio del desarrollador.