Skip to content

Instantly share code, notes, and snippets.

@barnabywalters
Last active December 30, 2015 17:49
Show Gist options
  • Star 1 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save barnabywalters/7863676 to your computer and use it in GitHub Desktop.
Save barnabywalters/7863676 to your computer and use it in GitHub Desktop.
Taproot post-storage code as of 2013-12-08. Working live on waterpigs.co.uk, probably a little too rusty/specific for convenient use elsewhere but maybe of interest. storage.php is basic functional library — currently it depends on symfony/yaml for post serialisation but that can be replaced with any function capable of serializing an array stru…
<?php
use Taproot\Storage;
function toIndexRow($note) {
$timestamp = Storage\toDateTime($note['published'])->format('U');
return [$note['id'], $timestamp, ' ' . implode(' ', $note['tags']) . ' ', !empty($note['deleted'])];
}
$notes = new Storage($basepath, 'toIndexRow', ['id', 'published', 'tagged', 'deleted']);
// Now arbitrary array structures can be saved and queried
$note = [
'id' => '1',
'content' => 'Hello world!',
'published' => '2013-12-08 12:00:00',
'tags' => ['test', 'dev']
];
$notes->put($note);
$notes->index($note);
$note = $notes->get('1');
// by default posts can be queried by tags ('tagged' => 'tag') and deleted status
// limit sets maximin number of results, before/after allow for crude-but-stable datetime-based pagination
// query function/method returns array with results, then newer/older notes if they exist, or null if they don’t exist
list($notes, $olderNote, $newerNote) = $notes->query(['limit' => 20]);
<?php
namespace Taproot;
use SplQueue;
class FifoQueue extends SplQueue {
protected $capacity;
public function __construct($capacity) { $this->capacity = $capacity; }
function enqueue($item) {
$this->push($item);
if ($this->count() > $this->capacity)
$this->dequeue();
}
function each($callback) {
$out = [];
foreach ($this as $item)
$out[] = $callback($item);
return $out;
}
function atCapacity() {
return $this->count() == $this->capacity;
}
function second() {
return $this->offsetExists(1) ? $this->offsetGet(1) : null;
}
function penultimate() {
$o = $this->capacity - 2;
return $this->offsetExists($o) ? $this->offsetGet($o) : null;
}
function last() {
$o = $this->capacity - 1;
return $this->offsetExists($o) ? $this->offsetGet($o) : null;
}
}
<?php
namespace Taproot;
use Taproot\Storage;
class Storage {
protected $basepath;
protected $toIndexRow;
protected $columns;
protected $indexSorter;
protected $filters;
protected $idKey;
public function __construct($basepath, $toIndexRow, $columns=['id', 'published'], $indexSorter=null, $filters=null, $idKey='id') {
$this->basepath = $basepath;
$this->toIndexRow = $toIndexRow;
$this->columns = $columns;
$this->idKey = $idKey;
if (!file_exists($basepath))
@mkdir($basepath);
if ($indexSorter === null)
$indexSorter = function ($a, $b) {
return ($a[1] - $b[1]) * -1;
};
$this->indexSorter = $indexSorter;
if ($filters === null) {
$filters = [];
if (($key = array_search('tagged', $columns)) !== false)
$filters[] = Storage\makeTaggedFilter($key);
if (($key = array_search('deleted', $columns)) !== false)
$filters[] = Storage\makeDeletedFilter($key);
}
$this->filters = $filters;
}
public function get($id) {
return Storage\get($this->basepath, $id);
}
public function put($item) {
return Storage\put($this->basepath, $item, $this->idKey);
}
public function index($item) {
return Storage\index($this->basepath, $this->columns, $this->toIndexRow, $this->indexSorter, $item);
}
public function reindex() {
return Storage\reindex($this->basepath, $this->columns, $this->toIndexRow, $this->indexSorter);
}
public function query(array $query) {
return Storage\query($this->basepath, $query, $this->filters);
}
public function prevNextItems($idOrItem) {
return Storage\prevNextItems($this->basepath, $idOrItem);
}
}
<?php
namespace Taproot\Storage;
use DateTime;
use Symfony\Component\Yaml\Yaml;
use Taproot\FifoQueue;
function toDateTime($dt) {
return $dt instanceof Datetime ? $dt : new DateTime($dt);
}
function get($basepath, $id) {
$notepath = $basepath . DIRECTORY_SEPARATOR . $id . '.yaml';
if (file_exists($notepath) and $id !== null)
return Yaml::parse(file_get_contents($notepath));
else
return null;
}
// given a post, saves it
function put($basepath, array $note, $idKey='id') {
$notepath = $basepath . DIRECTORY_SEPARATOR . $note[$idKey] . '.yaml';
return file_put_contents($notepath, Yaml::dump($note));
}
// given a post, updates the relevant indexes
function index($basepath, $columns, $toIndexRow, $sorter, array $note) {
$indexFile = fopen($basepath . DIRECTORY_SEPARATOR . 'index.csv', 'c+');
$csvColumns = fgetcsv($indexFile);
$rowToSave = $toIndexRow($note);
$index = [];
while (($row = fgetcsv($indexFile)) !== false) {
if ($rowToSave[0] == $row[0]) {
$index[] = $rowToSave;
$updated = true;
} else {
$index[] = $row;
}
}
fclose($indexFile);
if (!isset($updated))
array_unshift($index, $rowToSave);
usort($index, $sorter);
$indexFile = fopen($basepath . DIRECTORY_SEPARATOR . 'index.csv', 'w');
fputcsv($indexFile, $csvColumns ?: $columns);
foreach ($index as $row)
fputcsv($indexFile, $row);
fclose($indexFile);
return true;
}
// reindexes all items in this basepath
function reindex($basepath, $columns, $toIndexRow, $sorter) {
$index = [];
$noteIds = array_map(function ($p) { return pathinfo($p, PATHINFO_FILENAME); }, glob($basepath . '/*.yaml'));
$notes = array_map(function ($id) use ($basepath) { return get($basepath, $id); }, $noteIds);
foreach ($notes as $note) {
$index[] = $toIndexRow($note);
}
usort($index, $sorter);
$indexFile = fopen($basepath . DIRECTORY_SEPARATOR . 'index.csv', 'w');
fputcsv($indexFile, $columns);
foreach ($index as $row)
fputcsv($indexFile, $row);
fclose($indexFile);
}
// a comparator for ordering by published datetime
function publishedComparator($dir) {
$dir = strpos($dir, 'asc') == 0 ? 1 : -1;
return function($a, $b) use ($dir) {
$ats = toDateTime($a['published'])->format('U');
$bts = toDateTime($b['published'])->format('U');
return ($bts - $ats) * $dir;
};
}
// returns a filter function allowing rows to be queried on the presence of a string within a space-separated string
function makeTaggedFilter($rowIndex = 2, $queryKey = 'tagged') {
return function ($row, $query) use ($rowIndex, $queryKey) {
if (!empty($query[$queryKey]))
return strpos(" {$row[$rowIndex]} ", " {$query[$queryKey]} ") !== false;
return true;
};
}
// syntatic sugar for common case of boolean filter
function makeDeletedFilter($rowIndex = 3) { return makeBooleanFilter($rowIndex, true); }
/**
* In : 1 0
* Inv: 1 0 1
* : 0 1 0
*/
function makeBooleanFilter($rowIndex, $invert = false) {
return function ($row, $query) use ($rowIndex, $invert) {
return $invert == empty($row[$rowIndex]);
};
}
// returns a filter function allowing rows to be queried on equality to a given value
function makeEqualityFilter($rowIndex, $queryKey) {
return function ($row, $query) use ($rowIndex, $queryKey) {
if (!empty($query[$queryKey]))
return $query[$queryKey] == $row[$rowIndex];
return true;
};
}
// queries items based on a query and a list of filters
// pagination behaviour is built-in and hardcoded to certain indexes, this is bad and should be made more flsxible
function query($basepath, array $query, array $filters = []) {
$itemFilter = count($filters) === 0
? function () { return true; }
: function ($row) use ($query, $filters) {
foreach ($filters as $filter) {
if ($filter($row, $query) === false) return false;
}
return true;
};
if (!isset($query['limit']))
$query['limit'] = 20;
$itemQueue = new FifoQueue($query['limit'] + 2);
$itemQueue->enqueue(null); // pre-fill the newer item buffer
$indexFile = fopen($basepath . "/index.csv", 'c+');
fgetcsv($indexFile);
if (isset($query['after'])) {
$after = Text\toDateTime($query['after'])->format('U');
$break = function ($itemQueue) use ($after) {
if ($itemQueue->atCapacity() and $itemQueue->last() !== null and $itemQueue->last()[1] <= $after) {
return true;
}
};
} else {
$before = isset($query['before']) ? Text\toDateTime($query['before']) : new DateTime;
$before = $before->format('U');
$break = function ($itemQueue) use ($before) {
if ($itemQueue->atCapacity() and $itemQueue->second() !== null and $itemQueue->second()[1] < $before) {
return true;
}
};
}
while (($row = fgetcsv($indexFile)) !== false) {
if (!$itemFilter($row)) continue;
$itemQueue->enqueue($row);
if ($break($itemQueue)) break;
}
fclose($indexFile);
$items = iterator_to_array($itemQueue);
if (count($items) > $query['limit']) {
$olderBuffer = array_pop($items);
$newerBuffer = array_shift($items);
$olderItem = $olderBuffer !== null ? get($basepath, $olderBuffer[0]) : null;
$newerItem = $newerBuffer !== null ? get($basepath, $newerBuffer[0]) : null;
} else {
$olderItem = null;
$newerItem = null;
}
$items = array_filter($items);
$items = array_map(function ($r) use ($basepath) { return get($basepath, $r[0]); }, $items);
return [$items, $olderItem, $newerItem];
}
// given an item id, return an array of [prevItem, nextItem] where each might be null if it doesn’t exist
// warning: calling this with a nonexistant id produces undefined behaviour
function prevNextItems($basepath, $itemOrId) {
$id = is_string($itemOrId) ? $itemOrId : $itemOrId['id'];
$indexFile = fopen($basepath . '/index.csv', 'r');
$noteQueue = new FifoQueue(3);
// to do: make this happen by default on queue creation
$noteQueue->enqueue(null); $noteQueue->enqueue(null); $noteQueue->enqueue(null);
fgetcsv($indexFile);
while (($row = fgetcsv($indexFile)) !== false) {
$noteQueue->enqueue($row);
if ($noteQueue->penultimate()[0] == $id)
break;
}
fclose($indexFile);
return [get($basepath, $noteQueue->last()[0]), get($basepath, $noteQueue[0][0])];
}
// Taproot-specific syntatic sugar for generating pagination links from query results
function paginationLinks($items, $olderItem, $newerItem, $request, $app, $url) {
if ($newerItem !== null) {
$q = $request->query->all();
unset($q['before']);
$q['after'] = Text\toDateTime($items[0]['published'])->format(DateTime::W3C);
$newerLink = $app['url_generator']->generate($url, $q);
} else {
$newerLink = null;
}
if ($olderItem !== null) {
$q = $request->query->all();
unset($q['after']);
$q['before'] = Text\toDateTime($items[count($items) - 1]['published'])->format(DateTime::W3C);
$olderLink = $app['url_generator']->generate($url, $q);
} else {
$olderLink = null;
}
return [$olderLink, $newerLink];
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment