Introduction

MicroDB is a minimalistic file-based JSON object database written in PHP.

Find the full, open source code at GitHub.

Features

  • Stores JSON objects as plain files
  • Concurrency safe
  • Arbitrary indices using custom key functions
  • Listen to database operations through events
  • Synchronize arbitrary operations

Basics

The following code demonstrates all basic operations on a MicroDB database.

<?php

    $db = new \MicroDB\Database('data/posts'); // data directory

    // create an item
    // id is an auto incrementing integer
    $id = $db->create(array(
    'title' => 'Lorem ipsum',
    'body' => 'At vero eos et accusam et justo duo dolores et ea rebum.'
    ));

    // load an item
    $post = $db->load($id);

    // save an item
    $post['tags'] = array('lorem', 'ipsum');
    $db->save($id, $post);

    // find items
    $posts = $db->find(function($post) {
    return is_array(@$post['tags']) && in_array('ipsum', @$post['tags']);
    });

    foreach($posts as $id => $post) {
    print_r($post);
    }

    // delete an item
    $db->delete($id);

Requirements

PHP 5.3+

Installation

The composer package name is morris/microdb. You can also download or fork the GitHub repository.

Indices

An index maps some scalar value to one or more database IDs. This mapping can be used to find objects more quickly.

Indices must be created before any database operation to work. Usually you just create them directly after creating the database.

<?php

    // create database
    $db = new \MicroDB\Database('data/users');

    // create index
    // maps the "email" attribute of any data object
    // updates itself whenever an object is saved or deleted
    $emailIndex = new \MicroDB\Index($db, 'email', 'email');

    // using the index, you can quickly find items
    // this is faster than using $db->find(array('email' => 'foo@bar.de'));
    $foo = $emailIndex->first('foo@bar.de');

    // let's create a more complex index
    // this index keeps track of the number of items on users' todo lists
    $todoIndex = new \MicroDB\Index($db, 'todo', function($user) {
    return count($user['todo']);
    });

    // then we can find all users with at least 10 tasks
    $users = $todoIndex->find(function($todo) { return $todo >= 10; });

Note that the Database class does not use Index directly. Indices work by listening to database events and are stored as database objects.

Events

The following events are triggered by MicroDB:

beforeSave
saved
beforeLoad
loaded
beforeDelete
deleted

Handlers of these events receive an object of class MicroDB\Event as argument with properties id and possibly data.

Note that IDs starting with an underscore are not getting any of these events because they are considered hidden, e.g. index data files.

For example, the following code prevents modifications on user objects unless you are an admin:

<?php

    $db->on('beforeSave, beforeDelete', function($event) use ($app) {
    if($data['type'] === 'user' && !$app->isAdmin())
    throw new \Exception(
    'Sorry, only admins are allowed to modify user data.'
    );
    });

In the next example, the IDs of items are automatically injected after loading:

<?php

    $db->on('loaded', function($event) use ($app) {
    $event->data['id'] = $event->id;
    });

You can also trigger events manually and unbind handlers:

<?php

    $handler = function($arg) {
    echo $arg;
    };

    // bind to custom event
    $db->on('hello', $handler);

    // trigger an event with arguments
    $db->trigger('hello', 'Hello World');

    // unbind a specific handler
    $db->off($handler);

    // unbind all handlers of an event
    $db->off('hello');

Tests

Before running the tests you must run composer update in the microdb directory. This will install development dependencies like PHPUnit. Then, you can run the tests with vendor/bin/phpunit tests.

To test the concurrent behavior you can run the tests twice in parallel: vendor/bin/phpunit tests & vendor/bin/phpunit tests

License

MicroDB is licensed under the MIT License. Attribution is not required but very welcome!

The MIT License (MIT)

Copyright (c) 2014 Morris Brodersen <mb@morrisbrodersen.de>

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.

MicroDB\Database

<?php

    namespace MicroDB;

    /**
    * A file-based JSON object database
    */
    class Database {

    /**
    * Constructor
    */
    function __construct($path, $mode = 0644);

    /**
    * Create an item with auto incrementing id
    */
    function create($data = array());

    /**
    * Save data to database
    */
    function save($id, $data) ;

    /**
    * Load data from database
    */
    function load($id, $key = null) ;

    /**
    * Delete data from database
    */
    function delete($id);

    /**
    * Find data matching key-value map or callback
    */
    function find($where = array(), $first = false);

    /**
    * Find first item key-value map or callback
    */
    function first($where = null);

    /**
    * Checks wether an id exists
    */
    function exists($id);

    /**
    * Triggers "repair" event.
    * On this event, applications should repair inconsistencies in the
    * database, e.g. rebuild indices.
    */
    function repair();

    /**
    * Call a function for each id in the database
    */
    function eachId($func);

    /**
    * Is this id hidden, i.e. no events should be triggered?
    * Hidden ids start with an underscore
    */
    function hidden($id);

    // SYNCHRONIZATION

    /**
    * Call a function in a mutually exclusive way, locking on files
    * A process will only block other processes and never block itself,
    * so you can safely nest synchronized operations.
    */
    function synchronized($locks, $func);

    /**
    * Get data path
    */
    function getPath();

    // EVENTS

    /**
    * Bind a handler to an event, with given priority.
    * Higher priority handlers will be executed earlier.
    * @param string|array Event keys
    * @param callable Handler
    * @param number Priority of handler
    */
    function on($event, $handler, $priority = 0);

    /**
    * Unbind a handler on one, multiple or all events
    * @param string|array Event keys, comma separated
    * @param callable Handler
    */
    function off($event, $handler = null);

    /**
    * Trigger one or more events with given arguments
    * @param string|array Event keys, whitespace/comma separated
    * @param mixed Optional arguments
    */
    function trigger($event, $args = null);
    }

MicroDB\Index

<?php

    namespace MicroDB;

    /**
    * Represents and manages an index on a database.
    * An index maps keys to ids where keys are computed from items.
    */
    class Index {

    /**
    * Creates an index on a database with a name and an index key
    * function. The index listens to database events to update itself.
    */
    function __construct($db, $name, $keyFunc, $compare = null);

    /**
    * Find ids that match a key/callback
    */
    function find($where, $first = false);

    /**
    * Get first matching id
    */
    function first($where);

    /**
    * Get slice of mapping, useful for paging
    */
    function slice($offset = 0, $length = null);

    /**
    * Load items that match a key/callback
    */
    function load($where, $first = false);

    /**
    * Load first matching item
    */
    function loadFirst($where);

    /**
    * Load slice of mapping
    */
    function loadSlice($offset = 0, $length = null);

    /**
    * Update item in index
    * Synchronized
    */
    function update($id, $data);

    /**
    * Delete item from index
    * Synchronized
    */
    function delete($id);

    /**
    * Rebuild index completely
    */
    function rebuild();

    /**
    * Apply a synchronized operation on the index
    */
    function apply($func);

    /**
    * Load index
    */
    function restore();

    /**
    * Save index
    */
    function store();

    /**
    * Compute index key(s) of data
    */
    function keys($data);

    /**
    * Get name of index
    */
    function getName();
    }

MicroDB\Cache

<?php

    namespace MicroDB;

    /**
    * A cache for data loading
    */
    class Cache {

    /**
    * Constructor
    */
    function __construct($db);

    /**
    * Load a possibly cached item
    */
    function load($id);

    /**
    * Execute a function on each item (id, data)
    */
    function each($func);
    }

MicroDB\Event

<?php

    namespace MicroDB;

    /**
    * A container for database events
    */
    class Event {

    /**
    * Constructor
    */
    function __construct($db, $id, $data = null);

    var $db;
    var $id;
    var $data;
    }