add and configure tsc, phpstan, prettier and php-cs-fixer, format and fix reported code errors

pull/13/head
Override-6 1 year ago committed by maxime.batista
parent 582a623576
commit 2e4f2eb10c

2
.gitignore vendored

@ -38,3 +38,5 @@ package-lock.json
npm-debug.log* npm-debug.log*
yarn-debug.log* yarn-debug.log*
yarn-error.log* yarn-error.log*
.php-cs-fixer.cache

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

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

@ -1,12 +0,0 @@
@startuml
class Connexion
class Modele
class Account
class AccountGateway
@enduml

@ -50,7 +50,6 @@ enum MemberRole {
class Team { class Team {
- name: String - name: String
- picture: Url - picture: Url
- members: array<int, MemberRole>
+ getName(): String + getName(): String
+ getPicture(): Url + getPicture(): Url
@ -61,6 +60,7 @@ class Team {
Team --> "- mainColor" Color Team --> "- mainColor" Color
Team --> "- secondaryColor" Color Team --> "- secondaryColor" Color
Team --> "- members *" Member
class Color { class Color {
- value: int - value: int

@ -11,5 +11,8 @@
"ext-pdo_sqlite": "*", "ext-pdo_sqlite": "*",
"twig/twig":"^2.0", "twig/twig":"^2.0",
"phpstan/phpstan": "*" "phpstan/phpstan": "*"
},
"require-dev": {
"friendsofphp/php-cs-fixer": "^3.38"
} }
} }

@ -5,7 +5,7 @@
// Please do not touch. // Please do not touch.
require /*PROFILE_FILE*/ "profiles/dev-config-profile.php"; require /*PROFILE_FILE*/ "profiles/dev-config-profile.php";
CONST SUPPORTS_FAST_REFRESH = _SUPPORTS_FAST_REFRESH; const SUPPORTS_FAST_REFRESH = _SUPPORTS_FAST_REFRESH;
/** /**
* Maps the given relative source uri (relative to the `/front` folder) to its actual location depending on imported profile. * Maps the given relative source uri (relative to the `/front` folder) to its actual location depending on imported profile.
@ -20,4 +20,3 @@ global $_data_source_name;
$data_source_name = $_data_source_name; $data_source_name = $_data_source_name;
const DATABASE_USER = _DATABASE_USER; const DATABASE_USER = _DATABASE_USER;
const DATABASE_PASSWORD = _DATABASE_PASSWORD; const DATABASE_PASSWORD = _DATABASE_PASSWORD;

@ -0,0 +1,9 @@
#!/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,4 +1,4 @@
/** /**
* This constant defines the API endpoint. * This constant defines the API endpoint.
*/ */
export const API = import.meta.env.VITE_API_ENDPOINT; export const API = import.meta.env.VITE_API_ENDPOINT

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

@ -1,58 +1,72 @@
import {ReactElement, useRef} from "react"; import { ReactElement, useRef } from "react"
import Draggable from "react-draggable"; import Draggable from "react-draggable"
export interface RackProps<E extends { key: string | number }> { export interface RackProps<E extends { key: string | number }> {
id: string, id: string
objects: E[], objects: E[]
onChange: (objects: E[]) => void, onChange: (objects: E[]) => void
canDetach: (ref: HTMLDivElement) => boolean, canDetach: (ref: HTMLDivElement) => boolean
onElementDetached: (ref: HTMLDivElement, el: E) => void, onElementDetached: (ref: HTMLDivElement, el: E) => void
render: (e: E) => ReactElement, render: (e: E) => ReactElement
} }
interface RackItemProps<E extends { key: string | number }> { interface RackItemProps<E extends { key: string | number }> {
item: E, item: E
onTryDetach: (ref: HTMLDivElement, el: E) => void, onTryDetach: (ref: HTMLDivElement, el: E) => void
render: (e: E) => ReactElement, render: (e: E) => ReactElement
} }
/** /**
* A container of draggable objects * A container of draggable objects
* */ * */
export function Rack<E extends {key: string | number}>({id, objects, onChange, canDetach, onElementDetached, render}: RackProps<E>) { export function Rack<E extends { key: string | number }>({
id,
objects,
onChange,
canDetach,
onElementDetached,
render,
}: RackProps<E>) {
return ( return (
<div id={id} style={{ <div
display: "flex" id={id}
style={{
display: "flex",
}}> }}>
{objects.map(element => ( {objects.map((element) => (
<RackItem key={element.key} <RackItem
key={element.key}
item={element} item={element}
render={render} render={render}
onTryDetach={(ref, element) => { onTryDetach={(ref, element) => {
if (!canDetach(ref)) if (!canDetach(ref)) return
return
const index = objects.findIndex(o => o.key === element.key) const index = objects.findIndex(
(o) => o.key === element.key,
)
onChange(objects.toSpliced(index, 1)) onChange(objects.toSpliced(index, 1))
onElementDetached(ref, element) onElementDetached(ref, element)
}}/> }}
/>
))} ))}
</div> </div>
) )
} }
function RackItem<E extends {key: string | number}>({item, onTryDetach, render}: RackItemProps<E>) { function RackItem<E extends { key: string | number }>({
const divRef = useRef<HTMLDivElement>(null); item,
onTryDetach,
render,
}: RackItemProps<E>) {
const divRef = useRef<HTMLDivElement>(null)
return ( return (
<Draggable <Draggable
position={{ x: 0, y: 0 }} position={{ x: 0, y: 0 }}
nodeRef={divRef} nodeRef={divRef}
onStop={() => onTryDetach(divRef.current!, item)}> onStop={() => onTryDetach(divRef.current!, item)}>
<div ref={divRef}> <div ref={divRef}>{render(item)}</div>
{render(item)}
</div>
</Draggable> </Draggable>
) )
} }

@ -1,27 +1,31 @@
import React, {CSSProperties, useRef, useState} from "react"; import React, { CSSProperties, useRef, useState } from "react"
import "../style/title_input.css"; import "../style/title_input.css"
export interface TitleInputOptions { export interface TitleInputOptions {
style: CSSProperties, style: CSSProperties
default_value: string, default_value: string
on_validated: (a: string) => void on_validated: (a: string) => void
} }
export default function TitleInput({style, default_value, on_validated}: TitleInputOptions) { export default function TitleInput({
const [value, setValue] = useState(default_value); style,
const ref = useRef<HTMLInputElement>(null); default_value,
on_validated,
}: TitleInputOptions) {
const [value, setValue] = useState(default_value)
const ref = useRef<HTMLInputElement>(null)
return ( return (
<input className="title_input" <input
className="title_input"
ref={ref} ref={ref}
style={style} style={style}
type="text" type="text"
value={value} value={value}
onChange={event => setValue(event.target.value)} onChange={(event) => setValue(event.target.value)}
onBlur={_ => on_validated(value)} onBlur={(_) => on_validated(value)}
onKeyDown={event => { onKeyDown={(event) => {
if (event.key == 'Enter') if (event.key == "Enter") ref.current?.blur()
ref.current?.blur();
}} }}
/> />
) )

@ -1,25 +1,27 @@
import CourtSvg from '../../assets/basketball_court.svg?react'; import CourtSvg from "../../assets/basketball_court.svg?react"
import '../../style/basket_court.css'; import "../../style/basket_court.css"
import {useRef} from "react"; import { useRef } from "react"
import CourtPlayer from "./CourtPlayer"; import CourtPlayer from "./CourtPlayer"
import {Player} from "../../data/Player"; import { Player } from "../../data/Player"
export interface BasketCourtProps { export interface BasketCourtProps {
players: Player[], players: Player[]
onPlayerRemove: (p: Player) => void, onPlayerRemove: (p: Player) => void
} }
export function BasketCourt({ players, onPlayerRemove }: BasketCourtProps) { export function BasketCourt({ players, onPlayerRemove }: BasketCourtProps) {
return ( return (
<div id="court-container" style={{ position: "relative" }}> <div id="court-container" style={{ position: "relative" }}>
<CourtSvg id="court-svg" /> <CourtSvg id="court-svg" />
{players.map(player => { {players.map((player) => {
return <CourtPlayer key={player.id} return (
<CourtPlayer
key={player.id}
player={player} player={player}
onRemove={() => onPlayerRemove(player)} onRemove={() => onPlayerRemove(player)}
/> />
)
})} })}
</div> </div>
) )
} }

@ -1,12 +1,12 @@
import {useRef} from "react"; import { useRef } from "react"
import "../../style/player.css"; import "../../style/player.css"
import RemoveIcon from "../../assets/icon/remove.svg?react"; import RemoveIcon from "../../assets/icon/remove.svg?react"
import Draggable from "react-draggable"; import Draggable from "react-draggable"
import {PlayerPiece} from "./PlayerPiece"; import { PlayerPiece } from "./PlayerPiece"
import {Player} from "../../data/Player"; import { Player } from "../../data/Player"
export interface PlayerProps { export interface PlayerProps {
player: Player, player: Player
onRemove: () => void onRemove: () => void
} }
@ -14,42 +14,36 @@ export interface PlayerProps {
* A player that is placed on the court, which can be selected, and moved in the associated bounds * A player that is placed on the court, which can be selected, and moved in the associated bounds
* */ * */
export default function CourtPlayer({ player, onRemove }: PlayerProps) { export default function CourtPlayer({ player, onRemove }: PlayerProps) {
const ref = useRef<HTMLDivElement>(null)
const ref = useRef<HTMLDivElement>(null); const x = player.rightRatio
const y = player.bottomRatio
const x = player.rightRatio;
const y = player.bottomRatio;
return ( return (
<Draggable <Draggable handle={".player-piece"} nodeRef={ref} bounds="parent">
handle={".player-piece"} <div
nodeRef={ref} ref={ref}
bounds="parent"
>
<div ref={ref}
className={"player"} className={"player"}
style={{ style={{
position: "absolute", position: "absolute",
left: `${x * 100}%`, left: `${x * 100}%`,
top: `${y * 100}%`, top: `${y * 100}%`,
}}> }}>
<div
<div tabIndex={0} tabIndex={0}
className="player-content" className="player-content"
onKeyUp={e => { onKeyUp={(e) => {
if (e.key == "Delete") if (e.key == "Delete") onRemove()
onRemove()
}}> }}>
<div className="player-selection-tab"> <div className="player-selection-tab">
<RemoveIcon <RemoveIcon
className="player-selection-tab-remove" className="player-selection-tab-remove"
onClick={onRemove}/> onClick={onRemove}
/>
</div> </div>
<PlayerPiece team={player.team} text={player.role} /> <PlayerPiece team={player.team} text={player.role} />
</div> </div>
</div> </div>
</Draggable> </Draggable>
) )
} }

@ -1,9 +1,8 @@
import React from "react"; import React from "react"
import '../../style/player.css' import "../../style/player.css"
import {Team} from "../../data/Team"; import { Team } from "../../data/Team"
export function PlayerPiece({ team, text }: { team: Team; text: string }) {
export function PlayerPiece({team, text}: { team: Team, text: string }) {
return ( return (
<div className={`player-piece ${team}`}> <div className={`player-piece ${team}`}>
<p>{text}</p> <p>{text}</p>

@ -1,21 +1,21 @@
import {Team} from "./Team"; import { Team } from "./Team"
export interface Player { export interface Player {
/** /**
* unique identifier of the player. * unique identifier of the player.
* This identifier must be unique to the associated court. * This identifier must be unique to the associated court.
*/ */
id: number, id: number
/** /**
* the player's team * the player's team
* */ * */
team: Team, team: Team
/** /**
* player's position * player's position
* */ * */
role: string, role: string
/** /**
* Percentage of the player's position to the bottom (0 means top, 1 means bottom, 0.5 means middle) * Percentage of the player's position to the bottom (0 means top, 1 means bottom, 0.5 means middle)
@ -25,5 +25,5 @@ export interface Player {
/** /**
* Percentage of the player's position to the right (0 means left, 1 means right, 0.5 means middle) * Percentage of the player's position to the right (0 means left, 1 means right, 0.5 means middle)
*/ */
rightRatio: number, rightRatio: number
} }

@ -1,4 +1,4 @@
export enum Team { export enum Team {
Allies = "allies", Allies = "allies",
Opponents = "opponents" Opponents = "opponents",
} }

@ -1,9 +1,6 @@
#court-container { #court-container {
display: flex; display: flex;
background-color: var(--main-color); background-color: var(--main-color);
} }
@ -13,8 +10,6 @@
-webkit-user-drag: none; -webkit-user-drag: none;
} }
#court-svg * { #court-svg * {
stroke: var(--selected-team-secondarycolor); stroke: var(--selected-team-secondarycolor);
} }

@ -1,5 +1,3 @@
:root { :root {
--main-color: #ffffff; --main-color: #ffffff;
--second-color: #ccde54; --second-color: #ccde54;
@ -9,5 +7,5 @@
--selected-team-primarycolor: #ffffff; --selected-team-primarycolor: #ffffff;
--selected-team-secondarycolor: #000000; --selected-team-secondarycolor: #000000;
--selection-color: #3f7fc4 --selection-color: #3f7fc4;
} }

@ -1,6 +1,5 @@
@import "colors.css"; @import "colors.css";
#main-div { #main-div {
display: flex; display: flex;
height: 100%; height: 100%;
@ -31,7 +30,8 @@
height: 100%; height: 100%;
} }
#allies-rack .player-piece , #opponent-rack .player-piece { #allies-rack .player-piece,
#opponent-rack .player-piece {
margin-left: 5px; margin-left: 5px;
} }
@ -53,7 +53,6 @@
width: 60%; width: 60%;
} }
.react-draggable { .react-draggable {
z-index: 2; z-index: 2;
} }

@ -14,4 +14,3 @@
border-bottom-color: blueviolet; border-bottom-color: blueviolet;
} }

@ -1,19 +1,13 @@
interface DisplayResultsProps { interface DisplayResultsProps {
results: readonly { name: string, description: string}[] results: readonly { name: string; description: string }[]
} }
export default function DisplayResults({ results }: DisplayResultsProps) { export default function DisplayResults({ results }: DisplayResultsProps) {
const list = results const list = results.map(({ name, description }) => (
.map(({name, description}) =>
<div> <div>
<p>username: {name}</p> <p>username: {name}</p>
<p>description: {description}</p> <p>description: {description}</p>
</div> </div>
) ))
return ( return <div>{list}</div>
<div>
{list}
</div>
)
} }

@ -1,43 +1,42 @@
import {CSSProperties, useRef, useState} from "react"; import { CSSProperties, useRef, useState } from "react"
import "../style/editor.css"; import "../style/editor.css"
import TitleInput from "../components/TitleInput"; import TitleInput from "../components/TitleInput"
import {API} from "../Constants"; import { API } from "../Constants"
import {BasketCourt} from "../components/editor/BasketCourt"; import { BasketCourt } from "../components/editor/BasketCourt"
import {Rack} from "../components/Rack"; import { Rack } from "../components/Rack"
import {PlayerPiece} from "../components/editor/PlayerPiece"; import { PlayerPiece } from "../components/editor/PlayerPiece"
import {Player} from "../data/Player"; import { Player } from "../data/Player"
import {Team} from "../data/Team"; import { Team } from "../data/Team"
const ERROR_STYLE: CSSProperties = { const ERROR_STYLE: CSSProperties = {
borderColor: "red" borderColor: "red",
} }
/** /**
* information about a player that is into a rack * information about a player that is into a rack
*/ */
interface RackedPlayer { interface RackedPlayer {
team: Team, team: Team
key: string, key: string
} }
export default function Editor({id, name}: { id: number, name: string }) { export default function Editor({ id, name }: { id: number; name: string }) {
const [style, setStyle] = useState<CSSProperties>({}); const [style, setStyle] = useState<CSSProperties>({})
const positions = ["1", "2", "3", "4", "5"] const positions = ["1", "2", "3", "4", "5"]
const [allies, setAllies] = useState( const [allies, setAllies] = useState(
positions.map(key => ({team: Team.Allies, key})) positions.map((key) => ({ team: Team.Allies, key })),
) )
const [opponents, setOpponents] = useState( const [opponents, setOpponents] = useState(
positions.map(key => ({team: Team.Opponents, key})) positions.map((key) => ({ team: Team.Opponents, key })),
) )
const [players, setPlayers] = useState<Player[]>([]); const [players, setPlayers] = useState<Player[]>([])
const courtDivContentRef = useRef<HTMLDivElement>(null); const courtDivContentRef = useRef<HTMLDivElement>(null)
const canDetach = (ref: HTMLDivElement) => { const canDetach = (ref: HTMLDivElement) => {
const refBounds = ref.getBoundingClientRect(); const refBounds = ref.getBoundingClientRect()
const courtBounds = courtDivContentRef.current!.getBoundingClientRect(); const courtBounds = courtDivContentRef.current!.getBoundingClientRect()
// check if refBounds overlaps courtBounds // check if refBounds overlaps courtBounds
return !( return !(
@ -45,27 +44,30 @@ export default function Editor({id, name}: { id: number, name: string }) {
refBounds.right < courtBounds.left || refBounds.right < courtBounds.left ||
refBounds.bottom < courtBounds.top || refBounds.bottom < courtBounds.top ||
refBounds.left > courtBounds.right refBounds.left > courtBounds.right
); )
} }
const onPieceDetach = (ref: HTMLDivElement, element: RackedPlayer) => { const onPieceDetach = (ref: HTMLDivElement, element: RackedPlayer) => {
const refBounds = ref.getBoundingClientRect(); const refBounds = ref.getBoundingClientRect()
const courtBounds = courtDivContentRef.current!.getBoundingClientRect(); const courtBounds = courtDivContentRef.current!.getBoundingClientRect()
const relativeXPixels = refBounds.x - courtBounds.x; const relativeXPixels = refBounds.x - courtBounds.x
const relativeYPixels = refBounds.y - courtBounds.y; const relativeYPixels = refBounds.y - courtBounds.y
const xRatio = relativeXPixels / courtBounds.width; const xRatio = relativeXPixels / courtBounds.width
const yRatio = relativeYPixels / courtBounds.height; const yRatio = relativeYPixels / courtBounds.height
setPlayers(players => { setPlayers((players) => {
return [...players, { return [
...players,
{
id: players.length, id: players.length,
team: element.team, team: element.team,
role: element.key, role: element.key,
rightRatio: xRatio, rightRatio: xRatio,
bottomRatio: yRatio bottomRatio: yRatio,
}] },
]
}) })
} }
@ -73,74 +75,88 @@ export default function Editor({id, name}: { id: number, name: string }) {
<div id="main-div"> <div id="main-div">
<div id="topbar-div"> <div id="topbar-div">
<div>LEFT</div> <div>LEFT</div>
<TitleInput style={style} default_value={name} on_validated={new_name => { <TitleInput
style={style}
default_value={name}
on_validated={(new_name) => {
fetch(`${API}/tactic/${id}/edit/name`, { fetch(`${API}/tactic/${id}/edit/name`, {
method: "POST", method: "POST",
headers: { headers: {
'Accept': 'application/json', Accept: "application/json",
'Content-Type': 'application/json' "Content-Type": "application/json",
}, },
body: JSON.stringify({ body: JSON.stringify({
name: new_name, name: new_name,
}) }),
}).then(response => { }).then((response) => {
if (response.ok) { if (response.ok) {
setStyle({}) setStyle({})
} else { } else {
setStyle(ERROR_STYLE) setStyle(ERROR_STYLE)
} }
}) })
}}/> }}
/>
<div>RIGHT</div> <div>RIGHT</div>
</div> </div>
<div id="edit-div"> <div id="edit-div">
<div id="racks"> <div id="racks">
<Rack id="allies-rack" <Rack
id="allies-rack"
objects={allies} objects={allies}
onChange={setAllies} onChange={setAllies}
canDetach={canDetach} canDetach={canDetach}
onElementDetached={onPieceDetach} onElementDetached={onPieceDetach}
render={({team, key}) => <PlayerPiece team={team} text={key} key={key}/>}/> render={({ team, key }) => (
<Rack id="opponent-rack" <PlayerPiece team={team} text={key} key={key} />
)}
/>
<Rack
id="opponent-rack"
objects={opponents} objects={opponents}
onChange={setOpponents} onChange={setOpponents}
canDetach={canDetach} canDetach={canDetach}
onElementDetached={onPieceDetach} onElementDetached={onPieceDetach}
render={({team, key}) => <PlayerPiece team={team} text={key} key={key}/>}/> render={({ team, key }) => (
<PlayerPiece team={team} text={key} key={key} />
)}
/>
</div> </div>
<div id="court-div"> <div id="court-div">
<div id="court-div-bounds" ref={courtDivContentRef}> <div id="court-div-bounds" ref={courtDivContentRef}>
<BasketCourt <BasketCourt
players={players} players={players}
onPlayerRemove={(player) => { onPlayerRemove={(player) => {
setPlayers(players => { setPlayers((players) => {
const idx = players.indexOf(player) const idx = players.indexOf(player)
return players.toSpliced(idx, 1) return players.toSpliced(idx, 1)
}) })
switch (player.team) { switch (player.team) {
case Team.Opponents: case Team.Opponents:
setOpponents(opponents => ( setOpponents((opponents) => [
[...opponents, { ...opponents,
{
team: player.team, team: player.team,
pos: player.role, pos: player.role,
key: player.role key: player.role,
}] },
)) ])
break break
case Team.Allies: case Team.Allies:
setAllies(allies => ( setAllies((allies) => [
[...allies, { ...allies,
{
team: player.team, team: player.team,
pos: player.role, pos: player.role,
key: player.role key: player.role,
}] },
)) ])
} }
}}/> }}
/>
</div> </div>
</div> </div>
</div> </div>
</div> </div>
) )
} }

@ -1,5 +1,3 @@
export default function SampleForm() { export default function SampleForm() {
return ( return (
<div> <div>
@ -14,6 +12,3 @@ export default function SampleForm() {
</div> </div>
) )
} }

@ -20,7 +20,9 @@
"scripts": { "scripts": {
"start": "vite --host", "start": "vite --host",
"build": "vite build", "build": "vite build",
"test": "vite test" "test": "vite test",
"format": "prettier --config .prettierrc 'front' --write",
"tsc": "node_modules/.bin/tsc"
}, },
"eslintConfig": { "eslintConfig": {
"extends": [ "extends": [
@ -30,6 +32,8 @@
}, },
"devDependencies": { "devDependencies": {
"@vitejs/plugin-react": "^4.1.0", "@vitejs/plugin-react": "^4.1.0",
"vite-plugin-svgr": "^4.1.0" "vite-plugin-svgr": "^4.1.0",
"prettier": "^3.1.0",
"typescript": "^5.2.2"
} }
} }

@ -0,0 +1,15 @@
parameters:
phpVersion: 70400
level: 6
paths:
- src
- public
- sql
ignoreErrors:
-
message: '#.*#'
path: sql/database.php
-
message: '#.*#'
path: src/react-display-file.php

@ -10,11 +10,7 @@ $_data_source_name = "sqlite:${_SERVER['DOCUMENT_ROOT']}/../dev-database.sqlite"
const _DATABASE_USER = null; const _DATABASE_USER = null;
const _DATABASE_PASSWORD = null; const _DATABASE_PASSWORD = null;
function _asset(string $assetURI): string function _asset(string $assetURI): string {
{
global $front_url; global $front_url;
return $front_url . "/" . $assetURI; return $front_url . "/" . $assetURI;
} }

@ -17,7 +17,6 @@ use Twig\Loader\FilesystemLoader;
use App\Validation\ValidationFail; use App\Validation\ValidationFail;
use App\Controller\ErrorController; use App\Controller\ErrorController;
$loader = new FilesystemLoader('../src/Views/'); $loader = new FilesystemLoader('../src/Views/');
$twig = new \Twig\Environment($loader); $twig = new \Twig\Environment($loader);
@ -28,7 +27,7 @@ $con = new Connexion(get_database());
$router = new AltoRouter(); $router = new AltoRouter();
$router->setBasePath($basePath); $router->setBasePath($basePath);
$sampleFormController = new SampleFormController(new FormResultGateway($con), $twig); $sampleFormController = new SampleFormController(new FormResultGateway($con));
$editorController = new EditorController(new TacticModel(new TacticInfoGateway($con))); $editorController = new EditorController(new TacticModel(new TacticInfoGateway($con)));
@ -65,7 +64,7 @@ if ($response instanceof ViewHttpResponse) {
} catch (\Twig\Error\RuntimeError|\Twig\Error\SyntaxError $e) { } catch (\Twig\Error\RuntimeError|\Twig\Error\SyntaxError $e) {
http_response_code(500); 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"); 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; throw $e;
} }
break; break;
} }

@ -3,13 +3,14 @@
/** /**
* relative path of the public directory from the server's document root. * relative path of the public directory from the server's document root.
*/ */
function get_public_path() { function get_public_path(): string {
// find the server path of the index.php file // find the server path of the index.php file
$basePath = substr(__DIR__, strlen($_SERVER['DOCUMENT_ROOT'])); $basePath = substr(__DIR__, strlen($_SERVER['DOCUMENT_ROOT']));
$basePathLen = strlen($basePath); $basePathLen = strlen($basePath);
if ($basePathLen == 0) if ($basePathLen == 0) {
return ""; return "";
}
$c = $basePath[$basePathLen - 1]; $c = $basePath[$basePathLen - 1];

@ -25,6 +25,3 @@ function get_database(): PDO {
return $pdo; return $pdo;
} }

@ -1,28 +1,27 @@
<?php <?php
namespace App; namespace App;
use \PDO;
class Connexion { use PDO;
class Connexion {
private PDO $pdo; private PDO $pdo;
/** /**
* @param PDO $pdo * @param PDO $pdo
*/ */
public function __construct(PDO $pdo) public function __construct(PDO $pdo) {
{
$this->pdo = $pdo; $this->pdo = $pdo;
} }
public function lastInsertId() { public function lastInsertId(): string {
return $this->pdo->lastInsertId(); return $this->pdo->lastInsertId();
} }
/** /**
* execute a request * execute a request
* @param string $query * @param string $query
* @param array $args * @param array<string, array<mixed, int>> $args
* @return void * @return void
*/ */
public function exec(string $query, array $args) { public function exec(string $query, array $args) {
@ -33,8 +32,8 @@ class Connexion {
/** /**
* Execute a request, and return the returned rows * Execute a request, and return the returned rows
* @param string $query the SQL request * @param string $query the SQL request
* @param array $args an array containing the arguments label, value and type: ex: `[":label" => [$value, PDO::PARAM_TYPE]` * @param array<string, array<mixed, int>> $args an array containing the arguments label, value and type: ex: `[":label" => [$value, PDO::PARAM_TYPE]`
* @return array the returned rows of the request * @return array<string, mixed>[] the returned rows of the request
*/ */
public function fetch(string $query, array $args): array { public function fetch(string $query, array $args): array {
$stmnt = $this->prepare($query, $args); $stmnt = $this->prepare($query, $args);
@ -42,6 +41,11 @@ class Connexion {
return $stmnt->fetchAll(PDO::FETCH_ASSOC); return $stmnt->fetchAll(PDO::FETCH_ASSOC);
} }
/**
* @param string $query
* @param array<string, array<mixed, int>> $args
* @return \PDOStatement
*/
private function prepare(string $query, array $args): \PDOStatement { private function prepare(string $query, array $args): \PDOStatement {
$stmnt = $this->pdo->prepare($query); $stmnt = $this->pdo->prepare($query);
foreach ($args as $name => $value) { foreach ($args as $name => $value) {

@ -25,7 +25,7 @@ class APITacticController {
public function updateName(int $tactic_id): HttpResponse { public function updateName(int $tactic_id): HttpResponse {
return Control::runChecked([ return Control::runChecked([
"name" => [Validators::lenBetween(1, 50), Validators::nameWithSpaces()] "name" => [Validators::lenBetween(1, 50), Validators::nameWithSpaces()],
], function (HttpRequest $request) use ($tactic_id) { ], function (HttpRequest $request) use ($tactic_id) {
$this->model->updateName($tactic_id, $request["name"]); $this->model->updateName($tactic_id, $request["name"]);
return HttpResponse::fromCode(HttpCodes::OK); return HttpResponse::fromCode(HttpCodes::OK);
@ -34,7 +34,7 @@ class APITacticController {
public function newTactic(): HttpResponse { public function newTactic(): HttpResponse {
return Control::runChecked([ return Control::runChecked([
"name" => [Validators::lenBetween(1, 50), Validators::nameWithSpaces()] "name" => [Validators::lenBetween(1, 50), Validators::nameWithSpaces()],
], function (HttpRequest $request) { ], function (HttpRequest $request) {
$tactic = $this->model->makeNew($request["name"]); $tactic = $this->model->makeNew($request["name"]);
$id = $tactic->getId(); $id = $tactic->getId();

@ -8,14 +8,16 @@ use App\Http\HttpResponse;
use App\Http\JsonHttpResponse; use App\Http\JsonHttpResponse;
use App\Http\ViewHttpResponse; use App\Http\ViewHttpResponse;
use App\Validation\ValidationFail; use App\Validation\ValidationFail;
use App\Validation\Validator;
class Control { class Control {
/** /**
* Runs given callback, if the request's json validates the given schema. * Runs given callback, if the request's json validates the given schema.
* @param array $schema an array of `fieldName => Validators` which represents the request object schema * @param array<string, Validator[]> $schema an array of `fieldName => Validators` which represents the request object schema
* @param callable $run the callback to run if the request is valid according to the given schema. * @param callable $run the callback to run if the request is valid according to the given schema.
* THe callback must accept an HttpRequest, and return an HttpResponse object. * THe callback must accept an HttpRequest, and return an HttpResponse object.
* @param bool $errorInJson if set to true, the returned response, in case of errors, will be a JsonHttpResponse, instead
* of the ViewHttpResponse for an error view.
* @return HttpResponse * @return HttpResponse
*/ */
public static function runChecked(array $schema, callable $run, bool $errorInJson): HttpResponse { public static function runChecked(array $schema, callable $run, bool $errorInJson): HttpResponse {
@ -34,10 +36,12 @@ class Control {
/** /**
* Runs given callback, if the given request data array validates the given schema. * Runs given callback, if the given request data array validates the given schema.
* @param array $data the request's data array. * @param array<string, mixed> $data the request's data array.
* @param array $schema an array of `fieldName => Validators` which represents the request object schema * @param array<string, Validator[]> $schema an array of `fieldName => Validators` which represents the request object schema
* @param callable $run the callback to run if the request is valid according to the given schema. * @param callable $run the callback to run if the request is valid according to the given schema.
* THe callback must accept an HttpRequest, and return an HttpResponse object. * THe callback must accept an HttpRequest, and return an HttpResponse object.
* @param bool $errorInJson if set to true, the returned response, in case of errors, will be a JsonHttpResponse, instead
* of the ViewHttpResponse for an error view.
* @return HttpResponse * @return HttpResponse
*/ */
public static function runCheckedFrom(array $data, array $schema, callable $run, bool $errorInJson): HttpResponse { public static function runCheckedFrom(array $data, array $schema, callable $run, bool $errorInJson): HttpResponse {
@ -55,7 +59,4 @@ class Control {
} }
} }

@ -11,7 +11,6 @@ use App\Http\ViewHttpResponse;
use App\Model\TacticModel; use App\Model\TacticModel;
class EditorController { class EditorController {
private TacticModel $model; private TacticModel $model;
/** /**

@ -3,14 +3,20 @@
namespace App\Controller; namespace App\Controller;
require_once __DIR__ . "/../react-display.php"; require_once __DIR__ . "/../react-display.php";
use \Twig\Environment;
use App\Validation\ValidationFail;
use Twig\Environment;
use Twig\Error\LoaderError; use Twig\Error\LoaderError;
use Twig\Error\RuntimeError; use Twig\Error\RuntimeError;
use Twig\Error\SyntaxError; use Twig\Error\SyntaxError;
class ErrorController class ErrorController {
{ /**
public static function displayFailures(array $failures, Environment $twig) { * @param ValidationFail[] $failures
* @param Environment $twig
* @return void
*/
public static function displayFailures(array $failures, Environment $twig): void {
try { try {
$twig->display("error.html.twig", ['failures' => $failures]); $twig->display("error.html.twig", ['failures' => $failures]);
} catch (LoaderError|RuntimeError|SyntaxError $e) { } catch (LoaderError|RuntimeError|SyntaxError $e) {

@ -11,7 +11,6 @@ use App\Http\ViewHttpResponse;
use App\Validation\Validators; use App\Validation\Validators;
class SampleFormController { class SampleFormController {
private FormResultGateway $gateway; private FormResultGateway $gateway;
/** /**
@ -30,10 +29,15 @@ class SampleFormController {
return ViewHttpResponse::twig('sample_form.html.twig', []); return ViewHttpResponse::twig('sample_form.html.twig', []);
} }
/**
* @param array<string, mixed> $form
* @param callable $response
* @return HttpResponse
*/
private function submitForm(array $form, callable $response): HttpResponse { private function submitForm(array $form, callable $response): HttpResponse {
return Control::runCheckedFrom($form, [ return Control::runCheckedFrom($form, [
"name" => [Validators::lenBetween(0, 32), Validators::name("Le nom ne peut contenir que des lettres, des chiffres et des accents")], "name" => [Validators::lenBetween(0, 32), Validators::name("Le nom ne peut contenir que des lettres, des chiffres et des accents")],
"description" => [Validators::lenBetween(0, 512)] "description" => [Validators::lenBetween(0, 512)],
], function (HttpRequest $req) use ($response) { ], function (HttpRequest $req) use ($response) {
$description = htmlspecialchars($req["description"]); $description = htmlspecialchars($req["description"]);
$this->gateway->insert($req["name"], $description); $this->gateway->insert($req["name"], $description);
@ -42,10 +46,18 @@ class SampleFormController {
}, false); }, false);
} }
/**
* @param array<string, mixed> $form
* @return HttpResponse
*/
public function submitFormTwig(array $form): HttpResponse { public function submitFormTwig(array $form): HttpResponse {
return $this->submitForm($form, fn(array $results) => ViewHttpResponse::twig('display_results.html.twig', $results)); return $this->submitForm($form, fn(array $results) => ViewHttpResponse::twig('display_results.html.twig', $results));
} }
/**
* @param array<string, mixed> $form
* @return HttpResponse
*/
public function submitFormReact(array $form): HttpResponse { public function submitFormReact(array $form): HttpResponse {
return $this->submitForm($form, fn(array $results) => ViewHttpResponse::react('views/DisplayResults.tsx', $results)); return $this->submitForm($form, fn(array $results) => ViewHttpResponse::react('views/DisplayResults.tsx', $results));
} }

@ -4,7 +4,7 @@ namespace App\Data;
use http\Exception\InvalidArgumentException; use http\Exception\InvalidArgumentException;
const PHONE_NUMBER_REGEXP = "\\+[0-9]+"; const PHONE_NUMBER_REGEXP = "/^\\+[0-9]+$/";
/** /**
* Base class of a user account. * Base class of a user account.
@ -29,7 +29,7 @@ class Account {
private AccountUser $user; private AccountUser $user;
/** /**
* @var array account's teams * @var array<int, Team> account's teams
*/ */
private array $teams; private array $teams;
@ -38,10 +38,13 @@ class Account {
*/ */
private int $id; private int $id;
/** /**
* @param string $email * @param string $email
* @param string $phoneNumber * @param string $phoneNumber
* @param AccountUser $user * @param AccountUser $user
* @param array<int, Team> $teams
* @param int $id
*/ */
public function __construct(string $email, string $phoneNumber, AccountUser $user, array $teams, int $id) { public function __construct(string $email, string $phoneNumber, AccountUser $user, array $teams, int $id) {
$this->email = $email; $this->email = $email;
@ -79,7 +82,7 @@ class Account {
* @param string $phoneNumber * @param string $phoneNumber
*/ */
public function setPhoneNumber(string $phoneNumber): void { public function setPhoneNumber(string $phoneNumber): void {
if (!filter_var($phoneNumber, FILTER_VALIDATE_REGEXP, PHONE_NUMBER_REGEXP)) { if (!preg_match(PHONE_NUMBER_REGEXP, $phoneNumber)) {
throw new InvalidArgumentException("Invalid phone number"); throw new InvalidArgumentException("Invalid phone number");
} }
$this->phoneNumber = $phoneNumber; $this->phoneNumber = $phoneNumber;
@ -89,6 +92,9 @@ class Account {
return $this->id; return $this->id;
} }
/**
* @return Team[]
*/
public function getTeams(): array { public function getTeams(): array {
return $this->teams; return $this->teams;
} }

@ -36,15 +36,15 @@ class AccountUser implements User {
return $this->age; return $this->age;
} }
public function setName(string $name) { public function setName(string $name): void {
$this->name = $name; $this->name = $name;
} }
public function setProfilePicture(Url $profilePicture) { public function setProfilePicture(Url $profilePicture): void {
$this->profilePicture = $profilePicture; $this->profilePicture = $profilePicture;
} }
public function setAge(int $age) { public function setAge(int $age): void {
$this->age = $age; $this->age = $age;
} }

@ -14,7 +14,7 @@ class Color {
* @param int $value 6 bytes unsigned int that represents an RGB color * @param int $value 6 bytes unsigned int that represents an RGB color
* @throws \InvalidArgumentException if the value is negative or greater than 0xFFFFFF * @throws \InvalidArgumentException if the value is negative or greater than 0xFFFFFF
*/ */
public function __constructor(int $value) { public function __construct(int $value) {
if ($value < 0 || $value > 0xFFFFFF) { if ($value < 0 || $value > 0xFFFFFF) {
throw new InvalidArgumentException("int color value is invalid, must be positive and lower than 0xFFFFFF"); throw new InvalidArgumentException("int color value is invalid, must be positive and lower than 0xFFFFFF");
} }

@ -2,7 +2,6 @@
namespace App\Data; namespace App\Data;
use http\Exception\InvalidArgumentException; use http\Exception\InvalidArgumentException;
/** /**

@ -30,7 +30,10 @@ class TacticInfo implements \JsonSerializable {
return $this->creation_date; return $this->creation_date;
} }
public function jsonSerialize() { /**
* @return array<string, mixed>
*/
public function jsonSerialize(): array {
return get_object_vars($this); return get_object_vars($this);
} }
} }

@ -11,7 +11,7 @@ class Team {
private Color $secondColor; private Color $secondColor;
/** /**
* @var array maps users with their role * @var array<int, Member> maps users with their role
*/ */
private array $members; private array $members;
@ -20,7 +20,7 @@ class Team {
* @param Url $picture * @param Url $picture
* @param Color $mainColor * @param Color $mainColor
* @param Color $secondColor * @param Color $secondColor
* @param array $members * @param array<int, Member> $members
*/ */
public function __construct(string $name, Url $picture, Color $mainColor, Color $secondColor, array $members) { public function __construct(string $name, Url $picture, Color $mainColor, Color $secondColor, array $members) {
$this->name = $name; $this->name = $name;
@ -58,8 +58,11 @@ class Team {
return $this->secondColor; return $this->secondColor;
} }
/**
* @return array<int, Member>
*/
public function listMembers(): array { public function listMembers(): array {
return array_map(fn ($id, $role) => new Member($id, $role), $this->members); return $this->members;
} }
} }

@ -4,7 +4,6 @@ namespace App\Data;
use http\Url; use http\Url;
/** /**
* Public information about a user * Public information about a user
*/ */

@ -9,7 +9,6 @@ use App\Connexion;
* A sample gateway, that stores the sample form's result. * A sample gateway, that stores the sample form's result.
*/ */
class FormResultGateway { class FormResultGateway {
private Connexion $con; private Connexion $con;
public function __construct(Connexion $con) { public function __construct(Connexion $con) {
@ -17,17 +16,20 @@ class FormResultGateway {
} }
function insert(string $username, string $description) { public function insert(string $username, string $description): void {
$this->con->exec( $this->con->exec(
"INSERT INTO FormEntries VALUES (:name, :description)", "INSERT INTO FormEntries VALUES (:name, :description)",
[ [
":name" => [$username, PDO::PARAM_STR], ":name" => [$username, PDO::PARAM_STR],
"description" => [$description, PDO::PARAM_STR] "description" => [$description, PDO::PARAM_STR],
] ]
); );
} }
function listResults(): array { /**
* @return array<string, mixed>
*/
public function listResults(): array {
return $this->con->fetch("SELECT * FROM FormEntries", []); return $this->con->fetch("SELECT * FROM FormEntries", []);
} }
} }

@ -4,7 +4,7 @@ namespace App\Gateway;
use App\Connexion; use App\Connexion;
use App\Data\TacticInfo; use App\Data\TacticInfo;
use \PDO; use PDO;
class TacticInfoGateway { class TacticInfoGateway {
private Connexion $con; private Connexion $con;
@ -43,12 +43,12 @@ class TacticInfoGateway {
return new TacticInfo(intval($row["id"]), $name, strtotime($row["creation_date"])); return new TacticInfo(intval($row["id"]), $name, strtotime($row["creation_date"]));
} }
public function updateName(int $id, string $name) { public function updateName(int $id, string $name): void {
$this->con->exec( $this->con->exec(
"UPDATE TacticInfo SET name = :name WHERE id = :id", "UPDATE TacticInfo SET name = :name WHERE id = :id",
[ [
":name" => [$name, PDO::PARAM_STR], ":name" => [$name, PDO::PARAM_STR],
":id" => [$id, PDO::PARAM_INT] ":id" => [$id, PDO::PARAM_INT],
] ]
); );
} }

@ -4,12 +4,23 @@ namespace App\Http;
use App\Validation\FieldValidationFail; use App\Validation\FieldValidationFail;
use App\Validation\Validation; use App\Validation\Validation;
use App\Validation\ValidationFail;
use App\Validation\Validator;
use ArrayAccess; use ArrayAccess;
use Exception; use Exception;
/**
* @implements ArrayAccess<string, mixed>
* */
class HttpRequest implements ArrayAccess { class HttpRequest implements ArrayAccess {
/**
* @var array<string, mixed>
*/
private array $data; private array $data;
/**
* @param array<string, mixed> $data
*/
private function __construct(array $data) { private function __construct(array $data) {
$this->data = $data; $this->data = $data;
} }
@ -17,9 +28,9 @@ class HttpRequest implements ArrayAccess {
/** /**
* Creates a new HttpRequest instance, and ensures that the given request data validates the given schema. * 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.) * This is a simple function that only supports flat schemas (non-composed, the data must only be a k/v array pair.)
* @param array $request the request's data * @param array<string, mixed> $request the request's data
* @param array $fails a reference to a failure array, that will contain the reported validation failures. * @param array<string, ValidationFail> $fails a reference to a failure array, that will contain the reported validation failures.
* @param array $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 * @param array<string, array<int, 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 * @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 { public static function from(array $request, array &$fails, array $schema): ?HttpRequest {
@ -43,14 +54,28 @@ class HttpRequest implements ArrayAccess {
return isset($this->data[$offset]); return isset($this->data[$offset]);
} }
/**
* @param $offset
* @return mixed
*/
public function offsetGet($offset) { public function offsetGet($offset) {
return $this->data[$offset]; return $this->data[$offset];
} }
/**
* @param $offset
* @param $value
* @return mixed
* @throws Exception
*/
public function offsetSet($offset, $value) { public function offsetSet($offset, $value) {
throw new Exception("requests are immutable objects."); throw new Exception("requests are immutable objects.");
} }
/**
* @param $offset
* @throws Exception
*/
public function offsetUnset($offset) { public function offsetUnset($offset) {
throw new Exception("requests are immutable objects."); throw new Exception("requests are immutable objects.");
} }

@ -3,7 +3,6 @@
namespace App\Http; namespace App\Http;
class HttpResponse { class HttpResponse {
private int $code; private int $code;
/** /**

@ -3,7 +3,6 @@
namespace App\Http; namespace App\Http;
class JsonHttpResponse extends HttpResponse { class JsonHttpResponse extends HttpResponse {
/** /**
* @var mixed Any JSON serializable value * @var mixed Any JSON serializable value
*/ */

@ -3,7 +3,6 @@
namespace App\Http; namespace App\Http;
class ViewHttpResponse extends HttpResponse { class ViewHttpResponse extends HttpResponse {
public const TWIG_VIEW = 0; public const TWIG_VIEW = 0;
public const REACT_VIEW = 1; public const REACT_VIEW = 1;
@ -12,7 +11,7 @@ class ViewHttpResponse extends HttpResponse {
*/ */
private string $file; private string $file;
/** /**
* @var array View arguments * @var array<string, mixed> View arguments
*/ */
private array $arguments; private array $arguments;
/** /**
@ -24,7 +23,7 @@ class ViewHttpResponse extends HttpResponse {
* @param int $code * @param int $code
* @param int $kind * @param int $kind
* @param string $file * @param string $file
* @param array $arguments * @param array<string, mixed> $arguments
*/ */
private function __construct(int $kind, string $file, array $arguments, int $code = HttpCodes::OK) { private function __construct(int $kind, string $file, array $arguments, int $code = HttpCodes::OK) {
parent::__construct($code); parent::__construct($code);
@ -41,6 +40,9 @@ class ViewHttpResponse extends HttpResponse {
return $this->file; return $this->file;
} }
/**
* @return array<string, string>
*/
public function getArguments(): array { public function getArguments(): array {
return $this->arguments; return $this->arguments;
} }
@ -48,7 +50,7 @@ class ViewHttpResponse extends HttpResponse {
/** /**
* Create a twig view response * Create a twig view response
* @param string $file * @param string $file
* @param array $arguments * @param array<string, mixed> $arguments
* @param int $code * @param int $code
* @return ViewHttpResponse * @return ViewHttpResponse
*/ */
@ -59,7 +61,7 @@ class ViewHttpResponse extends HttpResponse {
/** /**
* Create a react view response * Create a react view response
* @param string $file * @param string $file
* @param array $arguments * @param array<string, mixed> $arguments
* @param int $code * @param int $code
* @return ViewHttpResponse * @return ViewHttpResponse
*/ */

@ -6,7 +6,6 @@ use App\Data\TacticInfo;
use App\Gateway\TacticInfoGateway; use App\Gateway\TacticInfoGateway;
class TacticModel { class TacticModel {
public const TACTIC_DEFAULT_NAME = "Nouvelle tactique"; public const TACTIC_DEFAULT_NAME = "Nouvelle tactique";
@ -40,7 +39,7 @@ class TacticModel {
* Update the name of a tactic * Update the name of a tactic
* @param int $id the tactic identifier * @param int $id the tactic identifier
* @param string $name the new name to set * @param string $name the new name to set
* @return true if the update was done successfully * @return bool true if the update was done successfully
*/ */
public function updateName(int $id, string $name): bool { public function updateName(int $id, string $name): bool {
if ($this->tactics->get($id) == null) { if ($this->tactics->get($id) == null) {

@ -3,7 +3,6 @@
namespace App\Validation; namespace App\Validation;
class ComposedValidator extends Validator { class ComposedValidator extends Validator {
private Validator $first; private Validator $first;
private Validator $then; private Validator $then;

@ -2,7 +2,6 @@
namespace App\Validation; namespace App\Validation;
/** /**
* An error that concerns a field, with a bound message name * An error that concerns a field, with a bound message name
*/ */
@ -34,7 +33,10 @@ class FieldValidationFail extends ValidationFail {
return new FieldValidationFail($fieldName, "field is missing"); return new FieldValidationFail($fieldName, "field is missing");
} }
public function jsonSerialize() { /**
* @return array<string, string>
*/
public function jsonSerialize(): array {
return ["field" => $this->fieldName, "message" => $this->getMessage()]; return ["field" => $this->fieldName, "message" => $this->getMessage()];
} }
} }

@ -3,7 +3,9 @@
namespace App\Validation; namespace App\Validation;
class FunctionValidator extends Validator { class FunctionValidator extends Validator {
/**
* @var callable
*/
private $validate_fn; private $validate_fn;
/** /**

@ -6,8 +6,13 @@ namespace App\Validation;
* A simple validator that takes a predicate and an error factory * A simple validator that takes a predicate and an error factory
*/ */
class SimpleFunctionValidator extends Validator { class SimpleFunctionValidator extends Validator {
/**
* @var callable
*/
private $predicate; private $predicate;
/**
* @var callable
*/
private $errorFactory; private $errorFactory;
/** /**

@ -6,12 +6,11 @@ namespace App\Validation;
* Utility class for validation * Utility class for validation
*/ */
class Validation { class Validation {
/** /**
* Validate a value from validators, appending failures in the given errors array. * Validate a value from validators, appending failures in the given errors array.
* @param mixed $val the value to validate * @param mixed $val the value to validate
* @param string $valName the name of the value * @param string $valName the name of the value
* @param array $failures array to push when a validator fails * @param array<int, ValidationFail> $failures array to push when a validator fails
* @param Validator ...$validators given validators * @param Validator ...$validators given validators
* @return bool true if any of the given validators did fail * @return bool true if any of the given validators did fail
*/ */

@ -2,7 +2,9 @@
namespace App\Validation; namespace App\Validation;
class ValidationFail implements \JsonSerializable { use JsonSerializable;
class ValidationFail implements JsonSerializable {
private string $kind; private string $kind;
private string $message; private string $message;
@ -24,7 +26,10 @@ class ValidationFail implements \JsonSerializable {
return $this->kind; return $this->kind;
} }
public function jsonSerialize() { /**
* @return array<string, string>
*/
public function jsonSerialize(): array {
return ["error" => $this->kind, "message" => $this->message]; return ["error" => $this->kind, "message" => $this->message];
} }

@ -3,14 +3,13 @@
namespace App\Validation; namespace App\Validation;
abstract class Validator { abstract class Validator {
/** /**
* validates a variable string * validates a variable string
* @param string $name the name of the tested value * @param string $name the name of the tested value
* @param mixed $val the value to validate * @param mixed $val the value to validate
* @return array the errors the validator has reported * @return array<int, ValidationFail> the errors the validator has reported
*/ */
public abstract function validate(string $name, $val): array; abstract public function validate(string $name, $val): array;
/** /**
* Creates a validator composed of this validator, and given validator * Creates a validator composed of this validator, and given validator

@ -6,7 +6,6 @@ namespace App\Validation;
* A collection of standard validators * A collection of standard validators
*/ */
class Validators { class Validators {
/** /**
* @return Validator a validator that validates a given regex * @return Validator a validator that validates a given regex
*/ */
@ -20,7 +19,7 @@ class Validators {
/** /**
* @return Validator a validator that validates strings that only contains numbers, letters, accents letters, `-` and `_`. * @return Validator a validator that validates strings that only contains numbers, letters, accents letters, `-` and `_`.
*/ */
public static function name($msg = null): Validator { public static function name(string $msg = null): Validator {
return self::regex("/^[0-9a-zA-Zà-üÀ-Ü_-]*$/", $msg); return self::regex("/^[0-9a-zA-Zà-üÀ-Ü_-]*$/", $msg);
} }

@ -3,7 +3,7 @@
/** /**
* sends a react view to the user client. * sends a react view to the user client.
* @param string $url url of the react file to render * @param string $url url of the react file to render
* @param array $arguments arguments to pass to the rendered react component * @param array<string, mixed> $arguments arguments to pass to the rendered react component
* The arguments must be a json-encodable key/value dictionary. * The arguments must be a json-encodable key/value dictionary.
* @return void * @return void
*/ */

@ -0,0 +1,9 @@
#!/usr/bin/env bash
## verify php and typescript types
echo "running php typechecking"
vendor/bin/phpstan analyze && echo "php types are respected"
echo "running typescript typechecking"
npm run tsc && echo "typescript types are respected"
Loading…
Cancel
Save