Appearance
Workflow edytorski (epik WFL)
Dokumentacja user- i developer-facing dla warstwy workflow PIM. Źródło decyzji architektonicznych:
docs/adr/0029-workflow-engine-and-placement.md. Backlog i benchmark:Project Plan/feature-workflow-tickets.md. Polityka stanów RBAC:Project Plan/PRD/PRD-PIM-rbac.md§3.8. Kontrakt pól JSONB (gate kompletności):docs/api/jsonb-schemas.md§7.
1. Po co to jest
Workflow to warstwa autoryzacyjna nad cyklem życia obiektu (produkt, kategoria, asset). Odpowiada na pytanie „kto, kiedy i pod jakimi warunkami może zmienić stan i treść obiektu". Wyróżnik nad Pimcore OSS: dedykowana kolejka przeglądu (inbox), zadania generowane automatycznie z przejść, oraz — od WFL-P5 — definicje procesu edytowalne per tenant bez deployu (czego Pimcore nie umie bez płatnego visual designera).
2. Maszyna stanów object_editorial
Silnik: symfony/workflow typu state_machine. Marking trzymany na kolumnie objects.status przez accessor getEditorialMarking()/setEditorialMarking() na CatalogObject (celowo NIE przez publiczny status — uniknięcie leaka schematu do OpenAPI).
submit_for_review approve
draft ────────────────► review ────────────► published
▲ │ │
│ reject │ unpublish │
└────────────────────────┘◄────────────────────┘
│
│ publish (skrót draft→published)
└──────────────────────────────────────────────► published
published ──unpublish──► draft (dowolny) ──archive──► archived ──restore──► draft| Stan | Znaczenie |
|---|---|
draft | edycja treści dozwolona; stan początkowy |
review | zgłoszony do przeglądu; treść zablokowana dla nie-approverów |
published | opublikowany; treść zablokowana (patrz §5 edit-lock) |
archived | zarchiwizowany; przywracalny przez restore |
Przejścia i wymagane uprawnienia (statyczna mapa TransitionPermissionGuard; definicje DB nadpisują per-przejście metadanymi — §6):
| Przejście | Z → do | Uprawnienie |
|---|---|---|
submit_for_review | draft → review | products.edit |
publish | draft → published (skrót) | workflow.approve_reject |
approve | review → published | workflow.approve_reject |
reject | review → draft | workflow.approve_reject |
unpublish | published → draft | workflow.transition.unpublish |
archive | * → archived | workflow.approve_reject |
restore | archived → draft | workflow.approve_reject |
3. API
Wszystkie trasy są w OpenAPI (docs/api-spec/v0.json). Powierzchnia proceduralna (CQRS, custom #[Route]):
GET /api/objects/{id}/workflow— discovery: aktualny stan + dostępne przejścia zblockers[](kody: brak uprawnienia,completeness_gate,comment_required) +reviewer({type: role|user, label}— rozwiązany akceptant do którego trafi task po zgłoszeniu; §4). FE jest źródłem prawdy które przyciski renderować i pokazuje hint „zadanie trafi do…" pod kontrolką.POST /api/objects/{id}/workflow/transitions/{transition}— zastosuj przejście. Body opcjonalne{comment}(≤2000 znaków). 409 +blockersgdy przejście niedozwolone; 422 gdycomment_requireda komentarz pusty.GET /api/objects/{id}/workflow/transitions— log przejść (kursor UUIDv7, najnowsze pierwsze) +actor_name(nazwa autora przejścia z seamIdentity\Contracts\Directory\UserDirectoryInterface;nulldla systemu/CLI).POST /api/objects/{id}/workflow/request-unpublish— prośba o depublikację (dla użytkownika bezworkflow.transition.unpublish).GET/POST/PATCH /api/workflow/tasks— zadania (listamine/status/object/due; tworzenie custom; complete/cancel/reassign gated assignee-or-approver). Lista wzbogaca każdy task oobject_title/object_sku/object_kind(batch seamCatalog\Contracts\Query\ObjectSummaryPortpoobject_id;nulldla usuniętych obiektów),created_by_nameiassignee_name(UserDirectory).minematchuje bezpośrednie przypisanie, członkostwo roli-akceptanta oraz posiadaczyworkflow.approve_reject(superset — task widoczny niezależnie od tego czy akceptant to rola czy konkretny user).GET/PATCH /api/notifications— powiadomienia in-app (own-data, gatedworkflow.view).GET/POST/PUT /api/workflow/definitions+enable/disable— CRUD definicji (gatedworkflow.manage_definitions, §6).
Bulk: POST /api/objects/bulk-actions/change_status — przejście na wielu obiektach; per-row sprawdzenie can() (marketing bulk-approve = 100% zablokowane, nie eskalacja przez coarse permission).
4. Zadania i powiadomienia (WFL-P4)
Przejście generuje zadania automatycznie (WorkflowTaskAutomation, idempotentnie — obiekt nigdy nie ma dwóch OPEN tasków jednego typu):
submit_for_review→ taskreviewdla skonfigurowanego akceptanta (§4a).reject→ zamknięcie tasku review + taskfixdla autora submitu z komentarzem recenzenta.approve→ zamknięcie tasku review.request-unpublish→ taskrequest_unpublishdla skonfigurowanego akceptanta (§4a).
4a. Konfigurowalny akceptant (routing zadań, ADR-0029)
Akceptant (odbiorca tasków review / request_unpublish) jest konfigurowalny per ObjectType: rola LUB konkretny user (XOR). Rozwiązanie: EditorialWorkflowProvider::reviewerFor(?objectTypeId): ?TaskAssignee — definicja ObjectType-specific > tenant-global; gdy flaga OFF / brak definicji / brak reviewer → null, a automatyka spada na wbudowaną rolę approver (ObjectEditorialWorkflow::REVIEWER_ROLE). VO Workflow\Contracts\TaskAssignee trzyma XOR roleCode/userId.
Ustawiane w UI Ustawienia przepływu (§6) — picker akceptanta (rola albo user). Pole workflow_definitions.reviewer (JSONB {role_code}|{user_id}, migracja Version20260712100000) walidowane przez WorkflowDefinitionValidator (istnienie roli/usera w tenancie + XOR). Discovery endpoint (§3) zwraca rozwiązanego akceptanta jako reviewer, więc kontrolka na karcie produktu pokazuje „Po zgłoszeniu zadanie trafi do: <Rola: X | Imię usera>". Fix-task zawsze idzie do autora submitu (bez zmian).
Powiadomienia (WorkflowNotificationFanOut, post-flush pipeline przez Messenger): submit → grantees workflow.approve_reject (minus submitter); approve/reject → autor submitu; unpublish_requested → grantees workflow.transition.unpublish. Zadania nie wysyłają własnego powiadomienia — fan-out zdarzenia już powiadomił tę samą publiczność (unika duplikatu).
5. Edit-lock na opublikowanych obiektach (WFL-P1-02/03)
Treść obiektu w stanie published/review jest zablokowana dla użytkowników bez products.edit_any_state. Próba PATCH treści → 403 workflow_state_locked z flagą request_unpublish_available. FE pokazuje workflow-lock-banner z akcją „poproś o depublikację". Ścieżka auto-unpublish (dla uprawnionych): edycja publikowanego obiektu automatycznie stosuje unpublish z kontekstem {auto_unpublish: true}.
6. Definicje per tenant (WFL-P5, ADR-0029 filar 7)
Za flagą WORKFLOW_CUSTOM_DEFINITIONS (default OFF). Gdy OFF — statyczna maszyna YAML object_editorial (100% jak dotąd). Gdy ON — EditorialWorkflowProvider buduje maszynę z rekordu workflow_definitions (ObjectType-specific > tenant-global), zachowując tę samą NAZWĘ object_editorial i współdzielony dispatcher, więc wszystkie listenery (guard, gate, log, event recorder) działają bez zmian. Metadane per-przejście (permission, comment_required, completeness_gate) czytają guardy przez InMemoryMetadataStore.
Builder UI: Workflow → „Ustawienia przepływu" (CTA w topbarze huba Workflow, trasa /workflow/settings, gated workflow.manage_definitions + flaga runtime na GET /api/auth/me → feature_flags.workflow_custom_definitions; stara ścieżka /settings/workflow przekierowuje). Form-based (świadomie NIE canvas-graph): legenda ról, karta flagi, selektor definicji per built-in ObjectType, read-only diagram stanów, picker akceptanta (rola albo user — §4a), przejścia z permission/komentarzem, gate kompletności (slider %). Walidator (WorkflowDefinitionValidator) egzekwuje: snake_case, initial draft, osiągalność BFS, istnienie permission code, istnienie akceptanta (rola/user w tenancie) + XOR, zakaz usunięcia stanu z żywymi obiektami. Cache workera keyed definitionId@updatedAt — edycja definicji invaliduje bez restartu.
7. Bezpieczeństwo i znane ograniczenia (WFL-P6-01)
Pokryte testami adwersaryjnymi (WorkflowSecurityApiTest + suity per-obszar):
- Cross-tenant: discovery / log / taski / notyfikacje / definicje = 0 wyników dla obcego tenanta; przejście na obiekcie obcego tenanta → 404 (bez existence oracle).
- Eskalacje: raw PATCH
statusnie omija maszyny (409 na nieosiągalny stan); bulk per-action; task/notification IDOR → 403. - Race: podwójny
approve→ drugi 409, bez duplikatu w logu ani duplikatu zamknięcia tasku.
Znane ograniczenie — self-approve (świadoma decyzja MVP): autor submitu posiadający workflow.approve_reject może zatwierdzić własny submit — brak zasady four-eyes w MVP. Zabezpieczone testem WorkflowSecurityApiTest::selfApproveIsAllowedInMvp (pin). Kandydat do Fazy 2 (opcjonalna polityka four-eyes per definicja). Odwrócenie decyzji musi świadomie złamać ten test.
8. Skąd co czytać (mapa kodu)
- Kontrakty + statyczna maszyna:
apps/api/src/Workflow/Contracts/(m.in.TaskAssignee,EditorialWorkflowProviderInterface::reviewerFor),apps/api/config/packages/workflow.yaml. - Guardy/log/eventy:
apps/api/src/Workflow/Infrastructure/EventSubscriber/,apps/api/src/Catalog/Infrastructure/Workflow/. - Provider definicji + walidator + routing akceptanta:
apps/api/src/Workflow/Application/(EditorialWorkflowProvider,WorkflowDefinitionValidator). - Taski + automatyka:
apps/api/src/Workflow/Domain/Entity/WorkflowTask.php,.../Infrastructure/Messenger/WorkflowTaskAutomation.php. - Cross-BC seamy do wzbogaceń:
apps/api/src/Identity/Contracts/Directory/UserDirectoryInterface.php(+Application/SqlUserDirectory— id→imię),apps/api/src/Catalog/Contracts/Query/ObjectSummaryPort.php(+Application/Query/ObjectSummaryReader— batch id→{title,sku,kind}). - FE:
apps/admin/src/features/workflow/(hub,WorkflowTaskCard,TasksPanel,ReviewQueuePage,task-presentation,settings/),.../features/catalog/products/components/workflow-*(kontrolka + historia),.../features/dashboard/components/MyTasksCard.tsx(widget Pulpitu),.../lib/workflow/(api,tasks-api,definitions-api,directory-api).
