Recognition Pipeline
The recognition is a chain of processors built on the ChainProcessor component, the same component that runs the API processors. This page describes the pipeline service, the context the processors share, their execution order, and the extension points a custom bundle can use: a custom processor, a replacement of a built-in processor, a file mapper, and a source provider. Reacting to the draft order after it is created is described in OrderDraftCreated Event.
Pipeline Service
Service —
oro_entity_recognition.processor, an instance ofOro\Bundle\EntityRecognitionBundle\Processor\EntityRecognitionPipeline. It extendsOro\Component\ChainProcessor\ActionProcessorand throws\RuntimeExceptionwith the messageFailed to recognize the entity.when no processor sets a result.Action —
get_entity_recognition, the only action of the pipeline.Group —
processing, the only group of the action (OroEntityRecognitionBundle/Resources/config/services.yml).Tag —
oro_entity_recognition.processor. A processor is a service that implements ProcessorInterface and has this tag with theaction,group, andpriorityattributes. Higher priorities run first.Entry point —
Oro\Bundle\OrderRecognitionBundle\Manager\OrderRecognizer::recognizeUsing(mixed $input): mixed. The$inputvalue is an array that accepts thefilekey (ContextKeysAwareInterface::FILE) and the optionalorderOwnerandorderCustomerUserkeys (OrderRecognizer::ORDER_OWNERandOrderRecognizer::ORDER_CUSTOMER_USER). The method returns the pipeline result, the createdOrder.
The bundle registers its processors in OroOrderRecognitionBundle/Resources/config/chain_processors.yml with the class name as the service ID:
services:
Oro\Bundle\OrderRecognitionBundle\Processor\CustomerProcessor:
arguments:
- '@Oro\Bundle\OrderRecognitionBundle\Finder\CustomerFinder'
calls:
- [ setLogger, ['@oro_order_recognition.logger'] ]
tags:
- { name: oro_entity_recognition.processor, action: get_entity_recognition, group: processing, priority: 150 }
Context Keys
The context is the generic Oro\Component\ChainProcessor\Context. The keys are constants of Oro\Bundle\EntityRecognitionBundle\Context\ContextKeysAwareInterface, which every bundle processor implements. The interface also requires a dataFrom(ContextInterface $context): array method that returns the values the processor reads, in the order the processor destructures them.
INPUT(input)The value sent to the microservice.
FileProcessorsets it to a SymfonyUploadedFile; the source providers turn it into the base64 string.OUTPUT(output)The
recognized_dataobject of the microservice response, as an array. Every order-building processor reads it.ENTITY(entity)The
Orderbeing built.OrderProcessorcreates it; every later processor reads it and sets it back.RECOGNITION_ENTITY(recognition)The
OrderRecognitionentity created byOrderRecognitionProcessor.FILE(file)The raw input passed to
OrderRecognizer: a Symfony file, anOro\Bundle\EmailBundle\Entity\EmailAttachment, or anOro\Bundle\AttachmentBundle\Entity\Fileentity.FILE_ID(file_id)The ID of the
Fileentity whenFILEis one.AttachmentProcessorreuses the existing entity instead of creating a new one.ATTACHMENT(attachment)The
Oro\Bundle\AttachmentBundle\Entity\Attachmentthat links the source document to the order.SYMFONY_FILE(symfonyFile)The temporary
UploadedFilecreated byFileProcessor. Used by the attachment processors.PREVIEW_FILE_BLOB(previewFileBlob)The base64 image returned by the microservice as
data.image_base64_blob_file, ornull.PREVIEW_ATTACHMENT(previewAttachment)The
Attachmentcreated from the preview image, when one was returned.LINE_ITEM_CONTEXT(lineItemContext)An array with one element per line item, in payload order, holding the
skuCandidatesandpositionOnPagevalues.FinalProcessorre-keys it by line item ID before storing it onOrderRecognition.
Execution Order
Priority |
Processor |
Reads |
Writes |
|---|---|---|---|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
Marks the state invalid when there is no customer, fills the totals, persists and flushes the order, sets the source entity and the preview attachment ID, stores the line item context keyed by line item ID, flushes again, dispatches |
Note
Processors with equal priorities have no guaranteed order. Pick a unique value inside the intended window.
Insertion Windows
The windows describe the state of the context at each stage and are derived from the priorities in chain_processors.yml; a processor may use any free priority.
Between 250 and 210 —
inputis set, the request is not sent yet.Between 210 and 190 —
outputis set, no entity exists yet.Between 90 and 70 — The order and its line items exist; the addresses (
70) and the shipping method (50) are not set yet.Between 50 and 30 — Every built-in mapping is done, nothing is flushed, and the state is not copied yet. This is the default window for mapping additional fields that need the whole order.
Below -255 — Not recommended. The order is flushed, the event is dispatched, and the result is set. Use the
OrderDraftCreatedevent instead.
Add a Custom Processor
Create a class that implements ProcessorInterface and ContextKeysAwareInterface, and tag it with oro_entity_recognition.processor. The following processor runs after OrderProcessor and copies an additional order.externalOrderNumber key from the payload into an externalOrderNumber extended field that the Acme bundle adds to the Order entity. dataFrom() returns the output and entity context values, process() destructures them, changes the order, and sets the order back into the context:
namespace Acme\Bundle\SmartOrderBundle\Processor;
use Oro\Bundle\EntityRecognitionBundle\Context\ContextKeysAwareInterface;
use Oro\Component\ChainProcessor\ContextInterface;
use Oro\Component\ChainProcessor\ProcessorInterface;
class ExternalOrderNumberProcessor implements ProcessorInterface, ContextKeysAwareInterface
{
#[\Override]
public function process(ContextInterface $context): void
{
[$data, $order] = $this->dataFrom($context);
// ...
$context->set(static::ENTITY, $order);
}
#[\Override]
public function dataFrom(ContextInterface $context): array
{
return [$context->get(static::OUTPUT), $context->get(static::ENTITY)];
}
}
services:
Acme\Bundle\SmartOrderBundle\Processor\ExternalOrderNumberProcessor:
tags:
- { name: oro_entity_recognition.processor, action: get_entity_recognition, group: processing, priority: 185 }
Priority 185 places the processor after OrderProcessor (190) and before OrderRecognitionProcessor (180), so the order exists and nothing depends on the new field yet. The body of process() reads $data['order']['externalOrderNumber'], validates it, and calls the setter of the extended field.
Replace or Disable a Built-In Processor
The built-in processors are services whose ID is the class name and whose oro_entity_recognition.processor tag lives on the service definition. A definition with the same ID in a bundle loaded after OroOrderRecognitionBundle replaces the built-in definition as a whole, so it must restate the arguments and the calls (for example setLogger, and setToken and setSmartOrderCurrencyProvider of OrderProcessor) and repeat the tag with the same action, group, and priority attributes; otherwise, the step disappears from the pipeline.
To change one step of a built-in processor, extend its class and point class: of the redefinition at the subclass. The collaborators injected through the constructor are private in the built-in processors, so a subclass that needs one of them must accept it in its own constructor and pass it on to parent::__construct(). The collaborators that OrderProcessor receives through setToken and setSmartOrderCurrencyProvider are private as well, so a subclass that needs one of them overrides the setter, keeps its own reference, and calls the parent setter. Only the logger set by setLogger is available to subclasses, as the protected $logger property of Psr\Log\LoggerAwareTrait. The following redefinition replaces LineItemsProcessor with a subclass and keeps its arguments, logger call, and priority from chain_processors.yml:
services:
Oro\Bundle\OrderRecognitionBundle\Processor\LineItemsProcessor:
class: Acme\Bundle\SmartOrderBundle\Processor\LineItemsProcessor
arguments:
- '@Oro\Bundle\OrderRecognitionBundle\State\LineItemPriceState'
- '@Oro\Bundle\OrderRecognitionBundle\Finder\ProductFinder'
- '@Oro\Bundle\OrderRecognitionBundle\Finder\ProductUnitFinder'
- '@oro_locale.formatter.number'
- '@oro_pricing.provider.product_line_item_price'
calls:
- [ setLogger, ['@oro_order_recognition.logger'] ]
tags:
- { name: oro_entity_recognition.processor, action: get_entity_recognition, group: processing, priority: 90 }
Decoration with decorates: also works and does not depend on the bundle load order. The pipeline compiler pass (Oro\Component\ChainProcessor\DependencyInjection\ProcessorsLoader) registers the outermost decorator in place of the decorated service. As a result, the decorator takes the place of the built-in processor in the pipeline with the same priority, and the decorated processor is no longer registered on its own. The decorator must implement ProcessorInterface and receive the decorated processor through @.inner.
To disable a step, redefine the service with a subclass whose process() returns early. Do not remove LineItemsProcessor or FinalProcessor this way: without LineItemsProcessor the order has no line items, and without FinalProcessor the order is not flushed and no result is set, so the pipeline throws \RuntimeException.
Map Additional Payload Fields
When Oro adds a field to the contract for a specific document layout, the bundle stores it in OrderRecognition::rawData but does not map it. A processor in the 90 to 30 window maps it while the raw payload and the line items are both available and nothing is flushed.
The following processor copies a lineItems[].notes key into the comment field of each line item, shown as Notes on the order line item. The line items were added by LineItemsProcessor in payload order, so the index of a line item in $order->getLineItems() is the index of its element in $data['lineItems']. FinalProcessor::prepareLineItemContext() relies on the same correlation.
namespace Acme\Bundle\SmartOrderBundle\Processor;
use Oro\Bundle\EntityRecognitionBundle\Context\ContextKeysAwareInterface;
use Oro\Bundle\OrderBundle\Entity\Order;
use Oro\Component\ChainProcessor\ContextInterface;
use Oro\Component\ChainProcessor\ProcessorInterface;
class LineItemNotesProcessor implements ProcessorInterface, ContextKeysAwareInterface
{
#[\Override]
public function process(ContextInterface $context): void
{
/** @var array $data */
/** @var Order $order */
[$data, $order] = $this->dataFrom($context);
foreach ($order->getLineItems() as $index => $lineItem) {
$notes = $data['lineItems'][$index]['notes'] ?? null;
if (\is_string($notes) && '' !== $notes) {
$lineItem->setComment($notes);
}
}
$context->set(static::ENTITY, $order);
}
#[\Override]
public function dataFrom(ContextInterface $context): array
{
return [$context->get(static::OUTPUT), $context->get(static::ENTITY)];
}
}
services:
Acme\Bundle\SmartOrderBundle\Processor\LineItemNotesProcessor:
tags:
- { name: oro_entity_recognition.processor, action: get_entity_recognition, group: processing, priority: 80 }
Priority 80 runs right after LineItemsProcessor (90): the processor needs only the line items, so it does not wait for the addresses and the shipping method. A processor that needs the whole order uses the 50 to 30 window instead. Because RecognitionDataProcessor copies the state mapper content into OrderRecognition::mappingData at priority 30, a processor that runs before it can also record a state for the new field through a custom StateInterface implementation, which makes the field part of the progress calculation. See Recognition State and Progress.
Important
The line item and payload correlation is by index. A custom processor that removes line items from the order must also remove the matching elements of the lineItemContext context key, otherwise FinalProcessor stores the context of the wrong line item.
Note
An alternative is a listener of the OrderDraftCreated event that reads $order->getRecognition()->getRawData() after the flush. The processor is preferred for mapping fields; see OrderDraftCreated Event for when the event is the better choice.
Add a File Mapper
FileProcessor converts the file input into a temporary UploadedFile through Oro\Bundle\OrderRecognitionBundle\File\FileMapperRegistry. The registry asks every service tagged with oro_order_recognition.file_mapper whether it supports the value and uses the first one that does. The bundle includes three mappers: SymfonyFileMapper for Symfony\Component\HttpFoundation\File\File, EmailAttachmentMapper for Oro\Bundle\EmailBundle\Entity\EmailAttachment, and FileEntityMapper for Oro\Bundle\AttachmentBundle\Entity\File.
A mapper implements Oro\Bundle\OrderRecognitionBundle\File\FileMapperInterface and returns an Oro\Bundle\OrderRecognitionBundle\File\FileInfo built from the content, the file name, and the optional MIME type. The following mapper accepts a path of a readable local file, which lets a custom entry point pass a path instead of a file object. map() reads the file content, derives the file name and the MIME type from the path, and returns the FileInfo:
namespace Acme\Bundle\SmartOrderBundle\File;
use Oro\Bundle\OrderRecognitionBundle\File\FileInfo;
use Oro\Bundle\OrderRecognitionBundle\File\FileMapperInterface;
class StringFileMapper implements FileMapperInterface
{
#[\Override]
public function map(mixed $file): ?FileInfo
{
// ...
return new FileInfo($content, $fileName, $mimeType);
}
#[\Override]
public function supports(mixed $file): bool
{
return \is_string($file) && \is_file($file) && \is_readable($file);
}
}
services:
Acme\Bundle\SmartOrderBundle\File\StringFileMapper:
tags:
- { name: oro_order_recognition.file_mapper }
When no mapper supports the input, or a mapper throws, FileProcessor logs an error and returns without setting input. EntityRecognitionProcessor then finds no source provider for the empty input and returns as well, and the recognition fails in the order-building processors because output is empty. A custom mapper therefore must return a FileInfo for every value it supports and must throw a \RuntimeException when the content cannot be read.
Add a Source Provider
EntityRecognitionProcessor turns the input context key into the base64_blob_file request value through Oro\Bundle\EntityRecognitionBundle\Source\SourceProviderRegistry. The registry iterates the services tagged with oro_entity_recognition.source_provider and uses the first one whose supports() returns true. The default SymfonyFileSourceProvider supports Symfony\Component\HttpFoundation\File\File and returns the base64-encoded file content.
A source provider is needed when a custom processor in the 250 to 210 window sets input to a value that is not a Symfony file. The following provider accepts a non-empty string that is already base64-encoded and passes it through:
namespace Acme\Bundle\SmartOrderBundle\Source;
use Oro\Bundle\EntityRecognitionBundle\Source\SourceProviderInterface;
class Base64StringSourceProvider implements SourceProviderInterface
{
#[\Override]
public function getInputString(mixed $data): ?string
{
return $data;
}
#[\Override]
public function supports(mixed $data): bool
{
return \is_string($data) && '' !== $data && false !== \base64_decode($data, true);
}
}
services:
Acme\Bundle\SmartOrderBundle\Source\Base64StringSourceProvider:
tags:
- { name: oro_entity_recognition.source_provider, priority: 10 }
The registry receives the providers as a tagged iterator, so the priority tag attribute orders them; a higher priority is checked first. When no provider supports the input, EntityRecognitionProcessor returns without sending a request and without setting output.
Error Handling
Oro\Component\ChainProcessor\ChainProcessor::executeProcessors() catches every exception thrown by a processor and rethrows it as Oro\Component\ChainProcessor\Exception\ExecutionFailedException with the message Processor failed: "<service id>". Reason: <original message> and the original exception as getPrevious(). Code that handles a recognition failure must therefore inspect the previous exception.
The bundle throws the following exceptions from inside the pipeline:
Oro\Bundle\OrderRecognitionBundle\Exception\IncompleteOrderExceptionThrown by
LineItemsProcessorwhenlineItemsis empty, with the codeIncompleteOrderException::NO_LINE_ITEMS_CODE(2001). TheERROR_TO_MESSAGES_MAPconstant maps the code to theoro_order_recognition.email.error_cause.no_line_itemtranslation key that the failure email uses as the error cause. The message queue processor reads the map fromIncompleteOrderExceptionby class name, so a subclass that adds codes to its own copy of the constant does not add causes to the email; a custom message processor that overridescollectErrors()is needed for that.Oro\Bundle\OrderRecognitionBundle\Exception\InvalidResponseExceptionThrown by
OrderRecognitionConnectorwhen the microservice returns a non-200status, with the message selected byErrorMessageHandler. See Error Codes.\RuntimeExceptionThrown by
EntityRecognitionProcessorwhen the API key is empty, byOrderRecognitionApiClientwhen the URL is empty, and byEntityRecognitionPipelinewhen no processor sets a result.
The entry points handle the wrapped exception as follows:
DashboardControllershows the message of the previous exception in the widget only when its class is exactlyInvalidResponseExceptionorIncompleteOrderException; every other failure shows the genericoro_order_recognition.error.genericmessage. A custom processor that wants its message shown in the widget throws one of these two classes.AbstractOrderRecognitionProcessorwalks thegetPrevious()chain, uses the message of anInvalidResponseExceptionor the mapped cause of anIncompleteOrderExceptionas the error cause of the failure email, and rejects the message. A message that the broker redelivers because a previous consumer did not finish it (for example, a consumer that died during the HTTP call) is processed again until itsoro-redeliver-countproperty reachesMAX_REDELIVER_ATTEMPTS(5); after that the processor sends the failure notification and rejects it without running the recognition. See Asynchronous Processing.