Merge pull request #547 from arabcoders/dev

Updates testcases, and fixed bug in emby progress ync
This commit is contained in:
Abdulmohsen
2024-09-07 22:18:53 +03:00
committed by GitHub
59 changed files with 2956 additions and 1162 deletions

View File

@@ -24,6 +24,7 @@ use Psr\EventDispatcher\EventDispatcherInterface;
use Psr\Http\Message\UriInterface;
use Psr\Log\LoggerInterface as iLogger;
use Psr\SimpleCache\CacheInterface;
use Symfony\Component\Cache\Adapter\ArrayAdapter;
use Symfony\Component\Cache\Adapter\FilesystemAdapter;
use Symfony\Component\Cache\Adapter\NullAdapter;
use Symfony\Component\Cache\Adapter\RedisAdapter;
@@ -121,6 +122,10 @@ return (function (): array {
return new Psr16Cache(new NullAdapter());
}
if (true === (defined('IN_TEST_MODE') && true === IN_TEST_MODE)) {
return new Psr16Cache(new ArrayAdapter());
}
$ns = getAppVersion();
if (null !== ($prefix = Config::get('cache.prefix')) && true === isValidName($prefix)) {
@@ -199,8 +204,15 @@ return (function (): array {
},
],
DBLayer::class => [
'class' => fn(PDO $pdo): DBLayer => new DBLayer($pdo),
'args' => [
PDO::class,
],
],
iDB::class => [
'class' => function (iLogger $logger, PDO $pdo): iDB {
'class' => function (iLogger $logger, DBLayer $pdo): iDB {
$adapter = new PDOAdapter($logger, $pdo);
if (true !== $adapter->isMigrated()) {
@@ -216,14 +228,7 @@ return (function (): array {
},
'args' => [
iLogger::class,
PDO::class,
],
],
DBLayer::class => [
'class' => fn(PDO $pdo): DBLayer => new DBLayer($pdo),
'args' => [
PDO::class,
DBLayer::class,
],
],

View File

@@ -6,7 +6,7 @@ namespace App\API\Backend;
use App\Libs\Config;
use App\Libs\ConfigFile;
use App\Libs\Database\DatabaseInterface as iDB;
use App\Libs\Database\DBLayer;
use App\Libs\Enums\Http\Status;
use App\Libs\Traits\APITraits;
use Psr\Http\Message\ResponseInterface as iResponse;
@@ -16,12 +16,8 @@ final class Delete
{
use APITraits;
public function __construct(private iDB $db)
{
}
#[\App\Libs\Attributes\Route\Delete(Index::URL . '/{name:backend}[/]', name: 'backend.delete')]
public function __invoke(iRequest $request, array $args = []): iResponse
public function __invoke(DBLayer $db, iRequest $request, array $args = []): iResponse
{
if (null === ($name = ag($args, 'name'))) {
return api_error('Invalid value for name path parameter.', Status::BAD_REQUEST);
@@ -47,13 +43,13 @@ final class Delete
)
";
$stmt = $this->db->getPDO()->prepare($sql);
$stmt = $db->prepare($sql);
$stmt->execute(['name_metadata' => $name, 'name_extra' => $name]);
$removedReference = $stmt->rowCount();
$sql = "DELETE FROM state WHERE id IN ( SELECT id FROM state WHERE length(metadata) < 10 )";
$stmt = $this->db->getPDO()->query($sql);
$stmt = $db->query($sql);
$deletedRecords = $stmt->rowCount();

View File

@@ -10,6 +10,7 @@ use App\Libs\Attributes\Route\Get;
use App\Libs\Attributes\Route\Route;
use App\Libs\Container;
use App\Libs\Database\DatabaseInterface as iDB;
use App\Libs\Database\DBLayer;
use App\Libs\DataUtil;
use App\Libs\Entity\StateInterface as iState;
use App\Libs\Enums\Http\Status;
@@ -17,7 +18,6 @@ use App\Libs\Guid;
use App\Libs\Mappers\Import\DirectMapper;
use App\Libs\Traits\APITraits;
use JsonException;
use PDO;
use Psr\Http\Message\ResponseInterface as iResponse;
use Psr\Http\Message\ServerRequestInterface as iRequest;
use Psr\SimpleCache\CacheInterface as iCache;
@@ -47,17 +47,15 @@ final class Index
use APITraits;
public const string URL = '%{api.prefix}/history';
private PDO $pdo;
public function __construct(private readonly iDB $db, private DirectMapper $mapper, private iCache $cache)
{
$this->pdo = $this->db->getPDO();
}
#[Get(self::URL . '[/]', name: 'history.list')]
public function list(iRequest $request): iResponse
public function list(DBLayer $db, iRequest $request): iResponse
{
$es = fn(string $val) => $this->db->identifier($val);
$es = fn(string $val) => $db->escapeIdentifier($val, true);
$data = DataUtil::fromArray($request->getQueryParams());
$filters = [];
@@ -278,7 +276,7 @@ final class Index
$sql[] = 'WHERE ' . implode(' AND ', $where);
}
$stmt = $this->pdo->prepare('SELECT COUNT(*) ' . implode(' ', array_map('trim', $sql)));
$stmt = $db->prepare('SELECT COUNT(*) ' . implode(' ', array_map('trim', $sql)));
$stmt->execute($params);
$total = $stmt->fetchColumn();
@@ -324,7 +322,7 @@ final class Index
$params['_limit'] = $perpage <= 0 ? 20 : $perpage;
$sql[] = 'ORDER BY ' . implode(', ', $sorts) . ' LIMIT :_start,:_limit';
$stmt = $this->pdo->prepare('SELECT * ' . implode(' ', array_map('trim', $sql)));
$stmt = $db->prepare('SELECT * ' . implode(' ', array_map('trim', $sql)));
$stmt->execute($params);
$getUri = $request->getUri()->withHost('')->withPort(0)->withScheme('');

View File

@@ -10,7 +10,7 @@ use App\Libs\Attributes\Route\Post;
use App\Libs\Config;
use App\Libs\ConfigFile;
use App\Libs\Container;
use App\Libs\Database\DatabaseInterface as iDB;
use App\Libs\Database\DBLayer;
use App\Libs\DataUtil;
use App\Libs\Entity\StateInterface as iState;
use App\Libs\Enums\Http\Status;
@@ -26,14 +26,10 @@ final class Index
private array $cache = [];
private PDO $db;
private ConfigFile $config;
public function __construct(iDB $db)
public function __construct(private readonly DBLayer $db)
{
$this->db = $db->getPDO();
$this->config = ConfigFile::open(
file: Config::get('path') . '/config/ignore.yaml',
type: 'yaml',

View File

@@ -7,14 +7,13 @@ namespace App\API\System;
use App\Libs\Attributes\Route\Delete;
use App\Libs\Attributes\Route\Get;
use App\Libs\Container;
use App\Libs\Database\DatabaseInterface as iDB;
use App\Libs\Database\DBLayer;
use App\Libs\DataUtil;
use App\Libs\Entity\StateInterface as iState;
use App\Libs\Enums\Http\Status;
use App\Libs\Middlewares\ExceptionHandlerMiddleware;
use App\Libs\Traits\APITraits;
use DateInterval;
use PDO;
use Psr\Http\Message\ResponseInterface as iResponse;
use Psr\Http\Message\ServerRequestInterface as iRequest;
use Psr\SimpleCache\CacheInterface as iCache;
@@ -31,22 +30,20 @@ final class Integrity
private array $checkedFile = [];
private bool $fromCache = false;
private PDO $pdo;
/**
* @throws InvalidArgumentException
*/
public function __construct(private iDB $db, private readonly iCache $cache)
public function __construct(private readonly iCache $cache)
{
set_time_limit(0);
$this->pdo = $this->db->getPDO();
}
/**
* @throws InvalidArgumentException
*/
#[Get(self::URL . '[/]', middleware: [ExceptionHandlerMiddleware::class], name: 'system.integrity')]
public function __invoke(iRequest $request): iResponse
public function __invoke(DBLayer $db, iRequest $request): iResponse
{
$params = DataUtil::fromArray($request->getQueryParams());
@@ -66,7 +63,7 @@ final class Integrity
];
$sql = "SELECT * FROM state";
$stmt = $this->db->getPDO()->prepare($sql);
$stmt = $db->prepare($sql);
$stmt->execute();
$base = Container::get(iState::class);
@@ -157,7 +154,7 @@ final class Integrity
* @throws InvalidArgumentException
*/
#[Delete(self::URL . '[/]', name: 'system.integrity.reset')]
public function resetCache(iRequest $request): iResponse
public function resetCache(): iResponse
{
if ($this->cache->has('system.integrity')) {
$this->cache->delete('system.integrity');

View File

@@ -6,11 +6,10 @@ namespace App\API\System;
use App\Libs\Attributes\Route\Delete;
use App\Libs\Attributes\Route\Get;
use App\Libs\Database\DatabaseInterface as iDB;
use App\Libs\Database\DBLayer;
use App\Libs\DataUtil;
use App\Libs\Enums\Http\Status;
use App\Libs\Traits\APITraits;
use PDO;
use Psr\Http\Message\ResponseInterface as iResponse;
use Psr\Http\Message\ServerRequestInterface as iRequest;
use Psr\SimpleCache\InvalidArgumentException;
@@ -21,11 +20,8 @@ final class Parity
public const string URL = '%{api.prefix}/system/parity';
private PDO $pdo;
public function __construct(private iDB $db)
public function __construct(private readonly DBLayer $db)
{
$this->pdo = $this->db->getPDO();
}
/**
@@ -60,7 +56,7 @@ final class Parity
$counter = 0 === $counter ? $backendsCount : $counter;
$sql = "SELECT COUNT(*) FROM state WHERE ( SELECT COUNT(*) FROM JSON_EACH(state.metadata) ) < {$counter}";
$stmt = $this->pdo->query($sql);
$stmt = $this->db->query($sql);
$total = (int)$stmt->fetchColumn();
$lastPage = @ceil($total / $perpage);
@@ -84,7 +80,7 @@ final class Parity
:_start, :_perpage
";
$stmt = $this->db->getPDO()->prepare($sql);
$stmt = $this->db->prepare($sql);
$stmt->execute([
'_start' => $start,
'_perpage' => $perpage,
@@ -135,7 +131,7 @@ final class Parity
WHERE
( SELECT COUNT(*) FROM JSON_EACH(state.metadata) ) < {$counter}
";
$stmt = $this->db->getPDO()->query($sql);
$stmt = $this->db->query($sql);
return api_response(Status::OK, [
'deleted_records' => $stmt->rowCount(),

View File

@@ -36,6 +36,7 @@ trait CommonTrait
return new Response(
status: false,
error: new Error(
...lw(
message: "{client}: '{backend}' {action} thrown unhandled exception '{error.kind}'. '{error.message}' at '{error.file}:{error.line}'.",
context: [
'action' => $action ?? '',
@@ -56,6 +57,8 @@ trait CommonTrait
'trace' => $e->getTrace(),
]
],
e: $e
),
level: Levels::WARNING,
previous: $e
)

View File

@@ -4,6 +4,8 @@ declare(strict_types=1);
namespace App\Backends\Common;
use App\Libs\Database\DBLayer;
use App\Libs\Exceptions\DBLayerException;
use Stringable;
use Throwable;
@@ -67,7 +69,17 @@ final readonly class Error implements Stringable
return $this->message;
}
return r($this->message, $this->context, ['log_behavior' => true]);
$context = $this->context;
if (true === ($this->previous instanceof DBLayerException)) {
$context[DBLayer::class] = [
'query' => $this->previous->getQueryString(),
'bind' => $this->previous->getQueryBind(),
'error' => $this->previous->errorInfo ?? [],
];
}
return r($this->message, $context, ['log_behavior' => true]);
}
public function __toString(): string

View File

@@ -307,7 +307,8 @@ final class ParseWebhook
],
'trace' => $e->getTrace(),
],
level: Levels::ERROR
level: Levels::ERROR,
previous: $e
),
extra: [
'http_code' => 200,

View File

@@ -226,26 +226,29 @@ class Progress
}
} catch (\App\Libs\Exceptions\RuntimeException|RuntimeException|InvalidArgumentException $e) {
$this->logger->error(
message: "{action}: Exception '{error.kind}' was thrown unhandled during '{client}: {backend}' get {item.type} '{item.title}' status. '{error.message}' at '{error.file}:{error.line}'.",
contexT: [
'action' => $this->action,
'client' => $context->clientName,
'backend' => $context->backendName,
...$logContext,
'error' => [
'kind' => $e::class,
'line' => $e->getLine(),
'message' => $e->getMessage(),
'file' => after($e->getFile(), ROOT_PATH),
...lw(
message: "{action}: Exception '{error.kind}' was thrown unhandled during '{client}: {backend}' get {item.type} '{item.title}' status. '{error.message}' at '{error.file}:{error.line}'.",
context: [
'action' => $this->action,
'client' => $context->clientName,
'backend' => $context->backendName,
...$logContext,
'error' => [
'kind' => $e::class,
'line' => $e->getLine(),
'message' => $e->getMessage(),
'file' => after($e->getFile(), ROOT_PATH),
],
'exception' => [
'file' => $e->getFile(),
'line' => $e->getLine(),
'kind' => get_class($e),
'message' => $e->getMessage(),
'trace' => $e->getTrace(),
],
],
'exception' => [
'file' => $e->getFile(),
'line' => $e->getLine(),
'kind' => get_class($e),
'message' => $e->getMessage(),
'trace' => $e->getTrace(),
],
]
e: $e
)
);
continue;
}
@@ -293,26 +296,29 @@ class Progress
}
} catch (Throwable $e) {
$this->logger->error(
message: "{action}: Exception '{error.kind}' was thrown unhandled during '{client}: {backend}' change {item.type} '{item.title}' watch progress. '{error.message}' at '{error.file}:{error.line}'.",
context: [
'action' => $this->action,
'client' => $context->clientName,
'backend' => $context->backendName,
'error' => [
'kind' => $e::class,
'line' => $e->getLine(),
'message' => $e->getMessage(),
'file' => after($e->getFile(), ROOT_PATH),
...lw(
message: "{action}: Exception '{error.kind}' was thrown unhandled during '{client}: {backend}' change {item.type} '{item.title}' watch progress. '{error.message}' at '{error.file}:{error.line}'.",
context: [
'action' => $this->action,
'client' => $context->clientName,
'backend' => $context->backendName,
'error' => [
'kind' => $e::class,
'line' => $e->getLine(),
'message' => $e->getMessage(),
'file' => after($e->getFile(), ROOT_PATH),
],
...$logContext,
'exception' => [
'file' => $e->getFile(),
'line' => $e->getLine(),
'kind' => get_class($e),
'message' => $e->getMessage(),
'trace' => $e->getTrace(),
],
],
...$logContext,
'exception' => [
'file' => $e->getFile(),
'line' => $e->getLine(),
'kind' => get_class($e),
'message' => $e->getMessage(),
'trace' => $e->getTrace(),
],
]
e: $e
)
);
}
}

View File

@@ -91,11 +91,17 @@ class Export extends Import
'type' => $type,
];
} catch (InvalidArgumentException $e) {
$this->logger->info($e->getMessage(), [
'backend' => $context->backendName,
...$logContext,
'body' => $item,
]);
$this->logger->info(
...lw(
message: $e->getMessage(),
context: [
'backend' => $context->backendName,
...$logContext,
'body' => $item,
],
e: $e
)
);
return;
}
@@ -257,25 +263,28 @@ class Export extends Import
);
} catch (Throwable $e) {
$this->logger->error(
message: 'Exception [{error.kind}] was thrown unhandled during [{client}: {backend}] export. Error [{error.message} @ {error.file}:{error.line}].',
context: [
'backend' => $context->backendName,
'client' => $context->clientName,
'error' => [
'kind' => $e::class,
'line' => $e->getLine(),
'message' => $e->getMessage(),
'file' => after($e->getFile(), ROOT_PATH),
...lw(
message: 'Exception [{error.kind}] was thrown unhandled during [{client}: {backend}] export. Error [{error.message} @ {error.file}:{error.line}].',
context: [
'backend' => $context->backendName,
'client' => $context->clientName,
'error' => [
'kind' => $e::class,
'line' => $e->getLine(),
'message' => $e->getMessage(),
'file' => after($e->getFile(), ROOT_PATH),
],
...$logContext,
'exception' => [
'file' => $e->getFile(),
'line' => $e->getLine(),
'kind' => get_class($e),
'message' => $e->getMessage(),
'trace' => $e->getTrace(),
],
],
...$logContext,
'exception' => [
'file' => $e->getFile(),
'line' => $e->getLine(),
'kind' => get_class($e),
'message' => $e->getMessage(),
'trace' => $e->getTrace(),
],
]
e: $e
)
);
}
}

View File

@@ -77,7 +77,10 @@ class GetLibrariesList
$this->logger
);
} catch (RuntimeException $e) {
return new Response(status: false, error: new Error(message: $e->getMessage(), level: Levels::ERROR));
return new Response(
status: false,
error: new Error(message: $e->getMessage(), level: Levels::ERROR, previous: $e)
);
}
if ($context->trace) {

View File

@@ -192,69 +192,78 @@ class Import
}
} catch (ExceptionInterface $e) {
$this->logger->error(
message: "Request for '{client}: {backend}' libraries has failed. '{error.kind}' with message '{error.message}' at '{error.file}:{error.line}'.",
context: [
'client' => $context->clientName,
'backend' => $context->backendName,
'error' => [
'line' => $e->getLine(),
'kind' => $e::class,
'message' => $e->getMessage(),
'file' => after($e->getFile(), ROOT_PATH),
...lw(
message: "Request for '{client}: {backend}' libraries has failed. '{error.kind}' with message '{error.message}' at '{error.file}:{error.line}'.",
context: [
'client' => $context->clientName,
'backend' => $context->backendName,
'error' => [
'line' => $e->getLine(),
'kind' => $e::class,
'message' => $e->getMessage(),
'file' => after($e->getFile(), ROOT_PATH),
],
'exception' => [
'file' => $e->getFile(),
'line' => $e->getLine(),
'kind' => $e::class,
'message' => $e->getMessage(),
'trace' => $e->getTrace(),
],
],
'exception' => [
'file' => $e->getFile(),
'line' => $e->getLine(),
'kind' => $e::class,
'message' => $e->getMessage(),
'trace' => $e->getTrace(),
],
]
e: $e
)
);
Message::add("{$context->backendName}.has_errors", true);
return [];
} catch (JsonException $e) {
$this->logger->error(
message: "Request for '{client}: {backend}' libraries returned with invalid body. '{error.kind}' with message '{error.message}' at '{error.file}:{error.line}'.",
context: [
'client' => $context->clientName,
'backend' => $context->backendName,
'error' => [
'line' => $e->getLine(),
'kind' => $e::class,
'message' => $e->getMessage(),
'file' => after($e->getFile(), ROOT_PATH),
...lw(
message: "Request for '{client}: {backend}' libraries returned with invalid body. '{error.kind}' with message '{error.message}' at '{error.file}:{error.line}'.",
context: [
'client' => $context->clientName,
'backend' => $context->backendName,
'error' => [
'line' => $e->getLine(),
'kind' => $e::class,
'message' => $e->getMessage(),
'file' => after($e->getFile(), ROOT_PATH),
],
'exception' => [
'file' => $e->getFile(),
'line' => $e->getLine(),
'message' => $e->getMessage(),
'trace' => $e->getTrace(),
],
],
'exception' => [
'file' => $e->getFile(),
'line' => $e->getLine(),
'message' => $e->getMessage(),
'trace' => $e->getTrace(),
],
]
e: $e
)
);
Message::add("{$context->backendName}.has_errors", true);
return [];
} catch (Throwable $e) {
$this->logger->error(
message: "Exception '{error.kind}' was thrown unhandled during '{client}: {backend}' request for libraries. '{error.message}' at '{error.file}:{error.line}'.",
context: [
'backend' => $context->backendName,
'client' => $context->clientName,
'error' => [
'kind' => $e::class,
'line' => $e->getLine(),
'message' => $e->getMessage(),
'file' => after($e->getFile(), ROOT_PATH),
...lw(
message: "Exception '{error.kind}' was thrown unhandled during '{client}: {backend}' request for libraries. '{error.message}' at '{error.file}:{error.line}'.",
context: [
'backend' => $context->backendName,
'client' => $context->clientName,
'error' => [
'kind' => $e::class,
'line' => $e->getLine(),
'message' => $e->getMessage(),
'file' => after($e->getFile(), ROOT_PATH),
],
'exception' => [
'file' => $e->getFile(),
'line' => $e->getLine(),
'kind' => get_class($e),
'message' => $e->getMessage(),
'trace' => $e->getTrace(),
],
],
'exception' => [
'file' => $e->getFile(),
'line' => $e->getLine(),
'kind' => get_class($e),
'message' => $e->getMessage(),
'trace' => $e->getTrace(),
],
]
e: $e
)
);
Message::add("{$context->backendName}.has_errors", true);
return [];
@@ -319,48 +328,54 @@ class Import
);
} catch (ExceptionInterface $e) {
$this->logger->error(
message: "Request for '{client}: {backend}' '{library.title}' items count failed. '{error.kind}' with message '{error.message}' at '{error.file}:{error.line}'.",
context: [
'client' => $context->clientName,
'backend' => $context->backendName,
'error' => [
'line' => $e->getLine(),
'kind' => $e::class,
'message' => $e->getMessage(),
'file' => after($e->getFile(), ROOT_PATH),
...lw(
message: "Request for '{client}: {backend}' '{library.title}' items count failed. '{error.kind}' with message '{error.message}' at '{error.file}:{error.line}'.",
context: [
'client' => $context->clientName,
'backend' => $context->backendName,
'error' => [
'line' => $e->getLine(),
'kind' => $e::class,
'message' => $e->getMessage(),
'file' => after($e->getFile(), ROOT_PATH),
],
'exception' => [
'file' => $e->getFile(),
'line' => $e->getLine(),
'kind' => get_class($e),
'message' => $e->getMessage(),
'trace' => $e->getTrace(),
],
...$logContext,
],
'exception' => [
'file' => $e->getFile(),
'line' => $e->getLine(),
'kind' => get_class($e),
'message' => $e->getMessage(),
'trace' => $e->getTrace(),
],
...$logContext,
]
e: $e
)
);
continue;
} catch (Throwable $e) {
$this->logger->error(
message: "Exception '{error.kind}' was thrown unhandled during '{client}: {backend}' '{library.title}' items count request. '{error.message}' at '{error.file}:{error.line}'.",
context: [
'client' => $context->clientName,
'backend' => $context->backendName,
'error' => [
'kind' => $e::class,
'line' => $e->getLine(),
'message' => $e->getMessage(),
'file' => after($e->getFile(), ROOT_PATH),
...lw(
message: "Exception '{error.kind}' was thrown unhandled during '{client}: {backend}' '{library.title}' items count request. '{error.message}' at '{error.file}:{error.line}'.",
context: [
'client' => $context->clientName,
'backend' => $context->backendName,
'error' => [
'kind' => $e::class,
'line' => $e->getLine(),
'message' => $e->getMessage(),
'file' => after($e->getFile(), ROOT_PATH),
],
'exception' => [
'file' => $e->getFile(),
'line' => $e->getLine(),
'kind' => get_class($e),
'message' => $e->getMessage(),
'trace' => $e->getTrace(),
],
...$logContext,
],
'exception' => [
'file' => $e->getFile(),
'line' => $e->getLine(),
'kind' => get_class($e),
'message' => $e->getMessage(),
'trace' => $e->getTrace(),
],
...$logContext,
]
e: $e
)
);
continue;
}
@@ -404,6 +419,7 @@ class Import
$total[ag($logContext, 'library.id')] = $totalCount;
} catch (ExceptionInterface $e) {
$this->logger->error(
...lw(
message: "Request for '{client}: {backend}' '{library.title}' total items has failed. '{error.kind}' '{error.message}' at '{error.file}:{error.line}'.",
context: [
'client' => $context->backendName,
@@ -422,31 +438,35 @@ class Import
'trace' => $e->getTrace(),
],
...$logContext,
]
],
e: $e
),
);
continue;
} catch (Throwable $e) {
$this->logger->error(
message: "Exception '{error.kind}' was thrown unhandled during '{client}: {backend}' requests for items count. '{error.message}' at '{error.file}:{error.line}'.",
context: [
'client' => $context->clientName,
'backend' => $context->backendName,
'error' => [
'kind' => $e::class,
'line' => $e->getLine(),
'message' => $e->getMessage(),
'file' => after($e->getFile(), ROOT_PATH),
...lw(
message: "Exception '{error.kind}' was thrown unhandled during '{client}: {backend}' requests for items count. '{error.message}' at '{error.file}:{error.line}'.",
context: [
'client' => $context->clientName,
'backend' => $context->backendName,
'error' => [
'kind' => $e::class,
'line' => $e->getLine(),
'message' => $e->getMessage(),
'file' => after($e->getFile(), ROOT_PATH),
],
'exception' => [
'file' => $e->getFile(),
'line' => $e->getLine(),
'kind' => get_class($e),
'message' => $e->getMessage(),
'trace' => $e->getTrace(),
],
...$logContext,
],
'exception' => [
'file' => $e->getFile(),
'line' => $e->getLine(),
'kind' => get_class($e),
'message' => $e->getMessage(),
'trace' => $e->getTrace(),
],
...$logContext,
]
e: $e
)
);
continue;
}
@@ -515,29 +535,33 @@ class Import
);
} catch (ExceptionInterface $e) {
$this->logger->error(
message: "Request for '{client}: {backend}' '{library.title}' series external ids has failed. '{error.kind}' with message '{error.message}' at '{error.file}:{error.line}'.",
context: [
'client' => $context->clientName,
'backend' => $context->backendName,
'error' => [
'line' => $e->getLine(),
'kind' => $e::class,
'message' => $e->getMessage(),
'file' => after($e->getFile(), ROOT_PATH),
...lw(
message: "Request for '{client}: {backend}' '{library.title}' series external ids has failed. '{error.kind}' with message '{error.message}' at '{error.file}:{error.line}'.",
context: [
'client' => $context->clientName,
'backend' => $context->backendName,
'error' => [
'line' => $e->getLine(),
'kind' => $e::class,
'message' => $e->getMessage(),
'file' => after($e->getFile(), ROOT_PATH),
],
'exception' => [
'file' => $e->getFile(),
'line' => $e->getLine(),
'kind' => get_class($e),
'message' => $e->getMessage(),
'trace' => $e->getTrace(),
],
...$logContext,
],
'exception' => [
'file' => $e->getFile(),
'line' => $e->getLine(),
'kind' => get_class($e),
'message' => $e->getMessage(),
'trace' => $e->getTrace(),
],
...$logContext,
]
e: $e
)
);
continue;
} catch (Throwable $e) {
$this->logger->error(
...lw(
message: "Exception '{error.kind}' was thrown unhandled during '{client}: {backend}' '{library.title}' series external ids request. '{error.message}' at '{error.file}:{error.line}'.",
context: [
'backend' => $context->backendName,
@@ -556,7 +580,9 @@ class Import
'trace' => $e->getTrace(),
],
...$logContext,
]
],
e: $e
),
);
continue;
}
@@ -663,48 +689,54 @@ class Import
);
} catch (ExceptionInterface $e) {
$this->logger->error(
"Request for '{client}: {backend}' '{library.title} {segment.number}/{segment.of}' content list has failed. {error.kind}' with message '{error.message}' at '{error.file}:{error.line}'.",
[
'client' => $context->clientName,
'backend' => $context->backendName,
'error' => [
'line' => $e->getLine(),
'kind' => $e::class,
'message' => $e->getMessage(),
'file' => after($e->getFile(), ROOT_PATH),
...lw(
message: "Request for '{client}: {backend}' '{library.title} {segment.number}/{segment.of}' content list has failed. {error.kind}' with message '{error.message}' at '{error.file}:{error.line}'.",
context: [
'client' => $context->clientName,
'backend' => $context->backendName,
'error' => [
'line' => $e->getLine(),
'kind' => $e::class,
'message' => $e->getMessage(),
'file' => after($e->getFile(), ROOT_PATH),
],
...$logContext,
'exception' => [
'file' => $e->getFile(),
'line' => $e->getLine(),
'kind' => get_class($e),
'message' => $e->getMessage(),
'trace' => $e->getTrace(),
],
],
...$logContext,
'exception' => [
'file' => $e->getFile(),
'line' => $e->getLine(),
'kind' => get_class($e),
'message' => $e->getMessage(),
'trace' => $e->getTrace(),
],
]
e: $e
)
);
continue;
} catch (Throwable $e) {
$this->logger->error(
message: "Exception '{error.kind}' was thrown unhandled during '{client}: {backend}' '{library.title} {segment.number}/{segment.of}' content list request. '{error.message}' at '{error.file}:{error.line}'.",
context: [
'backend' => $context->backendName,
'client' => $context->clientName,
'error' => [
'kind' => $e::class,
'line' => $e->getLine(),
'message' => $e->getMessage(),
'file' => after($e->getFile(), ROOT_PATH),
...lw(
message: "Exception '{error.kind}' was thrown unhandled during '{client}: {backend}' '{library.title} {segment.number}/{segment.of}' content list request. '{error.message}' at '{error.file}:{error.line}'.",
context: [
'backend' => $context->backendName,
'client' => $context->clientName,
'error' => [
'kind' => $e::class,
'line' => $e->getLine(),
'message' => $e->getMessage(),
'file' => after($e->getFile(), ROOT_PATH),
],
...$logContext,
'exception' => [
'file' => $e->getFile(),
'line' => $e->getLine(),
'kind' => get_class($e),
'message' => $e->getMessage(),
'trace' => $e->getTrace(),
],
],
...$logContext,
'exception' => [
'file' => $e->getFile(),
'line' => $e->getLine(),
'kind' => get_class($e),
'message' => $e->getMessage(),
'trace' => $e->getTrace(),
],
]
e: $e
)
);
continue;
}
@@ -850,50 +882,56 @@ class Import
}
} catch (Throwable $e) {
$this->logger->error(
message: "Exception '{error.kind}' was thrown unhandled during '{client}: {backend}' parsing '{library.title} {segment.number}/{segment.of}' item response. '{error.message}' at '{error.file}:{error.line}'.",
context: [
'backend' => $context->backendName,
'client' => $context->clientName,
'error' => [
'kind' => $e::class,
'line' => $e->getLine(),
'message' => $e->getMessage(),
'file' => after($e->getFile(), ROOT_PATH),
...lw(
message: "Exception '{error.kind}' was thrown unhandled during '{client}: {backend}' parsing '{library.title} {segment.number}/{segment.of}' item response. '{error.message}' at '{error.file}:{error.line}'.",
context: [
'backend' => $context->backendName,
'client' => $context->clientName,
'error' => [
'kind' => $e::class,
'line' => $e->getLine(),
'message' => $e->getMessage(),
'file' => after($e->getFile(), ROOT_PATH),
],
'entity' => $entity,
'exception' => [
'kind' => $e::class,
'line' => $e->getLine(),
'trace' => $e->getTrace(),
'message' => $e->getMessage(),
'file' => after($e->getFile(), ROOT_PATH),
],
...$logContext,
],
'entity' => $entity,
'exception' => [
'kind' => $e::class,
'line' => $e->getLine(),
'trace' => $e->getTrace(),
'message' => $e->getMessage(),
'file' => after($e->getFile(), ROOT_PATH),
],
...$logContext,
]
e: $e
)
);
}
}
} catch (Throwable $e) {
$this->logger->error(
message: "Exception '{error.kind}' was thrown unhandled during '{client}: {backend}' parsing of '{library.title} {segment.number}/{segment.of}' response. '{error.message}' at '{error.file}:{error.line}'.",
context: [
'backend' => $context->backendName,
'client' => $context->clientName,
'error' => [
'kind' => $e::class,
'line' => $e->getLine(),
'message' => $e->getMessage(),
'file' => after($e->getFile(), ROOT_PATH),
...lw(
message: "Exception '{error.kind}' was thrown unhandled during '{client}: {backend}' parsing of '{library.title} {segment.number}/{segment.of}' response. '{error.message}' at '{error.file}:{error.line}'.",
context: [
'backend' => $context->backendName,
'client' => $context->clientName,
'error' => [
'kind' => $e::class,
'line' => $e->getLine(),
'message' => $e->getMessage(),
'file' => after($e->getFile(), ROOT_PATH),
],
'exception' => [
'line' => $e->getLine(),
'kind' => get_class($e),
'message' => $e->getMessage(),
'trace' => $e->getTrace(),
'file' => after($e->getFile(), ROOT_PATH),
],
...$logContext,
],
'exception' => [
'line' => $e->getLine(),
'kind' => get_class($e),
'message' => $e->getMessage(),
'trace' => $e->getTrace(),
'file' => after($e->getFile(), ROOT_PATH),
],
...$logContext,
]
e: $e
)
);
}
@@ -1035,19 +1073,22 @@ class Import
];
} catch (InvalidArgumentException $e) {
$this->logger->error(
"Failed to parse '{client}: {backend}' item response. '{error.kind}' with '{error.message}' at '{error.file}:{error.line}' ",
[
'client' => $context->clientName,
'backend' => $context->backendName,
'error' => [
'kind' => $e::class,
'line' => $e->getLine(),
'message' => $e->getMessage(),
'file' => after($e->getFile(), ROOT_PATH),
...lw(
message: "Failed to parse '{client}: {backend}' item response. '{error.kind}' with '{error.message}' at '{error.file}:{error.line}' ",
context: [
'client' => $context->clientName,
'backend' => $context->backendName,
'error' => [
'kind' => $e::class,
'line' => $e->getLine(),
'message' => $e->getMessage(),
'file' => after($e->getFile(), ROOT_PATH),
],
'body' => $item,
...$logContext,
],
'body' => $item,
...$logContext,
]
e: $e
)
);
return;
}
@@ -1110,18 +1151,21 @@ class Import
);
} catch (Throwable $e) {
$this->logger->error(
message: "Exception '{error.kind}' occurred during '{client}: {backend}' '{library.title}' '{item.id}: {item.title}' entity creation. '{error.message}' at '{error.file}:{error.line}'.",
context: [
'backend' => $context->backendName,
'client' => $context->clientName,
'error' => [
'kind' => $e::class,
'line' => $e->getLine(),
'message' => $e->getMessage(),
'file' => after($e->getFile(), ROOT_PATH),
...lw(
message: "Exception '{error.kind}' occurred during '{client}: {backend}' '{library.title}' '{item.id}: {item.title}' entity creation. '{error.message}' at '{error.file}:{error.line}'.",
context: [
'backend' => $context->backendName,
'client' => $context->clientName,
'error' => [
'kind' => $e::class,
'line' => $e->getLine(),
'message' => $e->getMessage(),
'file' => after($e->getFile(), ROOT_PATH),
],
...$logContext,
],
...$logContext,
]
e: $e
)
);
Message::increment("{$context->backendName}.{$mappedType}.ignored_no_date_is_set");
@@ -1155,26 +1199,29 @@ class Import
$mapper->add(entity: $entity, opts: $opts);
} catch (Throwable $e) {
$this->logger->error(
message: "Exception '{error.kind}' was thrown unhandled during '{client}: {backend}' '{library.title}' '{item.title}' {action}. '{error.message}' at '{error.file}:{error.line}'.",
context: [
'action' => property_exists($this, 'action') ? $this->action : 'import',
'backend' => $context->backendName,
'client' => $context->clientName,
'error' => [
'kind' => $e::class,
'line' => $e->getLine(),
'message' => $e->getMessage(),
'file' => after($e->getFile(), ROOT_PATH),
...lw(
message: "Exception '{error.kind}' was thrown unhandled during '{client}: {backend}' '{library.title}' '{item.title}' {action}. '{error.message}' at '{error.file}:{error.line}'.",
context: [
'action' => property_exists($this, 'action') ? $this->action : 'import',
'backend' => $context->backendName,
'client' => $context->clientName,
'error' => [
'kind' => $e::class,
'line' => $e->getLine(),
'message' => $e->getMessage(),
'file' => after($e->getFile(), ROOT_PATH),
],
'exception' => [
'file' => $e->getFile(),
'line' => $e->getLine(),
'kind' => get_class($e),
'message' => $e->getMessage(),
'trace' => $e->getTrace(),
],
...$logContext,
],
'exception' => [
'file' => $e->getFile(),
'line' => $e->getLine(),
'kind' => get_class($e),
'message' => $e->getMessage(),
'trace' => $e->getTrace(),
],
...$logContext,
]
e: $e
)
);
}
}

View File

@@ -265,7 +265,8 @@ final class ParseWebhook
'payload' => $request->getParsedBody(),
],
],
level: Levels::ERROR
level: Levels::ERROR,
previous: $e
),
extra: [
'http_code' => 200,

View File

@@ -254,6 +254,7 @@ class Progress
}
} catch (\App\Libs\Exceptions\RuntimeException|RuntimeException|InvalidArgumentException $e) {
$this->logger->error(
...lw(
message: "{action}: Exception '{error.kind}' was thrown unhandled during '{client}: {backend}' get {item.type} '{item.title}' status. '{error.message}' at '{error.file}:{error.line}'.",
context: [
'action' => $this->action,
@@ -273,7 +274,9 @@ class Progress
'message' => $e->getMessage(),
'trace' => $e->getTrace(),
],
]
],
e: $e
),
);
continue;
}
@@ -321,26 +324,29 @@ class Progress
}
} catch (Throwable $e) {
$this->logger->error(
message: "{action}: Exception '{error.kind}' was thrown unhandled during '{client}: {backend}' change {item.type} '{item.title}' watch progress. '{error.message}' at '{error.file}:{error.line}'.",
context: [
'action' => $this->action,
'backend' => $context->backendName,
'client' => $context->clientName,
'error' => [
'kind' => $e::class,
'line' => $e->getLine(),
'message' => $e->getMessage(),
'file' => after($e->getFile(), ROOT_PATH),
...lw(
message: "{action}: Exception '{error.kind}' was thrown unhandled during '{client}: {backend}' change {item.type} '{item.title}' watch progress. '{error.message}' at '{error.file}:{error.line}'.",
context: [
'action' => $this->action,
'backend' => $context->backendName,
'client' => $context->clientName,
'error' => [
'kind' => $e::class,
'line' => $e->getLine(),
'message' => $e->getMessage(),
'file' => after($e->getFile(), ROOT_PATH),
],
...$logContext,
'exception' => [
'file' => $e->getFile(),
'line' => $e->getLine(),
'kind' => get_class($e),
'message' => $e->getMessage(),
'trace' => $e->getTrace(),
],
],
...$logContext,
'exception' => [
'file' => $e->getFile(),
'line' => $e->getLine(),
'kind' => get_class($e),
'message' => $e->getMessage(),
'trace' => $e->getTrace(),
],
]
e: $e
)
);
}
}

View File

@@ -145,25 +145,28 @@ class Push
);
} catch (Throwable $e) {
$this->logger->error(
message: 'Exception [{error.kind}] was thrown unhandled during [{client}: {backend}] request for {item.type} [{item.title}] metadata. Error [{error.message} @ {error.file}:{error.line}].',
context: [
'backend' => $context->backendName,
'client' => $context->clientName,
'error' => [
'kind' => $e::class,
'line' => $e->getLine(),
'message' => $e->getMessage(),
'file' => after($e->getFile(), ROOT_PATH),
...lw(
message: 'Exception [{error.kind}] was thrown unhandled during [{client}: {backend}] request for {item.type} [{item.title}] metadata. Error [{error.message} @ {error.file}:{error.line}].',
context: [
'backend' => $context->backendName,
'client' => $context->clientName,
'error' => [
'kind' => $e::class,
'line' => $e->getLine(),
'message' => $e->getMessage(),
'file' => after($e->getFile(), ROOT_PATH),
],
...$logContext,
'exception' => [
'file' => $e->getFile(),
'line' => $e->getLine(),
'kind' => get_class($e),
'message' => $e->getMessage(),
'trace' => $e->getTrace(),
],
],
...$logContext,
'exception' => [
'file' => $e->getFile(),
'line' => $e->getLine(),
'kind' => get_class($e),
'message' => $e->getMessage(),
'trace' => $e->getTrace(),
],
]
e: $e
)
);
}
}
@@ -327,25 +330,28 @@ class Push
}
} catch (Throwable $e) {
$this->logger->error(
message: 'Exception [{error.kind}] was thrown unhandled during [{client}: {backend}] parsing [{library.title}] [{segment.number}/{segment.of}] response. Error [{error.message} @ {error.file}:{error.line}].',
context: [
'backend' => $context->backendName,
'client' => $context->clientName,
'error' => [
'kind' => $e::class,
'line' => $e->getLine(),
'message' => $e->getMessage(),
'file' => after($e->getFile(), ROOT_PATH),
...lw(
message: 'Exception [{error.kind}] was thrown unhandled during [{client}: {backend}] parsing [{library.title}] [{segment.number}/{segment.of}] response. Error [{error.message} @ {error.file}:{error.line}].',
context: [
'backend' => $context->backendName,
'client' => $context->clientName,
'error' => [
'kind' => $e::class,
'line' => $e->getLine(),
'message' => $e->getMessage(),
'file' => after($e->getFile(), ROOT_PATH),
],
...$logContext,
'exception' => [
'file' => $e->getFile(),
'line' => $e->getLine(),
'kind' => get_class($e),
'message' => $e->getMessage(),
'trace' => $e->getTrace(),
],
],
...$logContext,
'exception' => [
'file' => $e->getFile(),
'line' => $e->getLine(),
'kind' => get_class($e),
'message' => $e->getMessage(),
'trace' => $e->getTrace(),
],
]
e: $e
)
);
}
}

View File

@@ -76,19 +76,22 @@ final class Export extends Import
];
} catch (InvalidArgumentException $e) {
$this->logger->error(
"Failed to parse '{client}: {backend}' item response. '{error.kind}' with '{error.message}' at '{error.file}:{error.line}' ",
[
'client' => $context->clientName,
'backend' => $context->backendName,
'error' => [
'kind' => $e::class,
'line' => $e->getLine(),
'message' => $e->getMessage(),
'file' => after($e->getFile(), ROOT_PATH),
...lw(
message: "Failed to parse '{client}: {backend}' item response. '{error.kind}' with '{error.message}' at '{error.file}:{error.line}' ",
context: [
'client' => $context->clientName,
'backend' => $context->backendName,
'error' => [
'kind' => $e::class,
'line' => $e->getLine(),
'message' => $e->getMessage(),
'file' => after($e->getFile(), ROOT_PATH),
],
...$logContext,
'body' => $item,
],
...$logContext,
'body' => $item,
]
e: $e
)
);
return;
}
@@ -242,25 +245,28 @@ final class Export extends Import
])));
} catch (Throwable $e) {
$this->logger->error(
message: "Exception '{error.kind}' was thrown unhandled during '{client}: {backend}' export. '{error.message}' at '{error.file}:{error.line}'.",
context: [
'backend' => $context->backendName,
'client' => $context->clientName,
'error' => [
'kind' => $e::class,
'line' => $e->getLine(),
'message' => $e->getMessage(),
'file' => after($e->getFile(), ROOT_PATH),
...lw(
message: "Exception '{error.kind}' was thrown unhandled during '{client}: {backend}' export. '{error.message}' at '{error.file}:{error.line}'.",
context: [
'backend' => $context->backendName,
'client' => $context->clientName,
'error' => [
'kind' => $e::class,
'line' => $e->getLine(),
'message' => $e->getMessage(),
'file' => after($e->getFile(), ROOT_PATH),
],
...$logContext,
'exception' => [
'file' => $e->getFile(),
'line' => $e->getLine(),
'kind' => get_class($e),
'message' => $e->getMessage(),
'trace' => $e->getTrace(),
],
],
...$logContext,
'exception' => [
'file' => $e->getFile(),
'line' => $e->getLine(),
'kind' => get_class($e),
'message' => $e->getMessage(),
'trace' => $e->getTrace(),
],
]
e: $e
)
);
}
}

View File

@@ -66,7 +66,10 @@ final class GetLibrariesList
$this->logger
);
} catch (RuntimeException $e) {
return new Response(status: false, error: new Error(message: $e->getMessage(), level: Levels::ERROR));
return new Response(
status: false,
error: new Error(message: $e->getMessage(), level: Levels::ERROR, previous: $e)
);
}
if ($context->trace) {
@@ -82,7 +85,7 @@ final class GetLibrariesList
return new Response(
status: false,
error: new Error(
message: 'Request for [{backend}] libraries returned empty list.',
message: "Request for '{backend}' libraries returned empty list.",
context: [
'backend' => $context->backendName,
'response' => [
@@ -153,7 +156,7 @@ final class GetLibrariesList
{
$url = $context->backendUrl->withPath('/library/sections');
$this->logger->debug('Requesting [{backend}] libraries list.', [
$this->logger->debug("Requesting '{backend}' libraries list.", [
'backend' => $context->backendName,
'url' => (string)$url
]);
@@ -163,7 +166,7 @@ final class GetLibrariesList
$payload = $response->getContent(false);
if ($context->trace) {
$this->logger->debug('Processing [{backend}] response.', [
$this->logger->debug("Processing '{backend}' response.", [
'backend' => $context->backendName,
'url' => (string)$url,
'response' => $payload,
@@ -172,13 +175,10 @@ final class GetLibrariesList
if (200 !== $response->getStatusCode()) {
throw new RuntimeException(
r(
'Request for [{backend}] libraries returned with unexpected [{status_code}] status code.',
[
'backend' => $context->backendName,
'status_code' => $response->getStatusCode(),
]
)
r("Request for '{backend}' libraries returned with unexpected '{status_code}' status code.", [
'backend' => $context->backendName,
'status_code' => $response->getStatusCode(),
])
);
}

View File

@@ -246,7 +246,8 @@ final class GetUserToken
'trace' => $e->getTrace(),
],
],
level: Levels::ERROR
level: Levels::ERROR,
previous: $e
),
);
}

View File

@@ -173,69 +173,78 @@ class Import
}
} catch (ExceptionInterface $e) {
$this->logger->error(
message: "Request for '{client}: {backend}' libraries has failed. '{error.kind}' with message '{error.message}' at '{error.file}:{error.line}'.",
context: [
'client' => $context->clientName,
'backend' => $context->backendName,
'error' => [
'line' => $e->getLine(),
'kind' => $e::class,
'message' => $e->getMessage(),
'file' => after($e->getFile(), ROOT_PATH),
...lw(
message: "Request for '{client}: {backend}' libraries has failed. '{error.kind}' with message '{error.message}' at '{error.file}:{error.line}'.",
context: [
'client' => $context->clientName,
'backend' => $context->backendName,
'error' => [
'line' => $e->getLine(),
'kind' => $e::class,
'message' => $e->getMessage(),
'file' => after($e->getFile(), ROOT_PATH),
],
'exception' => [
'file' => $e->getFile(),
'line' => $e->getLine(),
'kind' => $e::class,
'message' => $e->getMessage(),
'trace' => $e->getTrace(),
],
],
'exception' => [
'file' => $e->getFile(),
'line' => $e->getLine(),
'kind' => $e::class,
'message' => $e->getMessage(),
'trace' => $e->getTrace(),
],
]
e: $e
)
);
Message::add("{$context->backendName}.has_errors", true);
return [];
} catch (JsonException $e) {
$this->logger->error(
message: "Request for '{client}: {backend}' libraries returned with invalid body. '{error.kind}' with message '{error.message}' at '{error.file}:{error.line}'.",
context: [
'client' => $context->clientName,
'backend' => $context->backendName,
'error' => [
'line' => $e->getLine(),
'kind' => $e::class,
'message' => $e->getMessage(),
'file' => after($e->getFile(), ROOT_PATH),
...lw(
message: "Request for '{client}: {backend}' libraries returned with invalid body. '{error.kind}' with message '{error.message}' at '{error.file}:{error.line}'.",
context: [
'client' => $context->clientName,
'backend' => $context->backendName,
'error' => [
'line' => $e->getLine(),
'kind' => $e::class,
'message' => $e->getMessage(),
'file' => after($e->getFile(), ROOT_PATH),
],
'exception' => [
'file' => $e->getFile(),
'line' => $e->getLine(),
'message' => $e->getMessage(),
'trace' => $e->getTrace(),
],
],
'exception' => [
'file' => $e->getFile(),
'line' => $e->getLine(),
'message' => $e->getMessage(),
'trace' => $e->getTrace(),
],
]
e: $e
)
);
Message::add("{$context->backendName}.has_errors", true);
return [];
} catch (Throwable $e) {
$this->logger->error(
message: "Exception '{error.kind}' was thrown unhandled during '{client}: {backend}' request for libraries. '{error.message}' at '{error.file}:{error.line}'.",
context: [
'backend' => $context->backendName,
'client' => $context->clientName,
'error' => [
'kind' => $e::class,
'line' => $e->getLine(),
'message' => $e->getMessage(),
'file' => after($e->getFile(), ROOT_PATH),
...lw(
message: "Exception '{error.kind}' was thrown unhandled during '{client}: {backend}' request for libraries. '{error.message}' at '{error.file}:{error.line}'.",
context: [
'backend' => $context->backendName,
'client' => $context->clientName,
'error' => [
'kind' => $e::class,
'line' => $e->getLine(),
'message' => $e->getMessage(),
'file' => after($e->getFile(), ROOT_PATH),
],
'exception' => [
'file' => $e->getFile(),
'line' => $e->getLine(),
'kind' => get_class($e),
'message' => $e->getMessage(),
'trace' => $e->getTrace(),
],
],
'exception' => [
'file' => $e->getFile(),
'line' => $e->getLine(),
'kind' => get_class($e),
'message' => $e->getMessage(),
'trace' => $e->getTrace(),
],
]
e: $e
)
);
Message::add("{$context->backendName}.has_errors", true);
return [];
@@ -351,48 +360,54 @@ class Import
}
} catch (ExceptionInterface $e) {
$this->logger->error(
message: "Request for '{client}: {backend}' '{library.title}' items count has failed. '{error.kind}' with message '{error.message}' at '{error.file}:{error.line}'.",
context: [
'client' => $context->clientName,
'backend' => $context->backendName,
'error' => [
'line' => $e->getLine(),
'kind' => $e::class,
'message' => $e->getMessage(),
'file' => after($e->getFile(), ROOT_PATH),
...lw(
message: "Request for '{client}: {backend}' '{library.title}' items count has failed. '{error.kind}' with message '{error.message}' at '{error.file}:{error.line}'.",
context: [
'client' => $context->clientName,
'backend' => $context->backendName,
'error' => [
'line' => $e->getLine(),
'kind' => $e::class,
'message' => $e->getMessage(),
'file' => after($e->getFile(), ROOT_PATH),
],
'exception' => [
'file' => $e->getFile(),
'line' => $e->getLine(),
'kind' => get_class($e),
'message' => $e->getMessage(),
'trace' => $e->getTrace(),
],
...$logContext,
],
'exception' => [
'file' => $e->getFile(),
'line' => $e->getLine(),
'kind' => get_class($e),
'message' => $e->getMessage(),
'trace' => $e->getTrace(),
],
...$logContext,
]
e: $e
)
);
continue;
} catch (Throwable $e) {
$this->logger->error(
message: "Exception '{error.kind}' was thrown unhandled during '{client}: {backend}' request for libraries. '{error.message}' at '{error.file}:{error.line}'.",
context: [
'client' => $context->clientName,
'backend' => $context->backendName,
'error' => [
'kind' => $e::class,
'line' => $e->getLine(),
'message' => $e->getMessage(),
'file' => after($e->getFile(), ROOT_PATH),
...lw(
message: "Exception '{error.kind}' was thrown unhandled during '{client}: {backend}' request for libraries. '{error.message}' at '{error.file}:{error.line}'.",
context: [
'client' => $context->clientName,
'backend' => $context->backendName,
'error' => [
'kind' => $e::class,
'line' => $e->getLine(),
'message' => $e->getMessage(),
'file' => after($e->getFile(), ROOT_PATH),
],
'exception' => [
'file' => $e->getFile(),
'line' => $e->getLine(),
'kind' => get_class($e),
'message' => $e->getMessage(),
'trace' => $e->getTrace(),
],
...$logContext,
],
'exception' => [
'file' => $e->getFile(),
'line' => $e->getLine(),
'kind' => get_class($e),
'message' => $e->getMessage(),
'trace' => $e->getTrace(),
],
...$logContext,
]
e: $e
)
);
continue;
}
@@ -438,48 +453,54 @@ class Import
}
} catch (ExceptionInterface $e) {
$this->logger->error(
message: "Request for '{client}: {backend}' '{library.title}' total items has failed. '{error.kind}' '{error.message}' at '{error.file}:{error.line}'.",
context: [
'client' => $context->backendName,
'backend' => $context->backendName,
'error' => [
'kind' => $e::class,
'line' => $e->getLine(),
'message' => $e->getMessage(),
'file' => after($e->getFile(), ROOT_PATH),
...lw(
message: "Request for '{client}: {backend}' '{library.title}' total items has failed. '{error.kind}' '{error.message}' at '{error.file}:{error.line}'.",
context: [
'client' => $context->backendName,
'backend' => $context->backendName,
'error' => [
'kind' => $e::class,
'line' => $e->getLine(),
'message' => $e->getMessage(),
'file' => after($e->getFile(), ROOT_PATH),
],
'exception' => [
'file' => $e->getFile(),
'line' => $e->getLine(),
'kind' => get_class($e),
'message' => $e->getMessage(),
'trace' => $e->getTrace(),
],
...$logContext,
],
'exception' => [
'file' => $e->getFile(),
'line' => $e->getLine(),
'kind' => get_class($e),
'message' => $e->getMessage(),
'trace' => $e->getTrace(),
],
...$logContext,
]
e: $e
)
);
continue;
} catch (Throwable $e) {
$this->logger->error(
message: "Exception '{error.kind}' was thrown unhandled during '{client}: {backend}' request for items count. '{error.message}' at '{error.file}:{error.line}'.",
context: [
'client' => $context->clientName,
'backend' => $context->backendName,
'error' => [
'kind' => $e::class,
'line' => $e->getLine(),
'message' => $e->getMessage(),
'file' => after($e->getFile(), ROOT_PATH),
...lw(
message: "Exception '{error.kind}' was thrown unhandled during '{client}: {backend}' request for items count. '{error.message}' at '{error.file}:{error.line}'.",
context: [
'client' => $context->clientName,
'backend' => $context->backendName,
'error' => [
'kind' => $e::class,
'line' => $e->getLine(),
'message' => $e->getMessage(),
'file' => after($e->getFile(), ROOT_PATH),
],
'exception' => [
'file' => $e->getFile(),
'line' => $e->getLine(),
'kind' => get_class($e),
'message' => $e->getMessage(),
'trace' => $e->getTrace(),
],
...$logContext,
],
'exception' => [
'file' => $e->getFile(),
'line' => $e->getLine(),
'kind' => get_class($e),
'message' => $e->getMessage(),
'trace' => $e->getTrace(),
],
...$logContext,
]
e: $e
)
);
continue;
}
@@ -576,48 +597,54 @@ class Import
);
} catch (ExceptionInterface $e) {
$this->logger->error(
message: "Request for '{client}: {backend}' '{library.title} {segment.number}/{segment.of}' series external ids has failed. '{error.kind}' with message '{error.message}' at '{error.file}:{error.line}'.",
context: [
'client' => $context->clientName,
'backend' => $context->backendName,
'error' => [
'line' => $e->getLine(),
'kind' => $e::class,
'message' => $e->getMessage(),
'file' => after($e->getFile(), ROOT_PATH),
...lw(
message: "Request for '{client}: {backend}' '{library.title} {segment.number}/{segment.of}' series external ids has failed. '{error.kind}' with message '{error.message}' at '{error.file}:{error.line}'.",
context: [
'client' => $context->clientName,
'backend' => $context->backendName,
'error' => [
'line' => $e->getLine(),
'kind' => $e::class,
'message' => $e->getMessage(),
'file' => after($e->getFile(), ROOT_PATH),
],
'exception' => [
'file' => $e->getFile(),
'line' => $e->getLine(),
'kind' => get_class($e),
'message' => $e->getMessage(),
'trace' => $e->getTrace(),
],
...$logContext,
],
'exception' => [
'file' => $e->getFile(),
'line' => $e->getLine(),
'kind' => get_class($e),
'message' => $e->getMessage(),
'trace' => $e->getTrace(),
],
...$logContext,
]
e: $e
)
);
continue;
} catch (Throwable $e) {
$this->logger->error(
message: "Exception '{error.kind}' was thrown unhandled during '{client}: {backend}' '{library.title} {segment.number}/{segment.of}' series external ids request. '{error.message}' at '{error.file}:{error.line}'.",
context: [
'backend' => $context->backendName,
'client' => $context->clientName,
'error' => [
'kind' => $e::class,
'line' => $e->getLine(),
'message' => $e->getMessage(),
'file' => after($e->getFile(), ROOT_PATH),
...lw(
message: "Exception '{error.kind}' was thrown unhandled during '{client}: {backend}' '{library.title} {segment.number}/{segment.of}' series external ids request. '{error.message}' at '{error.file}:{error.line}'.",
context: [
'backend' => $context->backendName,
'client' => $context->clientName,
'error' => [
'kind' => $e::class,
'line' => $e->getLine(),
'message' => $e->getMessage(),
'file' => after($e->getFile(), ROOT_PATH),
],
...$logContext,
'exception' => [
'file' => $e->getFile(),
'line' => $e->getLine(),
'kind' => get_class($e),
'message' => $e->getMessage(),
'trace' => $e->getTrace(),
],
],
...$logContext,
'exception' => [
'file' => $e->getFile(),
'line' => $e->getLine(),
'kind' => get_class($e),
'message' => $e->getMessage(),
'trace' => $e->getTrace(),
],
]
e: $e
)
);
continue;
}
@@ -732,48 +759,54 @@ class Import
);
} catch (ExceptionInterface $e) {
$this->logger->error(
"Request for '{client}: {backend}' '{library.title} {segment.number}/{segment.of}' content list has failed. {error.kind}' with message '{error.message}' at '{error.file}:{error.line}'.",
[
'client' => $context->clientName,
'backend' => $context->backendName,
'error' => [
'line' => $e->getLine(),
'kind' => $e::class,
'message' => $e->getMessage(),
'file' => after($e->getFile(), ROOT_PATH),
...lw(
message: "Request for '{client}: {backend}' '{library.title} {segment.number}/{segment.of}' content list has failed. {error.kind}' with message '{error.message}' at '{error.file}:{error.line}'.",
context: [
'client' => $context->clientName,
'backend' => $context->backendName,
'error' => [
'line' => $e->getLine(),
'kind' => $e::class,
'message' => $e->getMessage(),
'file' => after($e->getFile(), ROOT_PATH),
],
...$logContext,
'exception' => [
'file' => $e->getFile(),
'line' => $e->getLine(),
'kind' => get_class($e),
'message' => $e->getMessage(),
'trace' => $e->getTrace(),
],
],
...$logContext,
'exception' => [
'file' => $e->getFile(),
'line' => $e->getLine(),
'kind' => get_class($e),
'message' => $e->getMessage(),
'trace' => $e->getTrace(),
],
]
e: $e
)
);
continue;
} catch (Throwable $e) {
$this->logger->error(
message: "Exception '{error.kind}' was thrown unhandled during '{client}: {backend}' '{library.title} {segment.number}/{segment.of}' content list request. '{error.message}' at '{error.file}:{error.line}'.",
context: [
'backend' => $context->backendName,
'client' => $context->clientName,
'error' => [
'kind' => $e::class,
'line' => $e->getLine(),
'message' => $e->getMessage(),
'file' => after($e->getFile(), ROOT_PATH),
...lw(
message: "Exception '{error.kind}' was thrown unhandled during '{client}: {backend}' '{library.title} {segment.number}/{segment.of}' content list request. '{error.message}' at '{error.file}:{error.line}'.",
context: [
'backend' => $context->backendName,
'client' => $context->clientName,
'error' => [
'kind' => $e::class,
'line' => $e->getLine(),
'message' => $e->getMessage(),
'file' => after($e->getFile(), ROOT_PATH),
],
...$logContext,
'exception' => [
'file' => $e->getFile(),
'line' => $e->getLine(),
'kind' => get_class($e),
'message' => $e->getMessage(),
'trace' => $e->getTrace(),
],
],
...$logContext,
'exception' => [
'file' => $e->getFile(),
'line' => $e->getLine(),
'kind' => get_class($e),
'message' => $e->getMessage(),
'trace' => $e->getTrace(),
],
]
e: $e
)
);
continue;
}
@@ -863,50 +896,56 @@ class Import
$callback(item: $entity, logContext: $logContext);
} catch (Throwable $e) {
$this->logger->error(
message: "Exception '{error.kind}' was thrown unhandled during '{client}: {backend}' parsing '{library.title} {segment.number}/{segment.of}' item response. '{error.message}' at '{error.file}:{error.line}'.",
context: [
'backend' => $context->backendName,
'client' => $context->clientName,
'error' => [
'kind' => $e::class,
'line' => $e->getLine(),
'message' => $e->getMessage(),
'file' => after($e->getFile(), ROOT_PATH),
...lw(
message: "Exception '{error.kind}' was thrown unhandled during '{client}: {backend}' parsing '{library.title} {segment.number}/{segment.of}' item response. '{error.message}' at '{error.file}:{error.line}'.",
context: [
'backend' => $context->backendName,
'client' => $context->clientName,
'error' => [
'kind' => $e::class,
'line' => $e->getLine(),
'message' => $e->getMessage(),
'file' => after($e->getFile(), ROOT_PATH),
],
'entity' => $entity,
'exception' => [
'kind' => $e::class,
'line' => $e->getLine(),
'trace' => $e->getTrace(),
'message' => $e->getMessage(),
'file' => after($e->getFile(), ROOT_PATH),
],
...$logContext,
],
'entity' => $entity,
'exception' => [
'kind' => $e::class,
'line' => $e->getLine(),
'trace' => $e->getTrace(),
'message' => $e->getMessage(),
'file' => after($e->getFile(), ROOT_PATH),
],
...$logContext,
]
e: $e
)
);
}
}
} catch (Throwable $e) {
$this->logger->error(
message: "Exception '{error.kind}' was thrown unhandled during '{client}: {backend}' parsing of '{library.title} {segment.number}/{segment.of}' response. '{error.message}' at '{error.file}:{error.line}'.",
context: [
'backend' => $context->backendName,
'client' => $context->clientName,
'error' => [
'kind' => $e::class,
'line' => $e->getLine(),
'message' => $e->getMessage(),
'file' => after($e->getFile(), ROOT_PATH),
...lw(
message: "Exception '{error.kind}' was thrown unhandled during '{client}: {backend}' parsing of '{library.title} {segment.number}/{segment.of}' response. '{error.message}' at '{error.file}:{error.line}'.",
context: [
'backend' => $context->backendName,
'client' => $context->clientName,
'error' => [
'kind' => $e::class,
'line' => $e->getLine(),
'message' => $e->getMessage(),
'file' => after($e->getFile(), ROOT_PATH),
],
'exception' => [
'line' => $e->getLine(),
'kind' => get_class($e),
'message' => $e->getMessage(),
'trace' => $e->getTrace(),
'file' => after($e->getFile(), ROOT_PATH),
],
...$logContext,
],
'exception' => [
'line' => $e->getLine(),
'kind' => get_class($e),
'message' => $e->getMessage(),
'trace' => $e->getTrace(),
'file' => after($e->getFile(), ROOT_PATH),
],
...$logContext,
]
e: $e
)
);
}
@@ -1051,19 +1090,22 @@ class Import
];
} catch (InvalidArgumentException $e) {
$this->logger->error(
"Failed to parse '{client}: {backend}' item response. '{error.kind}' with '{error.message}' at '{error.file}:{error.line}' ",
[
'client' => $context->clientName,
'backend' => $context->backendName,
'error' => [
'kind' => $e::class,
'line' => $e->getLine(),
'message' => $e->getMessage(),
'file' => after($e->getFile(), ROOT_PATH),
...lw(
message: "Failed to parse '{client}: {backend}' item response. '{error.kind}' with '{error.message}' at '{error.file}:{error.line}' ",
context: [
'client' => $context->clientName,
'backend' => $context->backendName,
'error' => [
'kind' => $e::class,
'line' => $e->getLine(),
'message' => $e->getMessage(),
'file' => after($e->getFile(), ROOT_PATH),
],
...$logContext,
'body' => $item,
],
...$logContext,
'body' => $item,
]
e: $e
)
);
return;
}
@@ -1103,18 +1145,21 @@ class Import
);
} catch (Throwable $e) {
$this->logger->error(
message: "Exception '{error.kind}' occurred during '{client}: {backend}' '{library.title}' '{item.id}: {item.title}' entity creation. '{error.message}' at '{error.file}:{error.line}'.",
context: [
'backend' => $context->backendName,
'client' => $context->clientName,
'error' => [
'kind' => $e::class,
'line' => $e->getLine(),
'message' => $e->getMessage(),
'file' => after($e->getFile(), ROOT_PATH),
...lw(
message: "Exception '{error.kind}' occurred during '{client}: {backend}' '{library.title}' '{item.id}: {item.title}' entity creation. '{error.message}' at '{error.file}:{error.line}'.",
context: [
'backend' => $context->backendName,
'client' => $context->clientName,
'error' => [
'kind' => $e::class,
'line' => $e->getLine(),
'message' => $e->getMessage(),
'file' => after($e->getFile(), ROOT_PATH),
],
...$logContext,
],
...$logContext,
]
e: $e
)
);
return;
}
@@ -1151,26 +1196,29 @@ class Import
]);
} catch (Throwable $e) {
$this->logger->error(
message: "Exception '{error.kind}' was thrown unhandled during '{client}: {backend}' '{library.title}' '{item.title}' {action}. '{error.message}' at '{error.file}:{error.line}'.",
context: [
'action' => property_exists($this, 'action') ? $this->action : 'import',
'backend' => $context->backendName,
'client' => $context->clientName,
'error' => [
'kind' => $e::class,
'line' => $e->getLine(),
'message' => $e->getMessage(),
'file' => after($e->getFile(), ROOT_PATH),
...lw(
message: "Exception '{error.kind}' was thrown unhandled during '{client}: {backend}' '{library.title}' '{item.title}' {action}. '{error.message}' at '{error.file}:{error.line}'.",
context: [
'action' => property_exists($this, 'action') ? $this->action : 'import',
'backend' => $context->backendName,
'client' => $context->clientName,
'error' => [
'kind' => $e::class,
'line' => $e->getLine(),
'message' => $e->getMessage(),
'file' => after($e->getFile(), ROOT_PATH),
],
'exception' => [
'file' => $e->getFile(),
'line' => $e->getLine(),
'kind' => get_class($e),
'message' => $e->getMessage(),
'trace' => $e->getTrace(),
],
...$logContext,
],
'exception' => [
'file' => $e->getFile(),
'line' => $e->getLine(),
'kind' => get_class($e),
'message' => $e->getMessage(),
'trace' => $e->getTrace(),
],
...$logContext,
]
e: $e
)
);
}
}

View File

@@ -230,26 +230,29 @@ class Progress
}
} catch (\App\Libs\Exceptions\RuntimeException|RuntimeException|InvalidArgumentException $e) {
$this->logger->error(
message: "{action}: Exception '{error.kind}' was thrown unhandled during '{client}: {backend}' get {item.type} '{item.title}' status. '{error.message}' at '{error.file}:{error.line}'.",
context: [
'action' => $this->action,
'backend' => $context->backendName,
'client' => $context->clientName,
'error' => [
'kind' => $e::class,
'line' => $e->getLine(),
'message' => $e->getMessage(),
'file' => after($e->getFile(), ROOT_PATH),
...lw(
message: "{action}: Exception '{error.kind}' was thrown unhandled during '{client}: {backend}' get {item.type} '{item.title}' status. '{error.message}' at '{error.file}:{error.line}'.",
context: [
'action' => $this->action,
'backend' => $context->backendName,
'client' => $context->clientName,
'error' => [
'kind' => $e::class,
'line' => $e->getLine(),
'message' => $e->getMessage(),
'file' => after($e->getFile(), ROOT_PATH),
],
...$logContext,
'exception' => [
'file' => $e->getFile(),
'line' => $e->getLine(),
'kind' => get_class($e),
'message' => $e->getMessage(),
'trace' => $e->getTrace(),
],
],
...$logContext,
'exception' => [
'file' => $e->getFile(),
'line' => $e->getLine(),
'kind' => get_class($e),
'message' => $e->getMessage(),
'trace' => $e->getTrace(),
],
]
e: $e
)
);
continue;
}
@@ -305,26 +308,29 @@ class Progress
}
} catch (Throwable $e) {
$this->logger->error(
message: "{action}: Exception '{error.kind}' was thrown unhandled during '{client}: {backend}' change {item.type} '{item.title}' watch progress. '{error.message}' at '{error.file}:{error.line}'.",
context: [
'action' => $this->action,
'backend' => $context->backendName,
'client' => $context->clientName,
'error' => [
'kind' => $e::class,
'line' => $e->getLine(),
'message' => $e->getMessage(),
'file' => after($e->getFile(), ROOT_PATH),
...lw(
message: "{action}: Exception '{error.kind}' was thrown unhandled during '{client}: {backend}' change {item.type} '{item.title}' watch progress. '{error.message}' at '{error.file}:{error.line}'.",
context: [
'action' => $this->action,
'backend' => $context->backendName,
'client' => $context->clientName,
'error' => [
'kind' => $e::class,
'line' => $e->getLine(),
'message' => $e->getMessage(),
'file' => after($e->getFile(), ROOT_PATH),
],
...$logContext,
'exception' => [
'file' => $e->getFile(),
'line' => $e->getLine(),
'kind' => get_class($e),
'message' => $e->getMessage(),
'trace' => $e->getTrace(),
],
],
...$logContext,
'exception' => [
'file' => $e->getFile(),
'line' => $e->getLine(),
'kind' => get_class($e),
'message' => $e->getMessage(),
'trace' => $e->getTrace(),
],
]
e: $e
)
);
}
}

View File

@@ -115,25 +115,28 @@ final class Push
);
} catch (Throwable $e) {
$this->logger->error(
message: 'Exception [{error.kind}] was thrown unhandled during [{client}: {backend}] request for {item.type} [{item.title}] metadata. Error [{error.message} @ {error.file}:{error.line}].',
context: [
'backend' => $context->backendName,
'client' => $context->clientName,
'error' => [
'kind' => $e::class,
'line' => $e->getLine(),
'message' => $e->getMessage(),
'file' => after($e->getFile(), ROOT_PATH),
...lw(
message: 'Exception [{error.kind}] was thrown unhandled during [{client}: {backend}] request for {item.type} [{item.title}] metadata. Error [{error.message} @ {error.file}:{error.line}].',
context: [
'backend' => $context->backendName,
'client' => $context->clientName,
'error' => [
'kind' => $e::class,
'line' => $e->getLine(),
'message' => $e->getMessage(),
'file' => after($e->getFile(), ROOT_PATH),
],
...$logContext,
'exception' => [
'file' => after($e->getFile(), ROOT_PATH),
'line' => $e->getLine(),
'kind' => get_class($e),
'message' => $e->getMessage(),
'trace' => $e->getTrace(),
],
],
...$logContext,
'exception' => [
'file' => after($e->getFile(), ROOT_PATH),
'line' => $e->getLine(),
'kind' => get_class($e),
'message' => $e->getMessage(),
'trace' => $e->getTrace(),
],
]
e: $e
)
);
}
}
@@ -308,25 +311,28 @@ final class Push
}
} catch (Throwable $e) {
$this->logger->error(
message: 'Exception [{error.kind}] was thrown unhandled during [{client}: {backend}] push. Error [{error.message} @ {error.file}:{error.line}].',
context: [
'backend' => $context->backendName,
'client' => $context->clientName,
'error' => [
'kind' => $e::class,
'line' => $e->getLine(),
'message' => $e->getMessage(),
'file' => after($e->getFile(), ROOT_PATH),
...lw(
message: 'Exception [{error.kind}] was thrown unhandled during [{client}: {backend}] push. Error [{error.message} @ {error.file}:{error.line}].',
context: [
'backend' => $context->backendName,
'client' => $context->clientName,
'error' => [
'kind' => $e::class,
'line' => $e->getLine(),
'message' => $e->getMessage(),
'file' => after($e->getFile(), ROOT_PATH),
],
...$logContext,
'exception' => [
'file' => $e->getFile(),
'line' => $e->getLine(),
'kind' => get_class($e),
'message' => $e->getMessage(),
'trace' => $e->getTrace(),
],
],
...$logContext,
'exception' => [
'file' => $e->getFile(),
'line' => $e->getLine(),
'kind' => get_class($e),
'message' => $e->getMessage(),
'trace' => $e->getTrace(),
],
]
e: $e
)
);
}
}

View File

@@ -103,7 +103,7 @@ final class ReportCommand extends Command
$output->writeln(
r('Is the tasks runner working? <flag>{answer}</flag>', [
'answer' => (function () {
$info = isTaskWorkerRunning(true);
$info = isTaskWorkerRunning(ignoreContainer: true);
return r("{status} '{container}' - {message}", [
'status' => $info['status'] ? 'Yes' : 'No',
'message' => $info['message'],
@@ -246,7 +246,7 @@ final class ReportCommand extends Command
if (true === $includeSample) {
$sql = "SELECT * FROM state WHERE via = :name ORDER BY updated DESC LIMIT 3";
$stmt = $this->db->getPDO()->prepare($sql);
$stmt = $this->db->getDBLayer()->prepare($sql);
$stmt->execute([
'name' => $name,
]);

View File

@@ -161,7 +161,7 @@ final class ConfigFile implements ArrayAccess, LoggerAwareInterface
if (false === $override) {
clearstatcache(true, $this->file);
$newHash = $this->getFileHash();
if ($newHash !== $this->file_hash) {
if (false === hash_equals($this->file_hash, $newHash)) {
$this->logger?->warning(
"File '{file}' has been modified since last load. re-applying changes on top of the new data.",
[

View File

@@ -5,15 +5,19 @@ declare(strict_types=1);
namespace App\Libs\Database;
use App\Libs\Exceptions\DatabaseException as DBException;
use App\Libs\Exceptions\DBLayerException;
use Closure;
use PDO;
use PDOException;
use PDOStatement;
use Psr\Log\LoggerAwareInterface;
use Psr\Log\LoggerAwareTrait;
use RuntimeException;
final class DBLayer
final class DBLayer implements LoggerAwareInterface
{
use LoggerAwareTrait;
private const int LOCK_RETRY = 4;
private int $count = 0;
@@ -61,55 +65,71 @@ final class DBLayer
public function exec(string $sql, array $options = []): int|false
{
try {
$queryString = $sql;
return $this->wrap(function (DBLayer $db) use ($sql, $options) {
$queryString = $sql;
$this->last = [
'sql' => $queryString,
'bind' => [],
];
$this->last = [
'sql' => $queryString,
'bind' => [],
];
$stmt = $this->pdo->exec($queryString);
return $db->pdo->exec($queryString);
});
} catch (PDOException $e) {
throw (new DBException($e->getMessage()))
->setInfo($queryString, [], $e->errorInfo ?? [], $e->getCode())
->setFile($e->getTrace()[$options['tracer'] ?? 1]['file'] ?? $e->getFile())
->setLine($e->getTrace()[$options['tracer'] ?? 1]['line'] ?? $e->getLine())
->setOptions([]);
}
return $stmt;
}
public function query(string $queryString, array $bind = [], array $options = []): PDOStatement
{
try {
$this->last = [
'sql' => $queryString,
'bind' => $bind,
];
$stmt = $this->pdo->prepare($queryString);
if (!($stmt instanceof PDOStatement)) {
throw new PDOException('Unable to prepare statement.');
if ($e instanceof DBLayerException) {
throw $e;
}
$stmt->execute($bind);
if (false !== stripos($queryString, 'SQL_CALC_FOUND_ROWS')) {
if (false !== ($countStatement = $this->pdo->query('SELECT FOUND_ROWS();'))) {
$this->count = (int)$countStatement->fetch(PDO::FETCH_COLUMN);
}
}
} catch (PDOException $e) {
throw (new DBException($e->getMessage()))
->setInfo($queryString, $bind, $e->errorInfo ?? [], $e->getCode())
throw (new DBLayerException($e->getMessage()))
->setInfo($sql, [], $e->errorInfo ?? [], $e->getCode())
->setFile($e->getTrace()[$options['tracer'] ?? 1]['file'] ?? $e->getFile())
->setLine($e->getTrace()[$options['tracer'] ?? 1]['line'] ?? $e->getLine())
->setOptions($options);
}
}
return $stmt;
public function query(string|PDOStatement $sql, array $bind = [], array $options = []): PDOStatement
{
try {
return $this->wrap(function (DBLayer $db) use ($sql, $bind, $options) {
$isStatement = $sql instanceof PDOStatement;
$queryString = $isStatement ? $sql->queryString : $sql;
$this->last = [
'sql' => $queryString,
'bind' => $bind,
];
$stmt = $isStatement ? $sql : $db->prepare($sql);
if (false === ($stmt instanceof PDOStatement)) {
throw new PDOException('Unable to prepare statement.');
}
$stmt->execute($bind);
if (false !== stripos($queryString, 'SQL_CALC_FOUND_ROWS')) {
if (false !== ($countStatement = $this->pdo->query('SELECT FOUND_ROWS();'))) {
$this->count = (int)$countStatement->fetch(PDO::FETCH_COLUMN);
}
}
return $stmt;
});
} catch (PDOException $e) {
if ($e instanceof DBLayerException) {
throw $e;
}
throw (new DBLayerException($e->getMessage(), (int)$e->getCode(), $e))
->setInfo(
(true === ($sql instanceof PDOStatement)) ? $sql->queryString : $sql,
$bind,
$e->errorInfo ?? [],
$e->getCode()
)
->setFile($e->getTrace()[$options['tracer'] ?? 1]['file'] ?? $e->getFile())
->setLine($e->getTrace()[$options['tracer'] ?? 1]['line'] ?? $e->getLine())
->setOptions($options);
}
}
public function start(): bool
@@ -136,6 +156,25 @@ final class DBLayer
return $this->pdo->inTransaction();
}
/**
* @return bool
* @deprecated Use {@link self::start()} instead.
*/
public function beginTransaction(): bool
{
return $this->start();
}
public function prepare(string $sql, array $options = []): PDOStatement|false
{
return $this->pdo->prepare($sql, $options);
}
public function lastInsertId(): string|false
{
return $this->pdo->lastInsertId();
}
public function delete(string $table, array $conditions, array $options = []): PDOStatement
{
if (empty($conditions)) {
@@ -422,6 +461,11 @@ final class DBLayer
return $this->driver;
}
public function getBackend(): PDO
{
return $this->pdo;
}
private function conditionParser(array $conditions): array
{
$keys = $bind = [];
@@ -699,21 +743,22 @@ final class DBLayer
for ($i = 1; $i <= self::LOCK_RETRY; $i++) {
try {
if (!$autoStartTransaction) {
if (true === $autoStartTransaction) {
$this->start();
}
$result = $callback($this);
if (!$autoStartTransaction) {
if (true === $autoStartTransaction) {
$this->commit();
}
$this->last = $this->getLastStatement();
return $result;
} catch (DBException $e) {
if (!$autoStartTransaction && $this->inTransaction()) {
} catch (DBLayerException $e) {
/** @noinspection PhpConditionAlreadyCheckedInspection */
if ($autoStartTransaction && $this->inTransaction()) {
$this->rollBack();
}
@@ -737,4 +782,37 @@ final class DBLayer
*/
return null;
}
private function wrap(Closure $callback): mixed
{
for ($i = 0; $i <= self::LOCK_RETRY; $i++) {
try {
return $callback($this);
} catch (PDOException $e) {
if (true === str_contains(strtolower($e->getMessage()), 'database is locked')) {
if ($i >= self::LOCK_RETRY) {
throw (new DBLayerException($e->getMessage(), (int)$e->getCode(), $e))
->setInfo($stnt->queryString, $bind, $e->errorInfo ?? [], $e->getCode())
->setFile($e->getFile())
->setLine($e->getLine());
}
$sleep = self::LOCK_RETRY + random_int(1, 3);
$this->logger?->warning("PDOAdapter: Database is locked. sleeping for '{sleep}s'.", [
'sleep' => $sleep
]);
sleep($sleep);
} else {
throw (new DBLayerException($e->getMessage(), (int)$e->getCode(), $e))
->setInfo($stnt->queryString, $bind, $e->errorInfo ?? [], $e->getCode())
->setFile($e->getFile())
->setLine($e->getLine());
}
}
}
return false;
}
}

View File

@@ -7,10 +7,8 @@ namespace App\Libs\Database;
use App\Libs\Entity\StateInterface;
use Closure;
use DateTimeInterface;
use PDO;
use PDOException;
use Psr\Log\LoggerInterface;
use RuntimeException;
interface DatabaseInterface
{
@@ -55,15 +53,6 @@ interface DatabaseInterface
*/
public function getAll(DateTimeInterface|null $date = null, array $opts = []): array;
/**
* Return number of items.
*
* @param DateTimeInterface|null $date if provided, it will return items changes since this date.
*
* @return int Number of items.
*/
public function getCount(DateTimeInterface|null $date = null): int;
/**
* Return database records for given items.
*
@@ -184,13 +173,11 @@ interface DatabaseInterface
public function setLogger(LoggerInterface $logger): self;
/**
* Get PDO instance.
* Get DBLayer instance.
*
* @return PDO
*
* @throws RuntimeException if PDO is not initialized yet.
* @return DBLayer
*/
public function getPDO(): PDO;
public function getDBLayer(): DBLayer;
/**
* Enable single transaction mode.

View File

@@ -6,8 +6,10 @@ namespace App\Libs\Database\PDO;
use App\Libs\Container;
use App\Libs\Database\DatabaseInterface as iDB;
use App\Libs\Database\DBLayer;
use App\Libs\Entity\StateInterface as iState;
use App\Libs\Exceptions\DatabaseException as DBException;
use App\Libs\Exceptions\DBAdapterException as DBException;
use App\Libs\Exceptions\DBLayerException;
use App\Libs\Options;
use Closure;
use DateTimeInterface;
@@ -34,14 +36,17 @@ final class PDOAdapter implements iDB
* @var bool Whether the current operation is in a transaction.
*/
private bool $viaTransaction = false;
/**
* @var bool Whether the current operation is using a single transaction.
*/
private bool $singleTransaction = false;
/**
* @var array Adapter options.
*/
private array $options = [];
/**
* @var array<array-key, PDOStatement> Prepared statements.
*/
@@ -49,6 +54,7 @@ final class PDOAdapter implements iDB
'insert' => null,
'update' => null,
];
/**
* @var string The database driver to be used.
*/
@@ -58,15 +64,11 @@ final class PDOAdapter implements iDB
* Creates a new instance of the class.
*
* @param LoggerInterface $logger The logger object used for logging.
* @param PDO $pdo The PDO object used for database connections.
* @param DBLayer $db The PDO object used for database connections.
*/
public function __construct(private LoggerInterface $logger, private PDO $pdo)
public function __construct(private LoggerInterface $logger, private readonly DBLayer $db)
{
$driver = $this->pdo->getAttribute(PDO::ATTR_DRIVER_NAME);
if (is_string($driver)) {
$this->driver = $driver;
}
$this->driver = $this->db->getDriver();
}
/**
@@ -151,14 +153,14 @@ final class PDOAdapter implements iDB
}
if (null === ($this->stmt['insert'] ?? null)) {
$this->stmt['insert'] = $this->pdo->prepare(
$this->stmt['insert'] = $this->db->prepare(
$this->pdoInsert('state', iState::ENTITY_KEYS)
);
}
$this->execute($this->stmt['insert'], $data);
$entity->id = (int)$this->pdo->lastInsertId();
$entity->id = (int)$this->db->lastInsertId();
} catch (PDOException $e) {
$this->stmt['insert'] = null;
if (false === $this->viaTransaction && false === $this->singleTransaction) {
@@ -282,21 +284,6 @@ final class PDOAdapter implements iDB
return $arr;
}
/**
* @inheritdoc
* @throws RandomException if an error occurs while generating a random number.
*/
public function getCount(DateTimeInterface|null $date = null): int
{
$sql = 'SELECT COUNT(id) AS total FROM state';
if (null !== $date) {
$sql .= ' WHERE ' . iState::COLUMN_UPDATED . ' > ' . $date->getTimestamp();
}
return (int)$this->query($sql)->fetchColumn();
}
/**
* @inheritdoc
* @throws RandomException if an error occurs while generating a random number.
@@ -334,7 +321,7 @@ final class PDOAdapter implements iDB
}
$sql = "SELECT * FROM state WHERE {$type_sql} JSON_EXTRACT(" . iState::COLUMN_META_DATA . ",'$.{$key}') = :id LIMIT 1";
$stmt = $this->pdo->prepare($sql);
$stmt = $this->db->prepare($sql);
if (false === $this->execute($stmt, $cond)) {
throw new DBException(
@@ -404,7 +391,7 @@ final class PDOAdapter implements iDB
}
if (null === ($this->stmt['update'] ?? null)) {
$this->stmt['update'] = $this->pdo->prepare(
$this->stmt['update'] = $this->db->prepare(
$this->pdoUpdate('state', iState::ENTITY_KEYS)
);
}
@@ -554,16 +541,14 @@ final class PDOAdapter implements iDB
*/
public function migrations(string $dir, array $opts = []): mixed
{
$class = new PDOMigrations($this->pdo, $this->logger);
$class = new PDOMigrations($this->db, $this->logger);
return match (strtolower($dir)) {
iDB::MIGRATE_UP => $class->up(),
iDB::MIGRATE_DOWN => $class->down(),
default => throw new DBException(
r("PDOAdapter: Unknown migration direction '{dir}' was given.", [
'name' => $dir
]), 91
),
default => throw new DBException(r("PDOAdapter: Unknown migration direction '{dir}' was given.", [
'name' => $dir
]), 91),
};
}
@@ -572,7 +557,7 @@ final class PDOAdapter implements iDB
*/
public function ensureIndex(array $opts = []): mixed
{
return (new PDOIndexer($this->pdo, $this->logger))->ensureIndex($opts);
return (new PDOIndexer($this->db, $this->logger))->ensureIndex($opts);
}
/**
@@ -580,7 +565,7 @@ final class PDOAdapter implements iDB
*/
public function migrateData(string $version, LoggerInterface|null $logger = null): mixed
{
return (new PDODataMigration($this->pdo, $logger ?? $this->logger))->automatic();
return (new PDODataMigration($this->db, $logger ?? $this->logger))->automatic();
}
/**
@@ -588,7 +573,7 @@ final class PDOAdapter implements iDB
*/
public function isMigrated(): bool
{
return (new PDOMigrations($this->pdo, $this->logger))->isMigrated();
return (new PDOMigrations($this->db, $this->logger))->isMigrated();
}
/**
@@ -596,7 +581,7 @@ final class PDOAdapter implements iDB
*/
public function makeMigration(string $name, array $opts = []): mixed
{
return (new PDOMigrations($this->pdo, $this->logger))->make($name);
return (new PDOMigrations($this->db, $this->logger))->make($name);
}
/**
@@ -604,7 +589,7 @@ final class PDOAdapter implements iDB
*/
public function maintenance(array $opts = []): mixed
{
return (new PDOMigrations($this->pdo, $this->logger))->runMaintenance();
return (new PDOMigrations($this->db, $this->logger))->runMaintenance();
}
/**
@@ -613,19 +598,19 @@ final class PDOAdapter implements iDB
*/
public function reset(): bool
{
$this->pdo->beginTransaction();
$this->db->transactional(function (DBLayer $db) {
/** @noinspection SqlResolve */
$tables = $db->query(
'SELECT name FROM sqlite_master WHERE "type" = "table" AND "name" NOT LIKE "sqlite_%"'
);
$tables = $this->pdo->query(
'SELECT name FROM sqlite_master WHERE "type" = "table" AND "name" NOT LIKE "sqlite_%"'
);
foreach ($tables->fetchAll(PDO::FETCH_COLUMN) as $table) {
$db->exec('DELETE FROM "' . $table . '"');
$db->exec('DELETE FROM sqlite_sequence WHERE "name" = "' . $table . '"');
}
});
foreach ($tables->fetchAll(PDO::FETCH_COLUMN) as $table) {
$this->pdo->exec('DELETE FROM "' . $table . '"');
$this->pdo->exec('DELETE FROM sqlite_sequence WHERE "name" = "' . $table . '"');
}
$this->pdo->commit();
$this->pdo->exec('VACUUM');
$this->db->exec('VACUUM');
return true;
}
@@ -640,12 +625,9 @@ final class PDOAdapter implements iDB
return $this;
}
/**
* @inheritdoc
*/
public function getPDO(): PDO
public function getDBLayer(): DBLayer
{
return $this->pdo;
return $this->db;
}
/**
@@ -655,11 +637,11 @@ final class PDOAdapter implements iDB
{
$this->singleTransaction = true;
if (false === $this->pdo->inTransaction()) {
$this->pdo->beginTransaction();
if (false === $this->db->inTransaction()) {
$this->db->start();
}
return $this->pdo->inTransaction();
return $this->db->inTransaction();
}
/**
@@ -667,7 +649,7 @@ final class PDOAdapter implements iDB
*/
public function transactional(Closure $callback): mixed
{
if (true === $this->pdo->inTransaction()) {
if (true === $this->db->inTransaction()) {
$this->viaTransaction = true;
$result = $callback($this);
$this->viaTransaction = false;
@@ -675,19 +657,20 @@ final class PDOAdapter implements iDB
}
try {
$this->pdo->beginTransaction();
$this->db->start();
$this->viaTransaction = true;
$result = $callback($this);
$this->viaTransaction = false;
$this->pdo->commit();
$this->db->commit();
return $result;
} catch (PDOException $e) {
$this->pdo->rollBack();
$this->viaTransaction = false;
$this->db->rollBack();
throw $e;
} finally {
$this->viaTransaction = false;
}
}
@@ -701,8 +684,8 @@ final class PDOAdapter implements iDB
*/
public function __destruct()
{
if (true === $this->singleTransaction && true === $this->pdo->inTransaction()) {
$this->pdo->commit();
if (true === $this->singleTransaction && true === $this->db->inTransaction()) {
$this->db->commit();
}
$this->stmt = [];
@@ -827,7 +810,7 @@ final class PDOAdapter implements iDB
$sql = "SELECT * FROM state WHERE " . iState::COLUMN_TYPE . " = :type {$sqlEpisode} {$sqlGuids} LIMIT 1";
$stmt = $this->pdo->prepare($sql);
$stmt = $this->db->prepare($sql);
if (false === $this->execute($stmt, $cond)) {
throw new DBException(
@@ -857,29 +840,7 @@ final class PDOAdapter implements iDB
*/
private function execute(PDOStatement $stmt, array $cond = []): bool
{
for ($i = 0; $i <= self::LOCK_RETRY; $i++) {
try {
return $stmt->execute($cond);
} catch (PDOException $e) {
if (true === str_contains(strtolower($e->getMessage()), 'database is locked')) {
if ($i >= self::LOCK_RETRY) {
throw $e;
}
$sleep = self::LOCK_RETRY + random_int(1, 3);
$this->logger->warning("PDOAdapter: Database is locked. sleeping for '{sleep}s'.", [
'sleep' => $sleep
]);
sleep($sleep);
} else {
throw $e;
}
}
}
return false;
return $this->wrap(fn(PDOAdapter $adapter) => $stmt->execute($cond));
}
/**
@@ -895,29 +856,7 @@ final class PDOAdapter implements iDB
*/
private function query(string $sql): PDOStatement|false
{
for ($i = 0; $i <= self::LOCK_RETRY; $i++) {
try {
return $this->pdo->query($sql);
} catch (PDOException $e) {
if (true === str_contains(strtolower($e->getMessage()), 'database is locked')) {
if ($i >= self::LOCK_RETRY) {
throw $e;
}
$sleep = self::LOCK_RETRY + random_int(1, 3);
$this->logger->warning("PDOAdapter: Database is locked. sleeping for '{sleep}s'.", [
'sleep' => $sleep,
]);
sleep($sleep);
} else {
throw $e;
}
}
}
return false;
return $this->wrap(fn(PDOAdapter $adapter) => $adapter->db->query($sql));
}
/**
@@ -981,4 +920,57 @@ final class PDOAdapter implements iDB
default => '"' . $text . '"',
};
}
/**
* Wraps the given callback function with a retry mechanism to handle database locks.
*
* @param Closure $callback The callback function to be executed.
*
* @return mixed The result of the callback function.
*
* @throws DBLayerException If an error occurs while executing the callback function.
* @throws RandomException If an error occurs while generating a random number.
*/
private function wrap(Closure $callback): mixed
{
for ($i = 0; $i <= self::LOCK_RETRY; $i++) {
try {
return $callback($this);
} catch (PDOException $e) {
if (true === str_contains(strtolower($e->getMessage()), 'database is locked')) {
if ($i >= self::LOCK_RETRY) {
throw (new DBLayerException($e->getMessage(), (int)$e->getCode(), $e))
->setInfo(
ag($this->db->getLastStatement(), 'sql', ''),
ag($this->db->getLastStatement(), 'bind', []),
$e->errorInfo ?? [],
$e->getCode()
)
->setFile($e->getFile())
->setLine($e->getLine());
}
$sleep = self::LOCK_RETRY + random_int(1, 3);
$this->logger->warning("PDOAdapter: Database is locked. sleeping for '{sleep}s'.", [
'sleep' => $sleep
]);
sleep($sleep);
} else {
throw (new DBLayerException($e->getMessage(), (int)$e->getCode(), $e))
->setInfo(
ag($this->db->getLastStatement(), 'sql', ''),
ag($this->db->getLastStatement(), 'bind', []),
$e->errorInfo ?? [],
$e->getCode()
)
->setFile($e->getFile())
->setLine($e->getLine());
}
}
}
return false;
}
}

View File

@@ -5,6 +5,7 @@ declare(strict_types=1);
namespace App\Libs\Database\PDO;
use App\Libs\Config;
use App\Libs\Database\DBLayer;
use App\Libs\Entity\StateInterface as iFace;
use App\Libs\Guid;
use PDO;
@@ -41,10 +42,10 @@ final class PDODataMigration
/**
* Class constructor.
*
* @param PDO $pdo The PDO instance to use for database connection.
* @param DBLayer $db The PDO instance to use for database connection.
* @param LoggerInterface $logger The logger instance to use for logging.
*/
public function __construct(private PDO $pdo, private LoggerInterface $logger)
public function __construct(private DBLayer $db, private LoggerInterface $logger)
{
$this->version = Config::get('database.version');
$this->dbPath = dirname(after(Config::get('database.dsn'), 'sqlite:'));
@@ -125,15 +126,15 @@ final class PDODataMigration
]
);
if (!$this->pdo->inTransaction()) {
$this->pdo->beginTransaction();
if (!$this->db->inTransaction()) {
$this->db->start();
}
$columns = implode(', ', iFace::ENTITY_KEYS);
$binds = ':' . implode(', :', iFace::ENTITY_KEYS);
/** @noinspection SqlInsertValues */
$insert = $this->pdo->prepare("INSERT INTO state ({$columns}) VALUES({$binds})");
$insert = $this->db->prepare("INSERT INTO state ({$columns}) VALUES({$binds})");
$stmt = $oldDB->query("SELECT * FROM state");
@@ -230,8 +231,8 @@ final class PDODataMigration
]);
}
if ($this->pdo->inTransaction()) {
$this->pdo->commit();
if ($this->db->inTransaction()) {
$this->db->commit();
}
$stmt = null;
@@ -290,15 +291,15 @@ final class PDODataMigration
PDO::SQLITE_ATTR_OPEN_FLAGS => PDO::SQLITE_OPEN_READONLY,
]);
if (!$this->pdo->inTransaction()) {
$this->pdo->beginTransaction();
if (!$this->db->inTransaction()) {
$this->db->start();
}
$columns = implode(', ', iFace::ENTITY_KEYS);
$binds = ':' . implode(', :', iFace::ENTITY_KEYS);
/** @noinspection SqlInsertValues */
$insert = $this->pdo->prepare("INSERT INTO state ({$columns}) VALUES({$binds})");
$insert = $this->db->prepare("INSERT INTO state ({$columns}) VALUES({$binds})");
foreach ($oldDB->query("SELECT * FROM state") as $row) {
$row[iFace::COLUMN_EXTRA] = json_decode(
@@ -419,8 +420,8 @@ final class PDODataMigration
$insert->execute($arr);
}
if ($this->pdo->inTransaction()) {
$this->pdo->commit();
if ($this->db->inTransaction()) {
$this->db->commit();
}
$oldDB = null;

View File

@@ -5,10 +5,10 @@ declare(strict_types=1);
namespace App\Libs\Database\PDO;
use App\Libs\Config;
use App\Libs\Database\DBLayer;
use App\Libs\Entity\StateInterface as iState;
use App\Libs\Guid;
use App\Libs\Options;
use PDO;
use Psr\Log\LoggerInterface as iLogger;
/**
@@ -40,10 +40,10 @@ final class PDOIndexer
/**
* Class constructor.
*
* @param PDO $db The PDO object used for database connections and queries.
* @param DBLayer $db The PDO object used for database connections and queries.
* @param iLogger $logger The logger object used for logging information.
*/
public function __construct(private PDO $db, private iLogger $logger)
public function __construct(private DBLayer $db, private iLogger $logger)
{
}
@@ -169,7 +169,7 @@ final class PDOIndexer
if (!$this->db->inTransaction()) {
$startedTransaction = true;
$this->db->beginTransaction();
$this->db->start();
}
foreach ($queries as $query) {

View File

@@ -5,8 +5,8 @@ declare(strict_types=1);
namespace App\Libs\Database\PDO;
use App\Libs\Database\DatabaseInterface as iDB;
use App\Libs\Database\DBLayer;
use App\Libs\Stream;
use PDO;
use Psr\Log\LoggerInterface;
use RuntimeException;
use SplFileObject;
@@ -36,12 +36,12 @@ final class PDOMigrations
/**
* Constructs a new instance of the class.
*
* @param PDO $pdo The database connection object.
* @param DBLayer $db The database connection object.
* @param LoggerInterface $logger The logger instance.
*
* @return void
*/
public function __construct(private PDO $pdo, private LoggerInterface $logger)
public function __construct(private DBLayer $db, private LoggerInterface $logger)
{
$this->path = __DIR__ . '/../../../../migrations';
$this->driver = $this->getDriver();
@@ -119,7 +119,7 @@ final class PDOMigrations
'name' => ag($migrate, 'name')
]));
$this->pdo->exec((string)ag($migrate, iDB::MIGRATE_UP));
$this->db->exec((string)ag($migrate, iDB::MIGRATE_UP));
$this->setVersion(ag($migrate, 'id'));
}
@@ -200,7 +200,7 @@ final class PDOMigrations
public function runMaintenance(): int|bool
{
if ('sqlite' === $this->driver) {
return $this->pdo->exec('VACUUM;');
return $this->db->exec('VACUUM;');
}
return false;
@@ -213,7 +213,7 @@ final class PDOMigrations
*/
private function getVersion(): int
{
return (int)$this->pdo->query('PRAGMA user_version')->fetchColumn();
return (int)$this->db->query('PRAGMA user_version')->fetchColumn();
}
/**
@@ -225,7 +225,7 @@ final class PDOMigrations
*/
private function setVersion(int $version): void
{
$this->pdo->exec('PRAGMA user_version = ' . $version);
$this->db->exec('PRAGMA user_version = ' . $version);
}
/**
@@ -235,9 +235,9 @@ final class PDOMigrations
*/
private function getDriver(): string
{
$driver = $this->pdo->getAttribute($this->pdo::ATTR_DRIVER_NAME);
$driver = $this->db->getDriver();
if (empty($driver) || !is_string($driver)) {
if (empty($driver)) {
$driver = 'unknown';
}

View File

@@ -0,0 +1,41 @@
<?php
namespace App\Libs\Exceptions;
interface AppExceptionInterface
{
/**
* Add extra context to the exception.
*
* @param string $key The context key
* @param mixed $val The context value
*
* @return AppExceptionInterface
*/
public function addContext(string $key, mixed $val): AppExceptionInterface;
/**
* Replace the context with the provided array.
*
* @param array $context The context array.
*
* @return AppExceptionInterface
*/
public function setContext(array $context): AppExceptionInterface;
/**
* Get the context array or a specific key from the context.
*
* @param string|null $key The context key.
*
* @return mixed
*/
public function getContext(string|null $key = null): mixed;
/**
* Does the exception contain context?
*
* @return bool
*/
public function hasContext(): bool;
}

View File

@@ -4,11 +4,14 @@ declare(strict_types=1);
namespace App\Libs\Exceptions\Backends;
use App\Libs\Exceptions\AppExceptionInterface;
use App\Libs\Exceptions\UseAppException;
use ErrorException;
/**
* Base Exception class for the backends errors.
*/
class BackendException extends ErrorException
class BackendException extends ErrorException implements AppExceptionInterface
{
use UseAppException;
}

View File

@@ -12,8 +12,10 @@ use RuntimeException;
* The DatabaseException class extends the RuntimeException class and represents an exception
* that is thrown when there is an error related to the database operation.
*/
class DatabaseException extends RuntimeException
class DBAdapterException extends RuntimeException implements AppExceptionInterface
{
use UseAppException;
public string $queryString = '';
public array $bind = [];
@@ -52,21 +54,21 @@ class DatabaseException extends RuntimeException
return $this->bind;
}
public function setFile(string $file): DatabaseException
public function setFile(string $file): DBAdapterException
{
$this->file = $file;
return $this;
}
public function setLine(int $line): DatabaseException
public function setLine(int $line): DBAdapterException
{
$this->line = $line;
return $this;
}
public function setOptions(array $options): DatabaseException
public function setOptions(array $options): DBAdapterException
{
$this->options = $options;

View File

@@ -0,0 +1,77 @@
<?php
declare(strict_types=1);
namespace App\Libs\Exceptions;
use PDOException;
/**
* Class DatabaseException
*
* The DatabaseException class extends the RuntimeException class and represents an exception
* that is thrown when there is an error related to the database operation.
*/
class DBLayerException extends PDOException implements AppExceptionInterface
{
use UseAppException;
public string $queryString = '';
public array $bind = [];
public array $options = [];
public array|null $errorInfo = [];
/**
* @param string $queryString
* @param array $bind
* @param array $errorInfo
* @param string|int $errorCode
*
* @return $this
*/
public function setInfo(
string $queryString,
array $bind = [],
array $errorInfo = [],
mixed $errorCode = 0
): self {
$this->queryString = $queryString;
$this->bind = $bind;
$this->errorInfo = $errorInfo;
$this->code = $errorCode;
return $this;
}
public function getQueryString(): string
{
return $this->queryString;
}
public function getQueryBind(): array
{
return $this->bind;
}
public function setFile(string $file): DBLayerException
{
$this->file = $file;
return $this;
}
public function setLine(int $line): DBLayerException
{
$this->line = $line;
return $this;
}
public function setOptions(array $options): DBLayerException
{
$this->options = $options;
return $this;
}
}

View File

@@ -6,8 +6,10 @@ namespace App\Libs\Exceptions;
use RuntimeException;
class EmitterException extends RuntimeException
class EmitterException extends RuntimeException implements AppExceptionInterface
{
use UseAppException;
public static function forHeadersSent(string $filename, int $line): self
{
return new self(r('Unable to emit response. Headers already sent in %s:%d', [

View File

@@ -7,6 +7,7 @@ namespace App\Libs\Exceptions;
/**
* Class ErrorException
*/
class ErrorException extends \ErrorException
class ErrorException extends \ErrorException implements AppExceptionInterface
{
use UseAppException;
}

View File

@@ -4,6 +4,7 @@ declare(strict_types=1);
namespace App\Libs\Exceptions;
class HttpException extends RuntimeException
class HttpException extends RuntimeException implements AppExceptionInterface
{
use UseAppException;
}

View File

@@ -7,6 +7,7 @@ namespace App\Libs\Exceptions;
/**
* Class InvalidArgumentException
*/
class InvalidArgumentException extends \InvalidArgumentException
class InvalidArgumentException extends \InvalidArgumentException implements AppExceptionInterface
{
use UseAppException;
}

View File

@@ -7,6 +7,7 @@ namespace App\Libs\Exceptions;
/**
* General runtime exception.
*/
class RuntimeException extends \RuntimeException
class RuntimeException extends \RuntimeException implements AppExceptionInterface
{
use UseAppException;
}

View File

@@ -0,0 +1,35 @@
<?php
namespace App\Libs\Exceptions;
trait UseAppException
{
protected array $appContext = [];
public function addContext(string $key, mixed $value): AppExceptionInterface
{
$this->appContext = ag_set($this->appContext, $key, $value);
return $this;
}
public function setContext(array $context): AppExceptionInterface
{
$this->appContext = $context;
return $this;
}
public function getContext(string|null $key = null): mixed
{
if (null === $key) {
return $this->appContext;
}
return ag($this->appContext, $key);
}
public function hasContext(): bool
{
return !empty($this->appContext);
}
}

View File

@@ -7,6 +7,7 @@ namespace App\Libs\Exceptions;
/**
* Class ValidationException
*/
class ValidationException extends RuntimeException
class ValidationException extends RuntimeException implements AppExceptionInterface
{
use UseAppException;
}

View File

@@ -6,7 +6,7 @@ namespace App\Libs;
use App\Libs\Exceptions\InvalidArgumentException;
use JsonSerializable;
use Psr\Log\LoggerInterface;
use Psr\Log\LoggerInterface as iLogger;
use Stringable;
/**
@@ -91,9 +91,9 @@ final class Guid implements JsonSerializable, Stringable
*/
private array $data = [];
/**
* @var null|LoggerInterface $logger The logger instance used for logging.
* @var null|iLogger $logger The logger instance used for logging.
*/
private static LoggerInterface|null $logger = null;
private static iLogger|null $logger = null;
/**
* Create list of db => external id list.
@@ -114,7 +114,7 @@ final class Guid implements JsonSerializable, Stringable
if (false === is_string($key)) {
$this->getLogger()->info(
'Ignoring [{backend}] {item.type} [{item.title}] external id. Unexpected key type [{given}] was given.',
"Ignoring '{backend}' {item.type} '{item.title}' external id. Unexpected key type '{given}' was given.",
[
'key' => (string)$key,
'given' => get_debug_type($key),
@@ -126,7 +126,7 @@ final class Guid implements JsonSerializable, Stringable
if (null === (self::SUPPORTED[$key] ?? null)) {
$this->getLogger()->info(
'Ignoring [{backend}] {item.type} [{item.title}] [{key}] external id. Not supported.',
"Ignoring '{backend}' {item.type} '{item.title}' '{key}' external id. Not supported.",
[
'key' => $key,
...$context,
@@ -137,7 +137,7 @@ final class Guid implements JsonSerializable, Stringable
if (self::SUPPORTED[$key] !== ($valueType = get_debug_type($value))) {
$this->getLogger()->info(
'Ignoring [{backend}] {item.type} [{item.title}] [{key}] external id. Unexpected value type.',
"Ignoring '{backend}' {item.type} '{item.title}' '{key}' external id. Unexpected value type.",
[
'key' => $key,
'condition' => [
@@ -153,7 +153,7 @@ final class Guid implements JsonSerializable, Stringable
if (null !== (self::VALIDATE_GUID[$key] ?? null)) {
if (1 !== preg_match(self::VALIDATE_GUID[$key]['pattern'], $value)) {
$this->getLogger()->info(
'Ignoring [{backend}] {item.type} [{item.title}] [{key}] external id. Unexpected [{given}] value, expecting [{expected}].',
"Ignoring '{backend}' {item.type} '{item.title}' '{key}' external id. Unexpected value '{given}'. Expecting '{expected}'.",
[
'key' => $key,
'expected' => self::VALIDATE_GUID[$key]['example'],
@@ -172,9 +172,9 @@ final class Guid implements JsonSerializable, Stringable
/**
* Set the logger instance for the class.
*
* @param LoggerInterface $logger The logger instance to be set.
* @param iLogger $logger The logger instance to be set.
*/
public static function setLogger(LoggerInterface $logger): void
public static function setLogger(iLogger $logger): void
{
self::$logger = $logger;
}
@@ -229,12 +229,10 @@ final class Guid implements JsonSerializable, Stringable
$lookup = 'guid_' . $db;
if (false === array_key_exists($lookup, self::SUPPORTED)) {
throw new InvalidArgumentException(
r('Invalid db [{db}] source was given. Expecting [{db_list}].', [
'db' => $db,
'db_list' => implode(', ', array_map(fn($f) => after($f, 'guid_'), array_keys(self::SUPPORTED))),
])
);
throw new InvalidArgumentException(r("Invalid db '{db}' source was given. Expecting '{db_list}'.", [
'db' => $db,
'db_list' => implode(', ', array_map(fn($f) => after($f, 'guid_'), array_keys(self::SUPPORTED))),
]));
}
if (null === (self::VALIDATE_GUID[$lookup] ?? null)) {
@@ -242,13 +240,11 @@ final class Guid implements JsonSerializable, Stringable
}
if (1 !== @preg_match(self::VALIDATE_GUID[$lookup]['pattern'], $id)) {
throw new InvalidArgumentException(
r('Invalid [{value}] value for [{db}]. Expecting [{example}].', [
'db' => $db,
'value' => $id,
'example' => self::VALIDATE_GUID[$lookup]['example'],
])
);
throw new InvalidArgumentException(r("Invalid value '{value}' for '{db}' GUID. Expecting '{example}'.", [
'db' => $db,
'value' => $id,
'example' => self::VALIDATE_GUID[$lookup]['example'],
]));
}
return true;
@@ -283,12 +279,12 @@ final class Guid implements JsonSerializable, Stringable
/**
* Get instance of logger.
*
* @return LoggerInterface
* @return iLogger
*/
private function getLogger(): LoggerInterface
private function getLogger(): iLogger
{
if (null === self::$logger) {
self::$logger = Container::get(LoggerInterface::class);
self::$logger = Container::get(iLogger::class);
}
return self::$logger;

View File

@@ -120,7 +120,7 @@ final class DirectMapper implements iImport
$this->addPointers($entity, $pointer);
}
$this->logger->info("MAPPER: Preloaded '{pointers}' pointers into memory.", [
$this->logger->info("DirectMapper: Preloaded '{pointers}' pointers into memory.", [
'mapper' => afterLast(self::class, '\\'),
'pointers' => number_format(count($this->pointers)),
]);
@@ -147,7 +147,7 @@ final class DirectMapper implements iImport
Message::increment("{$entity->via}.{$entity->type}.failed");
$this->logger->notice(
"MAPPER: Ignoring '{backend}' '{title}'. Does not exist in database. And backend set as metadata source only.",
"DirectMapper: Ignoring '{backend}: {title}'. Does not exist in database. And backend set as metadata source only.",
[
'metaOnly' => true,
'backend' => $entity->via,
@@ -191,7 +191,7 @@ final class DirectMapper implements iImport
}
}
$this->logger->notice("MAPPER: '{backend}' added '{title}' as new item.", [
$this->logger->notice("DirectMapper: '{backend}' added '{title}' as new item.", [
'backend' => $entity->via,
'title' => $entity->getName(),
true === $this->inTraceMode() ? 'trace' : 'metadata' => $data,
@@ -209,18 +209,21 @@ final class DirectMapper implements iImport
$this->actions[$entity->type]['failed']++;
Message::increment("{$entity->via}.{$entity->type}.failed");
$this->logger->error(
message: "MAPPER: Exception '{error.kind}' was thrown unhandled in adding '{backend}' '{title}'. '{error.message}' at '{error.file}:{error.line}'.",
context: [
'error' => [
'kind' => $e::class,
'line' => $e->getLine(),
'message' => $e->getMessage(),
'file' => after($e->getFile(), ROOT_PATH),
...lw(
message: "DirectMapper: Exception '{error.kind}' was thrown unhandled in adding '{backend}: {title}'. '{error.message}' at '{error.file}:{error.line}'.",
context: [
'error' => [
'kind' => $e::class,
'line' => $e->getLine(),
'message' => $e->getMessage(),
'file' => after($e->getFile(), ROOT_PATH),
],
'backend' => $entity->via,
'title' => $entity->getName(),
'state' => $entity->getAll()
],
'backend' => $entity->via,
'title' => $entity->getName(),
'state' => $entity->getAll()
]
e: $e
)
);
}
@@ -248,7 +251,7 @@ final class DirectMapper implements iImport
$this->removePointers($local)->addPointers($local, $local->id);
$this->logger->notice("MAPPER: '{backend}' updated '{title}' metadata.", [
$this->logger->notice("DirectMapper: '{backend}' updated '{title}' metadata.", [
'id' => $local->id,
'backend' => $entity->via,
'title' => $local->getName(),
@@ -269,22 +272,25 @@ final class DirectMapper implements iImport
$this->actions[$local->type]['failed']++;
Message::increment("{$entity->via}.{$local->type}.failed");
$this->logger->error(
message: "MAPPER: Exception '{error.kind}' was thrown unhandled in '{backend}' '{title}' handle tainted. '{error.message}' at '{error.file}:{error.line}'.",
context: [
'error' => [
'kind' => $e::class,
'line' => $e->getLine(),
'message' => $e->getMessage(),
'file' => after($e->getFile(), ROOT_PATH),
...lw(
message: "DirectMapper: Exception '{error.kind}' was thrown unhandled in '{backend}: {title}' handle tainted. '{error.message}' at '{error.file}:{error.line}'.",
context: [
'error' => [
'kind' => $e::class,
'line' => $e->getLine(),
'message' => $e->getMessage(),
'file' => after($e->getFile(), ROOT_PATH),
],
'id' => $local->id,
'backend' => $entity->via,
'title' => $local->getName(),
'state' => [
'database' => $local->getAll(),
'backend' => $entity->getAll()
],
],
'id' => $local->id,
'backend' => $entity->via,
'title' => $local->getName(),
'state' => [
'database' => $local->getAll(),
'backend' => $entity->getAll()
],
]
e: $e
)
);
}
@@ -306,7 +312,7 @@ final class DirectMapper implements iImport
}
$this->logger->notice(
"MAPPER: '{backend}' item '{id}: {title}' is marked as '{state}' vs local state '{local_state}', However due to the following reason '{reasons}' it was not considered as valid state.",
"DirectMapper: '{backend}' item '{id}: {title}' is marked as '{state}' vs local state '{local_state}', However due to the following reason '{reasons}' it was not considered as valid state.",
[
'id' => $local->id,
'backend' => $entity->via,
@@ -321,7 +327,7 @@ final class DirectMapper implements iImport
}
if ($this->inTraceMode()) {
$this->logger->info("MAPPER: '{backend}' '{title}' No metadata changes detected.", [
$this->logger->info("DirectMapper: '{backend}: {title}' No metadata changes detected.", [
'id' => $local->id,
'backend' => $entity->via,
'title' => $local->getName(),
@@ -364,7 +370,7 @@ final class DirectMapper implements iImport
}
}
$this->logger->notice("MAPPER: '{backend}' marked '{title}' as 'unplayed'.", [
$this->logger->notice("DirectMapper: '{backend}' marked '{title}' as 'unplayed'.", [
'id' => $cloned->id,
'backend' => $entity->via,
'title' => $cloned->getName(),
@@ -381,22 +387,25 @@ final class DirectMapper implements iImport
$this->actions[$local->type]['failed']++;
Message::increment("{$entity->via}.{$local->type}.failed");
$this->logger->error(
message: "MAPPER: Exception '{error.kind}' was thrown unhandled in '{backend}' '{title}' handle old entity unplayed. '{error.message}' at '{error.file}:{error.line}'.",
context: [
'error' => [
'kind' => $e::class,
'line' => $e->getLine(),
'message' => $e->getMessage(),
'file' => after($e->getFile(), ROOT_PATH),
...lw(
message: "DirectMapper: Exception '{error.kind}' was thrown unhandled in '{backend}: {title}' handle old entity unplayed. '{error.message}' at '{error.file}:{error.line}'.",
context: [
'error' => [
'kind' => $e::class,
'line' => $e->getLine(),
'message' => $e->getMessage(),
'file' => after($e->getFile(), ROOT_PATH),
],
'id' => $cloned->id,
'backend' => $entity->via,
'title' => $cloned->getName(),
'state' => [
'database' => $cloned->getAll(),
'backend' => $entity->getAll()
],
],
'id' => $cloned->id,
'backend' => $entity->via,
'title' => $cloned->getName(),
'state' => [
'database' => $cloned->getAll(),
'backend' => $entity->getAll()
],
]
e: $e
)
);
}
@@ -428,7 +437,7 @@ final class DirectMapper implements iImport
}
$local = $local->apply($entity, fields: $_keys);
$this->logger->notice(
$progress ? "MAPPER: '{backend}' updated '{title}' due to play progress change." : "MAPPER: '{backend}' updated '{title}' metadata.",
$progress ? "DirectMapper: '{backend}' updated '{title}' due to play progress change." : "DirectMapper: '{backend}' updated '{title}' metadata.",
[
'id' => $cloned->id,
'backend' => $entity->via,
@@ -463,22 +472,25 @@ final class DirectMapper implements iImport
$this->actions[$local->type]['failed']++;
Message::increment("{$entity->via}.{$local->type}.failed");
$this->logger->error(
message: "MAPPER: Exception '{error.kind}' was thrown unhandled in '{backend}' '{title}' handle old entity always update metadata. '{error.message}' at '{error.file}:{error.line}'.",
context: [
'error' => [
'kind' => $e::class,
'line' => $e->getLine(),
'message' => $e->getMessage(),
'file' => after($e->getFile(), ROOT_PATH),
...lw(
message: "DirectMapper: Exception '{error.kind}' was thrown unhandled in '{backend}: {title}' handle old entity always update metadata. '{error.message}' at '{error.file}:{error.line}'.",
context: [
'error' => [
'kind' => $e::class,
'line' => $e->getLine(),
'message' => $e->getMessage(),
'file' => after($e->getFile(), ROOT_PATH),
],
'id' => $cloned->id,
'backend' => $entity->via,
'title' => $cloned->getName(),
'state' => [
'database' => $cloned->getAll(),
'backend' => $entity->getAll()
],
],
'id' => $cloned->id,
'backend' => $entity->via,
'title' => $cloned->getName(),
'state' => [
'database' => $cloned->getAll(),
'backend' => $entity->getAll()
],
]
e: $e
)
);
}
@@ -490,7 +502,7 @@ final class DirectMapper implements iImport
if ($entity->isWatched() !== $local->isWatched()) {
$this->logger->notice(
"MAPPER: '{backend}' item '{id}: {title}' is marked as '{state}' vs local state '{local_state}', However due to the remote item date '{remote_date}' being older than the last backend sync date '{local_date}'. it was not considered as valid state.",
"DirectMapper: '{backend}' item '{id}: {title}' is marked as '{state}' vs local state '{local_state}', However due to the remote item date '{remote_date}' being older than the last backend sync date '{local_date}'. it was not considered as valid state.",
[
'id' => $cloned->id,
'backend' => $entity->via,
@@ -505,7 +517,7 @@ final class DirectMapper implements iImport
}
if ($this->inTraceMode()) {
$this->logger->debug("MAPPER: Ignoring '{backend}' '{title}'. No changes detected.", [
$this->logger->debug("DirectMapper: Ignoring '{backend}: {title}'. No changes detected.", [
'id' => $cloned->id,
'backend' => $entity->via,
'title' => $cloned->getName(),
@@ -521,7 +533,7 @@ final class DirectMapper implements iImport
public function add(iState $entity, array $opts = []): self
{
if (!$entity->hasGuids() && !$entity->hasRelativeGuid()) {
$this->logger->warning("MAPPER: Ignoring '{backend}' '{title}'. No valid/supported external ids.", [
$this->logger->warning("DirectMapper: Ignoring '{backend}: {title}'. No valid/supported external ids.", [
'id' => $entity->id,
'backend' => $entity->via,
'title' => $entity->getName(),
@@ -532,7 +544,7 @@ final class DirectMapper implements iImport
if (true === $entity->isEpisode() && $entity->episode < 1) {
$this->logger->warning(
"MAPPER: Ignoring '{backend}' '{id}: {title}'. Item was marked as episode but no episode number was provided.",
"DirectMapper: Ignoring '{backend}' '{id}: {title}'. Item was marked as episode but no episode number was provided.",
[
'id' => $entity->id ?? ag($entity->getMetadata($entity->via), iState::COLUMN_ID, ''),
'backend' => $entity->via,
@@ -584,7 +596,7 @@ final class DirectMapper implements iImport
* 3 - mark entity as tainted and re-process it.
*/
if (true === $hasAfter && true === $cloned->isWatched() && false === $entity->isWatched()) {
$message = "MAPPER: Watch state conflict detected in '{backend}: {title}' '{new_state}' vs local state '{id}: {current_state}'.";
$message = "DirectMapper: Watch state conflict detected in '{backend}: {title}' '{new_state}' vs local state '{id}: {current_state}'.";
$hasMeta = count($cloned->getMetadata($entity->via)) >= 1;
$hasDate = $entity->updated === ag($cloned->getMetadata($entity->via), iState::COLUMN_META_DATA_PLAYED_AT);
@@ -630,10 +642,10 @@ final class DirectMapper implements iImport
$changes = $local->diff(fields: $keys);
$message = "MAPPER: '{backend}' Updated '{title}'.";
$message = "DirectMapper: '{backend}' Updated '{title}'.";
if ($cloned->isWatched() !== $local->isWatched()) {
$message = "MAPPER: '{backend}' Updated and marked '{id}: {title}' as '{state}'.";
$message = "DirectMapper: '{backend}' Updated and marked '{id}: {title}' as '{state}'.";
if (null !== $onStateUpdate) {
$onStateUpdate($local);
@@ -664,23 +676,26 @@ final class DirectMapper implements iImport
$this->actions[$local->type]['failed']++;
Message::increment("{$entity->via}.{$local->type}.failed");
$this->logger->error(
message: "MAPPER: Exception '{error.kind}' was thrown unhandled in '{backend}' '{title}' add. '{error.message}' at '{error.file}:{error.line}'.",
context: [
'error' => [
'kind' => $e::class,
'line' => $e->getLine(),
'message' => $e->getMessage(),
'file' => after($e->getFile(), ROOT_PATH),
...lw(
message: "DirectMapper: Exception '{error.kind}' was thrown unhandled in '{backend}: {title}' add. '{error.message}' at '{error.file}:{error.line}'.",
context: [
'error' => [
'kind' => $e::class,
'line' => $e->getLine(),
'message' => $e->getMessage(),
'file' => after($e->getFile(), ROOT_PATH),
],
'id' => $cloned->id,
'backend' => $entity->via,
'title' => $cloned->getName(),
'state' => [
'database' => $cloned->getAll(),
'backend' => $entity->getAll()
],
'trace' => $e->getTrace(),
],
'id' => $cloned->id,
'backend' => $entity->via,
'title' => $cloned->getName(),
'state' => [
'database' => $cloned->getAll(),
'backend' => $entity->getAll()
],
'trace' => $e->getTrace(),
]
e: $e
)
);
}
@@ -700,7 +715,10 @@ final class DirectMapper implements iImport
];
}
$this->logger->debug("MAPPER: Ignoring '{backend}' '{title}'. Metadata & play state are identical.", $context);
$this->logger->debug(
"DirectMapper: Ignoring '{backend}: {title}'. Metadata & play state are identical.",
$context
);
Message::increment("{$entity->via}.{$entity->type}.ignored_no_change");

View File

@@ -604,7 +604,24 @@ final class MemoryMapper implements iImport
}
} catch (PDOException $e) {
$list[$entity->type]['failed']++;
$this->logger->error($e->getMessage(), $entity->getAll());
$this->logger->error(
...lw(
message: "MemoryMapper: Exception '{error.kind}' was thrown unhandled in {mode} '{backend}: {title}'. '{error.message}' at '{error.file}:{error.line}'.",
context: [
'error' => [
'kind' => $e::class,
'line' => $e->getLine(),
'message' => $e->getMessage(),
'file' => after($e->getFile(), ROOT_PATH),
],
'state' => $entity->getAll(),
'backend' => $entity->via,
'title' => $entity->getName(),
'mode' => $entity->id === null ? 'add' : 'update',
],
e: $e
)
);
}
}

View File

@@ -4,7 +4,9 @@ declare(strict_types=1);
namespace App\Libs;
use Closure;
use Monolog\Handler\TestHandler;
use Throwable;
class TestCase extends \PHPUnit\Framework\TestCase
{
@@ -25,4 +27,46 @@ class TestCase extends \PHPUnit\Framework\TestCase
fwrite(STDOUT, $logs['formatted'] . PHP_EOL);
}
}
/**
* Checks if the given closure throws an exception.
*
* @param Closure $closure
* @param string $reason
* @param Throwable|string $exception Expected exception class
* @param string $exceptionMessage (optional) Exception message
* @param int|null $exceptionCode (optional) Exception code
* @return void
*/
protected function checkException(
Closure $closure,
string $reason,
Throwable|string $exception,
string $exceptionMessage = '',
int|null $exceptionCode = null,
): void {
$caught = null;
try {
$closure();
} catch (Throwable $e) {
$caught = $e;
} finally {
if (null === $caught) {
$this->fail($reason);
} else {
$this->assertInstanceOf(
is_object($exception) ? $exception::class : $exception,
$caught,
$reason
);
if (!empty($exceptionMessage)) {
$this->assertStringContainsString($exceptionMessage, $caught->getMessage(), $reason);
}
if (!empty($exceptionCode)) {
$this->assertEquals($exceptionCode, $caught->getCode(), $reason);
}
}
}
}
}

View File

@@ -12,10 +12,13 @@ use App\Libs\Attributes\Scanner\Item as ScannerItem;
use App\Libs\Config;
use App\Libs\ConfigFile;
use App\Libs\Container;
use App\Libs\Database\DBLayer;
use App\Libs\DataUtil;
use App\Libs\Entity\StateInterface as iState;
use App\Libs\Enums\Http\Status;
use App\Libs\Events\DataEvent;
use App\Libs\Exceptions\AppExceptionInterface;
use App\Libs\Exceptions\DBLayerException;
use App\Libs\Exceptions\InvalidArgumentException;
use App\Libs\Exceptions\RuntimeException;
use App\Libs\Extends\Date;
@@ -256,9 +259,8 @@ if (!function_exists('ag_delete')) {
}
if (is_int($path)) {
if (isset($array[$path])) {
unset($array[$path]);
}
// -- if the path is int, and it's exists, it should have been caught by
// -- the first if condition. So, we can safely return the array as is.
return $array;
}
@@ -966,21 +968,13 @@ if (false === function_exists('r_array')) {
$pattern = '#' . preg_quote($tagLeft, '#') . '([\w_.]+)' . preg_quote($tagRight, '#') . '#is';
$status = preg_match_all($pattern, $text, $matches);
if (false === $status || $status < 1) {
return ['message' => $text, 'context' => $context];
}
preg_match_all($pattern, $text, $matches);
$replacements = [];
foreach ($matches[1] as $key) {
$placeholder = $tagLeft . $key . $tagRight;
if (false === str_contains($text, $placeholder)) {
continue;
}
if (false === ag_exists($context, $key)) {
continue;
}
@@ -1025,7 +1019,7 @@ if (false === function_exists('generateRoutes')) {
*
* @return array The generated routes.
*/
function generateRoutes(string $type = 'cli'): array
function generateRoutes(string $type = 'cli', array $opts = []): array
{
$dirs = [__DIR__ . '/../Commands'];
foreach (array_keys(Config::get('supported', [])) as $backend) {
@@ -1040,7 +1034,7 @@ if (false === function_exists('generateRoutes')) {
$routes_cli = (new Router($dirs))->generate();
$cache = Container::get(iCache::class);
$cache = $opts[iCache::class] ?? Container::get(iCache::class);
try {
$cache->set('routes_cli', $routes_cli, new DateInterval('PT1H'));
@@ -1144,7 +1138,7 @@ if (false === function_exists('getSystemMemoryInfo')) {
*
* @return array{ MemTotal: float, MemFree: float, MemAvailable: float, SwapTotal: float, SwapFree: float }
*/
function getSystemMemoryInfo(): array
function getSystemMemoryInfo(string $memFile = '/proc/meminfo'): array
{
$keys = [
'MemTotal' => 'mem_total',
@@ -1156,11 +1150,11 @@ if (false === function_exists('getSystemMemoryInfo')) {
$result = [];
if (!is_readable('/proc/meminfo')) {
if (!is_readable($memFile)) {
return $result;
}
if (false === ($lines = @file('/proc/meminfo', FILE_IGNORE_NEW_LINES | FILE_SKIP_EMPTY_LINES))) {
if (false === ($lines = @file($memFile, FILE_IGNORE_NEW_LINES | FILE_SKIP_EMPTY_LINES))) {
return $result;
}
@@ -1212,6 +1206,10 @@ if (!function_exists('checkIgnoreRule')) {
{
$urlParts = parse_url($guid);
if (false === is_array($urlParts)) {
throw new RuntimeException('Invalid ignore rule was given.');
}
if (null === ($db = ag($urlParts, 'user'))) {
throw new RuntimeException('No db source was given.');
}
@@ -1541,10 +1539,6 @@ if (!function_exists('parseEnvFile')) {
}
foreach (file($file, FILE_IGNORE_NEW_LINES | FILE_SKIP_EMPTY_LINES) as $line) {
if (empty($line)) {
continue;
}
if (true === str_starts_with($line, '#') || false === str_contains($line, '=')) {
continue;
}
@@ -1613,11 +1607,12 @@ if (!function_exists('isTaskWorkerRunning')) {
/**
* Check if the task worker is running. This function is only available when running in a container.
*
* @param string $pidFile (Optional) The PID file to check.
* @param bool $ignoreContainer (Optional) Whether to ignore the container check.
*
* @return array{ status: bool, message: string }
*/
function isTaskWorkerRunning(bool $ignoreContainer = false): array
function isTaskWorkerRunning(string $pidFile = '/tmp/ws-job-runner.pid', bool $ignoreContainer = false): array
{
if (false === $ignoreContainer && !inContainer()) {
return [
@@ -1635,8 +1630,6 @@ if (!function_exists('isTaskWorkerRunning')) {
];
}
$pidFile = '/tmp/ws-job-runner.pid';
if (!file_exists($pidFile)) {
return [
'status' => false,
@@ -1651,7 +1644,33 @@ if (!function_exists('isTaskWorkerRunning')) {
return ['status' => false, 'message' => $e->getMessage()];
}
if (file_exists(r('/proc/{pid}/status', ['pid' => $pid]))) {
switch (PHP_OS) {
case 'Linux':
{
$status = file_exists(r('/proc/{pid}/status', ['pid' => $pid]));
}
break;
case 'WINNT':
{
// -- Windows does not have a /proc directory so we need different way to get the status.
@exec("tasklist /FI \"PID eq {$pid}\" 2>NUL", $output);
// -- windows doesn't return 0 if the process is not found. we need to parse the output.
$status = false;
foreach ($output as $line) {
if (false === str_contains($line, $pid)) {
continue;
}
$status = true;
break;
}
}
break;
default:
$status = false;
break;
}
if (true === $status) {
return ['status' => true, 'restartable' => true, 'message' => 'Task worker is running.'];
}
@@ -1865,6 +1884,7 @@ if (!function_exists('cacheableItem')) {
* @param Closure $function
* @param DateInterval|int|null $ttl
* @param bool $ignoreCache
* @param array $opts
*
* @return mixed
*/
@@ -1872,15 +1892,16 @@ if (!function_exists('cacheableItem')) {
string $key,
Closure $function,
DateInterval|int|null $ttl = null,
bool $ignoreCache = false
bool $ignoreCache = false,
array $opts = [],
): mixed {
$cache = Container::get(iCache::class);
$cache = $opts[iCache::class] ?? Container::get(iCache::class);
if (!$ignoreCache && $cache->has($key)) {
return $cache->get($key);
}
$reflectContainer = Container::get(ReflectionContainer::class);
$reflectContainer = $opts[ReflectionContainer::class] ?? Container::get(ReflectionContainer::class);
$item = $reflectContainer->call($function);
if (null === $ttl) {
@@ -2050,7 +2071,73 @@ if (!function_exists('getBackend')) {
$default = $configFile->get($name);
$default['name'] = $name;
$data = array_replace_recursive($default, $config);
return makeBackend(array_replace_recursive($default, $config), $name);
return makeBackend($data, $name);
}
}
if (!function_exists('lw')) {
/**
* log wrapper.
*
* The use case for this wrapper is to enhance the log context with db exception information.
* All logs should be wrapped with this function. it will probably be enhanced to include further context.
* in the future.
*
* @param string $message The log message.
* @param array $context The log context.
* @param Throwable|null $e The exception.
*
* @return array{ message: string, context: array} The wrapped log message and context.
*/
function lw(string $message, array $context, Throwable|null $e = null): array
{
if (null === $e) {
return [
'message' => $message,
'context' => $context,
];
}
if (true === ($e instanceof DBLayerException)) {
$context[DBLayer::class] = [
'query' => $e->getQueryString(),
'bind' => $e->getQueryBind(),
'error' => $e->errorInfo ?? [],
];
}
if (true === ($e instanceof AppExceptionInterface) && $e->hasContext()) {
$context[AppExceptionInterface::class] = $e->getContext();
}
return [
'message' => $message,
'context' => $context,
];
}
}
if (!function_exists('timeIt')) {
/**
* Time the execution of a function.
*
* @param Closure $function The function to time.
* @param string $name The name of the function.
* @param int $round (Optional) The number of decimal places to round to.
*
* @return string
*/
function timeIt(Closure $function, string $name, int $round = 6): string
{
$start = microtime(true);
$function();
$end = microtime(true);
return r("Execution time is '{time}' for '{name}'", [
'name' => $name,
'time' => round($end - $start, $round),
]);
}
}

View File

@@ -118,12 +118,12 @@ class GetLibrariesListTest extends TestCase
$this->assertFalse($response->status);
$this->assertNotNull($response->error);
$this->assertSame(
'ERROR: Request for [Plex] libraries returned with unexpected [401] status code.',
$this->assertStringContainsString(
"ERROR: Request for 'Plex' libraries returned with unexpected '401' status code.",
(string)$response->error
);
$this->assertNull($response->response);
$this->assertFalse($response->error->hasException());
$this->assertTrue($response->error->hasException());
}
}

View File

@@ -6,10 +6,11 @@ declare(strict_types=1);
namespace Tests\Database;
use App\Libs\Database\DatabaseInterface as iDB;
use App\Libs\Database\DBLayer;
use App\Libs\Database\PDO\PDOAdapter;
use App\Libs\Entity\StateEntity;
use App\Libs\Entity\StateInterface;
use App\Libs\Exceptions\DatabaseException as DBException;
use App\Libs\Exceptions\DBAdapterException as DBException;
use App\Libs\Guid;
use App\Libs\TestCase;
use DateTimeImmutable;
@@ -46,7 +47,7 @@ class PDOAdapterTest extends TestCase
$logger->pushHandler($this->handler);
Guid::setLogger($logger);
$this->db = new PDOAdapter($logger, new PDO('sqlite::memory:'));
$this->db = new PDOAdapter($logger, new DBLayer(new PDO('sqlite::memory:')));
$this->db->migrations('up');
}

View File

View File

View File

@@ -0,0 +1 @@
0

View File

View File

@@ -0,0 +1 @@
0

View File

@@ -0,0 +1,55 @@
MemTotal: 131598708 kB
MemFree: 10636272 kB
MemAvailable: 113059644 kB
Buffers: 1851320 kB
Cached: 96874768 kB
SwapCached: 199724 kB
Active: 15252072 kB
Inactive: 96757496 kB
Active(anon): 4046452 kB
Inactive(anon): 11604056 kB
Active(file): 11205620 kB
Inactive(file): 85153440 kB
Unevictable: 43880 kB
Mlocked: 0 kB
SwapTotal: 144758584 kB
SwapFree: 140512824 kB
Zswap: 0 kB
Zswapped: 0 kB
Dirty: 28 kB
Writeback: 0 kB
AnonPages: 12869660 kB
Mapped: 760892 kB
Shmem: 2367028 kB
KReclaimable: 7315620 kB
Slab: 8107296 kB
SReclaimable: 7315620 kB
SUnreclaim: 791676 kB
KernelStack: 42512 kB
PageTables: 78184 kB
SecPageTables: 0 kB
NFS_Unstable: 0 kB
Bounce: 0 kB
WritebackTmp: 0 kB
CommitLimit: 210557936 kB
Committed_AS: 38290840 kB
VmallocTotal: 34359738367 kB
VmallocUsed: 198508 kB
VmallocChunk: 0 kB
Percpu: 37760 kB
HardwareCorrupted: 0 kB
AnonHugePages: 5797888 kB
ShmemHugePages: 47104 kB
ShmemPmdMapped: 0 kB
FileHugePages: 0 kB
FilePmdMapped: 0 kB
HugePages_Total: 0
HugePages_Free: 0
HugePages_Rsvd: 0
HugePages_Surp: 0
Hugepagesize: 2048 kB
Hugetlb: 0 kB
DirectMap4k: 11644304 kB
DirectMap2M: 119123968 kB
DirectMap1G: 3145728 kB

View File

@@ -0,0 +1,62 @@
test_plex:
type: plex
url: 'https://plex.example.invalid'
token: t000000000000000000p
user: 11111111
uuid: s00000000000000000000000000000000000000p
export:
enabled: true
lastSync: null
import:
enabled: true
lastSync: 1724173445
webhook:
match:
user: true
uuid: true
options:
ignore: '22,1,2,3'
LIBRARY_SEGMENT: 1000
ADMIN_TOKEN: plex_admin_token
use_old_progress_endpoint: true
plex_user_uuid: r00000000000000p
test_jellyfin:
type: jellyfin
url: 'https://jellyfin.example.invalid'
token: t000000000000000000000000000000j
user: u000000000000000000000000000000j
uuid: s000000000000000000000000000000j
export:
enabled: true
lastSync: null
import:
enabled: true
lastSync: 1724173445
webhook:
match:
user: false
uuid: true
options:
ignore: 'i000000000000000000000000000000j,i100000000000000000000000000000j'
MAX_EPISODE_RANGE: 6
test_emby:
type: emby
url: 'https://emby.example.invalid'
token: t000000000000000000000000000000e
user: u000000000000000000000000000000e
uuid: s000000000000000000000000000000e
import:
enabled: true
lastSync: 1724173445
export:
enabled: true
lastSync: null
webhook:
match:
user: false
uuid: true
options:
MAX_EPISODE_RANGE: 6
IMPORT_METADATA_ONLY: true

View File

@@ -0,0 +1,267 @@
<?php
declare(strict_types=1);
namespace Tests\Libs;
use App\Libs\ConfigFile;
use App\Libs\Extends\LogMessageProcessor;
use App\Libs\TestCase;
use InvalidArgumentException;
use Monolog\Handler\TestHandler;
use Monolog\Logger;
use Throwable;
class ConfigFileTest extends TestCase
{
private array $data = ['foo' => 'bar', 'baz' => 'kaz', 'sub' => ['key' => 'val']];
private array $params = [];
private Logger|null $logger = null;
protected function setUp(): void
{
parent::setUp();
$this->handler = new TestHandler();
$this->logger = new Logger('logger', processors: [new LogMessageProcessor()]);
$this->logger->pushHandler($this->handler);
$this->params = [
'file' => __DIR__ . '/../Fixtures/test_servers.yaml',
'type' => 'yaml',
'autoSave' => false,
'autoCreate' => false,
'autoBackup' => false,
'opts' => [
'json_decode' => JSON_UNESCAPED_UNICODE,
'json_encode' => JSON_UNESCAPED_UNICODE,
],
];
}
public function test_constructor()
{
$this->checkException(
closure: fn() => new ConfigFile(
'nonexistent.json',
'json',
autoSave: false,
autoCreate: false,
autoBackup: false
),
reason: 'If file does not exist, and autoCreate is set to false, an exception should be thrown.',
exception: InvalidArgumentException::class,
exceptionMessage: "File 'nonexistent.json' does not exist.",
);
$this->checkException(
closure: fn() => new ConfigFile(
'nonexistent.json',
'php',
autoSave: false,
autoCreate: false,
autoBackup: false
),
reason: 'If type is not supported, an exception should be thrown.',
exception: InvalidArgumentException::class,
exceptionMessage: "Invalid content type 'php'.",
);
$this->checkException(
closure: fn() => new ConfigFile(
'/root/test.json',
'json',
autoSave: false,
autoCreate: true,
autoBackup: false
),
reason: 'If file is not writable, an exception should be thrown.',
exception: InvalidArgumentException::class,
exceptionMessage: "could not be created",
);
try {
$class = new ConfigFile(...$this->params);
$this->assertInstanceOf(ConfigFile::class, $class);
} catch (Throwable $e) {
$this->fail('If all conditions are met, NO exception should be been thrown. ' . $e->getMessage());
}
try {
$class = ConfigFile::open(...$this->params);
$this->assertInstanceOf(ConfigFile::class, $class);
} catch (Throwable $e) {
$this->fail('If all conditions are met, NO exception should be been thrown. ' . $e->getMessage());
}
}
public function test_setLogger()
{
$tmpFile = tempnam(sys_get_temp_dir(), 'test');
copy(__DIR__ . '/../Fixtures/test_servers.yaml', $tmpFile);
$params = $this->params;
$params['file'] = $tmpFile;
$params['autoBackup'] = true;
$class = new ConfigFile(...$params);
try {
$class->setLogger($this->logger);
$class->set('foo', 'bar');
$class->delete('kaz');
// -- trigger external change
ConfigFile::open(...$params)->set('bar', 'kaz')->persist();
// -- should trigger warning.
$class->persist();
$this->assertStringContainsString(
'has been modified since last load.',
$this->handler->getRecords()[0]['message']
);
} catch (Throwable $e) {
$this->fail('If correct logger is passed, no exception should be thrown. ' . $e->getMessage());
} finally {
if (file_exists($tmpFile)) {
unlink($tmpFile);
}
if (file_exists($tmpFile . '.bak')) {
unlink($tmpFile . '.bak');
}
}
}
public function test_delete()
{
$tmpFile = tempnam(sys_get_temp_dir(), 'test');
copy(__DIR__ . '/../Fixtures/test_servers.yaml', $tmpFile);
$params = $this->params;
$params['file'] = $tmpFile;
try {
$this->assertArrayNotHasKey(
'test_jellyfin',
ConfigFile::open(...$params)->delete('test_jellyfin')->getAll(),
'->delete: Failed to delete key from YAML file.'
);
$class = ConfigFile::open(...$params);
unset($class['test_jellyfin']);
$this->assertArrayNotHasKey(
'test_jellyfin',
$class->getAll(),
'ArrayAccess: Failed to delete key from YAML file.'
);
} catch (Throwable $e) {
$this->fail('If correct logger is passed, no exception should be thrown. ' . $e->getMessage());
} finally {
if (file_exists($tmpFile)) {
unlink($tmpFile);
}
}
}
public function test_get()
{
$class = new ConfigFile(...$this->params);
$this->assertArrayHasKey(
'token',
$class->get('test_plex', []),
'->get: Invalid response from parsing YAML file.'
);
$this->assertArrayHasKey('token', $class['test_plex'], 'ArrayAccess: Invalid response from parsing YAML file.');
$this->assertArrayNotHasKey(
'token',
$class->get('test_not_set', []),
'Invalid response from parsing YAML file.'
);
$this->assertNull($class['test_not_set'], 'ArrayAccess: Must return null if key does not exist.');
}
public function test_has()
{
$class = new ConfigFile(...$this->params);
$this->assertTrue($class->has('test_plex'), '->has: Must return true if key exists.');
$this->assertTrue(isset($class['test_plex']), 'ArrayAccess: Must return true if key exists.');
$this->assertFalse($class->has('test_not_set'), '->has: Must return false if key does not exist.');
$this->assertFalse(isset($class['test_not_set']), 'ArrayAccess: Must return false if key does not exist.');
}
public function test_set()
{
$tmpFile = tempnam(sys_get_temp_dir(), 'test');
copy(__DIR__ . '/../Fixtures/test_servers.yaml', $tmpFile);
$params = $this->params;
$params['file'] = $tmpFile;
try {
$this->assertArrayHasKey(
'test_foo',
ConfigFile::open(...$params)->set('test_foo', $this->data)->getAll(),
'->set: Failed to set key in YAML file.'
);
$class = ConfigFile::open(...$params);
$class['test_foo'] = $this->data;
$this->assertArrayHasKey('test_foo', $class->getAll(), 'ArrayAccess: Failed to set key.');
// -- test deep array.
$class->set('test_plex.options.foo', 'bar');
$this->assertArrayHasKey('foo', $class->get('test_plex.options', []), 'Failed to set deep key.');
$class['foo'] = ['bar' => ['jaz' => ['kaz' => 'baz']]];
$this->assertArrayHasKey('foo', $class->getAll(), 'ArrayAccess: Failed to set key.');
$class->set('foo', ['bar' => ['tax' => 'max']]);
$class->override()->persist();
$class = ConfigFile::open(...$params);
$this->assertArrayHasKey('test_foo', $class->getAll(), 'failed to persist changes.');
$this->assertArrayHasKey('foo', $class->getAll(), 'ArrayAccess: failed to persist changes.');
$this->assertArrayHasKey('bar', $class->get('foo', []), 'failed to persist changes.');
} catch (Throwable $e) {
$this->fail('If correct logger is passed, no exception should be thrown. ' . $e->getMessage());
} finally {
if (file_exists($tmpFile)) {
unlink($tmpFile);
}
}
}
public function test_configFile_set()
{
$params['file'] = tempnam(sys_get_temp_dir(), 'test');
try {
try {
copy(__DIR__ . '/../Fixtures/test_servers.yaml', $params['file']);
$class = new ConfigFile(...$params);
$this->assertInstanceOf(ConfigFile::class, $class);
} catch (Throwable $e) {
$this->fail('If file exists and is readable, no exception should be thrown. ' . $e->getMessage());
}
$class = ConfigFile::open(...$params);
$this->assertInstanceOf(ConfigFile::class, $class);
$this->assertArrayHasKey('token', $class->get('test_plex', []), 'Invalid response from parsing YAML file.');
$this->assertArrayNotHasKey(
'token',
$class->get('test_not_set', []),
'Invalid response from parsing YAML file.'
);
$this->assertTrue($class->has('test_plex'), 'Must return true if key exists.');
$this->assertFalse($class->has('test_not_set'), 'Must return false if key does not exist.');
$this->assertArrayHasKey('token', $class['test_plex'], 'Failed to get arrayAccess key correctly.');
$this->assertTrue(isset($class['test_plex']), 'Must return true if arrayAccess key exists.');
$this->assertNull($class['test_not_set'], 'Must return null if arrayAccess key does not exist.');
$this->assertFalse(isset($class['test_not_set']), 'Must return false if arrayAccess key does not exist.');
} finally {
if (file_exists($params['file'])) {
unlink($params['file']);
}
}
}
}

View File

@@ -4,23 +4,108 @@ declare(strict_types=1);
namespace Tests\Libs;
use App\Backends\Plex\PlexClient;
use App\Libs\Config;
use App\Libs\Container;
use App\Libs\Database\DBLayer;
use App\Libs\Entity\StateEntity;
use App\Libs\Enums\Http\Method;
use App\Libs\Enums\Http\Status;
use App\Libs\Exceptions\AppExceptionInterface;
use App\Libs\Exceptions\DBLayerException;
use App\Libs\Exceptions\InvalidArgumentException;
use App\Libs\Exceptions\RuntimeException;
use App\Libs\Extends\ReflectionContainer;
use App\Libs\TestCase;
use JsonMachine\Items;
use JsonMachine\JsonDecoder\ErrorWrappingDecoder;
use JsonMachine\JsonDecoder\ExtJsonDecoder;
use JsonSerializable;
use Nyholm\Psr7\Factory\Psr17Factory;
use Nyholm\Psr7\Stream;
use Nyholm\Psr7Server\ServerRequestCreator;
use Psr\SimpleCache\CacheInterface;
use Stringable;
use Symfony\Component\HttpClient\MockHttpClient;
use Symfony\Component\HttpClient\Response\MockResponse;
use Symfony\Component\Yaml\Yaml;
use TypeError;
class HelpersTest extends TestCase
{
protected CacheInterface|null $cache = null;
protected function setUp(): void
{
parent::setUp();
$this->cache = new class implements CacheInterface {
public array $cache = [];
public bool $throw = false;
public function get(string $key, mixed $default = null): mixed
{
return $this->cache[$key] ?? $default;
}
public function set(string $key, mixed $value, \DateInterval|int|null $ttl = null): bool
{
if ($this->throw) {
throw new class() extends \InvalidArgumentException implements
\Psr\SimpleCache\InvalidArgumentException {
};
}
$this->cache[$key] = $value;
return true;
}
public function delete(string $key): bool
{
unset($this->cache[$key]);
return true;
}
public function clear(): bool
{
$this->cache = [];
return true;
}
public function getMultiple(iterable $keys, mixed $default = null): iterable
{
foreach ($keys as $key) {
yield $key => $this->get($key, $default);
}
}
public function setMultiple(iterable $values, \DateInterval|int|null $ttl = null): bool
{
foreach ($values as $key => $value) {
$this->set($key, $value, $ttl);
}
return true;
}
public function deleteMultiple(iterable $keys): bool
{
foreach ($keys as $key) {
$this->delete($key);
}
return true;
}
public function has(string $key): bool
{
return isset($this->cache[$key]);
}
public function reset(): void
{
$this->cache = [];
}
};
}
public function test_env_conditions(): void
{
$values = [
@@ -171,6 +256,32 @@ class HelpersTest extends TestCase
ag_set([], 'foo.kaz', 'taz'),
'When a nested key is passed, it will be saved in format of [key => [nested_key => value]]'
);
$exception = null;
try {
ag_set(['foo' => 'bar'], 'foo.bar.taz', 'baz');
} catch (\Throwable $e) {
$exception = $e;
} finally {
$this->assertSame(
TypeError::class,
$exception ? $exception::class : null,
'When trying to set value to non-array, exception is thrown.'
);
}
$exception = null;
try {
ag_set(['foo' => ['bar' => ['taz' => 'tt']]], 'foo.bar.taz.tt', 'baz');
} catch (\Throwable $e) {
$exception = $e;
} finally {
$this->assertSame(
RuntimeException::class,
$exception ? $exception::class : null,
'When trying to set value to existing key, exception is thrown.'
);
}
}
public function test_ag_exits(): void
@@ -221,6 +332,23 @@ class HelpersTest extends TestCase
ag_delete($arr, 'foo'),
'When simple key is passed, and it exists, it is deleted, and copy of the modified array is returned'
);
$this->assertSame(
[0 => 'foo', 1 => 'bar'],
ag_delete([0 => 'foo', 1 => 'bar', 2 => 'taz'], 2),
'When an int key is passed, and it exists, it is deleted, and copy of the modified array is returned'
);
$this->assertSame(
$arr,
ag_delete($arr, 121),
'When an int key is passed, and it does not exist, original array is returned'
);
$this->assertSame(
$arr,
ag_delete($arr, 'test.bar'),
'When a non-existing key is passed, original array is returned.'
);
}
public function test_fixPath(): void
@@ -332,6 +460,7 @@ class HelpersTest extends TestCase
]
]);
$data = ['foo' => 'bar'];
api_response(200, $data);
$response = api_response(Status::OK, $data);
$this->assertSame(Status::OK->value, $response->getStatusCode());
$this->assertSame('application/json', $response->getHeaderLine('Content-Type'));
@@ -354,11 +483,50 @@ class HelpersTest extends TestCase
]);
$data = ['error' => ['code' => Status::BAD_REQUEST->value, 'message' => 'error message']];
$response = api_error('error message', Status::BAD_REQUEST);
$response = api_error('error message', Status::BAD_REQUEST, headers: [
'X-Test-Header' => 'test',
]);
$this->assertSame(Status::BAD_REQUEST->value, $response->getStatusCode());
$this->assertSame('application/json', $response->getHeaderLine('Content-Type'));
$this->assertSame(getAppVersion(), $response->getHeaderLine('X-Application-Version'));
$this->assertSame('test', $response->getHeaderLine('X-Test-Header'));
$this->assertSame($data, json_decode($response->getBody()->getContents(), true));
$response = api_error('error message', Status::BAD_REQUEST, opts: [
'callback' => fn($response) => $response->withStatus(Status::INTERNAL_SERVER_ERROR->value)
]);
$this->assertSame(Status::INTERNAL_SERVER_ERROR->value, $response->getStatusCode());
}
public function test_api_message(): void
{
Config::append([
'api' => [
'response' => [
'headers' => [
'Content-Type' => 'application/json',
'X-Application-Version' => fn() => getAppVersion(),
'Access-Control-Allow-Origin' => '*',
],
],
]
]);
$data = ['info' => ['code' => Status::OK->value, 'message' => 'info message']];
$response = api_message('info message', Status::OK, headers: [
'X-Test-Header' => 'test',
]);
$this->assertSame(Status::OK->value, $response->getStatusCode());
$this->assertSame('application/json', $response->getHeaderLine('Content-Type'));
$this->assertSame(getAppVersion(), $response->getHeaderLine('X-Application-Version'));
$this->assertSame('test', $response->getHeaderLine('X-Test-Header'));
$this->assertSame($data, json_decode($response->getBody()->getContents(), true));
$response = api_message('info message', Status::OK, opts: [
'callback' => fn($response) => $response->withStatus(Status::INTERNAL_SERVER_ERROR->value)
]);
$this->assertSame(Status::INTERNAL_SERVER_ERROR->value, $response->getStatusCode());
}
public function test_httpClientChunks(): void
@@ -461,6 +629,51 @@ class HelpersTest extends TestCase
arrayToString($data, '@ '),
'When array is passed, it is converted into array text separated by delimiter.'
);
$cl = new class implements JsonSerializable {
public function jsonSerialize(): array
{
return ['foo' => 'bar'];
}
public function __toString(): string
{
return json_encode($this->jsonSerialize());
}
};
$cl2 = new class implements Stringable {
public function __toString(): string
{
return json_encode(['foo' => 'bar']);
}
};
$cl3 = new class() {
public string $foo = 'bar';
};
$this->assertSame(
'(baz: {"foo":"bar"})',
arrayToString(['baz' => $cl]),
'When array contains a class that implements JsonSerializable it is converted into array string.'
);
$this->assertSame(
'(baz: {"foo":"bar"}), (foo: true), (bar: false)',
arrayToString([
'baz' => $cl2,
'foo' => true,
'bar' => false,
]),
"When an object that implements Stringable is passed, it's casted to string"
);
$this->assertSame(
'(baz: [ (foo: bar) ])',
arrayToString(['baz' => $cl3]),
"When a class doesn't implement JsonSerializable or Stringable, it's converted to array. using object vars."
);
}
public function test_isValidName(): void
@@ -563,6 +776,9 @@ class HelpersTest extends TestCase
isIgnoredId('home_plex', 'movie', 'guid_tvdb', '1201', '121'),
'When ignore url is passed with and ignore list does not contain the url, false is returned.'
);
$this->expectException(InvalidArgumentException::class);
isIgnoredId('home_plex', 'not_real_type', 'guid_tvdb', '1200', '121');
}
public function test_r(): void
@@ -597,6 +813,32 @@ class HelpersTest extends TestCase
'When array is passed, it is converted into array and placeholders are replaced with values.'
);
$message = 'foo bar,taz';
$context = ['obj' => ['foo' => 'bar', 'baz' => 'taz']];
$this->assertSame(
['message' => $message, 'context' => $context],
r_array($message, $context),
'When non-existing placeholder is passed, string is returned as it is.'
);
$this->assertSame(
'Time is: 2020-01-01T00:00:00+00:00',
r('Time is: {date}', ['date' => makeDate('2020-01-01', 'UTC')]),
'When date is passed, it is converted into string and placeholders are replaced with values.'
);
$this->assertSame(
'HTTP Status: 200',
r('HTTP Status: {status}', ['status' => Status::OK]),
'When Int backed Enum is passed, it is converted into its value and the placeholder is replaced with it.'
);
$this->assertSame(
'HTTP Method: POST',
r('HTTP Method: {method}', ['method' => Method::POST]),
'When String backed Enum is passed, it is converted into its value and the placeholder is replaced with it.'
);
$res = fopen('php://memory', 'r');
$this->assertSame(
'foo [resource]',
@@ -759,4 +1001,630 @@ class HelpersTest extends TestCase
$this->fail('This function shouldn\'t throw exception when invalid file is given.');
}
}
public function test_generateRoutes()
{
$routes = generateRoutes('cli', [CacheInterface::class => $this->cache]);
$this->assertCount(
2,
$this->cache->cache,
'It should have generated two cache buckets for http and cli routes.'
);
$this->assertGreaterThanOrEqual(
1,
count($this->cache->cache['routes_cli']),
'It should have more than 1 route for cli routes.'
);
$this->assertGreaterThanOrEqual(
1,
count($this->cache->cache['routes_http']),
'It should have more than 1 route for cli routes.'
);
$this->assertSame(
$routes,
$this->cache->cache['routes_cli'],
'It should return cli routes when called with cli type.'
);
$this->cache->reset();
$this->assertSame(
generateRoutes('http', [CacheInterface::class => $this->cache]),
$this->cache->cache['routes_http'],
'It should return http routes. when called with http type.'
);
$this->cache->reset();
$this->cache->throw = true;
$routes = generateRoutes('http', [CacheInterface::class => $this->cache]);
$this->assertCount(0, $this->cache->cache, 'When cache throws exception, it should not save anything.');
$this->assertNotSame([], $routes, 'Routes should be generated even if cache throws exception.');
// --
$save = Config::get('supported', []);
Config::save('supported', ['not_set' => 'not_set_client', 'plex' => PlexClient::class,]);
$routes = generateRoutes('http', [CacheInterface::class => $this->cache]);
Config::save('supported', $save);
}
public function test_getSystemMemoryInfo()
{
/** @noinspection PhpUnhandledExceptionInspection */
$none = getSystemMemoryInfo(bin2hex(random_bytes(32)));
$this->assertIsArray($none, 'It should return array.');
$this->assertSame([], $none, 'When mem-file is not readable, it should return empty array.');
$info = getSystemMemoryInfo(__DIR__ . '/../Fixtures/meminfo_data.txt');
$this->assertIsArray($info, 'It should return array of memory info.');
$this->assertArrayHasKey('mem_total', $info, 'It should have total memory key.');
$this->assertArrayHasKey('mem_free', $info, 'It should have free memory key.');
$this->assertArrayHasKey('mem_available', $info, 'It should have available memory key.');
$this->assertArrayHasKey('swap_total', $info, 'It should have swap total key.');
$this->assertArrayHasKey('swap_free', $info, 'It should have swap free key.');
$keysValues = [
"mem_total" => 131598708000.0,
"mem_free" => 10636272000.0,
"mem_available" => 113059644000.0,
"swap_total" => 144758584000.0,
"swap_free" => 140512824000.0,
];
foreach ($keysValues as $key => $value) {
$this->assertSame($value, $info[$key], "It should have correct value for {$key} key.");
}
if (is_writeable(sys_get_temp_dir())) {
try {
$fileName = tempnam(sys_get_temp_dir(), 'meminfo');
$none = getSystemMemoryInfo($fileName);
$this->assertIsArray($none, 'It should return array.');
$this->assertSame([], $none, 'When mem-file is empty it should return empty array.');
} finally {
if (file_exists($fileName)) {
unlink($fileName);
}
}
} else {
$this->markTestSkipped('Temp directory is not writable.');
}
}
public function test_checkIgnoreRule()
{
Config::save('servers', ['test_backend' => []]);
$rule = 'movie://tvdb:276923@test_backend?id=133367';
$this->assertTrue(checkIgnoreRule($rule));
// -- if no db source is given, it should throw exception.
$exception = null;
try {
checkIgnoreRule('movie://test_backend?id=133367&garbage=1');
} catch (\Throwable $e) {
$exception = $e;
} finally {
$this->assertSame(RuntimeException::class, $exception ? $exception::class : null);
$this->assertSame(
'No db source was given.',
$exception?->getMessage(),
'When no db source is given, it should throw exception.'
);
}
$exception = null;
try {
checkIgnoreRule('movie://foo@test_backend?id=133367&garbage=1');
} catch (\Throwable $e) {
$exception = $e;
} finally {
$this->assertSame(RuntimeException::class, $exception ? $exception::class : null);
$this->assertStringContainsString(
"Invalid db source name 'foo' was given.",
$exception?->getMessage(),
'When invalid db source is given, it should throw exception.'
);
}
$exception = null;
try {
checkIgnoreRule('movie://tvdb@test_backend?id=133367&garbage=1');
} catch (\Throwable $e) {
$exception = $e;
} finally {
$this->assertSame(RuntimeException::class, $exception ? $exception::class : null);
$this->assertSame(
'No external id was given.',
$exception?->getMessage(),
'When no external id is given in the password part of url, it should throw exception.'
);
}
$exception = null;
try {
checkIgnoreRule('http://tvdb:123@test_backend?id=133367&garbage=1');
} catch (\Throwable $e) {
$exception = $e;
} finally {
$this->assertSame(
RuntimeException::class,
$exception ? $exception::class : null,
$exception?->getMessage() ?? ''
);
$this->assertStringContainsString(
"Invalid type 'http' was given.",
$exception?->getMessage(),
'When invalid type is given, it should throw exception.'
);
}
$exception = null;
try {
checkIgnoreRule('movie://tvdb:123@not_set?id=133367&garbage=1');
} catch (\Throwable $e) {
$exception = $e;
} finally {
$this->assertSame(
RuntimeException::class,
$exception ? $exception::class : null,
$exception?->getMessage() ?? ''
);
$this->assertStringContainsString(
"Invalid backend name 'not_set' was given.",
$exception?->getMessage(),
'When invalid backend name is given, it should throw exception.'
);
}
$exception = null;
try {
checkIgnoreRule('//tvdb:123@not_set?id=133367&garbage=1');
} catch (\Throwable $e) {
$exception = $e;
} finally {
$this->assertSame(
RuntimeException::class,
$exception ? $exception::class : null,
$exception?->getMessage() ?? ''
);
$this->assertStringContainsString(
'No type was given.',
$exception?->getMessage(),
'When no type is given, it should throw exception.'
);
}
$exception = null;
try {
checkIgnoreRule('//');
} catch (\Throwable $e) {
$exception = $e;
} finally {
$this->assertSame(
RuntimeException::class,
$exception ? $exception::class : null,
$exception?->getMessage() ?? ''
);
$this->assertStringContainsString(
'Invalid ignore rule was given.',
$exception?->getMessage(),
'When parse_url fails to parse url, it should throw exception.'
);
}
}
public function test_addCors()
{
$response = api_response(Status::OK, headers: ['X-Request-Id' => '1']);
$response = addCors($response, headers: [
'X-Test-Add' => 'test',
'X-Request-Id' => '2',
], methods: ['GET', 'POST', 'PUT', 'DELETE', 'PATCH', 'OPTIONS']);
$this->assertSame('*', $response->getHeaderLine('Access-Control-Allow-Origin'));
$this->assertSame(
'GET, POST, PUT, DELETE, PATCH, OPTIONS',
$response->getHeaderLine('Access-Control-Allow-Methods')
);
$this->assertSame(
'X-Application-Version, X-Request-Id, *',
$response->getHeaderLine('Access-Control-Allow-Headers')
);
$this->assertGreaterThanOrEqual(600, (int)$response->getHeaderLine('Access-Control-Max-Age'));
$this->assertSame('test', $response->getHeaderLine('X-Test-Add'));
$this->assertSame('1', $response->getHeaderLine('X-Request-Id'), 'The original header should not be altered.');
$this->assertNotSame(
'2',
$response->getHeaderLine('X-Request-Id'),
'AddCors: headers should not alter already set headers.'
);
}
public function test_deepArrayMerge()
{
$array1 = [
'foo' => 'bar',
'baz' => 'taz',
'kaz' => [
'raz' => 'maz',
'naz' => 'laz',
],
];
$array2 = [
'foo' => 'baz',
'kaz' => [
'raz' => 'baz',
'naz' => 'baz',
],
];
$expected = [
'foo' => 'baz',
'baz' => 'taz',
'kaz' => [
'raz' => 'baz',
'naz' => 'baz',
],
];
$this->assertSame($expected, deepArrayMerge([$array1, $array2]), 'It should merge arrays correctly.');
$this->assertSame(
[['foo' => 'baz'], ['baz' => 'taz'],],
deepArrayMerge([[['foo' => 'bar']], [['foo' => 'baz'], ['baz' => 'taz'],]], true),
'if preserve keys is true'
);
$this->assertSame(
[['foo' => 'bar'], ['foo' => 'baz'], ['baz' => 'taz'],],
deepArrayMerge([[['foo' => 'bar']], [['foo' => 'baz'], ['baz' => 'taz'],]], false),
'if preserve keys is false'
);
}
public function test_tryCatch()
{
$f = null;
$x = tryCatch(fn() => throw new RuntimeException(), fn($e) => $e, function () use (&$f) {
$f = 'finally_was_called';
});
$this->assertInstanceOf(
RuntimeException::class,
$x,
'When try block is successful, it should return the value.'
);
$this->assertSame('finally_was_called', $f, 'finally block should be executed.');
}
public function test_getServerColumnSpec()
{
$this->assertSame(
[
'key' => 'user',
'type' => 'string',
'visible' => true,
'description' => 'The user ID of the backend.',
],
getServerColumnSpec('user'),
'It should return correct column spec.'
);
$this->assertSame([], getServerColumnSpec('not_set'), 'It should return empty array when column is not set.');
}
public function test_getEnvSpec()
{
$this->assertSame(
[
'key' => 'WS_DATA_PATH',
'description' => 'Where to store main data. (config, db).',
'type' => 'string',
],
getEnvSpec('WS_DATA_PATH'),
'It should return correct env spec.'
);
$this->assertSame([], getEnvSpec('not_set'), 'It should return empty array when env is not set.');
}
public function test_isTaskWorkerRunning()
{
$_ENV['IN_CONTAINER'] = false;
$d = isTaskWorkerRunning();
$this->assertTrue($d['status'], 'When not in container, and $ignoreContainer is false, it should return true.');
unset($_ENV['IN_CONTAINER']);
$_ENV['DISABLE_CRON'] = true;
$d = isTaskWorkerRunning(ignoreContainer: true);
$this->assertFalse($d['status'], 'When DISABLE_CRON is set, it should return false.');
unset($_ENV['DISABLE_CRON']);
$d = isTaskWorkerRunning(pidFile: __DIR__ . '/../Fixtures/worker.pid', ignoreContainer: true);
$this->assertFalse($d['status'], 'When pid file is not found, it should return false.');
$tmpFile = tempnam(sys_get_temp_dir(), 'worker');
try {
file_put_contents($tmpFile, getmypid());
$d = isTaskWorkerRunning(pidFile: $tmpFile, ignoreContainer: true);
$this->assertTrue($d['status'], 'When pid file is found, and process exists it should return true.');
} finally {
if (file_exists($tmpFile)) {
unlink($tmpFile);
}
}
$tmpFile = tempnam(sys_get_temp_dir(), 'worker');
try {
/** @noinspection PhpUnhandledExceptionInspection */
file_put_contents($tmpFile, random_int(1, 9999) . getmypid());
$d = isTaskWorkerRunning(pidFile: $tmpFile, ignoreContainer: true);
$this->assertFalse(
$d['status'],
'When pid file is found, and process does not exists it should return false.'
);
} finally {
if (file_exists($tmpFile)) {
unlink($tmpFile);
}
}
}
public function test_findSideCarFiles()
{
$n = new \SplFileInfo(__DIR__ . '/../Fixtures/local_data/test.mkv');
$this->assertCount(
4,
findSideCarFiles($n),
'It should return side car files for given file.'
);
}
public function test_array_change_key_case_recursive()
{
$array = [
'foo' => 'bar',
'baz' => 'taz',
'kaz' => [
'raz' => 'maz',
'naz' => 'laz',
],
];
$expected = [
'FOO' => 'bar',
'BAZ' => 'taz',
'KAZ' => [
'RAZ' => 'maz',
'NAZ' => 'laz',
],
];
$this->assertSame(
$expected,
array_change_key_case_recursive($array, CASE_UPPER),
'It should change keys case.'
);
$this->assertSame(
$array,
array_change_key_case_recursive($expected, CASE_LOWER),
'It should change keys case.'
);
$this->expectException(RuntimeException::class);
array_change_key_case_recursive($array, 999);
}
public function test_getMimeType()
{
$this->assertSame(
'application/json',
getMimeType(__DIR__ . '/../Fixtures/plex_data.json'),
'It should return correct mime type.'
);
}
public function test_getExtension()
{
$this->assertSame(
'json',
getExtension(__DIR__ . '/../Fixtures/plex_data.json'),
'It should return correct extension.'
);
}
public function test_generateUUID()
{
#1ef6d04c-23c3-6442-9fd5-c87f54c3d8d1
$this->assertMatchesRegularExpression(
'/^[0-9a-f]{8}-[0-9a-f]{4}-6[0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/',
generateUUID(),
'It should match valid UUID6 pattern.'
);
$this->assertMatchesRegularExpression(
'/^test\-[0-9a-f]{8}-[0-9a-f]{4}-6[0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/',
generateUUID('test'),
'It should match valid UUID6 pattern.'
);
}
public function test_cacheableItem()
{
$reflectContainer = new class() {
public function call(callable $callable, array $args = []): mixed
{
return $callable(...$args);
}
};
$item = fn() => cacheableItem(
key: 'test',
function: fn() => 'foo',
ignoreCache: false,
opts: [
CacheInterface::class => $this->cache,
ReflectionContainer::class => $reflectContainer,
]
);
$this->assertSame('foo', $item(), 'It should return correct value.');
$this->assertSame('foo', $item(), 'It should return correct value.');
}
public function test_getPagination()
{
$factory = new Psr17Factory();
$creator = new ServerRequestCreator($factory, $factory, $factory, $factory);
$request = $creator->fromArrays([
'REQUEST_METHOD' => 'GET',
'QUERY_STRING' => 'page=2&perpage=10'
], get: ['page' => 2, 'perpage' => 10]);
[$page, $perpage, $start] = getPagination($request, 1);
$this->assertSame(2, $page, 'It should return correct page number.');
$this->assertSame(10, $perpage, 'It should return correct perpage number.');
$this->assertSame(10, $start, 'It should return correct start number.');
}
public function test_getBackend()
{
Container::init();
Config::init(require __DIR__ . '/../../config/config.php');
foreach ((array)require __DIR__ . '/../../config/services.php' as $name => $definition) {
Container::add($name, $definition);
}
Config::save('backends_file', __DIR__ . '/../Fixtures/test_servers.yaml');
$this->assertInstanceOf(
PlexClient::class,
getBackend('test_plex'),
'It should return correct backend client.'
);
$this->expectException(RuntimeException::class);
getBackend('not_set');
}
public function test_makeBackend()
{
Container::init();
Config::init(require __DIR__ . '/../../config/config.php');
foreach ((array)require __DIR__ . '/../../config/services.php' as $name => $definition) {
Container::add($name, $definition);
}
Config::save('backends_file', __DIR__ . '/../Fixtures/test_servers.yaml');
$exception = null;
try {
makeBackend([], 'foo');
} catch (\Throwable $e) {
$exception = $e;
} finally {
$this->assertInstanceOf(
InvalidArgumentException::class,
$exception,
'Should throw exception when no type is given.'
);
$this->assertStringContainsString('No backend type was set.', $exception?->getMessage());
}
$exception = null;
try {
makeBackend(['type' => 'plex'], 'foo');
} catch (\Throwable $e) {
$exception = $e;
} finally {
$this->assertInstanceOf(
InvalidArgumentException::class,
$exception,
'Should throw exception when no url is given.'
);
$this->assertStringContainsString('No backend url was set.', $exception?->getMessage());
}
$exception = null;
try {
makeBackend(['type' => 'far', 'url' => 'http://test.example.invalid'], 'foo');
} catch (\Throwable $e) {
$exception = $e;
} finally {
$this->assertInstanceOf(
InvalidArgumentException::class,
$exception,
'Should throw exception when no type is not supported.'
);
$this->assertStringContainsString('Unexpected client type', $exception?->getMessage());
}
$data = Yaml::parseFile(__DIR__ . '/../Fixtures/test_servers.yaml');
$this->assertInstanceOf(
PlexClient::class,
makeBackend($data['test_plex'], 'test_plex'),
'It should return correct backend client.'
);
}
public function test_lw()
{
$exception = new RuntimeException();
$exception->addContext('foo', 'bar');
$this->assertSame(
[AppExceptionInterface::class => ['foo' => 'bar']],
lw('test', [], $exception)['context'],
'it should return the added AppContext'
);
$this->assertSame(
['bar' => 'foo'],
lw('test', ['bar' => 'foo'], new \RuntimeException())['context'],
'If exception is not AppExceptionInterface, it should return same data.'
);
$exception = new DBLayerException();
/** @noinspection SqlResolve */
$exception->setInfo('SELECT * FROM foo WHERE id = :id', ['id' => 1], [], 122);
/** @noinspection SqlResolve */
$this->assertSame(
[
'bar' => 'foo',
DBLayer::class => [
'query' => 'SELECT * FROM foo WHERE id = :id',
'bind' => ['id' => 1],
'error' => [],
],
],
lw('test', ['bar' => 'foo'], $exception)['context'],
'If exception is not AppExceptionInterface, it should return same data.'
);
$this->assertSame(
['bar' => 'foo'],
lw('test', ['bar' => 'foo'])['context'],
'If no exception is given, it should return same data.'
);
}
public function test_commandContext()
{
$_ENV['IN_CONTAINER'] = true;
$this->assertSame(
'docker exec -ti watchstate console',
trim(commandContext()),
'It should return correct command context. When in container.'
);
unset($_ENV['IN_CONTAINER']);
$_ENV['IN_CONTAINER'] = false;
$this->assertSame(
$_SERVER['argv'][0] ?? 'php bin/console',
trim(commandContext()),
'If not in container, it should return argv[0] or defaults to php bin/console.'
);
unset($_ENV['IN_CONTAINER']);
}
}

View File

@@ -5,10 +5,11 @@ declare(strict_types=1);
namespace Tests\Mappers\Import;
use App\Libs\Database\DatabaseInterface as iDB;
use App\Libs\Database\DBLayer;
use App\Libs\Database\PDO\PDOAdapter;
use App\Libs\Entity\StateEntity;
use App\Libs\Entity\StateInterface as iState;
use App\Libs\Exceptions\DatabaseException;
use App\Libs\Exceptions\DBAdapterException;
use App\Libs\Extends\LogMessageProcessor;
use App\Libs\Guid;
use App\Libs\Mappers\ImportInterface;
@@ -51,7 +52,7 @@ abstract class AbstractTestsMapper extends TestCase
$this->logger->pushHandler($this->handler);
Guid::setLogger($this->logger);
$this->db = new PDOAdapter($this->logger, new PDO('sqlite::memory:'));
$this->db = new PDOAdapter($this->logger, new DBLayer(new PDO('sqlite::memory:')));
$this->db->migrations('up');
$this->mapper = $this->setupMapper();
@@ -529,7 +530,7 @@ abstract class AbstractTestsMapper extends TestCase
{
$testEpisode = new StateEntity($this->testEpisode);
$testEpisode->episode = 0;
$this->expectException(DatabaseException::class);
$this->expectException(DBAdapterException::class);
$this->db->commit([$testEpisode]);
}
@@ -540,7 +541,7 @@ abstract class AbstractTestsMapper extends TestCase
{
$testEpisode = new StateEntity($this->testEpisode);
$testEpisode->episode = 0;
$this->expectException(DatabaseException::class);
$this->expectException(DBAdapterException::class);
$this->db->insert($testEpisode);
}
@@ -551,7 +552,7 @@ abstract class AbstractTestsMapper extends TestCase
{
$testEpisode = new StateEntity($this->testEpisode);
$testEpisode->episode = 0;
$this->expectException(DatabaseException::class);
$this->expectException(DBAdapterException::class);
$this->db->update($testEpisode);
}

View File

@@ -2,6 +2,10 @@
declare(strict_types=1);
if (false === defined('IN_TEST_MODE')) {
define('IN_TEST_MODE', true);
}
require __DIR__ . '/../pre_init.php';
if (!file_exists(__DIR__ . '/../vendor/autoload.php')) {