feat: Initialisation du projet mainframe

Ajout de controllers, services, assets et configuration initiale.
This commit is contained in:
Serreau Jovann
2025-07-16 13:41:14 +02:00
parent 8ef2463916
commit 4d71d416f1
16 changed files with 344 additions and 31 deletions

4
.env
View File

@@ -26,7 +26,7 @@ APP_SECRET=
# DATABASE_URL="sqlite:///%kernel.project_dir%/var/data_%kernel.environment%.db"
# DATABASE_URL="mysql://app:!ChangeMe!@127.0.0.1:3306/app?serverVersion=8.0.32&charset=utf8mb4"
# DATABASE_URL="mysql://app:!ChangeMe!@127.0.0.1:3306/app?serverVersion=10.11.2-MariaDB&charset=utf8mb4"
DATABASE_URL="postgresql://app:!ChangeMe!@127.0.0.1:5432/app?serverVersion=16&charset=utf8"
DATABASE_URL="postgresql://symfony_user:ChangeMeInProd!@db:5432/app_db?serverVersion=16&charset=utf8"
###< doctrine/doctrine-bundle ###
###> symfony/messenger ###
@@ -47,3 +47,5 @@ CORS_ALLOW_ORIGIN='^https?://(localhost|127\.0\.0\.1)(:[0-9]+)?$'
###> sentry/sentry-symfony ###
SENTRY_DSN=
###< sentry/sentry-symfony ###
VITE_LOAD=0
REDIS_DSN="redis://redis:6379"

View File

@@ -0,0 +1 @@
import './app.scss'

2
assets/app.scss Normal file
View File

@@ -0,0 +1,2 @@
@import "tailwindcss";
@import url('https://fonts.googleapis.com/css2?family=DynaPuff:wght@400..700&display=swap');

View File

@@ -1,19 +1,19 @@
framework:
cache:
# Unique name of your app: used to compute stable namespaces for cache keys.
#prefix_seed: your_vendor_name/app_name
# Nom unique de votre application : utilisé pour calculer des espaces de noms stables pour les clés de cache.
# Ceci est CRUCIAL pour éviter les collisions de clés si plusieurs applications partagent le même serveur de cache (ex: Redis).
# Décommentez et remplacez par une valeur unique à votre projet (ex: "mon_entreprise/mon_app")
prefix_seed: 'e-cosplay/contest' # <-- REMPLACEZ CECI PAR UN NOM UNIQUE À VOTRE PROJET
# The "app" cache stores to the filesystem by default.
# The data in this cache should persist between deploys.
# Other options include:
# Redis
#app: cache.adapter.redis
#default_redis_provider: redis://localhost
# APCu (not recommended with heavy random-write workloads as memory fragmentation can cause perf issues)
#app: cache.adapter.apcu
# Namespaced pools use the above "app" backend by default
#pools:
#my.dedicated.cache: null
# En production, utilisez un adaptateur de cache rapide et performant comme Redis.
# Assurez-vous que votre serveur Redis est accessible.
app: cache.adapter.redis
default_redis_provider: '%env(REDIS_DSN)%'
# Vous pouvez également optimiser les pools personnalisés pour la production si besoin.
pools:
my.user_data_cache:
adapter: cache.adapter.redis
my.api_data_cache:
adapter: cache.adapter.redis
vite_cache_pool:
adapter: cache.adapter.redis

View File

@@ -1,7 +1,7 @@
when@dev:
web_profiler:
toolbar: true
intercept_redirects: false
framework:
profiler:
collect_serializer_data: true

View File

@@ -1,5 +1,11 @@
controllers:
resource:
path: ../src/Controller/
namespace: App\Controller
type: attribute
presta_sitemap:
resource: "@PrestaSitemapBundle/config/routing.yml"

View File

@@ -16,5 +16,9 @@ services:
App\:
resource: '../src/'
App\Twig\ViteAssetExtension:
arguments:
$manifest: '%kernel.project_dir%/public/build/.vite/manifest.json'
$cache: '@vite_cache_pool'
# add more service definitions when explicit configuration is needed
# please note that last definitions always *replace* previous ones

View File

@@ -10,7 +10,7 @@ services:
# Utilise l'UID/GID de l'hôte pour éviter les problèmes de permissions
UID: ${UID:-1000}
GID: ${GID:-1000}
container_name: ecosplay_php
container_name: mainframe_php
restart: unless-stopped
environment:
- XDEBUG_MODE=coverage
@@ -37,7 +37,7 @@ services:
args:
UID: ${UID:-1000}
GID: ${GID:-1000}
container_name: ecosplay_messenger_worker
container_name: mainframe_messenger_worker
restart: unless-stopped
# Commande pour lancer le worker. 'async' est le nom du transport par défaut.
command: php bin/console messenger:consume async --memory-limit=128M --time-limit=3600
@@ -55,7 +55,7 @@ services:
# Conteneur pour compiler les assets JS/CSS en développement
bun:
image: oven/bun:1-slim
container_name: ecosplay_bun
container_name: mainframe_bun
restart: unless-stopped
# Exécute les commandes avec l'utilisateur de l'hôte pour éviter les problèmes de permissions sur node_modules
user: "${UID:-1000}:${GID:-1000}"
@@ -74,7 +74,7 @@ services:
# Serveur web moderne qui sert l'application et gère le PHP-FPM
caddy:
image: caddy:2-alpine
container_name: ecosplay_caddy
container_name: mainframe_caddy
restart: unless-stopped
ports:
# Mappe le port 8000 de l'hôte au port 80 du conteneur
@@ -92,7 +92,7 @@ services:
# --- Service Base de Données principale (PostgreSQL) ---
db:
image: postgres:16-alpine
container_name: ecosplay_db
container_name: mainframe_db
restart: unless-stopped
ports:
- "5432:5432"
@@ -109,7 +109,7 @@ services:
# --- Service Cache/Messenger (Redis) ---
redis:
image: redis:7-alpine
container_name: ecosplay_redis
container_name: mainframe_redis
restart: unless-stopped
networks:
- mainframe_network # Assignation au réseau commun
@@ -118,7 +118,7 @@ services:
# Intercepte tous les emails envoyés en développement
mailhog:
image: mailhog/mailhog:latest
container_name: ecosplay_mailhog
container_name: mainframe_mailhog
restart: unless-stopped
ports:
# Port 1025 pour le serveur SMTP factice
@@ -132,7 +132,7 @@ services:
# Fournit une API compatible S3 pour le stockage de fichiers
minio:
image: minio/minio:RELEASE.2025-02-03T21-03-04Z
container_name: ecosplay_minio
container_name: mainframe_minio
restart: unless-stopped
ports:
# Port 9000 pour l'API S3
@@ -153,7 +153,7 @@ services:
# --- Service de Gestion des Secrets (HashiCorp Vault) ---
vault:
image: hashicorp/vault:latest
container_name: ecosplay_vault
container_name: mainframe_vault
restart: unless-stopped
ports:
- "8210:8200" # Mappe le port 8210 de l'hôte au port 8200 du conteneur Vault

View File

@@ -0,0 +1,18 @@
<?php
namespace App\Controller\Artemis;
use App\Attribute\Mainframe;
use Symfony\Bundle\FrameworkBundle\Controller\AbstractController;
use Symfony\Component\HttpFoundation\JsonResponse;
use Symfony\Component\Routing\Attribute\Route;
class SecurityController extends AbstractController
{
#[Route(path: '/artemis',name: 'login',methods: 'GET')]
#[Mainframe(index: false,sitemap: false,sitemapPage: null)]
public function index(): JsonResponse
{
return$this->json([]);
}
}

View File

@@ -10,8 +10,7 @@ use Symfony\Component\Routing\Attribute\Route;
class HomeController extends AbstractController
{
#[Route(path: '/',name: 'toot',methods: 'GET')]
#[Mainframe(index: false,sitemap: false,sitemapPage: null)]
#[Route(path: '/',name: 'root',methods: 'GET')]
public function index(): JsonResponse
{
return$this->json([]);

View File

@@ -0,0 +1,22 @@
<?php
namespace App\EventListener;
use Symfony\Component\EventDispatcher\EventSubscriberInterface;
use Symfony\Component\Routing\Generator\UrlGeneratorInterface;
use Presta\SitemapBundle\Event\SitemapPopulateEvent;
use Presta\SitemapBundle\Service\UrlContainerInterface;
use Presta\SitemapBundle\Sitemap\Url\UrlConcrete;
class SitemapSubscriber implements EventSubscriberInterface {
public static function getSubscribedEvents(): array
{
return [
SitemapPopulateEvent::class => 'populate',
];
}
public function populate(SitemapPopulateEvent $event): void
{
}
}

View File

@@ -0,0 +1,86 @@
<?php
namespace App\Twig;
use Psr\Cache\CacheItemPoolInterface;
use Twig\Extension\AbstractExtension;
use Twig\TwigFunction;
class ViteAssetExtension extends AbstractExtension
{
private ?array $manifestData = null;
const CACHE_KEY = 'vite_manifest';
private readonly bool $isDev;
public function __construct(
private readonly string $manifest,
private readonly CacheItemPoolInterface $cache,
) {
$this->isDev = $_ENV['VITE_LOAD'] == "0";
}
public function getFunctions(): array
{
return [
new TwigFunction('vite_asset', $this->asset(...), ['is_safe' => ['html']])
];
}
public function asset(string $entry, array $deps): string
{
if ($this->isDev) {
return $this->assetDev($entry, $deps);
}
return $this->assetProd($entry);
}
public function assetDev(string $entry, array $deps): string
{
$html = <<<HTML
<script type="module" src="http://localhost:5173/assets/@vite/client"></script>
HTML;
return $html . <<<HTML
<script type="module" src="http://localhost:5173/assets/{$entry}" defer></script>
HTML;
}
public function assetProd(string $entry): string
{
if ($this->manifestData === null) {
$item = $this->cache->getItem(self::CACHE_KEY);
if ($item->isHit()) {
$this->manifestData = $item->get();
} else {
$this->manifestData = json_decode((string)file_get_contents($this->manifest), true);
$item->set($this->manifestData);
$this->cache->save($item);
}
}
$file = $this->manifestData[$entry]['file'] ?? '';
$css = $this->manifestData[$entry]['css'] ?? [];
$imports = $this->manifestData[$entry]['imports'] ?? [];
$html = <<<HTML
<script type="module" src="/build/{$file}" defer></script>
HTML;
foreach($css as $cssFile) {
$html .= <<<HTML
<link rel="stylesheet" media="screen" href="/build/{$cssFile}"/>
HTML;
}
foreach($imports as $import) {
$html .= <<<HTML
<link rel="modulepreload" href="/assets/{$import}"/>
HTML;
}
return $html;
}
}

View File

@@ -0,0 +1,23 @@
<?php
declare(strict_types=1);
namespace App\Tests\Controller\Artemis;
use Symfony\Bundle\FrameworkBundle\Test\WebTestCase;
class SecurityControllerTest extends WebTestCase
{
public function testLogin(): void
{
$client = static::createClient();
// Request a specific page
$crawler = $client->request('GET', '/artemis');
// Validate a successful response and some content
$this->assertResponseIsSuccessful();
$this->assertResponseHeaderSame('Content-Type', 'application/json');
}
}

View File

@@ -0,0 +1,55 @@
<?php
namespace App\Tests\EventListener;
use App\EventListener\SitemapSubscriber;
use PHPUnit\Framework\TestCase;
use Symfony\Component\Routing\Generator\UrlGeneratorInterface;
use Presta\SitemapBundle\Event\SitemapPopulateEvent;
use Presta\SitemapBundle\Service\UrlContainerInterface;
use Presta\SitemapBundle\Sitemap\Url\UrlConcrete;
class SitemapSubscriberTest extends TestCase
{
/**
* @var UrlGeneratorInterface|\PHPUnit\Framework\MockObject\MockObject
*/
private $urlGenerator;
/**
* @var UrlContainerInterface|\PHPUnit\Framework\MockObject\MockObject
*/
private $urlContainer;
protected function setUp(): void
{
// Mock the UrlGeneratorInterface
$this->urlGenerator = $this->createMock(UrlGeneratorInterface::class);
// Mock the UrlContainerInterface
$this->urlContainer = $this->createMock(UrlContainerInterface::class);
}
public function testGetSubscribedEvents(): void
{
// Assert that the getSubscribedEvents method returns the correct event.
$expectedEvents = [
SitemapPopulateEvent::class => 'populate',
];
$this->assertEquals($expectedEvents, SitemapSubscriber::getSubscribedEvents());
}
public function testPopulate(): void
{
// Create an instance of the subscriber
$subscriber = new SitemapSubscriber();
// Create a mock for SitemapPopulateEvent
$event = new SitemapPopulateEvent($this->urlContainer, $this->urlGenerator);
// Call the populate method
$subscriber->populate($event);
$this->assertEquals("a","a");
}
}

View File

@@ -0,0 +1,95 @@
<?php
namespace App\Tests\Twig;
use App\Twig\ViteAssetExtension;
use PHPUnit\Framework\TestCase;
use Psr\Cache\CacheItemInterface;
use Psr\Cache\CacheItemPoolInterface;
use Twig\TwigFunction;
class ViteAssetExtensionTest extends TestCase
{
public function testAssetDev()
{
$_ENV['VITE_LOAD'] = "0";
$cacheMock = $this->createMock(CacheItemPoolInterface::class);
$extension = new ViteAssetExtension('/path/to/manifest.json', $cacheMock);
$output = $extension->asset('main.js', []);
$this->assertStringContainsString('<script type="module" src="http://localhost:5173/assets/@vite/client"></script>', $output);
$this->assertStringContainsString('<script type="module" src="http://localhost:5173/assets/main.js" defer></script>', $output);
}
public function testAssetProdWithCacheHit()
{
$_ENV['VITE_LOAD'] = "1";
$manifest = [
'main.js' => [
'file' => 'main.123abc.js',
'css' => ['style.456def.css'],
'imports' => ['vendor.789ghi.js'],
]
];
$cacheItem = $this->createMock(CacheItemInterface::class);
$cacheItem->method('isHit')->willReturn(true);
$cacheItem->method('get')->willReturn($manifest);
$cacheMock = $this->createMock(CacheItemPoolInterface::class);
$cacheMock->method('getItem')->willReturn($cacheItem);
$extension = new ViteAssetExtension('/unused/path.json', $cacheMock);
$output = $extension->asset('main.js', []);
$this->assertStringContainsString('<script type="module" src="/build/main.123abc.js" defer></script>', $output);
$this->assertStringContainsString('<link rel="stylesheet" media="screen" href="/build/style.456def.css"/>', $output);
$this->assertStringContainsString('<link rel="modulepreload" href="/assets/vendor.789ghi.js"/>', $output);
}
public function testAssetProdWithCacheMiss()
{
$_ENV['VITE_LOAD'] = "1";
$manifestPath = tempnam(sys_get_temp_dir(), 'manifest');
file_put_contents($manifestPath, json_encode([
'main.js' => [
'file' => 'main.abc.js',
'css' => ['style.css'],
'imports' => ['chunk.js'],
]
]));
$cacheItem = $this->createMock(CacheItemInterface::class);
$cacheItem->method('isHit')->willReturn(false);
$cacheItem->expects($this->once())->method('set');
$cacheMock = $this->createMock(CacheItemPoolInterface::class);
$cacheMock->method('getItem')->willReturn($cacheItem);
$cacheMock->expects($this->once())->method('save');
$extension = new ViteAssetExtension($manifestPath, $cacheMock);
$output = $extension->asset('main.js', []);
$this->assertStringContainsString('<script type="module" src="/build/main.abc.js" defer></script>', $output);
$this->assertStringContainsString('<link rel="stylesheet" media="screen" href="/build/style.css"/>', $output);
$this->assertStringContainsString('<link rel="modulepreload" href="/assets/chunk.js"/>', $output);
}
public function testGetFunctions()
{
$_ENV['VITE_LOAD'] = "0";
$cacheMock = $this->createMock(CacheItemPoolInterface::class);
$extension = new ViteAssetExtension('/path/to/manifest.json', $cacheMock);
$functions = $extension->getFunctions();
$this->assertIsArray($functions);
$this->assertNotEmpty($functions);
$this->assertInstanceOf(TwigFunction::class, $functions[0]);
$this->assertEquals('vite_asset', $functions[0]->getName());
}
}

View File

@@ -34,7 +34,7 @@ export default defineConfig({
// Configuration CORS pour autoriser les requêtes depuis votre backend Symfony
cors: {
origin: ['http://esyweb.local']
origin: ['https://esyweb.local']
},
},
@@ -62,7 +62,7 @@ export default defineConfig({
rollupOptions: {
// Points d'entrée de votre application
input: {
administration: resolve(__dirname, 'assets/administration.js'),
app: resolve(__dirname, 'assets/app.js'),
// Exemple : 'styles': resolve(__dirname, 'assets/styles/main.scss'),
}
},