4
0
mirror of https://github.com/AzuraCast/AzuraCast.git synced 2024-06-12 12:24:33 +00:00

Class finalization part 2.

This commit is contained in:
Buster "Silver Eagle" Neece 2022-07-01 02:41:04 -05:00
parent e0e3001a78
commit 1179baab65
No known key found for this signature in database
GPG Key ID: F1D2E64A0005E80E
58 changed files with 190 additions and 248 deletions

View File

@ -17,17 +17,17 @@ use Psr\Http\Message\ServerRequestInterface;
use function in_array; use function in_array;
use function is_array; use function is_array;
class Acl final class Acl
{ {
use RequestAwareTrait; use RequestAwareTrait;
protected array $permissions; private array $permissions;
protected ?array $actions; private ?array $actions;
public function __construct( public function __construct(
protected EntityManagerInterface $em, private readonly EntityManagerInterface $em,
protected EventDispatcherInterface $dispatcher private readonly EventDispatcherInterface $dispatcher
) { ) {
$this->reload(); $this->reload();
} }

View File

@ -18,7 +18,7 @@ use Slim\App;
use Slim\Factory\ServerRequestCreatorFactory; use Slim\Factory\ServerRequestCreatorFactory;
use Slim\Handlers\Strategies\RequestResponseNamedArgs; use Slim\Handlers\Strategies\RequestResponseNamedArgs;
class AppFactory final class AppFactory
{ {
public static function createApp( public static function createApp(
array $appEnvironment = [], array $appEnvironment = [],

View File

@ -21,30 +21,30 @@ use function random_bytes;
* Inspired by Asseter by Adam Banaszkiewicz: https://github.com/requtize * Inspired by Asseter by Adam Banaszkiewicz: https://github.com/requtize
* @link https://github.com/requtize/assetter * @link https://github.com/requtize/assetter
*/ */
class Assets final class Assets
{ {
use RequestAwareTrait; use RequestAwareTrait;
/** @var array<string, array> Known libraries loaded in initialization. */ /** @var array<string, array> Known libraries loaded in initialization. */
protected array $libraries = []; private array $libraries = [];
/** @var array<string, string> An optional array lookup for versioned files. */ /** @var array<string, string> An optional array lookup for versioned files. */
protected array $versioned_files = []; private array $versioned_files = [];
/** @var array<string, array> Loaded libraries. */ /** @var array<string, array> Loaded libraries. */
protected array $loaded = []; private array $loaded = [];
/** @var bool Whether the current loaded libraries have been sorted by order. */ /** @var bool Whether the current loaded libraries have been sorted by order. */
protected bool $is_sorted = true; private bool $is_sorted = true;
/** @var string A randomly generated number-used-once (nonce) for inline CSP. */ /** @var string A randomly generated number-used-once (nonce) for inline CSP. */
protected string $csp_nonce; private string $csp_nonce;
/** @var array The loaded domains that should be included in the CSP header. */ /** @var array The loaded domains that should be included in the CSP header. */
protected array $csp_domains; private array $csp_domains;
public function __construct( public function __construct(
protected Environment $environment, private readonly Environment $environment,
array $libraries, array $libraries,
) { ) {
foreach ($libraries as $library_name => $library) { foreach ($libraries as $library_name => $library) {
@ -61,7 +61,7 @@ class Assets
$this->csp_domains = []; $this->csp_domains = [];
} }
protected function addVueComponents(array $vueComponents = []): void private function addVueComponents(array $vueComponents = []): void
{ {
if (!empty($vueComponents['entrypoints'])) { if (!empty($vueComponents['entrypoints'])) {
foreach ($vueComponents['entrypoints'] as $componentName => $componentDeps) { foreach ($vueComponents['entrypoints'] as $componentName => $componentDeps) {
@ -417,7 +417,7 @@ class Assets
/** /**
* Sort the list of loaded libraries. * Sort the list of loaded libraries.
*/ */
protected function sort(): void private function sort(): void
{ {
if (!$this->is_sorted) { if (!$this->is_sorted) {
uasort( uasort(
@ -431,7 +431,7 @@ class Assets
} }
} }
protected function resolveAttributes(array $file, array $defaults): array private function resolveAttributes(array $file, array $defaults): array
{ {
if (isset($file['src'])) { if (isset($file['src'])) {
$defaults['src'] = $this->getUrl($file['src']); $defaults['src'] = $this->getUrl($file['src']);
@ -457,7 +457,7 @@ class Assets
* *
* @return string[] * @return string[]
*/ */
protected function compileAttributes(array $attributes): array private function compileAttributes(array $attributes): array
{ {
$compiled_attributes = []; $compiled_attributes = [];
foreach ($attributes as $attr_key => $attr_val) { foreach ($attributes as $attr_key => $attr_val) {
@ -531,7 +531,7 @@ class Assets
* *
* @param string $src * @param string $src
*/ */
protected function addDomainToCsp(string $src): void private function addDomainToCsp(string $src): void
{ {
$uri = new Uri($src); $uri = new Uri($src);

View File

@ -9,7 +9,7 @@ use App\Entity\User;
use App\Exception\NotLoggedInException; use App\Exception\NotLoggedInException;
use Mezzio\Session\SessionInterface; use Mezzio\Session\SessionInterface;
class Auth final class Auth
{ {
public const SESSION_IS_LOGIN_COMPLETE_KEY = 'is_login_complete'; public const SESSION_IS_LOGIN_COMPLETE_KEY = 'is_login_complete';
public const SESSION_USER_ID_KEY = 'user_id'; public const SESSION_USER_ID_KEY = 'user_id';
@ -18,14 +18,14 @@ class Auth
/** @var int The window of valid one-time passwords outside the current timestamp. */ /** @var int The window of valid one-time passwords outside the current timestamp. */
public const TOTP_WINDOW = 5; public const TOTP_WINDOW = 5;
protected bool|User|null $user = null; private bool|User|null $user = null;
protected bool|User|null $masqueraded_user = null; private bool|User|null $masqueraded_user = null;
public function __construct( public function __construct(
protected UserRepository $userRepo, private readonly UserRepository $userRepo,
protected SessionInterface $session, private readonly SessionInterface $session,
protected Environment $environment private readonly Environment $environment
) { ) {
} }

View File

@ -129,6 +129,10 @@ final class LogsAction
'tail' => false, 'tail' => false,
]; ];
break; break;
case FrontendAdapters::Remote:
// Noop
break;
} }
return $logPaths; return $logPaths;

View File

@ -12,22 +12,22 @@ use App\Enums\SupportedThemes;
use App\Http\ServerRequest; use App\Http\ServerRequest;
use Psr\Http\Message\ServerRequestInterface; use Psr\Http\Message\ServerRequestInterface;
class Customization final class Customization
{ {
protected ?Entity\User $user = null; private ?Entity\User $user = null;
protected Entity\Settings $settings; private Entity\Settings $settings;
protected SupportedLocales $locale; private SupportedLocales $locale;
protected SupportedThemes $theme; private SupportedThemes $theme;
protected SupportedThemes $publicTheme; private SupportedThemes $publicTheme;
protected string $instanceName = ''; private string $instanceName = '';
public function __construct( public function __construct(
protected Environment $environment, private readonly Environment $environment,
Entity\Repository\SettingsRepository $settingsRepo, Entity\Repository\SettingsRepository $settingsRepo,
ServerRequestInterface $request ServerRequestInterface $request
) { ) {
@ -46,7 +46,7 @@ class Customization
$this->locale = SupportedLocales::createFromRequest($this->environment, $request); $this->locale = SupportedLocales::createFromRequest($this->environment, $request);
} }
protected function determineTheme( private function determineTheme(
ServerRequestInterface $request, ServerRequestInterface $request,
bool $isPublicTheme = false bool $isPublicTheme = false
): SupportedThemes { ): SupportedThemes {
@ -186,8 +186,4 @@ class Customization
{ {
return $this->settings->getEnableWebsockets(); return $this->settings->getEnableWebsockets();
} }
public function registerLocale(SupportedLocales $locale): void
{
}
} }

View File

@ -6,6 +6,7 @@ namespace App\Nginx;
use App\Entity\Station; use App\Entity\Station;
use Doctrine\ORM\EntityManagerInterface; use Doctrine\ORM\EntityManagerInterface;
use JsonException;
use NowPlaying\Result\Client; use NowPlaying\Result\Client;
use NowPlaying\Result\Result; use NowPlaying\Result\Result;
use Psr\Log\LoggerInterface; use Psr\Log\LoggerInterface;
@ -102,7 +103,7 @@ final class HlsListeners
): ?Client { ): ?Client {
try { try {
$rowJson = json_decode($row, true, 512, JSON_THROW_ON_ERROR); $rowJson = json_decode($row, true, 512, JSON_THROW_ON_ERROR);
} catch (\JsonException) { } catch (JsonException) {
return null; return null;
} }

View File

@ -104,7 +104,7 @@ use OpenApi\Attributes as OA;
in: "header" in: "header"
) )
] ]
class OpenApi final class OpenApi
{ {
public const SAMPLE_TIMESTAMP = 1609480800; public const SAMPLE_TIMESTAMP = 1609480800;

View File

@ -25,30 +25,30 @@ use Traversable;
* @template T of mixed * @template T of mixed
* @implements IteratorAggregate<TKey, T> * @implements IteratorAggregate<TKey, T>
*/ */
class Paginator implements IteratorAggregate, Countable final class Paginator implements IteratorAggregate, Countable
{ {
protected RouterInterface $router; private RouterInterface $router;
/** @var int<1,max> The maximum number of records that can be viewed per page for unauthenticated users. */ /** @var int<1,max> The maximum number of records that can be viewed per page for unauthenticated users. */
protected int $maxPerPage = 25; private int $maxPerPage = 25;
/** @var bool Whether the current request is from jQuery Bootgrid */ /** @var bool Whether the current request is from jQuery Bootgrid */
protected bool $isBootgrid = false; private bool $isBootgrid = false;
/** @var bool Whether the user is currently authenticated on this request. */ /** @var bool Whether the user is currently authenticated on this request. */
protected bool $isAuthenticated = false; private bool $isAuthenticated = false;
/** @var bool Whether to show pagination controls. */ /** @var bool Whether to show pagination controls. */
protected bool $isDisabled = true; private bool $isDisabled = true;
/** @var callable|null A callable postprocessor that can be run on each result. */ /** @var callable|null A callable postprocessor that can be run on each result. */
protected $postprocessor; private $postprocessor;
/** /**
* @param Pagerfanta<T> $paginator * @param Pagerfanta<T> $paginator
*/ */
public function __construct( public function __construct(
protected Pagerfanta $paginator, private readonly Pagerfanta $paginator,
ServerRequestInterface $request ServerRequestInterface $request
) { ) {
$this->router = $request->getAttribute(ServerRequest::ATTR_ROUTER); $this->router = $request->getAttribute(ServerRequest::ATTR_ROUTER);

View File

@ -10,12 +10,12 @@ use Doctrine\Inflector\InflectorFactory;
use Symfony\Component\Finder\Finder; use Symfony\Component\Finder\Finder;
use Symfony\Component\Finder\SplFileInfo; use Symfony\Component\Finder\SplFileInfo;
class Plugins final class Plugins
{ {
/** @var array An array of all plugins and their capabilities. */ /** @var array An array of all plugins and their capabilities. */
protected array $plugins = []; private array $plugins = [];
protected Inflector $inflector; private Inflector $inflector;
public function __construct(string $baseDir) public function __construct(string $baseDir)
{ {

View File

@ -30,7 +30,7 @@ final class Icecast extends AbstractFrontend
try { try {
$this->supervisor->signalProcess($program_name, 'HUP'); $this->supervisor->signalProcess($program_name, 'HUP');
$this->logger->info( $this->logger->info(
'Adapter "' . static::class . '" reloaded.', 'Adapter "' . self::class . '" reloaded.',
['station_id' => $station->getId(), 'station_name' => $station->getName()] ['station_id' => $station->getId(), 'station_name' => $station->getName()]
); );
} catch (SupervisorLibException $e) { } catch (SupervisorLibException $e) {

View File

@ -6,6 +6,7 @@ namespace App\Radio;
use App\Entity; use App\Entity;
use App\Environment; use App\Environment;
use RuntimeException;
use Symfony\Component\Process\Process; use Symfony\Component\Process\Process;
final class StereoTool final class StereoTool
@ -45,7 +46,7 @@ final class StereoTool
try { try {
$process->run(); $process->run();
} catch (\RuntimeException $e) { } catch (RuntimeException) {
return null; return null;
} }

View File

@ -11,13 +11,13 @@ use Symfony\Component\Cache\Adapter\ProxyAdapter;
use Symfony\Component\RateLimiter\RateLimiterFactory; use Symfony\Component\RateLimiter\RateLimiterFactory;
use Symfony\Component\RateLimiter\Storage\CacheStorage; use Symfony\Component\RateLimiter\Storage\CacheStorage;
class RateLimit final class RateLimit
{ {
protected CacheItemPoolInterface $psr6Cache; private CacheItemPoolInterface $psr6Cache;
public function __construct( public function __construct(
protected LockFactory $lockFactory, private readonly LockFactory $lockFactory,
protected Environment $environment, private readonly Environment $environment,
CacheItemPoolInterface $cacheItemPool CacheItemPoolInterface $cacheItemPool
) { ) {
$this->psr6Cache = new ProxyAdapter($cacheItemPool, 'ratelimit.'); $this->psr6Cache = new ProxyAdapter($cacheItemPool, 'ratelimit.');

View File

@ -7,10 +7,10 @@ namespace App\Sync\NowPlaying\Task;
use App\Entity\Station; use App\Entity\Station;
use App\Radio\AutoDJ; use App\Radio\AutoDJ;
class BuildQueueTask implements NowPlayingTaskInterface final class BuildQueueTask implements NowPlayingTaskInterface
{ {
public function __construct( public function __construct(
protected AutoDJ\Queue $queue private readonly AutoDJ\Queue $queue
) { ) {
} }

View File

@ -23,20 +23,20 @@ use Psr\SimpleCache\CacheInterface;
use Symfony\Component\EventDispatcher\EventSubscriberInterface; use Symfony\Component\EventDispatcher\EventSubscriberInterface;
use Symfony\Component\Messenger\MessageBus; use Symfony\Component\Messenger\MessageBus;
class NowPlayingTask implements NowPlayingTaskInterface, EventSubscriberInterface final class NowPlayingTask implements NowPlayingTaskInterface, EventSubscriberInterface
{ {
public function __construct( public function __construct(
protected Adapters $adapters, private readonly Adapters $adapters,
protected CacheInterface $cache, private readonly CacheInterface $cache,
protected EventDispatcherInterface $eventDispatcher, private readonly EventDispatcherInterface $eventDispatcher,
protected MessageBus $messageBus, private readonly MessageBus $messageBus,
protected RouterInterface $router, private readonly RouterInterface $router,
protected Entity\Repository\ListenerRepository $listenerRepo, private readonly Entity\Repository\ListenerRepository $listenerRepo,
protected Entity\Repository\SettingsRepository $settingsRepo, private readonly Entity\Repository\SettingsRepository $settingsRepo,
protected Entity\ApiGenerator\NowPlayingApiGenerator $nowPlayingApiGenerator, private readonly Entity\ApiGenerator\NowPlayingApiGenerator $nowPlayingApiGenerator,
protected ReloadableEntityManagerInterface $em, private readonly ReloadableEntityManagerInterface $em,
protected LoggerInterface $logger, private readonly LoggerInterface $logger,
protected HlsListeners $hlsListeners, private readonly HlsListeners $hlsListeners,
) { ) {
} }

View File

@ -11,11 +11,10 @@ use Azura\Files\ExtendedFilesystemInterface;
use Doctrine\ORM\Query; use Doctrine\ORM\Query;
use Psr\Log\LoggerInterface; use Psr\Log\LoggerInterface;
class CheckFolderPlaylistsTask extends AbstractTask final class CheckFolderPlaylistsTask extends AbstractTask
{ {
public function __construct( public function __construct(
protected Entity\Repository\StationPlaylistMediaRepository $spmRepo, private readonly Entity\Repository\StationPlaylistMediaRepository $spmRepo,
protected Entity\Repository\StationPlaylistFolderRepository $folderRepo,
ReloadableEntityManagerInterface $em, ReloadableEntityManagerInterface $em,
LoggerInterface $logger, LoggerInterface $logger,
) { ) {

View File

@ -19,14 +19,13 @@ use League\Flysystem\UnableToRetrieveMetadata;
use Psr\Log\LoggerInterface; use Psr\Log\LoggerInterface;
use Symfony\Component\Messenger\MessageBus; use Symfony\Component\Messenger\MessageBus;
class CheckMediaTask extends AbstractTask final class CheckMediaTask extends AbstractTask
{ {
public function __construct( public function __construct(
protected Entity\Repository\StationMediaRepository $mediaRepo, private readonly Entity\Repository\StationMediaRepository $mediaRepo,
protected Entity\Repository\StorageLocationRepository $storageLocationRepo, private readonly Entity\Repository\UnprocessableMediaRepository $unprocessableMediaRepo,
protected Entity\Repository\UnprocessableMediaRepository $unprocessableMediaRepo, private readonly MessageBus $messageBus,
protected MessageBus $messageBus, private readonly QueueManagerInterface $queueManager,
protected QueueManagerInterface $queueManager,
ReloadableEntityManagerInterface $em, ReloadableEntityManagerInterface $em,
LoggerInterface $logger LoggerInterface $logger
) { ) {

View File

@ -13,12 +13,12 @@ use App\Radio\Enums\LiquidsoapQueues;
use Psr\EventDispatcher\EventDispatcherInterface; use Psr\EventDispatcher\EventDispatcherInterface;
use Psr\Log\LoggerInterface; use Psr\Log\LoggerInterface;
class CheckRequestsTask extends AbstractTask final class CheckRequestsTask extends AbstractTask
{ {
public function __construct( public function __construct(
protected Entity\Repository\StationRequestRepository $requestRepo, private readonly Entity\Repository\StationRequestRepository $requestRepo,
protected Adapters $adapters, private readonly Adapters $adapters,
protected EventDispatcherInterface $dispatcher, private readonly EventDispatcherInterface $dispatcher,
ReloadableEntityManagerInterface $em, ReloadableEntityManagerInterface $em,
LoggerInterface $logger LoggerInterface $logger
) { ) {

View File

@ -11,13 +11,13 @@ use App\Service\AzuraCastCentral;
use GuzzleHttp\Exception\TransferException; use GuzzleHttp\Exception\TransferException;
use Psr\Log\LoggerInterface; use Psr\Log\LoggerInterface;
class CheckUpdatesTask extends AbstractTask final class CheckUpdatesTask extends AbstractTask
{ {
protected const UPDATE_THRESHOLD = 3780; private const UPDATE_THRESHOLD = 3780;
public function __construct( public function __construct(
protected Entity\Repository\SettingsRepository $settingsRepo, private readonly Entity\Repository\SettingsRepository $settingsRepo,
protected AzuraCastCentral $azuracastCentral, private readonly AzuraCastCentral $azuracastCentral,
ReloadableEntityManagerInterface $em, ReloadableEntityManagerInterface $em,
LoggerInterface $logger LoggerInterface $logger
) { ) {

View File

@ -8,13 +8,13 @@ use App\Doctrine\ReloadableEntityManagerInterface;
use App\Entity; use App\Entity;
use Psr\Log\LoggerInterface; use Psr\Log\LoggerInterface;
class CleanupHistoryTask extends AbstractTask final class CleanupHistoryTask extends AbstractTask
{ {
public function __construct( public function __construct(
protected Entity\Repository\SettingsRepository $settingsRepo, private readonly Entity\Repository\SettingsRepository $settingsRepo,
protected Entity\Repository\SongHistoryRepository $historyRepo, private readonly Entity\Repository\SongHistoryRepository $historyRepo,
protected Entity\Repository\StationQueueRepository $queueRepo, private readonly Entity\Repository\StationQueueRepository $queueRepo,
protected Entity\Repository\ListenerRepository $listenerRepo, private readonly Entity\Repository\ListenerRepository $listenerRepo,
ReloadableEntityManagerInterface $em, ReloadableEntityManagerInterface $em,
LoggerInterface $logger LoggerInterface $logger
) { ) {

View File

@ -8,10 +8,10 @@ use App\Doctrine\ReloadableEntityManagerInterface;
use App\Entity; use App\Entity;
use Psr\Log\LoggerInterface; use Psr\Log\LoggerInterface;
class CleanupLoginTokensTask extends AbstractTask final class CleanupLoginTokensTask extends AbstractTask
{ {
public function __construct( public function __construct(
protected Entity\Repository\UserLoginTokenRepository $loginTokenRepo, private readonly Entity\Repository\UserLoginTokenRepository $loginTokenRepo,
ReloadableEntityManagerInterface $em, ReloadableEntityManagerInterface $em,
LoggerInterface $logger LoggerInterface $logger
) { ) {

View File

@ -4,7 +4,7 @@ declare(strict_types=1);
namespace App\Sync\Task; namespace App\Sync\Task;
class CleanupRelaysTask extends AbstractTask final class CleanupRelaysTask extends AbstractTask
{ {
public static function getSchedulePattern(): string public static function getSchedulePattern(): string
{ {

View File

@ -10,7 +10,7 @@ use League\Flysystem\StorageAttributes;
use Symfony\Component\Finder\Finder; use Symfony\Component\Finder\Finder;
use Throwable; use Throwable;
class CleanupStorageTask extends AbstractTask final class CleanupStorageTask extends AbstractTask
{ {
public static function getSchedulePattern(): string public static function getSchedulePattern(): string
{ {

View File

@ -10,7 +10,7 @@ use Psr\Log\LoggerInterface;
use Symfony\Component\Finder\Finder; use Symfony\Component\Finder\Finder;
use Throwable; use Throwable;
class MoveBroadcastsTask extends AbstractTask final class MoveBroadcastsTask extends AbstractTask
{ {
public static function getSchedulePattern(): string public static function getSchedulePattern(): string
{ {
@ -20,8 +20,8 @@ class MoveBroadcastsTask extends AbstractTask
public function __construct( public function __construct(
ReloadableEntityManagerInterface $em, ReloadableEntityManagerInterface $em,
LoggerInterface $logger, LoggerInterface $logger,
protected Entity\Repository\StationStreamerBroadcastRepository $broadcastRepo, private readonly Entity\Repository\StationStreamerBroadcastRepository $broadcastRepo,
protected Entity\Repository\StorageLocationRepository $storageLocationRepo, private readonly Entity\Repository\StorageLocationRepository $storageLocationRepo,
) { ) {
parent::__construct($em, $logger); parent::__construct($em, $logger);
} }

View File

@ -15,13 +15,13 @@ use Monolog\Logger;
use Monolog\LogRecord; use Monolog\LogRecord;
use Psr\EventDispatcher\EventDispatcherInterface; use Psr\EventDispatcher\EventDispatcherInterface;
class QueueInterruptingTracks extends AbstractTask final class QueueInterruptingTracks extends AbstractTask
{ {
public function __construct( public function __construct(
protected Queue $queue, private readonly Queue $queue,
protected Adapters $adapters, private readonly Adapters $adapters,
protected EventDispatcherInterface $eventDispatcher, private readonly EventDispatcherInterface $eventDispatcher,
protected Logger $monolog, private readonly Logger $monolog,
ReloadableEntityManagerInterface $em, ReloadableEntityManagerInterface $em,
) { ) {
parent::__construct($em, $monolog); parent::__construct($em, $monolog);

View File

@ -4,20 +4,8 @@ declare(strict_types=1);
namespace App\Sync\Task; namespace App\Sync\Task;
use App\Doctrine\ReloadableEntityManagerInterface; final class ReactivateStreamerTask extends AbstractTask
use App\Entity;
use Psr\Log\LoggerInterface;
class ReactivateStreamerTask extends AbstractTask
{ {
public function __construct(
protected Entity\Repository\StationStreamerRepository $streamerRepo,
ReloadableEntityManagerInterface $em,
LoggerInterface $logger
) {
parent::__construct($em, $logger);
}
public static function getSchedulePattern(): string public static function getSchedulePattern(): string
{ {
return self::SCHEDULE_EVERY_MINUTE; return self::SCHEDULE_EVERY_MINUTE;

View File

@ -6,28 +6,22 @@ namespace App\Sync\Task;
use App\Doctrine\ReloadableEntityManagerInterface; use App\Doctrine\ReloadableEntityManagerInterface;
use App\Entity; use App\Entity;
use App\Environment;
use App\Nginx\ConfigWriter; use App\Nginx\ConfigWriter;
use App\Nginx\Nginx; use App\Nginx\Nginx;
use App\Radio\Adapters;
use League\Flysystem\StorageAttributes; use League\Flysystem\StorageAttributes;
use Psr\Log\LoggerInterface; use Psr\Log\LoggerInterface;
use Supervisor\SupervisorInterface;
use Symfony\Component\Filesystem\Filesystem; use Symfony\Component\Filesystem\Filesystem;
use Symfony\Component\Finder\Finder; use Symfony\Component\Finder\Finder;
use Throwable; use Throwable;
class RotateLogsTask extends AbstractTask final class RotateLogsTask extends AbstractTask
{ {
public function __construct( public function __construct(
ReloadableEntityManagerInterface $em, ReloadableEntityManagerInterface $em,
LoggerInterface $logger, LoggerInterface $logger,
protected Environment $environment, private readonly Entity\Repository\SettingsRepository $settingsRepo,
protected Adapters $adapters, private readonly Entity\Repository\StorageLocationRepository $storageLocationRepo,
protected SupervisorInterface $supervisor, private readonly Nginx $nginx,
protected Entity\Repository\SettingsRepository $settingsRepo,
protected Entity\Repository\StorageLocationRepository $storageLocationRepo,
protected Nginx $nginx,
) { ) {
parent::__construct($em, $logger); parent::__construct($em, $logger);
} }
@ -85,7 +79,7 @@ class RotateLogsTask extends AbstractTask
} }
} }
protected function rotateBackupStorage( private function rotateBackupStorage(
Entity\StorageLocation $storageLocation, Entity\StorageLocation $storageLocation,
int $copiesToKeep int $copiesToKeep
): void { ): void {
@ -115,7 +109,7 @@ class RotateLogsTask extends AbstractTask
} }
} }
protected function cleanUpIcecastLog(Entity\Station $station): void private function cleanUpIcecastLog(Entity\Station $station): void
{ {
$config_path = $station->getRadioConfigDir(); $config_path = $station->getRadioConfigDir();
@ -135,7 +129,7 @@ class RotateLogsTask extends AbstractTask
} }
} }
protected function rotateHlsLog(Entity\Station $station): bool private function rotateHlsLog(Entity\Station $station): bool
{ {
$hlsLogFile = ConfigWriter::getHlsLogFile($station); $hlsLogFile = ConfigWriter::getHlsLogFile($station);
$hlsLogBackup = $hlsLogFile . '.1'; $hlsLogBackup = $hlsLogFile . '.1';

View File

@ -9,13 +9,13 @@ use App\Entity;
use Carbon\CarbonImmutable; use Carbon\CarbonImmutable;
use Psr\Log\LoggerInterface; use Psr\Log\LoggerInterface;
class RunAnalyticsTask extends AbstractTask final class RunAnalyticsTask extends AbstractTask
{ {
public function __construct( public function __construct(
protected Entity\Repository\SettingsRepository $settingsRepo, private readonly Entity\Repository\SettingsRepository $settingsRepo,
protected Entity\Repository\AnalyticsRepository $analyticsRepo, private readonly Entity\Repository\AnalyticsRepository $analyticsRepo,
protected Entity\Repository\ListenerRepository $listenerRepo, private readonly Entity\Repository\ListenerRepository $listenerRepo,
protected Entity\Repository\SongHistoryRepository $historyRepo, private readonly Entity\Repository\SongHistoryRepository $historyRepo,
ReloadableEntityManagerInterface $em, ReloadableEntityManagerInterface $em,
LoggerInterface $logger LoggerInterface $logger
) { ) {

View File

@ -13,12 +13,12 @@ use Carbon\CarbonInterface;
use Psr\Log\LoggerInterface; use Psr\Log\LoggerInterface;
use Symfony\Component\Messenger\MessageBus; use Symfony\Component\Messenger\MessageBus;
class RunBackupTask extends AbstractTask final class RunBackupTask extends AbstractTask
{ {
public function __construct( public function __construct(
protected MessageBus $messageBus, private readonly MessageBus $messageBus,
protected Application $console, private readonly Application $console,
protected Entity\Repository\SettingsRepository $settingsRepo, private readonly Entity\Repository\SettingsRepository $settingsRepo,
ReloadableEntityManagerInterface $em, ReloadableEntityManagerInterface $em,
LoggerInterface $logger LoggerInterface $logger
) { ) {

View File

@ -6,7 +6,6 @@ namespace App\Sync\Task;
use App\Doctrine\ReloadableEntityManagerInterface; use App\Doctrine\ReloadableEntityManagerInterface;
use App\Entity; use App\Entity;
use App\Service\IpGeolocation;
use App\Service\IpGeolocator\GeoLite; use App\Service\IpGeolocator\GeoLite;
use Exception; use Exception;
use GuzzleHttp\Client; use GuzzleHttp\Client;
@ -15,14 +14,13 @@ use Psr\Log\LoggerInterface;
use RuntimeException; use RuntimeException;
use Symfony\Component\Process\Process; use Symfony\Component\Process\Process;
class UpdateGeoLiteTask extends AbstractTask final class UpdateGeoLiteTask extends AbstractTask
{ {
protected const UPDATE_THRESHOLD = 86000; private const UPDATE_THRESHOLD = 86000;
public function __construct( public function __construct(
protected Client $httpClient, private readonly Client $httpClient,
protected IpGeolocation $geoLite, private readonly Entity\Repository\SettingsRepository $settingsRepo,
protected Entity\Repository\SettingsRepository $settingsRepo,
ReloadableEntityManagerInterface $em, ReloadableEntityManagerInterface $em,
LoggerInterface $logger, LoggerInterface $logger,
) { ) {

View File

@ -12,7 +12,7 @@ use Exception;
use League\Flysystem\FileAttributes; use League\Flysystem\FileAttributes;
use League\Flysystem\StorageAttributes; use League\Flysystem\StorageAttributes;
class UpdateStorageLocationSizesTask extends AbstractTask final class UpdateStorageLocationSizesTask extends AbstractTask
{ {
public static function getSchedulePattern(): string public static function getSchedulePattern(): string
{ {
@ -70,7 +70,7 @@ class UpdateStorageLocationSizesTask extends AbstractTask
$this->logger->info('Storage location size updated.', [ $this->logger->info('Storage location size updated.', [
'storageLocation' => (string)$storageLocation, 'storageLocation' => (string)$storageLocation,
'size' => Quota::getReadableSize($used), 'size' => Quota::getReadableSize($used),
]); ]);
} }
} }

View File

@ -4,7 +4,7 @@ declare(strict_types=1);
namespace App\Utilities; namespace App\Utilities;
class Arrays final class Arrays
{ {
/** /**
* Flatten an array from format: * Flatten an array from format:

View File

@ -13,7 +13,7 @@ use function stripos;
/** /**
* Static class that facilitates the uploading, reading and deletion of files in a controlled directory. * Static class that facilitates the uploading, reading and deletion of files in a controlled directory.
*/ */
class File final class File
{ {
/** /**
* @param string $path * @param string $path

View File

@ -7,7 +7,7 @@ namespace App\Utilities;
use JsonException; use JsonException;
use RuntimeException; use RuntimeException;
class Json final class Json
{ {
public static function loadFromFile( public static function loadFromFile(
string $path, string $path,

View File

@ -6,7 +6,7 @@ namespace App\Utilities;
use RuntimeException; use RuntimeException;
class Strings final class Strings
{ {
/** /**
* Truncate text (adding "..." if needed) * Truncate text (adding "..." if needed)

View File

@ -4,7 +4,7 @@ declare(strict_types=1);
namespace App\Utilities; namespace App\Utilities;
class Time final class Time
{ {
public static function displayTimeToSeconds(string|float|int $seconds = null): ?float public static function displayTimeToSeconds(string|float|int $seconds = null): ?float
{ {

View File

@ -10,7 +10,7 @@ use GuzzleHttp\Psr7\Uri;
use LogicException; use LogicException;
use Psr\Http\Message\UriInterface; use Psr\Http\Message\UriInterface;
class Urls final class Urls
{ {
public static function getUri(?string $url): ?UriInterface public static function getUri(?string $url): ?UriInterface
{ {

View File

@ -8,7 +8,7 @@ use Attribute;
use Symfony\Component\Validator\Constraint; use Symfony\Component\Validator\Constraint;
#[Attribute(Attribute::TARGET_CLASS)] #[Attribute(Attribute::TARGET_CLASS)]
class StationPortChecker extends Constraint final class StationPortChecker extends Constraint
{ {
public function getTargets(): string public function getTargets(): string
{ {

View File

@ -10,13 +10,11 @@ use Symfony\Component\Validator\Constraint;
use Symfony\Component\Validator\ConstraintValidator; use Symfony\Component\Validator\ConstraintValidator;
use Symfony\Component\Validator\Exception\UnexpectedTypeException; use Symfony\Component\Validator\Exception\UnexpectedTypeException;
class StationPortCheckerValidator extends ConstraintValidator final class StationPortCheckerValidator extends ConstraintValidator
{ {
protected Configuration $configuration; public function __construct(
private readonly Configuration $configuration
public function __construct(Configuration $configuration) ) {
{
$this->configuration = $configuration;
} }
public function validate(mixed $value, Constraint $constraint): void public function validate(mixed $value, Constraint $constraint): void

View File

@ -8,7 +8,7 @@ use Attribute;
use Symfony\Component\Validator\Constraint; use Symfony\Component\Validator\Constraint;
#[Attribute(Attribute::TARGET_CLASS)] #[Attribute(Attribute::TARGET_CLASS)]
class StorageLocation extends Constraint final class StorageLocation extends Constraint
{ {
public function getTargets(): string public function getTargets(): string
{ {

View File

@ -5,18 +5,16 @@ declare(strict_types=1);
namespace App\Validator\Constraints; namespace App\Validator\Constraints;
use App\Entity; use App\Entity;
use App\Radio\Configuration;
use Doctrine\ORM\EntityManagerInterface; use Doctrine\ORM\EntityManagerInterface;
use Exception; use Exception;
use Symfony\Component\Validator\Constraint; use Symfony\Component\Validator\Constraint;
use Symfony\Component\Validator\ConstraintValidator; use Symfony\Component\Validator\ConstraintValidator;
use Symfony\Component\Validator\Exception\UnexpectedTypeException; use Symfony\Component\Validator\Exception\UnexpectedTypeException;
class StorageLocationValidator extends ConstraintValidator final class StorageLocationValidator extends ConstraintValidator
{ {
public function __construct( public function __construct(
protected Configuration $configuration, private readonly EntityManagerInterface $em,
protected EntityManagerInterface $em,
) { ) {
} }

View File

@ -25,7 +25,7 @@ use function is_string;
* @author Benjamin Eberlei <kontakt@beberlei.de> * @author Benjamin Eberlei <kontakt@beberlei.de>
*/ */
#[Attribute(Attribute::TARGET_CLASS)] #[Attribute(Attribute::TARGET_CLASS)]
class UniqueEntity extends Constraint final class UniqueEntity extends Constraint
{ {
/** @var class-string|null */ /** @var class-string|null */
public ?string $entityClass = null; public ?string $entityClass = null;

View File

@ -35,10 +35,10 @@ use function is_string;
* *
* @author Benjamin Eberlei <kontakt@beberlei.de> * @author Benjamin Eberlei <kontakt@beberlei.de>
*/ */
class UniqueEntityValidator extends ConstraintValidator final class UniqueEntityValidator extends ConstraintValidator
{ {
public function __construct( public function __construct(
protected EntityManagerInterface $em private readonly EntityManagerInterface $em
) { ) {
} }

View File

@ -13,24 +13,19 @@ use Symfony\Component\Process\Process;
/** /**
* App Core Framework Version * App Core Framework Version
*/ */
class Version final class Version
{ {
/** @var string Version that is displayed if no Git repository information is present. */ /** @var string Version that is displayed if no Git repository information is present. */
public const FALLBACK_VERSION = '0.17.1'; public const FALLBACK_VERSION = '0.17.1';
// phpcs:disable Generic.Files.LineLength
public const LATEST_COMPOSE_REVISION = 12;
public const LATEST_COMPOSE_URL = 'https://raw.githubusercontent.com/AzuraCast/AzuraCast/main/docker-compose.sample.yml';
public const UPDATE_URL = 'https://docs.azuracast.com/en/getting-started/updates'; public const UPDATE_URL = 'https://docs.azuracast.com/en/getting-started/updates';
public const CHANGELOG_URL = 'https://github.com/AzuraCast/AzuraCast/blob/main/CHANGELOG.md'; public const CHANGELOG_URL = 'https://github.com/AzuraCast/AzuraCast/blob/main/CHANGELOG.md';
// phpcs:enable
protected string $repoDir; private string $repoDir;
public function __construct( public function __construct(
protected CacheInterface $cache, private readonly CacheInterface $cache,
protected Environment $environment private readonly Environment $environment
) { ) {
$this->repoDir = $environment->getBaseDirectory(); $this->repoDir = $environment->getBaseDirectory();
} }
@ -84,7 +79,7 @@ class Version
* *
* @return mixed[] * @return mixed[]
*/ */
protected function getRawDetails(): array private function getRawDetails(): array
{ {
if (!is_dir($this->repoDir . '/.git')) { if (!is_dir($this->repoDir . '/.git')) {
return []; return [];
@ -127,7 +122,7 @@ class Version
/** /**
* Run the specified process and return its output. * Run the specified process and return its output.
*/ */
protected function runProcess(array $proc, string $default = ''): string private function runProcess(array $proc, string $default = ''): string
{ {
$process = new Process($proc); $process = new Process($proc);
$process->setWorkingDirectory($this->repoDir); $process->setWorkingDirectory($this->repoDir);
@ -183,18 +178,4 @@ class Version
$details = $this->getDetails(); $details = $this->getDetails();
return $details['commit_short'] ?? null; return $details['commit_short'] ?? null;
} }
/**
* Check if the installation has been modified by the user from the release build.
*/
public function isInstallationModified(): bool
{
// We can't detect if release builds are changed, so always return true.
if (!is_dir($this->repoDir . '/.git')) {
return true;
}
$changed_files = $this->runProcess(['git', 'status', '-s']);
return !empty($changed_files);
}
} }

View File

@ -17,7 +17,7 @@ use Psr\Http\Message\ServerRequestInterface;
use Symfony\Component\VarDumper\Cloner\VarCloner; use Symfony\Component\VarDumper\Cloner\VarCloner;
use Symfony\Component\VarDumper\Dumper\CliDumper; use Symfony\Component\VarDumper\Dumper\CliDumper;
class View extends Engine final class View extends Engine
{ {
use RequestAwareTrait; use RequestAwareTrait;
@ -26,7 +26,7 @@ class View extends Engine
EventDispatcherInterface $dispatcher, EventDispatcherInterface $dispatcher,
Version $version, Version $version,
RouterInterface $router, RouterInterface $router,
protected Assets $assets private Assets $assets
) { ) {
parent::__construct($environment->getViewsDirectory(), 'phtml'); parent::__construct($environment->getViewsDirectory(), 'phtml');

View File

@ -14,11 +14,11 @@ use DateTime;
use DateTimeZone; use DateTimeZone;
use Symfony\Component\Intl\Countries; use Symfony\Component\Intl\Countries;
class StationFormComponent implements VueComponentInterface final class StationFormComponent implements VueComponentInterface
{ {
public function __construct( public function __construct(
protected Adapters $adapters, private readonly Adapters $adapters,
protected SettingsRepository $settingsRepo private readonly SettingsRepository $settingsRepo
) { ) {
} }
@ -39,7 +39,7 @@ class StationFormComponent implements VueComponentInterface
]; ];
} }
protected function getTimezones(): array private function getTimezones(): array
{ {
$tzSelect = [ $tzSelect = [
'UTC' => [ 'UTC' => [

View File

@ -60,7 +60,7 @@ use Monolog\Level;
* inline bool whether or not this field should display inline * inline bool whether or not this field should display inline
*/ */
class Discord extends AbstractConnector final class Discord extends AbstractConnector
{ {
public const NAME = 'discord'; public const NAME = 'discord';

View File

@ -10,14 +10,14 @@ use GuzzleHttp\Client;
use Monolog\Logger; use Monolog\Logger;
use Symfony\Component\Mailer\Exception\TransportExceptionInterface; use Symfony\Component\Mailer\Exception\TransportExceptionInterface;
class Email extends AbstractConnector final class Email extends AbstractConnector
{ {
public const NAME = 'email'; public const NAME = 'email';
public function __construct( public function __construct(
Logger $logger, Logger $logger,
Client $httpClient, Client $httpClient,
protected Mail $mail private readonly Mail $mail
) { ) {
parent::__construct($logger, $httpClient); parent::__construct($logger, $httpClient);
} }

View File

@ -7,7 +7,7 @@ namespace App\Webhook\Connector;
use App\Entity; use App\Entity;
use GuzzleHttp\Exception\TransferException; use GuzzleHttp\Exception\TransferException;
class Generic extends AbstractConnector final class Generic extends AbstractConnector
{ {
public const NAME = 'generic'; public const NAME = 'generic';

View File

@ -5,24 +5,20 @@ declare(strict_types=1);
namespace App\Webhook\Connector; namespace App\Webhook\Connector;
use App\Entity; use App\Entity;
use App\Radio\Adapters;
use Doctrine\ORM\EntityManagerInterface;
use GuzzleHttp\Client; use GuzzleHttp\Client;
use GuzzleHttp\Psr7\Uri; use GuzzleHttp\Psr7\Uri;
use Monolog\Logger; use Monolog\Logger;
use TheIconic\Tracking\GoogleAnalytics\Analytics; use TheIconic\Tracking\GoogleAnalytics\Analytics;
use TheIconic\Tracking\GoogleAnalytics\Network\HttpClient; use TheIconic\Tracking\GoogleAnalytics\Network\HttpClient;
class GoogleAnalytics extends AbstractConnector final class GoogleAnalytics extends AbstractConnector
{ {
public const NAME = 'google_analytics'; public const NAME = 'google_analytics';
public function __construct( public function __construct(
Logger $logger, Logger $logger,
Client $httpClient, Client $httpClient,
protected EntityManagerInterface $em, private readonly Entity\Repository\ListenerRepository $listenerRepo
protected Adapters $adapters,
protected Entity\Repository\ListenerRepository $listenerRepo
) { ) {
parent::__construct($logger, $httpClient); parent::__construct($logger, $httpClient);
} }

View File

@ -6,25 +6,21 @@ namespace App\Webhook\Connector;
use App\Entity; use App\Entity;
use App\Http\RouterInterface; use App\Http\RouterInterface;
use App\Radio\Adapters;
use App\Utilities\Urls; use App\Utilities\Urls;
use Doctrine\ORM\EntityManagerInterface;
use GuzzleHttp\Client; use GuzzleHttp\Client;
use GuzzleHttp\Exception\TransferException; use GuzzleHttp\Exception\TransferException;
use Monolog\Logger; use Monolog\Logger;
use Psr\Http\Message\UriInterface; use Psr\Http\Message\UriInterface;
class MatomoAnalytics extends AbstractConnector final class MatomoAnalytics extends AbstractConnector
{ {
public const NAME = 'matomo_analytics'; public const NAME = 'matomo_analytics';
public function __construct( public function __construct(
Logger $logger, Logger $logger,
Client $httpClient, Client $httpClient,
protected RouterInterface $router, private readonly RouterInterface $router,
protected EntityManagerInterface $em, private readonly Entity\Repository\ListenerRepository $listenerRepo
protected Adapters $adapters,
protected Entity\Repository\ListenerRepository $listenerRepo
) { ) {
parent::__construct($logger, $httpClient); parent::__construct($logger, $httpClient);
} }

View File

@ -12,7 +12,7 @@ use GuzzleHttp\Exception\TransferException;
* *
* @package App\Webhook\Connector * @package App\Webhook\Connector
*/ */
class Telegram extends AbstractConnector final class Telegram extends AbstractConnector
{ {
public const NAME = 'telegram'; public const NAME = 'telegram';

View File

@ -7,7 +7,7 @@ namespace App\Webhook\Connector;
use App\Entity; use App\Entity;
use GuzzleHttp\Exception\TransferException; use GuzzleHttp\Exception\TransferException;
class TuneIn extends AbstractConnector final class TuneIn extends AbstractConnector
{ {
public const NAME = 'tunein'; public const NAME = 'tunein';

View File

@ -5,22 +5,20 @@ declare(strict_types=1);
namespace App\Webhook\Connector; namespace App\Webhook\Connector;
use App\Entity; use App\Entity;
use Doctrine\ORM\EntityManagerInterface;
use GuzzleHttp\Client; use GuzzleHttp\Client;
use GuzzleHttp\Exception\TransferException; use GuzzleHttp\Exception\TransferException;
use GuzzleHttp\HandlerStack; use GuzzleHttp\HandlerStack;
use GuzzleHttp\Subscriber\Oauth\Oauth1; use GuzzleHttp\Subscriber\Oauth\Oauth1;
use Monolog\Logger; use Monolog\Logger;
class Twitter extends AbstractConnector final class Twitter extends AbstractConnector
{ {
public const NAME = 'twitter'; public const NAME = 'twitter';
public function __construct( public function __construct(
Logger $logger, Logger $logger,
Client $httpClient, Client $httpClient,
protected EntityManagerInterface $em, private readonly HandlerStack $handlerStack,
protected HandlerStack $handlerStack,
) { ) {
parent::__construct($logger, $httpClient); parent::__construct($logger, $httpClient);
} }

View File

@ -14,19 +14,17 @@ use Monolog\Handler\StreamHandler;
use Monolog\Handler\TestHandler; use Monolog\Handler\TestHandler;
use Monolog\Logger; use Monolog\Logger;
use Psr\Log\LogLevel; use Psr\Log\LogLevel;
use Symfony\Component\Messenger\MessageBus;
class Dispatcher final class Dispatcher
{ {
public function __construct( public function __construct(
protected Environment $environment, private readonly Environment $environment,
protected Logger $logger, private readonly Logger $logger,
protected EntityManagerInterface $em, private readonly EntityManagerInterface $em,
protected MessageBus $messageBus, private readonly RouterInterface $router,
protected RouterInterface $router, private readonly LocalWebhookHandler $localHandler,
protected LocalWebhookHandler $localHandler, private readonly ConnectorLocator $connectors,
protected ConnectorLocator $connectors, private readonly Entity\ApiGenerator\NowPlayingApiGenerator $nowPlayingApiGen
protected Entity\ApiGenerator\NowPlayingApiGenerator $nowPlayingApiGen
) { ) {
} }

View File

@ -8,21 +8,18 @@ use App\Entity;
use App\Environment; use App\Environment;
use GuzzleHttp\Client; use GuzzleHttp\Client;
use Monolog\Logger; use Monolog\Logger;
use Psr\SimpleCache\CacheInterface;
use Symfony\Component\Filesystem\Filesystem; use Symfony\Component\Filesystem\Filesystem;
use const JSON_PRETTY_PRINT; use const JSON_PRETTY_PRINT;
class LocalWebhookHandler final class LocalWebhookHandler
{ {
public const NAME = 'local'; public const NAME = 'local';
public function __construct( public function __construct(
protected Logger $logger, private readonly Logger $logger,
protected Client $httpClient, private readonly Client $httpClient,
protected CacheInterface $cache, private readonly Environment $environment,
protected Entity\Repository\SettingsRepository $settingsRepo,
protected Environment $environment,
) { ) {
} }

View File

@ -16,12 +16,12 @@ use const LIBXML_XINCLUDE;
/** /**
* XML config reader. * XML config reader.
*/ */
class Reader final class Reader
{ {
/** /**
* Nodes to handle as plain text. * Nodes to handle as plain text.
*/ */
protected static array $textNodes = [ private static array $textNodes = [
XMLReader::TEXT, XMLReader::TEXT,
XMLReader::CDATA, XMLReader::CDATA,
XMLReader::WHITESPACE, XMLReader::WHITESPACE,
@ -58,7 +58,7 @@ class Reader
return $return; return $return;
} }
protected static function processNextElement(XMLReader $reader): string|array private static function processNextElement(XMLReader $reader): string|array
{ {
$children = []; $children = [];
$text = ''; $text = '';
@ -114,7 +114,7 @@ class Reader
* *
* @return string[] * @return string[]
*/ */
protected static function getAttributes(XMLReader $reader): array private static function getAttributes(XMLReader $reader): array
{ {
$attributes = []; $attributes = [];

View File

@ -11,7 +11,7 @@ namespace App\Xml;
use RuntimeException; use RuntimeException;
use XMLWriter; use XMLWriter;
class Writer final class Writer
{ {
public static function toString( public static function toString(
array $config, array $config,
@ -20,7 +20,7 @@ class Writer
return self::processConfig($config, $baseElement); return self::processConfig($config, $baseElement);
} }
protected static function processConfig( private static function processConfig(
array $config, array $config,
string $baseElement = 'xml-config' string $baseElement = 'xml-config'
): string { ): string {
@ -53,7 +53,7 @@ class Writer
return $writer->outputMemory(); return $writer->outputMemory();
} }
protected static function addBranch( private static function addBranch(
mixed $branchName, mixed $branchName,
array $config, array $config,
XMLWriter $writer XMLWriter $writer
@ -98,7 +98,7 @@ class Writer
} }
} }
protected static function attributesFirst(mixed $a, mixed $b): int private static function attributesFirst(mixed $a, mixed $b): int
{ {
if (str_starts_with((string)$a, '@')) { if (str_starts_with((string)$a, '@')) {
return -1; return -1;