The PHP framework behind SitioYa

Koshkil documentation

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.