434 lines
12 KiB
PHP
434 lines
12 KiB
PHP
<?php
|
||
|
||
namespace lightdb\core;
|
||
|
||
|
||
class lightdb{
|
||
|
||
// REPERTOIRE RACINE DE TOUTES LES BDD
|
||
public static function default_root(){ return __BUILD__.'/lightdb/storage/'; }
|
||
|
||
// ATTRIBUTS
|
||
private $root;
|
||
private $dbname;
|
||
private $dir;
|
||
private $index;
|
||
private $driver;
|
||
private $line;
|
||
|
||
private $tmp;
|
||
|
||
/* CONSTRUCTEUR -> CREER LES FICHIERS S'ILS N'EXISTENT PAS SINON, RECUPERE LES DONNES
|
||
*
|
||
* @dbname<String> 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<String> Index pour lequel on veut la ligne et le hash
|
||
*
|
||
* @return Index<Array> 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<String> Clé qui permettra l'accès direct
|
||
* @data<mixed*> Objet qui sera enregistré dans la base
|
||
*
|
||
* @return status<Boolean> 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<Array> Tableau de 'clés'->'valeurs' à insérer
|
||
* @data<mixed*> Objet qui sera enregistré dans la base
|
||
*
|
||
* @return status<Boolean> 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<String> Clé associée à la valeur à récupérer
|
||
*
|
||
* @return data<mixed*> 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<Array> Clés associées aux valeurs à récupérer
|
||
*
|
||
* @return data<mixed*> 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<String> Clé de l'entrée à supprimer
|
||
*
|
||
* @return status<Boolean> 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 = __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<Array> Clés des entrées à supprimer
|
||
*
|
||
* @return status<Boolean> 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 = __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<typeParam> Description du param
|
||
*
|
||
* @return nomRetour<typeRetour> 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; }
|
||
|
||
|
||
|
||
}
|