ryangjchandler / orbit
Power your Eloquent models with flat-file storage.
Fund package maintenance!
Requires
- php: ^8.3
- ext-json: *
- ext-pdo_sqlite: *
- illuminate/console: ^13.0
- illuminate/database: ^13.0
- illuminate/events: ^13.0
- illuminate/support: ^13.0
- spatie/laravel-package-tools: ^1.16.5
- spatie/yaml-front-matter: ^2.0.9
- symfony/yaml: ^7.1.6
Requires (Dev)
- larastan/larastan: ^3.0
- laravel/pint: ^1.18.1
- laravel/scout: ^10.11.5
- orchestra/testbench: ^11.0
- pestphp/pest: ^4.0
- pestphp/pest-plugin-laravel: ^4.0
- phpstan/extension-installer: ^1.4.3
- phpstan/phpstan: ^2.0
- phpstan/phpstan-deprecation-rules: ^2.0
- phpstan/phpstan-phpunit: ^2.0
- phpunit/phpunit: ^12.0
Suggests
None
Provides
None
Conflicts
None
Replaces
None
- 2.x-dev
- v2.0.0
- v2.0.0-rc1
- v2.0.0-beta7
- v2.0.0-beta6
- v2.0.0-beta5
- v2.0.0-beta4
- v2.0.0-beta3
- v2.0.0-beta2
- v2.0.0-beta1
- 1.x-dev
- v1.4.3
- v1.4.2
- v1.4.1
- v1.4.0
- v1.3.0
- v1.2.1
- v1.2.0
- v1.1.4
- v1.1.3
- v1.1.2
- v1.1.1
- v1.1.0
- v1.0.2
- v1.0.1
- v1.0.0
- v0.9.1
- v0.9.0
- v0.8.0
- v0.7.4
- v0.7.3
- v0.7.2
- v0.7.1
- v0.7.0
- v0.6.4
- v0.6.3
- v0.6.2
- v0.6.1
- v0.6.0
- v0.5.2
- v0.5.1
- v0.5.0
- v0.4.1
- v0.4.0
- v0.3.1
- v0.3.0
- v0.2.0
- v0.1.1
- v0.1.0
- dev-single-file-drivers
- dev-fix/multitenancy
- dev-fix/empty-many-to-many
- dev-docs/custom-drivers
- dev-rebuild-test-suite
- dev-test/model-initialization-when-indexing
- dev-2.x-improvements
- dev-2.x-laravel-13
- dev-1.x-laravel-13
- dev-package-rename
This package is auto-updated.
Last update: 2026-11-03 20:36:14 UTC
README
Orbit is a flat-file driver for Laravel Eloquent. Your records live in Markdown, YAML or JSON files that you can edit by hand, and you query them with the Eloquent methods you already know.
Installation
composer require ryangjchandler/orbit
Usage
Implement the Orbit interface, use the Orbital trait and describe your columns in a schema() method, just like a migration:
use Illuminate\Database\Eloquent\Model; use Illuminate\Database\Schema\Blueprint; use Orbit\Concerns\Orbital; use Orbit\Contracts\Orbit; class Post extends Model implements Orbit { use Orbital; protected $guarded = []; public function schema(Blueprint $table): void { $table->id(); $table->string('title'); $table->text('content')->nullable(); } }
Each record is stored as content/posts/{key}.md:
--- id: 1 title: 'Hello world' updated_at: '2026-01-01T00:00:00+00:00' created_at: '2026-01-01T00:00:00+00:00' --- The body of the Markdown file is stored in the `content` column.
Timestamps and Laravel's SoftDeletes trait work as usual. Soft deleted records keep their file with a deleted_at value.
Hand-written files
If a file doesn't include the model's key, Orbit uses the file name. For names like hello-world.md, give the model a string key:
protected $primaryKey = 'slug'; protected $keyType = 'string'; public $incrementing = false; public function schema(Blueprint $table): void { $table->string('slug')->primary(); // ... }
A file that can't be parsed or doesn't match the schema is skipped, and a warning is written to your log.
Drivers
Markdown with YAML front matter is the default. To use another format, return a driver from getOrbitDriver():
public function getOrbitDriver(): string { return \Orbit\Drivers\Yaml::class; // or \Orbit\Drivers\Json::class }
To store a model's files in a different directory, override getOrbitSource().
Storing every record in one file
The FlatJson driver stores all of a model's records in a single file, such as content/posts.json, keyed by each record's key:
{
"1": {
"id": 1,
"title": "Hello world"
}
}
As with one file per record, a record that doesn't include its key uses its key in the file. A record that can't be indexed is skipped and left in the file.
Custom drivers
To support another file format, write a class that implements Orbit\Contracts\Driver:
use Orbit\Contracts\Driver; class Toml implements Driver { public function parse(string $fileContents): array { // Turn the contents of a file into an array of attributes. } public function compile(array $attributes): string { // Turn an array of attributes into the contents of a file. } public function extension(): string { return 'toml'; } }
Then return it from getOrbitDriver(). Orbit resolves drivers from the container, so their constructor dependencies are injected.
compile() receives attributes that are ready to store: array casts are arrays, dates are ISO 8601 strings, and null values are left out. parse() should return attributes in the same shape.
If the driver needs extra columns, also implement Orbit\Contracts\ModifiesSchema and add them in its schema(Blueprint $table) method. The Markdown driver uses this to add the content column.
To store every record in one file, also implement Orbit\Contracts\SingleFile. parse() then returns all of the records in the file, and compile() receives them, keyed by each record's key.
Multitenancy
To keep each tenant's records apart, return a different table and directory per tenant:
public function getTable() { return 'posts_'.tenant()->id; } public function getOrbitSource(): string { return 'tenants/'.tenant()->id.'/posts'; }
Orbit builds a separate cache for each table and directory, the first time it's queried.
How it works
Your files are the source of truth. Orbit keeps a SQLite cache of them so queries, relationships and pagination behave exactly like a normal database.
- On the first query of each request, Orbit checks whether any file or the model's schema has changed, and rebuilds that model's cache if so.
- Saving, deleting or incrementing a model writes its file inside a transaction. If the file can't be written, the change is rolled back.
Production
Run php artisan orbit:cache when you deploy. It builds the cache for every Orbit model in the directories listed in the models config option (app/Models by default) and stops checking files for changes. Run it again, or php artisan orbit:clear, after changing content.
Publish the config file to change these directories or the content and cache paths:
php artisan vendor:publish --tag=orbit-config
Limitations
Query-level writes such as Post::where('draft', true)->delete(), update(), insert() or truncate() don't load models, so they don't update files, and their changes are lost when the cache is rebuilt. Load the models and save or delete them one by one:
Post::where('draft', true)->get()->each->delete();
