filebase install WIP

This commit is contained in:
fredtempez 2022-03-25 11:51:01 +01:00
parent 91266119de
commit 9a4fd20017
22 changed files with 2930 additions and 0 deletions

View File

@ -0,0 +1,192 @@
<?php namespace Filebase;
class Backup
{
/**
* $backupLocation
*
* Current backup location..
* $backupLocation
*/
protected $backupLocation;
/**
* $config
*
* Stores all the configuration object settings
* \Filebase\Config
*/
protected $config;
/**
* $database
*
* \Filebase\Database
*/
protected $database;
/**
* __construct
*
*/
public function __construct($backupLocation = '', Database $database)
{
$this->backupLocation = $backupLocation;
$this->config = $database->getConfig();
$this->database = $database;
// Check directory and create it if it doesn't exist
if (!is_dir($this->backupLocation))
{
if (!@mkdir($this->backupLocation, 0777, true))
{
throw new \Exception(sprintf('`%s` doesn\'t exist and can\'t be created.', $this->backupLocation));
}
}
else if (!is_writable($this->backupLocation))
{
throw new \Exception(sprintf('`%s` is not writable.', $this->backupLocation));
}
}
/**
* save()
*
*/
public function create()
{
$backupFile = $this->backupLocation.'/'.time().'.zip';
if ($results = $this->zip($this->config->dir, $backupFile))
{
$basename = basename($backupFile);
return $basename;
}
throw new \Exception('Error backing up database.');
}
/**
* find()
*
* Returns an array of all the backups currently available
*
*/
public function find()
{
$backups = [];
$files = glob(realpath($this->backupLocation)."/*.zip");
foreach($files as $file)
{
$basename = str_replace('.zip','',basename($file));
$backups[$basename] = $this->backupLocation.'/'.$basename.'.zip';
}
krsort($backups);
return $backups;
}
/**
* clean()
*
* Clears and deletes all backups (zip files only)
*
*/
public function clean()
{
return array_map('unlink', glob(realpath($this->backupLocation)."/*.zip"));
}
/**
* rollback()
*
* Rollback database to the last backup available
*
*/
public function rollback()
{
$backups = $this->find();
$restore = current($backups);
$this->database->truncate();
return $this->extract($restore, $this->config->dir);
}
/**
* extract()
*
* @param string $source (zip location)
* @param string $target (unload files to location)
* @return bool
*/
protected function extract($source = '', $target = '')
{
if (!extension_loaded('zip') && !file_exists($source))
{
return false;
}
$zip = new \ZipArchive();
if ($zip->open($source) === TRUE)
{
$zip->extractTo($target);
$zip->close();
return true;
}
return false;
}
/**
* zip()
*
* Prevents the zip from zipping up the storage diretories
*
*/
protected function zip($source = '', $target = '')
{
if (!extension_loaded('zip') || !file_exists($source))
{
return false;
}
$zip = new \ZipArchive();
if (!$zip->open($target, \ZIPARCHIVE::CREATE))
{
$zip->addFromString(basename($source), file_get_contents($source));
}
$source = realpath($source);
if (is_dir($source))
{
$iterator = new \RecursiveDirectoryIterator($source);
$iterator->setFlags(\RecursiveDirectoryIterator::SKIP_DOTS);
$files = new \RecursiveIteratorIterator($iterator, \RecursiveIteratorIterator::SELF_FIRST);
foreach ($files as $file)
{
$file = realpath($file);
if (preg_match('|'.realpath($this->backupLocation).'|',$file))
{
continue;
}
if (is_dir($file))
{
$zip->addEmptyDir(str_replace($source . '/', '', $file . '/'));
}
else if (is_file($file))
{
$zip->addFromString(str_replace($source . '/', '', $file), file_get_contents($file));
}
}
}
return $zip->close();
}
}

View File

@ -0,0 +1,145 @@
<?php namespace Filebase;
class Cache
{
/**
* $database
*
* \Filebase\Database
*/
protected $database;
/**
* $cache_database
*
* \Filebase\Database
*/
protected $cache_database;
/**
* $key
*
*/
protected $key;
/**
* __construct()
*
*/
public function __construct(Database $database)
{
$this->database = $database;
$this->cache_database = new \Filebase\Database([
'dir' => $this->database->getConfig()->dir.'/__cache',
'cache' => false,
'pretty' => false
]);
}
/**
* setKey()
*
* This key is used to identify the cache
* and know how to call the cache again
*
*/
public function setKey($key)
{
$this->key = md5($key);
}
/**
* getKey()
*
*/
public function getKey()
{
return $this->key;
}
/**
* flush()
*
*/
public function flush()
{
$this->cache_database->flush(true);
}
/**
* expired()
*
* @param $time (date format)
* @return bool (true/false)
*/
public function expired($time)
{
if ( (strtotime($time)+$this->database->getConfig()->cache_expires) > time() )
{
return false;
}
return true;
}
/**
* getDocuments()
*
*/
public function getDocuments($documents)
{
$d = [];
foreach($documents as $document)
{
$d[] = $this->database->get($document)->setFromCache(true);
}
return $d;
}
/**
* get()
*
*/
public function get()
{
if (!$this->getKey())
{
throw new \Exception('You must supply a cache key using setKey to get cache data.');
}
$cache_doc = $this->cache_database->get( $this->getKey() );
if (!$cache_doc->toArray())
{
return false;
}
if ( $this->expired( $cache_doc->updatedAt() ) )
{
return false;
}
return $this->getDocuments($cache_doc->toArray());
}
/**
* store()
*
*/
public function store($data)
{
if (!$this->getKey())
{
throw new \Exception('You must supply a cache key using setKey to store cache data.');
}
return $this->cache_database->get( $this->getKey() )->set($data)->save();
}
}

View File

@ -0,0 +1,125 @@
<?php namespace Filebase;
class Config
{
/**
* $dir
* Database Directory
* Where to store information
*/
public $dir = __DIR__;
/**
* $format
* Format Class
* Must implement Format\FormatInterface
*/
public $format = \Filebase\Format\Json::class;
/**
* $cache
* Caching for queries
*
* default true
*/
public $cache = true;
/**
* $cache_time
* When should cache be cleared?
*
* default (1800 seconds) 30 minutes
*/
public $cache_expires = 1800;
/**
* $safe_filename
* (if true) Be sure to automatically change the file name if it does not fit validation
* (if false) File names that are not valid will thrown an error.
*
* default true
*/
public $safe_filename = true;
/**
* $read_only
* (if true) We will not attempt to create the database directory or allow the user to create anything
* (if false) Functions as normal
*
* default false
*/
public $read_only = false;
/**
* $backupLocation
* The location to store backups
*
* default current location
*/
public $backupLocation = '';
/**
* $pretty
*
* if true, saves the data as human readable
* Otherwise, its difficult to understand.
*
* default true
*/
public $pretty = true;
/**
* $validate
*
*/
public $validate = [];
/**
* __construct
*
* This sets all the config variables (replacing its defaults)
*/
public function __construct($config)
{
// let's define all our config variables
foreach ($config as $key => $value)
{
$this->{$key} = $value;
}
// if "backupLocation" is not set, let's set one automatically
if (!isset($config['backupLocation']))
{
$this->backupLocation = $this->dir.'/backups';
}
$this->validateFormatClass();
}
/**
* format
*
* kind of a quick fix since we are using static methods,
* currently need to instantiate teh class to check instanceof why??
*
* Checks the format of the database being accessed
*/
protected function validateFormatClass()
{
if (!class_exists($this->format))
{
throw new \Exception('Filebase Error: Missing format class in config.');
}
// instantiate the format class
$format_class = new $this->format;
// check now if that class is part of our interface
if (!$format_class instanceof \Filebase\Format\FormatInterface)
{
throw new \Exception('Filebase Error: Format Class must be an instance of Filebase\Format\FormatInterface');
}
}
}

View File

@ -0,0 +1,432 @@
<?php namespace Filebase;
use Exception;
use Filebase\Format\EncodingException;
use Filebase\Filesystem\SavingException;
use Filebase\Filesystem\ReadingException;
use Filebase\Filesystem\FilesystemException;
class Database
{
/**
* VERSION
*
* Stores the version of Filebase
* use $db->getVersion()
*/
const VERSION = '1.0.24';
/**
* $config
*
* Stores all the configuration object settings
* \Filebase\Config
*/
protected $config;
/**
* Database constructor.
*
* @param array $config
*
* @throws FilesystemException
*/
public function __construct(array $config = [])
{
$this->config = new Config($config);
// if we are set to read only, don't care to look at the directory.
if ($this->config->read_only === true) return false;
// Check directory and create it if it doesn't exist
if (!is_dir($this->config->dir))
{
if (!@mkdir($this->config->dir, 0777, true))
{
throw new FilesystemException(sprintf('`%s` doesn\'t exist and can\'t be created.', $this->config->dir));
}
}
else if (!is_writable($this->config->dir))
{
throw new FilesystemException(sprintf('`%s` is not writable.', $this->config->dir));
}
}
/**
* version
*
* gets the Filebase version
*
* @return VERSION
*/
public function version()
{
return self::VERSION;
}
/**
* findAll()
*
* Finds all documents in database directory.
* Then returns you a list of those documents.
*
* @param bool $include_documents (include all document objects in array)
* @param bool $data_only (if true only return the documents data not the full object)
*
* @return array $items
*/
public function findAll($include_documents = true, $data_only = false)
{
$format = $this->config->format;
$file_extension = $format::getFileExtension();
$file_location = $this->config->dir.'/';
$all_items = Filesystem::getAllFiles($file_location, $file_extension);
if (!$include_documents)
{
return $all_items;
}
$items = [];
foreach($all_items as $a)
{
if ($data_only === true)
{
$items[] = $this->get($a)->getData();
}
else
{
$items[] = $this->get($a);
}
}
return $items;
}
/**
* get
*
* retrieves a single result (file)
*
* @param mixed $id
*
* @return $document \Filebase\Document object
*/
public function get($id)
{
$content = $this->read($id);
$document = new Document($this);
$document->setId($id);
if ($content)
{
if (isset($content['__created_at'])) $document->setCreatedAt($content['__created_at']);
if (isset($content['__updated_at'])) $document->setUpdatedAt($content['__updated_at']);
$this->set($document,(isset($content['data']) ? $content['data'] : []));
}
return $document;
}
/**
* has
*
* Check if a record already exists
*
* @param mixed $id
*
* @return bool true/false
*/
public function has($id)
{
$format = $this->config->format;
$record = Filesystem::read( $this->config->dir.'/'.Filesystem::validateName($id, $this->config->safe_filename).'.'.$format::getFileExtension() );
return $record ? true : false;
}
/**
* backup
*
* @param string $location (optional)
*
* @return $document \Filebase\Backup object
*/
public function backup($location = '')
{
if ($location)
{
return new Backup($location, $this);
}
return new Backup($this->config->backupLocation, $this);
}
/**
* set
*
* @param $document \Filebase\Document object
* @param mixed $data should be an array
*
* @return $document \Filebase\Document object
*/
public function set(Document $document, $data)
{
if ($data)
{
foreach($data as $key => $value)
{
if (is_array($value)) $value = (array) $value;
$document->{$key} = $value;
}
}
return $document;
}
/**
* count
*
*
* @return int $total
*/
public function count()
{
return count($this->findAll(false));
}
/**
* @param Document $document
* @param string $wdata
* @return bool|Document
* @throws SavingException
*/
public function save(Document $document, $wdata = '')
{
if ($this->config->read_only === true)
{
throw new SavingException("This database is set to be read-only. No modifications can be made.");
}
$format = $this->config->format;
$id = $document->getId();
$file_extension = $format::getFileExtension();
$file_location = $this->config->dir.'/'.Filesystem::validateName($id, $this->config->safe_filename).'.'.$file_extension;
$created = $document->createdAt(false);
if (isset($wdata) && $wdata !== '')
{
$document = new Document( $this );
$document->setId($id);
$document->set($wdata);
$document->setCreatedAt($created);
}
if (!Filesystem::read($file_location) || $created==false)
{
$document->setCreatedAt(time());
}
$document->setUpdatedAt(time());
try {
$data = $format::encode( $document->saveAs(), $this->config->pretty );
} catch (EncodingException $e) {
// TODO: add logging
throw new SavingException("Can not encode document.", 0, $e);
}
if (Filesystem::write($file_location, $data))
{
$this->flushCache();
return $document;
}
return false;
}
/**
* query
*
*
*/
public function query()
{
return new Query($this);
}
/**
* Read and return Document from filesystem by name.
* If doesn't exists return new empty Document.
*
* @param $name
*
* @throws Exception|ReadingException
* @return array|null
*/
protected function read($name)
{
$format = $this->config->format;
$file = Filesystem::read(
$this->config->dir . '/'
. Filesystem::validateName($name, $this->config->safe_filename)
. '.' . $format::getFileExtension()
);
if ($file !== false) {
return $format::decode($file);
}
return null;
}
/**
* delete
*
* @param $document \Filebase\Document object
* @return (bool) true/false if file was deleted
*/
public function delete(Document $document)
{
if ($this->config->read_only === true)
{
throw new Exception("This database is set to be read-only. No modifications can be made.");
}
$format = $this->config->format;
$d = Filesystem::delete($this->config->dir.'/'.Filesystem::validateName($document->getId(), $this->config->safe_filename).'.'.$format::getFileExtension());
$this->flushCache();
return $d;
}
/**
* truncate
*
* Alias for flush(true)
*
* @return @see flush
*/
public function truncate()
{
return $this->flush(true);
}
/**
* flush
*
* This will DELETE all the documents within the database
*
* @param bool $confirm (confirmation before proceeding)
* @return void
*/
public function flush($confirm = false)
{
if ($this->config->read_only === true)
{
throw new Exception("This database is set to be read-only. No modifications can be made.");
}
if ($confirm!==true)
{
throw new Exception("Database Flush failed. You must send in TRUE to confirm action.");
}
$format = $this->config->format;
$documents = $this->findAll(false);
foreach($documents as $document)
{
Filesystem::delete($this->config->dir.'/'.$document.'.'.$format::getFileExtension());
}
if ($this->count() === 0)
{
return true;
}
throw new Exception("Could not delete all database files in ".$this->config->dir);
}
/**
* flushCache
*
*
*/
public function flushCache()
{
if ($this->getConfig()->cache===true)
{
$cache = new Cache($this);
$cache->flush();
}
}
/**
* toArray
*
* @param \Filebase\Document
* @return array
*/
public function toArray(Document $document)
{
return $this->objectToArray( $document->getData() );
}
/**
* objectToArray
*
*/
public function objectToArray($obj)
{
if (!is_object($obj) && !is_array($obj))
{
return $obj;
}
$arr = [];
foreach ($obj as $key => $value)
{
$arr[$key] = $this->objectToArray($value);
}
return $arr;
}
/**
* getConfig
*
* @return $config
*/
public function getConfig()
{
return $this->config;
}
/**
* __call
*
* Magic method to give us access to query methods on db class
*
*/
public function __call($method,$args)
{
if(method_exists($this,$method)) {
return $this->$method(...$args);
}
if(method_exists(Query::class,$method)) {
return (new Query($this))->$method(...$args);
}
throw new \BadMethodCallException("method {$method} not found on 'Database::class' and 'Query::class'");
}
}

View File

@ -0,0 +1,393 @@
<?php namespace Filebase;
class Document
{
private $__database;
private $__id;
private $__created_at;
private $__updated_at;
private $__cache = false;
private $data = [];
/**
* __construct
*
* Sets the database property
*/
public function __construct($database)
{
$this->__database = $database;
}
/**
* saveAs
*
*/
public function saveAs()
{
$data = (object) [];
$vars = get_object_vars($this);
foreach($vars as $k=>$v)
{
if (in_array($k,['__database','__id','__cache'])) continue;
$data->{$k} = $v;
}
return $data;
}
/**
* save()
*
* Saving the document to disk (file)
*
* @param mixed $data (optional, only if you want to "replace" entire doc data)
* @return @see \Filebase\Database save()
*/
public function save($data = '')
{
Validate::valid($this);
return $this->__database->save($this, $data);
}
/**
* delete
*
* Deletes document from disk (file)
*
* @return @see \Filebase\Database delete()
*/
public function delete()
{
return $this->__database->delete($this);
}
/**
* set
*
*/
public function set($data)
{
return $this->__database->set($this, $data);
}
/**
* toArray
*
*/
public function toArray()
{
return $this->__database->toArray($this);
}
/**
* __set
*
*/
public function __set($name, $value)
{
$this->data[$name] = $value;
}
/**
* __get
*
*/
public function &__get($name)
{
if (!array_key_exists($name, $this->data))
{
$this->data[$name] = null;
}
return $this->data[$name];
}
/**
* __isset
*
*/
public function __isset($name)
{
return isset($this->data[$name]);
}
/**
* __unset
*
*/
public function __unset($name)
{
unset($this->data[$name]);
}
//--------------------------------------------------------------------
/**
* filter
*
* Alias of customFilter
*
* @see customFilter
*/
public function filter($field = 'data', $paramOne = '', $paramTwo = '')
{
return $this->customFilter($field, $paramOne, $paramTwo);
}
/**
* customFilter
*
* Allows you to run a custom function around each item
*
* @param string $field
* @param callable $function
* @return array $r items that the callable function returned
*/
public function customFilter($field = 'data', $paramOne = '', $paramTwo = '')
{
$items = $this->field($field);
if (is_callable($paramOne))
{
$function = $paramOne;
$param = $paramTwo;
}
else
{
if (is_callable($paramTwo))
{
$function = $paramTwo;
$param = $paramOne;
}
}
if (!is_array($items) || empty($items))
{
return [];
}
$r = [];
foreach($items as $index => $item)
{
$i = $function($item, $param);
if ($i!==false && !is_null($i))
{
$r[$index] = $i;
}
}
$r = array_values($r);
return $r;
}
/**
* getDatabase
*
* @return $database
*/
public function getDatabase()
{
return $this->__database;
}
/**
* getId
*
* @return mixed $__id
*/
public function getId()
{
return $this->__id;
}
/**
* getData
*
* @return mixed data
*/
public function getData()
{
return $this->data;
}
/**
* setId
*
* @param mixed $id
*/
public function setId($id)
{
$this->__id = $id;
return $this;
}
/**
* setCache
*
* @param boolean $cache
*/
public function setFromCache($cache = true)
{
$this->__cache = $cache;
return $this;
}
/**
* isCache
*
*/
public function isCache()
{
return $this->__cache;
}
/**
* createdAt
*
* When this document was created (or complete replaced)
*
* @param string $format php date format (default Y-m-d H:i:s)
* @return string date format
*/
public function createdAt($format = 'Y-m-d H:i:s')
{
if (!$this->__created_at)
{
return date($format);
}
if ($format !== false)
{
return date($format, $this->__created_at);
}
return $this->__created_at;
}
/**
* updatedAt
*
* When this document was updated
*
* @param string $format php date format (default Y-m-d H:i:s)
* @return string date format
*/
public function updatedAt($format = 'Y-m-d H:i:s')
{
if (!$this->__updated_at)
{
return date($format);
}
if ($format !== false)
{
return date($format, $this->__updated_at);
}
return $this->__updated_at;
}
/**
* setCreatedAt
*
* @param int $created_at php time()
*/
public function setCreatedAt($created_at)
{
$this->__created_at = $created_at;
return $this;
}
/**
* setuUpdatedAt
*
* @param int $updated_at php time()
*/
public function setUpdatedAt($updated_at)
{
$this->__updated_at = $updated_at;
return $this;
}
/**
* field
*
* Gets property based on a string
*
* You can also use string separated by dots for nested arrays
* key_1.key_2.key_3 etc
*
* @param string $field
* @return string $context property
*/
public function field($field)
{
$parts = explode('.', $field);
$context = $this->data;
if ($field=='data') {
return $context;
}
if ($field == '__created_at') {
return $this->__created_at;
}
if ($field == '__updated_at') {
return $this->__updated_at;
}
if ($field == '__id') {
return $this->__id;
}
foreach($parts as $part)
{
if (trim($part) == '')
{
return false;
}
if (is_object($context))
{
if(!property_exists($context, $part))
{
return false;
}
$context = $context->{$part};
}
else if (is_array($context))
{
if(!array_key_exists($part, $context))
{
return false;
}
$context = $context[$part];
}
}
return $context;
}
}

View File

@ -0,0 +1,114 @@
<?php namespace Filebase;
class Filesystem
{
/**
* read
*
*
*/
public static function read($path)
{
if(!file_exists($path))
{
return false;
}
$file = fopen($path, 'r');
$contents = fread($file, filesize($path));
fclose($file);
return $contents;
}
/**
* Writes data to the filesystem.
*
* @param string $path The absolute file path to write to
* @param string $contents The contents of the file to write
*
* @return boolean Returns true if write was successful, false if not.
*/
public static function write($path, $contents)
{
$fp = fopen($path, 'w+');
if(!flock($fp, LOCK_EX))
{
return false;
}
$result = fwrite($fp, $contents);
flock($fp, LOCK_UN);
fclose($fp);
return $result !== false;
}
/**
* delete
*
* @param string $path
*
* @return boolean True if deleted, false if not.
*/
public static function delete($path)
{
if (!file_exists($path)) {
return true;
}
return unlink($path);
}
/**
* Validates the name of the file to ensure it can be stored in the
* filesystem.
*
* @param string $name The name to validate against
* @param boolean $safe_filename Allows filename to be converted if fails validation
*
* @return bool Returns true if valid. Throws an exception if not.
*/
public static function validateName($name, $safe_filename)
{
if (!preg_match('/^[0-9A-Za-z\_\-]{1,63}$/', $name))
{
if ($safe_filename === true)
{
// rename the file
$name = preg_replace('/[^0-9A-Za-z\_\-]/','', $name);
// limit the file name size
$name = substr($name,0,63);
}
else
{
throw new \Exception(sprintf('`%s` is not a valid file name.', $name));
}
}
return $name;
}
/**
* Get an array containing the path of all files in this repository
*
* @return array An array, item is a file
*/
public static function getAllFiles($path = '',$ext = 'json')
{
$files = [];
$_files = glob($path.'*.'.$ext);
foreach($_files as $file)
{
$files[] = str_replace('.'.$ext,'',basename($file));
}
return $files;
}
}

View File

@ -0,0 +1,6 @@
<?php namespace Filebase\Filesystem;
class FilesystemException extends \Exception
{
}

View File

@ -0,0 +1,6 @@
<?php namespace Filebase\Filesystem;
class ReadingException extends FilesystemException
{
}

View File

@ -0,0 +1,6 @@
<?php namespace Filebase\Filesystem;
class SavingException extends FilesystemException
{
}

View File

@ -0,0 +1,6 @@
<?php namespace Filebase\Format;
class DecodingException extends FormatException
{
}

View File

@ -0,0 +1,6 @@
<?php namespace Filebase\Format;
class EncodingException extends FormatException
{
}

View File

@ -0,0 +1,18 @@
<?php namespace Filebase\Format;
class FormatException extends \Exception
{
private $inputData;
public function __construct($message, $code = 0, \Exception $previous = null, $inputData = null)
{
parent::__construct($message, $code, $previous);
$this->inputData = $inputData;
}
public function getInputData()
{
return $this->inputData;
}
}

View File

@ -0,0 +1,8 @@
<?php namespace Filebase\Format;
interface FormatInterface
{
public static function getFileExtension();
public static function encode($data, $pretty);
public static function decode($data);
}

View File

@ -0,0 +1,60 @@
<?php namespace Filebase\Format;
class Json implements FormatInterface
{
/**
* @return string
*/
public static function getFileExtension()
{
return 'json';
}
/**
* @param array $data
* @param bool $pretty
* @return string
* @throws FormatException
*/
public static function encode($data = [], $pretty = true)
{
$options = 0;
if ($pretty == true) {
$options = JSON_PRETTY_PRINT|JSON_UNESCAPED_SLASHES|JSON_UNESCAPED_UNICODE;
}
$encoded = json_encode($data, $options);
if ($encoded === false) {
throw new EncodingException(
"json_encode: '" . json_last_error_msg() . "'",
0,
null,
$data
);
}
return $encoded;
}
/**
* @param $data
* @return mixed
* @throws FormatException
*/
public static function decode($data)
{
$decoded = json_decode($data, true);
if ($data !== false && $decoded === null) {
throw new DecodingException(
"json_decode: '" . json_last_error_msg() . "'",
0,
null,
$data
);
}
return $decoded;
}
}

View File

@ -0,0 +1,37 @@
<?php namespace Filebase\Format;
use Symfony\Component\Yaml\Yaml as YamlParser;
class Yaml implements FormatInterface
{
/**
* @return string
*/
public static function getFileExtension()
{
return 'yaml';
}
/**
* @param array $data
* @param bool $pretty
* @return string
* @throws FormatException
*/
public static function encode($data = [], $pretty = true)
{
$encoded = YamlParser::dump((array)$data);
return $encoded;
}
/**
* @param $data
* @return mixed
* @throws FormatException
*/
public static function decode($data)
{
$decoded = YamlParser::parse($data);
return $decoded;
}
}

View File

@ -0,0 +1,21 @@
MIT License
Copyright (c) 2018 Timothy Marois
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.

View File

@ -0,0 +1,98 @@
<?php namespace Filebase;
class Predicate
{
/**
* $allowed_operators
*
* Allowed operators within the query
*/
protected $allowed_operators = [
'=',
'==',
'===',
'!=',
'!==',
'>',
'<',
'>=',
'<=',
'IN',
'NOT',
'LIKE',
'NOT LIKE',
'REGEX'
];
/**
* $predicates
*
* Query clauses
*/
protected $predicates = [];
/**
* add
*
*/
public function add($logic,$arg)
{
if (!is_array($arg))
{
throw new \InvalidArgumentException('Predicate Error: argument passed must be type of array');
}
if (count($arg) == 1)
{
if (isset($arg[0]) && is_array($arg[0]))
{
foreach($arg[0] as $key => $value)
{
if ($value == '') continue;
$arg = $this->formatWhere($key, $value);
}
}
}
if (count($arg) != 3)
{
throw new \InvalidArgumentException('Predicate Error: Must have 3 arguments passed - '.count($arg).' given');
}
if (!in_array($arg[1], $this->allowed_operators))
{
throw new \InvalidArgumentException('Predicate Error: Unknown Operator '.$arg[1]);
}
$arg[0] = trim($arg[0]);
if ($arg[0] == '')
{
throw new \InvalidArgumentException('Field name can not be empty');
}
$this->predicates[$logic][] = $arg;
}
/**
* formatWhere
*
*/
protected function formatWhere($key, $value)
{
return [$key,'==',$value];
}
/**
* get
*
*/
public function get()
{
return array_filter($this->predicates);
}
}

View File

@ -0,0 +1,249 @@
<?php namespace Filebase;
class Query extends QueryLogic
{
protected $fields = [];
protected $limit = 0;
protected $offset = 0;
protected $sortBy = ['ASC'];
protected $orderBy = [''];
/**
* $documents
*
*/
protected $documents = [];
/**
* ->select()
*
* Set the selected fields you wish to return from each document
*
*/
public function select($fields)
{
if (is_string($fields))
{
$fields = explode(',',trim($fields));
}
if (is_array($fields))
{
$this->fields = $fields;
}
return $this;
}
/**
* ->where()
*
*/
public function where(...$arg)
{
$this->addPredicate('and', $arg);
return $this;
}
/**
* ->andWhere()
*
*/
public function andWhere(...$arg)
{
$this->addPredicate('and', $arg);
return $this;
}
/**
* ->orWhere()
*
*/
public function orWhere(...$arg)
{
$this->addPredicate('or', $arg);
return $this;
}
/**
* ->limit()
*
*/
public function limit($limit, $offset = 0)
{
$this->limit = (int) $limit;
if ($this->limit === 0)
{
$this->limit = 9999999;
}
$this->offset = (int) $offset;
return $this;
}
/**
* ->orderBy()
*
*/
public function orderBy($field, $sort = 'ASC')
{
if (count($this->orderBy) == 1 && $this->orderBy[0] == '') {
// Just set the initial index
$this->orderBy[0] = $field;
$this->sortBy[0] = strtoupper($sort);
} else {
$this->orderBy[] = $field;
$this->sortBy[] = strtoupper($sort);
}
return $this;
}
/**
* addPredicate
*
*/
protected function addPredicate($logic,$arg)
{
$this->predicate->add($logic, $arg);
}
/**
* ->getDocuments()
*
*
*/
public function getDocuments()
{
return $this->documents;
}
/**
* ->results()
*
* @param bool $data_only - default:true (if true only return the documents data not the full object)
*
*/
public function results( $data_only = true )
{
if ($data_only === true && empty($this->fields))
{
return parent::run()->toArray();
}
return $this->resultDocuments();
}
/**
* ->resultDocuments()
*
*/
public function resultDocuments()
{
return parent::run()->getDocuments();
}
/**
* ->first()
*
* @param bool $data_only - default:true (if true only return the documents data not the full object)
*
*/
public function first( $data_only = true )
{
if ($data_only === true && empty($this->fields))
{
$results = parent::run()->toArray();
return current($results);
}
$results = parent::run()->getDocuments();
return current($results);
}
/**
* ->last()
*
* @param bool $data_only - default:true (if true only return the documents data not the full object)
*
*/
public function last( $data_only = true )
{
if ($data_only === true && empty($this->fields))
{
$results = parent::run()->toArray();
return end($results);
}
$results = parent::run()->getDocuments();
return end($results);
}
/**
* ->count()
*
* Count and return the number of documents in array
*
*/
public function count()
{
$results = parent::run()->getDocuments();
return count($results);
}
/**
* toArray
*
* @param \Filebase\Document
* @return array
*/
public function toArray()
{
$docs = [];
if (!empty($this->documents))
{
foreach($this->documents as $document)
{
$docs[] = (array) $document->getData();
}
}
return $docs;
}
/**
* delete
*
* The ability to delete items using queries
*
* Delete by condition or delete all within clause
*
* @return void
*/
public function delete($input = null)
{
$items = $this->resultDocuments();
$condition = $input;
foreach($items as $item)
{
if (is_object($input)) {
$condition = $input($item);
if ($condition) {
$item->delete();
}
}
else {
$item->delete();
}
}
}
}

View File

@ -0,0 +1,243 @@
<?php namespace Filebase;
use Filebase\SortLogic;
class QueryLogic
{
/**
* $database
*
* \Filebase\Database
*/
protected $database;
/**
* $predicate
*
* \Filebase\Predicate
*/
protected $predicate;
/**
* $cache
*
* \Filebase\Cache
*/
protected $cache = false;
/**
* __construct
*
*/
public function __construct(Database $database)
{
$this->database = $database;
$this->predicate = new Predicate();
if ($this->database->getConfig()->cache===true)
{
$this->cache = new Cache($this->database);
}
}
/**
* loadDocuments
*
*/
private function loadDocuments()
{
$predicates = $this->predicate->get();
if ($this->cache===false)
{
$this->documents = $this->database->findAll(true,false);
return $this;
}
$this->cache->setKey(json_encode($predicates));
if ($cached_documents = $this->cache->get())
{
$this->documents = $cached_documents;
$this->sort();
$this->offsetLimit();
return $this;
}
$this->documents = $this->database->findAll(true,false);
return $this;
}
/**
* run
*
*/
public function run()
{
$predicates = $this->predicate->get();
$this->documents = [];
$cached_documents = false;
if (empty($predicates))
{
$predicates = 'findAll';
}
$this->loadDocuments();
if ($predicates !== 'findAll')
{
$this->documents = $this->filter($this->documents, $predicates);
}
if ($this->cache !== false)
{
if ($cached_documents === false)
{
$dsave = [];
foreach($this->documents as $document)
{
$dsave[] = $document->getId();
}
$this->cache->store($dsave);
}
}
$this->sort();
$this->offsetLimit();
if (is_array($this->fields) && !empty($this->fields))
{
foreach($this->documents as $index => $document)
{
$fields = [];
foreach($this->fields as $fieldTarget)
{
$fields[ $fieldTarget ] = $document->field($fieldTarget);
}
$this->documents[$index] = $fields;
}
}
return $this;
}
/**
* filter
*
*/
protected function filter($documents, $predicates)
{
$results = [];
$org_docs = $documents;
if (isset($predicates['and']) && !empty($predicates['and']))
{
foreach($predicates['and'] as $predicate)
{
list($field, $operator, $value) = $predicate;
$documents = array_values(array_filter($documents, function ($document) use ($field, $operator, $value) {
return $this->match($document, $field, $operator, $value);
}));
$results = $documents;
}
}
if (isset($predicates['or']) && !empty($predicates['or']))
{
foreach($predicates['or'] as $predicate)
{
list($field, $operator, $value) = $predicate;
$documents = array_values(array_filter($org_docs, function ($document) use ($field, $operator, $value) {
return $this->match($document, $field, $operator, $value);
}));
$results = array_unique(array_merge($results, $documents), SORT_REGULAR);
}
}
return $results;
}
/**
* offsetLimit
*
*/
protected function offsetLimit()
{
if ($this->limit != 0 || $this->offset != 0)
{
$this->documents = array_slice($this->documents, $this->offset, $this->limit);
}
}
/**
* sort
*
*/
protected function sort()
{
if ($this->orderBy[0] == '')
{
return false;
}
$sortlogic = new SortLogic($this->orderBy, $this->sortBy, 0);
usort($this->documents, [$sortlogic, 'sort']);
}
/**
* match
*
*/
public function match($document, $field, $operator, $value)
{
$d_value = $document->field($field);
switch (true)
{
case ($operator === '=' && $d_value == $value):
return true;
case ($operator === '==' && $d_value == $value):
return true;
case ($operator === '===' && $d_value === $value):
return true;
case ($operator === '!=' && $d_value != $value):
return true;
case ($operator === '!==' && $d_value !== $value):
return true;
case (strtoupper($operator) === 'NOT' && $d_value != $value):
return true;
case ($operator === '>' && $d_value > $value):
return true;
case ($operator === '>=' && $d_value >= $value):
return true;
case ($operator === '<' && $d_value < $value):
return true;
case ($operator === '<=' && $d_value <= $value):
return true;
case (strtoupper($operator) === 'LIKE' && preg_match('/'.$value.'/is',$d_value)):
return true;
case (strtoupper($operator) === 'NOT LIKE' && !preg_match('/'.$value.'/is',$d_value)):
return true;
case (strtoupper($operator) === 'IN' && in_array($d_value, (array) $value)):
return true;
case (strtoupper($operator) === 'IN' && in_array($value, (array) $d_value)):
return true;
case (strtoupper($operator) === 'REGEX' && preg_match($value, $d_value)):
return true;
default:
return false;
}
}
}

View File

@ -0,0 +1,547 @@
# Filebase
[![Build Status](https://travis-ci.org/tmarois/Filebase.svg?branch=1.0)](https://travis-ci.org/tmarois/Filebase) [![Coverage Status](https://coveralls.io/repos/github/tmarois/Filebase/badge.svg?branch=1.0)](https://coveralls.io/github/tmarois/Filebase?branch=1.0)
[Join Discord](https://discord.gg/kywDsDnJ6C)  For support, updates and collaboration.
A Simple but Powerful Flat File Database Storage. No need for MySQL or an expensive SQL server, in fact, you just need your current site or application setup. All database entries are stored in files ([formatted](README.md#2-formatting) the way you like).
You can even modify the raw data within the files themselves without ever needing to use the API. And even better you can put all your files in **version control** and pass them to your team without having out-of-sync SQL databases.
Doesn't that sound awesome?
With Filebase, you are in complete control. Design your data structure the way you want. Use arrays and objects like you know how in PHP. Update and share your data with others and teams using version control. Just remember, upgrading your web/apache server is a lot less than your database server.
Works with **PHP 5.6** and **PHP 7+**
### Features
Filebase is simple by design, but has enough features for the more advanced.
* Key/Value and Array-based Data Storing
* [Querying data](README.md#8-queries)
* [Custom filters](README.md#7-custom-filters)
* [Caching](README.md#9-caching) (queries)
* [Database Backups](README.md#10-database-backups)
* [Formatting](README.md#2-formatting) (encode/decode)
* [Validation](README.md#6-validation-optional) (on save)
* CRUD (method APIs)
* File locking (on save)
* Intuitive Method Naming
## Installation
Use [Composer](http://getcomposer.org/) to install package.
Run `composer require tmarois/filebase:^1.0`
If you do not want to use composer, download the files, and include it within your application, it does not have any dependencies, you will just need to keep it updated with any future releases.
## Usage
```php
// setting the access and configration to your database
$database = new \Filebase\Database([
'dir' => 'path/to/database/dir'
]);
// in this example, you would search an exact user name
// it would technically be stored as user_name.json in the directories
// if user_name.json doesn't exists get will return new empty Document
$item = $database->get('kingslayer');
// display property values
echo $item->first_name;
echo $item->last_name;
echo $item->email;
// change existing or add new properties
$item->email = 'example@example.com';
$item->tags = ['php','developer','html5'];
// need to save? thats easy!
$item->save();
// check if a record exists and do something if it does or does not
if ($database->has('kingslayer'))
{
// do some action
}
// Need to find all the users that have a tag for "php" ?
$users = $db->where('tags','IN','php')->results();
// Need to search for all the users who use @yahoo.com email addresses?
$users = $db->where('email','LIKE','@yahoo.com')->results();
```
## (1) Config Options
The config is *required* when defining your database. The options are *optional* since they have defaults.
Usage Example (all options)
```php
$db = new \Filebase\Database([
'dir' => 'path/to/database/dir',
'backupLocation' => 'path/to/database/backup/dir',
'format' => \Filebase\Format\Json::class,
'cache' => true,
'cache_expires' => 1800,
'pretty' => true,
'safe_filename' => true,
'read_only' => false,
'validate' => [
'name' => [
'valid.type' => 'string',
'valid.required' => true
]
]
]);
```
|Name |Type |Default Value |Description |
|--- |--- |--- |--- |
|`dir` |string |current directory |The directory where the database files are stored. |
|`backupLocation` |string |current directory (`/backups`) |The directory where the backup zip files will be stored. |
|`format` |object |`\Filebase\Format\Json` |The format class used to encode/decode data |
|`validate` |array | |Check [Validation Rules](README.md#6-validation-optional) for more details |
|`cache` |bool |true |Stores [query](README.md#8-queries) results into cache for faster loading. |
|`cache_expire` |int |1800 |How long caching will last (in seconds) |
|`pretty` |bool |true |Store the data for human readability? Pretty Print |
|`safe_filename` |bool |true |Automatically converts the file name to a valid name (added: 1.0.13) |
|`read_only` |bool |false |Prevents the database from creating/modifying files or directories (added: 1.0.14) |
## (2) Formatting
Format Class is what defines the encoding and decoding of data within your database files.
You can write your own or change the existing format class in the config. The methods in the class must be `static` and the class must implement `\Filebase\Format\FormatInterface`
The Default Format Class: `JSON`
```php
\Filebase\Format\Json::class
```
Additional Format Classes: `Yaml`
```php
\Filebase\Format\Yaml::class
```
## (3) GET (and methods)
After you've loaded up your database config, then you can use the `get()` method to retrieve a single document of data.
If document does not exist, it will create a empty object for you to store data into. You can then call the `save()` method and it will create the document (or update an existing one).
```php
// my user id
$userId = '92832711';
// get the user information by id
$item = $db->get($userId);
```
`get()` returns `\Filebase\Document` object and has its own methods which you can call.
|Method|Details|
|---|---|
|`save()` | Saves document in current state |
|`delete()` | Deletes current document (can not be undone) |
|`toArray()` | Array of items in document |
|`getId()` | Document Id |
|`createdAt()` | Document was created (default Y-m-d H:i:s) |
|`updatedAt()` | Document was updated (default Y-m-d H:i:s) |
|`field()` | You can also use `.` dot delimiter to find values from nested arrays |
|`isCache()` | (true/false) if the current document is loaded from cache |
|`filter()` | Refer to the [Custom Filters](README.md#7-custom-filters) |
Example:
```php
// get the timestamp when the user was created
echo $db->get($userId)->createdAt();
// grabbing a specific field "tags" within the user
// in this case, tags might come back as an array ["php","html","javascript"]
$user_tags = $db->get($userId)->field('tags');
// or if "tags" is nested in the user data, such as about[tags]
$user_tags = $db->get($userId)->field('about.tags');
// and of course you can do this as well for getting "tags"
$user = $db->get($userId);
$user_tags = $user->tags;
$user_tags = $user->about['tags'];
```
## (4) Create | Update | Delete
As listed in the above example, its **very simple**. Use `$item->save()`, the `save()` method will either **Create** or **Update** an existing document by default. It will log all changes with `createdAt` and `updatedAt`. If you want to replace *all* data within a single document pass the new data in the `save($data)` method, otherwise don't pass any data to allow it to save the current instance.
```php
// SAVE or CREATE
// this will save the current data and any changed variables
// but it will leave existing variables that you did not modify unchanged.
// This will also create a document if none exist.
$item->title = 'My Document';
$item->save()
// This will replace all data within the document
// Allows you to reset the document and put in fresh data
// Ignoring any above changes or changes to variables, since
// This sets its own within the save method.
$item->save([
'title' => 'My Document'
]);
// DELETE
// This will delete the current document
// This action can not be undone.
$item->delete();
```
## (5) Database Methods
```php
$db = new \Filebase\Database($config);
```
Here is a list of methods you can use on the database class.
|Method|Details|
|---|---|
|`version()` | Current version of your Filebase library |
|`get($id)` | Refer to [get()](README.md#3-get-and-methods) |
|`has($id)` | Check if a record exist returning true/false |
|`findAll()` | Returns all documents in database |
|`count()` | Number of documents in database |
|`flush(true)` | Deletes all documents. |
|`flushCache()` | Clears all the cache |
|`truncate()` | Deletes all documents. Alias of `flush(true)` |
|`query()` | Refer to the [Queries](README.md#8-queries) |
|`backup()` | Refer to the [Backups](README.md#10-database-backups) |
Examples
```php
$users = new \Filebase\Database([
'dir' => '/storage/users',
]);
// displays number of users in the database
echo $users->count();
// Find All Users and display their email addresses
$results = $users->findAll();
foreach($results as $user)
{
echo $user->email;
// you can also run GET methods on each user (document found)
// Displays when the user was created.
echo $user->createdAt();
}
// deletes all users in the database
// this action CAN NOT be undone (be warned)
$users->flush(true);
```
## (6) Validation *(optional)*
When invoking `save()` method, the document will be checked for validation rules (if set).
These rules MUST pass in order for the document to save.
```php
$db = new \Filebase\Database([
'dir' => '/path/to/database/dir',
'validate' => [
'name' => [
'valid.type' => 'string',
'valid.required' => true
],
'description' => [
'valid.type' => 'string',
'valid.required' => false
],
'emails' => [
'valid.type' => 'array',
'valid.required' => true
],
'config' => [
'settings' => [
'valid.type' => 'array',
'valid.required' => true
]
]
]
]);
```
In the above example `name`, `description`, `emails` and `config` array keys would be replaced with your own that match your data. Notice that `config` has a nested array `settings`, yes you can nest validations.
**Validation rules:**
|Name |Allowed Values |Description |
|--- |--- |--- |
|`valid.type` |`string`, `str`, `integer`, `int`, `array` |Checks if the property is the current type |
|`valid.required` |`true`, `false` |Checks if the property is on the document |
## (7) Custom Filters
*NOTE Custom filters only run on a single document*
Item filters allow you to customize the results, and do simple querying within the same document. These filters are great if you have an array of items within one document. Let's say you store "users" as an array in `users.json`, then you could create a filter to show you all the users that have a specific tag, or field matching a specific value.
This example will output all the emails of users who are blocked.
```php
// Use [data] for all items within the document
// But be sure that each array item uses the same format (otherwise except isset errors)
$users = $db->get('users')->filter('data','blocked',function($item, $status) {
return (($item['status']==$status) ? $item['email'] : false);
});
// Nested Arrays?
// This uses NESTED properties. If the users array was stored as an array inside [list]
// You can also use `.` dot delimiter to get arrays from nested arrays
$users = $db->get('users')->filter('list.users','blocked',function($item, $status) {
return (($item['status']==$status) ? $item['email'] : false);
});
```
## (8) Queries
Queries allow you to search **multiple documents** and return only the ones that match your criteria.
If caching is enabled, queries will use `findAll()` and then cache results for the next run.
> Note: You no longer need to call `query()`, you can now call query methods directly on the database class.
```php
// Simple (equal to) Query
// return all the users that are blocked.
$users = $db->where(['status' => 'blocked'])->results();
// Stackable WHERE clauses
// return all the users who are blocked,
// AND have "php" within the tag array
$users = $db->where('status','=','blocked')
->andWhere('tag','IN','php')
->results();
// You can also use `.` dot delimiter to use on nested keys
$users = $db->where('status.language.english','=','blocked')->results();
// Limit Example: Same query as above, except we only want to limit the results to 10
$users = $db->where('status.language.english','=','blocked')->limit(10)->results();
// Query LIKE Example: how about find all users that have a gmail account?
$usersWithGmail = $db->where('email','LIKE','@gmail.com')->results();
// OrderBy Example: From the above query, what if you want to order the results by nested array
$usersWithGmail = $db->where('email','LIKE','@gmail.com')
->orderBy('profile.name', 'ASC')
->results();
// or just order the results by email address
$usersWithGmail = $db->where('email','LIKE','@gmail.com')
->orderBy('email', 'ASC')
->results();
// OrderBy can be applied multiple times to perform a multi-sort
$usersWithGmail = $db->query()
->where('email','LIKE','@gmail.com')
->orderBy('last_name', 'ASC')
->orderBy('email', 'ASC')
->results();
// this will return the first user in the list based on ascending order of user name.
$user = $db->orderBy('name','ASC')->first();
// print out the user name
echo $user['name'];
// You can also order multiple columns as such (stacking)
$orderMultiples = $db->orderBy('field1','ASC')
->orderBy('field2','DESC')
->results();
// What about regex search? Finds emails within a field
$users = $db->where('email','REGEX','/[a-z\d._%+-]+@[a-z\d.-]+\.[a-z]{2,4}\b/i')->results();
// Find all users that have gmail addresses and only returning their name and age fields (excluding the rest)
$users = $db->select('name,age')->where('email','LIKE','@gmail.com')->results();
// Instead of returning users, how about just count how many users are found.
$totalUsers = $db->where('email','LIKE','@gmail.com')->count();
// You can delete all documents that match the query (BULK DELETE)
$db->where('name','LIKE','john')->delete();
// Delete all items that match query and match custom filter
$db->where('name','LIKE','john')->delete(function($item){
return ($item->name == 'John' && $item->email == 'some@mail.com');
});
// GLOBAL VARIABLES
// ability to sort the results by created at or updated at times
$documents = $db->orderBy('__created_at', 'DESC')->results();
$documents = $db->orderBy('__updated_at', 'DESC')->results();
// search for items that match the (internal) id
$documents = $db->where('__id', 'IN', ['id1', 'id2'])->results();
```
To run the query use `results()` or if you only want to return the first item use `first()`
### Query Methods:
*These methods are optional and they are stackable*
|Method |Arguments |Details
|--- |--- |---|
|`select()` | `array` or `string` (comma separated) | Select only the fields you wish to return (for each document), usage: `field1,field2` |
|`where()` | `mixed` | `array` for simple "equal to" OR `where($field, $operator, $value)` |
|`andWhere()` | `mixed` | see `where()`, uses the logical `AND` |
|`orWhere()` | `mixed` | see `where()`, this uses the logical `OR` |
|`limit()` | `int` limit, `int` offset | How many documents to return, and offset |
|`orderBy()` | `field` , `sort order` | Order documents by a specific field and order by `ASC` or `DESC` |
|`delete()` | `Closure` | Ability to Bulk-delete all items that match |
The below **methods execute the query** and return results *(do not try to use them together)*
|Method |Details|
|--- |---|
|`count()` | Counts and returns the number of documents in results. |
|`first()` | Returns only the first document in results. |
|`last()` | Returns only the last document in results. |
|`results()` | This will return all the documents found and their data as an array. Passing the argument of `false` will be the same as `resultDocuments()` (returning the full document objects) |
|`resultDocuments()` | This will return all the documents found and their data as document objects, or you can do `results(false)` which is the alias. |
### Comparison Operators:
|Name |Details|
|--- |---|
|`=` or `==` |Equality|
|`===` |Strict Equality|
|`!=` |Not Equals|
|`NOT` |Not Equals (same as `!=`)|
|`!==` |Strict Not Equals|
|`>` |Greater than|
|`>=` |Greater than or equal|
|`<` |Less than|
|`<=` |Less than or equal|
|`IN` |Checks if the value is within a array|
|`LIKE` |case-insensitive regex expression search|
|`NOT LIKE` |case-insensitive regex expression search (opposite)|
|`REGEX` |Regex search|
## (9) Caching
If caching is enabled, it will automatically store your results from queries into sub-directories within your database directory.
Cached queries will only be used if a specific saved cache is less than the expire time, otherwise it will use live data and automatically replace the existing cache for next time use.
## (10) Database Backups
By default you can backup your database using `$db->backup()->create()`, this will create a `.zip` file of your entire database based on your `dir` path.
### Methods:
These methods can be used when invoking `backup()` on your `Database`.
- `create()` Creates a backup of your database (in your backup location `.zip`)
- `clean()` Purges all existing backups (`.zip` files in your backup location)
- `find()` Returns an `array` of all existing backups (array key by `time()` when backup was created)
- `rollback()` Restore an existing backup (latest available), replaces existing database `dir`
**Example:**
```php
// invoke your database
$database = new \Filebase\Database([
'dir' => '/storage/users',
'backupLocation' => '/storage/backup',
]);
// create a new backup of your database
// will look something like /storage/backup/1504631092.zip
$database->backup()->create();
// delete all existing backups
$database->backup()->clean();
// get a list of all existing backups (organized from new to old)
$backups = $database->backup()->find();
// restore an existing backup (latest backup available)
$database->backup()->rollback();
```
## Why Filebase?
Filebase was built for the flexibility to help manage simple data storage without the hassle of a heavy database engine. The concept of Filebase is to provide very intuitive API methods, and make it easy for the developer to maintain and manage (even on a large scale).
Inspired by [Flywheel](https://github.com/jamesmoss/flywheel) and [Flinetone](https://github.com/fire015/flintstone).
## How Versions Work
Versions are as follows: Major.Minor.Patch
* Major: Rewrites with completely new code-base.
* Minor: New Features/Changes that breaks compatibility.
* Patch: New Features/Fixes that does not break compatibility.
Filebase will work-hard to be **backwards-compatible** when possible.
## Sites and Users of Filebase
* [Grayscale Inc](https://grayscale.com)
* [VIP Auto](http://vipautoli.com)
* [Ideal Internet](http://idealinternet.com)
* [OnlineFun](http://onlinefun.com)
* [PuzzlePlay](http://puzzleplay.com)
* [Square Media LLC](http://squaremedia.com)
* [My Map Directions](https://mymapdirections.com)
* [Discount Savings](https://discount-savings.com)
* [Vivint - Smart Homes](http://smarthomesecurityplans.com/)
*If you are using Filebase send in a pull request and we will add your project here.*
## Contributions
Anyone can contribute to Filebase. Please do so by posting issues when you've found something that is unexpected or sending a pull request for improvements.
## License
Filebase is open-sourced software licensed under the [MIT license](https://opensource.org/licenses/MIT).

View File

@ -0,0 +1,79 @@
<?php namespace Filebase;
/**
* Recursive sort logic class
*
* @package Filebase
*/
class SortLogic
{
/**
* Field names to sort by
*
* @var array
*/
public $orderBy = [];
/**
* Sort direction (ASC, DESC)
*
* @var array
*/
public $sortDirection = [];
/**
* Index of the current sort data
*
* @var int
*/
public $index = 0;
/**
* Constructor
*
* @param array $orderBy
* @param array $sortDirection
* @param int $index
* @return void
*/
public function __construct($orderBy, $sortDirection, $index = 0)
{
$this->orderBy = $orderBy;
$this->sortDirection = $sortDirection;
$this->index = $index;
}
/**
* Sorting callback
*
* @param Document $docA
* @param Document $docB
* @return return int (-1, 0, 1)
*/
public function sort($docA, $docB)
{
$propA = $docA->field($this->orderBy[$this->index]);
$propB = $docB->field($this->orderBy[$this->index]);
if (strnatcasecmp($propA, $propB) == 0)
{
if (!isset($this->orderBy[$this->index + 1]))
{
return 0;
}
// If they match and there are multiple orderBys, go deeper (recurse)
$sortlogic = new self($this->orderBy, $this->sortDirection, $this->index + 1);
return $sortlogic->sort($docA, $docB);
}
if ($this->sortDirection[$this->index] == 'DESC')
{
return strnatcasecmp($propB, $propA);
}
else
{
return strnatcasecmp($propA, $propB);
}
}
}

View File

@ -0,0 +1,139 @@
<?php namespace Filebase;
class Validate
{
/**
* valid
*
* @param object document $object
* @return bool ( true ) if no exception is fired
*/
public static function valid(Document $object)
{
$document = $object->toArray();
self::validateLoop($document,$object,self::getValidateRules($object));
return true;
}
/**
* getValidateRules
*
* @param \Filebase\Document
* @return database->config
*/
public static function getValidateRules(Document $object)
{
return $object->getDatabase()->getConfig()->validate;
}
/**
* validateLoop
*
* Loops over the document and finds invaild data
* Throws an exception if found, otherwise returns nothing
*
* @param array (of document data)
* @return vold
*/
protected static function validateLoop($document,$object,$rules)
{
foreach($rules as $key => $rule)
{
if ( (!isset($rule['valid.type']) ) && isset($document[$key]))
{
self::validateLoop($document[$key],$object,$rules[$key]);
continue;
}
self::validateRules($document,$key,$rules[$key],$object);
}
}
/**
* validateRules
*
* Checks "valid.type"
* Checks "valid.requred"
*
* Throws exception error if matches are not met.
*
* @return \Filebase\Document Object
*/
protected static function validateRules($document,$key,$rules,$object)
{
// checks variable type
if (isset($document[$key],$rules['valid.type']))
{
if (!in_array($rules['valid.type'],['string','str','int','integer','arr','array']))
{
throw new \Exception('Validation Failed: Invaild Property Type "'.$rules['valid.type'].'"');
}
if (!self::checkType($document[$key],$rules['valid.type']))
{
throw new \Exception('Validation Failed setting variable on '.$object->getId().' - ['.$key.'] does not match type "'.$rules['valid.type'].'"');
}
}
// check if variable is required
if (isset($rules['valid.required']) && $rules['valid.required']===true)
{
if (!isset($document[$key]))
{
throw new \Exception('Validation Failed setting variable on '.$object->getId().' - ['.$key.'] is required');
}
}
return $object;
}
/**
* checkType
*
* Checks type of variable and sees if it matches
*
* @return boolean (true or false)
*/
protected static function checkType($variable, $type)
{
switch($type)
{
case 'string':
case 'str':
if (is_string($variable))
{
return true;
}
break;
case 'integer':
case 'int':
if (is_integer($variable))
{
return true;
}
break;
case 'array':
case 'arr':
if (is_array($variable))
{
return true;
}
break;
default:
return false;
}
return false;
}
}