Database
Marko’s database layer uses an entity-driven approach with the Data Mapper pattern. Your PHP entities define the schema — no separate migration files to write by hand.
composer require marko/database marko/database-pgsqlConfigure your connection in config/database.php:
<?php
declare(strict_types=1);
return [ 'default' => env('DB_CONNECTION', 'pgsql'), 'connections' => [ 'pgsql' => [ 'driver' => 'pgsql', 'host' => env('DB_HOST', 'localhost'), 'port' => (int) env('DB_PORT', '5432'), 'database' => env('DB_DATABASE', 'marko'), 'username' => env('DB_USERNAME', 'marko'), 'password' => env('DB_PASSWORD', ''), ], ],];Defining Entities
Section titled “Defining Entities”Entities are plain PHP classes. Marko infers the database schema from PHP types:
<?php
declare(strict_types=1);
namespace App\Blog\Entity;
use Marko\Database\Attribute\Entity;use Marko\Database\Attribute\Id;use DateTimeImmutable;
#[Entity(table: 'posts')]class Post{ #[Id] public int $id;
public string $title;
public string $body;
public bool $published = false;
public DateTimeImmutable $createdAt;}Type Inference
Section titled “Type Inference”Marko maps PHP types to database columns automatically:
| PHP Type | PostgreSQL | MySQL |
|---|---|---|
int | INTEGER | INT |
string | TEXT | VARCHAR(255) |
bool | BOOLEAN | TINYINT(1) |
float | DOUBLE PRECISION | DOUBLE |
DateTimeImmutable | TIMESTAMP | DATETIME |
Migrations
Section titled “Migrations”Generate and run migrations from your entity definitions:
# Generate a migration from entity changesmarko db:migrate
# Roll back the last migrationmarko db:rollback
# Reset and re-run all migrationsmarko db:reset
# Check migration statusmarko db:statusQuerying
Section titled “Querying”Query Builder
Section titled “Query Builder”Use QueryBuilderInterface for fluent query building:
<?php
declare(strict_types=1);
namespace App\Blog\Repository;
use Marko\Database\Query\QueryBuilderInterface;use DateTimeImmutable;
class PostRepository{ public function __construct( private readonly QueryBuilderInterface $queryBuilder, ) {}
public function findById(int $id): ?array { return $this->queryBuilder->table('posts') ->where('id', '=', $id) ->first(); }
public function findPublished(): array { return $this->queryBuilder->table('posts') ->where('published', '=', true) ->orderBy('created_at', 'DESC') ->get(); }
public function create(string $title, string $body): int { return $this->queryBuilder->table('posts')->insert([ 'title' => $title, 'body' => $body, 'published' => false, 'created_at' => new DateTimeImmutable(), ]); }}Seeders
Section titled “Seeders”Populate your database with test or default data:
<?php
declare(strict_types=1);
namespace App\Blog\Database\Seeder;
use Marko\Database\Query\QueryBuilderInterface;use Marko\Database\SeederInterface;use DateTimeImmutable;
class PostSeeder implements SeederInterface{ public function __construct( private readonly QueryBuilderInterface $queryBuilder, ) {}
public function run(): void { $this->queryBuilder->table('posts')->insert([ 'title' => 'Hello World', 'body' => 'Welcome to Marko.', 'published' => true, 'created_at' => new DateTimeImmutable(), ]); }}marko db:seedSwitching Database Drivers
Section titled “Switching Database Drivers”Thanks to the interface/implementation split, switching from MySQL to PostgreSQL (or vice versa) is a Composer swap. Each driver package automatically binds ConnectionInterface to its implementation via its module.php:
# Remove the old driver, install the new onecomposer remove marko/database-mysqlcomposer require marko/database-pgsqlThat’s it — no binding changes needed. The driver package handles the wiring. Update your config/database.php connection settings to match the new driver, and your application code stays the same since it depends on ConnectionInterface, not a specific driver.
Next Steps
Section titled “Next Steps”- Authentication — user management and guards
- Caching — cache query results
- Testing — test database interactions
- Database package reference — full API details