meita/jsonbolt

JSONBolt: fast JSON file database engine with relations and caching for PHP.

Installs: 1

Dependents: 1

Suggesters: 0

Security: 0

Stars: 0

Watchers: 0

Forks: 0

Open Issues: 0

pkg:composer/meita/jsonbolt

1.0.0 2025-12-21 06:52 UTC

This package is auto-updated.

Last update: 2025-12-21 06:56:44 UTC


README

JSONBolt is a fast file-based JSON database engine for PHP. It stores collections as JSON files, provides CRUD, a fluent query builder, relationships, and caching. It works in any PHP framework and includes optional Laravel integration.

Composer package: meita/jsonbolt. PHP namespace: Meita\\JsonBolt.

Features

  • JSON file storage with atomic writes and file locks
  • CRUD operations and bulk inserts
  • Fluent query builder with common operators
  • Relationships: hasOne, hasMany, belongsTo, belongsToMany
  • PSR-16 caching with built-in FileCache and ArrayCache
  • Framework-agnostic core with optional Laravel service provider and facade

Requirements

  • PHP 8.0+
  • ext-json

Installation

composer require meita/jsonbolt

Quick start

<?php

use Meita\JsonBolt\Cache\FileCache;
use Meita\JsonBolt\Database;

$db = new Database(__DIR__ . '/data', [
    'cache' => new FileCache(__DIR__ . '/cache'),
    'cache_ttl' => 300,
]);

$users = $db->collection('users');

$user = $users->insert([
    'name' => 'Sara',
    'email' => 'sara@example.com',
]);

$found = $users->find($user['id']);

$active = $users
    ->where('active', true)
    ->orderBy('name')
    ->limit(10)
    ->get();

Data layout

Each collection is stored as a single JSON file:

  • data/users.json
  • data/orders.json

Metadata is stored in data/.dbe.meta.json for ID counters and cache versions.

Configuration options

$db = new Database(__DIR__ . '/data', [
    'id_key' => 'id',           // default
    'id_strategy' => 'increment', // increment or random
    'cache' => $cache,          // any PSR-16 cache
    'cache_ttl' => 300,         // seconds or DateInterval
    'cache_enabled' => true,
    'cache_prefix' => 'dbe',
    'relations' => [/* ... */],
]);

ID strategies

  • increment (default): auto-increment integer IDs per collection
  • random: 16 hex chars from random_bytes

CRUD operations

$users = $db->collection('users');

$user = $users->insert(['name' => 'Mona']);
$users->insertMany([
    ['name' => 'Ali'],
    ['name' => 'Noor'],
]);

$user = $users->find(1);
$many = $users->findMany([1, 2, 3]);

$updated = $users->update(1, ['name' => 'Mona A.']);
$deleted = $users->delete(1);

Query builder

$results = $users
    ->where('age', '>=', 18)
    ->orWhere('role', 'admin')
    ->orderBy('name', 'asc')
    ->offset(10)
    ->limit(20)
    ->select('id', 'name', 'email')
    ->get();

$first = $users->where('email', 'like', '%@example.com')->first();
$count = $users->where('active', true)->count();

Supported operators

  • =, ==, !=, <>, >, >=, <, <=
  • in, not in
  • contains (arrays or substring in strings)
  • starts_with, ends_with
  • between (array with [min, max])
  • like (SQL-style % and _ wildcards)

Bulk updates and deletes

$updated = $users->where('active', false)->update(['flagged' => true]);
$deleted = $users->where('last_login', '<', '2022-01-01')->delete();

Relationships

Define relations in the database options:

$db = new Database(__DIR__ . '/data', [
    'relations' => [
        'users' => [
            'posts' => [
                'type' => 'hasMany',
                'collection' => 'posts',
                'foreignKey' => 'user_id',
                'localKey' => 'id',
            ],
            'profile' => [
                'type' => 'hasOne',
                'collection' => 'profiles',
                'foreignKey' => 'user_id',
                'localKey' => 'id',
            ],
        ],
        'posts' => [
            'user' => [
                'type' => 'belongsTo',
                'collection' => 'users',
                'foreignKey' => 'user_id',
                'ownerKey' => 'id',
            ],
            'tags' => [
                'type' => 'belongsToMany',
                'collection' => 'tags',
                'pivot' => 'post_tag',
                'foreignPivotKey' => 'post_id',
                'relatedPivotKey' => 'tag_id',
                'localKey' => 'id',
                'relatedKey' => 'id',
            ],
        ],
    ],
]);

Pivot collection example:

[
  { "post_id": 1, "tag_id": 5 },
  { "post_id": 1, "tag_id": 9 }
]

Use with() for eager loading:

$users = $db->collection('users')->with(['posts', 'profile'])->get();
$posts = $db->collection('posts')->with('tags')->get();

Nested relations are supported:

$users = $db->collection('users')->with('posts.tags')->get();

Tip: For best results, always set foreignKey and localKey explicitly. The default key guessing is intentionally simple.

Caching

JSONBolt uses PSR-16 caching. You can use any adapter. Built-in caches:

  • Meita\JsonBolt\Cache\ArrayCache (in-memory)
  • Meita\JsonBolt\Cache\FileCache (fast disk cache)
  • Meita\JsonBolt\Cache\NullCache (disable caching)
use Meita\JsonBolt\Cache\FileCache;

$db = new Database(__DIR__ . '/data', [
    'cache' => new FileCache(__DIR__ . '/cache'),
    'cache_ttl' => 300,
]);

Cache keys are automatically versioned per collection, so writes invalidate old query caches without clearing the entire cache store.

Full example

<?php

require __DIR__ . '/vendor/autoload.php';

use Meita\JsonBolt\Cache\FileCache;
use Meita\JsonBolt\Database;

$db = new Database(__DIR__ . '/data', [
    'cache' => new FileCache(__DIR__ . '/cache'),
    'cache_ttl' => 600,
    'relations' => [
        'users' => [
            'posts' => [
                'type' => 'hasMany',
                'collection' => 'posts',
                'foreignKey' => 'user_id',
                'localKey' => 'id',
            ],
        ],
        'posts' => [
            'user' => [
                'type' => 'belongsTo',
                'collection' => 'users',
                'foreignKey' => 'user_id',
                'ownerKey' => 'id',
            ],
            'tags' => [
                'type' => 'belongsToMany',
                'collection' => 'tags',
                'pivot' => 'post_tag',
                'foreignPivotKey' => 'post_id',
                'relatedPivotKey' => 'tag_id',
                'localKey' => 'id',
                'relatedKey' => 'id',
            ],
        ],
    ],
]);

$users = $db->collection('users');
$posts = $db->collection('posts');
$tags = $db->collection('tags');
$postTag = $db->collection('post_tag');

$alice = $users->insert(['name' => 'Alice', 'email' => 'alice@example.com']);
$bob = $users->insert(['name' => 'Bob', 'email' => 'bob@example.com']);

$post1 = $posts->insert(['user_id' => $alice['id'], 'title' => 'Hello JSONBolt']);
$post2 = $posts->insert(['user_id' => $alice['id'], 'title' => 'File DB Tips']);
$post3 = $posts->insert(['user_id' => $bob['id'], 'title' => 'Caching Fast']);

$tagFast = $tags->insert(['name' => 'fast']);
$tagJson = $tags->insert(['name' => 'json']);

$postTag->insertMany([
    ['post_id' => $post1['id'], 'tag_id' => $tagFast['id']],
    ['post_id' => $post1['id'], 'tag_id' => $tagJson['id']],
    ['post_id' => $post2['id'], 'tag_id' => $tagJson['id']],
]);

$recent = $posts
    ->where('title', 'like', '%JSON%')
    ->orderBy('title', 'asc')
    ->with(['user', 'tags'])
    ->get();

print_r($recent);

$activeUsers = $users
    ->where('email', 'contains', '@example.com')
    ->with('posts')
    ->get();

print_r($activeUsers);

Laravel integration (optional)

  1. Require Laravel support:
composer require illuminate/support
  1. Register the service provider and facade:
// config/app.php
'providers' => [
    Meita\JsonBolt\Laravel\DBEServiceProvider::class,
],
'aliases' => [
    'DBE' => Meita\JsonBolt\Laravel\Facades\DBE::class,
],
  1. Publish config (optional):
php artisan vendor:publish --tag=dbe-config

Example in Laravel:

use Meita\JsonBolt\Database;

$db = app(Database::class);
$users = $db->collection('users')->where('active', true)->get();

Notes and limitations

  • This is a file-based engine; it loads a collection into memory on read.
  • Writes replace the entire collection file (with locks and atomic rename).
  • For very large datasets, consider a dedicated database server.

License

MIT