Co-authored-by: Juan Pablo Vial <jpvialb@incoviba.cl>
Reviewed-on: #45
This commit is contained in:
2025-10-04 11:40:52 -03:00
parent 6ddc48ec60
commit 742de657c5
815 changed files with 62089 additions and 3287 deletions

View File

@ -22,6 +22,7 @@ class Connection implements Define\Connection
}
return $this;
}
public function getPDO(): PDO
{
$this->connect();

View File

@ -81,6 +81,9 @@ class Insert extends Ideal\Query implements Define\Query\Insert
if ($value === (int) $value) {
return $value;
}
if (str_starts_with($value, ':')) {
return $value;
}
return "'{$value}'";
}, $this->values)) . ')';
}

View File

@ -64,10 +64,10 @@ class Select extends Ideal\Query implements Define\Query\Select
public function having(array|string $conditions): Select
{
if (is_string($conditions)) {
return $this->addCondition($conditions);
return $this->addHaving($conditions);
}
foreach ($conditions as $condition) {
$this->addCondition($condition);
$this->addHaving($condition);
}
return $this;
}
@ -126,7 +126,7 @@ class Select extends Ideal\Query implements Define\Query\Select
}
protected function addCondition(string $condition): Select
{
if (!isset($this->coditions)) {
if (!isset($this->conditions)) {
$this->conditions = [];
}
$this->conditions []= $condition;

View File

@ -6,10 +6,15 @@ use Throwable;
class EmptyResult extends Exception
{
public function __construct(string $query, ?Throwable $previous = null)
public function __construct(public string $query, ?Throwable $previous = null, protected ?array $data = null)
{
$message = "Empty results for {$query}";
$code = 700;
parent::__construct($message, $code, $previous);
}
public function getData(): ?array
{
return $this->data;
}
}

View File

@ -0,0 +1,13 @@
<?php
namespace Incoviba\Common\Implement\Exception;
use Throwable;
use Exception;
class HttpException extends Exception
{
public function __construct(string $message = "", int $code = 0, ?Throwable $previous = null)
{
parent::__construct($message, $code, $previous);
}
}

View File

@ -0,0 +1,19 @@
<?php
namespace Incoviba\Common\Implement\Exception;
use Throwable;
use Exception;
class HttpResponse extends Exception
{
public function __construct($reason = "", $message = "", $code = 0, Throwable $previous = null)
{
$this->reason = "HTTP Reason: {$reason}";
parent::__construct($message, $code, $previous);
}
protected string $reason;
public function getReason(): string
{
return $this->reason;
}
}

View File

@ -0,0 +1,48 @@
<?php
namespace Incoviba\Common\Implement\Log\Formatter;
use Throwable;
use Monolog\Formatter\JsonFormatter;
use Monolog\LogRecord;
class PDO extends JsonFormatter
{
public function __construct(int $batchMode = self::BATCH_MODE_JSON, bool $appendNewline = false, bool $ignoreEmptyContextAndExtra = false, bool $includeStacktraces = true)
{
parent::__construct($batchMode, $appendNewline, $ignoreEmptyContextAndExtra, $includeStacktraces);
}
public function format(LogRecord $record): string
{
if (is_a($record->message, Throwable::class)) {
$exception = $record->message;
$message = $this->normalizeException($exception);
$context = $record->context;
$context['exception'] = $exception;
if ($exception->getPrevious()) {
$context['previous'] = $this->walkException($exception);
}
$new_record = new LogRecord(
$record->datetime,
$record->channel,
$record->level,
json_encode($message),
$context,
$record->extra
);
$record = $new_record;
}
$normalized = $this->normalize($record, $this->maxNormalizeDepth);
return $normalized['message'];
}
protected function walkException(Throwable $exception, int $depth = 0): array
{
$output = [];
$currentDepth = $depth;
while ($previous = $exception->getPrevious() and $currentDepth < $this->maxNormalizeDepth) {
$output []= $this->normalizeException($previous);
}
return $output;
}
}

View File

@ -0,0 +1,116 @@
<?php
namespace Incoviba\Common\Implement\Log\Handler;
use Incoviba\Common\Define\Connection;
use Monolog\Handler\AbstractProcessingHandler;
use Monolog\Level;
use Monolog\LogRecord;
use PDOException;
use PDOStatement;
class MySQL extends AbstractProcessingHandler
{
private bool $initialized = false;
private array $tables = [
'default' => 'monolog',
'deprecated' => 'monolog_deprecated'
];
private array $statements = [
'default' => null,
'deprecated' => null
];
private array $baseQueries = [
'check' => "SHOW TABLES LIKE '%s'",
'create' => "CREATE TABLE IF NOT EXISTS %s (channel VARCHAR(255), level VARCHAR(100), message LONGTEXT, time DATETIME, context LONGTEXT, extra LONGTEXT)",
'insert' => "INSERT INTO %s (channel, level, message, time, context, extra) VALUES (:channel, :level, :message, :time, :context, :extra)",
'delete' => "DELETE FROM %s WHERE time < DATE_SUB(CURDATE(), INTERVAL %d DAY)"
];
public function __construct(protected Connection $connection, protected int $retainDays = 90,
int|string|Level $level = Level::Debug, bool $bubble = true)
{
parent::__construct($level, $bubble);
}
public function write(LogRecord $record): void
{
if (!$this->initialized) {
if (!$this->checkTablesExist()) {
$this->createTables();
}
$this->cleanup();
$this->initialized();
}
if (str_contains(strtolower($record->message), 'deprecated:')) {
$this->statements['deprecated']->execute([
'channel' => $record->channel,
'level' => $record->level->getName(),
'message' => $record->formatted,
'time' => $record->datetime->format('Y-m-d H:i:s.u'),
'context' => (count($record->context) > 0) ? json_encode($record->context, JSON_UNESCAPED_SLASHES) : '',
'extra' => (count($record->extra) > 0) ? json_encode($record->extra, JSON_UNESCAPED_SLASHES) : ''
]);
return;
}
$this->statements['default']->execute([
'channel' => $record->channel,
'level' => $record->level->getName(),
'message' => $record->formatted,
'time' => $record->datetime->format('Y-m-d H:i:s.u'),
'context' => (count($record->context) > 0) ? json_encode($record->context, JSON_UNESCAPED_SLASHES) : '',
'extra' => (count($record->extra) > 0) ? json_encode($record->extra, JSON_UNESCAPED_SLASHES) : ''
]);
}
private function initialized(): void
{
foreach ($this->tables as $type => $table) {
$query = sprintf($this->baseQueries['insert'], $table);
$this->statements[$type] = $this->connection->getPDO()->prepare($query);
}
$this->initialized = true;
}
private function checkTablesExist(): bool
{
return array_all($this->tables, fn($table) => $this->checkTableExists($table));
}
private function checkTableExists(string $table): bool
{
$query = sprintf($this->baseQueries['check'], $table);
try {
$result = $this->connection->query($query);
} catch (PDOException) {
return false;
}
return $result->rowCount() > 0;
}
private function createTables(): void
{
foreach ($this->tables as $table) {
if (!$this->checkTableExists($table)) {
$this->createTable($table);
}
}
}
private function createTable(string $table): void
{
$query = sprintf($this->baseQueries['create'], $table);
try {
$result = $this->connection->getPDO()->exec($query);
if ($result === false) {
throw new PDOException('Failed to create table: ' . $table);
}
} catch (PDOException) {}
}
private function cleanup(): void
{
foreach ($this->tables as $table) {
$query = sprintf($this->baseQueries['delete'], $table, $this->retainDays);
try {
$result = $this->connection->getPDO()->query($query);
if ($result === false) {
throw new PDOException('Failed to delete from table: ' . $table);
}
} catch (PDOException) {}
}
}
}

View File

@ -0,0 +1,140 @@
<?php
namespace Incoviba\Common\Implement\Log\Processor;
use DateInvalidTimeZoneException;
use DateMalformedStringException;
use DateTimeImmutable;
use DateTimeZone;
use Psr\Container\ContainerExceptionInterface;
use Psr\Container\ContainerInterface;
use Psr\Container\NotFoundExceptionInterface;
use Monolog\Formatter;
use Monolog\Handler;
use Monolog\Level;
use Predis;
use Incoviba;
use Throwable;
class ArrayBuilder
{
public function __construct(protected ContainerInterface $container) {}
public function build(array $data): array
{
$handlers = [];
foreach ($data as $handlerData) {
if (in_array($handlerData['handler'], [Handler\StreamHandler::class, Handler\RotatingFileHandler::class,])) {
$params = [
"/logs/{$handlerData['filename']}",
];
if ($handlerData['handler'] === Handler\RotatingFileHandler::class) {
$params []= 10;
}
try {
$formatter = Formatter\LineFormatter::class;
if (array_key_exists('formatter', $handlerData)) {
$formatter = $handlerData['formatter'];
}
$handler = new $handlerData['handler'](...$params)
->setFormatter($this->container->get($formatter));
} catch (NotFoundExceptionInterface | ContainerExceptionInterface $exception) {
$this->log($exception, ['handlerData' => $handlerData]);
continue;
}
} elseif ($handlerData['handler'] === Incoviba\Common\Implement\Log\Handler\MySQL::class) {
try {
$params = [
$this->container->get(Incoviba\Common\Define\Connection::class)
];
$formatter = Incoviba\Common\Implement\Log\Formatter\PDO::class;
if (array_key_exists('formatter', $handlerData)) {
$formatter = $handlerData['formatter'];
}
$handler = new $handlerData['handler'](...$params)
->setFormatter($this->container->get($formatter));
} catch (NotFoundExceptionInterface | ContainerExceptionInterface $exception) {
$this->log($exception, ['handlerData' => $handlerData]);
continue;
}
} elseif ($handlerData['handler'] === Handler\RedisHandler::class) {
try {
$params = [
$this->container->get(Predis\ClientInterface::class),
"logs:{$handlerData['name']}",
'capSize' => $handlerData['capSize'] ?? 100
];
} catch (NotFoundExceptionInterface | ContainerExceptionInterface $exception) {
$this->log($exception, ['handlerData' => $handlerData]);
continue;
}
$handler = new $handlerData['handler'](...$params);
}
if (!isset($handler)) {
$this->log("Invalid handler", ['handlerData' => $handlerData]);
continue;
}
$params = [
$handler,
];
if (is_array($handlerData['levels'])) {
foreach ($handlerData['levels'] as $level) {
$params []= $level;
}
} else {
$params []= $handlerData['levels'];
$params []= Level::Emergency;
}
$params []= false;
$handlers []= new Handler\FilterHandler(...$params);
}
return $handlers;
}
protected function log(string|Throwable $message, array $context = []): void
{
try {
$dateTime = new DateTimeImmutable('now', new DateTimeZone($_ENV['TZ'] ?? 'America/Santiago'));
} catch (DateMalformedStringException | DateInvalidTimeZoneException $exception) {
$dateTime = new DateTimeImmutable();
}
if (is_a($message, Throwable::class)) {
$exception = $message;
$message = $exception->getMessage();
}
$context = json_encode($context, JSON_UNESCAPED_SLASHES | JSON_UNESCAPED_UNICODE);
if ($context === false) {
$context = '[]';
}
$extra = [];
$extra['from'] = __FILE__;
if (isset($exception)) {
$extra['file'] = $exception->getFile();
$extra['line'] = $exception->getLine();
$extra['trace'] = $exception->getTrace();
}
$extra = json_encode($extra, JSON_UNESCAPED_SLASHES | JSON_UNESCAPED_UNICODE);
$code = 0;
if (isset($exception)) {
$code = $exception->getCode();
}
if ($extra === false) {
$extra = '[]';
}
$output = "[{$dateTime->format('Y-m-d H:i:s P')}] [{$code}] {$message} {$context} {$extra}";
$filename = '/logs/error.json';
$fileContents = [];
if (file_exists($filename)) {
$fileContents = file_get_contents($filename);
$fileContents = json_decode($fileContents, true);
if ($fileContents === false) {
$fileContents = [];
}
}
$fileContents[$dateTime->getTimestamp()] = $output;
$fileContents = json_encode($fileContents, JSON_UNESCAPED_SLASHES | JSON_UNESCAPED_UNICODE);
if ($fileContents === false) {
$fileContents = '[]';
}
file_put_contents($filename, $fileContents);
}
}

View File

@ -0,0 +1,68 @@
<?php
namespace Incoviba\Common\Implement\Log\Processor;
use Throwable;
use Monolog\LogRecord;
use Monolog\Processor\ProcessorInterface;
class Exception implements ProcessorInterface
{
public function __invoke(LogRecord $record): LogRecord
{
$context = $record->context;
$changed = false;
array_walk_recursive($context, function (&$item) use (&$changed) {
if (is_a($item, Throwable::class)) {
$item = $this->processException($item);
$changed = true;
}
});
if ($changed) {
$new_record = new LogRecord(
$record->datetime,
$record->channel,
$record->level,
$record->message,
$context,
$record->extra
);
$record = $new_record;
}
if (is_a($record->message, Throwable::class)) {
$exception = $record->message;
$output = $this->processException($exception);
$message = $output['message'];
if (array_key_exists('exception', $context)) {
$context['other_exception'] = $context['exception'];
}
$context['exception'] = $output;
$new_record = new LogRecord(
$record->datetime,
$record->channel,
$record->level,
$message,
$context,
$record->extra
);
$record = $new_record;
}
return $record;
}
protected function processException(Throwable $exception): array
{
$output = [
'class' => get_class($exception),
'code' => $exception->getCode(),
'message' => $exception->getMessage(),
'file' => $exception->getFile(),
'line' => $exception->getLine(),
'trace' => $exception->getTraceAsString(),
];
if ($exception->getPrevious() !== null) {
$output['previous'] = $this->processException($exception);
}
return $output;
}
}

View File

@ -0,0 +1,18 @@
<?php
namespace Incoviba\Common\Implement\Log\Processor;
use Incoviba\Service;
use Monolog\LogRecord;
use Monolog\Processor\ProcessorInterface;
class User implements ProcessorInterface
{
public function __construct(protected Service\Login $loginService) {}
public function __invoke(LogRecord $record): LogRecord
{
if ($this->loginService->isIn()) {
$record->extra['user'] = $this->loginService->getUser()->name;
}
return $record;
}
}

View File

@ -2,7 +2,9 @@
namespace Incoviba\Common\Implement\Repository;
use Closure;
use Exception;
use Incoviba\Common\Define;
use Incoviba\Common\Implement\Exception\EmptyResult;
class Factory implements Define\Repository\Factory
{
@ -20,8 +22,16 @@ class Factory implements Define\Repository\Factory
return $this;
}
/**
* @return mixed
* @throws EmptyResult
*/
public function run(): mixed
{
return call_user_func_array($this->callable, $this->args);
try {
return call_user_func_array($this->callable, $this->args);
} catch (Exception $exception) {
throw new EmptyResult($exception->getMessage(), $exception);
}
}
}

View File

@ -1,8 +1,10 @@
<?php
namespace Incoviba\Common\Implement\Repository;
use Error;
use Closure;
use Incoviba\Common\Define;
use Incoviba\Common\Implement\Exception\EmptyResult;
class Mapper implements Define\Repository\Mapper
{
@ -46,7 +48,11 @@ class Mapper implements Define\Repository\Mapper
}
public function hasDefault(): bool
{
return isset($this->default);
try {
return isset($this->default) or $this->default === null;
} catch (Error) {
return false;
}
}
public function parse(Define\Model &$model, string $column, ?array $data): bool
@ -62,10 +68,14 @@ class Mapper implements Define\Repository\Mapper
}
$value = $data[$column];
if ($this->hasFunction()) {
if ($value !== null) {
try {
$value = ($this->function)($data);
} elseif ($this->hasDefault()) {
$value = $this->default;
} catch (EmptyResult $exception) {
if ($this->hasDefault()) {
$value = $this->default;
} else {
throw $exception;
}
}
}
$model->{$property} = $value;

View File

@ -2,6 +2,7 @@
namespace Incoviba\Common\Implement\Repository\Mapper;
use DateTimeImmutable;
use DateMalformedStringException;
use Incoviba\Common\Implement\Repository\Mapper;
class DateTime extends Mapper
@ -9,7 +10,17 @@ class DateTime extends Mapper
public function __construct(string $column, ?string $property = null)
{
$this->setFunction(function($data) use ($column) {
return new DateTimeImmutable($data[$column] ?? '');
if (!isset($data[$column])) {
return null;
}
if (is_a($data[$column], DateTimeImmutable::class)) {
return $data[$column];
}
try {
return new DateTimeImmutable($data[$column] ?? '');
} catch (DateMalformedStringException) {
return new DateTimeImmutable();
}
});
if ($property !== null) {
$this->setProperty($property);