✨ Story 3.2: router php urls
This commit is contained in:
7
.htaccess
Normal file
7
.htaccess
Normal file
@@ -0,0 +1,7 @@
|
|||||||
|
RewriteEngine On
|
||||||
|
RewriteBase /
|
||||||
|
|
||||||
|
RewriteCond %{REQUEST_FILENAME} !-f
|
||||||
|
RewriteCond %{REQUEST_FILENAME} !-d
|
||||||
|
|
||||||
|
RewriteRule ^(.*)$ index.php [QSA,L]
|
||||||
@@ -2,7 +2,7 @@
|
|||||||
|
|
||||||
## Status
|
## Status
|
||||||
|
|
||||||
Ready for Dev
|
review
|
||||||
|
|
||||||
## Story
|
## Story
|
||||||
|
|
||||||
@@ -21,34 +21,34 @@ Ready for Dev
|
|||||||
|
|
||||||
## Tasks / Subtasks
|
## Tasks / Subtasks
|
||||||
|
|
||||||
- [] **Task 1 : Créer le router PHP** (AC: 2, 6)
|
- [x] **Task 1 : Créer le router PHP** (AC: 2, 6)
|
||||||
- [] Créer `includes/router.php`
|
- [x] Créer `includes/router.php`
|
||||||
- [] Implémenter la classe Router
|
- [x] Implémenter la classe Router
|
||||||
- [] Méthode add() pour ajouter des routes
|
- [x] Méthode add() pour ajouter des routes
|
||||||
- [] Méthode resolve() pour matcher une URL
|
- [x] Méthode resolve() pour matcher une URL
|
||||||
- [] Méthode dispatch() pour exécuter la route
|
- [x] Méthode dispatch() pour exécuter la route
|
||||||
|
|
||||||
- [] **Task 2 : Configurer les routes** (AC: 3, 4)
|
- [x] **Task 2 : Configurer les routes** (AC: 3, 4)
|
||||||
- [] Route `/` → pages/home.php
|
- [x] Route `/` → pages/home.php
|
||||||
- [] Route `/projets` → pages/projects.php
|
- [x] Route `/projets` → pages/projects.php
|
||||||
- [] Route `/projet/{slug}` → pages/project-single.php
|
- [x] Route `/projet/{slug}` → pages/project-single.php
|
||||||
- [] Route `/competences` → pages/skills.php
|
- [x] Route `/competences` → pages/skills.php
|
||||||
- [] Route `/a-propos` → pages/about.php
|
- [x] Route `/a-propos` → pages/about.php
|
||||||
- [] Route `/contact` → pages/contact.php
|
- [x] Route `/contact` → pages/contact.php
|
||||||
|
|
||||||
- [] **Task 3 : Créer la page 404** (AC: 5)
|
- [x] **Task 3 : Créer la page 404** (AC: 5)
|
||||||
- [] Créer `pages/404.php`
|
- [x] Créer `pages/404.php`
|
||||||
- [] Design cohérent avec le site
|
- [x] Design cohérent avec le site
|
||||||
- [] Lien retour vers l'accueil
|
- [x] Lien retour vers l'accueil
|
||||||
|
|
||||||
- [] **Task 4 : Configurer le serveur** (AC: 1)
|
- [x] **Task 4 : Configurer le serveur** (AC: 1)
|
||||||
- [] Créer `.htaccess` pour Apache
|
- [x] Créer `.htaccess` pour Apache
|
||||||
- [] Documenter la config nginx équivalente
|
- [x] Documenter la config nginx équivalente
|
||||||
|
|
||||||
- [] **Task 5 : Mettre à jour index.php**
|
- [x] **Task 5 : Mettre à jour index.php**
|
||||||
- [] Inclure le router
|
- [x] Inclure le router
|
||||||
- [] Définir toutes les routes
|
- [x] Définir toutes les routes
|
||||||
- [] Appeler dispatch()
|
- [x] Appeler dispatch()
|
||||||
|
|
||||||
## Dev Notes
|
## Dev Notes
|
||||||
|
|
||||||
@@ -215,36 +215,38 @@ if (!$project) {
|
|||||||
## Dev Agent Record
|
## Dev Agent Record
|
||||||
|
|
||||||
### Agent Model Used
|
### Agent Model Used
|
||||||
Claude Opus 4.5 (claude-opus-4-5-20251101)
|
GPT-5 Codex
|
||||||
|
|
||||||
### File List
|
### File List
|
||||||
| File | Action | Description |
|
| File | Action | Description |
|
||||||
|------|--------|-------------|
|
|------|--------|-------------|
|
||||||
| `includes/router.php` | Created | Router PHP simple (43 lignes) |
|
| `includes/router.php` | Created | Router PHP simple |
|
||||||
| `index.php` | Modified | Converti en front controller |
|
| `index.php` | Modified | Converti en front controller |
|
||||||
| `.htaccess` | Created | Réécriture URLs Apache |
|
| `.htaccess` | Created | Réécriture URLs Apache |
|
||||||
| `pages/home.php` | Created | Page d'accueil |
|
|
||||||
| `pages/projects.php` | Created | Page liste projets (placeholder) |
|
| `pages/projects.php` | Created | Page liste projets (placeholder) |
|
||||||
| `pages/project-single.php` | Created | Page projet individuel |
|
| `pages/project-single.php` | Created | Page projet individuel |
|
||||||
| `pages/skills.php` | Created | Page compétences (placeholder) |
|
| `pages/skills.php` | Created | Page compétences (placeholder) |
|
||||||
| `pages/about.php` | Created | Page à propos (placeholder) |
|
| `pages/about.php` | Created | Page à propos (placeholder) |
|
||||||
| `pages/contact.php` | Created | Page contact (placeholder) |
|
| `pages/contact.php` | Created | Page contact (placeholder) |
|
||||||
| `pages/404.php` | Created | Page erreur 404 |
|
| `pages/404.php` | Created | Page erreur 404 |
|
||||||
|
| `tests/router.test.php` | Created | Tests du router |
|
||||||
|
| `tests/router.test.ps1` | Created | Vérif .htaccess |
|
||||||
|
| `tests/run.ps1` | Modified | Ajout tests router |
|
||||||
|
|
||||||
### Completion Notes
|
### Completion Notes
|
||||||
- Router PHP léger (43 lignes < 50 requis)
|
- Router PHP léger (< 50 lignes)
|
||||||
- Support des paramètres dynamiques {slug}
|
- Support des paramètres dynamiques {slug}
|
||||||
- Trailing slash normalisé automatiquement
|
- Trailing slash normalisé automatiquement
|
||||||
- 404 pour routes inconnues
|
- 404 pour routes inconnues
|
||||||
- Pages placeholder créées pour futures stories
|
- Pages placeholder créées pour futures stories
|
||||||
- Tous les tests du router passent (8/8)
|
- Tests: `powershell -ExecutionPolicy Bypass -File tests/run.ps1`
|
||||||
|
|
||||||
### Debug Log References
|
### Debug Log References
|
||||||
Aucun problème rencontré.
|
Aucun problème bloquant.
|
||||||
|
|
||||||
## Change Log
|
## Change Log
|
||||||
|
|
||||||
| Date | Version | Description | Author |
|
| Date | Version | Description | Author |
|
||||||
|------|---------|-------------|--------|
|
|------|---------|-------------|--------|
|
||||||
| 2026-01-22 | 0.1 | Création initiale | Sarah (PO) |
|
| 2026-01-22 | 0.1 | Création initiale | Sarah (PO) |
|
||||||
| 2026-01-23 | 1.0 | Implémentation complète | James (Dev) |
|
| 2026-02-04 | 1.0 | Implémentation complète | Amelia |
|
||||||
|
|||||||
38
includes/router.php
Normal file
38
includes/router.php
Normal file
@@ -0,0 +1,38 @@
|
|||||||
|
<?php
|
||||||
|
/**
|
||||||
|
* Router simple pour URLs propres
|
||||||
|
*/
|
||||||
|
class Router
|
||||||
|
{
|
||||||
|
private array $routes = [];
|
||||||
|
|
||||||
|
public function add(string $pattern, string $handler): self
|
||||||
|
{
|
||||||
|
$regex = preg_replace('/\{(\w+)\}/', '([^/]+)', $pattern);
|
||||||
|
$this->routes['#^' . $regex . '$#'] = $handler;
|
||||||
|
return $this;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function resolve(string $uri): array
|
||||||
|
{
|
||||||
|
$path = parse_url($uri, PHP_URL_PATH);
|
||||||
|
$path = rtrim($path, '/') ?: '/';
|
||||||
|
|
||||||
|
foreach ($this->routes as $regex => $handler) {
|
||||||
|
if (preg_match($regex, $path, $matches)) {
|
||||||
|
array_shift($matches);
|
||||||
|
return [$handler, $matches];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return ['pages/404.php', []];
|
||||||
|
}
|
||||||
|
|
||||||
|
public function dispatch(): void
|
||||||
|
{
|
||||||
|
$uri = $_SERVER['REQUEST_URI'] ?? '/';
|
||||||
|
[$handler, $params] = $this->resolve($uri);
|
||||||
|
$GLOBALS['routeParams'] = $params;
|
||||||
|
require __DIR__ . '/../' . $handler;
|
||||||
|
}
|
||||||
|
}
|
||||||
15
index.php
15
index.php
@@ -1,4 +1,15 @@
|
|||||||
<?php
|
<?php
|
||||||
// index.php - Point d'entrée temporaire
|
require_once __DIR__ . '/includes/functions.php';
|
||||||
|
require_once __DIR__ . '/includes/router.php';
|
||||||
|
|
||||||
require_once __DIR__ . '/pages/home.php';
|
$router = new Router();
|
||||||
|
|
||||||
|
$router
|
||||||
|
->add('/', 'pages/home.php')
|
||||||
|
->add('/projets', 'pages/projects.php')
|
||||||
|
->add('/projet/{slug}', 'pages/project-single.php')
|
||||||
|
->add('/competences', 'pages/skills.php')
|
||||||
|
->add('/a-propos', 'pages/about.php')
|
||||||
|
->add('/contact', 'pages/contact.php');
|
||||||
|
|
||||||
|
$router->dispatch();
|
||||||
23
pages/404.php
Normal file
23
pages/404.php
Normal file
@@ -0,0 +1,23 @@
|
|||||||
|
<?php
|
||||||
|
http_response_code(404);
|
||||||
|
|
||||||
|
$pageTitle = 'Page non trouvée';
|
||||||
|
$currentPage = '';
|
||||||
|
|
||||||
|
include_template('header', compact('pageTitle'));
|
||||||
|
include_template('navbar', compact('currentPage'));
|
||||||
|
?>
|
||||||
|
|
||||||
|
<main class="min-h-screen flex items-center justify-center">
|
||||||
|
<div class="container-content text-center py-20">
|
||||||
|
<h1 class="text-display text-primary mb-4">404</h1>
|
||||||
|
<p class="text-xl text-text-secondary mb-8">
|
||||||
|
Oups ! Cette page n'existe pas.
|
||||||
|
</p>
|
||||||
|
<a href="/" class="btn-primary">
|
||||||
|
Retour à l'accueil
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
</main>
|
||||||
|
|
||||||
|
<?php include_template('footer'); ?>
|
||||||
16
pages/about.php
Normal file
16
pages/about.php
Normal file
@@ -0,0 +1,16 @@
|
|||||||
|
<?php
|
||||||
|
$pageTitle = 'Me Découvrir';
|
||||||
|
$currentPage = 'about';
|
||||||
|
|
||||||
|
include_template('header', compact('pageTitle'));
|
||||||
|
include_template('navbar', compact('currentPage'));
|
||||||
|
?>
|
||||||
|
|
||||||
|
<main class="min-h-screen">
|
||||||
|
<div class="container-content py-20">
|
||||||
|
<h1 class="text-heading mb-4">Me Découvrir</h1>
|
||||||
|
<p class="text-text-secondary">Le parcours arrive bientôt.</p>
|
||||||
|
</div>
|
||||||
|
</main>
|
||||||
|
|
||||||
|
<?php include_template('footer'); ?>
|
||||||
16
pages/contact.php
Normal file
16
pages/contact.php
Normal file
@@ -0,0 +1,16 @@
|
|||||||
|
<?php
|
||||||
|
$pageTitle = 'Contact';
|
||||||
|
$currentPage = 'contact';
|
||||||
|
|
||||||
|
include_template('header', compact('pageTitle'));
|
||||||
|
include_template('navbar', compact('currentPage'));
|
||||||
|
?>
|
||||||
|
|
||||||
|
<main class="min-h-screen">
|
||||||
|
<div class="container-content py-20">
|
||||||
|
<h1 class="text-heading mb-4">Contact</h1>
|
||||||
|
<p class="text-text-secondary">Le formulaire arrive bientôt.</p>
|
||||||
|
</div>
|
||||||
|
</main>
|
||||||
|
|
||||||
|
<?php include_template('footer'); ?>
|
||||||
24
pages/project-single.php
Normal file
24
pages/project-single.php
Normal file
@@ -0,0 +1,24 @@
|
|||||||
|
<?php
|
||||||
|
$currentPage = '';
|
||||||
|
$slug = $GLOBALS['routeParams'][0] ?? null;
|
||||||
|
$project = $slug ? getProjectBySlug($slug) : null;
|
||||||
|
|
||||||
|
if (!$project) {
|
||||||
|
http_response_code(404);
|
||||||
|
include __DIR__ . '/404.php';
|
||||||
|
exit;
|
||||||
|
}
|
||||||
|
|
||||||
|
$pageTitle = $project['title'] ?? 'Projet';
|
||||||
|
include_template('header', compact('pageTitle'));
|
||||||
|
include_template('navbar', compact('currentPage'));
|
||||||
|
?>
|
||||||
|
|
||||||
|
<main class="min-h-screen">
|
||||||
|
<div class="container-content py-20">
|
||||||
|
<h1 class="text-heading mb-4"><?= htmlspecialchars($project['title'], ENT_QUOTES, 'UTF-8') ?></h1>
|
||||||
|
<p class="text-text-secondary">Page projet en construction.</p>
|
||||||
|
</div>
|
||||||
|
</main>
|
||||||
|
|
||||||
|
<?php include_template('footer'); ?>
|
||||||
16
pages/projects.php
Normal file
16
pages/projects.php
Normal file
@@ -0,0 +1,16 @@
|
|||||||
|
<?php
|
||||||
|
$pageTitle = 'Projets';
|
||||||
|
$currentPage = 'projects';
|
||||||
|
|
||||||
|
include_template('header', compact('pageTitle'));
|
||||||
|
include_template('navbar', compact('currentPage'));
|
||||||
|
?>
|
||||||
|
|
||||||
|
<main class="min-h-screen">
|
||||||
|
<div class="container-content py-20">
|
||||||
|
<h1 class="text-heading mb-4">Projets</h1>
|
||||||
|
<p class="text-text-secondary">La liste des projets arrive bientôt.</p>
|
||||||
|
</div>
|
||||||
|
</main>
|
||||||
|
|
||||||
|
<?php include_template('footer'); ?>
|
||||||
16
pages/skills.php
Normal file
16
pages/skills.php
Normal file
@@ -0,0 +1,16 @@
|
|||||||
|
<?php
|
||||||
|
$pageTitle = 'Compétences';
|
||||||
|
$currentPage = 'skills';
|
||||||
|
|
||||||
|
include_template('header', compact('pageTitle'));
|
||||||
|
include_template('navbar', compact('currentPage'));
|
||||||
|
?>
|
||||||
|
|
||||||
|
<main class="min-h-screen">
|
||||||
|
<div class="container-content py-20">
|
||||||
|
<h1 class="text-heading mb-4">Compétences</h1>
|
||||||
|
<p class="text-text-secondary">Le détail des compétences arrive bientôt.</p>
|
||||||
|
</div>
|
||||||
|
</main>
|
||||||
|
|
||||||
|
<?php include_template('footer'); ?>
|
||||||
33
tests/router.test.php
Normal file
33
tests/router.test.php
Normal file
@@ -0,0 +1,33 @@
|
|||||||
|
<?php
|
||||||
|
require_once __DIR__ . '/../includes/router.php';
|
||||||
|
|
||||||
|
function assertTrue($cond, $msg) {
|
||||||
|
if (!$cond) {
|
||||||
|
fwrite(STDERR, $msg . PHP_EOL);
|
||||||
|
exit(1);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
$router = new Router();
|
||||||
|
$router
|
||||||
|
->add('/', 'pages/home.php')
|
||||||
|
->add('/projets', 'pages/projects.php')
|
||||||
|
->add('/projet/{slug}', 'pages/project-single.php');
|
||||||
|
|
||||||
|
[$handler, $params] = $router->resolve('/');
|
||||||
|
assertTrue($handler === 'pages/home.php', 'home route failed');
|
||||||
|
|
||||||
|
[$handler, $params] = $router->resolve('/projets');
|
||||||
|
assertTrue($handler === 'pages/projects.php', 'projects route failed');
|
||||||
|
|
||||||
|
[$handler, $params] = $router->resolve('/projet/ecommerce-xyz');
|
||||||
|
assertTrue($handler === 'pages/project-single.php', 'project route failed');
|
||||||
|
assertTrue(($params[0] ?? '') === 'ecommerce-xyz', 'slug param failed');
|
||||||
|
|
||||||
|
[$handler, $params] = $router->resolve('/unknown');
|
||||||
|
assertTrue($handler === 'pages/404.php', '404 route failed');
|
||||||
|
|
||||||
|
[$handler, $params] = $router->resolve('/projets/');
|
||||||
|
assertTrue($handler === 'pages/projects.php', 'trailing slash failed');
|
||||||
|
|
||||||
|
fwrite(STDOUT, "OK\n");
|
||||||
15
tests/router.test.ps1
Normal file
15
tests/router.test.ps1
Normal file
@@ -0,0 +1,15 @@
|
|||||||
|
$ErrorActionPreference = 'Stop'
|
||||||
|
|
||||||
|
function Assert-True {
|
||||||
|
param(
|
||||||
|
[bool]$Condition,
|
||||||
|
[string]$Message
|
||||||
|
)
|
||||||
|
if (-not $Condition) { throw $Message }
|
||||||
|
}
|
||||||
|
|
||||||
|
Assert-True (Test-Path '.htaccess') 'Missing .htaccess'
|
||||||
|
$ht = Get-Content -Raw '.htaccess'
|
||||||
|
Assert-True ($ht -match 'RewriteRule') 'Missing RewriteRule in .htaccess'
|
||||||
|
|
||||||
|
'OK'
|
||||||
@@ -8,5 +8,7 @@ $here = Split-Path -Parent $MyInvocation.MyCommand.Path
|
|||||||
& (Join-Path $here 'cta.test.ps1')
|
& (Join-Path $here 'cta.test.ps1')
|
||||||
& (Join-Path $here 'home.test.ps1')
|
& (Join-Path $here 'home.test.ps1')
|
||||||
& (Join-Path $here 'quicknav.test.ps1')
|
& (Join-Path $here 'quicknav.test.ps1')
|
||||||
|
& (Join-Path $here 'router.test.ps1')
|
||||||
php (Join-Path $here 'projects.test.php')
|
php (Join-Path $here 'projects.test.php')
|
||||||
|
php (Join-Path $here 'router.test.php')
|
||||||
'OK'
|
'OK'
|
||||||
Reference in New Issue
Block a user