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/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/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..33e47db --- /dev/null +++ b/src/Domain/Pages/PagesController.php @@ -0,0 +1,35 @@ + 'bla' ] ); + } + + public function test( string $id ): void { + echo "Coucou" . $id; + } + +} \ No newline at end of file diff --git a/src/Exceptions/ConfigFailedLoadingException.php b/src/Exceptions/ConfigFailedLoadingException.php new file mode 100644 index 0000000..aea3887 --- /dev/null +++ b/src/Exceptions/ConfigFailedLoadingException.php @@ -0,0 +1,28 @@ +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/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 = $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..3c04833 --- /dev/null +++ b/src/Http/Router.php @@ -0,0 +1,164 @@ +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 { + + $clientRouteName = trim( self::$clientRouteString, '/' ); + foreach( self::$routes as $route ){ + $routeName = self::getRegexRoute( $route ); + if( preg_match( $routeName, $clientRouteName, $matches ) ){ + array_shift( $matches ); + self::$clientRouteArguments = $matches; + return $route; + } + } + + return false; + + } + + private static function getRegexRoute( Route $route ): string { + $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. + * @return void + */ + private static function executeRouteAction(): void { + $controller = self::$clientRoute->routeController; + $method = self::$clientRoute->routeAction; + + 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 new file mode 100644 index 0000000..01a8013 --- /dev/null +++ b/src/Kernel.php @@ -0,0 +1,97 @@ + [ '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'); + self::$configs['route_arguments'] = ConfigFactory::loadConfigFile('route_arguments'); + self::$configs['views'] = ConfigFactory::loadConfigFile('views'); + + } catch( ConfigFailedLoadingException $e ){ + die( $e->getMessage() ); + } + } + +} \ No newline at end of file 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(); ?> + + +