Núcleo
Controladores
Jerarquía
Controller (core/web)
├── SuperController (src) sitio público: templateFolder "front", acceso abierto
│ └── IndexController, CuentasController, DocsController…
├── AdminSuperController (src) panel: templateFolder "admin", requiere login
│ └── Admin/Sistema/UsuariosController…
├── AjaxController (core/web) respuestas JSON
│ ├── FrontAjaxController (src)
│ └── AdminAjaxController (src)
├── JavascriptController (core/web) respuestas JavaScript
│ ├── FrontJavascriptController (src)
│ └── AdminJavascriptController (src)
└── ApiController (src) punto de entrada de la API REST
Heredá siempre de la clase de src/ que corresponda, no de las de core/: ahí está la carga del usuario de sesión, el menú y los datos comunes de cada zona.
Un controlador completo
<?php
Koshkil::Uses('com.SuperController');
Koshkil::UsesModel('productos');
class ProductosController extends SuperController {
public $selected_menu = 'productos'; // marca el ítem activo del menú
protected function init() {
$retVal = parent::init(); // carga $this->usuario, reCAPTCHA…
if ($retVal instanceof Response) {
return $retVal;
}
WebElements::addScript(Koshkil::getThemePath('js/productos.js'));
}
// GET /productos
public function index() {
$pagina = max(1, intval($this->Request->pagina));
$productos = TMProductos::order('prd_nombre')->pageSize(20)->page($pagina)->get();
$this->set([
'productos' => $productos,
'total' => $productos->totalRecords,
'webpageTitle' => 'Productos',
]);
}
// GET /productos/ver/15
public function ver($id = null) {
$producto = TMProductos::find(intval($id));
if (!$producto) {
$this->Response->setCode(404);
return;
}
$this->set(['producto' => $producto]);
}
}
Propiedades
| Propiedad | Uso |
|---|---|
$Request |
Datos de GET y POST (ver abajo). |
$Response |
Código, cuerpo, tipo MIME. |
$Session |
Sesión del usuario. |
$view |
Vista Smarty. Se completa con $this->set([...]). |
$usuario |
Usuario logueado (TMUsuarios) o null. |
$templateFile |
Layout que envuelve la plantilla (main.tpl). |
$templateFolder |
Carpeta de plantillas dentro del theme (front, admin). |
$openAccess |
true: no requiere login. |
$roles, $rules, $strictRules |
Permisos necesarios (ver Seguridad). |
$lifeCycle |
Hooks que se ejecutan en orden: ["dispatch","init","run"]. |
$selected_menu |
Clave del menú activo (la usan los widgets de cabecera). |
Pasar datos a la vista
$this->set([
'producto' => $producto,
'webpageTitle' => $producto->prd_nombre, // lo usa main.tpl en <title>
'template' => 'productos/ficha', // otra plantilla en vez de productos/ver
]);
Variables con significado especial:
| Variable | Efecto |
|---|---|
template |
Plantilla a incluir (sin .tpl, relativa a la carpeta del theme). |
class |
Clase CSS del <main> (si el layout la usa). |
data_controller |
Atributo data-controller del <main>; los módulos JavaScript del núcleo lo usan para arrancar. |
Request
$this->Request->email; // $_GET o $_POST (POST tiene prioridad)
$this->Request->get('email');
$this->Request['email']; // también funciona como arreglo
$this->Request->getData(); // todo
$this->Request->isPost();
$this->Request->isAjax();
$this->Request->isEmpty('email');
$this->Request->hasFiles();
$this->Request->file('foto'); // ['name','type','tmp_name','error','size']
$this->Request->isUploadedFile('foto');
$this->Request->uploadError('foto');
Los datos del Request no están escapados
El constructor de consultas escapa los valores de where() y having(), pero no el SQL que escribís a mano. Validá los datos (intval(), listas permitidas) y leé Seguridad en las consultas.
Response
$this->Response->setCode(404); // 200, 404, 4xx, 500…
$this->Response->setMessage('Detalle'); // lo ven las plantillas de error
$this->Response->setMimeType('application/json');
$this->Response->setBody(json_encode($datos)); // con cuerpo, se envía tal cual
$this->Response->asFile('reporte.csv'); // descarga (con setBody)
$this->Response->redirect('/cuentas'); // redirige y termina
Mensajes al usuario
Los layouts muestran flash_message y error_message (en los themes incluidos, con toastr). Si se guardan en la sesión, sobreviven a una redirección:
$this->Session->write('flash_message', 'Datos guardados.');
return $this->Response->redirect('/cuentas');
Para rellenar un formulario después de un error, Koshkil::old('campo') devuelve el valor enviado en el POST.
Controladores AJAX
Heredan de FrontAjaxController (o AdminAjaxController) y devuelven JSON con jsonize(). Como en src/Controllers/Ajax/ hay una carpeta, sus URLs empiezan con /ajax/.
<?php
// src/Controllers/Ajax/ProductosController.php -> POST /ajax/productos/buscar
Koshkil::Uses('com.FrontAjaxController');
Koshkil::UsesModel('productos');
class ProductosController extends FrontAjaxController {
public function buscar() {
if (!$this->recaptchaOk()) {
return $this->jsonize(['status' => 'error', 'message' => $this->recaptchaError()]);
}
$texto = (string)$this->Request->q; // where() escapa el valor
$items = TMProductos::where('prd_nombre', 'like', "%{$texto}%")->take(10)->getAsArray();
return $this->jsonize(['status' => 'success', 'items' => $items]);
}
}
Componentes
Un componente es lógica reutilizable entre controladores. Vive en src/Controllers/Components/<Nombre>Component.php, extiende Component y recibe el controlador en initialize().
<?php
Koshkil::uses('sys.web.Controller.Component');
class CarritoComponent extends Component {
public function total() {
return array_sum($this->controller->Session->read('carrito') ?: []);
}
}
public function create() { // create() es público en Controller
$this->loadComponent('Carrito'); // src/Controllers/Components/CarritoComponent.php
}
public function index() {
$this->set(['total' => $this->Carrito->total()]);
}
Respuestas JavaScript
Una URL que termina en .js y no es un archivo real ejecuta la acción javascript del controlador: /js/recaptcha.js llama a Js/RecaptchaController::javascript(). Heredá de FrontJavascriptController para que la respuesta salga con tipo text/javascript.