Data Layer

Preflow's data layer uses PHP attributes to map models to storage backends. DataManager is the single entry point for all data operations. Different models can use different backends simultaneously -- SQLite for structured data, JSON files for content.

Defining Models

A model is a PHP class extending Model with attribute annotations:

use Preflow\Data\Model;
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';
}

Attributes

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

DataManager API

The DataManager provides four core operations:

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

// Start a query builder
$posts = $dm->query(Post::class);

// Save (insert or update)
$dm->save($post);

// Delete by ID
$dm->delete(Post::class, 'abc-123');

QueryBuilder

Build queries with a fluent, chainable API:

$results = $dm->query(Post::class)
    ->where('status', 'published')
    ->where('views', '>', 100)
    ->orWhere('featured', true)
    ->orderBy('created_at', SortDirection::Desc)
    ->limit(10)
    ->offset(20)
    ->get();

Search across all fields marked with searchable: true:

$results = $dm->query(Post::class)->search('preflow')->get();

Pagination

$paginated = $dm->query(Post::class)
    ->where('status', 'published')
    ->orderBy('created_at', SortDirection::Desc)
    ->paginate(perPage: 15, currentPage: 2);

The PaginatedResult contains the items, total count, current page, and page count.

Terminal Methods

Method Returns
get() ResultSet -- all matching results
first() ?Model -- first matching result or null
paginate(perPage, currentPage) PaginatedResult

Storage Drivers

Preflow ships two storage drivers:

Driver Backend Use Case
JsonFileDriver One .json file per record Content, configuration, simple data
SqliteDriver PDO-based SQLite Structured data, relations, search

Multi-Storage Setup

Configure multiple drivers in config/data.php and assign models to specific backends:

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

Saving Data

Create and persist a new record:

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

The fill() method hydrates from an associative array. toArray() exports back.

Migrations

Migrations manage your database schema over time.

Creating a Migration

php preflow make:migration create_posts

This creates a timestamped file in migrations/:

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

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');
    }
}

Table Builder Methods

uuid, string, text, integer, boolean, json, timestamps -- each returns a column builder with nullable(), primary(), and index() modifiers.

Running Migrations

php preflow migrate

This executes all pending migrations in order.