diff --git a/.gitignore b/.gitignore index 9124809..1c4404c 100644 --- a/.gitignore +++ b/.gitignore @@ -38,3 +38,5 @@ package-lock.json npm-debug.log* yarn-debug.log* yarn-error.log* + +.php-cs-fixer.cache \ No newline at end of file diff --git a/.php-cs-fixer.php b/.php-cs-fixer.php new file mode 100644 index 0000000..77ef0e7 --- /dev/null +++ b/.php-cs-fixer.php @@ -0,0 +1,16 @@ +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); diff --git a/.prettierrc b/.prettierrc new file mode 100644 index 0000000..7db0434 --- /dev/null +++ b/.prettierrc @@ -0,0 +1,7 @@ +{ + "bracketSameLine": true, + "trailingComma": "all", + "printWidth": 80, + "tabWidth": 4, + "semi": false +} \ No newline at end of file diff --git a/Documentation/conception.puml b/Documentation/conception.puml deleted file mode 100644 index 06ae256..0000000 --- a/Documentation/conception.puml +++ /dev/null @@ -1,12 +0,0 @@ -@startuml - -class Connexion - -class Modele - -class Account - -class AccountGateway - - -@enduml diff --git a/Documentation/models.puml b/Documentation/models.puml index 0ad5135..1f4877a 100755 --- a/Documentation/models.puml +++ b/Documentation/models.puml @@ -50,7 +50,6 @@ enum MemberRole { class Team { - name: String - picture: Url - - members: array + getName(): String + getPicture(): Url @@ -61,6 +60,7 @@ class Team { Team --> "- mainColor" Color Team --> "- secondaryColor" Color +Team --> "- members *" Member class Color { - value: int diff --git a/ci/.drone.yml b/ci/.drone.yml index d6c474c..8b7058d 100644 --- a/ci/.drone.yml +++ b/ci/.drone.yml @@ -1,6 +1,6 @@ kind: pipeline type: docker -name: "Deploy on maxou.dev" +name: "CI and Deploy on maxou.dev" volumes: - name: server @@ -11,11 +11,26 @@ trigger: - 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" 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 @@ -24,14 +39,15 @@ steps: - - /root/.local/bin/moshell ci/build_react.msh - - image: composer:latest + - 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 - - composer install && composer update - rm profiles/dev-config-profile.php - mv src config.php sql profiles vendor /outputs/ diff --git a/ci/build_react.msh b/ci/build_react.msh index c893498..64a0cb6 100755 --- a/ci/build_react.msh +++ b/ci/build_react.msh @@ -3,13 +3,14 @@ mkdir -p /outputs/public apt update && apt install jq -y -npm install val drone_branch = std::env("DRONE_BRANCH").unwrap() val base = "/IQBall/$drone_branch/public" npm run build -- --base=$base --mode PROD +npm run build -- --base=/IQBall/public --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') diff --git a/composer.json b/composer.json index e5b80e0..a3c0e4b 100644 --- a/composer.json +++ b/composer.json @@ -11,5 +11,8 @@ "ext-pdo_sqlite": "*", "twig/twig":"^2.0", "phpstan/phpstan": "*" + }, + "require-dev": { + "friendsofphp/php-cs-fixer": "^3.38" } -} \ No newline at end of file +} diff --git a/config.php b/config.php index 592ee38..6e510c8 100644 --- a/config.php +++ b/config.php @@ -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; - diff --git a/format.sh b/format.sh new file mode 100755 index 0000000..a9ff1c2 --- /dev/null +++ b/format.sh @@ -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 diff --git a/front/Constants.ts b/front/Constants.ts index aaaaa43..76b37c2 100644 --- a/front/Constants.ts +++ b/front/Constants.ts @@ -1,4 +1,4 @@ /** * This constant defines the API endpoint. */ -export const API = import.meta.env.VITE_API_ENDPOINT; \ No newline at end of file +export const API = import.meta.env.VITE_API_ENDPOINT diff --git a/front/ViewRenderer.tsx b/front/ViewRenderer.tsx index ffaf886..57f2e34 100644 --- a/front/ViewRenderer.tsx +++ b/front/ViewRenderer.tsx @@ -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( - - - ); -} \ No newline at end of file + + , + ) +} diff --git a/front/components/Rack.tsx b/front/components/Rack.tsx index 09681e7..2a7511f 100644 --- a/front/components/Rack.tsx +++ b/front/components/Rack.tsx @@ -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 { - id: string, - objects: E[], - onChange: (objects: E[]) => void, - canDetach: (ref: HTMLDivElement) => boolean, - onElementDetached: (ref: HTMLDivElement, el: E) => void, - render: (e: E) => ReactElement, +export interface RackProps { + id: string + objects: E[] + onChange: (objects: E[]) => void + canDetach: (ref: HTMLDivElement) => boolean + onElementDetached: (ref: HTMLDivElement, el: E) => void + render: (e: E) => ReactElement } -interface RackItemProps { - item: E, - onTryDetach: (ref: HTMLDivElement, el: E) => void, - render: (e: E) => ReactElement, +interface RackItemProps { + item: E + onTryDetach: (ref: HTMLDivElement, el: E) => void + render: (e: E) => ReactElement } /** * A container of draggable objects * */ -export function Rack({id, objects, onChange, canDetach, onElementDetached, render}: RackProps) { +export function Rack({ + id, + objects, + onChange, + canDetach, + onElementDetached, + render, +}: RackProps) { return ( -
- {objects.map(element => ( - { - if (!canDetach(ref)) - return +
+ {objects.map((element) => ( + { + if (!canDetach(ref)) return - const index = objects.findIndex(o => o.key === element.key) - onChange(objects.toSpliced(index, 1)) + const index = objects.findIndex( + (o) => o.key === element.key, + ) + onChange(objects.toSpliced(index, 1)) - onElementDetached(ref, element) - }}/> + onElementDetached(ref, element) + }} + /> ))}
) } -function RackItem({item, onTryDetach, render}: RackItemProps) { - const divRef = useRef(null); +function RackItem({ + item, + onTryDetach, + render, +}: RackItemProps) { + const divRef = useRef(null) return ( onTryDetach(divRef.current!, item)}> -
- {render(item)} -
+
{render(item)}
) -} \ No newline at end of file +} diff --git a/front/components/TitleInput.tsx b/front/components/TitleInput.tsx index eb162d1..6e4acb0 100644 --- a/front/components/TitleInput.tsx +++ b/front/components/TitleInput.tsx @@ -1,28 +1,32 @@ -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(null); +export default function TitleInput({ + style, + default_value, + on_validated, +}: TitleInputOptions) { + const [value, setValue] = useState(default_value) + const ref = useRef(null) return ( - setValue(event.target.value)} - onBlur={_ => on_validated(value)} - onKeyDown={event => { - if (event.key == 'Enter') - ref.current?.blur(); - }} + setValue(event.target.value)} + onBlur={(_) => on_validated(value)} + onKeyDown={(event) => { + if (event.key == "Enter") ref.current?.blur() + }} /> ) -} \ No newline at end of file +} diff --git a/front/components/editor/BasketCourt.tsx b/front/components/editor/BasketCourt.tsx index 7e839a8..9f4cb5d 100644 --- a/front/components/editor/BasketCourt.tsx +++ b/front/components/editor/BasketCourt.tsx @@ -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) { +export function BasketCourt({ players, onPlayerRemove }: BasketCourtProps) { return ( -
- - {players.map(player => { - return onPlayerRemove(player)} - /> +
+ + {players.map((player) => { + return ( + onPlayerRemove(player)} + /> + ) })}
) } - diff --git a/front/components/editor/CourtPlayer.tsx b/front/components/editor/CourtPlayer.tsx index b6b64de..9b08e7b 100644 --- a/front/components/editor/CourtPlayer.tsx +++ b/front/components/editor/CourtPlayer.tsx @@ -1,55 +1,49 @@ -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 } /** * 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(null) - const ref = useRef(null); - - const x = player.rightRatio; - const y = player.bottomRatio; + const x = player.rightRatio + const y = player.bottomRatio return ( - -
- -
{ - if (e.key == "Delete") - onRemove() - }}> + +
+
{ + if (e.key == "Delete") onRemove() + }}>
+ onClick={onRemove} + />
- +
-
- ) -} \ No newline at end of file +} diff --git a/front/components/editor/PlayerPiece.tsx b/front/components/editor/PlayerPiece.tsx index 83e7dfc..08bf36d 100644 --- a/front/components/editor/PlayerPiece.tsx +++ b/front/components/editor/PlayerPiece.tsx @@ -1,12 +1,11 @@ -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 (

{text}

) -} \ No newline at end of file +} diff --git a/front/data/Player.ts b/front/data/Player.ts index af88c1c..f2667b9 100644 --- a/front/data/Player.ts +++ b/front/data/Player.ts @@ -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, -} \ No newline at end of file + rightRatio: number +} diff --git a/front/data/Team.tsx b/front/data/Team.tsx index ea4c384..5b35943 100644 --- a/front/data/Team.tsx +++ b/front/data/Team.tsx @@ -1,4 +1,4 @@ export enum Team { Allies = "allies", - Opponents = "opponents" -} \ No newline at end of file + Opponents = "opponents", +} diff --git a/front/style/basket_court.css b/front/style/basket_court.css index a5bc688..c001cc0 100644 --- a/front/style/basket_court.css +++ b/front/style/basket_court.css @@ -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); -} \ No newline at end of file +} diff --git a/front/style/colors.css b/front/style/colors.css index f3287cb..3c17a25 100644 --- a/front/style/colors.css +++ b/front/style/colors.css @@ -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 -} \ No newline at end of file + --selection-color: #3f7fc4; +} diff --git a/front/style/editor.css b/front/style/editor.css index 3aad26c..b586a36 100644 --- a/front/style/editor.css +++ b/front/style/editor.css @@ -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; -} \ No newline at end of file +} diff --git a/front/style/player.css b/front/style/player.css index 264b479..7bea36e 100644 --- a/front/style/player.css +++ b/front/style/player.css @@ -76,4 +76,4 @@ on the court. .player:focus-within { z-index: 1000; -} \ No newline at end of file +} diff --git a/front/style/title_input.css b/front/style/title_input.css index 57af59b..1b6be10 100644 --- a/front/style/title_input.css +++ b/front/style/title_input.css @@ -14,4 +14,3 @@ border-bottom-color: blueviolet; } - diff --git a/front/views/DisplayResults.tsx b/front/views/DisplayResults.tsx index c4bbd1b..7e22df3 100644 --- a/front/views/DisplayResults.tsx +++ b/front/views/DisplayResults.tsx @@ -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}) => +export default function DisplayResults({ results }: DisplayResultsProps) { + const list = results.map(({ name, description }) => (

username: {name}

description: {description}

- ) - return ( -
- {list} -
- ) + )) + return
{list}
} diff --git a/front/views/Editor.tsx b/front/views/Editor.tsx index 423385f..d98062d 100644 --- a/front/views/Editor.tsx +++ b/front/views/Editor.tsx @@ -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({}); - +export default function Editor({ id, name }: { id: number; name: string }) { + const [style, setStyle] = useState({}) 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([]); - const courtDivContentRef = useRef(null); + const [players, setPlayers] = useState([]) + const courtDivContentRef = useRef(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, { - id: players.length, - team: element.team, - role: element.key, - rightRatio: xRatio, - bottomRatio: yRatio - }] + setPlayers((players) => { + return [ + ...players, + { + id: players.length, + team: element.team, + role: element.key, + rightRatio: xRatio, + bottomRatio: yRatio, + }, + ] }) } @@ -73,74 +75,88 @@ export default function Editor({id, name}: { id: number, name: string }) {
LEFT
- { - fetch(`${API}/tactic/${id}/edit/name`, { - method: "POST", - headers: { - 'Accept': 'application/json', - 'Content-Type': 'application/json' - }, - body: JSON.stringify({ - name: new_name, + { + fetch(`${API}/tactic/${id}/edit/name`, { + method: "POST", + headers: { + Accept: "application/json", + "Content-Type": "application/json", + }, + body: JSON.stringify({ + name: new_name, + }), + }).then((response) => { + if (response.ok) { + setStyle({}) + } else { + setStyle(ERROR_STYLE) + } }) - }).then(response => { - if (response.ok) { - setStyle({}) - } else { - setStyle(ERROR_STYLE) - } - }) - }}/> + }} + />
RIGHT
- }/> - }/> + ( + + )} + /> + ( + + )} + />
{ - 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, + }, + ]) } - }}/> + }} + />
) } - diff --git a/front/views/SampleForm.tsx b/front/views/SampleForm.tsx index 604e362..00309e4 100644 --- a/front/views/SampleForm.tsx +++ b/front/views/SampleForm.tsx @@ -1,19 +1,14 @@ - - export default function SampleForm() { return (

Hello, this is a sample form made in react !

- + - - + +
) } - - - diff --git a/package.json b/package.json index 97f0039..79c9d46 100644 --- a/package.json +++ b/package.json @@ -20,7 +20,9 @@ "scripts": { "start": "vite --host", "build": "vite build", - "test": "vite test" + "test": "vite test", + "format": "prettier --config .prettierrc 'front' --write", + "tsc": "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" } } diff --git a/phpstan.neon b/phpstan.neon new file mode 100644 index 0000000..bc6c041 --- /dev/null +++ b/phpstan.neon @@ -0,0 +1,13 @@ +parameters: + phpVersion: 70400 + level: 6 + paths: + - src + - public + scanFiles: + - config.php + - sql/database.php + - profiles/dev-config-profile.php + - profiles/prod-config-profile.php + excludePaths: + - src/react-display-file.php diff --git a/profiles/dev-config-profile.php b/profiles/dev-config-profile.php index 316ff44..bd87f1d 100644 --- a/profiles/dev-config-profile.php +++ b/profiles/dev-config-profile.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; } - - - diff --git a/profiles/prod-config-profile.php b/profiles/prod-config-profile.php index e185dfc..e9bb12c 100644 --- a/profiles/prod-config-profile.php +++ b/profiles/prod-config-profile.php @@ -19,4 +19,4 @@ function _asset(string $assetURI): string { // If the asset uri does not figure in the available assets array, // fallback to the uri itself. return $basePath . "/" . (ASSETS[$assetURI] ?? $assetURI); -} \ No newline at end of file +} diff --git a/public/api/index.php b/public/api/index.php index b6327e1..3ed5caa 100644 --- a/public/api/index.php +++ b/public/api/index.php @@ -37,6 +37,6 @@ http_response_code($response->getCode()); if ($response instanceof JsonHttpResponse) { header('Content-type: application/json'); echo $response->getJson(); -} else if ($response instanceof ViewHttpResponse) { +} elseif ($response instanceof ViewHttpResponse) { throw new Exception("API returned a view http response."); -} \ No newline at end of file +} diff --git a/public/index.php b/public/index.php index ba9d7c0..e81d098 100644 --- a/public/index.php +++ b/public/index.php @@ -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,12 +64,12 @@ 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; } -} else if ($response instanceof JsonHttpResponse) { +} elseif ($response instanceof JsonHttpResponse) { header('Content-type: application/json'); echo $response->getJson(); -} \ No newline at end of file +} diff --git a/public/utils.php b/public/utils.php index a3566fe..7151386 100644 --- a/public/utils.php +++ b/public/utils.php @@ -3,18 +3,19 @@ /** * 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]; if ($c == "/" || $c == "\\") { $basePath = substr($basePath, 0, $basePathLen - 1); } return $basePath; -} \ No newline at end of file +} diff --git a/sql/database.php b/sql/database.php index d49ddfd..8f5aa9d 100644 --- a/sql/database.php +++ b/sql/database.php @@ -25,6 +25,3 @@ function get_database(): PDO { return $pdo; } - - - diff --git a/src/Connexion.php b/src/Connexion.php index 788c0fb..987d35b 100644 --- a/src/Connexion.php +++ b/src/Connexion.php @@ -1,28 +1,27 @@ pdo = $pdo; } - public function lastInsertId() { + public function lastInsertId(): string { return $this->pdo->lastInsertId(); } /** * execute a request * @param string $query - * @param array $args + * @param array> $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> $args an array containing the arguments label, value and type: ex: `[":label" => [$value, PDO::PARAM_TYPE]` + * @return array[] 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> $args + * @return \PDOStatement + */ private function prepare(string $query, array $args): \PDOStatement { $stmnt = $this->pdo->prepare($query); foreach ($args as $name => $value) { @@ -50,4 +54,4 @@ class Connexion { return $stmnt; } -} \ No newline at end of file +} diff --git a/src/Controller/Api/APITacticController.php b/src/Controller/Api/APITacticController.php index a39b2ce..ec0edc8 100644 --- a/src/Controller/Api/APITacticController.php +++ b/src/Controller/Api/APITacticController.php @@ -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(); @@ -52,4 +52,4 @@ class APITacticController { return new JsonHttpResponse($tactic_info); } -} \ No newline at end of file +} diff --git a/src/Controller/Control.php b/src/Controller/Control.php index 8c00d2e..2b428d9 100644 --- a/src/Controller/Control.php +++ b/src/Controller/Control.php @@ -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 callable $run the callback to run if the request is valid according to the given schema. + * @param array $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. + * @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 { @@ -23,7 +25,7 @@ class Control { $payload_obj = json_decode($request_body); if (!$payload_obj instanceof \stdClass) { $fail = new ValidationFail("bad-payload", "request body is not a valid json object"); - if($errorInJson) { + if ($errorInJson) { return new JsonHttpResponse([$fail, HttpCodes::BAD_REQUEST]); } return ViewHttpResponse::twig("error.html.twig", ["failures" => [$fail]], HttpCodes::BAD_REQUEST); @@ -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 callable $run the callback to run if the request is valid according to 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 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. + * @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 { @@ -45,7 +49,7 @@ class Control { $request = HttpRequest::from($data, $fails, $schema); if (!empty($fails)) { - if($errorInJson) { + if ($errorInJson) { return new JsonHttpResponse($fails, HttpCodes::BAD_REQUEST); } return ViewHttpResponse::twig("error.html.twig", ['failures' => $fails], HttpCodes::BAD_REQUEST); @@ -53,9 +57,6 @@ class Control { return call_user_func_array($run, [$request]); } - - - -} \ No newline at end of file +} diff --git a/src/Controller/EditorController.php b/src/Controller/EditorController.php index bf5dccc..ed270d1 100644 --- a/src/Controller/EditorController.php +++ b/src/Controller/EditorController.php @@ -11,7 +11,6 @@ use App\Http\ViewHttpResponse; use App\Model\TacticModel; class EditorController { - private TacticModel $model; /** @@ -45,4 +44,4 @@ class EditorController { return $this->openEditor($tactic); } -} \ No newline at end of file +} diff --git a/src/Controller/ErrorController.php b/src/Controller/ErrorController.php index e91d05f..7fc5239 100644 --- a/src/Controller/ErrorController.php +++ b/src/Controller/ErrorController.php @@ -3,17 +3,23 @@ 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) { + } catch (LoaderError|RuntimeError|SyntaxError $e) { echo "Twig error: $e"; } } diff --git a/src/Controller/SampleFormController.php b/src/Controller/SampleFormController.php index 4241ad4..773a4db 100644 --- a/src/Controller/SampleFormController.php +++ b/src/Controller/SampleFormController.php @@ -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 $form + * @param callable(array>): ViewHttpResponse $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,11 +46,19 @@ class SampleFormController { }, false); } + /** + * @param array $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 $form + * @return HttpResponse + */ public function submitFormReact(array $form): HttpResponse { return $this->submitForm($form, fn(array $results) => ViewHttpResponse::react('views/DisplayResults.tsx', $results)); } -} \ No newline at end of file +} diff --git a/src/Data/Account.php b/src/Data/Account.php index 155f2ae..2a21bf1 100755 --- a/src/Data/Account.php +++ b/src/Data/Account.php @@ -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 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 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; } @@ -96,4 +102,4 @@ class Account { public function getUser(): AccountUser { return $this->user; } -} \ No newline at end of file +} diff --git a/src/Data/AccountUser.php b/src/Data/AccountUser.php index 7808062..5d3b497 100755 --- a/src/Data/AccountUser.php +++ b/src/Data/AccountUser.php @@ -36,17 +36,17 @@ 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; } -} \ No newline at end of file +} diff --git a/src/Data/Color.php b/src/Data/Color.php index f841731..0b1fbb3 100755 --- a/src/Data/Color.php +++ b/src/Data/Color.php @@ -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"); } @@ -27,4 +27,4 @@ class Color { public function getValue(): int { return $this->value; } -} \ No newline at end of file +} diff --git a/src/Data/Member.php b/src/Data/Member.php index 91b09c4..6500733 100755 --- a/src/Data/Member.php +++ b/src/Data/Member.php @@ -39,4 +39,4 @@ class Member { return $this->role; } -} \ No newline at end of file +} diff --git a/src/Data/MemberRole.php b/src/Data/MemberRole.php index 05d746d..a1d4d31 100755 --- a/src/Data/MemberRole.php +++ b/src/Data/MemberRole.php @@ -2,7 +2,6 @@ namespace App\Data; - use http\Exception\InvalidArgumentException; /** @@ -37,4 +36,4 @@ final class MemberRole { return ($this->value == self::ROLE_COACH); } -} \ No newline at end of file +} diff --git a/src/Data/TacticInfo.php b/src/Data/TacticInfo.php index 901280d..eef7bb3 100644 --- a/src/Data/TacticInfo.php +++ b/src/Data/TacticInfo.php @@ -30,7 +30,10 @@ class TacticInfo implements \JsonSerializable { return $this->creation_date; } - public function jsonSerialize() { + /** + * @return array + */ + public function jsonSerialize(): array { return get_object_vars($this); } -} \ No newline at end of file +} diff --git a/src/Data/Team.php b/src/Data/Team.php index 48643d9..aca4e0d 100755 --- a/src/Data/Team.php +++ b/src/Data/Team.php @@ -11,7 +11,7 @@ class Team { private Color $secondColor; /** - * @var array maps users with their role + * @var 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 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 Member[] + */ public function listMembers(): array { - return array_map(fn ($id, $role) => new Member($id, $role), $this->members); + return $this->members; } -} \ No newline at end of file +} diff --git a/src/Data/User.php b/src/Data/User.php index 15c9995..6cb55c2 100755 --- a/src/Data/User.php +++ b/src/Data/User.php @@ -4,7 +4,6 @@ namespace App\Data; use http\Url; - /** * Public information about a user */ @@ -24,4 +23,4 @@ interface User { * @return int The user's age */ public function getAge(): int; -} \ No newline at end of file +} diff --git a/src/Gateway/FormResultGateway.php b/src/Gateway/FormResultGateway.php index fe0c601..36178ad 100644 --- a/src/Gateway/FormResultGateway.php +++ b/src/Gateway/FormResultGateway.php @@ -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 + */ + public function listResults(): array { return $this->con->fetch("SELECT * FROM FormEntries", []); } -} \ No newline at end of file +} diff --git a/src/Gateway/TacticInfoGateway.php b/src/Gateway/TacticInfoGateway.php index 20d2957..3441c9a 100644 --- a/src/Gateway/TacticInfoGateway.php +++ b/src/Gateway/TacticInfoGateway.php @@ -4,7 +4,7 @@ namespace App\Gateway; use App\Connexion; use App\Data\TacticInfo; -use \PDO; +use PDO; class TacticInfoGateway { private Connexion $con; @@ -43,14 +43,14 @@ 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], ] ); } -} \ No newline at end of file +} diff --git a/src/Http/HttpCodes.php b/src/Http/HttpCodes.php index b41af8a..f9d550c 100644 --- a/src/Http/HttpCodes.php +++ b/src/Http/HttpCodes.php @@ -10,4 +10,4 @@ class HttpCodes { public const BAD_REQUEST = 400; public const NOT_FOUND = 404; -} \ No newline at end of file +} diff --git a/src/Http/HttpRequest.php b/src/Http/HttpRequest.php index f841752..d199227 100644 --- a/src/Http/HttpRequest.php +++ b/src/Http/HttpRequest.php @@ -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 + * */ class HttpRequest implements ArrayAccess { + /** + * @var array + */ private array $data; + /** + * @param array $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 $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 * @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,15 +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 + * @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."); } -} \ No newline at end of file +} diff --git a/src/Http/HttpResponse.php b/src/Http/HttpResponse.php index 9f081a5..5d8c3bf 100644 --- a/src/Http/HttpResponse.php +++ b/src/Http/HttpResponse.php @@ -3,7 +3,6 @@ namespace App\Http; class HttpResponse { - private int $code; /** @@ -21,4 +20,4 @@ class HttpResponse { return new HttpResponse($code); } -} \ No newline at end of file +} diff --git a/src/Http/JsonHttpResponse.php b/src/Http/JsonHttpResponse.php index 9d7423f..bbd3d80 100644 --- a/src/Http/JsonHttpResponse.php +++ b/src/Http/JsonHttpResponse.php @@ -3,7 +3,6 @@ namespace App\Http; class JsonHttpResponse extends HttpResponse { - /** * @var mixed Any JSON serializable value */ @@ -26,4 +25,4 @@ class JsonHttpResponse extends HttpResponse { return $result; } -} \ No newline at end of file +} diff --git a/src/Http/ViewHttpResponse.php b/src/Http/ViewHttpResponse.php index 0e92054..2e517d7 100644 --- a/src/Http/ViewHttpResponse.php +++ b/src/Http/ViewHttpResponse.php @@ -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 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 $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 + */ 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 $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 $arguments * @param int $code * @return ViewHttpResponse */ @@ -67,4 +69,4 @@ class ViewHttpResponse extends HttpResponse { return new ViewHttpResponse(self::REACT_VIEW, $file, $arguments, $code); } -} \ No newline at end of file +} diff --git a/src/Model/TacticModel.php b/src/Model/TacticModel.php index c0b1ffe..cabcdec 100644 --- a/src/Model/TacticModel.php +++ b/src/Model/TacticModel.php @@ -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) { @@ -51,4 +50,4 @@ class TacticModel { return true; } -} \ No newline at end of file +} diff --git a/src/Validation/ComposedValidator.php b/src/Validation/ComposedValidator.php index 418b1ed..cc6e9e5 100644 --- a/src/Validation/ComposedValidator.php +++ b/src/Validation/ComposedValidator.php @@ -3,7 +3,6 @@ namespace App\Validation; class ComposedValidator extends Validator { - private Validator $first; private Validator $then; @@ -21,4 +20,4 @@ class ComposedValidator extends Validator { $thenFailures = $this->then->validate($name, $val); return array_merge($firstFailures, $thenFailures); } -} \ No newline at end of file +} diff --git a/src/Validation/FieldValidationFail.php b/src/Validation/FieldValidationFail.php index 5b535f7..af2ce3a 100644 --- a/src/Validation/FieldValidationFail.php +++ b/src/Validation/FieldValidationFail.php @@ -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 + */ + public function jsonSerialize(): array { return ["field" => $this->fieldName, "message" => $this->getMessage()]; } -} \ No newline at end of file +} diff --git a/src/Validation/FunctionValidator.php b/src/Validation/FunctionValidator.php index 6874d63..4949d8b 100644 --- a/src/Validation/FunctionValidator.php +++ b/src/Validation/FunctionValidator.php @@ -3,11 +3,13 @@ namespace App\Validation; class FunctionValidator extends Validator { - + /** + * @var callable(string, mixed): ValidationFail[] + */ private $validate_fn; /** - * @param callable $validate_fn the validate function. Must have the same signature as the {@link Validator::validate()} method. + * @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; @@ -16,4 +18,4 @@ class FunctionValidator extends Validator { public function validate(string $name, $val): array { return call_user_func_array($this->validate_fn, [$name, $val]); } -} \ No newline at end of file +} diff --git a/src/Validation/SimpleFunctionValidator.php b/src/Validation/SimpleFunctionValidator.php index 079452d..cec52c0 100644 --- a/src/Validation/SimpleFunctionValidator.php +++ b/src/Validation/SimpleFunctionValidator.php @@ -6,13 +6,18 @@ namespace App\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 $predicate a function predicate with signature: `(string) => bool`, to validate the given string - * @param callable $errorsFactory a factory function with signature `(string) => array` to emit failures when the predicate fails + * @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; @@ -25,4 +30,4 @@ class SimpleFunctionValidator extends Validator { } return []; } -} \ No newline at end of file +} diff --git a/src/Validation/Validation.php b/src/Validation/Validation.php index 4372380..f1392e6 100644 --- a/src/Validation/Validation.php +++ b/src/Validation/Validation.php @@ -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 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 */ @@ -27,4 +26,4 @@ class Validation { return $had_errors; } -} \ No newline at end of file +} diff --git a/src/Validation/ValidationFail.php b/src/Validation/ValidationFail.php index fa5139c..4f1ec22 100644 --- a/src/Validation/ValidationFail.php +++ b/src/Validation/ValidationFail.php @@ -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 + */ + public function jsonSerialize(): array { return ["error" => $this->kind, "message" => $this->message]; } @@ -32,4 +37,4 @@ class ValidationFail implements \JsonSerializable { return new ValidationFail("not found", $message); } -} \ No newline at end of file +} diff --git a/src/Validation/Validator.php b/src/Validation/Validator.php index 6cdafb9..8227e46 100644 --- a/src/Validation/Validator.php +++ b/src/Validation/Validator.php @@ -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 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 @@ -21,4 +20,4 @@ abstract class Validator { return new ComposedValidator($this, $other); } -} \ No newline at end of file +} diff --git a/src/Validation/Validators.php b/src/Validation/Validators.php index ea9da46..94aea23 100644 --- a/src/Validation/Validators.php +++ b/src/Validation/Validators.php @@ -6,11 +6,10 @@ namespace App\Validation; * A collection of standard validators */ class Validators { - /** * @return Validator a validator that validates a given regex */ - public static function regex(string $regex, string $msg = null): Validator { + public static function regex(string $regex, ?string $msg = null): Validator { return new SimpleFunctionValidator( fn(string $str) => preg_match($regex, $str), fn(string $name) => [new FieldValidationFail($name, $msg == null ? "field does not validates pattern $regex" : $msg)] @@ -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); } @@ -51,4 +50,4 @@ class Validators { } ); } -} \ No newline at end of file +} diff --git a/src/react-display.php b/src/react-display.php index b965a3a..5baf41b 100644 --- a/src/react-display.php +++ b/src/react-display.php @@ -3,11 +3,11 @@ /** * 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 - * The arguments must be a json-encodable key/value dictionary. + * @param array $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"; -} \ No newline at end of file +} diff --git a/verify.sh b/verify.sh new file mode 100755 index 0000000..314b8bc --- /dev/null +++ b/verify.sh @@ -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" \ No newline at end of file