diff --git a/app-modules/bot-discord/phpstan.ignore.neon b/app-modules/bot-discord/phpstan.ignore.neon index 4570743a2..bb33f1d5e 100644 --- a/app-modules/bot-discord/phpstan.ignore.neon +++ b/app-modules/bot-discord/phpstan.ignore.neon @@ -1,55 +1,10 @@ parameters: ignoreErrors: - - - message: '#^Access to an undefined property Discord\\Parts\\Guild\\Guild::\$icon\.$#' - identifier: property.notFound - count: 1 - path: src/SlashCommands/DontAskCommand.php - - - message: '#^Access to an undefined property Discord\\Parts\\Guild\\Guild::\$icon\.$#' - identifier: property.notFound - count: 1 - path: src/SlashCommands/EditProfileCommand.php - - - message: '#^Access to an undefined property Discord\\Parts\\Guild\\Guild::\$icon\.$#' - identifier: property.notFound - count: 1 - path: src/SlashCommands/IntroductionCommand.php - - - message: '#^Access to an undefined property Discord\\Parts\\Guild\\Guild::\$icon\.$#' - identifier: property.notFound - count: 1 - path: src/SlashCommands/ProfileCommand.php - - - message: '#^Access to an undefined property Discord\\Parts\\User\\Member::\$user\.$#' - identifier: property.notFound - count: 7 - path: src/Events/WelcomeMember.php - - - message: '#^Access to an undefined property Discord\\Parts\\User\\User::\$avatar\.$#' - identifier: property.notFound - count: 1 - path: src/SlashCommands/EditProfileCommand.php - - - message: '#^Access to an undefined property Discord\\Parts\\User\\User::\$avatar\.$#' - identifier: property.notFound - count: 2 - path: src/SlashCommands/IntroductionCommand.php - - - message: '#^Access to an undefined property Discord\\Parts\\WebSockets\\VoiceStateUpdate::\$channel_id\.$#' - identifier: property.notFound - count: 1 - path: src/Events/DynamicVoiceEvent.php - message: '#^Method He4rt\\BotDiscord\\Tasks\\RichPresence::makeActivities\(\) should return array but returns array\.$#' identifier: return.type count: 1 path: src/Tasks/RichPresence.php - - - message: '#^Parameter \#1 \$guild of method Discord\\Repository\\Guild\\ChannelRepository::build\(\) expects Discord\\Repository\\Guild\\Guild\|string, Discord\\Parts\\Guild\\Guild\|null given\.$#' - identifier: argument.type - count: 1 - path: src/SlashCommands/DynamicVoiceCommand.php - message: '#^Unable to resolve the template type TKey in call to function collect$#' identifier: argument.templateType @@ -60,38 +15,18 @@ parameters: identifier: argument.templateType count: 1 path: src/SlashCommands/EditVoiceChannelLimitCommand.php - - - message: '#^Access to an undefined property Discord\\Parts\\WebSockets\\VoiceStateUpdate::\$channel_id\.$#' - identifier: property.notFound - count: 1 - path: src/Tasks/VoiceExperienceTask.php - - - message: '#^Access to an undefined property Discord\\Parts\\WebSockets\\VoiceStateUpdate::\$member\.$#' - identifier: property.notFound - count: 1 - path: src/Tasks/VoiceExperienceTask.php - message: '#^Using nullsafe property access "\?->user" on left side of \?\? is unnecessary. Use -> instead\.$#' identifier: nullsafe.neverNull count: 1 path: src/Tasks/VoiceExperienceTask.php - - message: '#^Cannot call method find\(\) on array\|\(Discord\\Helpers\\ExCollectionInterface&iterable\)\.$#' - identifier: method.nonObject - count: 3 + message: '#^Parameter \#1 \$roles of method Discord\\Parts\\User\\Member::setRoles\(\) expects#' + identifier: argument.type + count: 1 path: src/SlashCommands/CargoDelasCommand.php - - message: '#^Cannot call method find\(\) on array\|\(Discord\\Helpers\\ExCollectionInterface&iterable\)\.$#' - identifier: method.nonObject + message: '#^Parameter \#1 \$roles of method Discord\\Parts\\User\\Member::setRoles\(\) expects#' + identifier: argument.type count: 1 path: src/SlashCommands/IntroductionCommand.php - - - message: '#^Cannot call method get\(\) on array\|\(Discord\\Helpers\\ExCollectionInterface&iterable\)\.$#' - identifier: method.nonObject - count: 1 - path: src/SlashCommands/DontAskCommand.php - - - message: '#^Cannot call method get\(\) on array\|\(Discord\\Helpers\\ExCollectionInterface&iterable\)\.$#' - identifier: method.nonObject - count: 1 - path: src/SlashCommands/ProfileCommand.php diff --git a/app-modules/bot-discord/phpstan.neon b/app-modules/bot-discord/phpstan.neon index b577d0f29..ed8f31b55 100644 --- a/app-modules/bot-discord/phpstan.neon +++ b/app-modules/bot-discord/phpstan.neon @@ -2,5 +2,8 @@ includes: - phpstan.ignore.neon parameters: + stubFiles: + - stubs/discord-php.stub + paths: - src/ diff --git a/app-modules/bot-discord/src/Actions/VoiceChannel/JoiningChannelAction.php b/app-modules/bot-discord/src/Actions/VoiceChannel/JoiningChannelAction.php index de64bb358..f34bfb3fd 100644 --- a/app-modules/bot-discord/src/Actions/VoiceChannel/JoiningChannelAction.php +++ b/app-modules/bot-discord/src/Actions/VoiceChannel/JoiningChannelAction.php @@ -9,7 +9,7 @@ final class JoiningChannelAction { /** - * @param array $activeChannels + * @param list $activeChannels */ public function execute(string $channelId, array $activeChannels, string $user): void { @@ -27,7 +27,7 @@ public function execute(string $channelId, array $activeChannels, string $user): } /** - * @param array $activeChannels + * @param list $activeChannels */ private function saveActiveChannels(array $activeChannels): void { diff --git a/app-modules/bot-discord/src/Actions/VoiceChannel/LeftChannelAction.php b/app-modules/bot-discord/src/Actions/VoiceChannel/LeftChannelAction.php index 897dfa6b5..d6514a6a9 100644 --- a/app-modules/bot-discord/src/Actions/VoiceChannel/LeftChannelAction.php +++ b/app-modules/bot-discord/src/Actions/VoiceChannel/LeftChannelAction.php @@ -9,14 +9,14 @@ final class LeftChannelAction { /** - * @param array $activeChannels + * @param list $activeChannels */ public function execute(array $activeChannels, string $user): void { foreach ($activeChannels as $index => $channel) { /** @var VoiceChannelDTO $channel */ - if (in_array($user, $channel->users)) { - $activeChannels[$index]->users = array_values(array_filter($channel->users, fn ($userId) => $userId !== $user)); + if (in_array($user, $channel->users, true)) { + $activeChannels[$index]->users = array_values(array_filter($channel->users, fn (string $userId) => $userId !== $user)); $activeChannels[$index]->usersCount--; $this->saveActiveChannels($activeChannels); @@ -26,7 +26,7 @@ public function execute(array $activeChannels, string $user): void } /** - * @param array $activeChannels + * @param list $activeChannels */ private function saveActiveChannels(array $activeChannels): void { diff --git a/app-modules/bot-discord/src/Commands/PingCommand.php b/app-modules/bot-discord/src/Commands/PingCommand.php index 0495d1efc..d9cf7354a 100644 --- a/app-modules/bot-discord/src/Commands/PingCommand.php +++ b/app-modules/bot-discord/src/Commands/PingCommand.php @@ -8,6 +8,9 @@ use Discord\Parts\Interactions\Interaction; use Laracord\Commands\Command; +/** + * @method \Laracord\Discord\Message message(string $content = '') + */ class PingCommand extends Command { /** diff --git a/app-modules/bot-discord/src/DTO/VoiceChannelDTO.php b/app-modules/bot-discord/src/DTO/VoiceChannelDTO.php index dfca5af9b..4ed3691a6 100644 --- a/app-modules/bot-discord/src/DTO/VoiceChannelDTO.php +++ b/app-modules/bot-discord/src/DTO/VoiceChannelDTO.php @@ -13,7 +13,7 @@ public function __construct( public readonly string $channelId, public readonly string $ownerId, public int $usersCount, - /** @var array */ + /** @var list */ public array $users, public ?CarbonInterface $lastJoinedAt = null, ) {} diff --git a/app-modules/bot-discord/src/Events/RawGatewayEvent.php b/app-modules/bot-discord/src/Events/RawGatewayEvent.php index 5f3110343..457fdf9a6 100644 --- a/app-modules/bot-discord/src/Events/RawGatewayEvent.php +++ b/app-modules/bot-discord/src/Events/RawGatewayEvent.php @@ -9,21 +9,25 @@ final class RawGatewayEvent { + /** @param object{t?: string, d?: object} $payload */ public function handle(object $payload): void { if (!isset($payload->t, $payload->d)) { return; } - $encodedPayload = json_encode($payload->d); + /** @var object{guild_id?: string, user_id?: string, channel_id?: string} $data */ + $data = $payload->d; + + $encodedPayload = json_encode($data); throw_if($encodedPayload === false, RuntimeException::class, 'Failed to encode Discord gateway payload to JSON.'); DiscordEventLog::query()->create([ 'event_type' => $payload->t, - 'guild_id' => $payload->d->guild_id ?? null, - 'user_id' => $payload->d->user_id ?? $payload->d->author->id ?? null, - 'channel_id' => $payload->d->channel_id ?? null, + 'guild_id' => $data->guild_id ?? null, + 'user_id' => $data->user_id ?? data_get($payload->d, 'author.id'), + 'channel_id' => $data->channel_id ?? null, 'payload' => json_decode($encodedPayload, true), ]); } diff --git a/app-modules/bot-discord/src/Events/WelcomeMember.php b/app-modules/bot-discord/src/Events/WelcomeMember.php index e3665ae51..b39cff9f2 100644 --- a/app-modules/bot-discord/src/Events/WelcomeMember.php +++ b/app-modules/bot-discord/src/Events/WelcomeMember.php @@ -12,9 +12,13 @@ use He4rt\Identity\User\Actions\ResolveUserContext; use He4rt\Identity\User\Models\User; use Illuminate\Support\Facades\Log; +use Laracord\Discord\Message; use Laracord\Events\Event; use Throwable; +/** + * @method Message message(string $content = '') + */ class WelcomeMember extends Event { protected $handler = Events::GUILD_MEMBER_ADD; diff --git a/app-modules/bot-discord/src/SlashCommands/AbstractSlashCommand.php b/app-modules/bot-discord/src/SlashCommands/AbstractSlashCommand.php index 0b060879e..210ffb139 100644 --- a/app-modules/bot-discord/src/SlashCommands/AbstractSlashCommand.php +++ b/app-modules/bot-discord/src/SlashCommands/AbstractSlashCommand.php @@ -13,7 +13,11 @@ use Illuminate\Pipeline\Pipeline; use Laracord\Commands\Middleware\Context; use Laracord\Commands\SlashCommand; +use Laracord\Discord\Message; +/** + * @method Message message(string $content = '') + */ abstract class AbstractSlashCommand extends SlashCommand { protected ?ExternalIdentity $memberProvider = null; diff --git a/app-modules/bot-discord/src/SlashCommands/CargoDelasCommand.php b/app-modules/bot-discord/src/SlashCommands/CargoDelasCommand.php index 2cf8b1403..ef8377c75 100644 --- a/app-modules/bot-discord/src/SlashCommands/CargoDelasCommand.php +++ b/app-modules/bot-discord/src/SlashCommands/CargoDelasCommand.php @@ -108,6 +108,7 @@ public function handle(Interaction $interaction): void private function validateTarget(Interaction $interaction, string $targetUserId): ?Member { + /** @var Member|null $targetMember */ $targetMember = $interaction->guild->members->get('id', $targetUserId); if (!$targetMember) { diff --git a/app-modules/bot-discord/src/SlashCommands/DontAskCommand.php b/app-modules/bot-discord/src/SlashCommands/DontAskCommand.php index 410fea3be..dc4b81805 100644 --- a/app-modules/bot-discord/src/SlashCommands/DontAskCommand.php +++ b/app-modules/bot-discord/src/SlashCommands/DontAskCommand.php @@ -6,6 +6,7 @@ use Discord\Parts\Interactions\Command\Option; use Discord\Parts\Interactions\Interaction; +use Discord\Parts\User\User; use Illuminate\Support\Facades\Date; class DontAskCommand extends AbstractSlashCommand @@ -57,11 +58,14 @@ class DontAskCommand extends AbstractSlashCommand */ public function handle(Interaction $interaction): void { + /** @var User $targetUser */ + $targetUser = $interaction->data->resolved->users->get('id', $this->value('user')); + $this ->message() ->title('Não peça para perguntar.') ->footerIcon($interaction->guild->icon) - ->thumbnailUrl($interaction->data->resolved->users->get('id', $this->value('user'))->avatar) + ->thumbnailUrl($targetUser->avatar) ->footerText(Date::now()->format('Y').' © He4rt Developers') ->imageUrl('https://media.discordapp.net/attachments/546151895010508827/1046092564513701909/Frame_1282_1.png') ->timestamp(now()) diff --git a/app-modules/bot-discord/src/SlashCommands/DynamicVoiceCommand.php b/app-modules/bot-discord/src/SlashCommands/DynamicVoiceCommand.php index 94bdfb514..6cb88b78c 100644 --- a/app-modules/bot-discord/src/SlashCommands/DynamicVoiceCommand.php +++ b/app-modules/bot-discord/src/SlashCommands/DynamicVoiceCommand.php @@ -11,6 +11,7 @@ use Discord\Parts\Interactions\Command\Option; use Discord\Parts\Interactions\Interaction; use He4rt\BotDiscord\DTO\VoiceChannelDTO; +use React\Promise\PromiseInterface; use function React\Async\await; @@ -63,13 +64,15 @@ class DynamicVoiceCommand extends AbstractSlashCommand */ public function handle(Interaction $interaction): void { - $channel = await($interaction->guild->channels->build( + /** @var PromiseInterface $buildPromise */ + $buildPromise = $interaction->guild->channels->build( $interaction->guild, ChannelBuilder::new($this->value('tipo')) ->setType(Channel::TYPE_GUILD_VOICE) ->setUserLimit($this->value('quantidade')) ->setParentId(config('he4rt.channels.dynamic_voice_category')), - )); + ); + $channel = await($buildPromise); $channels = cache()->tags(['voice_channels'])->get('active_voice_channels_keys', []); $channelDto = VoiceChannelDTO::make([ @@ -84,7 +87,9 @@ public function handle(Interaction $interaction): void cache()->tags(['voice_channels'])->put('active_voice_channels_keys', $channels); - await($interaction->guild->channels->freshen()); + /** @var PromiseInterface $freshenPromise */ + $freshenPromise = $interaction->guild->channels->freshen(); + await($freshenPromise); $this->interactionWithUser($interaction, $channel); } diff --git a/app-modules/bot-discord/src/SlashCommands/EditProfileCommand.php b/app-modules/bot-discord/src/SlashCommands/EditProfileCommand.php index 6d441c16d..c4734ff4a 100644 --- a/app-modules/bot-discord/src/SlashCommands/EditProfileCommand.php +++ b/app-modules/bot-discord/src/SlashCommands/EditProfileCommand.php @@ -122,9 +122,16 @@ private function persistData( Interaction $interaction, Collection $components ): void { - $name = $components->get('custom_id', 'name')?->value; - $nickname = $components->get('custom_id', 'nickname')?->value; - $about = $components->get('custom_id', 'about')?->value; + /** @var object{value: string}|null $nameComponent */ + $nameComponent = $components->get('custom_id', 'name'); + /** @var object{value: string}|null $nicknameComponent */ + $nicknameComponent = $components->get('custom_id', 'nickname'); + /** @var object{value: string}|null $aboutComponent */ + $aboutComponent = $components->get('custom_id', 'about'); + + $name = $nameComponent->value ?? ''; + $nickname = $nicknameComponent->value ?? ''; + $about = $aboutComponent->value ?? ''; try { $this->memberProvider->user->update(['name' => $name]); diff --git a/app-modules/bot-discord/src/SlashCommands/EditVoiceChannelLimitCommand.php b/app-modules/bot-discord/src/SlashCommands/EditVoiceChannelLimitCommand.php index 848984058..ee5e7067e 100644 --- a/app-modules/bot-discord/src/SlashCommands/EditVoiceChannelLimitCommand.php +++ b/app-modules/bot-discord/src/SlashCommands/EditVoiceChannelLimitCommand.php @@ -4,9 +4,11 @@ namespace He4rt\BotDiscord\SlashCommands; +use Discord\Parts\Channel\Channel; use Discord\Parts\Interactions\Command\Option; use Discord\Parts\Interactions\Interaction; use Exception; +use React\Promise\PromiseInterface; use function React\Async\await; @@ -52,6 +54,7 @@ public function handle(Interaction $interaction): void return; } + /** @var Channel|null $voiceChannel */ $voiceChannel = $interaction->guild->channels->get('id', $channelId); if (!$voiceChannel) { @@ -65,8 +68,10 @@ public function handle(Interaction $interaction): void $newLimit = $this->value('limite'); try { - $voiceChannel->user_limit = $newLimit; - await($interaction->guild->channels->save($voiceChannel)); + $voiceChannel->user_limit = (int) $newLimit; + /** @var PromiseInterface $promise */ + $promise = $interaction->guild->channels->save($voiceChannel); + await($promise); $this->message() ->content(sprintf('✅ Limite da sala atualizado para **%d** membros!', $newLimit)) diff --git a/app-modules/bot-discord/src/SlashCommands/IntroductionCommand.php b/app-modules/bot-discord/src/SlashCommands/IntroductionCommand.php index bdaa4f76b..104971fcb 100644 --- a/app-modules/bot-discord/src/SlashCommands/IntroductionCommand.php +++ b/app-modules/bot-discord/src/SlashCommands/IntroductionCommand.php @@ -135,9 +135,16 @@ private function persistData(Interaction $interaction, Collection $components): $userContext = resolve(ResolveUserContext::class)->handle($userDto); - $name = $components->get('custom_id', 'name')->value; - $nickname = $components->get('custom_id', 'nickname')->value; - $about = $components->get('custom_id', 'about')->value; + /** @var object{value: string} $nameComponent */ + $nameComponent = $components->get('custom_id', 'name'); + /** @var object{value: string} $nicknameComponent */ + $nicknameComponent = $components->get('custom_id', 'nickname'); + /** @var object{value: string} $aboutComponent */ + $aboutComponent = $components->get('custom_id', 'about'); + + $name = $nameComponent->value; + $nickname = $nicknameComponent->value; + $about = $aboutComponent->value; $userContext->user->update(['name' => $name]); diff --git a/app-modules/bot-discord/src/SlashCommands/ProfileCommand.php b/app-modules/bot-discord/src/SlashCommands/ProfileCommand.php index 0d3625a3e..a7cda08b9 100644 --- a/app-modules/bot-discord/src/SlashCommands/ProfileCommand.php +++ b/app-modules/bot-discord/src/SlashCommands/ProfileCommand.php @@ -6,6 +6,7 @@ use Discord\Parts\Interactions\Command\Option; use Discord\Parts\Interactions\Interaction; +use Discord\Parts\User\User; use He4rt\Identity\ExternalIdentity\Models\ExternalIdentity; use He4rt\Profile\Models\Profile; use Illuminate\Support\Facades\Date; @@ -67,10 +68,12 @@ class ProfileCommand extends AbstractSlashCommand */ public function handle(Interaction $interaction): void { + /** @var User $mentionedUser */ $mentionedUser = $interaction->user; if ($userId = $this->value('user')) { $this->memberProvider = $this->getMemberProviderQuery()->where('external_account_id', $userId)->first(); + /** @var User $mentionedUser */ $mentionedUser = $interaction->data->resolved->users->get('id', $userId); } @@ -79,7 +82,7 @@ public function handle(Interaction $interaction): void if (!$this->memberProvider instanceof ExternalIdentity) { $this ->message() - ->content($mentionedUser.' ainda não se apresentou! Use o comando `/apresentar` primeiro.') + ->content($mentionedUser->username.' ainda não se apresentou! Use o comando `/apresentar` primeiro.') ->reply($interaction, true); return; @@ -93,7 +96,7 @@ public function handle(Interaction $interaction): void if (!$profile) { $this ->message() - ->content($mentionedUser.' ainda não possui um perfil.') + ->content($mentionedUser->username.' ainda não possui um perfil.') ->reply($interaction, true); return; diff --git a/app-modules/bot-discord/stubs/discord-php.stub b/app-modules/bot-discord/stubs/discord-php.stub new file mode 100644 index 000000000..4090adba0 --- /dev/null +++ b/app-modules/bot-discord/stubs/discord-php.stub @@ -0,0 +1,154 @@ + $roles + * @property \DateTimeInterface|null $joined_at + * @property bool $deaf + * @property bool $mute + * @property string|null $guild_id + * @property \Discord\Parts\Guild\Guild|null $guild + * @property string $status + * @property iterable $activities + */ +class Member +{ +} + +/** + * @property string $name + * @property int $type + * @property string|null $url + * @property string|null $application_id + * @property string|null $details + * @property string|null $state + */ +class Activity +{ +} + +namespace Discord\Parts\Guild; + +/** + * @property string $id + * @property string $name + * @property string|null $icon + * @property string|null $splash + * @property string|null $owner_id + * @property string|null $afk_channel_id + * @property int|null $afk_timeout + * @property int|null $member_count + * @property mixed $members + * @property mixed $channels + * @property \Discord\Helpers\CollectionInterface $roles + * @property-read mixed $voice_states + */ +class Guild +{ +} + +/** + * @property string $id + * @property string $name + * @property int|null $color + * @property bool $hoist + * @property int $position + * @property bool $managed + * @property bool $mentionable + */ +class Role +{ +} + +namespace Discord\Parts\Channel; + +/** + * @property string $id + * @property int $type + * @property string|null $guild_id + * @property string|null $name + * @property string|null $topic + * @property int|null $user_limit + * @property int|null $bitrate + */ +class Channel +{ +} + +namespace Discord\Parts\WebSockets; + +/** + * @property string|null $guild_id + * @property \Discord\Parts\Guild\Guild|null $guild + * @property string|null $channel_id + * @property \Discord\Parts\Channel\Channel|null $channel + * @property string $user_id + * @property \Discord\Parts\User\User|null $user + * @property \Discord\Parts\User\Member|null $member + * @property string $session_id + * @property bool $deaf + * @property bool $mute + * @property bool $self_deaf + * @property bool $self_mute + */ +class VoiceStateUpdate +{ +} + +namespace Discord\Parts\Interactions\Request; + +use Discord\Helpers\CollectionInterface; +use Discord\Parts\User\User; + +/** + * @property CollectionInterface $users + */ +class Resolved +{ +} diff --git a/app-modules/community/src/Feedback/Exceptions/FeedbackException.php b/app-modules/community/src/Feedback/Exceptions/FeedbackException.php index 9931635d6..71760804c 100644 --- a/app-modules/community/src/Feedback/Exceptions/FeedbackException.php +++ b/app-modules/community/src/Feedback/Exceptions/FeedbackException.php @@ -18,6 +18,6 @@ public static function idNotFound(int $id): self public function render(Request $request): JsonResponse { - return response()->json($this->getMessage(), $this->code); + return response()->json($this->getMessage(), $this->getCode()); } } diff --git a/app-modules/docs/src/Discovery/Actions/ParseDocumentMetadataAction.php b/app-modules/docs/src/Discovery/Actions/ParseDocumentMetadataAction.php index c3ff5fbe6..b67deb4b7 100644 --- a/app-modules/docs/src/Discovery/Actions/ParseDocumentMetadataAction.php +++ b/app-modules/docs/src/Discovery/Actions/ParseDocumentMetadataAction.php @@ -20,8 +20,9 @@ public function execute(string $content, ?string $filename = null): DocumentMeta $parser = new FrontMatterParser(new SymfonyYamlFrontMatterParser()); $result = $parser->parse($content); - $frontMatter = $result->getFrontMatter(); - $frontMatter = is_array($frontMatter) ? $frontMatter : []; + $raw = $result->getFrontMatter(); + /** @var array $frontMatter */ + $frontMatter = is_array($raw) ? $raw : []; $body = $result->getContent(); diff --git a/app-modules/docs/src/DocsServiceProvider.php b/app-modules/docs/src/DocsServiceProvider.php index e901aa9d9..3f684e128 100644 --- a/app-modules/docs/src/DocsServiceProvider.php +++ b/app-modules/docs/src/DocsServiceProvider.php @@ -8,6 +8,7 @@ use He4rt\Docs\Console\Commands\CacheDocsCommand; use He4rt\Docs\Discovery\Actions\BuildDocumentTreeAction; use He4rt\Docs\Discovery\Actions\DiscoverDocumentSourcesAction; +use He4rt\Docs\Discovery\Contracts\DocumentTypeStrategyContract; use He4rt\Docs\Discovery\DocumentRegistry; use He4rt\Docs\Discovery\Strategies\AdrStrategy; use He4rt\Docs\Discovery\Strategies\ContextMapStrategy; @@ -40,10 +41,15 @@ public function register(): void GuideStrategy::class, ], 'docs.strategies'); - $this->app->bind(BuildDocumentTreeAction::class, static fn (Application $app): BuildDocumentTreeAction => new BuildDocumentTreeAction( - $app->make(DiscoverDocumentSourcesAction::class), - $app->tagged('docs.strategies'), - )); + $this->app->bind(BuildDocumentTreeAction::class, static function (Application $app): BuildDocumentTreeAction { + /** @var iterable $strategies */ + $strategies = $app->tagged('docs.strategies'); + + return new BuildDocumentTreeAction( + $app->make(DiscoverDocumentSourcesAction::class), + $strategies, + ); + }); $this->app->singleton(DocumentRegistry::class); } diff --git a/app-modules/integration-discord/src/ETL/Actions/ImportDiscordMessageAction.php b/app-modules/integration-discord/src/ETL/Actions/ImportDiscordMessageAction.php index f172d104e..62e1c8d10 100644 --- a/app-modules/integration-discord/src/ETL/Actions/ImportDiscordMessageAction.php +++ b/app-modules/integration-discord/src/ETL/Actions/ImportDiscordMessageAction.php @@ -155,6 +155,7 @@ public function prewarm(iterable $dtos, string $tenantId, array $existingCache = return $existingCache; } + /** @var array $existing */ $existing = ExternalIdentity::query() ->where('provider', IdentityProvider::Discord) ->where('tenant_id', $tenantId) @@ -198,6 +199,7 @@ public function prewarmReplyTargets(iterable $dtos, string $tenantId): array return []; } + /** @var array */ return Message::query() ->where('tenant_id', $tenantId) ->whereIn('provider_message_id', array_keys($replyProviderIds)) diff --git a/app-modules/integration-discord/src/ETL/Adapters/DiscordMessageAdapter.php b/app-modules/integration-discord/src/ETL/Adapters/DiscordMessageAdapter.php index 73f84e406..50d2ebb6d 100644 --- a/app-modules/integration-discord/src/ETL/Adapters/DiscordMessageAdapter.php +++ b/app-modules/integration-discord/src/ETL/Adapters/DiscordMessageAdapter.php @@ -162,6 +162,7 @@ public function extractEmbeds(array $raw): array continue; } + /** @var array $embed */ $url = isset($embed['url']) ? (string) $embed['url'] : null; $out[] = new EmbedData( diff --git a/app-modules/integration-discord/src/ETL/Console/ImportDiscordMessagesCommand.php b/app-modules/integration-discord/src/ETL/Console/ImportDiscordMessagesCommand.php index 806c992ab..96bcee289 100644 --- a/app-modules/integration-discord/src/ETL/Console/ImportDiscordMessagesCommand.php +++ b/app-modules/integration-discord/src/ETL/Console/ImportDiscordMessagesCommand.php @@ -331,7 +331,9 @@ private function assertSchema(): array 'created_at', 'updated_at', ]; - $existing = array_flip(Schema::getColumnListing('messages')); + /** @var array $columns */ + $columns = Schema::getColumnListing('messages'); + $existing = array_flip($columns); return array_values(array_filter( $required, diff --git a/app-modules/integration-discord/src/ETL/Console/ImportDiscordProfilesCommand.php b/app-modules/integration-discord/src/ETL/Console/ImportDiscordProfilesCommand.php index 89c523c64..596e1a433 100644 --- a/app-modules/integration-discord/src/ETL/Console/ImportDiscordProfilesCommand.php +++ b/app-modules/integration-discord/src/ETL/Console/ImportDiscordProfilesCommand.php @@ -218,7 +218,9 @@ private function assertSchema(): array $missing = []; foreach ($required as $table => $cols) { - $existing = array_flip(Schema::getColumnListing($table)); + /** @var array $columnList */ + $columnList = Schema::getColumnListing($table); + $existing = array_flip($columnList); foreach ($cols as $col) { if (!isset($existing[$col])) { $missing[] = $table.'.'.$col; diff --git a/app-modules/integration-discord/src/ETL/Console/MergeDuplicateDiscordProfilesCommand.php b/app-modules/integration-discord/src/ETL/Console/MergeDuplicateDiscordProfilesCommand.php index 23eee3898..75fa6c27c 100644 --- a/app-modules/integration-discord/src/ETL/Console/MergeDuplicateDiscordProfilesCommand.php +++ b/app-modules/integration-discord/src/ETL/Console/MergeDuplicateDiscordProfilesCommand.php @@ -12,6 +12,7 @@ use Illuminate\Console\Attributes\Description; use Illuminate\Console\Attributes\Signature; use Illuminate\Console\Command; +use Illuminate\Database\Query\Builder; use Illuminate\Support\Facades\DB; use Illuminate\Support\Facades\Log; use Throwable; @@ -200,7 +201,7 @@ private function runFromHeuristic(MergeDuplicateDiscordUserAction $merge): int ->where('provider', IdentityProvider::Discord) ->where('model_type', $userMorph) ->where('tenant_id', $tenant->getKey()) - ->whereExists(fn ($q) => $q + ->whereExists(fn (Builder $q) => $q ->select(DB::raw(1)) ->from('users') ->whereRaw('users.id::text = external_identities.model_id') diff --git a/app-modules/integration-discord/src/ETL/DTOs/DiscordMessageDTO.php b/app-modules/integration-discord/src/ETL/DTOs/DiscordMessageDTO.php index b5db59056..00ff9ade0 100644 --- a/app-modules/integration-discord/src/ETL/DTOs/DiscordMessageDTO.php +++ b/app-modules/integration-discord/src/ETL/DTOs/DiscordMessageDTO.php @@ -28,6 +28,7 @@ public function __construct( */ public static function fromDump(array $message): self { + /** @var array $author */ $author = $message['author'] ?? []; $rawUsername = $author['username'] ?? $author['id']; $authorDiscordId = (string) $author['id']; @@ -49,7 +50,7 @@ public static function fromDump(array $message): self isBot: $author['bot'] ?? false, content: $message['content'] ?? '', sentAt: $message['timestamp'], - authorRaw: is_array($author) ? $author : [], + authorRaw: $author, metadata: $message, ); } diff --git a/app-modules/integration-discord/src/ETL/DTOs/DiscordProfileDTO.php b/app-modules/integration-discord/src/ETL/DTOs/DiscordProfileDTO.php index 6d14abd43..b4c475c49 100644 --- a/app-modules/integration-discord/src/ETL/DTOs/DiscordProfileDTO.php +++ b/app-modules/integration-discord/src/ETL/DTOs/DiscordProfileDTO.php @@ -32,10 +32,10 @@ public static function fromDump(array $profile): self username: $user['username'] ?? $user['id'], name: $user['global_name'] ?? $user['username'] ?? $user['id'], joinedAt: $guildMember['joined_at'] ?? null, - connectedAccounts: array_map( + connectedAccounts: array_values(array_map( ConnectedAccountDTO::fromDump(...), $profile['connected_accounts'] ?? [], - ), + )), metadata: $profile, ); } diff --git a/app-modules/integration-discord/src/OAuth/DiscordOAuthClient.php b/app-modules/integration-discord/src/OAuth/DiscordOAuthClient.php index c5baf73b3..194e44122 100644 --- a/app-modules/integration-discord/src/OAuth/DiscordOAuthClient.php +++ b/app-modules/integration-discord/src/OAuth/DiscordOAuthClient.php @@ -39,6 +39,7 @@ public function auth(string $code): OAuthAccessDTO redirectUri: $this->callbackUrl(), )); + /** @var array $payload */ $payload = $response->json(); $tokenExchangeFailed = !isset($payload['access_token']); @@ -55,7 +56,10 @@ public function getAuthenticatedUser(OAuthAccessDTO $credentials): OAuthUserDTO accessToken: $credentials->accessToken, )); - return DiscordOAuthUser::make($credentials, $response->json()); + /** @var array $userData */ + $userData = $response->json(); + + return DiscordOAuthUser::make($credentials, $userData); } private function callbackUrl(): string diff --git a/app-modules/integration-discord/src/Sync/Actions/SyncDiscordGuildAction.php b/app-modules/integration-discord/src/Sync/Actions/SyncDiscordGuildAction.php index 8e80ef5b1..4202d9af8 100644 --- a/app-modules/integration-discord/src/Sync/Actions/SyncDiscordGuildAction.php +++ b/app-modules/integration-discord/src/Sync/Actions/SyncDiscordGuildAction.php @@ -124,7 +124,7 @@ private function syncRoles(DiscordGuild $guild, string $discordGuildId): array ], ); - $roleMap[$roleData['id']] = $role->id; + $roleMap[(string) $roleData['id']] = $role->id; } return $roleMap; diff --git a/app-modules/integration-github/src/Backfill/BackfillRepository.php b/app-modules/integration-github/src/Backfill/BackfillRepository.php index 18c1975eb..131e3eb8f 100644 --- a/app-modules/integration-github/src/Backfill/BackfillRepository.php +++ b/app-modules/integration-github/src/Backfill/BackfillRepository.php @@ -89,11 +89,14 @@ function (array $pr) use ($tenantId, $repo, $onProgress): void { }, // PRs vêm ordenados por updated desc: ao cruzar o corte, todo o resto é mais // antigo — paramos de paginar (mata o N+1 de GetPullRequest + reviews). - reachedEnd: $since === null ? null : function (array $pr) use ($since): bool { - $updatedAt = $this->stringFrom($pr, 'updated_at'); - - return $updatedAt !== '' && $updatedAt < $since; - }, + reachedEnd: $since === null + ? null + : function (array $pr) use ($since): bool { + /** @var array $pr */ + $updatedAt = $this->stringFrom($pr, 'updated_at'); + + return $updatedAt !== '' && $updatedAt < $since; + }, ); } diff --git a/app-modules/integration-github/src/OAuth/GitHubOAuthClient.php b/app-modules/integration-github/src/OAuth/GitHubOAuthClient.php index c098d18d3..8c8986bca 100644 --- a/app-modules/integration-github/src/OAuth/GitHubOAuthClient.php +++ b/app-modules/integration-github/src/OAuth/GitHubOAuthClient.php @@ -41,6 +41,7 @@ public function auth(string $code): GitHubOAuthAccessDTO redirectUri: $this->callbackUrl(), )); + /** @var array $payload */ $payload = $response->json(); $tokenExchangeFailed = !isset($payload['access_token']); @@ -57,7 +58,10 @@ public function getAuthenticatedUser(OAuthAccessDTO $credentials): GitHubOAuthUs accessToken: $credentials->accessToken, )); - return GitHubOAuthUserDTO::make($credentials, $response->json()); + /** @var array $userPayload */ + $userPayload = $response->json(); + + return GitHubOAuthUserDTO::make($credentials, $userPayload); } private function callbackUrl(): string diff --git a/app-modules/integration-github/src/Webhook/ProjectGithubEvent.php b/app-modules/integration-github/src/Webhook/ProjectGithubEvent.php index c8566a27f..946a8725c 100644 --- a/app-modules/integration-github/src/Webhook/ProjectGithubEvent.php +++ b/app-modules/integration-github/src/Webhook/ProjectGithubEvent.php @@ -258,7 +258,12 @@ private function arrayFrom(array $source, string $path): array { $value = data_get($source, $path); - return is_array($value) ? $value : []; + if (!is_array($value)) { + return []; + } + + /** @var array $value */ + return $value; } private function isBot(string $login): bool diff --git a/app-modules/integration-github/src/Webhook/VerifyGithubSignature.php b/app-modules/integration-github/src/Webhook/VerifyGithubSignature.php index c81a33c54..80d6341d2 100644 --- a/app-modules/integration-github/src/Webhook/VerifyGithubSignature.php +++ b/app-modules/integration-github/src/Webhook/VerifyGithubSignature.php @@ -26,6 +26,9 @@ public function handle(Request $request, Closure $next): Response abort_unless(hash_equals($expected, $signature), 403, 'Invalid signature'); - return $next($request); + /** @var Response $response */ + $response = $next($request); + + return $response; } } diff --git a/app-modules/integration-twitch/src/Console/SubscribeTwitchEventsCommand.php b/app-modules/integration-twitch/src/Console/SubscribeTwitchEventsCommand.php index c4abc7918..db0162779 100644 --- a/app-modules/integration-twitch/src/Console/SubscribeTwitchEventsCommand.php +++ b/app-modules/integration-twitch/src/Console/SubscribeTwitchEventsCommand.php @@ -137,9 +137,7 @@ private function clearAllSubscriptions(TwitchHelixConnector $helix, string $broa */ private function getExistingSubscriptionTypes(TwitchHelixConnector $helix, string $broadcasterId): array { - return collect($this->getExistingSubscriptions($helix, $broadcasterId)) - ->pluck('type') - ->all(); + return array_column($this->getExistingSubscriptions($helix, $broadcasterId), 'type'); } /** diff --git a/app-modules/integration-twitch/src/Http/Controllers/TwitchWebhookController.php b/app-modules/integration-twitch/src/Http/Controllers/TwitchWebhookController.php index cdeb25c77..ab0084a6a 100644 --- a/app-modules/integration-twitch/src/Http/Controllers/TwitchWebhookController.php +++ b/app-modules/integration-twitch/src/Http/Controllers/TwitchWebhookController.php @@ -22,8 +22,15 @@ public function __invoke(Request $request, Tenant $tenant): Response } $body = $request->all(); - $subscription = $body['subscription'] ?? []; - $event = $body['event'] ?? []; + + /** @var array $subscription */ + $subscription = is_array($body['subscription'] ?? null) ? $body['subscription'] : []; + + /** @var array $event */ + $event = is_array($body['event'] ?? null) ? $body['event'] : []; + + /** @var array $condition */ + $condition = is_array($subscription['condition'] ?? null) ? $subscription['condition'] : []; $messageId = $request->header('Twitch-Eventsub-Message-Id'); $inserted = TwitchEventLog::query()->insertOrIgnore([ @@ -31,8 +38,8 @@ public function __invoke(Request $request, Tenant $tenant): Response 'event_type' => $subscription['type'] ?? $messageType, 'broadcaster_user_id' => $event['broadcaster_user_id'] ?? $event['to_broadcaster_user_id'] - ?? $subscription['condition']['broadcaster_user_id'] - ?? $subscription['condition']['to_broadcaster_user_id'] + ?? $condition['broadcaster_user_id'] + ?? $condition['to_broadcaster_user_id'] ?? null, 'user_id' => $event['user_id'] ?? $event['chatter_user_id'] diff --git a/app-modules/integration-twitch/src/Http/Middleware/VerifyTwitchSignature.php b/app-modules/integration-twitch/src/Http/Middleware/VerifyTwitchSignature.php index 9b1d1950c..8bd08280b 100644 --- a/app-modules/integration-twitch/src/Http/Middleware/VerifyTwitchSignature.php +++ b/app-modules/integration-twitch/src/Http/Middleware/VerifyTwitchSignature.php @@ -27,6 +27,9 @@ public function handle(Request $request, Closure $next): Response abort_unless(hash_equals($expectedSignature, $signature), 403, 'Invalid signature'); - return $next($request); + /** @var Response $response */ + $response = $next($request); + + return $response; } } diff --git a/app-modules/integration-twitch/src/OAuth/TwitchOAuthClient.php b/app-modules/integration-twitch/src/OAuth/TwitchOAuthClient.php index cb0f2a189..2ea1cd7c7 100644 --- a/app-modules/integration-twitch/src/OAuth/TwitchOAuthClient.php +++ b/app-modules/integration-twitch/src/OAuth/TwitchOAuthClient.php @@ -47,6 +47,7 @@ public function auth(string $code): TwitchOAuthAccessDTO redirectUri: $this->callbackUrl(), )); + /** @var array $payload */ $payload = $response->json(); $tokenExchangeFailed = !isset($payload['access_token']); @@ -63,7 +64,10 @@ public function getAuthenticatedUser(OAuthAccessDTO $credentials): TwitchOAuthDT accessToken: $credentials->accessToken, )); - return TwitchOAuthDTO::make($credentials, $response->json()); + /** @var array $payload */ + $payload = $response->json(); + + return TwitchOAuthDTO::make($credentials, $payload); } private function callbackUrl(): string diff --git a/app-modules/integration-whatsapp/src/Ingest/Http/Middleware/VerifyWhatsAppSignature.php b/app-modules/integration-whatsapp/src/Ingest/Http/Middleware/VerifyWhatsAppSignature.php index 875b3e3df..602f75af3 100644 --- a/app-modules/integration-whatsapp/src/Ingest/Http/Middleware/VerifyWhatsAppSignature.php +++ b/app-modules/integration-whatsapp/src/Ingest/Http/Middleware/VerifyWhatsAppSignature.php @@ -11,6 +11,9 @@ final class VerifyWhatsAppSignature { + /** + * @param Closure(Request): Response $next + */ public function handle(Request $request, Closure $next): Response { $secret = config('whatsapp.webhook_secret'); diff --git a/app-modules/moderation/src/Platform/PlatformRegistry.php b/app-modules/moderation/src/Platform/PlatformRegistry.php index 316ca87af..4e5bec422 100644 --- a/app-modules/moderation/src/Platform/PlatformRegistry.php +++ b/app-modules/moderation/src/Platform/PlatformRegistry.php @@ -36,7 +36,13 @@ public function resolve(Platform $platform): ModerationPlatformContract throw new RuntimeException('No adapter registered for platform: '.$platform->value); } - return resolve($class); + $adapter = app()->make($class); + + if (!$adapter instanceof ModerationPlatformContract) { + throw new RuntimeException('Resolved adapter for platform '.$platform->value.' does not implement ModerationPlatformContract'); + } + + return $adapter; } public function has(Platform $platform): bool diff --git a/app-modules/panel-admin/src/Filament/Resources/ExternalIdentities/RelationManagers/MessagesRelationManager.php b/app-modules/panel-admin/src/Filament/Resources/ExternalIdentities/RelationManagers/MessagesRelationManager.php index 593e2e10d..d390c2225 100644 --- a/app-modules/panel-admin/src/Filament/Resources/ExternalIdentities/RelationManagers/MessagesRelationManager.php +++ b/app-modules/panel-admin/src/Filament/Resources/ExternalIdentities/RelationManagers/MessagesRelationManager.php @@ -278,11 +278,17 @@ public function table(Table $table): Table ], ]); + /** @var ViolationType $reason */ + $reason = $data['reason']; + + /** @var string|null $details */ + $details = $data['details'] ?? null; + resolve(SubmitReport::class)->handle( reporter: auth()->user(), contentDTO: $contentDTO, - reason: $data['reason'], - details: $data['details'] ?? null, + reason: $reason, + details: $details, platform: Platform::Web, ); }) diff --git a/app-modules/panel-admin/src/Http/Middleware/ApplyTenantScopes.php b/app-modules/panel-admin/src/Http/Middleware/ApplyTenantScopes.php index 2f1ef43ba..ed270f022 100644 --- a/app-modules/panel-admin/src/Http/Middleware/ApplyTenantScopes.php +++ b/app-modules/panel-admin/src/Http/Middleware/ApplyTenantScopes.php @@ -7,11 +7,15 @@ use Closure; use Filament\Facades\Filament; use Illuminate\Database\Eloquent\Builder; +use Illuminate\Database\Eloquent\Model; use Illuminate\Http\Request; use Symfony\Component\HttpFoundation\Response; class ApplyTenantScopes { + /** + * @param Closure(Request): Response $next + */ public function handle(Request $request, Closure $next): Response { $tenant = Filament::getTenant(); @@ -20,7 +24,10 @@ public function handle(Request $request, Closure $next): Response return $next($request); } - foreach (config('panel-admin.tenant_scoped_models', []) as $model) { + /** @var array> $models */ + $models = config('panel-admin.tenant_scoped_models', []); + + foreach ($models as $model) { $model::addGlobalScope( 'tenant', fn (Builder $query) => $query->whereBelongsTo($tenant), diff --git a/app-modules/panel-admin/src/Marketing/Pages/Discord/Dashboard/Queries/ActivityPerDay.php b/app-modules/panel-admin/src/Marketing/Pages/Discord/Dashboard/Queries/ActivityPerDay.php index 43ebb12c7..d8defa6d0 100644 --- a/app-modules/panel-admin/src/Marketing/Pages/Discord/Dashboard/Queries/ActivityPerDay.php +++ b/app-modules/panel-admin/src/Marketing/Pages/Discord/Dashboard/Queries/ActivityPerDay.php @@ -20,7 +20,8 @@ public function get(): Collection $tz = config('app.display_timezone'); $start = Date::now($tz)->subDays($this->rangeDays)->startOfDay()->utc(); - return DB::table('messages') + /** @var Collection $rows */ + $rows = DB::table('messages') ->selectRaw('(sent_at AT TIME ZONE ?)::date AS day', [$tz]) ->selectRaw('COUNT(*) AS total_messages') ->selectRaw('COUNT(DISTINCT external_identity_id) AS unique_users') @@ -28,11 +29,12 @@ public function get(): Collection ->whereNotNull('sent_at') ->groupBy('day') ->orderBy('day') - ->get() - ->map(fn (object $row): array => [ - 'day' => (string) $row->day, - 'msgs' => (int) $row->total_messages, - 'users' => (int) $row->unique_users, - ]); + ->get(); + + return $rows->map(fn (object $row): array => [ + 'day' => (string) $row->day, + 'msgs' => (int) $row->total_messages, + 'users' => (int) $row->unique_users, + ]); } } diff --git a/app-modules/panel-admin/src/Marketing/Pages/Discord/Dashboard/Queries/MessageHeatmap.php b/app-modules/panel-admin/src/Marketing/Pages/Discord/Dashboard/Queries/MessageHeatmap.php index aff6633e4..021b71aa6 100644 --- a/app-modules/panel-admin/src/Marketing/Pages/Discord/Dashboard/Queries/MessageHeatmap.php +++ b/app-modules/panel-admin/src/Marketing/Pages/Discord/Dashboard/Queries/MessageHeatmap.php @@ -4,6 +4,7 @@ namespace He4rt\PanelAdmin\Marketing\Pages\Discord\Dashboard\Queries; +use Illuminate\Support\Collection; use Illuminate\Support\Facades\Date; use Illuminate\Support\Facades\DB; @@ -19,7 +20,8 @@ public function get(): array $tz = config('app.display_timezone'); $start = Date::now($tz)->subDays($this->rangeDays)->startOfDay()->utc(); - return DB::table('messages') + /** @var Collection $rows */ + $rows = DB::table('messages') ->selectRaw('EXTRACT(DOW FROM sent_at AT TIME ZONE ?)::int AS dow', [$tz]) ->selectRaw('EXTRACT(HOUR FROM sent_at AT TIME ZONE ?)::int AS hour', [$tz]) ->selectRaw('COUNT(*) AS total') @@ -28,12 +30,12 @@ public function get(): array ->groupBy('dow', 'hour') ->orderBy('dow') ->orderBy('hour') - ->get() - ->map(fn (object $row): array => [ - 'row' => ((int) $row->dow + 6) % 7, // DOW 0=Sun → row Mon=0..Sun=6 - 'col' => (int) $row->hour, - 'value' => (int) $row->total, - ]) - ->all(); + ->get(); + + return $rows->map(fn (object $row): array => [ + 'row' => ((int) $row->dow + 6) % 7, // DOW 0=Sun → row Mon=0..Sun=6 + 'col' => (int) $row->hour, + 'value' => (int) $row->total, + ])->all(); } } diff --git a/app-modules/panel-admin/src/Marketing/Pages/Discord/Dashboard/Queries/PeriodStats.php b/app-modules/panel-admin/src/Marketing/Pages/Discord/Dashboard/Queries/PeriodStats.php index 8b220bb90..d5700e00f 100644 --- a/app-modules/panel-admin/src/Marketing/Pages/Discord/Dashboard/Queries/PeriodStats.php +++ b/app-modules/panel-admin/src/Marketing/Pages/Discord/Dashboard/Queries/PeriodStats.php @@ -114,6 +114,7 @@ private function queryMessageStats(array $subdivisions): array SQL, [$startsLiteral, $endsLiteral]); $byOrdinal = []; + /** @var object{idx: int, msgs: int, users: int} $row */ foreach ($rows as $row) { $byOrdinal[(int) $row->idx] = [ 'msgs' => (int) $row->msgs, @@ -153,6 +154,7 @@ private function queryVoiceStats(array $subdivisions): array SQL, [$startsLiteral, $endsLiteral]); $byOrdinal = []; + /** @var object{idx: int, joins: int} $row */ foreach ($rows as $row) { $byOrdinal[(int) $row->idx] = (int) $row->joins; } diff --git a/app-modules/panel-admin/src/Marketing/Pages/Discord/Dashboard/Queries/TopChannels.php b/app-modules/panel-admin/src/Marketing/Pages/Discord/Dashboard/Queries/TopChannels.php index f4791aa7a..42fcada46 100644 --- a/app-modules/panel-admin/src/Marketing/Pages/Discord/Dashboard/Queries/TopChannels.php +++ b/app-modules/panel-admin/src/Marketing/Pages/Discord/Dashboard/Queries/TopChannels.php @@ -20,7 +20,8 @@ public function get(): Collection $tz = config('app.display_timezone'); $start = Date::now($tz)->subDays($this->rangeDays)->startOfDay()->utc(); - return DB::table('messages') + /** @var Collection $rows */ + $rows = DB::table('messages') ->select('channel_id') ->selectRaw('COUNT(*) AS total_messages') ->selectRaw('COUNT(DISTINCT external_identity_id) AS unique_users') @@ -29,11 +30,12 @@ public function get(): Collection ->groupBy('channel_id') ->orderByDesc('total_messages') ->limit(10) - ->get() - ->map(fn (object $row): array => [ - 'channel_id' => (string) $row->channel_id, - 'total_messages' => (int) $row->total_messages, - 'unique_users' => (int) $row->unique_users, - ]); + ->get(); + + return $rows->map(fn (object $row): array => [ + 'channel_id' => (string) $row->channel_id, + 'total_messages' => (int) $row->total_messages, + 'unique_users' => (int) $row->unique_users, + ]); } } diff --git a/app-modules/panel-admin/src/Marketing/Pages/Discord/Dashboard/Queries/VoiceHeatmap.php b/app-modules/panel-admin/src/Marketing/Pages/Discord/Dashboard/Queries/VoiceHeatmap.php index d7b770976..478a1a4ce 100644 --- a/app-modules/panel-admin/src/Marketing/Pages/Discord/Dashboard/Queries/VoiceHeatmap.php +++ b/app-modules/panel-admin/src/Marketing/Pages/Discord/Dashboard/Queries/VoiceHeatmap.php @@ -4,6 +4,7 @@ namespace He4rt\PanelAdmin\Marketing\Pages\Discord\Dashboard\Queries; +use Illuminate\Support\Collection; use Illuminate\Support\Facades\Date; use Illuminate\Support\Facades\DB; @@ -19,7 +20,8 @@ public function get(): array $tz = config('app.display_timezone'); $start = Date::now($tz)->subDays($this->rangeDays)->startOfDay()->utc(); - return DB::table('voice_messages') + /** @var Collection $rows */ + $rows = DB::table('voice_messages') ->selectRaw('EXTRACT(DOW FROM occurred_at AT TIME ZONE ?)::int AS dow', [$tz]) ->selectRaw('EXTRACT(HOUR FROM occurred_at AT TIME ZONE ?)::int AS hour', [$tz]) ->selectRaw('COUNT(*) AS total') @@ -29,12 +31,12 @@ public function get(): array ->groupBy('dow', 'hour') ->orderBy('dow') ->orderBy('hour') - ->get() - ->map(fn (object $row): array => [ - 'row' => ((int) $row->dow + 6) % 7, // DOW 0=Sun → row Mon=0..Sun=6 - 'col' => (int) $row->hour, - 'value' => (int) $row->total, - ]) - ->all(); + ->get(); + + return $rows->map(fn (object $row): array => [ + 'row' => ((int) $row->dow + 6) % 7, // DOW 0=Sun → row Mon=0..Sun=6 + 'col' => (int) $row->hour, + 'value' => (int) $row->total, + ])->all(); } } diff --git a/app-modules/panel-admin/src/Marketing/Pages/Discord/Dashboard/Queries/VoicePerDay.php b/app-modules/panel-admin/src/Marketing/Pages/Discord/Dashboard/Queries/VoicePerDay.php index 9cf832b5d..611b136a8 100644 --- a/app-modules/panel-admin/src/Marketing/Pages/Discord/Dashboard/Queries/VoicePerDay.php +++ b/app-modules/panel-admin/src/Marketing/Pages/Discord/Dashboard/Queries/VoicePerDay.php @@ -20,7 +20,8 @@ public function get(): Collection $tz = config('app.display_timezone'); $start = Date::now($tz)->subDays($this->rangeDays)->startOfDay()->utc(); - return DB::table('voice_messages') + /** @var Collection $rows */ + $rows = DB::table('voice_messages') ->selectRaw('(occurred_at AT TIME ZONE ?)::date AS day', [$tz]) ->selectRaw('COUNT(*) AS total_joins') ->where('occurred_at', '>=', $start) @@ -28,11 +29,12 @@ public function get(): Collection ->where('state', 'joined') ->groupBy('day') ->orderBy('day') - ->get() - ->map(fn (object $row): array => [ - 'day' => (string) $row->day, - 'joins' => (int) $row->total_joins, - 'hours' => round((int) $row->total_joins * 0.75, 2), - ]); + ->get(); + + return $rows->map(fn (object $row): array => [ + 'day' => (string) $row->day, + 'joins' => (int) $row->total_joins, + 'hours' => round((int) $row->total_joins * 0.75, 2), + ]); } } diff --git a/app-modules/panel-admin/src/Marketing/Pages/MeetingShowcasePage.php b/app-modules/panel-admin/src/Marketing/Pages/MeetingShowcasePage.php index 123a5a0e0..e9869d625 100644 --- a/app-modules/panel-admin/src/Marketing/Pages/MeetingShowcasePage.php +++ b/app-modules/panel-admin/src/Marketing/Pages/MeetingShowcasePage.php @@ -11,6 +11,7 @@ use He4rt\Activity\Message\Models\Message; use He4rt\Identity\ExternalIdentity\Models\ExternalIdentity; use He4rt\PanelAdmin\Marketing\MarketingCluster; +use Illuminate\Support\Collection; use Illuminate\Support\Facades\Date; use Illuminate\Support\Facades\DB; @@ -59,6 +60,7 @@ public function loadParticipants(): void $start = Date::parse($this->startDate, $tz)->utc(); $end = Date::parse($this->endDate, $tz)->utc(); + /** @var Collection $messageStats */ $messageStats = Message::query() ->where('channel_id', $this->channelId) ->whereBetween('sent_at', [$start, $end]) @@ -78,7 +80,9 @@ public function loadParticipants(): void $this->participants = $messageStats->map(function (Message $stat) use ($identities): array { $identity = $identities->get($stat->external_identity_id); - return $this->extractDiscordData($identity, (int) $stat->total_messages); // @phpstan-ignore property.notFound + $totalMessages = (int) $stat->getAttribute('total_messages'); + + return $this->extractDiscordData($identity, $totalMessages); })->all(); $this->loaded = true; diff --git a/app-modules/panel-admin/src/Moderation/Livewire/AppealQueue.php b/app-modules/panel-admin/src/Moderation/Livewire/AppealQueue.php index 381a36f3e..21a467a03 100644 --- a/app-modules/panel-admin/src/Moderation/Livewire/AppealQueue.php +++ b/app-modules/panel-admin/src/Moderation/Livewire/AppealQueue.php @@ -19,6 +19,7 @@ use He4rt\Moderation\Enums\AppealStatus; use Illuminate\Contracts\View\Factory; use Illuminate\Contracts\View\View; +use Illuminate\Database\Eloquent\Relations\Relation; use Illuminate\Support\Collection; use Livewire\Attributes\Computed; use Livewire\Component; @@ -60,12 +61,13 @@ public function selectedAppeal(): ?ModerationAppeal return null; } + /** @var ModerationAppeal|null */ return ModerationAppeal::query() ->with([ 'appellant', 'reviewer', 'action.moderator', - 'action.case' => fn ($q) => $q->with(['author', 'reports.reporter']), + 'action.case' => fn (Relation $q) => $q->with(['author', 'reports.reporter']), ]) ->find($this->selectedAppealId); } @@ -102,11 +104,14 @@ public function upholdAction(): Action } try { + /** @var string $reviewerNotes */ + $reviewerNotes = $data['reviewer_notes']; + resolve(ReviewAppeal::class)->handle( $appeal, auth()->user(), AppealStatus::Upheld, - $data['reviewer_notes'], + $reviewerNotes, ); Notification::make() @@ -145,11 +150,14 @@ public function overturnAction(): Action } try { + /** @var string $reviewerNotes */ + $reviewerNotes = $data['reviewer_notes']; + resolve(ReviewAppeal::class)->handle( $appeal, auth()->user(), AppealStatus::Overturned, - $data['reviewer_notes'], + $reviewerNotes, ); Notification::make() diff --git a/app-modules/panel-admin/src/Moderation/Livewire/ModerationDashboardLivewire.php b/app-modules/panel-admin/src/Moderation/Livewire/ModerationDashboardLivewire.php index eccce4994..6c725cb92 100644 --- a/app-modules/panel-admin/src/Moderation/Livewire/ModerationDashboardLivewire.php +++ b/app-modules/panel-admin/src/Moderation/Livewire/ModerationDashboardLivewire.php @@ -220,7 +220,8 @@ public function moderatorStats(): Collection { $start = $this->periodStart; - return DB::table('moderation_actions') + /** @var Collection $rows */ + $rows = DB::table('moderation_actions') ->join('users', 'moderation_actions.moderator_id', '=', 'users.id') ->join('moderation_cases', 'moderation_actions.case_id', '=', 'moderation_cases.id') ->where('moderation_actions.created_at', '>=', $start) @@ -231,22 +232,26 @@ public function moderatorStats(): Collection ->groupBy('users.id', 'users.username') ->orderByDesc('total_cases') ->limit(10) - ->get() - ->map(static function (object $row) use ($start): object { - $actionIds = ModerationAction::query() - ->where('moderator_id', $row->id) - ->where('created_at', '>=', $start) - ->pluck('id'); + ->get(); - $overturned = ModerationAppeal::query() - ->whereIn('action_id', $actionIds) - ->where('status', 'overturned') - ->count(); + return $rows->map(static function (stdClass $row) use ($start): stdClass { + /** @var int $totalCases */ + $totalCases = $row->total_cases; - $row->overturn_rate = $row->total_cases > 0 ? (int) round(($overturned / $row->total_cases) * 100) : 0; + $actionIds = ModerationAction::query() + ->where('moderator_id', $row->id) + ->where('created_at', '>=', $start) + ->pluck('id'); - return $row; - }); + $overturned = ModerationAppeal::query() + ->whereIn('action_id', $actionIds) + ->where('status', 'overturned') + ->count(); + + $row->overturn_rate = $totalCases > 0 ? (int) round(($overturned / $totalCases) * 100) : 0; + + return $row; + }); } #[Computed] @@ -334,7 +339,8 @@ public function repeatOffenders(): Collection ->orderByDesc('offense_count') ->limit(5) ->get() - ->map(static function (object $row): object { + ->map(/** @param object{author_id: int, offense_count: int} $row */ static function (object $row): object { + /** @var object{username: string}|null $user */ $user = DB::table('users')->where('id', $row->author_id)->first(['username']); $row->username = $user->username ?? 'unknown'; @@ -358,6 +364,7 @@ public function activityHeatmap(): array $grid = array_fill(0, 7, array_fill(0, 24, 0)); + /** @var object{dow: int, hour: int, total: int} $row */ foreach ($dbData as $row) { $grid[(int) $row->dow][(int) $row->hour] = (int) $row->total; } diff --git a/app-modules/panel-admin/src/Moderation/Resources/ModerationAppealResource.php b/app-modules/panel-admin/src/Moderation/Resources/ModerationAppealResource.php index c3ac259ca..6ce5634ef 100644 --- a/app-modules/panel-admin/src/Moderation/Resources/ModerationAppealResource.php +++ b/app-modules/panel-admin/src/Moderation/Resources/ModerationAppealResource.php @@ -5,6 +5,7 @@ namespace He4rt\PanelAdmin\Moderation\Resources; use BackedEnum; +use DateTimeInterface; use Filament\Resources\Resource; use Filament\Support\Icons\Heroicon; use Filament\Tables\Columns\TextColumn; @@ -64,7 +65,7 @@ public static function table(Table $table): Table ->label('Reviewer'), TextColumn::make('sla_deadline') ->dateTime() - ->color(fn ($state) => $state && now()->gt($state) ? 'danger' : null), + ->color(fn (DateTimeInterface|string|null $state) => $state && now()->gt($state) ? 'danger' : null), TextColumn::make('created_at') ->dateTime(), ]) diff --git a/app-modules/panel-admin/src/Moderation/Resources/ModerationRuleResource.php b/app-modules/panel-admin/src/Moderation/Resources/ModerationRuleResource.php index 52c7570ad..389ef64c4 100644 --- a/app-modules/panel-admin/src/Moderation/Resources/ModerationRuleResource.php +++ b/app-modules/panel-admin/src/Moderation/Resources/ModerationRuleResource.php @@ -115,7 +115,10 @@ public static function testRuleAction(): Action ->rows(4), ]) ->action(static function (array $data, ModerationRule $record): void { - if ($record->matches($data['test_input'])) { + /** @var string $testInput */ + $testInput = $data['test_input']; + + if ($record->matches($testInput)) { Notification::make() ->success() ->title(__('panel-admin::moderation.rules.actions.test_match', [ diff --git a/app-modules/panel-admin/src/Moderation/Widgets/ModeratorPerformanceWidget.php b/app-modules/panel-admin/src/Moderation/Widgets/ModeratorPerformanceWidget.php index e765126b6..c645af99f 100644 --- a/app-modules/panel-admin/src/Moderation/Widgets/ModeratorPerformanceWidget.php +++ b/app-modules/panel-admin/src/Moderation/Widgets/ModeratorPerformanceWidget.php @@ -12,6 +12,7 @@ use He4rt\Moderation\Appeals\ModerationAppeal; use He4rt\Moderation\Enforcement\ModerationAction; use He4rt\PanelAdmin\Moderation\Widgets\Concerns\ResolvesFilterPeriod; +use Illuminate\Database\Query\Builder; class ModeratorPerformanceWidget extends TableWidget { @@ -32,7 +33,7 @@ public function table(Table $table): Table return $table ->query( User::query() - ->whereIn('id', static function ($query) use ($start): void { + ->whereIn('id', static function (Builder $query) use ($start): void { $query->select('moderator_id') ->from('moderation_actions') ->where('created_at', '>=', $start) diff --git a/app-modules/panel-app/phpstan.ignore.neon b/app-modules/panel-app/phpstan.ignore.neon index 8e3469166..f51e71c3f 100644 --- a/app-modules/panel-app/phpstan.ignore.neon +++ b/app-modules/panel-app/phpstan.ignore.neon @@ -1,17 +1,2 @@ parameters: - ignoreErrors: - - - message: '#^Access to an undefined property He4rt\\PanelApp\\Livewire\\Timeline\\Composer::\$form\.$#' - identifier: property.notFound - count: 3 - path: src/Livewire/Timeline/Composer.php - - - message: '#^Access to an undefined property Illuminate\\Database\\Eloquent\\Model::\$id\.$#' - identifier: property.notFound - count: 1 - path: src/Livewire/Timeline/Composer.php - - - message: '#^Access to an undefined property He4rt\\PanelApp\\Livewire\\Timeline\\ReplyComposer::\$form\.$#' - identifier: property.notFound - count: 3 - path: src/Livewire/Timeline/ReplyComposer.php + ignoreErrors: [] diff --git a/app-modules/panel-app/src/Livewire/Timeline/Composer.php b/app-modules/panel-app/src/Livewire/Timeline/Composer.php index c3362c5ba..288f0fb28 100644 --- a/app-modules/panel-app/src/Livewire/Timeline/Composer.php +++ b/app-modules/panel-app/src/Livewire/Timeline/Composer.php @@ -16,6 +16,9 @@ use Illuminate\View\View; use Livewire\Component; +/** + * @property-read Schema $form + */ final class Composer extends Component implements HasSchemas { use InteractsWithSchemas; @@ -57,16 +60,18 @@ public function form(Schema $schema): Schema public function post(): void { + /** @var array{content: string, images?: list} $state */ $state = $this->form->getState(); - $tenant = Filament::getTenant(); + /** @var string $tenantId */ + $tenantId = Filament::getTenant()->getKey(); /** @var User $user */ $user = auth()->user(); resolve(CreatePost::class)->handle(new CreatePostDTO( userId: $user->id, - tenantId: $tenant->id, + tenantId: $tenantId, content: $state['content'], images: $state['images'] ?? [], )); diff --git a/app-modules/panel-app/src/Livewire/Timeline/PostShow.php b/app-modules/panel-app/src/Livewire/Timeline/PostShow.php index 550a6c547..1157445b2 100644 --- a/app-modules/panel-app/src/Livewire/Timeline/PostShow.php +++ b/app-modules/panel-app/src/Livewire/Timeline/PostShow.php @@ -8,6 +8,7 @@ use He4rt\Activity\Timeline\Timeline; use He4rt\Identity\User\Models\User; use Illuminate\Container\Attributes\CurrentUser; +use Illuminate\Database\Eloquent\Relations\Relation; use Illuminate\View\View; use Livewire\Attributes\Locked; use Livewire\Attributes\On; @@ -48,7 +49,7 @@ public function render(): View 'user', 'postable', 'reactions', - 'children' => fn ($q) => $q->with('user', 'postable')->latest(), + 'children' => fn (Relation $q) => $q->with('user', 'postable')->latest(), ]) ->withCount('children', 'reactions') ->firstOrFail(); diff --git a/app-modules/panel-app/src/Livewire/Timeline/ReplyComposer.php b/app-modules/panel-app/src/Livewire/Timeline/ReplyComposer.php index 925785df4..c09b3f24d 100644 --- a/app-modules/panel-app/src/Livewire/Timeline/ReplyComposer.php +++ b/app-modules/panel-app/src/Livewire/Timeline/ReplyComposer.php @@ -16,6 +16,9 @@ use Livewire\Attributes\Locked; use Livewire\Component; +/** + * @property-read Schema $form + */ final class ReplyComposer extends Component implements HasSchemas { use InteractsWithSchemas; @@ -61,14 +64,18 @@ public function form(Schema $schema): Schema public function reply(): void { + /** @var array{content: string, images?: list} $state */ $state = $this->form->getState(); /** @var User $user */ $user = auth()->user(); + /** @var string $tenantId */ + $tenantId = filament()->getTenant()->getKey(); + resolve(CreateReply::class)->handle(new CreateReplyDTO( userId: $user->id, - tenantId: filament()->getTenant()->getKey(), + tenantId: $tenantId, parentTimelineId: $this->timelineId, content: $state['content'], images: $state['images'] ?? [], diff --git a/app-modules/panel-app/src/Pages/ProfilePage.php b/app-modules/panel-app/src/Pages/ProfilePage.php index 76907ad2d..559a7c5a8 100644 --- a/app-modules/panel-app/src/Pages/ProfilePage.php +++ b/app-modules/panel-app/src/Pages/ProfilePage.php @@ -307,20 +307,28 @@ public function initials(): string public function avatarPreviewUrl(): ?string { if ($this->avatarUpload instanceof TemporaryUploadedFile) { + /** @var string */ return $this->avatarUpload->temporaryUrl(); } - return auth()->user()->getFirstMediaUrl('avatar') ?: null; + /** @var User $user */ + $user = auth()->user(); + + return $user->getFirstMediaUrl('avatar') ?: null; } #[Computed] public function coverPreviewUrl(): ?string { if ($this->coverUpload instanceof TemporaryUploadedFile) { + /** @var string */ return $this->coverUpload->temporaryUrl(); } - return auth()->user()->getFirstMediaUrl('cover') ?: null; + /** @var User $user */ + $user = auth()->user(); + + return $user->getFirstMediaUrl('cover') ?: null; } public function removeAvatar(): void diff --git a/app-modules/portal/src/Livewire/HeroSection.php b/app-modules/portal/src/Livewire/HeroSection.php index 536562863..02042dc45 100644 --- a/app-modules/portal/src/Livewire/HeroSection.php +++ b/app-modules/portal/src/Livewire/HeroSection.php @@ -78,7 +78,12 @@ private function fetchAvatars(): array return []; } - $githubHandle = fn (array $metadata): ?string => $metadata['username'] ?? $metadata['name'] ?? null; + /** @param array $metadata */ + $githubHandle = static function (array $metadata): ?string { + $handle = $metadata['username'] ?? $metadata['name'] ?? null; + + return is_string($handle) ? $handle : null; + }; return ExternalIdentity::query() ->where('provider', IdentityProvider::GitHub) diff --git a/app-modules/profile/src/Models/Profile.php b/app-modules/profile/src/Models/Profile.php index 119305691..ed4adf8f3 100644 --- a/app-modules/profile/src/Models/Profile.php +++ b/app-modules/profile/src/Models/Profile.php @@ -91,12 +91,11 @@ protected static function newFactory(): ProfileFactory } /** - * @return Attribute|null, array|null> + * @return Attribute|null> */ protected function socialLinks(): Attribute { - /** @phpstan-ignore return.type */ - return Attribute::set(static function (?array $value): ?string { + return Attribute::make(set: /** @param array|null $value */ static function (?array $value): ?string { if ($value === null) { return null; } diff --git a/app/Console/Commands/AnalyzeDiscordProfiles.php b/app/Console/Commands/AnalyzeDiscordProfiles.php index 8f53e8f29..8fb15595e 100644 --- a/app/Console/Commands/AnalyzeDiscordProfiles.php +++ b/app/Console/Commands/AnalyzeDiscordProfiles.php @@ -24,8 +24,11 @@ public function handle(): void intro('Discord Profiles — Social Connections Report'); // Load all chunk files - $files = collect(Storage::disk('local')->files('discord')) - ->filter(fn (string $f) => str_contains($f, 'profiles_chunk_')) + /** @var list $allFiles */ + $allFiles = Storage::disk('local')->files('discord'); + + $files = collect($allFiles) + ->filter(fn (string $f): bool => str_contains($f, 'profiles_chunk_')) ->sort() ->values(); diff --git a/app/Console/Commands/CommunityReport.php b/app/Console/Commands/CommunityReport.php index 0f8ca16eb..f272687f6 100644 --- a/app/Console/Commands/CommunityReport.php +++ b/app/Console/Commands/CommunityReport.php @@ -10,6 +10,7 @@ use Illuminate\Console\Attributes\Signature; use Illuminate\Console\Command; use Illuminate\Contracts\Database\Query\Builder; +use Illuminate\Support\Collection; use Illuminate\Support\Facades\DB; use Illuminate\Support\Facades\Storage; @@ -91,6 +92,7 @@ private function reportUserProfileCompleteness(): void $withInfo = DB::table('user_information')->distinct('user_id')->count('user_id'); + /** @var object{github: int, linkedin: int, birthdate: int, about: int} $infoFills */ $infoFills = DB::table('user_information') ->selectRaw(' COUNT(github_url) as github, @@ -116,6 +118,7 @@ private function reportUserProfileCompleteness(): void ], ); + /** @var Collection $topCountries */ $topCountries = DB::table('user_address') ->select('country', DB::raw('COUNT(*) as cnt')) ->whereNotNull('country') @@ -252,6 +255,7 @@ private function reportGamification(): void } } + /** @var object{p25: float, p50: float, p75: float, p90: float, p99: float, avg_xp: float, max_xp: int} $percentiles */ $percentiles = DB::selectOne(' SELECT percentile_cont(0.25) WITHIN GROUP (ORDER BY experience) as p25, @@ -345,6 +349,7 @@ private function reportBadges(): void ], ); + /** @var Collection $topBadges */ $topBadges = DB::table('characters_badges') ->join('badges', 'badges.id', '=', 'characters_badges.badge_id') ->select('badges.name', DB::raw('COUNT(*) as claims')) @@ -384,6 +389,7 @@ private function reportEconomy(): void return; } + /** @var Collection $currencyStats */ $currencyStats = DB::table('wallets') ->select( 'currency', @@ -407,6 +413,7 @@ private function reportEconomy(): void ])->all(), ); + /** @var Collection $txByType */ $txByType = DB::table('transactions') ->select('type', DB::raw('COUNT(*) as cnt'), DB::raw('SUM(amount) as total_amount')) ->groupBy('type') @@ -455,6 +462,7 @@ private function reportMessagesAndVoice(): void $userModelType = User::class; + /** @var object{unique_users: int, avg_msgs: int, median_msgs: int}|null $msgStats */ $msgStats = DB::selectOne(' SELECT COUNT(*) as unique_users, @@ -482,6 +490,7 @@ private function reportMessagesAndVoice(): void ], ); + /** @var Collection $topChannels */ $topChannels = DB::table('messages') ->select('channel_id', DB::raw('COUNT(*) as cnt')) ->whereNotNull('channel_id') @@ -561,6 +570,7 @@ private function reportEvents(): void ); } + /** @var Collection $topEvents */ $topEvents = DB::table('events') ->select('title', 'attendees_count', 'event_type', 'event_at') ->orderByDesc('attendees_count') @@ -627,6 +637,7 @@ private function reportMeetings(): void ], ); + /** @var Collection $topTypes */ $topTypes = DB::table('meetings') ->join('meeting_types', 'meeting_types.id', '=', 'meetings.meeting_type_id') ->select('meeting_types.name', DB::raw('COUNT(*) as cnt')) @@ -684,6 +695,7 @@ private function reportFeedback(): void ], ); + /** @var Collection $byType */ $byType = DB::table('feedbacks') ->select('type', DB::raw('COUNT(*) as cnt')) ->groupBy('type') @@ -729,6 +741,7 @@ private function reportSeasonalRankings(): void $totalRankings = DB::table('seasons_rankings')->count(); + /** @var object{id: int, name: string, started_at: string|null, ended_at: string|null}|null $currentSeason */ $currentSeason = DB::table('seasons') ->where(static function (Builder $q): void { $q->whereNull('ended_at') @@ -752,6 +765,7 @@ private function reportSeasonalRankings(): void $top10 = collect(); if ($currentSeason) { + /** @var Collection $top10 */ $top10 = DB::table('seasons_rankings') ->join('characters', 'characters.id', '=', 'seasons_rankings.character_id') ->join('users', 'users.id', '=', 'characters.user_id') @@ -772,9 +786,9 @@ private function reportSeasonalRankings(): void table( headers: ['#', 'Username', 'Level', 'XP', 'Messages'], rows: $top10->map(fn ($r) => [ - $r->ranking_position, + (string) $r->ranking_position, $r->username, - $r->level, + (string) $r->level, number_format($r->experience), number_format($r->messages_count), ])->all(), @@ -784,6 +798,7 @@ private function reportSeasonalRankings(): void // Fallback: live top 10 from characters table if ($top10->isEmpty()) { + /** @var Collection $liveTop */ $liveTop = DB::table('characters') ->join('users', 'users.id', '=', 'characters.user_id') ->select('users.username', 'characters.experience', 'characters.reputation') @@ -874,8 +889,10 @@ private function reportDiscordScrapeCorrelation(): void info('GitHub connections from scrape: '.$githubConnections->count()); + /** @var Collection $scrapedGithub */ $scrapedGithub = $githubConnections->keyBy('discord_id'); + /** @var Collection $platformGithub */ $platformGithub = DB::table('providers') ->join('user_information', 'providers.model_id', '=', 'user_information.user_id') ->where('providers.provider', 'discord') @@ -889,6 +906,7 @@ private function reportDiscordScrapeCorrelation(): void $matches = 0; $conflicts = 0; $newFromScrape = 0; + /** @var list $conflictDetails */ $conflictDetails = []; foreach ($scrapedGithub as $discordId => $conn) { diff --git a/app/Console/Commands/FixPostSwitchTimestampsCommand.php b/app/Console/Commands/FixPostSwitchTimestampsCommand.php index 09f3e0f85..42fc88fd9 100644 --- a/app/Console/Commands/FixPostSwitchTimestampsCommand.php +++ b/app/Console/Commands/FixPostSwitchTimestampsCommand.php @@ -62,6 +62,7 @@ public function handle(): int private function validateSchemaReady(): bool { + /** @var list $remaining */ $remaining = DB::select( "SELECT table_name, column_name FROM information_schema.columns @@ -81,7 +82,7 @@ private function validateSchemaReady(): bool table( headers: ['Table', 'Column'], - rows: array_map(fn ($row) => [$row->table_name, $row->column_name], $remaining), + rows: array_map(fn (object $row): array => [$row->table_name, $row->column_name], $remaining), ); return false; @@ -259,6 +260,7 @@ private function showSummary(array $allStats): void private function runVerification(): void { + /** @var list $remaining */ $remaining = DB::select( "SELECT table_name, column_name FROM information_schema.columns @@ -280,7 +282,7 @@ private function runVerification(): void table( headers: ['Table', 'Column'], - rows: array_map(fn ($row) => [$row->table_name, $row->column_name], $remaining), + rows: array_map(fn (object $row): array => [$row->table_name, $row->column_name], $remaining), ); } diff --git a/phpstan.ignore.neon b/phpstan.ignore.neon index 2218a1432..c337eb190 100644 --- a/phpstan.ignore.neon +++ b/phpstan.ignore.neon @@ -5,3 +5,4 @@ parameters: identifier: method.notFound count: 1 path: app/Providers/FilamentServiceProvider.php + diff --git a/phpstan.neon b/phpstan.neon index 2c5ec17c7..ac57cebb1 100644 --- a/phpstan.neon +++ b/phpstan.neon @@ -4,6 +4,8 @@ includes: parameters: level: 7 + checkImplicitMixed: true + paths: - app/