true, 'job' => array('id' => int, * 'job_type' => string, 'status' => string))`. * * - The `job.status` field uses the §6.0 REST / UI mapping: * the DB-side `pending` value is surfaced as `'queued'` so * the wire shape matches the spec's success example exactly. * The mapping table is centralized in {@see self::STATUS_MAP} * so future status-axis additions stay consistent with the * §6.0 "ジョブ status 写像" table. * * @package Affilun\Application\Sync\UseCase */ declare(strict_types=1); namespace Affilun\Application\Sync\UseCase; use Affilun\Infrastructure\Persistence\SyncJobRecord; use InvalidArgumentException; use RuntimeException; /** * Immutable result of {@see EnqueueProductSyncUseCase::execute()}. * * See the file-level docblock for the §11.2 envelope-shape * contract and the §6.0 DB-status → REST-status mapping rule. */ final class EnqueueProductSyncResult { /** * §6.0 mapping from `affilun_sync_jobs.status` (DB) to REST / UI * publication value. * * Held as a class-level constant so the mapping is colocated * with the {@see self::toArray()} consumer and so future * status-axis additions stay consistent with the §6.0 table. * * @var array */ private const STATUS_MAP = array( 'pending' => 'queued', 'running' => 'running', 'completed' => 'completed', 'failed' => 'failed', 'cancelled' => 'cancelled', ); /** * Persisted job record returned by the dispatcher. */ private SyncJobRecord $job; /** * Correlation id stamped onto the job's payload. */ private string $correlationId; /** * Construct a result with explicit values. * * @param SyncJobRecord $job Persisted job record; * MUST carry a non-null id. * @param string $correlationId Correlation id stamped * onto the payload; MUST be * a non-empty trimmed * string. * * @throws InvalidArgumentException When `$job->id()` is null or * `$correlationId` is empty * after trimming. */ public function __construct( SyncJobRecord $job, string $correlationId ) { if ( $job->id() === null ) { $message = 'EnqueueProductSyncResult requires a persisted SyncJobRecord (non-null id).'; // phpcs:ignore WordPress.Security.EscapeOutput.ExceptionNotEscaped -- Developer-facing message, never rendered to HTML. throw new InvalidArgumentException( $message ); } $trimmed = trim( $correlationId ); if ( $trimmed === '' ) { $message = 'EnqueueProductSyncResult requires a non-empty correlation id.'; // phpcs:ignore WordPress.Security.EscapeOutput.ExceptionNotEscaped -- Developer-facing message, never rendered to HTML. throw new InvalidArgumentException( $message ); } $this->job = $job; $this->correlationId = $trimmed; } /** * Persisted job record. */ public function job(): SyncJobRecord { return $this->job; } /** * Correlation id stamped onto the job's payload. */ public function correlationId(): string { return $this->correlationId; } /** * Render the §11.2 success envelope `data` shape: * * `array( * 'queued' => true, * 'job' => array( * 'id' => int, * 'job_type' => string, * 'status' => string, * ), * )` * * The `job.status` field projects the DB-side * `affilun_sync_jobs.status` through {@see self::STATUS_MAP} so the * wire shape uses the §6.0 REST / UI publication value * (`'queued'` rather than `'pending'`). * * @return array * * @throws RuntimeException When the DB status is not a member * of the §6.0 mapping table (a * defensive guard against future * status-axis drift). */ public function toArray(): array { $dbStatus = $this->job->status(); if ( ! array_key_exists( $dbStatus, self::STATUS_MAP ) ) { $message = sprintf( 'EnqueueProductSyncResult: unmapped affilun_sync_jobs.status "%s"; ' . 'update §6.0 STATUS_MAP to cover the new value.', $dbStatus ); // phpcs:ignore WordPress.Security.EscapeOutput.ExceptionNotEscaped -- Developer-facing message, never rendered to HTML. throw new RuntimeException( $message ); } return array( 'queued' => true, 'job' => array( 'id' => (int) $this->job->id(), 'job_type' => $this->job->jobType(), 'status' => self::STATUS_MAP[ $dbStatus ], ), ); } }