El framework PHP detrás de SitioYa

Documentación de Koshkil

Servicios

API REST

Cómo funciona

La API REST tiene un único punto de entrada, /api (ApiController). Cada llamada es un POST con:

Parámetro Contenido
username, password Credenciales de un usuario del sistema (el tenant).
action clase.método para el núcleo, o plugins.<plugin>.clase.método.
el resto Parámetros de la acción.
curl -X POST https://misitio.com/api \
  -d username=apiuser -d password=secreto \
  -d action=usuarios.obtenerPerfilUsuario -d usr_codigo=12 -d language=en
{ "status": "ok", "data": { "profile": { "…": "…" } } }
status Significado
ok Éxito; los datos están en data.
error Error de la operación (credenciales, validación, registro inexistente…). Incluye message.
fail La llamada está mal formada (acción inexistente, plugin inactivo…).

El parámetro language cambia el idioma de los mensajes de esa llamada.

Resolución de la acción

Acción Archivo Clase
usuarios.loginUsuario src/api/usuarios.php UsuariosApiController::loginUsuario()
plugins.noticias.manager.obtenerNoticias plugins/noticias/src/Api/manager.php ManagerApiController::obtenerNoticias()

Solo se pueden llamar métodos públicos declarados en la propia clase, que debe extender RestProvider. Los nombres se validan como identificadores, así que la acción no puede apuntar a otro archivo.

Llamadas en lote

action=batch.call ejecuta varias acciones en una sola petición. bulk es un objeto (o un JSON) con una clave por acción y sus parámetros; lo que se envía fuera de bulk se comparte entre todas. returnAs cambia la clave del resultado (para llamar dos veces a la misma acción).

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

Escribir un endpoint

<?php
// src/api/productos.php  ->  acciones productos.*
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');   // clave del dominio "api"
        }
        return $this->successResponse(['product' => $this->onlyRequestedFields($producto->record())]);
    }
}

Ayudas de RestProvider

Método Uso
apiUser() Usuario autenticado (el tenant). Filtrá siempre por él.
params(), param($k, $def) Parámetros de la llamada.
intParam($k), arrayParam($k), intList($v) Parámetros convertidos.
esc($v) Escapa un valor para escribirlo dentro de SQL a mano. No hace falta con where(), que ya escapa.
paginate($qb) Aplica limit y offset de los parámetros.
countRows($qb) Cuenta registros de una consulta.
onlyRequestedFields($r) Filtra campos según shortVersion.
translated($record), requestedLanguage() Contenido traducible.
findSubUser($valor, $columna) Busca un subusuario del tenant.
baseUrl(), absoluteUrl($ruta) URLs absolutas del sitio del tenant.
successResponse($data, $msg) ['status' => 'ok', 'data' => …]
errorResponse($clave, $params) ['status' => 'error', 'message' => …] (mensaje del dominio api)
failResponse($clave, $params) ['status' => 'fail', …]

Uso interno, sin HTTP

El sitio puede consumir su propia API sin hacer una petición, con ApiService. Las credenciales del cliente van en config/domains/<dominio>/api.php:

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

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

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

SuperController y FrontAjaxController ya incluyen ApiAccessTrait:

$respuesta = $this->api('usuarios.loginUsuario', ['subuser' => $email, 'pass' => $clave]);
$usuario   = $this->apiData($respuesta, 'users');   // data['users'] o null si hubo error

Así el sitio y las aplicaciones externas usan exactamente la misma lógica y las mismas validaciones.