From c36b95a15f51c9a5de8e6619bea2a4a97f55df35 Mon Sep 17 00:00:00 2001 From: Benjamin Date: Fri, 20 Mar 2026 11:40:11 +0100 Subject: [PATCH 1/2] Start project PHP Back. 50% routes. --- README.md | 58 ++++++- config/general.php | 8 + public/index.php | 13 ++ src/Domain/Controller.php | 37 ++++ src/Domain/Pages/PagesController.php | 34 ++++ .../ConfigFailedLoadingException.php | 28 ++++ src/Exceptions/InvalidRouteException.php | 15 ++ src/Helpers/AutoLoader.php | 38 +++++ src/Helpers/CacheManager.php | 1 + src/Helpers/ConfigFactory.php | 42 +++++ src/Http/Route.php | 31 ++++ src/Http/Router.php | 158 ++++++++++++++++++ src/Kernel.php | 93 +++++++++++ 13 files changed, 555 insertions(+), 1 deletion(-) create mode 100644 config/general.php create mode 100644 public/index.php create mode 100644 src/Domain/Controller.php create mode 100644 src/Domain/Pages/PagesController.php create mode 100644 src/Exceptions/ConfigFailedLoadingException.php create mode 100644 src/Exceptions/InvalidRouteException.php create mode 100644 src/Helpers/AutoLoader.php create mode 100644 src/Helpers/CacheManager.php create mode 100644 src/Helpers/ConfigFactory.php create mode 100644 src/Http/Route.php create mode 100644 src/Http/Router.php create mode 100644 src/Kernel.php diff --git a/README.md b/README.md index 27df611..9a34211 100644 --- a/README.md +++ b/README.md @@ -1 +1,57 @@ -# Les Recettes des Papis +# Les recettes des Papis + +## Installation + - Décompressez l'archive du site. + - Pointez votre domaine sur le dossier "public". + - Modifiez les fichiers de configuration dans "config". + +## Informations pratiques sur le PHP. + + - Ce site utilise le principe des routes. Afin de ne pas avoir x fichiers publiques identiques pour chaque page. + - Tout est géré par le fichier public/index.php. + - index.php va charger le Kernel (Noyau) de l'application. + - Le noyau fonctionne sur le principe d'élément unique. La méthode ``Kernel::getInstance()`` va permettre d'avoir l'unique instance de la classe Kernel. + - Une fois ``Kernel::getInstance()->init()`` déclenché, le noyau enregistre l'autoloading des fichiers, puis enregistre la configuration. + - La configuration sont des fichiers dans le dossier config/ qui vont renvoyer un tableau clé/valeur. Exemple + +```php + // Fichier config/test.php + "Val1", + 'cle_2' => "Val2", + // ... + ] ?> +``` + +- Les fichiers de configuration sont chargés via la méthode ``ConfigFactory::loadConfigFile()`` qui va vérifier si le fichier de configuration existe et le charge en conséquence. +- On enchaine ensuite sur le Router. +- Le Router est une classe utilitaire qui va permettre de faire le lien entre la route saisie ``/test/...`` de l'utilisateur et les routes enregistrés par le site. +- Le Router va d'abord commencer par récupérer la route voulue par l'utilisateur. +- Puis, le router va rechercher tous les Controllers existants. +- Un Controller est une classe qui va permettre de définir les routes et de dire, qu'est-ce qui se passe quand je prends cette route. Les routes du Controller sont définis par la methode ``Controller::defineRoutes()`` +- Pour définir les routes dans la méthode précédente, vous pouvez soit créer un objet ``Route`` en remplissant tous les champs, ou bien la méthode ``Controller::Route()`` qui va préremplir certains champs. +- Exemple de Controller +```php + class TestController extends Controller { + + public static function defineRoutes(): array { + return [ + self::Route( routeUrl: '/test', routeName: "Test", routeAction: "test" ), + new Route( routeUrl: "/test2", routeName: "Test2", routeController: "TestController", routeAction: "test2", routeMethods = [ 'GET' ] ) + ]; + } + + public function test(){ + echo "Je suis déclenché lorsque j'atteinds la route /test"; + } + + public function test2(){ + echo "Je suis déclenché lorsque j'atteinds la route /test2 !!!"; + } + } +``` +- Retournons au routeur, pour chercher les controllers, il va scanner les fichiers du dossier src/Domain avec un Iterator Récursif. (Pour se simplifier un peu la tâche.) +- Il va traiter le nom du chemin absolu pour obtenir le nom de la classe, puis va vérifier si cette classe est une sous-classe de Controller. +- Après avoir trouvé tous les Controllers, il va récupérer toutes les routes de chaque controller. +- Enfin, il va vérifier si la route utilisateur correspond à une des routes dans sa liste, si ça correspond, le Router va charger la méthode ``new {RouteController}()->{RouteAction}()`` +- Si par exemple, le router trouve la route ``/test`` voulu par l'utilisateur, il va déclencher la méthode ``new TestController()->test()``. \ No newline at end of file diff --git a/config/general.php b/config/general.php new file mode 100644 index 0000000..862b32d --- /dev/null +++ b/config/general.php @@ -0,0 +1,8 @@ + 'http://127.0.0.1:8080/', + 'website_name' => 'Les recettes des Papis', + + 'website_path' => APP_ROOT, +]; \ No newline at end of file diff --git a/public/index.php b/public/index.php new file mode 100644 index 0000000..8bbcd78 --- /dev/null +++ b/public/index.php @@ -0,0 +1,13 @@ + static::class, + 'routeMethods' => [ 'GET' ], + ]; + + $args = array_merge($defaults, $args); + return new Route( ...$args ); + } + + /** + * Permet de définir les routes du controller sous le format d'une liste d'objets Route. + * Vous pouvez utiliser la method self::Route() pour préremplir des champs. + * + * @return Route[] + * @see self::Route() + */ + abstract public static function defineRoutes(): array; + +} \ No newline at end of file diff --git a/src/Domain/Pages/PagesController.php b/src/Domain/Pages/PagesController.php new file mode 100644 index 0000000..b8da0c1 --- /dev/null +++ b/src/Domain/Pages/PagesController.php @@ -0,0 +1,34 @@ +configFilePath = $configFilePath; + $this->message = "Failed to load configuration file '{$configFilePath}'."; + parent::__construct(); + } + +} \ No newline at end of file diff --git a/src/Exceptions/InvalidRouteException.php b/src/Exceptions/InvalidRouteException.php new file mode 100644 index 0000000..4b97e00 --- /dev/null +++ b/src/Exceptions/InvalidRouteException.php @@ -0,0 +1,15 @@ +clientRoute = $clientRoute; + $message = "{$clientRoute} doesn't exist"; + parent::__construct($message, 404); + } + +} \ No newline at end of file diff --git a/src/Helpers/AutoLoader.php b/src/Helpers/AutoLoader.php new file mode 100644 index 0000000..841fd6a --- /dev/null +++ b/src/Helpers/AutoLoader.php @@ -0,0 +1,38 @@ +routeUrl = $routeUrl; + $this->routeName = $routeName; + $this->routeController = $routeController; + $this->routeAction = $routeAction; + $this->routeMethods = $routeMethods; + } + +} \ No newline at end of file diff --git a/src/Http/Router.php b/src/Http/Router.php new file mode 100644 index 0000000..6dff479 --- /dev/null +++ b/src/Http/Router.php @@ -0,0 +1,158 @@ +routeMethods ) ){ + throw new InvalidRouteException( self::$clientRouteString ); + } else { + self::executeRouteAction(); + } + + } + + /** + * Permet de récupérer tous les controllers du dossier Domain. + * @return class-string[] + */ + private static function fetchControllers(): array { + + $classes = []; + + $iterator = new \RecursiveIteratorIterator( + new \RecursiveDirectoryIterator( APP_ROOT . 'src/Domain', FilesystemIterator::SKIP_DOTS ), + ); + + foreach( $iterator as $file ){ + + if( $file->isFile() && $file->getExtension() === 'php' ){ + + $fileName = $file->getPathname(); + + // Transformation du chemin du fichier vers le nom complet de la classe. + $fileName = str_replace( APP_ROOT . 'src/', AutoLoader::PRIMARY_NAMESPACE, $fileName ); + $fileName = str_replace( '.php', '', $fileName ); + $fileName = str_replace( '/', '\\', $fileName ); + + + /* + * Vérifie que l'on est bien sur un Controller. + * Ignore Controller car Controller n'est pas une sous-classe de Controller. + */ + if( is_subclass_of( $fileName, Controller::class ) ){ + $classes[] = $fileName; + } + + } + + } + + return $classes; + + } + + /** + * Récupérer toutes les routes des controllers récupéré avant. + * @return Route[] + */ + private static function fetchRoutes(): array { + + $routes = []; + + foreach( self::$controllers as $controllerClassString ){ + $routes = array_merge( $routes, $controllerClassString::defineRoutes() ); + } + + return $routes; + } + + /** + * Permet de savoir si la route que le client veut existe. + * @return Route|bool Retourne la route de l'utilisateur ou false si inexistante. + */ + private static function clientRouteExist(): Route|bool { + + foreach( self::$routes as $route ){ + /* + if( preg_match( self::getRegexRoute( $route), self::$clientRouteString, $matches ) ){ + var_dump( $matches ); + } + */ + /* + if( $route->routeUrl === self::$clientRouteString ){ + return $route; + } + */ + } + + return false; + + } + + private static function getRegexRoute( Route $route ): string { + $routeUrl = $route->routeUrl; + $routeUrl = str_replace( "{int}", "([0-9]+)", $routeUrl ); + return $routeUrl; + } + + /** + * Va permettre d'exécuter la méthode du Controller->action() en parsant les arguments si existants. + * @return void + */ + private static function executeRouteAction(): void { + $controller = self::$clientRoute->routeController; + $method = self::$clientRoute->routeAction; + + new $controller()->$method(); + } + +} \ No newline at end of file diff --git a/src/Kernel.php b/src/Kernel.php new file mode 100644 index 0000000..163d0cb --- /dev/null +++ b/src/Kernel.php @@ -0,0 +1,93 @@ + [ 'cle" → valeur ] ] + * + * @var array + */ + public private(set) static array $configs = []; + + /** + * Instance actuelle de l'application. + * @var Kernel|null + */ + private static ?self $instance = null; + + /** + * Méthode qui permet de démarrer le site. + * @return self + */ + public static function start(): self { + self::$instance = new self(); + self::$instance->init(); + return self::$instance; + } + + /** + * Permet d'obtenir l'instance actuelle du site. + * @return self + */ + public static function getInstance(): self { + return self::$instance; + } + + /** + * Constructeur. + */ + public function __construct() { + } + + /** + * Permet de préparer le démarrage du site. + * Lancé automatiquement par start(). + * + * @return void + * @see self::start() + */ + public function init(): void { + $this->buildAutoloader(); + $this->loadConfig(); + + try { + Router::routeTo(); + } catch ( InvalidRouteException $e ){ + die( $e->getMessage() ); + } + } + + /** + * Permet de mettre en place l'Autoloader. + * @return void + */ + private function buildAutoloader(): void { + require_once 'Helpers/AutoLoader.php'; + AutoLoader::register(); + } + + /** + * Permet de charger tous les fichiers de configuration du site. + * @return void + */ + private function loadConfig(): void { + try { + self::$configs['general'] = ConfigFactory::loadConfigFile('general'); + } catch( ConfigFailedLoadingException $e ){ + die( $e->getMessage() ); + } + } + +} \ No newline at end of file -- 2.39.5 From 43eb93653292bac1de99016317880d5491da1c23 Mon Sep 17 00:00:00 2001 From: Benjamin Date: Fri, 20 Mar 2026 13:59:34 +0100 Subject: [PATCH 2/2] Fix Router and finish base view system. --- config/route_arguments.php | 6 + config/views.php | 4 + src/Domain/Pages/PagesController.php | 9 +- src/Exceptions/InvalidViewException.php | 13 ++ src/Http/Router.php | 34 +++-- src/Infrastructure/View.php | 194 ++++++++++++++++++++++++ src/Kernel.php | 4 + views/base.php | 7 + views/home.php | 1 + views/partials/footer.php | 4 + views/partials/header.php | 9 ++ 11 files changed, 267 insertions(+), 18 deletions(-) create mode 100644 config/route_arguments.php create mode 100644 config/views.php create mode 100644 src/Exceptions/InvalidViewException.php create mode 100644 src/Infrastructure/View.php create mode 100644 views/base.php create mode 100644 views/home.php create mode 100644 views/partials/footer.php create mode 100644 views/partials/header.php diff --git a/config/route_arguments.php b/config/route_arguments.php new file mode 100644 index 0000000..88b0198 --- /dev/null +++ b/config/route_arguments.php @@ -0,0 +1,6 @@ + '([0-9]+)', + '{chars}' => '([A-Za-z]+)', + '{string}' => '([0-9A-Za-z]+)', +]; \ No newline at end of file diff --git a/config/views.php b/config/views.php new file mode 100644 index 0000000..28e6f05 --- /dev/null +++ b/config/views.php @@ -0,0 +1,4 @@ + 'base' +]; \ No newline at end of file diff --git a/src/Domain/Pages/PagesController.php b/src/Domain/Pages/PagesController.php index b8da0c1..33e47db 100644 --- a/src/Domain/Pages/PagesController.php +++ b/src/Domain/Pages/PagesController.php @@ -3,6 +3,7 @@ namespace App\Domain\Pages; use App\Domain\Controller; +use App\Infrastructure\View; /** * Controller pour les pages sans lien avec un contenu spécifique. @@ -14,7 +15,7 @@ class PagesController extends Controller { { return [ self::Route( routeUrl: '/', routeName: 'Homepage', routeAction: 'index' ), - self::ROute( routeUrl: '/test/{int}', routeName: 'test', routeAction: 'test' ), + self::Route( routeUrl: '/test/{string}/baba', routeName: 'test', routeAction: 'test' ), ]; } @@ -23,11 +24,11 @@ class PagesController extends Controller { * Route de la page d'accueil. * @return void */ - public function index(): void { - echo "Coucou"; + public function index(): View { + return new View( 'home', [ 'ok' => 'bla' ] ); } - public function test( int $id ): void { + public function test( string $id ): void { echo "Coucou" . $id; } diff --git a/src/Exceptions/InvalidViewException.php b/src/Exceptions/InvalidViewException.php new file mode 100644 index 0000000..9447cde --- /dev/null +++ b/src/Exceptions/InvalidViewException.php @@ -0,0 +1,13 @@ +routeUrl === self::$clientRouteString ){ + $routeName = self::getRegexRoute( $route ); + if( preg_match( $routeName, $clientRouteName, $matches ) ){ + array_shift( $matches ); + self::$clientRouteArguments = $matches; return $route; } - */ } return false; @@ -139,20 +143,22 @@ final class Router { } private static function getRegexRoute( Route $route ): string { - $routeUrl = $route->routeUrl; - $routeUrl = str_replace( "{int}", "([0-9]+)", $routeUrl ); - return $routeUrl; + $routeUrl = trim( $route->routeUrl, '/' ); + foreach ( Kernel::$configs['route_arguments'] as $key => $value ){ + $routeUrl = str_replace( $key, $value, $routeUrl ); + } + return "#^{$routeUrl}$#"; } /** - * Va permettre d'exécuter la méthode du Controller->action() en parsant les arguments si existants. + * Va permettre d'exécuter la méthode du Controller→action() en parsant les arguments si existants. * @return void */ private static function executeRouteAction(): void { $controller = self::$clientRoute->routeController; $method = self::$clientRoute->routeAction; - new $controller()->$method(); + new $controller()->$method( ...self::$clientRouteArguments); } } \ No newline at end of file diff --git a/src/Infrastructure/View.php b/src/Infrastructure/View.php new file mode 100644 index 0000000..2edf666 --- /dev/null +++ b/src/Infrastructure/View.php @@ -0,0 +1,194 @@ + + */ + public private(set) array $integratedContent; + + /** + * Garde la dernière instance de vue. + * Si il y a un squelette, ce sera l'instance du squelette. + * + * @var View + */ + private static self $lastInstance; + + /** + * Permet de construire les informations de la vue. + * + * @param string $viewName Le nom du fichier de vue. + * @param array $viewArgs Les arguments de la vue. + * @param bool $autoRender Si la méthode $this->render() est automatiquement exécuté. + * @param string|null $skeleton Le squelette du fichier, null correspond à aucun squelette, default correspond au + * squelette défini dans la configuration des vues. + * @param array $integratedContent Le contenu intégré dans le nouveau squelette. + */ + public function __construct( + string $viewName, + array $viewArgs = [], + bool $autoRender = true, + ?string $skeleton = 'default', + array $integratedContent = [] + ) { + + if( !str_ends_with( $viewName, '.php' ) ) { + $viewName .= '.php'; + } + + $this->viewName = $viewName; + $this->viewArgs = $viewArgs; + + $this->skeleton = $skeleton; + if( $this->skeleton === "default" ){ + $this->skeleton = Kernel::$configs['views']['base_view_skeleton']; + } + + $this->integratedContent = $integratedContent; + + try { + if (!file_exists(self::VIEW_PATH . $this->viewName)) { + throw new InvalidViewException($this->viewName); + } + + if ($autoRender) { + $this->render(); + } + } catch (InvalidViewException $e) { + die( $e->getMessage() ); + } + + } + + /** + * Permet de démarrer le rendu d'une vue. + * @return void + */ + public function startView(): void { + ob_start(); + } + + /** + * Permet de finir le rendu d'une vue. + * @return void + */ + public function endView(): void { + echo ob_get_clean(); + } + + /** + * Permet de faire le rendu proprement de la vue. + * @return void + */ + public function render(): void { + + self::$lastInstance = $this; + + if( $this->skeleton === null ){ + + // Si on a pas de squelette, on inclut la vue. + // pour accéder aux éléments de la vue, on utilise les méthodes statiques de cette classe. + $this->startView(); + require self::VIEW_PATH . $this->viewName; + $this->endView(); + + } else { + + // Si on a un squelette, on charge tout le contenu de la vue enfante. + $content = file_get_contents( self::VIEW_PATH . $this->viewName ); + + // On démarre la vue du squelette. + $base = new View( $this->skeleton, $this->viewArgs, skeleton: null, integratedContent: [ 'content' => $content ] ); + + } + + + } + + /* + * VIEW TOOLS + */ + + /** + * Permet d'injecter un contenu sauvegardé dans $this->integratedContent. + * + * @param string $integratedContentName + * @return void N'affiche rien si le contenu n'existe pas. + */ + public static function inject( string $integratedContentName ): void { + if( isset( self::$lastInstance->integratedContent[ $integratedContentName ] ) ) + echo self::$lastInstance->integratedContent[ $integratedContentName ]; + } + + /** + * Permet de récupérer un argument passé à la vue. + * + * @param string $argumentName + * @return mixed null si l'argument n'existe pas. + */ + public static function arg( string $argumentName ): mixed { + if( isset( self::$lastInstance->viewArgs[ $argumentName ] ) ) + return self::$lastInstance->viewArgs[ $argumentName ]; + return null; + } + + /** + * Permet d'intégrer un bloc à la vue. + * + * @param string $partialName + * @return void N'affiche rien si le bloc n'existe pas. + */ + public static function partial( string $partialName ): void { + + if( !str_ends_with( $partialName, '.php' ) ) { + $partialName .= '.php'; + } + + if( file_exists( self::VIEW_PATH . 'partials/' . $partialName ) ) { + require self::VIEW_PATH . 'partials/' . $partialName; + } + } + + public static function getHeadTitle(): string { + $siteUrl = Kernel::$configs['general']['website_name']; + return Router::$clientRoute->routeName . ' - ' . $siteUrl; + } + +} \ No newline at end of file diff --git a/src/Kernel.php b/src/Kernel.php index 163d0cb..01a8013 100644 --- a/src/Kernel.php +++ b/src/Kernel.php @@ -84,7 +84,11 @@ final class Kernel { */ private function loadConfig(): void { try { + self::$configs['general'] = ConfigFactory::loadConfigFile('general'); + self::$configs['route_arguments'] = ConfigFactory::loadConfigFile('route_arguments'); + self::$configs['views'] = ConfigFactory::loadConfigFile('views'); + } catch( ConfigFailedLoadingException $e ){ die( $e->getMessage() ); } diff --git a/views/base.php b/views/base.php new file mode 100644 index 0000000..c7aef16 --- /dev/null +++ b/views/base.php @@ -0,0 +1,7 @@ + + + + + + + diff --git a/views/home.php b/views/home.php new file mode 100644 index 0000000..eda17ba --- /dev/null +++ b/views/home.php @@ -0,0 +1 @@ +

Coucou

\ No newline at end of file diff --git a/views/partials/footer.php b/views/partials/footer.php new file mode 100644 index 0000000..da78eac --- /dev/null +++ b/views/partials/footer.php @@ -0,0 +1,4 @@ + + + + diff --git a/views/partials/header.php b/views/partials/header.php new file mode 100644 index 0000000..150db74 --- /dev/null +++ b/views/partials/header.php @@ -0,0 +1,9 @@ + + + + + + <?php echo V::getHeadTitle(); ?> + + + -- 2.39.5