Version 5.1 - All the GraphQL #32

Closed
timw4mail wants to merge 1160 commits from develop into master
24 changed files with 78 additions and 127 deletions
Showing only changes of commit 5242b95a0e - Show all commits

View File

@ -283,7 +283,7 @@ final class Model
if ($list === NULL) if ($list === NULL)
{ {
$data = $this->getList(MediaType::ANIME, $status) ?? []; $data = $this->getList(MediaType::ANIME, $status);
// Bail out on no data // Bail out on no data
if (empty($data)) if (empty($data))
@ -320,7 +320,7 @@ final class Model
/** /**
* Get all the anime entries, that are organized for output to html * Get all the anime entries, that are organized for output to html
* *
* @return array<string, mixed[]> * @return array<string, array>
*/ */
public function getFullOrganizedAnimeList(): array public function getFullOrganizedAnimeList(): array
{ {
@ -331,7 +331,7 @@ final class Model
foreach ($statuses as $status) foreach ($statuses as $status)
{ {
$mappedStatus = AnimeWatchingStatus::KITSU_TO_TITLE[$status]; $mappedStatus = AnimeWatchingStatus::KITSU_TO_TITLE[$status];
$output[$mappedStatus] = $this->getAnimeList($status) ?? []; $output[$mappedStatus] = $this->getAnimeList($status);
} }
return $output; return $output;
@ -413,7 +413,7 @@ final class Model
if ($list === NULL) if ($list === NULL)
{ {
$data = $this->getList(MediaType::MANGA, $status) ?? []; $data = $this->getList(MediaType::MANGA, $status);
// Bail out on no data // Bail out on no data
if (empty($data)) if (empty($data))
@ -787,7 +787,7 @@ final class Model
} }
} }
private function getUserId(): string protected function getUserId(): string
{ {
static $userId = NULL; static $userId = NULL;

View File

@ -78,7 +78,7 @@ final class RequestBuilder extends APIRequestBuilder
elseif ($url !== K::AUTH_URL && $sessionSegment->get('auth_token') !== NULL) elseif ($url !== K::AUTH_URL && $sessionSegment->get('auth_token') !== NULL)
{ {
$token = $sessionSegment->get('auth_token'); $token = $sessionSegment->get('auth_token');
if ( ! (empty($token) || $cache->has(K::AUTH_TOKEN_CACHE_KEY))) if ( ! empty($token))
{ {
$cache->set(K::AUTH_TOKEN_CACHE_KEY, $token); $cache->set(K::AUTH_TOKEN_CACHE_KEY, $token);
} }
@ -239,43 +239,4 @@ final class RequestBuilder extends APIRequestBuilder
'body' => $body, 'body' => $body,
]); ]);
} }
/**
* Make a request
*/
private function request(string $type, string $url, array $options = []): array
{
$logger = $this->container->getLogger('kitsu-request');
$response = $this->getResponse($type, $url, $options);
$statusCode = $response->getStatus();
// Check for requests that are unauthorized
if ($statusCode === 401 || $statusCode === 403)
{
Event::emit(EventType::UNAUTHORIZED);
}
$rawBody = wait($response->getBody()->buffer());
// Any other type of failed request
if ($statusCode > 299 || $statusCode < 200)
{
if ($logger !== NULL)
{
$logger->warning('Non 2xx response for api call', (array) $response);
}
}
try
{
return Json::decode($rawBody);
}
catch (JsonException)
{
// dump($e);
dump($rawBody);
exit();
}
}
} }

View File

@ -23,8 +23,6 @@ trait RequestBuilderTrait
/** /**
* Set the request builder object * Set the request builder object
*
* @return ListItem|Model|RequestBuilderTrait
*/ */
public function setRequestBuilder(RequestBuilder $requestBuilder): self public function setRequestBuilder(RequestBuilder $requestBuilder): self
{ {

View File

@ -54,7 +54,7 @@ final class MangaTransformer extends AbstractTransformer
} }
$details = $rawCharacter['character']; $details = $rawCharacter['character'];
if (array_key_exists($details['id'], $characters[$type])) if (array_key_exists($details['id'], (array)$characters[$type]))
{ {
$characters[$type][$details['id']] = [ $characters[$type][$details['id']] = [
'image' => Kitsu::getImage($details), 'image' => Kitsu::getImage($details),

View File

@ -18,7 +18,6 @@ use Amp\Http\Client\{HttpClient, HttpClientBuilder, Request, Response};
use Aviat\Ion\{ConfigInterface, ImageBuilder}; use Aviat\Ion\{ConfigInterface, ImageBuilder};
use DateTimeImmutable; use DateTimeImmutable;
use PHPUnit\Framework\Attributes\CodeCoverageIgnore;
use Psr\SimpleCache\CacheInterface; use Psr\SimpleCache\CacheInterface;
use Throwable; use Throwable;
@ -40,7 +39,6 @@ const MINUTES_IN_YEAR = 525_600;
* *
* @param string $path - Path to load config * @param string $path - Path to load config
*/ */
#[CodeCoverageIgnore]
function loadConfig(string $path): array function loadConfig(string $path): array
{ {
$output = []; $output = [];
@ -80,7 +78,6 @@ function loadConfig(string $path): array
/** /**
* Load config from one specific TOML file * Load config from one specific TOML file
*/ */
#[CodeCoverageIgnore]
function loadTomlFile(string $filename): array function loadTomlFile(string $filename): array
{ {
return Toml::parseFile($filename); return Toml::parseFile($filename);
@ -250,7 +247,6 @@ function getLocalImg(string $kitsuUrl, bool $webp = TRUE): string
/** /**
* Create a transparent placeholder image * Create a transparent placeholder image
*/ */
#[CodeCoverageIgnore]
function createPlaceholderImage(string $path, int $width = 200, int $height = 200, string $text = 'Image Unavailable'): bool function createPlaceholderImage(string $path, int $width = 200, int $height = 200, string $text = 'Image Unavailable'): bool
{ {
$img = ImageBuilder::new($width, $height) $img = ImageBuilder::new($width, $height)
@ -303,7 +299,6 @@ function clearCache(CacheInterface $cache): bool
/** /**
* Render a PHP code template as a string * Render a PHP code template as a string
*/ */
#[CodeCoverageIgnore]
function renderTemplate(string $path, array $data): string function renderTemplate(string $path, array $data): string
{ {
ob_start(); ob_start();

View File

@ -306,8 +306,6 @@ final class Anime extends BaseController
'Anime not found', 'Anime not found',
'Anime Not Found' 'Anime Not Found'
); );
return;
} }
$this->outputHTML('anime/details', [ $this->outputHTML('anime/details', [
@ -345,8 +343,6 @@ final class Anime extends BaseController
'Anime not found', 'Anime not found',
'Anime Not Found' 'Anime Not Found'
); );
return;
} }
$this->outputHTML('anime/details', [ $this->outputHTML('anime/details', [

View File

@ -59,8 +59,6 @@ final class Character extends BaseController
), ),
'Character Not Found' 'Character Not Found'
); );
return;
} }
$data = (new CharacterTransformer())->transform($rawData)->toArray(); $data = (new CharacterTransformer())->transform($rawData)->toArray();

View File

@ -96,7 +96,7 @@ final class Images extends BaseController
$kitsuUrl .= $imageType['kitsuUrl']; $kitsuUrl .= $imageType['kitsuUrl'];
$width = $imageType['width']; $width = $imageType['width'];
$height = $imageType['height']; $height = $imageType['height'] ?? 225;
$filePrefix = "{$baseSavePath}/{$type}/{$id}"; $filePrefix = "{$baseSavePath}/{$type}/{$id}";
$response = getResponse($kitsuUrl); $response = getResponse($kitsuUrl);
@ -120,11 +120,11 @@ final class Images extends BaseController
if ($display) if ($display)
{ {
$this->getPlaceholder("{$baseSavePath}/{$type}", $width, $height); $this->getPlaceholder("{$baseSavePath}/{$type}", $width ?? 225, $height);
} }
else else
{ {
createPlaceholderImage("{$baseSavePath}/{$type}", $width, $height); createPlaceholderImage("{$baseSavePath}/{$type}", $width ?? 225, $height);
} }
return; return;
@ -132,7 +132,13 @@ final class Images extends BaseController
$data = wait($response->getBody()->buffer()); $data = wait($response->getBody()->buffer());
[$origWidth] = getimagesizefromstring($data); $size = getimagesizefromstring($data);
if ($size === FALSE)
{
return;
}
[$origWidth] = $size;
$gdImg = imagecreatefromstring($data); $gdImg = imagecreatefromstring($data);
if ($gdImg === FALSE) if ($gdImg === FALSE)
{ {
@ -182,15 +188,15 @@ final class Images extends BaseController
/** /**
* Get a placeholder for a missing image * Get a placeholder for a missing image
*/ */
private function getPlaceholder(string $path, ?int $width = 200, ?int $height = NULL): void private function getPlaceholder(string $path, ?int $width = NULL, ?int $height = NULL): void
{ {
$height ??= $width; $height ??= $width ?? 200;
$filename = $path . '/placeholder.png'; $filename = $path . '/placeholder.png';
if ( ! file_exists($path . '/placeholder.png')) if ( ! file_exists($path . '/placeholder.png'))
{ {
createPlaceholderImage($path, $width, $height); createPlaceholderImage($path, $width ?? 200, $height);
} }
header('Content-Type: image/png'); header('Content-Type: image/png');

View File

@ -277,8 +277,6 @@ final class Manga extends BaseController
'Manga not found', 'Manga not found',
'Manga Not Found' 'Manga Not Found'
); );
return;
} }
$this->outputHTML('manga/details', [ $this->outputHTML('manga/details', [
@ -306,8 +304,6 @@ final class Manga extends BaseController
'Manga not found', 'Manga not found',
'Manga Not Found' 'Manga Not Found'
); );
return;
} }
$this->outputHTML('manga/details', [ $this->outputHTML('manga/details', [

View File

@ -101,11 +101,7 @@ final class Misc extends BaseController
} }
$this->setFlashMessage('Invalid username or password.'); $this->setFlashMessage('Invalid username or password.');
$this->redirect($this->url->generate('login'), 303);
$redirectUrl = $this->url->generate('login');
$redirectUrl = ($redirectUrl !== FALSE) ? $redirectUrl : '';
$this->redirect($redirectUrl, 303);
} }
/** /**
@ -145,8 +141,6 @@ final class Misc extends BaseController
), ),
'Character Not Found' 'Character Not Found'
); );
return;
} }
$data = (new CharacterTransformer())->transform($rawData)->toArray(); $data = (new CharacterTransformer())->transform($rawData)->toArray();
@ -178,8 +172,6 @@ final class Misc extends BaseController
), ),
'Person Not Found' 'Person Not Found'
); );
return;
} }
$this->outputHTML('person/details', [ $this->outputHTML('person/details', [

View File

@ -60,8 +60,6 @@ final class People extends BaseController
), ),
'Person Not Found' 'Person Not Found'
); );
return;
} }
$this->outputHTML('person/details', [ $this->outputHTML('person/details', [

View File

@ -86,10 +86,7 @@ final class Settings extends BaseController
? $this->setFlashMessage('Saved config settings.', 'success') ? $this->setFlashMessage('Saved config settings.', 'success')
: $this->setFlashMessage('Failed to save config file.', 'error'); : $this->setFlashMessage('Failed to save config file.', 'error');
$redirectUrl = $this->url->generate('settings'); $this->redirect($this->url->generate('settings'), 303);
$redirectUrl = ($redirectUrl !== FALSE) ? $redirectUrl : '';
$this->redirect($redirectUrl, 303);
} }
/** /**
@ -152,9 +149,6 @@ final class Settings extends BaseController
? $this->setFlashMessage('Linked Anilist Account', 'success') ? $this->setFlashMessage('Linked Anilist Account', 'success')
: $this->setFlashMessage('Error Linking Anilist Account', 'error'); : $this->setFlashMessage('Error Linking Anilist Account', 'error');
$redirectUrl = $this->url->generate('settings'); $this->redirect($this->url->generate('settings'), 303);
$redirectUrl = ($redirectUrl !== FALSE) ? $redirectUrl : '';
$this->redirect($redirectUrl, 303);
} }
} }

View File

@ -72,8 +72,6 @@ final class User extends BaseController
if ($rawData['data']['findProfileBySlug'] === NULL) if ($rawData['data']['findProfileBySlug'] === NULL)
{ {
$this->notFound('Sorry, user not found', "The user '{$username}' does not seem to exist."); $this->notFound('Sorry, user not found', "The user '{$username}' does not seem to exist.");
return;
} }
$data = (new UserTransformer())->transform($rawData)->toArray(); $data = (new UserTransformer())->transform($rawData)->toArray();

View File

@ -82,11 +82,8 @@ final class Dispatcher extends RoutingBase
{ {
$route = $this->getRoute(); $route = $this->getRoute();
if ($logger !== NULL) $logger?->info('Dispatcher - Route invoke arguments');
{ $logger?->info(print_r($route, TRUE));
$logger->info('Dispatcher - Route invoke arguments');
$logger->info(print_r($route, TRUE));
}
} }
if ( ! $route) if ( ! $route)
@ -183,10 +180,7 @@ final class Dispatcher extends RoutingBase
} }
$logger = $this->container->getLogger(); $logger = $this->container->getLogger();
if ($logger !== NULL) $logger?->info(Json::encode($params));
{
$logger->info(Json::encode($params));
}
return [ return [
'controller_name' => $controllerName, 'controller_name' => $controllerName,
@ -208,10 +202,7 @@ final class Dispatcher extends RoutingBase
$controller = reset($segments); $controller = reset($segments);
$logger = $this->container->getLogger(); $logger = $this->container->getLogger();
if ($logger !== NULL) $logger?->info('Controller: ' . $controller);
{
$logger->info('Controller: ' . $controller);
}
if (empty($controller)) if (empty($controller))
{ {
@ -224,7 +215,7 @@ final class Dispatcher extends RoutingBase
/** /**
* Get the list of controllers in the default namespace * Get the list of controllers in the default namespace
* *
* @return mixed[] * @return array
*/ */
public function getControllerList(): array public function getControllerList(): array
{ {
@ -300,7 +291,6 @@ final class Dispatcher extends RoutingBase
/** /**
* Get the appropriate params for the error page * Get the appropriate params for the error page
* passed on the failed route * passed on the failed route
* @return mixed[][]
*/ */
protected function getErrorParams(): array protected function getErrorParams(): array
{ {
@ -317,7 +307,7 @@ final class Dispatcher extends RoutingBase
$params = []; $params = [];
switch ($failure->failedRule) switch ($failure?->failedRule)
{ {
case Rule\Allows::class: case Rule\Allows::class:
$params = [ $params = [
@ -349,8 +339,6 @@ final class Dispatcher extends RoutingBase
/** /**
* Select controller based on the current url, and apply its relevant routes * Select controller based on the current url, and apply its relevant routes
*
* @return mixed[]
*/ */
protected function setupRoutes(): array protected function setupRoutes(): array
{ {

View File

@ -91,7 +91,7 @@ final class AnimeCollection extends Collection
$genres = $this->getGenreList(); $genres = $this->getGenreList();
$media = $this->getMediaList(); $media = $this->getMediaList();
if ($rows === FALSE) if (empty($rows))
{ {
return []; return [];
} }
@ -133,7 +133,7 @@ final class AnimeCollection extends Collection
->get(); ->get();
$rows = $query->fetchAll(PDO::FETCH_ASSOC); $rows = $query->fetchAll(PDO::FETCH_ASSOC);
if ($rows === FALSE) if (empty($rows))
{ {
return []; return [];
} }
@ -349,7 +349,7 @@ final class AnimeCollection extends Collection
->get() ->get()
->fetchAll(PDO::FETCH_ASSOC); ->fetchAll(PDO::FETCH_ASSOC);
if ($mediaRows === FALSE) if (empty($mediaRows))
{ {
return []; return [];
} }
@ -411,7 +411,7 @@ final class AnimeCollection extends Collection
->get(); ->get();
$rows = $query->fetchAll(PDO::FETCH_ASSOC); $rows = $query->fetchAll(PDO::FETCH_ASSOC);
if ($rows === FALSE) if (empty($rows))
{ {
return []; return [];
} }
@ -479,7 +479,7 @@ final class AnimeCollection extends Collection
->get(); ->get();
$rows = $query->fetchAll(PDO::FETCH_ASSOC); $rows = $query->fetchAll(PDO::FETCH_ASSOC);
if ($rows === FALSE) if (empty($rows))
{ {
return []; return [];
} }
@ -659,7 +659,7 @@ final class AnimeCollection extends Collection
->get(); ->get();
$rows = $query->fetchAll(PDO::FETCH_ASSOC); $rows = $query->fetchAll(PDO::FETCH_ASSOC);
if ($rows === FALSE) if (empty($rows))
{ {
return []; return [];
} }
@ -691,7 +691,7 @@ final class AnimeCollection extends Collection
->get(); ->get();
$rows = $query->fetchAll(PDO::FETCH_ASSOC); $rows = $query->fetchAll(PDO::FETCH_ASSOC);
if ($rows === FALSE) if (empty($rows))
{ {
return []; return [];
} }
@ -737,7 +737,7 @@ final class AnimeCollection extends Collection
// Add genres associated with each item // Add genres associated with each item
$rows = $query->fetchAll(PDO::FETCH_ASSOC); $rows = $query->fetchAll(PDO::FETCH_ASSOC);
if ($rows === FALSE) if (empty($rows))
{ {
return []; return [];
} }

View File

@ -25,7 +25,10 @@ class Json
/** /**
* Encode data in json format * Encode data in json format
* *
* @throws JsonException * @param mixed $data
* @param int $options
* @param int<1, max> $depth
* @return string
*/ */
public static function encode(mixed $data, int $options = 0, int $depth = 512): string public static function encode(mixed $data, int $options = 0, int $depth = 512): string
{ {
@ -54,7 +57,11 @@ class Json
/** /**
* Decode data from json * Decode data from json
* *
* @throws JsonException * @param string|null $json
* @param bool $assoc
* @param int<1, max> $depth
* @param int $options
* @return mixed
*/ */
public static function decode(?string $json, bool $assoc = TRUE, int $depth = 512, int $options = 0): mixed public static function decode(?string $json, bool $assoc = TRUE, int $depth = 512, int $options = 0): mixed
{ {
@ -74,7 +81,11 @@ class Json
/** /**
* Decode json data loaded from the passed filename * Decode json data loaded from the passed filename
* *
* @throws JsonException * @param string $filename
* @param bool $assoc
* @param int<1, max> $depth
* @param int $options
* @return mixed
*/ */
public static function decodeFile(string $filename, bool $assoc = TRUE, int $depth = 512, int $options = 0): mixed public static function decodeFile(string $filename, bool $assoc = TRUE, int $depth = 512, int $options = 0): mixed
{ {

View File

@ -24,9 +24,9 @@ final class StringType extends Stringy
/** /**
* Alias for `create` static constructor * Alias for `create` static constructor
*/ */
public static function from(string $str): self public static function from(string $str = '', ?string $encoding = NULL): self
{ {
return self::create($str); return self::create($str, $encoding);
} }
/** /**

View File

@ -66,7 +66,7 @@ abstract class Stringy implements Countable, IteratorAggregate, ArrayAccess
* @throws InvalidArgumentException if an array or object without a * @throws InvalidArgumentException if an array or object without a
* __toString method is passed as the first argument * __toString method is passed as the first argument
*/ */
public function __construct(mixed $str = '', ?string $encoding = NULL) final public function __construct(mixed $str = '', ?string $encoding = NULL)
{ {
if (is_array($str)) if (is_array($str))
{ {

View File

@ -34,11 +34,11 @@ class HtmlView extends HttpView
/** /**
* Create the Html View * Create the Html View
*/ */
public function __construct(ContainerInterface $container) public function __construct()
{ {
parent::__construct(); parent::__construct();
$this->setContainer($container); $this->setContainer(func_get_arg(0));
$this->response = new HtmlResponse(''); $this->response = new HtmlResponse('');
} }

View File

@ -15,12 +15,17 @@
namespace Aviat\AnimeClient\Tests; namespace Aviat\AnimeClient\Tests;
use DateTime; use DateTime;
use PHPUnit\Framework\Attributes\IgnoreFunctionForCodeCoverage;
use function Aviat\AnimeClient\{arrayToToml, checkFolderPermissions, clearCache, colNotEmpty, friendlyTime, getLocalImg, getResponse, isSequentialArray, tomlToArray}; use function Aviat\AnimeClient\{arrayToToml, checkFolderPermissions, clearCache, colNotEmpty, friendlyTime, getLocalImg, getResponse, isSequentialArray, tomlToArray};
use const Aviat\AnimeClient\{MINUTES_IN_DAY, MINUTES_IN_HOUR, MINUTES_IN_YEAR, SECONDS_IN_MINUTE}; use const Aviat\AnimeClient\{MINUTES_IN_DAY, MINUTES_IN_HOUR, MINUTES_IN_YEAR, SECONDS_IN_MINUTE};
/** /**
* @internal * @internal
*/ */
#[IgnoreFunctionForCodeCoverage('Aviat\AnimeClient\loadConfig')]
#[IgnoreFunctionForCodeCoverage('Aviat\AnimeClient\createPlaceholderImage')]
#[IgnoreFunctionForCodeCoverage('Aviat\AnimeClient\renderTemplate')]
#[IgnoreFunctionForCodeCoverage('Aviat\AnimeClient\getLocalImg')]
final class AnimeClientTest extends AnimeClientTestCase final class AnimeClientTest extends AnimeClientTestCase
{ {
public function testArrayToToml(): void public function testArrayToToml(): void

View File

@ -16,10 +16,12 @@ namespace Aviat\Ion\Tests;
use Aviat\Ion\Config; use Aviat\Ion\Config;
use PHPUnit\Framework\Attributes\DataProvider; use PHPUnit\Framework\Attributes\DataProvider;
use PHPUnit\Framework\Attributes\IgnoreMethodForCodeCoverage;
/** /**
* @internal * @internal
*/ */
#[IgnoreMethodForCodeCoverage(Config::class, 'set')]
final class ConfigTest extends IonTestCase final class ConfigTest extends IonTestCase
{ {
protected Config $config; protected Config $config;

View File

@ -16,11 +16,15 @@ namespace Aviat\Ion\Tests\Type;
use Aviat\Ion\Tests\IonTestCase; use Aviat\Ion\Tests\IonTestCase;
use Aviat\Ion\Type\StringType; use Aviat\Ion\Type\StringType;
use Aviat\Ion\Type\Stringy;
use PHPUnit\Framework\Attributes\DataProvider; use PHPUnit\Framework\Attributes\DataProvider;
use PHPUnit\Framework\Attributes\IgnoreClassForCodeCoverage;
use PHPUnit\Framework\Attributes\Test;
/** /**
* @internal * @internal
*/ */
#[IgnoreClassForCodeCoverage(Stringy::class)]
final class StringTypeTest extends IonTestCase final class StringTypeTest extends IonTestCase
{ {
public static function dataFuzzyCaseMatch(): array public static function dataFuzzyCaseMatch(): array
@ -55,7 +59,8 @@ final class StringTypeTest extends IonTestCase
} }
#[DataProvider('dataFuzzyCaseMatch')] #[DataProvider('dataFuzzyCaseMatch')]
public function testFuzzyCaseMatch(string $str1, string $str2, bool $expected): void #[Test]
public function fuzzyCaseMatch(string $str1, string $str2, bool $expected): void
{ {
$actual = StringType::from($str1)->fuzzyCaseMatch($str2); $actual = StringType::from($str1)->fuzzyCaseMatch($str2);
$this->assertSame($expected, $actual); $this->assertSame($expected, $actual);

View File

@ -27,6 +27,7 @@ class HttpViewTest extends IonTestCase
{ {
parent::setUp(); parent::setUp();
$this->view = new TestHttpView(); $this->view = new TestHttpView();
$this->view = TestHttpView::new();
$this->friend = new Friend($this->view); $this->friend = new Friend($this->view);
} }

View File

@ -14,6 +14,8 @@
namespace Aviat\Ion\Tests; namespace Aviat\Ion\Tests;
use PHPUnit\Framework\Attributes\IgnoreClassForCodeCoverage;
use PHPUnit\Framework\Attributes\Test;
use PHPUnit\Framework\TestCase; use PHPUnit\Framework\TestCase;
use function Aviat\Ion\_dir; use function Aviat\Ion\_dir;
@ -22,9 +24,14 @@ use const DIRECTORY_SEPARATOR;
/** /**
* @internal * @internal
*/ */
#[IgnoreClassForCodeCoverage(\Aviat\Ion\ImageBuilder::class)]
#[IgnoreClassForCodeCoverage(\Aviat\Ion\Attribute\Controller::class)]
#[IgnoreClassForCodeCoverage(\Aviat\Ion\Attribute\DefaultController::class)]
#[IgnoreClassForCodeCoverage(\Aviat\Ion\Attribute\Route::class)]
final class functionsTest extends TestCase final class functionsTest extends TestCase
{ {
public function testDir() #[Test]
public function dir(): void
{ {
$args = ['foo', 'bar', 'baz']; $args = ['foo', 'bar', 'baz'];
$expected = implode(DIRECTORY_SEPARATOR, $args); $expected = implode(DIRECTORY_SEPARATOR, $args);