188 lines
7.0 KiB
PHP
188 lines
7.0 KiB
PHP
<?php
|
||
|
||
declare(strict_types=1);
|
||
|
||
namespace App\Models;
|
||
|
||
/**
|
||
* Contrôle de conformité du schéma, joué à CHAQUE requête par app/bootstrap.php.
|
||
*
|
||
* Ce garde-fou existe parce que les migrations sont des fichiers SQL appliqués
|
||
* par un service séparé : rien ne garantit que la base en face du code soit à
|
||
* jour. Plutôt que de laisser l'application échouer plus tard sur une erreur SQL
|
||
* cryptique au milieu d'un écran, on refuse de démarrer avec un message qui
|
||
* nomme précisément ce qui manque et quel fichier jouer.
|
||
*
|
||
* CONSÉQUENCE POUR LE DÉVELOPPEUR : ajouter une table ou une colonne obligatoire
|
||
* impose de compléter les listes ci-dessous EN MÊME TEMPS que la migration.
|
||
* Oublier de le faire ne casse rien tout de suite — c'est justement le piège :
|
||
* l'application démarrera sur une base incomplète.
|
||
*
|
||
* Le coût est de trois requêtes sur information_schema par requête HTTP,
|
||
* accepté au regard du volume d'utilisateurs de l'application.
|
||
*/
|
||
final class SchemaModel extends BaseModel
|
||
{
|
||
/**
|
||
* Vérifie tables, colonnes puis vues d'accès.
|
||
*
|
||
* Seules les colonnes AJOUTÉES PAR MIGRATION sont listées, pas l'intégralité
|
||
* du schéma : ce sont les seules susceptibles de manquer sur une base créée
|
||
* à partir de database/pta_prod_schema_mariadb.sql, qui ne contient que le
|
||
* socle métier.
|
||
*
|
||
* @throws \RuntimeException Au premier manque constaté, avec le détail et
|
||
* le fichier SQL à exécuter.
|
||
*/
|
||
public function assertCurrent(): void
|
||
{
|
||
$requiredTables = [
|
||
'agent',
|
||
'structure',
|
||
'agent_structure',
|
||
'motif_planning',
|
||
'semaine',
|
||
'planning',
|
||
'creneau_horaire',
|
||
'quota_agent_annuel',
|
||
'modele_semaine_agent',
|
||
'modele_semaine_creneau',
|
||
'structure_couverture_horaire',
|
||
'periode_vacance',
|
||
'poste_agent',
|
||
'pta_annuel',
|
||
'agent_structure_souhait',
|
||
'agent_contrainte',
|
||
'equipe_pta',
|
||
'equipe_pta_membre',
|
||
'structure_creneau_reference',
|
||
'compte_acces',
|
||
'journal_activite',
|
||
];
|
||
|
||
$placeholders = implode(',', array_fill(0, count($requiredTables), '?'));
|
||
$stmt = $this->pdo->prepare(
|
||
"SELECT TABLE_NAME
|
||
FROM information_schema.TABLES
|
||
WHERE TABLE_SCHEMA = DATABASE()
|
||
AND TABLE_NAME IN ($placeholders)"
|
||
);
|
||
$stmt->execute($requiredTables);
|
||
$existing = $stmt->fetchAll(\PDO::FETCH_COLUMN);
|
||
$missingTables = array_values(array_diff($requiredTables, $existing));
|
||
|
||
if ($missingTables) {
|
||
throw new \RuntimeException(
|
||
'Base PTA incomplète : table(s) manquante(s) : '
|
||
. implode(', ', $missingTables)
|
||
. '. Exécutez database/install_complet.sql ou les migrations nécessaires.'
|
||
);
|
||
}
|
||
|
||
$requiredColumns = [
|
||
'creneau_horaire' => ['id_motif'],
|
||
'planning' => ['date_validation'],
|
||
'agent' => ['email', 'telephone', 'adresse', 'est_diplome', 'diplome_libelle', 'id_structure', 'id_poste', 'type_contrat', 'date_debut_contrat', 'date_fin_contrat', 'droit_enveloppe_formation', 'commentaire_pta'],
|
||
'structure' => ['type_affectation'],
|
||
'compte_acces' => ['mot_de_passe_hash'],
|
||
];
|
||
|
||
foreach ($requiredColumns as $table => $columns) {
|
||
$columnPlaceholders = implode(',', array_fill(0, count($columns), '?'));
|
||
$params = array_merge([$table], $columns);
|
||
$columnStmt = $this->pdo->prepare(
|
||
"SELECT COLUMN_NAME
|
||
FROM information_schema.COLUMNS
|
||
WHERE TABLE_SCHEMA = DATABASE()
|
||
AND TABLE_NAME = ?
|
||
AND COLUMN_NAME IN ($columnPlaceholders)"
|
||
);
|
||
$columnStmt->execute($params);
|
||
$existingColumns = $columnStmt->fetchAll(\PDO::FETCH_COLUMN);
|
||
$missingColumns = array_values(array_diff($columns, $existingColumns));
|
||
|
||
if ($missingColumns) {
|
||
throw new \RuntimeException(sprintf(
|
||
'Base PTA incomplète : colonne(s) manquante(s) dans %s : %s.',
|
||
$table,
|
||
implode(', ', $missingColumns)
|
||
));
|
||
}
|
||
}
|
||
|
||
$this->assertAccessViews();
|
||
$this->assertJetonViews();
|
||
}
|
||
|
||
/**
|
||
* Les vues d'accès portent l'intégralité des règles de droits. Si elles
|
||
* manquent, l'application doit refuser de démarrer plutôt que de tourner
|
||
* sans contrôle de périmètre.
|
||
*/
|
||
private function assertAccessViews(): void
|
||
{
|
||
$requiredViews = [
|
||
'v_acces_role_page',
|
||
'v_acces_role_permission',
|
||
'v_acces_identite_agent',
|
||
'v_acces_identite_structure',
|
||
'v_acces_agent_par_structure',
|
||
'v_acces_perimetre_creneau',
|
||
'v_acces_perimetre_planning',
|
||
];
|
||
|
||
$placeholders = implode(',', array_fill(0, count($requiredViews), '?'));
|
||
$stmt = $this->pdo->prepare(
|
||
"SELECT TABLE_NAME
|
||
FROM information_schema.VIEWS
|
||
WHERE TABLE_SCHEMA = DATABASE()
|
||
AND TABLE_NAME IN ($placeholders)"
|
||
);
|
||
$stmt->execute($requiredViews);
|
||
$missing = array_values(array_diff($requiredViews, $stmt->fetchAll(\PDO::FETCH_COLUMN)));
|
||
|
||
if ($missing) {
|
||
throw new \RuntimeException(
|
||
'Contrôle d’accès indisponible : vue(s) manquante(s) : '
|
||
. implode(', ', $missing)
|
||
. '. Exécutez database/migration_v7_8_securite_acces.sql.'
|
||
);
|
||
}
|
||
}
|
||
|
||
/**
|
||
* Vues des comptes à jeton et du journal (V7.11).
|
||
*
|
||
* Même logique fail-closed que pour les vues de droits : sans elles,
|
||
* personne ne peut plus s'authentifier et aucune action ne serait tracée.
|
||
* Mieux vaut un message explicite au démarrage qu'une application ouverte
|
||
* sans journal.
|
||
*/
|
||
private function assertJetonViews(): void
|
||
{
|
||
$requiredViews = [
|
||
'v_acces_compte',
|
||
'v_acces_compte_manquant',
|
||
'v_journal_activite',
|
||
];
|
||
|
||
$placeholders = implode(',', array_fill(0, count($requiredViews), '?'));
|
||
$stmt = $this->pdo->prepare(
|
||
"SELECT TABLE_NAME
|
||
FROM information_schema.VIEWS
|
||
WHERE TABLE_SCHEMA = DATABASE()
|
||
AND TABLE_NAME IN ($placeholders)"
|
||
);
|
||
$stmt->execute($requiredViews);
|
||
$missing = array_values(array_diff($requiredViews, $stmt->fetchAll(\PDO::FETCH_COLUMN)));
|
||
|
||
if ($missing) {
|
||
throw new \RuntimeException(
|
||
'Comptes d’accès indisponibles : vue(s) manquante(s) : '
|
||
. implode(', ', $missing)
|
||
. '. Exécutez database/migration_v7_9e_dsi_comptes_journal.sql.'
|
||
);
|
||
}
|
||
}
|
||
}
|