Skip to main content

13. Características avanzadas

13.1. Transacciones atómicas

El framework implementa soporte para transacciones atómicas a nivel de base de datos que garantizan que un conjunto de operaciones se ejecuten completamente o no se ejecuten en absoluto, manteniendo la integridad de los datos.

Implementación de transacciones SQL

Las transacciones a nivel de base de datos están implementadas mediante métodos específicos que interactúan con el sistema subyacente de Quartup:

public function startTransaction()
{
try {
$ko = CoreQuartup::startTransaction();
if ($ko) {
throw new Exception($ko, 500);
}
} catch (\Throwable $th) {
throw $th;
}
}

public function commitTransaction()
{
try {
$err = '';
CoreQuartup::finishTransaction($err);
if ($err) {
throw new Exception($err, 500);
}
} catch (\Throwable $th) {
throw $th;
}
}

public function rollbackTransaction($err)
{
try {
CoreQuartup::finishTransaction($err);
if ($err) {
throw new Exception($err, 400);
}
} catch (\Throwable $th) {
throw $th;
}
}

El sistema subyacente CoreQuartup maneja la comunicación real con la base de datos:

public static function startTransaction()
{
self::getQuBasic();
// abrimos transacción
// si retorna string vacío se abrió bien
// si retorna cualquier otra cosa es un error al abrir transacción
return Pool::start();
}

public static function finishTransaction($error = '')
{
self::getQuBasic();
// cerrar transacción
// Si error es un string vacío finish intentará
// hacer commit y si todo va bien devuelve string vacío
return Pool::finish($error);
}

Uso de transacciones en repositorios

Los repositorios ofrecen una interfaz más amigable para utilizar transacciones mediante una función de alto nivel que acepta una función callback:

public function transaction($callable)
{
$this->model->startTransaction();
try {
$res = call_user_func($callable);
$this->model->commitTransaction();
return $res;
} catch (\Throwable $th) {
$this->model->rollbackTransaction($th->getMessage());
throw new Exception("Error durante la transacción: " . $th->getMessage(), 500, $th);
}
}

Ejemplo de uso de transacción atómica

public function transferirFondos($cuentaOrigenId, $cuentaDestinoId, $monto)
{
return $this->cuentaRepository->transaction(function() use ($cuentaOrigenId, $cuentaDestinoId, $monto) {
$origen = $this->cuentaRepository->selectById($cuentaOrigenId);
$destino = $this->cuentaRepository->selectById($cuentaDestinoId);

if ($origen['saldo'] < $monto) {
throw new Exception("Saldo insuficiente");
}

$this->cuentaRepository->update($cuentaOrigenId, [
'saldo' => $origen['saldo'] - $monto
]);

$this->cuentaRepository->update($cuentaDestinoId, [
'saldo' => $destino['saldo'] + $monto
]);

return true;
});
}

13.2. Registro de actividad y auditoría

El framework implementa un sistema de registro de actividad que documenta todas las operaciones realizadas a través de la API.

Arquitectura del sistema de registro

El framework utiliza middlewares para capturar el ciclo completo de una solicitud HTTP:

// TransactionStartMiddleware.php - Captura el inicio de la operación
public function handle(Request $request, callable $next)
{
$transaction = [
'method' => $request->getMethod(),
'url' => $request->getUri(),
'timestamp' => new \DateTime(),
'host' => $request->getHost(),
'origin' => $request->headers->get('origin'),
'referer' => $request->headers->get('referer'),
'user_agent' => $request->headers->get('user-agent'),
'ip' => $request->getClientIp(),
'ips' => $request->getClientIps(),
'nombre_usuario' => null,
'cod_usuario_ext' => null,
'num_usuario' => null,
'actions' => [],
'payload' => [
'body' => $request->request->all(),
'query' => $request->query->all(),
],
'request_id' => generateUUID()
];

$request->attributes->set('transaction', $transaction);
$request->attributes->set('actions', []);

return $next($request);
}
// TransactionEndMiddleware.php - Completa la información después de procesar la solicitud
public function handle(Request $request, callable $next)
{
// Llama al siguiente middleware y captura la respuesta
$response = $next($request);
// Después de la lógica de negocio
$str = $response->getContent();
$res = json_decode($str, true);

$transaction = $request->attributes->get('transaction');
$params = $request->attributes->get('params');
$transaction['response'] = $res;
$transaction['payload']['params'] = $params;
$transaction['payload']['content'] = $request->getContent();
$transaction['actions'] = QuApp::getActions();

$transaction['nombre_usuario'] = $request->attributes->get("nombre_usuario");
$transaction['cod_usuario_ext'] = $request->attributes->get("cod_usuario_ext");
$transaction['num_usuario'] = $request->attributes->get("num_usuario");

$request->attributes->set('transaction', $transaction);
return $response;
}

Registro de acciones específicas

Durante una operación, se registran las acciones específicas realizadas:

public function store(array $request)
{
$entity = $this->model->store($request);

$action = [$this->model->table => ['stored' => $entity]];
$actions = QuApp::getActions();
$actions[] = $action;
QuApp::setActions($actions);

return $this->selectById($entity['id']);
}
public function update($id, array $request)
{
$res = $this->model->update($id, $request);
return $this->_update($res);
}

private function _update(array $res)
{
$action = [$this->model->table => ['updated' => [
"id" => $res["id"],
'newdata' => $res["newData"],
'olddata' => $res["oldData"]
]]];
$actions = QuApp::getActions();
$actions[] = $action;
QuApp::setActions($actions);
return $this->selectById($res['id']);
}

Almacenamiento de registros de actividad

El registro de actividad se almacena usando el servicio StoreTransactionService:

class StoreTransactionLog implements StoreTransactionServiceInterface
{
public function handle(array $transaction)
{
// Lógica de envío
Log::transaction($transaction);
}
}

La clase Log guarda estos registros en archivos JSON:

public static function transaction($transaction)
{
$instance = self::getInstance();
$instance->writeTransaction($transaction);
}

protected function writeTransaction($message)
{
$filename = $this->logDirectory . 'transaction_' . date('Y-m-d') . '.json';

// Creamos un arreglo con la entrada de log
$logEntry = $message;

// Verificamos si el archivo ya existe
if (file_exists($filename)) {
// Leemos el contenido actual del archivo
$currentContent = file_get_contents($filename);
// Decodificamos el JSON a un arreglo PHP
$entries = json_decode($currentContent, true);
if (!is_array($entries)) {
// Si el contenido no es un arreglo, creamos uno nuevo
$entries = [];
}
} else {
// Si el archivo no existe, creamos un nuevo arreglo
$entries = [];
}

// Añadimos la nueva entrada al arreglo de entradas
$entries[] = $logEntry;

// Codificamos el arreglo a JSON
$jsonContent = json_encode($entries, JSON_PRETTY_PRINT);

// Guardamos el JSON en el archivo
file_put_contents($filename, $jsonContent);
}

13.3. Filtrado dinámico

Una característica avanzada del framework es su sistema de filtrado dinámico mediante JSON, que permite construir condiciones de filtrado complejas.

Motor de filtrado

La clase QueryBuilderFilters procesa los filtros expresados en JSON y los convierte en condiciones SQL:

class QueryBuilderFilters
{
protected $str = "";
protected $arr = [];

public function __construct($array, $map)
{
$replaced = $this->replaceFields($array, array_flip($map));
$this->arr = $replaced;
$this->run();
}

protected function parseCondition($array)
{
$conditions = [];
foreach ($array as $key => $value) {
// Manejar operadores lógicos $and, $or
if ($key === '$and' || $key === '$or') {
$nestedConditions = array_map([$this, 'parseCondition'], $value);
$logico = $this->parsekey($key);
$conditions[] = '(' . implode(" $logico ", $nestedConditions) . ')';
} elseif ($key === '$not') { // Manejar $not
$nestedConditions = array_map([$this, 'parseCondition'], $value);
$conditions[] = 'NOT (' . implode(" ", $nestedConditions) . ')';
} else {
// Procesamiento de operadores específicos
foreach ($value as $operator => $val) {
$this->isValidOperator(strtolower($operator));

if (strtolower($operator) === 'like') {
$val = "%$val%";
$conditions[] = "$key $operator '$val'";
continue;
}

if (strtolower($operator) === 'not like') {
$val = "%$val%";
$conditions[] = "$key $operator '$val'";
continue;
}

if (strtolower($operator) === "in") {
$val = $this->convertArrayInToString($val);
$conditions[] = "$key $operator $val";
continue;
}

// Otros operadores...

$conditions[] = "$key $operator '$val'";
}
}
}
return implode(' AND ', $conditions);
}

// Otros métodos de procesamiento...
}

Formato de filtrado JSON

Los filtros se especifican en formato JSON con esta estructura:

{
"$and": [
{ "campo1": { "like": "valor" } },
{
"$or": [
{ "campo2": { "=": "texto" } },
{ "campo3": { ">": 10 } }
]
}
]
}

Los operadores disponibles incluyen:

  • Operadores de comparación: =, !=, >, <, >=, <=
  • Operadores de texto: like, not like
  • Operadores de colección: in, not in
  • Operadores lógicos: $and, $or, $not
  • Operadores de rango: between
  • Operadores de nulidad: is null, is not null

Validación de operadores

El sistema valida los operadores para prevenir inyecciones SQL:

protected function isValidOperator($operator, $validOperators = null)
{
if ($validOperators === null) {
$validOperators = ['=', '!=', '<>', '>', '<', '>=', '<=', 'like', 'not like', 'is null', 'is not null', 'in', 'not in', 'between', '$and', '$or', '$not'];
}
if (in_array($operator, $validOperators)) {
return true;
}

throw new \Exception("operador sql '{$operator}' no es valido");
}

13.4. Paginación y ordenamiento

El framework implementa un sistema avanzado de paginación y ordenamiento que se integra perfectamente con el ciclo de solicitud-respuesta.

Paginación

El sistema de paginación procesa parámetros como page e itemsPerPage:

$page = $request["page"] ?? 1;
$itemsPerPage = $request["itemsPerPage"] ?? 30;
// ...
$totalPages = (int) ceil($totalFilteredRecords / $itemsPerPage);
$page = max(min($page, $totalPages), 1);
$offset = ($page - 1) * $itemsPerPage;

La respuesta incluye metadatos de paginación:

return [
'collection' => $recordsForCurrentPage,
'totalRows' => (int) $totalRecords,
'totalFilteredRows' => (int) $totalFilteredRecords,
'pages' => (int) $totalPages,
'page' => (int) $page,
'itemsPerPage' => (int) $itemsPerPage,
];

Ordenamiento

El sistema de ordenamiento procesa parámetros como sortBy y sortDesc:

if (isset($request["sortDesc"]) && isset($request["sortBy"])) {
$order = [
"by" => json_decode($request["sortBy"]),
"desc" => json_decode($request["sortDesc"]),
];
}

Estos parámetros se transmiten al constructor de consultas:

public function order($orderArray)
{
if ($orderArray) {
$orderByClause = '';

if (
!empty($orderArray['by']) && is_array($orderArray['by']) &&
!empty($orderArray['desc']) && is_array($orderArray['desc']) &&
count($orderArray['by']) == count($orderArray['desc'])
) {
$orderParts = [];
foreach ($orderArray['by'] as $index => $column) {
$direction = $orderArray['desc'][$index] ? 'DESC' : 'ASC';
$orderParts[] = "$column $direction";
}

$orderByClause = 'ORDER BY ' . implode(', ', $orderParts);
}

$this->orderByClause = $orderByClause;
}
return $this;
}

Filtros permanentes

Un concepto avanzado es el de "filtros permanentes", que se aplican automáticamente antes de cualquier filtro especificado por el usuario:

// Combinar permanentFilters con filters para el conteo filtrado
$combinedFilters = $this->combineFilters($permanentFilters, $filters);

Estos filtros permanentes permiten implementar restricciones de seguridad o lógica de negocio a nivel de filtrado que serán aplicadas independientemente de las solicitudes del cliente:

private function combineFilters($permanentFilters, $filters)
{
// Si no hay permanentFilters, devolvemos filters tal cual
if (!$permanentFilters) {
return $filters;
}

// Si no hay filters, devolvemos permanentFilters tal cual
if (!$filters) {
return $permanentFilters;
}

// Convertir a array si son strings
$permanentFiltersArray = is_string($permanentFilters)
? json_decode($permanentFilters, true)
: $permanentFilters;

// ...

// Combinar los filtros con operador AND
$combinedFilters = [
'$and' => [$permanentFiltersArray, $filtersArray]
];

return json_encode($combinedFilters);
}

Con estas características avanzadas, el framework proporciona una infraestructura robusta para desarrollar aplicaciones complejas con necesidades de transaccionalidad a nivel de base de datos, registro de actividad para auditoría, filtrado dinámico de datos y paginación/ordenamiento, manteniendo siempre un modelo de programación coherente y fácil de usar.