メインコンテンツまでスキップ

EntityWorkspace

EntityWorkspace is Weaver ORM's central unit-of-work service. It tracks entities across a single request, batches all writes, and flushes them to the database in a single coordinated operation. Think of it as the improved successor to Doctrine's EntityManager, redesigned for clarity, worker safety, and explicit intent.

Key differences from Doctrine EntityManager

Doctrine EntityManagerWeaver EntityWorkspace
persist($entity)add($entity)
flush()push()
remove($entity)delete($entity)
refresh($entity)reload($entity)
detach($entity)untrack($entity)
contains($entity)isTracked($entity)
clear()reset()

One EntityWorkspace instance exists per HTTP request. It holds no shared state between requests and implements ResetInterface so long-running workers (RoadRunner, FrankenPHP) can call reset() between requests instead of rebuilding the container.


Tracking entities

add($entity) — schedule an entity for insertion

Marks a new entity for INSERT on the next push(). No SQL is executed immediately.

<?php

use App\Entity\User;
use Weaver\ORM\EntityWorkspace;

final class RegisterUserHandler
{
public function __construct(
private readonly EntityWorkspace $workspace,
) {}

public function handle(RegisterUserCommand $command): void
{
$user = new User(
email: $command->email,
name: $command->name,
);

$this->workspace->add($user);
$this->workspace->push(); // INSERT executed here
}
}

Calling add() on an already-tracked entity is a no-op — it is safe to call multiple times.

push() — flush all pending changes

Executes all queued INSERTs, UPDATEs, and DELETEs in a single database round-trip (wrapped in an implicit transaction). After push() completes, every tracked entity is in the MANAGED state with an up-to-date identity.

<?php

$order = new Order(customerId: $customerId, total: $total);
$this->workspace->add($order);

foreach ($lineItems as $item) {
$this->workspace->add($item);
}

$this->workspace->push(); // all INSERTs committed atomically

Insert ordering is resolved automatically via topological sort on foreign-key relationships — child entities are always inserted after their parents, regardless of the order in which they were add()-ed.

delete($entity) — schedule an entity for deletion

Marks a tracked entity for DELETE on the next push(). The entity stays in memory until push() is called.

<?php

$post = $this->postRepository->findOrFail($id);

$this->workspace->delete($post);
$this->workspace->push(); // DELETE executed here

If the entity's mapper declares cascade: ['delete'] on a relationship, associated children are automatically scheduled for deletion as well.


Refreshing and detaching

reload($entity) — refresh from the database

Discards any in-memory changes and re-populates the entity from its current row in the database. Useful after an optimistic lock conflict or when another process may have updated the record.

<?php

try {
$this->workspace->push();
} catch (OptimisticLockException $e) {
$this->workspace->reload($product); // reload the latest version
// re-apply changes and retry
}

untrack($entity) — detach from the workspace

Removes the entity from the identity map and change-tracking. After calling untrack(), modifications to the entity are invisible to the workspace and will not be flushed.

<?php

// Detach a read-only projection to avoid accidental writes
$report = $this->reportRepository->buildSummary();
$this->workspace->untrack($report);

Inspecting entity state

isTracked($entity) — check if the workspace knows about this entity

Returns true if the entity is currently in the identity map (NEW, MANAGED, or REMOVED state).

<?php

if (!$this->workspace->isTracked($entity)) {
$this->workspace->add($entity);
}

isNew($entity) — check if the entity has never been persisted

Returns true when the entity was add()-ed but push() has not yet been called.

<?php

if ($this->workspace->isNew($user)) {
$this->mailer->sendWelcomeEmail($user);
}

isDirty($entity) — check if the entity has unsaved changes

Returns true when one or more properties have changed since the entity was last loaded or flushed.

<?php

if ($this->workspace->isDirty($product)) {
$this->auditLogger->log('product.modified', $product->getId());
}

$this->workspace->push();

isDeleted($entity) — check if the entity is scheduled for deletion

Returns true after delete() has been called but before push() executes the DELETE.

<?php

if ($this->workspace->isDeleted($entity)) {
throw new \LogicException('Entity is already scheduled for deletion.');
}

getChanges($entity) — inspect dirty fields

Returns an associative array of [fieldName => [original, current]] pairs for every property that has changed since the last snapshot. Returns an empty array if the entity is clean.

<?php

$changes = $this->workspace->getChanges($user);
// [
// 'email' => ['old@example.com', 'new@example.com'],
// 'name' => ['Alice', 'Alice Smith'],
// ]

foreach ($changes as $field => [$old, $new]) {
$this->auditLog->record($user, $field, $old, $new);
}

Resetting workspace state

reset() — clear all tracked entities

Evicts every entity from the identity map and clears all pending queues. Called automatically by the worker kernel between requests when using RoadRunner or FrankenPHP.

<?php

// Manually clear state after a batch import
foreach ($chunks as $chunk) {
foreach ($chunk as $row) {
$entity = $this->hydrate($row);
$this->workspace->add($entity);
}
$this->workspace->push();
$this->workspace->reset(); // free memory before the next chunk
}

Transaction support

EntityWorkspace integrates with TransactionManager to wrap push() calls in explicit transactions. See the Transactions page for full details.

<?php

use Weaver\ORM\Transaction\TransactionManager;

final class TransferFundsHandler
{
public function __construct(
private readonly EntityWorkspace $workspace,
private readonly TransactionManager $txManager,
) {}

public function handle(TransferCommand $cmd): void
{
$this->txManager->run(function () use ($cmd): void {
$source = $this->accountRepo->findOrFail($cmd->sourceId);
$destination = $this->accountRepo->findOrFail($cmd->destinationId);

$source->debit($cmd->amount);
$destination->credit($cmd->amount);

$this->workspace->push(); // both UPDATEs within the same transaction
});
}
}

push() reuses any active transaction. If no transaction is open, it opens one implicitly and commits after all SQL is written.


Worker safety

EntityWorkspace implements ResetInterface. In long-running PHP workers you must call reset() (or let the kernel do so) at the start of each request to prevent state leaking between requests.

<?php

// In a RoadRunner worker loop:
while ($request = $worker->waitRequest()) {
$this->workspace->reset(); // clean slate for each request
$this->kernel->handle($request);
}