Skip to content

Custom IDs

The repository supports both database-generated IDs (AUTO_INCREMENT / SERIAL / IDENTITY) and application-supplied IDs (UUID, ULID, prefixed). Behavior is controlled by the $autoIncrement flag and whether you pass the PK in $data.

Auto-Increment (Default)

$autoIncrement = true is the default. When you don't pass the PK, create() calls lastInsertId():

php
$user = $repo->create([
    'name' => 'John Doe',
    'email' => 'john@example.com'
]);

echo $user->id; // Auto-generated: 1, 2, 3, ...

If you pass the PK explicitly on an AUTO_INCREMENT table, create() uses your value and skips lastInsertId().

Manual PK (UUID, ULID, prefixed)

For tables without AUTO_INCREMENT, set protected bool $autoIncrement = false; on the repository. The PK is then required in $datacreate() throws InvalidArgumentException if it's missing:

php
final class ProductRepository extends BaseRepository
{
    protected bool $autoIncrement = false;   // ← PK must be supplied

    public function __construct(Connection $connection)
    {
        parent::__construct($connection, Product::class, 'products');
    }
}

$product = $productRepo->create([
    'id' => 'PROD-001',   // mandatory
    'name' => 'Widget',
    'price' => 99.99,
]);

// Missing PK → fail fast, before the INSERT
$productRepo->create(['name' => 'Bad']);
// → InvalidArgumentException: primary key "id" must be provided in $data when $autoIncrement = false.

Why the flag?

Without $autoIncrement = false, a misconfigured create call would silently call lastInsertId() (which returns 0 for non-AUTO_INCREMENT tables) and then find(0) — leading to confusing "row not found" errors instead of a clear "you forgot the PK" message.

Custom ID

Provide your own ID in the data array:

php
// UUID
$user = $repo->create([
    'id' => '550e8400-e29b-41d4-a716-446655440000',
    'name' => 'Jane Doe',
    'email' => 'jane@example.com'
]);

// Prefixed ID
$product = $repo->create([
    'id' => 'PROD-000001',
    'name' => 'Widget',
    'price' => 99.99
]);

// ULID
$order = $repo->create([
    'id' => '01ARZ3NDEKTSV4RRFFQ69G5FAV',
    'user_id' => 1,
    'total' => 150.00
]);

UUID Examples

Using ramsey/uuid

bash
composer require ramsey/uuid
php
use Ramsey\Uuid\Uuid;

$user = $repo->create([
    'id' => Uuid::uuid4()->toString(),
    'name' => 'Alice'
]);
// id: "550e8400-e29b-41d4-a716-446655440000"

Using symfony/uid

bash
composer require symfony/uid
php
use Symfony\Component\Uid\Uuid;
use Symfony\Component\Uid\Ulid;

// UUID v4
$user = $repo->create([
    'id' => Uuid::v4()->toRfc4122(),
    'name' => 'Bob'
]);

// ULID (time-sortable)
$order = $repo->create([
    'id' => (new Ulid())->toBase32(),
    'total' => 99.99
]);

Prefixed IDs

php
class OrderRepository extends BaseRepository
{
    private int $sequence = 0;

    public function createOrder(array $data): Order
    {
        // Get next sequence (or use database sequence)
        $lastOrder = $this->findOneBy([], ['id' => 'DESC']);
        $nextNum = $lastOrder 
            ? ((int) substr($lastOrder->id, 4)) + 1 
            : 1;

        $data['id'] = 'ORD-' . str_pad($nextNum, 6, '0', STR_PAD_LEFT);
        
        return $this->create($data);
    }
}

// Usage
$order = $orderRepo->createOrder(['user_id' => 1, 'total' => 99.99]);
echo $order->id; // "ORD-000001"

Custom Primary Key Column

If your table uses a different column name for the primary key:

php
class OrderRepository extends BaseRepository
{
    protected string $primaryKey = 'order_id';  // Instead of 'id'

    public function __construct(Connection $connection)
    {
        parent::__construct($connection, Order::class, 'orders');
    }
}

// Usage
$order = $repo->find('ORD-001');           // Uses order_id column
$repo->update('ORD-001', ['status' => 'shipped']);
$repo->delete('ORD-001');

Database Considerations

UUID Column Type

sql
-- MySQL
CREATE TABLE users (
    id CHAR(36) PRIMARY KEY,
    name VARCHAR(255)
);

-- PostgreSQL (native UUID type)
CREATE TABLE users (
    id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
    name VARCHAR(255)
);

-- With index for performance
CREATE INDEX idx_users_created ON users(created_at);

Binary UUID (More Efficient)

sql
-- MySQL with binary storage
CREATE TABLE users (
    id BINARY(16) PRIMARY KEY,
    name VARCHAR(255)
);
php
use Ramsey\Uuid\Uuid;

// Store as binary
$user = $repo->create([
    'id' => Uuid::uuid4()->getBytes(),
    'name' => 'Alice'
]);

Best Practices

When to Use UUIDs

Good for:

  • Distributed systems
  • Preventing ID enumeration
  • Merging data from multiple sources
  • Public-facing IDs (URLs)

Consider alternatives when:

  • High insert volume (auto-increment is faster)
  • Need sequential ordering
  • Storage space is critical

When to Use Prefixed IDs

Good for:

  • Human-readable identifiers
  • Different entity types in URLs
  • Customer-facing order numbers
  • Import/export scenarios
php
// Clear what type of entity
'/users/USR-000123'
'/orders/ORD-000456'
'/products/PROD-000789'

Model Support

Ensure your model handles the ID type:

php
class User
{
    public function __construct(
        public readonly string $id,  // String for UUID/custom
        public readonly string $name,
    ) {}

    public static function fromArray(array $data): self
    {
        return new self(
            id: (string) $data['id'],  // Cast to string
            name: $data['name'],
        );
    }
}

For integer IDs:

php
class User
{
    public function __construct(
        public readonly int $id,
        public readonly string $name,
    ) {}

    public static function fromArray(array $data): self
    {
        return new self(
            id: (int) $data['id'],
            name: $data['name'],
        );
    }
}

Released under the MIT License.