Core
Controllers
Hierarchy
Controller (core/web)
├── SuperController (src) public site: templateFolder "front", open access
│ └── IndexController, CuentasController, DocsController…
├── AdminSuperController (src) admin panel: templateFolder "admin", login required
│ └── Admin/Sistema/UsuariosController…
├── AjaxController (core/web) JSON responses
│ ├── FrontAjaxController (src)
│ └── AdminAjaxController (src)
├── JavascriptController (core/web) JavaScript responses
│ ├── FrontJavascriptController (src)
│ └── AdminJavascriptController (src)
└── ApiController (src) REST API entry point
Always extend the matching class from src/, not the ones in core/: that is where the session user, the menu and each area's shared data are loaded.
A complete controller
<?php
Koshkil::Uses('com.SuperController');
Koshkil::UsesModel('productos');
class ProductosController extends SuperController {
public $selected_menu = 'productos'; // marks the active menu item
protected function init() {
$retVal = parent::init(); // loads $this->usuario, reCAPTCHA…
if ($retVal instanceof Response) {
return $retVal;
}
WebElements::addScript(Koshkil::getThemePath('js/productos.js'));
}
// GET /productos
public function index() {
$page = max(1, intval($this->Request->pagina));
$productos = TMProductos::order('prd_nombre')->pageSize(20)->page($page)->get();
$this->set([
'productos' => $productos,
'total' => $productos->totalRecords,
'webpageTitle' => 'Products',
]);
}
// 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]);
}
}
Properties
| Property | Purpose |
|---|---|
$Request |
GET and POST data (see below). |
$Response |
Status code, body, MIME type. |
$Session |
The user's session. |
$view |
Smarty view. Filled with $this->set([...]). |
$usuario |
Logged-in user (TMUsuarios) or null. |
$templateFile |
Layout that wraps the template (main.tpl). |
$templateFolder |
Template folder inside the theme (front, admin). |
$openAccess |
true: no login required. |
$roles, $rules, $strictRules |
Required permissions (see Security). |
$lifeCycle |
Hooks run in order: ["dispatch","init","run"]. |
$selected_menu |
Key of the active menu item (used by the header widgets). |
Passing data to the view
$this->set([
'producto' => $producto,
'webpageTitle' => $producto->prd_nombre, // used by main.tpl in <title>
'template' => 'productos/ficha', // another template instead of productos/ver
]);
Variables with a special meaning:
| Variable | Effect |
|---|---|
template |
Template to include (without .tpl, relative to the theme folder). |
class |
CSS class of <main> (if the layout uses it). |
data_controller |
data-controller attribute of <main>; the core JavaScript modules use it to boot. |
Request
$this->Request->email; // $_GET or $_POST (POST wins)
$this->Request->get('email');
$this->Request['email']; // also works as an array
$this->Request->getData(); // everything
$this->Request->isPost();
$this->Request->isAjax();
$this->Request->isEmpty('email');
$this->Request->hasFiles();
$this->Request->file('photo'); // ['name','type','tmp_name','error','size']
$this->Request->isUploadedFile('photo');
$this->Request->uploadError('photo');
Request data is not escaped
The query builder escapes the values given to where() and having(), but not SQL you write by hand. Validate data (intval(), allow-lists) and read Query safety.
Response
$this->Response->setCode(404); // 200, 404, 4xx, 500…
$this->Response->setMessage('Details'); // shown by the error templates
$this->Response->setMimeType('application/json');
$this->Response->setBody(json_encode($data)); // with a body, it is sent as is
$this->Response->asFile('report.csv'); // download (with setBody)
$this->Response->redirect('/cuentas'); // redirects and stops
Messages for the user
Layouts show flash_message and error_message (with toastr in the bundled themes). When stored in the session, they survive a redirect:
$this->Session->write('flash_message', 'Data saved.');
return $this->Response->redirect('/cuentas');
To refill a form after an error, Koshkil::old('field') returns the value sent in the POST.
AJAX controllers
They extend FrontAjaxController (or AdminAjaxController) and return JSON with jsonize(). Since src/Controllers/Ajax/ is a folder, their URLs start with /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()]);
}
$text = (string)$this->Request->q; // where() escapes the value
$items = TMProductos::where('prd_nombre', 'like', "%{$text}%")->take(10)->getAsArray();
return $this->jsonize(['status' => 'success', 'items' => $items]);
}
}
Components
A component is logic shared between controllers. It lives in src/Controllers/Components/<Name>Component.php, extends Component and receives the controller in 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() is public in Controller
$this->loadComponent('Carrito'); // src/Controllers/Components/CarritoComponent.php
}
public function index() {
$this->set(['total' => $this->Carrito->total()]);
}
JavaScript responses
A URL ending in .js that is not a real file runs the controller's javascript action: /js/recaptcha.js calls Js/RecaptchaController::javascript(). Extend FrontJavascriptController so the response is sent as text/javascript.