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);
use Marko\Config\Env;
return [ 'driver' => 'pgsql', 'host' => Env::string('DB_HOST', 'localhost'), 'port' => Env::int('DB_PORT', 5432, min: 1, max: 65535), 'database' => Env::string('DB_DATABASE', 'marko'), 'username' => Env::string('DB_USERNAME', 'marko'), 'password' => Env::string('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\Attributes\Column;use Marko\Database\Attributes\Table;use Marko\Database\Entity\Entity;use DateTimeImmutable;
#[Table('posts')]class Post extends Entity{ #[Column(primaryKey: true, autoIncrement: true)] public ?int $id = null;
#[Column] public string $title;
#[Column] public string $body;
#[Column] public bool $published = false;
#[Column] public ?string $createdAt = null;}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:statusdb:migrate generates migrations only when APP_ENV is development, dev or local. In every other environment, including an unset APP_ENV, it applies the committed files and warns about drift; db:rollback, db:reset, db:rebuild and db:seed run freely only in development and testing (testing, test), need --force in any other environment such as staging, and are always refused in production. See Environment Behaviour.
Querying
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;
readonly class PostRepository{ public function __construct( private 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(), ]); }}Repositories
Section titled “Repositories”Extend Repository for entity-aware queries. findAll() and findBy() return an EntityCollection — an iterable, countable collection with filter, map, sortBy, groupBy, chunk, and pluck methods.
<?php
declare(strict_types=1);
namespace App\Blog\Repository;
use App\Blog\Entity\Post;use Marko\Database\Entity\EntityCollection;use Marko\Database\Repository\Repository;
class PostRepository extends Repository{ protected const ENTITY_CLASS = Post::class;
public function findBySlug(string $slug): ?Post { return $this->findOneBy(['slug' => $slug]); }
public function findPublished(): EntityCollection { return $this->query() ->where('status', '=', 'published') ->orderBy('created_at', 'desc') ->getEntities(); }}query() returns a RepositoryQueryBuilder pre-scoped to the repository’s table. It exposes the full query builder (where, whereIn, whereNotNull, joins, orderBy, limit, etc.), filters on relationships with whereHas() / whereDoesntHave(), and terminates with getEntities() for an EntityCollection or firstEntity() for a single hydrated entity. Fall back to get() / first() only when you want raw arrays (reports, aggregates).
Use with() to eager-load relationships and avoid N+1 queries. Nested relationships use dot notation:
$posts = $postRepository->with('comments', 'tags')->findAll();$posts = $postRepository->with('comments.author')->findAll();Use matching() with QuerySpecification objects to compose reusable query logic:
use App\Blog\Query\PublishedSpec;use App\Blog\Query\RecentSpec;
$posts = $postRepository->matching( new PublishedSpec(), new RecentSpec(limit: 5),);See the Database package reference for the full Relationships, EntityCollection, and Query Specifications API.
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\Seed\SeederInterface;use DateTimeImmutable;
readonly class PostSeeder implements SeederInterface{ public function __construct( private 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