feat: add nginx + export ics cal

This commit is contained in:
Aymeric GUERACAGUE 2025-12-21 00:44:47 +01:00
parent 7870058af6
commit 5879fbd146
Signed by: Superkooka
GPG Key ID: F78F2B172E894865
16 changed files with 384 additions and 21 deletions

View File

@ -5,12 +5,14 @@
"prefer-stable": true,
"require": {
"php": ">=8.4",
"ext-apcu": "*",
"ext-ctype": "*",
"ext-iconv": "*",
"doctrine/doctrine-bundle": "^3.1",
"doctrine/doctrine-migrations-bundle": "^4.0",
"doctrine/orm": "^3.6",
"ramsey/uuid-doctrine": "^2.1",
"spatie/icalendar-generator": "^3.2",
"symfony/console": "8.0.*",
"symfony/dotenv": "8.0.*",
"symfony/flex": "^2",

62
composer.lock generated
View File

@ -4,7 +4,7 @@
"Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies",
"This file is @generated automatically"
],
"content-hash": "02657b010196b6bde0f0c61b3c41708c",
"content-hash": "72529ff122c6d2ca2b568e195ae17b67",
"packages": [
{
"name": "brick/math",
@ -1724,6 +1724,65 @@
],
"time": "2024-05-27T00:00:21+00:00"
},
{
"name": "spatie/icalendar-generator",
"version": "3.2.0",
"source": {
"type": "git",
"url": "https://github.com/spatie/icalendar-generator.git",
"reference": "410885abfd26d8653234cead2ae1da78e7558cdb"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/spatie/icalendar-generator/zipball/410885abfd26d8653234cead2ae1da78e7558cdb",
"reference": "410885abfd26d8653234cead2ae1da78e7558cdb",
"shasum": ""
},
"require": {
"ext-mbstring": "*",
"php": "^8.1"
},
"require-dev": {
"ext-json": "*",
"larapack/dd": "^1.1",
"nesbot/carbon": "^3.5",
"pestphp/pest": "^2.34 || ^3.0 || ^4.0",
"phpstan/phpstan": "^2.0",
"spatie/pest-plugin-snapshots": "^2.1"
},
"type": "library",
"autoload": {
"psr-4": {
"Spatie\\IcalendarGenerator\\": "src"
}
},
"notification-url": "https://packagist.org/downloads/",
"license": [
"MIT"
],
"authors": [
{
"name": "Ruben Van Assche",
"email": "ruben@spatie.be",
"homepage": "https://spatie.be",
"role": "Developer"
}
],
"description": "Build calendars in the iCalendar format",
"homepage": "https://github.com/spatie/icalendar-generator",
"keywords": [
"calendar",
"iCalendar",
"ical",
"ics",
"spatie"
],
"support": {
"issues": "https://github.com/spatie/icalendar-generator/issues",
"source": "https://github.com/spatie/icalendar-generator/tree/3.2.0"
},
"time": "2025-12-03T11:07:27+00:00"
},
{
"name": "symfony/cache",
"version": "v8.0.1",
@ -6267,6 +6326,7 @@
"prefer-lowest": false,
"platform": {
"php": ">=8.4",
"ext-apcu": "*",
"ext-ctype": "*",
"ext-iconv": "*"
},

View File

@ -2,7 +2,7 @@ framework:
router:
# Configure how to generate URLs in non-HTTP contexts, such as CLI commands.
# See https://symfony.com/doc/current/routing.html#generating-urls-in-commands
default_uri: '%env(DEFAULT_URI)%'
# default_uri: '%env(DEFAULT_URI)%'
when@prod:
framework:

View File

@ -1,11 +1,6 @@
# yaml-language-server: $schema=../vendor/symfony/routing/Loader/schema/routing.schema.json
# This file is the entry point to configure the routes of your app.
# Methods with the #[Route] attribute are automatically imported.
# See also https://symfony.com/doc/current/routing.html
# To list all registered routes, run the following command:
# bin/console debug:router
controllers:
resource: routing.controllers
get_calendar:
path: /calendar
controller: App\Infrastructure\Controller\Calendar::getCalendar
methods: GET

View File

@ -1,4 +1,30 @@
services:
traefik:
image: traefik:3.6.5
command:
- "--api.insecure=true" # Dashboard Traefik (optionnel)
- "--providers.docker=true" # Labels Docker
- "--entrypoints.web.address=:80" # HTTP
ports:
- "80:80"
- "8080:8080" # Dashboard (facultatif)
volumes:
- "/var/run/docker.sock:/var/run/docker.sock:ro"
nhl-schedule-nginx:
image: nginx:1.29.4
volumes:
- ./docker/nginx/default.conf:/etc/nginx/conf.d/default.conf:rw,cached
- ./public:/var/www/public:rw,cached
labels:
- traefik.http.routers.nhl-schedule.rule=Host(`local.match-schedule.home`)
- traefik.http.routers.nhl-schedule.middlewares=nhl-schedule
- traefik.http.middlewares.nhl-schedule.headers.customresponseheaders.Access-Control-Allow-Methods=POST, PATCH, GET, PUT, OPTIONS, DELETE
- traefik.http.middlewares.nhl-schedule.headers.customresponseheaders.Access-Control-Allow-Origin=*
- traefik.http.middlewares.nhl-schedule.headers.customresponseheaders.Access-Control-Allow-Headers=x-requested-with, Content-Type,Authorization,Location
- traefik.http.middlewares.nhl-schedule.headers.customresponseheaders.Access-Control-Expose-Headers=link, Location
- traefik.port=80
nhl-schedule:
image: nhl-schedule:dev
user: 1000:1000
@ -8,8 +34,6 @@ services:
- ./:/var/www:rw,cached
- ./docker/php-fpm/ini/xdebug.ini:/usr/local/etc/php/conf.d/xdebug.ini
- ./docker/php-fpm/ini/local.ini:/usr/local/etc/php/conf.d/local.ini
# extra_hosts:
# - host.docker.internal:host-gateway
postgres:
image: postgres:18.1

28
docker/nginx/default.conf Normal file
View File

@ -0,0 +1,28 @@
server {
listen 80;
server_name local.match-schedule.home;
client_max_body_size 100M;
index index.php index.html;
error_log /var/log/nginx/error.log;
access_log /var/log/nginx/access.log;
root /var/www/public;
location ~ \.php$ {
try_files $uri =404;
fastcgi_split_path_info ^(.+\.php)(/.+)$;
fastcgi_pass nhl-schedule:9000;
fastcgi_index index.php;
include fastcgi_params;
fastcgi_param SCRIPT_FILENAME $document_root$fastcgi_script_name;
fastcgi_param PATH_INFO $fastcgi_path_info;
fastcgi_buffers 16 16k;
fastcgi_buffer_size 32k;
}
location / {
try_files $uri $uri/ /index.php?$query_string;
gzip_static on;
}
}

View File

@ -0,0 +1,19 @@
<?php
namespace App\Application\ReadModel;
readonly class Game
{
public function __construct(
public string $id,
public string $providerId,
public string $providerGameId,
public \DateTimeImmutable $startTimeScheduled,
public ?\DateTimeImmutable $endTimeScheduled,
public string $seasonId,
public string $homeTeamId,
public string $awayTeamId,
public string $venue,
) {
}
}

View File

@ -2,11 +2,11 @@
namespace App\Application\ReadModel;
class Provider
readonly class Provider
{
public function __construct(
public readonly string $id,
public readonly string $name,
public string $id,
public string $name,
) {
}
}

View File

@ -0,0 +1,16 @@
<?php
namespace App\Application\ReadModel;
readonly class Team
{
public function __construct(
public string $id,
public string $providerId,
public string $providerTeamId,
public string $name,
public string $alias,
public bool $active,
) {
}
}

View File

@ -22,10 +22,14 @@ class FetchNHLMatch
public function __invoke(FetchNHLMatchRequest $request): void
{
$provider = $this->entityManager->getRepository(Provider::class)->findOneBy(['id' => '885fe581-c4c3-45e7-a06c-29ece7d47fad']); // id should not be here
$provider =
$this->entityManager->getRepository(Provider::class)->findOneBy(['id' => '885fe581-c4c3-45e7-a06c-29ece7d47fad']) // id should not be here
?? throw new \Exception('Provider not found');
$matchs = $this->sportRadarEngine->getNHLSchedule($request->year, ENHLSeasonType::from($request->type));
$season = $this->entityManager->getRepository(Season::class)->findOneBy(['providerSeasonId' => $matchs['season']['providerId']]);
$season =
$this->entityManager->getRepository(Season::class)->findOneBy(['providerSeasonId' => $matchs['season']['providerId']])
?? throw new \Exception('Season not found');
/** @var array<string, Team> $teams */
$teams = [];

View File

@ -4,11 +4,11 @@ declare(strict_types=1);
namespace App\Application\UseCase;
class FetchNHLMatchRequest
readonly class FetchNHLMatchRequest
{
public function __construct(
public readonly int $year,
public readonly string $type,
public int $year,
public string $type,
) {
}
}

View File

@ -0,0 +1,70 @@
<?php
namespace App\Application\UseCase;
use App\Application\ReadModel\Game;
use App\Application\ReadModel\Team;
use Doctrine\Common\Collections\Criteria;
use Doctrine\ORM\EntityManagerInterface;
use Spatie\IcalendarGenerator\Components\Calendar;
use Spatie\IcalendarGenerator\Components\Event;
class GetGamesCalendar
{
public function __construct(
private readonly EntityManagerInterface $entityManager,
) {
}
public function __invoke(GetGamesCalendarRequest $request): string
{
$calendar = Calendar::create()
->name('Game')
->description('List game schedule');
if (empty($request->teams)) {
return $calendar->get();
}
$teams = [];
foreach ($request->teams as $team) {
$teams[$team] = $this->entityManager->getRepository(Team::class)->find($team);
}
// $criteria = Criteria::create()
// ->where(Criteria::expr()->in('homeTeamId', $request->teams))
// ->orWhere(Criteria::expr()->in('awayTeamId', $request->teams))
// ->orderBy(['startTimeScheduled' => 'ASC']);
//
// /** @var Game[] $games */
// $games = $this->entityManager->getRepository(Game::class)->matching($criteria);
/** @var Game[] $homeGames */
$homeGames = $this->entityManager->getRepository(Game::class)->findBy(['homeTeamId' => $request->teams]);
/** @var Game[] $awayGames */
$awayGames = $this->entityManager->getRepository(Game::class)->findBy(['awayTeamId' => $request->teams]);
$gamesById = [];
foreach (array_merge($homeGames, $awayGames) as $game) {
$gamesById[$game->id] = $game;
}
/** @var Game[] $games */
$games = array_values($gamesById);
$events = [];
foreach ($games as $game) {
$home = $this->entityManager->getRepository(Team::class)->find($game->homeTeamId) ?? throw new \Exception('Team not found');
$away = $this->entityManager->getRepository(Team::class)->find($game->awayTeamId) ?? throw new \Exception('Team not found');
$events[] = Event::create($home->name . ' vs ' . $away->name)
->startsAt($game->startTimeScheduled)
->endsAt($game->endTimeScheduled ?? $game->startTimeScheduled->modify('+3 hours')) // should be a parameter of the league
->address($game->venue);
}
return $calendar
->event($events)
->get()
;
}
}

View File

@ -0,0 +1,12 @@
<?php
namespace App\Application\UseCase;
readonly class GetGamesCalendarRequest
{
public function __construct(
/** @var string[] $teams */
public array $teams,
) {
}
}

View File

@ -0,0 +1,27 @@
<?php
namespace App\Infrastructure\Controller;
use App\Application\CommandBus\UseCaseCommandBus;
use App\Application\UseCase\GetGamesCalendarRequest;
use Symfony\Bundle\FrameworkBundle\Controller\AbstractController;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\HttpFoundation\Response;
class Calendar extends AbstractController
{
public function __construct(
private readonly UseCaseCommandBus $commandBus,
) {
}
public function getCalendar(Request $request): Response
{
$calendar = $this->commandBus->ask(new GetGamesCalendarRequest($request->query->all('teams')));
return new Response($calendar, 200, [
'Content-Type' => 'text/calendar; charset=utf-8',
'Content-Disposition' => 'attachment; filename="calendar.ics"',
]);
}
}

View File

@ -0,0 +1,63 @@
<?php
declare(strict_types=1);
$builder = new Doctrine\ORM\Mapping\Builder\ClassMetadataBuilder($metadata);
$builder
->setTable('game')
->setReadOnly()
;
$builder
->createField('id', 'uuid')
->nullable(false)
->makePrimaryKey()
->build();
$builder
->createField('providerId', 'uuid')
->columnName('provider_id')
->nullable(false)
->build();
$builder
->createField('providerGameId', 'uuid')
->columnName('provider_game_id')
->nullable(false)
->build();
$builder
->createField('startTimeScheduled', 'datetimetz_immutable')
->columnName('start_time_scheduled')
->nullable(false)
->build();
$builder
->createField('endTimeScheduled', 'datetimetz_immutable')
->columnName('end_time_scheduled')
->nullable()
->build();
$builder
->createField('seasonId', 'uuid')
->columnName('season_id')
->nullable(false)
->build();
$builder
->createField('homeTeamId', 'uuid')
->columnName('home_team_id')
->nullable(false)
->build();
$builder
->createField('awayTeamId', 'uuid')
->columnName('away_team_id')
->nullable(false)
->build();
$builder
->createField('venue', 'string')
->nullable(false)
->build();

View File

@ -0,0 +1,43 @@
<?php
declare(strict_types=1);
$builder = new Doctrine\ORM\Mapping\Builder\ClassMetadataBuilder($metadata);
$builder
->setReadOnly()
->setTable('team')
;
$builder
->createField('id', 'uuid')
->nullable(false)
->makePrimaryKey()
->build();
$builder
->createField('providerId', 'uuid')
->columnName('provider_id')
->nullable(false)
->build();
$builder
->createField('providerTeamId', 'string')
->columnName('provider_team_id')
->nullable(false)
->build();
$builder
->createField('name', 'string')
->nullable(false)
->build();
$builder
->createField('alias', 'string')
->nullable(false)
->build();
$builder
->createField('active', 'boolean')
->nullable(false)
->build();