Merge remote-tracking branch 'origin/remove-php' into settings
continuous-integration/drone/push Build is failing Details

d_yanis 1 year ago
commit b3490bd9c2

@ -1,2 +1,2 @@
VITE_API_ENDPOINT=/api
VITE_BASE=
VITE_API_ENDPOINT=https://iqball.maxou.dev/api/dotnet-master
#VITE_API_ENDPOINT=http://localhost:5254

@ -0,0 +1,20 @@
module.exports = {
root: true,
env: { browser: true, es2021: true },
extends: [
"eslint:recommended",
"plugin:@typescript-eslint/recommended",
"plugin:react-hooks/recommended",
"plugin:react/recommended",
"plugin:react/jsx-runtime",
],
ignorePatterns: ["dist", ".eslintrc.cjs"],
parser: "@typescript-eslint/parser",
plugins: ["react-refresh"],
rules: {
"react-refresh/only-export-components": [
"warn",
{ allowConstantExport: true },
],
},
}

61
.gitignore vendored

@ -1,44 +1,25 @@
.vs
.vscode
.idea
.code
.vite
vendor
.nfs*
composer.lock
*.phar
/dist
.guard
# sqlite database files
*.sqlite
views-mappings.php
# See https://help.github.com/articles/ignoring-files/ for more about ignoring files.
# dependencies
/node_modules
/.pnp
.pnp.js
package-lock.json
# testing
/coverage
# production
/build
# misc
.DS_Store
.env.local
.env.development.local
.env.test.local
.env.production.local
# Logs
logs
*.log
npm-debug.log*
yarn-debug.log*
yarn-error.log*
pnpm-debug.log*
lerna-debug.log*
node_modules
dist
dist-ssr
*.local
.php-cs-fixer.cache
# Editor directories and files
.vscode/*
.idea
.DS_Store
*.suo
*.ntvs*
*.njsproj
*.sln
*.sw?
package-lock.json

@ -1,16 +0,0 @@
<?php
$finder = (new PhpCsFixer\Finder())->in(__DIR__);
return (new PhpCsFixer\Config())
->setRules([
'@PER-CS' => true,
'@PHP74Migration' => true,
'array_syntax' => ['syntax' => 'short'],
'braces_position' => [
'classes_opening_brace' => 'same_line',
'functions_opening_brace' => 'same_line'
]
])
->setIndent(" ")
->setFinder($finder);

@ -1,7 +1,7 @@
{
"bracketSameLine": true,
"trailingComma": "all",
"printWidth": 80,
"tabWidth": 4,
"semi": false
"bracketSameLine": true,
"trailingComma": "all",
"printWidth": 80,
"tabWidth": 4,
"semi": false
}

@ -4,21 +4,23 @@
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.
- `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
@ -56,6 +58,7 @@ Bien souvent, lorsque le prédicat échoue, un message est ajouté à la liste d
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 :
@ -64,7 +67,7 @@ et de reporter le plus d'erreurs possibles lorsqu'une requête ne valide pas le
public function doPostAction(array $form): HttpResponse {
$failures = [];
$req = HttpRequest::from($form, $failures, [
'email' => [Validators::email(), Validators::isLenBetween(6, 64)]
'email' => [DefaultValidators::email(), DefaultValidators::isLenBetween(6, 64)]
]);
if (!empty($failures)) { //ou $req == null
@ -74,6 +77,7 @@ public function doPostAction(array $form): HttpResponse {
// 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
@ -84,6 +88,7 @@ plus de précision sur une erreur, comme le nom du champ qui est invalidé, et q
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.
@ -92,6 +97,7 @@ et de garder à un seul endroit la responsabilitée d'écrire le contenu de la r
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.
@ -100,12 +106,12 @@ L'index définit aussi quoi faire lorsque l'application retourne une réponse. D
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
@ -120,4 +126,3 @@ il faut que les flèches qui y sont liés bougent aussi, il faut que les joueurs
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.

@ -0,0 +1,90 @@
# Welcome on the documentation's description
## Let's get started with the architecture diagram.
![architecture diagram](./assets/architecture.svg)
As you can see our entire application is build around three main package.
All of them contained in "src" package.
The core represent the main code of the web application.
It contains all the validation protocol, detailed below, the model of the imposed MVC architecture.
It also has a package named "data", it is a package of the structure of all the data we use in our application.
Of course there is package containing all the gateways as its name indicates. It is where we use the connection to our database.
Allowing to operate on it.
The App now is more about the web application itself.
Having all the controllers of the MVC architecture the use the model, the validation system and the http system in the core.
It also calls the twig's views inside of App. Finally, it uses the package Session. This one replace the $\_SESSION we all know in PHP.
Thanks to this we have a way cleaner use of all session's data.
Nevertheless, all the controllers call not only twig views but also react ones.
Those are present in the package "front", dispatched in several other packages.
Such as assets having all the image and stuff, model containing all the data's structure, style centralizing all css file and eventually components the last package used for the editor.
Finally, we have the package "Api" that allows to share code and bind all the different third-hand application such as the web admin one.
## Main data class diagram.
![Class diagram](./assets/models.svg)
You can see how our data is structured contained in the package "data" as explained right above.
There is two clear part.
First of all, the Tactic one.
We got a nice class named TacticInfo representing as it says the information about a tactic, nothing to discuss more about.
It associates an attribute of type "CourtType". This last is just an "evoluated" type of enum with some more features.
We had to do it this way because of the language PHP that doesn't implement such a thing as an enum.
Now, let's discuss a much bigger part of the diagram.
In this part we find all the team logic. Actually, a team only have an array of members and a "TeamInfo".
The class "TeamInfo" only exists to split the team's information data (name, id etc) from the members.
The type Team does only link the information about a team and its members.
Talking about them, their class indicate what role they have (either Coach or Player) in the team.
Because a member is registered in the app, therefore he is a user of it. Represented by the type of the same name.
This class does only contain all the user's basic information.
The last class we have is the Account. It could directly be incorporated in User but we decided to split it the same way we did for the team.
Then, Account only has a user and a token which is an identifier.
## Validation's class diagram
![validation's class diagram](./assets/validation.svg)
We implemented our own validation system, here it is!
For the validation methods (for instance those in DefaultValidators) we use lambda to instantiate a Validator.
In general, we use the implementation "SimpleFunctionValidator".
We reconize the strategy pattern. Indeed, we need a family of algorithms because we have many classes that only differ by the way they validate.
Futhermore, you may have notices the ComposedValidator that allows to chain several Validator.
We can see that this system uses the composite pattern
The other part of the diagram is about the failure a specific field's validation.
We have a concrete class to return a something more general. All the successors are just more precise about the failure.
## Http's class diagram
![Http's class diagram](./assets/http.svg)
It were we centralize what the app can render, and what the api can receive.
Then, we got the "basic" response (HttpResponse) that just render a HttpCodes.
We have two successors for now. ViewHttpResponse render not only a code but also a view, either react or twig ones.
Finally, we have the JsonHttpResponse that renders, as it's name says, some Json.
## Session's class diagram
![Session's class diagram](./assets/session.svg)
It encapsulates the PHP's array "$\_SESSION". With two interfaces that dictate how a session should be handled, and same for a mutable one.
## Model View Controller
All class diagram, separated by their range of action, of the imposed MVC architecture.
All of them have a controller that validates entries with the validation system and check the permission the user has,and whether or not actually do the action.
These controllers are composed by a Model that handle the pure data and is the point of contact between these and the gateways.
Speaking of which, Gateways are composing Models. They use the connection class to access the database and send their query.
### Team
![team's mvc](./assets/team.svg)
### Editor
![editor's mvc](./assets/editor.svg)
### Authentification
![auth's mvc](./assets/auth.svg)

@ -1,3 +1,3 @@
# The wiki also exists
Some of our explanation are contained in the [wiki](https://codefirst.iut.uca.fr/git/IQBall/Application-Web/wiki)
- [Description.md](Description.md)
- [Conception.md](Conception.md)
- [how-to-dev.md](how-to-dev.md)

@ -0,0 +1,60 @@
@startuml
'https://plantuml.com/component-diagram
package front{
package assets
package components
package model
package style
package views
}
database sql{
}
package src {
package "Api"{
}
package "App" {
package Controller
package Session
package Views
}
package Core{
package Data
package Gateway
package Http
package Model
package Validation
[Connection]
}
}
[sql] -- [Connection]
[views] -- [style]
[views] -- [components]
[views] -- [assets]
[views] -- [model]
[Gateway] -- [Connection]
[Validation] -- [Controller]
[Controller] -- [Session]
[Controller] -- [Http]
[Controller] -- [Views]
[Controller] -- [views]
[Controller] -- [Model]
[Model] -- [Gateway]
[Api] -- [Validation]
[Api] -- [Model]
[Api] -- [Http]
@enduml

File diff suppressed because one or more lines are too long

After

Width:  |  Height:  |  Size: 16 KiB

File diff suppressed because one or more lines are too long

After

Width:  |  Height:  |  Size: 14 KiB

File diff suppressed because one or more lines are too long

After

Width:  |  Height:  |  Size: 19 KiB

File diff suppressed because one or more lines are too long

After

Width:  |  Height:  |  Size: 24 KiB

File diff suppressed because one or more lines are too long

After

Width:  |  Height:  |  Size: 32 KiB

File diff suppressed because one or more lines are too long

After

Width:  |  Height:  |  Size: 8.7 KiB

File diff suppressed because one or more lines are too long

After

Width:  |  Height:  |  Size: 29 KiB

File diff suppressed because one or more lines are too long

After

Width:  |  Height:  |  Size: 32 KiB

@ -5,9 +5,9 @@ object Account {
name
age
email
phoneNumber
passwordHash
profilePicture
phone_number
password_hash
profile_picture
}
object TacticFolder {

@ -1,19 +1,21 @@
# THIS FILE IS DEPRECATED
See [#107](https://codefirst.iut.uca.fr/git/IQBall/Application-Web/pulls/107) for more details
---
This documentation file explains how to start a development server on your
machine, and how it works under the hood.
# How to run the project on my local computer
1. Use phpstorm to run a local php server:
1) Use phpstorm to run a local php server:
* Go to configuration > add new configuration
* Select "PHP Built-in Web Server", then enter options as follow:
![](assets/php-server-config.png)
- port 8080
- name the configuration "RunServer" to be more explicit
- place the "Document Root" in `/public`
- host is localhost
* Click apply, OK
* Now run it.
- Go to configuration > add new configuration
- Select "PHP Built-in Web Server", then enter options as follow:
![](assets/php-server-config.png) - port 8080 - name the configuration "RunServer" to be more explicit - place the "Document Root" in `/public` - host is localhost
- Click apply, OK
- Now run it.
If you go to `http://localhost:8080` you'll see a blank page.
This is expected ! On your browser, open inspection tab (ctrl+shift+i) go to network and refresh the page.
@ -39,12 +41,14 @@ Now refresh your page, you should now see all request being fulfilled and a form
Caution: **NEVER** directly connect on the `localhost:5173` node development server, always pass through the php (`localhost:8080`) server.
# How it works
I'm glad you are interested in how that stuff works, it's a bit tricky, lets go.
If you look at our `index.php` (located in `/public` folder), you'll see that it define our routes, it uses an `AltoRouter` then delegates the request's action processing to a controller.
We can see that there are two registered routes, the `GET:/` (that then calls `SampleFormController#displayForm()`) and `POST:/result` (that calls `SampleFormController#displayResults()`).
Implementation of those two methods are very simple: there is no verification to make nor model to control, thus they directly sends the view back to the client.
here's the implementation of the `SampleFormController`
```php
require_once "react-display.php";
class SampleFormController {
@ -63,7 +67,7 @@ As our views are now done using react (and defined under the `front/views` folde
If you look at the `send_react_front($viewURI, $viewArguments)` function, you'll see that is simply loads the file `src/react-display-file.php` with given arguments.
The file is a simple html5 template with a `<script>` block in the `<body>` section.
The script block imports the requested view and will render it.
The view entry is a function, named in PascalCase, which __must__ be be exported by default (`export default function MyReactView(args: {..})`).
The view entry is a function, named in PascalCase, which **must** be be exported by default (`export default function MyReactView(args: {..})`).
```html
<!--
@ -72,9 +76,9 @@ imports the given view URL, and assume that the view exports a function named `C
see ViewRenderer.tsx::renderView for more info
-->
<script type="module">
import {renderView} from "<?= asset("ViewRenderer.tsx") ?>"
import Component from "<?= asset($url) ?>"
renderView(Component, <?= json_encode($arguments) ?>)
import {renderView} from "<?= asset("ViewRenderer.tsx") ?>"
import Component from "<?= asset($url) ?>"
renderView(Component, <?= json_encode($arguments) ?>)
</script>
```
@ -86,6 +90,7 @@ This method then uses the `send_react_front`, to render the `views/SampleForm.ts
The view file **must export by default its react function component**.
## Server Profiles
If you go on the staging server, you'll see that, for the exact same request equivalent, the generated `src/render-display-file` file changes :
![](assets/staging-server-render-react-php-file-processed.png)
(we can also see that much less files are downloaded than with our localhost aka development server).
@ -101,6 +106,7 @@ By default, the `/config.php` file uses the `dev-config-profile.php` profile,
the file is replaced with `prod-config-file.php` by the CI when deploying to the staging server (see the pipeline "prepare php" step in `/ci/.drone.yml`)
The two profiles declares an `_asset(string $uri)` function, used by the `/config.php::asset` method, but with different implementations :
### Development profile
```php
@ -112,9 +118,11 @@ function _asset(string $assetURI): string {
return $front_url . "/" . $assetURI;
}
```
The simplest profile, simply redirect all assets to the development server
### Production profile
Before the CD workflow step deploys the generated files to the server,
it generates a `/views-mappings.php` file that will map the react views file names to their compiled javascript files :
@ -126,7 +134,9 @@ const ASSETS = [
... // other files that does not have to be directly used by the `send_react_front()` function
];
```
The `_asset` function will then get the right javascript for the given typescript file.
```php
require "../views-mappings.php";

@ -15,37 +15,51 @@ class HttpRequest implements ArrayAccess {
class HttpResponse {
- code: int
+ __construct(code: int)
- headers : array
+ __construct(code: int,headers:array)
+ getCode(): int
<u>fromCode(code: int): HttpResponse
+ getHeaders(): array
+ redirect(url:string, code:int): HttpResponse {static}
+ fromCode(code: int): HttpResponse {static}
}
class JsonHttpResponse extends HttpResponse {
- payload: mixed
+ __construct(payload: mixed, code: int = HttpCodes::OK)
+ __construct(payload: mixed, code: int)
+ getJson(): string
}
class ViewHttpResponse extends HttpResponse {
+ <u>TWIG_VIEW: int {frozen}
+ <u>REACT_VIEW: int {frozen}
+ TWIG_VIEW: int {frozen} {static}
+ REACT_VIEW: int {frozen} {static}
- file: string
- arguments: array
- kind: int
- __construct(kind: int, file: string, arguments: array, code: int = HttpCodes::OK)
- __construct(kind: int, file: string, arguments: array, code: int)
+ getViewKind(): int
+ getFile(): string
+ getArguments(): array
+ <u>twig(file: string, arguments: array, code: int = HttpCodes::OK): ViewHttpResponse
+ <u>react(file: string, arguments: array, code: int = HttpCodes::OK): ViewHttpResponse
+ <u>twig(file: string, arguments: array, code: int): ViewHttpResponse
+ <u>react(file: string, arguments: array, code: int): ViewHttpResponse
}
note right of ViewHttpResponse
Into src/App
end note
class HttpCodes{
+ OK : int {static} {frozen}
+ FOUND : int {static} {frozen}
+ BAD_REQUEST : int {static} {frozen}
+ UNAUTHORIZED : int {static} {frozen}
+ FORBIDDEN : int {static} {frozen}
+ NOT_FOUND : int {static} {frozen}
}
HttpCodes <.. ViewHttpResponse
HttpCodes <.. HttpResponse
@enduml

@ -7,69 +7,92 @@ class TacticInfo {
- ownerId: string
- content: string
+ __construct(id:int,name:string,creationDate:int,ownerId:int,courtType:CourtType,content:string)
+ getId(): int
+ getOwnerId(): int
+ getCreationTimestamp(): int
+ getName(): string
+ getContent(): string
+ getCourtType() : CourtType
}
TacticInfo -->"- courtType" CourtType
class CourtType{
- value : int
- COURT_PLAIN : int {static} {frozen}
- COURT_HALF : int {static} {frozen}
- __construct(val:int)
+ plain() : CourtType {static}
+ half() : CourtType {static}
+ name() : string
+ fromName(name:string) : CourtType
+ isPlain() : bool
+ isHalf() : bool
}
note bottom: Basically an evoluated enum
class Account {
- email: string
- token: string
- name: string
- id: int
+ getMailAddress(): string
+ getToken(): string
+ getName(): string
+ getId(): int
+ __construct(token:string,user:User)
+ getUser() : User
+ getToken() : string
}
Account -->"- user" User
class Member {
- userId: int
- teamId: int
- role : string
+ __construct(role : MemberRole)
+ getUserId(): int
+ __construct(role : string)
+ getUser(): User
+ getTeamId(): int
+ getRole(): MemberRole
+ getRole(): string
}
Member --> "- role" MemberRole
enum MemberRole {
PLAYER
COACH
}
note bottom: Member's role is either "Coach" or "Player"
Member -->"- user" User
class TeamInfo {
- creationDate: int
- name: string
- picture: string
- mainColor : string
- secondColor : string
+ __construct(id:int,name:string,picture:string,mainColor:string,secondColor:string)
+ getName(): string
+ getPicture(): string
+ getMainColor(): Color
+ getSecondColor(): Color
+ getMainColor(): string
+ getSecondColor(): string
}
TeamInfo --> "- mainColor" Color
TeamInfo --> "- secondaryColor" Color
note left: Both team's color are the hex code of the color
class Team {
getInfo(): TeamInfo
listMembers(): Member[]
+ __construct(info:TeamInfo,members: Member[])
+ getInfo(): TeamInfo
+ listMembers(): Member[]
}
Team --> "- info" TeamInfo
Team --> "- members *" Member
class Color {
- value: int
+ getValue(): int
class User{
- id : int
- name : string
- email : string
- profilePicture : string
+ __construct(id : int,name : string,email: string,profilePicture:string)
+ getId() : id
+ getName() : string
+ getEmail() : string
+ getProfilePicture() : string
}
@enduml

@ -7,26 +7,26 @@ class AuthController {
+ displayLogin() : HttpResponse
+ login(request : array , session : MutableSessionHandle) : HttpResponse
}
AuthController --> "- model" AuthModel
AuthController *-- "- model" AuthModel
class AuthModel {
+__construct(gateway : AccountGateway)
+ register(username : string, password : string, confirmPassword : string, email : string, failures : array): Account
+ generateToken() : string
+ login(email : string, password : string)
+ __construct(gateway : AccountGateway)
+ register(username:string, password:string, confirmPassword:string, email:string, &failures:array): ?Account + generateToken() : string
+ generateToken(): string
+ login(email:string, password:string, &failures:array): ?Account
}
AuthModel --> "- gateway" AccountGateway
class AccountGateway {
-con : Connection
+__construct(con : Connection)
+ insertAccount(name : string, email : string, hash : string, token : string) : int
+ getRowsFromMail(email : string): array
+ getHash(email : string) : array
+ exists(email : string) : bool
+ getAccountFromMail(email : string ): Account
+ getAccountFromToken(email : string ): Account
AuthModel *-- "- gateway" AccountGateway
class AccountGateway{
+ __construct(con : Connexion)
+ insertAccount(name:string, email:string, token:string, hash:string, profilePicture:string): int
+ getRowsFromMail(email:string): ?array
+ getHash(email:string): ?string
+ exists(email:string): bool
+ getAccountFromMail(email:string): ?Account
+ getAccountFromToken(token:string): ?Account
}
AccountGateway *--"- con" Connexion
@enduml

@ -0,0 +1,44 @@
@startuml
class EditorController {
+__construct (model : TacticModel)
+ openEditorFor(tactic:TacticInfo): ViewHttpResponse
+ createNew(): ViewHttpResponse
+ openTestEditor(courtType:CourtType): ViewHttpResponse
+ createNewOfKind(type:CourtType, session:SessionHandle): ViewHttpResponse
+ openEditor(id:int, session:SessionHandle): ViewHttpResponse
}
EditorController *-- "- model" TacticModel
class TacticModel {
+ TACTIC_DEFAULT_NAME:int {static}{frozen}
+ __construct(tactics : TacticInfoGateway)
+ makeNew(name:string, ownerId:int, type:CourtType): TacticInfo
+ makeNewDefault(ownerId:int, type:CourtType): ?TacticInfo
+ get(id:int): ?TacticInfo
+ getLast(nb:int, ownerId:int): array
+ getAll(ownerId:int): ?array
+ updateName(id:int, name:string, authId:int): array
+ updateContent(id:int, json:string): ?ValidationFail
}
TacticModel *-- "- tactics" TacticInfoGateway
class TacticInfoGateway{
+ __construct(con : Connexion)
+ get(id:int): ?TacticInfo
+ getLast(nb:int, ownerId:int): ?array
+ getAll(ownerId:int): ?array
+ insert(name:string, owner:int, type:CourtType): int
+ updateName(id:int, name:string): bool
+ updateContent(id:int, json:string): bool
}
TacticInfoGateway *--"- con" Connexion
class TacticValidator{
+ validateAccess(tacticId:int, tactic:?TacticInfo, ownerId:int): ?ValidationFail {static}
}
EditorController ..> TacticValidator
@enduml

@ -1,63 +1,87 @@
@startuml
class Team {
- name: string
- picture: Url
- members: array<int, MemberRole>
+ __construct(name : string, picture : string, mainColor : Colo, secondColor : Color)
+ getName(): string
+ getPicture(): Url
+ getMainColor(): Color
+ getSecondColor(): Color
+ listMembers(): array<Member>
}
Team --> "- mainColor" Color
Team --> "- secondColor" Color
class Color {
- value: string
- __construct(value : string)
+ getValue(): string
+ from(value: string): Color
+ tryFrom(value : string) : ?Color
}
class TeamGateway{
--
+ __construct(con : Connexion)
+ insert(name : string ,picture : string, mainColor : Color, secondColor : Color)
+ listByName(name : string): array
+ getTeamById(id:int): ?TeamInfo
+ getTeamIdByName(name:string): ?int
+ deleteTeam(idTeam:int): void
+ editTeam(idTeam:int, newName:string, newPicture:string, newMainColor:string, newSecondColor:string)
+ getAll(user:int): array
}
TeamGateway *--"- con" Connexion
TeamGateway ..> Color
class MemberGateway{
+ __construct(con : Connexion)
+ insert(idTeam:int, userId:int, role:string): void
+ getMembersOfTeam(teamId:int): array
+ remove(idTeam:int, idMember:int): void
+ isCoach(email:string, idTeam:int): bool
+ isMemberOfTeam(idTeam:int, idCurrentUser:int): bool
}
MemberGateway *--"- con" Connexion
class AccountGateway{
+ __construct(con : Connexion)
+ insertAccount(name:string, email:string, token:string, hash:string, profilePicture:string): int
+ getRowsFromMail(email:string): ?array
+ getHash(email:string): ?string
+ exists(email:string): bool
+ getAccountFromMail(email:string): ?Account
+ getAccountFromToken(token:string): ?Account
}
AccountGateway *--"- con" Connexion
class TeamModel{
---
+ __construct(gateway : TeamGateway)
+ createTeam(name : string,picture : string, mainColorValue : int, secondColorValue : int, errors : array)
+ addMember(mail:string, teamId:int, role:string): int
+ listByName(name : string ,errors : array) : ?array
+ displayTeam(id : int): Team
+ getTeam(idTeam:int, idCurrentUser:int): ?Team
+ deleteMember(idMember:int, teamId:int): int
+ deleteTeam(email:string, idTeam:int): int
+ isCoach(idTeam:int, email:string): bool
+ editTeam(idTeam:int, newName:string, newPicture:string, newMainColor:string, newSecondColor:string)
+ getAll(user:int): array
}
TeamModel *--"- gateway" TeamGateway
TeamModel ..> Team
TeamModel ..> Color
TeamModel *--"- members" MemberGateway
TeamModel *--"- teams" TeamGateway
TeamModel *--"- teams" AccountGateway
class TeamController{
- twig : Environement
--
+ __construct( model : TeamModel, twig : Environement)
+ displaySubmitTeam() : HttpResponse
+ submitTeam(request : array) : HttpResponse
+ displayListTeamByName(): HttpResponse
+ listTeamByName(request : array) : HttpResponse
+ displayTeam(id : int): HttpResponse
+ __construct( model : TeamModel)
+ displayCreateTeam(session:SessionHandle): ViewHttpResponse
+ displayDeleteMember(session:SessionHandle): ViewHttpResponse
+ submitTeam(request:array, session:SessionHandle): HttpResponse
+ displayListTeamByName(session:SessionHandle): ViewHttpResponse
+ listTeamByName(request:array, session:SessionHandle): HttpResponse
+ deleteTeamById(id:int, session:SessionHandle): HttpResponse
+ displayTeam(id:int, session:SessionHandle): ViewHttpResponse
+ displayAddMember(idTeam:int, session:SessionHandle): ViewHttpResponse
+ addMember(idTeam:int, request:array, session:SessionHandle): HttpResponse
+ deleteMember(idTeam:int, idMember:int, session:SessionHandle): HttpResponse
+ displayEditTeam(idTeam:int, session:SessionHandle): ViewHttpResponse
+ editTeam(idTeam:int, request:array, session:SessionHandle): HttpResponse
}
TeamController *--"- model" TeamModel
class Connexion { }
@enduml

@ -0,0 +1,27 @@
@startuml
interface SessionHandle{
+ getInitialTarget(): ?string {abstract}
+ getAccount(): ?Account {abstract}
}
interface MutableSessionHandle{
+ setInitialTarget(url:?string): void
+ setAccount(account:Account): void
+ destroy(): void
}
class PhpSessionHandle{
+ init(): self {static}
+ getAccount(): ?Account
+ getInitialTarget(): ?string
+ setAccount(account:Account): void
+ setInitialTarget(url:?string): void
+ destroy(): void
}
PhpSessionHandle ..|> MutableSessionHandle
MutableSessionHandle ..|> SessionHandle
@enduml

@ -1,18 +1,20 @@
@startuml
abstract class Validator {
+ validate(name: string, val: mixed): array
+ validate(name: string, val: mixed): array {abstract}
+ then(other: Validator): Validator
}
class ComposedValidator extends Validator {
- first: Validator
- then: Validator
+ __construct(first: Validator, then: Validator)
validate(name: string, val: mixed): array
+ validate(name: string, val: mixed): array
}
ComposedValidator -->"- first" Validator
ComposedValidator -->"- then" Validator
class SimpleFunctionValidator extends Validator {
- predicate: callable
- error_factory: callable
@ -28,9 +30,9 @@ class ValidationFail implements JsonSerialize {
+ __construct(kind: string, message: string)
+ getMessage(): string
+ getKind(): string
+ jsonSerialize()
+ <u>notFound(message: string): ValidationFail
+ <u>unauthorized(message:string): ValidationFail
+ <u>error(message:string): ValidationFail
}
class FieldValidationFail extends ValidationFail {
@ -49,13 +51,31 @@ class Validation {
<u> + validate(val: mixed, valName: string, failures: &array, validators: Validator...): bool
}
class Validators {
---
Validation ..> Validator
class DefaultValidators {
+ <u>nonEmpty(): Validator
+ <u>shorterThan(limit: int): Validator
+ <u>userString(maxLen: int): Validator
...
+ <u>regex(regex:string, msg:string): Validator
+ <u>hex(msg:string): Validator
+ <u>name(msg:string): Validator
+ <u>nameWithSpaces(): Validator
+ <u>lenBetween(min:int, max:int): Validator
+ <u>email(msg:string): Validator
+ <u>isInteger(): Validator
+ <u>isIntInRange(min:int, max:int): Validator
+ <u>isURL(): Validator
}
DefaultValidators ..> Validator
class FunctionValidator{
- validate_fn: callable
+ __construct(validate_fn:callable)
+ validate(name:string, val:mixed): array
}
Validator <|-- FunctionValidator
@enduml

@ -1,7 +1,8 @@
# IQBall - Web Application
This repository hosts the IQBall application for web
## Read the docs !
You can find some additional documentation in the [Documentation](Documentation) folder,
and in the [wiki](https://codefirst.iut.uca.fr/git/IQBall/Application-Web/wiki).

@ -4,63 +4,41 @@ name: "CI and Deploy on maxou.dev"
volumes:
- name: server
temp: {}
temp: { }
trigger:
event:
- push
steps:
- image: node:latest
name: "front CI"
commands:
- npm install
- npm run tsc
- image: composer:latest
name: "php CI"
commands:
- composer install
- vendor/bin/phpstan analyze
- image: node:latest
name: "build node"
name: "build react"
volumes: &outputs
- name: server
path: /outputs
depends_on:
- "front CI"
commands:
- curl -L moshell.dev/setup.sh > /tmp/moshell_setup.sh
- chmod +x /tmp/moshell_setup.sh
- echo n | /tmp/moshell_setup.sh
- echo "VITE_API_ENDPOINT=/IQBall/$DRONE_BRANCH/public/api" >> .env.PROD
- echo "VITE_BASE=/IQBall/$DRONE_BRANCH/public" >> .env.PROD
-
- /root/.local/bin/moshell ci/build_react.msh
- # force to use the backend master branch if pushing on master
- echo "VITE_API_ENDPOINT=https://iqball.maxou.dev/api/dotnet-$([ "$DRONE_BRANCH" = master ] && echo master || cat .stage-backend-branch | tr / _)" > .env.STAGE
- npm run build -- --base=/$DRONE_BRANCH/ --mode STAGE
- mv dist/* /outputs
- image: ubuntu:latest
name: "prepare php"
volumes: *outputs
depends_on:
- "php CI"
commands:
- mkdir -p /outputs/public
# this sed command will replace the included `profile/dev-config-profile.php` to `profile/prod-config-file.php` in the config.php file.
- sed -iE 's/\\/\\*PROFILE_FILE\\*\\/\\s*".*"/"profiles\\/prod-config-profile.php"/' config.php
- rm profiles/dev-config-profile.php
- mv src config.php sql profiles vendor /outputs/
- image: eeacms/rsync:latest
name: Deliver on staging server branch
depends_on:
- "prepare php"
- "build node"
- "build react"
volumes: *outputs
environment:
SERVER_PRIVATE_KEY:
from_secret: SERVER_PRIVATE_KEY
commands:
- chmod +x ci/deploy.sh
- ci/deploy.sh
- chmod +x ci/deploy.sh
- ci/deploy.sh

@ -0,0 +1,18 @@
set -e
export OUTPUT=$1
export BASE=$2
rm -rf "$OUTPUT"/*
echo "VITE_API_ENDPOINT=$BASE/api" >> .env.PROD
echo "VITE_BASE=$BASE" >> .env.PROD
ci/build_react.msh
mkdir -p $OUTPUT/profiles/
sed -E 's/\/\*PROFILE_FILE\*\/\s*".*"/"profiles\/prod-config-profile.php"/' config.php > $OUTPUT/config.php
sed -E "s/const BASE_PATH = .*;/const BASE_PATH = \"$(sed s/\\//\\\\\\//g <<< "$BASE")\";/" profiles/prod-config-profile.php > $OUTPUT/profiles/prod-config-profile.php
cp -r vendor sql src public $OUTPUT

@ -1,20 +1,17 @@
#!/usr/bin/env moshell
mkdir -p /outputs/public
val base = std::env("BASE").unwrap()
val outputs = std::env("OUTPUT").unwrap()
apt update && apt install jq -y
mkdir -p $outputs/public
val drone_branch = std::env("DRONE_BRANCH").unwrap()
val base = "/IQBall/$drone_branch/public"
npm run build -- --base=$base --mode PROD
// Read generated mappings from build
val result = $(jq -r 'to_entries|map(.key + " " +.value.file)|.[]' dist/manifest.json)
val mappings = $result.split('\n')
echo '<?php\nconst ASSETS = [' > views-mappings.php
echo '<?php const ASSETS = [' > views-mappings.php
while $mappings.len() > 0 {
val mapping = $mappings.pop().unwrap();
@ -28,5 +25,5 @@ echo "];" >> views-mappings.php
chmod +r views-mappings.php
mv dist/* front/assets/ front/style/ public/* /outputs/public/
mv views-mappings.php /outputs/
cp -r dist/* front/assets/ front/style/ public/* $outputs/public/
cp -r views-mappings.php $outputs/

@ -5,7 +5,7 @@ echo "$SERVER_PRIVATE_KEY" > ~/.ssh/id_rsa
chmod 0600 ~/.ssh
chmod 0500 ~/.ssh/id_rsa*
SERVER_ROOT=/srv/www/IQBall
SERVER_ROOT=/srv/www/iqball
ssh -p 80 -o 'StrictHostKeyChecking=no' iqball@maxou.dev mkdir -p $SERVER_ROOT/$DRONE_BRANCH
rsync -avz -e "ssh -p 80 -o 'StrictHostKeyChecking=no'" --delete /outputs/* iqball@maxou.dev:$SERVER_ROOT/$DRONE_BRANCH

@ -1,18 +0,0 @@
{
"autoload": {
"psr-4": {
"IQBall\\": "src/"
}
},
"require": {
"altorouter/altorouter": "1.2.0",
"ext-json": "*",
"ext-pdo": "*",
"ext-pdo_sqlite": "*",
"twig/twig":"^2.0",
"phpstan/phpstan": "*"
},
"require-dev": {
"friendsofphp/php-cs-fixer": "^3.38"
}
}

@ -1,9 +0,0 @@
#!/usr/bin/env bash
## verify php and typescript types
echo "formatting php typechecking"
vendor/bin/php-cs-fixer fix
echo "formatting typescript typechecking"
npm run format

@ -1,19 +0,0 @@
import ReactDOM from "react-dom/client"
import React, { FunctionComponent } from "react"
/**
* Dynamically renders a React component, with given arguments
* @param Component the react component to render
* @param args the arguments to pass to the react component.
*/
export function renderView(Component: FunctionComponent, args: {}) {
const root = ReactDOM.createRoot(
document.getElementById("root") as HTMLElement,
)
root.render(
<React.StrictMode>
<Component {...args} />
</React.StrictMode>,
)
}

@ -1,272 +0,0 @@
import { CourtBall } from "./CourtBall"
import {
ReactElement,
RefObject,
useCallback,
useLayoutEffect,
useState,
} from "react"
import CourtPlayer from "./CourtPlayer"
import { Player } from "../../model/tactic/Player"
import { Action, ActionKind } from "../../model/tactic/Action"
import ArrowAction from "../actions/ArrowAction"
import { middlePos, ratioWithinBase } from "../arrows/Pos"
import BallAction from "../actions/BallAction"
import { CourtObject } from "../../model/tactic/Ball"
import { contains } from "../arrows/Box"
import { CourtAction } from "../../views/editor/CourtAction"
export interface BasketCourtProps {
players: Player[]
actions: Action[]
objects: CourtObject[]
renderAction: (a: Action, key: number) => ReactElement
setActions: (f: (a: Action[]) => Action[]) => void
onPlayerRemove: (p: Player) => void
onPlayerChange: (p: Player) => void
onBallRemove: () => void
onBallMoved: (ball: DOMRect) => void
courtImage: ReactElement
courtRef: RefObject<HTMLDivElement>
}
export function BasketCourt({
players,
actions,
objects,
renderAction,
setActions,
onPlayerRemove,
onPlayerChange,
onBallMoved,
onBallRemove,
courtImage,
courtRef,
}: BasketCourtProps) {
function placeArrow(origin: Player, arrowHead: DOMRect) {
const originRef = document.getElementById(origin.id)!
const courtBounds = courtRef.current!.getBoundingClientRect()
const start = ratioWithinBase(
middlePos(originRef.getBoundingClientRect()),
courtBounds,
)
for (const player of players) {
if (player.id == origin.id) {
continue
}
const playerBounds = document
.getElementById(player.id)!
.getBoundingClientRect()
if (
!(
playerBounds.top > arrowHead.bottom ||
playerBounds.right < arrowHead.left ||
playerBounds.bottom < arrowHead.top ||
playerBounds.left > arrowHead.right
)
) {
const targetPos = document
.getElementById(player.id)!
.getBoundingClientRect()
const end = ratioWithinBase(middlePos(targetPos), courtBounds)
const action: Action = {
fromPlayerId: originRef.id,
toPlayerId: player.id,
type: origin.hasBall ? ActionKind.SHOOT : ActionKind.SCREEN,
moveFrom: start,
segments: [{ next: end }],
}
setActions((actions) => [...actions, action])
return
}
}
const action: Action = {
fromPlayerId: originRef.id,
type: origin.hasBall ? ActionKind.DRIBBLE : ActionKind.MOVE,
moveFrom: ratioWithinBase(
middlePos(originRef.getBoundingClientRect()),
courtBounds,
),
segments: [
{ next: ratioWithinBase(middlePos(arrowHead), courtBounds) },
],
}
setActions((actions) => [...actions, action])
}
const [previewAction, setPreviewAction] = useState<Action | null>(null)
const updateActionsRelatedTo = useCallback((player: Player) => {
const newPos = ratioWithinBase(
middlePos(
document.getElementById(player.id)!.getBoundingClientRect(),
),
courtRef.current!.getBoundingClientRect(),
)
setActions((actions) =>
actions.map((a) => {
if (a.fromPlayerId == player.id) {
return { ...a, moveFrom: newPos }
}
if (a.toPlayerId == player.id) {
const segments = a.segments.toSpliced(
a.segments.length - 1,
1,
{
...a.segments[a.segments.length - 1],
next: newPos,
},
)
return { ...a, segments }
}
return a
}),
)
}, [])
const [internActions, setInternActions] = useState<Action[]>([])
useLayoutEffect(() => setInternActions(actions), [actions])
return (
<div
className="court-container"
ref={courtRef}
style={{ position: "relative" }}>
{courtImage}
{players.map((player) => (
<CourtPlayer
key={player.id}
player={player}
onDrag={() => updateActionsRelatedTo(player)}
onChange={onPlayerChange}
onRemove={() => onPlayerRemove(player)}
courtRef={courtRef}
availableActions={(pieceRef) => [
<ArrowAction
key={1}
onHeadMoved={(headPos) => {
const baseBounds =
courtRef.current!.getBoundingClientRect()
const arrowHeadPos = middlePos(headPos)
const target = players.find(
(p) =>
p != player &&
contains(
document
.getElementById(p.id)!
.getBoundingClientRect(),
arrowHeadPos,
),
)
setPreviewAction((action) => ({
...action!,
segments: [
{
next: ratioWithinBase(
arrowHeadPos,
baseBounds,
),
},
],
type: player.hasBall
? target
? ActionKind.SHOOT
: ActionKind.DRIBBLE
: target
? ActionKind.SCREEN
: ActionKind.MOVE,
}))
}}
onHeadPicked={(headPos) => {
;(document.activeElement as HTMLElement).blur()
const baseBounds =
courtRef.current!.getBoundingClientRect()
setPreviewAction({
type: player.hasBall
? ActionKind.DRIBBLE
: ActionKind.MOVE,
fromPlayerId: player.id,
toPlayerId: undefined,
moveFrom: ratioWithinBase(
middlePos(
pieceRef.getBoundingClientRect(),
),
baseBounds,
),
segments: [
{
next: ratioWithinBase(
middlePos(headPos),
baseBounds,
),
},
],
})
}}
onHeadDropped={(headRect) => {
placeArrow(player, headRect)
setPreviewAction(null)
}}
/>,
player.hasBall && (
<BallAction
key={2}
onDrop={(ref) =>
onBallMoved(ref.getBoundingClientRect())
}
/>
),
]}
/>
))}
{internActions.map((action, idx) => renderAction(action, idx))}
{objects.map((object) => {
if (object.type == "ball") {
return (
<CourtBall
onMoved={onBallMoved}
ball={object}
onRemove={onBallRemove}
key="ball"
/>
)
}
throw new Error("unknown court object" + object.type)
})}
{previewAction && (
<CourtAction
courtRef={courtRef}
action={previewAction}
//do nothing on change, not really possible as it's a preview arrow
onActionDeleted={() => {}}
onActionChanges={() => {}}
/>
)}
</div>
)
}

@ -1,17 +0,0 @@
export type CourtObject = { type: "ball" } & Ball
export interface Ball {
/**
* The ball is a "ball" court object
*/
readonly type: "ball"
/**
* Percentage of the player's position to the bottom (0 means top, 1 means bottom, 0.5 means middle)
*/
readonly bottomRatio: number
/**
* Percentage of the player's position to the right (0 means left, 1 means right, 0.5 means middle)
*/
readonly rightRatio: number
}

@ -1,35 +0,0 @@
export type PlayerId = string
export enum PlayerTeam {
Allies = "allies",
Opponents = "opponents",
}
export interface Player {
readonly id: PlayerId
/**
* the player's team
* */
readonly team: PlayerTeam
/**
* player's role
* */
readonly role: string
/**
* Percentage of the player's position to the bottom (0 means top, 1 means bottom, 0.5 means middle)
*/
readonly bottomRatio: number
/**
* Percentage of the player's position to the right (0 means left, 1 means right, 0.5 means middle)
*/
readonly rightRatio: number
/**
* True if the player has a basketball
*/
readonly hasBall: boolean
}

@ -1,15 +0,0 @@
import { Player } from "./Player"
import { CourtObject } from "./Ball"
import { Action } from "./Action"
export interface Tactic {
id: number
name: string
content: TacticContent
}
export interface TacticContent {
players: Player[]
objects: CourtObject[]
actions: Action[]
}

@ -1,9 +0,0 @@
:root {
--main-color: #191a21;
--second-color: #282a36;
--third-color: #303341;
--accent-color: #ffa238;
--main-contrast-color: #e6edf3;
--font-title: Helvetica;
--font-content: Helvetica;
}

@ -1,23 +0,0 @@
import React, { CSSProperties, useState } from "react"
import "../style/visualizer.css"
import Court from "../assets/court/full_court.svg"
export default function Visualizer({ id, name }: { id: number; name: string }) {
const [style, setStyle] = useState<CSSProperties>({})
return (
<div id="main">
<div id="topbar">
<h1>{name}</h1>
</div>
<div id="court-container">
<img
id="court"
src={Court}
style={style}
alt="Basketball Court"
/>
</div>
</div>
)
}

@ -0,0 +1,13 @@
<!doctype html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<link rel="icon" type="image/svg+xml" href="/src/assets/vite.svg" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>IQBall</title>
</head>
<body>
<div id="root"></div>
<script type="module" src="/src/main.tsx"></script>
</body>
</html>

@ -20,7 +20,9 @@
"react-draggable": "^4.4.6",
"typescript": "^5.2.2",
"vite": "^4.5.0",
"vite-plugin-css-injected-by-js": "^3.3.0"
"vite-plugin-css-injected-by-js": "^3.3.0",
"eslint-plugin-react-refresh": "^0.4.5",
"react-router-dom": "^6.22.0"
},
"scripts": {
"start": "vite --host",
@ -41,6 +43,11 @@
"@vitejs/plugin-react": "^4.1.0",
"prettier": "^3.1.0",
"typescript": "^5.2.2",
"vite-plugin-svgr": "^4.1.0"
"vite-plugin-svgr": "^4.1.0",
"@typescript-eslint/eslint-plugin": "^6.11.0",
"@typescript-eslint/parser": "^6.11.0",
"eslint": "^8.53.0",
"eslint-plugin-react": "^7.33.2",
"eslint-plugin-react-hooks": "^4.6.0"
}
}

@ -1,12 +0,0 @@
parameters:
phpVersion: 70400
level: 6
paths:
- src
scanFiles:
- config.php
- sql/database.php
- profiles/dev-config-profile.php
- profiles/prod-config-profile.php
excludePaths:
- src/App/react-display-file.php

@ -1,16 +0,0 @@
<?php
$hostname = getHostName();
$front_url = "http://$hostname:5173";
const _SUPPORTS_FAST_REFRESH = true;
$_data_source_name = "sqlite:${_SERVER['DOCUMENT_ROOT']}/../dev-database.sqlite";
// no user and password needed for sqlite databases
const _DATABASE_USER = null;
const _DATABASE_PASSWORD = null;
function _asset(string $assetURI): string {
global $front_url;
return $front_url . "/" . $assetURI;
}

@ -1,22 +0,0 @@
<?php
// This file only exists on production servers, and defines the available assets mappings
// in an `ASSETS` array constant.
require __DIR__ . "/../views-mappings.php";
const _SUPPORTS_FAST_REFRESH = false;
$database_file = __DIR__ . "/../database.sqlite";
$_data_source_name = "sqlite:/$database_file";
// no user and password needed for sqlite databases
const _DATABASE_USER = null;
const _DATABASE_PASSWORD = null;
function _asset(string $assetURI): string {
// use index.php's base path
global $basePath;
// If the asset uri does not figure in the available assets array,
// fallback to the uri itself.
return $basePath . "/" . (ASSETS[$assetURI] ?? $assetURI);
}

@ -1 +0,0 @@
<svg xmlns="http://www.w3.org/2000/svg" height="24" viewBox="0 -960 960 960" width="24"><path d="M234-276q51-39 114-61.5T480-360q69 0 132 22.5T726-276q35-41 54.5-93T800-480q0-133-93.5-226.5T480-800q-133 0-226.5 93.5T160-480q0 59 19.5 111t54.5 93Zm246-164q-59 0-99.5-40.5T340-580q0-59 40.5-99.5T480-720q59 0 99.5 40.5T620-580q0 59-40.5 99.5T480-440Zm0 360q-83 0-156-31.5T197-197q-54-54-85.5-127T80-480q0-83 31.5-156T197-763q54-54 127-85.5T480-880q83 0 156 31.5T763-763q54 54 85.5 127T880-480q0 83-31.5 156T763-197q-54 54-127 85.5T480-80Zm0-80q53 0 100-15.5t86-44.5q-39-29-86-44.5T480-280q-53 0-100 15.5T294-220q39 29 86 44.5T480-160Zm0-360q26 0 43-17t17-43q0-26-17-43t-43-17q-26 0-43 17t-17 43q0 26 17 43t43 17Zm0-60Zm0 360Z" fill="#e6edf3"/></svg>

Before

Width:  |  Height:  |  Size: 747 B

@ -1 +0,0 @@
../front/assets

@ -1 +0,0 @@
../front

@ -1,27 +0,0 @@
<?php
/**
* @return PDO The PDO instance of the configuration's database connexion.
*/
function get_database(): PDO {
// defined by profiles.
global $data_source_name;
$pdo = new PDO($data_source_name, DATABASE_USER, DATABASE_PASSWORD, [PDO::ERRMODE_EXCEPTION]);
$database_exists = $pdo->query("SELECT COUNT(*) FROM sqlite_master WHERE type = 'table'")->fetchColumn() > 0;
if ($database_exists) {
return $pdo;
}
foreach (scandir(__DIR__) as $file) {
if (preg_match("/.*\.sql$/i", $file)) {
$content = file_get_contents(__DIR__ . "/" . $file);
$pdo->exec($content);
}
}
return $pdo;
}

@ -1,55 +0,0 @@
<?php
namespace IQBall\Api;
use Exception;
use IQBall\Core\Action;
use IQBall\Core\Http\HttpResponse;
use IQBall\Core\Http\JsonHttpResponse;
use IQBall\Core\Validation\ValidationFail;
class API {
public static function render(HttpResponse $response): void {
http_response_code($response->getCode());
foreach ($response->getHeaders() as $header => $value) {
header("$header: $value");
}
if ($response instanceof JsonHttpResponse) {
header('Content-type: application/json');
echo $response->getJson();
} elseif (get_class($response) != HttpResponse::class) {
throw new Exception("API returned unknown Http Response");
}
}
/**
* @param array<string, mixed> $match
* @param callable $tryGetAuthorization function to return an authorisation object for the given action (if required)
* @return HttpResponse
* @throws Exception
*/
public static function handleMatch(array $match, callable $tryGetAuthorization): HttpResponse {
if (!$match) {
return new JsonHttpResponse([ValidationFail::notFound("not found")]);
}
$action = $match['target'];
if (!$action instanceof Action) {
throw new Exception("routed action is not an AppAction object.");
}
$auth = null;
if ($action->isAuthRequired()) {
$auth = call_user_func($tryGetAuthorization);
if ($auth == null) {
return new JsonHttpResponse([ValidationFail::unauthorized("Missing or invalid 'Authorization' header.")]);
}
}
return $action->run($match['params'], $auth);
}
}

@ -1,44 +0,0 @@
<?php
namespace IQBall\Api\Controller;
use IQBall\App\Control;
use IQBall\Core\Http\HttpCodes;
use IQBall\Core\Http\HttpRequest;
use IQBall\Core\Http\HttpResponse;
use IQBall\Core\Http\JsonHttpResponse;
use IQBall\Core\Model\AuthModel;
use IQBall\Core\Validation\Validators;
class APIAuthController {
private AuthModel $model;
/**
* @param AuthModel $model
*/
public function __construct(AuthModel $model) {
$this->model = $model;
}
/**
* From given email address and password, authenticate the user and respond with its authorization token.
* @return HttpResponse
*/
public function authorize(): HttpResponse {
return Control::runChecked([
"email" => [Validators::email(), Validators::lenBetween(5, 256)],
"password" => [Validators::lenBetween(6, 256)],
], function (HttpRequest $req) {
$failures = [];
$account = $this->model->login($req["email"], $req["password"], $failures);
if (!empty($failures)) {
return new JsonHttpResponse($failures, HttpCodes::UNAUTHORIZED);
}
return new JsonHttpResponse(["authorization" => $account->getToken()]);
});
}
}

@ -1,65 +0,0 @@
<?php
namespace IQBall\Api\Controller;
use IQBall\App\Control;
use IQBall\Core\Data\Account;
use IQBall\Core\Http\HttpCodes;
use IQBall\Core\Http\HttpRequest;
use IQBall\Core\Http\HttpResponse;
use IQBall\Core\Http\JsonHttpResponse;
use IQBall\Core\Model\TacticModel;
use IQBall\Core\Validation\FieldValidationFail;
use IQBall\Core\Validation\Validators;
/**
* API endpoint related to tactics
*/
class APITacticController {
private TacticModel $model;
/**
* @param TacticModel $model
*/
public function __construct(TacticModel $model) {
$this->model = $model;
}
/**
* update name of tactic, specified by tactic identifier, given in url.
* @param int $tactic_id
* @param Account $account
* @return HttpResponse
*/
public function updateName(int $tactic_id, Account $account): HttpResponse {
return Control::runChecked([
"name" => [Validators::lenBetween(1, 50), Validators::nameWithSpaces()],
], function (HttpRequest $request) use ($tactic_id, $account) {
$failures = $this->model->updateName($tactic_id, $request["name"], $account->getUser()->getId());
if (!empty($failures)) {
//TODO find a system to handle Unauthorized error codes more easily from failures.
return new JsonHttpResponse($failures, HttpCodes::BAD_REQUEST);
}
return HttpResponse::fromCode(HttpCodes::OK);
});
}
/**
* @param int $id
* @param Account $account
* @return HttpResponse
*/
public function saveContent(int $id, Account $account): HttpResponse {
return Control::runChecked([
"content" => [],
], function (HttpRequest $req) use ($id) {
if ($fail = $this->model->updateContent($id, json_encode($req["content"]))) {
return new JsonHttpResponse([$fail], HttpCodes::BAD_REQUEST);
}
return HttpResponse::fromCode(HttpCodes::OK);
});
}
}

@ -0,0 +1,67 @@
import { BrowserRouter, Outlet, Route, Routes } from "react-router-dom"
import { Header } from "./pages/template/Header.tsx"
import "./style/app.css"
import { lazy } from "react"
import { BASE } from "./Constants.ts"
const HomePage = lazy(() => import("./pages/HomePage.tsx"))
const LoginPage = lazy(() => import("./pages/LoginPage.tsx"))
const RegisterPage = lazy(() => import("./pages/RegisterPage.tsx"))
const NotFoundPage = lazy(() => import("./pages/404.tsx"))
const CreateTeamPage = lazy(() => import("./pages/CreateTeamPage.tsx"))
const TeamPanelPage = lazy(() => import("./pages/TeamPanel.tsx"))
const NewTacticPage = lazy(() => import("./pages/NewTacticPage.tsx"))
const Editor = lazy(() => import("./pages/Editor.tsx"))
export default function App() {
return (
<div id="app">
<BrowserRouter basename={BASE}>
<Outlet />
<Routes>
<Route path={"/login"} element={<LoginPage />} />
<Route path={"/register"} element={<RegisterPage />} />
<Route path={"/"} element={<AppLayout />}>
<Route path={"/"} element={<HomePage />} />
<Route path={"/home"} element={<HomePage />} />
<Route
path={"/team/new"}
element={<CreateTeamPage />}
/>
<Route
path={"/team/:teamId"}
element={<TeamPanelPage />}
/>
<Route
path={"/tactic/new"}
element={<NewTacticPage />}
/>
<Route
path={"/tactic/:tacticId/edit"}
element={<Editor guestMode={false} />}
/>
<Route
path={"/tactic/edit-guest"}
element={<Editor guestMode={true} />}
/>
<Route path={"*"} element={<NotFoundPage />} />
</Route>
</Routes>
</BrowserRouter>
</div>
)
}
function AppLayout() {
return (
<>
<Header />
<Outlet />
</>
)
}

@ -1,92 +0,0 @@
<?php
namespace IQBall\App;
use IQBall\App\Session\MutableSessionHandle;
use IQBall\Core\Action;
use IQBall\Core\Http\HttpResponse;
use IQBall\Core\Http\JsonHttpResponse;
use Twig\Environment;
use Twig\Error\LoaderError;
use Twig\Error\RuntimeError;
use Twig\Error\SyntaxError;
use Twig\Loader\FilesystemLoader;
class App {
/**
* renders (prints out) given HttpResponse to the client
* @param HttpResponse $response
* @param callable(): Environment $twigSupplier
* @return void
* @throws LoaderError
* @throws RuntimeError
* @throws SyntaxError
*/
public static function render(HttpResponse $response, callable $twigSupplier): void {
http_response_code($response->getCode());
foreach ($response->getHeaders() as $header => $value) {
header("$header: $value");
}
if ($response instanceof ViewHttpResponse) {
self::renderView($response, $twigSupplier);
} elseif ($response instanceof JsonHttpResponse) {
header('Content-type: application/json');
echo $response->getJson();
}
}
/**
* renders (prints out) given ViewHttpResponse to the client
* @param ViewHttpResponse $response
* @param callable(): Environment $twigSupplier
* @return void
* @throws LoaderError
* @throws RuntimeError
* @throws SyntaxError
*/
private static function renderView(ViewHttpResponse $response, callable $twigSupplier): void {
$file = $response->getFile();
$args = $response->getArguments();
switch ($response->getViewKind()) {
case ViewHttpResponse::REACT_VIEW:
send_react_front($file, $args);
break;
case ViewHttpResponse::TWIG_VIEW:
try {
$twig = call_user_func($twigSupplier);
$twig->display($file, $args);
} catch (RuntimeError|SyntaxError|LoaderError $e) {
http_response_code(500);
echo "There was an error rendering your view, please refer to an administrator.\nlogs date: " . date("YYYD, d M Y H:i:s");
throw $e;
}
break;
}
}
/**
* run a user action, and return the generated response
* @param string $authRoute the route towards an authentication page to response with a redirection
* if the run action requires auth but session does not contain a logged-in account.
* @param Action<MutableSessionHandle> $action
* @param mixed[] $params
* @param MutableSessionHandle $session
* @return HttpResponse
*/
public static function runAction(string $authRoute, Action $action, array $params, MutableSessionHandle $session): HttpResponse {
if ($action->isAuthRequired()) {
$account = $session->getAccount();
if ($account == null) {
// put in the session the initial url the user wanted to get
$session->setInitialTarget($_SERVER['REQUEST_URI']);
return HttpResponse::redirect($authRoute);
}
}
return $action->run($params, $session);
}
}

@ -1,48 +0,0 @@
<?php
namespace IQBall\App;
use IQBall\Core\Http\HttpCodes;
use IQBall\Core\Http\HttpRequest;
use IQBall\Core\Http\HttpResponse;
use IQBall\Core\Validation\ValidationFail;
use IQBall\Core\Validation\Validator;
class Control {
/**
* Runs given callback, if the request's json validates the given 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.
* THe callback must accept an HttpRequest, and return an HttpResponse object.
* @return HttpResponse
*/
public static function runChecked(array $schema, callable $run): HttpResponse {
$request_body = file_get_contents('php://input');
$payload_obj = json_decode($request_body);
if (!$payload_obj instanceof \stdClass) {
$fail = new ValidationFail("bad-payload", "request body is not a valid json object");
return ViewHttpResponse::twig("error.html.twig", ["failures" => [$fail]], HttpCodes::BAD_REQUEST);
}
$payload = get_object_vars($payload_obj);
return self::runCheckedFrom($payload, $schema, $run);
}
/**
* Runs given callback, if the given request data array validates the given schema.
* @param array<string, mixed> $data the request's data array.
* @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.
* THe callback must accept an HttpRequest, and return an HttpResponse object.
* @return HttpResponse
*/
public static function runCheckedFrom(array $data, array $schema, callable $run): HttpResponse {
$fails = [];
$request = HttpRequest::from($data, $fails, $schema);
if (!empty($fails)) {
return ViewHttpResponse::twig("error.html.twig", ['failures' => $fails], HttpCodes::BAD_REQUEST);
}
return call_user_func_array($run, [$request]);
}
}

@ -1,246 +0,0 @@
<?php
namespace IQBall\App\Controller;
use IQBall\App\Session\SessionHandle;
use IQBall\App\ViewHttpResponse;
use IQBall\Core\Data\Account;
use IQBall\Core\Http\HttpCodes;
use IQBall\Core\Http\HttpRequest;
use IQBall\Core\Http\HttpResponse;
use IQBall\Core\Model\TeamModel;
use IQBall\Core\Validation\FieldValidationFail;
use IQBall\Core\Validation\ValidationFail;
use IQBall\Core\Validation\Validators;
class TeamController {
private TeamModel $model;
/**
* @param TeamModel $model
*/
public function __construct(TeamModel $model) {
$this->model = $model;
}
/**
* @param SessionHandle $session
* @return ViewHttpResponse the team creation panel
*/
public function displayCreateTeam(SessionHandle $session): ViewHttpResponse {
return ViewHttpResponse::twig("insert_team.html.twig", []);
}
/**
* @param SessionHandle $session
* @return ViewHttpResponse the team panel to delete a member
*/
public function displayDeleteMember(SessionHandle $session): ViewHttpResponse {
return ViewHttpResponse::twig("delete_member.html.twig", []);
}
/**
* create a new team from given request name, mainColor, secondColor and picture url
* @param array<string, mixed> $request
* @param SessionHandle $session
* @return HttpResponse
*/
public function submitTeam(array $request, SessionHandle $session): HttpResponse {
$failures = [];
$request = HttpRequest::from($request, $failures, [
"name" => [Validators::lenBetween(1, 32), Validators::nameWithSpaces()],
"main_color" => [Validators::hexColor()],
"second_color" => [Validators::hexColor()],
"picture" => [Validators::isURL()],
]);
if (!empty($failures)) {
$badFields = [];
foreach ($failures as $e) {
if ($e instanceof FieldValidationFail) {
$badFields[] = $e->getFieldName();
}
}
return ViewHttpResponse::twig('insert_team.html.twig', ['bad_fields' => $badFields]);
}
$teamId = $this->model->createTeam($request['name'], $request['picture'], $request['main_color'], $request['second_color']);
$this->model->addMember($session->getAccount()->getUser()->getEmail(), $teamId, 'COACH');
return HttpResponse::redirect('/team/' . $teamId);
}
/**
* @param SessionHandle $session
* @return ViewHttpResponse the panel to search a team by its name
*/
public function displayListTeamByName(SessionHandle $session): ViewHttpResponse {
return ViewHttpResponse::twig("list_team_by_name.html.twig", []);
}
/**
* returns a view that contains all the teams description whose name matches the given name needle.
* @param array<string, mixed> $request
* @param SessionHandle $session
* @return HttpResponse
*/
public function listTeamByName(array $request, SessionHandle $session): HttpResponse {
$errors = [];
$request = HttpRequest::from($request, $errors, [
"name" => [Validators::lenBetween(1, 32), Validators::nameWithSpaces()],
]);
if (!empty($errors) && $errors[0] instanceof FieldValidationFail) {
$badField = $errors[0]->getFieldName();
return ViewHttpResponse::twig('list_team_by_name.html.twig', ['bad_field' => $badField]);
}
$teams = $this->model->listByName($request['name'], $session->getAccount()->getUser()->getId());
if (empty($teams)) {
return ViewHttpResponse::twig('display_teams.html.twig', []);
}
return ViewHttpResponse::twig('display_teams.html.twig', ['teams' => $teams]);
}
/**
* Delete a team with its id
* @param int $id
* @param SessionHandle $session
* @return HttpResponse
*/
public function deleteTeamById(int $id, SessionHandle $session): HttpResponse {
$a = $session->getAccount();
$ret = $this->model->deleteTeam($a->getUser()->getEmail(), $id);
if($ret != 0) {
return ViewHttpResponse::twig('display_team.html.twig', ['notDeleted' => true]);
}
return HttpResponse::redirect('/');
}
/**
* Display a team with its id
* @param int $id
* @param SessionHandle $session
* @return ViewHttpResponse a view that displays given team information
*/
public function displayTeam(int $id, SessionHandle $session): ViewHttpResponse {
$result = $this->model->getTeam($id, $session->getAccount()->getUser()->getId());
if($result == null) {
return ViewHttpResponse::twig('error.html.twig', [
'failures' => [ValidationFail::unauthorized("Vous n'avez pas accès à cette équipe.")],
], HttpCodes::FORBIDDEN);
}
$role = $this->model->isCoach($id, $session->getAccount()->getUser()->getEmail());
return ViewHttpResponse::react(
'views/TeamPanel.tsx',
[
'team' => [
"info" => $result->getInfo(),
"members" => $result->listMembers(),
],
'isCoach' => $role,
'currentUserId' => $session->getAccount()->getUser()->getId()]
);
}
/**
* @param int $idTeam
* @param SessionHandle $session
* @return ViewHttpResponse the team panel to add a member
*/
public function displayAddMember(int $idTeam, SessionHandle $session): ViewHttpResponse {
return ViewHttpResponse::twig("add_member.html.twig", ['idTeam' => $idTeam]);
}
/**
* add a member to a team
* @param int $idTeam
* @param array<string, mixed> $request
* @param SessionHandle $session
* @return HttpResponse
*/
public function addMember(int $idTeam, array $request, SessionHandle $session): HttpResponse {
$errors = [];
if(!$this->model->isCoach($idTeam, $session->getAccount()->getUser()->getEmail())) {
return ViewHttpResponse::twig('error.html.twig', [
'failures' => [ValidationFail::unauthorized("Vous n'avez pas accès à cette action pour cette équipe.")],
], HttpCodes::FORBIDDEN);
}
$request = HttpRequest::from($request, $errors, [
"email" => [Validators::email(), Validators::lenBetween(5, 256)],
]);
if(!empty($errors)) {
return ViewHttpResponse::twig('add_member.html.twig', ['badEmail' => true,'idTeam' => $idTeam]);
}
$ret = $this->model->addMember($request['email'], $idTeam, $request['role']);
switch($ret) {
case -1:
return ViewHttpResponse::twig('add_member.html.twig', ['notFound' => true,'idTeam' => $idTeam]);
case -2:
return ViewHttpResponse::twig('add_member.html.twig', ['alreadyExisting' => true,'idTeam' => $idTeam]);
default:
return HttpResponse::redirect('/team/' . $idTeam);
}
}
/**
* remove a member from a team with their ids
* @param int $idTeam
* @param int $idMember
* @param SessionHandle $session
* @return HttpResponse
*/
public function deleteMember(int $idTeam, int $idMember, SessionHandle $session): HttpResponse {
if(!$this->model->isCoach($idTeam, $session->getAccount()->getUser()->getEmail())) {
return ViewHttpResponse::twig('error.html.twig', [
'failures' => [ValidationFail::unauthorized("Vous n'avez pas accès à cette action pour cette équipe.")],
], HttpCodes::FORBIDDEN);
}
$teamId = $this->model->deleteMember($idMember, $idTeam);
if($teamId == -1 || $session->getAccount()->getUser()->getId() == $idMember) {
return HttpResponse::redirect('/');
}
return $this->displayTeam($teamId, $session);
}
/**
* @param int $idTeam
* @param SessionHandle $session
* @return ViewHttpResponse
*/
public function displayEditTeam(int $idTeam, SessionHandle $session): ViewHttpResponse {
return ViewHttpResponse::twig("edit_team.html.twig", ['team' => $this->model->getTeam($idTeam, $session->getAccount()->getUser()->getId())]);
}
/**
* @param int $idTeam
* @param array<string,mixed> $request
* @param SessionHandle $session
* @return HttpResponse
*/
public function editTeam(int $idTeam, array $request, SessionHandle $session): HttpResponse {
if(!$this->model->isCoach($idTeam, $session->getAccount()->getUser()->getEmail())) {
return ViewHttpResponse::twig('error.html.twig', [
'failures' => [ValidationFail::unauthorized("Vous n'avez pas accès à cette action pour cette équipe.")],
], HttpCodes::FORBIDDEN);
}
$failures = [];
$request = HttpRequest::from($request, $failures, [
"name" => [Validators::lenBetween(1, 32), Validators::nameWithSpaces()],
"main_color" => [Validators::hexColor()],
"second_color" => [Validators::hexColor()],
"picture" => [Validators::isURL()],
]);
if (!empty($failures)) {
$badFields = [];
foreach ($failures as $e) {
if ($e instanceof FieldValidationFail) {
$badFields[] = $e->getFieldName();
}
}
return ViewHttpResponse::twig('edit_team.html.twig', ['bad_fields' => $badFields]);
}
$this->model->editTeam($idTeam, $request['name'], $request['picture'], $request['main_color'], $request['second_color']);
return HttpResponse::redirect('/team/' . $idTeam);
}
}

@ -1,39 +0,0 @@
<?php
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\HttpResponse;
use IQBall\Core\Model\TacticModel;
class VisualizerController {
private TacticModel $tacticModel;
/**
* @param TacticModel $tacticModel
*/
public function __construct(TacticModel $tacticModel) {
$this->tacticModel = $tacticModel;
}
/**
* Opens a visualisation page for the tactic specified by its identifier in the url.
* @param int $id
* @param SessionHandle $session
* @return HttpResponse
*/
public function openVisualizer(int $id, SessionHandle $session): HttpResponse {
$tactic = $this->tacticModel->get($id);
$failure = TacticValidator::validateAccess($id, $tactic, $session->getAccount()->getUser()->getId());
if ($failure != null) {
return ViewHttpResponse::twig('error.html.twig', ['failures' => [$failure]], HttpCodes::NOT_FOUND);
}
return ViewHttpResponse::react("views/Visualizer.tsx", ["name" => $tactic->getName()]);
}
}

@ -1,22 +0,0 @@
<?php
namespace IQBall\App\Session;
use IQBall\Core\Data\Account;
/**
* The mutable side of a session handle
*/
interface MutableSessionHandle extends SessionHandle {
/**
* @param string|null $url the url to redirect the user to after authentication.
*/
public function setInitialTarget(?string $url): void;
/**
* @param Account $account update the session's account
*/
public function setAccount(Account $account): void;
public function destroy(): void;
}

@ -1,38 +0,0 @@
<?php
namespace IQBall\App\Session;
use IQBall\Core\Data\Account;
/**
* A PHP session handle
*/
class PhpSessionHandle implements MutableSessionHandle {
public static function init(): self {
if (session_status() !== PHP_SESSION_NONE) {
throw new \Exception("A php session is already started !");
}
session_start();
return new PhpSessionHandle();
}
public function getAccount(): ?Account {
return $_SESSION["account"] ?? null;
}
public function getInitialTarget(): ?string {
return $_SESSION["target"] ?? null;
}
public function setAccount(Account $account): void {
$_SESSION["account"] = $account;
}
public function setInitialTarget(?string $url): void {
$_SESSION["target"] = $url;
}
public function destroy(): void {
session_destroy();
}
}

@ -1,23 +0,0 @@
<?php
namespace IQBall\App\Session;
use IQBall\Core\Data\Account;
/**
* An immutable session handle
*/
interface SessionHandle {
/**
* The initial target url if the user wanted to perform an action that requires authentication
* but has been required to login first in the application.
* @return string|null Get the initial targeted URL
*/
public function getInitialTarget(): ?string;
/**
* The session account if the user is logged in.
* @return Account|null
*/
public function getAccount(): ?Account;
}

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

@ -1,75 +0,0 @@
<?php
namespace IQBall\App;
use IQBall\Core\Http\HttpCodes;
use IQBall\Core\Http\HttpResponse;
class ViewHttpResponse extends HttpResponse {
public const TWIG_VIEW = 0;
public const REACT_VIEW = 1;
/**
* @var string File path of the responded view
*/
private string $file;
/**
* @var array<string, mixed> View arguments
*/
private array $arguments;
/**
* @var int Kind of view, see {@link self::TWIG_VIEW} and {@link self::REACT_VIEW}
*/
private int $kind;
/**
* @param int $code
* @param int $kind
* @param string $file
* @param array<string, mixed> $arguments
*/
private function __construct(int $kind, string $file, array $arguments, int $code = HttpCodes::OK) {
parent::__construct($code, []);
$this->kind = $kind;
$this->file = $file;
$this->arguments = $arguments;
}
public function getViewKind(): int {
return $this->kind;
}
public function getFile(): string {
return $this->file;
}
/**
* @return array<string, string>
*/
public function getArguments(): array {
return $this->arguments;
}
/**
* Create a twig view response
* @param string $file
* @param array<string, mixed> $arguments
* @param int $code
* @return ViewHttpResponse
*/
public static function twig(string $file, array $arguments, int $code = HttpCodes::OK): ViewHttpResponse {
return new ViewHttpResponse(self::TWIG_VIEW, $file, $arguments, $code);
}
/**
* Create a react view response
* @param string $file
* @param array<string, mixed> $arguments
* @param int $code
* @return ViewHttpResponse
*/
public static function react(string $file, array $arguments, int $code = HttpCodes::OK): ViewHttpResponse {
return new ViewHttpResponse(self::REACT_VIEW, $file, $arguments, $code);
}
}

@ -1,23 +0,0 @@
<!doctype html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport"
content="width=device-width, user-scalable=no, initial-scale=1.0, maximum-scale=1.0, minimum-scale=1.0">
<meta http-equiv="X-UA-Compatible" content="ie=edge">
<title>Paramètres</title>
<style>
body {
padding-left: 10%;
padding-right: 10%;
}
</style>
</head>
<body>
<button onclick="location.pathname='{{ path('/home') }}'">Retour</button>
<h1>Paramètres</h1>
</body>
</html>

@ -1,118 +0,0 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Ajouter un membre</title>
<style>
body {
font-family: Arial, sans-serif;
background-color: #f1f1f1;
}
.container {
max-width: 400px;
margin: 0 auto;
padding: 20px;
background-color: #fff;
border-radius: 5px;
box-shadow: 0 0 10px rgba(0, 0, 0, 0.1);
}
h2 {
text-align: center;
}
.form-group {
margin-bottom: 20px;
}
label {
display: block;
margin-bottom: 5px;
}
input[type="text"],
input[type="radio"] {
width: 100%;
padding: 10px;
border: 1px solid #ccc;
border-radius: 5px;
}
fieldset {
border: 1px solid #ccc;
border-radius: 5px;
padding: 10px;
margin-bottom: 10px;
}
input[type="submit"] {
background-color: #007bff;
color: #fff;
padding: 10px 20px;
border: none;
border-radius: 5px;
cursor: pointer;
}
input[type="submit"]:hover {
background-color: #0056b3;
}
.role {
margin-top: 10px;
}
.radio {
display: flex;
justify-content: space-between;
}
.failed{
color: red;
}
</style>
</head>
<body>
<header>
<h1><a href="{{ path('/') }}">IQBall</a></h1>
</header>
<div class="container">
<h2>Ajouter un membre à votre équipe</h2>
<form action="{{ path("/team/#{idTeam}/addMember") }}" method="POST">
<div class="form-group">
<label for="email">Email du membre :</label>
{% if badEmail %}
<p class="failed">Email invalide</p>
{% endif %}
{%if notFound %}
<p class="failed">Cette personne n'a pas été trouvé</p>
{% endif %}
{% if alreadyExisting %}
<p class="failed">Cette personne est déjà dans l'équipe</p>
{% endif %}
<input type="text" id="email" name="email" required>
<fieldset class="role">
<legend>Rôle du membre dans l'équipe :</legend>
<div class="radio">
<label for="P">Joueur</label>
<input type="radio" id="P" name="role" value="PLAYER" checked />
</div>
<div class="radio">
<label for="C">Coach</label>
<input type="radio" id="C" name="role" value="COACH" />
</div>
</fieldset>
</div>
<div class="form-group">
<input type="submit" value="Confirmer">
</div>
</form>
</div>
</body>
</html>

@ -1,73 +0,0 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Ajouter un membre</title>
<style>
body {
font-family: Arial, sans-serif;
background-color: #f1f1f1;
}
.container {
max-width: 400px;
margin: 0 auto;
padding: 20px;
background-color: #fff;
border-radius: 5px;
box-shadow: 0 0 10px rgba(0, 0, 0, 0.1);
}
h2 {
text-align: center;
}
.form-group {
margin-bottom: 20px;
}
label {
display: block;
margin-bottom: 5px;
}
input[type="text"] {
width: 100%;
padding: 10px;
border: 1px solid #ccc;
border-radius: 5px;
}
input[type="submit"] {
background-color: #007bff;
color: #fff;
padding: 10px 20px;
border: none;
border-radius: 5px;
cursor: pointer;
}
input[type="submit"]:hover {
background-color: #0056b3;
}
</style>
</head>
<body>
<div class="container">
<h2>Supprimez un membre de votre équipe</h2>
<form action="{{ path('/team/members/remove') }}" method="POST">
<div class="form-group">
<label for="team">Team où supprimer le membre :</label>
<input type="text" id="team" name="team" required>
<label for="mail">Email du membre :</label>
<input type="text" id="mail" name="mail" required>
</div>
<div class="form-group">
<input type="submit" value="Confirmer">
</div>
</form>
</div>
</body>
</html>

@ -1,46 +0,0 @@
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Profil Utilisateur</title>
<style>
body {
font-family: Arial, sans-serif;
background-color: #f4f4f4;
margin: 0;
padding: 0;
display: flex;
align-items: start;
justify-content: center;
height: 100vh;
}
.user-profile {
background-color: #7FBFFF;
padding: 20px;
border-radius: 10px;
box-shadow: 0 0 10px rgba(0, 0, 0, 0.1);
max-width: 400px;
width: 100%;
text-align: center;
}
h1 {
color: #333;
}
p {
color: #666;
}
</style>
</head>
<body>
<div class="user-profile">
<h1>Votre profil</h1>
<p><strong>Pseudo : </strong> {{ username }} </p>
<p><strong>Email : {{ email }} </strong></p>
</div>
</body>
</html>

@ -1,107 +0,0 @@
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Connexion</title>
</head>
<body>
<style>
body {
font-family: Arial, sans-serif;
background-color: #f1f1f1;
}
.container {
max-width: 400px;
margin: 0 auto;
padding: 20px;
background-color: #fff;
border-radius: 5px;
box-shadow: 0 0 10px rgba(0, 0, 0, 0.1);
}
h2 {
text-align: center;
}
.form-group {
margin-bottom: 20px;
}
label {
display: block;
margin-bottom: 5px;
}
input[type="text"], input[type="password"] {
width: 100%;
padding: 10px;
border: 1px solid #ccc;
border-radius: 5px;
}
.error-messages {
color: #ff331a;
font-style: italic;
}
{% for err in fails %}
.form-group
#
{{ err.getFieldName() }}
{
border-color: red
;
}
{% endfor %}
.inscr {
font-size: small;
}
#buttons{
display: flex;
justify-content: center;
padding: 10px 20px;
}
.button{
background-color: #007bff;
color: #fff;
padding: 10px 20px;
border: none;
border-radius: 5px;
cursor: pointer;
}
.button:hover{
background-color: #0056b3;
}
</style>
<div class="container">
<center><h2>Se connecter</h2></center>
<form action="{{ path('/login') }}" method="post">
<div class="form-group">
{% for name in fails %}
<label class="error-messages"> {{ name.getMessage() }} </label>
{% endfor %}
<label for="email">Email :</label>
<input type="text" id="email" name="email" required>
<label for="password">Mot de passe :</label>
<input type="password" id="password" name="password" required>
<a href="{{ path('/register') }}" class="inscr">Vous n'avez pas de compte ?</a>
<br><br>
<div id = "buttons">
<input class = "button" type="submit" value="Se connecter">
</div>
</form>
</div>
</body>
</html>

@ -1,116 +0,0 @@
<html lang="en">
<head>
<meta charset="UTF-8">
<title>S'enregistrer</title>
</head>
<body>
<style>
body {
font-family: Arial, sans-serif;
background-color: #f1f1f1;
}
.container {
max-width: 400px;
margin: 0 auto;
padding: 20px;
background-color: #fff;
border-radius: 5px;
box-shadow: 0 0 10px rgba(0, 0, 0, 0.1);
}
h2 {
text-align: center;
}
.form-group {
margin-bottom: 20px;
}
label {
display: block;
margin-bottom: 5px;
}
input[type="text"], input[type="password"] {
width: 100%;
padding: 10px;
border: 1px solid #ccc;
border-radius: 5px;
}
.error-messages {
color: #ff331a;
font-style: italic;
}
{% for err in fails %}
.form-group
#
{{ err.getFieldName() }}
{
border-color: red
;
}
{% endfor %}
.inscr{
font-size: small;
text-align: right;
}
#buttons{
display: flex;
justify-content: center;
padding: 10px 20px;
}
.button{
background-color: #007bff;
color: #fff;
padding: 10px 20px;
border: none;
border-radius: 5px;
cursor: pointer;
}
.button:hover{
background-color: #0056b3;
}
</style>
<div class="container">
<center><h2>S'enregistrer</h2></center>
<form action="{{ path('/register') }}" method="post">
<div class="form-group">
{% for name in fails %}
<label class="error-messages"> {{ name.getFieldName() }} : {{ name.getMessage() }} </label>
{% endfor %}
<label for="username">Nom d'utilisateur :</label>
<input type="text" id="username" name="username" required>
<label for="password">Mot de passe :</label>
<input type="password" id="password" name="password" required>
<label for="confirmpassword">Confirmer le mot de passe :</label>
<input type="password" id="confirmpassword" name="confirmpassword" required>
<label for="email">Email :</label>
<input type="text" id="email" name="email" required>
<a href="{{ path('/login') }}" class="inscr">Vous avez déjà un compte ?</a>
</div>
<div id = "buttons">
<input class = "button" type="submit" value="Créer votre compte">
</div>
</form>
</div>
</body>
</html>

@ -1,18 +0,0 @@
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Twig view</title>
</head>
<body>
<h1>Hello world</h1>
{% for v in results %}
<p>username: {{ v.name }}</p>
<p>description: {{ v.description }}</p>
{% endfor %}
</body>
</html>

@ -1,109 +0,0 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Twig view</title>
<style>
body {
background-color: #f1f1f1;
display: flex;
flex-direction: column;
align-items: center;
}
.square {
width: 50px;
height: 50px;
}
#main_color {
border: solid;
background-color: {{ team.getInfo().getMainColor().getValue() }};
}
#second_color {
background-color: {{ team.getInfo().getSecondColor().getValue() }};
border: solid;
}
section {
background-color: #fff;
display: flex;
flex-direction: column;
align-items: center;
width: 60%;
}
#colors{
flex-direction: row;
}
.color {
flex-direction: row;
justify-content: space-between;
}
.logo {
height: 80px;
width: 80px;
}
#delete{
border-radius:10px ;
background-color: red;
color: white;
}
.player{
flex-direction: row;
justify-content: space-evenly;
}
</style>
</head>
<body>
<header>
<h1><a href="{{ path('/') }}">IQBall</a></h1>
</header>
<section class="container">
{% if notDeleted %}
<popup>
<p>Cette équipe ne peut être supprimée.</p>
</popup>
{% endif %}
{% if team is defined %}
<div class="team">
<div>
<h1>{{ team.getInfo().getName() }}</h1>
<img src="{{ team.getInfo().getPicture() }}" alt="Logo d'équipe" class="logo">
</div>
<div id="colors">
<div class="color"><p>Couleur principale : </p>
<div class="square" id="main_color"></div>
</div>
<div class="color"><p>Couleur secondaire : </p>
<div class="square" id="second_color"></div>
</div>
</div>
{% if isCoach %}
<button id="delete" onclick="confirm('Êtes-vous sûr de supprimer cette équipe?') ? window.location.href = '{{ path("/team/#{team.getInfo().getId()}/delete") }}' : {}">Supprimer</button>
<button></button>
{% endif %}
{% for m in team.listMembers() %}
<div class="player">
<p> {{ m.getUserId() }} </p>
{% if m.getRole().isCoach() %}
<p> : Coach</p>
{% else %}
<p> : Joueur</p>
{% endif %}
</div>
{% endfor %}
</div>
{% else %}
<div>
<h3>Cette équipe ne peut être affichée</h3>
</div>
{% endif %}
</section>
</body>
</html>

@ -1,61 +0,0 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Twig view</title>
<style>
body {
display: flex;
flex-direction: column;
background-color: #f1f1f1;
align-items: center;
}
section{
flex-direction: row;
justify-content: space-around;
background-color: white;
width: 60%;
}
.team {
border-radius: 10px;
border-color: darkgrey;
}
.logo_team {
width: 15%;
aspect-ratio: 3/2;
object-fit: contain;
}
</style>
</head>
<body>
<header>
<h1><a href="{{ path('/') }}">IQBall</a></h1>
</header>
<section>
{% if teams is empty %}
<p>Aucune équipe n'a été trouvée</p>
<div class="container">
<h2>Chercher une équipe</h2>
<form action="{{ path('/team/search') }}" method="post">
<div class="form-group">
<label for="name">Nom de l'équipe :</label>
<input type="text" id="name" name="name" required>
</div>
<div class="form-group">
<input type="submit" value="Confirmer">
</div>
</form>
</div>
{% else %}
{% for t in teams %}
<div class="team" onclick="window.location.href = '{{ path("/team/#{t.getId()}") }}'">
<p>Nom de l'équipe : {{ t.getName() }}</p>
<img src="{{ t.getPicture() }}" alt="logo de l'équipe" class="logo_team">
</div>
{% endfor %}
{% endif %}
</section>
</body>
</html>

@ -1,81 +0,0 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Insertion view</title>
<style>
body {
font-family: Arial, sans-serif;
background-color: #f1f1f1;
}
.container {
max-width: 400px;
margin: 5px auto;
padding: 20px;
background-color: #fff;
border-radius: 5px;
box-shadow: 0 0 10px rgba(0, 0, 0, 0.1);
}
h2 {
text-align: center;
}
.form-group {
margin-bottom: 20px;
}
label {
display: block;
margin-bottom: 5px;
}
{% for item in bad_fields %}
#{{ item }}{
border-color: red;
}{% endfor %} input[type="text"], input[type="password"] {
width: 100%;
padding: 10px;
border: 1px solid #ccc;
border-radius: 5px;
}
input[type="submit"] {
background-color: #007bff;
color: #fff;
padding: 10px 20px;
border: none;
border-radius: 5px;
cursor: pointer;
}
input[type="submit"]:hover {
background-color: #0056b3;
}
</style>
</head>
<body>
<div class="container">
<h2>Modifier votre équipe</h2>
<form action="{{ path('/team/' ~ team.getInfo().getId() ~ '/edit') }}" method="post">
<div class="form-group">
<label for="name">Nom de l'équipe :</label>
<input type="text" id="name" name="name" value="{{ team.getInfo().getName() }}" required>
<label for="picture">Logo:</label>
<input type="text" id="picture" name="picture" value="{{ team.getInfo().getPicture() }}" required>
<label for="main_color">Couleur principale</label>
<input type="color" value="{{ team.getInfo().getMainColor() }}" id="main_color" name="main_color" required>
<label for="second_color">Couleur secondaire</label>
<input type="color" id="second_color" name="second_color" value="{{ team.getInfo().getSecondColor() }}" required>
</div>
<div class="form-group">
<input type="submit" value="Confirmer">
</div>
</form>
</div>
</body>
</html>

@ -1,57 +0,0 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Error</title>
<style>
body {
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
height: 100vh;
margin: 0;
}
h1 {
color: #da6110;
text-align: center;
margin-bottom: 15px
}
h2 {
text-align: center;
margin-bottom: 15px;
margin-top: 15px
}
.button {
display: block;
cursor: pointer;
background-color: white;
color: black;
text-align: center;
font-size: 20px;
border-radius: 12px;
border: 2px solid #da6110;
margin-top: 15px;
}
.button:hover {
background-color: #da6110
}
</style>
</head>
<body>
<h1>IQBall</h1>
{% for fail in failures %}
<h2>{{ fail.getKind() }} : {{ fail.getMessage() }}</h2>
{% endfor %}
<button class="button" onclick="location.href='{{ path('/home') }}'" type="button">Retour à la page d'accueil</button>
</body>
</html>

@ -1,97 +0,0 @@
<!doctype html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport"
content="width=device-width, user-scalable=no, initial-scale=1.0, maximum-scale=1.0, minimum-scale=1.0">
<meta http-equiv="X-UA-Compatible" content="ie=edge">
<title>Page d'accueil</title>
<style>
body {
padding-left: 10%;
padding-right: 10%;
}
#bandeau {
display: flex;
flex-direction: row;
}
#bandeau > h1 {
self-align: center;
padding: 0%;
margin: 0%;
justify-content: center;
}
#account {
display: flex;
flex-direction: column;
align-content: center;
}
#account:hover {
background-color: gray;
}
#account img {
width: 70%;
height: auto;
align-self: center;
padding: 5%;
margin: 0%;
}
#account p {
align-self: center;
}
</style>
</head>
<body>
<button onclick="location.pathname='{{ path('/disconnect') }}'"> Se déconnecter</button>
<div id="bandeau">
<h1>IQ Ball</h1>
<div id="account" onclick="location.pathname='{{ path('/settings') }}'">
<img
src="{{ path('/assets/icon/account.svg') }}"
alt="Account logo"
/>
<p>Mon profil
<p>
</div>
</div>
<h2>Mes équipes</h2>
<button onclick="location.pathname='{{ path('/team/new') }}'"> Créer une nouvelle équipe</button>
{% if recentTeam != null %}
{% for team in recentTeam %}
<div>
<p> {{ team.name }} </p>
</div>
{% endfor %}
{% else %}
<p>Aucune équipe créée !</p>
{% endif %}
<h2> Mes strategies </h2>
<button onclick="location.pathname='{{ path('/tactic/new') }}'"> Créer une nouvelle tactique</button>
{% if recentTactic != null %}
{% for tactic in recentTactic %}
<div onclick="location.pathname='{{ path("/tactic/#{strategie.id}/edit") }}'">
<p> {{ tactic.id }} - {{ tactic.name }} - {{ tactic.creation_date }} </p>
<button onclick="location.pathname='{{ path("/tactic/#{tactic.id}/edit") }}'"> Editer la
stratégie {{ tactic.id }} </button>
</div>
{% endfor %}
{% else %}
<p> Aucune tactique créée !</p>
{% endif %}
</body>
</html>

@ -1,81 +0,0 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Insertion view</title>
<style>
body {
font-family: Arial, sans-serif;
background-color: #f1f1f1;
}
.container {
max-width: 400px;
margin: 0 auto;
padding: 20px;
background-color: #fff;
border-radius: 5px;
box-shadow: 0 0 10px rgba(0, 0, 0, 0.1);
}
h2 {
text-align: center;
}
.form-group {
margin-bottom: 20px;
}
label {
display: block;
margin-bottom: 5px;
}
{% for item in bad_fields %}
#{{ item }}{
border-color: red;
}{% endfor %} input[type="text"], input[type="password"] {
width: 100%;
padding: 10px;
border: 1px solid #ccc;
border-radius: 5px;
}
input[type="submit"] {
background-color: #007bff;
color: #fff;
padding: 10px 20px;
border: none;
border-radius: 5px;
cursor: pointer;
}
input[type="submit"]:hover {
background-color: #0056b3;
}
</style>
</head>
<body>
<div class="container">
<h2>Créer une équipe</h2>
<form action="{{ path('/team/new') }}" method="post">
<div class="form-group">
<label for="name">Nom de l'équipe :</label>
<input type="text" id="name" name="name" required>
<label for="picture">Logo:</label>
<input type="text" id="picture" name="picture" required>
<label for="main_color">Couleur principale</label>
<input type="color" value="#ffffff" id="main_color" name="main_color" required>
<label for="second_color">Couleur secondaire</label>
<input type="color" id="second_color" name="second_color" required>
</div>
<div class="form-group">
<input type="submit" value="Confirmer">
</div>
</form>
</div>
</body>
</html>

@ -1,79 +0,0 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Insertion view</title>
<style>
body {
font-family: Arial, sans-serif;
background-color: #f1f1f1;
display: flex;
flex-direction: column;
align-items: center;
}
.container {
max-width: 400px;
margin: 0 auto;
padding: 20px;
background-color: #fff;
border-radius: 5px;
box-shadow: 0 0 10px rgba(0, 0, 0, 0.1);
}
h2 {
text-align: center;
}
.form-group {
margin-bottom: 20px;
}
label {
display: block;
margin-bottom: 5px;
}
{% for item in bad_fields %}
#{{ item }}{
border-color: red;
}{% endfor %} input[type="text"], input[type="password"] {
width: 100%;
padding: 10px;
border: 1px solid #ccc;
border-radius: 5px;
}
input[type="submit"] {
background-color: #007bff;
color: #fff;
padding: 10px 20px;
border: none;
border-radius: 5px;
cursor: pointer;
}
input[type="submit"]:hover {
background-color: #0056b3;
}
</style>
</head>
<body>
<header>
<h1><a href="{{ path('/') }}">IQBall</a></h1>
</header>
<div class="container">
<h2>Chercher une équipe</h2>
<form action="{{ path('/team/search') }}" method="post">
<div class="form-group">
<label for="name">Nom de l'équipe :</label>
<input type="text" id="name" name="name" required>
</div>
<div class="form-group">
<input type="submit" value="Confirmer">
</div>
</form>
</div>
</body>
</html>

@ -1,58 +0,0 @@
<!doctype html>
<html lang="en">
<head>
<script type="module">
<?php
if (SUPPORTS_FAST_REFRESH) {
$asset_server = asset("");
echo "
import RefreshRuntime from '{$asset_server}front/@react-refresh'
RefreshRuntime.injectIntoGlobalHook(window)
window.\$RefreshReg$ = () => {}
window.\$RefreshSig$ = () => (type) => type
window.__vite_plugin_react_preamble_installed__ = true
";
}
?>
</script>
<link rel="icon" href="<?= asset("assets/favicon.ico") ?>">
<meta charset="UTF-8">
<meta name="viewport"
content="width=device-width, user-scalable=no, initial-scale=1.0, maximum-scale=1.0, minimum-scale=1.0">
<meta http-equiv="X-UA-Compatible" content="ie=edge">
<title>Document</title>
<!-- remove default screen margin,
html and body to take full screen size -->
<style>
body, html, #root {
height: 100%;
width: 100%;
margin: 0;
}
</style>
</head>
<body>
<div id="root"></div>
<!--
here's the magic.
imports the given view URL, and assume that the view exports a function named `Component`.
see ViewRenderer.tsx::renderView for more info
-->
<script type="module">
import {renderView} from "<?= asset("ViewRenderer.tsx") ?>"
import Component from "<?= asset($url) ?>"
renderView(Component, <?= json_encode($arguments) ?>)
</script>
<script>
</script>
</body>
</html>

@ -1,13 +0,0 @@
<?php
/**
* sends a react view to the user client.
* @param string $url url of the react file to render
* @param array<string, mixed> $arguments arguments to pass to the rendered react component
* The arguments must be a json-encodable key/value dictionary.
* @return void
*/
function send_react_front(string $url, array $arguments) {
// the $url and $argument values are used into the included file
require_once "react-display-file.php";
}

@ -6,4 +6,4 @@ export const API = import.meta.env.VITE_API_ENDPOINT
/**
* This constant defines the base app's endpoint.
*/
export const BASE = import.meta.env.VITE_BASE
export const BASE = import.meta.env.BASE_URL.slice(0, import.meta.env.BASE_URL.length - 1)

@ -1,58 +0,0 @@
<?php
namespace IQBall\Core;
use IQBall\Core\Http\HttpResponse;
/**
* Represent an action.
* @template S session
*/
class Action {
/**
* @var callable(mixed[], S): HttpResponse $action action to call
*/
protected $action;
private bool $isAuthRequired;
/**
* @param callable(mixed[], S): HttpResponse $action
*/
protected function __construct(callable $action, bool $isAuthRequired) {
$this->action = $action;
$this->isAuthRequired = $isAuthRequired;
}
public function isAuthRequired(): bool {
return $this->isAuthRequired;
}
/**
* Runs an action
* @param mixed[] $params
* @param S $session
* @return HttpResponse
*/
public function run(array $params, $session): HttpResponse {
$params = array_values($params);
$params[] = $session;
return call_user_func_array($this->action, $params);
}
/**
* @param callable(mixed[], S): HttpResponse $action
* @return Action<S> an action that does not require to have an authorization.
*/
public static function noAuth(callable $action): Action {
return new Action($action, false);
}
/**
* @param callable(mixed[], S): HttpResponse $action
* @return Action<S> an action that does require to have an authorization.
*/
public static function auth(callable $action): Action {
return new Action($action, true);
}
}

@ -1,41 +0,0 @@
<?php
namespace IQBall\Core\Data;
/**
* Base class of a user account.
* Contains the private information that we don't want
* to share to other users, or non-needed public information
*/
class Account {
/**
* @var string string token
*/
private string $token;
/**
* @var User contains all the account's "public" information
*/
private User $user;
/**
* @param string $token
* @param User $user
*/
public function __construct(string $token, User $user) {
$this->token = $token;
$this->user = $user;
}
public function getToken(): string {
return $this->token;
}
/**
* @return User
*/
public function getUser(): User {
return $this->user;
}
}

@ -1,61 +0,0 @@
<?php
namespace IQBall\Core\Data;
use InvalidArgumentException;
/**
* Enumeration class workaround
* As there is no enumerations in php 7.4, this class
* encapsulates an integer value and use it as a variant discriminant
*/
final class CourtType {
private const COURT_PLAIN = 0;
private const COURT_HALF = 1;
private int $value;
private function __construct(int $val) {
if ($val < self::COURT_PLAIN || $val > self::COURT_HALF) {
throw new InvalidArgumentException("Valeur du rôle invalide");
}
$this->value = $val;
}
public static function plain(): CourtType {
return new CourtType(CourtType::COURT_PLAIN);
}
public static function half(): CourtType {
return new CourtType(CourtType::COURT_HALF);
}
public function name(): string {
switch ($this->value) {
case self::COURT_HALF:
return "HALF";
case self::COURT_PLAIN:
return "PLAIN";
}
die("unreachable");
}
public static function fromName(string $name): ?CourtType {
switch ($name) {
case "HALF":
return CourtType::half();
case "PLAIN":
return CourtType::plain();
default:
return null;
}
}
public function isPlain(): bool {
return ($this->value == self::COURT_PLAIN);
}
public function isHalf(): bool {
return ($this->value == self::COURT_HALF);
}
}

@ -1,57 +0,0 @@
<?php
namespace IQBall\Core\Data;
/**
* information about a team member
*/
class Member implements \JsonSerializable {
private User $user;
/**
* @var int The member's team id
*/
private int $teamId;
/**
* @var string the member's role
*/
private string $role;
/**
* @param User $user
* @param int $teamId
* @param string $role
*/
public function __construct(User $user, int $teamId, string $role) {
$this->user = $user;
$this->teamId = $teamId;
$this->role = $role;
}
/**
* @return string
*/
public function getRole(): string {
return $this->role;
}
/**
* @return int
*/
public function getTeamId(): int {
return $this->teamId;
}
/**
* @return User
*/
public function getUser(): User {
return $this->user;
}
public function jsonSerialize() {
return get_object_vars($this);
}
}

@ -1,62 +0,0 @@
<?php
namespace IQBall\Core\Data;
class TacticInfo {
private int $id;
private string $name;
private int $creationDate;
private int $ownerId;
private CourtType $courtType;
private string $content;
/**
* @param int $id
* @param string $name
* @param int $creationDate
* @param int $ownerId
* @param CourtType $type
* @param string $content
*/
public function __construct(int $id, string $name, int $creationDate, int $ownerId, CourtType $type, string $content) {
$this->id = $id;
$this->name = $name;
$this->ownerId = $ownerId;
$this->creationDate = $creationDate;
$this->courtType = $type;
$this->content = $content;
}
/**
* @return string
*/
public function getContent(): string {
return $this->content;
}
public function getId(): int {
return $this->id;
}
public function getName(): string {
return $this->name;
}
/**
* @return int
*/
public function getOwnerId(): int {
return $this->ownerId;
}
public function getCourtType(): CourtType {
return $this->courtType;
}
/**
* @return int
*/
public function getCreationDate(): int {
return $this->creationDate;
}
}

@ -1,38 +0,0 @@
<?php
namespace IQBall\Core\Data;
class Team implements \JsonSerializable {
private TeamInfo $info;
/**
* @var Member[] maps users with their role
*/
private array $members;
/**
* @param TeamInfo $info
* @param Member[] $members
*/
public function __construct(TeamInfo $info, array $members = []) {
$this->info = $info;
$this->members = $members;
}
public function getInfo(): TeamInfo {
return $this->info;
}
/**
* @return Member[]
*/
public function listMembers(): array {
return $this->members;
}
public function jsonSerialize() {
return get_object_vars($this);
}
}

@ -1,53 +0,0 @@
<?php
namespace IQBall\Core\Data;
class TeamInfo implements \JsonSerializable {
private int $id;
private string $name;
private string $picture;
private string $mainColor;
private string $secondColor;
/**
* @param int $id
* @param string $name
* @param string $picture
* @param string $mainColor
* @param string $secondColor
*/
public function __construct(int $id, string $name, string $picture, string $mainColor, string $secondColor) {
$this->id = $id;
$this->name = $name;
$this->picture = $picture;
$this->mainColor = $mainColor;
$this->secondColor = $secondColor;
}
public function getId(): int {
return $this->id;
}
public function getName(): string {
return $this->name;
}
public function getPicture(): string {
return $this->picture;
}
public function getMainColor(): string {
return $this->mainColor;
}
public function getSecondColor(): string {
return $this->secondColor;
}
public function jsonSerialize() {
return get_object_vars($this);
}
}

@ -1,72 +0,0 @@
<?php
namespace IQBall\Core\Data;
use _PHPStan_4c4f22f13\Nette\Utils\Json;
class User implements \JsonSerializable {
/**
* @var string $email user's mail address
*/
private string $email;
/**
* @var string the user's username
*/
private string $name;
/**
* @var int the user's id
*/
private int $id;
/**
* @var string user's profile picture
*/
private string $profilePicture;
/**
* @param string $email
* @param string $name
* @param int $id
* @param string $profilePicture
*/
public function __construct(string $email, string $name, int $id, string $profilePicture) {
$this->email = $email;
$this->name = $name;
$this->id = $id;
$this->profilePicture = $profilePicture;
}
/**
* @return string
*/
public function getEmail(): string {
return $this->email;
}
/**
* @return string
*/
public function getName(): string {
return $this->name;
}
/**
* @return int
*/
public function getId(): int {
return $this->id;
}
/**
* @return string
*/
public function getProfilePicture(): string {
return $this->profilePicture;
}
public function jsonSerialize() {
return get_object_vars($this);
}
}

@ -1,100 +0,0 @@
<?php
namespace IQBall\Core\Gateway;
use IQBall\Core\Connection;
use IQBall\Core\Data\Member;
use IQBall\Core\Data\User;
use PDO;
class MemberGateway {
private Connection $con;
/**
* @param Connection $con
*/
public function __construct(Connection $con) {
$this->con = $con;
}
/**
* insert member to a team
* @param int $idTeam
* @param int $userId
* @param string $role
* @return void
*/
public function insert(int $idTeam, int $userId, string $role): void {
$this->con->exec(
"INSERT INTO Member(id_team, id_user, role) VALUES (:id_team, :id_user, :role)",
[
":id_team" => [$idTeam, PDO::PARAM_INT],
":id_user" => [$userId, PDO::PARAM_INT],
":role" => [$role, PDO::PARAM_STR],
]
);
}
/**
* @param int $teamId
* @return Member[]
*/
public function getMembersOfTeam(int $teamId): array {
$rows = $this->con->fetch(
"SELECT a.id,a.email,a.username,a.profilePicture,m.role FROM Account a,team t,Member m WHERE t.id = :id AND m.id_team = t.id AND m.id_user = a.id",
[
":id" => [$teamId, PDO::PARAM_INT],
]
);
return array_map(fn($row) => new Member(new User($row['email'], $row['username'], $row['id'], $row['profilePicture']), $teamId, $row['role']), $rows);
}
/**
* remove member from given team
* @param int $idTeam
* @param int $idMember
* @return void
*/
public function remove(int $idTeam, int $idMember): void {
$this->con->exec(
"DELETE FROM Member WHERE id_team = :id_team AND id_user = :id_user",
[
":id_team" => [$idTeam, PDO::PARAM_INT],
":id_user" => [$idMember, PDO::PARAM_INT],
]
);
}
/**
* @param string $email
* @param int $idTeam
* @return bool
*/
public function isCoach(string $email, int $idTeam): bool {
$result = $this->con->fetch(
"SELECT role FROM Member WHERE id_team=:team AND id_user = (SELECT id FROM Account WHERE email=:email)",
[
"team" => [$idTeam, PDO::PARAM_INT],
"email" => [$email, PDO::PARAM_STR],
]
)[0]['role'];
return $result == 'COACH';
}
/**
* @param int $idTeam
* @param int $idCurrentUser
* @return bool
*/
public function isMemberOfTeam(int $idTeam, int $idCurrentUser): bool {
$result = $this->con->fetch(
"SELECT id_user FROM Member WHERE id_team = :team AND id_user = :user",
[
"team" => [$idTeam, PDO::PARAM_INT],
"user" => [$idCurrentUser, PDO::PARAM_INT],
]
);
return !empty($result);
}
}

@ -1,133 +0,0 @@
<?php
namespace IQBall\Core\Gateway;
use IQBall\Core\Connection;
use IQBall\Core\Data\CourtType;
use IQBall\Core\Data\TacticInfo;
use PDO;
class TacticInfoGateway {
private Connection $con;
/**
* @param Connection $con
*/
public function __construct(Connection $con) {
$this->con = $con;
}
/**
* get tactic information from given identifier
* @param int $id
* @return TacticInfo|null
*/
public function get(int $id): ?TacticInfo {
$res = $this->con->fetch(
"SELECT * FROM Tactic WHERE id = :id",
[":id" => [$id, PDO::PARAM_INT]]
);
if (!isset($res[0])) {
return null;
}
$row = $res[0];
$type = CourtType::fromName($row['court_type']);
return new TacticInfo($id, $row["name"], strtotime($row["creation_date"]), $row["owner"], $type, $row['content']);
}
/**
* Return the nb last tactics created
*
* @param integer $nb
* @return array<array<string,mixed>>
*/
public function getLast(int $nb, int $ownerId): ?array {
$res = $this->con->fetch(
"SELECT *
FROM Tactic
WHERE owner = :ownerId
ORDER BY creation_date DESC
LIMIT :nb",
[
":ownerId" => [$ownerId, PDO::PARAM_INT],":nb" => [$nb, PDO::PARAM_INT],
]
);
if (count($res) == 0) {
return [];
}
return $res;
}
/**
* Get all the tactics of the owner
*
* @return array<array<string,mixed>>
*/
public function getAll(int $ownerId): ?array {
$res = $this->con->fetch(
"SELECT *
FROM Tactic
WHERE owner = :ownerId
ORDER BY name DESC",
[
":ownerId" => [$ownerId, PDO::PARAM_INT],
]
);
if (count($res) == 0) {
return [];
}
return $res;
}
/**
* @param string $name
* @param int $owner
* @param CourtType $type
* @return int inserted tactic id
*/
public function insert(string $name, int $owner, CourtType $type): int {
$this->con->exec(
"INSERT INTO Tactic(name, owner, court_type) VALUES(:name, :owner, :court_type)",
[
":name" => [$name, PDO::PARAM_STR],
":owner" => [$owner, PDO::PARAM_INT],
":court_type" => [$type->name(), PDO::PARAM_STR],
]
);
return intval($this->con->lastInsertId());
}
/**
* update name of given tactic identifier
* @param int $id
* @param string $name
* @return bool
*/
public function updateName(int $id, string $name): bool {
$stmnt = $this->con->prepare("UPDATE Tactic SET name = :name WHERE id = :id");
$stmnt->execute([
":name" => $name,
":id" => $id,
]);
return $stmnt->rowCount() == 1;
}
/***
* Updates a given tactics content
* @param int $id
* @param string $json
* @return bool
*/
public function updateContent(int $id, string $json): bool {
$stmnt = $this->con->prepare("UPDATE Tactic SET content = :content WHERE id = :id");
$stmnt->execute([
":content" => $json,
":id" => $id,
]);
return $stmnt->rowCount() == 1;
}
}

@ -1,18 +0,0 @@
<?php
namespace IQBall\Core\Http;
/**
* Utility class to define constants of used http codes
*/
class HttpCodes {
public const OK = 200;
public const FOUND = 302;
public const BAD_REQUEST = 400;
public const UNAUTHORIZED = 401;
public const FORBIDDEN = 403;
public const NOT_FOUND = 404;
}

@ -1,81 +0,0 @@
<?php
namespace IQBall\Core\Http;
use IQBall\Core\Validation\FieldValidationFail;
use IQBall\Core\Validation\Validation;
use IQBall\Core\Validation\ValidationFail;
use IQBall\Core\Validation\Validator;
use ArrayAccess;
use Exception;
/**
* @implements ArrayAccess<string, mixed>
* */
class HttpRequest implements ArrayAccess {
/**
* @var array<string, mixed>
*/
private array $data;
/**
* @param array<string, mixed> $data
*/
private function __construct(array $data) {
$this->data = $data;
}
/**
* Creates a new HttpRequest instance, and ensures that the given request data validates the given schema.
* This is a simple function that only supports flat schemas (non-composed, the data must only be a k/v array pair.)
* @param array<string, mixed> $request the request's data
* @param array<string, ValidationFail> $fails a reference to a failure array, that will contain the reported validation failures.
* @param array<string, Validator[]> $schema the schema to satisfy. a schema is a simple array with a string key (which is the top-level field name), and a set of validators
* @return HttpRequest|null the built HttpRequest instance, or null if a field is missing, or if any of the schema validator failed
*/
public static function from(array $request, array &$fails, array $schema): ?HttpRequest {
$failure = false;
foreach ($schema as $fieldName => $fieldValidators) {
if (!isset($request[$fieldName])) {
$fails[] = FieldValidationFail::missing($fieldName);
$failure = true;
continue;
}
$failure |= Validation::validate($request[$fieldName], $fieldName, $fails, ...$fieldValidators);
}
if ($failure) {
return null;
}
return new HttpRequest($request);
}
public function offsetExists($offset): bool {
return isset($this->data[$offset]);
}
/**
* @param $offset
* @return mixed
*/
public function offsetGet($offset) {
return $this->data[$offset];
}
/**
* @param $offset
* @param $value
* @throws Exception
*/
public function offsetSet($offset, $value) {
throw new Exception("requests are immutable objects.");
}
/**
* @param $offset
* @throws Exception
*/
public function offsetUnset($offset) {
throw new Exception("requests are immutable objects.");
}
}

@ -1,65 +0,0 @@
<?php
namespace IQBall\Core\Http;
class HttpResponse {
/**
* @var array<string, string>
*/
private array $headers;
private int $code;
/**
* @param int $code
* @param array<string, string> $headers
*/
public function __construct(int $code, array $headers) {
$this->code = $code;
$this->headers = $headers;
}
public function getCode(): int {
return $this->code;
}
/**
* @return array<string, string>
*/
public function getHeaders(): array {
return $this->headers;
}
/**
* @param int $code
* @return HttpResponse
*/
public static function fromCode(int $code): HttpResponse {
return new HttpResponse($code, []);
}
/**
* @param string $url the url to redirect
* @param int $code only HTTP 3XX codes are accepted.
* @return HttpResponse a response that will redirect client to given url
*/
public static function redirect(string $url, int $code = HttpCodes::FOUND): HttpResponse {
global $basePath;
return self::redirect_absolute($basePath . $url, $code);
}
/**
* @param string $url the url to redirect
* @param int $code only HTTP 3XX codes are accepted.
* @return HttpResponse a response that will redirect client to given url
*/
public static function redirect_absolute(string $url, int $code = HttpCodes::FOUND): HttpResponse {
if ($code < 300 || $code >= 400) {
throw new \InvalidArgumentException("given code is not a redirection http code");
}
return new HttpResponse($code, ["Location" => $url]);
}
}

@ -1,28 +0,0 @@
<?php
namespace IQBall\Core\Http;
class JsonHttpResponse extends HttpResponse {
/**
* @var mixed Any JSON serializable value
*/
private $payload;
/**
* @param mixed $payload
*/
public function __construct($payload, int $code = HttpCodes::OK) {
parent::__construct($code, []);
$this->payload = $payload;
}
public function getJson(): string {
$result = json_encode($this->payload);
if (!$result) {
throw new \RuntimeException("Given payload is not json encodable");
}
return $result;
}
}

@ -1,113 +0,0 @@
<?php
namespace IQBall\Core\Model;
use IQBall\Core\Data\CourtType;
use IQBall\App\Session\SessionHandle;
use IQBall\Core\Data\TacticInfo;
use IQBall\Core\Gateway\TacticInfoGateway;
use IQBall\Core\Validation\ValidationFail;
class TacticModel {
public const TACTIC_DEFAULT_NAME = "Nouvelle tactique";
private TacticInfoGateway $tactics;
/**
* @param TacticInfoGateway $tactics
*/
public function __construct(TacticInfoGateway $tactics) {
$this->tactics = $tactics;
}
/**
* creates a new empty tactic, with given name
* @param string $name
* @param int $ownerId
* @param CourtType $type
* @return TacticInfo
*/
public function makeNew(string $name, int $ownerId, CourtType $type): TacticInfo {
$id = $this->tactics->insert($name, $ownerId, $type);
return $this->tactics->get($id);
}
/**
* creates a new empty tactic, with a default name
* @param int $ownerId
* @param CourtType $type
* @return TacticInfo|null
*/
public function makeNewDefault(int $ownerId, CourtType $type): ?TacticInfo {
return $this->makeNew(self::TACTIC_DEFAULT_NAME, $ownerId, $type);
}
/**
* Tries to retrieve information about a tactic
* @param int $id tactic identifier
* @return TacticInfo|null or null if the identifier did not match a tactic
*/
public function get(int $id): ?TacticInfo {
return $this->tactics->get($id);
}
/**
* Return the nb last tactics created
*
* @param integer $nb
* @return array<array<string,mixed>>
*/
/**
* Return the nb last tactics
*
* @param integer $nb
* @param integer $ownerId
* @return array<array<string,mixed>>
*/
public function getLast(int $nb, int $ownerId): array {
return $this->tactics->getLast($nb, $ownerId);
}
/**
* Get all the tactics of the owner
*
* @param integer $ownerId
* @return array<array<string,mixed>>
*/
public function getAll(int $ownerId): ?array {
return $this->tactics->getAll($ownerId);
}
/**
* Update the name of a tactic
* @param int $id the tactic identifier
* @param string $name the new name to set
* @return ValidationFail[] failures, if any
*/
public function updateName(int $id, string $name, int $authId): array {
$tactic = $this->tactics->get($id);
if ($tactic == null) {
return [ValidationFail::notFound("Could not find tactic")];
}
if ($tactic->getOwnerId() != $authId) {
return [ValidationFail::unauthorized()];
}
if (!$this->tactics->updateName($id, $name)) {
return [ValidationFail::error("Could not update name")];
}
return [];
}
public function updateContent(int $id, string $json): ?ValidationFail {
if (!$this->tactics->updateContent($id, $json)) {
return ValidationFail::error("Could not update content");
}
return null;
}
}

@ -1,142 +0,0 @@
<?php
namespace IQBall\Core\Model;
use IQBall\Core\Data\Team;
use IQBall\Core\Data\TeamInfo;
use IQBall\Core\Gateway\AccountGateway;
use IQBall\Core\Gateway\MemberGateway;
use IQBall\Core\Gateway\TeamGateway;
class TeamModel {
private AccountGateway $users;
private TeamGateway $teams;
private MemberGateway $members;
/**
* @param TeamGateway $gateway
* @param MemberGateway $members
* @param AccountGateway $users
*/
public function __construct(TeamGateway $gateway, MemberGateway $members, AccountGateway $users) {
$this->teams = $gateway;
$this->members = $members;
$this->users = $users;
}
/**
* Create a team
* @param string $name
* @param string $picture
* @param string $mainColor
* @param string $secondColor
* @return int
*/
public function createTeam(string $name, string $picture, string $mainColor, string $secondColor): int {
return $this->teams->insert($name, $picture, $mainColor, $secondColor);
}
/**
* add a member to a team
* @param string $mail
* @param int $teamId
* @param string $role
* @return int
*/
public function addMember(string $mail, int $teamId, string $role): int {
$user = $this->users->getAccountFromMail($mail);
if($user == null) {
return -1;
}
if(!$this->members->isMemberOfTeam($teamId, $user->getUser()->getId())) {
$this->members->insert($teamId, $user->getUser()->getId(), $role);
return 1;
}
return -2;
}
/**
* @param string $name
* @param int $id
* @return TeamInfo[]
*/
public function listByName(string $name, int $id): array {
return $this->teams->listByName($name, $id);
}
/**
* @param int $idTeam
* @param int $idCurrentUser
* @return Team|null
*/
public function getTeam(int $idTeam, int $idCurrentUser): ?Team {
if(!$this->members->isMemberOfTeam($idTeam, $idCurrentUser)) {
return null;
}
$teamInfo = $this->teams->getTeamById($idTeam);
$members = $this->members->getMembersOfTeam($idTeam);
return new Team($teamInfo, $members);
}
/**
* delete a member from given team identifier
* @param int $idMember
* @param int $teamId
* @return int
*/
public function deleteMember(int $idMember, int $teamId): int {
$this->members->remove($teamId, $idMember);
if(empty($this->members->getMembersOfTeam($teamId))) {
$this->teams->deleteTeam($teamId);
return -1;
}
return $teamId;
}
/**
* Delete a team
* @param string $email
* @param int $idTeam
* @return int
*/
public function deleteTeam(string $email, int $idTeam): int {
if($this->members->isCoach($email, $idTeam)) {
$this->teams->deleteTeam($idTeam);
return 0;
}
return -1;
}
/**
* Verify if the account associated to an email is in a specific team indicated with its id
* @param int $idTeam
* @param string $email
* @return bool
*/
public function isCoach(int $idTeam, string $email): bool {
return $this->members->isCoach($email, $idTeam);
}
/**
* Edit a team with its id, and replace the current attributes with the new ones
* @param int $idTeam
* @param string $newName
* @param string $newPicture
* @param string $newMainColor
* @param string $newSecondColor
* @return void
*/
public function editTeam(int $idTeam, string $newName, string $newPicture, string $newMainColor, string $newSecondColor) {
$this->teams->editTeam($idTeam, $newName, $newPicture, $newMainColor, $newSecondColor);
}
/**
* Get all user's teams
*
* @param integer $user
* @return array<array<string, mixed>>
*/
public function getAll(int $user): array {
return $this->teams->getAll($user);
}
}

@ -1,26 +0,0 @@
<?php
namespace IQBall\Core\Validation;
class ComposedValidator extends Validator {
private Validator $first;
private Validator $then;
/**
* @param Validator $first
* @param Validator $then
*/
public function __construct(Validator $first, Validator $then) {
$this->first = $first;
$this->then = $then;
}
public function validate(string $name, $val): array {
$firstFailures = $this->first->validate($name, $val);
$thenFailures = [];
if (empty($firstFailures)) {
$thenFailures = $this->then->validate($name, $val);
}
return array_merge($firstFailures, $thenFailures);
}
}

@ -1,42 +0,0 @@
<?php
namespace IQBall\Core\Validation;
/**
* An error that concerns a field, with a bound message name
*/
class FieldValidationFail extends ValidationFail {
private string $fieldName;
/**
* @param string $fieldName
* @param string $message
*/
public function __construct(string $fieldName, string $message) {
parent::__construct("Champ invalide", $message);
$this->fieldName = $fieldName;
}
public function getFieldName(): string {
return $this->fieldName;
}
public static function invalidChars(string $fieldName): FieldValidationFail {
return new FieldValidationFail($fieldName, "field contains illegal chars");
}
public static function empty(string $fieldName): FieldValidationFail {
return new FieldValidationFail($fieldName, "field is empty");
}
public static function missing(string $fieldName): FieldValidationFail {
return new FieldValidationFail($fieldName, "field is missing");
}
/**
* @return array<string, string>
*/
public function jsonSerialize(): array {
return ["field" => $this->fieldName, "message" => $this->getMessage()];
}
}

@ -1,21 +0,0 @@
<?php
namespace IQBall\Core\Validation;
class FunctionValidator extends Validator {
/**
* @var callable(string, mixed): ValidationFail[]
*/
private $validate_fn;
/**
* @param callable(string, mixed): ValidationFail[] $validate_fn the validate function. Must have the same signature as the {@link Validator::validate()} method.
*/
public function __construct(callable $validate_fn) {
$this->validate_fn = $validate_fn;
}
public function validate(string $name, $val): array {
return call_user_func_array($this->validate_fn, [$name, $val]);
}
}

@ -1,33 +0,0 @@
<?php
namespace IQBall\Core\Validation;
/**
* A simple validator that takes a predicate and an error factory
*/
class SimpleFunctionValidator extends Validator {
/**
* @var callable(mixed): bool
*/
private $predicate;
/**
* @var callable(string): ValidationFail[]
*/
private $errorFactory;
/**
* @param callable(mixed): bool $predicate a function predicate with signature: `(string) => bool`, to validate the given string
* @param callable(string): ValidationFail[] $errorsFactory a factory function with signature `(string) => array` to emit failures when the predicate fails
*/
public function __construct(callable $predicate, callable $errorsFactory) {
$this->predicate = $predicate;
$this->errorFactory = $errorsFactory;
}
public function validate(string $name, $val): array {
if (!call_user_func_array($this->predicate, [$val])) {
return call_user_func_array($this->errorFactory, [$name]);
}
return [];
}
}

Some files were not shown because too many files have changed in this diff Show More

Loading…
Cancel
Save