PHPackages                             likeuntomurphy/serverless-ogm - PHPackages - PHPackages  [Skip to content](#main-content)[PHPackages](/)[Directory](/)[Categories](/categories)[Trending](/trending)[Leaderboard](/leaderboard)[Changelog](/changelog)[Analyze](/analyze)[Collections](/collections)[Log in](/login)[Sign up](/register)

1. [Directory](/)
2. /
3. [Database &amp; ORM](/categories/database)
4. /
5. likeuntomurphy/serverless-ogm

ActiveLibrary[Database &amp; ORM](/categories/database)

likeuntomurphy/serverless-ogm
=============================

PHP DynamoDB Object Graph Mapper

v0.1.0-alpha2(3mo ago)014↓93.8%1MITPHPPHP &gt;=8.5CI passing

Since Apr 8Pushed 2mo agoCompare

[ Source](https://github.com/likeuntomurphy/serverless-ogm)[ Packagist](https://packagist.org/packages/likeuntomurphy/serverless-ogm)[ RSS](/packages/likeuntomurphy-serverless-ogm/feed)WikiDiscussions main Synced today

READMEChangelogDependencies (5)Versions (2)Used By (1)

Serverless OGM
==============

[](#serverless-ogm)

An **Object-Graph Mapper** for DynamoDB. Maps PHP objects to items, tracks identity, hydrates references lazily, and flushes changes with batched writes or transactions.

The "G" is deliberate. Your domain is a graph of entities and relationships; storage choice determines only how that graph is serialized — normalized across tables with JOINs, nested in documents, or materialized as adjacency-list rows. As Rick Houlihan puts it: **NoSQL is not non-relational — it's non-normalized.** This package treats the graph as first-class: relationships are modeled as references between entities and stored as adjacency rows or inline ID lists, never collapsed into nested documents or overloaded sort keys.

**Scope.** This OGM owns object lifecycle — identity map, dirty tracking, lazy references, adjacency-backed collections, flush. For most apps, the built-in primitives cover the read path end-to-end: `find()` by `Identity`, `batchFind()`, and traversal through `#[Reference]` / `#[ReferenceMany]`. When an access pattern needs a custom `Query`, `Scan`, or GSI lookup, write it against the AWS SDK and hand the raw items to `$dm->attach()` — the OGM takes over from there. No query builder, no repository pattern, no `findBy`.

**Pitched against raw SDK + prayer, not against Doctrine.** If you're writing DynamoDB code with no identity map, no dirty tracking, and no structure for relationships, this is for you. If you're looking for a NoSQL Doctrine with a repository pattern and a query builder, this isn't.

When not to use this
--------------------

[](#when-not-to-use-this)

- **Single-table design.** Overloaded sort-key prefixes and filter-by-type rules belong in your app, not a framework. If that's your access-pattern model, stay with the raw SDK and build helpers as needed.
- **Heavy ad-hoc querying with no identifiable access patterns.** If most reads are unpredictable filter/sort combinations you can't model up front, you want a different storage engine (Postgres, OpenSearch) — DynamoDB itself will fight you, not just this OGM.
- **Search.** Full-text, geo, complex filter composition — use a secondary store (OpenSearch, Algolia) and let DynamoDB do what it's best at.
- **Cross-partition transactions &gt; 100 items.** DynamoDB limit; the OGM can't lift it.

Installation
------------

[](#installation)

```
composer require likeuntomurphy/serverless-ogm
```

For Symfony integration, see [serverless-ogm-bundle](https://github.com/likeuntomurphy/serverless-ogm-bundle).

Requirements: PHP &gt;= 8.5, `aws/aws-sdk-php` ^3.0, `psr/event-dispatcher` ^1.0.

Defining documents
------------------

[](#defining-documents)

```
use Likeuntomurphy\Serverless\OGM\Mapping\Document;
use Likeuntomurphy\Serverless\OGM\Mapping\Field;
use Likeuntomurphy\Serverless\OGM\Mapping\PartitionKey;

#[Document(table: 'users', pk: 'PK')]
class User
{
    #[PartitionKey]
    public string $email;

    #[Field]
    public string $name;

    #[Field]
    public ?string $role = null;
}
```

### Mapping attributes

[](#mapping-attributes)

AttributeTargetDescription`#[Document]`ClassMarks a class as a mapped document. Requires `table`; optional `pk`/`sk` attribute names.`#[PartitionKey]`PropertyThe partition key. Optional `name` overrides the DynamoDB attribute name.`#[SortKey]`PropertyThe sort key for composite primary keys.`#[Field]`PropertyA persisted field. Optional `name` overrides the attribute name.`#[Reference]`PropertyA lazy reference to another document, stored as its partition key value.`#[ReferenceMany]`PropertyA collection of references. Inline by default; opt into an adjacency table with `adjacencyTable:`.`#[EmbedOne]`PropertyAn embedded sub-document, stored as a DynamoDB map.`#[EmbedMany]`PropertyA list of embedded sub-documents, stored as a DynamoDB list of maps.`#[Embedded]`ClassMarks a class as an embeddable (no table of its own).`#[Id]`PropertyLegacy single-key identifier (prefer `#[PartitionKey]`).Using the DocumentManager
-------------------------

[](#using-the-documentmanager)

```
use Aws\DynamoDb\DynamoDbClient;
use Likeuntomurphy\Serverless\OGM\DocumentManager;

$client = new DynamoDbClient([
    'region' => 'us-east-1',
    'version' => 'latest',
]);

$dm = new DocumentManager($client);
```

### Persist and flush

[](#persist-and-flush)

```
$user = new User();
$user->email = 'alice@example.com';
$user->name = 'Alice';

$dm->persist($user);
$dm->flush();
```

### Find

[](#find)

Every document is identified by an `Identity` value object carrying its partition key and, for composite-keyed documents, its sort key.

```
use Likeuntomurphy\Serverless\OGM\Identity;

$user = $dm->find(User::class, new Identity('alice@example.com'));

// Composite key
$order = $dm->find(Order::class, new Identity('user-123', 'order-456'));
```

`Identity::of($pk, ?$sk)` is an equivalent shorthand.

### Batch find

[](#batch-find)

Fetches multiple documents in a single `BatchGetItem` request. Identity-map hits skip the network.

```
$users = $dm->batchFind(User::class, [
    new Identity('alice@example.com'),
    new Identity('bob@example.com'),
]);
```

### Update

[](#update)

Change properties on a managed document and flush. Updates become `UpdateItem` with `SET`/`REMOVE` expressions — only changed fields are written.

```
$user = $dm->find(User::class, new Identity('alice@example.com'));
$user->name = 'Alice Smith';
$dm->flush();
```

### Remove

[](#remove)

```
$user = $dm->find(User::class, new Identity('alice@example.com'));
$dm->remove($user);
$dm->flush();
```

### Attach (bring-your-own query)

[](#attach-bring-your-own-query)

When you run a `Query`, `Scan`, or `Index` query yourself, hand the raw unmarshaled item to `attach()` to register it with the identity map and unit of work:

```
$result = $client->query([
    'TableName' => 'users',
    'IndexName' => 'role-index',
    'KeyConditionExpression' => '#role = :role',
    'ExpressionAttributeNames' => ['#role' => 'role'],
    'ExpressionAttributeValues' => [':role' => ['S' => 'admin']],
]);

$marshaler = new Aws\DynamoDb\Marshaler();
$admins = $dm->attachAll(
    User::class,
    array_map(fn (array $item): array => $marshaler->unmarshalItem($item), $result['Items']),
);
```

`attach()` handles one item; `attachAll()` handles a list. Duplicate identities dedupe via the identity map.

References
----------

[](#references)

A `#[Reference]` stores another document's partition key and loads it lazily via a PHP 8+ lazy ghost. The referenced document is not fetched until a non-identity property is accessed.

```
#[Document(table: 'orders', pk: 'PK')]
class Order
{
    #[PartitionKey]
    public string $id;

    #[Reference(targetDocument: User::class)]
    public User $customer;
}
```

### ReferenceMany: two modes

[](#referencemany-two-modes)

`#[ReferenceMany]` has two storage modes. The target property must be typed `Collection`.

**Inline (default).** IDs stored as a list attribute on the parent item. Hydrates to an `ArrayCollection` of lazy ghosts. Good for small-to-medium relationships that comfortably fit inside the parent item.

```
use Likeuntomurphy\Serverless\OGM\ArrayCollection;
use Likeuntomurphy\Serverless\OGM\Collection;

#[Document(table: 'playlists', pk: 'PK')]
class Playlist
{
    #[PartitionKey]
    public string $id;

    #[ReferenceMany(targetDocument: Song::class)]
    public Collection $songs;

    public function __construct()
    {
        $this->songs = new ArrayCollection();
    }
}
```

**Adjacency table (opt-in).** A dedicated `{parentId, childId}` table. Hydrates to a `PersistentCollection` with lazy count, ids-only enumeration, and paginated `slice()`. Good for large relationships, relationships that need independent queryability, or anywhere the ID list would push the parent item toward DynamoDB's 400 KB item limit.

```
#[ReferenceMany(
    targetDocument: Song::class,
    adjacencyTable: 'playlist_songs',
    adjacencyPk: 'playlistId',
    adjacencySk: 'songId',
)]
public Collection $songs;
```

Switching between the modes is a pure mapping change — both return `Collection`, so no entity source edits are needed.

Mutations on a `PersistentCollection` (`$collection->add($song)`, `$collection->remove($song)`) are tracked and flushed as adjacency-table `PutItem`/`DeleteItem` calls on the next `$dm->flush()`.

Embeds
------

[](#embeds)

Embedded documents are stored inline as DynamoDB maps. They have no table or identity of their own.

```
use Likeuntomurphy\Serverless\OGM\Mapping\Embedded;
use Likeuntomurphy\Serverless\OGM\Mapping\Field;

#[Embedded]
class Address
{
    #[Field]
    public string $street;

    #[Field]
    public string $city;
}

#[Document(table: 'contacts', pk: 'PK')]
class Contact
{
    #[PartitionKey]
    public string $id;

    #[EmbedOne(targetDocument: Address::class)]
    public Address $address;
}
```

Type coercion
-------------

[](#type-coercion)

The hydrator coerces DynamoDB values to match PHP property types:

- `int`, `float`, `bool` from DynamoDB numbers/strings
- `DateTime`, `DateTimeImmutable` from ISO 8601 strings
- Backed enums from their stored values
- Nested embeds recursively

Flush strategies
----------------

[](#flush-strategies)

The DocumentManager accepts an optional `FlushStrategyInterface`.

StrategyBehavior`BatchWriteStrategy` (default)Inserts and deletes via `BatchWriteItem` (up to 25 per request with retry). Updates via individual `UpdateItem` with expressions.`TransactWriteStrategy`All-or-nothing via `TransactWriteItems` (up to 100 items, 2x WCU cost).`SingleOperationStrategy`One `PutItem`/`UpdateItem`/`DeleteItem` per entity. No batching.```
use Likeuntomurphy\Serverless\OGM\FlushStrategy\TransactWriteStrategy;

$dm = new DocumentManager(
    $client,
    flushStrategy: new TransactWriteStrategy($client, $marshaler),
);
```

Events
------

[](#events)

The DocumentManager dispatches events via any PSR-14 event dispatcher:

EventWhen`PrePersistEvent`Before a new document is written`PostPersistEvent`After a new document is written`PreUpdateEvent`Before a managed document is updated (includes field-level changeset)`PostUpdateEvent`After a managed document is updated`PreRemoveEvent`Before a document is deleted`PostRemoveEvent`After a document is deleted`PostFlushEvent`After all writes and deletes in a flushIdentity map
------------

[](#identity-map)

Every `find()` / `batchFind()` / `attach()` checks the identity map first. The same entity is never hydrated twice within a single DocumentManager lifecycle. `$dm->clear()` resets the map (and the relation preload cache).

Profiling
---------

[](#profiling)

Implement `ProfilingLogger` to receive identity-map hit/miss and hydration counts:

```
use Likeuntomurphy\Serverless\OGM\ProfilingLogger;

class MyLogger implements ProfilingLogger
{
    public function recordIdentityMapHit(): void { /* ... */ }
    public function recordIdentityMapMiss(): void { /* ... */ }
    public function recordHydration(): void { /* ... */ }
}

$dm->setProfilingLogger(new MyLogger());
```

The [serverless-ogm-bundle](https://github.com/likeuntomurphy/serverless-ogm-bundle) provides a Symfony profiler integration that uses this interface.

Limitations
-----------

[](#limitations)

- **References to composite-keyed targets.** A `#[Reference]` or `#[ReferenceMany]` whose target has both a `#[PartitionKey]` and a `#[SortKey]` currently drops the sort key — the reference stores the pk only. Loading the referenced ghost throws at init. Planned.
- **No query builder, no repository pattern.** By design. Use app-owned queries + `attach()`.
- **No optimistic locking / conditional writes yet.** Planned.

License
-------

[](#license)

MIT

###  Health Score

35

—

LowBetter than 77% of packages

Maintenance84

Actively maintained with recent releases

Popularity6

Limited adoption so far

Community8

Small or concentrated contributor base

Maturity37

Early-stage or recently created project

 Bus Factor1

Top contributor holds 100% of commits — single point of failure

How is this calculated?**Maintenance (25%)** — Last commit recency, latest release date, and issue-to-star ratio. Uses a 2-year decay window.

**Popularity (30%)** — Total and monthly downloads, GitHub stars, and forks. Logarithmic scaling prevents top-heavy scores.

**Community (15%)** — Contributors, dependents, forks, watchers, and maintainers. Measures real ecosystem engagement.

**Maturity (30%)** — Project age, version count, PHP version support, and release stability.

###  Release Activity

Cadence

Unknown

Total

1

Last Release

90d ago

### Community

Maintainers

![](https://www.gravatar.com/avatar/bd90e82304f5690e9659448faac4dd0755f8b08600cda4a7691caacea1227831?d=identicon)[jc@jonathancox.dev](/maintainers/jc@jonathancox.dev)

---

Top Contributors

[![likeuntomurphy](https://avatars.githubusercontent.com/u/2012253?v=4)](https://github.com/likeuntomurphy "likeuntomurphy (12 commits)")

###  Code Quality

TestsPHPUnit

Static AnalysisPHPStan

Code StylePHP CS Fixer

Type Coverage Yes

### Embed Badge

![Health badge](/badges/likeuntomurphy-serverless-ogm/health.svg)

```
[![Health](https://phpackages.com/badges/likeuntomurphy-serverless-ogm/health.svg)](https://phpackages.com/packages/likeuntomurphy-serverless-ogm)
```

###  Alternatives

[symfony/symfony

The Symfony PHP framework

31.4k87.2M2.2k](/packages/symfony-symfony)[symfony/mailer

Helps sending emails

1.6k409.1M1.5k](/packages/symfony-mailer)[leantime/leantime

Open source project management system for non-project managers. Simple like Trello, powerful like Jira. Built with neurodiversity in mind.

10.2k3.7k](/packages/leantime-leantime)[drupal/core-recommended

Locked core dependencies; require this project INSTEAD OF drupal/core.

6942.5M421](/packages/drupal-core-recommended)[web-auth/webauthn-lib

FIDO2/Webauthn Support For PHP

12310.5M135](/packages/web-auth-webauthn-lib)[patchlevel/event-sourcing

A lightweight but also all-inclusive event sourcing library with a focus on developer experience

207362.9k13](/packages/patchlevel-event-sourcing)

PHPackages © 2026

[Directory](/)[Categories](/categories)[Trending](/trending)[Changelog](/changelog)[Analyze](/analyze)
