Stockage dans un fichier

Alexandre Niveau
GREYC — Université de Caen

Les classes FileStore et ObjectFileDB

<?php

/*
 * Une classe qui permet la persistance de données en utilisant
 * un fichier. Plus facile que d'utiliser une vraie base de données,
 * si l'application est simple et si le nombre de personnes
 * connectées en même temps est raisonnable (sinon la manipulation
 * des données ralentit, puisqu'il faut que chacun·e attende
 * son tour).
 *
 * Pour minimiser le temps d'attente, il faut faire le moins
 * de choses possibles entre un appel à loadData() et l'appel
 * à saveData() suivant (car entre les deux, le fichier est verrouillé).
 */
class FileStore {


    
/* le nom du fichier dans lequel les données sont sérialisées */
    
private $file;
    
/* le pointeur de fichier (null si le fichier n'est pas ouvert) */
    
private $fp;

    
/* Construit une nouvelle instance, qui utilise le fichier donné
     * en paramètre. */
    
private function __construct($file) {
        
$this->file $file;
        
$this->fp null;
    }

    public function 
getFileName() {
        return 
$this->file;
    }

    
/**
     * Construit une instance qui utilise le fichier donné en paramètre,
     * et initialise le contenu si besoin.
     */
    
public static function makeStore($file$defaultContent) {
//         $absfile = stream_resolve_include_path($file);
//         if ($absfile === false) {
//             throw new Exception("Could not resolve file name '$file'");
//         }
        
$store = new FileStore($file);
        
$store->initializeIfEmpty($defaultContent);
        return 
$store;
    }

    private function 
initializeIfEmpty($defaultContent) {
        
/* on verrouille le fichier avant de vérifier s'il existe */
        
$this->lockFile();
        if (
file_exists($this->file)) {
            
$content file_get_contents($this->file);
            if (
$content !== '') {
                
$this->unlockFile();
                return 
false;
            }
        }
        
$this->saveData($defaultContent);
        return 
true;
    }

    public function 
lockFile() {
        if (
$this->fp !== null) {
            
/* le fichier est déjà ouvert (et donc verrouillé) */
            
return;
        }
        
/* ouverture du fichier */
        
$this->fp fopen($this->file'ab+');
        if (
$this->fp === false) {
            throw new 
Exception("Impossible d'ouvrir le fichier '{$this->file}' en écriture");
        }
        
/* verrouillage du fichier */
//         if ( ! flock($this->fp, LOCK_EX) ) {
//             throw new Exception('Impossible de verrouiller le fichier "'
//                 . $this->file . '"');
//         }
    
}
    public function 
unlockFile() {
        if (
$this->fp === null) {
            
/* le fichier n'est pas ouvert, on n'a rien à faire */
            
return;
        }
        
/* on libère le verrou et on ferme le fichier */
//         flock($this->fp, LOCK_UN);
        
fclose($this->fp);
        
$this->fp null;
    }

    
/*
      * Renvoie les données stockées dans le fichier,
      * et verrouille le fichier (si ce n'est pas déjà fait).
      */
    
public function loadData() {
        
/* Ouverture et verrouillage du fichier */
        
$this->lockFile();
        
/* on lit le contenu */
        
$content file_get_contents($this->file);
        
/* on désérialise le contenu,
         * pour récupérer ce qui y a été stocké  */
        
$data unserialize(base64_decode($content));
        if (
$data === false) {
            throw new 
Exception("Could not unserialize data!");
        }
        return 
$data;
    }

    
/*
      * Écrit les données dans le fichier, et libère le verrou.
      */
    
public function saveData($data) {
        
/* on verrouille le fichier si besoin */
        
$this->lockFile();
        if (
$data === false) {
            throw new 
Exception("Cannot save constant FALSE");
        }
        
/* on sérialise le tableau */
        
$content base64_encode(serialize($data));
        
/* on remet le curseur au début */
        
ftruncate($this->fp0);
        
/* on écrit le tableau sérialisé */
        
fwrite($this->fp$content);
        
/* on libère le verrou et on ferme le fichier */
        
$this->unlockFile();
    }

    
/*
     * Lors de la destruction de cette instance, on libère le verrou,
     * au cas où ça n'a pas été fait.
     */
    
public function __destruct() {
        
$this->unlockFile();
    }
}

/*
 * Une classe qui permet de stocker facilement des objets
 * dans un tableau stocké dans un fichier (à l'aide de la classe
 * FileStore). Les méthodes sont proches de l'utilisation
 * d'une table dans une BD.
 */
class ObjectFileDB {

    
/* Le FileStore qui permet la persistance des données. */
    
private $file_store;

    
/* Construit une nouvelle instance, qui utilise le fichier donné
     * en paramètre. Si le fichier n'existe pas, une base vide
     * est créée automatiquement. */
    
public function __construct($file) {
        
$this->file_store FileStore::makeStore($file, array());
    }

    
/* Génère un nouvel identifiant aléatoire qui n'existe pas
     * encore dans la BD donnée en paramètre. */
    
static private function generate_id($db) {
        do {
            
/* implémentation simple avec un générateur de relativement
             * bonne qualité ; mais les identifiants sont longs si
             * on veut en avoir beaucoup. (avec 8 octets on en a seulement
             * 10^20 environ -- c'est pas mal, mais pas gigantesque
             * en termes de probabilité de collision lors de la génération) */
            
$id bin2hex(openssl_random_pseudo_bytes(8));

            
/* on recommence le tirage si le premier caractère est un chiffre
             * (pour éviter les problèmes d'interprétation de chaînes en
             * nombres avec PHP) ou si l'identifiant est déjà utilisé */
        
} while (is_numeric($id[0]) || key_exists($id$db));

        return 
$id;
    }

    private function 
loadArray() {
        
$data $this->file_store->loadData();
        if ( ! 
is_array($data) ) {
            throw new 
Exception("File '".$this->file_store->getFileName()."' does not contain an array; maybe it was corrupted?");
        }
        return 
$data;
    }

    
/* Renvoie true si l'identifiant existe dans la base. */
    
public function exists($id) {
        
$db $this->loadArray();
        
$this->file_store->unlockFile();
        return 
key_exists($id$db);
    }

    
/* Insère un objet dans la base. Renvoie l'identifiant
     * aléatoire qui lui a été attribué.
     */
    
public function insert($obj) {
        
$db $this->loadArray();
        
$id self::generate_id($db);
        
$db[$id] = $obj;
        
$this->file_store->saveData($db);
        return 
$id;
    }

    
/* Renvoie l'objet d'identifiant $id.
     * Lance une exception si l'identifiant est inconnu.
     */
    
public function fetch($id) {
        
$db $this->loadArray();
        
$this->file_store->unlockFile();
        if ( ! 
key_exists($id$db)) {
            throw new 
Exception("Key does not exist");
        }
        return 
$db[$id];
    }

    
/* Renvoie tous les objets de la base dans un tableau
     * associatif { identifiant => objet }.
     */
    
public function fetchAll() {
        
$db $this->loadArray();
        
$this->file_store->unlockFile();
        return 
$db;
    }

    
/* Supprime l'objet d'identifiant $id de la base.
     * Lance une exception si l'identifiant est inconnu.
     */
    
public function delete($id) {
        
$db $this->loadArray();
        if ( ! 
key_exists($id$db)) {
            throw new 
Exception("Key does not exist");
        }
        unset(
$db[$id]);
        
$this->file_store->saveData($db);
    }

    
/* Remplace l'objet d'identifiant $id dans la base
     * par celui passé en paramètre.
     * Lance une exception si l'identifiant est inconnu.
     */
    
public function update($id$obj) {
        
$db $this->loadArray();
        if ( ! 
key_exists($id$db)) {
            throw new 
Exception("Key does not exist");
        }
        
$db[$id] = $obj;
        
$this->file_store->saveData($db);
    }

    
/* Supprime tous les objets de la base.
     */
    
public function deleteAll() {
        
$this->file_store->saveData(array());
    }

}

?>