Files
DomFramework/src/Dbjson.php

511 lines
17 KiB
PHP

<?php
/**
* DomFramework
* @package domframework
* @author Dominique Fournier <dominique@fournier38.fr>
* @license BSD
*/
namespace Domframework;
/**
* DBJSON : a NoSQL DB in JSON
* 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
*/
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(
["_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(
["_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 = [],
$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 = [];
foreach ($keys as $key) {
// Limit the fields
$tmp = [];
if ($fields === "*") {
$tmp = $this->db[$collection]["content"][$key];
} elseif (is_array($fields)) {
if (! in_array("_id", $fields, true)) {
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 = [];
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 = [];
$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 = [];
if (! array_key_exists($collection, $this->db)) {
$this->db[$collection]["content"] = [];
}
foreach ($this->db[$collection]["content"] as $key => $document) {
if ($filter === []) {
$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], true)
) {
$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 = [];
}
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));
}
}