承接 likeuntomurphy/serverless-ogm 相关项目开发

从需求分析到上线部署,全程专人跟进,保证项目质量与交付效率

邮箱:yvsm@zunyunkeji.com | QQ:316430983 | 微信:yvsm316

likeuntomurphy/serverless-ogm

Composer 安装命令:

composer require likeuntomurphy/serverless-ogm

包简介

PHP DynamoDB Object Graph Mapper

README 文档

README

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

  • 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 > 100 items. DynamoDB limit; the OGM can't lift it.

Installation

composer require likeuntomurphy/serverless-ogm

For Symfony integration, see serverless-ogm-bundle.

Requirements: PHP >= 8.5, aws/aws-sdk-php ^3.0, psr/event-dispatcher ^1.0.

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

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

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

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

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

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

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

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

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

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

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

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

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

The DocumentManager accepts an optional FlushStrategyInterface.

Strategy Behavior
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

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

Event When
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 flush

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

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 provides a Symfony profiler integration that uses this interface.

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

MIT

likeuntomurphy/serverless-ogm 适用场景与选型建议

likeuntomurphy/serverless-ogm 是一款 基于 PHP 开发的 Composer 扩展包,目前已累计 14 次下载、GitHub Stars 达 0, 最近一次更新时间为 2026 年 04 月 06 日, 在 PHP 生态内属于活跃度较高的组件。

我们在过去多个企业项目中使用过 likeuntomurphy/serverless-ogm 或与其功能相近的方案,如果你在选型或落地过程中遇到问题,例如 版本兼容、二次改造、私有化封装、与内部系统对接、生产 BUG 排查,欢迎联系我们协助评估。

围绕 likeuntomurphy/serverless-ogm 我们能提供哪些服务?
定制开发 / 二次开发

基于 likeuntomurphy/serverless-ogm 在你已有业务上做功能扩展、字段裁剪、UI 适配、与内部账号 / 权限 / 日志系统的深度对接。

BUG 修复 & 性能优化

线上偶发问题、内存泄漏、慢查询、并发异常等排查修复;针对高流量场景做缓存、队列、索引层面的调优。

项目外包 & 长期维护

承接完整的项目从需求 → 设计 → 开发 → 上线 → 长期运维;也可按月提供技术保姆服务。

yvsm@zunyunkeji.com QQ:316430983 微信:yvsm316 西安尊云信息科技 · 专注 PHP / Go / 分布式系统研发

统计信息

  • 总下载量: 14
  • 月度下载量: 0
  • 日度下载量: 0
  • 收藏数: 0
  • 点击次数: 32
  • 依赖项目数: 1
  • 推荐数: 0

GitHub 信息

  • Stars: 0
  • Watchers: 0
  • Forks: 0
  • 开发语言: PHP

其他信息

  • 授权协议: MIT
  • 更新时间: 2026-04-06