5 Commits
master ... Aco3

Author SHA1 Message Date
Aco
5c3de7ccf3 ajout login dans UtilisateursRepository et implémentation dans AuthentificationController atuh 2026-04-03 13:17:48 +02:00
Aco
59b9829feb ajoutUtilisateur 2026-04-03 12:23:35 +02:00
Aco
7b886fef86 ajoutUtilisateurs 2026-04-03 12:21:09 +02:00
Aco
b1f3806983 ajoutUtilisateurs 2026-04-03 12:09:48 +02:00
Aco
455eee67f7 projetAcoBDDv30.03.26 2026-03-30 16:10:26 +02:00
50 changed files with 3054 additions and 1 deletions

View File

@@ -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
<?php return [
'cle_1' => "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()``.

9
config/database.php Normal file
View File

@@ -0,0 +1,9 @@
<?php
return [
'host' => 'localhost',
'port' => 3306,
'user' => 'admin',
'pass' => 'adminpass123',
'name' => 'siterecette'
];

8
config/general.php Normal file
View File

@@ -0,0 +1,8 @@
<?php
return [
'website_url' => 'http://127.0.0.1:8080/',
'website_name' => 'Les recettes des Papis',
'website_path' => APP_ROOT,
];

View File

@@ -0,0 +1,6 @@
<?php
return [
'{int}' => '([0-9]+)',
'{chars}' => '([A-Za-z]+)',
'{string}' => '([0-9A-Za-z]+)',
];

4
config/views.php Normal file
View File

@@ -0,0 +1,4 @@
<?php
return [
'base_view_skeleton' => 'base'
];

462
public/assets/css/style.css Normal file
View File

@@ -0,0 +1,462 @@
@import url('https://fonts.googleapis.com/css2?family=Roboto:ital,wght@0,100..900;1,100..900&display=swap');
/* Sommaire :
- body et html
- Header et ses contenues
- Body contenue
- Content
- Page de présentation des recettes
- Page d'une recette
- Footer
*/
/*Tout*/
html, body{
height: 100%;
text-align: center;
/*font-size: 98%;*/
font-family: "Roboto", sans-serif;
}
/*Header et son contenu*/
#header {
display: flex;
flex-direction: row;
padding: 3px;
padding-left: 2%;
padding-bottom: 10px;
background-color: blanchedalmond;
}
#logo {
background-color: aqua;
background-image: url("Logo.jpg");
/*background:no-repeat;*/
height: 103px;
width: 141px;
}
.logo{
height: 100%;
width: 100%;
border: 1px white solid;
border-radius: 4px;
box-shadow: 1px 1px 1px black;
}
.logo:hover{
box-shadow: 1px 1px 2px #0bbd90;
border: 1px solid #0bbd90;
}
nav {
flex: 9;
text-align: initial;
letter-spacing: 2px;
padding: 10px;
}
.nav-list {
list-style-type: none;
margin: 0;
padding: 0;
background-color: rgb(255, 217, 160);
display: flex;
border: 1px solid rgba(0,0,0,.125);
border-radius: 40px;
}
.nav-element {
line-height: 2.5;
padding: 10px;
text-decoration: none;
color: black;
}
.nav-list{
li:nth-of-type(3){
position: fixed;
right: 25px;
}
}
#login {
border: 1px solid black;
border-radius: 6px;
background-color: #ffc478;
}
#login:hover {
background-color: #0bbd90;
}
/*Body et son contenu */
body {
background-color: blanchedalmond;
display: flex;
flex-direction: column;
margin: 0;
padding: 0;
height: 100%;
}
.sidebar {
background-color: #caecd6;
border: 1px solid rgba(0,0,0,.125);
border-radius: 6px;
width: 128px;
display: flex;
flex-direction: column;
overflow: scroll;
}
.tag-cont {
/*display:none;*/
}
.ingr-cont {
/*display:none;*/
}
.tag-selected-div {
flex: 4;
}
.tag-unselected-div {
flex: 4;
}
.sidebar-search {
flex: 1;
}
.search-form-tag {
width: 85%;
}
.search-form-recette {
width: 100%;
height: 25px;
}
.search-form {
border: 1px solid black;
border-radius: 20px;
box-shadow: 0px 0px 10px rgb(131, 131, 131);
}
nav {
display: flex;
flex-direction: column;
justify-content: space-around;
}
.tag {
border: 1px solid black;
border-radius: 10px;
width: 95%;
max-width: calc(128px*0.95);
box-shadow: 1px 1px 1px black;
}
.tag-selected {
background-color: #ffa04d;
}
.tag-selected:hover {
background-color: white;
}
.tag-unselected {
background-color: white;
}
.tag-unselected:hover {
background-color: #ffa04d;
}
#hr {
border: 1px solid #0bbd90;
width: 93%;
box-shadow: 1px 3px 5px #0bbd90;
}
.main-body {
display: flex;
flex-direction: row;
flex: 1;
background-clip: border-box;
padding-right: 2%;
padding-left: 2%;
overflow: hidden;
}
/* content : le cadre de base du centre de notre site
*/
.content {
background-color: #ffe4bb;
text-align: left;
width: 100%;
height: 100%;
border: 1px solid rgba(0,0,0,.125);
border-radius: 6px;
overflow: scroll;
}
/*Recettes :
représente le contenue centrale de la page.
*/
.recettes {
display: grid;
grid-template-columns: repeat(3, 1fr);
grid-gap: 10px;
/* grid-auto-rows: minmax(300px, auto);
grid-auto-columns: auto;*/
padding: 10px;
}
@media screen and (max-width : 1200px) {
.recettes {
grid-template-columns: repeat(2, 1fr);
}
}
@media screen and (max-width : 800px) {
.recettes {
grid-template-columns: repeat(1, 1fr);
}
}
@media screen and (max-width : 600px) {
.sidebar {
display: none;
}
}
ul {
list-style-type: none;
margin-left: 0px;
text-align: center;
padding : 0px;
}
/* Classe recette icone :
C'est la classe de chaques éléments de notre liste de recette
*/
.recette-icone{
display: flex;
border: 3px solid rgba(0,0,0,.125);
border-radius: 40px;
text-align: left;
padding: 15px;
vertical-align: middle;
box-shadow: 3px 5px 5px black;
max-width: 400px;
text-decoration: none;
color: black;
}
.recette-icone:hover{
border-color: #0bbd90;
}
.recette-icone-content{
display: flex;
flex-direction: column;
padding-left: 10px;
}
.recette-preview-image{
max-width: calc( 5vh + 5vw );
max-height: calc( 5vh + 5vw );
border: 1px solid rgb(252, 223, 57);
border-radius: 25px;
}
/*Ici commence le CSS pour la page de chaques recettes */
/*contenue*/
.recette-content {
display: flex;
flex-direction: column;
}
/*titre*/
.recette-title {
text-align: center;
}
/*image*/
.recette-image {
background-color: red;
aspect-ratio: 1/1;
max-height: calc( 30vh + 30vw );
max-width: calc( 30vh + 30vw );
border: 1px solid white;
border-radius: 30px;
}
.recette-div-image {
display: flex;
justify-content: center;
}
.recette-div-info {
display: flex;
flex-direction: row;
padding: 20px;
}
.recette-div-liste-info{
padding: 30px;
margin: 10px;
width: 100%;
flex:1;
}
.recette-list-buttons {
display: flex;
flex-direction: row-reverse;
}
.recette-button {
border: 1px solid black;
border-radius: 10px;
padding : 10px;
box-shadow: 1px 1px 1px black;
background-color: blanchedalmond;
}
.recette-button:hover{
background-color: rgb(243, 215, 174);
}
.recette-liste-info-elem {
text-align: left;
padding: 4px;
padding-top: 6px;
padding-bottom: 6px;
color: #535353bd;
border-right: 1px solid #535353bd;
border-left: 1px solid #535353bd;
}
.recette-liste-info {
display: flex;
flex-direction: row;
}
.recette-liste-tag {
display: flex;
flex-wrap: wrap; /*trouvé à l'aide de ChatGPT car je n'arrivais pas à trouver*/
}
/*description*/
.recette-desc {
padding : 30px;
}
/*Login form*/
.formcont {
flex: 1;
display: flex;
justify-content: center;
align-items: center;
}
.formcont form {
width: 50vmin;
height: 40vmin;
border: 2px solid rgb(0, 0, 0);
padding: 20px;
display: flex;
flex-direction: column;
gap: 1.5vmin;
background:white;
border-radius: 12px;
box-shadow: 5px 5px 30px #555;
}
.formcont form .form-group {
width: 100%;
display: flex;
flex-direction: column;
align-items: center;
padding: 1.2vmin;
padding-left:0px;
}
.formcont form .form-group label {
width: 95%;
text-align: center;
color:black;
font-size:5vmin;
}
.formcont form .form-group input {
width: 95%;
box-sizing: border-box;
font-size:3vmin;
padding:1vmin;
}
.formcont form button {
width: 48%;
height: 19%;
padding:0.5vmin;
box-sizing: border-box;
margin: 0 auto;
background: #ffa04d;
color: black;
font-size: 4vmin;
border-radius: 7px;
box-shadow: 2px 2px 0px #000000;
}
.btn:hover {
background-color: #0bbd90;
}
.form-control {
border-radius: 10px;
box-shadow: 3px 4px 5px #8c8c8c;
}
.recette-form-div-desc {
height: 90%;
}
.recette-form {
height: 100%;
width: 100%;
margin: 10px;
padding: 10px;
}
.recette-form-group {
display: flex;
flex-direction: column;
}
#recette-form-div-desc {
height: 400px;
}
#recette-form-description {
height: 95%;
}
/*Footer et son contenue*/
footer{
padding: 25px;
background: rgb(11, 189, 144);
color: white;
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 141 KiB

66
public/assets/js/login.js Normal file
View File

@@ -0,0 +1,66 @@
document.addEventListener( 'DOMContentLoaded', function(){
const FORM = document.getElementById( 'login-form' );
const FORM_ERROR = document.getElementById( 'login-errors' );
if( !FORM )
return;
/**
* Permet d'afficher une erreur dans le formulaire.
* @param message
*/
FORM.showError = function( message ){
FORM_ERROR.style.display = 'block';
FORM_ERROR.innerText = message;
}
/**
* Permet de retirer l'erreur du formulaire.
*/
FORM.removeError = function(){
FORM_ERROR.style.display = 'none';
FORM_ERROR.innerText = '';
}
/**
* Gère l'envoi du formulaire.
* @param e
* @returns {boolean}
*/
FORM.onsubmit = function( e ){
e.preventDefault();
FORM.removeError();
let userField = e.target.username;
let passwordField = e.target.password;
if( !userField || !passwordField || userField.value === "" || passwordField.value === "" ){
FORM.showError( "Le nom d'utilisateur ou le mot de passe ne doivent pas être vide.");
return false;
}
let formData = new FormData( FORM );
let options = {
method: "POST",
contentType: "application/json",
body: formData,
}
fetch( FORM.action, options ).then( ( response ) => {
if( response.ok ){
response.json().then( ( responseJSON ) => {
if( responseJSON.success === true ){
window.location.href = window.location.origin; // Redirection sur la page d'accueil si succès.
} else {
FORM.showError( responseJSON.message || "Mauvais nom d'utilisateur ou mauvais mot de passe." );
}
})
}
})
}
})

13
public/index.php Normal file
View File

@@ -0,0 +1,13 @@
<?php
use App\Kernel;
if( !defined( 'APP_ROOT' ) )
/**
* Définit la racine de l'application en chemin absolue.
*/
define( 'APP_ROOT', realpath( __DIR__ . '/../') . '/' );
require_once '../src/Kernel.php';
Kernel::start();

41
src/Domain/Controller.php Normal file
View File

@@ -0,0 +1,41 @@
<?php
namespace App\Domain;
use App\Http\Route;
/**
* Classe abstraite des controllers qui vont pouvoir permettre de gérer les routes et leur retour.
*/
abstract class Controller {
/**
* Va permettre de créer une route en préremplissant certains arguments.
* Pour les paramètres, veuillez voir le constructeur d'une Route.
*
* @param mixed ...$args
* @return Route
*
* @see Route::__construct()
*/
public static function Route( ...$args ): Route {
$defaults = [
'routeController' => static::class,
'routeMethods' => [ 'GET' ],
'pageHeadTitle' => 'Page',
];
$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;
}

View File

@@ -0,0 +1,33 @@
<?php
namespace App\Domain\Ingredients;
use App\Domain\Model;
/**
* Classe qui va gérer les ingrédients.
*/
class Ingredient extends Model {
/**
* Le numéro de l'ingrédient.
* @var int
*/
public int $num_ingredient;
/**
* Le nom de l'ingrédient
* @var string
*/
public string $nom_ingredient;
/**
* Retourne le numéro de l'ingrédient.
* @return int
*/
public function getID(): int
{
return $this->num_ingredient;
}
}

View File

@@ -0,0 +1,175 @@
<?php
namespace App\Domain\Ingredients;
use App\Domain\LinkableInterface;
use App\Domain\Recettes\Recette;
use App\Domain\Repository;
use App\Domain\Model;
use App\Domain\Tags\Tag;
use App\Kernel;
/**
* Classe qui va permettre de gérer les requêtes BDD en lien avec les ingrédients.
* Les ingrédients sont liables à d'autres entités comme les Recettes.
*/
class IngredientRepository extends Repository implements LinkableInterface {
/**
* Permet d'avoir le nom de l'entité dont dépend ce repo.
* @return string
*/
public static function getEntity(): string
{
return Ingredient::class;
}
/**
* La structure de notre table dans la BDD.
* Un champ link_recettes a été ajouté pour donner le nom de la table de lien entre nos recettes.
*
* @return array
*/
public static function getStructure(): array
{
return [
'table' => 'Ingredient',
'columns' => [
'num_ingredient', 'nom_ingredient'
],
'link_recettes' => 'Listeingredient'
];
}
/**
* Permet d'obtenir une liste de tous les ingrédients objet Ingreident.
*
* @return Ingredient[]|null
*/
public function getAll(): ?array {
$sqlQuery = "SELECT * FROM {$this->tableName};";
$results = $this->selectGetAll($sqlQuery);
if( $results === null )
return null;
return $results;
}
/**
* Permet d'obtenir un ingrédient spécifique par son ID.
*
* @param int $id
*
* @return Ingredient|null
*/
public function getByID( int $id ): ?Ingredient {
$sqlQuery = "SELECT * FROM {$this->tableName} WHERE num_ingredient = {$id}";
$results = $this->selectGetAll($sqlQuery);
if( $results === null || count( $results ) > 1 )
return null;
return $results[0];
}
/**
* Permet d'obtenir, sous forme de liste, toutes les entrées qui lient des ingrédients.
*
* @param string $linkedTo La table qui permet de faire la liaison des ingrédients avec une autre entité.
* @param string $linkingField Le champ qui permet de faire la liaison des ingrédients avec une autre entité.
* @param Model $linkedEntity L'autre entité.
*
* @see LinkableInterface
* @return array|null
*/
public function getIdLinkedTo( string $linkedTo, string $linkingField, Model $linkedEntity ): ?array {
$linkName = 'link_' . $linkedTo;
if( !isset( $this->globalStructure[$linkName]))
return null;
$sqlQuery = "SELECT * FROM {$this->globalStructure[$linkName]} WHERE {$linkingField} = {$linkedEntity->getId()};";
$results = $this->selectGetAll($sqlQuery, true);
if( $results === null )
return null;
return $results;
}
/**
* Permet d'ajouter un lien entre un ingrédient et une autre entité comme Recette.
*
* @param string $linkedTo
* @param string $linkingField
* @param Model $linkedEntity
* @param Model $ingredientEntity
*
* @see LinkableInterface
* @return bool
*/
public function addLinkBetween( string $linkedTo, string $linkingField, Model $linkedEntity, Model $ingredientEntity ): bool {
$linkName = 'link_' . $linkedTo;
if( !isset( $this->globalStructure[$linkName]))
return false;
$query = "INSERT INTO {$this->globalStructure[$linkName]} ({$linkingField},num_ingredient) VALUES ({$linkedEntity->getId()}, {$ingredientEntity->getID()});";
$statement = Kernel::$DB->pdo->prepare( $query );
return $statement->execute();
}
/**
* Retire un lien dans la BDD entre un ingrédient et une autre entité.
*
* @param string $linkedTo
* @param string $linkingField
* @param Model $linkedEntity
* @param Model $ingredientEntity
*
* @see LinkableInterface
* @return bool
*/
public function removeLinkBetween(string $linkedTo, string $linkingField, Model $linkedEntity, Model $ingredientEntity ): bool
{
$linkName = 'link_' . $linkedTo;
if( !isset( $this->globalStructure[$linkName]))
return false;
$query = "DELETE FROM {$this->globalStructure[$linkName]} WHERE {$linkingField} = {$linkedEntity->getId()} AND num_ingredient = {$ingredientEntity->getId()};";
$statement = Kernel::$DB->pdo->prepare( $query );
return $statement->execute();
}
/**
* Ajouter un ingrédient dans la BDD.
*
* @param Model $ingredient
*
* @return bool
*/
public function add( Model $ingredient ): bool {
return $this->addEntity( $ingredient );
}
/**
* Met à jour un ingrédient dans la BDD.
*
* @param Model $ingredient
*
* @return bool
*/
public function update( Model $ingredient ): bool {
return $this->updateEntity( $ingredient, 'num_ingredient' );
}
/**
* Supprime un ingrédient de la BDD.
*
* @param Model $ingredient
*
* @return bool
*/
public function delete( Model $ingredient ): bool {
return $this->deleteEntity( $ingredient, 'num_ingredient' );
}
}

View File

@@ -0,0 +1,38 @@
<?php
namespace App\Domain\Ingredients;
use App\Domain\Model;
/**
* Interface utilisée par tous les repository qui sont en lien avec les ingrédients.
*/
interface UseIngredientsInterface {
/**
* Permet de récupérer tous les ingrédients liés à ce Modèle.
* @return array|null
*/
public function getAllLinkedIngredients( Model $entity ): ?array;
/**
* Permet d'ajouter un ingrédient en lien avec notre autre entité.
*
* @param Ingredient $ingredient
* @param Model $entity
*
* @return bool
*/
public function addAnIngredient( Ingredient $ingredient, Model $entity ): bool;
/**
* Permet de retirer un lien entre un ingrédient et une autre entité.
*
* @param Ingredient $ingredient
* @param Model $entity
*
* @return bool
*/
public function removeAnIngredient( Ingredient $ingredient, Model $entity ): bool;
}

View File

@@ -0,0 +1,51 @@
<?php
namespace App\Domain;
/**
* Interface pour dire qu'un objet peut avoir un lien avec un autre objet par une table.
* Il s'agit d'une interface qui s'utilise sur des repositories de meta-données (Tag, Ingrédients)
*
* Les champs de toutes les méthodes se présentent de la même façon.
* $linkedTo : Champ qui va désigner le nom de la table de liens que vous avez dans getStructure().
* $linkingField : Champ dans la BDD qui va permettre de lier l'élément courant et l'étranger.
* $linkedEntity : Une instance de l'entité étrangère que l'on veut lier.
*/
interface LinkableInterface {
/**
* Permet de récupérer tous les liens entre notre entité implémentée et notre entité étrangère.
*
* @param string $linkedTo
* @param string $linkingField
* @param Model $linkedEntity
*
* @return array|null
*/
public function getIdLinkedTo( string $linkedTo, string $linkingField, Model $linkedEntity ): ?array;
/**
* Permet d'ajouter un lien entre notre entité implémentée et notre entité étrangère.
*
* @param string $linkedTo
* @param string $linkingField
* @param Model $linkedEntity
* @param Model $entity
*
* @return bool
*/
public function addLinkBetween( string $linkedTo, string $linkingField, Model $linkedEntity, Model $entity ): bool;
/**
* Permet de retirer un lien entre notre entité implémentée et notre entité étrangère.
*
* @param string $linkedTo
* @param string $linkingField
* @param Model $linkedEntity
* @param Model $entity
*
* @return bool
*/
public function removeLinkBetween( string $linkedTo, string $linkingField, Model $linkedEntity, Model $entity ): bool;
}

16
src/Domain/Model.php Normal file
View File

@@ -0,0 +1,16 @@
<?php
namespace App\Domain;
/**
* Classe abstraite pour gérer l'implémentation des entités de données.
*/
abstract class Model {
/**
* Getter de l'ID primaire du model spécifique.
* @return int
*/
abstract public function getID(): int;
}

View File

@@ -0,0 +1,34 @@
<?php
namespace App\Domain\Pages;
use App\Domain\Controller;
use App\Infrastructure\View;
/**
* Controller pour les pages sans lien avec un contenu spécifique.
* Exemple : Page d'accueil.
*/
class PagesController extends Controller {
/**
* Définit les routes globales d'acceuil.
* @return array|\App\Http\Route[]
*/
public static function defineRoutes(): array
{
return [
self::Route( routeUrl: '/', routeName: 'home', routeAction: 'index', pageHeadTitle: "Home Page" ),
];
}
/**
* Route de la page d'accueil.
* @return void
*/
public function index(): View {
return new View( 'home', [ 'ok' => 'bla' ] );
}
}

View File

@@ -0,0 +1,52 @@
<?php
namespace App\Domain\Recettes;
use App\Domain\Ingredients\Ingredient;
use App\Domain\Ingredients\IngredientRepository;
use App\Domain\Ingredients\UseIngredientsInterface;
use App\Domain\Model;
use App\Helpers\Markdown;
/**
* Classe qui va permettre de gérer tous les objets recette.
*/
class Recette extends Model {
public int $num_recette;
public string $titre_recette;
public string $slug;
public string $description_recette;
public string $photo;
public string $publication_date;
public int $temps_de_preparation;
public function getID(): int
{
return $this->num_recette;
}
/**
* Convertit la description de Markdown à HTML.
* @return string
*/
public function getHTMLDescription(): string {
return Markdown::convertToHTML( $this->description_recette );
}
/**
* Récupère une liste de tous les ingrédients liés à la recette.
* @return Ingredient[]|null
*/
public function getAllLinkedIngredients(): ?array
{
return new RecetteRepository()->getAllLinkedIngredients( $this );
}
public function getNumberOfIngredients(): int {
$response = $this->getAllLinkedIngredients();
return $response !== null ? count( $response ) : 0;
}
}

View File

@@ -0,0 +1,162 @@
<?php
namespace App\Domain\Recettes;
use App\Domain\Ingredients\Ingredient;
use App\Domain\Ingredients\IngredientRepository;
use App\Domain\Ingredients\UseIngredientsInterface;
use App\Domain\Model;
use App\Domain\Repository;
use App\Domain\Tags\Tag;
use App\Domain\Tags\TagRepository;
use App\Domain\Tags\UseTagsInterface;
/**
* Classe qui permet de faire le lien entre la BDD et le site pour les recettes.
* Les recettes sont en lien avec les ingrédients.
*/
class RecetteRepository extends Repository implements UseIngredientsInterface, UseTagsInterface {
public static function getEntity(): string
{
return Recette::class;
}
public static function getStructure(): array
{
return [
'table' => 'Recette',
'columns' => [
'num_recette', 'titre_recette', 'slug', 'description_recette', 'photo', 'publication_date', 'temps_de_preparation'
]
];
}
/**
* Permet d'obtenir une liste de toutes les recettes objet Recette.
*
* @return Recette[]|null
*/
public function getAll(): ?array {
$sqlQuery = "SELECT * FROM {$this->tableName};";
$results = $this->selectGetAll($sqlQuery);
if( $results === null )
return null;
return $results;
}
/**
* Permet d'obtenir toutes les recettes paginées.
* Mise par défaut dans l'ordre croissant du titre et avec une pagination de 15 éléments.
*
* @param int $page
* @param int $pagination
*
* @return array|null
*/
public function getAllRecettesBrowse( int $page = 1, int $pagination = 15 ){
if( $page <= 0 )
$page = 1;
$offset = ( $page - 1 ) * $pagination;
$sqlQuery = "SELECT * FROM {$this->tableName} ORDER BY titre_recette ASC LIMIT {$pagination} OFFSET {$offset};";
$results = $this->selectGetAll($sqlQuery);
if( $results === null )
return null;
return $results;
}
/**
* Permet d'avoir une recette par un ID.
*
* @param int $id
* @return Recette|null
*/
public function getByID( int $id ): ?Recette {
$sqlQuery = "SELECT * FROM {$this->tableName} WHERE num_recette = {$id}";
$results = $this->selectGetAll($sqlQuery);
if( $results === null || count( $results ) > 1 )
return null;
return $results[0];
}
/**
* Permet d'avoir une recette par un slug.
*
* @param string $slug
* @return Recette|null
*/
public function getBySlug( string $slug ): ?Recette {
$sqlQuery = "SELECT * FROM {$this->tableName} WHERE slug = {$slug}";
$results = $this->selectGetAll($sqlQuery);
if( $results === null || count( $results ) > 1 )
return null;
return $results[0];
}
public function add( Model $recette ): bool {
return $this->addEntity( $recette );
}
public function update( Model $recette ): bool {
return $this->updateEntity( $recette, 'num_recette' );
}
public function delete( Model $recette ): bool {
return $this->deleteEntity( $recette, 'num_recette' );
}
public function getAllLinkedIngredients(Model $entity): ?array
{
$ingredientRepo = new IngredientRepository();
$response = $ingredientRepo->getIdLinkedTo( 'recettes', 'num_recette', $entity );
if( $response === null )
return null;
return array_map( function($arr) use($ingredientRepo) {
return $ingredientRepo->getByID( $arr['num_ingredient'] );
}, $response );
}
public function addAnIngredient(Ingredient $ingredient, Model $entity): bool
{
$ingredientRepo = new IngredientRepository();
return $ingredientRepo->addLinkBetween( 'recettes', 'num_recette', $entity, $ingredient );
}
public function removeAnIngredient(Ingredient $ingredient, Model $entity): bool
{
$ingredientRepo = new IngredientRepository();
return $ingredientRepo->removeLinkBetween( 'recettes', 'num_recette', $entity, $ingredient );
}
public function getAllLinkedTags(Model $entity): ?array
{
$tagRepo = new TagRepository();
$response = $tagRepo->getIdLinkedTo( 'recettes', 'num_recette', $entity );
if( $response === null )
return null;
return array_map( function($arr) use($tagRepo) {
return $tagRepo->getByID( $arr['num_tag'] );
}, $response );
}
public function addATag(Tag $tag, Model $entity): bool
{
$tagRepo = new TagRepository();
return $tagRepo->addLinkBetween( 'recettes', 'num_recette', $entity, $tag );
}
public function removeATag(Tag $tag, Model $entity): bool
{
$tagRepo = new TagRepository();
return $tagRepo->removeLinkBetween( 'recettes', 'num_recette', $entity, $tag );
}
}

View File

@@ -0,0 +1,16 @@
<?php
namespace App\Domain\Recettes;
use App\Domain\Controller;
class RecettesAPIController extends Controller {
public static function defineRoutes(): array
{
return [
self::Route( routeUrl: '/api/recettes/list', routeName: 'api->recettes->list', routeAction: 'list', routeMethods: ['POST'] ),
];
}
}

View File

@@ -0,0 +1,36 @@
<?php
namespace App\Domain\Recettes;
use App\Domain\Controller;
use App\Domain\Ingredients\IngredientRepository;
use App\Domain\Tags\TagRepository;
use App\Http\JSONResponse;
use App\Http\Request;
use App\Infrastructure\View;
class RecettesController extends Controller {
public static function defineRoutes(): array
{
return [
self::Route( routeUrl: '/recettes', routeName: 'recettes->index', routeAction: 'index', pageHeadTitle: 'Liste des recettes' ),
self::Route( routeUrl: '/recettes/{string}', routeName: 'recettes->show', routeAction: 'show', pageHeadTitle: 'Recette' ),
];
}
public function index(): View {
$page = Request::get( 'page' );
if( $page == null )
$page = 1;
return new View( 'recettes/index', [
'tagsList' => new TagRepository()->getAll(),
'ingredientsList' => new IngredientRepository()->getAll(),
'recettesList' => new RecetteRepository()->getAllRecettesBrowse( $page ),
] );
}
}

View File

@@ -0,0 +1,17 @@
<?php
namespace App\Domain\Recettes;
use App\Domain\Controller;
class RecettesManagementController extends Controller {
public static function defineRoutes(): array
{
return [
self::Route( routeUrl: '/recettes/create', routeName: 'recettes->create', routeAction: 'create' ),
self::Route( routeUrl: '/recettes/edit/{int}', routeName: 'recettes->edit', routeAction: 'edit' ),
];
}
}

170
src/Domain/Repository.php Normal file
View File

@@ -0,0 +1,170 @@
<?php
namespace App\Domain;
use App\Domain\Recettes\Recette;
use App\Kernel;
use PDO;
/**
* Classe abstraite avec des méthodes pour pouvoir gérer les liens entre le site et la base de données.
*/
abstract class Repository {
/**
* Instancie des éléments de cette classe par rapport au repository.
* @return class-string
*/
abstract public static function getEntity(): string;
/**
* Doit retourner une liste du type :
* ['table' => '', 'columns' => [ ...Liste des colonnes dans la BDD ] ].
* @return array
*/
abstract public static function getStructure(): array;
/**
* Contient le nom de la table principale du repo.
* @var string|mixed
*/
final public string $tableName;
/**
* Contient le nom des colonnes de la table du repo.
* @var array|mixed
*/
final public array $tableColumns;
/**
* Contient les mêmes données que getStructure().
* @var array
*/
public private(set) array $globalStructure;
/**
* Constructeur.
* Reprend les informations de getStructure et les met dans des attributs.
*/
public function __construct(){
$structure = static::getStructure();
$this->tableName = $structure['table'];
$this->tableColumns = $structure['columns'];
$this->globalStructure = $structure;
}
/**
* Permet d'avoir tous les éléments correspondant à la requête passée en paramètre.
*
* @param string $sqlQuery
* @param bool $asArray Permet de savoir si on veut que le retour soit une array ou bien un tableau de Model.
* @return array|null
*/
public function selectGetAll( string $sqlQuery, bool $asArray = false ): ?array {
$statement = Kernel::$DB->pdo->prepare( $sqlQuery );
if( !$statement->execute() )
return null;
if( $asArray )
$results = $statement->fetchAll( PDO::FETCH_ASSOC );
else
$results = $statement->fetchAll( PDO::FETCH_CLASS, static::getEntity() );
if( empty( $results ) )
return null;
return $results;
}
/**
* Permet d'ajouter une entité à la base de données.
*
* @param Model $entity
* @return bool
*/
public function addEntity( Model $entity ): bool {
$query = "INSERT INTO {$this->tableName} (";
$values = "(";
foreach( $this->tableColumns as $column ) {
$query .= "`{$column}`,";
$values .= ":{$column},";
}
$query = substr( $query, 0, -1 ) . ")";
$values = substr( $values, 0, -1 ) . ")";
$query .= " VALUES " . $values . ";";
$statement = Kernel::$DB->pdo->prepare( $query );
foreach( $this->tableColumns as $column ) {
$statement->bindValue(":{$column}", $entity->{$column} );
}
return $statement->execute();
}
/**
* Permet de mettre à jour les informations de l'entité dans la base de données.
*
* @param Model $entity
* @param string $identifier
*
* @return bool
*/
public function updateEntity( Model $entity, string $identifier ): bool {
$query = "UPDATE {$this->tableName} SET ";
foreach( $this->tableColumns as $column ) {
$query .= "`{$column}` = :{$column},";
}
$query = substr( $query, 0, -1 ) . " WHERE {$identifier} = :{$identifier};";
$statement = Kernel::$DB->pdo->prepare( $query );
foreach( $this->tableColumns as $column ) {
$statement->bindValue(":{$column}", $entity->{$column} );
}
$statement->bindValue( ":{$identifier}", $entity->{$identifier} );
return $statement->execute();
}
/**
* Permet de supprimer une entrée d'entité dans la base de données.
*
* @param Model $entity
* @param string $identifier
*
* @return bool
*/
public function deleteEntity( Model $entity, string $identifier ): bool {
$query = "DELETE FROM {$this->tableName} WHERE {$identifier} = :{$identifier};";
$statement = Kernel::$DB->pdo->prepare( $query );
$statement->bindValue( ":{$identifier}", $entity->{$identifier} );
return $statement->execute();
}
/**
* Fonction raccourcie pour préparer les champs nécessaires pour addEntity.
*
* @param Model $entity
*
* @return bool
*/
abstract public function add( Model $entity ): bool;
/**
* Fonction raccourcie pour préparer les champs nécessaires pour updateEntity
*
* @param Model $entity
*
* @return bool
*/
abstract public function update( Model $entity ): bool;
/**
* Fonction raccourcie pour préparer les champs nécessaires pour deleteEntity
*
* @param Model $entity
*
* @return bool
*/
abstract public function delete( Model $entity ): bool;
}

33
src/Domain/Tags/Tag.php Normal file
View File

@@ -0,0 +1,33 @@
<?php
namespace App\Domain\Tags;
use App\Domain\Model;
/**
* Classe qui va gérer les tags.
*/
class Tag extends Model {
/**
* Le numéro du tag.
* @var int
*/
public int $num_tag;
/**
* Le nom du tag
* @var string
*/
public string $nom_tag;
/**
* Retourne le numéro du tag.
* @return int
*/
public function getID(): int
{
return $this->num_tag;
}
}

View File

@@ -0,0 +1,174 @@
<?php
namespace App\Domain\Tags;
use App\Domain\LinkableInterface;
use App\Domain\Recettes\Recette;
use App\Domain\Repository;
use App\Domain\Model;
use App\Kernel;
/**
* Classe qui va permettre de gérer les requêtes BDD en lien avec les tags.
* Les tags sont liables à d'autres entités comme les Recettes.
*/
class TagRepository extends Repository implements LinkableInterface {
/**
* Permet d'avoir le nom de l'entité dont dépend ce repo.
* @return string
*/
public static function getEntity(): string
{
return Tag::class;
}
/**
* La structure de notre table dans la BDD.
* Un champ link_recettes a été ajouté pour donner le nom de la table de lien entre nos recettes.
*
* @return array
*/
public static function getStructure(): array
{
return [
'table' => 'Tag',
'columns' => [
'num_tag', 'nom_tag'
],
'link_recettes' => 'Referencetag'
];
}
/**
* Permet d'obtenir une liste de tous les tags objet Tag.
*
* @return Tag[]|null
*/
public function getAll(): ?array {
$sqlQuery = "SELECT * FROM {$this->tableName};";
$results = $this->selectGetAll($sqlQuery);
if( $results === null )
return null;
return $results;
}
/**
* Permet d'obtenir un tag spécifique par son ID.
*
* @param int $id
*
* @return Tag|null
*/
public function getByID( int $id ): ?Tag {
$sqlQuery = "SELECT * FROM {$this->tableName} WHERE num_tag = {$id}";
$results = $this->selectGetAll($sqlQuery);
if( $results === null || count( $results ) > 1 )
return null;
return $results[0];
}
/**
* Permet d'obtenir, sous forme de liste, toutes les entrées qui lient des tags.
*
* @param string $linkedTo La table qui permet de faire la liaison des tags avec une autre entité.
* @param string $linkingField Le champ qui permet de faire la liaison des tags avec une autre entité.
* @param Model $linkedEntity L'autre entité.
*
* @see LinkableInterface
* @return array|null
*/
public function getIdLinkedTo( string $linkedTo, string $linkingField, Model $linkedEntity ): ?array {
$linkName = 'link_' . $linkedTo;
if( !isset( $this->globalStructure[$linkName]))
return null;
$sqlQuery = "SELECT * FROM {$this->globalStructure[$linkName]} WHERE {$linkingField} = {$linkedEntity->getId()};";
$results = $this->selectGetAll($sqlQuery, true);
if( $results === null )
return null;
return $results;
}
/**
* Permet d'ajouter un lien entre un tag et une autre entité comme Recette.
*
* @param string $linkedTo
* @param string $linkingField
* @param Model $linkedEntity
* @param Model $tagEntity
*
* @see LinkableInterface
* @return bool
*/
public function addLinkBetween( string $linkedTo, string $linkingField, Model $linkedEntity, Model $tagEntity ): bool {
$linkName = 'link_' . $linkedTo;
if( !isset( $this->globalStructure[$linkName]))
return false;
$query = "INSERT INTO {$this->globalStructure[$linkName]} ({$linkingField},num_tag) VALUES ({$linkedEntity->getId()}, {$tagEntity->getID()});";
$statement = Kernel::$DB->pdo->prepare( $query );
return $statement->execute();
}
/**
* Retire un lien dans la BDD entre un tag et une autre entité.
*
* @param string $linkedTo
* @param string $linkingField
* @param Model $linkedEntity
* @param Model $tagEntity
*
* @see LinkableInterface
* @return bool
*/
public function removeLinkBetween(string $linkedTo, string $linkingField, Model $linkedEntity, Model $tagEntity ): bool
{
$linkName = 'link_' . $linkedTo;
if( !isset( $this->globalStructure[$linkName]))
return false;
$query = "DELETE FROM {$this->globalStructure[$linkName]} WHERE {$linkingField} = {$linkedEntity->getId()} AND num_tag = {$tagEntity->getId()};";
$statement = Kernel::$DB->pdo->prepare( $query );
return $statement->execute();
}
/**
* Ajouter un tag dans la BDD.
*
* @param Model $ingredient
*
* @return bool
*/
public function add( Model $tag ): bool {
return $this->addEntity( $tag );
}
/**
* Met à jour un tag dans la BDD.
*
* @param Model $tag
*
* @return bool
*/
public function update( Model $tag ): bool {
return $this->updateEntity( $tag, 'num_tag' );
}
/**
* Supprime un tag de la BDD.
*
* @param Model $tag
*
* @return bool
*/
public function delete( Model $tag ): bool {
return $this->deleteEntity( $tag, 'num_tag' );
}
}

View File

@@ -0,0 +1,38 @@
<?php
namespace App\Domain\Tags;
use App\Domain\Model;
/**
* Interface utilisée par tous les repository qui sont en lien avec les tags.
*/
interface UseTagsInterface {
/**
* Permet de récupérer tous les tags liés à ce Modèle.
* @return array|null
*/
public function getAllLinkedTags( Model $entity ): ?array;
/**
* Permet d'ajouter un tag en lien avec notre autre entité.
*
* @param Tag $tag
* @param Model $entity
*
* @return bool
*/
public function addATag( Tag $tag, Model $entity ): bool;
/**
* Permet de retirer un lien entre un tag et une autre entité.
*
* @param Tag $tag
* @param Model $entity
*
* @return bool
*/
public function removeATag( Tag $tag, Model $entity ): bool;
}

View File

@@ -0,0 +1,72 @@
<?php
namespace App\Domain\Utilisateurs;
use App\Domain\Controller;
use App\Helpers\Authentification;
use App\Http\JSONResponse;
use App\Http\Request;
use App\Infrastructure\View;
class AuthentificationController extends Controller {
public static function defineRoutes(): array {
return [
// Public routes.
self::Route( routeUrl: '/login', routeName: 'login', routeAction: 'loginForm', pageHeadTitle: 'Connexion' ),
self::Route( routeUrl: '/logout', routeName: 'logout', routeAction: 'logoutPage', pageHeadTitle: 'Déconnexion' ),
// API Routes.
self::Route( routeUrl: '/api/auth', routeName: 'api->auth', routeAction: 'auth', routeMethods: ['POST'] ),
// self::Route( routeUrl: '/api/auth/logout', routeName: 'api->auth->logout', routeAction: 'logout', routeMethods: ['POST'] ),
];
}
public function loginForm(): View {
return new View( 'login' );
}
public function logoutPage(){
if( !Authentification::isLoggedIn() ) {
Request::redirectTo( 'home' );
}
Authentification::destroySession();
Request::redirectTo( 'home' );
}
public function auth(): JSONResponse {
Request::setCORS();
$username = Request::get( 'username' );
$password = Request::get( 'password' );
$success = new \App\Domain\Utilisateurs\UtilisateursRepository()->login($username,$password);
if(!$success) return JSONResponse::sendError( [ 'user_id' => $userId ] );
$userId = $success;
Authentification::loginUser( $userId );
return JSONResponse::sendSuccess( [ 'user_id' => $userId ] );
}
public function logout(): JSONResponse {
if( !Authentification::isLoggedIn() ) {
return JSONResponse::sendError( [ 'message' => 'Already disconnected' ] );
}
Authentification::destroySession();
return JSONResponse::sendSuccess( [ 'message' => 'Logged out' ] );
}
}

View File

@@ -0,0 +1,23 @@
<?php
namespace App\Domain\Utilisateurs;
use App\Domain\Model;
//use App\Helpers\Markdown;
/**
* Classe qui va permettre de gérer tous les objets recette.
*/
class Utilisateurs extends Model {
public int $num_user;
public string $username;
public string $userpassword;
public function getID(): int
{
return $this->num_user;
}
}

View File

@@ -0,0 +1,89 @@
<?php
namespace App\Domain\Utilisateurs;
use App\Domain\Model;
use App\Domain\Repository;
/**
* Classe qui permet de faire le lien entre la BDD et le site pour les recettes.
* Les recettes sont en lien avec les ingrédients.
*/
class UtilisateursRepository extends Repository {
public static function getEntity(): string
{
return Utilisateurs::class;
}
public static function getStructure(): array
{
return [
'table' => 'User',
'columns' => [
'num_user', 'username', 'userpassword'
]
];
}
/**
* Permet d'obtenir une liste de toutes les recettes objet Recette.
*
* @return Recette[]|null
*/
public function getAll(): ?array {
$sqlQuery = "SELECT * FROM {$this->tableName};";
$results = $this->selectGetAll($sqlQuery);
if( $results === null )
return null;
return $results;
}
/**
* Permet d'avoir une recette par un ID.
*
* @param int $id
* @return Recette|null
*/
public function getByID( int $id ): ?Utilisateurs {
$sqlQuery = "SELECT * FROM {$this->tableName} WHERE num_user = {$id}";
$results = $this->selectGetAll($sqlQuery);
if( $results === null || count( $results ) > 1 )
return null;
return $results[0];
}
public function login(string $name, string $pass): int|bool{
$users = new UtilisateursRepository()->getAll();
echo "\n\n";
var_dump($users);
foreach($users as $user){
if((strcmp($name,$user->username)==0) and (strcmp($pass,$user->userpassword)==0))return $user->num_user;
return false;
}
}
public function add( Model $utilisateurs ): bool {
return $this->addEntity( $utilisateurs );
}
public function update( Model $utilisateurs ): bool {
return $this->updateEntity( $utilisateurs, 'num_user' );
}
public function delete( Model $utilisateurs ): bool {
return $this->deleteEntity( $utilisateurs, 'num_user' );
}
}

View File

@@ -0,0 +1,28 @@
<?php
namespace App\Exceptions;
/**
* Exception qui se déclenche lorsqu'un fichier de configuration n'a pas pu être chargé.
*
* - Peut se déclencher si un fichier de configuration non voulu.
*/
class ConfigFailedLoadingException extends \Exception {
/**
* Chemin vers le fichier de configuration manquant.
* @var string
*/
public private(set) string $configFilePath;
/**
* Constructeur.
* @param string $configFilePath Le fichier de configuration manquant.
*/
public function __construct( string $configFilePath ) {
$this->configFilePath = $configFilePath;
$this->message = "Failed to load configuration file '{$configFilePath}'.";
parent::__construct();
}
}

View File

@@ -0,0 +1,15 @@
<?php
namespace App\Exceptions;
class InvalidRouteException extends \Exception {
private string $clientRoute;
public function __construct(string $clientRoute)
{
$this->clientRoute = $clientRoute;
$message = "{$clientRoute} doesn't exist";
parent::__construct($message, 404);
}
}

View File

@@ -0,0 +1,13 @@
<?php
namespace App\Exceptions;
use Throwable;
class InvalidViewException extends \Exception {
public function __construct( string $viewName )
{
parent::__construct( "View {$viewName} does not exist.", 500 );
}
}

View File

@@ -0,0 +1,49 @@
<?php
namespace App\Helpers;
class Authentification {
/**
* Permet de démarrer la variable Session.
*
* @return void
*/
public static function startSession(): void {
session_start();
}
/**
* Permet de supprimer la session.
*
* @return void
*/
public static function destroySession(): void {
session_destroy();
}
/**
* Permet de connecter un utilisateur.
*
* @param int $userId
* @return void
*/
public static function loginUser( int $userId ){
$_SESSION['user'] = $userId;
}
// TODO : Complete when user.
public static function getCurrentUser() {
return $_SESSION['user'] ?? false;
}
/**
* Permet de savoir si un utilisateur est connecté ou pas.
*
* @return bool
*/
public static function isLoggedIn(): bool {
return self::getCurrentUser() !== false;
}
}

View File

@@ -0,0 +1,38 @@
<?php
namespace App\Helpers;
/**
* Classe utilitaire contenant des méthodes pour charger automatiquement les classes.
*/
class AutoLoader {
/**
* Namespace principal de l'application.
*/
public const string PRIMARY_NAMESPACE = 'App\\';
/**
* Permet d'enregistrer la méthode _autoload_ en tant que méthode pour charger des classes.
* @return void
*/
public static function register(): void {
spl_autoload_register( [ self::class, '_autoload_' ] );
}
/**
* Permet de charger une classe.
*
* @param string $className
* @return void
*/
public static function _autoload_( string $className ): void {
if( !str_starts_with( $className, self::PRIMARY_NAMESPACE ) ) // Ne s'occupe que des classes à charger de cette application.
return;
$className = str_replace( self::PRIMARY_NAMESPACE, '', $className );
$className = str_replace( '\\', '/', $className );
require APP_ROOT . 'src/' . $className . '.php';
}
}

View File

@@ -0,0 +1,42 @@
<?php
namespace App\Helpers;
use App\Exceptions\ConfigFailedLoadingException;
/**
* Classe outillage qui permet de charger les fichiers de configuration.
*/
class ConfigFactory {
/**
* Chemin relatif à partir de APP_ROOT du dossier contenant les fichiers de configuration.
*
* @see APP_ROOT
*/
const string CONFIG_RELATIVE_FOLDER = "config/";
/**
* Permet de charger un fichier de configuration.
*
* @param string $fileName Le nom du fichier de configuration, avec ou sans l'extension .php.
* @return array
*
* @throws ConfigFailedLoadingException
*/
public static function loadConfigFile( string $fileName ): array {
if( !str_ends_with( $fileName, ".php" ) ) {
$fileName .= ".php";
}
$filePath = APP_ROOT . self::CONFIG_RELATIVE_FOLDER . $fileName;
if( !file_exists( $filePath ) ){
throw new ConfigFailedLoadingException( "Config file '$fileName' does not exist" );
}
$configArray = require $filePath;
return $configArray;
}
}

29
src/Helpers/Markdown.php Normal file
View File

@@ -0,0 +1,29 @@
<?php
namespace App\Helpers;
class Markdown {
public static function getMarkdownEntities(): array {
return [
// Gras & Italique
'/\*\*(.*?)\*\*/' => '<b>$1</b>',
'/\*(.*?)\*/' => '<i>$1</i>',
// Titres
'/^## (.*?)$/m' => '<h3>$1</h3>',
'/^# (.*?)$/m' => '<h2>$1</h2>',
];
}
public static function convertToHTML( string $markdown ): string {
$safeMD = htmlspecialchars( $markdown, ENT_QUOTES );
foreach( Markdown::getMarkdownEntities() as $key => $value ) {
$safeMD = preg_replace( $key, $value, $safeMD );
}
return nl2br( $safeMD );
}
}

View File

@@ -0,0 +1,27 @@
<?php
namespace App\Helpers;
/**
* Trait qui permet de désinfecter une variable.
*/
trait SanitizeTrait {
/**
* Permet de désinfecter une variable
*
* @param mixed $data
* @return mixed
*/
public static function sanitize( mixed $data ): mixed {
if( is_string( $data ) ) {
return htmlspecialchars( $data, ENT_QUOTES );
} else if( is_integer( $data ) ) {
return $data;
}
return $data;
}
}

51
src/Http/JSONResponse.php Normal file
View File

@@ -0,0 +1,51 @@
<?php
namespace App\Http;
/**
* Permet de renvoyer une réponse de route au format JSON.
*/
class JSONResponse {
/**
* Les données ajoutés au fichier JSON.
* @var array|mixed
*/
public private(set) array $data;
/**
* Le code HTML de la réponse.
* @var int|mixed
*/
public private(set) int $htmlCode;
public function __construct( $data = [], $code = 200 ){
$this->data = $data;
$this->htmlCode = $code;
$this->returnResponse();
}
public function returnResponse(): never {
header( 'Content-type: application/json' );
http_response_code( $this->htmlCode );
$this->data['_status'] = $this->htmlCode;
$json = json_encode( $this->data );
echo $json;
die();
}
public static function sendSuccess( $data = [] ): self {
$data['success'] = true;
return new self( $data, 200 );
}
public static function sendError( $data = [] ): self {
$data['success'] = false;
return new self( $data, 400 );
}
}

69
src/Http/Request.php Normal file
View File

@@ -0,0 +1,69 @@
<?php
namespace App\Http;
use App\Helpers\SanitizeTrait;
use App\Kernel;
/**
* Classe utilitaire ayant plusieurs méthodes pour gérer la requête actuelle.
*/
class Request {
use SanitizeTrait;
/**
* Bloquer les CORS venant d'autres sites.
* @return void
*/
public static function setCORS(): void {
$siteUrl = Kernel::$configs['general']['website_url'];
header("Access-Control-Allow-Origin: {$siteUrl}");
}
/**
* Permet d'obtenir une variable GET et nettoyé.
*
* @param string $name
* @return mixed
*/
public static function get( string $name ): mixed {
if( !isset( $_GET[$name] ) ) {
return null;
}
return self::sanitize( $_GET[$name] );
}
/**
* Permet d'obtenir une variable POST et nettoyé.
*
* @param string $name
* @return mixed
*/
public static function post( string $name ): mixed {
if( !isset( $_POST[$name] ) ) {
return null;
}
return self::sanitize( $_POST[$name] );
}
/**
* Permet de rediriger un utilisateur.
* /!\ Arrête tout traitement sur la page.
*
* @param string $routeName
* @param ...$args
*
* @return never
*/
public static function redirectTo( string $routeName, ...$args ): never {
$routeUrl = Router::getRouteURL( $routeName, ...$args );
header("Location: {$routeUrl}");
die();
}
}

42
src/Http/Route.php Normal file
View File

@@ -0,0 +1,42 @@
<?php
namespace App\Http;
/**
* Permet de définir une route précise.
*/
final class Route {
public private(set) string $routeUrl;
public private(set) string $routeName;
public private(set) string $routeController;
public private(set) string $routeAction;
public private(set) array $routeMethods;
public private(set) string $pageHeadTitle;
/**
* @param string $routeUrl Le chemin vers la route.
* @param string $routeName Le nom de la route.
* @param string $routeController Le controller qui va interpréter cette route.
* @param string $routeAction L'action du controller qui va interpréter cette route.
* @param array $routeMethods Une liste de methodes GET, POST que la page accepte.
* @param string $pageHeadTitle Le nom de la page qui sera affiché dans <title></title>.
*/
public function __construct(
string $routeUrl,
string $routeName,
string $routeController,
string $routeAction,
array $routeMethods,
string $pageHeadTitle,
){
$this->routeUrl = $routeUrl;
$this->routeName = $routeName;
$this->routeController = $routeController;
$this->routeAction = $routeAction;
$this->routeMethods = $routeMethods;
$this->pageHeadTitle = $pageHeadTitle;
}
}

202
src/Http/Router.php Normal file
View File

@@ -0,0 +1,202 @@
<?php
namespace App\Http;
use App\Domain\Controller;
use App\Exceptions\InvalidRouteException;
use App\Helpers\AutoLoader;
use App\Kernel;
use FilesystemIterator;
final class Router {
/**
* Route voulue par le client.
* @var string
*/
public private(set) static string $clientRouteString;
// private(set)
/**
* Informations de la route du client.
* @var Route
*/
public static Route $clientRoute;
// private(set)
/**
* Informations sur les arguments passés à la route.
* @var array
*/
public static array $clientRouteArguments;
// private(set)
/**
* Liste des controllers sorti du cache ou récupéré via fetchControllers.
* @var array
*/
private static array $controllers;
/**
* Liste des routes tirées des controllers. Rempli par la méthode fetchRoutes.
* @var Route[]
*/
private static array $routes;
/**
* Fonction principale qui va router le contenu vers la bonne méthode du bon Controller.
* @return void
*
* @throws InvalidRouteException Si la route voulue n'existe pas ou n'est pas bien formaté.
*/
public static function routeTo(): void {
self::$clientRouteString = parse_url($_SERVER['REQUEST_URI'], PHP_URL_PATH);
self::$controllers = self::fetchControllers();
self::$routes = self::fetchRoutes();
$clientRoute = self::clientRouteExist();
if( !$clientRoute ){
throw new InvalidRouteException( self::$clientRouteString );
}
self::$clientRoute = $clientRoute;
if( !in_array( $_SERVER['REQUEST_METHOD'], self::$clientRoute->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 ); // On enlève la chaine complète.
self::$clientRouteArguments = $matches;
return $route;
}
}
return false;
}
/**
* Permet de remplacer les expressions définies dans la configuration par leurs équivalents Regex.
*
* @param Route $route
* @return string
*/
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);
//correction controler()->method to controler().method
// new $controller()->$method( ...self::$clientRouteArguments);
}
/**
* Permet d'obtenir l'adresse du site vers une route spécifique.
*
* @param string $routeName Le nom de la route.
* @param ...$args - Les arguments de la route, si existants.
*
* @return string Renvoie l'URL demandé ou bien la racine du site si la route n'existe pas.
*/
public static function getRouteURL( string $routeName, ...$args ): string {
foreach( self::$routes as $route ){
if( $routeName === $route->routeName ){
$i = 0;
$routeUrl = preg_replace_callback( '/\{([^}]+)}/', function( $match ) use ( $args, &$i ){
return $args[$i++] ?? "";
}, $route->routeUrl);
return rtrim( rtrim( Kernel::$configs['general']['website_url'], '/' ) . $routeUrl, '/' );
}
}
return Kernel::$configs['general']['website_url'];
}
public static function getAssetURL( string $assetPath ): string {
return Kernel::$configs['general']['website_url'] . 'assets/' . $assetPath;
}
}

View File

@@ -0,0 +1,45 @@
<?php
namespace App\Infrastructure;
/**
* Permet de se connecter à la base de données et de faire le pont
* entre la classe PDO et nos actions.
*/
final class Database {
/**
* Contient les informations de connexion à notre BDD.
* @var array
*/
private array $databaseInfos;
/**
* Instance PDO.
* @var \PDO
*/
public private(set) \PDO $pdo;
/**
* Constructeur, lance la connexion.
* @param array $loginInfos ['name','host','port','user','pass']
*/
public function __construct( array $loginInfos ){
$this->databaseInfos = $loginInfos;
$this->tryLogin();
}
/**
* Permet d'essayer un login à la base de données.
* @return void - never si la connexion échoue.
*/
private function tryLogin(): void {
try {
$dsn = 'mysql:dbname=' . $this->databaseInfos['name'] . ';host=' . $this->databaseInfos['host'] . ';port=' . $this->databaseInfos['port'];
$this->pdo = new \PDO( $dsn, $this->databaseInfos['user'], $this->databaseInfos['pass'] );
} catch ( \PDOException $e ) {
die( $e->getMessage() );
}
}
}

220
src/Infrastructure/View.php Normal file
View File

@@ -0,0 +1,220 @@
<?php
namespace App\Infrastructure;
use App\Exceptions\InvalidViewException;
use App\Http\Router;
use App\Kernel;
/**
* Permet de rendre une vue / contenu de la page.
* /!\ : Une seule vue peut-être appellé par page, hormis celle en plus pour le squelette.
*/
final class View {
/**
* Répertoire où se trouvent les vues.
*/
const string VIEW_PATH = APP_ROOT . 'views/';
/**
* Le nom du fichier de vue.
* @var string
*/
public private(set) string $viewName;
/**
* Les arguments passés à la vue.
* @var array
*/
public private(set) array $viewArgs;
/**
* La base de la vue. (Pour ne pas répéter header/footer à chaque fois).
* null correspond à pas de squelette.
*
* @var string|mixed|null
*/
public private(set) ?string $skeleton;
/**
* Utilisé par les squelettes, permet d'injecter des contenus comme le contenu de son enfant.
* @var array<string,string>
*/
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<string,string> $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.
$contentName = $this->viewName;
// On démarre la vue du squelette.
$base = new View( $this->skeleton, $this->viewArgs, skeleton: null, integratedContent: [ 'content' => $contentName ] );
}
}
/*
* 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 ] ) )
require self::VIEW_PATH . 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;
}
}
/**
* Permet d'obtenir le titre de la page à mettre dans <title></title>
* @return string
*/
public static function getHeadTitle(): string {
$siteUrl = Kernel::$configs['general']['website_name'];
return Router::$clientRoute->pageHeadTitle . ' - ' . $siteUrl;
}
/**
* Permet d'obtenir une route
*
* @param string $routeName Le nom de la route.
* @param ...$args - Les arguments de la route, si existant.
*
* @return void Affiche uniquement.
*/
public static function routeUrl( string $routeName, ...$args ): void {
echo Router::getRouteURL( $routeName, ...$args );
}
/**
* Permet d'obtenir l'URL vers un asset (CSS,JS,Images).
* @param string $assetPath
*
* @return void
*/
public static function assetUrl( string $assetPath ): void {
echo Router::getAssetURL( $assetPath );
}
}

119
src/Kernel.php Normal file
View File

@@ -0,0 +1,119 @@
<?php
namespace App;
use App\Exceptions\ConfigFailedLoadingException;
use App\Exceptions\InvalidRouteException;
use App\Helpers\Authentification;
use App\Helpers\AutoLoader;
use App\Helpers\ConfigFactory;
use App\Http\Router;
use App\Infrastructure\Database;
/**
* Classe primaire du site.
* Permet de lancer l'application.
*/
final class Kernel {
/**
* Liste des fichiers de configuration sous le format suivant :
* [ 'nom_de_la_config' -> [ 'cle" → valeur ] ]
*
* @var array<string,array>
*/
public private(set) static array $configs = [];
/**
* Instance à la base de données.
* Pour utiliser la classe PDO : $DB->PDO.
*
* @var Database|null
*/
public private(set) static ?Database $DB = null;
/**
* 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();
$this->loadDatabase();
Authentification::startSession();
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['database'] = ConfigFactory::loadConfigFile('database');
self::$configs['route_arguments'] = ConfigFactory::loadConfigFile('route_arguments');
self::$configs['views'] = ConfigFactory::loadConfigFile('views');
} catch( ConfigFailedLoadingException $e ){
die( $e->getMessage() );
}
}
/**
* Permet de se connecter à la base de données principale.
* @return void
*/
private function loadDatabase(): void {
self::$DB = new Database( self::$configs['database'] );
}
}

9
views/base.php Normal file
View File

@@ -0,0 +1,9 @@
<?php use App\Infrastructure\View as V; ?>
<?php V::partial( 'header' ); ?>
<div class="main-body">
<?php V::inject( 'content'); ?>
</div>
<?php V::partial( 'footer' ); ?>

36
views/home.php Normal file
View File

@@ -0,0 +1,36 @@
<h1>Coucou</h1>
<?php $str = "Markdown **text**\n# SOLONG\n*text*."; $md = \App\Helpers\Markdown::convertToHTML( $str ); echo "<p>$md</p>"; ?>
<?php use App\Infrastructure\View as V; use App\Http\Router; ?>
<?php
$recette = new \App\Domain\Recettes\RecetteRepository()->getByID( 2 );
$modelingredient = new \App\Domain\Ingredients\Ingredient();
$modelingredient->num_ingredient=0;
$modelingredient->nom_ingredient='nomdingredientaupif';
var_dump($modelingredient);
//$ingredient = new \App\Domain\Ingredients\IngredientRepository()->add($modelingredient);
//pas de add si numing existe déjà use autoincrement
$ingredient = new \App\Domain\Ingredients\IngredientRepository()->getByID( 1 );
var_dump( $recette );
var_dump( $ingredient );
// new \App\Domain\Recettes\RecetteRepository()->removeAnIngredient( $ingredient, $recette );
var_dump( $recette->getAllLinkedIngredients() );
echo "\n\n";
$modelUtilisateurs = new \App\Domain\Utilisateurs\Utilisateurs();
$modelUtilisateurs->num_user=0;
$modelUtilisateurs->username="usnmaemaee";
$modelUtilisateurs->userpassword="pawssorddd";
//$user = new \App\Domain\Utilisateurs\UtilisateursRepository()->add($modelUtilisateurs);
$user = new \App\Domain\Utilisateurs\UtilisateursRepository()->getByID( 2 );
var_dump($user);
$success = new \App\Domain\Utilisateurs\UtilisateursRepository()->login("usnmaemaee","pawssorddd");
echo "\n\n";
var_dump($success);
?>

23
views/login.php Normal file
View File

@@ -0,0 +1,23 @@
<?php use App\Infrastructure\View as V; use App\Http\Router; ?>
<div class=formcont>
<form id='login-form' action='<?php echo Router::getRouteURL( 'api->auth' ); ?>' method='post'>
<div class='form-group'>
<label for='logmsg'>Se Connecter</label>
</div>
<div id="login-errors" class="form-error" style="display:none;color:red;">
</div>
<div class='form-group'>
<input type='text' class='form-control' name='username' id='username' value='' placeholder="Pseudo">
</div>
<div class='form-group'>
<input type='text' class='form-control' name='password' id='password' value='' placeholder="Mot de Passe">
</div>
<button type='submit' class='btn btn-primary'>Confirmer</button>
</form>
</div>
<script src="<?php echo Router::getAssetURL( 'js/login.js' ); ?>" defer></script>

14
views/partials/footer.php Normal file
View File

@@ -0,0 +1,14 @@
<?php use App\Infrastructure\View as V; ?>
<footer id="footer">
<div class="src">
<a href="google.com">Une source</a>
</div>
<div class="Contact">
Front end : Bousquet Sébastien
--- Back end : Thorel Benjamin
--- Base de donné : Glaudis Jordan
</div>
</footer>
</body>
</html>

30
views/partials/header.php Normal file
View File

@@ -0,0 +1,30 @@
<?php use App\Infrastructure\View as V; ?>
<!DOCTYPE html>
<html>
<head>
<title><?php echo V::getHeadTitle(); ?></title>
<meta charset="UTF-8" />
<link rel="stylesheet" href="<?php V::assetUrl( 'css/style.css' ); ?>" />
</head>
<body>
<header id="header">
<div id="logo">
<a href="index.php">
<img src="<?php V::assetUrl( 'images/Logo.jpg'); ?>" class="logo">
</a>
</div>
<nav>
<ul class="nav-list">
<li><a id="google" class="nav-element" href="<?php V::routeUrl( 'home'); ?>">Acceuil</a></li>
<li><a id="unTest" class="nav-element" href="<?php V::routeUrl( 'recettes->index'); ?>">Liste des recettes</a></li>
<?php if( \App\Helpers\Authentification::isLoggedIn() ): ?>
<li><a id="login" class= "nav-element" href="<?php V::routeUrl( 'logout'); ?>">Logout</a></li>
<li><a id="addBouton" class="nav-element" href="<?php V::routeUrl( 'recettes->create'); ?>">Ajouter une Recette</a></li>
<?php else: ?>
<li><a id="login" class= "nav-element" href="<?php V::routeUrl( 'login'); ?>">Login</a></li>
<?php endif; ?>
</ul>
<input type="text" class="search-form search-form-recette" placeholder="Rechercher une recette ...">
</nav>
</header>

View File

@@ -0,0 +1,39 @@
<?php use App\Infrastructure\View as V; ?>
<div class="sidebar">
<div class="tag-cont">
<h4>Tag</h4>
<div class="tag-selected-div">
<ul>
</ul>
</div>
<form class="sidebar-search" action="none">
<input type="text" class="search-form search-form-tag" name="search-tag" placeholder="Rechercher..." >
</form>
<div class="tag-unselected-div">
<ul>
<?php foreach( V::arg( 'tagsList') as $tag ): ?>
<li class="tag tag-unselected" onclick="test()"><?php echo $tag->nom_tag; ?></li>
<?php endforeach; ?>
</ul>
</div>
</div>
<hr id="hr">
<div class="ingr-cont">
<h4>Ingrédient</h4>
<div class="tag-selected-div">
<ul>
</ul>
</div>
<form class="sidebar-search" action="none">
<input type="text" class="search-form search-form-tag" name="search-ingr" placeholder="Rechercher..." >
</form>
<div class="tag-unselected-div">
<ul>
<?php foreach( V::arg( 'ingredientsList') as $tag ): ?>
<li class="tag tag-unselected" onclick="test()"><?php echo $tag->nom_ingredient; ?></li>
<?php endforeach; ?>
</ul>
</div>
</div>
</div>

19
views/recettes/index.php Normal file
View File

@@ -0,0 +1,19 @@
<?php use App\Infrastructure\View as V; ?>
<?php V::partial( 'tag-sidebar' ); ?>
<div class="content">
<div class="recettes">
<?php if( V::arg( 'recettesList' ) != null ) foreach( V::arg( 'recettesList' ) as $recette ): ?>
<a class="recette-icone" href="<?php V::routeUrl( 'recettes->show', $recette->slug ); ?>">
<img class="recette-preview-image" src="random-recette.jpg">
<div class="recette-icone-content">
<h3><?php echo $recette->titre_recette; ?></h3>
<ul>
<li>Temps de préparation : <?php echo $recette->temps_de_preparation; ?></li>
<li>Nombre d'ingrédients : <?php echo $recette->getNumberOfIngredients(); ?></li>
</ul>
</div>
</a>
<?php endforeach; ?>
</div>
</div>