"val") <== Look for the key equal val * array ("key=>array ("val", "<=")) <== Look for the key lighter or equal * than val * array () <== Look for all the documents (no * filter) * array ("key"=>"val", "key2"=>"val2") <== Look for two parameters * - A document is an array containing the fields and the values to store * array ("key"=>"val) * * - The field named _id is the document key */ /** DBJSON : a NoSQL DB in JSON */ class dbjson { /** The DSN of the connection */ private $dsn = ""; /** The database file */ private $dbfile = ""; /** The lock file */ private $dbfileLock = ""; /** The last Insert Id */ private $lastInsertId = 0; /** The database content */ private $db; /** The constructor * @param string $dsn The DSN of the connection */ public function __construct ($dsn) { if (! function_exists ("openssl_random_pseudo_bytes")) throw new \Exception ("Function openssl_random_pseudo_bytes missing", 500); $pos = strpos ($dsn, "://"); if ($pos === false) throw new \Exception (dgettext ("domframework", "No DSN provided to dbjson"), 500); if (substr ($dsn, 0, $pos) !== "dbjson") throw new \Exception (dgettext ("domframework", "Invalid database type provided in dbjson"), 500); $this->dbfile = substr ($dsn, $pos+3); $directory = dirname ($this->dbfile); if (! file_exists ($directory)) @mkdir ($directory, 0777, true); if (! file_exists ($directory)) throw new \Exception (sprintf (dgettext ("domframework", "Directory '%s' doesn't exists"), $directory), 500); if (! file_exists ($this->dbfile)) { if (! is_readable ($directory)) throw new \Exception (sprintf ( dgettext ("domframework", "Directory '%s' not writeable and dbfile '%s' not exists"), $directory, $this->dbfile), 500); touch ($this->dbfile); } if (! is_readable ($this->dbfile)) throw new \Exception(sprintf (dgettext ("domframework", "File '%s' not readable"), $this->dbfile), 500); if (! is_writeable ($this->dbfile)) throw new \Exception(sprintf (dgettext ("domframework", "File '%s' not readable"), $this->dbfile), 500); $this->dsn = $dsn; $this->dbfile = $this->dbfile; } /** Store one document in database * @param string $collection The collection name * @param array $document The document to insert * @return integer return The number of document inserted in the database */ public function insertOne ($collection, $document) { $uniqueKey = $this->uniqueKey (); $this->lockEX (); $this->db = $this->readDB (); $this->db[$collection]["content"][$uniqueKey] = array_merge ( array ("_id"=>$uniqueKey), $document); $this->writeDB (); $this->lockUN (); return 1; } /** Store multiple documents in database * @param string $collection The collection name * @param array $documents array(array ()) * @return integer The number of documents inserted in the database */ public function insertMany ($collection, $documents) { foreach ($documents as $document) if (! is_array ($document)) throw new \Exception ("insertMany need an array of array", 406); $this->lockEX (); $this->db = $this->readDB (); foreach ($documents as $document) { $uniqueKey = $this->uniqueKey (); $this->db[$collection]["content"][$uniqueKey] = array_merge ( array ("_id"=>$uniqueKey), $document); } $this->writeDB (); $this->db = null; $this->lockUN (); return count ($documents); } /** Look at the documents matching $filter (all by default). * Then return only the $fields (all by default). * The field _id is always returned * Return $limit maximum documents (no limit by default) * @param string $collection The collection name * @param array $filter The filter to apply to found the documents * @param array|string $fields The fields to display (* for all, empty array * for none) * @param integer $limit The number of documents to display * @return array The documents matching the parameters */ public function find ($collection, $filter = array (), $fields = "*", $limit = null) { $this->lockSH (); $this->db = $this->readDB (); // Get the keys of the documents based on the filter $keys = $this->filter ($collection, $filter); $res = array (); foreach ($keys as $key) { // Limit the fields $tmp = array (); if ($fields === "*") $tmp = $this->db[$collection]["content"][$key]; elseif (is_array ($fields)) { if (! in_array ("_id", $fields)) array_unshift ($fields, "_id"); foreach ($fields as $field) { if (array_key_exists ($field, $this->db[$collection]["content"][$key])) $tmp[$field] = $this->db[$collection]["content"][$key][$field]; } } else throw new \Exception ("Invalid field list provided", 500); $res[$key] = $tmp; // Limit the number of results if ($limit !== null && count ($res) >= $limit) break; } $this->lockUN (); $this->db = null; return $res; } /** Update some existing documents. Do not change the _id keys * @param string $collection The collection name * @param array $filter The filter to apply to found the documents * @param array $document The data to update * @return integer The number of modified documents * To unset a field, add in the document array a "_unset"=>array("field)" */ public function update ($collection, $filter, $document) { $this->lockEX (); $this->db = $this->readDB (); // Get the keys of the documents based on the filter $keys = $this->filter ($collection, $filter); $unset = array (); if (array_key_exists ("_unset", $document)) { $unset = $document["_unset"]; unset ($document["_unset"]); } foreach ($keys as $key) { // Merge the new document with the old $tmp = $this->db[$collection]["content"][$key]; // We need to merge the old and the new document. // If there is an array in the document, it is overwrited foreach ($document as $k=>$v) { if (is_array ($v)) { if (isset ($tmp[$k])) $tmp[$k] = array_merge ($tmp[$k], $v); else $tmp[$k] = $v; } else { $tmp[$k] = $v; } } $this->db[$collection]["content"][$key] = $tmp; // Remove the needed unset fields foreach ($unset as $field) if (array_key_exists ($field, $this->db[$collection]["content"][$key])) unset ($this->db[$collection]["content"][$key][$field]); } $this->writeDB (); $this->lockUN (); $this->db = null; return count ($keys); } /** Replace some existing documents. Do not change the _id keys * @param string $collection The collection name * @param array $filter The filter to apply to found the documents * @param array $document The data to update * @return integer The number of modified documents */ public function replace ($collection, $filter, $document) { $this->lockEX (); $this->db = $this->readDB (); // Get the keys of the documents based on the filter $keys = $this->filter ($collection, $filter); foreach ($keys as $key) { $tmp = $this->db[$collection]["content"][$key]; $replace = array (); $replace["_id"] = $tmp["_id"]; $replace = array_merge ($replace, $document); $this->db[$collection]["content"][$key] = $replace; } $this->writeDB (); $this->lockUN (); $this->db = null; return count ($keys); } /** Delete the first document matching the filter * @param string $collection The collection name * @param array $filter The filter to found the documents * @return integer The number of deleted documents */ public function deleteOne ($collection, $filter) { $this->lockEX (); $this->db = $this->readDB (); // Get the keys of the documents based on the filter $keys = $this->filter ($collection, $filter); if (count ($keys) === 0) return 0; reset ($keys); $key = reset ($keys); unset ($this->db[$collection]["content"][$key]); $this->writeDB (); $this->lockUN (); $this->db = null; return 1; } /** Delete all the documents matching the filter * @param string $collection The collection name * @param array $filter The filter to apply to found the documents * @return integer The number of deleted documents */ public function deleteMany ($collection, $filter) { $this->lockEX (); $this->db = $this->readDB (); // Get the keys of the documents based on the filter $keys = $this->filter ($collection, $filter); foreach ($keys as $key) unset ($this->db[$collection]["content"][$key]); $this->writeDB (); $this->lockUN (); $this->db = null; return count ($keys); } /** Look for the keys corresponding to the filter in the collection * Don't manage the locks ! * @param string $collection The collection name * @param array $filter The filter to apply to found the documents * - A filter is an array containing the fields and the values to found * array () <== Look for all the documents (no * filter) * array ("key"=>"val") <== Look for the key equal val * array ("key=>array ("val", "<=")) <== Look for the key lighter or * equal than val * array ("key"=>"val", "key2"=>"val2") <== Look for two parameters * array ("key"=>array ("val", "=="), * "key2"=>array ("val2", "==")) <== Look for two complex parameters * Here is the comparison types available : ==, * @return array the keys matching the filter */ public function filter ($collection, $filter) { if ($this->db === null) $this->db = $this->readDB (); $keys = array (); if (! array_key_exists ($collection, $this->db)) $this->db[$collection]["content"] = array (); foreach ($this->db[$collection]["content"] as $key=>$document) { if ($filter === array ()) { $keys[] = $key; continue; } $matchFilter = false; foreach ($filter as $fkey=>$fvals) { if (is_array ($fvals)) { // $fvals = array ("key=>array ("val", "<=")) if (array_key_exists ($fkey, $document)) { if ($fvals[1] !== "==" && $fvals[1] !== "<=" && $fvals[1] !== ">=" && $fvals[1] !== "<" && $fvals[1] !== ">" && $fvals[1] !== "exists" && $fvals[1] !== "not exists" && $fvals[1] !== "in_array") throw new \Exception ("Invalid filter operator provided", 500); if ($fvals[1] === "==" && $document[$fkey] === $fvals[0]) $matchFilter = true; elseif ($fvals[1] === "<=" && $document[$fkey] <= $fvals[0]) $matchFilter = true; elseif ($fvals[1] === ">=" && $document[$fkey] >= $fvals[0]) $matchFilter = true; elseif ($fvals[1] === "<" && $document[$fkey] < $fvals[0]) $matchFilter = true; elseif ($fvals[1] === ">" && $document[$fkey] > $fvals[0]) $matchFilter = true; elseif (strtolower ($fvals[1]) === "exists" && array_key_exists ($fkey, $document)) $matchFilter = true; elseif (strtolower ($fvals[1]) === "not exists" && ! array_key_exists ($fkey, $document)) $matchFilter = true; elseif (strtolower ($fvals[1]) === "in_array" && in_array ($fvals[0], $document[$fkey])) $matchFilter = true; else { $matchFilter = false; break; } } } else { // $fvals = array ("key"=>"val") if (array_key_exists ($fkey, $document) && $document[$fkey] === $fvals) $matchFilter = true; else { $matchFilter = false; break; } } } if ($matchFilter === true) $keys[] = $key; } return $keys; } /** Generate a unique key * @return string the Unique key generated */ private function uniqueKey () { $data = openssl_random_pseudo_bytes(16); $data[6] = chr(ord($data[6]) & 0x0f | 0x40); // set version to 0010 $data[8] = chr(ord($data[8]) & 0x3f | 0x80); // set bits 6-7 to 10 return vsprintf('%s%s-%s-%s-%s-%s%s%s', str_split(bin2hex($data), 4)); } /** Exclusive lock the database file */ private function lockEX () { $this->dbfileLock = fopen ($this->dbfile, "rt"); if (flock ($this->dbfileLock, LOCK_EX) === false) throw new \Exception ("Can't get exclusive lock on dbfile", 500); } /** Shared lock the database file */ private function lockSH () { $this->dbfileLock = fopen ($this->dbfile, "rt"); if (flock ($this->dbfileLock, LOCK_SH) === false) throw new \Exception ("Can't get shared lock on dbfile", 500); } /** Unlock the database file */ private function lockUN () { if ($this->dbfileLock !== null) { flock ($this->dbfileLock, LOCK_UN); fclose ($this->dbfileLock); $this->dbfileLock = null; } } /** Read the dbfile and return an array containing the data. This function * don't do locks ! * @return array The database content from the dbfile */ private function readDB () { $res = json_decode (file_get_contents ($this->dbfile), true); if ($res === null) $res = array (); return $res; } /** Write the dbfile with the provided data. This function don't do locks ! * @return bool True if the recording is OK, false if there is a problem */ private function writeDB () { return !! file_put_contents ($this->dbfile, json_encode ($this->db)); } }