1414use Workflow \V2 \Support \ExternalPayloads ;
1515use Workflow \V2 \Support \HistoryExport ;
1616use Workflow \V2 \Support \QueryStateReplayer ;
17- use Workflow \V2 \Support \TypeRegistry ;
1817use Workflow \V2 \Support \WorkerProtocolVersion ;
18+ use Workflow \V2 \Support \WorkflowDefinition ;
1919use Workflow \V2 \Support \WorkflowQueryContract ;
2020use Workflow \V2 \Support \WorkflowReplayer ;
21+ use Workflow \V2 \Workflow ;
2122
2223/**
2324 * Executes server-routed query tasks inside a standalone PHP worker process.
@@ -32,6 +33,19 @@ final class WorkflowQueryTaskExecutor
3233{
3334 public const CAPABILITY = WorkerProtocolVersion::CAPABILITY_QUERY_TASKS ;
3435
36+ /**
37+ * @var array<string, class-string<Workflow>>
38+ */
39+ private readonly array $ workflowClassesByType ;
40+
41+ /**
42+ * @param array<string, class-string<Workflow>> $workflowClassesByType
43+ */
44+ public function __construct (array $ workflowClassesByType = [])
45+ {
46+ $ this ->workflowClassesByType = $ this ->normalizeWorkflowClasses ($ workflowClassesByType );
47+ }
48+
3549 /**
3650 * @param array<string, mixed> $task
3751 * @return array<string, mixed>
@@ -111,10 +125,153 @@ public function execute(array $task): array
111125 private function runFromTask (array $ task ): WorkflowRun
112126 {
113127 $ historyExport = $ task ['history_export ' ] ?? null ;
128+ $ historyExport = is_array ($ historyExport ) ? $ historyExport : $ this ->historyExportFromTask ($ task );
114129
115- return (new WorkflowReplayer ())->runFromHistoryExport (
116- is_array ($ historyExport ) ? $ historyExport : $ this ->historyExportFromTask ($ task ),
117- );
130+ $ workflowType = $ this ->workflowTypeFromHistoryExport ($ historyExport )
131+ ?? $ this ->stringValue ($ task ['workflow_type ' ] ?? null );
132+ $ workflowClass = $ workflowType !== null
133+ ? ($ this ->workflowClassesByType [$ workflowType ] ?? null )
134+ : null ;
135+
136+ if ($ workflowClass !== null ) {
137+ $ historyExport = $ this ->historyExportWithWorkflowClass ($ historyExport , $ workflowType , $ workflowClass );
138+ }
139+
140+ return (new WorkflowReplayer ())->runFromHistoryExport ($ historyExport );
141+ }
142+
143+ /**
144+ * @param array<string, mixed> $historyExport
145+ * @param class-string<Workflow> $workflowClass
146+ * @return array<string, mixed>
147+ */
148+ private function historyExportWithWorkflowClass (
149+ array $ historyExport ,
150+ string $ workflowType ,
151+ string $ workflowClass ,
152+ ): array {
153+ $ workflow = is_array ($ historyExport ['workflow ' ] ?? null )
154+ ? $ historyExport ['workflow ' ]
155+ : [];
156+ $ workflow ['workflow_type ' ] = $ workflowType ;
157+ $ workflow ['workflow_class ' ] = $ workflowClass ;
158+ $ historyExport ['workflow ' ] = $ workflow ;
159+
160+ $ contract = WorkflowDefinition::commandContract ($ workflowClass );
161+ $ fingerprint = WorkflowDefinition::fingerprint ($ workflowClass );
162+ $ events = is_array ($ historyExport ['history_events ' ] ?? null )
163+ ? $ historyExport ['history_events ' ]
164+ : [];
165+
166+ foreach ($ events as $ index => $ event ) {
167+ if (! is_array ($ event ) || $ this ->historyEventType ($ event ) !== 'WorkflowStarted ' ) {
168+ continue ;
169+ }
170+
171+ $ payload = is_array ($ event ['payload ' ] ?? null ) ? $ event ['payload ' ] : [];
172+ $ payload ['workflow_class ' ] = $ workflowClass ;
173+ $ payload ['workflow_type ' ] = $ workflowType ;
174+
175+ if ($ fingerprint !== null ) {
176+ $ payload ['workflow_definition_fingerprint ' ] = $ fingerprint ;
177+ }
178+
179+ if (! $ this ->hasStrictDeclaredContract ($ payload )) {
180+ $ payload ['declared_queries ' ] = $ contract ['queries ' ];
181+ $ payload ['declared_query_contracts ' ] = $ contract ['query_contracts ' ];
182+ $ payload ['declared_signals ' ] = $ contract ['signals ' ];
183+ $ payload ['declared_signal_contracts ' ] = $ contract ['signal_contracts ' ];
184+ $ payload ['declared_updates ' ] = $ contract ['updates ' ];
185+ $ payload ['declared_update_contracts ' ] = $ contract ['update_contracts ' ];
186+ $ payload ['declared_entry_method ' ] = $ contract ['entry_method ' ];
187+ $ payload ['declared_entry_mode ' ] = $ contract ['entry_mode ' ];
188+ $ payload ['declared_entry_declaring_class ' ] = $ contract ['entry_declaring_class ' ];
189+ }
190+
191+ $ event ['payload ' ] = $ payload ;
192+ $ events [$ index ] = $ event ;
193+ }
194+
195+ $ historyExport ['history_events ' ] = $ events ;
196+
197+ return $ historyExport ;
198+ }
199+
200+ /**
201+ * @param array<string, class-string<Workflow>> $workflowClassesByType
202+ * @return array<string, class-string<Workflow>>
203+ */
204+ private function normalizeWorkflowClasses (array $ workflowClassesByType ): array
205+ {
206+ $ normalized = [];
207+
208+ foreach ($ workflowClassesByType as $ workflowType => $ workflowClass ) {
209+ if (! is_string ($ workflowType ) || trim ($ workflowType ) === '' ) {
210+ throw new LogicException (
211+ 'Workflow query task executor registry keys must be non-empty workflow types. ' ,
212+ );
213+ }
214+
215+ if (! is_string ($ workflowClass ) || ! is_subclass_of ($ workflowClass , Workflow::class)) {
216+ throw new LogicException (sprintf (
217+ 'Workflow query task executor registry entry [%s] must point to a loadable %s subclass. ' ,
218+ trim ($ workflowType ),
219+ Workflow::class,
220+ ));
221+ }
222+
223+ /** @var class-string<Workflow> $workflowClass */
224+ $ normalized [trim ($ workflowType )] = $ workflowClass ;
225+ }
226+
227+ ksort ($ normalized );
228+
229+ return $ normalized ;
230+ }
231+
232+ /**
233+ * @param array<string, mixed> $historyExport
234+ */
235+ private function workflowTypeFromHistoryExport (array $ historyExport ): ?string
236+ {
237+ $ workflow = is_array ($ historyExport ['workflow ' ] ?? null )
238+ ? $ historyExport ['workflow ' ]
239+ : [];
240+
241+ return $ this ->stringValue ($ workflow ['workflow_type ' ] ?? null );
242+ }
243+
244+ /**
245+ * @param array<string, mixed> $event
246+ */
247+ private function historyEventType (array $ event ): ?string
248+ {
249+ return $ this ->stringValue ($ event ['type ' ] ?? null )
250+ ?? $ this ->stringValue ($ event ['event_type ' ] ?? null );
251+ }
252+
253+ /**
254+ * @param array<string, mixed> $payload
255+ */
256+ private function hasStrictDeclaredContract (array $ payload ): bool
257+ {
258+ foreach ([
259+ 'declared_queries ' ,
260+ 'declared_query_contracts ' ,
261+ 'declared_signals ' ,
262+ 'declared_signal_contracts ' ,
263+ 'declared_updates ' ,
264+ 'declared_update_contracts ' ,
265+ 'declared_entry_method ' ,
266+ 'declared_entry_mode ' ,
267+ 'declared_entry_declaring_class ' ,
268+ ] as $ key ) {
269+ if (! array_key_exists ($ key , $ payload )) {
270+ return false ;
271+ }
272+ }
273+
274+ return true ;
118275 }
119276
120277 /**
0 commit comments