add and fix conception
continuous-integration/drone/push Build is passing Details

pull/24/head
Override-6 1 year ago
parent 53b184afe0
commit 1bf3dfa3b6
Signed by untrusted user who does not match committer: maxime.batista
GPG Key ID: 8002CC4B4DD9ECA5

@ -0,0 +1,123 @@
# Conception
## Organisation
Notre projet est divisé en plusieurs parties:
- `src/API`, qui définit les classes qui implémentent les actions de lapi
- `src/App`, qui définit les contrôleurs et les vues de lapplication web
- `src/Core`, définit les modèles, les classes métiers, les librairies internes (validation, http), les gateways, en somme, les élements logiques de lapplication et les actions que lont peut faire avec.
- `sql`, définit la base de donnée utilisée, et éxécute les fichiers sql lorsque la base de donnée nest pas initialisée.
- `profiles`, définit les profiles dexecution, voir `Documentation/how-to-dev.md` pour plus dinfo
- `front` contient le code front-end react/typescript
- `ci` contient les scripts de déploiement et la définition du workflow dintégration continue et de déploiement constant vers notre staging server ([maxou.dev/<branch>/public/](https://maxou.dev/IQBall/master/public)).
- `public` point dentrée, avec :
- `public/index.php` point dentrée pour la webapp
- `public/api/index.php` point dentrée pour lapi.
## Backend
### Validation et résilience des erreurs
#### Motivation
Un controlleur a pour but de valider les données d'une requête avant de les manipuler.
Nous nous sommes rendu compte que la vérification des données d'une requête était redondante, même en factorisant les différents
types de validation, nous devions quand même explicitement vérifier la présence des champs utilisés dans la requête.
```php
public function doPostAction(array $form) {
$failures = [];
$req = new HttpRequest($form);
$email = $req['email'] ?? null;
if ($email == null) {
$failures[] = "Vous n'avez pas entré d'adresse email.";
return;
}
if (Validation::isEmail($email)) {
$failures[] = "Format d'adresse email invalide.";
}
if (Validation::isLenBetween($email, 6, 64))) {
$failures[] = "L'adresse email doit être d'une longueur comprise entre 6 et 64 charactères.";
}
if (!empty($failures)) {
return ViewHttpResponse::twig('error.html.twig', ['failures' => $failures]);
}
// traitement ...
}
```
Nous sommes obligés de tester à la main la présence des champs dans la requête, et nous avons une paire condition/erreur par type de validation,
ce qui, pour des requêtes avec plusieurs champs, peut vite devenir illisible si nous voulons être précis sur les erreurs.
Ici, une validation est une règle, un prédicat qui permet de valider une donnée sur un critère bien précis (injection html, adresse mail, longueur, etc.).
Bien souvent, lorsque le prédicat échoue, un message est ajouté à la liste des erreurs rencontrés, mais ce message est souvent le même, ce qui rajoute en plus
de la redondance sur les messages d'erreurs choisis lorsqu'une validation échoue.
#### Schéma
Toutes ces lignes de code pour définir que notre requête doit contenir un champ nommé `email`, dont la valeur est une adresse mail, d'une longueur comprise entre 6 et 64.
Nous avons donc trouvé l'idée de définir un système nous permettant de déclarer le schéma d'une requête,
et de reporter le plus d'erreurs possibles lorsqu'une requête ne valide pas le schéma :
```php
public function doPostAction(array $form): HttpResponse {
$failures = [];
$req = HttpRequest::from($form, $failures, [
'email' => [Validators::email(), Validators::isLenBetween(6, 64)]
]);
if (!empty($failures)) { //ou $req == null
return ViewHttpResponse::twig('error.html.twig', ['failures' => $failures])
}
// traitement ...
}
```
Ce système nous permet de faire de la programmation _déclarative_, nous disons à _quoi_ ressemble la forme de la requête que l'on souhaite,
plustot que de définir _comment_ réagir face à notre requête.
Ce système permet d'être beaucoup plus clair sur la forme attendue d'une requête, et de garder une lisibilité satisfaisante peu importe le nombre de
champs que celle-ci contient.
Nous pouvons ensuite emballer les erreurs de validation dans des `ValidationFail` et `FieldValidationFail`, ce qui permet ensuite d'obtenir
plus de précision sur une erreur, comme le nom du champ qui est invalidé, et qui permet ensuite à nos vues de pouvoir manipuler plus facilement
les erreurs et facilement entourer les champs invalides en rouge, ainsi que d'afficher toutes les erreurs que l'utilisateur a fait, d'un coup.
### HttpRequest, HttpResponse
Nous avons choisi d'encapsuler les requêtes et les réponses afin de faciliter leur manipulation.
Cela permet de mieux repérer les endroits du code qui manipulent une requête d'un simple tableau,
et de garder à un seul endroit la responsabilitée d'écrire le contenu de la requête vers client.
`src/App` définit une `ViewHttpResponse`, qui permet aux controlleurs de retourner la vue qu'ils ont choisit.
C'est ensuite à la classe `src/App/App` d'afficher la réponse.
### index.php
Il y a deux points d'entrés, celui de la WebApp (`public/index.php`), et celui de l'API (`public/api/index.php`).
Ces fichiers ont pour but de définir ce qui va être utilisé dans l'application, comme les routes, la base de donnée, les classes métiers utilisés,
comment gérer l'authentification dans le cadre de l'API, et une session dans le cadre de la web app.
L'index définit aussi quoi faire lorsque l'application retourne une réponse. Dans les implémentations actuelles, elle délègue simplement
l'affichage dans une classe (`IQBall\App\App` et `IQBall\API\API`).
### API
Nous avons définit une petite API (`src/Api`) pour nous permettre de faire des actions en arrière plan depuis le front end.
Par exemple, l'API permet de changer le nom d'une tactique, ou de sauvegarder son contenu.
C'est plus pratique de faire des requêtes en arrière-plan plustot que faire une requête directement à la partie WebApp, ce qui
aurait eu pour conséquences de recharger la page
## Frontend
### Utilisation de React
Notre application est une application de création et de visualisation de stratégies pour des match de basket.
Léditeur devra comporter un terrain sur lequel il est possible de placer et bouger des pions, représentant les joueurs.
Une stratégie est un arbre composé de plusieurs étapes, une étape étant constituée dun ensemble de joueurs et dadversaires sur le terrain,
aillant leur position de départ, et leur position cible, avec des flèches représentant le type de mouvement (dribble, écran, etc) effectué.
les enfants dune étape, sont dautres étapes en fonction des cas de figures (si tel joueur fait tel mouvement, ou si tel joueur fait telle passe etc).
Pour rendre le tout agréable à utiliser, il faut que linterface soit réactive : si lon bouge un joueur,
il faut que les flèches qui y sont liés bougent aussi, il faut que les joueurs puissent bouger le long des flèches en mode visualiseur etc…
Le front-end de léditeur et du visualiseur étant assez ambitieux, et occupant une place importante du projet, nous avons décidés de leffectuer en utilisant
le framework React qui rend simple le développement dinterfaces dynamiques, et dutiliser typescript parce quici on code bien et quon impose une type safety a notre code.

@ -3,11 +3,12 @@
require "../../config.php"; require "../../config.php";
require "../../vendor/autoload.php"; require "../../vendor/autoload.php";
require "../../sql/database.php"; require "../../sql/database.php";
require "../utils.php"; require "../../src/index-utils.php";
use IQBall\Api\API; use IQBall\Api\API;
use IQBall\Api\Controller\APIAuthController; use IQBall\Api\Controller\APIAuthController;
use IQBall\Api\Controller\APITacticController; use IQBall\Api\Controller\APITacticController;
use IQBall\App\Session\PhpSessionHandle;
use IQBall\Core\Action; use IQBall\Core\Action;
use IQBall\Core\Connection; use IQBall\Core\Connection;
use IQBall\Core\Data\Account; use IQBall\Core\Data\Account;
@ -34,4 +35,25 @@ function getRoutes(): AltoRouter {
return $router; return $router;
} }
Api::render(API::handleMatch(getRoutes()->match())); /**
* Defines the way of being authorised through the API
* By checking if an Authorisation header is set, and by expecting its value to be a valid token of an account.
* If the header is not set, fallback to the App's PHP session system, and try to extract the account from it.
* @return Account|null
* @throws Exception
*/
function tryGetAuthorization(): ?Account {
$headers = getallheaders();
// If no authorization header is set, try fallback to php session.
if (!isset($headers['Authorization'])) {
$session = PhpSessionHandle::init();
return $session->getAccount();
}
$token = $headers['Authorization'];
$gateway = new AccountGateway(new Connection(get_database()));
return $gateway->getAccountFromToken($token);
}
Api::render(API::handleMatch(getRoutes()->match(), fn() => tryGetAuthorization()));

@ -4,8 +4,8 @@
require "../vendor/autoload.php"; require "../vendor/autoload.php";
require "../config.php"; require "../config.php";
require "../sql/database.php"; require "../sql/database.php";
require "../src/utils.php";
require "../src/App/react-display.php"; require "../src/App/react-display.php";
require "../src/index-utils.php";
use IQBall\App\App; use IQBall\App\App;
use IQBall\App\Controller\AuthController; use IQBall\App\Controller\AuthController;
@ -13,6 +13,9 @@ use IQBall\App\Controller\EditorController;
use IQBall\App\Controller\TeamController; use IQBall\App\Controller\TeamController;
use IQBall\App\Controller\UserController; use IQBall\App\Controller\UserController;
use IQBall\App\Controller\VisualizerController; use IQBall\App\Controller\VisualizerController;
use IQBall\App\Session\MutableSessionHandle;
use IQBall\App\Session\PhpSessionHandle;
use IQBall\App\Session\SessionHandle;
use IQBall\App\ViewHttpResponse; use IQBall\App\ViewHttpResponse;
use IQBall\Core\Action; use IQBall\Core\Action;
use IQBall\Core\Connection; use IQBall\Core\Connection;
@ -25,9 +28,6 @@ use IQBall\Core\Http\HttpResponse;
use IQBall\Core\Model\AuthModel; use IQBall\Core\Model\AuthModel;
use IQBall\Core\Model\TacticModel; use IQBall\Core\Model\TacticModel;
use IQBall\Core\Model\TeamModel; use IQBall\Core\Model\TeamModel;
use IQBall\Core\Session\MutableSessionHandle;
use IQBall\Core\Session\PhpSessionHandle;
use IQBall\Core\Session\SessionHandle;
use IQBall\Core\Validation\ValidationFail; use IQBall\Core\Validation\ValidationFail;
function getConnection(): Connection { function getConnection(): Connection {
@ -68,8 +68,8 @@ function getRoutes(): AltoRouter {
$ar->map("POST", "/register", Action::noAuth(fn(SessionHandle $s) => getAuthController()->register($_POST, $s))); $ar->map("POST", "/register", Action::noAuth(fn(SessionHandle $s) => getAuthController()->register($_POST, $s)));
//user-related //user-related
$ar->map("GET", "/home", Action::auth(fn(SessionHandle $s) => getUserController()->home($s)));
$ar->map("GET", "/", Action::auth(fn(SessionHandle $s) => getUserController()->home($s))); $ar->map("GET", "/", Action::auth(fn(SessionHandle $s) => getUserController()->home($s)));
$ar->map("GET", "/home", Action::auth(fn(SessionHandle $s) => getUserController()->home($s)));
$ar->map("GET", "/settings", Action::auth(fn(SessionHandle $s) => getUserController()->settings($s))); $ar->map("GET", "/settings", Action::auth(fn(SessionHandle $s) => getUserController()->settings($s)));
//tactic-related //tactic-related

@ -4,12 +4,8 @@ namespace IQBall\Api;
use Exception; use Exception;
use IQBall\Core\Action; use IQBall\Core\Action;
use IQBall\Core\Connection;
use IQBall\Core\Data\Account;
use IQBall\Core\Gateway\AccountGateway;
use IQBall\Core\Http\HttpResponse; use IQBall\Core\Http\HttpResponse;
use IQBall\Core\Http\JsonHttpResponse; use IQBall\Core\Http\JsonHttpResponse;
use IQBall\Core\Session\PhpSessionHandle;
use IQBall\Core\Validation\ValidationFail; use IQBall\Core\Validation\ValidationFail;
class API { class API {
@ -28,12 +24,14 @@ class API {
} }
} }
/** /**
* @param mixed[] $match * @param array<string, mixed> $match
* @param callable $tryGetAuthorization function to return an authorisation object for the given action (if required)
* @return HttpResponse * @return HttpResponse
* @throws Exception * @throws Exception
*/ */
public static function handleMatch(array $match): HttpResponse { public static function handleMatch(array $match, callable $tryGetAuthorization): HttpResponse {
if (!$match) { if (!$match) {
return new JsonHttpResponse([ValidationFail::notFound("not found")]); return new JsonHttpResponse([ValidationFail::notFound("not found")]);
} }
@ -46,7 +44,7 @@ class API {
$auth = null; $auth = null;
if ($action->isAuthRequired()) { if ($action->isAuthRequired()) {
$auth = self::tryGetAuthorization(); $auth = call_user_func($tryGetAuthorization);
if ($auth == null) { if ($auth == null) {
return new JsonHttpResponse([ValidationFail::unauthorized("Missing or invalid 'Authorization' header.")]); return new JsonHttpResponse([ValidationFail::unauthorized("Missing or invalid 'Authorization' header.")]);
} }
@ -54,18 +52,4 @@ class API {
return $action->run($match['params'], $auth); return $action->run($match['params'], $auth);
} }
private static function tryGetAuthorization(): ?Account {
$headers = getallheaders();
// If no authorization header is set, try fallback to php session.
if (!isset($headers['Authorization'])) {
$session = PhpSessionHandle::init();
return $session->getAccount();
}
$token = $headers['Authorization'];
$gateway = new AccountGateway(new Connection(get_database()));
return $gateway->getAccountFromToken($token);
}
} }

@ -38,7 +38,7 @@ class APIAuthController {
} }
return new JsonHttpResponse(["authorization" => $account->getToken()]); return new JsonHttpResponse(["authorization" => $account->getToken()]);
}, true); });
} }
} }

@ -43,6 +43,6 @@ class APITacticController {
} }
return HttpResponse::fromCode(HttpCodes::OK); return HttpResponse::fromCode(HttpCodes::OK);
}, true); });
} }
} }

@ -2,10 +2,10 @@
namespace IQBall\App; namespace IQBall\App;
use IQBall\App\Session\MutableSessionHandle;
use IQBall\Core\Action; use IQBall\Core\Action;
use IQBall\Core\Http\HttpResponse; use IQBall\Core\Http\HttpResponse;
use IQBall\Core\Http\JsonHttpResponse; use IQBall\Core\Http\JsonHttpResponse;
use IQBall\Core\Session\MutableSessionHandle;
use Twig\Environment; use Twig\Environment;
use Twig\Error\LoaderError; use Twig\Error\LoaderError;
use Twig\Error\RuntimeError; use Twig\Error\RuntimeError;

@ -16,22 +16,17 @@ class Control {
* @param array<string, Validator[]> $schema an array of `fieldName => Validators` which represents the request object schema * @param array<string, Validator[]> $schema an array of `fieldName => Validators` which represents the request object schema
* @param callable(HttpRequest): HttpResponse $run the callback to run if the request is valid according to the given schema. * @param callable(HttpRequest): HttpResponse $run the callback to run if the request is valid according to the given schema.
* THe callback must accept an HttpRequest, and return an HttpResponse object. * THe callback must accept an HttpRequest, and return an HttpResponse object.
* @param bool $errorInJson if set to true, the returned response, in case of errors, will be a JsonHttpResponse, instead
* of the ViewHttpResponse for an error view.
* @return HttpResponse * @return HttpResponse
*/ */
public static function runChecked(array $schema, callable $run, bool $errorInJson): HttpResponse { public static function runChecked(array $schema, callable $run): HttpResponse {
$request_body = file_get_contents('php://input'); $request_body = file_get_contents('php://input');
$payload_obj = json_decode($request_body); $payload_obj = json_decode($request_body);
if (!$payload_obj instanceof \stdClass) { if (!$payload_obj instanceof \stdClass) {
$fail = new ValidationFail("bad-payload", "request body is not a valid json object"); $fail = new ValidationFail("bad-payload", "request body is not a valid json object");
if ($errorInJson) {
return new JsonHttpResponse([$fail, HttpCodes::BAD_REQUEST]);
}
return ViewHttpResponse::twig("error.html.twig", ["failures" => [$fail]], HttpCodes::BAD_REQUEST); return ViewHttpResponse::twig("error.html.twig", ["failures" => [$fail]], HttpCodes::BAD_REQUEST);
} }
$payload = get_object_vars($payload_obj); $payload = get_object_vars($payload_obj);
return self::runCheckedFrom($payload, $schema, $run, $errorInJson); return self::runCheckedFrom($payload, $schema, $run);
} }
/** /**
@ -40,18 +35,13 @@ class Control {
* @param array<string, Validator[]> $schema an array of `fieldName => Validators` which represents the request object schema * @param array<string, Validator[]> $schema an array of `fieldName => Validators` which represents the request object schema
* @param callable(HttpRequest): HttpResponse $run the callback to run if the request is valid according to the given schema. * @param callable(HttpRequest): HttpResponse $run the callback to run if the request is valid according to the given schema.
* THe callback must accept an HttpRequest, and return an HttpResponse object. * THe callback must accept an HttpRequest, and return an HttpResponse object.
* @param bool $errorInJson if set to true, the returned response, in case of errors, will be a JsonHttpResponse, instead
* of the ViewHttpResponse for an error view.
* @return HttpResponse * @return HttpResponse
*/ */
public static function runCheckedFrom(array $data, array $schema, callable $run, bool $errorInJson): HttpResponse { public static function runCheckedFrom(array $data, array $schema, callable $run): HttpResponse {
$fails = []; $fails = [];
$request = HttpRequest::from($data, $fails, $schema); $request = HttpRequest::from($data, $fails, $schema);
if (!empty($fails)) { if (!empty($fails)) {
if ($errorInJson) {
return new JsonHttpResponse($fails, HttpCodes::BAD_REQUEST);
}
return ViewHttpResponse::twig("error.html.twig", ['failures' => $fails], HttpCodes::BAD_REQUEST); return ViewHttpResponse::twig("error.html.twig", ['failures' => $fails], HttpCodes::BAD_REQUEST);
} }

@ -2,12 +2,11 @@
namespace IQBall\App\Controller; namespace IQBall\App\Controller;
use IQBall\App\Session\MutableSessionHandle;
use IQBall\App\ViewHttpResponse;
use IQBall\Core\Http\HttpRequest; use IQBall\Core\Http\HttpRequest;
use IQBall\Core\Http\HttpResponse; use IQBall\Core\Http\HttpResponse;
use IQBall\App\ViewHttpResponse;
use IQBall\Core\Model\AuthModel; use IQBall\Core\Model\AuthModel;
use IQBall\Core\Session\MutableSessionHandle;
use IQBall\Core\Validation\ValidationFail;
use IQBall\Core\Validation\Validators; use IQBall\Core\Validation\Validators;
class AuthController { class AuthController {

@ -2,13 +2,13 @@
namespace IQBall\App\Controller; namespace IQBall\App\Controller;
use IQBall\App\Session\SessionHandle;
use IQBall\App\Validator\TacticValidator;
use IQBall\App\ViewHttpResponse;
use IQBall\Core\Data\TacticInfo; use IQBall\Core\Data\TacticInfo;
use IQBall\Core\Http\HttpCodes; use IQBall\Core\Http\HttpCodes;
use IQBall\Core\Http\HttpResponse;
use IQBall\App\ViewHttpResponse;
use IQBall\Core\Model\TacticModel; use IQBall\Core\Model\TacticModel;
use IQBall\Core\Session\SessionHandle; use IQBall\Core\Validation\ValidationFail;
use IQBall\Core\Validator\TacticValidator;
class EditorController { class EditorController {
private TacticModel $model; private TacticModel $model;
@ -44,7 +44,7 @@ class EditorController {
public function openEditor(int $id, SessionHandle $session): ViewHttpResponse { public function openEditor(int $id, SessionHandle $session): ViewHttpResponse {
$tactic = $this->model->get($id); $tactic = $this->model->get($id);
$failure = TacticValidator::validateAccess($tactic, $id, $session->getAccount()->getId()); $failure = TacticValidator::validateAccess($id, $tactic, $session->getAccount()->getId());
if ($failure != null) { if ($failure != null) {
return ViewHttpResponse::twig('error.html.twig', ['failures' => [$failure]], HttpCodes::NOT_FOUND); return ViewHttpResponse::twig('error.html.twig', ['failures' => [$failure]], HttpCodes::NOT_FOUND);

@ -2,11 +2,11 @@
namespace IQBall\App\Controller; namespace IQBall\App\Controller;
use IQBall\App\Session\SessionHandle;
use IQBall\App\ViewHttpResponse;
use IQBall\Core\Http\HttpRequest; use IQBall\Core\Http\HttpRequest;
use IQBall\Core\Http\HttpResponse; use IQBall\Core\Http\HttpResponse;
use IQBall\App\ViewHttpResponse;
use IQBall\Core\Model\TeamModel; use IQBall\Core\Model\TeamModel;
use IQBall\Core\Session\SessionHandle;
use IQBall\Core\Validation\FieldValidationFail; use IQBall\Core\Validation\FieldValidationFail;
use IQBall\Core\Validation\Validators; use IQBall\Core\Validation\Validators;

@ -2,10 +2,9 @@
namespace IQBall\App\Controller; namespace IQBall\App\Controller;
use IQBall\Core\Http\HttpResponse; use IQBall\App\Session\SessionHandle;
use IQBall\App\ViewHttpResponse; use IQBall\App\ViewHttpResponse;
use IQBall\Core\Model\TacticModel; use IQBall\Core\Model\TacticModel;
use IQBall\Core\Session\SessionHandle;
class UserController { class UserController {
private TacticModel $tactics; private TacticModel $tactics;

@ -2,12 +2,12 @@
namespace IQBall\App\Controller; namespace IQBall\App\Controller;
use IQBall\App\Session\SessionHandle;
use IQBall\App\Validator\TacticValidator;
use IQBall\App\ViewHttpResponse;
use IQBall\Core\Http\HttpCodes; use IQBall\Core\Http\HttpCodes;
use IQBall\Core\Http\HttpResponse; use IQBall\Core\Http\HttpResponse;
use IQBall\App\ViewHttpResponse;
use IQBall\Core\Model\TacticModel; use IQBall\Core\Model\TacticModel;
use IQBall\Core\Session\SessionHandle;
use IQBall\Core\Validator\TacticValidator;
class VisualizerController { class VisualizerController {
private TacticModel $tacticModel; private TacticModel $tacticModel;
@ -28,7 +28,7 @@ class VisualizerController {
public function openVisualizer(int $id, SessionHandle $session): HttpResponse { public function openVisualizer(int $id, SessionHandle $session): HttpResponse {
$tactic = $this->tacticModel->get($id); $tactic = $this->tacticModel->get($id);
$failure = TacticValidator::validateAccess($tactic, $id, $session->getAccount()->getId()); $failure = TacticValidator::validateAccess($id, $tactic, $session->getAccount()->getId());
if ($failure != null) { if ($failure != null) {
return ViewHttpResponse::twig('error.html.twig', ['failures' => [$failure]], HttpCodes::NOT_FOUND); return ViewHttpResponse::twig('error.html.twig', ['failures' => [$failure]], HttpCodes::NOT_FOUND);

@ -1,6 +1,6 @@
<?php <?php
namespace IQBall\Core\Session; namespace IQBall\App\Session;
use IQBall\Core\Data\Account; use IQBall\Core\Data\Account;

@ -1,6 +1,6 @@
<?php <?php
namespace IQBall\Core\Session; namespace IQBall\App\Session;
use IQBall\Core\Data\Account; use IQBall\Core\Data\Account;

@ -1,6 +1,6 @@
<?php <?php
namespace IQBall\Core\Session; namespace IQBall\App\Session;
use IQBall\Core\Data\Account; use IQBall\Core\Data\Account;

@ -1,26 +1,19 @@
<?php <?php
namespace IQBall\Core\Validator; namespace IQBall\App\Validator;
use IQBall\Core\Data\TacticInfo; use IQBall\Core\Data\TacticInfo;
use IQBall\Core\Validation\ValidationFail; use IQBall\Core\Validation\ValidationFail;
class TacticValidator { class TacticValidator {
/** public static function validateAccess(int $tacticId, ?TacticInfo $tactic, int $ownerId): ?ValidationFail {
* @param TacticInfo|null $tactic
* @param int $tacticId
* @param int $ownerId
* @return ValidationFail|null
*/
public static function validateAccess(?TacticInfo $tactic, int $tacticId, int $ownerId): ?ValidationFail {
if ($tactic == null) { if ($tactic == null) {
return ValidationFail::notFound("La tactique $tacticId n'existe pas"); return ValidationFail::notFound("La tactique $tacticId n'existe pas");
} }
if ($tactic->getOwnerId() != $ownerId) { if ($tactic->getOwnerId() != $ownerId) {
return ValidationFail::unauthorized("Vous ne pouvez pas accéder à cette tactique."); return ValidationFail::unauthorized("Vous ne pouvez pas accéder à cette tactique.");
} }
return null; return null;
} }
} }

@ -17,7 +17,10 @@ class ComposedValidator extends Validator {
public function validate(string $name, $val): array { public function validate(string $name, $val): array {
$firstFailures = $this->first->validate($name, $val); $firstFailures = $this->first->validate($name, $val);
$thenFailures = [];
if (empty($firstFailures)) {
$thenFailures = $this->then->validate($name, $val); $thenFailures = $this->then->validate($name, $val);
}
return array_merge($firstFailures, $thenFailures); return array_merge($firstFailures, $thenFailures);
} }
} }

@ -13,7 +13,7 @@ abstract class Validator {
/** /**
* Creates a validator composed of this validator, and given validator * Creates a validator composed of this validator, and given validator
* @param Validator $other the second validator to chain with * @param Validator $other the second validator to validate if this validator succeeded
* @return Validator a composed validator * @return Validator a composed validator
*/ */
public function then(Validator $other): Validator { public function then(Validator $other): Validator {

Loading…
Cancel
Save