diff --git a/composer.json b/composer.json index 14ccceb..429f046 100644 --- a/composer.json +++ b/composer.json @@ -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", diff --git a/composer.lock b/composer.lock index dbe7620..18f5aae 100644 --- a/composer.lock +++ b/composer.lock @@ -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": "*" }, diff --git a/config/packages/routing.yaml b/config/packages/routing.yaml index 0f34f87..281f2c1 100644 --- a/config/packages/routing.yaml +++ b/config/packages/routing.yaml @@ -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: diff --git a/config/routes.yaml b/config/routes.yaml index cef258c..391d209 100644 --- a/config/routes.yaml +++ b/config/routes.yaml @@ -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 diff --git a/docker-compose.yaml b/docker-compose.yaml index d1e9d89..3e5475d 100644 --- a/docker-compose.yaml +++ b/docker-compose.yaml @@ -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 diff --git a/docker/nginx/default.conf b/docker/nginx/default.conf new file mode 100644 index 0000000..86e52c1 --- /dev/null +++ b/docker/nginx/default.conf @@ -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; + } +} diff --git a/src/Application/ReadModel/Game.php b/src/Application/ReadModel/Game.php new file mode 100644 index 0000000..8306c45 --- /dev/null +++ b/src/Application/ReadModel/Game.php @@ -0,0 +1,19 @@ +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 $teams */ $teams = []; diff --git a/src/Application/UseCase/FetchNHLMatchRequest.php b/src/Application/UseCase/FetchNHLMatchRequest.php index c440d2f..1e5ec77 100644 --- a/src/Application/UseCase/FetchNHLMatchRequest.php +++ b/src/Application/UseCase/FetchNHLMatchRequest.php @@ -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, ) { } } diff --git a/src/Application/UseCase/GetGamesCalendar.php b/src/Application/UseCase/GetGamesCalendar.php new file mode 100644 index 0000000..afecbc0 --- /dev/null +++ b/src/Application/UseCase/GetGamesCalendar.php @@ -0,0 +1,70 @@ +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() + ; + } +} diff --git a/src/Application/UseCase/GetGamesCalendarRequest.php b/src/Application/UseCase/GetGamesCalendarRequest.php new file mode 100644 index 0000000..4f7a2ff --- /dev/null +++ b/src/Application/UseCase/GetGamesCalendarRequest.php @@ -0,0 +1,12 @@ +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"', + ]); + } +} diff --git a/src/Infrastructure/Persistence/Mapping/App.Application.ReadModel.Game.php b/src/Infrastructure/Persistence/Mapping/App.Application.ReadModel.Game.php new file mode 100644 index 0000000..45277bb --- /dev/null +++ b/src/Infrastructure/Persistence/Mapping/App.Application.ReadModel.Game.php @@ -0,0 +1,63 @@ +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(); diff --git a/src/Infrastructure/Persistence/Mapping/App.Application.ReadModel.Team.php b/src/Infrastructure/Persistence/Mapping/App.Application.ReadModel.Team.php new file mode 100644 index 0000000..353035d --- /dev/null +++ b/src/Infrastructure/Persistence/Mapping/App.Application.ReadModel.Team.php @@ -0,0 +1,43 @@ +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();