Tables

Tables in PHPNomad are schema definitions that describe the structure of your database tables. They define columns, indexes, primary keys, and constraints in a database-agnostic way, allowing handlers and query builders to generate the correct SQL for your target database.

A table object is not a query builder or active record. It's a metadata container that describes what a table looks like, which handlers use to create schemas and QueryBuilder uses to generate queries.

What Tables Define

A table definition specifies:

These definitions are created using factory classes from the phpnomad/database package, which provide a fluent API for building schema definitions.

Why Table Objects Exist

In PHPNomad, schema lives in code, not in migration scripts or raw SQL. This has several benefits:

Handlers use table definitions to:

The Base Table Class

The Table class is the standard base for defining entity tables. You extend it and provide column definitions, a primary key, and optional indexes.

Basic example

<?php

use PHPNomad\Database\Interfaces\Table;
use PHPNomad\Database\Factories\Column;
use PHPNomad\Database\Factories\PrimaryKey;

final class PostTable implements Table
{
    public function __construct(
        private Column $columnFactory,
        private PrimaryKey $primaryKeyFactory
    ) {}

    public function getTableName(): string
    {
        return 'posts';
    }

    public function getColumns(): array
    {
        return [
            $this->columnFactory->int('id')->autoIncrement(),
            $this->columnFactory->string('title', 255)->notNull(),
            $this->columnFactory->text('content')->notNull(),
            $this->columnFactory->int('author_id')->notNull(),
            $this->columnFactory->datetime('published_date')->nullable(),
            $this->columnFactory->datetime('created_at')->default('CURRENT_TIMESTAMP'),
            $this->columnFactory->datetime('updated_at')->default('CURRENT_TIMESTAMP')->onUpdate('CURRENT_TIMESTAMP'),
        ];
    }

    public function getPrimaryKey(): PrimaryKey
    {
        return $this->primaryKeyFactory->create('id');
    }

    public function getIndexes(): array
    {
        return [
            // Add index on author_id for faster lookups
            $this->indexFactory->create('idx_author', ['author_id']),
        ];
    }
}

This defines a posts table with:

Column Factories

Column definitions are created using factory methods that return Column objects. PHPNomad provides several included factories for common patterns:

You can also use the base Column factory to define custom columns with full control over type, size, nullability, defaults, and constraints.

Junction Tables

PHPNomad provides a specialized JunctionTable class for many-to-many relationships. Junction tables store associations between two entities (e.g., posts and tags) without additional data.

A junction table:

Example: PostTag junction table

<?php

use PHPNomad\Database\Interfaces\JunctionTable;
use PHPNomad\Database\Factories\ForeignKey;

final class PostTagTable implements JunctionTable
{
    public function __construct(
        private ForeignKey $foreignKeyFactory
    ) {}

    public function getTableName(): string
    {
        return 'post_tags';
    }

    public function getColumns(): array
    {
        return [
            $this->foreignKeyFactory->create('post_id', 'posts', 'id'),
            $this->foreignKeyFactory->create('tag_id', 'tags', 'id'),
        ];
    }

    public function getPrimaryKey(): array
    {
        return ['post_id', 'tag_id']; // Compound key
    }

    public function getIndexes(): array
    {
        return [
            // Index for "which tags are on this post?"
            $this->indexFactory->create('idx_post', ['post_id']),
            // Index for "which posts have this tag?"
            $this->indexFactory->create('idx_tag', ['tag_id']),
        ];
    }
}

Junction tables are used with the JunctionTable class to manage many-to-many relationships efficiently.

Table Lifecycle

Tables are:

  1. Defined — you create a class that implements Table and describes the schema.
  2. Injected — your handler receives the table instance via constructor DI.
  3. Used — handlers call getTableName(), getColumns(), etc. to generate queries.
  4. Created — on first use (or during migrations), the handler ensures the table exists in the database.

You don't "run" a table or call methods on it directly. It's a passive descriptor that other components consume.

Column Types and Constraints

The Column factory supports these types:

And these constraints:

Chaining these methods produces expressive column definitions:

$this->columnFactory
    ->string('email', 255)
    ->notNull()
    ->unique();

Primary Keys

Every table must define a primary key. Most tables use a single auto-increment integer:

public function getPrimaryKey(): PrimaryKey
{
    return $this->primaryKeyFactory->create('id');
}

Tables with compound primary keys (like junction tables) return an array:

public function getPrimaryKey(): array
{
    return ['user_id', 'session_token'];
}

Indexes

Indexes improve query performance by allowing the database to find rows faster. Add indexes on:

Example: adding indexes

public function getIndexes(): array
{
    return [
        $this->indexFactory->create('idx_author', ['author_id']),
        $this->indexFactory->create('idx_published', ['published_date']),
        $this->indexFactory->composite('idx_author_date', ['author_id', 'published_date']),
    ];
}

Composite indexes support queries that filter on multiple columns.

Best Practices

When defining tables:

Schema Evolution

When your schema changes (adding columns, indexes, etc.), update the table definition. The handler will detect changes and can update the database schema, though this depends on your migration strategy.

For production systems, consider:

What's Next

To understand how tables fit into the larger system, see: