\ No newline at end of file
diff --git a/src/Auth/views/email/password.text.twig b/src/Auth/views/email/password.text.twig
new file mode 100644
index 0000000..4595714
--- /dev/null
+++ b/src/Auth/views/email/password.text.twig
@@ -0,0 +1,3 @@
+Vous avez demandé la réinitialisation de votre de mot de passe.
+
+{{ domain }}{{ path('auth.reset', {id: id, token: token}) }}
\ No newline at end of file
diff --git a/src/Auth/views/login.twig b/src/Auth/views/login.twig
new file mode 100644
index 0000000..cdc4ede
--- /dev/null
+++ b/src/Auth/views/login.twig
@@ -0,0 +1,27 @@
+{% extends 'layout.twig' %}
+
+
+{% block body %}
+
+
+ {% if flash('error') %}
+
+ {{ flash('error') }}
+
+ {% endif %}
+
+
+
+
+
+{% endblock %}
diff --git a/src/Auth/views/password.twig b/src/Auth/views/password.twig
new file mode 100644
index 0000000..66eebf5
--- /dev/null
+++ b/src/Auth/views/password.twig
@@ -0,0 +1,24 @@
+{% extends 'layout.twig' %}
+
+
+{% block body %}
+
+
+ {% if flash('error') %}
+
+ {{ flash('error') }}
+
+ {% endif %}
+
+
+
+
+{% endblock %}
diff --git a/src/Auth/views/reset.twig b/src/Auth/views/reset.twig
new file mode 100644
index 0000000..c99dc5c
--- /dev/null
+++ b/src/Auth/views/reset.twig
@@ -0,0 +1,20 @@
+{% extends 'layout.twig' %}
+
+
+{% block body %}
+
+
+ {% if flash('error') %}
+
Utilisateurs et membres de la Monnaie Libre depuis deux ans, nous
+ avons voulu créer un outil permettant de gérer et d'organiser les
+ G-marchés dans toute la France.
+
Avec Ğ1-MARCHÉ, vous pouvez proposer à l'avance, consulter,
+ pré-acheter, faire une demande en vue du prochain G-Marché près de
+ chez vous... Votre ville n'apparaît pas ? Il suffit de nous en faire
+ la demande, et nous l'intégrons dans le site. Nous vous fournissons
+ une redirection, un dossier générique avec le code source, vous
+ n'avez plus qu'à changer l'image du fond et le logo par ceux de
+ votre choix pour symboliser votre ville, et votre Ğ1-marché local
+ est prêt...
+
Pour nous contacter : gmarche@axiom-team.fr
+
+
+
+
+{% endblock %}
\ No newline at end of file
diff --git a/src/Contact/definitions.php b/src/Contact/definitions.php
new file mode 100644
index 0000000..bf5615d
--- /dev/null
+++ b/src/Contact/definitions.php
@@ -0,0 +1,5 @@
+ \DI\get('mail.to'),
+ // \App\Contact\ContactAction::class => \DI\object()->constructorParameter('to', \DI\get('contact.to'))
+];
diff --git a/src/Framework/Actions/CrudAction.php b/src/Framework/Actions/CrudAction.php
new file mode 100644
index 0000000..a71f166
--- /dev/null
+++ b/src/Framework/Actions/CrudAction.php
@@ -0,0 +1,255 @@
+ "L'élément truc a bien été créé", /* truc sera remplacé par le vrai nom ensuite */
+ 'edit' => "L'élément truc a bien été modifié", /* truc sera remplacé par le vrai nom ensuite */
+ 'delete' => "L'élément truc a bien été supprimé" /* truc sera remplacé par le vrai nom ensuite */
+ ];
+
+ /**
+ * @var array
+ */
+ protected $acceptedParams = [];
+
+ use RouterAwareAction;
+
+ public function __construct(
+ RendererInterface $renderer,
+ Router $router,
+ Table $table,
+ FlashService $flash
+ ) {
+ $this->renderer = $renderer;
+ $this->router = $router;
+ $this->table = $table;
+ $this->flash = $flash;
+ }
+
+ public function __invoke(Request $request)
+ {
+ $this->renderer->addGlobal('viewPath', $this->viewPath);
+ $this->renderer->addGlobal('routePrefix', $this->routePrefix);
+ if ($request->getMethod() === 'DELETE') {
+ return $this->delete($request);
+ }
+ if (substr((string)$request->getUri(), -3) === 'new') {
+ return $this->create($request);
+ }
+ if ($request->getAttribute('id')) {
+ return $this->edit($request);
+ }
+ return $this->index($request);
+ }
+
+ /**
+ * Affiche la liste des éléments
+ * @param Request $request
+ * @return string
+ */
+ public function index(Request $request): string
+ {
+ $params = $request->getQueryParams();
+ $items = $this->table->findAll()->paginate(12, $params['p'] ?? 1);
+ return $this->renderer->render($this->viewPath . '/index', compact('items'));
+ }
+
+ /**
+ * Edite un élément
+ * @param Request $request
+ * @return ResponseInterface|string
+ */
+ public function edit(Request $request)
+ {
+ $id = (int)$request->getAttribute('id');
+ $item = $this->table->find($id);
+ $antenne = $item->antenneId;
+
+ if ($request->getMethod() === 'POST') {
+ $validator = $this->getValidator($request);
+ if ($validator->isValid()) {
+ $this->table->update($id, $this->prePersist($request, $item));
+ $this->postPersist($request, $item);
+ // On récupère le nom du produit pour l'afficher dans le message de success
+ $requete = $this->prePersist($request, $item);
+ $occurrence = $requete['name'];
+ $message_edit = $this->messages['edit'];
+ $message_edit = str_replace('truc', "'".$occurrence."'", $message_edit );
+ $this->flash->success($message_edit);
+ $region = $request->getAttribute('region');
+ $slug = $request->getAttribute('slug');
+ return $this->redirect($this->routePrefix . '.index',compact('region','slug','antenne' ));
+ }
+ $errors = $validator->getErrors();
+ Hydrator::hydrate($request->getParsedBody(), $item);
+ }
+ $errors = $errors ?? '';
+ return $this->renderer->render(
+ $this->viewPath . '/edit',
+ $this->formParams(compact('item', 'errors'))
+ );
+ }
+
+ /**
+ * Crée un nouvel élément
+ * @param Request $request
+ * @return ResponseInterface|string
+ */
+ public function create(Request $request)
+ {
+ $item = $this->getNewEntity();
+
+ if ($request->getMethod() === 'POST') {
+ $validator = $this->getValidator($request);
+ if ($validator->isValid()) {
+ $this->table->insert($this->prePersist($request, $item));
+ $region = $request->getAttribute('region');
+ $slug = $request->getAttribute('slug');
+ $antenne = $request->getParsedBody()['antenne_id'];
+ $this->postPersist($request, $item);
+ // On récupère le nom du produit pour l'afficher dans le message de success
+ $requete = $this->prePersist($request, $item);
+ $occurrence = $requete['name'];
+ $message_create = $this->messages['create'];
+ $message_create = str_replace('truc', "'".$occurrence."'", $message_create );
+ $this->flash->success($this->messages['create']);
+
+ return $this->redirect($this->routePrefix . '.index',compact('region','slug','antenne' ));
+ }
+ Hydrator::hydrate($request->getParsedBody(), $item);
+ $errors = $validator->getErrors();
+ }
+ $errors = $errors ?? '';
+ return $this->renderer->render(
+ $this->viewPath . '/create',
+ $this->formParams(compact('item', 'errors'))
+ );
+ }
+
+ /**
+ * Action de suppression
+ *
+ * @param Request $request
+ * @return ResponseInterface
+ */
+ public function delete(Request $request)
+ {
+ $this->table->delete($request->getAttribute('id'));
+ $region = $request->getAttribute('region');
+ $slug = $request->getAttribute('slug');
+ $antenne = $request->getParsedBody()['antenne_id'];
+ // On récupère le nom du produit pour l'afficher dans le message de success
+ $occurrence = $request->getParsedBody()['produit_name'];
+ $message_delete = $this->messages['delete'];
+ $message_delete = str_replace('truc', "'".$occurrence."'", $message_delete );
+ $this->flash->success($message_delete);
+ return $this->redirect($this->routePrefix . '.index',compact('region','slug','antenne' ));
+ }
+
+ /**
+ * Filtre les paramètres reçus par la requête
+ *
+ * @param Request $request
+ * @return array
+ */
+ protected function prePersist(Request $request, $item): array
+ {
+ return array_filter(array_merge($request->getParsedBody(), $request->getUploadedFiles()), function ($key) {
+ return in_array($key, $this->acceptedParams);
+ }, ARRAY_FILTER_USE_KEY);
+ }
+
+ /**
+ * Permet d'effectuer un traitement après la persistence
+ * @param Request $request
+ * @param $item
+ */
+ protected function postPersist(Request $request, $item): void
+ {
+ }
+
+ /**
+ * Génère le validateur pour valider les données
+ *
+ * @param Request $request
+ * @return Validator
+ */
+ protected function getValidator(Request $request)
+ {
+ return new Validator(array_merge($request->getParsedBody(), $request->getUploadedFiles()));
+ }
+
+ /**
+ * Génère une nouvelle entité pour l'action de création
+ *
+ * @return mixed
+ */
+ protected function getNewEntity()
+ {
+ $entity = $this->table->getEntity();
+ return new $entity();
+ }
+
+ /**
+ * Permet de traiter les paramètres à envoyer à la vue
+ *
+ * @param $params
+ * @return array
+ */
+ protected function formParams(array $params): array
+ {
+ return $params;
+ }
+}
diff --git a/src/Framework/Actions/RouterAwareAction.php b/src/Framework/Actions/RouterAwareAction.php
new file mode 100644
index 0000000..2159128
--- /dev/null
+++ b/src/Framework/Actions/RouterAwareAction.php
@@ -0,0 +1,30 @@
+router->generateUri($path, $params);
+ return (new Response())
+ ->withStatus(301)
+ ->withHeader('Location',$redirectUri);
+ }
+}
diff --git a/src/Framework/App.php b/src/Framework/App.php
new file mode 100644
index 0000000..502ec97
--- /dev/null
+++ b/src/Framework/App.php
@@ -0,0 +1,150 @@
+isSequential($definitions)) {
+ $definitions = [$definitions];
+ }
+ $this->definitions = $definitions;
+ }
+
+ /**
+ * Rajoute un module à l'application
+ *
+ * @param string $module
+ * @return App
+ */
+ public function addModule(string $module): self
+ {
+ $this->modules[] = $module;
+ return $this;
+ }
+
+ /**
+ * Ajoute un middleware
+ *
+ * @param string|callable|MiddlewareInterface $routePrefix
+ * @param null|string|callable|MiddlewareInterface $middleware
+ * @return App
+ */
+ public function pipe($routePrefix, $middleware = null): self
+ {
+ if ($middleware === null) {
+ $this->middlewares[] = $routePrefix;
+ } else {
+ $this->middlewares[] = new RoutePrefixedMiddleware($this->getContainer(), $routePrefix, $middleware);
+ }
+ return $this;
+ }
+
+ public function process(ServerRequestInterface $request): ResponseInterface
+ {
+ $this->index++;
+ if ($this->index > 1) {
+ throw new \Exception();
+ }
+ $middleware = new CombinedMiddleware($this->getContainer(), $this->middlewares);
+ return $middleware->process($request, $this);
+ }
+
+ public function run(ServerRequestInterface $request): ResponseInterface
+ {
+ foreach ($this->modules as $module) {
+ $this->getContainer()->get($module);
+ }
+ return $this->process($request);
+ }
+
+ /**
+ * @return ContainerInterface
+ */
+ public function getContainer(): ContainerInterface
+ {
+ if ($this->container === null) {
+ $builder = new ContainerBuilder();
+ $env = getenv('ENV') ?: 'production';
+ if ($env === 'production') {
+ //$builder->setDefinitionCache(new FilesystemCache('tmp/di'));
+ $builder->writeProxiesToFile(true, 'tmp/proxies');
+ }
+ foreach ($this->definitions as $definition) {
+ $builder->addDefinitions($definition);
+ }
+ foreach ($this->modules as $module) {
+ if ($module::DEFINITIONS) {
+ $builder->addDefinitions($module::DEFINITIONS);
+ }
+ }
+ $builder->addDefinitions([
+ App::class => $this
+ ]);
+ $this->container = $builder->build();
+ }
+ return $this->container;
+ }
+
+ /**
+ * @return array
+ */
+ public function getModules(): array
+ {
+ return $this->modules;
+ }
+
+ private function isSequential(array $array): bool
+ {
+ if (empty($array)) {
+ return true;
+ }
+ return array_keys($array) === range(0, count($array) - 1);
+ }
+}
diff --git a/src/Framework/Auth.php b/src/Framework/Auth.php
new file mode 100644
index 0000000..1eb050f
--- /dev/null
+++ b/src/Framework/Auth.php
@@ -0,0 +1,13 @@
+auth = $auth;
+ }
+
+ public function process(ServerRequestInterface $request, DelegateInterface $delegate): ResponseInterface
+ {
+ $user = $this->auth->getUser();
+ if (is_null($user)) {
+ throw new ForbiddenException();
+ }
+ return $delegate->process($request->withAttribute('user', $user));
+ }
+}
diff --git a/src/Framework/Auth/RoleMiddleware.php b/src/Framework/Auth/RoleMiddleware.php
new file mode 100644
index 0000000..585744d
--- /dev/null
+++ b/src/Framework/Auth/RoleMiddleware.php
@@ -0,0 +1,36 @@
+auth = $auth;
+ $this->role = $role;
+ }
+
+ public function process(ServerRequestInterface $request, DelegateInterface $delegate): ResponseInterface
+ {
+ $user = $this->auth->getUser();
+ if ($user === null || !in_array($this->role, $user->getRoles())) {
+ throw new ForbiddenException();
+ }
+ return $delegate->process($request);
+ }
+}
diff --git a/src/Framework/Auth/RoleMiddlewareFactory.php b/src/Framework/Auth/RoleMiddlewareFactory.php
new file mode 100644
index 0000000..7b21392
--- /dev/null
+++ b/src/Framework/Auth/RoleMiddlewareFactory.php
@@ -0,0 +1,24 @@
+auth = $auth;
+ }
+
+ public function makeForRole($role): RoleMiddleware
+ {
+ return new RoleMiddleware($this->auth, $role);
+ }
+}
diff --git a/src/Framework/Auth/User.php b/src/Framework/Auth/User.php
new file mode 100644
index 0000000..d47f809
--- /dev/null
+++ b/src/Framework/Auth/User.php
@@ -0,0 +1,17 @@
+ $value) {
+ $method = self::getSetter($key);
+ if (method_exists($instance, $method)) {
+ $instance->$method($value);
+ } else {
+ $property = lcfirst(self::getProperty($key));
+ $instance->$property = $value;
+ }
+ }
+ return $instance;
+ }
+
+ private static function getSetter(string $fieldName): string
+ {
+ return 'set' . self::getProperty($fieldName);
+ }
+
+ private static function getProperty(string $fieldName): string
+ {
+ return join('', array_map('ucfirst', explode('_', $fieldName)));
+ }
+}
diff --git a/src/Framework/Database/NoRecordException.php b/src/Framework/Database/NoRecordException.php
new file mode 100644
index 0000000..f34a86e
--- /dev/null
+++ b/src/Framework/Database/NoRecordException.php
@@ -0,0 +1,8 @@
+query = $query;
+ }
+
+ /**
+ * Returns the number of results.
+ *
+ * @return integer The number of results.
+ */
+ public function getNbResults(): int
+ {
+ return $this->query->count();
+ }
+
+ /**
+ * Returns an slice of the results.
+ *
+ * @param integer $offset The offset.
+ * @param integer $length The length.
+ *
+ * @return \Traversable The slice.
+ */
+ public function getSlice($offset, $length): QueryResult
+ {
+ $query = clone $this->query;
+ return $query->limit($length, $offset)->fetchAll();
+ }
+}
diff --git a/src/Framework/Database/Query.php b/src/Framework/Database/Query.php
new file mode 100644
index 0000000..a2f615a
--- /dev/null
+++ b/src/Framework/Database/Query.php
@@ -0,0 +1,275 @@
+pdo = $pdo;
+ }
+
+ /**
+ * Definit le FROM
+ * @param string $table
+ * @param null|string $alias
+ * @return Query
+ */
+ public function from(string $table, ?string $alias = null): self
+ {
+ if ($alias) {
+ $this->from[$table] = $alias;
+ } else {
+ $this->from[] = $table;
+ }
+ return $this;
+ }
+
+ /**
+ * Spécifie les champs à récupérer
+ * @param string[] ...$fields
+ * @return Query
+ */
+ public function select(string ...$fields): self
+ {
+ $this->select = $fields;
+ return $this;
+ }
+
+ /**
+ * Spécifie la limite
+ * @param int $length
+ * @param int $offset
+ * @return Query
+ */
+ public function limit(int $length, int $offset = 0): self
+ {
+ $this->limit = "$offset, $length";
+ return $this;
+ }
+
+ /**
+ * Spécifie l'ordre de récupération
+ * @param string $order
+ * @return Query
+ */
+ public function order(string $order): self
+ {
+ $this->order[] = $order;
+ return $this;
+ }
+
+ /**
+ * Ajoute une liaison
+ * @param string $table
+ * @param string $condition
+ * @param string $type
+ * @return Query
+ */
+ public function join(string $table, string $condition, string $type = "left"): self
+ {
+ $this->joins[$type][] = [$table, $condition];
+ return $this;
+ }
+
+ /**
+ * Définit la condition de récupération
+ * @param string[] ...$condition
+ * @return Query
+ */
+ public function where(string ...$condition): self
+ {
+ $this->where = array_merge($this->where, $condition);
+ return $this;
+ }
+
+ /**
+ * Execute un COUNT() et renvoie la colonne
+ * @return int
+ */
+ public function count(): int
+ {
+ $query = clone $this;
+ $table = current($this->from);
+ return $query->select("COUNT($table.id)")->execute()->fetchColumn();
+ }
+
+ /**
+ * Définit les paramètre pour la requête
+ * @param array $params
+ * @return Query
+ */
+ public function params(array $params): self
+ {
+ $this->params = array_merge($this->params, $params);
+ return $this;
+ }
+
+ /**
+ * Spécifie l'entité à utiliser
+ * @param string $entity
+ * @return Query
+ */
+ public function into(string $entity): self
+ {
+ $this->entity = $entity;
+ return $this;
+ }
+
+ /**
+ * Récupère un résultat
+ */
+ public function fetch()
+ {
+ $record = $this->execute()->fetch(\PDO::FETCH_ASSOC);
+ if ($record === false) {
+ return false;
+ }
+ if ($this->entity) {
+ return Hydrator::hydrate($record, $this->entity);
+ }
+ return $record;
+ }
+
+ /**
+ * Récupère un résultat
+ * @param int $columnNumber
+ * @return mixed
+ */
+ public function fetchColumn(int $columnNumber = 0)
+ {
+ return $this->execute()->fetchColumn($columnNumber);
+ }
+
+ /**
+ * Retournera un résultat ou renvoie une exception
+ * @return bool|mixed
+ * @throws NoRecordException
+ */
+ public function fetchOrFail()
+ {
+ $record = $this->fetch();
+ if ($record === false) {
+ throw new NoRecordException();
+ }
+ return $record;
+ }
+
+ /**
+ * Lance la requête
+ * @return QueryResult
+ */
+ public function fetchAll(): QueryResult
+ {
+ return new QueryResult(
+ $this->execute()->fetchAll(\PDO::FETCH_ASSOC),
+ $this->entity
+ );
+ }
+
+ /**
+ * Pagine les résultats
+ * @param int $perPage
+ * @param int $currentPage
+ * @return Pagerfanta
+ */
+ public function paginate(int $perPage, int $currentPage = 1): Pagerfanta
+ {
+ $paginator = new PaginatedQuery($this);
+ return (new Pagerfanta($paginator))->setMaxPerPage($perPage)->setCurrentPage($currentPage);
+ }
+
+ /**
+ * Génère la requête SQL
+ * @return string
+ */
+ public function __toString()
+ {
+ $parts = ['SELECT'];
+ if ($this->select) {
+ $parts[] = join(', ', $this->select);
+ } else {
+ $parts[] = '*';
+ }
+ $parts[] = 'FROM';
+ $parts[] = $this->buildFrom();
+ if (!empty($this->joins)) {
+ foreach ($this->joins as $type => $joins) {
+ foreach ($joins as [$table, $condition]) {
+ $parts[] = strtoupper($type) . " JOIN $table ON $condition";
+ }
+ }
+ }
+ if (!empty($this->where)) {
+ $parts[] = "WHERE";
+ $parts[] = "(" . join(') AND (', $this->where) . ')';
+ }
+ if (!empty($this->order)) {
+ $parts[] = 'ORDER BY';
+ $parts[] = join(', ', $this->order);
+ }
+ if ($this->limit) {
+ $parts[] = 'LIMIT ' . $this->limit;
+ }
+ return join(' ', $parts);
+ }
+
+ /**
+ * Construit le FROM a as b ....
+ * @return string
+ */
+ private function buildFrom(): string
+ {
+ $from = [];
+ foreach ($this->from as $key => $value) {
+ if (is_string($key)) {
+ $from[] = "$key as $value";
+ } else {
+ $from[] = $value;
+ }
+ }
+ return join(', ', $from);
+ }
+
+ /**
+ * Exécute la requête
+ * @return \PDOStatement
+ */
+ private function execute()
+ {
+ $query = $this->__toString();
+ if (!empty($this->params)) {
+ $statement = $this->pdo->prepare($query);
+ $statement->execute($this->params);
+ return $statement;
+ }
+ return $this->pdo->query($query);
+ }
+
+ public function getIterator()
+ {
+ return $this->fetchAll();
+ }
+}
diff --git a/src/Framework/Database/QueryResult.php b/src/Framework/Database/QueryResult.php
new file mode 100644
index 0000000..f29624b
--- /dev/null
+++ b/src/Framework/Database/QueryResult.php
@@ -0,0 +1,171 @@
+records = $records;
+ $this->entity = $entity;
+ }
+
+ /**
+ * Récupère un éléments à l'index définit
+ * @param int $index
+ * @return mixed|null|string
+ */
+ public function get(int $index)
+ {
+ if ($this->entity) {
+ if (!isset($this->hydratedRecords[$index])) {
+ $this->hydratedRecords[$index] = Hydrator::hydrate($this->records[$index], $this->entity);
+ }
+ return $this->hydratedRecords[$index];
+ }
+ return $this->entity;
+ }
+
+ /**
+ * Return the current element
+ * @link http://php.net/manual/en/iterator.current.php
+ * @return mixed Can return any type.
+ * @since 5.0.0
+ */
+ public function current()
+ {
+ return $this->get($this->index);
+ }
+
+ /**
+ * Move forward to next element
+ * @link http://php.net/manual/en/iterator.next.php
+ * @return void Any returned value is ignored.
+ * @since 5.0.0
+ */
+ public function next(): void
+ {
+ $this->index++;
+ }
+
+ /**
+ * Return the key of the current element
+ * @link http://php.net/manual/en/iterator.key.php
+ * @return mixed scalar on success, or null on failure.
+ * @since 5.0.0
+ */
+ public function key()
+ {
+ return $this->index;
+ }
+
+ /**
+ * Checks if current position is valid
+ * @link http://php.net/manual/en/iterator.valid.php
+ * @return boolean The return value will be casted to boolean and then evaluated.
+ * Returns true on success or false on failure.
+ * @since 5.0.0
+ */
+ public function valid()
+ {
+ return isset($this->records[$this->index]);
+ }
+
+ /**
+ * Rewind the Iterator to the first element
+ * @link http://php.net/manual/en/iterator.rewind.php
+ * @return void Any returned value is ignored.
+ * @since 5.0.0
+ */
+ public function rewind()
+ {
+ $this->index = 0;
+ }
+
+ /**
+ * Whether a offset exists
+ * @link http://php.net/manual/en/arrayaccess.offsetexists.php
+ * @param mixed $offset
+ * An offset to check for.
+ *
+ * @return boolean true on success or false on failure.
+ *
+ *
+ * The return value will be casted to boolean if non-boolean was returned.
+ * @since 5.0.0
+ */
+ public function offsetExists($offset)
+ {
+ return isset($this->records[$offset]);
+ }
+
+ /**
+ * Offset to retrieve
+ * @link http://php.net/manual/en/arrayaccess.offsetget.php
+ * @param mixed $offset
+ * The offset to retrieve.
+ *
+ * @return mixed Can return all value types.
+ * @since 5.0.0
+ */
+ public function offsetGet($offset)
+ {
+ return $this->get($offset);
+ }
+
+ /**
+ * Offset to set
+ * @link http://php.net/manual/en/arrayaccess.offsetset.php
+ * @param mixed $offset
+ * The offset to assign the value to.
+ *
+ * @param mixed $value
+ * The value to set.
+ *
+ * @return void
+ * @throws \Exception
+ * @since 5.0.0
+ */
+ public function offsetSet($offset, $value)
+ {
+ throw new \Exception("Can't alter records");
+ }
+
+ /**
+ * Offset to unset
+ * @link http://php.net/manual/en/arrayaccess.offsetunset.php
+ * @param mixed $offset
+ * The offset to unset.
+ *
+ * @return void
+ * @throws \Exception
+ * @since 5.0.0
+ */
+ public function offsetUnset($offset)
+ {
+ throw new \Exception("Can't alter records");
+ }
+}
diff --git a/src/Framework/Database/Table.php b/src/Framework/Database/Table.php
new file mode 100644
index 0000000..f8a6243
--- /dev/null
+++ b/src/Framework/Database/Table.php
@@ -0,0 +1,198 @@
+pdo = $pdo;
+ }
+
+ /**
+ * Récupère une liste clef valeur de nos enregistrements
+ */
+ public function findList(): array
+ {
+ if ( $this->table === 'users') {
+ $champ = 'username';
+ } else {
+ $champ = 'name';
+ }
+ //die();
+ $results = $this->pdo
+ ->query("SELECT id, $champ FROM {$this->table}")
+ ->fetchAll(\PDO::FETCH_NUM);
+ $list = [];
+ foreach ($results as $result) {
+ $list[$result[0]] = $result[1];
+ }
+ return $list;
+ }
+
+ /**
+ * @return Query
+ */
+ public function makeQuery(): Query
+ {
+ return (new Query($this->pdo))
+ ->from($this->table, $this->table[0])
+ ->into($this->entity);
+ }
+
+ /**
+ * Récupère tous les enregistrements
+ *
+ * @return Query
+ */
+ public function findAll(): Query
+ {
+ return $this->makeQuery();
+ }
+
+ /**
+ * Récupère une ligne par rapport à un champ
+ *
+ * @param string $field
+ * @param string $value
+ * @return array
+ * @throws NoRecordException
+ */
+ public function findBy(string $field, string $value)
+ {
+ // echo "field = ".$field;
+ // die();
+ return $this->makeQuery()->where("$field = :field")->params(["field" => $value])->fetchOrFail();
+ }
+
+ /**
+ * Récupère un élément à partir de son ID
+ *
+ * @param int $id
+ * @return mixed
+ * @throws NoRecordException
+ */
+ public function find(int $id)
+ {
+ return $this->makeQuery()->where("id = $id")->fetchOrFail();
+ }
+
+ /**
+ * Récupère le nbre d'enregistrement
+ *
+ * @return int
+ */
+ public function count(): int
+ {
+ return $this->makeQuery()->count();
+ }
+
+ /**
+ * Met à jour un enregistrement au niveau de la base de données
+ *
+ * @param int $id
+ * @param array $params
+ * @return bool
+ */
+ public function update(int $id, array $params): bool
+ {
+ $fieldQuery = $this->buildFieldQuery($params);
+ $params["id"] = $id;
+ $query = $this->pdo->prepare("UPDATE {$this->table} SET $fieldQuery WHERE id = :id");
+ return $query->execute($params);
+ }
+
+ /**
+ * Crée un nouvel enregistrement
+ *
+ * @param array $params
+ * @return bool
+ */
+ public function insert(array $params): bool
+ {
+ $fields = array_keys($params);
+ $values = join(', ', array_map(function ($field) {
+ return ':' . $field;
+ }, $fields));
+ $fields = join(', ', $fields);
+ $query = $this->pdo->prepare("INSERT INTO {$this->table} ($fields) VALUES ($values)");
+ // echo 'query =';
+ // var_dump($query);
+ // die();
+ return $query->execute($params);
+ }
+
+ /**
+ * Supprime un enregistrment
+ * @param int $id
+ * @return bool
+ */
+ public function delete(int $id): bool
+ {
+ $query = $this->pdo->prepare("DELETE FROM {$this->table} WHERE id = ?");
+ return $query->execute([$id]);
+ }
+
+ private function buildFieldQuery(array $params)
+ {
+ return join(', ', array_map(function ($field) {
+ return "$field = :$field";
+ }, array_keys($params)));
+ }
+
+ /**
+ * @return mixed
+ */
+ public function getEntity(): string
+ {
+ return $this->entity;
+ }
+
+ /**
+ * @return string
+ */
+ public function getTable(): string
+ {
+ return $this->table;
+ }
+
+ /**
+ * Vérifie qu'un enregistrement existe
+ * @param $id
+ * @return bool
+ */
+ public function exists($id): bool
+ {
+ $query = $this->pdo->prepare("SELECT id FROM {$this->table} WHERE id = ?");
+ $query->execute([$id]);
+ return $query->fetchColumn() !== false;
+ }
+
+ /**
+ * @return \PDO
+ */
+ public function getPdo(): \PDO
+ {
+ return $this->pdo;
+ }
+}
diff --git a/src/Framework/Entity/Timestamp.php b/src/Framework/Entity/Timestamp.php
new file mode 100644
index 0000000..56bdb39
--- /dev/null
+++ b/src/Framework/Entity/Timestamp.php
@@ -0,0 +1,56 @@
+updatedAt;
+ }
+
+ /**
+ * @param \DateTime|string|null $datetime
+ */
+ public function setUpdatedAt($datetime): void
+ {
+ if (is_string($datetime)) {
+ $this->updatedAt = new \DateTime($datetime);
+ } else {
+ $this->updatedAt = $datetime;
+ }
+ }
+
+ /**
+ * @return \DateTime|null
+ */
+ public function getCreatedAt(): ?\DateTime
+ {
+ return $this->createdAt;
+ }
+
+ /**
+ * @param \DateTime|string|null $datetime
+ */
+ public function setCreatedAt($datetime): void
+ {
+ if (is_string($datetime)) {
+ $this->createdAt = new \DateTime($datetime);
+ } else {
+ $this->createdAt = $datetime;
+ }
+ }
+}
diff --git a/src/Framework/Middleware/CombinedMiddleware.php b/src/Framework/Middleware/CombinedMiddleware.php
new file mode 100644
index 0000000..3534bb5
--- /dev/null
+++ b/src/Framework/Middleware/CombinedMiddleware.php
@@ -0,0 +1,33 @@
+container = $container;
+ $this->middlewares = $middlewares;
+ }
+
+ public function process(ServerRequestInterface $request, DelegateInterface $delegate): ResponseInterface
+ {
+ $delegate = new CombinedMiddlewareDelegate($this->container, $this->middlewares, $delegate);
+ return $delegate->process($request);
+ }
+}
diff --git a/src/Framework/Middleware/CombinedMiddlewareDelegate.php b/src/Framework/Middleware/CombinedMiddlewareDelegate.php
new file mode 100644
index 0000000..30e5eec
--- /dev/null
+++ b/src/Framework/Middleware/CombinedMiddlewareDelegate.php
@@ -0,0 +1,71 @@
+middlewares = $middlewares;
+ $this->container = $container;
+ $this->delegate = $delegate;
+ }
+
+ public function process(ServerRequestInterface $request): ResponseInterface
+ {
+ $middleware = $this->getMiddleware();
+ if (is_null($middleware)) {
+ return $this->delegate->process($request);
+ } elseif (is_callable($middleware)) {
+ $response = call_user_func_array($middleware, [$request, [$this, 'process']]);
+ if (is_string($response)) {
+ return new Response(200, [], $response);
+ }
+ return $response;
+ } elseif ($middleware instanceof MiddlewareInterface) {
+ return $middleware->process($request, $this);
+ }
+ }
+
+ /**
+ * @return object
+ */
+ private function getMiddleware()
+ {
+ if (array_key_exists($this->index, $this->middlewares)) {
+ if (is_string($this->middlewares[$this->index])) {
+ $middleware = $this->container->get($this->middlewares[$this->index]);
+ } else {
+ $middleware = $this->middlewares[$this->index];
+ }
+ $this->index++;
+ return $middleware;
+ }
+ return null;
+ }
+}
diff --git a/src/Framework/Middleware/CsrfMiddleware.php b/src/Framework/Middleware/CsrfMiddleware.php
new file mode 100644
index 0000000..f857168
--- /dev/null
+++ b/src/Framework/Middleware/CsrfMiddleware.php
@@ -0,0 +1,113 @@
+validSession($session);
+ $this->session = &$session;
+ $this->formKey = $formKey;
+ $this->sessionKey = $sessionKey;
+ $this->limit = $limit;
+ }
+
+ public function process(ServerRequestInterface $request, DelegateInterface $delegate): ResponseInterface
+ {
+ if (in_array($request->getMethod(), ['POST', 'PUT', 'DELETE'])) {
+ $params = $request->getParsedBody() ?: [];
+ if (!array_key_exists($this->formKey, $params)) {
+ $this->reject();
+ } else {
+ $csrfList = $this->session[$this->sessionKey] ?? [];
+ if (in_array($params[$this->formKey], $csrfList)) {
+ $this->useToken($params[$this->formKey]);
+ return $delegate->process($request);
+ } else {
+ $this->reject();
+ }
+ }
+ } else {
+ return $delegate->process($request);
+ }
+ }
+
+ public function generateToken(): string
+ {
+ $token = bin2hex(random_bytes(16));
+ $csrfList = $this->session[$this->sessionKey] ?? [];
+ $csrfList[] = $token;
+ $this->session[$this->sessionKey] = $csrfList;
+ $this->limitTokens();
+ return $token;
+ }
+
+ private function reject(): void
+ {
+ throw new CsrfInvalidException();
+ }
+
+ private function useToken($token): void
+ {
+ $tokens = array_filter($this->session[$this->sessionKey], function ($t) use ($token) {
+ return $token !== $t;
+ });
+ $this->session[$this->sessionKey] = $tokens;
+ }
+
+ private function limitTokens(): void
+ {
+ $tokens = $this->session[$this->sessionKey] ?? [];
+ if (count($tokens) > $this->limit) {
+ array_shift($tokens);
+ }
+ $this->session[$this->sessionKey] = $tokens;
+ }
+
+ private function validSession($session)
+ {
+ if (!is_array($session) && !$session instanceof \ArrayAccess) {
+ throw new \TypeError('La session passé au middleware CSRF n\'est pas traitable comme un tableau');
+ }
+ }
+
+ /**
+ * @return string
+ */
+ public function getFormKey(): string
+ {
+ return $this->formKey;
+ }
+}
diff --git a/src/Framework/Middleware/DispatcherMiddleware.php b/src/Framework/Middleware/DispatcherMiddleware.php
new file mode 100644
index 0000000..a470fd9
--- /dev/null
+++ b/src/Framework/Middleware/DispatcherMiddleware.php
@@ -0,0 +1,37 @@
+container = $container;
+ }
+
+ public function process(ServerRequestInterface $request, DelegateInterface $delegate)
+ {
+ $route = $request->getAttribute(Router\Route::class);
+ if (is_null($route)) {
+ return $delegate->process($request);
+ }
+ $callback = $route->getCallback();
+ if (!is_array($callback)) {
+ $callback = [$callback];
+ }
+ return (new CombinedMiddleware($this->container, $callback))->process($request, $delegate);
+ }
+}
diff --git a/src/Framework/Middleware/MethodMiddleware.php b/src/Framework/Middleware/MethodMiddleware.php
new file mode 100644
index 0000000..8b1940f
--- /dev/null
+++ b/src/Framework/Middleware/MethodMiddleware.php
@@ -0,0 +1,21 @@
+getParsedBody();
+ if (array_key_exists('_method', $parsedBody) &&
+ in_array($parsedBody['_method'], ['DELETE', 'PUT'])
+ ) {
+ $request = $request->withMethod($parsedBody['_method']);
+ }
+ return $next->process($request);
+ }
+
+}
\ No newline at end of file
diff --git a/src/Framework/Middleware/NotFoundMiddleware.php b/src/Framework/Middleware/NotFoundMiddleware.php
new file mode 100644
index 0000000..0d92466
--- /dev/null
+++ b/src/Framework/Middleware/NotFoundMiddleware.php
@@ -0,0 +1,20 @@
+
+