Skip to content

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.

Terminal window
composer require marko/database marko/database-pgsql

Configure your connection in config/database.php:

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', ''),
],
],
];

Entities are plain PHP classes. Marko infers the database schema from PHP types:

app/blog/Entity/Post.php
<?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;
}

Marko maps PHP types to database columns automatically:

PHP TypePostgreSQLMySQL
intINTEGERINT
stringTEXTVARCHAR(255)
boolBOOLEANTINYINT(1)
floatDOUBLE PRECISIONDOUBLE
DateTimeImmutableTIMESTAMPDATETIME

Generate and run migrations from your entity definitions:

Terminal window
# Generate a migration from entity changes
marko db:migrate
# Roll back the last migration
marko db:rollback
# Reset and re-run all migrations
marko db:reset
# Check migration status
marko db:status

Use QueryBuilderInterface for fluent query building:

app/blog/Repository/PostRepository.php
<?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(),
]);
}
}

Populate your database with test or default data:

app/blog/Database/Seeder/PostSeeder.php
<?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(),
]);
}
}
Terminal window
marko db:seed

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:

Terminal window
# Remove the old driver, install the new one
composer remove marko/database-mysql
composer require marko/database-pgsql

That’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.