O framework PHP por trás do SitioYa

Documentação do Koshkil

Núcleo

Controladores

Hierarquia

Controller (core/web)
├── SuperController (src)            site público: templateFolder "front", acesso aberto
│   └── IndexController, CuentasController, DocsController…
├── AdminSuperController (src)       painel: templateFolder "admin", exige login
│   └── Admin/Sistema/UsuariosController…
├── AjaxController (core/web)        respostas JSON
│   ├── FrontAjaxController (src)
│   └── AdminAjaxController (src)
├── JavascriptController (core/web)  respostas JavaScript
│   ├── FrontJavascriptController (src)
│   └── AdminJavascriptController (src)
└── ApiController (src)              ponto de entrada da API REST

Sempre estenda a classe de src/ correspondente, não as de core/: é nelas que se carregam o usuário da sessão, o menu e os dados comuns de cada área.

Um controlador completo

<?php
Koshkil::Uses('com.SuperController');
Koshkil::UsesModel('productos');

class ProductosController extends SuperController {

    public $selected_menu = 'productos';     // marca o item ativo do menu

    protected function init() {
        $retVal = parent::init();             // carrega $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' => 'Produtos',
        ]);
    }

    // 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]);
    }
}

Propriedades

Propriedade Uso
$Request Dados de GET e POST (veja abaixo).
$Response Código, corpo, tipo MIME.
$Session Sessão do usuário.
$view View Smarty. Preenchida com $this->set([...]).
$usuario Usuário logado (TMUsuarios) ou null.
$templateFile Layout que envolve o template (main.tpl).
$templateFolder Pasta de templates dentro do tema (front, admin).
$openAccess true: não exige login.
$roles, $rules, $strictRules Permissões necessárias (veja Segurança).
$lifeCycle Hooks executados em ordem: ["dispatch","init","run"].
$selected_menu Chave do item de menu ativo (usada pelos widgets de cabeçalho).

Passar dados para a view

$this->set([
    'producto'     => $producto,
    'webpageTitle' => $producto->prd_nombre,   // usado pelo main.tpl no <title>
    'template'     => 'productos/ficha',        // outro template no lugar de productos/ver
]);

Variáveis com significado especial:

Variável Efeito
template Template a incluir (sem .tpl, relativo à pasta do tema).
class Classe CSS do <main> (se o layout a usar).
data_controller Atributo data-controller do <main>; os módulos JavaScript do núcleo o usam para iniciar.

Request

$this->Request->email;             // $_GET ou $_POST (o POST tem prioridade)
$this->Request->get('email');
$this->Request['email'];           // também funciona como array
$this->Request->getData();         // tudo
$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');

Os dados do Request não são escapados

O construtor de consultas escapa os valores de where() e having(), mas não o SQL que você escreve à mão. Valide os dados (intval(), listas permitidas) e leia Segurança nas consultas.

Response

$this->Response->setCode(404);                  // 200, 404, 4xx, 500…
$this->Response->setMessage('Detalhe');           // exibido pelos templates de erro
$this->Response->setMimeType('application/json');
$this->Response->setBody(json_encode($dados));    // com corpo, é enviado como está
$this->Response->asFile('relatorio.csv');         // download (com setBody)
$this->Response->redirect('/cuentas');            // redireciona e encerra

Mensagens para o usuário

Os layouts exibem flash_message e error_message (nos temas incluídos, com toastr). Se forem guardadas na sessão, sobrevivem a um redirecionamento:

$this->Session->write('flash_message', 'Dados salvos.');
return $this->Response->redirect('/cuentas');

Para preencher de novo um formulário após um erro, Koshkil::old('campo') retorna o valor enviado no POST.

Controladores AJAX

Estendem FrontAjaxController (ou AdminAjaxController) e retornam JSON com jsonize(). Como src/Controllers/Ajax/ é uma pasta, suas URLs começam com /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;           // o where() escapa o valor
        $items = TMProductos::where('prd_nombre', 'like', "%{$texto}%")->take(10)->getAsArray();
        return $this->jsonize(['status' => 'success', 'items' => $items]);
    }
}

Componentes

Um componente é lógica compartilhada entre controladores. Fica em src/Controllers/Components/<Nome>Component.php, estende Component e recebe o controlador em 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() é público em Controller
    $this->loadComponent('Carrito');          // src/Controllers/Components/CarritoComponent.php
}

public function index() {
    $this->set(['total' => $this->Carrito->total()]);
}

Respostas JavaScript

Uma URL que termina em .js e não é um arquivo real executa a ação javascript do controlador: /js/recaptcha.js chama Js/RecaptchaController::javascript(). Estenda FrontJavascriptController para que a resposta saia com o tipo text/javascript.