SMMP/manager/ORM/Rows.php

863 lines
25 KiB
PHP

<?php
namespace manager\ORM;
use \manager\Database;
use \manager\ORM\SQLBuilder;
class Rows{
/* CONSTANTES */
const COND_EQUAL = '__=__';
const COND_NOTEQ = '__<>__';
const COND_INF = '__<__';
const COND_SUP = '__>__';
const COND_INFEQ = '__<=__';
const COND_SUPEQ = '__>=__';
const COND_LIKE = '__LIKE__';
const COND_IN = '__IN__';
const DEFAULT = '__DEFAULT__'; // Valeur DEFAULT (pour insertion)
/* Attributs */
private $where; // Tableau associatif contenant les conditions
private $select; // Tableau contenant la liste des champs à afficher
private $unique; // VRAI si on attend une valeur unique
private $schema; // Tableau contenant les informations associées aux données
private $joined; // Tableau contenant les Rows liés
/* CONSTRUCTEUR
*
* @schema<Array> Tableau contenant les informations de la requête
*
*/
public function __construct($schema){
/* (1) On récupère les informations */
$this->schema = $schema;
/* (2) On initialise les conditions */
$this->where = [];
/* (3) On initialise les champs à retourner */
$this->select = [];
/* (4) On initialise le caractère 'unique' du résultat */
$this->unique = false;
/* (5) On initialise les jointures */
$this->joined = [];
}
/* FILTRE LES ENTREES D'UNE TABLE AVEC LA CLE PRIMAIRE SPECIFIEE
*
* @primary<mixed> Clé primaire simple
* OU
* @primary<Array> Clé primaire composée
*
* @return Rows<Rows> Tableau contenant toutes les entrées de la table
*
*/
public function whereId($primary){
/* [0] Vérification des paramètres
=========================================================*/
if( $primary == null )
return $this;
/* [1] On récupère les clés primaires
=========================================================*/
$keys = [];
foreach($this->schema['columns'] as $k=>$v)
if( $v['primary'] ) $keys[] = $k;
/* [2] Si clé simple
=========================================================*/
/* (1) On met au même format qu'une clé composée */
if( count($keys) == 1 )
$primary = [ $primary ];
/* [3] Si clé composée
=========================================================*/
$defaultWhere = $this->where;
/* (1) Pour chaque clé, On vérifie les TYPES */
foreach($keys as $i=>$key){
$inCond = is_array($primary[$i]) && count($primary[$i]) >= 2 && is_array($primary[$i][0]) && $primary[$i][1] == self::COND_IN;
/* (1) Si c'est une condition "IN"
---------------------------------------------------------*/
if( $inCond ){
/* (1) On vérifie le type de chaque valeur du IN */
$type = $this->schema['columns'][$key]['type'];
foreach($primary[$i][0] as $value){
if( $type == 'int' && !is_numeric($value) ){ $this->where = $defaultWhere; return $this; }
if( $type == 'float' && !is_numeric($value) ){ $this->where = $defaultWhere; return $this; }
if( in_array($type, ['text', 'varchar']) && !is_string($value) ){ $this->where = $defaultWhere; return $this; }
}
/* (2) Si c'est une condition "simple"
---------------------------------------------------------*/
}else{
/* (1) Si le type de condition est manquant, on met EQUAL par défaut */
if( !is_array($primary[$i]) )
$primary[$i] = [ $primary[$i], self::COND_EQUAL ];
/* (2) On vérifie le type de chaque valeur */
$type = $this->schema['columns'][$key]['type'];
if( $type == 'int' && !is_numeric($primary[$i][0]) ){ $this->where = $defaultWhere; return $this; }
if( $type == 'float' && !is_numeric($primary[$i][0]) ){ $this->where = $defaultWhere; return $this; }
if( in_array($type, ['text', 'varchar']) && !is_string($primary[$i][0]) ){ $this->where = $defaultWhere; return $this; }
}
/* (6) Si type OK, on enregistre la condition */
if( !isset($this->where[$key]) )
$this->where[$key] = [];
/* (7) On ajoute la condition */
$this->where[$key][] = $primary[$i];
}
/* [4] On renvoie l'object courant
=========================================================*/
return $this;
}
/* FILTRAGE DYNAMIQUES
*
* @method<String> Nom de la méthode
* @parameter<mixed> Valeur du paramètre
* @parameter<Array> Valeur du paramètre + type de vérification (tableau)
*
* @return this<Rows> Retourne l'object courant
*
*/
public function __call($m, $a){
/* [0] On vérifie que la requête est du type 'getBy{Attribute}'
=========================================================*/
if( !preg_match('/^where(.+)$/', $m, $regex) ) // si requête incorrecte, on ne fais rien
return $this;
/* [1] On récupère le nom de la colonne
=========================================================*/
$column_name = '';
/* (1) formatte la requête 'MyAttribute' -> 'my_attribute' */
for( $l = 0 ; $l < strlen($regex[1]) ; $l++ ){
$letter = $regex[1][$l];
// Si la lettre est en majuscule mais que c'est pas la première
if( strtoupper($letter) == $letter && $l > 0 )
$column_name .= '_';
$column_name .= strtolower($letter);
}
/* (2) On vérifie que la colonne existe */
if( !isset($this->schema['columns'][$column_name]) )
return $this; // si n'existe pas, on ne fait rien
/* [2] On vérifie le type du paramètre
=========================================================*/
/* (1) Si aucun param, on quitte */
if( count($a) == 0 )
return $this;
/* (2) Si c'est un paramètre seul, on ajoute par défaut self::COND_EQUAL */
if( !is_array($a[0]) )
$a[0] = [ $a[0], self::COND_EQUAL ];
/* (3) Si type INT et pas numérique */
if( $this->schema['columns'][$column_name]['type'] == 'int' && !is_numeric($a[0][0]) )
return $this;
/* (4) Si type FLOAT et pas numérique */
if( $this->schema['columns'][$column_name]['type'] == 'float' && !is_numeric($a[0][0]) )
return $this;
/* (5) Si type STRING et pas string */
if( $this->schema['columns'][$column_name]['type'] == 'text' && !is_string($a[0][0]) )
return $this;
/* [3] Si type OK, on enregistre la condition
=========================================================*/
/* (1) Si aucune condition pour ce champ, on crée un tableau */
if( !isset($this->where[$column_name]) )
$this->where[$column_name] = [];
/* (2) On ajoute la condition */
$this->where[$column_name][] = $a[0];
// On retourne l'object courant
return $this;
}
/* SELECTIONNE UNIQUEMENT LES CHAMPS SELECTIONNES
*
* @fields<Array> Libellé du champ à afficher
*
* @return this<Rows> Retourne l'object courant
*
*/
public function select($fields=[]){
/* [1] On formatte les champs
=========================================================*/
/* (1) On met en tableau quoi qu'il en soit */
$fields = is_array($fields) ? $fields : [$fields];
/* (2) On vérifie que chaque champ existe, sinon on le retire */
foreach($fields as $f=>$field)
if( !isset($this->schema['columns'][$field]) && $field != '*' )
unset($fields[$f]);
/* (3) Permet d'avoir les indices de 0 à count-1 */
sort($fields);
/* [2] On enregistre la liste des champs
=========================================================*/
foreach($fields as $f=>$field)
if( !in_array($field, $this->select) ) // On ajoute si pas déja
$this->select[] = $field;
/* [3] On retourne l'object courant
=========================================================*/
return $this;
}
/* JOINT UNE SECONDE TABLE ()
*
* @field<String> Nom d'une colonne
* @rows<Rows> Rows d'une autre table
*
* @return this<Rows> Retourne l'object courant
*
*/
public function join($field, $rows){
/* [0] Vérification des paramètres
=========================================================*/
if( !is_string($field) || !($rows instanceof Rows) )
return $this;
/* [1] On vérifie que la clé étrangère est correcte
=========================================================*/
/* (1) On vérifie que la colonne existe et qu'elle est étrangère*/
if( !isset($this->schema['columns'][$field]['references']) )
return $this;
/* (2) On vérifie que la table de @rows est correcte */
if( $this->schema['columns'][$field]['references'][0] != $rows->schema['table'] )
return $this;
/* [2] On enregistre la référence
=========================================================*/
$this->joined[$field] = $rows;
/* [3] On retourne l'object courant
=========================================================*/
return $this;
}
/* PERMET DE DIRE QUE L'ON VEUT UN RESULTAT UNIQUE
*
* @return this<Rows> Retourne l'object courant
*
*/
public function unique(){
/* [1] On enregistre le choix
=========================================================*/
$this->unique = true;
/* [2] On retourne l'object courant
=========================================================*/
return $this;
}
/* MODIFIE DES ENTREES (SANS MODIFICATION DE CLE PRIMAIRE POSSIBLE)
*
* @updates<Array> Tableau associatif contenant les nouvelles valeurs
*
* @return updated<Boolean> Retourne si TRUE/FALSE la modification a bien été faite
*
*/
public function edit($updates){
/* [0] Vérification des paramètres
=========================================================*/
/* (1) Si c'est pas un tableau, erreur */
if( !is_array($updates) )
return false;
/* (2) On retire les champ inconnus / clés primaires */
$cleared = [];
// Pour chaque entrée du tableau
foreach($updates as $field=>$value)
if( isset($this->schema['columns'][$field]) && !$this->schema['columns'][$field]['primary'] ) // Champ existe et n'est pas clé primaire
$cleared[$field] = $value;
/* (3) On vérifie les types des champs */
foreach($cleared as $field=>$value){
$type = $this->schema['columns'][$field]['type'];
// {1} Si de type INT/FLOAT et pas numérique, on retire le champ //
if( in_array($type, ['int', 'float']) && !is_numeric($value) )
unset($cleared[$field]);
// {2} Si de type TEXT/VARCHAR et pas string, on retire le champ //
if( in_array($type, ['text', 'varchar']) && !is_string($value) )
unset($cleared[$field]);
}
/* (4) Si on a plus de champ, on retourne l'object courant */
if( count($cleared) == 0 )
return false;
/* [1] Rédaction de la clause UPDATE
=========================================================*/
/* (1) On initialise la requête */
$requestS = SQLBuilder::UPDATE($this->schema['table'])."\n";
/* [2] Rédaction de la clause SET
=========================================================*/
/* (0) On initialise les variables à "binder" */
$binded = [];
/* (1) On met tout les champs à modifier */
$requestS .= SQLBuilder::SET($cleared, $binded)."\n";
/* [3] On rédige la clause WHERE/AND
=========================================================*/
/* (1) On met les conditions locales */
$c = 0;
foreach($this->where as $field=>$conditions)
foreach($conditions as $cdt=>$value){
if( $value[1] == self::COND_IN ) // Si condition de type IN
$requestS .= SQLBuilder::IN([$this->schema['table'], $field], $value[0], $c, $binded)."\n";
else // Sinon
$requestS .= SQLBuilder::WHERE([$this->schema['table'], $field], $value, $c, $binded)."\n";
$c++;
}
/* (2) On ajoute les jointures */
foreach($this->joined as $localField=>$rows){
if( $c == 0 ) $requestS .= 'WHERE ';
else $requestS .= 'AND ';
// {1} Clause SELECT interne //
$requestS .= $this->schema['table'].'.'.$localField." = (\n\t";
// Jointure du SELECT, champ joint lié au champ local
$requestS .= SQLBuilder::SELECT([
$rows->schema['table'] => [ $this->schema['columns'][$localField]['references'][1] ]
])."\n";
// {2} Clause FROM interne //
$requestS .= "\t".SQLBuilder::FROM([$this->schema['columns'][$localField]['references'][0]])."\n";
// {3} Clause WHERE/AND interne //
$c2 = 0;
foreach($rows->where as $field=>$conditions)
foreach($conditions as $cdt=>$value){
if( $value[1] == self::COND_IN ) // Si condition de type IN
$requestS .= "\t".SQLBuilder::IN([$rows->schema['table'], $field], $value[0], $c2, $binded)."\n";
else // Sinon
$requestS .= "\t".SQLBuilder::WHERE([$rows->schema['table'], $field], $value, $c2, $binded)."\n";
$c2++;
}
$requestS .= "\tLIMIT 1)\n";
}
/* [4] On prépare la requête
=========================================================*/
/* (0) On prépare la requête */
$request = Database::getPDO()->prepare($requestS.';');
/* [5] On exécute la requête et retourne le résultat
=========================================================*/
/* (1) On exécute la requête */
$updated = $request->execute($binded);
/* (2) On retourne l'état de la requête */
return $updated;
}
/* AJOUTE UNE ENTREE DANS LA TABLE
*
* @entry<Array> Tableau associatif de la forme (colonne => valeur)
* OU
* @entries<Array> Tableau de la forme ([entry1, entry2])
*
* @return status<Boolean> Retourne si TRUE ou FALSE les entrées ont bien été supprimées
*
*/
public function insert($entry){
/* [0] On vérifie les paramètres
=========================================================*/
/* (1) Si c'est pas un tableau avec au moins une entrée, erreur */
if( !is_array($entry) || count($entry) == 0 )
return false;
// S'il n'y a qu'une entrée, on met au même format que s'il y en avait plusieurs
$firstIndex = array_keys($entry)[0];
if( !is_array($entry[$firstIndex]) )
$entry = [ $entry ];
/* (2) On retire les champ inconnus */
$cleared = [];
// Pour chaque entrée du tableau
foreach($entry as $i=>$set){
$cleared[$i] = [];
foreach($set as $field=>$value){
if( isset($this->schema['columns'][$field]) ) // Champ existe
$cleared[$i][$field] = $value;
}
}
/* (3) On vérifie les types des champs */
foreach($cleared as $i=>$set){
foreach($set as $field=>$value){
$type = $this->schema['columns'][$field]['type'];
// {1} Si de type INT/FLOAT et pas numérique, on retire le champ //
if( in_array($type, ['int', 'float']) && !is_numeric($value) && $value != self::DEFAULT )
unset($cleared[$i][$field]);
// {2} Si de type TEXT/VARCHAR et pas string, on retire le champ //
if( in_array($type, ['text', 'varchar']) && !is_string($value) && $value != self::DEFAULT )
unset($cleared[$i][$field]);
}
/* (4) Si il manque des données, erreur */
if( count($cleared[$i]) != count($this->schema['columns']) )
return false;
}
/* [1] On crée la requête
=========================================================*/
/* (1) Clause INSERT INTO table */
$requestS = 'INSERT INTO '.$this->schema['table']."(";
/* (2) Clause : table(col1, col2, ...) */
$c = 0;
foreach($this->schema['columns'] as $field=>$value){
if( $c > 0 ) $requestS .= ', ';
$requestS .= $field;
$c++;
}
// Fin de clause
$requestS .= ")\n";
/* (3) Clause : VALUES(val1, val2, ...) */
$v = 0;
foreach($cleared as $i=>$set){
if( $v == 0 ) $requestS .= 'VALUES(';
else $requestS .= ",\n\t(";
$c = 0;
foreach($this->schema['columns'] as $field=>$column){
if( $c > 0 ) $requestS .= ', ';
// Si l'entrée est donnée
if( isset($set[$field]) )
if( $set[$field] == self::DEFAULT ) $requestS .= 'DEFAULT'; // On insère directement les valeurs 'DEFAULT'
else $requestS .= ':insert_'.$field.'_'.$i;
else
$requestS .= 'DEFAULT';
$c++;
}
// Fin de clause
$requestS .= ")";
$v++;
}
/* [2] On bind les paramètres et exécute la requête
=========================================================*/
/* (0) On initialise la requête et les paramètres */
$request = Database::getPDO()->prepare($requestS.';');
$binded = [];
/* (1) On bind les paramètres */
foreach($cleared as $i=>$set)
foreach($this->schema['columns'] as $field=>$column)
if( isset($set[$field]) && $set[$field] != self::DEFAULT )
$binded[':insert_'.$field.'_'.$i] = $set[$field];
/* [3] On exécute la requête et envoie le status
=========================================================*/
$inserted = $request->execute($binded);
// On retourne le status
return $inserted;
}
/* SUPPRIME LES ENTREES
*
* @return status<Boolean> Retourne si TRUE ou FALSE les entrées ont bien été supprimées
*
*/
public function delete(){
/* [1] Rédaction de la clause DELETE FROM
=========================================================*/
/* (1) On initialise la requête */
$requestS = "DELETE FROM ".$this->schema['table']."\n";
/* [2] On rédige la clause WHERE/AND
=========================================================*/
/* (0) On initialise les variables à "binder" */
$binded = [];
/* (1) On met les conditions locales */
$c = 0;
foreach($this->where as $field=>$conditions)
foreach($conditions as $cdt=>$value){
if( $value[1] == self::COND_IN ) // Si condition de type IN
$requestS .= SQLBuilder::IN([$this->schema['table'], $field], $value[0], $c, $binded)."\n";
else // Sinon
$requestS .= SQLBuilder::WHERE([$this->schema['table'], $field], $value, $c, $binded)."\n";
$c++;
}
/* (2) On ajoute les jointures */
foreach($this->joined as $localField=>$rows){
if( $c == 0 ) $requestS .= 'WHERE ';
else $requestS .= 'AND ';
// {1} Clause SELECT interne //
$requestS .= $this->schema['table'].'.'.$localField." = (\n\t";
// Jointure du SELECT, champ joint lié au champ local
$requestS .= SQLBuilder::SELECT([
$rows->schema['table'] => [ $this->schema['columns'][$localField]['references'][1] ]
])."\n";
// {2} Clause FROM interne //
$requestS .= "\t".SQLBuilder::FROM([$this->schema['columns'][$localField]['references'][0]])."\n";
// {3} Clause WHERE/AND interne //
$c2 = 0;
foreach($rows->where as $field=>$conditions)
foreach($conditions as $cdt=>$value){
if( $value[1] == self::COND_IN ) // Si condition de type IN
$requestS .= "\t".SQLBuilder::IN([$rows->schema['table'], $field], $value[0], $c2, $binded)."\n";
else // Sinon
$requestS .= "\t".SQLBuilder::WHERE([$rows->schema['table'], $field], $value, $c2, $binded)."\n";
$c2++;
}
$requestS .= "\tLIMIT 1)\n";
}
/* [3] On prépare la requête
=========================================================*/
/* (0) On prépare la requête */
$request = Database::getPDO()->prepare($requestS.';');
/* [4] On exécute la requête et retourne le résultat
=========================================================*/
/* (1) On exécute la requête */
$deleted = $request->execute($binded);
/* (2) On retourne l'état de la requête */
return $deleted;
}
/* RETOURNE LES DONNEES / NULL si une erreur survient
*
* @unique<Boolean> VRAI si on veut un seul résultat (itérateur)
*
* @return data<Array> Tableau contenant les champs sélectionnés
* @return data<mixed> Valeur du champ sélectionné (si 1 seul champ)
* @return ERROR<FALSE> Retourne FALSE si rien n'est trouvé
*
*/
public function fetch($unique=null){
/* [0] Initialisation des paramètres
=========================================================*/
$unique = !is_bool($unique) ? $this->unique : $unique;
/* [1] On rédige la clause SELECT
=========================================================*/
/* (1) On formatte les données */
$selectTables = [];
/* (2) On ajoute les champs locaux */
$selectTables[$this->schema['table']] = $this->select;
/* (3) On ajoute les champs des jointures */
foreach($this->joined as $rows)
$selectTables[$rows->schema['table']] = $rows->select;
/* (4) On génère la clause SELECT */
$requestS = SQLBuilder::SELECT($selectTables)."\n";
/* [2] On rédige la clause FROM
========================================================*/
/* (1) Table locale */
$requestS .= SQLBuilder::FROM( array_keys($selectTables) )."\n";
/* [5] On rédige la clause WHERE/AND
=========================================================*/
/* (0) On initialise le conteneur des variables "bindés" */
$binded = [];
/* (1) On met les conditions locales */
$c = 0;
foreach($this->where as $field=>$conditions)
foreach($conditions as $cdt=>$value){
if( $value[1] === self::COND_IN ) // Si condition IN
$requestS .= SQLBuilder::IN([$this->schema['table'], $field], $value[0], $c, $binded)."\n";
else // Sinon
$requestS .= SQLBuilder::WHERE([$this->schema['table'], $field], $value, $c, $binded)."\n";
$c++;
}
/* (2) On ajoute les jointures */
foreach($this->joined as $localField=>$rows){
if( $c == 0 ) $requestS .= 'WHERE '.$this->schema['table'].'.'.$localField.' = '.$this->schema['columns'][$localField]['references'][0].'.'.$this->schema['columns'][$localField]['references'][1]."\n";
else $requestS .= 'AND '.$this->schema['table'].'.'.$localField.' = '.$this->schema['columns'][$localField]['references'][0].'.'.$this->schema['columns'][$localField]['references'][1]."\n";
$c++;
}
/* (3) On ajoute les conditions des jointures */
foreach($this->joined as $rows)
foreach($rows->where as $field=>$conditions)
foreach($conditions as $cdt=>$value){
if( $value[1] === self::COND_IN ) // Si condition IN
$requestS .= SQLBuilder::IN([$rows->schema['table'], $field], $value[0], $c, $binded)."\n";
else // Sinon
$requestS .= SQLBuilder::WHERE([$rows->schema['table'], $field], $value, $c, $binded)."\n";
$c++;
}
/* [6] On exécute la requête
=========================================================*/
/* (0) On prépare la requête */
$request = Database::getPDO()->prepare($requestS.';');
/* [7] On exécute la requête et retourne le résultat
=========================================================*/
/* (1) On exécute la requête */
$request->execute($binded);
/* (2) Si unique */
if( $unique )
return $this->format( $request->fetch() );
/* (3) Si tout */
return $this->format( $request->fetchAll() );
}
/* ON FORMATTE LES DONNEES DE SORTIE
*
* @data<Array> Données / Tableau de données
*
* @return formatted<Array> Données formattées / Tableau de données formatté
*
*/
private function format($data){
/* [0] On initialise le processus
=========================================================*/
/* (0) Initialisation du retour */
$formatted = $data;
/* (1) On vérifie qu'il s'agit d'un tableau (non vide) */
if( !is_array($formatted) || count($formatted) < 1 )
return $formatted;
/* (2) On regarde si c'est des données simples */
$twoDimensions = is_array($formatted[0]);
/* (3) On regarde s'il s'agit d'un Tableau de données en bonne et due forme */
if( $twoDimensions ){
$sameKeys = true; // VRAI si chaque entrée a les mêmes clés
$last_keys = null; // Clés de l'entrée précédente
foreach($formatted as $i=>$entry){
if( !is_null($last_keys) && count(array_diff(array_keys($entry), $last_keys)) > 0 ){ // Si différent du précédent, ducoup on est pas bon
$sameKeys = false;
break;
}
$last_keys = array_keys($entry);
}
// Si pas les mêmes clés, on a une erreur
if( !$sameKeys )
return $formatted;
}
/* [1] On retire les doublons à indices numériques
=========================================================*/
/* (1) Si 1 dimensions, on met en 2 pour traiter tout de la même manière */
if( !$twoDimensions )
$formatted = [$formatted];
/* (2) On retire les indices numériques */
// {1} On récupère les colonnes locales //
$existingColumns = $this->schema['columns'];
// {2} On ajoute les colonnes des jointures //
foreach($this->joined as $rows)
$existingColumns = array_merge( $existingColumns, $rows->schema['columns'] );
// {3} On vérifie chaque clé, si c'est une colonne qui existe //
foreach($formatted as $i=>$entry)
// Pour chaque champ
foreach($entry as $index=>$value)
// Si la colonne existe on applique le type
if( isset($existingColumns[$index]) ){
if( $existingColumns[$index]['type'] == 'int' )
$formatted[$i][$index] = intval( $value );
else if( $existingColumns[$index]['type'] == 'float' )
$formatted[$i][$index] = floatval( $value );
}else // Si pas dans le schéma, on le retire
unset($formatted[$i][$index]);
/* (3) On remet 1 dimension si 1 dimension à la base */
if( !$twoDimensions )
$formatted = $formatted[0];
/* [2] On retourne le résultat
=========================================================*/
return $formatted;
}
}
?>