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();
Full-Text Search
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.