Updated libs to use custom exceptions.

This commit is contained in:
Abdulmhsen B. A. A
2023-12-16 15:21:17 +03:00
parent f08011dff2
commit b2ac9a6c89
26 changed files with 555 additions and 169 deletions

3
.gitignore vendored
View File

@@ -1,4 +1,5 @@
/.idea/*
/vendor/*
/.env
.phpunit.result.cache
/.phpunit.result.cache
/.vscode

View File

@@ -6,46 +6,45 @@ declare(strict_types=1);
use App\Command;
error_reporting(E_ALL);
ini_set('error_reporting', 'On');
ini_set('display_errors', 'On');
require __DIR__ . '/../pre_init.php';
set_error_handler(function (int $number, mixed $error, mixed $file, int $line) {
/**
* Throws an exception based on an error code.
*
* @param int $number The error code.
* @param mixed $error The error message.
* @param mixed $file The file where the error occurred.
* @param int $line The line number where the error occurred.
*
* @throws ErrorException When the error code is not suppressed by error_reporting.
*/
$errorHandler = function (int $number, mixed $error, mixed $file, int $line) {
$errno = $number & error_reporting();
static $errorLevels = [
E_ERROR => 'Error',
E_WARNING => 'Warning',
E_PARSE => 'Parser Error',
E_NOTICE => 'Notice',
E_CORE_ERROR => 'Core Error',
E_CORE_WARNING => 'Core Warning',
E_COMPILE_ERROR => 'Compile Error',
E_COMPILE_WARNING => 'Compile Warning',
E_USER_ERROR => 'User Error',
E_USER_WARNING => 'User Warning',
E_USER_NOTICE => 'User notice',
E_STRICT => 'Strict Notice',
E_RECOVERABLE_ERROR => 'Recoverable Error'
];
if (0 === $errno) {
return;
}
$message = sprintf('%s: %s (%s:%d).', $errorLevels[$number] ?? (string)$number, $error, $file, $line);
fwrite(STDERR, trim($message) . PHP_EOL);
exit(501);
});
throw new ErrorException($error, $number, 1, $file, $line);
};
set_error_handler($errorHandler);
set_exception_handler(function (Throwable $e) {
$message = sprintf('%s: %s (%s:%d).', get_class($e), $e->getMessage(), $e->getFile(), $e->getLine());
fwrite(STDERR, trim($message) . PHP_EOL);
$message = strtr('{kind}: {message} ({file}:{line}).', [
'{kind}' => $e::class,
'{line}' => $e->getLine(),
'{message}' => $e->getMessage(),
'{file}' => after($e->getFile(), ROOT_PATH),
]);
fwrite(STDERR, $message . PHP_EOL);
exit(502);
});
if (!file_exists(__DIR__ . '/../vendor/autoload.php')) {
fwrite(STDERR, 'Dependencies are missing.' . PHP_EOL);
print 'Dependencies are missing please refer to https://github.com/arabcoders/watchstate/blob/master/FAQ.md';
exit(Command::FAILURE);
}
@@ -54,14 +53,16 @@ require __DIR__ . '/../vendor/autoload.php';
try {
$app = (new App\Libs\Initializer())->boot();
} catch (Throwable $e) {
$message = sprintf(
'Unhandled Exception [%s] was thrown in CLI boot context. With message [%s] in [%s:%d].',
$e::class,
$e->getMessage(),
array_reverse(explode(ROOT_PATH, $e->getFile(), 2))[0],
$e->getLine()
$message = strtr(
'CLI: Exception [{kind}] was thrown unhandled during CLI boot context. Error [{message} @ {file}:{line}].',
[
'{kind}' => $e::class,
'{line}' => $e->getLine(),
'{message}' => $e->getMessage(),
'{file}' => array_reverse(explode(ROOT_PATH, $e->getFile(), 2))[0],
]
);
fwrite(STDERR, trim($message) . PHP_EOL);
fwrite(STDERR, $message . PHP_EOL);
exit(503);
}

View File

@@ -8,6 +8,7 @@ use App\Libs\Database\DatabaseInterface as iDB;
use App\Libs\Database\PDO\PDOAdapter;
use App\Libs\Entity\StateEntity;
use App\Libs\Entity\StateInterface;
use App\Libs\Exceptions\RuntimeException;
use App\Libs\Extends\ConsoleOutput;
use App\Libs\Extends\HttpClient;
use App\Libs\Extends\LogMessageProcessor;

View File

@@ -17,23 +17,35 @@ if (!file_exists(__DIR__ . '/../vendor/autoload.php')) {
require __DIR__ . '/../vendor/autoload.php';
set_error_handler(function (int $number, mixed $error, mixed $file, int $line) {
/**
* Throws an exception based on an error code.
*
* @param int $number The error code.
* @param mixed $error The error message.
* @param mixed $file The file where the error occurred.
* @param int $line The line number where the error occurred.
*
* @throws ErrorException When the error code is not suppressed by error_reporting.
*/
$errorHandler = function (int $number, mixed $error, mixed $file, int $line) {
$errno = $number & error_reporting();
if (0 === $errno) {
return;
}
$message = trim(sprintf('%s: %s (%s:%d)', $number, $error, $file, $line));
$out = fn($message) => inContainer() ? fwrite(STDERR, $message) : syslog(LOG_ERR, $message);
$out($message);
throw new ErrorException($error, $number, 1, $file, $line);
};
exit(Command::FAILURE);
});
set_error_handler($errorHandler);
set_exception_handler(function (Throwable $e) {
$message = trim(sprintf("%s: %s (%s:%d).", get_class($e), $e->getMessage(), $e->getFile(), $e->getLine()));
$out = fn($message) => inContainer() ? fwrite(STDERR, $message) : syslog(LOG_ERR, $message);
$out($message);
$out(r(text: '{kind}: {message} ({file}:{line}).', context: [
'kind' => $e::class,
'line' => $e->getLine(),
'message' => $e->getMessage(),
'file' => after($e->getFile(), ROOT_PATH),
]));
exit(Command::FAILURE);
});
@@ -45,16 +57,17 @@ try {
$app = (new App\Libs\Initializer())->boot();
} catch (Throwable $e) {
fwrite(
STDERR,
trim(
sprintf(
'Unhandled Exception [%s] was thrown at HTTP boot context. With message [%s] in [%s:%d].',
$e::class,
$e->getMessage(),
array_reverse(explode(ROOT_PATH, $e->getFile(), 2))[0],
$e->getLine()
)
$out = fn($message) => inContainer() ? fwrite(STDERR, $message) : syslog(LOG_ERR, $message);
$out(
r(
text: 'HTTP: Exception [{kind}] was thrown unhandled during HTTP boot context. Error [{message} @ {file}:{line}].',
context: [
'kind' => $e::class,
'line' => $e->getLine(),
'message' => $e->getMessage(),
'file' => after($e->getFile(), ROOT_PATH),
]
)
);

View File

@@ -29,7 +29,7 @@ use App\Backends\Jellyfin\JellyfinClient;
use App\Libs\Config;
use App\Libs\Container;
use App\Libs\Entity\StateInterface as iState;
use App\Libs\HttpException;
use App\Libs\Exceptions\HttpException;
use App\Libs\Mappers\ImportInterface as iImport;
use App\Libs\Options;
use App\Libs\QueueRequests;

View File

@@ -27,7 +27,7 @@ use App\Backends\Jellyfin\Action\SearchQuery;
use App\Libs\Config;
use App\Libs\Container;
use App\Libs\Entity\StateInterface as iState;
use App\Libs\HttpException;
use App\Libs\Exceptions\HttpException;
use App\Libs\Mappers\ImportInterface as iImport;
use App\Libs\Options;
use App\Libs\QueueRequests;

View File

@@ -29,7 +29,7 @@ use App\Backends\Plex\Action\SearchQuery;
use App\Libs\Config;
use App\Libs\Container;
use App\Libs\Entity\StateInterface as iState;
use App\Libs\HttpException;
use App\Libs\Exceptions\HttpException;
use App\Libs\Mappers\ImportInterface as iImport;
use App\Libs\Options;
use App\Libs\QueueRequests;

View File

@@ -6,9 +6,9 @@ namespace App;
use App\Backends\Common\ClientInterface as iClient;
use App\Libs\Config;
use App\Libs\Exceptions\RuntimeException;
use Closure;
use DirectoryIterator;
use RuntimeException;
use Symfony\Component\Console\Command\Command as BaseCommand;
use Symfony\Component\Console\Command\LockableTrait;
use Symfony\Component\Console\Completion\CompletionInput;

View File

@@ -14,8 +14,8 @@ use App\Libs\Message;
use App\Libs\Options;
use App\Libs\QueueRequests;
use App\Libs\Routable;
use App\Libs\Stream;
use Monolog\Logger;
use Nyholm\Psr7\Stream;
use Psr\Log\LoggerInterface as iLogger;
use RuntimeException;
use Symfony\Component\Console\Input\InputInterface;

View File

@@ -17,8 +17,8 @@ use App\Libs\Mappers\ImportInterface as iImport;
use App\Libs\Message;
use App\Libs\Options;
use App\Libs\Routable;
use App\Libs\Stream;
use Monolog\Logger;
use Nyholm\Psr7\Stream;
use Psr\Log\LoggerInterface as iLogger;
use RuntimeException;
use Symfony\Component\Console\Helper\Table;

View File

@@ -4,9 +4,9 @@ declare(strict_types=1);
namespace App\Libs;
use App\Libs\Exceptions\RuntimeException;
use App\Libs\Extends\PSRContainer as BaseContainer;
use League\Container\ReflectionContainer;
use RuntimeException;
/**
* Container class provides a dependency injection container implementation.

View File

@@ -0,0 +1,12 @@
<?php
declare(strict_types=1);
namespace App\Libs\Exceptions;
/**
* Class ErrorException
*/
class ErrorException extends \ErrorException
{
}

View File

@@ -2,9 +2,7 @@
declare(strict_types=1);
namespace App\Libs;
use RuntimeException;
namespace App\Libs\Exceptions;
class HttpException extends RuntimeException
{

View File

@@ -0,0 +1,12 @@
<?php
declare(strict_types=1);
namespace App\Libs\Exceptions;
/**
* Class InvalidArgumentException
*/
class InvalidArgumentException extends \InvalidArgumentException
{
}

View File

@@ -5,7 +5,7 @@ namespace App\Libs\Extends;
use App\Libs\Config;
use DateTimeInterface;
use Monolog\LogRecord;
use Nyholm\Psr7\Stream;
use Psr\Http\Message\StreamInterface;
use Symfony\Component\Console\Output\OutputInterface;
/**
@@ -17,7 +17,7 @@ class StreamLogHandler extends ConsoleHandler
/**
* Constructor method for the class.
*
* @param Stream $stream The Stream object used for logging.
* @param StreamInterface $stream The Stream object used for logging.
* @param OutputInterface|null $output (optional) The OutputInterface object to handle the output. Default is null.
* @param bool $bubble (optional) Flag to determine if the log messages should bubble up the logging hierarchy. Default is true.
* @param array $levelsMapper (optional) An array that maps log levels to specific handlers. Default is an empty array.
@@ -25,7 +25,7 @@ class StreamLogHandler extends ConsoleHandler
* @return void
*/
public function __construct(
private Stream $stream,
private StreamInterface $stream,
OutputInterface|null $output = null,
bool $bubble = true,
array $levelsMapper = []

View File

@@ -4,10 +4,9 @@ declare(strict_types=1);
namespace App\Libs;
use InvalidArgumentException;
use App\Libs\Exceptions\InvalidArgumentException;
use JsonSerializable;
use Psr\Log\LoggerInterface;
use RuntimeException;
use Stringable;
/**
@@ -17,7 +16,8 @@ use Stringable;
* retrieve the supported external id sources, and obtain the pointers linking the
* entity to the external ids.
*
* @implements JsonSerializable, Stringable
* @implements JsonSerializable
* @implements Stringable
*/
final class Guid implements JsonSerializable, Stringable
{
@@ -210,8 +210,7 @@ final class Guid implements JsonSerializable, Stringable
*
* @return bool
*
* @throws RuntimeException When source db not supported.
* @throws InvalidArgumentException When id validation fails.
* @throws InvalidArgumentException if the db source is not supported or the value validation fails.
*/
public static function validate(string $db, string|int $id): bool
{
@@ -220,7 +219,7 @@ final class Guid implements JsonSerializable, Stringable
$lookup = 'guid_' . $db;
if (false === array_key_exists($lookup, self::SUPPORTED)) {
throw new RuntimeException(
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))),
@@ -232,7 +231,7 @@ final class Guid implements JsonSerializable, Stringable
return true;
}
if (1 !== preg_match(self::VALIDATE_GUID[$lookup]['pattern'], $id)) {
if (1 !== @preg_match(self::VALIDATE_GUID[$lookup]['pattern'], $id)) {
throw new InvalidArgumentException(
r('Invalid [{value}] value for [{db}]. Expecting [{example}].', [
'db' => $db,

View File

@@ -6,6 +6,9 @@ namespace App\Libs;
use App\Cli;
use App\Libs\Entity\StateInterface as iState;
use App\Libs\Exceptions\Backends\RuntimeException;
use App\Libs\Exceptions\HttpException;
use App\Libs\Exceptions\InvalidArgumentException;
use App\Libs\Extends\ConsoleHandler;
use App\Libs\Extends\ConsoleOutput;
use Closure;
@@ -24,8 +27,6 @@ use Psr\Http\Message\ResponseInterface;
use Psr\Http\Message\ServerRequestInterface as iRequest;
use Psr\Log\LoggerInterface;
use Psr\SimpleCache\CacheInterface;
use Psr\SimpleCache\InvalidArgumentException;
use RuntimeException;
use Symfony\Component\Console\CommandLoader\ContainerCommandLoader;
use Symfony\Component\Dotenv\Dotenv;
use Symfony\Component\Yaml\Yaml;
@@ -129,7 +130,6 @@ final class Initializer
if (!(error_reporting() & $severity)) {
return;
}
/** @noinspection PhpUnhandledExceptionInspection */
throw new ErrorException($message, 0, $severity, $file, $line);
}
);
@@ -222,7 +222,8 @@ final class Initializer
* @param iRequest $realRequest The incoming HTTP request.
*
* @return ResponseInterface The HTTP response.
* @throws InvalidArgumentException If an error occurs.
*
* @throws \Psr\SimpleCache\InvalidArgumentException If an error occurs.
*/
private function defaultHttpServer(iRequest $realRequest): ResponseInterface
{
@@ -262,7 +263,7 @@ final class Initializer
try {
$class = makeBackend($info, $name);
} catch (RuntimeException $e) {
} catch (InvalidArgumentException $e) {
$this->write(
request: $request,
level: Level::Error,

View File

@@ -15,8 +15,7 @@ use DateTimeInterface as iDate;
use Exception;
use PDOException;
use Psr\Log\LoggerInterface as iLogger;
use Psr\SimpleCache\CacheInterface;
use Psr\SimpleCache\InvalidArgumentException;
use Psr\SimpleCache\CacheInterface as iCache;
/**
* DirectMapper Class.
@@ -73,9 +72,9 @@ final class DirectMapper implements iImport
*
* @param iLogger $logger The logger instance.
* @param iDB $db The database instance.
* @param CacheInterface $cache The cache instance.
* @param iCache $cache The cache instance.
*/
public function __construct(protected iLogger $logger, protected iDB $db, protected CacheInterface $cache)
public function __construct(protected iLogger $logger, protected iDB $db, protected iCache $cache)
{
}
@@ -632,7 +631,7 @@ final class DirectMapper implements iImport
$progress[$itemId] = $entity;
}
$this->cache->set('progress', $progress, new DateInterval('P1D'));
} catch (InvalidArgumentException) {
} catch (\Psr\SimpleCache\InvalidArgumentException) {
}
}

View File

@@ -13,8 +13,7 @@ use DateInterval;
use DateTimeInterface as iDate;
use PDOException;
use Psr\Log\LoggerInterface as iLogger;
use Psr\SimpleCache\CacheInterface;
use Psr\SimpleCache\InvalidArgumentException;
use Psr\SimpleCache\CacheInterface as iCache;
/**
* MemoryMapper Class
@@ -63,9 +62,9 @@ final class MemoryMapper implements iImport
*
* @param iLogger $logger The instance of the logger interface.
* @param iDB $db The instance of the database interface.
* @param CacheInterface $cache The instance of the cache interface.
* @param iCache $cache The instance of the cache interface.
*/
public function __construct(protected iLogger $logger, protected iDB $db, protected CacheInterface $cache)
public function __construct(protected iLogger $logger, protected iDB $db, protected iCache $cache)
{
}
@@ -455,7 +454,7 @@ final class MemoryMapper implements iImport
$progress[$itemId] = $entity;
}
$this->cache->set('progress', $progress, new DateInterval('P1D'));
} catch (InvalidArgumentException) {
} catch (\Psr\SimpleCache\InvalidArgumentException) {
}
}
}

View File

@@ -13,16 +13,14 @@ use Attribute;
* The attribute can be repeated, and it can target a class.
*/
#[Attribute(Attribute::TARGET_CLASS | Attribute::IS_REPEATABLE)]
final class Routable
final readonly class Routable
{
/**
* Class constructor.
*
* @param string $command The command string.
*
* @return void
*/
public function __construct(public readonly string $command)
public function __construct(public string $command)
{
}
}

View File

@@ -4,27 +4,30 @@ declare(strict_types=1);
namespace App\Libs;
use App\Libs\Exceptions\RuntimeException;
use FilesystemIterator;
use PhpToken;
use RecursiveDirectoryIterator;
use RecursiveIteratorIterator;
use ReflectionAttribute;
use ReflectionClass;
use RuntimeException;
use SplFileInfo;
use Throwable;
/**
* Router class handles the generation of routes based on scanned directories and class attributes.
* Class Router
*
* The Router class is responsible for generating an array of routes by scanning directories.
* It parses PHP files to extract namespaces and classes, and retrieves routes using reflection.
*/
final class Router
final readonly class Router
{
/**
* Class constructor.
* Class Constructor.
*
* @param array $dirs An optional array of directories.
* @param array $dirs An array containing directory names.
*/
public function __construct(private readonly array $dirs = [])
public function __construct(private array $dirs)
{
}
@@ -132,14 +135,7 @@ final class Router
$classes = [];
$namespace = '';
if (false === ($content = @file_get_contents($file))) {
throw new RuntimeException(r("Unable to read '{file}' - '{message}'.", [
'file' => $file,
'message' => error_get_last()['message'] ?? 'unknown',
]));
}
$tokens = PhpToken::tokenize($content);
$tokens = PhpToken::tokenize((string)(new Stream($file, 'r')));
$count = count($tokens);
foreach ($tokens as $i => $iValue) {

View File

@@ -4,8 +4,8 @@ declare(strict_types=1);
namespace App\Libs;
use App\Libs\Exceptions\RuntimeException;
use Closure;
use RuntimeException;
use Symfony\Component\Process\PhpExecutableFinder;
use Symfony\Component\Process\Process;

375
src/Libs/Stream.php Normal file
View File

@@ -0,0 +1,375 @@
<?php
declare(strict_types=1);
namespace App\Libs;
use App\Libs\Exceptions\InvalidArgumentException;
use App\Libs\Exceptions\RuntimeException;
use GdImage;
use Psr\Http\Message\StreamInterface;
use Stringable;
use Throwable;
/**
* Class Stream
*
* The Stream class represents a stream resource or file path.
*
* @implements StreamInterface
*/
final class Stream implements StreamInterface, Stringable
{
/**
* @var array<string> A list of allowed stream resource types that are allowed to instantiate a stream
*/
private const ALLOWED_STREAM_RESOURCE_TYPES = ['gd', 'stream'];
/**
* @var resource|null The underlying stream resource.
*/
protected $resource;
/**
* @param string|resource $stream The stream resource or file path.
* @param string $mode The stream mode. Default is 'r'.
*
* @throws RuntimeException If an invalid stream reference is provided.
* @throws InvalidArgumentException If the stream type is unexpected.
*/
public function __construct(mixed $stream, string $mode = 'r')
{
$error = null;
$resource = $stream;
if (is_string($stream)) {
set_error_handler(function ($e) use (&$error) {
if ($e !== E_WARNING) {
return;
}
$error = $e;
});
$resource = fopen($stream, $mode);
restore_error_handler();
}
if ($error) {
throw new RuntimeException(r('Stream: Invalid stream reference provided. Error {error}.', [
'error' => ag(error_get_last(), 'message', '??'),
]));
}
if (!self::isValidStreamResourceType($resource)) {
throw new InvalidArgumentException(
r(
text: 'Stream: Unexpected [{type}] type was given. Stream must be a file path or stream resource.',
context: [
'type' => gettype($resource),
]
)
);
}
$this->resource = $resource;
}
/**
* Create a new Stream instance.
*
* @param string|resource $stream The stream resource or file path.
* @param string $mode The stream mode. Default is 'r'.
*
* @return StreamInterface The new Stream instance.
*
* @throws RuntimeException If an invalid stream reference is provided.
* @throws InvalidArgumentException If the stream type is unexpected.
*/
public static function make(mixed $stream, string $mode = 'r'): StreamInterface
{
return new self($stream, $mode);
}
/**
* Create in-memory stream with given contents.
*
* @param string|resource|StreamInterface $body The stream contents.
*
* @throws InvalidArgumentException If the $body arg is not a string, resource or StreamInterface.
*/
public static function create(mixed $body = ''): StreamInterface
{
if ($body instanceof StreamInterface) {
return $body;
}
if (is_string($body)) {
$resource = \fopen('php://memory', 'r+');
fwrite($resource, $body);
fseek($resource, 0);
return new self($resource);
}
if (!self::isValidStreamResourceType($body)) {
throw new InvalidArgumentException(
'First argument to Stream::create() must be a string, resource or StreamInterface'
);
}
return new self($body);
}
/**
* {@inheritdoc}
*/
public function __toString(): string
{
if (!$this->isReadable()) {
return '';
}
try {
if ($this->isSeekable()) {
$this->rewind();
}
return $this->getContents();
} catch (Throwable) {
return '';
}
}
/**
* {@inheritdoc}
*/
public function close(): void
{
if (!$this->resource) {
return;
}
$resource = $this->detach();
fclose($resource);
}
/**
* {@inheritdoc}
*/
public function detach()
{
$resource = $this->resource;
$this->resource = null;
return $resource;
}
/**
* {@inheritdoc}
*/
public function getSize(): ?int
{
if (null === $this->resource) {
return null;
}
$stats = fstat($this->resource);
if (false !== $stats) {
return $stats['size'];
}
return null;
}
/**
* {@inheritdoc}
*/
public function tell(): int
{
if (!$this->resource) {
throw new RuntimeException('Stream: No resource available; cannot tell position');
}
$result = ftell($this->resource);
if (!is_int($result)) {
throw new RuntimeException('Stream: Error occurred during tell operation.');
}
return $result;
}
/**
* {@inheritdoc}
*/
public function eof(): bool
{
if (!$this->resource) {
return true;
}
return feof($this->resource);
}
/**
* {@inheritdoc}
*/
public function isSeekable(): bool
{
if (!$this->resource) {
return false;
}
$meta = stream_get_meta_data($this->resource);
return $meta['seekable'];
}
/**
* {@inheritdoc}
*/
public function seek($offset, $whence = SEEK_SET): void
{
if (!$this->resource) {
throw new RuntimeException('Stream: No resource available; cannot seek position');
}
if (!$this->isSeekable()) {
throw new RuntimeException('Stream: Stream is not seekable');
}
$result = fseek($this->resource, $offset, $whence);
if (0 !== $result) {
throw new RuntimeException('Stream: Error seeking within stream');
}
}
/**
* {@inheritdoc}
*/
public function rewind(): void
{
$this->seek(0);
}
/**
* {@inheritdoc}
*/
public function isWritable(): bool
{
if (!$this->resource) {
return false;
}
$meta = stream_get_meta_data($this->resource);
$mode = $meta['mode'];
return (str_contains($mode, 'x') || str_contains($mode, 'w') ||
str_contains($mode, 'c') || str_contains($mode, 'a') || str_contains($mode, '+'));
}
/**
* {@inheritdoc}
*/
public function write(string $string): int
{
if (!$this->resource) {
throw new RuntimeException('Stream: No resource available; cannot write.');
}
if (!$this->isWritable()) {
throw new RuntimeException('Stream: Stream is not writable.');
}
$result = fwrite($this->resource, $string);
if (false === $result) {
throw new RuntimeException('Stream: Error writing to stream.');
}
return $result;
}
/**
* {@inheritdoc}
*/
public function isReadable(): bool
{
if (!$this->resource) {
return false;
}
$meta = stream_get_meta_data($this->resource);
$mode = $meta['mode'];
return str_contains($mode, 'r') || str_contains($mode, '+');
}
/**
* {@inheritdoc}
*/
public function read(int $length): string
{
if (!$this->resource) {
throw new RuntimeException('Stream: No resource available; cannot read');
}
if (!$this->isReadable()) {
throw new RuntimeException('Stream: Stream is not readable');
}
$result = fread($this->resource, $length);
if (false === $result) {
throw new RuntimeException('Stream: Error reading stream');
}
return $result;
}
/**
* {@inheritdoc}
*/
public function getContents(): string
{
if (!$this->isReadable()) {
throw new RuntimeException('Stream: Stream is not readable.');
}
$result = stream_get_contents($this->resource);
if (false === $result) {
throw new RuntimeException('Stream: Error reading stream');
}
return $result;
}
/**
* {@inheritdoc}
*/
public function getMetadata(string|null $key = null)
{
$metadata = stream_get_meta_data($this->resource);
return null !== $key ? ($metadata[$key] ?? null) : $metadata;
}
/**
* Determine if a resource is one of the resource types allowed to instantiate a Stream
*
* @param resource $resource Stream resource.
*
* @return bool True if the resource is one of the allowed types, false otherwise.
*/
private static function isValidStreamResourceType($resource): bool
{
if (is_resource($resource)) {
return in_array(get_resource_type($resource), self::ALLOWED_STREAM_RESOURCE_TYPES, true);
}
if (PHP_VERSION_ID >= 80000 && $resource instanceof GdImage) {
return true;
}
return false;
}
}

View File

@@ -4,6 +4,7 @@ declare(strict_types=1);
namespace App\Libs;
use App\Libs\Exceptions\InvalidArgumentException;
use Psr\Http\Message\UriInterface;
use Stringable;
@@ -154,7 +155,7 @@ final class Uri implements UriInterface, Stringable
public function withScheme($scheme): self
{
if (!\is_string($scheme)) {
throw new \InvalidArgumentException('Scheme must be a string');
throw new InvalidArgumentException('Scheme must be a string');
}
if ($this->scheme === $scheme = \strtr($scheme, 'ABCDEFGHIJKLMNOPQRSTUVWXYZ', 'abcdefghijklmnopqrstuvwxyz')) {
@@ -188,7 +189,7 @@ final class Uri implements UriInterface, Stringable
public function withHost($host): self
{
if (!\is_string($host)) {
throw new \InvalidArgumentException('Host must be a string');
throw new InvalidArgumentException('Host must be a string');
}
if ($this->host === $host = \strtr($host, 'ABCDEFGHIJKLMNOPQRSTUVWXYZ', 'abcdefghijklmnopqrstuvwxyz')) {
@@ -312,7 +313,7 @@ final class Uri implements UriInterface, Stringable
$port = (int)$port;
if (0 > $port || 0xFFFF < $port) {
throw new \InvalidArgumentException(\sprintf('Invalid port: %d. Must be between 0 and 65535', $port));
throw new InvalidArgumentException(\sprintf('Invalid port: %d. Must be between 0 and 65535', $port));
}
return self::isNonStandardPort($this->scheme, $port) ? $port : null;
@@ -321,7 +322,7 @@ final class Uri implements UriInterface, Stringable
private function filterPath($path): string
{
if (!is_string($path)) {
throw new \InvalidArgumentException('Path must be a string');
throw new InvalidArgumentException('Path must be a string');
}
return preg_replace_callback(
@@ -334,7 +335,7 @@ final class Uri implements UriInterface, Stringable
private function filterQueryAndFragment($str): string
{
if (!is_string($str)) {
throw new \InvalidArgumentException('Query and fragment must be a string');
throw new InvalidArgumentException('Query and fragment must be a string');
}
return preg_replace_callback(

View File

@@ -8,15 +8,18 @@ use App\Backends\Common\Context;
use App\Libs\Config;
use App\Libs\Container;
use App\Libs\Entity\StateInterface as iFace;
use App\Libs\Exceptions\InvalidArgumentException;
use App\Libs\Exceptions\RuntimeException;
use App\Libs\Extends\Date;
use App\Libs\Options;
use App\Libs\Router;
use App\Libs\Stream;
use App\Libs\Uri;
use Monolog\Utils;
use Nyholm\Psr7\Response;
use Nyholm\Psr7\Stream;
use Psr\Http\Message\ResponseInterface;
use Psr\Http\Message\ServerRequestInterface;
use Psr\Http\Message\StreamInterface;
use Psr\Http\Message\UriInterface;
use Psr\Log\LoggerInterface;
use Psr\SimpleCache\CacheInterface;
@@ -300,9 +303,9 @@ if (!function_exists('saveWebhookPayload')) {
*
* @param iFace $entity Entity object.
* @param ServerRequestInterface $request Request object.
* @param Stream|null $file When given a stream, it will be used to write payload.
* @param StreamInterface|null $file When given a stream, it will be used to write payload.
*/
function saveWebhookPayload(iFace $entity, ServerRequestInterface $request, Stream|null $file = null): void
function saveWebhookPayload(iFace $entity, ServerRequestInterface $request, StreamInterface|null $file = null): void
{
$content = [
'request' => [
@@ -315,9 +318,7 @@ if (!function_exists('saveWebhookPayload')) {
'entity' => $entity->getAll(),
];
$closeStream = false;
if (null === $file) {
$fp = @fopen(
$stream = $file ?? new Stream(
r('{path}/webhooks/' . Config::get('webhook.file_format', 'webhook.{backend}.{event}.{id}.json'), [
'path' => Config::get('tmpDir'),
'time' => (string)time(),
@@ -326,27 +327,18 @@ if (!function_exists('saveWebhookPayload')) {
'id' => ag($request->getServerParams(), 'X_REQUEST_ID', time()),
'date' => makeDate('now')->format('Ymd'),
'context' => $content,
]),
'w'
]), 'w'
);
if (false === $fp) {
throw new Error(ag(error_get_last(), 'message', ''));
}
$file = new Stream($fp);
$closeStream = true;
}
$file->write(
$stream->write(
json_encode(
value: $content,
flags: JSON_PRETTY_PRINT | JSON_UNESCAPED_SLASHES | JSON_INVALID_UTF8_IGNORE | JSON_UNESCAPED_UNICODE
)
);
if ($closeStream) {
$file->close();
if (null === $file) {
$stream->close();
}
}
}
@@ -356,9 +348,9 @@ if (!function_exists('saveRequestPayload')) {
* Save request payload to stream.
*
* @param ServerRequestInterface $request Request object.
* @param Stream|null $file When given a stream, it will be used to write payload.
* @param StreamInterface|null $file When given a stream, it will be used to write payload.
*/
function saveRequestPayload(ServerRequestInterface $request, Stream|null $file = null): void
function saveRequestPayload(ServerRequestInterface $request, StreamInterface|null $file = null): void
{
$content = [
'query' => $request->getQueryParams(),
@@ -368,33 +360,20 @@ if (!function_exists('saveRequestPayload')) {
'attributes' => $request->getAttributes(),
];
$closeStream = false;
if (null === $file) {
$fp = @fopen(
r('{path}/debug/request.{id}.json', [
$stream = $file ?? new Stream(r('{path}/debug/request.{id}.json', [
'path' => Config::get('tmpDir'),
'id' => ag($request->getServerParams(), 'X_REQUEST_ID', (string)time()),
]),
'w'
);
]), 'w');
if (false === $fp) {
throw new Error(ag(error_get_last(), 'message', ''));
}
$file = new Stream($fp);
$closeStream = true;
}
$file->write(
$stream->write(
json_encode(
value: $content,
flags: JSON_PRETTY_PRINT | JSON_UNESCAPED_SLASHES | JSON_INVALID_UTF8_IGNORE | JSON_UNESCAPED_UNICODE
)
);
if ($closeStream) {
$file->close();
if (null === $file) {
$stream->close();
}
}
}
@@ -554,20 +533,20 @@ if (!function_exists('makeBackend')) {
* @param string|null $name server name.
*
* @return iClient backend client instance.
* @throws RuntimeException if configuration is wrong.
* @throws InvalidArgumentException if configuration is wrong.
*/
function makeBackend(array $backend, string|null $name = null): iClient
{
if (null === ($backendType = ag($backend, 'type'))) {
throw new RuntimeException('No backend type was set.');
throw new InvalidArgumentException('No backend type was set.');
}
if (null === ag($backend, 'url')) {
throw new RuntimeException('No backend url was set.');
throw new InvalidArgumentException('No backend url was set.');
}
if (null === ($class = Config::get("supported.{$backendType}", null))) {
throw new RuntimeException(
throw new InvalidArgumentException(
r('Unexpected client type [{type}] was given. Expecting [{list}]', [
'type' => $backendType,
'list' => array_keys(Config::get('supported', [])),
@@ -645,7 +624,7 @@ if (!function_exists('commandContext')) {
]);
}
return ($_SERVER['argv'][0] ?? 'php console') . ' ';
return ($_SERVER['argv'][0] ?? 'php bin/console') . ' ';
}
}
@@ -801,7 +780,7 @@ if (false === function_exists('isIgnoredId')) {
* @param int|string|null $backendId The backend ID (optional).
*
* @return bool Returns true if the ID is ignored, false otherwise.
* @throws RuntimeException Throws an exception if an invalid context type is given.
* @throws InvalidArgumentException Throws an exception if an invalid context type is given.
*/
function isIgnoredId(
string $backend,
@@ -811,7 +790,7 @@ if (false === function_exists('isIgnoredId')) {
string|int|null $backendId = null
): bool {
if (false === in_array($type, iFace::TYPES_LIST)) {
throw new RuntimeException(sprintf('Invalid context type \'%s\' was given.', $type));
throw new InvalidArgumentException(sprintf('Invalid context type \'%s\' was given.', $type));
}
$list = Config::get('ignore', []);

View File

@@ -6,6 +6,7 @@ namespace Tests\Libs;
use App\Libs\Config;
use App\Libs\Entity\StateEntity;
use App\Libs\Exceptions\RuntimeException;
use App\Libs\TestCase;
use JsonMachine\Items;
use JsonMachine\JsonDecoder\ErrorWrappingDecoder;
@@ -273,7 +274,7 @@ class HelpersTest extends TestCase
'saveWebhookPayload() should save webhook payload into given stream if it is provided otherwise it should save it into default stream.'
);
$this->expectException(\Error::class);
$this->expectException(RuntimeException::class);
saveWebhookPayload(entity: $entity, request: $request);
}
@@ -311,7 +312,7 @@ class HelpersTest extends TestCase
$this->assertSame($request->getAttributes(), $fromFile->getAttributes());
$this->assertSame($request->getParsedBody(), $fromFile->getParsedBody());
$this->expectException(\Error::class);
$this->expectException(RuntimeException::class);
saveRequestPayload(request: $request);
}