develop (#45)
Co-authored-by: Juan Pablo Vial <jpvialb@incoviba.cl> Reviewed-on: #45
This commit is contained in:
@ -1,9 +1,6 @@
|
||||
<?php
|
||||
namespace Incoviba\Common\Alias;
|
||||
|
||||
use Illuminate\Events\Dispatcher;
|
||||
use Slim\Views\Blade;
|
||||
|
||||
class View extends Blade
|
||||
{
|
||||
}
|
||||
class View extends Blade {}
|
||||
|
@ -5,6 +5,11 @@ use Psr\Http\Message\UploadedFileInterface;
|
||||
|
||||
interface Banco
|
||||
{
|
||||
/**
|
||||
* Process bank movements for database inserts
|
||||
* @param UploadedFileInterface $file
|
||||
* @return array
|
||||
*/
|
||||
public function process(UploadedFileInterface $file): array;
|
||||
|
||||
}
|
||||
|
@ -3,13 +3,42 @@ namespace Incoviba\Common\Define;
|
||||
|
||||
use PDO;
|
||||
use PDOStatement;
|
||||
use PDOException;
|
||||
|
||||
interface Connection
|
||||
{
|
||||
/**
|
||||
* @return Connection
|
||||
* @throws PDOException
|
||||
*/
|
||||
public function connect(): Connection;
|
||||
|
||||
/**
|
||||
* @param string $query
|
||||
* @return PDOStatement
|
||||
* @throws PDOException
|
||||
*/
|
||||
public function query(string $query): PDOStatement;
|
||||
|
||||
/**
|
||||
* @param string $query
|
||||
* @return PDOStatement
|
||||
* @throws PDOException
|
||||
*/
|
||||
public function prepare(string $query): PDOStatement;
|
||||
|
||||
/**
|
||||
* @param string $query
|
||||
* @param array|null $data
|
||||
* @return PDOStatement
|
||||
* @throws PDOException
|
||||
*/
|
||||
public function execute(string $query, ?array $data = null): PDOStatement;
|
||||
|
||||
/**
|
||||
* @return PDO
|
||||
* @throws PDOException
|
||||
*/
|
||||
public function getPDO(): PDO;
|
||||
public function getQueryBuilder(): Query\Builder;
|
||||
}
|
||||
|
@ -6,5 +6,5 @@ use Incoviba\Model;
|
||||
|
||||
interface Exporter
|
||||
{
|
||||
public function export(Model\Inmobiliaria $inmobiliaria, Model\Banco $banco, DateTimeInterface $mes, array $movimientos): string;
|
||||
public function export(Model\Inmobiliaria $inmobiliaria, Model\Contabilidad\Banco $banco, DateTimeInterface $mes, array $movimientos): string;
|
||||
}
|
||||
|
@ -2,8 +2,15 @@
|
||||
namespace Incoviba\Common\Define\Money;
|
||||
|
||||
use DateTimeInterface;
|
||||
use Incoviba\Common\Implement\Exception\EmptyResponse;
|
||||
|
||||
interface Provider
|
||||
{
|
||||
public function get(string $money_symbol, DateTimeInterface $dateTime): float;
|
||||
/**
|
||||
* @param string $money_symbol
|
||||
* @param ?DateTimeInterface $dateTime = null
|
||||
* @return float
|
||||
* @throws EmptyResponse
|
||||
*/
|
||||
public function get(string $money_symbol, ?DateTimeInterface $dateTime = null): float;
|
||||
}
|
||||
|
@ -1,11 +1,42 @@
|
||||
<?php
|
||||
namespace Incoviba\Common\Define;
|
||||
|
||||
use Incoviba\Common\Implement\Exception\EmptyResult;
|
||||
use PDOException;
|
||||
|
||||
interface Repository
|
||||
{
|
||||
/**
|
||||
* @param array|null $data
|
||||
* @return Model
|
||||
*/
|
||||
public function create(?array $data = null): Model;
|
||||
|
||||
/**
|
||||
* @param Model $model
|
||||
* @return Model
|
||||
* @throws PDOException
|
||||
*/
|
||||
public function save(Model $model): Model;
|
||||
|
||||
/**
|
||||
* @param array $data_row
|
||||
* @return Model
|
||||
*/
|
||||
public function load(array $data_row): Model;
|
||||
|
||||
/**
|
||||
* @param Model $model
|
||||
* @param array $new_data
|
||||
* @return Model
|
||||
* @throws EmptyResult
|
||||
*/
|
||||
public function edit(Model $model, array $new_data): Model;
|
||||
|
||||
/**
|
||||
* @param Model $model
|
||||
* @return void
|
||||
* @throws PDOException
|
||||
*/
|
||||
public function remove(Model $model): void;
|
||||
}
|
||||
|
@ -9,9 +9,55 @@ abstract class Banco extends Service implements Define\Cartola\Banco
|
||||
{
|
||||
public function process(UploadedFileInterface $file): array
|
||||
{
|
||||
$data = $this->parseFile($file);
|
||||
$filename = $this->processUploadedFile($file);
|
||||
$data = $this->processFile($filename);
|
||||
return $this->mapColumns($data);
|
||||
}
|
||||
|
||||
/**
|
||||
* There are banks that need some post-processing
|
||||
* @param array $movimientos
|
||||
* @return array
|
||||
*/
|
||||
public function processMovimientosDiarios(array $movimientos): array
|
||||
{
|
||||
return $movimientos;
|
||||
}
|
||||
|
||||
/**
|
||||
* Move the UploadedFile into a temp file from getFilename
|
||||
* @param UploadedFileInterface $uploadedFile
|
||||
* @return string
|
||||
*/
|
||||
protected function processUploadedFile(UploadedFileInterface $uploadedFile): string
|
||||
{
|
||||
$filename = $this->getFilename($uploadedFile);
|
||||
$uploadedFile->moveTo($filename);
|
||||
return $filename;
|
||||
}
|
||||
|
||||
/**
|
||||
* Process the temp file from getFilename and remove it
|
||||
* @param string $filename
|
||||
* @return array
|
||||
*/
|
||||
protected function processFile(string $filename): array
|
||||
{
|
||||
$data = $this->parseFile($filename);
|
||||
unlink($filename);
|
||||
return $data;
|
||||
}
|
||||
|
||||
/**
|
||||
* Map columns from uploaded file data to database columns
|
||||
* @param array $data
|
||||
* @return array
|
||||
*/
|
||||
protected function mapColumns(array $data): array
|
||||
{
|
||||
$temp = [];
|
||||
$columns = $this->columnMap();
|
||||
|
||||
foreach ($data as $row) {
|
||||
$r = [];
|
||||
foreach ($columns as $old => $new) {
|
||||
@ -24,11 +70,24 @@ abstract class Banco extends Service implements Define\Cartola\Banco
|
||||
}
|
||||
return $temp;
|
||||
}
|
||||
public function processMovimientosDiarios(array $movimientos): array
|
||||
{
|
||||
return $movimientos;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get filename where to move UploadedFile
|
||||
* @param UploadedFileInterface $uploadedFile
|
||||
* @return string
|
||||
*/
|
||||
abstract protected function getFilename(UploadedFileInterface $uploadedFile): string;
|
||||
|
||||
/**
|
||||
* Mapping of uploaded file data columns to database columns
|
||||
* @return array
|
||||
*/
|
||||
abstract protected function columnMap(): array;
|
||||
abstract protected function parseFile(UploadedFileInterface $uploadedFile): array;
|
||||
|
||||
/**
|
||||
* Translate uploaded file data to database data
|
||||
* @param string $filename
|
||||
* @return array
|
||||
*/
|
||||
abstract protected function parseFile(string $filename): array;
|
||||
}
|
||||
|
18
app/common/Ideal/LoggerEnabled.php
Normal file
18
app/common/Ideal/LoggerEnabled.php
Normal file
@ -0,0 +1,18 @@
|
||||
<?php
|
||||
namespace Incoviba\Common\Ideal;
|
||||
|
||||
use Psr\Log\LoggerAwareInterface;
|
||||
use Psr\Log\LoggerInterface;
|
||||
|
||||
abstract class LoggerEnabled implements LoggerAwareInterface
|
||||
{
|
||||
public LoggerInterface $logger;
|
||||
public function setLogger(LoggerInterface $logger): void
|
||||
{
|
||||
$this->logger = $logger;
|
||||
}
|
||||
public function getLogger(): LoggerInterface
|
||||
{
|
||||
return $this->logger;
|
||||
}
|
||||
}
|
@ -26,7 +26,13 @@ abstract class Model implements Define\Model
|
||||
public function jsonSerialize(): mixed
|
||||
{
|
||||
return [
|
||||
'id' => $this->id
|
||||
'id' => $this->id ?? '',
|
||||
...$this->jsonComplement()
|
||||
];
|
||||
}
|
||||
|
||||
protected function jsonComplement(): array
|
||||
{
|
||||
return [];
|
||||
}
|
||||
}
|
||||
|
@ -2,7 +2,9 @@
|
||||
namespace Incoviba\Common\Ideal;
|
||||
|
||||
use PDO;
|
||||
use PDOException;
|
||||
use ReflectionProperty;
|
||||
use ReflectionException;
|
||||
use Incoviba\Common\Define;
|
||||
use Incoviba\Common\Implement;
|
||||
use Incoviba\Common\Implement\Exception\EmptyResult;
|
||||
@ -22,10 +24,15 @@ abstract class Repository implements Define\Repository
|
||||
return $this;
|
||||
}
|
||||
|
||||
public function getConnection(): Define\Connection
|
||||
{
|
||||
return $this->connection;
|
||||
}
|
||||
|
||||
public function load(array $data_row): Define\Model
|
||||
{
|
||||
$model = $this->create($data_row);
|
||||
$model->{$this->getKey()} = $data_row[$this->getKey()];
|
||||
$this->setIndex($model, $data_row[$this->getKey()]);
|
||||
return $model;
|
||||
}
|
||||
|
||||
@ -34,9 +41,12 @@ abstract class Repository implements Define\Repository
|
||||
$query = $this->connection->getQueryBuilder()
|
||||
->delete()->from($this->getTable())
|
||||
->where("{$this->getKey()} = ?");
|
||||
$this->connection->execute($query, [$model->id]);
|
||||
$this->connection->execute($query, [$this->getIndex($model)]);
|
||||
}
|
||||
|
||||
/**
|
||||
* @throws EmptyResult
|
||||
*/
|
||||
public function fetchById(int $id): Define\Model
|
||||
{
|
||||
$query = $this->connection->getQueryBuilder()
|
||||
@ -45,6 +55,10 @@ abstract class Repository implements Define\Repository
|
||||
->where("{$this->getKey()} = ?");
|
||||
return $this->fetchOne($query, [$id]);
|
||||
}
|
||||
|
||||
/**
|
||||
* @throws EmptyResult
|
||||
*/
|
||||
public function fetchAll(null|string|array $ordering = null): array
|
||||
{
|
||||
$query = $this->connection->getQueryBuilder()
|
||||
@ -56,10 +70,27 @@ abstract class Repository implements Define\Repository
|
||||
return $this->fetchMany($query);
|
||||
}
|
||||
|
||||
protected string $key = 'id';
|
||||
public function setKey(string $key): Repository
|
||||
{
|
||||
$this->key = $key;
|
||||
return $this;
|
||||
}
|
||||
protected function getKey(): string
|
||||
{
|
||||
return 'id';
|
||||
return $this->key;
|
||||
}
|
||||
|
||||
protected function setIndex(Define\Model &$model, mixed $value): Repository
|
||||
{
|
||||
$model->{$this->getKey()} = $value;
|
||||
return $this;
|
||||
}
|
||||
protected function getIndex(Define\Model $model): mixed
|
||||
{
|
||||
return $model->id;
|
||||
}
|
||||
|
||||
protected function parseData(Define\Model $model, ?array $data, Implement\Repository\MapperParser $data_map): Define\Model
|
||||
{
|
||||
if ($data === null) {
|
||||
@ -92,9 +123,20 @@ abstract class Repository implements Define\Repository
|
||||
}
|
||||
$this->setDefault($model, $property);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param Define\Model $model
|
||||
* @param string $property
|
||||
* @return void
|
||||
*/
|
||||
protected function setDefault(Define\Model &$model, string $property): void
|
||||
{
|
||||
$prop = new ReflectionProperty($model, $property);
|
||||
try {
|
||||
$prop = new ReflectionProperty($model, $property);
|
||||
} catch (ReflectionException) {
|
||||
$model->{$property} = null;
|
||||
return;
|
||||
}
|
||||
$type = $prop->getType()->getName();
|
||||
$value = match ($type) {
|
||||
'int' => 0,
|
||||
@ -104,6 +146,13 @@ abstract class Repository implements Define\Repository
|
||||
};
|
||||
$model->{$property} = $value;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param array $columns
|
||||
* @param array $values
|
||||
* @return int
|
||||
* @throws PDOException
|
||||
*/
|
||||
protected function saveNew(array $columns, array $values): int
|
||||
{
|
||||
$query = $this->connection->getQueryBuilder()
|
||||
@ -114,6 +163,14 @@ abstract class Repository implements Define\Repository
|
||||
$this->connection->execute($query, $values);
|
||||
return $this->connection->getPDO()->lastInsertId();
|
||||
}
|
||||
|
||||
/**
|
||||
* @param Define\Model $model
|
||||
* @param array $columns
|
||||
* @param array $data
|
||||
* @return Define\Model
|
||||
* @throws EmptyResult
|
||||
*/
|
||||
protected function update(Define\Model $model, array $columns, array $data): Define\Model
|
||||
{
|
||||
$changes = [];
|
||||
@ -133,32 +190,68 @@ abstract class Repository implements Define\Repository
|
||||
->update($this->getTable())
|
||||
->set($columns_string)
|
||||
->where("{$this->getKey()} = ?");
|
||||
$values []= $model->{$this->getKey()};
|
||||
$this->connection->execute($query, $values);
|
||||
return $this->fetchById($model->{$this->getKey()});
|
||||
$values []= $this->getIndex($model);
|
||||
try {
|
||||
$this->connection->execute($query, $values);
|
||||
} catch (PDOException $exception) {
|
||||
throw new EmptyResult($query, $exception, $data);
|
||||
}
|
||||
return $this->fetchById($this->getIndex($model));
|
||||
}
|
||||
|
||||
/**
|
||||
* @param string $query
|
||||
* @param array|null $data
|
||||
* @return Define\Model
|
||||
* @throws EmptyResult
|
||||
*/
|
||||
protected function fetchOne(string $query, ?array $data = null): Define\Model
|
||||
{
|
||||
$result = $this->connection->execute($query, $data)->fetch(PDO::FETCH_ASSOC);
|
||||
if ($result === false) {
|
||||
throw new EmptyResult($query);
|
||||
try {
|
||||
$result = $this->connection->execute($query, $data)->fetch(PDO::FETCH_ASSOC);
|
||||
if ($result === false) {
|
||||
throw new EmptyResult($query, null, $data);
|
||||
}
|
||||
} catch (PDOException $exception) {
|
||||
throw new EmptyResult($query, $exception, $data);
|
||||
}
|
||||
return $this->load($result);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param string $query
|
||||
* @param array|null $data
|
||||
* @return array
|
||||
* @throws EmptyResult
|
||||
*/
|
||||
protected function fetchMany(string $query, ?array $data = null): array
|
||||
{
|
||||
$results = $this->connection->execute($query, $data)->fetchAll(PDO::FETCH_ASSOC);
|
||||
if ($results === false) {
|
||||
throw new EmptyResult($query);
|
||||
try {
|
||||
$results = $this->connection->execute($query, $data)->fetchAll(PDO::FETCH_ASSOC);
|
||||
} catch (PDOException $exception) {
|
||||
throw new EmptyResult($query, $exception, $data);
|
||||
}
|
||||
return array_map([$this, 'load'], $results);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param string $query
|
||||
* @param array|null $data
|
||||
* @return array
|
||||
* @throws EmptyResult
|
||||
*/
|
||||
protected function fetchAsArray(string $query, ?array $data = null): array
|
||||
{
|
||||
$results = $this->connection->execute($query, $data)->fetchAll(PDO::FETCH_ASSOC);
|
||||
if ($results === false) {
|
||||
throw new EmptyResult($query);
|
||||
try {
|
||||
$results = $this->connection->execute($query, $data)->fetchAll(PDO::FETCH_ASSOC);
|
||||
} catch (PDOException $exception) {
|
||||
throw new EmptyResult($query, $exception, $data);
|
||||
}
|
||||
return $results;
|
||||
}
|
||||
|
||||
public function filterData(array $data): array
|
||||
{
|
||||
return $data;
|
||||
}
|
||||
}
|
||||
|
56
app/common/Ideal/Service/API.php
Normal file
56
app/common/Ideal/Service/API.php
Normal file
@ -0,0 +1,56 @@
|
||||
<?php
|
||||
namespace Incoviba\Common\Ideal\Service;
|
||||
|
||||
use Psr\Log\LoggerInterface;
|
||||
use Incoviba\Common\Define;
|
||||
use Incoviba\Common\Ideal;
|
||||
use Incoviba\Exception\ServiceAction;
|
||||
|
||||
abstract class API extends Ideal\Service
|
||||
{
|
||||
public function __construct(LoggerInterface $logger)
|
||||
{
|
||||
parent::__construct($logger);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param string|array|null $order
|
||||
* @return array
|
||||
*/
|
||||
abstract public function getAll(null|string|array $order = null): array;
|
||||
|
||||
/**
|
||||
* @param int $id
|
||||
* @return Define\Model
|
||||
* @throws ServiceAction\Read
|
||||
*/
|
||||
abstract public function get(int $id): Define\Model;
|
||||
|
||||
/**
|
||||
* @param array $data
|
||||
* @return Define\Model
|
||||
* @throws ServiceAction\Create
|
||||
*/
|
||||
abstract public function add(array $data): Define\Model;
|
||||
|
||||
/**
|
||||
* @param Define\Model $model
|
||||
* @param array $new_data
|
||||
* @return Define\Model
|
||||
* @throws ServiceAction\Update
|
||||
*/
|
||||
abstract public function edit(Define\Model $model, array $new_data): Define\Model;
|
||||
|
||||
/**
|
||||
* @param int $id
|
||||
* @return Define\Model
|
||||
* @throws ServiceAction\Delete
|
||||
*/
|
||||
abstract public function delete(int $id): Define\Model;
|
||||
|
||||
/**
|
||||
* @param Define\Model $model
|
||||
* @return Define\Model
|
||||
*/
|
||||
abstract protected function process(Define\Model $model): Define\Model;
|
||||
}
|
10
app/common/Ideal/Service/Repository.php
Normal file
10
app/common/Ideal/Service/Repository.php
Normal file
@ -0,0 +1,10 @@
|
||||
<?php
|
||||
namespace Incoviba\Common\Ideal\Service;
|
||||
|
||||
use Incoviba\Common\Define;
|
||||
use Incoviba\Common\Ideal;
|
||||
|
||||
abstract class Repository extends Ideal\Service
|
||||
{
|
||||
abstract public function getRepository(): Define\Repository;
|
||||
}
|
@ -22,6 +22,7 @@ class Connection implements Define\Connection
|
||||
}
|
||||
return $this;
|
||||
}
|
||||
|
||||
public function getPDO(): PDO
|
||||
{
|
||||
$this->connect();
|
||||
|
@ -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)) . ')';
|
||||
}
|
||||
|
@ -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;
|
||||
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
13
app/common/Implement/Exception/HttpException.php
Normal file
13
app/common/Implement/Exception/HttpException.php
Normal 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);
|
||||
}
|
||||
}
|
19
app/common/Implement/Exception/HttpResponse.php
Normal file
19
app/common/Implement/Exception/HttpResponse.php
Normal 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;
|
||||
}
|
||||
}
|
48
app/common/Implement/Log/Formatter/PDO.php
Normal file
48
app/common/Implement/Log/Formatter/PDO.php
Normal 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;
|
||||
}
|
||||
}
|
116
app/common/Implement/Log/Handler/MySQL.php
Normal file
116
app/common/Implement/Log/Handler/MySQL.php
Normal 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) {}
|
||||
}
|
||||
}
|
||||
}
|
140
app/common/Implement/Log/Processor/ArrayBuilder.php
Normal file
140
app/common/Implement/Log/Processor/ArrayBuilder.php
Normal 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);
|
||||
}
|
||||
}
|
68
app/common/Implement/Log/Processor/Exception.php
Normal file
68
app/common/Implement/Log/Processor/Exception.php
Normal 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;
|
||||
}
|
||||
}
|
18
app/common/Implement/Log/Processor/User.php
Normal file
18
app/common/Implement/Log/Processor/User.php
Normal 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;
|
||||
}
|
||||
}
|
@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -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;
|
||||
|
@ -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);
|
||||
|
Reference in New Issue
Block a user