RowSink
BetterData\Sink\RowSink writes a DataObject to a custom database table via $wpdb.
Methods
RowSink::toArray(DataObject $dto, ?array $only = null): array
RowSink::insert(\wpdb $wpdb, string $table, DataObject $dto, ?array $only = null, ?array $formats = null): int
RowSink::update(\wpdb $wpdb, string $table, DataObject $dto, array $where, ?array $only = null, ?array $formats = null, ?array $whereFormats = null): int
insert() returns the affected row count (typically 1 on success). update() returns the affected row count too.
Both throw RuntimeException if $wpdb->last_error is non-empty after the call.
Projection: toArray()
Returns a column-keyed array ready for $wpdb->insert/update:
- Applies
#[Column]aliases (property → column) - Applies
#[DateFormat]overrides; default for row columns is MySQL'Y-m-d H:i:s' - Unwraps
BackedEnumto.value - Converts
DateTimeInterfaceto UTC before formatting - Recurses into arrays of
DataObject(callstoArray()on each)
final readonly class OrderRowDto extends DataObject {
use HasWpSinks;
public function __construct(
#[Column('id')] public int $id = 0,
#[Column('order_id')] public int $orderId = 0,
#[Column('product_id')] public int $productId = 0,
#[Column('quantity')] public int $quantity = 0,
public string $status = 'pending',
public ?\DateTimeImmutable $createdAt = null,
) {}
}
$dto = OrderRowDto::fromArray([
'orderId' => 1,
'productId' => 42,
'quantity' => 3,
'createdAt' => new \DateTimeImmutable('now'),
]);
RowSink::toArray($dto);
/*
[
'id' => 0,
'order_id' => 1,
'product_id' => 42,
'quantity' => 3,
'status' => 'pending',
'created_at' => '2024-03-15 09:30:00', // ← UTC-converted MySQL format
]
*/
Insert and update
global $wpdb;
$table = "{$wpdb->prefix}order_items";
// Insert
$dto = OrderRowDto::fromArray([...]);
RowSink::insert($wpdb, $table, $dto);
// Or via the trait shortcut:
$dto->saveAsRow($wpdb, $table);
// Update
$existing = OrderRowDto::fromArray([...]);
RowSink::update(
$wpdb,
$table,
$existing->with(['status' => 'shipped']),
where: ['id' => $existing->id],
only: ['status'],
);
// Or via trait — when the DTO has a positive id, saveAsRow updates
$existing->with(['status' => 'shipped'])
->saveAsRow($wpdb, $table, where: ['id' => $existing->id], only: ['status']);
Format specifiers
$wpdb->insert/update accepts an optional formats array (['%d', '%s', ...]) that maps to columns. Pass it through:
RowSink::insert($wpdb, $table, $dto, formats: ['%d', '%d', '%d', '%d', '%s', '%s']);
Without formats, $wpdb infers types — usually fine, but explicit formats prevent edge cases on numeric strings.
Slashing
$wpdb does not call wp_unslash() internally, so RowSink does not apply wp_slash() either. Values are passed raw. If your data needs explicit escaping beyond what $wpdb->prepare() already does, handle it before calling the sink.
No system/meta split
Unlike post/user/term sinks, RowSink writes a flat row. There's no meta_input equivalent — every property maps to a column.
Common mistakes
- Passing a positive
idand expectinginsert()to skip it —insert()always inserts; it's the caller's responsibility to chooseinsert()vsupdate() - Forgetting
where:on update —update()requires$where; without itwpdb->updatewould update every row - Mixing UTC and site-local datetimes in the same column —
RowSinkconverts allDateTimeInterfaceto UTC; if you need site-local persistence, format the value yourself before passing it (or usecomputestep in awith()call)