data-store.php

Basic data store utility -- unfinished
git clone git://git.finwo.net/lib/data-store.php
Log | Files | Refs

commit 72118c0f9976e3d4a0d1017d987153fb7af80f89
Author: finwo <finwo@pm.me>
Date:   Thu, 18 May 2017 12:24:05 +0200

git init

Diffstat:
A.gitignore | 2++
Adata/.gitignore | 2++
Adocs/.htaccess | 4++++
Adocs/api/collections.php | 9+++++++++
Adocs/api/data.php | 64++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Adocs/api/init.php | 79+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Adocs/css/base.css | 82+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Adocs/css/collections.css | 19+++++++++++++++++++
Adocs/css/grid.css.php | 41+++++++++++++++++++++++++++++++++++++++++
Adocs/index.html | 31+++++++++++++++++++++++++++++++
Adocs/js/ajax.js | 155+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Adocs/js/data-source.js | 24++++++++++++++++++++++++
Adocs/js/domchange.js | 116+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Adocs/js/require.js | 33+++++++++++++++++++++++++++++++++
Adocs/not-found.php | 13+++++++++++++
Adocs/php_errors.log | 13+++++++++++++
Asrc/autoload.php | 14++++++++++++++
17 files changed, 701 insertions(+), 0 deletions(-)

diff --git a/.gitignore b/.gitignore @@ -0,0 +1,2 @@ +.idea +*.bak diff --git a/data/.gitignore b/data/.gitignore @@ -0,0 +1,2 @@ +* +!.gitignore diff --git a/docs/.htaccess b/docs/.htaccess @@ -0,0 +1,4 @@ +RewriteEngine On +RewriteCond %{REQUEST_FILENAME} !-d +RewriteCond %{REQUEST_FILENAME} !-f +RewriteRule . not-found.php [NC,L] diff --git a/docs/api/collections.php b/docs/api/collections.php @@ -0,0 +1,9 @@ +<?php +require_once 'init.php'; +$list = array_values(array_map(function($entry) { + return array('name'=>$entry); +}, array_filter(scandir(APPROOT.DS.'data'), function($entry) { + return substr($entry,0,1) != '.'; +}))); + +echo json_encode($list); diff --git a/docs/api/data.php b/docs/api/data.php @@ -0,0 +1,64 @@ +<?php +require_once 'init.php'; +$method = $_SERVER['REQUEST_METHOD']; +$params = url_params("/api/data/:collection/:id"); +if(is_null($params['collection'])) { + header('HTTP/1.0 400 Bad Request'); + echo 'Bad Request - No collection given'; + exit(0); +} + +switch($method) { + case 'GET': + $dir = APPROOT.DS.'data'.DS.$params['collection'].DS; + if (!is_dir($dir)) { + header('HTTP/1.0 404 Not Found'); + echo 'Not Found - Collection does not exist'; + exit(0); + } + + header('Content-Type: application/json'); + if (!isset($params['id'])) print('['); + $dh = opendir($dir); + $sep = ''; + while( $file = readdir($dh) ) { + if ( substr($file,0,1) == '.' ) continue; + $id = explode('.',$file); + $ext = array_pop($id); + $id = implode('.',$id); + if ( $ext != 'json' ) continue; + if ( isset($params['id']) ) { + if ( $params['id'] == $id ) { + $entity = json_decode(file_get_contents($dir.$file), true); + $entity['_id'] = $id; + print(json_encode($entity)); + exit(0); + } + continue; + } + $entity = json_decode(file_get_contents($dir.$file), true); + $entity['_id'] = $id; + if ( isset($params['filter']) && !entity_matches($entity,$params['filter']) ) { + continue; + } + print($sep); + $sep = ','; + print(json_encode($entity)); + } + if (!isset($params['id'])) print(']'); + break; + + case 'POST': + $dir = APPROOT.DS.'data'.DS.$params['collection'].DS; + if (!is_dir($dir)) { + mkdir($dir); + } + + $id = uuid($params['collection']); + $file = $dir.$id.'.json'; + file_put_contents($file,json_encode($_POST)); + $_POST['_id'] = $id; + print(json_encode($_POST)); + + break; +} diff --git a/docs/api/init.php b/docs/api/init.php @@ -0,0 +1,79 @@ +<?php + +require_once '..' . DIRECTORY_SEPARATOR . 'src' . DIRECTORY_SEPARATOR . 'autoload.php'; + +function url_params( $template ) { + $data = $_GET; + $url = $_SERVER['REQUEST_URI']; + $url = explode('?',$url); + $url = explode('/',array_shift($url)); + $template = explode('/',$template); + foreach ( $template as $index => $name ) { + if (substr($name,0,1)!=':') continue; + $name = substr($name,1); + $data[$name] = isset($url[$index]) ? $url[$index] : null; + } + return $data; +} + +function get_deep( $data, $key = "" ) { + if (is_string($key)) $key = explode('.', $key); + if (!is_array($key)) return null; + if ( is_object($data) ) $data = (array) $data; + if ( !is_array($data) ) return null; + if ( count($key) == 1 ) { + $key = array_shift($key); + if ( isset($data[$key]) ) { + return $data[$key]; + } + return null; + } + $current_key = array_shift($key); + return get_deep( $data[$current_key], $key ); +} + +function entity_matches( $entity, $filter ) { + foreach ( $filter as $key => $query ) { + $value = get_deep( $entity, $key ); + switch(substr($query,0,1)) { + case '/': + if ( !preg_match($query, $value) ) return false; + break; + case '>': + $query = substr($query,1); + if ( !is_numeric($query) ) return false; + if ( !is_numeric($value) ) return false; + if (!(floatval($value) > floatval($query))) return false; + break; + case '<': + $query = substr($query,1); + if ( !is_numeric($query) ) return false; + if ( !is_numeric($value) ) return false; + if (!(floatval($value) < floatval($query))) return false; + break; + case '!': + $query = substr($query,1); + if ( $query == $value ) return false; + break; + default: + if ( $query != $value ) return false; + break; + } + } + return true; +} + +function random_char() { + $alphabet = '0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ'; + return $alphabet[rand(0, strlen($alphabet)-1)]; +} + +function uuid( $collection = null ) { + if (is_null($collection)) return uniqid('_'); + $dir = APPROOT.DS.'data'.DS.$collection.DS; + if (!is_dir($dir)) return uniqid('_'); + $output = '_'; + while ( strlen($output) < 5 ) $output .= random_char(); + while ( is_file($dir.$output.'.json') ) $output .= random_char(); + return $output; +} diff --git a/docs/css/base.css b/docs/css/base.css @@ -0,0 +1,82 @@ +@import url('https://fonts.googleapis.com/css?family=Oswald|Roboto'); + +/* base */ +* { + -webkit-font-smoothing: subpixel-antialiased; + box-sizing : border-box; + line-height: 1em; + margin : 0; + padding : 0; +} +html { + color : #222; + font-family: 'Roboto', sans-serif; + font-size : calc( 1rem + 0.1vw ); +} +h1, h2, h3, h4, h5, h6 { + font-family: 'Oswald', sans-serif; + padding-top : 1.00em; + padding-bottom: 0.50em; + margin-bottom : 0.25em; +} +h1 { font-size: 2.00rem; border-bottom: 1px solid #AAA; } +h2 { font-size: 1.68rem; border-bottom: 1px solid #AAA; } +h3 { font-size: 1.41rem; } +h4 { font-size: 1.19rem; } +h5 { font-size: 1.00rem; } +h6 { font-size: 0.84rem; } +.grid { + display : flex; + flex-flow: row wrap; + margin : 0 auto; +} +.grid > * { + flex-basis: 15rem; + flex-grow : 1; + margin : 0.5rem; +} +.padded { + padding: 0.5rem; +} +.padded > .title { + margin-top : 0.5rem; + padding-top: 0; +} +.shadow { + box-shadow: 0 0.05rem 0.15rem rgba(0,0,0,0.8); +} +a { + color : inherit; + text-decoration: none; +} +.container { + margin : 0 auto; + max-width: 50rem; +} +hr, p { + margin-bottom: 0.50rem; +} +ol, ul { + padding-left: 1.5em; +} + +/* nav */ +nav { + background: #5AF; + color : #FFF; +} +nav a { + display: inline-block; + padding: 0.5rem; +} +nav a:hover { + background: #FFF; + color : #5AF; +} +.view, +[data-source] { + display: none; +} +.view:target { + display: initial; +} diff --git a/docs/css/collections.css b/docs/css/collections.css @@ -0,0 +1,19 @@ +ul.collections { + list-style-type : none; + padding : 0; +} +ul.collections li a { + display : inline-block; + padding : 0.5em; + width : 100%; +} +ul.collections li a:hover { + background-color: rgba(0,0,0,0.1); +} +ul.collections li a.active { + background: #5AF; + color : #FFF; +} +ul.collections li + li { + border-top: 1px solid rgba(0,0,0,0.2); +} diff --git a/docs/css/grid.css.php b/docs/css/grid.css.php @@ -0,0 +1,41 @@ +<?php header('Content-Type: text/css'); ?> +[class^=col-] { float:left;width:99.9% } +@media all and (min-width: 720px) { +<?php + function string_format( $template, $data, $prefix = "" ) { + foreach ($data as $key => $value) { + $compositeKey = $prefix . $key; + switch(gettype($value)) { + case 'string': + case 'double': + case 'float': + case 'integer': + $template = str_replace( '{'.$compositeKey.'}', $value, $template); + break; + case 'boolean': + $template = str_replace( '{'.$compositeKey.'}', $value ? 'true' : 'false', $template); + break; + case 'object': + $value = (array) $value; + case 'array': + $template = string_format( $template, $value, $compositeKey . '.'); + break; + } + } + return $template; + } + function print_grid( $columns, $current = 1 ) { + if ( $current > $columns ) {return;} + print(string_format(" .col-{current}-{columns} {width:{width}%}\n",array( + 'columns'=>$columns, + 'current'=>$current, + 'width'=>$current/$columns*99.9 + ))); + print_grid($columns,$current+1); + } + $grid = array( 3, 5, 9, 12 ); + foreach ($grid as $columns) { + print_grid($columns); + } +?> +} diff --git a/docs/index.html b/docs/index.html @@ -0,0 +1,31 @@ +<!DOCTYPE html> +<html> + <head> + <title>Data Store</title> + <meta name="viewport" content="width=device-width, initial-scale=1" /> + <link rel="stylesheet" href="/css/base.css" /> + <link rel="stylesheet" href="/css/grid.css" /> + <link rel="stylesheet" href="/css/collections.css" /> + </head> + <body> + <nav class="shadow"> + <div class="container"> + <a href="/">Data Store</a> + </div> + </nav> + <div class="container"> + <div> + <div class="col-1-5 padded"> + <ul class="shadow collections" data-source="/api/collections"> + <li><a href="#{name}">{name}</a></li> + </ul> + </div> + <div class="col-4-5 padded"> + WIP + </div> + </div> + </div> + <script src="/js/require.js"></script> + <script src="/js/data-source.js"></script> + </body> +</html> diff --git a/docs/js/ajax.js b/docs/js/ajax.js @@ -0,0 +1,155 @@ +(function(exports) { + + var factories = [ + function () {return new XMLHttpRequest()}, + function () {return new ActiveXObject("Msxml2.XMLHTTP")}, + function () {return new ActiveXObject("Msxml3.XMLHTTP")}, + function () {return new ActiveXObject("Microsoft.XMLHTTP")} + ]; + + function httpObject() { + var xmlhttp = false; + factories.forEach(function(factory) { + try { + xmlhttp = xmlhttp || factory(); + } catch(e) { + return; + } + }); + return xmlhttp; + } + + function serializeObject(obj,prefix) { + var str = [], p; + for(p in obj) { + if (obj.hasOwnProperty(p)) { + var k = prefix ? prefix + "[" + p + "]" : p, v = obj[p]; + str.push((v !== null && typeof v === "object") ? + serializeObject(v, k) : + encodeURIComponent(k) + "=" + encodeURIComponent(v)); + } + } + return str.join("&"); + } + + function SimplePromise() + { + var self = this, + queue = [], + doneFunction = null, + failFunction = function(e){throw e;}, + started = false, + running = false; + this.then = function(callback) { + queue.push(callback); + if(started&&!running) self.run(); + return self; + }; + this.fail = function(callback) { + failFunction = callback; + if(started&&!running) self.run(); + return self; + }; + this.done = function(callback) { + doneFunction = callback; + if(started&&!running) self.run(); + return self; + }; + this.start = function(callback) { + queue.push(callback); + self.run(); + return self; + }; + this.run = function(data, done) { + started = true; + running = true; + var returnValue; + if(this!=self) { + done = this; + } + while(queue.length) { + var func = queue.shift(); + if (!func) { + running = false; + if (typeof done === 'function') return done(data); + if (typeof doneFunction === 'function') return doneFunction(data); + return data; + } + try { + returnValue = null; + returnValue = func.call(null, data, self.run.bind(done), failFunction); + } catch(e) { + running = false; + if (typeof failFunction === 'function') return failFunction(e, data); + throw e; + } + if(!returnValue) { + return; + } + } + running = false; + if (typeof done === 'function') return done(data); + if (typeof doneFunction === 'function') return doneFunction(data); + return data; + }; + } + + function ajax( uri, options ) { + options = options || {}; + + var method = (options.method || 'GET').toUpperCase(), + data = options.data || {}, + promise = new SimplePromise(); + + var req = httpObject(); + if (!req) return; + + // Insert data? + if(Object.keys(data).length) { + var serializedData = serializeObject(data); + switch(method) { + case 'GET': + uri += ((uri.indexOf('?')===false) ? '?' : '&') + serializedData; + data = {}; + break; + case 'POST': + req.setRequestHeader('Content-Type', 'application/x-www-form-urlencoded'); + break; + } + } + + // Let's start + req.open(method, uri, true); + + return promise.start(function(d, resolve, reject) { + req.onreadystatechange = function() { + if(req.readyState!=4) return; + if(req.status<200||req.status>=300) { + reject('Invalid response'); + } + var receivedData = req.responseText; + try { + receivedData = JSON.parse(receivedData); + } catch(e) { + // Nothing to worry about + } + resolve(receivedData); + }; + req.send(data); + }); + } + + // Export our freshly created plugin + exports.ajax = ajax; + if (typeof define === 'function' && define.amd) { + define('ajax', function() { + return ajax; + }) + } + + // Attach to window as well + if (typeof window !== 'undefined') { + window.ajax = ajax; + } + +})(typeof exports === 'object' && exports || this); diff --git a/docs/js/data-source.js b/docs/js/data-source.js @@ -0,0 +1,24 @@ +require('data-source', ['ajax', 'domchange'], function(ajax, onDomChange) { + function render( template, data ) { + if ( Array.isArray(data) ) { + return data.map(render.bind(null,template)).join(''); + } + return template.format(data); + } + function process( element ) { + if(!element) { + document.querySelectorAll('[data-source]').forEach(process); + return; + } + var template = element.innerHTML, + url = element.getAttribute('data-source'); + element.innerHTML = ''; + element.removeAttribute('data-source'); + ajax(url) + .then(function( data ) { + element.innerHTML = render(template,data); + }) + } + onDomChange(process,10); + process(); +}); diff --git a/docs/js/domchange.js b/docs/js/domchange.js @@ -0,0 +1,116 @@ +// See: http://stackoverflow.com/a/3219767 +(function (window) { + var last = +new Date(); + var delay = 100; // default delay + + // Manage event queue + var stack = []; + + function callback() { + var now = +new Date(); + if (now - last > delay) { + for (var i = 0; i < stack.length; i++) { + stack[i](); + } + last = now; + } + } + + // Public interface + var onDomChange = function (fn, newdelay) { + if (newdelay) delay = newdelay; + stack.push(fn); + }; + + // Naive approach for compatibility + function naive() { + + var last = document.getElementsByTagName('*'); + var lastlen = last.length; + var timer = setTimeout(function check() { + + // get current state of the document + var current = document.getElementsByTagName('*'); + var len = current.length; + + // if the length is different + // it's fairly obvious + if (len != lastlen) { + // just make sure the loop finishes early + last = []; + } + + // go check every element in order + for (var i = 0; i < len; i++) { + if (current[i] !== last[i]) { + callback(); + last = current; + lastlen = len; + break; + } + } + + // over, and over, and over again + setTimeout(check, delay); + + }, delay); + } + + // + // Check for mutation events support + // + + var support = {}; + + var el = document.documentElement; + var remain = 3; + + // callback for the tests + function decide() { + if (support.DOMNodeInserted) { + window.addEventListener("DOMContentLoaded", function () { + if (support.DOMSubtreeModified) { // for FF 3+, Chrome + el.addEventListener('DOMSubtreeModified', callback, false); + } else { // for FF 2, Safari, Opera 9.6+ + el.addEventListener('DOMNodeInserted', callback, false); + el.addEventListener('DOMNodeRemoved', callback, false); + } + }, false); + } else if (document.onpropertychange) { // for IE 5.5+ + document.onpropertychange = callback; + } else { // fallback + naive(); + } + } + + // checks a particular event + function test(event) { + el.addEventListener(event, function fn() { + support[event] = true; + el.removeEventListener(event, fn, false); + if (--remain === 0) decide(); + }, false); + } + + // attach test events + if (window.addEventListener) { + test('DOMSubtreeModified'); + test('DOMNodeInserted'); + test('DOMNodeRemoved'); + } else { + decide(); + } + + // do the dummy test + var dummy = document.createElement("div"); + el.appendChild(dummy); + el.removeChild(dummy); + + // expose + if (typeof define === 'function' && define.amd) { + define('domchange', function() { + return onDomChange; + }) + } + window.onDomChange = onDomChange; +})(window); diff --git a/docs/js/require.js b/docs/js/require.js @@ -0,0 +1,33 @@ +String.prototype.format = function(data) { + var output = this, + flatData = {}; + (function flatten( obj, prefix ) { + prefix = prefix || ''; + Object.keys(obj).forEach(function( key ) { + var compositeKey = prefix + key; + switch(typeof obj[key]) { + case 'string': + case 'number': + flatData[compositeKey] = obj[key]; + break; + case 'object': + flatData[compositeKey] = obj[key]; + flatten( obj[key], compositeKey + '.' ); + break; + } + }); + })(data); + Object.keys(flatData).forEach(function(key) { + while(output.indexOf('{'+key+'}')>=0) output = output.replace('{'+key+'}',flatData[key]); + }); + return output; +}; +window.require=window.define= (function() { + var a=function(o) { + return Object.keys(o).map(function(k){return o[k]}); + }, + q=[], + m={}, + k=[], + l=function(n){if(k.indexOf(n)>=0)return;k.push(n);var x=new XMLHttpRequest();x.onreadystatechange=function(){if(x.readyState==XMLHttpRequest.DONE&&x.status==200)eval(x.responseText)};x.open('GET',r.uri+n+'.js',!0);x.send();},p=function(){var r=!0,ar=[],e=q.shift();if(!e)return;if(e.s&&m[e.s]){q.length&&setTimeout(p,5);return;}e.o.map(function(d){if(!r)return;if(m[d]){ar.push(m[d])}else{r=!1;q.push(e);l(d)}});if(r){e.s?m[e.s]=e.f.apply(null,ar):e.f.apply(null,ar)}q.length&&setTimeout(p,5)};function r(){var e={s:null,o:[],f:function(){}};a(arguments).map(function(arg){e[(typeof arg).substr(0,1)]=arg});q.push(e);p()}r.uri='/js/';r.amd=!0;return r +})(); diff --git a/docs/not-found.php b/docs/not-found.php @@ -0,0 +1,13 @@ +<?php +include dirname(__DIR__).DIRECTORY_SEPARATOR.'src'.DIRECTORY_SEPARATOR.'autoload.php'; +$path = explode('/',$_SERVER['REQUEST_URI']); +while(count($path)) { + if ( is_file(__DIR__.implode(DIRECTORY_SEPARATOR,$path).'.php') ) { + include(__DIR__.implode(DIRECTORY_SEPARATOR,$path).'.php'); + exit(0); + } + array_pop($path); +} + +header('HTTP/1.0 404 Not Found'); +echo 'Not Found'; diff --git a/docs/php_errors.log b/docs/php_errors.log @@ -0,0 +1,13 @@ +[18-May-2017 10:46:24 Europe/Berlin] PHP Parse error: syntax error, unexpected ')', expecting variable (T_VARIABLE) or '$' in /var/www/vps/data-store/docs/css/grid.css.php on line 19 +[18-May-2017 10:50:15 Europe/Berlin] PHP Parse error: syntax error, unexpected '12' (T_LNUMBER), expecting ')' in /var/www/vps/data-store/docs/css/grid.css.php on line 30 +[18-May-2017 11:27:04 Europe/Berlin] PHP Parse error: syntax error, unexpected ')' in /var/www/vps/data-store/docs/api/data.php on line 4 +[18-May-2017 11:27:36 Europe/Berlin] PHP Warning: scandir(/var/www/vps/data-store/user): failed to open dir: No such file or directory in /var/www/vps/data-store/docs/api/data.php on line 4 +[18-May-2017 11:27:36 Europe/Berlin] PHP Warning: scandir(): (errno 2): No such file or directory in /var/www/vps/data-store/docs/api/data.php on line 4 +[18-May-2017 11:47:40 Europe/Berlin] PHP Parse error: syntax error, unexpected ')' in /var/www/vps/data-store/docs/api/data.php on line 4 +[18-May-2017 11:58:41 Europe/Berlin] PHP Notice: Undefined variable: entity in /var/www/vps/data-store/docs/api/data.php on line 29 +[18-May-2017 12:06:29 Europe/Berlin] PHP Notice: Undefined variable: contents in /var/www/vps/data-store/docs/api/data.php on line 36 +[18-May-2017 12:18:28 Europe/Berlin] PHP Fatal error: Call to undefined function random_int() in /var/www/vps/data-store/docs/api/init.php on line 68 +[18-May-2017 12:19:14 Europe/Berlin] PHP Warning: rand() expects exactly 2 parameters, 1 given in /var/www/vps/data-store/docs/api/init.php on line 68 +[18-May-2017 12:19:15 Europe/Berlin] PHP Warning: rand() expects exactly 2 parameters, 1 given in /var/www/vps/data-store/docs/api/init.php on line 68 +[18-May-2017 12:19:23 Europe/Berlin] PHP Warning: rand() expects exactly 2 parameters, 1 given in /var/www/vps/data-store/docs/api/init.php on line 68 +[18-May-2017 12:19:24 Europe/Berlin] PHP Warning: rand() expects exactly 2 parameters, 1 given in /var/www/vps/data-store/docs/api/init.php on line 68 diff --git a/src/autoload.php b/src/autoload.php @@ -0,0 +1,14 @@ +<?php + +if (!defined('APPROOT')) define('APPROOT', dirname(__DIR__)); +if (!defined('DS')) define('DS' , DIRECTORY_SEPARATOR); + +// Simple PSR-0 autoloader +spl_autoload_register(function( $className ) { + $path = __DIR__ . DIRECTORY_SEPARATOR; + $path .= str_replace("\\", DIRECTORY_SEPARATOR, $className); + $path .= '.php'; + if ( file_exists($path) && is_readable($path) ) { + include_once $path; + } +});