SkillAgentSearch skills...

Puremapper

Lightweight data mapper with Query Builder integration for PHP 8.1+

Install / Use

/learn @puremapper/Puremapper
About this skill

Quality Score

0/100

Supported Platforms

Universal

README

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

// 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

View on GitHub
GitHub Stars10
CategoryDevelopment
Updated3mo ago
Forks1

Languages

PHP

Security Score

87/100

Audited on Jan 6, 2026

No findings