preflow/data

Multi-storage data layer for Preflow. Supports JSON files and SQLite out of the box; different models can use different backends simultaneously.

Installation

composer require preflow/data

Requires PHP 8.4+, ext-pdo, ext-pdo_sqlite.

What It Does

Models are annotated with PHP attributes. DataManager is the single entry point -- it reads model metadata via reflection, selects the right storage driver, and returns typed results. QueryBuilder provides a fluent API for filtering, sorting, searching, and pagination. Migrations are handled by Schema + Table + Migrator.

Attributes

Attribute Target Description
#[Entity(table: 'posts', storage: 'sqlite')] Class Maps model to a table/collection and a named driver. storage defaults to 'default'.
#[Id] Property Marks the primary key field.
#[Field(searchable: true)] Property Marks a field; searchable: true includes it in full-text search.
#[Timestamps] Class Adds created_at / updated_at handling.

Model

$model->fill(array $data): void   // hydrate from associative array
$model->toArray(): array          // export public properties

DataManager

$dm->find(Post::class, $id): ?Post
$dm->query(Post::class): QueryBuilder
$dm->save(Model $model): void
$dm->delete(Post::class, $id): void

QueryBuilder

All methods return $this for chaining except the terminal methods.

->where('status', 'published')
->where('views', '>', 100)
->orWhere('featured', true)
->orderBy('created_at', SortDirection::Desc)
->limit(10)->offset(20)
->search('php')          // searches all #[Field(searchable: true)] fields
->get(): ResultSet       // all results
->first(): ?Model
->paginate(perPage: 15, currentPage: 2): PaginatedResult

Storage Drivers

Class Backend
JsonFileDriver One .json file per record at {basePath}/{table}/{id}.json
SqliteDriver PDO-based SQLite via PdoDriver + QueryCompiler

Migrations

abstract class Migration
{
    abstract public function up(Schema $schema): void;
    public function down(Schema $schema): void {}
}

Schema methods: create(string $table, callable $callback), drop(string $table).

Table builder: uuid, string, text, integer, boolean, json, timestamps, nullable(), primary(), index().

Usage

Model:

use Preflow\Data\{Model, ModelMetadata};
use Preflow\Data\Attributes\{Entity, Id, Field, Timestamps};

#[Entity(table: 'posts', storage: 'sqlite')]
#[Timestamps]
final class Post extends Model
{
    #[Id]
    public string $id;

    #[Field(searchable: true)]
    public string $title;

    #[Field(searchable: true)]
    public string $body;

    public string $status = 'draft';
}

Querying:

// Find by ID
$post = $dm->find(Post::class, 'abc-123');

// Filtered query
$posts = $dm->query(Post::class)
    ->where('status', 'published')
    ->orderBy('created_at', SortDirection::Desc)
    ->paginate(perPage: 10, currentPage: 1);

// Full-text search across searchable fields
$results = $dm->query(Post::class)->search('preflow')->get();

Saving:

$post = new Post();
$post->fill(['id' => 'abc-123', 'title' => 'Hello', 'body' => '...', 'status' => 'published']);
$dm->save($post);

Migration:

use Preflow\Data\Migration\{Migration, Schema};

final class CreatePostsTable extends Migration
{
    public function up(Schema $schema): void
    {
        $schema->create('posts', function (Table $t) {
            $t->uuid('id')->primary();
            $t->string('title')->index();
            $t->text('body');
            $t->string('status');
            $t->timestamps();
        });
    }

    public function down(Schema $schema): void
    {
        $schema->drop('posts');
    }
}

Multi-storage setup:

$dm = new DataManager([
    'default' => new JsonFileDriver(basePath: '/storage/data'),
    'sqlite'  => new SqliteDriver(new \PDO('sqlite:/storage/db.sqlite')),
]);

Models with #[Entity(storage: 'sqlite')] use SQLite; all others fall back to 'default'.