Puremapper
Lightweight data mapper with Query Builder integration for PHP 8.1+
Install / Use
/learn @puremapper/PuremapperREADME
PureMapper
PureMapper is a lightweight Data Mapper and Unit of Work library for pure PHP entities.
It is designed for developers who want:
- Pure PHP domain models (no annotations, no attributes)
- No Active Record, no magic methods
- Clear separation between domain and infrastructure
- A small, understandable alternative to heavy ORMs
- Zero external dependencies - only PHP core + PDO
Doctrine ideas, without Doctrine weight.
Table of Contents
- Quick Start
- Philosophy
- Requirements
- Installation
- Database Connection
- Defining Entities
- Mapping with Fluent DSL
- Type Conversion
- Relations
- Query Builder
- SQL Builder
- Unit of Work
- Metadata Caching
- Identity Map
- Repository Interface
- Advanced Usage
- Why PureMapper?
- When NOT to Use PureMapper
- Upgrading
- Roadmap
- License
Quick Start
// 1. Set up database connection (pure PDO)
use PureMapper\Query\Connection;
use PureMapper\Query\DatabaseDriver;
$pdo = new PDO('mysql:host=localhost;dbname=myapp', 'root', '');
$connection = new Connection($pdo, DatabaseDriver::MySQL);
// 2. Define a pure entity
final class User
{
public ?int $id = null;
public string $name;
public string $email;
public DateTimeImmutable $createdAt;
}
// 3. Set up PureMapper
use PureMapper\EntityManager;
use PureMapper\Mapping\EntityMapper;
use PureMapper\Mapping\MetadataRegistry;
use PureMapper\Type\TypeRegistry;
$typeRegistry = new TypeRegistry();
$registry = new MetadataRegistry();
$registry->register(
(new EntityMapper(User::class))
->table('users')
->id('id')
->field('name', 'string')
->field('email', 'string')
->field('createdAt', 'datetime', column: 'created_at')
->build()
);
$em = new EntityManager($connection, $registry, $typeRegistry);
// 4. Query entities with relations
$user = $em->query(User::class)
->with('posts')
->find(1);
// Or use repositories for domain-specific queries
class UserRepository implements RepositoryInterface
{
public function __construct(
private EntityManager $em,
) {}
public function findActiveWithPosts(): array
{
return $this->em->query(User::class)
->with('posts')
->where('status', '=', 'active')
->get();
}
}
// 5. Persist entities
$user = new User();
$user->name = 'John';
$user->email = 'john@example.com';
$user->createdAt = new DateTimeImmutable();
$em->persist($user);
$em->commit(); // INSERT executed, $user->id populated
Philosophy
PureMapper follows the Data Mapper pattern:
- Entities are plain PHP objects with no persistence awareness
- Mapping is defined externally using a fluent DSL
- Persistence logic lives outside your domain
- Infrastructure can be replaced without touching entities
Domain (Pure PHP Entities)
|
RepositoryInterface
|
EntityManager
|
EntityQuery + UnitOfWork + Hydrator
|
SqlBuilder + Connection (PDO)
|
Database
Requirements
- PHP 8.1+
- PDO extension (
ext-pdo)
PureMapper has zero external dependencies. Only PHP core and PDO are required.
Installation
composer require puremapper/puremapper
Database Connection
PureMapper uses PDO directly with a thin wrapper for database abstraction.
Supported Databases
| Database | Driver Enum | Identifier Quote |
|------------|--------------------------|------------------|
| MySQL | DatabaseDriver::MySQL | Backtick () | | PostgreSQL | DatabaseDriver::PostgreSQL| Double quote (") | | SQLite |DatabaseDriver::SQLite` | Double quote (") |
Connection Setup
use PDO;
use PureMapper\Query\Connection;
use PureMapper\Query\DatabaseDriver;
// MySQL
$pdo = new PDO('mysql:host=localhost;dbname=myapp', 'user', 'password', [
PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION,
]);
$connection = new Connection($pdo, DatabaseDriver::MySQL);
// PostgreSQL
$pdo = new PDO('pgsql:host=localhost;dbname=myapp', 'user', 'password');
$connection = new Connection($pdo, DatabaseDriver::PostgreSQL);
// SQLite
$pdo = new PDO('sqlite:/path/to/database.db');
$connection = new Connection($pdo, DatabaseDriver::SQLite);
// SQLite in-memory (for testing)
$pdo = new PDO('sqlite::memory:');
$connection = new Connection($pdo, DatabaseDriver::SQLite);
Connection Methods
| Method | Returns | Description |
|--------|---------|-------------|
| select(CompiledQuery) | array | Execute SELECT, return rows as assoc arrays |
| execute(CompiledQuery) | int | Execute INSERT/UPDATE/DELETE, return affected rows |
| insert(CompiledQuery) | string | Execute INSERT, return last insert ID |
| table(string) | SqlBuilder | Create query builder for table |
| beginTransaction() | void | Start transaction |
| commit() | void | Commit transaction |
| rollBack() | void | Rollback transaction |
| getPdo() | PDO | Get underlying PDO instance |
| statement(string) | bool | Execute raw SQL (DDL) |
Defining Entities
Entities are pure PHP classes with no persistence logic, no annotations, and no base class.
final class User
{
public ?int $id = null;
public string $name;
public string $email;
/** @var Post[] */
public array $posts = [];
public ?Profile $profile = null;
}
final class Post
{
public ?int $id = null;
public string $title;
public string $content;
public DateTimeImmutable $publishedAt;
}
Hydration assigns values directly to public properties. No setters required.
Mapping with Fluent DSL
Mappings are defined externally using a fluent builder API.
use PureMapper\Mapping\EntityMapper;
$mapper = (new EntityMapper(User::class))
->table('users')
->id('id') // Single primary key
->field('name', 'string')
->field('email', 'string')
->field('createdAt', 'datetime', column: 'created_at')
->hasMany('posts', Post::class, foreignKey: 'user_id')
->hasOne('profile', Profile::class, foreignKey: 'user_id');
$metadata = $mapper->build();
Composite Primary Keys
$mapper = (new EntityMapper(TenantUser::class))
->table('tenant_users')
->id(['tenant_id', 'user_id']) // Composite key
->field('role', 'string');
Column Name Mapping
->field('createdAt', 'datetime', column: 'created_at')
->field('isActive', 'bool', column: 'is_active')
Type Conversion
PureMapper includes built-in type converters and supports custom converters.
Built-in Types
| Type | PHP Type | Database Type |
|------------|---------------------------|------------------|
| string | string | VARCHAR/TEXT |
| int | int | INTEGER |
| float | float | DECIMAL/FLOAT |
| bool | bool | BOOLEAN/TINYINT |
| datetime | DateTimeImmutable | DATETIME |
| date | DateTimeImmutable | DATE |
| json | array | JSON/TEXT |
| enum | BackedEnum | VARCHAR/INTEGER |
Custom Type Converters
use PureMapper\Type\TypeConverter;
final class MoneyConverter implements TypeConverter
{
public function toPHP(mixed $value): Money
{
return Money::fromCents((int) $value);
}
public function toDatabase(mixed $value): int
{
return $value->cents();
}
}
// Register custom type
$typeRegistry->register('money', new MoneyConverter());
// Use in mapping
->field('price', 'money')
Relations
Supported Relation Types
| Relation | Example |
|--------------|-----------------------------------------------------------|
| hasOne | ->hasOne('profile', Profile::class, 'user_id') |
| hasMany | ->hasMany('posts', Post::class, 'user_id') |
| belongsTo | ->belongsTo('author', User::class, 'author_id') |
| manyToMany | ->manyToMany('tags', Tag::class, 'post_tags', 'post_id', 'tag_id') |
Loading Strategy
Relations use eager loading only - no lazy loading or N+1 surprises. Use the Query Builder's with() method to load relations.
Query Builder
PureMapper provides a fluent Query Builder for querying entities with eager-loaded relations.
Basic Queries
// Get all users
$users = $em->query(User::class)->get();
// Find by primary key
$user = $em->query(User::class)->find(1);
// Find with conditions
$users = $em->query(User::class)
->where('status', '=', 'active')
->where('created_at', '>', '2024-01-01')
->orderBy('name', 'asc')
->limit(10)
->get();
// Get first matching result
$user = $em->query(User::class)
->where('email', '=', 'john@example.com')
->first();
Eager Loading Relations
Use the with() method to eager load relations in a single query batch:
// Load user with posts
$user = $em->query(User::class)
->with('posts')
->find(1);
// Load multiple relations
$users = $em->query(User::class)
->with('posts', 'profile', 'roles')
->where('status', '=', 'active')
->get();
How Eager Loading Works
Relations are loaded using separate queries (not JOINs) to avoid cartesian product issues:
$users = $em->query(User::class)
->with('posts')
Related Skills
node-connect
353.1kDiagnose OpenClaw node connection and pairing failures for Android, iOS, and macOS companion apps
frontend-design
111.6kCreate distinctive, production-grade frontend interfaces with high design quality. Use this skill when the user asks to build web components, pages, or applications. Generates creative, polished code that avoids generic AI aesthetics.
openai-whisper-api
353.1kTranscribe audio via OpenAI Audio Transcriptions API (Whisper).
qqbot-media
353.1kQQBot 富媒体收发能力。使用 <qqmedia> 标签,系统根据文件扩展名自动识别类型(图片/语音/视频/文件)。
