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():
$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 $data — create() throws InvalidArgumentException if it's missing:
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:
// 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
composer require ramsey/uuiduse Ramsey\Uuid\Uuid;
$user = $repo->create([
'id' => Uuid::uuid4()->toString(),
'name' => 'Alice'
]);
// id: "550e8400-e29b-41d4-a716-446655440000"Using symfony/uid
composer require symfony/uiduse 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
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:
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
-- 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)
-- MySQL with binary storage
CREATE TABLE users (
id BINARY(16) PRIMARY KEY,
name VARCHAR(255)
);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
// Clear what type of entity
'/users/USR-000123'
'/orders/ORD-000456'
'/products/PROD-000789'Model Support
Ensure your model handles the ID type:
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:
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'],
);
}
}