O framework PHP por trás do SitioYa

Documentação do Koshkil

Serviços

API REST

Como funciona

A API REST tem um único ponto de entrada, /api (ApiController). Cada chamada é um POST com:

Parâmetro Conteúdo
username, password Credenciais de um usuário do sistema (o tenant).
action classe.método para o núcleo, ou plugins.<plugin>.classe.método.
o resto Parâmetros da ação.
curl -X POST https://meusite.com/api \
  -d username=apiuser -d password=segredo \
  -d action=usuarios.obtenerPerfilUsuario -d usr_codigo=12 -d language=pt
{ "status": "ok", "data": { "profile": { "…": "…" } } }
status Significado
ok Sucesso; os dados estão em data.
error Erro da operação (credenciais, validação, registro inexistente…). Inclui message.
fail A chamada está malformada (ação inexistente, plugin inativo…).

O parâmetro language muda o idioma das mensagens daquela chamada.

Resolução da ação

Ação Arquivo Classe
usuarios.loginUsuario src/api/usuarios.php UsuariosApiController::loginUsuario()
plugins.noticias.manager.obtenerNoticias plugins/noticias/src/Api/manager.php ManagerApiController::obtenerNoticias()

Só podem ser chamados métodos públicos declarados na própria classe, que deve estender RestProvider. Os nomes são validados como identificadores, então a ação não pode apontar para outro arquivo.

Chamadas em lote

action=batch.call executa várias ações em uma única requisição. bulk é um objeto (ou um JSON) com uma chave por ação e seus parâmetros; o que for enviado fora de bulk é compartilhado por todas. returnAs muda a chave do resultado (para chamar a mesma ação duas vezes).

curl -X POST https://meusite.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"}}'

Escrever um endpoint

<?php
// src/api/productos.php  ->  ações 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');   // chave do domínio "api"
        }
        return $this->successResponse(['product' => $this->onlyRequestedFields($producto->record())]);
    }
}

Ajudas do RestProvider

Método Uso
apiUser() Usuário autenticado (o tenant). Filtre sempre por ele.
params(), param($k, $def) Parâmetros da chamada.
intParam($k), arrayParam($k), intList($v) Parâmetros convertidos.
esc($v) Escapa um valor para escrevê-lo dentro de SQL feito à mão. Não é necessário com where(), que já escapa.
paginate($qb) Aplica limit e offset dos parâmetros.
countRows($qb) Conta os registros de uma consulta.
onlyRequestedFields($r) Filtra campos conforme shortVersion.
translated($record), requestedLanguage() Conteúdo traduzível.
findSubUser($valor, $coluna) Busca um subusuário do tenant.
baseUrl(), absoluteUrl($caminho) URLs absolutas do site do tenant.
successResponse($data, $msg) ['status' => 'ok', 'data' => …]
errorResponse($chave, $params) ['status' => 'error', 'message' => …] (mensagem do domínio api)
failResponse($chave, $params) ['status' => 'fail', …]

Uso interno, sem HTTP

O site pode consumir a própria API sem fazer uma requisição, com o ApiService. As credenciais do cliente ficam em config/domains/<domínio>/api.php:

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

$api = ApiService::domain();                    // cliente 'default' do domínio atual
$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 e FrontAjaxController já incluem o ApiAccessTrait:

$resposta = $this->api('usuarios.loginUsuario', ['subuser' => $email, 'pass' => $senha]);
$usuario  = $this->apiData($resposta, 'users');   // data['users'], ou null se houve erro

Assim, o site e as aplicações externas usam exatamente a mesma lógica e as mesmas validações.