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'.