The PHP framework behind SitioYa

Koshkil documentation

Services

REST API

How it works

The REST API has a single entry point, /api (ApiController). Every call is a POST with:

Parameter Contents
username, password Credentials of a system user (the tenant).
action class.method for the core, or plugins.<plugin>.class.method.
the rest The action's parameters.
curl -X POST https://mysite.com/api \
  -d username=apiuser -d password=secret \
  -d action=usuarios.obtenerPerfilUsuario -d usr_codigo=12 -d language=en
{ "status": "ok", "data": { "profile": { "…": "…" } } }
status Meaning
ok Success; the data is in data.
error The operation failed (credentials, validation, missing record…). Includes message.
fail The call is malformed (unknown action, inactive plugin…).

The language parameter changes the language of that call's messages.

Action resolution

Action File Class
usuarios.loginUsuario src/api/usuarios.php UsuariosApiController::loginUsuario()
plugins.noticias.manager.obtenerNoticias plugins/noticias/src/Api/manager.php ManagerApiController::obtenerNoticias()

Only public methods declared in the class itself can be called, and the class must extend RestProvider. Names are validated as identifiers, so an action cannot point to another file.

Batch calls

action=batch.call runs several actions in one request. bulk is an object (or a JSON string) with one key per action and its parameters; anything sent outside bulk is shared by every call. returnAs changes a result's key (to call the same action twice).

curl -X POST https://mysite.com/api -d username= -d password= \
  -d action=batch.call \
  --data-urlencode 'bulk={"plugins.tablas.paises.obtenerPaises":{},
                         "plugins.noticias.manager.obtenerNoticias":{"limit":5,"returnAs":"latest"}}'

Writing an endpoint

<?php
// src/api/productos.php  ->  productos.* actions
Koshkil::uses('sys.web.Rest.RestProvider');
Koshkil::usesModel('productos');

class ProductosApiController extends RestProvider {

    // action=productos.listar&limit=10&offset=0&categoria=3
    public function listar() {
        $qb = TMProductos::where('usr_codigo', $this->apiUser()->usr_codigo)
            ->order('prd_nombre');
        if ($categoria = $this->intParam('categoria')) {
            $qb->where('cat_codigo', $categoria);
        }
        $total = $this->countRows(clone $qb);
        $items = $this->paginate($qb)->getAsArray();
        return $this->successResponse(['products' => $items, 'total' => $total]);
    }

    // action=productos.obtener&prd_codigo=15
    public function obtener() {
        $producto = TMProductos::where('prd_codigo', $this->intParam('prd_codigo'))
            ->where('usr_codigo', $this->apiUser()->usr_codigo)
            ->first();
        if (!$producto) {
            return $this->errorResponse('productos.not_found');   // key in the "api" domain
        }
        return $this->successResponse(['product' => $this->onlyRequestedFields($producto->record())]);
    }
}

RestProvider helpers

Method Purpose
apiUser() The authenticated user (the tenant). Always filter by it.
params(), param($k, $def) The call's parameters.
intParam($k), arrayParam($k), intList($v) Typed parameters.
esc($v) Escapes a value for hand-written SQL. Not needed with where(), which already escapes.
paginate($qb) Applies the limit and offset parameters.
countRows($qb) Counts a query's records.
onlyRequestedFields($r) Filters fields according to shortVersion.
translated($record), requestedLanguage() Translatable content.
findSubUser($value, $column) Finds a sub-user of the tenant.
baseUrl(), absoluteUrl($path) Absolute URLs of the tenant's site.
successResponse($data, $msg) ['status' => 'ok', 'data' => …]
errorResponse($key, $params) ['status' => 'error', 'message' => …] (message from the api domain)
failResponse($key, $params) ['status' => 'fail', …]

In-process use, without HTTP

The site can consume its own API without making a request, through ApiService. The client credentials go in config/domains/<domain>/api.php:

<?php
$API = [
    'default' => ['username' => 'apiuser', 'password' => '…'],
    // 'mobile' => ['username' => '…', 'password' => '…'],
];
Koshkil::uses('sys.web.Rest.ApiService');

$api = ApiService::domain();                    // the current domain's 'default' client
$r = $api->call('plugins.noticias.manager.obtenerNoticias', ['limit' => 5]);
if ($r['status'] == 'ok') { $news = $r['data']['news']; }

$r = $api->batch([
    'plugins.tablas.paises.obtenerPaises' => [],
    'plugins.noticias.etiquetas.obtenerEtiquetas' => [],
]);

SuperController and FrontAjaxController already include ApiAccessTrait:

$response = $this->api('usuarios.loginUsuario', ['subuser' => $email, 'pass' => $password]);
$user     = $this->apiData($response, 'users');   // data['users'], or null on error

This way the site and external applications share exactly the same logic and validations.