commit f40dcd4adeb9603aab2ae2d8896669e00feeaee0 Author: xdrm-brackets Date: Tue Oct 18 19:29:25 2016 +0200 Création de la structure de base et de la config diff --git a/exporter/modules.json b/exporter/modules.json new file mode 100644 index 0000000..1a1253e --- /dev/null +++ b/exporter/modules.json @@ -0,0 +1,44 @@ +{ + + "available": { + + "error": { + "versions": [1] + }, + + "api": { + "versions": [1], + "dependencies": [ + ["error", 1] + ] + }, + + "orm": { + "versions": [0.8], + "dependencies": [ + ["database", 1] + ] + }, + + "database": { + "versions": [1], + "dependencies": [ + ["error", 1] + ] + }, + + "lightdb": { + "versions": [1] + }, + + "router": { + "versions": [1] + } + + }, + + "enabled": { + + } + +} diff --git a/src/config/api/1/modules.json b/src/config/api/1/modules.json new file mode 100755 index 0000000..1072af5 --- /dev/null +++ b/src/config/api/1/modules.json @@ -0,0 +1,66 @@ +{ + "module": { + "method": { + "description": "Test de l'API", + "permissions": [], + "parameters": {} + }, + + "phpunitParams": { + "description": "Méthode utile à phpunit pour le test des paramètres.", + "permissions": [], + "parameters": { + "p1": { "description": "Texte", "type": "text" }, + "p2": { "description": "Entier positif", "type": "id" } + }, + "output": { + "receivedArguments": { "description": "Liste des arguments reçus par la méthode", "type": "array" } + } + }, + + "phpunitOptionalParams": { + "description": "Méthode utile à phpunit pour le test des paramètres optionnels.", + "permissions": [], + "parameters": { + "p1": { "description": "Texte", "type": "text" }, + "p2": { "description": "Texte", "type": "text", "optional": false }, + "p3": { "description": "Entier positif (optionnel)", "type": "id", "optional": true } + } + }, + + "phpunitPermissions": { + "description": "Méthode utile à phpunit pour le test des permissions.", + "permissions": ["a", "b"], + "parameters": {} + }, + + + "markdown": { + "description": "Retourne une description en markdown des différents modules de l'API", + "permissions": [], + "options": { "download": true }, + "parameters": {} + }, + + + "apiBlueprint": { + "description": "Retourne une documentation de l'API au format API Blueprint.", + "permissions": [], + "options": { "download": true }, + "parameters": {} + } + }, + + "download": { + + "multiple": { + "description": "Download la carte d'un utilisateur.", + "permissions": ["admin"], + "options": { "download": true }, + "parameters": { + "id_user": { "description": "Identifiant de l'utilisateur.", "type": "id" } + } + } + + } +} diff --git a/src/config/database/1/database-local.json b/src/config/database/1/database-local.json new file mode 100755 index 0000000..f12ff29 --- /dev/null +++ b/src/config/database/1/database-local.json @@ -0,0 +1,6 @@ +{ + "host" : "your-local-host", + "dbname" : "your-local-database", + "user" : "your-local-user", + "password" : "your-local-password" +} diff --git a/src/config/database/1/database.json b/src/config/database/1/database.json new file mode 100755 index 0000000..e3f658e --- /dev/null +++ b/src/config/database/1/database.json @@ -0,0 +1,6 @@ +{ + "host" : "your-remote-host", + "dbname" : "your-remote-database", + "user" : "your-remote-user", + "password" : "your-remote-password" +} diff --git a/src/config/database/1/repositories.json b/src/config/database/1/repositories.json new file mode 100755 index 0000000..6c92aef --- /dev/null +++ b/src/config/database/1/repositories.json @@ -0,0 +1,12 @@ +{ + + "user": [ + "login", + "getById", + "getAll", + "checkUnique", + "create", + "remove" + ] + +} diff --git a/src/modules/api/1/core/Authentification.php b/src/modules/api/1/core/Authentification.php new file mode 100644 index 0000000..492274b --- /dev/null +++ b/src/modules/api/1/core/Authentification.php @@ -0,0 +1,66 @@ + Liste des permissions attendues + * + * @return status Si FALSE, pas la permission, sinon si + * + */ + public static function permission($expected){ + + } + + + + + + /* RENVOIE LE NIVEAU D'AUTHENTIFICATION + * + * @return auth Niveau d'authentification (0 à 2) + * + */ + public static function auth(){ + + } + + } + +?> diff --git a/src/modules/api/1/core/Checker.php b/src/modules/api/1/core/Checker.php new file mode 100644 index 0000000..a149e36 --- /dev/null +++ b/src/modules/api/1/core/Checker.php @@ -0,0 +1,146 @@ + Type que l'on veut verifier + * @value Valeur a verifier + * + * @return match Retourne si oui ou non la valeur @value est du bon type @type + * + */ + public static function run($type, $value){ + $checker = true; + + /* [0] On verifie que $value n'est pas nul + =========================================================*/ + if( is_null($value) ) return false; + + + + /* [1] Si de type VARCHAR(min, max, flags) + =========================================================*/ + if( preg_match('/^varchar\((\d+), ?(\d+)((?:, ?\w+)+)?\)$/', $type, $match) ){ + // On recupere la taille min + $min = (int) $match[1]; + // On recupere la taille max + $max = (int) $match[2]; + + // On recupere le sous-type si défini + $flags = isset($match[3]) ? explode(',', substr($match[3], 1)) : null; + + // On effectue la verification de taille + $lenCheck = $checker && is_string($value) && strlen($value) <= $max && strlen($value) >= $min; + + // On vérifie les FLAGS s'il est donné + if( is_array($flags) ) + foreach( $flags as $flag ) + $lenCheck = $lenCheck && self::run($flag, $value); + + return $lenCheck; + } + + + /* [2] Si de type ARRAY(type_elements) + =========================================================*/ + if( preg_match('/^array<(.+)>$/', $type, $match) ){ + + // Si c'est pas un tableau on retourne une erreur + if( !is_array($value) ) + return false; + + + $elements_type = $match[1]; + + // On verifie le type pour chaque element + foreach($value as $element) + // Si erreur dans au moins 1 element, on retourne que c'est incorrect + if( !self::run($elements_type, trim($element) ) ) + return false; + + // Si aucune erreur, on retourne que tout est bon + return true; + } + + + /* [n] Sinon, tous les autres types definis + =========================================================*/ + switch($type){ + // Quoi que ce soit + case 'mixed': + return $checker && !is_null($value); + break; + + // Entier positif (id dans BDD) + case 'id': + return $checker && is_numeric($value) && $value <= 2147483647 && $value >= 0; + break; + + // Code RFID + case 'rfid': + return $checker && is_string($value) && preg_match('/^[\dA-F]{2}(\-[\dA-F]{2}){3,5}$/i', $value); + break; + + // String quelconque (peut etre vide) + case 'text': + return $checker && is_string($value); + + // Adresse mail (255 caracteres max) + case 'mail': + return $checker && is_string($value) && strlen($value) <= 50 && preg_match('/^[\w\.-]+@[\w\.-]+\.[a-z]{2,4}$/i', $value); + break; + + // Hash sha1/md5 + case 'hash': + return $checker && is_string($value) && preg_match('/^[\da-f]+$/i', $value) && (strlen($value) == 40 || strlen($value) == 64); + break; + + case 'alphanumeric': + return $checker && is_string($value) && preg_match('/^[\w\.-]+$/ui', $value); + break; + + case 'letters': + return $checker && is_string($value) && preg_match('/^[a-z -]+$/i', $value); + break; + + case 'status': + return $checker && is_numeric($value) && floor($value) == $value && $value >= 0 && $value <= 100; + break; + + // Tableau non vide + case 'array': + return $checker && is_array($value) && count($value) > 0; + break; + + // Boolean + case 'boolean': + return $checker && is_bool($value); + break; + + // Objet non vide + case 'object': + return $checker && is_object($value) && count((array) $value) > 0; + break; + + // Chaine JSON (on vérifie via le parser) + case 'json': + return $checker && is_string($value) && json_decode($value, true) !== NULL; + break; + + default: + return false; + break; + } + + return $checker; + + } + + + } +?> diff --git a/src/modules/api/1/core/ModuleRequest.php b/src/modules/api/1/core/ModuleRequest.php new file mode 100755 index 0000000..052d7c5 --- /dev/null +++ b/src/modules/api/1/core/ModuleRequest.php @@ -0,0 +1,533 @@ + false + ]; + + // Attributs prives utiles (initialisation) + private $path; + private $params; + private $modules; + private $options; + + // Contiendra la reponse a la requete + public $answer; + + // Contiendra l'etat de la requete + public $error; + + + + + + /* CONSTRUCTEUR D'UNE REQUETE DE MODULE + * + * @path Chemin de delegation ("module/methode") + * @param Tableau associatif contenant les parametres utiles au traitement + * + * @return status Retourne si oui ou non tout s'est bien passe + * + */ + public function __construct($path=null, $params=null){ + // Si pas parametre manquant, on quitte + if( $path == null ){ + $this->error = Error::MissingPath; + return false; + } + + /* [0] On met a jour la configuration + =========================================================*/ + // Modules specifies + $this->modules = json_decode( file_get_contents(self::config_path()), true ); + + // Gestion de l'erreur de parsage + if( $this->modules == null ){ + $this->error = Error::ParsingFailed; + return false; + } + + + + /* [1] Verification des types des parametres + =========================================================*/ + // Type de @path + if( !is_string($path) ){ // Si le type est incorrect + $this->error = Error::WrongPathModule; + return false; // On retourne FALSE, si erreur + } + + // Type de @data (optionnel) + $params = (is_array($params)) ? $params : []; + + + /* [2] Verification du chemin (existence module+methode) + =========================================================*/ + if( !$this->checkPath($path) ) // Verification de la coherence du chemin + attribution + return false; + + + + /* [3] Verification des droits + =========================================================*/ + if( !$this->checkPermission() ) // Si on a pas les droits + return false; + + + /* [4] Verification des parametres (si @type est defini) + =========================================================*/ + if( !$this->checkParams($params) ){ // Verification de tous les types + $this->error = Error::ParamError; + return false; + } + + /* [5] Récupèration des options + =========================================================*/ + $this->buildOptions(); + + + /* [6] Construction de l'objet + =========================================================*/ + $this->params = $params; + $this->error = Error::Success; + + return true; // On retourne que tout s'est bien passe + + } + + + + /* EXECUTE LE TRAITEMENT ASSOCIE ET REMPLIE LA REPONSE + * + * @return answer Retourne une reponse de type si tout s'est bien passe + * + */ + public function dispatch(){ + /* [0] Si c'est un download, on lance la methode `download()` + =========================================================*/ + if( $this->options['download'] === true ) + return $this->download(); + + /* [1] On verifie qu'aucune erreur n'a ete signalee + =========================================================*/ + if( $this->error != Error::Success ) // si il y a une erreur + return new ModuleResponse($this->error); // on la passe a la reponse + + + /* [2] On verifie que la methode est amorcable + =========================================================*/ + if( !is_callable($this->getFunctionCaller()) ){ + $this->error = Error::UncallableMethod; + return new ModuleResponse($this->error); + } + + + /* [3] On amorce la methode + =========================================================*/ + $returned = call_user_func( $this->getFunctionCaller(), $this->params ); + + + /* [4] Gestion de la reponse + =========================================================*/ + $response = new ModuleResponse($this->error); + $response->appendAll($returned); + + return $response; + } + + + + + + + + + + /* EXECUTE LE TRAITEMENT ASSOCIE ET RENVOIE UN FICHIER AVEC LE HEADER ET LE BODY SPECIFIE + * + */ + public function download(){ + /* [1] On verifie qu'aucune erreur n'a ete signalee + =========================================================*/ + if( $this->error != Error::Success ) // si il y a une erreur + return new ModuleResponse($this->error); // on la passe a la reponse + + + /* [2] On verifie que la methode est amorcable + =========================================================*/ + if( !is_callable($this->getFunctionCaller()) ){ + $this->error = Error::UncallableMethod; + return new ModuleResponse($this->error); + } + + + /* [3] On amorce la methode + =========================================================*/ + $returned = call_user_func( $this->getFunctionCaller(), $this->params ); + + + /* [4] Vérification des erreurs et paramètres + =========================================================*/ + /* (1) Vérification de l'erreur retournée, si pas Success, on retourne l'erreur */ + if( isset($returned['ModuleError']) && $returned['ModuleError'] != Error::Success ){ + $this->error = $returned['ModuleError']; + return new ModuleResponse($this->error); + } + + /* (2) Vérification du contenu, si pas défini */ + if( !isset($returned['body']) ){ + $this->error = Error::ParamError; + return new ModuleResponse($this->error); + } + + /* (3) Si @headers n'est pas défini on met par défaut */ + if( !isset($returned['headers']) || !is_array($returned['headers']) ) + $returned['headers'] = []; + + + $fromAjax = isset($_SERVER['HTTP_X_REQUESTED_WITH']) && strtolower($_SERVER['HTTP_X_REQUESTED_WITH']) == 'xmlhttprequest'; + + /* [5] Si la requête vient d'ajax on crée un fichier temporaire et on renvoie son URL + =========================================================*/ + if( $fromAjax ){ + + + $tmpfname = '/tmp/download_'.uniqid().'.php'; + $bodyfname = __ROOT__.'/tmp/content_'.uniqid().'.php'; + + /* (1) On crée le fichier temporaire */ + $tmpfnameroot = __ROOT__.$tmpfname; + $tmpfile = fopen($tmpfnameroot, 'w'); + + fwrite($tmpfile, '$value) + fwrite($tmpfile, "header(\"$header: $value\");".PHP_EOL); + + /* (3) Script qui écrira le contenu */ + // 1) On écrit le contenu dans un fichier temporaire (et oui encore) + $bodyfile = fopen($bodyfname, 'w'); + fwrite($bodyfile, $returned['body']); + fclose($bodyfile); + chmod($bodyfname, 0775); + + fwrite($tmpfile, "readfile('$bodyfname');".PHP_EOL); + + /* (4) Script qui supprimera les fichiers temporaires */ + fwrite($tmpfile, "unlink('$bodyfname');".PHP_EOL); + fwrite($tmpfile, "unlink(__FILE__);".PHP_EOL); + + fwrite($tmpfile, '?>'.PHP_EOL); + + /* (5) On ferme le fichier */ + fclose($tmpfile); + chmod($tmpfnameroot, 0775); + + $response = new ModuleResponse(Error::Success); + $response->append('link', $tmpfname); + + return $response; + + /* [6] Gestion du download direct si pas AJAX + =========================================================*/ + }else{ + /* (1) On définit les headers */ + foreach($returned['headers'] as $header=>$value) + header($header.': '.$value); + + /* (2) On affiche le contenu */ + echo $returned['body']; + + return true; + } + } + + + /* DESERIALISATION A PARTIR DES DONNEES POST + * + * @url Contenu de l'url après api/ (si existe) + * @post Tableau des donnes $_POST => @path + @data (opt) + * + * @return instance Retourne un objet de type + * + */ + public static function fromPost($url, $post){ + + /* [1] On verifie que le @path est renseigne + =========================================================*/ + /* (1) Si le path est dans @url */ + $pathInUrl = count($url) > 0 && is_string($url[0]) && strlen($url[0]) > 0 && preg_match('#^([\w_-]+)/([\w_-]+)/?$#', $url[0], $urlMatches); + + // On l'utilise pour le chemin + if( $pathInUrl ) + $post['path'] = $urlMatches[1].'/'.$urlMatches[2]; + + /* (2) On vérifie dans tous les cas si le path existe */ + if( !isset($post['path']) ) + return new ModuleRequest(); + + + + /* [2] On verifie que @data est renseigne + =========================================================*/ + // Si variable n'existe pas, on cree un tableau vide + $params = $post; + + // On retire le @path de @params + unset($params['path']); + + + + /* [3] On met les paramètres JSON en JSON (si ils décodent sans erreur) + =========================================================*/ + foreach($params as $name=>$value){ + $json = json_decode( $value, true ); + // Si aucune erreur, on affecte la valeur + if( !is_null($json) ) + $params[$name] = $json; + } + /* [4] On retourne une instance de + =========================================================*/ + // On cree notre requete avec le token + return new ModuleRequest($post['path'], $params); + } + + + + + + /* VERIFICATION DU FORMAT ET DE LA COHERENCE DU CHEMIN SPECIFIE + * + * @path String correspondant au chemin de delegation ("module/methode") + * + * @return validity Retourne si oui ou non l'objet est correct + * + */ + private function checkPath($path){ + /* [1] Verification format general + =========================================================*/ + if( !preg_match('#^([\w_-]+)/([\w_-]+)$#i', $path, $matches) ){ // Si mauvais format + $this->error = Error::WrongPathModule; + return false; + } + + // On recupere les donnes de la regex + $module = $matches[1]; + $method = $matches[2]; + + + /* [2] Verification de l'existence du module (conf) + =========================================================*/ + if( !array_key_exists($module, $this->modules) ){ // Si le module n'est pas specifie dans la conf + $this->error = Error::UnknownModule; + return false; // On retourne FALSE, si erreur + } + + + /* [3] Verification de l'existence de la methode (conf) + =========================================================*/ + if( array_key_exists($method, $this->modules[$module]) === false ){ // Si la methode n'est pas specifie dans la conf + $this->error = Error::UnknownMethod; + return false; // On retourne FALSE, si erreur + } + + + + /* [4] Enregistrement du chemin et renvoi de SUCCESS + =========================================================*/ + $this->path = [ + 'module' => $module, + 'method' => $method + ]; + + return true; + } + + + + + + + /* RETOURNE SI ON A LA PERMISSION D'EXECUTER CETTE METHODE + * + * @return permission Retourne si on a les droits ou pas pour executer cette methode + * + */ + private function checkPermission(){ + /* [1] On recupere les informations utiles + =========================================================*/ + // On recupere le nom de la methode + $method = $this->modules[$this->path['module']][$this->path['method']]; + + // Si aucune permission n'est definie + if( !isset($method['permissions']) ) return true; + + + + /* [2] Vérification des permissions et de l'authentification + =========================================================*/ + $granted = Authentification::permission($method['permissions']); + + /* (1) On retourne FAUX si aucun droit n'a ete trouve */ + if( !$granted ){ + $this->error = Error::PermissionError; + return false; + } + + + /* [3] On vérifie que le module est autorisé + =========================================================*/ + $moduleName = $this->path['module']; + + /* (1) On vérifie que le module est actif dans l'entrepot */ + $allowedModule = isset($_SESSION['WAREHOUSE']['modules']) + && is_array($_SESSION['WAREHOUSE']['modules']) + && in_array($moduleName, $_SESSION['WAREHOUSE']['modules']); + + /* (2) On vérifie si le module est un module "Default" */ + $defaultModule = preg_match('/^(\w+)Default$/', $moduleName); + + /* (3) Si aucune autorisation et pas module "Default" */ + if( !$allowedModule && !$defaultModule ){ + $this->error = Error::DisabledModule; + return false; // On retourne FALSE, si erreur + } + + + + /* On retourne VRAI si la permission est ok */ + return true; + } + + + + + /* VERIFICATION DU TYPE DES PARAMETRES ENVOYES + * + * @params Tableau associatif contenant les parametres + * @params peut se voir rajouter les paramètres optionnels s'ils ne sont pas renseignés (initialisés à NULL) + * + * @return correct Retourne si oui ou non les parametres ont le bon type + * + */ + private function checkParams(&$params){ + /* [1] On verifie qu'il ne manque aucun parametre + =========================================================*/ + // Si @params n'est pas un tableau + if( !is_array($params) ) return false; + + $method = $this->modules[$this->path['module']][$this->path['method']]; + + + /* [2] Si le type est defini, pour chaque param, on teste + =========================================================*/ + foreach($method['parameters'] as $name=>$paramsdata){ + /* (1) On récupère si le paramètre est optionnel ou pas */ + $optional = isset($paramsdata['optional']) && $paramsdata['optional'] === true; + + /* (2) Récupère si le paramètre est un fichier et définit comme de type 'FILE' */ + $isFile = isset($paramsdata['type']) && $paramsdata['type'] == 'FILE' && isset($_FILES[$name]); + + /* (3) Si le paramètre est obligatoire et qu'il n'est pas donné -> erreur */ + if( !isset($params[$name]) && !$optional && !$isFile ) + return false; + + /* (4) Si le type n'est pas defini, on a pas besoin de le vérifier */ + if( !isset($paramsdata['type']) ) + continue; + + /* (5) Si le paramètre est optionnel et n'est pas donné */ + if( $isFile || $optional && (!isset($params[$name]) || is_null($params[$name])) ){ + // On le crée le param optionnel avec la valeur NULL + $params[$name] = null; + + // On donne une référence vers le fichier, si c'en est un + if( $isFile ) + $params[$name] = &$_FILES[$name]; + + continue; // On passe au paramètre suivant + + + /* (6) Si le paramètre est renseigné */ + }else + // Si la verification est fausse, on retourne faux + if( !Checker::run($paramsdata['type'], $params[$name]) ) + return false; + + } + + /* [3] Gestion du retour, si tout s'est bien passe + =========================================================*/ + return true; + } + + + + + + /* AJOUT DES OPTIONS A PARTIR DE LA CONFIGURATION + * + */ + private function buildOptions(){ + /* [0] On récupère les options de la méthode en cours + =========================================================*/ + $method = $this->modules[$this->path['module']][$this->path['method']]; + + /* (1) Si 'option' n'est pas défini (ou incorrect), on met les valeurs par défaut */ + if( !isset($method['options']) || !is_array($method['options']) ) + return true; + + /* (2) Par défaut on définit les options par défaut */ + $this->options = self::$default_options; + + + /* (3) On récupère les options données */ + $options = $method['options']; + + + /* [1] Gestion des différentes options + =========================================================*/ + foreach($options as $option=>$value){ + /* (1) On ne prend en compte l'option que si elle est dans les options par défaut */ + if( !isset(self::$default_options[$option]) ) + continue; + + /* (2) Le type de la valeur doit être le même que celui de la valeur par défaut */ + if( gettype($value) != gettype(self::$default_options[$option]) ) + continue; + + /* (3) Si tout est bon, on définit la valeur */ + $this->options[$option] = $value; + } + + return true; + + } + + + + + + /* RENVOI LE CHEMIN D'AMORCAGE DE LA METHODE + * + * @return path Retourne le chemin d'amorcage de la requete + * + */ + private function getFunctionCaller(){ + return [ '\\api\\module\\'.$this->path['module'], $this->path['method'] ]; + } + + + } + +?> diff --git a/src/modules/api/1/core/ModuleResponse.php b/src/modules/api/1/core/ModuleResponse.php new file mode 100755 index 0000000..bfd19d2 --- /dev/null +++ b/src/modules/api/1/core/ModuleResponse.php @@ -0,0 +1,121 @@ + Erreur passee par la requete (si existe) + * + */ + public function __construct($error=Error::Success){ + $this->data = []; + $this->error = $error; + } + + + /* AJOUTE UNE DONNEE A LA REPONSE + * + * @key Le nom de la valeur a ajouter + * @value La valeur a ajouter + * + */ + public function append($key, $value){ + // Ajoute une entree pour la cle @key et de valeur @value + $this->data[$key] = $value; + + return $this; + } + + + /* AJOUTE TOUTES LES DONNEES A LA REPONSE + * + * @dataset Le tableau associatif correspondant a la reponse + * + */ + public function appendAll($dataset){ + // Si ce n'est pas un tableau, on ne fais rien + if( !is_array($dataset) ) return $this; + + // Si une valeur contient une erreur + if( array_key_exists('ModuleError', $dataset) ){ + // On definit cette erreur + $this->error = $dataset['ModuleError']; + // On enleve cette entree des donnees + unset($dataset['ModuleError']); + } + + // Ajoute une entree pour la cle @key et de valeur @value + $this->data = $dataset; + + return $this; + } + + + /* RECUPERE UNE DONNEE DE LA REPONSE + * + * @key Le nom de la valeur a recuperer + * + * @return value La valeur a cette cle + * @return error Retourne NULL si aucune valeur pour cette cle + * + */ + public function get($key){ + // Si la valeur de cle @key n'existe pas, on retourne NULL + if( !isset($this->data[$key]) ) + return null; + + // Sinon, on retourne la valeur associee + return $this->data[$key]; + } + + + /* RECUPERE TOUTES LES DONNEES DE LA REPONSE + * + * @return data Les donnees de la reponse + * + */ + public function getAll(){ + // Sinon, on retourne la valeur associee + return $this->data; + } + + + /* SERIALISATION A PARTIR DES DONNEES + * + * @return json Retourne les donnees serialisees + * + */ + public function serialize(){ + + // Code Http + Error::setHttpCode($this->error); + + // Type de contenu + // header('Content-Type: application/json; charset=utf-8'); + + // On rajoute l'erreur au message + $returnData = array_merge([ + 'ModuleError' => $this->error, + 'ErrorDescription' => Error::explicit($this->error) + ], + $this->data + ); + + return json_encode($returnData); + + } + + + + } + +?> diff --git a/src/modules/api/1/module/module.php b/src/modules/api/1/module/module.php new file mode 100644 index 0000000..f022922 --- /dev/null +++ b/src/modules/api/1/module/module.php @@ -0,0 +1,275 @@ + Error::Success, + 'ReceivedArguments' => $params + ]; + + } + + /* RENVOIE UNE DESCRIPTION EN MARKDOWN DES MODULES DE L'API + * + * @return markdown Description des modules + * + */ + public static function markdown(){ + /* [1] Récupération de la configuration + =========================================================*/ + // On récupère le fichier et on le parse + $modules = json_decode( file_get_contents(__ROOT__/'/config/modules.json'), true ); + + // Gestion de l'erreur de parsage + if( $modules == null ) + return [ 'ModuleError' => Error::ParsingFailed ]; + + /* [2] Mise en forme de la liste des modules + =========================================================*/ + $markdown = "## Module List\n"; + + foreach($modules as $moduleName=>$moduleData) + $markdown .= "- $moduleName\n"; + + /* [3] Mise en forme des méthodes des modules + =========================================================*/ + $markdown .= "----\n## Method List & Description\n"; + + $count = 1; + foreach($modules as $moduleName=>$moduleData){ + $markdown .= "### $count - '$moduleName' methods\n"; + + foreach($moduleData as $methodName=>$methodData) + $markdown .= "`$methodName` - ".$methodData['description']."\n"; + + $markdown .= "----\n"; + + $count++; + } + + + + /* [n] Gestion du retour + =========================================================*/ + return [ + 'ModuleError' => Error::Success, + 'headers' => [ + 'Content-Type' => 'text/markdown; charset=utf-8', + 'Content-Transfer-Encoding' => 'binary', + 'Content-Disposition' => 'attachment; filename=NxTIC.apib', + 'Pragma' => 'no-cache', + 'Expires' => '0' + ], + 'body' => $markdown + ]; + } + + + /* RENVOIE UNE DOC API_BLUEPRINT DE L'API + * + * @return apiBlueprint Description des modules au format API Blueprint + * + */ + public static function apiBlueprint(){ + /* [0] Récupération de la configuration + =========================================================*/ + // On récupère le fichier et on le parse + $modules = json_decode( file_get_contents(__ROOT__/'/config/modules.json'), true ); + + // Gestion de l'erreur de parsage + if( $modules == null ) + return [ 'ModuleError' => Error::ParsingFailed ]; + + + /* [1] Début du fichier custom + =========================================================*/ + $content = "FORMAT: 1A\n"; + $content .= "HOST: https://socioview.xdrm.io/api/\n\n"; + + $content .= "# NxTIC API\n"; + $content .= "API de la plateforme d'étude **NxTIC**, cette documentation présentera toutes les méthodes accessibles depuis la plateforme elle-même et depuis un logiciel tiers.\n"; + $content .= "La plateforme **NxTIC** est une plateforme d'étude sociale développé par Adrien Marquès _(xdrm-brackets)_ pour un laboratoire de sociologie du _CNRS_.\n"; + $content .= "Elle a pour objectif l'acquisition, la visualisation et l'extraction de données relationnelles.\n"; + $content .= "> Cette plateforme est temporairement hébergée sur https://socioview.xdrm.io/.\n\n"; + + $content .= "## Structure et fonctionnement\n"; + $content .= "Le fonctionnement est basé sur une délégation à 2 niveaux : des __modules__ contenant des __méthodes__.\n\n"; + + $content .= "***\n\n"; + + $content .= "### Paramètres\n"; + $content .= "Tous les paramètres doivent être envoyés en `multipart/form-data`.\n\n"; + + $content .= "1. Chacun formatté en `json` \n"; + $content .= "2. Portant le `nom` défini dans la documentation \n"; + $content .= "3. L'ordre n'a pas d'importance \n"; + $content .= "4. Respectant le `type` défini dans la documentation (cf. [Types](#introduction/types-de-donnees)) \n\n"; + + $content .= "> **Note:** Les `paramètres URI` ne correspondent pas aux paramètres URI. \n"; + $content .= "Ils servent à expliciter les paramètres et leurs types, et correspondent aux variables notées `{nomVar}` dans le corps de la requête.\n\n"; + + $content .= "### Réponses\n\n"; + + $content .= "#### A. Les réponses seront formattées en json et contiendront:\n\n"; + + $content .= "1. `ModuleError` - Le code de l'erreur \n"; + $content .= "2. `ErrorDescription` - La description de l'erreur\n\n"; + + $content .= "****\n\n"; + + $content .= "#### B. Codes `HTTP` et leur signification.\n\n"; + + $content .= "|Status|Code HTTP|\n"; + $content .= "|---|---|\n"; + $content .= "|OK|`200` - Success|\n"; + $content .= "|Erreur|`417` - Erreur quelconque|\n\n"; + + + $content .= "## Types de données\n\n"; + + $content .= "### Types Simples \n"; + $content .= "|Type|Exemple|Description|\n"; + $content .= "|---|---|---|\n"; + $content .= "|`mixed`|`[9,\"a\"]`, `\"a\"`|Type quelconque (peut être simple ou composé)|\n"; + $content .= "|`id`|`10`, `\"23\"`|Nombre entier positif compris entre `0` et `2147483647`|\n"; + $content .= "|`text`|`\"Hello!\"`|Chaine de caractères de longueur quelconque (peut être vide)|\n"; + $content .= "|`mail`|`\"a.b@c.def\"`|Adresse mail avec une syntaxe valide|\n"; + $content .= "|`number`|`0102030405`|Numéro de téléphone valide suivant les formats : `06`, `+336`, `+33 6`|\n"; + $content .= "|`array`|`[1, 3]`|Tableau quelconque non vide|\n"; + $content .= "|`boolean`|`true`, `false`|Booléen|\n"; + $content .= "|`varchar(a,b)`|`\"Hello!\"`|Chaine de caractères de taille comprise entre `a` et `b` (inclus)|\n\n"; + + + $content .= "### Type composé : array\n\n"; + + $content .= "|Type|Sous-Type|Description|\n"; + $content .= "|---|---|---|\n"; + $content .= "|`array`|`mixed`|Tableau contenant uniquement des données de type `mixed`|\n"; + $content .= "|`array`|`id`|Tableau contenant uniquement des données de type `id`|\n"; + $content .= "|`array`|`text`|Tableau contenant uniquement des données de type `text`|\n"; + $content .= "|`array`|`mail`|Tableau contenant uniquement des données de type `mail`|\n"; + $content .= "|`array`|`number`|Tableau contenant uniquement des données de type `number`|\n"; + $content .= "|`array`|`array`|Tableau contenant uniquement des données de type `array`|\n"; + $content .= "|`array`|`boolean`|Tableau contenant uniquement des données de type `boolean`|\n"; + $content .= "|`array`|`varchar(a,b)`|Tableau contenant uniquement des données de type `varchar(a,b)`|\n\n"; + + $content .= "> **Note:** Il est possible de chainer le type `array` autant de fois que nécessaire. \n"; + $content .= "**Ex.:** `array>` - Soit un tableau contenant des tableaux contenant exclusivement des données de type `id`.\n"; + + $content .= "\n\n\n\n\n"; + + + /* [2] Pour chaque module + =========================================================*/ + foreach($modules as $module=>$methods){ + + $content .= "## $module [/$module] \n\n"; + + /* [3] Pour chaque méthode + =========================================================*/ + foreach($methods as $methName=>$method){ + + /* (1) Description */ + $content .= "### $methName [POST /$module/$methName]\n\n"; + $content .= $method['description']."\n"; + if( count($method['permissions']) > 0) + $content .= '> Permissions `'.implode('``', $method['permissions'])."`\n\n"; + + // Liste des paramètres + if( isset($method['parameters']) && count($method['parameters']) > 0 ){ + // On explicite tous les paramètres + $content .= "+ Parameters\n\n"; + foreach($method['parameters'] as $argName=>$argument){ + $optional = isset($argument['optional']) && $argument['optional'] === true; + $content .= " + $argName (${argument['type']}, ".( $optional ? 'optional' : 'required' ).") - ${argument['description']}\n"; + } + $content .= "\n"; + } + + + /* (2) Requête */ + $content .= "+ Request (multipart/form-data; boundary=xxxBOUNDARYxxx)\n\n"; + + // Header + $content .= " + Headers\n\n"; + $content .= " Authorization: Digest {yourAccessToken}\n"; + $content .= " Cache-Control: no-cache\n"; + + if( isset($method['parameters']) && count($method['parameters']) > 0 ){ + + // Body + $content .= " + Body\n\n"; + foreach($method['parameters'] as $argName=>$argument){ + + $content .= " --xxxBOUNDARYxxx\n"; + $content .= " Content-Disposition: form-data; name=\"$argName\"\n"; + $content .= " Content-Type: application/json\n\n"; + $content .= " {".$argName."}\n"; + } + + $content .= " --xxxBOUNDARYxxx--\n"; + + // Schema + $content .= " + Schema\n\n"; + $content .= " {\n"; + foreach($method['parameters'] as $argName=>$argData) + $content .= " \"$argName\": @$argName\n"; + $content .= " }\n"; + } + + + /* (3) Réponse */ + $content .= "\n+ Response 200 (application/json)\n\n"; + if( isset($method['output']) && count($method['output']) > 0 ){ + + // Body + $content .= " + Body\n\n"; + $content .= " {\n"; + foreach($method['output'] as $outName=>$outData) + $content .= " \"$outName\": @$outName\n"; + $content .= " }\n"; + + // Schema + $content .= " + Schema\n\n"; + $content .= " {\n"; + foreach($method['output'] as $outName=>$outData) + $content .= " \"$outName\": @$outName\n"; + $content .= " }\n"; + + // On explicite tous les paramètres + $content .= " + Attributes (object)\n\n"; + foreach($method['output'] as $outName=>$outData) + $content .= " + $outName (${outData['type']}) - ${outData['description']}\n"; + } + + $content .= "\n\n"; + + } + + } + + + return [ + 'ModuleError' => Error::Success, + 'headers' => [ + 'Content-Type' => 'application/octet-stream; charset=utf-8', + 'Content-Transfer-Encoding' => 'binary', + 'Content-Disposition' => 'attachment; filename=NxTIC.apib', + 'Pragma' => 'no-cache', + 'Expires' => '0' + ], + 'body' => $content + ]; + } + } + + + +?> diff --git a/src/modules/database/1/core/Database.php b/src/modules/database/1/core/Database.php new file mode 100755 index 0000000..877d98a --- /dev/null +++ b/src/modules/database/1/core/Database.php @@ -0,0 +1,446 @@ + __ROOT__.'/config/database-local.json', + 'remote' => __ROOT__.'/config/database.json' + ]; + } + + private static $pdo; + private static $instance; + + + /* ATTRIBUTS */ + private $host; + private $dbname; + private $username; + private $password; + + public static $error; + + + public function __construct($host, $dbname, $username, $password){ + $this->host = $host; + $this->dbname = $dbname; + $this->username = $username; + $this->password = $password; + + try{ + self::$pdo = new \PDO('mysql:host='.$this->host.';dbname='.$this->dbname, $this->username, $this->password); + + // On signale que tout s'est bien passe + self::$error = Error::Success; + + }catch(Exception $e){ + // On signale qu'il y a une erreur + self::$error = Error::PDOConnection; + } + } + + + /* retourne une instance de la classe */ + public static function getInstance(){ + if( self::$instance == null || self::$error != Error::Success ){ // Si aucune instance existante OU erreur de connection + + // chargement de la configuration du server SQL + if( !checkdnsrr($_SERVER['SERVER_NAME'], 'NS') ) + $conf = json_decode( file_get_contents(self::config_path()['local']), true ); + else + $conf = json_decode( file_get_contents(self::config_path()['remote']), true ); + + // creation de l'instance en fonction des parametres + self::$instance = new DataBase($conf['host'], $conf['dbname'], $conf['user'], $conf['password']); + + } + + return self::$instance; + } + + /* retourne la connection statique */ + public static function getPDO(){ + $instance = self::getInstance(); + + return self::$pdo; + } + + + + + + + + + + public function getConfig(){ + return [ + 'host' => $this->host, + 'username' => $this->username + ]; + } + + + + + + + + /*************************************************************/ + /* _____ ______ _ _ ______ _____ _ */ + /* / ____| ____| \ | | ____| __ \ /\ | | */ + /* | | __| |__ | \| | |__ | |__) | / \ | | */ + /* | | |_ | __| | . ` | __| | _ / / /\ \ | | */ + /* | |__| | |____| |\ | |____| | \ \ / ____ \| |____ */ + /* \_____|______|_| \_|______|_| \_\/_/ \_\______| */ + /* */ + /*************************************************************/ + + /* SUPPRIME LES VALEURS À CLÉS NUMÉRIQUES DANS UN FETCH D'UNE TABLE DE LA BDD + * + * @fetchData le résultat d'une $requeteSQL->fetchAll() + * @oneDimension FAUX <=> fetchAll ; VRAI <=> fetch + * + * @return newFetchData retourne le tableau donné en paramètre mais sans les valeurs à clés numériques + * + */ + public static function delNumeric($fetchData, $oneDimension=false){ + // On quitte si ce n'est pas un tableau + if( !is_array($fetchData) ) + return []; + + $nextEquivalent = false; // Vaut VRAI si le prochain est peut-etre un equivalent numerique + + /* [1] 2 dimensions + ===============================================*/ + if( !$oneDimension && isset($fetchData[0]) && is_array($fetchData[0]) ){ + + // on supprime les doublons des entrées (indice numérique) + for( $i = 0 ; $i < count($fetchData) ; $i++ ) // pour toutes les lignes + foreach($fetchData[$i] as $col => $val){ // pour toutes les entrées + + if( !\mb_detect_encoding($val, 'UTF-8') ) + $fetchData[$i][$col] = utf8_encode($val); + + if( is_int($col) ){ // Si indice numerique + if( $nextEquivalent ) // Si suit un indice textuel + unset( $fetchData[$i][$col] ); // on supprime l'indice + + $nextEquivalent = false; // Dans tous les cas, on dit que le prochain ne pourra pas etre supprime si numerique + + }else // Si l'indice n'est pas un entier + $nextEquivalent = true; // On signale qu'il y aura peut etre un indice numerique suivant + + } + + /* [2] 1 dimensions + ===============================================*/ + }else{ + + // on supprime les doublons des entrées (indice numérique) + foreach($fetchData as $i=>$val){ // pour toutes les entrées + + if( !\mb_detect_encoding($val, 'UTF-8') ) + $fetchData[$i] = utf8_encode($val); + + if( is_int($i) ){ // Si indice numerique + if( $nextEquivalent ) // Si suit un indice textuel + unset( $fetchData[$i] ); // on supprime l'indice + + $nextEquivalent = false; // Dans tous les cas, on dit que le prochain ne pourra pas etre supprime si numerique + + }else // Si l'indice n'est pas un entier + $nextEquivalent = true; // On signale qu'il y aura peut etre un indice numerique suivant + + } + + } + + return $fetchData; + } + + + + + /* GESTION DE L'AUTO-TYPAGE D'UN TABLEAU A UN/PLUSIEURS NIVEAU(X) OU D'UNE VALEUR + * + * @data Tableau de données/Valeur + * + * @return autoTyped Tableau/Valeur auto-typé(e) + * + */ + public static function autotype($data){ + $autotyped = $data; + + /* [0] Gestion des types simples + =========================================================*/ + if( !is_array($autotyped) ){ + + /* (1) Si Numérique Entier -> INT */ + if( preg_match('/^(0|([1-9][0-9]*))$/i', $autotyped) ) + return intval($autotyped); + + /* (2) Si Numérique Décimal -> FLOAT */ + else if( preg_match('/^(0|(?:[1-9][0-9]*))[\.\,]([0-9]+)$/i', $autotyped, $m) ) + return floatval("${m[1]}.${m[2]}"); + + /* (3) Sinon on retourne la même chose */ + else + return $autotyped; + + /* [1] Gestion des types composés + =========================================================*/ + }else{ + + /* (1) Pour chaque valeur, on applique récursivement */ + foreach($autotyped as $k=>$v){ + + // Si on met '_nomColonne', on ne le traite pas + if( is_string($k) && $k[0] == '_' ) continue; + + $autotyped[$k] = self::autotype($v); + + } + + } + + + /* [2] On retourne le résultat auto-typé + =========================================================*/ + return $autotyped; + } + + + //////////////////////////////////////////////////////////////// + // _ __ _ _ _ + // __ _____ _ __(_)/ _(_) ___ __ _| |_(_) ___ _ __ ___ + // \ \ / / _ \ '__| | |_| |/ __/ _` | __| |/ _ \| '_ \/ __| + // \ V / __/ | | | _| | (_| (_| | |_| | (_) | | | \__ \ + // \_/ \___|_| |_|_| |_|\___\__,_|\__|_|\___/|_| |_|___/ + // + //////////////////////////////////////////////////////////////// + + + /* VERIFICATIONS DES TYPES UTILES GENERIQUES + * + * @type Type que l'on veut verifier + * @value Valeur a verifier + * + * @return match Retourne si oui ou non la valeur @value est du bon type @type + * + */ + public static function check($type, $value){ + $checker = true; + + /* [0] On verifie que $value n'est pas nul + =========================================================*/ + if( is_null($value) ) return false; + + + + /* [1] Si de type VARCHAR(min, max, flags) + =========================================================*/ + if( preg_match('/^varchar\((\d+), ?(\d+)((?:, ?\w+)+)?\)$/', $type, $match) ){ + // On recupere la taille min + $min = (int) $match[1]; + // On recupere la taille max + $max = (int) $match[2]; + + // On recupere le sous-type si défini + $flags = isset($match[3]) ? explode(',', substr($match[3], 1)) : null; + + // On effectue la verification de taille + $lenCheck = $checker && is_string($value) && strlen($value) <= $max && strlen($value) >= $min; + + // On vérifie les FLAGS s'il est donné + if( is_array($flags) ) + foreach( $flags as $flag ) + $lenCheck = $lenCheck && self::check($flag, $value); + + return $lenCheck; + } + + + /* [2] Si de type ARRAY(type_elements) + =========================================================*/ + if( preg_match('/^array<(.+)>$/', $type, $match) ){ + + // Si c'est pas un tableau on retourne une erreur + if( !is_array($value) ) + return false; + + + $elements_type = $match[1]; + + // On verifie le type pour chaque element + foreach($value as $element) + // Si erreur dans au moins 1 element, on retourne que c'est incorrect + if( !self::check($elements_type, trim($element) ) ) + return false; + + // Si aucune erreur, on retourne que tout est bon + return true; + } + + + /* [n] Sinon, tous les autres types definis + =========================================================*/ + switch($type){ + // Quoi que ce soit + case 'mixed': + return $checker && !is_null($value); + break; + + // Entier positif (id dans BDD) + case 'id': + return $checker && is_numeric($value) && $value <= 2147483647 && $value >= 0; + break; + + // Code RFID + case 'rfid': + return $checker && is_string($value) && preg_match('/^[\dA-F]{2}(\-[\dA-F]{2}){3,5}$/i', $value); + break; + + // String quelconque (peut etre vide) + case 'text': + return $checker && is_string($value); + + // Adresse mail (255 caracteres max) + case 'mail': + return $checker && is_string($value) && strlen($value) <= 50 && preg_match('/^[\w\.-]+@[\w\.-]+\.[a-z]{2,4}$/i', $value); + break; + + // Hash sha1/md5 + case 'hash': + return $checker && is_string($value) && preg_match('/^[\da-f]{40}$/i', $value); + break; + + case 'alphanumeric': + case 'user.username': + case 'group.name': + return $checker && is_string($value) && preg_match('/^[\w-]+$/i', $value); + break; + + case 'user.firstname': + case 'user.lastname': + case 'letters': + return $checker && is_string($value) && preg_match('/^[a-z -]+$/i', $value); + break; + + case 'status': + return $checker && is_numeric($value) && floor($value) == $value && $value >= 0 && $value <= 100; + break; + + // Tableau non vide + case 'array': + return $checker && is_array($value) && count($value) > 0; + break; + + // Boolean + case 'boolean': + return $checker && is_bool($value); + break; + + // Objet non vide + case 'object': + return $checker && is_object($value) && count((array) $value) > 0; + break; + + // Chaine JSON (on vérifie via le parser) + case 'json': + return $checker && is_string($value) && json_decode($value, true) !== NULL; + break; + + default: + return false; + break; + } + + return $checker; + + } + + + + + + /* FONCTION QUI FORMATTE UN NUMÉRO DE TÉLÉPHONE + * + * @number Numéro de téléphone en +336/336/06/0336/00336 + * + * @return formatted Numéro formatté (06), on FALSE si erreur + * + */ + public static function formatNumber($number){ + // On met en quel que soit le type + $number = (string) $number; + + // On supprime tous les espaces + $number = str_replace(' ', '', $number); + + // On formatte le numéro + if( preg_match("/^(?:\+33|0?0?33|0)(.+)/", $number, $m) ) + $number = '0'.$m[1]; + + // On retourne le numéro formatté + return $number; + } + + + public static function readableNumber($number){ + /* (1) On formatte le numéro si c'est pas fait */ + $formatted = self::formatNumber($number); + + for( $i = 1 ; $i < strlen($formatted) ; $i++ ) + if( ($i-2) % 3 == 0 ) + $formatted = substr($formatted, 0, $i).' '.substr($formatted, $i); + + return $formatted; + } + + + //////////////////////////////////// + // _ _ + // __| | __ _| |_ ___ ___ + // / _` |/ _` | __/ _ \/ __| + // | (_| | (_| | || __/\__ \ + // \__,_|\__,_|\__\___||___/ + // + //////////////////////////////////// + // 1) Convertis une date en en francais explicite + public static function frDate($date){ + /* [1] On definit les traductions + =========================================================*/ + // Jours de la semaine + $days = ["Lundi", "Mardi", "Mercredi", "Jeudi", "Vendredi", "Samedi", "Dimanche"]; + // Mois de l'annee + $months = ["Janvier", "Février", "Mars", "Avril", "Mai", "Juin", "Juillet", "Août", "Septembre", "Octobre", "Novembre", "Décembre"]; + + /* [2] On recupere le timestamp et les indices + =========================================================*/ + $time = strtotime($date); // timestamp + $daynum = intval( date('N', $time)-1 ); // jour dans la semaine + $monthnum = intval( date('n', $time)-1 ); // numero du mois dans l'annee + + + /* [3] On recupere les infos independemment + =========================================================*/ + $result = [ + $days[$daynum], // nom de jour + date('j', $time), // jour du mois + $months[$monthnum], // nom du mois + date('Y', $time), // annee + ]; + + + return implode(" ", $result); + } + + } +?> diff --git a/src/modules/database/1/core/Repo.php b/src/modules/database/1/core/Repo.php new file mode 100755 index 0000000..75e82d8 --- /dev/null +++ b/src/modules/database/1/core/Repo.php @@ -0,0 +1,204 @@ + Chemin de delegation ("repo/methode") + * @params Tableau contenant les parametres utiles au traitement + * + * @return status Retourne si oui ou non tout s'est bien passe + * + */ + public function __construct($path=null, $params=null){ + + // Si pas parametre manquant, on quitte + if( $path == null ){ + $this->error = Error::MissingPath; + return false; + } + + /* [0] On met a jour la configuration + =========================================================*/ + // Modules specifies + $this->repositories = json_decode( file_get_contents(self::config_path()), true ); + + // Gestion de l'erreur de parsage + if( $this->repositories == null ){ + $this->error = Error::ParsingFailed; + return false; + } + + + + /* [1] Verification des types des parametres + =========================================================*/ + // Type de @path + if( !is_string($path) ){ // Si le type est incorrect + $this->error = Error::WrongPathRepo; + return false; // On retourne FALSE, si erreur + } + + // Type de @params (optionnel) + $params = (is_array($params)) ? $params : []; + + + /* [2] Verification du chemin (existence repo+methode) + =========================================================*/ + if( !$this->checkPath($path) ) // Verification de la coherence du chemin + attribution + return false; + // Gestion d'erreur interne + + + /* [3] Construction de l'objet + =========================================================*/ + $this->params = $params; + $this->error = Error::Success; + + /* [4] Enregistrement de la reponse + =========================================================*/ + $this->answer = $this->dispatch(); + + + + return true; // On retourne que tout s'est bien passe + + } + + + + + public function answer(){ + if( $this->error != Error::Success ) + return false; + + return $this->answer; + } + + + + /* EXECUTE LE TRAITEMENT ASSOCIE ET REMPLIE LA REPONSE + * + * @return answer Retourne une reponse, si tout s'est bien passe + * + */ + public function dispatch(){ + /* [1] On verifie qu'aucune erreur n'a ete signalee + =========================================================*/ + if( $this->error != Error::Success ) // si il y a une erreur + return false; // on la passe a la reponse + + + /* [2] On verifie que la methode est amorcable + =========================================================*/ + if( !is_callable($this->getFunctionCaller()) ){ + $this->error = Error::UncallableMethod; + return false; + } + + + /* [3] On amorce la methode + =========================================================*/ + return call_user_func_array( $this->getFunctionCaller(), $this->params ); + } + + + + /* VERIFICATION DU FORMAT ET DE LA COHERENCE DU CHEMIN SPECIFIE + * + * @path String correspondant au chemin de delegation ("repo/methode") + * + * @return validity Retourne si oui ou non l'objet est correct + * + */ + private function checkPath($path){ + /* [1] Verification format general + =========================================================*/ + if( !preg_match('#^([\w_-]+)/([\w_-]+)$#i', $path, $matches) ){ // Si mauvais format + $this->error = Error::WrongPathRepo; + return false; + } + + // On recupere les donnes de la regex + $repository = $matches[1]; + $method = $matches[2]; + + /* [2] Verification de l'existence du repo (conf) + =========================================================*/ + if( !array_key_exists($repository, $this->repositories) ){ // Si le repo n'est pas specifie dans la conf + $this->error = Error::UnknownRepo; + return false; // On retourne FALSE, si erreur + } + + /* [3] Verification de l'existence de la methode (conf) + =========================================================*/ + if( array_search($method, $this->repositories[$repository]) === false ){ // Si la methode n'est pas specifie dans la conf + $this->error = Error::UnknownMethod; + return false; // On retourne FALSE, si erreur + } + + + + /* [4] Enregistrement du chemin et renvoi de SUCCESS + =========================================================*/ + $this->path = [ + 'repo' => $repository, + 'method' => $method + ]; + + return true; + } + + + + + + /* RENVOI LE CHEMIN D'AMORCAGE DE LA METHODE + * + * @return path Retourne le chemin d'amorcage de la requete + * + */ + private function getFunctionCaller(){ + return [ '\\database\\repo\\'.$this->path['repo'], $this->path['method'] ]; + } + + + } + +?> diff --git a/src/modules/error/1/core/Error.php b/src/modules/error/1/core/Error.php new file mode 100755 index 0000000..8e08239 --- /dev/null +++ b/src/modules/error/1/core/Error.php @@ -0,0 +1,150 @@ + géré en js + + + /* EXPLICITE UN CODE D'ERREUR + * + * @error Code d'erreur + * + * @return explicit Description explicite du code d'erreur + * + */ + public static function explicit($error){ + switch($error){ + case self::Success: return "Tout s'est bien deroulé."; break; + + case self::ParsingFailed: return "La lecture du fichier JSON ou XML a echouée."; break; + + case self::InvalidFlags: return "Les spécifications (drapeaux) sont incorrects."; break; + case self::UnreachableResource: return "La ressource n'existe pas (404)."; break; + case self::DisabledModule: return "Vous n'avez pas accès au module demandé."; break; + case self::MissingPath: return "Le chemin de délégation n'a pas été renseigné."; break; + case self::WrongPathModule: return "Le chemin de délégation est incorrect ('nomModule/nomMethode')."; break; + case self::WrongPathRepo: return "Le chemin de délégation est incorrect ('nomRepo/nomMethode')."; break; + case self::UnknownModule: return "Le module n'existe pas."; break; + case self::UnknownRepo: return "Le repo n'existe pas."; break; + case self::UnknownMethod: return "Le méthode n'existe pas."; break; + case self::UncallableMethod: return "Le méthode n'est pas amorçable."; break; + + case self::ParamError: return "Un ou plusieurs paramètres sont manquants ou incorrects."; break; + case self::ModuleError: return "Erreur lors du traitement du module."; break; + case self::RepoError: return "Erreur lors du traitement du repo."; break; + + case self::PDOConnection: return "La connexion avec la base de données a echouée."; break; + + case self::TokenError: return "Le token de connection est absent, érroné ou expiré."; break; + case self::PermissionError: return "Vous n'avez pas la permission d'effectuer cette action."; break; + case self::UploadError: return "Une erreur d'upload est survenue."; break; + case self::FormatError: return "Le fichier n'est pas au bon format."; break; + + case self::NoMatchFound: return "Aucun résultat correspondant à la requête."; break; + + case self::UnknownTemplate: return "Modèle inconnu."; break; + + default: return "Erreur inconnue..."; break; + } + + // Erreur inconnue + return null; + } + + + public static function setHttpCode($error){ + http_response_code( $error == self::Success ? 200 : 417 ); + } + + } + +?> diff --git a/src/modules/lightdb/1/core/lightdb.php b/src/modules/lightdb/1/core/lightdb.php new file mode 100644 index 0000000..9c02a25 --- /dev/null +++ b/src/modules/lightdb/1/core/lightdb.php @@ -0,0 +1,442 @@ + CREER LES FICHIERS S'ILS N'EXISTENT PAS SINON, RECUPERE LES DONNES + * + * @dbname Nom de la base de données + * + */ + public function __construct($dbname, $root=null){ + /* [0] On récupère les attributs + =========================================================*/ + $this->root = is_null($root) ? self::default_root().'/' : $root; + $this->dbname = $dbname; + $this->dir = $this->root.$dbname.'/'; + + + /* [1] Création du répertoire s'il n'existe pas + =========================================================*/ + if( !is_dir($this->dir) ) + mkdir($this->dir); + + /* [2] Création du fichier d'index ou récupération + =========================================================*/ + /* (1) Si le fichier n'existe pas, on le crée */ + if( !file_exists($this->dir.'index') ){ + $fIndex = new \SplFileObject($this->dir.'index', 'w'); + $fIndex->fwrite('[]'); + $fIndex = null; + } + + + /* (2) On récupère le contenu du fichier */ + $fIndex = new \SplFileObject($this->dir.'index'); + $fIndex->seek(0); + + $index = json_decode( $fIndex->fgets(), true ); + + // Si erreur de parsage, on retourne une erreur + if( is_null($index) ) return; + $this->index = $index; + + /* [3] Initialisation du gestionnaire d'acces (SplFileObject) + =========================================================*/ + /* (1) Si le fichier n'existe pas, on le crée */ + if( !file_exists($this->dir.'data') ) + file_put_contents($this->dir.'data', '' ); + + /* (2) On place un 'driver' sur le fichier */ + $this->driver = new \SplFileObject($this->dir.'data', 'r+'); + // $this->driver->setFlags( \SplFileObject::SKIP_EMPTY ); + + /* (3) On récupère le nombre de lignes */ + $this->line = -1; + while( !$this->driver->eof() ){ + $this->line++; + $this->driver->fgetcsv(); + } + } + + + public function close(){ $this->driver = null; } + + + + + /* RETOURNE LA LISTE DES INDEX + * + * @i Index pour lequel on veut la ligne et le hash + * + * @return Index Tableau associatif contenant le hash et la ligne + * + */ + public function index($i=null){ + return is_numeric($i) ? $this->index : $this->index; + } + + + /* INSERTION D'UNE ENTREE DANS LA BASE DE DONNEES + * + * @key Clé qui permettra l'accès direct + * @data Objet qui sera enregistré dans la base + * + * @return status Retourne TRUE si tout s'est bien passé, sinon FALSE + * + */ + public function insert($key, $data){ + /* (1) On vérifie que la clé est unique */ + if( array_key_exists($key, $this->index) ) + return true; + + $key = (string) $key; + + /* (2) On ajoute les données aux fichier */ + $json_data = json_encode($data); + $this->driver->seek($this->line); + $this->line++; + $written = $this->driver->fwrite( $json_data.PHP_EOL ); + + // Si erreur d'écriture, on retourne FALSE + if( is_null($written) ) + return false; + + /* (3) On enregistre l'index */ + $this->index[$key] = [ + 'line' => $this->line - 1, + 'hash' => sha1($json_data) + ]; + + /* (4) On enregistre le fichier index */ + $fIndex = new \SplFileObject($this->dir.'index', 'w'); + $fIndex->fwrite( json_encode($this->index) ); + $fIndex = null; + + return true; + } + + + /* INSERTION D'UNE ENTREE DANS LA BASE DE DONNEES + * + * @dataset Tableau de 'clés'->'valeurs' à insérer + * @data Objet qui sera enregistré dans la base + * + * @return status Retourne TRUE si tout s'est bien passé, sinon FALSE + * + */ + public function insertAll($dataset){ + /* (1) On vérifie que la clé est unique */ + foreach($dataset as $key=>$data) + if( array_key_exists($key, $this->index) ) + unset($dataset[$key]); + + + /* (2) On ajoute les données aux fichier */ + $this->driver->seek($this->line); + foreach($dataset as $key=>$data){ + $json_data = json_encode($data); + $this->line++; + $written = $this->driver->fwrite( $json_data.PHP_EOL ); + + + /* (3) On enregistre les index */ + $this->index[$key] = [ + 'line' => $this->line - 1, + 'hash' => sha1($json_data) + ]; + } + + + + /* (4) On enregistre le fichier index */ + $fIndex = new \SplFileObject($this->dir.'index', 'w'); + $fIndex->fwrite( json_encode($this->index) ); + $fIndex = null; + + return true; + } + + + /* RENVOIE LES DONNEES ASSOCIEES A UNE CLE DONNEE + * + * @key Clé associée à la valeur à récupérer + * + * @return data Renvoie la valeur associée à la clé, FALSE si erreur + * + */ + public function fetch($key){ + /* (1) On vérifie que la clé existe bien */ + if( !array_key_exists($key, $this->index) ) + return false; + + /* (2) On récupère la ligne */ + $line = $this->index[$key]['line']; + + /* (3) On récupère le contenu */ + $this->driver->seek($line); + $json = json_decode( $this->driver->current(), true ); + + // Si erreur de parsage + if( is_null($json) ) + return false; + + return $json; + } + + + + + /* RENVOIE LES DONNEES ASSOCIEES AUX CLES DONNEES + * + * @keys Clés associées aux valeurs à récupérer + * + * @return data Renvoie les valeurs associées aux clé, ou un tableau vide si erreur + * + */ + public function fetchAll($keys){ + $data = []; + + /* (0) Pour chaque clé */ + foreach($keys as $i=>$key){ + + /* (1) On ne prend pas en compte les clés qui n'existent pas */ + if( !array_key_exists($key, $this->index) ) + continue; + + /* (2) On récupère la ligne */ + $line = $this->index[$key]['line']; + + /* (3) On récupère le contenu */ + $this->driver->seek($line); + $json = json_decode( $this->driver->current(), true ); + + /* (4) Si pas d'erreur de parsage, On enregistre */ + if( !is_null($json) ) + $data[$key] = $json; + + } + + + + return $data; + } + + + /* SUPPRIME UNE ENTREE DE CLE DONNEE DE LA BASE DE DONNEES + * + * @key Clé de l'entrée à supprimer + * + * @return status Retourne TRUE si tout s'est bien passé, sinon FALSE + * + */ + public function delete($key){ + /* (1) On vérifie l'existence de la clé */ + if( !array_key_exists($key, $this->index) ) + return true; // On considère que l'action souhaitée est effectuée + + $line = $this->index[$key]['line']; + + /* (2) On réarrange la bd pour supprimer la ligne */ + $tmpfilename = __BUILD__.'/tmp/'.uniqid().'.dat'; + $tmpfile = new \SplFileObject($tmpfilename, 'w'); + $this->driver->seek(0); + + // On recopie toutes les lignes sauf celle à supprimer dans un fichier temporaire + while( $this->driver->key() < $this->line ){ + + if( $this->driver->key() != $line ) + $tmpfile->fwrite( $this->driver->current() ); + + $this->driver->next(); + } + + // On décrémente le nb de lignes + $this->line--; + + $tmpfile = null; + + /* (3) On remplace le fichier original par le fichier temporaire */ + $this->driver = null; + rename($tmpfilename, $this->dir.'data'); + $this->driver = new \SplFileObject($this->dir.'data', 'r+'); + + /* (3) On supprime la ligne de l'index */ + unset( $this->index[$key] ); + + /* (4) On met à jour les index des lignes déplacées */ + foreach($this->index as $i=>$indexData) + if( $indexData['line'] > $line ) + $this->index[$i]['line']--; // on décrémente les lignes au dessus de la ligne supprimée + + + /* (5) On enregistre le fichier index */ + $fIndex = new \SplFileObject($this->dir.'index', 'w'); + $fIndex->fwrite( json_encode($this->index) ); + $fIndex = null; + + + return true; + } + + + + /* SUPPRIME PLUSIEURS ENTREES DE CLES DONNEES DE LA BASE DE DONNEES + * + * @keys Clés des entrées à supprimer + * + * @return status Retourne TRUE si tout s'est bien passé, sinon FALSE + * + */ + public function deleteAll($keys){ + $keyLines = []; + + /* [1] On récupère la ligne associée à chaque clé + =========================================================*/ + foreach($keys as $k=>$key){ + /* (1) Si la clé n'existe pas, on passe à la suivante */ + if( !array_key_exists($key, $this->index) ) + continue; + + /* (2) On récupère la ligne de la clé */ + $keyLines[$key] = $this->index[$key]['line']; + } + + /* [2] On trie les clés en fonction de leur ligne + =========================================================*/ + $sorted = []; + + // Tant que toute les clés ne sont pas triées + while( count($keyLines) > 0 ){ + // Contiendra la clé de la plus petite valeur + $min = null; + + // On cherche la ligne la plus petite + foreach($keyLines as $key=>$line) + if( is_null($min) || $line < $keyLines[$min] ) // Si valeur inf à min + $min = $key; + + // On ajoute la plus petite clé trouvée a la liste + $sorted[$min] = $keyLines[$min]; + + // On la supprime du tableau à trier + unset($keyLines[$min]); + + } + + /* [3] On supprime les lignes à supprimer + =========================================================*/ + /* (1) On réarrange la bd pour supprimer la ligne */ + $tmpfilename = __BUILD__.'/tmp/'.uniqid().'.dat'; + $tmpfile = new \SplFileObject($tmpfilename, 'w'); + $this->driver->seek(0); + + /* (2) On recopie toutes les lignes sauf celles à supprimer dans un fichier temporaire */ + while( $this->driver->key() < $this->line ){ + + // Si la ligne en cours n'est pas dans la liste des lignes à supprimer + if( !in_array($this->driver->key(), $sorted) ) + $tmpfile->fwrite( $this->driver->current() ); // On l'écrit dans le nouveau fichier + + $this->driver->next(); + } + + $tmpfile = null; + + /* (3) On remplace le fichier original par le fichier temporaire */ + $this->driver = null; + rename($tmpfilename, $this->dir.'data'); + $this->driver = new \SplFileObject($this->dir.'data', 'r+'); + + + /* [4] On met à jour les index + =========================================================*/ + $step = 0; + foreach($sorted as $key=>$line){ + + /* (1) On décrémente le nb de lignes */ + $this->line--; + + /* (2) On supprime la ligne de l'index */ + unset( $this->index[$key] ); + + /* (3) On met à jour les index des lignes déplacées du nombre d'index qu'on a supprimé */ + foreach($this->index as $i=>$indexData) + if( $indexData['line'] > $line-$step ) + $this->index[$i]['line']--; // on décrémente les lignes au dessus de la ligne supprimée + + $step++; + } + + /* (4) On enregistre le fichier index */ + $fIndex = new \SplFileObject($this->dir.'index', 'w'); + $fIndex->fwrite( json_encode($this->index) ); + $fIndex = null; + + + return true; + } + + + + + + + /* RENVOIE LES DONNEES ASSOCIEES A UN CHAMP DE RECHERCHE + * + * @nomParam Description du param + * + * @return nomRetour Description du retour + * + */ + public function filter($data){ + /* (1) Si @data est un tableau associatif */ + if( is_array($data) ){ + + $filtered = []; + foreach($this->index as $i=>$indexData){ + $this->driver->seek( $indexData['line'] ); + $dbData = json_decode( $this->driver->fgets(), true ); + + foreach($data as $key=>$value) + if( isset($dbData[$key]) && preg_match("#$value#", $dbData[$key]) ){ + $filtered[$i] = $dbData; + break; + } + } + + return $filtered; + + + /* (2) Sinon on compare @data en tant que valeur simple */ + }else{ + + $this->tmp = sha1( json_encode($data) ); + return array_filter($this->index, [$this, 'simpleFilter']); + + } + + } + protected function simpleFilter($e){ return $e['hash'] == $this->tmp; } + + + + + + + } diff --git a/src/modules/orm/0.8/core/Rows.php b/src/modules/orm/0.8/core/Rows.php new file mode 100644 index 0000000..99a1c43 --- /dev/null +++ b/src/modules/orm/0.8/core/Rows.php @@ -0,0 +1,1072 @@ +__'; + const COND_INF = '__<__'; + const COND_SUP = '__>__'; + const COND_INFEQ = '__<=__'; + const COND_SUPEQ = '__>=__'; + const COND_LIKE = '__LIKE__'; + const COND_IN = '__IN__'; + + // {2} Fonctions d'aggrégation // + const SEL_AVG = '__AVG__'; + const SEL_SUM = '__SUM__'; + const SEL_MAX = '__MAX__'; + const SEL_MIN = '__MIN__'; + const SEL_COUNT = '__COUNT__'; + const SEL_CONCAT = '__GROUP_CONCAT__'; + + const SEL_DISTINCT = true; + + // {3} Gestion du Order By // + const ORDER_ASC = '__ASC__'; + const ORDER_DESC = '__DESC__'; + + // {3} Constantes d'insertion // + const INSERT_DEFAULT = '__DEFAULT__'; // Valeur DEFAULT (pour insertion) + + /* Attributs */ + private $where; // Tableau associatif contenant les conditions + private $select; // Tableau contenant la liste des champs à afficher + private $orderby; // Tableau contenant la liste des orderby + 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 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 l'ordonnancement' */ + $this->orderby = []; + + /* (5) On initialise le caractère 'unique' du résultat */ + $this->unique = false; + + /* (6) On initialise les jointures */ + $this->joined = []; + } + + + + + /* FILTRE LES ENTREES D'UNE TABLE AVEC LA CLE PRIMAIRE SPECIFIEE + * + * @primary Clé primaire simple + * OU + * @primary Clé primaire composée + * + * @return 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 Nom de la méthode + * @parameter Valeur du paramètre + * @parameter Valeur du paramètre + type de vérification (tableau) + * + * @return this 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 LE CHAMP SELECTIONNE + * + * @field Libellé du champ à afficher + * @func Fonction d'aggrégation (ou NULL) + * @distinct Clause DISTINCT + * + * @return this Retourne l'object courant + * + */ + public function select($field=null, $func=null, $distinct=false){ + /* [1] On formatte les champs + =========================================================*/ + /* (1) On vérifie le type de @field */ + if( !is_string($field) ) + return $this; + + /* (2) On vérifie que la colonne @field existe, sinon on quitte */ + if( !isset($this->schema['columns'][$field]) && $field != '*' ) + return $this; + + /* (3) On vérifie @func */ + $funcList = [self::SEL_AVG, self::SEL_SUM, self::SEL_MAX, self::SEL_MIN, self::SEL_COUNT, self::SEL_CONCAT]; + + // Si condition non nulle et pas référencée, on quitte + if( !is_null($func) && !in_array($func, $funcList) ) + return $this; + + /* (4) On met la valeur par défaut à @distinct si type mauvais */ + $distinct = !is_bool($distinct) ? false : $distinct; + + + /* [2] On enregistre le champ + =========================================================*/ + /* (1) Si aucun SELECT pour ce champ, on le crée */ + if( !isset($this->select[$field]) ) + $this->select[$field] = [$func, $distinct]; + + + /* [3] On retourne l'object courant + =========================================================*/ + return $this; + } + + + + + + /* SELECTIONNE L'ORDONNANCEMENT DES RESULTATS + * + * @field Libellé du champ à afficher + * @order Gestion de l'ordre ASC/DESC (ou NULL) + * + * @return this Retourne l'object courant + * + */ + public function orderby($field=null, $order=null){ + /* [1] On formatte les champs + =========================================================*/ + /* (1) On vérifie le type de @field */ + if( !is_string($field) ) + return $this; + + /* (2) On vérifie que la colonne @field existe, sinon on quitte */ + if( !isset($this->schema['columns'][$field]) && $field != '*' ) + return $this; + + /* (3) On vérifie @order */ + $orderList = [self::ORDER_ASC, self::ORDER_DESC]; + + // Valeur si NULL + $order = is_null($order) ? $orderList[0] : $order; + + // Si ordre non référencée, on quitte + if( !in_array($order, $orderList) ) + return $this; + + + /* [2] On enregistre le champ + =========================================================*/ + /* (1) On crée le ORDER_BY pour ce champ */ + $this->orderby[$field] = $order; + + + /* [3] On retourne l'object courant + =========================================================*/ + return $this; + } + + + + + + + + /* JOINT UNE SECONDE TABLE () + * + * @localField Nom d'une colonne locale + * @rows Rows d'une autre table + * + * @return this Retourne l'object courant + * + */ + public function join($localField, $rows){ + /* [0] Vérification / Formattage des paramètres + =========================================================*/ + /* (1) Si le champ n'est pas au bon format */ + if( !is_string($localField) ) + return $this; + + /* (2) Si @rows n'est pas au bon format */ + if( !($rows instanceof Rows) ) + return $this; + + /* (3) Si le champ n'existe pas dans la table */ + if( !isset($this->schema['columns'][$localField]) ) + return $this; + + /* (4) On récupère les données du champ local dans une variable */ + $localFieldData = $this->schema['columns'][$localField]; + + /* [1] On vérifie que la clé étrangère est correcte + =========================================================*/ + /* (1) Si la colonne n'existe pas et qu'elle n'est pas primaire, on ne fait rien */ + if( !isset($localFieldData['references']) && !$localFieldData['primary'] ) + return $this; + + /* (2) On vérifie que la colonne a une référence vers la table de @rows */ + $referencesToRows = isset($localFieldData['references']) && $localFieldData['references'][0] == $rows->schema['table']; + $rowsField = null; + + /* (3) On vérifie que la colonne est la référence d'un champ de @rows */ + $referencesFromRows = false; + + // On vérifie chaque champ de @rows + foreach($rows->schema['columns'] as $field=>$data) + // Si un champ de la table de @rows a pour référence le champ local + if( isset($data['references']) && $data['references'][0] == $this->schema['table'] && $data['references'][1] == $localField ){ + $referencesFromRows = true; + $rowsField = $field; + break; + } + + /* (4) On vérifie que la colonne a la même référence qu'une colonne de @rows */ + $referencesSameTarget = false; + + // On vérifie toutes les colonnes de @rows + foreach($rows->schema['columns'] as $field=>$data) + // Si on trouve un champ avec la même référence + if( isset($data['references']) && isset($localFieldData['references']) && count(array_diff($data['references'], $localFieldData['references'])) == 0 ){ + $referencesSameTarget = true; + $rowsField = $field; // On enregistre le champ qui a la même cible + break; + } + + /* (4) Si aucune référence en commun, on ne fait rien */ + if( !$referencesToRows && !$referencesFromRows && !$referencesSameTarget ) + return $this; + + + /* [2] On enregistre la référence + =========================================================*/ + $this->joined[$localField] = [ + 'object' => $rows, + 'field' => is_null($rowsField) ? $localFieldData['references'][1] : $rowsField // On met le nom du champ de @rows à lier + ]; + + + /* [3] On retourne l'object courant + =========================================================*/ + return $this; + } + + + + + + + + /* PERMET DE DIRE QUE L'ON VEUT UN RESULTAT UNIQUE + * + * @return this 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 Tableau associatif contenant les nouvelles valeurs + * + * @return updated 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] Initialisation des paramètres + =========================================================*/ + /* (1) On initialise la requête */ + $requestS = []; + + /* (2) On initialise les paramètres */ + $bound = []; + + + + /* [2] Rédaction de la clause UPDATE + =========================================================*/ + $requestS['UPDATE'] = SQLBuilder::UPDATE($this->schema['table']); + + + /* [3] Rédaction de la clause SET + =========================================================*/ + /* (1) On met tout les champs à modifier */ + $requestS['SET'] = SQLBuilder::SET($cleared, $bound); + + + /* [4] On rédige la clause WHERE/AND + =========================================================*/ + /* (1) On met les conditions locales */ + $requestS['WHERE'] = []; + $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['WHERE'][$c] = SQLBuilder::IN([$this->schema['table'], $field], $value[0], $c, $bound); + else // Sinon + $requestS['WHERE'][$c] = SQLBuilder::WHERE([$this->schema['table'], $field], $value, $c, $bound); + + $c++; + } + + /* (2) On ajoute les jointures */ + // Note: On ajoute les requêtes des tables de jointures dans la clause WHERE // + foreach($this->joined as $field=>$data){ + // {1} On récupère la requête/les params de chaque jointure // + $joinedFetched = $data['object']->fetch(false); + + // {2} On met la clé étrangère pour la clause SELECT // + $joinedFetched['request']['SELECT'] = [ $data['object']->schema['table'].'.'.$data['field'] ]; + + // {3} On construit la nouvelle requête // + $joinedRequest = SQLBuilder::BUILD($joinedFetched['request']); + + // {4} On supprime les retours à la ligne // + $joinedRequest = str_replace("\n", " ", $joinedRequest); + + // {5} On l'ajoute à la clause FROM avec comme alias le nom de la table de @data['object'] // + $requestS['WHERE'][] = $this->schema['table'].".$field in ($joinedRequest)"; + + // {6} On ajoute les variables à la requête courante // + $bound = array_merge($bound, $joinedFetched['bound']); + } + + + /* [5] Clause LIMIT + =========================================================*/ + $requestS['LIMIT'] = ($this->unique) ? SQLBuilder::LIMIT(1) : SQLBuilder::LIMIT([]); + + + /* [6] On prépare et compose la requête + =========================================================*/ + /* (1) On compose la requête */ + $requestString = SQLBuilder::BUILD($requestS).';'; + + /* (2) On prépare la requête */ + $request = Database::getPDO()->prepare($requestString); + + + + /* [7] On exécute la requête et retourne le résultat + =========================================================*/ + /* (1) On exécute la requête */ + $updated = $request->execute($bound); + + /* (2) On retourne l'état de la requête */ + return $updated; + } + + + + + + /* AJOUTE UNE ENTREE DANS LA TABLE + * + * @entry Tableau associatif de la forme (colonne => valeur) + * OU + * @entries Tableau de la forme ([entry1, entry2]) + * + * @return status 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::INSERT_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::INSERT_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::INSERT_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.';'); + $bound = []; + + /* (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::INSERT_DEFAULT ) + $bound[':insert_'.$field.'_'.$i] = $set[$field]; + + + /* [3] On exécute la requête et envoie le status + =========================================================*/ + $inserted = $request->execute($bound); + + // On retourne le status + return $inserted; + } + + + + + /* SUPPRIME LES ENTREES + * + * @return status Retourne si TRUE ou FALSE les entrées ont bien été supprimées + * + */ + public function delete(){ + /* [0] Initialisation des paramètres + =========================================================*/ + /* (1) On initialise la requête */ + $requestS = []; + + /* (2) On initialise les paramètres */ + $bound = []; + + + /* [1] Clause DELETE FROM + =========================================================*/ + $requestS['DELETE'] = SQLBuilder::DELETE($this->schema['table']); + + + /* [2] On rédige la clause WHERE/AND + =========================================================*/ + /* (1) On met les conditions locales */ + $requestS['WHERE'] = []; + $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['WHERE'][$c] = SQLBuilder::IN([$this->schema['table'], $field], $value[0], $c, $bound); + else // Sinon + $requestS['WHERE'][$c] = SQLBuilder::WHERE([$this->schema['table'], $field], $value, $c, $bound); + + $c++; + } + + + /* (2) On ajoute les jointures */ + // Note: On ajoute les requêtes des tables de jointures dans la clause WHERE // + foreach($this->joined as $field=>$data){ + // {1} On récupère la requête/les params de chaque jointure // + $joinedFetched = $data['object']->fetch(false); + + // {2} On met la clé étrangère pour la clause SELECT // + $joinedFetched['request']['SELECT'] = [ $data['object']->schema['table'].'.'.$data['field'] ]; + + // {3} On construit la nouvelle requête // + $joinedRequest = SQLBuilder::BUILD($joinedFetched['request']); + + // {4} On supprime les retours à la ligne // + $joinedRequest = str_replace("\n", " ", $joinedRequest); + + // {5} On l'ajoute à la clause FROM avec comme alias le nom de la table de @data['object'] // + $requestS['WHERE'][] = $this->schema['table'].".$field in ($joinedRequest)"; + + // {6} On ajoute les variables à la requête courante // + $bound = array_merge($bound, $joinedFetched['bound']); + } + + + /* [3] Clause LIMIT + =========================================================*/ + $requestS['LIMIT'] = ($this->unique) ? SQLBuilder::LIMIT(1) : SQLBuilder::LIMIT([]); + + + /* [4] On prépare et compose la requête + =========================================================*/ + /* (1) On compose la requête */ + $requestString = SQLBuilder::BUILD($requestS).';'; + + /* (2) On prépare la requête */ + $request = Database::getPDO()->prepare($requestString); + + /* [5] On exécute la requête et retourne le résultat + =========================================================*/ + /* (1) On exécute la requête */ + $deleted = $request->execute($bound); + + /* (2) On retourne l'état de la requête */ + return $deleted; + } + + + + + + + /* RETOURNE LES DONNEES / NULL si une erreur survient + * + * @execute VRAI si on veut exécuter la requête, sinon renvoie [requete, boundParams] + * + * @return data Tableau contenant les champs sélectionnés + * @return data Valeur du champ sélectionné (si 1 seul champ) + * @return ERROR Retourne FALSE si rien n'est trouvé + * + */ + public function fetch($execute=true){ + /* [0] On initialise + =========================================================*/ + /* (1) On initialise la requête */ + $requestS = []; + + /* (2) On initialise le conteneur des variables "bindés" */ + $bound = []; + + /* (3) On récupère la requête générée par chaque @rows de jointure */ + $joinedFetched = []; + foreach($this->joined as $field=>$data) + $joinedFetched[$field] = $data['object']->fetch(false); + + + /* [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; + + + /* (4) On ajoute les champs des jointures (récursif)*/ + foreach($joinedFetched as $field=>$data){ + foreach($data['request']['SELECT'] as $table=>$fields) + foreach($fields as $field=>$sel){ + // Si aucune entrée pour cette table, on l'ajoute + if( !isset($selectTables[$table]) ) + $selectTables[$table] = []; + + $selectTables[$table][$field] = $sel; + } + } + + /* (3) On génère la clause SELECT */ + $requestS['SELECT'] = SQLBuilder::SELECT($selectTables); + + + /* [2] On rédige la clause FROM + ========================================================*/ + /* (0) On initialise la clause */ + $requestS['FROM'] = []; + + /* (1) Table locale */ + $requestS['FROM'][] = $this->schema['table']; + + /* (2) On ajoute les tables de jointures */ + // Note: On ajoute les tables de jointures dans la clause FROM avec comme alias le nom de la table + foreach($joinedFetched as $field=>$data) + // On ajoute la clause FROM de jointure à la clause FROM locale // + $requestS['FROM'] = array_merge($data['request']['FROM'], $requestS['FROM']); + + + /* [5] On rédige la clause WHERE/AND + =========================================================*/ + /* (1) On met les conditions locales */ + $c = 0; + $requestS['WHERE'] = []; + foreach($this->where as $field=>$conditions) + foreach($conditions as $cdt=>$value){ + + if( $value[1] === self::COND_IN ) // Si condition IN + $requestS['WHERE'][$c] = SQLBuilder::IN([$this->schema['table'], $field], $value[0], $c, $bound); + else // Sinon + $requestS['WHERE'][$c] = SQLBuilder::WHERE([$this->schema['table'], $field], $value, $c, $bound); + + $c++; + } + + /* (2) On ajoute les jointures */ + foreach($this->joined as $localField=>$data){ + $requestS['WHERE'][$c] = $this->schema['table'].".$localField = ".$data['object']->schema['table'].".".$data['field']; + $c++; + } + + /* (3) On ajoute les conditions des jointures */ + foreach($joinedFetched as $field=>$data){ + /* On ajoute la clause WHERE de jointure à la clause WHERE locale */ + $requestS['WHERE'] = array_merge($data['request']['WHERE'], $requestS['WHERE']); + + /* On ajoute les variables à la requête courante */ + $bound = array_merge($bound, $data['bound']); + } + + + /* [6] Clause GROUP BY + =========================================================*/ + /* (0) On initialise la liste des @rows non aggrégés */ + $groupBy = []; + + /* (1) On cherche dans les champs locaux local */ + foreach($selectTables as $table=>$fields) + foreach($fields as $field=>$sel) + // Si aucune fonction d'aggrégation + if( is_null($sel[0]) ){ + if( !isset($groupBy[$table]) ) + $groupBy[$table] = []; + + // Si le champ est *, on trouve les clés primaires + if( $field == '*' ){ + $columns = Table::get($table)->schema['columns']; + foreach($columns as $col=>$data) + if( $data['primary'] ) + $groupBy[$table][] = $col; + }else + $groupBy[$table][] = $field; + + + $groupBy[$table] = array_unique($groupBy[$table]); + } + + + /* (2) On rédige la clause GROUP BY */ + if( count($groupBy) > 0) + $requestS['GROUPBY'] = SQLBuilder::GROUPBY($groupBy); + + /* [6] Clause ORDER BY + =========================================================*/ + /* (1) On formatte les données */ + $orderTables = []; + + /* (2) On ajoute les champs locaux */ + if( count($this->orderby) > 0 ) + $orderTables[$this->schema['table']] = $this->orderby; + + /* (4) On ajoute les champs des jointures (récursif)*/ + foreach($joinedFetched as $field=>$data){ + foreach($data['request']['ORDERBY'] as $table=>$fields) // pour chaque ensemble de champ de chaque table + foreach($fields as $field=>$orderBy) // Pour chaque orderby de chaque champ + + if( count($orderBy) > 0 ) + $orderTables[$table][$field] = $orderBy; + + } + + /* (3) On génère la clause SELECT */ + $requestS['ORDERBY'] = SQLBuilder::ORDERBY($orderTables); + + /* [6] Clause LIMIT + =========================================================*/ + $requestS['LIMIT'] = ($this->unique) ? SQLBuilder::LIMIT(1) : SQLBuilder::LIMIT([]); + + + /* [7] On compose/prépare la requête + =========================================================*/ + /* (1) Si on veut pas exécuter on renvoie la requête + boundParams */ + if( !$execute ) + return [ 'request' => $requestS, 'bound' => $bound]; + + /* (2) On compose la requête */ + $requestString = SQLBuilder::BUILD($requestS).';'; + + /* (3) On prépare la requête */ + $request = Database::getPDO()->prepare($requestString); + // var_dump($requestString); + + + /* [8] On exécute la requête et retourne le résultat + =========================================================*/ + /* (1) On exécute la requête */ + $request->execute($bound); + + /* (2) Si unique */ + if( $this->unique ) + return $this->format( $request->fetch() ); + + /* (3) Si tout */ + return $this->format( $request->fetchAll() ); + } + + + + + + + /* ON FORMATTE LES DONNEES DE SORTIE + * + * @data Données / Tableau de données + * + * @return formatted 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 $j) + $existingColumns = array_merge( $existingColumns, $j['object']->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 ); + + // Si pas non plus une aggrégation et si indice numérique, on le retire + }else if( !preg_match('/^agg_.+/', $index) && is_numeric($index) ) + 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; + + } + + } + + + +?> diff --git a/src/modules/orm/0.8/core/SQLBuilder.php b/src/modules/orm/0.8/core/SQLBuilder.php new file mode 100644 index 0000000..0208822 --- /dev/null +++ b/src/modules/orm/0.8/core/SQLBuilder.php @@ -0,0 +1,428 @@ + Liste de champs : [table => field => [func, alias] ] + * + * @return sql Renvoie un tableau formatté + * + */ + public static function SELECT($sqlFields){ + return $sqlFields; + } + + + + + + + + /* CONSTRUIT LA REQUETE FORMATTEE "ORDER BY" AVEC UNE LISTE DE CHAMPS + * + * @tables Liste de champs : [table => fields] + * + * @return sql Renvoie un tableau formatté + * + */ + public static function ORDERBY($tables){ + return $tables; + } + + + + + + + + /* CONSTRUIT LA REQUETE FORMATTEE "GROUP BY" AVEC UNE LISTE DE CHAMPS + * + * @tables Liste de champs : [table => fields] + * + * @return sql Renvoie un tableau formatté + * + */ + public static function GROUPBY($tables){ + return $tables; + } + + + + + + + /* CONSTRUIT LA REQUETE FORMATTEE "FROM" AVEC UNE LISTE DE TABLES + * + * @tables Liste de tables OU SQL PUR + * + * @return sql Renvoie un tableau formatté + * + */ + public static function FROM($tables){ + return $tables; + } + + + + + + + /* CONSTRUIT LA REQUETE FORMATTEE "UPDATE" AVEC LA TABLE EN QUESTION + * + * @table Table en question + * + * @return sql Renvoie un tableau formatté + * + */ + public static function UPDATE($table){ + return $table; + } + + + + + + + /* CONSTRUIT LA REQUETE FORMATTEE "DELETE" AVEC LA TABLE EN QUESTION + * + * @table Table en question + * + * @return sql Renvoie un tableau formatté + * + */ + public static function DELETE($table){ + return $table; + } + + + + + + + /* CONSTRUIT LA REQUETE TEXTUELLE "IN" AVEC UNE LISTE DE TABLES + * + * @field Tableau contenant [table, field] + * @array Valeurs de la clause IN + * @offset Permet de rendre la condition unique (nommage des variables) + * @bound Tableau associatif contenant les variables "bindés" -> ajout des champs + * + * @return sql Renvoie le textuel formatté + * + */ + public static function IN($field, $array, $offset=0, &$bound){ + /* [0] Initialisation + =========================================================*/ + $sql = ''; + + /* [1] On construit la requête + =========================================================*/ + /* (1) Champ */ + $sql .= $field[0].'.'.$field[1].' IN ('; + + /* (2) Valeurs */ + $c = 0; + foreach($array as $i=>$value){ + if( $c > 0 ) $sql .= ', '; + + $sql .= ':'.$field[0].'_x_'.$field[1].'_'.$offset.'_'.$i; + + $bound[':'.$field[0].'_x_'.$field[1].'_'.$offset.'_'.$i] = $value; + + $c++; + } + + return $sql.")"; + } + + + + + + /* CONSTRUIT LA REQUETE TEXTUELLE "WHERE" AVEC UNE LISTE DE TABLES + * + * @field Tableau contenant [table, field] + * @valeur Valeurs de la clause WHERE [valeur, opérateur] + * @offset Permet de rendre la condition unique (nommage des variables) + * @bound Tableau associatif contenant les variables "bindés" -> ajout des champs + * + * @return sql Renvoie le textuel formatté + * + */ + public static function WHERE($field, $value, $offset=0, &$bound){ + /* [0] Initialisation + =========================================================*/ + $sql = ''; + + + /* [1] On construit la requête + =========================================================*/ + /* (1) Chamo */ + $sql .= $field[0].'.'.$field[1].' '; + + /* (2) Opérateur */ + $sql .= substr($value[1], 2, -2).' '; + + /* (3) Variable */ + $sql .= ':'.$field[0].'_x_'.$field[1].'_'.$offset; + + $bound[':'.$field[0].'_x_'.$field[1].'_'.$offset] = $value[0]; + + + return $sql; + } + + + + + + /* CONSTRUIT LA REQUETE FORMATTEE "SET" AVEC UNE LISTE DE TABLES + * + * @values Tableau de la forme [ field=>value, field2=>value2 ] + * @bound Tableau associatif contenant les variables "bindés" -> ajout des champs + * + * @return sql Renvoie un tableau formatté + * + */ + public static function SET($values, &$bound){ + /* [0] Initialisation + =========================================================*/ + $sql = []; + + + /* [1] On construit la requête + =========================================================*/ + $c = 0; + foreach($values as $field=>$value){ + /* (1) Champ */ + $sql[$c] = $field.' = '; + + /* (2) Variable */ + $sql[$c] .= ':update_'.$field; + + $bound[':update_'.$field] = $value; + + $c++; + } + + return $sql; + } + + + + + + /* CONSTRUIT LA REQUETE FORMATTEE "LIMIT" AVEC UN NOMBRE D'ENTREES + * + * @count Nombre limite + * + * @return sql Renvoie un sql formatté + * + */ + public static function LIMIT($count=null){ + /* [0] Initialisation + =========================================================*/ + $sql = ''; + + + /* [1] On construit la requête + =========================================================*/ + if( intval($count) == $count ) + $sql = intval($count); + + return $sql; + } + + + + + + + + + + + + + + /* CONSTRUIT LA REQUETE A PARTIR D'UNE REQUETTE FORMATTEE + * + * @request Requête formattée + * + * @return sql Requête formattée en SQL + * + */ + public static function BUILD($request){ + /* [0] On initialise le retour + =========================================================*/ + $sql = ''; + + /* [1] Gestion dans l'ordre + =========================================================*/ + foreach($request as $clause=>$statements){ + + switch($clause){ + + /* (1) Clause SELECT + ---------------------------------------------------------*/ + case 'SELECT': + $sql .= "SELECT "; + $c = 0; + foreach($statements as $table=>$fields) + foreach($fields as $field=>$select){ + + /* (1) On construit le nom du champ */ + $fieldStr = "$table.$field"; + + /* (2) On ajout le DISTINCT s'il y a lieu */ + if( isset($select[1]) && $select[1] ) + $fieldStr = "DISTINCT $fieldStr"; + + /* (3) On ajoute la fonction d'aggrégation s'il y a lieu */ + if( isset($select[0]) && !is_null($select[0]) ) + $fieldStr = substr($select[0], 2, -2)."($fieldStr)"; + + + /* (4) On ajoute l'alias */ + if( isset($select[0]) && !is_null($select[0]) ) + $fieldStr = "$fieldStr as agg_$field"; + else + $fieldStr = "$fieldStr"; + + $sql .= ($c==0) ? "$fieldStr" : ", $fieldStr"; + + $c++; + } + + $sql .= "\n"; + break; + + /* (2) Clause FROM + ---------------------------------------------------------*/ + case 'FROM': + $sql .= 'FROM '; + + $c = 0; + foreach($statements as $field){ + $sql .= ($c==0) ? "$field" : ", $field"; + $c++; + } + + $sql .= "\n"; + break; + + + /* (3) Clause WHERE + ---------------------------------------------------------*/ + case 'WHERE': + $c = 0; + foreach($statements as $field){ + $sql .= ($c==0) ? "WHERE $field\n" : "AND $field\n"; + $c++; + } + + $sql .= ($c==0) ? '' : "\n"; + break; + + + + /* (4) Clause LIMIT + ---------------------------------------------------------*/ + case 'LIMIT': + if( is_numeric($statements) ) + $sql .= 'LIMIT '.intval($statements); + break; + + + /* (5) Clause DELETE + ---------------------------------------------------------*/ + case 'DELETE': + $sql .= "DELETE FROM $statements\n"; + break; + + + /* (6) Clause UPDATE + ---------------------------------------------------------*/ + case 'UPDATE': + $sql .= "UPDATE $statements\n"; + break; + + + /* (7) Clause SET + ---------------------------------------------------------*/ + case 'SET': + $c = 0; + foreach($statements as $field){ + $sql .= ($c>0) ? "\n, $field" : "SET $field"; + $c++; + } + $sql .= "\n"; + break; + + /* (8) Clause GROUP BY + ---------------------------------------------------------*/ + case 'GROUPBY': + $sql .= 'GROUP BY '; + + $c = 0; + foreach($statements as $table=>$fields) + foreach($fields as $field){ + $sql .= ($c==0) ? "$table.$field" : ", $table.$field"; + $c++; + } + + $sql .= "\n"; + break; + + /* (9) Clause ORDER BY + ---------------------------------------------------------*/ + case 'ORDERBY': + + // si aucun ORDER BY, on quitte + if( count($statements) == 0 ) + continue; + + $sql .= 'ORDER BY '; + + $c = 0; + foreach($statements as $table=>$fields) + foreach($fields as $field=>$order){ + + if( $c > 0 ) $sql .= ', '; + + $sql .= "$table.$field ". substr($order, 2, -2); + + $c++; + } + + $sql .= "\n"; + break; + } + + + } + + + + + + + + /* [2] On retourne le résultat + =========================================================*/ + return $sql; + } + + + + } + + +?> diff --git a/src/modules/orm/0.8/core/Table.php b/src/modules/orm/0.8/core/Table.php new file mode 100644 index 0000000..f3bc922 --- /dev/null +++ b/src/modules/orm/0.8/core/Table.php @@ -0,0 +1,194 @@ + Nom de la table à selectionner + * + * @return this Retourne une instance de l'ORM + * + */ + public static function get($table_name){ + /* [0] Initialisation des attributs + =========================================================*/ + $schema = [ + 'database' => self::$database, + 'table' => null, + 'columns' => null + ]; + + + /* [1] On vérifie que la table existe + =========================================================*/ + /* (1) Requête */ + $checkTable = Database::getPDO()->query("SHOW tables FROM ".self::$database); + $checkTableResult = Database::delNumeric( $checkTable->fetchAll() ); + + /* (2) On met en forme les données */ + $tables = []; + foreach($checkTableResult as $table) + $tables[] = $table['Tables_in_'.self::$database]; + + /* (3) Si n'existe pas, on renvoie une erreur */ + if( !in_array($table_name, $tables) ) + return null; + + /* (4) On enregistre les données */ + $schema['table'] = $table_name; + + + + /* [2] Si la table existe, on récupère les colonnes + =========================================================*/ + /* (1) On récupère les colonnes */ + $getColumns = Database::getPDO()->query("SHOW columns FROM ".self::$database.'.'.$table_name); + $columnsResult = Database::delNumeric( $getColumns->fetchAll() ); + + /* (2) On met en forme les données */ + $columns = []; + foreach($columnsResult as $col){ + // On formatte le type // + $type = $col['Type']; + if( preg_match('/^(int|float|varchar|text)/i', $type, $m) ) + $type = strtolower($m[1]); + + // On ajoute la colonne // + $columns[$col['Field']] = [ + 'type' => $type, + 'primary' => $col['Key'] == 'PRI' + ]; + } + + + /* (3) Si on trouve rien, on envoie une erreur */ + if( !is_array($columns) || count($columns) == 0 ) + return null; + + /* (4) On enregistre les colonnes */ + $schema['columns'] = $columns; + + + + /* [3] On récupère les clés étrangères + =========================================================*/ + /* (1) On récupère le texte du 'CREATE TABLE' */ + $getCreateTable = Database::getPDO()->query("show create table ".$table_name); + $create_table = $getCreateTable->fetch()['Create Table']; + + /* (2) On découpte en lignes */ + $create_table_lines = explode("\n", $create_table); + + /* (3) Pour chaque ligne, si c'est une contrainte, on l'enregistre dans la colonne associée */ + foreach($create_table_lines as $i=>$line) + if( preg_match('/CONSTRAINT `.+` FOREIGN KEY \(`(.+)`\) REFERENCES `(.+)` \(`(.+)`\)+/i', $line, $m) ) + $schema['columns'][$m[1]]['references'] = [$m[2], $m[3]]; + + + + /* [3] On renvoie une instance de 'Rows' + =========================================================*/ + return new Rows($schema); + + } + + + + }; + + + + /*** USE CASE :: ACCESS TABLE `user` ***/ + // ORM::Table('user'); + + + /**** USE CASE :: WHERE ****/ + // WHERE `username` = 'someUsername' + // ORM::Table('user')->whereUsername('someUsername'); + // EQUIVALENT TO + // ORM::Table('user')->whereUsername('someUsername', Rows::COND_EQUAL); + + // WHERE `id_user` < 100 + // ORM::Table('user')->whereIdUser(100, Rows::COND_INF); + + // WHERE `id_user` <= 100 + // ORM::Table('user')->whereIdUser(100, Rows::COND_INFEQ); + + // WHERE `id_user` > 10 + // ORM::Table('user')->whereIdUser(10, Rows::COND_SUP); + + // WHERE `id_user` >= 10 + // ORM::Table('user')->whereIdUser(10, Rows::COND_SUPEQ); + + // WHERE `id_user` in (1, 2, 3, 8) + // ORM::Table('user')->whereIdUser([1, 2, 3, 8], Rows::COND_IN); + + // WHERE `id_user` LIKE 'John %' + // ORM::Table('user')->whereIdUser('John %', Rows::COND_LIKE); + + + /*** USE CASE :: ORDER BY ****/ + // ORDER BY `a` ASC, `b` DESC + // Table::get('someTable') + // ->orderby('a', Rows::ORDER_ASC) + // ->orderby('b', Rows::ORDER_DESC); + // + // Note: `Rows::ORDER_ASC` is set by default if the given FLAG is invalid + + + /**** USE CASE :: SELECT ****/ + // SELECT id_user, username + // Table::get('user') + // ->select('id_user') + // ->select('username'); + + + /**** USE CASE :: AGGREGATION FUNCTIONS ****/ + // SELECT COUNT(`count`) + // Table::get('user')->select('count', Rows::SEL_COUNT) + + // SELECT SUM(distinct `count`) + // Table::get('user')->select('count', Rows::SEL_SUM, Rows::SEL_DISTINCT); + + // SELECT AVG(`count`) + // Table::get('user')->select('count', Rows::SEL_AVG); + + // SELECT MAX(`id_user`) + // Table::get('user')->select('id_user', Rows::SEL_MAX); + + // SELECT MIN(`id_user`) + // Table::get('user')->select('id_user', Rows::SEL_MIN); + + // SELECT GROUP_CONCAT(`count`) + // Table::get('user')->select('count', Rows::SEL_CONCAT); + + + + /**** USE CASE :: FETCH ****/ + // SELECT ... FROM ... WHERE ... ORDERBY ... LIMIT ... + // Table::get('user') + // ->select('id_user') + // ->fetch(); + + // SELECT UNIQUE ... FROM ... WHERE ... ORDERBY ... LIMIT ... + // Table::get('user') + // ->select('id_user') + // ->unique->fetch(); + + + /**** USE CASE :: TABLE JOIN ****/ + // WHERE `user`.`id_user` = `user_merge`.`id_user` + // Table::get('user_merge')->join( + // Table::get('user')->whereIdUser(1, Rows::COND_SUP) + // ); diff --git a/src/modules/router/1/core/Route.php b/src/modules/router/1/core/Route.php new file mode 100644 index 0000000..6753091 --- /dev/null +++ b/src/modules/router/1/core/Route.php @@ -0,0 +1,63 @@ + Pattern correspondant a la route + * @callback Fonction de callback de la route + * + * @return this Retour de l'instance courante + * + */ + public function __construct($pattern, $callback){ + // On enregistre la fonction de callback + $this->callback = $callback; + + // On formatte le pattern en regexp + $this->pattern = '#^'.$pattern.'$#'; + + return $this; + } + + /* Verifie si l'URL correspond a la route + * + * @url URL pour laquelle on veut verifier + * + * @return match TRUE si match sinon FAUX + * + */ + public function match($url){ + // Si ne match pas -> FALSE + if( !preg_match($this->pattern, $url, $matches) ) + return false; + + // On supprime le premier match global + array_shift($matches); + + $this->matches = $matches; + + return true; + + } + + /* Amorcage de la fonction de callback + * + */ + public function call(){ + return call_user_func($this->callback, $this->matches); + } + +} + +?> diff --git a/src/modules/router/1/core/Router.php b/src/modules/router/1/core/Router.php new file mode 100644 index 0000000..ee62743 --- /dev/null +++ b/src/modules/router/1/core/Router.php @@ -0,0 +1,91 @@ + l'URL de la page courante + * + * @return this Retour de l'instance courante + * + */ + public function __construct($url){ + $this->url = $url; + + // On initialise les routes + $this->routes = [ + 'GET' => [], + 'POST' => [] + ]; + + return $this; + } + + /* Ajoute une route GET + * + * @pattern le format de l'URL associe + * @callback function a appeler si l'URL correspond + * + * @return this Retour de l'instance courante + * + */ + public function get($pattern, $callback){ + array_push( + $this->routes['GET'], + new Route($pattern, $callback) + ); + + return $this; + } + + + /* Ajoute une route POST + * + * @pattern le format de l'URL associe + * @callback function a appeler si l'URL correspond + * + * @return this Retour de l'instance courante + * + */ + public function post($pattern, $callback){ + array_push( + $this->routes['POST'], + new Route($pattern, $callback) + ); + + return $this; + } + + /* Demarre le routeur + * + * @return this Retour de l'instance courante + * + */ + public function run(){ + $httpMethod = $_SERVER['REQUEST_METHOD']; + + // Si aucune route pour la methode courante -> false + if( count($this->routes[$httpMethod]) <= 0 ) + return false; + + // Pour chaque route + foreach($this->routes[$httpMethod] as $route){ + // Si la route match + if( $route->match($this->url) ) + return $route->call(); // On l'amorce + } + + // Retourne false si erreur + return false; + } +} + +?>