Recurso derivado de PHP MVC
Crear comandos de consola para un framework PHP MVC propio
Cuando un framework crece, escribir controladores, modelos y acciones repetitivas a mano empieza a ser una perdida de tiempo. Este recurso muestra una forma simple de agregar comandos de consola a SimpleMVC para generar codigo desde la linea de comandos.
El objetivo no es copiar Rails, Symfony o CakePHP, sino entender el patron: un punto de entrada CLI, comandos separados, plantillas y validaciones minimas para generar archivos de aplicacion.
El problema
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.
En vez de crear cada controlador y cada modelo desde cero, podemos agregar una utilidad de consola capaz de generar archivos iniciales a partir del nombre de una entidad.
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
<?php
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()
{
$this->table = '<?= $table ?>';
parent::__construct($this->table, $this->db);
$this->key = 'id';
}
}
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";
}
}
Plantilla del controlador
<?php
echo "<?php\n";
?>
namespace App\Controllers;
use App\models\<?= $modelName ?>;
use SimpleMVC\core\Controller as Controller;
use SimpleMVC\core\View as View;
class <?= $controllerName ?> extends Controller
{
private <?= $modelName ?> $model;
private View $view;
public function __construct(<?= $modelName ?> $model, View $view)
{
$this->model = $model;
$this->view = $view;
}
public function index()
{
$this->view->setAction('index');
$this->view->set('items', $this->model->getAll());
return $this->view->render();
}
public function view($id)
{
$this->view->setAction('view');
$this->view->set('item', $this->model->loadModel($id));
return $this->view->render();
}
}
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:view para 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.