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*
yarn-debug.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 {
- name: String
- picture: Url
- members: array<int, MemberRole>
+ getName(): String
+ getPicture(): Url
@ -61,6 +60,7 @@ class Team {
Team --> "- mainColor" Color
Team --> "- secondaryColor" Color
Team --> "- members *" Member
class Color {
- value: int

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

@ -5,7 +5,7 @@
// Please do not touch.
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.
@ -20,4 +20,3 @@ global $_data_source_name;
$data_source_name = $_data_source_name;
const DATABASE_USER = _DATABASE_USER;
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.
*/
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 React, {FunctionComponent} from "react";
import ReactDOM from "react-dom/client"
import React, { FunctionComponent } from "react"
/**
* Dynamically renders a React component, with given arguments
@ -8,12 +8,12 @@ import React, {FunctionComponent} from "react";
*/
export function renderView(Component: FunctionComponent, args: {}) {
const root = ReactDOM.createRoot(
document.getElementById('root') as HTMLElement
);
document.getElementById("root") as HTMLElement,
)
root.render(
<React.StrictMode>
<Component {...args} />
</React.StrictMode>
);
</React.StrictMode>,
)
}

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

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

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

@ -1,12 +1,12 @@
import {useRef} from "react";
import "../../style/player.css";
import RemoveIcon from "../../assets/icon/remove.svg?react";
import Draggable from "react-draggable";
import {PlayerPiece} from "./PlayerPiece";
import {Player} from "../../data/Player";
import { useRef } from "react"
import "../../style/player.css"
import RemoveIcon from "../../assets/icon/remove.svg?react"
import Draggable from "react-draggable"
import { PlayerPiece } from "./PlayerPiece"
import { Player } from "../../data/Player"
export interface PlayerProps {
player: Player,
player: Player
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
* */
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 (
<Draggable
handle={".player-piece"}
nodeRef={ref}
bounds="parent"
>
<div ref={ref}
<Draggable handle={".player-piece"} nodeRef={ref} bounds="parent">
<div
ref={ref}
className={"player"}
style={{
position: "absolute",
left: `${x * 100}%`,
top: `${y * 100}%`,
}}>
<div tabIndex={0}
<div
tabIndex={0}
className="player-content"
onKeyUp={e => {
if (e.key == "Delete")
onRemove()
onKeyUp={(e) => {
if (e.key == "Delete") onRemove()
}}>
<div className="player-selection-tab">
<RemoveIcon
className="player-selection-tab-remove"
onClick={onRemove}/>
onClick={onRemove}
/>
</div>
<PlayerPiece team={player.team} text={player.role} />
</div>
</div>
</Draggable>
)
}

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

@ -1,21 +1,21 @@
import {Team} from "./Team";
import { Team } from "./Team"
export interface Player {
/**
* unique identifier of the player.
* This identifier must be unique to the associated court.
*/
id: number,
id: number
/**
* the player's team
* */
team: Team,
team: Team
/**
* 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)
@ -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)
*/
rightRatio: number,
rightRatio: number
}

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

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

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

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

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

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

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

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

@ -20,7 +20,9 @@
"scripts": {
"start": "vite --host",
"build": "vite build",
"test": "vite test"
"test": "vite test",
"format": "prettier --config .prettierrc 'front' --write",
"tsc": "node_modules/.bin/tsc"
},
"eslintConfig": {
"extends": [
@ -30,6 +32,8 @@
},
"devDependencies": {
"@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_PASSWORD = null;
function _asset(string $assetURI): string
{
function _asset(string $assetURI): string {
global $front_url;
return $front_url . "/" . $assetURI;
}

@ -17,7 +17,6 @@ use Twig\Loader\FilesystemLoader;
use App\Validation\ValidationFail;
use App\Controller\ErrorController;
$loader = new FilesystemLoader('../src/Views/');
$twig = new \Twig\Environment($loader);
@ -28,7 +27,7 @@ $con = new Connexion(get_database());
$router = new AltoRouter();
$router->setBasePath($basePath);
$sampleFormController = new SampleFormController(new FormResultGateway($con), $twig);
$sampleFormController = new SampleFormController(new FormResultGateway($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) {
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;
throw $e;
}
break;
}

@ -3,13 +3,14 @@
/**
* 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
$basePath = substr(__DIR__, strlen($_SERVER['DOCUMENT_ROOT']));
$basePathLen = strlen($basePath);
if ($basePathLen == 0)
if ($basePathLen == 0) {
return "";
}
$c = $basePath[$basePathLen - 1];

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

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

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

@ -8,14 +8,16 @@ use App\Http\HttpResponse;
use App\Http\JsonHttpResponse;
use App\Http\ViewHttpResponse;
use App\Validation\ValidationFail;
use App\Validation\Validator;
class Control {
/**
* 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.
* 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
*/
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.
* @param array $data the request's data array.
* @param array $schema an array of `fieldName => Validators` which represents the request object 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 $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.
* @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
*/
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;
class EditorController {
private TacticModel $model;
/**

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

@ -11,7 +11,6 @@ use App\Http\ViewHttpResponse;
use App\Validation\Validators;
class SampleFormController {
private FormResultGateway $gateway;
/**
@ -30,10 +29,15 @@ class SampleFormController {
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 {
return Control::runCheckedFrom($form, [
"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) {
$description = htmlspecialchars($req["description"]);
$this->gateway->insert($req["name"], $description);
@ -42,10 +46,18 @@ class SampleFormController {
}, false);
}
/**
* @param array<string, mixed> $form
* @return HttpResponse
*/
public function submitFormTwig(array $form): HttpResponse {
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 {
return $this->submitForm($form, fn(array $results) => ViewHttpResponse::react('views/DisplayResults.tsx', $results));
}

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

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

@ -14,7 +14,7 @@ class Color {
* @param int $value 6 bytes unsigned int that represents an RGB color
* @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) {
throw new InvalidArgumentException("int color value is invalid, must be positive and lower than 0xFFFFFF");
}

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

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

@ -11,7 +11,7 @@ class Team {
private Color $secondColor;
/**
* @var array maps users with their role
* @var array<int, Member> maps users with their role
*/
private array $members;
@ -20,7 +20,7 @@ class Team {
* @param Url $picture
* @param Color $mainColor
* @param Color $secondColor
* @param array $members
* @param array<int, Member> $members
*/
public function __construct(string $name, Url $picture, Color $mainColor, Color $secondColor, array $members) {
$this->name = $name;
@ -58,8 +58,11 @@ class Team {
return $this->secondColor;
}
/**
* @return array<int, Member>
*/
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;
/**
* Public information about a user
*/

@ -9,7 +9,6 @@ use App\Connexion;
* A sample gateway, that stores the sample form's result.
*/
class FormResultGateway {
private 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(
"INSERT INTO FormEntries VALUES (:name, :description)",
[
":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", []);
}
}

@ -4,7 +4,7 @@ namespace App\Gateway;
use App\Connexion;
use App\Data\TacticInfo;
use \PDO;
use PDO;
class TacticInfoGateway {
private Connexion $con;
@ -43,12 +43,12 @@ class TacticInfoGateway {
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(
"UPDATE TacticInfo SET name = :name WHERE id = :id",
[
":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\Validation;
use App\Validation\ValidationFail;
use App\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;
}
@ -17,9 +28,9 @@ class HttpRequest implements ArrayAccess {
/**
* 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 $request the request's data
* @param array $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, 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, 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
*/
public static function from(array $request, array &$fails, array $schema): ?HttpRequest {
@ -43,14 +54,28 @@ class HttpRequest implements ArrayAccess {
return isset($this->data[$offset]);
}
/**
* @param $offset
* @return mixed
*/
public function offsetGet($offset) {
return $this->data[$offset];
}
/**
* @param $offset
* @param $value
* @return mixed
* @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.");
}

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

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

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

@ -6,7 +6,6 @@ use App\Data\TacticInfo;
use App\Gateway\TacticInfoGateway;
class TacticModel {
public const TACTIC_DEFAULT_NAME = "Nouvelle tactique";
@ -40,7 +39,7 @@ class TacticModel {
* Update the name of a tactic
* @param int $id the tactic identifier
* @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 {
if ($this->tactics->get($id) == null) {

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

@ -2,7 +2,6 @@
namespace App\Validation;
/**
* 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");
}
public function jsonSerialize() {
/**
* @return array<string, string>
*/
public function jsonSerialize(): array {
return ["field" => $this->fieldName, "message" => $this->getMessage()];
}
}

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

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

@ -6,12 +6,11 @@ namespace App\Validation;
* Utility class for validation
*/
class Validation {
/**
* Validate a value from validators, appending failures in the given errors array.
* @param mixed $val the value to validate
* @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
* @return bool true if any of the given validators did fail
*/

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

@ -3,14 +3,13 @@
namespace App\Validation;
abstract class Validator {
/**
* validates a variable string
* @param string $name the name of the tested value
* @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

@ -6,7 +6,6 @@ namespace App\Validation;
* A collection of standard validators
*/
class Validators {
/**
* @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 `_`.
*/
public static function name($msg = null): Validator {
public static function name(string $msg = null): Validator {
return self::regex("/^[0-9a-zA-Zà-üÀ-Ü_-]*$/", $msg);
}

@ -3,7 +3,7 @@
/**
* sends a react view to the user client.
* @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.
* @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