Files
DomFramework/dbjson.php

404 lines
14 KiB
PHP

<?php
/** Documentation
* - A filter is an array containing the fields and the values to found
* array ("key"=>"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
{
private $dsn = "";
private $dbfile = "";
private $dbfileLock = "";
private $lastInsertId = 0;
// The database content
private $db;
/** The constructor */
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 (_("No DSN provided to dbjson"), 500);
if (substr ($dsn, 0, $pos) !== "dbjson")
throw new \Exception (_("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 (_("Directory '%s' doesn't exists"),
$directory), 500);
if (! file_exists ($this->dbfile))
{
if (! is_readable ($directory))
throw new \Exception (sprintf (
_("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 (_("File '%s' not readable"), $this->dbfile),
500);
if (! is_writeable ($this->dbfile))
throw new \Exception(sprintf (_("File '%s' not readable"), $this->dbfile),
500);
$this->dsn = $dsn;
$this->dbfile = $this->dbfile;
}
/** Store one document in database
* @param $collection string The collection name
* @param $document array()
* @return integer return The number of document inserted in the database
*/
public function insertOne ($collection, $document)
{
$this->lockEX ();
$this->db = $this->readDB ();
$this->db[$collection]["content"][] = array_merge (array (
"_id"=>$this->uniqueKey ()),
$document);
$this->writeDB ($this->db);
$this->lockUN ();
return 1;
}
/** Store multiple documents in database
* @param $collection string The collection name
* @param $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)
{
$this->db[$collection]["content"][] = array_merge (array (
"_id"=>$this->uniqueKey ()),
$document);
}
$this->writeDB ($this->db);
$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 $collection string The collection name
* @param $filter array The filter to apply to found the documents
* @param $fields array|string The fields to display (* for all, empty array
* for none)
* @param $limit integer 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 $collection string The collection name
* @param $filter array The filter to apply to found the documents
* @param $document array 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];
$tmp = array_merge ($tmp, $document);
$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->db);
$this->lockUN ();
$this->db = null;
return count ($keys);
}
/** Replace some existing documents. Do not change the _id keys
* @param $collection string The collection name
* @param $filter array The filter to apply to found the documents
* @param $document array 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->db);
$this->lockUN ();
$this->db = null;
return count ($keys);
}
/** Delete the first document matching the filter
* @param $collection string The collection name
* @param $filter array 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 = key ($keys);
unset ($this->db[$collection]["content"][$key]);
$this->writeDB ($this->db);
$this->lockUN ();
$this->db = null;
return 1;
}
/** Delete all the documents matching the filter
* @param $collection string The collection name
* @param $filter array 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->db);
$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 $collection string The collection name
* @param $filter array 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")
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;
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 !
* @param $data array The database to store
* @return bool True if the recording is OK, false if there is a problem
*/
private function writeDB ($data)
{
return !! file_put_contents ($this->dbfile, json_encode ($data));
}
}