From d768217edc7ad5aa3c4d019066d3a57c66b17208 Mon Sep 17 00:00:00 2001 From: sayghteight Date: Wed, 12 Nov 2025 13:27:32 +0100 Subject: [PATCH 001/132] feat(modules): add modular system with example and armory modules - Implement BaseModuleServiceProvider for module management - Add MakeModuleCommand for module scaffolding - Include example and armory modules with routes, controllers and providers - Update composer.json autoload configuration --- app/Console/Commands/MakeModuleCommand.php | 116 ++++++++++++++++++ .../Http/Controllers/ArmoryController.php | 13 ++ app/Modules/Armory/Http/routes.php | 9 ++ .../Providers/ArmoryServiceProvider.php | 10 ++ app/Modules/Armory/module.json | 8 ++ .../Http/Controllers/ExampleController.php | 13 ++ app/Modules/Example/Http/routes.php | 5 + .../Providers/ExampleServiceProvider.php | 10 ++ app/Modules/Example/module.json | 12 ++ app/Providers/BaseModuleServiceProvider.php | 74 +++++++++++ bootstrap/providers.php | 1 + composer.json | 3 +- 12 files changed, 273 insertions(+), 1 deletion(-) create mode 100644 app/Console/Commands/MakeModuleCommand.php create mode 100644 app/Modules/Armory/Http/Controllers/ArmoryController.php create mode 100644 app/Modules/Armory/Http/routes.php create mode 100644 app/Modules/Armory/Providers/ArmoryServiceProvider.php create mode 100644 app/Modules/Armory/module.json create mode 100644 app/Modules/Example/Http/Controllers/ExampleController.php create mode 100644 app/Modules/Example/Http/routes.php create mode 100644 app/Modules/Example/Providers/ExampleServiceProvider.php create mode 100644 app/Modules/Example/module.json create mode 100644 app/Providers/BaseModuleServiceProvider.php diff --git a/app/Console/Commands/MakeModuleCommand.php b/app/Console/Commands/MakeModuleCommand.php new file mode 100644 index 0000000..3554717 --- /dev/null +++ b/app/Console/Commands/MakeModuleCommand.php @@ -0,0 +1,116 @@ +argument('name')); + $path = base_path("app/Modules/{$name}"); + + if (File::exists($path) && !$this->option('force')) { + $this->error("The module [{$name}] already exists! Use --force to overwrite."); + return self::FAILURE; + } + + // Estructura de carpetas + $directories = [ + 'App/Services', + 'App/UseCases', + 'App/DTOs', + 'Domain/Models', + 'Domain/Interfaces', + 'Infrastructure/Repositories', + 'Http/Controllers', + 'Http/Requests', + 'Providers', + 'Resources/views', + 'Infrastructure/Database/migrations', + ]; + + foreach ($directories as $dir) { + File::makeDirectory("{$path}/{$dir}", 0755, true, true); + } + + // module.json + $moduleJson = [ + 'name' => $name, + 'enabled' => true, + 'routes' => true, + 'migrations' => true, + 'views' => true, + 'namespace' => "Modules\\{$name}" + ]; + + File::put("{$path}/module.json", json_encode($moduleJson, JSON_PRETTY_PRINT | JSON_UNESCAPED_SLASHES)); + + // routes.php + $routes = <<prefix(strtolower('{$name}')) + ->group(function () { + Route::get('/', [\\Modules\\{$name}\\Http\\Controllers\\{$name}Controller::class, 'index']); + }); + PHP; + File::put("{$path}/Http/routes.php", $routes); + + // Controller base + $controller = <<json(['message' => '{$name} module is working']); + } + } + PHP; + File::put("{$path}/Http/Controllers/{$name}Controller.php", $controller); + + // ServiceProvider + $provider = <<info("Module [{$name}] created successfully at app/Modules/{$name}"); + return self::SUCCESS; + } +} + diff --git a/app/Modules/Armory/Http/Controllers/ArmoryController.php b/app/Modules/Armory/Http/Controllers/ArmoryController.php new file mode 100644 index 0000000..e039839 --- /dev/null +++ b/app/Modules/Armory/Http/Controllers/ArmoryController.php @@ -0,0 +1,13 @@ +json(['message' => 'Armory module is working']); + } +} \ No newline at end of file diff --git a/app/Modules/Armory/Http/routes.php b/app/Modules/Armory/Http/routes.php new file mode 100644 index 0000000..422ab84 --- /dev/null +++ b/app/Modules/Armory/Http/routes.php @@ -0,0 +1,9 @@ +prefix(strtolower('Armory')) + ->group(function () { + Route::get('/', [\Modules\Armory\Http\Controllers\ArmoryController::class, 'index']); + }); \ No newline at end of file diff --git a/app/Modules/Armory/Providers/ArmoryServiceProvider.php b/app/Modules/Armory/Providers/ArmoryServiceProvider.php new file mode 100644 index 0000000..6b80901 --- /dev/null +++ b/app/Modules/Armory/Providers/ArmoryServiceProvider.php @@ -0,0 +1,10 @@ +json(['message' => 'Módulo Example funcionando']); + } +} diff --git a/app/Modules/Example/Http/routes.php b/app/Modules/Example/Http/routes.php new file mode 100644 index 0000000..6ab1818 --- /dev/null +++ b/app/Modules/Example/Http/routes.php @@ -0,0 +1,5 @@ +moduleName)) { + throw new \RuntimeException('Module name not defined in ' . static::class); + } + + $this->modulePath = base_path("app/Modules/{$this->moduleName}"); + $this->loadModuleConfig(); + + if (!($this->config['enabled'] ?? true)) { + return; + } + + if ($this->config['routes'] ?? false) { + $this->loadRoutes(); + } + + if ($this->config['migrations'] ?? false) { + $this->loadMigrations(); + } + + if ($this->config['views'] ?? false) { + $this->loadViews(); + } + } + + private function loadModuleConfig(): void + { + $configFile = $this->modulePath . '/module.json'; + if (File::exists($configFile)) { + $this->config = json_decode(File::get($configFile), true) ?? []; + } + } + + private function loadRoutes(): void + { + $routesPath = $this->modulePath . '/Http/routes.php'; + if (File::exists($routesPath)) { + Route::middleware('web') + ->namespace(($this->config['namespace'] ?? "Modules\\{$this->moduleName}") . '\\Http\\Controllers') + ->group($routesPath); + } + } + + private function loadMigrations(): void + { + $path = $this->modulePath . '/Infrastructure/Database/migrations'; + if (is_dir($path)) { + $this->loadMigrationsFrom($path); + } + } + + private function loadViews(): void + { + $path = $this->modulePath . '/Resources/views'; + if (is_dir($path)) { + $this->loadViewsFrom($path, strtolower($this->moduleName)); + } + } +} diff --git a/bootstrap/providers.php b/bootstrap/providers.php index df3195a..4ed90a5 100755 --- a/bootstrap/providers.php +++ b/bootstrap/providers.php @@ -4,4 +4,5 @@ App\Providers\AppServiceProvider::class, App\Providers\ArmoryServiceProvider::class, Spatie\Permission\PermissionServiceProvider::class, + Modules\Example\Providers\ExampleServiceProvider::class, ]; diff --git a/composer.json b/composer.json index d897428..771de4e 100755 --- a/composer.json +++ b/composer.json @@ -34,7 +34,8 @@ "psr-4": { "App\\": "app/", "Database\\Factories\\": "database/factories/", - "Database\\Seeders\\": "database/seeders/" + "Database\\Seeders\\": "database/seeders/", + "Modules\\": "app/Modules/" } }, "autoload-dev": { From c51a8d079d0076a0a9ad66737121c6258d2a7317 Mon Sep 17 00:00:00 2001 From: sayghteight Date: Wed, 12 Nov 2025 20:44:02 +0100 Subject: [PATCH 002/132] refactor(modules): remove armory module and implement dynamic module loading The Armory module was removed as part of transitioning to a dynamic module loading system. Added ModuleLoader helper and ModuleServiceProvider to automatically discover and register module service providers. --- app/Helpers/ModuleLoader.php | 45 +++++++++++++++++++ .../Http/Controllers/ArmoryController.php | 13 ------ app/Modules/Armory/Http/routes.php | 9 ---- .../Providers/ArmoryServiceProvider.php | 10 ----- app/Modules/Armory/module.json | 8 ---- app/Providers/ModuleServiceProvider.php | 16 +++++++ bootstrap/providers.php | 7 ++- 7 files changed, 66 insertions(+), 42 deletions(-) create mode 100644 app/Helpers/ModuleLoader.php delete mode 100644 app/Modules/Armory/Http/Controllers/ArmoryController.php delete mode 100644 app/Modules/Armory/Http/routes.php delete mode 100644 app/Modules/Armory/Providers/ArmoryServiceProvider.php delete mode 100644 app/Modules/Armory/module.json create mode 100644 app/Providers/ModuleServiceProvider.php diff --git a/app/Helpers/ModuleLoader.php b/app/Helpers/ModuleLoader.php new file mode 100644 index 0000000..d36c0a2 --- /dev/null +++ b/app/Helpers/ModuleLoader.php @@ -0,0 +1,45 @@ +json(['message' => 'Armory module is working']); - } -} \ No newline at end of file diff --git a/app/Modules/Armory/Http/routes.php b/app/Modules/Armory/Http/routes.php deleted file mode 100644 index 422ab84..0000000 --- a/app/Modules/Armory/Http/routes.php +++ /dev/null @@ -1,9 +0,0 @@ -prefix(strtolower('Armory')) - ->group(function () { - Route::get('/', [\Modules\Armory\Http\Controllers\ArmoryController::class, 'index']); - }); \ No newline at end of file diff --git a/app/Modules/Armory/Providers/ArmoryServiceProvider.php b/app/Modules/Armory/Providers/ArmoryServiceProvider.php deleted file mode 100644 index 6b80901..0000000 --- a/app/Modules/Armory/Providers/ArmoryServiceProvider.php +++ /dev/null @@ -1,10 +0,0 @@ -app->register($provider); + } + } +} diff --git a/bootstrap/providers.php b/bootstrap/providers.php index 4ed90a5..ae2545d 100755 --- a/bootstrap/providers.php +++ b/bootstrap/providers.php @@ -1,8 +1,11 @@ Date: Tue, 18 Nov 2025 18:25:22 +0100 Subject: [PATCH 003/132] feat(armory): implement modular armory feature with repository pattern - Add new Armory module with MVC structure - Implement TrinityCore repository for character data - Create service layer for business logic - Add blade templates for UI components - Move existing armory functionality to module - Update routes and service providers - Include wowhead integration for item tooltips --- .../Interfaces/ArmoryRepositoryInterface.php | 2 +- .../Http/Controllers/ArmoryController.php | 65 ++++ app/Modules/Armory/Http/routes.php | 12 + .../TrinityCoreArmoryRepository.php | 300 ++++++++++++++++++ .../Providers/ArmoryServiceProvider.php | 32 +- .../Resources/views/armory/index.blade.php | 166 ++++++++++ .../armory/partials/achievements.blade.php | 14 + .../armory/partials/equipment-slot.blade.php | 36 +++ .../views/armory/partials/equipment.blade.php | 80 +++++ .../armory/partials/professions.blade.php | 25 ++ .../Resources/views/armory/show.blade.php | 117 +++++++ .../Armory}/Services/ArmoryService.php | 16 +- app/Modules/Armory/module.json | 8 + app/Providers/AppServiceProvider.php | 4 +- routes/web.php | 10 +- 15 files changed, 844 insertions(+), 43 deletions(-) rename app/{ => Modules/Armory/Domain}/Interfaces/ArmoryRepositoryInterface.php (92%) create mode 100644 app/Modules/Armory/Http/Controllers/ArmoryController.php create mode 100644 app/Modules/Armory/Http/routes.php create mode 100644 app/Modules/Armory/Infrastructure/Repositories/TrinityCoreArmoryRepository.php rename app/{ => Modules/Armory}/Providers/ArmoryServiceProvider.php (66%) create mode 100644 app/Modules/Armory/Resources/views/armory/index.blade.php create mode 100644 app/Modules/Armory/Resources/views/armory/partials/achievements.blade.php create mode 100644 app/Modules/Armory/Resources/views/armory/partials/equipment-slot.blade.php create mode 100644 app/Modules/Armory/Resources/views/armory/partials/equipment.blade.php create mode 100644 app/Modules/Armory/Resources/views/armory/partials/professions.blade.php create mode 100644 app/Modules/Armory/Resources/views/armory/show.blade.php rename app/{ => Modules/Armory}/Services/ArmoryService.php (88%) create mode 100644 app/Modules/Armory/module.json diff --git a/app/Interfaces/ArmoryRepositoryInterface.php b/app/Modules/Armory/Domain/Interfaces/ArmoryRepositoryInterface.php similarity index 92% rename from app/Interfaces/ArmoryRepositoryInterface.php rename to app/Modules/Armory/Domain/Interfaces/ArmoryRepositoryInterface.php index 92c4fac..671d54a 100644 --- a/app/Interfaces/ArmoryRepositoryInterface.php +++ b/app/Modules/Armory/Domain/Interfaces/ArmoryRepositoryInterface.php @@ -1,6 +1,6 @@ 'armory::armory.index', + 'show' => 'armory::armory.show', + ]; + + protected ArmoryRepositoryInterface $armoryRepo; + protected WowheadParserService $wowheadParser; + protected ArmoryService $armoryService; + + public function __construct( + ArmoryRepositoryInterface $armoryRepo, + WowheadParserService $wowheadParser, + ArmoryService $armoryService + ) { + $this->armoryRepo = $armoryRepo; + $this->wowheadParser = $wowheadParser; + $this->armoryService = $armoryService; + } + + public function index(Request $request) + { + $q = $request->input('q'); + $faction = $request->input('faction') ?: null; + $realm = $request->input('realm') ?: 1; + $class = $request->input('class') ?: null; + $minLevel = $request->input('min_level') ?: null; + + $request->merge(['realm' => $realm]); + + $characters = ($q || $faction || $class || $minLevel) + ? $this->armoryService->searchCharacters($q, $faction, $class, $minLevel) + : collect(); + + return view($this->views['index'], [ + 'data' => $characters, + 'search' => $q ?? '', + 'realm' => $realm, + ]); + } + + public function show(int $guid, Request $request, ?int $realm = null) + { + $realm = $realm ?: $request->input('realm', 1); + $request->merge(['realm' => $realm]); + + $profile = $this->armoryService->getCharacterProfile($guid); + abort_if(empty($profile), 404); + + return view($this->views['show'], array_merge($profile, ['realm' => $realm])); + } +} \ No newline at end of file diff --git a/app/Modules/Armory/Http/routes.php b/app/Modules/Armory/Http/routes.php new file mode 100644 index 0000000..0aa2e79 --- /dev/null +++ b/app/Modules/Armory/Http/routes.php @@ -0,0 +1,12 @@ +group(function () { + Route::prefix('armory')->group(function () { + Route::get('/', [ArmoryController::class, 'index'])->name('armory'); + Route::get('{id}', [ArmoryController::class, 'show'])->where('id', '[0-9]+')->name('armory.show'); + Route::get('{id}/{realm?}', [ArmoryController::class, 'show'])->where('id', '[0-9]+')->where('realm', '[0-9]+')->name('armory.show.realm'); + }); +}); \ No newline at end of file diff --git a/app/Modules/Armory/Infrastructure/Repositories/TrinityCoreArmoryRepository.php b/app/Modules/Armory/Infrastructure/Repositories/TrinityCoreArmoryRepository.php new file mode 100644 index 0000000..28616d8 --- /dev/null +++ b/app/Modules/Armory/Infrastructure/Repositories/TrinityCoreArmoryRepository.php @@ -0,0 +1,300 @@ +realmConfig = $realmConfig; + } + + /** + * Get connection to characters database + * + * @return ?Connection Connection to characters database or null if connection failed + */ + protected function getCharacters(): ?Connection + { + if (!$this->characters) { + $this->characters = $this->connectToExternalDatabase($this->realmConfig['character_database'], 'characters'); + } + return $this->characters; + } + + /** + * Get connection to world database + * + * @return ?Connection Connection to world database or null if connection failed + */ + protected function getWorld(): ?Connection + { + if (!$this->world) { + $this->world = $this->connectToExternalDatabase($this->realmConfig['world_database'], 'world'); + } + return $this->world; + } + + /** + * Get all characters from characters database + * + * @return Collection Collection of all characters or empty collection if connection failed + */ + public function getAllCharacters() + { + $conn = $this->getCharacters(); + if (!$conn) return collect(); + + return $conn->table('characters')->get(); + } + + /** + * Search characters by name + * + * @param string $q Search query + * @return Collection Collection of characters matching the query or empty collection if connection failed + */ + public function search(string $q, ?string $faction, ?string $class, ?int $minLevel) + { + $conn = $this->getCharacters(); + if (!$conn) return collect(); + + $query = $conn->table('characters'); + + if ($q) { + $query->where('name', 'like', "%{$q}%"); + } + + if (!empty($faction)) { + $races = $faction === 'horde' + ? [2, 5, 6, 8, 9, 10] + : [1, 3, 4, 7, 11]; + + $query->whereIn('race', $races); + } + + if ($class) { + $query->where('class', $class); + } + if ($minLevel) { + $query->where('level', '>=', $minLevel); + } + + return $query->orderByDesc('level')->get(); + } + + /** + * Get character by GUID + * + * @param int $guid Character GUID + * @return Collection Collection of character or empty collection if connection failed + */ + public function getCharacter(int $guid) + { + $conn = $this->getCharacters(); + if (!$conn) return collect(); + + return $conn->table('characters')->where('guid', $guid)->first(); + } + + /** + * Get character items by GUID + * + * @param int $guid Character GUID + * @return Collection Collection of character items or empty collection if connection failed + */ + public function getCharacterItems(int $guid) + { + $characters = $this->getCharacters(); + $world = $this->getWorld(); + + if (!$characters || !$world) return collect(); + + $rows = $characters->table('character_inventory AS ci') + ->where('ci.guid', $guid) + ->whereBetween('ci.slot', [0, 18]) + ->join('item_instance AS ii', 'ii.guid', '=', 'ci.item') + ->orderBy('ci.slot') + ->get([ + 'ci.bag', + 'ci.slot', + 'ii.itemEntry', + ]); + + return $rows->map(function ($row) { + return [ + 'bag' => $row->bag, + 'slot' => $row->slot, + 'entry' => $row->itemEntry, + ]; + }); + } + + /** + * Get character achievements by GUID + * + * @param int $guid Character GUID + * @return Collection Collection of character achievements or empty collection if connection failed + */ + public function getAchievementsCharacter(int $guid) + { + $characters = $this->getCharacters(); + $world = $this->getWorld(); + + if (!$characters || !$world) return collect(); + + $rows = $characters->table('character_achievement') + ->where('guid', $guid) + ->whereNotIn('achievement', Achievement::BLOCK_ACHIEVEMENTS) + ->orderByDesc('date') + ->limit(10) + ->get([ + 'achievement', + 'date' + ]); + + return $rows->map(function ($row) { + return [ + 'id' => $row->achievement, + 'date' => $row->date, + ]; + }); + } + + /** + * Get guild by ID + * + * @param int $guildId Guild ID + * @return Collection Collection of guild or empty collection if connection failed + */ + public function getGuildByMember(int $memberGuid) + { + $conn = $this->getCharacters(); + if (!$conn) return collect(); + + return $conn->table('guild as g') + ->join('guild_member as gm', 'gm.guildid', '=', 'g.guildid') + ->where('gm.guid', $memberGuid) + ->first(); + } + + /** + * Get guild rank of member + * + * @param int $guildId Guild ID + * @param int $memberGuid Member GUID + * @return Collection Collection of guild rank or empty collection if connection failed + */ + public function getGuildRankMember(int $guildId, int $memberGuid) + { + $conn = $this->getCharacters(); + if (!$conn) return collect(); + + return $conn->table('guild_member as gm') + ->join('guild_rank as gr', 'gr.guildid', '=', 'gm.guildid') + ->where('gm.guildid', $guildId) + ->where('gm.guid', $memberGuid) + ->get([ + 'gr.rname', + ])->first(); + } + + /** + * Get Skill of character + * + * @param int $guid Character GUID + * @return Collection Collection of skill or empty collection if connection failed + */ + public function getSkillCharacter(int $guid) + { + $conn = $this->getCharacters(); + if (!$conn) return collect(); + + $skill = $conn->table('character_skills') + ->where('guid', $guid) + ->where('professionSlot', 0) + ->get([ + 'skill', + 'max', + 'value', + ]); + + return $skill->map(function ($row) { + return [ + 'id' => $row->skill, + 'name' => Professions::getName($row->skill), + 'type' => Professions::getType($row->skill), + 'icon' => Professions::getIcon($row->skill), + 'max' => $row->max, + 'value' => $row->value, + ]; + }); + } + + /** + * Get Arena Team of character + * + * @param int $guid Character GUID + * @return Collection Collection of arena team or empty collection if connection failed + */ + public function getArenaTeam(int $guid) + { + $conn = $this->getCharacters(); + if (!$conn) return collect(); + + $arenaTeam = $conn->table('arena_team') + ->join('arena_team_member as atm', 'atm.arenaTeamId', '=', 'arena_team.arenaTeamId') + ->where('atm.guid', $guid) + ->get([ + 'arena_team.arenaTeamId', + 'arena_team.name', + 'arena_team.rating', + 'atm.personalRating', + ]); + + return $arenaTeam->map(function ($row) { + return [ + 'id' => $row->arenaTeamId, + 'name' => $row->name, + 'points' => $row->rating, + 'personalRating' => $row->personalRating, + ]; + }); + } +} diff --git a/app/Providers/ArmoryServiceProvider.php b/app/Modules/Armory/Providers/ArmoryServiceProvider.php similarity index 66% rename from app/Providers/ArmoryServiceProvider.php rename to app/Modules/Armory/Providers/ArmoryServiceProvider.php index b0abd4a..5099ea6 100644 --- a/app/Providers/ArmoryServiceProvider.php +++ b/app/Modules/Armory/Providers/ArmoryServiceProvider.php @@ -1,18 +1,18 @@ app->bind(ArmoryRepositoryInterface::class, function ($app) { @@ -32,20 +32,12 @@ public function register(): void 'world_database' => json_decode($realm->world_database, true), ]; - $emulatorEnum = Emulator::from($realm->emulator); // lanza excepción si no existe + $emulatorEnum = Emulator::from($realm->emulator); return match ($emulatorEnum) { - Emulator::TRINITYCORE => new TrinityCoreArmoryRepository($realmConfig), + Emulator::TRINITYCORE => new TrinityCoreArmoryRepository($realmConfig), default => throw new \Exception("Unsupported emulator: $emulator") }; }); - } - - /** - * Bootstrap services. - */ - public function boot(): void - { - // - } -} + } +} \ No newline at end of file diff --git a/app/Modules/Armory/Resources/views/armory/index.blade.php b/app/Modules/Armory/Resources/views/armory/index.blade.php new file mode 100644 index 0000000..187de5b --- /dev/null +++ b/app/Modules/Armory/Resources/views/armory/index.blade.php @@ -0,0 +1,166 @@ +@extends('layouts.main') + +@section('content') + +
+
+
+

+ Character Armory +

+

Search and explore character profiles across all realms

+
+ + +
+
+
+ + +
+
+ + +
+
+ +
+
+ + +
+
+ + +
+
+ + +
+
+ + +
+
+
+ + +
+
+
+

Search Results

+

+ Found characters +

+
+
+ + @if(filled(request('q')) || filled(request('faction')) || filled(request('class')) || filled(request('min_level'))) + @if($data->count()) +
+ @foreach($data as $player) +
+
+ {{ $player->name }} +
+
+

{{ $player->name }}

+ {{ $player->level }} +
+

{{ App\Helpers\RealmHelper::getWoWConstant('race', $player->race) }} {{ App\Helpers\RealmHelper::getWoWConstant('class', $player->class) }}

+
+ <{{ $player->guild_name ?? 'N/A' }}> +
+
+
+ +
+
+ Realm + {{ $player->realm_name ?? 'N/A' }} +
+
+ Item Level + {{ $player->item_level ?? 'N/A' }} +
+
+ Achievement Points + {{ number_format($player->achievement_points ?? 0) }} +
+
+ +
+
+
Arena
+
{{ $player->arena_points ?? 0 }}
+
+
+
HKs
+
{{ number_format($player->honorable_kills ?? 0) }}
+
+
+ + + View Profile + +
+ @endforeach +
+ + +
+
+ @else +
+
+
+ + + +

No Character Found

+
+

We couldn't find any character matching your criteria. Please adjust your filters and try again.

+
+
+ @endif + @endif +
+@endsection \ No newline at end of file diff --git a/app/Modules/Armory/Resources/views/armory/partials/achievements.blade.php b/app/Modules/Armory/Resources/views/armory/partials/achievements.blade.php new file mode 100644 index 0000000..b594c14 --- /dev/null +++ b/app/Modules/Armory/Resources/views/armory/partials/achievements.blade.php @@ -0,0 +1,14 @@ + \ No newline at end of file diff --git a/app/Modules/Armory/Resources/views/armory/partials/equipment-slot.blade.php b/app/Modules/Armory/Resources/views/armory/partials/equipment-slot.blade.php new file mode 100644 index 0000000..8cb9ec4 --- /dev/null +++ b/app/Modules/Armory/Resources/views/armory/partials/equipment-slot.blade.php @@ -0,0 +1,36 @@ +@php + $qualityName = $equip['wowhead']['quality']['name'] ?? 'Common'; + $qualityClass = 'item-quality-' . strtolower($qualityName); + $icon = $equip['wowhead']['icon']['name'] ?? 'inv_misc_questionmark'; + $iconUrl = "https://wow.zamimg.com/images/wow/icons/large/{$icon}.jpg"; +@endphp + +
+ + + +
+
{{ $label }}
+
+ {{ $equip['wowhead']['name'] ?? 'No equipado' }} +
+ @if(isset($equip['wowhead']['level'])) +
Item Level {{ $equip['wowhead']['level'] }}
+ @endif +
+ + @if($equip && in_array(strtolower($qualityName), ['epic', 'legendary'])) +
+ +
+ @endif +
\ No newline at end of file diff --git a/app/Modules/Armory/Resources/views/armory/partials/equipment.blade.php b/app/Modules/Armory/Resources/views/armory/partials/equipment.blade.php new file mode 100644 index 0000000..94c441c --- /dev/null +++ b/app/Modules/Armory/Resources/views/armory/partials/equipment.blade.php @@ -0,0 +1,80 @@ +
+ @php + // Orden de slots (según el orden visual clásico de WoW) + $slotOrder = [ + 'Head', 'Neck', 'Shoulders', 'Back', 'Chest', 'Wrist', + 'Hands', 'Waist', 'Legs', 'Feet', 'Finger 1', 'Finger 2', + 'Trinket 1', 'Trinket 2', 'Main Hand', 'Off Hand' + ]; + + // Indexación de ítems por slot numérico si lo necesitas más adelante + $equippedBySlot = []; + foreach ($item as $equip) { + $equippedBySlot[$equip['slot']] = $equip; + } + + // Conversión simple para mostrar nombre de slot (slot numérico → texto) + $slotNames = [ + 0 => 'Head', 1 => 'Neck', 2 => 'Shoulders', 14 => 'Back', + 4 => 'Chest', 8 => 'Wrist', 9 => 'Hands', 5 => 'Waist', + 6 => 'Legs', 7 => 'Feet', 10 => 'Finger 1', 11 => 'Finger 2', + 12 => 'Trinket 1', 13 => 'Trinket 2', 15 => 'Main Hand', 16 => 'Off Hand' + ]; + @endphp + +
+ +
+ @foreach (array_slice($slotNames, 0, 6, true) as $slot => $label) + @php $equip = $equippedBySlot[$slot] ?? null; @endphp + @include('armory.partials.equipment-slot', ['equip' => $equip, 'label' => $label]) + @endforeach +
+ + +
+
+
+ {{ $character->name }} +
+
+ + +
+ @foreach (array_slice($slotNames, 6, 6, true) as $slot => $label) + @php $equip = $equippedBySlot[$slot] ?? null; @endphp + @include('armory::armory.partials.equipment-slot', ['equip' => $equip, 'label' => $label]) + @endforeach +
+
+ + +
+ @foreach (array_slice($slotNames, 12, 4, true) as $slot => $label) + @php $equip = $equippedBySlot[$slot] ?? null; @endphp +
+ +
+
{{ $label }}
+
+ {{ $equip['wowhead']['name'] ?? 'No equipado' }} +
+ @if(isset($equip['wowhead']['level'])) +
iLvl {{ $equip['wowhead']['level'] }}
+ @endif +
+
+ @endforeach +
+
\ No newline at end of file diff --git a/app/Modules/Armory/Resources/views/armory/partials/professions.blade.php b/app/Modules/Armory/Resources/views/armory/partials/professions.blade.php new file mode 100644 index 0000000..eaaf201 --- /dev/null +++ b/app/Modules/Armory/Resources/views/armory/partials/professions.blade.php @@ -0,0 +1,25 @@ + \ No newline at end of file diff --git a/app/Modules/Armory/Resources/views/armory/show.blade.php b/app/Modules/Armory/Resources/views/armory/show.blade.php new file mode 100644 index 0000000..f80bbd3 --- /dev/null +++ b/app/Modules/Armory/Resources/views/armory/show.blade.php @@ -0,0 +1,117 @@ +@extends('layouts.main') + +@section('content') +
+
+
+ +
+
+ Character +
+ {{ $character->level ?? '??' }} +
+
+
+ + +
+
+

{{ $character->name ?? 'Unknown' }}

+ + {{ strtoupper(App\Helpers\RealmHelper::getFactionByRace($character->race ?? 1)) }} + +
+ +
+ + {{ App\Helpers\RealmHelper::getWoWConstant('race', $character->race ?? 1) }} / {{ App\Helpers\RealmHelper::getWoWConstant('class', $character->class ?? 1) }} + + + {{ App\Helpers\RealmHelper::find($realm)->name }} + +
+ + + @if($guild && $memberRank) +
+
+ +
+
{{ $memberRank->rname ?? 'Member' }} of
+
<{{ $guild->name ?? 'Unknown Guild' }}>
+
+
+
+ @endif + + +
+
+
Item Level
+
{{ $promedItemLevel ?? '0' }}
+
+
+
Arena Rating
+
{{ $arenaTeam[0]['personalRating'] ?? '0' }}
+
+
+
Honorable Kills
+
{{ $character->totalKills ?? '0' }}
+
+
+
+
+
+}
+
+ +
+
+ + + +
+
+ + @include('armory::armory.partials.equipment', ['item' => $items ?? collect()]) + @include('armory::armory.partials.achievements', ['achievements' => $achievement ?? collect()]) + @include('armory::armory.partials.professions', ['skill' => $skill ?? collect()]) +
+ + + +@endsection \ No newline at end of file diff --git a/app/Services/ArmoryService.php b/app/Modules/Armory/Services/ArmoryService.php similarity index 88% rename from app/Services/ArmoryService.php rename to app/Modules/Armory/Services/ArmoryService.php index ec82bda..1081d4b 100644 --- a/app/Services/ArmoryService.php +++ b/app/Modules/Armory/Services/ArmoryService.php @@ -1,8 +1,8 @@ wowheadParser = $wowheadParser; } - /** - * Get complete character profile data - */ public function getCharacterProfile(int $guid): array { $character = $this->armoryRepo->getCharacter($guid); @@ -51,9 +48,6 @@ public function getCharacterProfile(int $guid): array ]; } - /** - * Get character items enriched with Wowhead data - */ public function getEnrichedCharacterItems(int $guid): Collection { $items = $this->armoryRepo->getCharacterItems($guid); @@ -64,9 +58,6 @@ public function getEnrichedCharacterItems(int $guid): Collection }); } - /** - * Calculate average item level from enriched items - */ public function calculateAverageItemLevel(Collection $items): int { $itemLevels = $items->pluck('wowhead.level') @@ -76,9 +67,6 @@ public function calculateAverageItemLevel(Collection $items): int return (int) RealmHelper::calculateItemLevelPromed($itemLevels); } - /** - * Search characters with filters - */ public function searchCharacters( string $q = '', ?string $faction = null, diff --git a/app/Modules/Armory/module.json b/app/Modules/Armory/module.json new file mode 100644 index 0000000..59c64ab --- /dev/null +++ b/app/Modules/Armory/module.json @@ -0,0 +1,8 @@ +{ + "name": "Armorytest", + "enabled": true, + "routes": true, + "migrations": true, + "views": true, + "namespace": "Modules\\Armorytest" +} \ No newline at end of file diff --git a/app/Providers/AppServiceProvider.php b/app/Providers/AppServiceProvider.php index 5a4632d..907465f 100755 --- a/app/Providers/AppServiceProvider.php +++ b/app/Providers/AppServiceProvider.php @@ -4,9 +4,9 @@ use Illuminate\Support\ServiceProvider; use Illuminate\Support\Facades\Config; -use App\Services\ArmoryService; +use Modules\Armory\Services\ArmoryService; use App\Services\Parser\WowheadParserService; -use App\Interfaces\ArmoryRepositoryInterface; +use Modules\Armory\Domain\Interfaces\ArmoryRepositoryInterface; class AppServiceProvider extends ServiceProvider { diff --git a/routes/web.php b/routes/web.php index 0f36f26..939870d 100644 --- a/routes/web.php +++ b/routes/web.php @@ -1,6 +1,5 @@ group(function () { Route::get('/', [NewsController::class, 'index'])->name('news'); Route::get('/{slug}', [NewsController::class, 'show'])->name('news.show'); -}); -Route::prefix('armory')->group(function () { - Route::get('/', [ArmoryController::class, 'index'])->name('armory'); - Route::get('{id}', [ArmoryController::class, 'show'])->where('id', '[0-9]+')->name('armory.show'); - Route::get('{id}/{realm?}', [ArmoryController::class, 'show'])->where('id', '[0-9]+')->where('realm', '[0-9]+')->name('armory.show.realm'); + // Comments for news Route::post('/{slug}/comment', [CommentController::class, 'store'])->name('news.comment.store'); Route::delete('/comment/{id}', [CommentController::class, 'destroy'])->name('news.comment.destroy'); + // Newsletter subscription under news Route::post('/subscribe', [SubscriptionController::class, 'subscribe'])->name('subscribe'); Route::get('/confirm-subscription/{token}', [SubscriptionController::class, 'confirmSubscription'])->name('confirm.subscription'); Route::get('/unsubscribe/{token}', [SubscriptionController::class, 'unsubscribe'])->name('unsubscribe'); }); +// Armory: ahora servido desde el módulo ArmoryTest +// Se dejan las rutas de comentarios/suscripción bajo news (si aplica) o se reubican aparte Route::prefix('auth')->group(function () { Route::get('/login', [AuthController::class,'showLoginForm'])->name('login'); From 8df5f238ced65856c88744a1fd23c90119dca2a5 Mon Sep 17 00:00:00 2001 From: sayghteight Date: Tue, 18 Nov 2025 18:31:54 +0100 Subject: [PATCH 004/132] refactor(module-structure): simplify module directory structure and clean up code - Remove redundant App/Services, App/UseCases, and App/DTOs from module creation - Add PHPDoc comments to ArmoryController for better documentation - Clean up blade template comments and improve readability - Remove outdated comments from routes file --- app/Console/Commands/MakeModuleCommand.php | 4 +- .../Http/Controllers/ArmoryController.php | 48 +++++++++++++++++++ .../views/armory/partials/equipment.blade.php | 10 ---- routes/web.php | 13 +---- 4 files changed, 51 insertions(+), 24 deletions(-) diff --git a/app/Console/Commands/MakeModuleCommand.php b/app/Console/Commands/MakeModuleCommand.php index 3554717..58419bf 100644 --- a/app/Console/Commands/MakeModuleCommand.php +++ b/app/Console/Commands/MakeModuleCommand.php @@ -33,9 +33,7 @@ public function handle(): int // Estructura de carpetas $directories = [ - 'App/Services', - 'App/UseCases', - 'App/DTOs', + 'Services', 'Domain/Models', 'Domain/Interfaces', 'Infrastructure/Repositories', diff --git a/app/Modules/Armory/Http/Controllers/ArmoryController.php b/app/Modules/Armory/Http/Controllers/ArmoryController.php index 74f2da0..db0c026 100644 --- a/app/Modules/Armory/Http/Controllers/ArmoryController.php +++ b/app/Modules/Armory/Http/Controllers/ArmoryController.php @@ -10,17 +10,51 @@ class ArmoryController extends Controller { + /** + * Number of characters to display per page in listings. + * + * @var int + */ protected int $perPage = 9; + /** + * View paths used by the controller. + * + * @var array + */ protected array $views = [ 'index' => 'armory::armory.index', 'show' => 'armory::armory.show', ]; + /** + * Armory repository instance. + * + * @var ArmoryRepositoryInterface + */ protected ArmoryRepositoryInterface $armoryRepo; + + /** + * Wowhead parser service instance. + * + * @var WowheadParserService + */ protected WowheadParserService $wowheadParser; + + /** + * Armory service instance. + * + * @var ArmoryService + */ protected ArmoryService $armoryService; + /** + * ArmoryController constructor. + * + * @param ArmoryRepositoryInterface $armoryRepo Armory repository instance. + * @param WowheadParserService $wowheadParser Wowhead parser service instance. + * @param ArmoryService $armoryService Armory service instance. + */ public function __construct( ArmoryRepositoryInterface $armoryRepo, WowheadParserService $wowheadParser, @@ -31,6 +65,12 @@ public function __construct( $this->armoryService = $armoryService; } + /** + * Display the character index page. + * + * @param Request $request Incoming request + * @return \Illuminate\View\View + */ public function index(Request $request) { $q = $request->input('q'); @@ -52,6 +92,14 @@ public function index(Request $request) ]); } + /** + * Display the character profile page. + * + * @param int $guid Character GUID + * @param Request $request Incoming request + * @param int|null $realm Realm ID (optional) + * @return \Illuminate\View\View + */ public function show(int $guid, Request $request, ?int $realm = null) { $realm = $realm ?: $request->input('realm', 1); diff --git a/app/Modules/Armory/Resources/views/armory/partials/equipment.blade.php b/app/Modules/Armory/Resources/views/armory/partials/equipment.blade.php index 94c441c..d092c67 100644 --- a/app/Modules/Armory/Resources/views/armory/partials/equipment.blade.php +++ b/app/Modules/Armory/Resources/views/armory/partials/equipment.blade.php @@ -1,19 +1,16 @@
@php - // Orden de slots (según el orden visual clásico de WoW) $slotOrder = [ 'Head', 'Neck', 'Shoulders', 'Back', 'Chest', 'Wrist', 'Hands', 'Waist', 'Legs', 'Feet', 'Finger 1', 'Finger 2', 'Trinket 1', 'Trinket 2', 'Main Hand', 'Off Hand' ]; - // Indexación de ítems por slot numérico si lo necesitas más adelante $equippedBySlot = []; foreach ($item as $equip) { $equippedBySlot[$equip['slot']] = $equip; } - // Conversión simple para mostrar nombre de slot (slot numérico → texto) $slotNames = [ 0 => 'Head', 1 => 'Neck', 2 => 'Shoulders', 14 => 'Back', 4 => 'Chest', 8 => 'Wrist', 9 => 'Hands', 5 => 'Waist', @@ -23,15 +20,12 @@ @endphp
-
@foreach (array_slice($slotNames, 0, 6, true) as $slot => $label) @php $equip = $equippedBySlot[$slot] ?? null; @endphp @include('armory.partials.equipment-slot', ['equip' => $equip, 'label' => $label]) @endforeach
- -
@@ -39,8 +33,6 @@ alt="{{ $character->name }}" class="w-80 h-80 mx-auto mb-6 opacity-90 rounded-full border-4 border-red-600 shadow-2xl">
- -
@foreach (array_slice($slotNames, 6, 6, true) as $slot => $label) @php $equip = $equippedBySlot[$slot] ?? null; @endphp @@ -48,8 +40,6 @@ @endforeach
- -
@foreach (array_slice($slotNames, 12, 4, true) as $slot => $label) @php $equip = $equippedBySlot[$slot] ?? null; @endphp diff --git a/routes/web.php b/routes/web.php index 939870d..58bf602 100644 --- a/routes/web.php +++ b/routes/web.php @@ -16,8 +16,8 @@ Route::get('/test-redis', function() { try { - $pong = Redis::ping(); // devuelve +PONG - $keys = Redis::keys('*'); // lista todas las keys + $pong = Redis::ping(); + $keys = Redis::keys('*'); return response()->json([ 'connected' => $pong === '+PONG', 'keys_count' => count($keys), @@ -44,16 +44,12 @@ Route::prefix('news')->group(function () { Route::get('/', [NewsController::class, 'index'])->name('news'); Route::get('/{slug}', [NewsController::class, 'show'])->name('news.show'); - // Comments for news Route::post('/{slug}/comment', [CommentController::class, 'store'])->name('news.comment.store'); Route::delete('/comment/{id}', [CommentController::class, 'destroy'])->name('news.comment.destroy'); - // Newsletter subscription under news Route::post('/subscribe', [SubscriptionController::class, 'subscribe'])->name('subscribe'); Route::get('/confirm-subscription/{token}', [SubscriptionController::class, 'confirmSubscription'])->name('confirm.subscription'); Route::get('/unsubscribe/{token}', [SubscriptionController::class, 'unsubscribe'])->name('unsubscribe'); }); -// Armory: ahora servido desde el módulo ArmoryTest -// Se dejan las rutas de comentarios/suscripción bajo news (si aplica) o se reubican aparte Route::prefix('auth')->group(function () { Route::get('/login', [AuthController::class,'showLoginForm'])->name('login'); @@ -63,7 +59,6 @@ Route::post('/logout', [AuthController::class, 'logout'])->name('logout'); }); -// Rutas para users Route::prefix('ucp')->middleware(['auth', 'role:User,GameMaster,Admin'])->group(function () { Route::get('/', [UserController::class, 'show'])->name('ucp.dashboard'); Route::get('/gameaccount', [UserController::class, 'gameAccount'])->name('ucp.gameaccount'); @@ -73,18 +68,14 @@ Route::get('/manage', [UserController::class, 'manage'])->name('ucp.manageAccount'); }); -// Forums routes Route::prefix('forums')->group(function () { Route::get('/', [ForumsController::class, 'index'])->name('forums'); - - // Routes that require authentication Route::prefix('ucp')->middleware(['auth', 'role:User,GameMaster,Admin'])->group(function () { Route::get('/{slug}/create', [ForumsController::class, 'createThread'])->name('forums.create_thread'); Route::post('/{slug}/create', [ForumsController::class, 'storeThread'])->name('forums.store_thread'); Route::post('/{forumSlug}/{threadSlug}/reply', [ForumsController::class, 'storePost'])->name('forums.store_post'); }); - // These routes must be defined after the more specific routes above Route::get('/{forumSlug}/{threadSlug}', [ForumsController::class, 'showThread'])->name('forums.thread'); Route::get('/{slug}', [ForumsController::class, 'showForum'])->name('forums.show'); }); From bfe343d48118c388989122574fb04085d5ea543f Mon Sep 17 00:00:00 2001 From: sayghteight Date: Tue, 18 Nov 2025 18:40:56 +0100 Subject: [PATCH 005/132] refactor(MakeModuleCommand): improve module scaffolding command - Add PHPDoc comments for better code documentation - Implement --force option to overwrite existing modules - Clean up code formatting and indentation - Improve generated files structure and readability --- app/Console/Commands/MakeModuleCommand.php | 88 +++++++++++++--------- 1 file changed, 52 insertions(+), 36 deletions(-) diff --git a/app/Console/Commands/MakeModuleCommand.php b/app/Console/Commands/MakeModuleCommand.php index 58419bf..56db658 100644 --- a/app/Console/Commands/MakeModuleCommand.php +++ b/app/Console/Commands/MakeModuleCommand.php @@ -6,20 +6,32 @@ use Illuminate\Support\Facades\File; use Illuminate\Support\Str; +/** + * Artisan command to scaffold a new module. + * + * Generates the full folder structure, base controller, + * service provider and configuration files. + */ class MakeModuleCommand extends Command { /** - * Nombre del comando para Artisan + * The console command signature. + * + * @var string */ protected $signature = 'make:module {name : The name of the module} {--force : Overwrite existing module if it exists}'; /** - * Descripción + * The console command description. + * + * @var string */ protected $description = 'Create a new module with its folder structure and base ServiceProvider'; /** - * Ejecuta el comando + * Execute the console command. + * + * @return int Exit code: 0 on success, 1 on failure. */ public function handle(): int { @@ -31,7 +43,12 @@ public function handle(): int return self::FAILURE; } - // Estructura de carpetas + // Ensure clean slate when --force is used + if ($this->option('force') && File::exists($path)) { + File::deleteDirectory($path); + } + + // Directory structure to be created inside the module $directories = [ 'Services', 'Domain/Models', @@ -48,7 +65,7 @@ public function handle(): int File::makeDirectory("{$path}/{$dir}", 0755, true, true); } - // module.json + // Generate module.json configuration file $moduleJson = [ 'name' => $name, 'enabled' => true, @@ -60,55 +77,54 @@ public function handle(): int File::put("{$path}/module.json", json_encode($moduleJson, JSON_PRETTY_PRINT | JSON_UNESCAPED_SLASHES)); - // routes.php + // Generate default routes file $routes = <<prefix(strtolower('{$name}')) - ->group(function () { - Route::get('/', [\\Modules\\{$name}\\Http\\Controllers\\{$name}Controller::class, 'index']); - }); - PHP; +Route::middleware('web') + ->prefix(strtolower('{$name}')) + ->group(function () { + Route::get('/', [\\Modules\\{$name}\\Http\\Controllers\\{$name}Controller::class, 'index']); + }); +PHP; File::put("{$path}/Http/routes.php", $routes); - // Controller base + // Generate base controller $controller = <<json(['message' => '{$name} module is working']); - } - } - PHP; +class {$name}Controller extends Controller +{ + public function index() + { + return response()->json(['message' => '{$name} module is working']); + } +} +PHP; File::put("{$path}/Http/Controllers/{$name}Controller.php", $controller); - // ServiceProvider + // Generate module service provider $provider = <<info("Module [{$name}] created successfully at app/Modules/{$name}"); return self::SUCCESS; } } - From 85cbb6ba16e4cd574a98558916ecbb407a51139e Mon Sep 17 00:00:00 2001 From: sayghteight Date: Thu, 27 Nov 2025 23:04:07 +0100 Subject: [PATCH 006/132] build: update laravel framework and remove mysql ssl options Update Laravel framework from 12.34.0 to 12.40.2 and remove MySQL SSL connection options that are no longer needed --- .../Controllers/Frontend/HomeController.php | 29 +- app/Libraries/Redis/RedisLibrary.php | 170 ++++ app/Providers/AppServiceProvider.php | 13 +- bootstrap/providers.php | 1 - composer.json | 2 +- composer.lock | 788 +++++++++--------- config/database.php | 20 +- 7 files changed, 587 insertions(+), 436 deletions(-) create mode 100644 app/Libraries/Redis/RedisLibrary.php diff --git a/app/Http/Controllers/Frontend/HomeController.php b/app/Http/Controllers/Frontend/HomeController.php index 4c43fc6..dfc92d7 100644 --- a/app/Http/Controllers/Frontend/HomeController.php +++ b/app/Http/Controllers/Frontend/HomeController.php @@ -6,6 +6,7 @@ use App\Helpers\RealmHelper; use Illuminate\Http\Request; use Illuminate\Support\Facades\View; +use App\Libraries\Redis\RedisLibrary; use App\Models\News; /** @@ -48,18 +49,36 @@ class HomeController extends Controller * @param string|null $view * @return \Illuminate\Contracts\View\View */ - public function index(Request $request, ?string $view = null) + public function index(Request $request, \App\Libraries\Redis\RedisLibrary $redis, ?string $view = null) { $realms = RealmHelper::all(); $perPage = $request->get('per_page', $this->perPage); - - $allNews = \App\Models\News::query() + $page = (int) ($request->get('page', 1)); + + $cacheKey = "news:list:perpage:{$perPage}:page:{$page}"; + $featuredKey = 'home_featured'; + + $news = $redis->get($cacheKey); + $featuredNews = $redis->get($featuredKey); + + if (!$news || !$featuredNews) { + $allNews = \App\Models\News::query() ->orderBy('created_at', 'desc') ->paginate($perPage); - $featuredNews = $allNews->shift(); - $news = $allNews; + $featuredNews = $allNews->shift(); + $news = $allNews; + + $redis->set($cacheKey, $news, 60); + $redis->set($featuredKey, $featuredNews, 60); + } + if ($featuredNews) + { + + var_dump($featuredNews); + die(); + } $data = [ 'realms' => $realms, 'featuredNews' => $featuredNews, diff --git a/app/Libraries/Redis/RedisLibrary.php b/app/Libraries/Redis/RedisLibrary.php new file mode 100644 index 0000000..5967983 --- /dev/null +++ b/app/Libraries/Redis/RedisLibrary.php @@ -0,0 +1,170 @@ +prefix = $prefix; + } + + /** + * Build a namespaced key. + * + * @param string $key + * @return string + */ + private function key(string $key): string + { + return $this->prefix . $key; + } + + /* ====================================================== + * Basic Key/Value Methods + * ====================================================== */ + + /** + * Store a value in Redis. + * + * @param string $key + * @param mixed $value + * @param int $ttl Expiration time in seconds (0 = persistent) + * + * @return bool + */ + public function set(string $key, mixed $value, int $ttl = 0): bool + { + $key = $this->key($key); + + if ($ttl > 0) { + return Redis::setex($key, $ttl, json_encode($value)); + } + + return Redis::set($key, json_encode($value)); + } + + /** + * Retrieve a value from Redis and decode it. + * + * @param string $key + * @return mixed|null + */ + public function get(string $key): mixed + { + $value = Redis::get($this->key($key)); + return $value ? json_decode($value, true) : null; + } + + /** + * Delete a Redis key. + * + * @param string $key + * @return bool + */ + public function delete(string $key): bool + { + return Redis::del($this->key($key)) > 0; + } + + /** + * Determine if a key exists. + * + * @param string $key + * @return bool + */ + public function exists(string $key): bool + { + return Redis::exists($this->key($key)) === 1; + } + + /** + * Get the TTL of a stored key. + * + * @param string $key + * @return int + */ + public function ttl(string $key): int + { + return Redis::ttl($this->key($key)); + } + + /** + * Increment a stored numeric key. + * + * @param string $key + * @param int $amount + * @return int + */ + public function increment(string $key, int $amount = 1): int + { + return Redis::incrby($this->key($key), $amount); + } + + /** + * Decrement a stored numeric key. + * + * @param string $key + * @param int $amount + * @return int + */ + public function decrement(string $key, int $amount = 1): int + { + return Redis::decrby($this->key($key), $amount); + } + + /* ====================================================== + * Token Management (JWT / Sessions) + * ====================================================== */ + + /** + * Store a user token using a namespaced key. + * + * @param string $userId + * @param string $token + * @param int $ttl + * @return bool + */ + public function storeToken(string $userId, string $token, int $ttl): bool + { + return $this->set("user:{$userId}:token", $token, $ttl); + } + + /** + * Retrieve a stored user token. + * + * @param string $userId + * @return string|null + */ + public function getToken(string $userId): ?string + { + return $this->get("user:{$userId}:token"); + } + + /** + * Delete a stored user token. + * + * @param string $userId + * @return bool + */ + public function deleteToken(string $userId): bool + { + return $this->delete("user:{$userId}:token"); + } +} diff --git a/app/Providers/AppServiceProvider.php b/app/Providers/AppServiceProvider.php index 907465f..9d518b4 100755 --- a/app/Providers/AppServiceProvider.php +++ b/app/Providers/AppServiceProvider.php @@ -2,6 +2,7 @@ namespace App\Providers; +use App\Libraries\Redis\RedisLibrary; use Illuminate\Support\ServiceProvider; use Illuminate\Support\Facades\Config; use Modules\Armory\Services\ArmoryService; @@ -10,9 +11,6 @@ class AppServiceProvider extends ServiceProvider { - /** - * Register any application services. - */ public function register(): void { // Register ArmoryService @@ -23,6 +21,12 @@ public function register(): void ); }); + // Register RedisLibrary (solo prefix) + $this->app->singleton(RedisLibrary::class, function ($app) { + $prefix = config('cache.prefix', ''); + return new RedisLibrary($prefix); + }); + if (app()->environment('production')) { $forbidden = [ 'laravel/telescope', @@ -38,9 +42,6 @@ public function register(): void } } - /** - * Bootstrap any application services. - */ public function boot(): void { if (!file_exists(storage_path('installed.lock'))) { diff --git a/bootstrap/providers.php b/bootstrap/providers.php index ae2545d..8e23487 100755 --- a/bootstrap/providers.php +++ b/bootstrap/providers.php @@ -3,7 +3,6 @@ // Core providers $providers = [ App\Providers\AppServiceProvider::class, - App\Providers\ArmoryServiceProvider::class, Spatie\Permission\PermissionServiceProvider::class, App\Providers\ModuleServiceProvider::class, ]; diff --git a/composer.json b/composer.json index 771de4e..80ccdab 100755 --- a/composer.json +++ b/composer.json @@ -13,7 +13,7 @@ ], "require": { "php": "^8.2", - "laravel/framework": "12.34.0", + "laravel/framework": "12.40.2", "laravel/pulse": "^1.4", "laravel/tinker": "2.10.1", "predis/predis": "3.2.0", diff --git a/composer.lock b/composer.lock index ec38e74..e353110 100755 --- a/composer.lock +++ b/composer.lock @@ -4,20 +4,20 @@ "Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies", "This file is @generated automatically" ], - "content-hash": "c6581f611a28c187561caa16c810b898", + "content-hash": "8d94a0b29b5ef74cbf8ba04ad7c94ba4", "packages": [ { "name": "brick/math", - "version": "0.14.0", + "version": "0.14.1", "source": { "type": "git", "url": "https://github.com/brick/math.git", - "reference": "113a8ee2656b882d4c3164fa31aa6e12cbb7aaa2" + "reference": "f05858549e5f9d7bb45875a75583240a38a281d0" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/brick/math/zipball/113a8ee2656b882d4c3164fa31aa6e12cbb7aaa2", - "reference": "113a8ee2656b882d4c3164fa31aa6e12cbb7aaa2", + "url": "https://api.github.com/repos/brick/math/zipball/f05858549e5f9d7bb45875a75583240a38a281d0", + "reference": "f05858549e5f9d7bb45875a75583240a38a281d0", "shasum": "" }, "require": { @@ -56,7 +56,7 @@ ], "support": { "issues": "https://github.com/brick/math/issues", - "source": "https://github.com/brick/math/tree/0.14.0" + "source": "https://github.com/brick/math/tree/0.14.1" }, "funding": [ { @@ -64,7 +64,7 @@ "type": "github" } ], - "time": "2025-08-29T12:40:03+00:00" + "time": "2025-11-24T14:40:29+00:00" }, { "name": "carbonphp/carbon-doctrine-types", @@ -379,26 +379,26 @@ }, { "name": "doctrine/sql-formatter", - "version": "1.5.2", + "version": "1.5.3", "source": { "type": "git", "url": "https://github.com/doctrine/sql-formatter.git", - "reference": "d6d00aba6fd2957fe5216fe2b7673e9985db20c8" + "reference": "a8af23a8e9d622505baa2997465782cbe8bb7fc7" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/doctrine/sql-formatter/zipball/d6d00aba6fd2957fe5216fe2b7673e9985db20c8", - "reference": "d6d00aba6fd2957fe5216fe2b7673e9985db20c8", + "url": "https://api.github.com/repos/doctrine/sql-formatter/zipball/a8af23a8e9d622505baa2997465782cbe8bb7fc7", + "reference": "a8af23a8e9d622505baa2997465782cbe8bb7fc7", "shasum": "" }, "require": { "php": "^8.1" }, "require-dev": { - "doctrine/coding-standard": "^12", - "ergebnis/phpunit-slow-test-detector": "^2.14", - "phpstan/phpstan": "^1.10", - "phpunit/phpunit": "^10.5" + "doctrine/coding-standard": "^14", + "ergebnis/phpunit-slow-test-detector": "^2.20", + "phpstan/phpstan": "^2.1.31", + "phpunit/phpunit": "^10.5.58" }, "bin": [ "bin/sql-formatter" @@ -428,35 +428,34 @@ ], "support": { "issues": "https://github.com/doctrine/sql-formatter/issues", - "source": "https://github.com/doctrine/sql-formatter/tree/1.5.2" + "source": "https://github.com/doctrine/sql-formatter/tree/1.5.3" }, - "time": "2025-01-24T11:45:48+00:00" + "time": "2025-10-26T09:35:14+00:00" }, { "name": "dragonmantank/cron-expression", - "version": "v3.4.0", + "version": "v3.6.0", "source": { "type": "git", "url": "https://github.com/dragonmantank/cron-expression.git", - "reference": "8c784d071debd117328803d86b2097615b457500" + "reference": "d61a8a9604ec1f8c3d150d09db6ce98b32675013" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/dragonmantank/cron-expression/zipball/8c784d071debd117328803d86b2097615b457500", - "reference": "8c784d071debd117328803d86b2097615b457500", + "url": "https://api.github.com/repos/dragonmantank/cron-expression/zipball/d61a8a9604ec1f8c3d150d09db6ce98b32675013", + "reference": "d61a8a9604ec1f8c3d150d09db6ce98b32675013", "shasum": "" }, "require": { - "php": "^7.2|^8.0", - "webmozart/assert": "^1.0" + "php": "^8.2|^8.3|^8.4|^8.5" }, "replace": { "mtdowling/cron-expression": "^1.0" }, "require-dev": { - "phpstan/extension-installer": "^1.0", - "phpstan/phpstan": "^1.0", - "phpunit/phpunit": "^7.0|^8.0|^9.0" + "phpstan/extension-installer": "^1.4.3", + "phpstan/phpstan": "^1.12.32|^2.1.31", + "phpunit/phpunit": "^8.5.48|^9.0" }, "type": "library", "extra": { @@ -487,7 +486,7 @@ ], "support": { "issues": "https://github.com/dragonmantank/cron-expression/issues", - "source": "https://github.com/dragonmantank/cron-expression/tree/v3.4.0" + "source": "https://github.com/dragonmantank/cron-expression/tree/v3.6.0" }, "funding": [ { @@ -495,7 +494,7 @@ "type": "github" } ], - "time": "2024-10-09T13:47:03+00:00" + "time": "2025-10-31T18:51:33+00:00" }, { "name": "egulias/email-validator", @@ -1110,16 +1109,16 @@ }, { "name": "laravel/framework", - "version": "v12.34.0", + "version": "v12.40.2", "source": { "type": "git", "url": "https://github.com/laravel/framework.git", - "reference": "f9ec5a5d88bc8c468f17b59f88e05c8ac3c8d687" + "reference": "1ccd99220b474500e672b373f32bd709ec38de50" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/laravel/framework/zipball/f9ec5a5d88bc8c468f17b59f88e05c8ac3c8d687", - "reference": "f9ec5a5d88bc8c468f17b59f88e05c8ac3c8d687", + "url": "https://api.github.com/repos/laravel/framework/zipball/1ccd99220b474500e672b373f32bd709ec38de50", + "reference": "1ccd99220b474500e672b373f32bd709ec38de50", "shasum": "" }, "require": { @@ -1231,13 +1230,13 @@ "league/flysystem-sftp-v3": "^3.25.1", "mockery/mockery": "^1.6.10", "opis/json-schema": "^2.4.1", - "orchestra/testbench-core": "^10.7.0", + "orchestra/testbench-core": "^10.8.0", "pda/pheanstalk": "^5.0.6|^7.0.0", "php-http/discovery": "^1.15", "phpstan/phpstan": "^2.0", "phpunit/phpunit": "^10.5.35|^11.5.3|^12.0.1", "predis/predis": "^2.3|^3.0", - "resend/resend-php": "^0.10.0", + "resend/resend-php": "^0.10.0|^1.0", "symfony/cache": "^7.2.0", "symfony/http-client": "^7.2.0", "symfony/psr-http-message-bridge": "^7.2.0", @@ -1271,7 +1270,7 @@ "predis/predis": "Required to use the predis connector (^2.3|^3.0).", "psr/http-message": "Required to allow Storage::put to accept a StreamInterface (^1.0).", "pusher/pusher-php-server": "Required to use the Pusher broadcast driver (^6.0|^7.0).", - "resend/resend-php": "Required to enable support for the Resend mail transport (^0.10.0).", + "resend/resend-php": "Required to enable support for the Resend mail transport (^0.10.0|^1.0).", "symfony/cache": "Required to PSR-6 cache bridge (^7.2).", "symfony/filesystem": "Required to enable support for relative symbolic links (^7.2).", "symfony/http-client": "Required to enable support for the Symfony API mail transports (^7.2).", @@ -1325,20 +1324,20 @@ "issues": "https://github.com/laravel/framework/issues", "source": "https://github.com/laravel/framework" }, - "time": "2025-10-14T13:58:31+00:00" + "time": "2025-11-26T19:24:25+00:00" }, { "name": "laravel/prompts", - "version": "v0.3.7", + "version": "v0.3.8", "source": { "type": "git", "url": "https://github.com/laravel/prompts.git", - "reference": "a1891d362714bc40c8d23b0b1d7090f022ea27cc" + "reference": "096748cdfb81988f60090bbb839ce3205ace0d35" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/laravel/prompts/zipball/a1891d362714bc40c8d23b0b1d7090f022ea27cc", - "reference": "a1891d362714bc40c8d23b0b1d7090f022ea27cc", + "url": "https://api.github.com/repos/laravel/prompts/zipball/096748cdfb81988f60090bbb839ce3205ace0d35", + "reference": "096748cdfb81988f60090bbb839ce3205ace0d35", "shasum": "" }, "require": { @@ -1354,7 +1353,7 @@ "require-dev": { "illuminate/collections": "^10.0|^11.0|^12.0", "mockery/mockery": "^1.5", - "pestphp/pest": "^2.3|^3.4", + "pestphp/pest": "^2.3|^3.4|^4.0", "phpstan/phpstan": "^1.12.28", "phpstan/phpstan-mockery": "^1.1.3" }, @@ -1382,22 +1381,22 @@ "description": "Add beautiful and user-friendly forms to your command-line applications.", "support": { "issues": "https://github.com/laravel/prompts/issues", - "source": "https://github.com/laravel/prompts/tree/v0.3.7" + "source": "https://github.com/laravel/prompts/tree/v0.3.8" }, - "time": "2025-09-19T13:47:56+00:00" + "time": "2025-11-21T20:52:52+00:00" }, { "name": "laravel/pulse", - "version": "v1.4.3", + "version": "v1.4.4", "source": { "type": "git", "url": "https://github.com/laravel/pulse.git", - "reference": "8c57f30aa6e094c138cd191314fe060d60773c14" + "reference": "4a39a82087c045a2e827f9ecb0e9a510d1d00dca" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/laravel/pulse/zipball/8c57f30aa6e094c138cd191314fe060d60773c14", - "reference": "8c57f30aa6e094c138cd191314fe060d60773c14", + "url": "https://api.github.com/repos/laravel/pulse/zipball/4a39a82087c045a2e827f9ecb0e9a510d1d00dca", + "reference": "4a39a82087c045a2e827f9ecb0e9a510d1d00dca", "shasum": "" }, "require": { @@ -1427,8 +1426,8 @@ "require-dev": { "guzzlehttp/guzzle": "^7.7", "mockery/mockery": "^1.0", - "orchestra/testbench": "^8.23.1|^9.0|^10.0", - "pestphp/pest": "^2.0", + "orchestra/testbench": "^8.36|^9.15|^10.8", + "pestphp/pest": "^2.0|^3.0|^4.0", "pestphp/pest-plugin-laravel": "^2.2", "phpstan/phpstan": "^1.12.21", "predis/predis": "^1.0|^2.0" @@ -1471,20 +1470,20 @@ "issues": "https://github.com/laravel/pulse/issues", "source": "https://github.com/laravel/pulse" }, - "time": "2025-07-18T15:54:11+00:00" + "time": "2025-11-24T14:05:55+00:00" }, { "name": "laravel/serializable-closure", - "version": "v2.0.6", + "version": "v2.0.7", "source": { "type": "git", "url": "https://github.com/laravel/serializable-closure.git", - "reference": "038ce42edee619599a1debb7e81d7b3759492819" + "reference": "cb291e4c998ac50637c7eeb58189c14f5de5b9dd" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/laravel/serializable-closure/zipball/038ce42edee619599a1debb7e81d7b3759492819", - "reference": "038ce42edee619599a1debb7e81d7b3759492819", + "url": "https://api.github.com/repos/laravel/serializable-closure/zipball/cb291e4c998ac50637c7eeb58189c14f5de5b9dd", + "reference": "cb291e4c998ac50637c7eeb58189c14f5de5b9dd", "shasum": "" }, "require": { @@ -1493,7 +1492,7 @@ "require-dev": { "illuminate/support": "^10.0|^11.0|^12.0", "nesbot/carbon": "^2.67|^3.0", - "pestphp/pest": "^2.36|^3.0", + "pestphp/pest": "^2.36|^3.0|^4.0", "phpstan/phpstan": "^2.0", "symfony/var-dumper": "^6.2.0|^7.0.0" }, @@ -1532,7 +1531,7 @@ "issues": "https://github.com/laravel/serializable-closure/issues", "source": "https://github.com/laravel/serializable-closure" }, - "time": "2025-10-09T13:42:30+00:00" + "time": "2025-11-21T20:52:36+00:00" }, { "name": "laravel/tinker", @@ -1602,16 +1601,16 @@ }, { "name": "league/commonmark", - "version": "2.7.1", + "version": "2.8.0", "source": { "type": "git", "url": "https://github.com/thephpleague/commonmark.git", - "reference": "10732241927d3971d28e7ea7b5712721fa2296ca" + "reference": "4efa10c1e56488e658d10adf7b7b7dcd19940bfb" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/thephpleague/commonmark/zipball/10732241927d3971d28e7ea7b5712721fa2296ca", - "reference": "10732241927d3971d28e7ea7b5712721fa2296ca", + "url": "https://api.github.com/repos/thephpleague/commonmark/zipball/4efa10c1e56488e658d10adf7b7b7dcd19940bfb", + "reference": "4efa10c1e56488e658d10adf7b7b7dcd19940bfb", "shasum": "" }, "require": { @@ -1648,7 +1647,7 @@ "type": "library", "extra": { "branch-alias": { - "dev-main": "2.8-dev" + "dev-main": "2.9-dev" } }, "autoload": { @@ -1705,7 +1704,7 @@ "type": "tidelift" } ], - "time": "2025-07-20T12:47:49+00:00" + "time": "2025-11-26T21:48:24+00:00" }, { "name": "league/config", @@ -1791,16 +1790,16 @@ }, { "name": "league/flysystem", - "version": "3.30.1", + "version": "3.30.2", "source": { "type": "git", "url": "https://github.com/thephpleague/flysystem.git", - "reference": "c139fd65c1f796b926f4aec0df37f6caa959a8da" + "reference": "5966a8ba23e62bdb518dd9e0e665c2dbd4b5b277" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/thephpleague/flysystem/zipball/c139fd65c1f796b926f4aec0df37f6caa959a8da", - "reference": "c139fd65c1f796b926f4aec0df37f6caa959a8da", + "url": "https://api.github.com/repos/thephpleague/flysystem/zipball/5966a8ba23e62bdb518dd9e0e665c2dbd4b5b277", + "reference": "5966a8ba23e62bdb518dd9e0e665c2dbd4b5b277", "shasum": "" }, "require": { @@ -1868,22 +1867,22 @@ ], "support": { "issues": "https://github.com/thephpleague/flysystem/issues", - "source": "https://github.com/thephpleague/flysystem/tree/3.30.1" + "source": "https://github.com/thephpleague/flysystem/tree/3.30.2" }, - "time": "2025-10-20T15:35:26+00:00" + "time": "2025-11-10T17:13:11+00:00" }, { "name": "league/flysystem-local", - "version": "3.30.0", + "version": "3.30.2", "source": { "type": "git", "url": "https://github.com/thephpleague/flysystem-local.git", - "reference": "6691915f77c7fb69adfb87dcd550052dc184ee10" + "reference": "ab4f9d0d672f601b102936aa728801dd1a11968d" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/thephpleague/flysystem-local/zipball/6691915f77c7fb69adfb87dcd550052dc184ee10", - "reference": "6691915f77c7fb69adfb87dcd550052dc184ee10", + "url": "https://api.github.com/repos/thephpleague/flysystem-local/zipball/ab4f9d0d672f601b102936aa728801dd1a11968d", + "reference": "ab4f9d0d672f601b102936aa728801dd1a11968d", "shasum": "" }, "require": { @@ -1917,9 +1916,9 @@ "local" ], "support": { - "source": "https://github.com/thephpleague/flysystem-local/tree/3.30.0" + "source": "https://github.com/thephpleague/flysystem-local/tree/3.30.2" }, - "time": "2025-05-21T10:34:19+00:00" + "time": "2025-11-10T11:23:37+00:00" }, { "name": "league/mime-type-detection", @@ -1979,33 +1978,38 @@ }, { "name": "league/uri", - "version": "7.5.1", + "version": "7.6.0", "source": { "type": "git", "url": "https://github.com/thephpleague/uri.git", - "reference": "81fb5145d2644324614cc532b28efd0215bda430" + "reference": "f625804987a0a9112d954f9209d91fec52182344" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/thephpleague/uri/zipball/81fb5145d2644324614cc532b28efd0215bda430", - "reference": "81fb5145d2644324614cc532b28efd0215bda430", + "url": "https://api.github.com/repos/thephpleague/uri/zipball/f625804987a0a9112d954f9209d91fec52182344", + "reference": "f625804987a0a9112d954f9209d91fec52182344", "shasum": "" }, "require": { - "league/uri-interfaces": "^7.5", - "php": "^8.1" + "league/uri-interfaces": "^7.6", + "php": "^8.1", + "psr/http-factory": "^1" }, "conflict": { "league/uri-schemes": "^1.0" }, "suggest": { "ext-bcmath": "to improve IPV4 host parsing", + "ext-dom": "to convert the URI into an HTML anchor tag", "ext-fileinfo": "to create Data URI from file contennts", "ext-gmp": "to improve IPV4 host parsing", "ext-intl": "to handle IDN host with the best performance", + "ext-uri": "to use the PHP native URI class", "jeremykendall/php-domain-parser": "to resolve Public Suffix and Top Level Domain", "league/uri-components": "Needed to easily manipulate URI objects components", + "league/uri-polyfill": "Needed to backport the PHP URI extension for older versions of PHP", "php-64bit": "to improve IPV4 host parsing", + "rowbot/url": "to handle WHATWG URL", "symfony/polyfill-intl-idn": "to handle IDN host via the Symfony polyfill if ext-intl is not present" }, "type": "library", @@ -2033,6 +2037,7 @@ "description": "URI manipulation library", "homepage": "https://uri.thephpleague.com", "keywords": [ + "URN", "data-uri", "file-uri", "ftp", @@ -2045,9 +2050,11 @@ "psr-7", "query-string", "querystring", + "rfc2141", "rfc3986", "rfc3987", "rfc6570", + "rfc8141", "uri", "uri-template", "url", @@ -2057,7 +2064,7 @@ "docs": "https://uri.thephpleague.com", "forum": "https://thephpleague.slack.com", "issues": "https://github.com/thephpleague/uri-src/issues", - "source": "https://github.com/thephpleague/uri/tree/7.5.1" + "source": "https://github.com/thephpleague/uri/tree/7.6.0" }, "funding": [ { @@ -2065,26 +2072,25 @@ "type": "github" } ], - "time": "2024-12-08T08:40:02+00:00" + "time": "2025-11-18T12:17:23+00:00" }, { "name": "league/uri-interfaces", - "version": "7.5.0", + "version": "7.6.0", "source": { "type": "git", "url": "https://github.com/thephpleague/uri-interfaces.git", - "reference": "08cfc6c4f3d811584fb09c37e2849e6a7f9b0742" + "reference": "ccbfb51c0445298e7e0b7f4481b942f589665368" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/thephpleague/uri-interfaces/zipball/08cfc6c4f3d811584fb09c37e2849e6a7f9b0742", - "reference": "08cfc6c4f3d811584fb09c37e2849e6a7f9b0742", + "url": "https://api.github.com/repos/thephpleague/uri-interfaces/zipball/ccbfb51c0445298e7e0b7f4481b942f589665368", + "reference": "ccbfb51c0445298e7e0b7f4481b942f589665368", "shasum": "" }, "require": { "ext-filter": "*", "php": "^8.1", - "psr/http-factory": "^1", "psr/http-message": "^1.1 || ^2.0" }, "suggest": { @@ -2092,6 +2098,7 @@ "ext-gmp": "to improve IPV4 host parsing", "ext-intl": "to handle IDN host with the best performance", "php-64bit": "to improve IPV4 host parsing", + "rowbot/url": "to handle WHATWG URL", "symfony/polyfill-intl-idn": "to handle IDN host via the Symfony polyfill if ext-intl is not present" }, "type": "library", @@ -2116,7 +2123,7 @@ "homepage": "https://nyamsprod.com" } ], - "description": "Common interfaces and classes for URI representation and interaction", + "description": "Common tools for parsing and resolving RFC3987/RFC3986 URI", "homepage": "https://uri.thephpleague.com", "keywords": [ "data-uri", @@ -2141,7 +2148,7 @@ "docs": "https://uri.thephpleague.com", "forum": "https://thephpleague.slack.com", "issues": "https://github.com/thephpleague/uri-src/issues", - "source": "https://github.com/thephpleague/uri-interfaces/tree/7.5.0" + "source": "https://github.com/thephpleague/uri-interfaces/tree/7.6.0" }, "funding": [ { @@ -2149,20 +2156,20 @@ "type": "github" } ], - "time": "2024-12-08T08:18:47+00:00" + "time": "2025-11-18T12:17:23+00:00" }, { "name": "livewire/livewire", - "version": "v3.6.4", + "version": "v3.7.0", "source": { "type": "git", "url": "https://github.com/livewire/livewire.git", - "reference": "ef04be759da41b14d2d129e670533180a44987dc" + "reference": "f5f9efe6d5a7059116bd695a89d95ceedf33f3cb" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/livewire/livewire/zipball/ef04be759da41b14d2d129e670533180a44987dc", - "reference": "ef04be759da41b14d2d129e670533180a44987dc", + "url": "https://api.github.com/repos/livewire/livewire/zipball/f5f9efe6d5a7059116bd695a89d95ceedf33f3cb", + "reference": "f5f9efe6d5a7059116bd695a89d95ceedf33f3cb", "shasum": "" }, "require": { @@ -2217,7 +2224,7 @@ "description": "A front-end framework for Laravel.", "support": { "issues": "https://github.com/livewire/livewire/issues", - "source": "https://github.com/livewire/livewire/tree/v3.6.4" + "source": "https://github.com/livewire/livewire/tree/v3.7.0" }, "funding": [ { @@ -2225,7 +2232,7 @@ "type": "github" } ], - "time": "2025-07-17T05:12:15+00:00" + "time": "2025-11-12T17:58:16+00:00" }, { "name": "monolog/monolog", @@ -2437,25 +2444,25 @@ }, { "name": "nette/schema", - "version": "v1.3.2", + "version": "v1.3.3", "source": { "type": "git", "url": "https://github.com/nette/schema.git", - "reference": "da801d52f0354f70a638673c4a0f04e16529431d" + "reference": "2befc2f42d7c715fd9d95efc31b1081e5d765004" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/nette/schema/zipball/da801d52f0354f70a638673c4a0f04e16529431d", - "reference": "da801d52f0354f70a638673c4a0f04e16529431d", + "url": "https://api.github.com/repos/nette/schema/zipball/2befc2f42d7c715fd9d95efc31b1081e5d765004", + "reference": "2befc2f42d7c715fd9d95efc31b1081e5d765004", "shasum": "" }, "require": { "nette/utils": "^4.0", - "php": "8.1 - 8.4" + "php": "8.1 - 8.5" }, "require-dev": { "nette/tester": "^2.5.2", - "phpstan/phpstan-nette": "^1.0", + "phpstan/phpstan-nette": "^2.0@stable", "tracy/tracy": "^2.8" }, "type": "library", @@ -2465,6 +2472,9 @@ } }, "autoload": { + "psr-4": { + "Nette\\": "src" + }, "classmap": [ "src/" ] @@ -2493,22 +2503,22 @@ ], "support": { "issues": "https://github.com/nette/schema/issues", - "source": "https://github.com/nette/schema/tree/v1.3.2" + "source": "https://github.com/nette/schema/tree/v1.3.3" }, - "time": "2024-10-06T23:10:23+00:00" + "time": "2025-10-30T22:57:59+00:00" }, { "name": "nette/utils", - "version": "v4.0.8", + "version": "v4.0.9", "source": { "type": "git", "url": "https://github.com/nette/utils.git", - "reference": "c930ca4e3cf4f17dcfb03037703679d2396d2ede" + "reference": "505a30ad386daa5211f08a318e47015b501cad30" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/nette/utils/zipball/c930ca4e3cf4f17dcfb03037703679d2396d2ede", - "reference": "c930ca4e3cf4f17dcfb03037703679d2396d2ede", + "url": "https://api.github.com/repos/nette/utils/zipball/505a30ad386daa5211f08a318e47015b501cad30", + "reference": "505a30ad386daa5211f08a318e47015b501cad30", "shasum": "" }, "require": { @@ -2582,9 +2592,9 @@ ], "support": { "issues": "https://github.com/nette/utils/issues", - "source": "https://github.com/nette/utils/tree/v4.0.8" + "source": "https://github.com/nette/utils/tree/v4.0.9" }, - "time": "2025-08-06T21:43:34+00:00" + "time": "2025-10-31T00:45:47+00:00" }, { "name": "nikic/php-parser", @@ -2646,31 +2656,31 @@ }, { "name": "nunomaduro/termwind", - "version": "v2.3.2", + "version": "v2.3.3", "source": { "type": "git", "url": "https://github.com/nunomaduro/termwind.git", - "reference": "eb61920a53057a7debd718a5b89c2178032b52c0" + "reference": "6fb2a640ff502caace8e05fd7be3b503a7e1c017" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/nunomaduro/termwind/zipball/eb61920a53057a7debd718a5b89c2178032b52c0", - "reference": "eb61920a53057a7debd718a5b89c2178032b52c0", + "url": "https://api.github.com/repos/nunomaduro/termwind/zipball/6fb2a640ff502caace8e05fd7be3b503a7e1c017", + "reference": "6fb2a640ff502caace8e05fd7be3b503a7e1c017", "shasum": "" }, "require": { "ext-mbstring": "*", "php": "^8.2", - "symfony/console": "^7.3.4" + "symfony/console": "^7.3.6" }, "require-dev": { "illuminate/console": "^11.46.1", "laravel/pint": "^1.25.1", "mockery/mockery": "^1.6.12", - "pestphp/pest": "^2.36.0 || ^3.8.4", + "pestphp/pest": "^2.36.0 || ^3.8.4 || ^4.1.3", "phpstan/phpstan": "^1.12.32", "phpstan/phpstan-strict-rules": "^1.6.2", - "symfony/var-dumper": "^7.3.4", + "symfony/var-dumper": "^7.3.5", "thecodingmachine/phpstan-strict-rules": "^1.0.0" }, "type": "library", @@ -2713,7 +2723,7 @@ ], "support": { "issues": "https://github.com/nunomaduro/termwind/issues", - "source": "https://github.com/nunomaduro/termwind/tree/v2.3.2" + "source": "https://github.com/nunomaduro/termwind/tree/v2.3.3" }, "funding": [ { @@ -2729,7 +2739,7 @@ "type": "github" } ], - "time": "2025-10-18T11:10:27+00:00" + "time": "2025-11-20T02:34:59+00:00" }, { "name": "phpoption/phpoption", @@ -3283,16 +3293,16 @@ }, { "name": "psy/psysh", - "version": "v0.12.13", + "version": "v0.12.14", "source": { "type": "git", "url": "https://github.com/bobthecow/psysh.git", - "reference": "d86c2f750e72017a5cdb1b9f1cef468a5cbacd1e" + "reference": "95c29b3756a23855a30566b745d218bee690bef2" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/bobthecow/psysh/zipball/d86c2f750e72017a5cdb1b9f1cef468a5cbacd1e", - "reference": "d86c2f750e72017a5cdb1b9f1cef468a5cbacd1e", + "url": "https://api.github.com/repos/bobthecow/psysh/zipball/95c29b3756a23855a30566b745d218bee690bef2", + "reference": "95c29b3756a23855a30566b745d218bee690bef2", "shasum": "" }, "require": { @@ -3313,7 +3323,6 @@ "suggest": { "composer/class-map-generator": "Improved tab completion performance with better class discovery.", "ext-pcntl": "Enabling the PCNTL extension makes PsySH a lot happier :)", - "ext-pdo-sqlite": "The doc command requires SQLite to work.", "ext-posix": "If you have PCNTL, you'll want the POSIX extension as well." }, "bin": [ @@ -3357,9 +3366,9 @@ ], "support": { "issues": "https://github.com/bobthecow/psysh/issues", - "source": "https://github.com/bobthecow/psysh/tree/v0.12.13" + "source": "https://github.com/bobthecow/psysh/tree/v0.12.14" }, - "time": "2025-10-20T22:48:29+00:00" + "time": "2025-10-27T17:15:31+00:00" }, { "name": "ralouphie/getallheaders", @@ -3561,16 +3570,16 @@ }, { "name": "spatie/laravel-permission", - "version": "6.21.0", + "version": "6.23.0", "source": { "type": "git", "url": "https://github.com/spatie/laravel-permission.git", - "reference": "6a118e8855dfffcd90403aab77bbf35a03db51b3" + "reference": "9e41247bd512b1e6c229afbc1eb528f7565ae3bb" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/spatie/laravel-permission/zipball/6a118e8855dfffcd90403aab77bbf35a03db51b3", - "reference": "6a118e8855dfffcd90403aab77bbf35a03db51b3", + "url": "https://api.github.com/repos/spatie/laravel-permission/zipball/9e41247bd512b1e6c229afbc1eb528f7565ae3bb", + "reference": "9e41247bd512b1e6c229afbc1eb528f7565ae3bb", "shasum": "" }, "require": { @@ -3632,7 +3641,7 @@ ], "support": { "issues": "https://github.com/spatie/laravel-permission/issues", - "source": "https://github.com/spatie/laravel-permission/tree/6.21.0" + "source": "https://github.com/spatie/laravel-permission/tree/6.23.0" }, "funding": [ { @@ -3640,20 +3649,20 @@ "type": "github" } ], - "time": "2025-07-23T16:08:05+00:00" + "time": "2025-11-03T20:16:13+00:00" }, { "name": "symfony/clock", - "version": "v7.3.0", + "version": "v7.4.0", "source": { "type": "git", "url": "https://github.com/symfony/clock.git", - "reference": "b81435fbd6648ea425d1ee96a2d8e68f4ceacd24" + "reference": "9169f24776edde469914c1e7a1442a50f7a4e110" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/clock/zipball/b81435fbd6648ea425d1ee96a2d8e68f4ceacd24", - "reference": "b81435fbd6648ea425d1ee96a2d8e68f4ceacd24", + "url": "https://api.github.com/repos/symfony/clock/zipball/9169f24776edde469914c1e7a1442a50f7a4e110", + "reference": "9169f24776edde469914c1e7a1442a50f7a4e110", "shasum": "" }, "require": { @@ -3698,7 +3707,7 @@ "time" ], "support": { - "source": "https://github.com/symfony/clock/tree/v7.3.0" + "source": "https://github.com/symfony/clock/tree/v7.4.0" }, "funding": [ { @@ -3709,25 +3718,29 @@ "url": "https://github.com/fabpot", "type": "github" }, + { + "url": "https://github.com/nicolas-grekas", + "type": "github" + }, { "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", "type": "tidelift" } ], - "time": "2024-09-25T14:21:43+00:00" + "time": "2025-11-12T15:39:26+00:00" }, { "name": "symfony/console", - "version": "v7.3.4", + "version": "v7.4.0", "source": { "type": "git", "url": "https://github.com/symfony/console.git", - "reference": "2b9c5fafbac0399a20a2e82429e2bd735dcfb7db" + "reference": "0bc0f45254b99c58d45a8fbf9fb955d46cbd1bb8" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/console/zipball/2b9c5fafbac0399a20a2e82429e2bd735dcfb7db", - "reference": "2b9c5fafbac0399a20a2e82429e2bd735dcfb7db", + "url": "https://api.github.com/repos/symfony/console/zipball/0bc0f45254b99c58d45a8fbf9fb955d46cbd1bb8", + "reference": "0bc0f45254b99c58d45a8fbf9fb955d46cbd1bb8", "shasum": "" }, "require": { @@ -3735,7 +3748,7 @@ "symfony/deprecation-contracts": "^2.5|^3", "symfony/polyfill-mbstring": "~1.0", "symfony/service-contracts": "^2.5|^3", - "symfony/string": "^7.2" + "symfony/string": "^7.2|^8.0" }, "conflict": { "symfony/dependency-injection": "<6.4", @@ -3749,16 +3762,16 @@ }, "require-dev": { "psr/log": "^1|^2|^3", - "symfony/config": "^6.4|^7.0", - "symfony/dependency-injection": "^6.4|^7.0", - "symfony/event-dispatcher": "^6.4|^7.0", - "symfony/http-foundation": "^6.4|^7.0", - "symfony/http-kernel": "^6.4|^7.0", - "symfony/lock": "^6.4|^7.0", - "symfony/messenger": "^6.4|^7.0", - "symfony/process": "^6.4|^7.0", - "symfony/stopwatch": "^6.4|^7.0", - "symfony/var-dumper": "^6.4|^7.0" + "symfony/config": "^6.4|^7.0|^8.0", + "symfony/dependency-injection": "^6.4|^7.0|^8.0", + "symfony/event-dispatcher": "^6.4|^7.0|^8.0", + "symfony/http-foundation": "^6.4|^7.0|^8.0", + "symfony/http-kernel": "^6.4|^7.0|^8.0", + "symfony/lock": "^6.4|^7.0|^8.0", + "symfony/messenger": "^6.4|^7.0|^8.0", + "symfony/process": "^6.4|^7.0|^8.0", + "symfony/stopwatch": "^6.4|^7.0|^8.0", + "symfony/var-dumper": "^6.4|^7.0|^8.0" }, "type": "library", "autoload": { @@ -3792,7 +3805,7 @@ "terminal" ], "support": { - "source": "https://github.com/symfony/console/tree/v7.3.4" + "source": "https://github.com/symfony/console/tree/v7.4.0" }, "funding": [ { @@ -3812,20 +3825,20 @@ "type": "tidelift" } ], - "time": "2025-09-22T15:31:00+00:00" + "time": "2025-11-27T13:27:24+00:00" }, { "name": "symfony/css-selector", - "version": "v7.3.0", + "version": "v7.4.0", "source": { "type": "git", "url": "https://github.com/symfony/css-selector.git", - "reference": "601a5ce9aaad7bf10797e3663faefce9e26c24e2" + "reference": "ab862f478513e7ca2fe9ec117a6f01a8da6e1135" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/css-selector/zipball/601a5ce9aaad7bf10797e3663faefce9e26c24e2", - "reference": "601a5ce9aaad7bf10797e3663faefce9e26c24e2", + "url": "https://api.github.com/repos/symfony/css-selector/zipball/ab862f478513e7ca2fe9ec117a6f01a8da6e1135", + "reference": "ab862f478513e7ca2fe9ec117a6f01a8da6e1135", "shasum": "" }, "require": { @@ -3861,7 +3874,7 @@ "description": "Converts CSS selectors to XPath expressions", "homepage": "https://symfony.com", "support": { - "source": "https://github.com/symfony/css-selector/tree/v7.3.0" + "source": "https://github.com/symfony/css-selector/tree/v7.4.0" }, "funding": [ { @@ -3872,12 +3885,16 @@ "url": "https://github.com/fabpot", "type": "github" }, + { + "url": "https://github.com/nicolas-grekas", + "type": "github" + }, { "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", "type": "tidelift" } ], - "time": "2024-09-25T14:21:43+00:00" + "time": "2025-10-30T13:39:42+00:00" }, { "name": "symfony/deprecation-contracts", @@ -3948,32 +3965,33 @@ }, { "name": "symfony/error-handler", - "version": "v7.3.4", + "version": "v7.4.0", "source": { "type": "git", "url": "https://github.com/symfony/error-handler.git", - "reference": "99f81bc944ab8e5dae4f21b4ca9972698bbad0e4" + "reference": "48be2b0653594eea32dcef130cca1c811dcf25c2" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/error-handler/zipball/99f81bc944ab8e5dae4f21b4ca9972698bbad0e4", - "reference": "99f81bc944ab8e5dae4f21b4ca9972698bbad0e4", + "url": "https://api.github.com/repos/symfony/error-handler/zipball/48be2b0653594eea32dcef130cca1c811dcf25c2", + "reference": "48be2b0653594eea32dcef130cca1c811dcf25c2", "shasum": "" }, "require": { "php": ">=8.2", "psr/log": "^1|^2|^3", - "symfony/var-dumper": "^6.4|^7.0" + "symfony/polyfill-php85": "^1.32", + "symfony/var-dumper": "^6.4|^7.0|^8.0" }, "conflict": { "symfony/deprecation-contracts": "<2.5", "symfony/http-kernel": "<6.4" }, "require-dev": { - "symfony/console": "^6.4|^7.0", + "symfony/console": "^6.4|^7.0|^8.0", "symfony/deprecation-contracts": "^2.5|^3", - "symfony/http-kernel": "^6.4|^7.0", - "symfony/serializer": "^6.4|^7.0", + "symfony/http-kernel": "^6.4|^7.0|^8.0", + "symfony/serializer": "^6.4|^7.0|^8.0", "symfony/webpack-encore-bundle": "^1.0|^2.0" }, "bin": [ @@ -4005,7 +4023,7 @@ "description": "Provides tools to manage errors and ease debugging PHP code", "homepage": "https://symfony.com", "support": { - "source": "https://github.com/symfony/error-handler/tree/v7.3.4" + "source": "https://github.com/symfony/error-handler/tree/v7.4.0" }, "funding": [ { @@ -4025,28 +4043,28 @@ "type": "tidelift" } ], - "time": "2025-09-11T10:12:26+00:00" + "time": "2025-11-05T14:29:59+00:00" }, { "name": "symfony/event-dispatcher", - "version": "v7.3.3", + "version": "v8.0.0", "source": { "type": "git", "url": "https://github.com/symfony/event-dispatcher.git", - "reference": "b7dc69e71de420ac04bc9ab830cf3ffebba48191" + "reference": "573f95783a2ec6e38752979db139f09fec033f03" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/event-dispatcher/zipball/b7dc69e71de420ac04bc9ab830cf3ffebba48191", - "reference": "b7dc69e71de420ac04bc9ab830cf3ffebba48191", + "url": "https://api.github.com/repos/symfony/event-dispatcher/zipball/573f95783a2ec6e38752979db139f09fec033f03", + "reference": "573f95783a2ec6e38752979db139f09fec033f03", "shasum": "" }, "require": { - "php": ">=8.2", + "php": ">=8.4", "symfony/event-dispatcher-contracts": "^2.5|^3" }, "conflict": { - "symfony/dependency-injection": "<6.4", + "symfony/security-http": "<7.4", "symfony/service-contracts": "<2.5" }, "provide": { @@ -4055,13 +4073,14 @@ }, "require-dev": { "psr/log": "^1|^2|^3", - "symfony/config": "^6.4|^7.0", - "symfony/dependency-injection": "^6.4|^7.0", - "symfony/error-handler": "^6.4|^7.0", - "symfony/expression-language": "^6.4|^7.0", - "symfony/http-foundation": "^6.4|^7.0", + "symfony/config": "^7.4|^8.0", + "symfony/dependency-injection": "^7.4|^8.0", + "symfony/error-handler": "^7.4|^8.0", + "symfony/expression-language": "^7.4|^8.0", + "symfony/framework-bundle": "^7.4|^8.0", + "symfony/http-foundation": "^7.4|^8.0", "symfony/service-contracts": "^2.5|^3", - "symfony/stopwatch": "^6.4|^7.0" + "symfony/stopwatch": "^7.4|^8.0" }, "type": "library", "autoload": { @@ -4089,7 +4108,7 @@ "description": "Provides tools that allow your application components to communicate with each other by dispatching events and listening to them", "homepage": "https://symfony.com", "support": { - "source": "https://github.com/symfony/event-dispatcher/tree/v7.3.3" + "source": "https://github.com/symfony/event-dispatcher/tree/v8.0.0" }, "funding": [ { @@ -4109,7 +4128,7 @@ "type": "tidelift" } ], - "time": "2025-08-13T11:49:31+00:00" + "time": "2025-10-30T14:17:19+00:00" }, { "name": "symfony/event-dispatcher-contracts", @@ -4189,23 +4208,23 @@ }, { "name": "symfony/finder", - "version": "v7.3.2", + "version": "v7.4.0", "source": { "type": "git", "url": "https://github.com/symfony/finder.git", - "reference": "2a6614966ba1074fa93dae0bc804227422df4dfe" + "reference": "340b9ed7320570f319028a2cbec46d40535e94bd" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/finder/zipball/2a6614966ba1074fa93dae0bc804227422df4dfe", - "reference": "2a6614966ba1074fa93dae0bc804227422df4dfe", + "url": "https://api.github.com/repos/symfony/finder/zipball/340b9ed7320570f319028a2cbec46d40535e94bd", + "reference": "340b9ed7320570f319028a2cbec46d40535e94bd", "shasum": "" }, "require": { "php": ">=8.2" }, "require-dev": { - "symfony/filesystem": "^6.4|^7.0" + "symfony/filesystem": "^6.4|^7.0|^8.0" }, "type": "library", "autoload": { @@ -4233,7 +4252,7 @@ "description": "Finds files and directories via an intuitive fluent interface", "homepage": "https://symfony.com", "support": { - "source": "https://github.com/symfony/finder/tree/v7.3.2" + "source": "https://github.com/symfony/finder/tree/v7.4.0" }, "funding": [ { @@ -4253,27 +4272,26 @@ "type": "tidelift" } ], - "time": "2025-07-15T13:41:35+00:00" + "time": "2025-11-05T05:42:40+00:00" }, { "name": "symfony/http-foundation", - "version": "v7.3.4", + "version": "v7.4.0", "source": { "type": "git", "url": "https://github.com/symfony/http-foundation.git", - "reference": "c061c7c18918b1b64268771aad04b40be41dd2e6" + "reference": "769c1720b68e964b13b58529c17d4a385c62167b" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/http-foundation/zipball/c061c7c18918b1b64268771aad04b40be41dd2e6", - "reference": "c061c7c18918b1b64268771aad04b40be41dd2e6", + "url": "https://api.github.com/repos/symfony/http-foundation/zipball/769c1720b68e964b13b58529c17d4a385c62167b", + "reference": "769c1720b68e964b13b58529c17d4a385c62167b", "shasum": "" }, "require": { "php": ">=8.2", - "symfony/deprecation-contracts": "^2.5|^3.0", - "symfony/polyfill-mbstring": "~1.1", - "symfony/polyfill-php83": "^1.27" + "symfony/deprecation-contracts": "^2.5|^3", + "symfony/polyfill-mbstring": "^1.1" }, "conflict": { "doctrine/dbal": "<3.6", @@ -4282,13 +4300,13 @@ "require-dev": { "doctrine/dbal": "^3.6|^4", "predis/predis": "^1.1|^2.0", - "symfony/cache": "^6.4.12|^7.1.5", - "symfony/clock": "^6.4|^7.0", - "symfony/dependency-injection": "^6.4|^7.0", - "symfony/expression-language": "^6.4|^7.0", - "symfony/http-kernel": "^6.4|^7.0", - "symfony/mime": "^6.4|^7.0", - "symfony/rate-limiter": "^6.4|^7.0" + "symfony/cache": "^6.4.12|^7.1.5|^8.0", + "symfony/clock": "^6.4|^7.0|^8.0", + "symfony/dependency-injection": "^6.4|^7.0|^8.0", + "symfony/expression-language": "^6.4|^7.0|^8.0", + "symfony/http-kernel": "^6.4|^7.0|^8.0", + "symfony/mime": "^6.4|^7.0|^8.0", + "symfony/rate-limiter": "^6.4|^7.0|^8.0" }, "type": "library", "autoload": { @@ -4316,7 +4334,7 @@ "description": "Defines an object-oriented layer for the HTTP specification", "homepage": "https://symfony.com", "support": { - "source": "https://github.com/symfony/http-foundation/tree/v7.3.4" + "source": "https://github.com/symfony/http-foundation/tree/v7.4.0" }, "funding": [ { @@ -4336,29 +4354,29 @@ "type": "tidelift" } ], - "time": "2025-09-16T08:38:17+00:00" + "time": "2025-11-13T08:49:24+00:00" }, { "name": "symfony/http-kernel", - "version": "v7.3.4", + "version": "v7.4.0", "source": { "type": "git", "url": "https://github.com/symfony/http-kernel.git", - "reference": "b796dffea7821f035047235e076b60ca2446e3cf" + "reference": "7348193cd384495a755554382e4526f27c456085" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/http-kernel/zipball/b796dffea7821f035047235e076b60ca2446e3cf", - "reference": "b796dffea7821f035047235e076b60ca2446e3cf", + "url": "https://api.github.com/repos/symfony/http-kernel/zipball/7348193cd384495a755554382e4526f27c456085", + "reference": "7348193cd384495a755554382e4526f27c456085", "shasum": "" }, "require": { "php": ">=8.2", "psr/log": "^1|^2|^3", "symfony/deprecation-contracts": "^2.5|^3", - "symfony/error-handler": "^6.4|^7.0", - "symfony/event-dispatcher": "^7.3", - "symfony/http-foundation": "^7.3", + "symfony/error-handler": "^6.4|^7.0|^8.0", + "symfony/event-dispatcher": "^7.3|^8.0", + "symfony/http-foundation": "^7.4|^8.0", "symfony/polyfill-ctype": "^1.8" }, "conflict": { @@ -4368,6 +4386,7 @@ "symfony/console": "<6.4", "symfony/dependency-injection": "<6.4", "symfony/doctrine-bridge": "<6.4", + "symfony/flex": "<2.10", "symfony/form": "<6.4", "symfony/http-client": "<6.4", "symfony/http-client-contracts": "<2.5", @@ -4385,27 +4404,27 @@ }, "require-dev": { "psr/cache": "^1.0|^2.0|^3.0", - "symfony/browser-kit": "^6.4|^7.0", - "symfony/clock": "^6.4|^7.0", - "symfony/config": "^6.4|^7.0", - "symfony/console": "^6.4|^7.0", - "symfony/css-selector": "^6.4|^7.0", - "symfony/dependency-injection": "^6.4|^7.0", - "symfony/dom-crawler": "^6.4|^7.0", - "symfony/expression-language": "^6.4|^7.0", - "symfony/finder": "^6.4|^7.0", + "symfony/browser-kit": "^6.4|^7.0|^8.0", + "symfony/clock": "^6.4|^7.0|^8.0", + "symfony/config": "^6.4|^7.0|^8.0", + "symfony/console": "^6.4|^7.0|^8.0", + "symfony/css-selector": "^6.4|^7.0|^8.0", + "symfony/dependency-injection": "^6.4|^7.0|^8.0", + "symfony/dom-crawler": "^6.4|^7.0|^8.0", + "symfony/expression-language": "^6.4|^7.0|^8.0", + "symfony/finder": "^6.4|^7.0|^8.0", "symfony/http-client-contracts": "^2.5|^3", - "symfony/process": "^6.4|^7.0", - "symfony/property-access": "^7.1", - "symfony/routing": "^6.4|^7.0", - "symfony/serializer": "^7.1", - "symfony/stopwatch": "^6.4|^7.0", - "symfony/translation": "^6.4|^7.0", + "symfony/process": "^6.4|^7.0|^8.0", + "symfony/property-access": "^7.1|^8.0", + "symfony/routing": "^6.4|^7.0|^8.0", + "symfony/serializer": "^7.1|^8.0", + "symfony/stopwatch": "^6.4|^7.0|^8.0", + "symfony/translation": "^6.4|^7.0|^8.0", "symfony/translation-contracts": "^2.5|^3", - "symfony/uid": "^6.4|^7.0", - "symfony/validator": "^6.4|^7.0", - "symfony/var-dumper": "^6.4|^7.0", - "symfony/var-exporter": "^6.4|^7.0", + "symfony/uid": "^6.4|^7.0|^8.0", + "symfony/validator": "^6.4|^7.0|^8.0", + "symfony/var-dumper": "^6.4|^7.0|^8.0", + "symfony/var-exporter": "^6.4|^7.0|^8.0", "twig/twig": "^3.12" }, "type": "library", @@ -4434,7 +4453,7 @@ "description": "Provides a structured process for converting a Request into a Response", "homepage": "https://symfony.com", "support": { - "source": "https://github.com/symfony/http-kernel/tree/v7.3.4" + "source": "https://github.com/symfony/http-kernel/tree/v7.4.0" }, "funding": [ { @@ -4454,20 +4473,20 @@ "type": "tidelift" } ], - "time": "2025-09-27T12:32:17+00:00" + "time": "2025-11-27T13:38:24+00:00" }, { "name": "symfony/mailer", - "version": "v7.3.4", + "version": "v7.4.0", "source": { "type": "git", "url": "https://github.com/symfony/mailer.git", - "reference": "ab97ef2f7acf0216955f5845484235113047a31d" + "reference": "a3d9eea8cfa467ece41f0f54ba28185d74bd53fd" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/mailer/zipball/ab97ef2f7acf0216955f5845484235113047a31d", - "reference": "ab97ef2f7acf0216955f5845484235113047a31d", + "url": "https://api.github.com/repos/symfony/mailer/zipball/a3d9eea8cfa467ece41f0f54ba28185d74bd53fd", + "reference": "a3d9eea8cfa467ece41f0f54ba28185d74bd53fd", "shasum": "" }, "require": { @@ -4475,8 +4494,8 @@ "php": ">=8.2", "psr/event-dispatcher": "^1", "psr/log": "^1|^2|^3", - "symfony/event-dispatcher": "^6.4|^7.0", - "symfony/mime": "^7.2", + "symfony/event-dispatcher": "^6.4|^7.0|^8.0", + "symfony/mime": "^7.2|^8.0", "symfony/service-contracts": "^2.5|^3" }, "conflict": { @@ -4487,10 +4506,10 @@ "symfony/twig-bridge": "<6.4" }, "require-dev": { - "symfony/console": "^6.4|^7.0", - "symfony/http-client": "^6.4|^7.0", - "symfony/messenger": "^6.4|^7.0", - "symfony/twig-bridge": "^6.4|^7.0" + "symfony/console": "^6.4|^7.0|^8.0", + "symfony/http-client": "^6.4|^7.0|^8.0", + "symfony/messenger": "^6.4|^7.0|^8.0", + "symfony/twig-bridge": "^6.4|^7.0|^8.0" }, "type": "library", "autoload": { @@ -4518,7 +4537,7 @@ "description": "Helps sending emails", "homepage": "https://symfony.com", "support": { - "source": "https://github.com/symfony/mailer/tree/v7.3.4" + "source": "https://github.com/symfony/mailer/tree/v7.4.0" }, "funding": [ { @@ -4538,24 +4557,25 @@ "type": "tidelift" } ], - "time": "2025-09-17T05:51:54+00:00" + "time": "2025-11-21T15:26:00+00:00" }, { "name": "symfony/mime", - "version": "v7.3.4", + "version": "v7.4.0", "source": { "type": "git", "url": "https://github.com/symfony/mime.git", - "reference": "b1b828f69cbaf887fa835a091869e55df91d0e35" + "reference": "bdb02729471be5d047a3ac4a69068748f1a6be7a" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/mime/zipball/b1b828f69cbaf887fa835a091869e55df91d0e35", - "reference": "b1b828f69cbaf887fa835a091869e55df91d0e35", + "url": "https://api.github.com/repos/symfony/mime/zipball/bdb02729471be5d047a3ac4a69068748f1a6be7a", + "reference": "bdb02729471be5d047a3ac4a69068748f1a6be7a", "shasum": "" }, "require": { "php": ">=8.2", + "symfony/deprecation-contracts": "^2.5|^3", "symfony/polyfill-intl-idn": "^1.10", "symfony/polyfill-mbstring": "^1.0" }, @@ -4570,11 +4590,11 @@ "egulias/email-validator": "^2.1.10|^3.1|^4", "league/html-to-markdown": "^5.0", "phpdocumentor/reflection-docblock": "^3.0|^4.0|^5.0", - "symfony/dependency-injection": "^6.4|^7.0", - "symfony/process": "^6.4|^7.0", - "symfony/property-access": "^6.4|^7.0", - "symfony/property-info": "^6.4|^7.0", - "symfony/serializer": "^6.4.3|^7.0.3" + "symfony/dependency-injection": "^6.4|^7.0|^8.0", + "symfony/process": "^6.4|^7.0|^8.0", + "symfony/property-access": "^6.4|^7.0|^8.0", + "symfony/property-info": "^6.4|^7.0|^8.0", + "symfony/serializer": "^6.4.3|^7.0.3|^8.0" }, "type": "library", "autoload": { @@ -4606,7 +4626,7 @@ "mime-type" ], "support": { - "source": "https://github.com/symfony/mime/tree/v7.3.4" + "source": "https://github.com/symfony/mime/tree/v7.4.0" }, "funding": [ { @@ -4626,7 +4646,7 @@ "type": "tidelift" } ], - "time": "2025-09-16T08:38:17+00:00" + "time": "2025-11-16T10:14:42+00:00" }, { "name": "symfony/polyfill-ctype", @@ -5459,16 +5479,16 @@ }, { "name": "symfony/process", - "version": "v7.3.4", + "version": "v7.4.0", "source": { "type": "git", "url": "https://github.com/symfony/process.git", - "reference": "f24f8f316367b30810810d4eb30c543d7003ff3b" + "reference": "7ca8dc2d0dcf4882658313aba8be5d9fd01026c8" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/process/zipball/f24f8f316367b30810810d4eb30c543d7003ff3b", - "reference": "f24f8f316367b30810810d4eb30c543d7003ff3b", + "url": "https://api.github.com/repos/symfony/process/zipball/7ca8dc2d0dcf4882658313aba8be5d9fd01026c8", + "reference": "7ca8dc2d0dcf4882658313aba8be5d9fd01026c8", "shasum": "" }, "require": { @@ -5500,7 +5520,7 @@ "description": "Executes commands in sub-processes", "homepage": "https://symfony.com", "support": { - "source": "https://github.com/symfony/process/tree/v7.3.4" + "source": "https://github.com/symfony/process/tree/v7.4.0" }, "funding": [ { @@ -5520,20 +5540,20 @@ "type": "tidelift" } ], - "time": "2025-09-11T10:12:26+00:00" + "time": "2025-10-16T11:21:06+00:00" }, { "name": "symfony/routing", - "version": "v7.3.4", + "version": "v7.4.0", "source": { "type": "git", "url": "https://github.com/symfony/routing.git", - "reference": "8dc648e159e9bac02b703b9fbd937f19ba13d07c" + "reference": "4720254cb2644a0b876233d258a32bf017330db7" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/routing/zipball/8dc648e159e9bac02b703b9fbd937f19ba13d07c", - "reference": "8dc648e159e9bac02b703b9fbd937f19ba13d07c", + "url": "https://api.github.com/repos/symfony/routing/zipball/4720254cb2644a0b876233d258a32bf017330db7", + "reference": "4720254cb2644a0b876233d258a32bf017330db7", "shasum": "" }, "require": { @@ -5547,11 +5567,11 @@ }, "require-dev": { "psr/log": "^1|^2|^3", - "symfony/config": "^6.4|^7.0", - "symfony/dependency-injection": "^6.4|^7.0", - "symfony/expression-language": "^6.4|^7.0", - "symfony/http-foundation": "^6.4|^7.0", - "symfony/yaml": "^6.4|^7.0" + "symfony/config": "^6.4|^7.0|^8.0", + "symfony/dependency-injection": "^6.4|^7.0|^8.0", + "symfony/expression-language": "^6.4|^7.0|^8.0", + "symfony/http-foundation": "^6.4|^7.0|^8.0", + "symfony/yaml": "^6.4|^7.0|^8.0" }, "type": "library", "autoload": { @@ -5585,7 +5605,7 @@ "url" ], "support": { - "source": "https://github.com/symfony/routing/tree/v7.3.4" + "source": "https://github.com/symfony/routing/tree/v7.4.0" }, "funding": [ { @@ -5605,20 +5625,20 @@ "type": "tidelift" } ], - "time": "2025-09-11T10:12:26+00:00" + "time": "2025-11-27T13:27:24+00:00" }, { "name": "symfony/service-contracts", - "version": "v3.6.0", + "version": "v3.6.1", "source": { "type": "git", "url": "https://github.com/symfony/service-contracts.git", - "reference": "f021b05a130d35510bd6b25fe9053c2a8a15d5d4" + "reference": "45112560a3ba2d715666a509a0bc9521d10b6c43" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/service-contracts/zipball/f021b05a130d35510bd6b25fe9053c2a8a15d5d4", - "reference": "f021b05a130d35510bd6b25fe9053c2a8a15d5d4", + "url": "https://api.github.com/repos/symfony/service-contracts/zipball/45112560a3ba2d715666a509a0bc9521d10b6c43", + "reference": "45112560a3ba2d715666a509a0bc9521d10b6c43", "shasum": "" }, "require": { @@ -5672,7 +5692,7 @@ "standards" ], "support": { - "source": "https://github.com/symfony/service-contracts/tree/v3.6.0" + "source": "https://github.com/symfony/service-contracts/tree/v3.6.1" }, "funding": [ { @@ -5683,43 +5703,47 @@ "url": "https://github.com/fabpot", "type": "github" }, + { + "url": "https://github.com/nicolas-grekas", + "type": "github" + }, { "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", "type": "tidelift" } ], - "time": "2025-04-25T09:37:31+00:00" + "time": "2025-07-15T11:30:57+00:00" }, { "name": "symfony/string", - "version": "v7.3.4", + "version": "v8.0.0", "source": { "type": "git", "url": "https://github.com/symfony/string.git", - "reference": "f96476035142921000338bad71e5247fbc138872" + "reference": "f929eccf09531078c243df72398560e32fa4cf4f" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/string/zipball/f96476035142921000338bad71e5247fbc138872", - "reference": "f96476035142921000338bad71e5247fbc138872", + "url": "https://api.github.com/repos/symfony/string/zipball/f929eccf09531078c243df72398560e32fa4cf4f", + "reference": "f929eccf09531078c243df72398560e32fa4cf4f", "shasum": "" }, "require": { - "php": ">=8.2", - "symfony/polyfill-ctype": "~1.8", - "symfony/polyfill-intl-grapheme": "~1.0", - "symfony/polyfill-intl-normalizer": "~1.0", - "symfony/polyfill-mbstring": "~1.0" + "php": ">=8.4", + "symfony/polyfill-ctype": "^1.8", + "symfony/polyfill-intl-grapheme": "^1.33", + "symfony/polyfill-intl-normalizer": "^1.0", + "symfony/polyfill-mbstring": "^1.0" }, "conflict": { "symfony/translation-contracts": "<2.5" }, "require-dev": { - "symfony/emoji": "^7.1", - "symfony/http-client": "^6.4|^7.0", - "symfony/intl": "^6.4|^7.0", + "symfony/emoji": "^7.4|^8.0", + "symfony/http-client": "^7.4|^8.0", + "symfony/intl": "^7.4|^8.0", "symfony/translation-contracts": "^2.5|^3.0", - "symfony/var-exporter": "^6.4|^7.0" + "symfony/var-exporter": "^7.4|^8.0" }, "type": "library", "autoload": { @@ -5758,7 +5782,7 @@ "utf8" ], "support": { - "source": "https://github.com/symfony/string/tree/v7.3.4" + "source": "https://github.com/symfony/string/tree/v8.0.0" }, "funding": [ { @@ -5778,27 +5802,27 @@ "type": "tidelift" } ], - "time": "2025-09-11T14:36:48+00:00" + "time": "2025-09-11T14:37:55+00:00" }, { "name": "symfony/translation", - "version": "v7.3.4", + "version": "v7.4.0", "source": { "type": "git", "url": "https://github.com/symfony/translation.git", - "reference": "ec25870502d0c7072d086e8ffba1420c85965174" + "reference": "2d01ca0da3f092f91eeedb46f24aa30d2fca8f68" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/translation/zipball/ec25870502d0c7072d086e8ffba1420c85965174", - "reference": "ec25870502d0c7072d086e8ffba1420c85965174", + "url": "https://api.github.com/repos/symfony/translation/zipball/2d01ca0da3f092f91eeedb46f24aa30d2fca8f68", + "reference": "2d01ca0da3f092f91eeedb46f24aa30d2fca8f68", "shasum": "" }, "require": { "php": ">=8.2", "symfony/deprecation-contracts": "^2.5|^3", "symfony/polyfill-mbstring": "~1.0", - "symfony/translation-contracts": "^2.5|^3.0" + "symfony/translation-contracts": "^2.5.3|^3.3" }, "conflict": { "nikic/php-parser": "<5.0", @@ -5817,17 +5841,17 @@ "require-dev": { "nikic/php-parser": "^5.0", "psr/log": "^1|^2|^3", - "symfony/config": "^6.4|^7.0", - "symfony/console": "^6.4|^7.0", - "symfony/dependency-injection": "^6.4|^7.0", - "symfony/finder": "^6.4|^7.0", + "symfony/config": "^6.4|^7.0|^8.0", + "symfony/console": "^6.4|^7.0|^8.0", + "symfony/dependency-injection": "^6.4|^7.0|^8.0", + "symfony/finder": "^6.4|^7.0|^8.0", "symfony/http-client-contracts": "^2.5|^3.0", - "symfony/http-kernel": "^6.4|^7.0", - "symfony/intl": "^6.4|^7.0", + "symfony/http-kernel": "^6.4|^7.0|^8.0", + "symfony/intl": "^6.4|^7.0|^8.0", "symfony/polyfill-intl-icu": "^1.21", - "symfony/routing": "^6.4|^7.0", + "symfony/routing": "^6.4|^7.0|^8.0", "symfony/service-contracts": "^2.5|^3", - "symfony/yaml": "^6.4|^7.0" + "symfony/yaml": "^6.4|^7.0|^8.0" }, "type": "library", "autoload": { @@ -5858,7 +5882,7 @@ "description": "Provides tools to internationalize your application", "homepage": "https://symfony.com", "support": { - "source": "https://github.com/symfony/translation/tree/v7.3.4" + "source": "https://github.com/symfony/translation/tree/v7.4.0" }, "funding": [ { @@ -5878,20 +5902,20 @@ "type": "tidelift" } ], - "time": "2025-09-07T11:39:36+00:00" + "time": "2025-11-27T13:27:24+00:00" }, { "name": "symfony/translation-contracts", - "version": "v3.6.0", + "version": "v3.6.1", "source": { "type": "git", "url": "https://github.com/symfony/translation-contracts.git", - "reference": "df210c7a2573f1913b2d17cc95f90f53a73d8f7d" + "reference": "65a8bc82080447fae78373aa10f8d13b38338977" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/translation-contracts/zipball/df210c7a2573f1913b2d17cc95f90f53a73d8f7d", - "reference": "df210c7a2573f1913b2d17cc95f90f53a73d8f7d", + "url": "https://api.github.com/repos/symfony/translation-contracts/zipball/65a8bc82080447fae78373aa10f8d13b38338977", + "reference": "65a8bc82080447fae78373aa10f8d13b38338977", "shasum": "" }, "require": { @@ -5940,7 +5964,7 @@ "standards" ], "support": { - "source": "https://github.com/symfony/translation-contracts/tree/v3.6.0" + "source": "https://github.com/symfony/translation-contracts/tree/v3.6.1" }, "funding": [ { @@ -5951,25 +5975,29 @@ "url": "https://github.com/fabpot", "type": "github" }, + { + "url": "https://github.com/nicolas-grekas", + "type": "github" + }, { "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", "type": "tidelift" } ], - "time": "2024-09-27T08:32:26+00:00" + "time": "2025-07-15T13:41:35+00:00" }, { "name": "symfony/uid", - "version": "v7.3.1", + "version": "v7.4.0", "source": { "type": "git", "url": "https://github.com/symfony/uid.git", - "reference": "a69f69f3159b852651a6bf45a9fdd149520525bb" + "reference": "2498e9f81b7baa206f44de583f2f48350b90142c" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/uid/zipball/a69f69f3159b852651a6bf45a9fdd149520525bb", - "reference": "a69f69f3159b852651a6bf45a9fdd149520525bb", + "url": "https://api.github.com/repos/symfony/uid/zipball/2498e9f81b7baa206f44de583f2f48350b90142c", + "reference": "2498e9f81b7baa206f44de583f2f48350b90142c", "shasum": "" }, "require": { @@ -5977,7 +6005,7 @@ "symfony/polyfill-uuid": "^1.15" }, "require-dev": { - "symfony/console": "^6.4|^7.0" + "symfony/console": "^6.4|^7.0|^8.0" }, "type": "library", "autoload": { @@ -6014,7 +6042,7 @@ "uuid" ], "support": { - "source": "https://github.com/symfony/uid/tree/v7.3.1" + "source": "https://github.com/symfony/uid/tree/v7.4.0" }, "funding": [ { @@ -6025,25 +6053,29 @@ "url": "https://github.com/fabpot", "type": "github" }, + { + "url": "https://github.com/nicolas-grekas", + "type": "github" + }, { "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", "type": "tidelift" } ], - "time": "2025-06-27T19:55:54+00:00" + "time": "2025-09-25T11:02:55+00:00" }, { "name": "symfony/var-dumper", - "version": "v7.3.4", + "version": "v7.4.0", "source": { "type": "git", "url": "https://github.com/symfony/var-dumper.git", - "reference": "b8abe7daf2730d07dfd4b2ee1cecbf0dd2fbdabb" + "reference": "41fd6c4ae28c38b294b42af6db61446594a0dece" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/var-dumper/zipball/b8abe7daf2730d07dfd4b2ee1cecbf0dd2fbdabb", - "reference": "b8abe7daf2730d07dfd4b2ee1cecbf0dd2fbdabb", + "url": "https://api.github.com/repos/symfony/var-dumper/zipball/41fd6c4ae28c38b294b42af6db61446594a0dece", + "reference": "41fd6c4ae28c38b294b42af6db61446594a0dece", "shasum": "" }, "require": { @@ -6055,10 +6087,10 @@ "symfony/console": "<6.4" }, "require-dev": { - "symfony/console": "^6.4|^7.0", - "symfony/http-kernel": "^6.4|^7.0", - "symfony/process": "^6.4|^7.0", - "symfony/uid": "^6.4|^7.0", + "symfony/console": "^6.4|^7.0|^8.0", + "symfony/http-kernel": "^6.4|^7.0|^8.0", + "symfony/process": "^6.4|^7.0|^8.0", + "symfony/uid": "^6.4|^7.0|^8.0", "twig/twig": "^3.12" }, "bin": [ @@ -6097,7 +6129,7 @@ "dump" ], "support": { - "source": "https://github.com/symfony/var-dumper/tree/v7.3.4" + "source": "https://github.com/symfony/var-dumper/tree/v7.4.0" }, "funding": [ { @@ -6117,7 +6149,7 @@ "type": "tidelift" } ], - "time": "2025-09-11T10:12:26+00:00" + "time": "2025-10-27T20:36:44+00:00" }, { "name": "tijsverkoyen/css-to-inline-styles", @@ -6332,64 +6364,6 @@ ], "time": "2024-11-21T01:49:47+00:00" }, - { - "name": "webmozart/assert", - "version": "1.12.0", - "source": { - "type": "git", - "url": "https://github.com/webmozarts/assert.git", - "reference": "541057574806f942c94662b817a50f63f7345360" - }, - "dist": { - "type": "zip", - "url": "https://api.github.com/repos/webmozarts/assert/zipball/541057574806f942c94662b817a50f63f7345360", - "reference": "541057574806f942c94662b817a50f63f7345360", - "shasum": "" - }, - "require": { - "ext-ctype": "*", - "ext-date": "*", - "ext-filter": "*", - "php": "^7.2 || ^8.0" - }, - "suggest": { - "ext-intl": "", - "ext-simplexml": "", - "ext-spl": "" - }, - "type": "library", - "extra": { - "branch-alias": { - "dev-master": "1.10-dev" - } - }, - "autoload": { - "psr-4": { - "Webmozart\\Assert\\": "src/" - } - }, - "notification-url": "https://packagist.org/downloads/", - "license": [ - "MIT" - ], - "authors": [ - { - "name": "Bernhard Schussek", - "email": "bschussek@gmail.com" - } - ], - "description": "Assertions to validate method input/output with nice error messages.", - "keywords": [ - "assert", - "check", - "validate" - ], - "support": { - "issues": "https://github.com/webmozarts/assert/issues", - "source": "https://github.com/webmozarts/assert/tree/1.12.0" - }, - "time": "2025-10-20T12:43:39+00:00" - }, { "name": "wowcrypto/wowcrypto", "version": "1.2.0", @@ -8751,28 +8725,28 @@ }, { "name": "symfony/yaml", - "version": "v7.3.3", + "version": "v7.4.0", "source": { "type": "git", "url": "https://github.com/symfony/yaml.git", - "reference": "d4f4a66866fe2451f61296924767280ab5732d9d" + "reference": "6c84a4b55aee4cd02034d1c528e83f69ddf63810" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/yaml/zipball/d4f4a66866fe2451f61296924767280ab5732d9d", - "reference": "d4f4a66866fe2451f61296924767280ab5732d9d", + "url": "https://api.github.com/repos/symfony/yaml/zipball/6c84a4b55aee4cd02034d1c528e83f69ddf63810", + "reference": "6c84a4b55aee4cd02034d1c528e83f69ddf63810", "shasum": "" }, "require": { "php": ">=8.2", - "symfony/deprecation-contracts": "^2.5|^3.0", + "symfony/deprecation-contracts": "^2.5|^3", "symfony/polyfill-ctype": "^1.8" }, "conflict": { "symfony/console": "<6.4" }, "require-dev": { - "symfony/console": "^6.4|^7.0" + "symfony/console": "^6.4|^7.0|^8.0" }, "bin": [ "Resources/bin/yaml-lint" @@ -8803,7 +8777,7 @@ "description": "Loads and dumps YAML files", "homepage": "https://symfony.com", "support": { - "source": "https://github.com/symfony/yaml/tree/v7.3.3" + "source": "https://github.com/symfony/yaml/tree/v7.4.0" }, "funding": [ { @@ -8823,20 +8797,20 @@ "type": "tidelift" } ], - "time": "2025-08-27T11:34:33+00:00" + "time": "2025-11-16T10:14:42+00:00" }, { "name": "theseer/tokenizer", - "version": "1.2.3", + "version": "1.3.1", "source": { "type": "git", "url": "https://github.com/theseer/tokenizer.git", - "reference": "737eda637ed5e28c3413cb1ebe8bb52cbf1ca7a2" + "reference": "b7489ce515e168639d17feec34b8847c326b0b3c" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/theseer/tokenizer/zipball/737eda637ed5e28c3413cb1ebe8bb52cbf1ca7a2", - "reference": "737eda637ed5e28c3413cb1ebe8bb52cbf1ca7a2", + "url": "https://api.github.com/repos/theseer/tokenizer/zipball/b7489ce515e168639d17feec34b8847c326b0b3c", + "reference": "b7489ce515e168639d17feec34b8847c326b0b3c", "shasum": "" }, "require": { @@ -8865,7 +8839,7 @@ "description": "A small library for converting tokenized PHP source code into XML and potentially other formats", "support": { "issues": "https://github.com/theseer/tokenizer/issues", - "source": "https://github.com/theseer/tokenizer/tree/1.2.3" + "source": "https://github.com/theseer/tokenizer/tree/1.3.1" }, "funding": [ { @@ -8873,7 +8847,7 @@ "type": "github" } ], - "time": "2024-03-03T12:36:25+00:00" + "time": "2025-11-17T20:03:58+00:00" } ], "aliases": [], diff --git a/config/database.php b/config/database.php index ca960a8..944683f 100755 --- a/config/database.php +++ b/config/database.php @@ -56,10 +56,7 @@ 'prefix' => '', 'prefix_indexes' => true, 'strict' => true, - 'engine' => null, - 'options' => extension_loaded('pdo_mysql') ? array_filter([ - PDO::MYSQL_ATTR_SSL_CA => env('MYSQL_ATTR_SSL_CA'), - ]) : [], + 'engine' => null ], 'mysql_characters' => [ @@ -76,10 +73,7 @@ 'prefix' => '', 'prefix_indexes' => true, 'strict' => true, - 'engine' => null, - 'options' => extension_loaded('pdo_mysql') ? array_filter([ - PDO::MYSQL_ATTR_SSL_CA => env('MYSQL_ATTR_SSL_CA'), - ]) : [], + 'engine' => null ], 'mysql_world' => [ @@ -96,10 +90,7 @@ 'prefix' => '', 'prefix_indexes' => true, 'strict' => true, - 'engine' => null, - 'options' => extension_loaded('pdo_mysql') ? array_filter([ - PDO::MYSQL_ATTR_SSL_CA => env('MYSQL_ATTR_SSL_CA'), - ]) : [], + 'engine' => null ], 'mariadb' => [ @@ -116,10 +107,7 @@ 'prefix' => '', 'prefix_indexes' => true, 'strict' => true, - 'engine' => null, - 'options' => extension_loaded('pdo_mysql') ? array_filter([ - PDO::MYSQL_ATTR_SSL_CA => env('MYSQL_ATTR_SSL_CA'), - ]) : [], + 'engine' => null ], 'pgsql' => [ From 68ae71813a7ce4128a07433b670dcf7629414268 Mon Sep 17 00:00:00 2001 From: sayghteight Date: Wed, 31 Dec 2025 20:21:06 +0100 Subject: [PATCH 007/132] refactor: improve RedisLibrary with fallback to Cache and add validation - Add Redis fallback to Laravel Cache when Redis is disabled or fails - Implement input validation in AccountLibrary for account creation - Remove debug code from HomeController - Add type hints and docblocks to NewsController methods - Add .env.example file with default configuration --- .env.example | 62 +++++++++++ .../Controllers/Frontend/HomeController.php | 6 - .../Controllers/Frontend/NewsController.php | 23 +++- app/Libraries/Auth/AccountLibrary.php | 39 ++++++- app/Libraries/Redis/RedisLibrary.php | 104 ++++++++++++++++-- 5 files changed, 212 insertions(+), 22 deletions(-) create mode 100644 .env.example diff --git a/.env.example b/.env.example new file mode 100644 index 0000000..ce7a7da --- /dev/null +++ b/.env.example @@ -0,0 +1,62 @@ +APP_NAME="NexusCMS" +APP_ENV=local +APP_KEY= +APP_DEBUG=true +APP_URL="https://nexuscms.test" + +APP_LOCALE="en" +APP_FALLBACK_LOCALE=en +APP_FAKER_LOCALE=en_US + +APP_MAINTENANCE_DRIVER=file +# APP_MAINTENANCE_STORE=database + +PHP_CLI_SERVER_WORKERS=4 + +BCRYPT_ROUNDS=12 + +LOG_CHANNEL=stack +LOG_STACK=single +LOG_DEPRECATIONS_CHANNEL=null +LOG_LEVEL=debug + +DB_CONNECTION="mysql" +DB_HOST="127.0.0.1" +DB_PORT="3306" +DB_DATABASE="test" +DB_USERNAME="root" +DB_PASSWORD="root" + +SESSION_DRIVER=database +SESSION_LIFETIME=120 +SESSION_ENCRYPT=false +SESSION_PATH=/ +SESSION_DOMAIN=null + +BROADCAST_CONNECTION=log +FILESYSTEM_DISK=local +QUEUE_CONNECTION=database +MEMCACHED_HOST=127.0.0.1 + +CACHE_DRIVER=redis +REDIS_CLIENT=phpredis +REDIS_HOST=127.0.0.1 +REDIS_PORT=6379 + +MAIL_MAILER=smtp +MAIL_HOST=localhost +MAIL_PORT=1025 +MAIL_USERNAME=null +MAIL_PASSWORD=null +MAIL_ENCRYPTION=null +MAIL_FROM_ADDRESS="noreply@nexuscms.test" +MAIL_FROM_NAME="${APP_NAME}" +MAIL_SCHEME=null + +AWS_ACCESS_KEY_ID= +AWS_SECRET_ACCESS_KEY= +AWS_DEFAULT_REGION=us-east-1 +AWS_BUCKET= +AWS_USE_PATH_STYLE_ENDPOINT=false + +VITE_APP_NAME="${APP_NAME}" diff --git a/app/Http/Controllers/Frontend/HomeController.php b/app/Http/Controllers/Frontend/HomeController.php index dfc92d7..4bfcf03 100644 --- a/app/Http/Controllers/Frontend/HomeController.php +++ b/app/Http/Controllers/Frontend/HomeController.php @@ -73,12 +73,6 @@ public function index(Request $request, \App\Libraries\Redis\RedisLibrary $redis $redis->set($featuredKey, $featuredNews, 60); } - if ($featuredNews) - { - - var_dump($featuredNews); - die(); - } $data = [ 'realms' => $realms, 'featuredNews' => $featuredNews, diff --git a/app/Http/Controllers/Frontend/NewsController.php b/app/Http/Controllers/Frontend/NewsController.php index dd73fe7..4b0f755 100755 --- a/app/Http/Controllers/Frontend/NewsController.php +++ b/app/Http/Controllers/Frontend/NewsController.php @@ -7,6 +7,8 @@ use App\Helpers\GeneralHelper; use App\Models\NewsCategory; use Illuminate\Http\Request; +use Illuminate\Contracts\View\View; + /** * Frontend Home Controller for handling main website pages */ @@ -28,20 +30,28 @@ class NewsController extends Controller /** * Per page + * + * @var int */ protected $perPage = 5; /** * Default view for the controller * - * @var string + * @var array */ protected $views = [ 'index' => 'news.index', 'show' => 'news.show', ]; - public function index(Request $request) + /** + * Display a listing of published news articles with optional category filtering. + * + * @param Request $request + * @return View + */ + public function index(Request $request): View { $category = $request->get('category', 'all'); $query = News::where('is_published', true)->orderBy('created_at', 'desc'); @@ -59,7 +69,14 @@ public function index(Request $request) return view($this->views['index'], ['data' => $items, 'recentNews' => $recentNews, 'category' => $category]); } - public function show(string $slug, ?string $view = null) + /** + * Display a single news article by slug. + * + * @param string $slug + * @param string|null $view + * @return View + */ + public function show(string $slug, ?string $view = null): View { $item = (new News)->getCachedByField('slug', $slug); if (!$item) abort(404); diff --git a/app/Libraries/Auth/AccountLibrary.php b/app/Libraries/Auth/AccountLibrary.php index 3ea0d3f..1eda22e 100755 --- a/app/Libraries/Auth/AccountLibrary.php +++ b/app/Libraries/Auth/AccountLibrary.php @@ -6,15 +6,27 @@ class AccountLibrary { + /** + * The SoapAccountCreator instance for account creation. + */ private SoapAccountCreator $soapCreator; + + /** + * The realm object containing console credentials. + */ private object $realm; + /** + * Constructor to initialize the AccountLibrary with realm credentials. + * + * @param object $realm The realm object containing console credentials. + */ public function __construct(object $realm) { $this->realm = $realm; $this->soapCreator = new SoapAccountCreator( $realm->console_hostname, - $realm->console_port, + $realm->console_port, $realm->console_username, $realm->console_password, $realm->console_urn, @@ -23,15 +35,36 @@ public function __construct(object $realm) } /** - * Creates a new account using either Battle.net or non-Battle.net authentication + * Creates a new account using either Battle.net or non-Battle.net authentication. + * + * @param string $username The username for the new account. + * @param string $password The password for the new account. + * @param string $email The email address for the new account. + * @param bool $isBnet Whether the account is for Battle.net authentication. + * + * @return bool True if the account was successfully created, false otherwise. + * + * @throws \Exception If the account creation fails. */ public function createNewAccount(string $username, string $password, string $email, bool $isBnet = false): bool { try { + // Validate username and password length + if (strlen($username) < 3 || strlen($password) < 6) { + throw new \Exception('Username must be at least 3 characters long and password must be at least 6 characters long.'); + } + + // Validate email format + if (!filter_var($email, FILTER_VALIDATE_EMAIL)) { + throw new \Exception('Invalid email format.'); + } + if ($isBnet) { + // Create Battle.net account return $this->soapCreator->createAccountBnet($email, $password); } - + + // Create non-Battle.net account return $this->soapCreator->createAccount($username, $password, $email); } catch (\Exception $e) { throw new \Exception('Account creation failed: ' . $e->getMessage()); diff --git a/app/Libraries/Redis/RedisLibrary.php b/app/Libraries/Redis/RedisLibrary.php index 5967983..dbaaa6d 100644 --- a/app/Libraries/Redis/RedisLibrary.php +++ b/app/Libraries/Redis/RedisLibrary.php @@ -3,6 +3,7 @@ namespace App\Libraries\Redis; use Illuminate\Support\Facades\Redis; +use Illuminate\Support\Facades\Cache; /** * Class RedisLibrary @@ -14,6 +15,8 @@ class RedisLibrary { /** @var string Global key prefix */ private string $prefix; + /** @var bool Whether Redis operations are enabled */ + private bool $enabled; /** * RedisLibrary constructor. @@ -23,6 +26,8 @@ class RedisLibrary public function __construct(string $prefix = '') { $this->prefix = $prefix; + // Leer flag desde .env (por petición explícita). Si no existe, por defecto deshabilitado. + $this->enabled = filter_var(env('REDIS_ENABLED', false), FILTER_VALIDATE_BOOL); } /** @@ -53,11 +58,28 @@ public function set(string $key, mixed $value, int $ttl = 0): bool { $key = $this->key($key); - if ($ttl > 0) { - return Redis::setex($key, $ttl, json_encode($value)); + if (!$this->enabled) { + if ($ttl > 0) { + Cache::put($key, $value, now()->addSeconds($ttl)); + } else { + Cache::forever($key, $value); + } + return true; } - return Redis::set($key, json_encode($value)); + try { + if ($ttl > 0) { + return Redis::set($key, $ttl, json_encode($value)); + } + return Redis::set($key, json_encode($value)); + } catch (\Throwable $e) { + if ($ttl > 0) { + Cache::put($key, $value, now()->addSeconds($ttl)); + } else { + Cache::forever($key, $value); + } + return true; + } } /** @@ -68,8 +90,18 @@ public function set(string $key, mixed $value, int $ttl = 0): bool */ public function get(string $key): mixed { - $value = Redis::get($this->key($key)); - return $value ? json_decode($value, true) : null; + $key = $this->key($key); + + if (!$this->enabled) { + return Cache::get($key, null); + } + + try { + $value = Redis::get($key); + return $value ? json_decode($value, true) : null; + } catch (\Throwable $e) { + return Cache::get($key, null); + } } /** @@ -80,7 +112,15 @@ public function get(string $key): mixed */ public function delete(string $key): bool { - return Redis::del($this->key($key)) > 0; + $key = $this->key($key); + if (!$this->enabled) { + return Cache::forget($key); + } + try { + return Redis::del($key) > 0; + } catch (\Throwable $e) { + return Cache::forget($key); + } } /** @@ -91,7 +131,15 @@ public function delete(string $key): bool */ public function exists(string $key): bool { - return Redis::exists($this->key($key)) === 1; + $key = $this->key($key); + if (!$this->enabled) { + return Cache::has($key); + } + try { + return Redis::exists($key) === 1; + } catch (\Throwable $e) { + return Cache::has($key); + } } /** @@ -102,7 +150,15 @@ public function exists(string $key): bool */ public function ttl(string $key): int { - return Redis::ttl($this->key($key)); + $key = $this->key($key); + if (!$this->enabled) { + return -1; // TTL no disponible en fallback de Cache + } + try { + return Redis::ttl($key); + } catch (\Throwable $e) { + return -1; + } } /** @@ -114,7 +170,21 @@ public function ttl(string $key): int */ public function increment(string $key, int $amount = 1): int { - return Redis::incrby($this->key($key), $amount); + $key = $this->key($key); + if (!$this->enabled) { + $current = (int) (Cache::get($key, 0) ?? 0); + $new = $current + $amount; + Cache::put($key, $new); + return $new; + } + try { + return Redis::incrby($key, $amount); + } catch (\Throwable $e) { + $current = (int) (Cache::get($key, 0) ?? 0); + $new = $current + $amount; + Cache::put($key, $new); + return $new; + } } /** @@ -126,7 +196,21 @@ public function increment(string $key, int $amount = 1): int */ public function decrement(string $key, int $amount = 1): int { - return Redis::decrby($this->key($key), $amount); + $key = $this->key($key); + if (!$this->enabled) { + $current = (int) (Cache::get($key, 0) ?? 0); + $new = $current - $amount; + Cache::put($key, $new); + return $new; + } + try { + return Redis::decrby($key, $amount); + } catch (\Throwable $e) { + $current = (int) (Cache::get($key, 0) ?? 0); + $new = $current - $amount; + Cache::put($key, $new); + return $new; + } } /* ====================================================== From 01f77f2e56a43dfb10843f5bb5dc6733fbfb18a2 Mon Sep 17 00:00:00 2001 From: sayghteight Date: Thu, 1 Jan 2026 14:22:30 +0100 Subject: [PATCH 008/132] build: add nixpacks configuration for PHP deployment --- nixpacks.toml | 11 +++++++++++ 1 file changed, 11 insertions(+) create mode 100644 nixpacks.toml diff --git a/nixpacks.toml b/nixpacks.toml new file mode 100644 index 0000000..73d56d8 --- /dev/null +++ b/nixpacks.toml @@ -0,0 +1,11 @@ +providers = ["php"] + +[phases.install] +cmds = [ + "composer install --no-dev --optimize-autoloader" +] + +[phases.build] +cmds = [ + "php artisan migrate --force || true" +] From 1877553dfec6e5df851277dd1fbba7a66317d6d5 Mon Sep 17 00:00:00 2001 From: sayghteight Date: Thu, 1 Jan 2026 14:27:43 +0100 Subject: [PATCH 009/132] build: update nixpacks configuration for deployment - Add --ignore-platform-reqs flag to composer install for compatibility - Move migration command to start phase and combine with nginx/php-fpm setup --- nixpacks.toml | 8 +++----- 1 file changed, 3 insertions(+), 5 deletions(-) diff --git a/nixpacks.toml b/nixpacks.toml index 73d56d8..f1f1dc3 100644 --- a/nixpacks.toml +++ b/nixpacks.toml @@ -2,10 +2,8 @@ providers = ["php"] [phases.install] cmds = [ - "composer install --no-dev --optimize-autoloader" + "composer install --no-dev --optimize-autoloader --ignore-platform-reqs" ] -[phases.build] -cmds = [ - "php artisan migrate --force || true" -] +[start] +cmd = "php artisan migrate --force || true && node /assets/scripts/prestart.mjs /assets/nginx.template.conf /nginx.conf && (php-fpm -y /assets/php-fpm.conf & nginx -c /nginx.conf)" From 75418544dab6f82f8de36b5b6e858d9977e22c58 Mon Sep 17 00:00:00 2001 From: sayghteight Date: Thu, 1 Jan 2026 14:32:20 +0100 Subject: [PATCH 010/132] build(nixpacks): add required php extensions and nodejs for laravel Add typical Laravel PHP extensions (pdo, mbstring, bcmath etc) and Node.js 18 with npm for nginx template support in the Nixpacks configuration. --- nixpacks.toml | 19 +++++++++++++++++++ 1 file changed, 19 insertions(+) diff --git a/nixpacks.toml b/nixpacks.toml index f1f1dc3..432659b 100644 --- a/nixpacks.toml +++ b/nixpacks.toml @@ -1,5 +1,24 @@ providers = ["php"] +[phases.setup] +nixPkgs = [ + "php84", + "php84Packages.composer", + + # Extensiones típicas Laravel + "php84Extensions.pdo", + "php84Extensions.pdo_mysql", + "php84Extensions.mbstring", + "php84Extensions.bcmath", + "php84Extensions.openssl", + "php84Extensions.tokenizer", + "php84Extensions.xml", + + # Necesario para nginx template + "nodejs_18", + "npm-9_x" +] + [phases.install] cmds = [ "composer install --no-dev --optimize-autoloader --ignore-platform-reqs" From 3b9ead661c2fdd2b7cc4816be47eec9d6cb0d394 Mon Sep 17 00:00:00 2001 From: sayghteight Date: Thu, 1 Jan 2026 14:40:29 +0100 Subject: [PATCH 011/132] build(nixpacks): disable nodejs and clean up php extensions --- nixpacks.toml | 11 ++++------- 1 file changed, 4 insertions(+), 7 deletions(-) diff --git a/nixpacks.toml b/nixpacks.toml index 432659b..7e6c104 100644 --- a/nixpacks.toml +++ b/nixpacks.toml @@ -1,22 +1,19 @@ providers = ["php"] +[variables] +NIXPACKS_NODE_DISABLED = "1" + [phases.setup] nixPkgs = [ "php84", "php84Packages.composer", - - # Extensiones típicas Laravel "php84Extensions.pdo", "php84Extensions.pdo_mysql", "php84Extensions.mbstring", "php84Extensions.bcmath", "php84Extensions.openssl", "php84Extensions.tokenizer", - "php84Extensions.xml", - - # Necesario para nginx template - "nodejs_18", - "npm-9_x" + "php84Extensions.xml" ] [phases.install] From 149bda5da7c7bbe1a7499c3e6a1488060287fa6d Mon Sep 17 00:00:00 2001 From: sayghteight Date: Thu, 1 Jan 2026 14:42:40 +0100 Subject: [PATCH 012/132] fix: simplify start command by removing unnecessary operations --- nixpacks.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/nixpacks.toml b/nixpacks.toml index 7e6c104..f64b4f3 100644 --- a/nixpacks.toml +++ b/nixpacks.toml @@ -22,4 +22,4 @@ cmds = [ ] [start] -cmd = "php artisan migrate --force || true && node /assets/scripts/prestart.mjs /assets/nginx.template.conf /nginx.conf && (php-fpm -y /assets/php-fpm.conf & nginx -c /nginx.conf)" +cmd = "php artisan migrate --force" From f58bccbd0bab298cbbd63da2caf0ec7eb78adb8a Mon Sep 17 00:00:00 2001 From: sayghteight Date: Thu, 1 Jan 2026 14:52:03 +0100 Subject: [PATCH 013/132] build(nixpacks): update nixpacks configuration for postbuild phase --- nixpacks.toml | 26 ++++---------------------- 1 file changed, 4 insertions(+), 22 deletions(-) diff --git a/nixpacks.toml b/nixpacks.toml index f64b4f3..f779ae9 100644 --- a/nixpacks.toml +++ b/nixpacks.toml @@ -1,25 +1,7 @@ -providers = ["php"] - -[variables] -NIXPACKS_NODE_DISABLED = "1" - -[phases.setup] -nixPkgs = [ - "php84", - "php84Packages.composer", - "php84Extensions.pdo", - "php84Extensions.pdo_mysql", - "php84Extensions.mbstring", - "php84Extensions.bcmath", - "php84Extensions.openssl", - "php84Extensions.tokenizer", - "php84Extensions.xml" -] - -[phases.install] +[phases.postbuild] cmds = [ - "composer install --no-dev --optimize-autoloader --ignore-platform-reqs" + "php /app/artisan optimize:clear", + "php /app/artisan migrate --force", ] -[start] -cmd = "php artisan migrate --force" +dependsOn = ["build"] \ No newline at end of file From aaa1f4e8688825a4670fc0813da7f9e2a50037fb Mon Sep 17 00:00:00 2001 From: sayghteight Date: Thu, 1 Jan 2026 14:59:38 +0100 Subject: [PATCH 014/132] style: remove extra blank line in routes file --- routes/web.php | 1 - 1 file changed, 1 deletion(-) diff --git a/routes/web.php b/routes/web.php index 58bf602..9c4cd1f 100644 --- a/routes/web.php +++ b/routes/web.php @@ -31,7 +31,6 @@ } }); - Route::middleware([])->group(function () { if (!file_exists(storage_path('installed.lock'))) { Route::get('/install', [InstallController::class, 'index'])->name('install.index'); From 2ccdf0602edc0885174fda3ed284056e8a8da0e9 Mon Sep 17 00:00:00 2001 From: sayghteight Date: Thu, 1 Jan 2026 15:09:07 +0100 Subject: [PATCH 015/132] refactor(RedisLibrary): remove redundant section comments --- app/Libraries/Redis/RedisLibrary.php | 8 -------- 1 file changed, 8 deletions(-) diff --git a/app/Libraries/Redis/RedisLibrary.php b/app/Libraries/Redis/RedisLibrary.php index dbaaa6d..d36fbc7 100644 --- a/app/Libraries/Redis/RedisLibrary.php +++ b/app/Libraries/Redis/RedisLibrary.php @@ -41,10 +41,6 @@ private function key(string $key): string return $this->prefix . $key; } - /* ====================================================== - * Basic Key/Value Methods - * ====================================================== */ - /** * Store a value in Redis. * @@ -213,10 +209,6 @@ public function decrement(string $key, int $amount = 1): int } } - /* ====================================================== - * Token Management (JWT / Sessions) - * ====================================================== */ - /** * Store a user token using a namespaced key. * From 38d8ff5367ec92e171cb889eaac759be286c7a8f Mon Sep 17 00:00:00 2001 From: sayghteight Date: Thu, 1 Jan 2026 15:14:38 +0100 Subject: [PATCH 016/132] refactor: remove redundant comment in AccountLinked model --- app/Models/AccountLinked.php | 40 +++++++++++++++++++++++++++++++++++- 1 file changed, 39 insertions(+), 1 deletion(-) diff --git a/app/Models/AccountLinked.php b/app/Models/AccountLinked.php index 80ce240..dde0b4b 100755 --- a/app/Models/AccountLinked.php +++ b/app/Models/AccountLinked.php @@ -4,22 +4,60 @@ use Illuminate\Database\Eloquent\Model; +/** + * App\Models\AccountLinked + * + * @property int $id + * @property int $user_id + * @property int $realm_id + * @property int $target_id + * @property \Illuminate\Support\Carbon|null $created_at + * @property \Illuminate\Support\Carbon|null $updated_at + * @property-read \App\Models\User $user + * @property-read \App\Models\Realm $realm + * + * @method static \Illuminate\Database\Eloquent\Builder|AccountLinked newModelQuery() + * @method static \Illuminate\Database\Eloquent\Builder|AccountLinked newQuery() + * @method static \Illuminate\Database\Eloquent\Builder|AccountLinked query() + * @method static \Illuminate\Database\Eloquent\Builder|AccountLinked whereUserId($value) + * @method static \Illuminate\Database\Eloquent\Builder|AccountLinked whereRealmId($value) + * @method static \Illuminate\Database\Eloquent\Builder|AccountLinked whereTargetId($value) + */ class AccountLinked extends Model { + /** + * The table associated with the model. + * + * @var string + */ protected $table = 'account_linked'; + /** + * The attributes that are mass assignable. + * + * @var array + */ protected $fillable = [ 'user_id', 'realm_id', 'target_id' ]; - // Relaciones + /** + * Get the user that owns the account link. + * + * @return \Illuminate\Database\Eloquent\Relations\BelongsTo<\App\Models\User, \App\Models\AccountLinked> + */ public function user() { return $this->belongsTo(User::class); } + /** + * Get the realm that owns the account link. + * + * @return \Illuminate\Database\Eloquent\Relations\BelongsTo<\App\Models\Realm, \App\Models\AccountLinked> + */ public function realm() { return $this->belongsTo(Realm::class); From 98fc509a96a42ee97438f5eb35c590f0049ee58d Mon Sep 17 00:00:00 2001 From: sayghteight Date: Sat, 3 Jan 2026 13:47:56 +0100 Subject: [PATCH 017/132] refactor: remove unused ArmoryController and add Redis enabled flag The ArmoryController was deleted as it's no longer used in the application. Added an enabled flag to RedisLibrary to control Redis operations. --- .../Controllers/Frontend/ArmoryController.php | 84 ------------------- app/Libraries/Redis/RedisLibrary.php | 1 + 2 files changed, 1 insertion(+), 84 deletions(-) delete mode 100644 app/Http/Controllers/Frontend/ArmoryController.php diff --git a/app/Http/Controllers/Frontend/ArmoryController.php b/app/Http/Controllers/Frontend/ArmoryController.php deleted file mode 100644 index ec86763..0000000 --- a/app/Http/Controllers/Frontend/ArmoryController.php +++ /dev/null @@ -1,84 +0,0 @@ - 'armory.index', - 'show' => 'armory.show', - ]; - - protected ArmoryRepositoryInterface $armoryRepo; - protected WowheadParserService $wowheadParser; - protected ArmoryService $armoryService; - - public function __construct( - ArmoryRepositoryInterface $armoryRepo, - WowheadParserService $wowheadParser, - ArmoryService $armoryService - ) { - $this->armoryRepo = $armoryRepo; - $this->wowheadParser = $wowheadParser; - $this->armoryService = $armoryService; - } - - /** - * Display a paginated list of Armory characters - */ - public function index(Request $request) - { - $q = $request->input('q'); - $faction = $request->input('faction') ?: null; - $realm = $request->input('realm') ?: 1; // Default to realm 1 if not specified - $class = $request->input('class') ?: null; - $minLevel = $request->input('min_level') ?: null; - - // Pass realm to the repository through the request - $request->merge(['realm' => $realm]); - - $characters = ($q || $faction || $class || $minLevel) - ? $this->armoryService->searchCharacters($q, $faction, $class, $minLevel) - : collect(); - - return view($this->views['index'], [ - 'data' => $characters, - 'search' => $q ?? '', - 'realm' => $realm, - ]); - } - - /** - * Show a single character by GUID - */ - public function show(int $guid, Request $request, ?int $realm = null) - { - // Get realm from URL parameter or query string, default to 1 - $realm = $realm ?: $request->input('realm', 1); - - // Pass realm to the repository through the request - $request->merge(['realm' => $realm]); - - $profile = $this->armoryService->getCharacterProfile($guid); - - abort_if(empty($profile), 404); - - return view($this->views['show'], array_merge($profile, ['realm' => $realm])); - } -} diff --git a/app/Libraries/Redis/RedisLibrary.php b/app/Libraries/Redis/RedisLibrary.php index d36fbc7..6366cf7 100644 --- a/app/Libraries/Redis/RedisLibrary.php +++ b/app/Libraries/Redis/RedisLibrary.php @@ -15,6 +15,7 @@ class RedisLibrary { /** @var string Global key prefix */ private string $prefix; + /** @var bool Whether Redis operations are enabled */ private bool $enabled; From 5a18e1a88ad03516f6f3148b17091c62514a5074 Mon Sep 17 00:00:00 2001 From: sayghteight Date: Sat, 3 Jan 2026 14:03:34 +0100 Subject: [PATCH 018/132] feat(WoWConstants): update race and expansion constants Add new expansion MIDNIGHT and update race constants to include missing races and correct existing values. Update corresponding expansion names, colors, and max levels to reflect current game state. --- app/Enums/WoWConstants.php | 46 ++++++++++++++++++++++++++------------ 1 file changed, 32 insertions(+), 14 deletions(-) diff --git a/app/Enums/WoWConstants.php b/app/Enums/WoWConstants.php index 469e3ca..87d588c 100755 --- a/app/Enums/WoWConstants.php +++ b/app/Enums/WoWConstants.php @@ -16,6 +16,7 @@ class WoWConstants public const EXPANSION_SHADOWLANDS = 8; public const EXPANSION_DRAGONFLIGHT = 9; public const EXPANSION_WAR_WITHIN = 10; + public const EXPANSION_MIDNIGHT = 11; // Classes public const CLASS_WARRIOR = 1; @@ -41,16 +42,28 @@ class WoWConstants public const RACE_TAUREN = 6; public const RACE_GNOME = 7; public const RACE_TROLL = 8; - public const RACE_BLOODELF = 9; - public const RACE_DRAENEI = 10; - public const RACE_WOLF = 11; - public const RACE_GOBLIN = 12; - public const RACE_PANDAREN = 13; - public const RACE_DARK_IRON_DWARF = 14; - public const RACE_HIGHMOUNTAIN_TAUREN = 15; - public const RACE_VOID_ELF = 16; - public const RACE_MAGHAR_ORC = 17; - + public const RACE_GOBLIN = 9; + public const RACE_BLOODELF = 10; + public const RACE_DRAENEI = 11; + public const RACE_WORGEN = 22; + public const RACE_PANDAREN = 24; // Neutral Pandaren + public const RACE_PANDAREN_ALLIANCE = 25; // Alliance Pandaren + public const RACE_PANDAREN_HORDE = 26; // Horde Pandaren + public const RACE_NIGHTBORNE = 27; // Nightborne + public const RACE_HIGHMOUNTAIN_TAUREN = 28; // Highmountain Tauren + public const RACE_VOID_ELF = 29; // Void Elf + public const RACE_LIGHTFORGED_DRAENEI = 30; // Lightforged Draenei + public const RACE_ZANDALARI_TROLL = 31; // Zandalari Troll + public const RACE_KUL_TIRAN = 32; // Kul'Tiran + public const RACE_DARK_IRON_DWARF = 34; // Dark Iron Dwarf + public const RACE_VULPERA = 35; // Vulpera + public const RACE_MAGHAR_ORC = 36; // Maghar Orc + public const RACE_MECAGHOME = 37; // Mecaghome + public const RACE_DRACTHYR_ALLIANCE = 52; // Draconic + public const RACE_DRACTHYR_HORDE = 70; // Draconic + public const RACE_EARTHEN_HORDE = 84; // Earthen + public const RACE_EARTHEN_ALLIANCE = 85; // Earthen + public const EXPANSION_NAMES = [ self::EXPANSION_VANILLA => 'Vanilla', self::EXPANSION_TBC => 'The Burning Crusade', @@ -63,6 +76,7 @@ class WoWConstants self::EXPANSION_SHADOWLANDS => 'Shadowlands', self::EXPANSION_DRAGONFLIGHT => 'Dragonflight', self::EXPANSION_WAR_WITHIN => 'The War Within', + self::EXPANSION_MIDNIGHT => 'Midnight', ]; public const EXPANSION_VERSIONS = [ @@ -91,6 +105,7 @@ class WoWConstants self::EXPANSION_SHADOWLANDS => 'border-blue-500', self::EXPANSION_DRAGONFLIGHT => 'border-green-500', self::EXPANSION_WAR_WITHIN => 'border-yellow-500', + self::EXPANSION_MIDNIGHT => 'border-pink-500', ]; public const EXPANSION_MAX_LEVEL = [ @@ -105,6 +120,7 @@ class WoWConstants self::EXPANSION_SHADOWLANDS => 60, self::EXPANSION_DRAGONFLIGHT => 70, self::EXPANSION_WAR_WITHIN => 80, + self::EXPANSION_MIDNIGHT => 90, ]; public const CLASS_NAMES = [ @@ -153,8 +169,8 @@ class WoWConstants 11 => 'Draenei', 22 => 'Worgen', 24 => 'Pandaren', - 25 => 'Pandaren', - 26 => 'Pandaren', + 25 => 'Pandaren Alliance', + 26 => 'Pandaren Horde', 27 => 'Nightborne', 28 => 'Highmountain Tauren', 29 => 'Void Elf', @@ -165,7 +181,9 @@ class WoWConstants 35 => 'Vulpera', 36 => "Mag'har Orc", 37 => 'Mechagnome', - 52 => 'Dracthyr', - 70 => 'Dracthyr', + 52 => 'Draconic Alliance', + 70 => 'Draconic Horde', + 84 => 'Earthen Horde', + 85 => 'Earthen Alliance', ]; } From 358b20424a0635122fa1d5945ccc2d8e5a76bb03 Mon Sep 17 00:00:00 2001 From: sayghteight Date: Sat, 3 Jan 2026 14:09:20 +0100 Subject: [PATCH 019/132] feat(RealmHelper): add new playable races to faction lists Update alliance and horde race lists to include newly added playable races from recent game expansions --- app/Helpers/RealmHelper.php | 20 ++++++++++++++++---- 1 file changed, 16 insertions(+), 4 deletions(-) diff --git a/app/Helpers/RealmHelper.php b/app/Helpers/RealmHelper.php index 9482da5..e0d843c 100755 --- a/app/Helpers/RealmHelper.php +++ b/app/Helpers/RealmHelper.php @@ -73,7 +73,15 @@ public static function getFactionByRace(int $race): ?string WoWConstants::RACE_NIGHT_ELF, WoWConstants::RACE_GNOME, WoWConstants::RACE_DRAENEI, + WoWConstants::RACE_WORGEN, + WoWConstants::RACE_PANDAREN_ALLIANCE, WoWConstants::RACE_VOID_ELF, + WoWConstants::RACE_LIGHTFORGED_DRAENEI, + WoWConstants::RACE_KUL_TIRAN, + WoWConstants::RACE_DARK_IRON_DWARF, + WoWConstants::RACE_MECAGHOME, + WoWConstants::RACE_DRACTHYR_ALLIANCE, + WoWConstants::RACE_EARTHEN_ALLIANCE, ]; $hordeRaces = [ @@ -81,12 +89,16 @@ public static function getFactionByRace(int $race): ?string WoWConstants::RACE_UNDEAD, WoWConstants::RACE_TAUREN, WoWConstants::RACE_TROLL, - WoWConstants::RACE_BLOODELF, WoWConstants::RACE_GOBLIN, - WoWConstants::RACE_MAGHAR_ORC, + WoWConstants::RACE_BLOODELF, + WoWConstants::RACE_PANDAREN_HORDE, + WoWConstants::RACE_NIGHTBORNE, WoWConstants::RACE_HIGHMOUNTAIN_TAUREN, - WoWConstants::RACE_DARK_IRON_DWARF, - WoWConstants::RACE_PANDAREN, + WoWConstants::RACE_ZANDALARI_TROLL, + WoWConstants::RACE_VULPERA, + WoWConstants::RACE_MAGHAR_ORC, + WoWConstants::RACE_DRACTHYR_HORDE, + WoWConstants::RACE_EARTHEN_HORDE, ]; if (in_array($race, $allianceRaces, true)) { From 737552da93441f11ec73a42736227976e793fb5b Mon Sep 17 00:00:00 2001 From: sayghteight Date: Sat, 3 Jan 2026 14:29:04 +0100 Subject: [PATCH 020/132] refactor(RealmHelper): improve type hints and switch to match expression - Add return type hints for all() and find() methods - Replace switch statement with match expression in getWoWConstant() - Standardize faction strings to lowercase in getFactionByRace() --- app/Helpers/RealmHelper.php | 36 +++++++++++++++--------------------- 1 file changed, 15 insertions(+), 21 deletions(-) diff --git a/app/Helpers/RealmHelper.php b/app/Helpers/RealmHelper.php index e0d843c..60f310c 100755 --- a/app/Helpers/RealmHelper.php +++ b/app/Helpers/RealmHelper.php @@ -4,6 +4,7 @@ use App\Models\Realm; use App\Enums\WoWConstants; +use Illuminate\Database\Eloquent\Collection; class RealmHelper { @@ -12,7 +13,7 @@ class RealmHelper * * @return \Illuminate\Database\Eloquent\Collection */ - public static function all() + public static function all(): Collection { return Realm::all(); } @@ -23,7 +24,7 @@ public static function all() * @param int $id The ID of the realm to find * @return Realm|null Returns the realm if found, null otherwise */ - public static function find($id) + public static function find(int $id): ?Realm { return Realm::find($id); } @@ -35,28 +36,21 @@ public static function find($id) * @param int|null $id The ID of the constant to get * @return string|null Returns the constant value for the given type and ID, or null if not found */ - public static function getWoWConstant(string $type = 'expansion', ?int $id = null) + public static function getWoWConstant(string $type = 'expansion', ?int $id = null): string|int|null { if ($id === null) { return null; } - switch ($type) { - case 'expansion': - return WoWConstants::EXPANSION_NAMES[$id] ?? null; - case 'version': - return WoWConstants::EXPANSION_VERSIONS[$id] ?? null; - case 'color': - return WoWConstants::EXPANSION_COLORS[$id] ?? null; - case 'class': - return WoWConstants::CLASS_NAMES[$id] ?? null; - case 'class_color': - return WoWConstants::CLASS_COLORS[$id] ?? null; - case 'race': - return WoWConstants::RACE_NAMES[$id] ?? null; - default: - return null; - } + return match ($type) { + 'expansion' => WoWConstants::EXPANSION_NAMES[$id] ?? null, + 'version' => WoWConstants::EXPANSION_VERSIONS[$id] ?? null, + 'color' => WoWConstants::EXPANSION_COLORS[$id] ?? null, + 'class' => WoWConstants::CLASS_NAMES[$id] ?? null, + 'class_color' => WoWConstants::CLASS_COLORS[$id] ?? null, + 'race' => WoWConstants::RACE_NAMES[$id] ?? null, + default => null, + }; } /** @@ -102,11 +96,11 @@ public static function getFactionByRace(int $race): ?string ]; if (in_array($race, $allianceRaces, true)) { - return 'Alliance'; + return 'alliance'; } if (in_array($race, $hordeRaces, true)) { - return 'Horde'; + return 'horde'; } return null; From 57fb270bcc832497ab436b862279c7ce54a56de7 Mon Sep 17 00:00:00 2001 From: sayghteight Date: Sat, 3 Jan 2026 23:54:33 +0100 Subject: [PATCH 021/132] feat(forum): implement forum module with models, routes and services Add initial implementation of forum module including: - Forum, Thread and Post models with relationships - Basic routing and controller setup - ForumService for business logic - Initial views structure --- app/Modules/Forum/Domain/Models/Forum.php | 54 ++++++ app/Modules/Forum/Domain/Models/Post.php | 36 ++++ app/Modules/Forum/Domain/Models/Thread.php | 64 +++++++ .../Http/Controllers/ForumController.php | 72 ++++++++ app/Modules/Forum/Http/routes.php | 9 + .../Forum/Providers/ForumServiceProvider.php | 10 ++ app/Modules/Forum/Services/ForumService.php | 162 ++++++++++++++++++ app/Modules/Forum/module.json | 8 + 8 files changed, 415 insertions(+) create mode 100644 app/Modules/Forum/Domain/Models/Forum.php create mode 100644 app/Modules/Forum/Domain/Models/Post.php create mode 100644 app/Modules/Forum/Domain/Models/Thread.php create mode 100644 app/Modules/Forum/Http/Controllers/ForumController.php create mode 100644 app/Modules/Forum/Http/routes.php create mode 100644 app/Modules/Forum/Providers/ForumServiceProvider.php create mode 100644 app/Modules/Forum/Services/ForumService.php create mode 100644 app/Modules/Forum/module.json diff --git a/app/Modules/Forum/Domain/Models/Forum.php b/app/Modules/Forum/Domain/Models/Forum.php new file mode 100644 index 0000000..4a1bbbc --- /dev/null +++ b/app/Modules/Forum/Domain/Models/Forum.php @@ -0,0 +1,54 @@ +hasMany(Thread::class); + } + + /** + * Get the parent forum if this is a subforum + */ + public function parent(): BelongsTo + { + return $this->belongsTo(Forum::class, 'parent_id'); + } + + /** + * Get all subforums for this forum + */ + public function subforums(): HasMany + { + return $this->hasMany(Forum::class, 'parent_id'); + } + + /** + * Get the latest thread in this forum + */ + public function latestThread(): BelongsTo + { + return $this->belongsTo(Thread::class, 'latest_thread_id'); + } +} \ No newline at end of file diff --git a/app/Modules/Forum/Domain/Models/Post.php b/app/Modules/Forum/Domain/Models/Post.php new file mode 100644 index 0000000..974cbd1 --- /dev/null +++ b/app/Modules/Forum/Domain/Models/Post.php @@ -0,0 +1,36 @@ +belongsTo(Thread::class); + } + + /** + * Get the user who created this post + */ + public function user(): BelongsTo + { + return $this->belongsTo(User::class); + } +} \ No newline at end of file diff --git a/app/Modules/Forum/Domain/Models/Thread.php b/app/Modules/Forum/Domain/Models/Thread.php new file mode 100644 index 0000000..8f4f77f --- /dev/null +++ b/app/Modules/Forum/Domain/Models/Thread.php @@ -0,0 +1,64 @@ +hasMany(Post::class); + } + + /** + * Get the forum this thread belongs to + */ + public function forum(): BelongsTo + { + return $this->belongsTo(Forum::class); + } + + /** + * Get the user who created this thread + */ + public function user(): BelongsTo + { + return $this->belongsTo(User::class); + } + + /** + * Get the first post of this thread + */ + public function firstPost(): BelongsTo + { + return $this->belongsTo(Post::class, 'first_post_id'); + } + + /** + * Get the latest post of this thread + */ + public function latestPost(): BelongsTo + { + return $this->belongsTo(Post::class, 'latest_post_id'); + } +} \ No newline at end of file diff --git a/app/Modules/Forum/Http/Controllers/ForumController.php b/app/Modules/Forum/Http/Controllers/ForumController.php new file mode 100644 index 0000000..99e05d7 --- /dev/null +++ b/app/Modules/Forum/Http/Controllers/ForumController.php @@ -0,0 +1,72 @@ + 'forums.index', + 'forum' => 'forums.forum', + 'thread' => 'forums.thread', + 'create_thread' => 'forums.create_thread', + 'create_post' => 'forums.create_post', + ]; + + public function __construct(ForumService $forumService) + { + $this->forumService = $forumService; + } + + public function index() : View + { + $categories = $this->forumService->getCategories(); + + return view($this->views['index'], compact('categories')); + } + + public function forum(string $slug) : View + { + $forum = $this->forumService->getForumWithThreads($slug); + + return view($this->views['forum'], compact('forum')); + } + + public function thread(string $forumSlug, string $threadSlug, ?string $view = null) : View + { + $data = $this->forumService->getThreadWithPosts($forumSlug, $threadSlug); + + return view($this->views['thread'], compact('thread')); + } + + public function createThread(string $slug) : View + { + $forum = Forum::where('slug', $slug)->firstOrFail(); + + return view($this->views['create_thread'], compact('forum')); + } + + public function storeThread(Request $request, string $slug) + { + $forum = Forum::where('slug', $slug)->firstOrFail(); + + $validated = $request->validate([ + 'title' => 'required|min:3|max:255', + 'content' => 'required|min:10', + ]); + + $threadSlug = $this->forumService->createThread($forum, $validated); + + return redirect()->route('forums.thread', [ + 'forumSlug' => $forum->slug, + 'threadSlug' => $threadSlug, + ])->with('success', 'Thread created successfully!'); + } +} \ No newline at end of file diff --git a/app/Modules/Forum/Http/routes.php b/app/Modules/Forum/Http/routes.php new file mode 100644 index 0000000..fd607c4 --- /dev/null +++ b/app/Modules/Forum/Http/routes.php @@ -0,0 +1,9 @@ +prefix(strtolower('Forum')) + ->group(function () { + Route::get('/', [\Modules\Forum\Http\Controllers\ForumController::class, 'index']); + }); \ No newline at end of file diff --git a/app/Modules/Forum/Providers/ForumServiceProvider.php b/app/Modules/Forum/Providers/ForumServiceProvider.php new file mode 100644 index 0000000..a1d60f6 --- /dev/null +++ b/app/Modules/Forum/Providers/ForumServiceProvider.php @@ -0,0 +1,10 @@ +orderBy('order') + ->with(['subforums' => function ($query) { + $query->orderBy('order'); + }]) + ->get(); + } + + /** + * Get a forum by slug with its threads. + * + * @param string $slug + * @return array{forum: Forum, threads: \Illuminate\Contracts\Pagination\LengthAwarePaginator} + */ + public function getForumWithThreads(string $slug) + { + $forum = Forum::where('slug', $slug) + ->with(['subforums' => function ($query) { + $query->orderBy('order'); + }]) + ->firstOrFail(); + + $threads = Thread::where('forum_id', $forum->id) + ->orderBy('is_sticky', 'desc') + ->orderBy('created_at', 'desc') + ->with(['user', 'latestPost.user']) + ->paginate(20); + + return compact('forum', 'threads'); + } + + /** + * Get a thread with its posts. + * + * @param string $forumSlug + * @param string $threadSlug + * @return array{forum: Forum, thread: Thread, posts: \Illuminate\Contracts\Pagination\LengthAwarePaginator} + */ + public function getThreadWithPosts(string $forumSlug, string $threadSlug) + { + $forum = Forum::where('slug', $forumSlug)->firstOrFail(); + $thread = Thread::where('slug', $threadSlug) + ->where('forum_id', $forum->id) + ->firstOrFail(); + + $thread->increment('view_count'); + + $posts = Post::where('thread_id', $thread->id) + ->orderBy('created_at') + ->with('user') + ->paginate(15); + + return compact('forum', 'thread', 'posts'); + } + + /** + * Create a new thread with its first post. + * + * @param Forum $forum + * @param array $validated Validated thread data ['title' => string, 'content' => string] + * @return string The unique slug of the created thread + * @throws \Throwable + */ + public function createThread(Forum $forum, array $validated) + { + $threadSlug = Str::slug($validated['title']); + + // Make slug unique if it already exists + $count = Thread::where('slug', $threadSlug)->count(); + if ($count > 0) { + $threadSlug .= '-' . time(); + } + + DB::transaction(function () use ($forum, $validated, $threadSlug) { + $thread = Thread::create([ + 'title' => $validated['title'], + 'slug' => $threadSlug, + 'forum_id' => $forum->id, + 'user_id' => Auth::id(), + ]); + + $post = Post::create([ + 'thread_id' => $thread->id, + 'user_id' => Auth::id(), + 'content' => $validated['content'], + 'is_first_post' => true, + ]); + + $thread->update([ + 'first_post_id' => $post->id, + 'latest_post_id' => $post->id, + ]); + + $forum->update([ + 'latest_thread_id' => $thread->id, + ]); + }); + + return $threadSlug; + } + + /** + * Create a new post in a thread. + * + * @param Thread $thread + * @param array $validated Validated post data ['content' => string] + * @return void + * @throws \Exception If the thread is locked + * @throws \Throwable + */ + public function createPost(Thread $thread, array $validated) + { + if ($thread->is_locked) { + throw new \Exception('Thread is locked.'); + } + + DB::transaction(function () use ($thread, $validated) { + $post = Post::create([ + 'thread_id' => $thread->id, + 'user_id' => Auth::id(), + 'content' => $validated['content'], + ]); + + $thread->update([ + 'latest_post_id' => $post->id, + ]); + + $thread->forum->update([ + 'latest_thread_id' => $thread->id, + ]); + }); + } +} diff --git a/app/Modules/Forum/module.json b/app/Modules/Forum/module.json new file mode 100644 index 0000000..173734b --- /dev/null +++ b/app/Modules/Forum/module.json @@ -0,0 +1,8 @@ +{ + "name": "Forum", + "enabled": true, + "routes": true, + "migrations": true, + "views": true, + "namespace": "Modules\\Forum" +} \ No newline at end of file From c57a1741699687d09215941bb545693fd06a0f07 Mon Sep 17 00:00:00 2001 From: sayghteight Date: Sun, 4 Jan 2026 01:16:47 +0100 Subject: [PATCH 022/132] feat(armory): add realm validation in character search Add getRealmById method to validate realm existence before character search --- .../Domain/Interfaces/ArmoryRepositoryInterface.php | 1 + app/Modules/Armory/Http/Controllers/ArmoryController.php | 9 +++++++++ app/Modules/Armory/Services/ArmoryService.php | 7 +++++++ 3 files changed, 17 insertions(+) diff --git a/app/Modules/Armory/Domain/Interfaces/ArmoryRepositoryInterface.php b/app/Modules/Armory/Domain/Interfaces/ArmoryRepositoryInterface.php index 671d54a..13bf2cf 100644 --- a/app/Modules/Armory/Domain/Interfaces/ArmoryRepositoryInterface.php +++ b/app/Modules/Armory/Domain/Interfaces/ArmoryRepositoryInterface.php @@ -13,4 +13,5 @@ public function getGuildRankMember(int $guildId, int $memberGuid); public function getAchievementsCharacter(int $guid); public function getSkillCharacter(int $guid); public function getArenaTeam(int $guid); + public function getRealmById(int $id); } \ No newline at end of file diff --git a/app/Modules/Armory/Http/Controllers/ArmoryController.php b/app/Modules/Armory/Http/Controllers/ArmoryController.php index db0c026..7a3f50e 100644 --- a/app/Modules/Armory/Http/Controllers/ArmoryController.php +++ b/app/Modules/Armory/Http/Controllers/ArmoryController.php @@ -79,6 +79,15 @@ public function index(Request $request) $class = $request->input('class') ?: null; $minLevel = $request->input('min_level') ?: null; + // Validate if realm is on db + $realm = $this->armoryRepo->getRealmById($realm); + + if (empty($realm)) { + return view($this->views['index'], [ + 'error' => 'Realm not found', + ]); + } + $request->merge(['realm' => $realm]); $characters = ($q || $faction || $class || $minLevel) diff --git a/app/Modules/Armory/Services/ArmoryService.php b/app/Modules/Armory/Services/ArmoryService.php index 1081d4b..be5a17c 100644 --- a/app/Modules/Armory/Services/ArmoryService.php +++ b/app/Modules/Armory/Services/ArmoryService.php @@ -75,4 +75,11 @@ public function searchCharacters( ): Collection { return $this->armoryRepo->search($q, $faction, $class, $minLevel); } + + public function getRealmById(int $id): array + { + $realm = $this->armoryRepo->getRealmById($id); + + return $realm ? $realm->toArray() : []; + } } \ No newline at end of file From 7578e69927517a52b603f7a1920c8b8389b6dd1c Mon Sep 17 00:00:00 2001 From: sayghteight Date: Sun, 4 Jan 2026 01:22:39 +0100 Subject: [PATCH 023/132] chore: remove obsolete nixpacks configuration file --- nixpacks.toml | 7 ------- 1 file changed, 7 deletions(-) delete mode 100644 nixpacks.toml diff --git a/nixpacks.toml b/nixpacks.toml deleted file mode 100644 index f779ae9..0000000 --- a/nixpacks.toml +++ /dev/null @@ -1,7 +0,0 @@ -[phases.postbuild] -cmds = [ - "php /app/artisan optimize:clear", - "php /app/artisan migrate --force", -] - -dependsOn = ["build"] \ No newline at end of file From 76c8380c49c231715259e678cf4d736c6875ba29 Mon Sep 17 00:00:00 2001 From: sayghteight Date: Sun, 4 Jan 2026 01:30:28 +0100 Subject: [PATCH 024/132] refactor(armory): remove realm validation and related methods The realm validation was moved to the service provider where it's more appropriate. Removed unused getRealmById method and related interface method to clean up the codebase. --- .../Domain/Interfaces/ArmoryRepositoryInterface.php | 1 - app/Modules/Armory/Http/Controllers/ArmoryController.php | 9 --------- app/Modules/Armory/Providers/ArmoryServiceProvider.php | 3 ++- app/Modules/Armory/Services/ArmoryService.php | 7 ------- 4 files changed, 2 insertions(+), 18 deletions(-) diff --git a/app/Modules/Armory/Domain/Interfaces/ArmoryRepositoryInterface.php b/app/Modules/Armory/Domain/Interfaces/ArmoryRepositoryInterface.php index 13bf2cf..671d54a 100644 --- a/app/Modules/Armory/Domain/Interfaces/ArmoryRepositoryInterface.php +++ b/app/Modules/Armory/Domain/Interfaces/ArmoryRepositoryInterface.php @@ -13,5 +13,4 @@ public function getGuildRankMember(int $guildId, int $memberGuid); public function getAchievementsCharacter(int $guid); public function getSkillCharacter(int $guid); public function getArenaTeam(int $guid); - public function getRealmById(int $id); } \ No newline at end of file diff --git a/app/Modules/Armory/Http/Controllers/ArmoryController.php b/app/Modules/Armory/Http/Controllers/ArmoryController.php index 7a3f50e..db0c026 100644 --- a/app/Modules/Armory/Http/Controllers/ArmoryController.php +++ b/app/Modules/Armory/Http/Controllers/ArmoryController.php @@ -79,15 +79,6 @@ public function index(Request $request) $class = $request->input('class') ?: null; $minLevel = $request->input('min_level') ?: null; - // Validate if realm is on db - $realm = $this->armoryRepo->getRealmById($realm); - - if (empty($realm)) { - return view($this->views['index'], [ - 'error' => 'Realm not found', - ]); - } - $request->merge(['realm' => $realm]); $characters = ($q || $faction || $class || $minLevel) diff --git a/app/Modules/Armory/Providers/ArmoryServiceProvider.php b/app/Modules/Armory/Providers/ArmoryServiceProvider.php index 5099ea6..61e57f5 100644 --- a/app/Modules/Armory/Providers/ArmoryServiceProvider.php +++ b/app/Modules/Armory/Providers/ArmoryServiceProvider.php @@ -20,8 +20,9 @@ public function register(): void $realmId = $request->get('realm', 1); $realm = Realm::find($realmId); + if (!$realm) { - throw new \Exception("Realm $realmId not found."); + return []; } $emulator = $realm->emulator; diff --git a/app/Modules/Armory/Services/ArmoryService.php b/app/Modules/Armory/Services/ArmoryService.php index be5a17c..1081d4b 100644 --- a/app/Modules/Armory/Services/ArmoryService.php +++ b/app/Modules/Armory/Services/ArmoryService.php @@ -75,11 +75,4 @@ public function searchCharacters( ): Collection { return $this->armoryRepo->search($q, $faction, $class, $minLevel); } - - public function getRealmById(int $id): array - { - $realm = $this->armoryRepo->getRealmById($id); - - return $realm ? $realm->toArray() : []; - } } \ No newline at end of file From bcba5277558c0f84568168291e6275dfce85c069 Mon Sep 17 00:00:00 2001 From: sayghteight Date: Sun, 4 Jan 2026 01:54:14 +0100 Subject: [PATCH 025/132] fix(ArmoryServiceProvider): throw exception when realm not found --- app/Modules/Armory/Providers/ArmoryServiceProvider.php | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/app/Modules/Armory/Providers/ArmoryServiceProvider.php b/app/Modules/Armory/Providers/ArmoryServiceProvider.php index 61e57f5..2b9b547 100644 --- a/app/Modules/Armory/Providers/ArmoryServiceProvider.php +++ b/app/Modules/Armory/Providers/ArmoryServiceProvider.php @@ -20,9 +20,9 @@ public function register(): void $realmId = $request->get('realm', 1); $realm = Realm::find($realmId); - + if (!$realm) { - return []; + throw new \Exception("Realm $realmId not found."); } $emulator = $realm->emulator; From 803d16964e6b6ff36bb367211a862050798b283b Mon Sep 17 00:00:00 2001 From: sayghteight Date: Sun, 4 Jan 2026 23:31:36 +0100 Subject: [PATCH 026/132] feat(donate): add donate module with routes and views Implement a new donate module including: - Module configuration - Service provider - Controller with index route - Donation page view with packages --- .../Http/Controllers/DonateController.php | 13 ++ app/Modules/Donate/Http/routes.php | 9 + .../Providers/DonateServiceProvider.php | 10 + .../Donate/Resources/views/home.blade.php | 210 ++++++++++++++++++ app/Modules/Donate/module.json | 8 + resources/views/layouts/main.blade.php | 4 +- 6 files changed, 252 insertions(+), 2 deletions(-) create mode 100644 app/Modules/Donate/Http/Controllers/DonateController.php create mode 100644 app/Modules/Donate/Http/routes.php create mode 100644 app/Modules/Donate/Providers/DonateServiceProvider.php create mode 100644 app/Modules/Donate/Resources/views/home.blade.php create mode 100644 app/Modules/Donate/module.json diff --git a/app/Modules/Donate/Http/Controllers/DonateController.php b/app/Modules/Donate/Http/Controllers/DonateController.php new file mode 100644 index 0000000..6c1066d --- /dev/null +++ b/app/Modules/Donate/Http/Controllers/DonateController.php @@ -0,0 +1,13 @@ +prefix(strtolower('Donate')) + ->group(function () { + Route::get('/', [\Modules\Donate\Http\Controllers\DonateController::class, 'index'])->name('donate'); + }); \ No newline at end of file diff --git a/app/Modules/Donate/Providers/DonateServiceProvider.php b/app/Modules/Donate/Providers/DonateServiceProvider.php new file mode 100644 index 0000000..fe2efd4 --- /dev/null +++ b/app/Modules/Donate/Providers/DonateServiceProvider.php @@ -0,0 +1,10 @@ + +
+
+

+ Support NexusCMS +

+

+ Your donations help us maintain high-quality servers, develop custom content, and keep the community thriving. Every contribution makes a difference. +

+
+
+ + +
+
+

Donation Packages

+

Choose a package that suits you best

+
+ +
+ +
+
+
+ +
+

Bronze Supporter

+
$5
+

One-time donation

+
+
    +
  • + + 500 Donation Points +
  • +
+ +
+ + +
+
+ MOST POPULAR +
+
+
+ +
+

Silver Supporter

+
$15
+

One-time donation

+
+
    +
  • + + 1,800 Donation Points +
  • +
+ +
+ + +
+
+
+ +
+

Gold Supporter

+
$30
+

One-time donation

+
+
    +
  • + + 4,000 Donation Points +
  • +
+ +
+
+ + +
+
+

Custom Donation Amount

+

Choose your own amount to support the server

+
+
+ $ + +
+ +
+

Conversion rate: $1 = 100 Donation Points

+
+
+ + +
+
+

What Can You Buy?

+

Spend your donation points on exclusive items and services

+
+ +
+
+

Epic Mount

+

Exclusive flying mount

+
+ 1,500 DP + +
+
+ +
+

Transmog Set

+

Legendary appearance

+
+ 2,000 DP + +
+
+ +
+

Name Change

+

Change character name

+
+ 500 DP + +
+
+ +
+

Level Boost

+

Instant level 70

+
+ 3,000 DP + +
+
+
+ + +
+ + +
+

Donation FAQ

+
+
+

+ + How do I receive my donation points? +

+

Donation points are automatically added to your account within 5 minutes after a successful payment.

+
+
+

+ + What payment methods do you accept? +

+

We accept PayPal, credit cards, and various cryptocurrency options for your convenience.

+
+
+

+ + Can I get a refund? +

+

Donations are generally non-refundable. However, if you encounter technical issues, please contact our support team.

+
+
+

+ + Are donations required to play? +

+

Absolutely not! NexusCMS is completely free to play. Donations help us improve the server but are entirely optional.

+
+
+
+
+ + +
+
+ +

Thank You for Your Support!

+

+ Every donation, no matter the size, helps us maintain our servers, develop new content, and create the best possible experience for our community. We couldn't do this without supporters like you. +

+
+
+@endsection \ No newline at end of file diff --git a/app/Modules/Donate/module.json b/app/Modules/Donate/module.json new file mode 100644 index 0000000..9604e23 --- /dev/null +++ b/app/Modules/Donate/module.json @@ -0,0 +1,8 @@ +{ + "name": "Donate", + "enabled": true, + "routes": true, + "migrations": true, + "views": true, + "namespace": "Modules\\Donate" +} \ No newline at end of file diff --git a/resources/views/layouts/main.blade.php b/resources/views/layouts/main.blade.php index 036354d..4edbd99 100644 --- a/resources/views/layouts/main.blade.php +++ b/resources/views/layouts/main.blade.php @@ -28,7 +28,7 @@ HOW TO PLAY ARMORY - DONATE + DONATE
@@ -60,7 +60,7 @@ NEWS HOW TO PLAY ARMORY - DONATE + DONATE @auth
From 08b7d7479a1d916e6c45bc840e2f24eb0fa716a6 Mon Sep 17 00:00:00 2001 From: sayghteight Date: Wed, 7 Jan 2026 20:50:05 +0100 Subject: [PATCH 027/132] feat(donate): implement braintree payment gateway integration - Add Braintree PHP SDK dependency - Create Braintree gateway implementation - Add donation transaction model and migration - Extend user model with dp/vp fields - Implement checkout flow with Braintree - Add donation routes and controller logic - Update donate view with payment form --- .env.example | 6 ++ app/Models/User.php | 4 + .../Interfaces/PaymentGatewayInterface.php | 15 ++++ .../Domain/Models/DonationTransaction.php | 28 +++++++ .../Http/Controllers/DonateController.php | 72 +++++++++++++++++- app/Modules/Donate/Http/routes.php | 8 +- .../Gateways/BraintreeGateway.php | 75 +++++++++++++++++++ .../Providers/DonateServiceProvider.php | 25 ++++++- .../Resources/views/braintree.blade.php | 44 +++++++++++ .../Donate/Resources/views/home.blade.php | 43 ++++++++--- .../Donate/Services/GatewayManager.php | 34 +++++++++ composer.json | 3 +- composer.lock | 51 ++++++++++++- config/donate.php | 16 ++++ ..._01_07_000001_add_dp_vp_to_users_table.php | 24 ++++++ ...002_create_donation_transactions_table.php | 31 ++++++++ 16 files changed, 462 insertions(+), 17 deletions(-) create mode 100644 app/Modules/Donate/Domain/Interfaces/PaymentGatewayInterface.php create mode 100644 app/Modules/Donate/Domain/Models/DonationTransaction.php create mode 100644 app/Modules/Donate/Infrastructure/Gateways/BraintreeGateway.php create mode 100644 app/Modules/Donate/Resources/views/braintree.blade.php create mode 100644 app/Modules/Donate/Services/GatewayManager.php create mode 100644 config/donate.php create mode 100644 database/migrations/2026_01_07_000001_add_dp_vp_to_users_table.php create mode 100644 database/migrations/2026_01_07_000002_create_donation_transactions_table.php diff --git a/.env.example b/.env.example index ce7a7da..400324a 100644 --- a/.env.example +++ b/.env.example @@ -59,4 +59,10 @@ AWS_DEFAULT_REGION=us-east-1 AWS_BUCKET= AWS_USE_PATH_STYLE_ENDPOINT=false +## Donate Gateways +BRAINTREE_ENVIRONMENT=sandbox +BRAINTREE_MERCHANT_ID= +BRAINTREE_PUBLIC_KEY= +BRAINTREE_PRIVATE_KEY= + VITE_APP_NAME="${APP_NAME}" diff --git a/app/Models/User.php b/app/Models/User.php index 2ae77e3..3cb83bc 100755 --- a/app/Models/User.php +++ b/app/Models/User.php @@ -17,6 +17,8 @@ class User extends Authenticatable 'email', 'password', 'created_at', + 'dp', + 'vp', ]; protected $hidden = [ @@ -27,6 +29,8 @@ class User extends Authenticatable protected $casts = [ 'email_verified_at' => 'datetime', 'password' => 'hashed', + 'dp' => 'integer', + 'vp' => 'integer', ]; /** diff --git a/app/Modules/Donate/Domain/Interfaces/PaymentGatewayInterface.php b/app/Modules/Donate/Domain/Interfaces/PaymentGatewayInterface.php new file mode 100644 index 0000000..87f5b90 --- /dev/null +++ b/app/Modules/Donate/Domain/Interfaces/PaymentGatewayInterface.php @@ -0,0 +1,15 @@ + 'integer', + 'dp_awarded' => 'integer', + 'meta' => 'array', + ]; +} + diff --git a/app/Modules/Donate/Http/Controllers/DonateController.php b/app/Modules/Donate/Http/Controllers/DonateController.php index 6c1066d..dcdf9d7 100644 --- a/app/Modules/Donate/Http/Controllers/DonateController.php +++ b/app/Modules/Donate/Http/Controllers/DonateController.php @@ -3,11 +3,77 @@ namespace Modules\Donate\Http\Controllers; use App\Http\Controllers\Controller; +use Illuminate\Http\Request; +use Modules\Donate\Services\GatewayManager; +use Modules\Donate\Domain\Models\DonationTransaction; +use Illuminate\Support\Facades\DB; class DonateController extends Controller { - public function index() + public function index(GatewayManager $gateways) { - return view('donate::home'); + $available = array_map(fn($g) => ['id' => $g->id(), 'name' => $g->displayName()], $gateways->all()); + return view('donate::home', ['gateways' => $available]); } -} \ No newline at end of file + + public function checkout(Request $request, GatewayManager $gateways) + { + $gatewayId = $request->string('gateway')->toString(); + $amount = (int) $request->input('amount', 0); + $gateway = $gateways->get($gatewayId); + abort_unless($gateway, 404); + $meta = ['user_id' => optional($request->user())->id]; + + if ($request->filled('nonce')) { + $meta['nonce'] = $request->input('nonce'); + } + $result = $gateway->createCheckout($amount, $meta); + if (isset($result['client_token'])) { + return view('donate::braintree', ['token' => $result['client_token'], 'amount' => $amount]); + } + if (isset($result['redirect_url'])) { + return redirect()->away($result['redirect_url']); + } + if (($result['status'] ?? null) === 'success') { + $user = $request->user(); + $rate = (int) config('donate.dp_rate', 100); + $dp = (int) ($amount * $rate); + DB::transaction(function () use ($user, $gatewayId, $amount, $dp, $result) { + $tx = DonationTransaction::create([ + 'user_id' => $user->id, + 'gateway' => $gatewayId, + 'transaction_id' => $result['transaction_id'] ?? null, + 'amount' => $amount, + 'currency' => 'USD', + 'dp_awarded' => $dp, + 'status' => 'completed', + 'meta' => ['raw' => $result], + ]); + $user->increment('dp', $dp); + }); + return response()->json([ + 'status' => 'success', + 'dp_awarded' => $dp, + 'amount' => $amount, + 'gateway' => $gatewayId, + ]); + } + return response()->json($result, 400); + } + + public function callback(string $gateway, Request $request, GatewayManager $gateways) + { + $gw = $gateways->get($gateway); + abort_unless($gw, 404); + $result = $gw->handleCallback($request); + return response()->json($result); + } + + public function webhook(string $gateway, Request $request, GatewayManager $gateways) + { + $gw = $gateways->get($gateway); + abort_unless($gw, 404); + $result = $gw->handleWebhook($request); + return response()->json($result); + } +} diff --git a/app/Modules/Donate/Http/routes.php b/app/Modules/Donate/Http/routes.php index 768fa1b..9b94c76 100644 --- a/app/Modules/Donate/Http/routes.php +++ b/app/Modules/Donate/Http/routes.php @@ -1,9 +1,13 @@ prefix(strtolower('Donate')) ->group(function () { - Route::get('/', [\Modules\Donate\Http\Controllers\DonateController::class, 'index'])->name('donate'); - }); \ No newline at end of file + Route::get('/', [DonateController::class, 'index'])->name('donate'); + Route::post('/checkout', [DonateController::class, 'checkout'])->middleware('auth')->name('donate.checkout'); + Route::match(['get', 'post'], '/callback/{gateway}', [DonateController::class, 'callback'])->name('donate.callback'); + Route::post('/webhook/{gateway}', [DonateController::class, 'webhook'])->name('donate.webhook'); + }); diff --git a/app/Modules/Donate/Infrastructure/Gateways/BraintreeGateway.php b/app/Modules/Donate/Infrastructure/Gateways/BraintreeGateway.php new file mode 100644 index 0000000..bb53f56 --- /dev/null +++ b/app/Modules/Donate/Infrastructure/Gateways/BraintreeGateway.php @@ -0,0 +1,75 @@ +gateway = new Gateway([ + 'environment' => $cfg['environment'] ?? 'sandbox', + 'merchantId' => $cfg['merchant_id'] ?? '', + 'publicKey' => $cfg['public_key'] ?? '', + 'privateKey' => $cfg['private_key'] ?? '', + ]); + } + + public function id(): string + { + return 'braintree'; + } + + public function displayName(): string + { + return 'Braintree'; + } + + public function createCheckout(int $amount, array $meta = []): array + { + if (isset($meta['nonce'])) { + $result = $this->gateway->transaction()->sale([ + 'amount' => number_format($amount, 2, '.', ''), + 'paymentMethodNonce' => $meta['nonce'], + 'options' => ['submitForSettlement' => true], + ]); + if ($result->success) { + return [ + 'status' => 'success', + 'transaction_id' => $result->transaction->id, + 'amount' => $amount, + ]; + } + $errors = []; + foreach ($result->errors->deepAll() as $error) { + $errors[] = "{$error->code}: {$error->message}"; + } + return [ + 'status' => 'error', + 'errors' => $errors, + ]; + } + + $token = $this->gateway->clientToken()->generate(); + return [ + 'client_token' => $token, + 'amount' => $amount, + ]; + } + + public function handleCallback(Request $request): array + { + return ['status' => 'success', 'gateway' => $this->id()]; + } + + public function handleWebhook(Request $request): array + { + return ['received' => true]; + } +} diff --git a/app/Modules/Donate/Providers/DonateServiceProvider.php b/app/Modules/Donate/Providers/DonateServiceProvider.php index fe2efd4..a8e8005 100644 --- a/app/Modules/Donate/Providers/DonateServiceProvider.php +++ b/app/Modules/Donate/Providers/DonateServiceProvider.php @@ -3,8 +3,31 @@ namespace Modules\Donate\Providers; use App\Providers\BaseModuleServiceProvider; +use Modules\Donate\Services\GatewayManager; +use Modules\Donate\Infrastructure\Gateways\StripeGateway; +use Modules\Donate\Infrastructure\Gateways\BraintreeGateway; +use Modules\Donate\Infrastructure\Gateways\MercadoPagoGateway; class DonateServiceProvider extends BaseModuleServiceProvider { protected string $moduleName = 'Donate'; -} \ No newline at end of file + + public function register(): void + { + $this->mergeConfigFrom(base_path('config/donate.php'), 'donate'); + + $this->app->singleton(GatewayManager::class, function () { + $enabled = config('donate.enabled_gateways', []); + $map = [ + 'braintree' => new BraintreeGateway(), + ]; + $gateways = []; + foreach ($enabled as $id) { + if (isset($map[$id])) { + $gateways[] = $map[$id]; + } + } + return new GatewayManager($gateways); + }); + } +} diff --git a/app/Modules/Donate/Resources/views/braintree.blade.php b/app/Modules/Donate/Resources/views/braintree.blade.php new file mode 100644 index 0000000..58ae155 --- /dev/null +++ b/app/Modules/Donate/Resources/views/braintree.blade.php @@ -0,0 +1,44 @@ +@extends('layouts.main') + +@section('title', 'Braintree Checkout') + +@section('content') +
+
+

Complete your payment

+

Amount: ${{ number_format($amount ?? 0, 2) }}

+
+ @csrf + + + +
+ +
+
+
+ + + +@endsection + diff --git a/app/Modules/Donate/Resources/views/home.blade.php b/app/Modules/Donate/Resources/views/home.blade.php index 95047e5..df6c1de 100644 --- a/app/Modules/Donate/Resources/views/home.blade.php +++ b/app/Modules/Donate/Resources/views/home.blade.php @@ -95,15 +95,40 @@
@@ -207,4 +232,4 @@

-@endsection \ No newline at end of file +@endsection diff --git a/app/Modules/Donate/Services/GatewayManager.php b/app/Modules/Donate/Services/GatewayManager.php new file mode 100644 index 0000000..b10e4c5 --- /dev/null +++ b/app/Modules/Donate/Services/GatewayManager.php @@ -0,0 +1,34 @@ +register($gateway); + } + } + + public function register(PaymentGatewayInterface $gateway): void + { + $this->gateways[$gateway->id()] = $gateway; + } + + public function all(): array + { + return $this->gateways; + } + + public function get(string $id): ?PaymentGatewayInterface + { + return Arr::get($this->gateways, $id); + } +} + diff --git a/composer.json b/composer.json index 80ccdab..61d193a 100755 --- a/composer.json +++ b/composer.json @@ -18,7 +18,8 @@ "laravel/tinker": "2.10.1", "predis/predis": "3.2.0", "spatie/laravel-permission": "^6.18", - "wowcrypto/wowcrypto": "1.2.0" + "wowcrypto/wowcrypto": "1.2.0", + "braintree/braintree_php": "^6.13" }, "require-dev": { "fakerphp/faker": "1.23", diff --git a/composer.lock b/composer.lock index e353110..8635ce0 100755 --- a/composer.lock +++ b/composer.lock @@ -4,8 +4,57 @@ "Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies", "This file is @generated automatically" ], - "content-hash": "8d94a0b29b5ef74cbf8ba04ad7c94ba4", + "content-hash": "b336f73b62082a66e2b07c931571e0f2", "packages": [ + { + "name": "braintree/braintree_php", + "version": "6.31.0", + "source": { + "type": "git", + "url": "https://github.com/braintree/braintree_php.git", + "reference": "5c41da561a821d4131bcd336322e196e0198ef83" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/braintree/braintree_php/zipball/5c41da561a821d4131bcd336322e196e0198ef83", + "reference": "5c41da561a821d4131bcd336322e196e0198ef83", + "shasum": "" + }, + "require": { + "ext-curl": "*", + "ext-dom": "*", + "ext-hash": "*", + "ext-openssl": "*", + "ext-xmlwriter": "*", + "php": ">=7.3.0" + }, + "require-dev": { + "phpunit/phpunit": "^9.0", + "squizlabs/php_codesniffer": "^3.0" + }, + "type": "library", + "autoload": { + "psr-4": { + "Braintree\\": "lib/Braintree" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Braintree", + "homepage": "https://www.braintreepayments.com" + } + ], + "description": "Braintree PHP Client Library", + "support": { + "issues": "https://github.com/braintree/braintree_php/issues", + "source": "https://github.com/braintree/braintree_php/tree/6.31.0" + }, + "time": "2025-12-11T16:27:18+00:00" + }, { "name": "brick/math", "version": "0.14.1", diff --git a/config/donate.php b/config/donate.php new file mode 100644 index 0000000..535c0d1 --- /dev/null +++ b/config/donate.php @@ -0,0 +1,16 @@ + [ + 'braintree', + ], + 'gateways' => [ + 'braintree' => [ + 'environment' => env('BRAINTREE_ENVIRONMENT'), + 'merchant_id' => env('BRAINTREE_MERCHANT_ID'), + 'public_key' => env('BRAINTREE_PUBLIC_KEY'), + 'private_key' => env('BRAINTREE_PRIVATE_KEY'), + ], + ], + 'dp_rate' => 100, +]; diff --git a/database/migrations/2026_01_07_000001_add_dp_vp_to_users_table.php b/database/migrations/2026_01_07_000001_add_dp_vp_to_users_table.php new file mode 100644 index 0000000..8361c94 --- /dev/null +++ b/database/migrations/2026_01_07_000001_add_dp_vp_to_users_table.php @@ -0,0 +1,24 @@ +unsignedInteger('dp')->default(0)->after('password'); + $table->unsignedInteger('vp')->default(0)->after('dp'); + }); + } + + public function down(): void + { + Schema::table('users', function (Blueprint $table) { + $table->dropColumn(['dp', 'vp']); + }); + } +}; + diff --git a/database/migrations/2026_01_07_000002_create_donation_transactions_table.php b/database/migrations/2026_01_07_000002_create_donation_transactions_table.php new file mode 100644 index 0000000..609bbaa --- /dev/null +++ b/database/migrations/2026_01_07_000002_create_donation_transactions_table.php @@ -0,0 +1,31 @@ +id(); + $table->unsignedBigInteger('user_id'); + $table->string('gateway', 64); + $table->string('transaction_id', 128)->nullable(); + $table->unsignedInteger('amount'); // in whole currency units (e.g., USD) + $table->string('currency', 8)->default('USD'); + $table->unsignedInteger('dp_awarded')->default(0); + $table->string('status', 32)->default('pending'); + $table->json('meta')->nullable(); + $table->timestamps(); + $table->index(['user_id', 'gateway', 'status']); + }); + } + + public function down(): void + { + Schema::dropIfExists('donation_transactions'); + } +}; + From 2cb8dde00cf0a5d3e5af279ac4230ba48cbd8817 Mon Sep 17 00:00:00 2001 From: sayghteight Date: Wed, 7 Jan 2026 20:59:10 +0100 Subject: [PATCH 028/132] feat(donate): add donation receipt page and redirect after checkout Add a new receipt page to display donation transaction details and modify checkout flow to redirect to this page. The receipt includes transaction ID, amount, DP awarded, and other relevant information with print/save functionality. --- .../Http/Controllers/DonateController.php | 20 ++++--- app/Modules/Donate/Http/routes.php | 1 + .../Donate/Resources/views/receipt.blade.php | 54 +++++++++++++++++++ 3 files changed, 68 insertions(+), 7 deletions(-) create mode 100644 app/Modules/Donate/Resources/views/receipt.blade.php diff --git a/app/Modules/Donate/Http/Controllers/DonateController.php b/app/Modules/Donate/Http/Controllers/DonateController.php index dcdf9d7..4a41b01 100644 --- a/app/Modules/Donate/Http/Controllers/DonateController.php +++ b/app/Modules/Donate/Http/Controllers/DonateController.php @@ -38,7 +38,8 @@ public function checkout(Request $request, GatewayManager $gateways) $user = $request->user(); $rate = (int) config('donate.dp_rate', 100); $dp = (int) ($amount * $rate); - DB::transaction(function () use ($user, $gatewayId, $amount, $dp, $result) { + $txId = null; + DB::transaction(function () use ($user, $gatewayId, $amount, $dp, $result, &$txId) { $tx = DonationTransaction::create([ 'user_id' => $user->id, 'gateway' => $gatewayId, @@ -50,13 +51,9 @@ public function checkout(Request $request, GatewayManager $gateways) 'meta' => ['raw' => $result], ]); $user->increment('dp', $dp); + $txId = $tx->id; }); - return response()->json([ - 'status' => 'success', - 'dp_awarded' => $dp, - 'amount' => $amount, - 'gateway' => $gatewayId, - ]); + return redirect()->route('donate.receipt', ['id' => $txId]); } return response()->json($result, 400); } @@ -76,4 +73,13 @@ public function webhook(string $gateway, Request $request, GatewayManager $gatew $result = $gw->handleWebhook($request); return response()->json($result); } + + public function receipt(int $id) + { + $tx = DonationTransaction::query() + ->where('id', $id) + ->where('user_id', auth()->id()) + ->firstOrFail(); + return view('donate::receipt', ['tx' => $tx]); + } } diff --git a/app/Modules/Donate/Http/routes.php b/app/Modules/Donate/Http/routes.php index 9b94c76..09cd3a4 100644 --- a/app/Modules/Donate/Http/routes.php +++ b/app/Modules/Donate/Http/routes.php @@ -10,4 +10,5 @@ Route::post('/checkout', [DonateController::class, 'checkout'])->middleware('auth')->name('donate.checkout'); Route::match(['get', 'post'], '/callback/{gateway}', [DonateController::class, 'callback'])->name('donate.callback'); Route::post('/webhook/{gateway}', [DonateController::class, 'webhook'])->name('donate.webhook'); + Route::get('/receipt/{id}', [DonateController::class, 'receipt'])->middleware('auth')->where('id', '[0-9]+')->name('donate.receipt'); }); diff --git a/app/Modules/Donate/Resources/views/receipt.blade.php b/app/Modules/Donate/Resources/views/receipt.blade.php new file mode 100644 index 0000000..f90bbfc --- /dev/null +++ b/app/Modules/Donate/Resources/views/receipt.blade.php @@ -0,0 +1,54 @@ +@extends('layouts.main') + +@section('title', 'Donation Receipt') + +@section('content') +
+
+
+

Donation Receipt

+ +
+
+
+
Receipt ID
+
{{ $tx->id }}
+
+
+
Date
+
{{ $tx->created_at }}
+
+
+
Gateway
+
{{ $tx->gateway }}
+
+
+
Transaction ID
+
{{ $tx->transaction_id ?? 'N/A' }}
+
+
+
Amount
+
${{ number_format($tx->amount, 2) }} {{ $tx->currency }}
+
+
+
DP Awarded
+
{{ number_format($tx->dp_awarded) }}
+
+
+
Status
+
{{ ucfirst($tx->status) }}
+
+
+
User
+
{{ Auth::user()->name }}
+
+
+
+ Keep this receipt as proof of your donation. Thank you for your support. +
+
+
+@endsection + From c1377c708f9ce982ee728f7b2bab509260b9b7bc Mon Sep 17 00:00:00 2001 From: sayghteight Date: Fri, 9 Jan 2026 00:31:25 +0100 Subject: [PATCH 029/132] feat(donate): implement donation transactions and UI improvements - Add donation transaction history view with stats and table - Update sidebar navigation with donations link - Replace hardcoded DP rate with config value - Improve UCP dashboard styling and layout - Add route for donation transactions - Update login page register link to use route helper --- .../Frontend/Users/UserController.php | 29 ++- .../Donate/Resources/views/home.blade.php | 2 +- config/donate.php | 31 ++- resources/views/auth/login.blade.php | 2 +- .../views/ucp/components/sidebar.blade.php | 86 ++++---- resources/views/ucp/donations.blade.php | 148 +++++++++++++ resources/views/ucp/index.blade.php | 198 +++++++++--------- routes/web.php | 2 +- 8 files changed, 354 insertions(+), 144 deletions(-) create mode 100644 resources/views/ucp/donations.blade.php diff --git a/app/Http/Controllers/Frontend/Users/UserController.php b/app/Http/Controllers/Frontend/Users/UserController.php index 34eb3ef..c4f78c2 100755 --- a/app/Http/Controllers/Frontend/Users/UserController.php +++ b/app/Http/Controllers/Frontend/Users/UserController.php @@ -12,6 +12,7 @@ use App\Models\AccountLinked; use App\Models\Realm; use App\Traits\ConnectsToExternalDatabase; +use Modules\Donate\Domain\Models\DonationTransaction; /** * User Controller for handling user-related actions in the frontend @@ -30,6 +31,7 @@ class UserController extends Controller 'create' => 'ucp.createAccount', 'gameAccount' => 'ucp.gameAccount', 'manage' => 'ucp.manageAccount', + 'transaction' => 'ucp.donations', ]; /** @@ -68,13 +70,32 @@ public function show() throw new UserNotFoundException('User not found', 404); } - // TODO: Remove this line when you have a real implementation - // at the moment we are just returning a dummy user - $user->coins = 0; - return view($this->views['index'], compact('user')); } + /** + * Show transaction page + * + * @throws UserNotFoundException + * @return \Illuminate\View\View + */ + public function transaction() + { + $user = $this->auth->guard()->user(); + + $transactions = DonationTransaction::where('user_id', $user->id)->get(); + + if ($transactions->isEmpty()) { + $transactions = []; + } + + if (!$user) { + throw new UserNotFoundException('User not found', 404); + } + + return view($this->views['transaction'], compact('user', 'transactions')); + } + /** * Show create account form * diff --git a/app/Modules/Donate/Resources/views/home.blade.php b/app/Modules/Donate/Resources/views/home.blade.php index df6c1de..f7e5ca9 100644 --- a/app/Modules/Donate/Resources/views/home.blade.php +++ b/app/Modules/Donate/Resources/views/home.blade.php @@ -129,7 +129,7 @@ @endauth -

Conversion rate: $1 = 100 Donation Points

+

Conversion rate: $1 = {{ config('donate.dp_rate') }} Donation Points

diff --git a/config/donate.php b/config/donate.php index 535c0d1..0e30611 100644 --- a/config/donate.php +++ b/config/donate.php @@ -1,9 +1,39 @@ ['braintree', 'paypal'], + */ 'enabled_gateways' => [ 'braintree', ], + + /** + * Conversion rate between currency and donation points. + * + * @var int + * + * Development note: Update this value as needed. + * + * @example 'dp_rate' => 100, + */ + 'dp_rate' => 100, + + /** + * Gateways configuration. + * + * @var array + * + * Development note: Add more gateways as needed. + * + * @example 'gateways' => ['braintree' => [...]], + */ 'gateways' => [ 'braintree' => [ 'environment' => env('BRAINTREE_ENVIRONMENT'), @@ -12,5 +42,4 @@ 'private_key' => env('BRAINTREE_PRIVATE_KEY'), ], ], - 'dp_rate' => 100, ]; diff --git a/resources/views/auth/login.blade.php b/resources/views/auth/login.blade.php index 6a49f84..33ad8fc 100755 --- a/resources/views/auth/login.blade.php +++ b/resources/views/auth/login.blade.php @@ -152,7 +152,7 @@ class="w-full py-3 px-4 bg-gradient-to-r from-blue-600 to-blue-700 hover:from-bl

Don't have an account? - Register here + Register here

diff --git a/resources/views/ucp/components/sidebar.blade.php b/resources/views/ucp/components/sidebar.blade.php index 0941e5f..c7bcb5c 100755 --- a/resources/views/ucp/components/sidebar.blade.php +++ b/resources/views/ucp/components/sidebar.blade.php @@ -1,54 +1,56 @@