El framework PHP detrás de SitioYa

Documentación de Koshkil

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.