jobs = $jobs; } /** * Execute the §11.3 list-sync-jobs query. * * Calls {@see SyncJobRepository::findPaginated()} with the * supplied query VO, projects every returned * {@see SyncJobRecord} through {@see self::projectJob()} (which * applies the DB `pending` → wire `queued` substitution), and * wraps the result in a {@see ListSyncJobsResult} carrying the * pagination axes echoed back from the query. * * @param SyncJobListQuery $query Validated list-query inputs. */ public function execute( SyncJobListQuery $query ): ListSyncJobsResult { $page = $this->jobs->findPaginated( $query ); $items = array(); foreach ( $page->items() as $record ) { $items[] = $this->projectJob( $record ); } return new ListSyncJobsResult( $items, $page->page(), $page->perPage(), $page->total(), $page->pageCount() ); } /** * Project a single {@see SyncJobRecord} onto the §6.5 `SyncJob` * wire shape. * * Returned shape: * * `array( * 'id' => ?int, * 'job_type' => string, * 'status' => string, // wire vocabulary * 'attempts' => int, * 'available_at' => ?string, // ISO-8601 UTC * 'locked_at' => ?string, // ISO-8601 UTC * 'finished_at' => ?string, // ISO-8601 UTC * 'last_error' => ?string, * 'payload' => array, * )` * * The DB `pending` → wire `queued` substitution per * `rest-api-spec.md` §6.0 is applied on the `status` axis; * every other status passes through verbatim. * * @param SyncJobRecord $record Record to project. * * @return array */ private function projectJob( SyncJobRecord $record ): array { $status = $record->status(); if ( $status === self::DB_STATUS_PENDING ) { $status = self::WIRE_STATUS_QUEUED; } return array( 'id' => $record->id(), 'job_type' => $record->jobType(), 'status' => $status, 'attempts' => $record->attempts(), 'available_at' => $this->formatTimestamp( $record->availableAt() ), 'locked_at' => $this->formatTimestamp( $record->lockedAt() ), 'finished_at' => $this->formatTimestamp( $record->finishedAt() ), 'last_error' => $record->lastError(), 'payload' => $this->decodePayload( $record->payloadJson() ), ); } /** * Format a {@see DateTimeInterface} as an ISO-8601 UTC string, * or return null when the input is null. * * @param DateTimeInterface|null $value Timestamp to format. */ private function formatTimestamp( ?DateTimeInterface $value ): ?string { if ( $value === null ) { return null; } return $value->format( DateTimeInterface::ATOM ); } /** * Decode the record's `payload_json` blob into an associative * array. Returns an empty array when decoding fails or yields a * non-array shape — the §6.5 surface treats payload as a * structured map and a malformed blob should not crash the * listing endpoint. * * @param string $payloadJson Raw JSON blob. * * @return array */ private function decodePayload( string $payloadJson ): array { $decoded = json_decode( $payloadJson, true ); if ( ! is_array( $decoded ) ) { return array(); } /** @var array $decoded */ return $decoded; } }