feat: architecture
Signed-off-by: Superkooka <aymeric.gueracague@gmail.com>
This commit is contained in:
parent
2b7144614f
commit
aa12eb92a4
|
|
@ -25,3 +25,5 @@ APP_SHARE_DIR=var/share
|
|||
# See https://symfony.com/doc/current/routing.html#generating-urls-in-commands
|
||||
DEFAULT_URI=http://localhost
|
||||
###< symfony/routing ###
|
||||
|
||||
SPORT_RADAR_APIKEY=
|
||||
4
Makefile
4
Makefile
|
|
@ -15,6 +15,10 @@ build:
|
|||
install:
|
||||
docker-compose -f docker-compose.yaml run --rm nhl-schedule composer install
|
||||
|
||||
.PHONY: run
|
||||
run:
|
||||
docker-compose -f docker-compose.yaml run --rm nhl-schedule php bin/console app:get-nhl-schedule
|
||||
|
||||
.PHONY: static-check
|
||||
static-check:
|
||||
docker-compose -f docker-compose.yaml run --rm nhl-schedule php -d memory_limit=4G vendor/bin/phpstan analyse -c phpstan.neon
|
||||
|
|
|
|||
|
|
@ -11,6 +11,7 @@
|
|||
"symfony/dotenv": "8.0.*",
|
||||
"symfony/flex": "^2",
|
||||
"symfony/framework-bundle": "8.0.*",
|
||||
"symfony/http-client": "8.0.*",
|
||||
"symfony/runtime": "8.0.*",
|
||||
"symfony/yaml": "8.0.*"
|
||||
},
|
||||
|
|
|
|||
|
|
@ -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": "1319f86cd294d1bc92527e45febb8246",
|
||||
"content-hash": "43beda2ab15c41986add9f99a79923ae",
|
||||
"packages": [
|
||||
{
|
||||
"name": "psr/cache",
|
||||
|
|
@ -1367,6 +1367,180 @@
|
|||
],
|
||||
"time": "2025-12-06T16:55:34+00:00"
|
||||
},
|
||||
{
|
||||
"name": "symfony/http-client",
|
||||
"version": "v8.0.1",
|
||||
"source": {
|
||||
"type": "git",
|
||||
"url": "https://github.com/symfony/http-client.git",
|
||||
"reference": "727fda60d0aebfdfcc4c8bc4661f0cb8f44153c0"
|
||||
},
|
||||
"dist": {
|
||||
"type": "zip",
|
||||
"url": "https://api.github.com/repos/symfony/http-client/zipball/727fda60d0aebfdfcc4c8bc4661f0cb8f44153c0",
|
||||
"reference": "727fda60d0aebfdfcc4c8bc4661f0cb8f44153c0",
|
||||
"shasum": ""
|
||||
},
|
||||
"require": {
|
||||
"php": ">=8.4",
|
||||
"psr/log": "^1|^2|^3",
|
||||
"symfony/http-client-contracts": "~3.4.4|^3.5.2",
|
||||
"symfony/service-contracts": "^2.5|^3"
|
||||
},
|
||||
"conflict": {
|
||||
"amphp/amp": "<3",
|
||||
"php-http/discovery": "<1.15"
|
||||
},
|
||||
"provide": {
|
||||
"php-http/async-client-implementation": "*",
|
||||
"php-http/client-implementation": "*",
|
||||
"psr/http-client-implementation": "1.0",
|
||||
"symfony/http-client-implementation": "3.0"
|
||||
},
|
||||
"require-dev": {
|
||||
"amphp/http-client": "^5.3.2",
|
||||
"amphp/http-tunnel": "^2.0",
|
||||
"guzzlehttp/promises": "^1.4|^2.0",
|
||||
"nyholm/psr7": "^1.0",
|
||||
"php-http/httplug": "^1.0|^2.0",
|
||||
"psr/http-client": "^1.0",
|
||||
"symfony/cache": "^7.4|^8.0",
|
||||
"symfony/dependency-injection": "^7.4|^8.0",
|
||||
"symfony/http-kernel": "^7.4|^8.0",
|
||||
"symfony/messenger": "^7.4|^8.0",
|
||||
"symfony/process": "^7.4|^8.0",
|
||||
"symfony/rate-limiter": "^7.4|^8.0",
|
||||
"symfony/stopwatch": "^7.4|^8.0"
|
||||
},
|
||||
"type": "library",
|
||||
"autoload": {
|
||||
"psr-4": {
|
||||
"Symfony\\Component\\HttpClient\\": ""
|
||||
},
|
||||
"exclude-from-classmap": [
|
||||
"/Tests/"
|
||||
]
|
||||
},
|
||||
"notification-url": "https://packagist.org/downloads/",
|
||||
"license": [
|
||||
"MIT"
|
||||
],
|
||||
"authors": [
|
||||
{
|
||||
"name": "Nicolas Grekas",
|
||||
"email": "p@tchwork.com"
|
||||
},
|
||||
{
|
||||
"name": "Symfony Community",
|
||||
"homepage": "https://symfony.com/contributors"
|
||||
}
|
||||
],
|
||||
"description": "Provides powerful methods to fetch HTTP resources synchronously or asynchronously",
|
||||
"homepage": "https://symfony.com",
|
||||
"keywords": [
|
||||
"http"
|
||||
],
|
||||
"support": {
|
||||
"source": "https://github.com/symfony/http-client/tree/v8.0.1"
|
||||
},
|
||||
"funding": [
|
||||
{
|
||||
"url": "https://symfony.com/sponsor",
|
||||
"type": "custom"
|
||||
},
|
||||
{
|
||||
"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-12-05T14:08:45+00:00"
|
||||
},
|
||||
{
|
||||
"name": "symfony/http-client-contracts",
|
||||
"version": "v3.6.0",
|
||||
"source": {
|
||||
"type": "git",
|
||||
"url": "https://github.com/symfony/http-client-contracts.git",
|
||||
"reference": "75d7043853a42837e68111812f4d964b01e5101c"
|
||||
},
|
||||
"dist": {
|
||||
"type": "zip",
|
||||
"url": "https://api.github.com/repos/symfony/http-client-contracts/zipball/75d7043853a42837e68111812f4d964b01e5101c",
|
||||
"reference": "75d7043853a42837e68111812f4d964b01e5101c",
|
||||
"shasum": ""
|
||||
},
|
||||
"require": {
|
||||
"php": ">=8.1"
|
||||
},
|
||||
"type": "library",
|
||||
"extra": {
|
||||
"thanks": {
|
||||
"url": "https://github.com/symfony/contracts",
|
||||
"name": "symfony/contracts"
|
||||
},
|
||||
"branch-alias": {
|
||||
"dev-main": "3.6-dev"
|
||||
}
|
||||
},
|
||||
"autoload": {
|
||||
"psr-4": {
|
||||
"Symfony\\Contracts\\HttpClient\\": ""
|
||||
},
|
||||
"exclude-from-classmap": [
|
||||
"/Test/"
|
||||
]
|
||||
},
|
||||
"notification-url": "https://packagist.org/downloads/",
|
||||
"license": [
|
||||
"MIT"
|
||||
],
|
||||
"authors": [
|
||||
{
|
||||
"name": "Nicolas Grekas",
|
||||
"email": "p@tchwork.com"
|
||||
},
|
||||
{
|
||||
"name": "Symfony Community",
|
||||
"homepage": "https://symfony.com/contributors"
|
||||
}
|
||||
],
|
||||
"description": "Generic abstractions related to HTTP clients",
|
||||
"homepage": "https://symfony.com",
|
||||
"keywords": [
|
||||
"abstractions",
|
||||
"contracts",
|
||||
"decoupling",
|
||||
"interfaces",
|
||||
"interoperability",
|
||||
"standards"
|
||||
],
|
||||
"support": {
|
||||
"source": "https://github.com/symfony/http-client-contracts/tree/v3.6.0"
|
||||
},
|
||||
"funding": [
|
||||
{
|
||||
"url": "https://symfony.com/sponsor",
|
||||
"type": "custom"
|
||||
},
|
||||
{
|
||||
"url": "https://github.com/fabpot",
|
||||
"type": "github"
|
||||
},
|
||||
{
|
||||
"url": "https://tidelift.com/funding/github/packagist/symfony/symfony",
|
||||
"type": "tidelift"
|
||||
}
|
||||
],
|
||||
"time": "2025-04-29T11:18:49+00:00"
|
||||
},
|
||||
{
|
||||
"name": "symfony/http-foundation",
|
||||
"version": "v8.0.1",
|
||||
|
|
|
|||
|
|
@ -463,7 +463,7 @@ namespace Symfony\Component\DependencyInjection\Loader\Configurator;
|
|||
* },
|
||||
* disallow_search_engine_index?: bool, // Enabled by default when debug is enabled. // Default: true
|
||||
* http_client?: bool|array{ // HTTP Client configuration
|
||||
* enabled?: bool, // Default: false
|
||||
* enabled?: bool, // Default: true
|
||||
* max_host_connections?: int, // The maximum number of connections to a single host.
|
||||
* default_options?: array{
|
||||
* headers?: array<string, mixed>,
|
||||
|
|
|
|||
|
|
@ -7,17 +7,23 @@
|
|||
# Put parameters here that don't need to change on each machine where the app is deployed
|
||||
# https://symfony.com/doc/current/best_practices.html#use-parameters-for-application-configuration
|
||||
parameters:
|
||||
|
||||
app.sport_radar_apikey: '%env(SPORT_RADAR_APIKEY)%'
|
||||
services:
|
||||
# default configuration for services in *this* file
|
||||
_defaults:
|
||||
autowire: true # Automatically injects dependencies in your services.
|
||||
autoconfigure: true # Automatically registers your services as commands, event subscribers, etc.
|
||||
|
||||
# makes classes in src/ available to be used as services
|
||||
# this creates a service per class whose id is the fully-qualified class name
|
||||
App\:
|
||||
resource: '../src/'
|
||||
|
||||
# add more service definitions when explicit configuration is needed
|
||||
# please note that last definitions always *replace* previous ones
|
||||
App\Application\CommandBus\UseCaseCommandBus:
|
||||
arguments:
|
||||
$handlers: !tagged_iterator 'app.usecase'
|
||||
|
||||
App\Application\UseCase\:
|
||||
resource: '../src/Application/UseCase/'
|
||||
tags: ['app.usecase']
|
||||
exclude:
|
||||
- '../src/Application/UseCase/*Request.php'
|
||||
|
||||
App\Application\SportRadar\SportRadarEngine: '@App\Infrastructure\SportRadar\HTTPSportRadarEngine'
|
||||
|
|
|
|||
File diff suppressed because it is too large
Load Diff
|
|
@ -2,3 +2,8 @@ parameters:
|
|||
level: 8
|
||||
paths:
|
||||
- src
|
||||
ignoreErrors:
|
||||
-
|
||||
message: "#^Property .+::.+ is never read, only written\\.$#"
|
||||
paths:
|
||||
- src/Domain/Entity/*.php
|
||||
|
|
|
|||
|
|
@ -0,0 +1,46 @@
|
|||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Application\CommandBus;
|
||||
|
||||
class UseCaseCommandBus
|
||||
{
|
||||
/** @var array<class-string, callable> */
|
||||
private array $handlers = [];
|
||||
|
||||
/**
|
||||
* @param iterable<object> $handlers
|
||||
*/
|
||||
public function __construct(iterable $handlers)
|
||||
{
|
||||
foreach ($handlers as $handler) {
|
||||
$this->handlers[$this->resolveQueryClass($handler)] = $handler;
|
||||
}
|
||||
}
|
||||
|
||||
public function ask(object $query): mixed
|
||||
{
|
||||
$queryClass = $query::class;
|
||||
|
||||
if (!isset($this->handlers[$queryClass])) {
|
||||
throw new \RuntimeException(sprintf('No query handler found for %s', $queryClass));
|
||||
}
|
||||
|
||||
return ($this->handlers[$queryClass])($query);
|
||||
}
|
||||
|
||||
private function resolveQueryClass(object $handler): string
|
||||
{
|
||||
$reflection = new \ReflectionMethod($handler, '__invoke');
|
||||
$param = $reflection->getParameters()[0];
|
||||
|
||||
$type = $param->getType();
|
||||
|
||||
if (!$type instanceof \ReflectionNamedType) {
|
||||
throw new \RuntimeException('Handler parameter must have a single named type');
|
||||
}
|
||||
|
||||
return $type->getName();
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,10 @@
|
|||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Application\SportRadar;
|
||||
|
||||
interface SportRadarEngine
|
||||
{
|
||||
public function getNHLSchedule(): void;
|
||||
}
|
||||
|
|
@ -0,0 +1,13 @@
|
|||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Application\UseCase;
|
||||
|
||||
class GetNHLMatch
|
||||
{
|
||||
public function __invoke(GetNHLMatchRequest $request): void
|
||||
{
|
||||
dd($request->type);
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,13 @@
|
|||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Application\UseCase;
|
||||
|
||||
class GetNHLMatchRequest
|
||||
{
|
||||
public function __construct(
|
||||
public readonly string $type,
|
||||
) {
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,78 @@
|
|||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Domain\Entity;
|
||||
|
||||
abstract class AEntity
|
||||
{
|
||||
protected string $id;
|
||||
|
||||
public function __construct()
|
||||
{
|
||||
$this->id = $this->generateUUIDv4();
|
||||
}
|
||||
|
||||
/**
|
||||
* @author Grégoire Pineau <lyrixx@lyrixx.info>
|
||||
*
|
||||
* https://github.com/symfony/polyfill-uuid/blob/a41886c1c81dc075a09c71fe6db5b9d68c79de23/Uuid.php#L378-L438
|
||||
*/
|
||||
private function generateUUIDv4(): string
|
||||
{
|
||||
$timeOffsetInt = 0x01B21DD213814000;
|
||||
|
||||
$time = microtime(false);
|
||||
$time = substr($time, 11) . substr($time, 2, 7);
|
||||
|
||||
$time = str_pad(dechex((int) $time + $timeOffsetInt), 16, '0', \STR_PAD_LEFT);
|
||||
|
||||
// https://tools.ietf.org/html/rfc4122#section-4.1.5
|
||||
// We are using a random data for the sake of simplicity: since we are
|
||||
// not able to get a super precise timeOfDay as a unique sequence
|
||||
$clockSeq = random_int(0, 0x3FFF);
|
||||
|
||||
static $node;
|
||||
if (null === $node) {
|
||||
if (\function_exists('apcu_fetch')) {
|
||||
$node = apcu_fetch('__symfony_uuid_node');
|
||||
if (false === $node) {
|
||||
$node = sprintf(
|
||||
'%06x%06x',
|
||||
random_int(0, 0xFFFFFF) | 0x010000,
|
||||
random_int(0, 0xFFFFFF)
|
||||
);
|
||||
apcu_store('__symfony_uuid_node', $node);
|
||||
}
|
||||
} else {
|
||||
$node = sprintf(
|
||||
'%06x%06x',
|
||||
random_int(0, 0xFFFFFF) | 0x010000,
|
||||
random_int(0, 0xFFFFFF)
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
return sprintf(
|
||||
'%08s-%04s-1%03s-%04x-%012s',
|
||||
// 32 bits for "time_low"
|
||||
substr($time, -8),
|
||||
|
||||
// 16 bits for "time_mid"
|
||||
substr($time, -12, 4),
|
||||
|
||||
// 16 bits for "time_hi_and_version",
|
||||
// four most significant bits holds version number 1
|
||||
substr($time, -15, 3),
|
||||
|
||||
// 16 bits:
|
||||
// * 8 bits for "clk_seq_hi_res",
|
||||
// * 8 bits for "clk_seq_low",
|
||||
// two most significant bits holds zero and one for variant DCE1.1
|
||||
$clockSeq | 0x8000,
|
||||
|
||||
// 48 bits for "node"
|
||||
$node
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,31 @@
|
|||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Domain\Entity;
|
||||
|
||||
class Game extends AEntity
|
||||
{
|
||||
private string $srId;
|
||||
private Season $season;
|
||||
private \DateTimeImmutable $scheduled;
|
||||
private Team $home;
|
||||
private Team $away;
|
||||
private string $venue_arena;
|
||||
|
||||
public function create(
|
||||
string $srId,
|
||||
Season $season,
|
||||
\DateTimeImmutable $scheduled,
|
||||
Team $home,
|
||||
Team $away,
|
||||
string $venue_arena,
|
||||
): void {
|
||||
$this->srId = $srId;
|
||||
$this->season = $season;
|
||||
$this->scheduled = $scheduled;
|
||||
$this->home = $home;
|
||||
$this->away = $away;
|
||||
$this->venue_arena = $venue_arena;
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,16 @@
|
|||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Domain\Entity;
|
||||
|
||||
class League extends AEntity
|
||||
{
|
||||
private string $name;
|
||||
|
||||
public function create(
|
||||
string $name,
|
||||
): void {
|
||||
$this->name = $name;
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,22 @@
|
|||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Domain\Entity;
|
||||
|
||||
class Season extends AEntity
|
||||
{
|
||||
private League $league;
|
||||
private string $type; // should be an enum
|
||||
private int $year;
|
||||
|
||||
public function create(
|
||||
League $league,
|
||||
string $type,
|
||||
int $year,
|
||||
): void {
|
||||
$this->league = $league;
|
||||
$this->type = $type;
|
||||
$this->year = $year;
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,22 @@
|
|||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Domain\Entity;
|
||||
|
||||
class Team extends AEntity
|
||||
{
|
||||
private string $name;
|
||||
private string $alias;
|
||||
private string $sr_id;
|
||||
|
||||
public function create(
|
||||
string $name,
|
||||
string $alias,
|
||||
string $sr_id,
|
||||
): void {
|
||||
$this->name = $name;
|
||||
$this->alias = $alias;
|
||||
$this->sr_id = $sr_id;
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,24 @@
|
|||
<?php
|
||||
|
||||
namespace App\Infrastructure\Console;
|
||||
|
||||
use App\Application\CommandBus\UseCaseCommandBus;
|
||||
use App\Application\UseCase\GetNHLMatchRequest;
|
||||
use Symfony\Component\Console\Attribute\AsCommand;
|
||||
use Symfony\Component\Console\Command\Command;
|
||||
|
||||
#[AsCommand(name: 'app:get-nhl-schedule')]
|
||||
class GetNHLScheduleCommand
|
||||
{
|
||||
public function __construct(
|
||||
private readonly UseCaseCommandBus $commandBus,
|
||||
) {
|
||||
}
|
||||
|
||||
public function __invoke(): int
|
||||
{
|
||||
$this->commandBus->ask(new GetNHLMatchRequest('REG'));
|
||||
|
||||
return Command::SUCCESS;
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,34 @@
|
|||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Infrastructure\SportRadar;
|
||||
|
||||
use App\Application\SportRadar\SportRadarEngine;
|
||||
use Symfony\Component\DependencyInjection\Attribute\Autowire;
|
||||
use Symfony\Contracts\HttpClient\HttpClientInterface;
|
||||
|
||||
class HTTPSportRadarEngine implements SportRadarEngine
|
||||
{
|
||||
public function __construct(
|
||||
private HttpClientInterface $client,
|
||||
#[Autowire('%app.sport_radar_apikey%')]
|
||||
string $apiKey,
|
||||
) {
|
||||
$this->client = $client->withOptions(
|
||||
[
|
||||
'headers' => [
|
||||
'x-api-key' => $apiKey,
|
||||
'accept' => 'application/json',
|
||||
],
|
||||
],
|
||||
);
|
||||
}
|
||||
|
||||
public function getNHLSchedule(): void
|
||||
{
|
||||
$response = $this->client->request('GET', 'https://api.sportradar.com/nhl/trial/v7/en/games/2025/REG/schedule.json');
|
||||
|
||||
// return ['a'];
|
||||
}
|
||||
}
|
||||
Loading…
Reference in New Issue