toutaphp/ogam-data-mapper

A MyBatis-inspired SQL mapping framework for PHP. Write the SQL you want, map results to objects automatically.

Installs: 0

Dependents: 0

Suggesters: 0

Security: 0

Stars: 0

Watchers: 0

Forks: 0

Open Issues: 0

pkg:composer/toutaphp/ogam-data-mapper

v0.2.0 2026-01-21 15:57 UTC

README

CI PHP Version License: MIT

Ogam (ᚑᚌᚐᚋ) is a MyBatis-inspired SQL mapping framework for PHP. Write the SQL you want, map results to objects automatically.

Ogam is the ancient Celtic alphabet used by Druids to inscribe sacred knowledge. Like Ogam mapped symbols to meaning, this library maps SQL results to PHP objects.

Features

  • SQL-First: Write exactly the SQL you want, optimized for your use case
  • Declarative Mapping: Configure how results become objects via XML or attributes
  • Dynamic SQL: Build queries conditionally with <if>, <foreach>, <where>, <set>
  • Type Handlers: Automatic conversion between PHP and SQL types (DateTime, Enums, JSON)
  • Zero Magic: Every SQL executed is visible and predictable
  • Framework Agnostic: Works standalone or with Symfony/Laravel

Requirements

  • PHP 8.3+
  • PDO extension
  • MySQL, PostgreSQL, or SQLite

Installation

composer require toutaphp/ogam-data-mapper

Quick Start

1. Define Your Entity

<?php
namespace App\Entities;

class User
{
    public function __construct(
        public int $id,
        public string $name,
        public string $email,
        public DateTimeImmutable|null $email_verified_at,
        public string $password,
        public string|null $remember_token,
        public datetime $created_at,
        public datetime $updated_at,
    )
    {}
}

2. Data mapper configuration

<!-- config/ogam.xml -->
<?xml version="1.0" encoding="UTF-8"?>
<configuration>
    <typeAliases>
        <typeAlias alias="User" type="App\Entity\User"/>
    </typeAliases>
    <typeHandlers>
        <typeHandler phpType="App\Enum\Status" handler="App\Handlers\StatusTypeHandler"/>
    </typeHandlers>
    <environments default="development">
        <environment id="development">
            <dataSource type="POOLED">
                <property name="driver" value="mysql"/>
                <property name="host" value=""/>
                <property name="port" value=""/>
                <property name="dbname" value=""/>
                <property name="username" value=""/>
                <property name="password" value=""/>
            </dataSource>
        </environment>
    </environments>
    <mappers>
        <mapper resource="../database/mappers/UserMapper.xml"/>
    </mappers>

</configuration>

3. Create XML Mapper

<!-- mappers/UserMapper.xml -->
<?xml version="1.0" encoding="UTF-8"?>
<mapper namespace="App\Mappers\UserMapper">

    <select id="findById" resultType="App\Entity\User">
        SELECT id, name, email, email_verified_at, password, remember_token, created_at, updated_at
        FROM users
        WHERE id = #{id}
    </select>

    <select id="findByStatus" resultType="App\Entity\User">
        SELECT id, name, email, email_verified_at, password, remember_token, created_at, updated_at
        FROM users
        <where>
            <if test="status != null">
                AND status = #{status}
            </if>
        </where>
    </select>

    <insert id="insert" useGeneratedKeys="true" keyProperty="id">
        INSERT INTO users (name, email, email_verified_at, password, remember_token, created_at, updated_at)
        VALUES (#{name}, #{email}, #{emailVerifiedAt}, #{password}, #{rememberToken}, #{createdAt}, #{updatedAt})
    </insert>

</mapper>

4. Define Mapper Interface

<?php

namespace App\Mappers;

use Touta\Ogam\Attribute\Mapper;
use App\Entities\User;

#[Mapper(resource: 'database/mappers/UserMapper.xml')]
interface UserMapper
{
    public function findById(int $id): ?User;
    public function findByStatus(?Status $status): array;
    public function insert(User $user): int;
}

5. Use the Mapper

<?php

use Touta\Ogam\SessionFactoryBuilder;

// Build session factory
$factory = (new SessionFactoryBuilder())
    ->withConfiguration(base_path('config/ogam.xml'))
    ->build();

// Open session
$session = $factory->openSession();

// Get mapper
$userMapper = $session->getMapper(UserMapper::class);

// Query
$user = $userMapper->findById(1);

// Insert
$newUser = new User(0, 'john', 'john@example.com', Status::ACTIVE, new DateTimeImmutable());
$userMapper->insert($newUser);
echo $newUser->id; // Generated ID

// Commit and close
$session->commit();
$session->close();

Documentation

Parameter Syntax

Ogam uses MyBatis-style parameter syntax:

<!-- Value binding (safe, parameterized) -->
SELECT * FROM users WHERE id = #{id} AND status = #{status}

<!-- Identifier substitution (for column/table names) -->
SELECT * FROM users ORDER BY ${orderColumn} ${orderDir}

Dynamic SQL

<select id="search" resultType="User">
    SELECT * FROM users
    <where>
        <if test="name != null">
            AND name LIKE #{name}
        </if>
        <if test="email != null">
            AND email = #{email}
        </if>
        <if test="statuses != null">
            AND status IN
            <foreach collection="statuses" item="s" open="(" separator="," close=")">
                #{s}
            </foreach>
        </if>
    </where>
</select>

Why Ogam?

vs Doctrine Ogam Advantage
Hydration overhead Constructor injection, no reflection
Hidden SQL Full SQL visibility and control
Learning curve Write SQL you know, not DQL
vs Eloquent Ogam Advantage
Performance 2-3x faster (no Active Record overhead)
SQL control Write optimized SQL directly
Testability Clean separation, no database for unit tests

Part of Toutā Framework

Ogam is part of the Toutā Framework ecosystem:

  • Toutā: Core framework
  • Cosan: HTTP router
  • Fíth: Template engine
  • Nasc: Dependency injection
  • Ogam: Data mapping (this library)

Each component works standalone or together.

Contributing

See CONTRIBUTING.md for guidelines.

License

MIT License. See LICENSE for details.