Important

You are browsing upcoming documentation for version 6.0 of OroCommerce, OroCRM, and OroPlatform, scheduled for release in 2024. Read version 5.1 (the latest LTS version) of the Oro documentation to get up-to-date information.

See our Release Process documentation for more information on the currently supported and upcoming releases.

Processors 

A processor is the main element that implements the business logic of the API. Each processor must implement ProcessorInterface and be registered in the dependency injection container using the oro.api.processor tag.

Please see the actions and context sections for more details about where and how processors are used.

Please see the actions section for more details about where and how processors are used.

You can also use the oro:api:debug command to see all actions and processors.

Creating a Processor 

To create a new processor, create a class that implements ProcessorInterface and tag it with the oro.api.processor name.

namespace Acme\Bundle\DemoBundle\Api\Processor;

use Oro\Bundle\ApiBundle\Processor\Context;
use Oro\Component\ChainProcessor\ContextInterface;
use Oro\Component\ChainProcessor\ProcessorInterface;

/**
 * A short description of what the processor does.
 **/
class DoSomething implements ProcessorInterface
{
    /**
     * {@inheritDoc}
     */
    public function process(ContextInterface $context): void
    {
        /** @var Context $context */

        // do some work here
    }
}
services:
    acme.api.do_something:
        class: Acme\Bundle\DemoBundle\Api\Processor\DoSomething
        tags:
            - { name: oro.api.processor, action: get, group: normalize_input, priority: 10 }

Please note that:

  • The name of a processor usually starts with a verb, and the Processor suffix is not used.

  • The priority attribute is used to control the order in which processors are executed. The higher the priority, the earlier a processor is executed. The default value is 0. The possible range is from -255 to 255. But for some types of processors, the range can be different. For details, see the documentation of the ChainProcessor component. If several processors have the same priority, the order they are executed is unpredictable.

  • Each processor should check whether its work is already done because there may be a processor with a higher priority that does the same thing in a different way. For example, such processors can be created for customization purposes.

  • Prefer Processor Conditions over a conditional logic inside a processor to avoid loading unnecessary processors.

  • As you can create API resources for any type of object (not only ORM entities), it is always a good idea to check whether a processor applies to ORM entities. This check is swift and helps avoid possible logic issues and performance impact. Please use the oro_api.doctrine_helper service to get an instance of Oro\Bundle\ApiBundle\Util\DoctrineHelper, as this class is optimized for use in the API stack.

An example:

public function process(ContextInterface $context): void
{
    /** @var Context $context */

    $entityClass = $context->getClassName();
    if (!$this->doctrineHelper->isManageableEntityClass($entityClass)) {
        // only manageable entities are supported
        return;
    }

    // do some work
}

You can find a list of all existing processors in the Processor directory.

Processor Conditions 

When you register a processor in the dependency injection container, you can specify conditions when the processor should be executed. Use the attributes of the oro.api.processor tag to specify conditions. Any context property which is scalar, array, or object (instance of the ToArrayInterface ) can be used in the conditions.

For example, a simple condition is used to filter processors by the action:

services:
    acme.api.do_something:
        class: Acme\Bundle\DemoBundle\Api\Processor\DoSomething
        tags:
            - { name: oro.api.processor, action: get }

In this case, the acme.api.do_something is executed only in the scope of the get action and is skipped for other actions.

Conditions provide a simple way to specify which processors are required to accomplish a work. Pay attention that the dependency injection container does not load processors that do not fit the conditions. Use conditions to create a fast API.

This allows building conditions based on any attribute from the context.

Condition types depend on the registered Applicable Checkers. By default, the following checkers are registered:

For performance reasons, the functionality of SkipGroupApplicableChecker and GroupRangeApplicableChecker was implemented as part of OptimizedProcessorIterator.

Examples of Processor Conditions 

  • No conditions. A processor is executed for all actions.

services:
    acme.api.do_something:
        tags:
            - { name: oro.api.processor }
  • A processor is executed only for a specified action.

services:
    acme.api.do_something:
        tags:
            - { name: oro.api.processor, action: get_list }
  • A processor is executed only for a specified action and group.

services:
    acme.api.do_something:
        tags:
            - { name: oro.api.processor, action: get_list, group: initialize }
  • A processor is executed only for a specified action, group and request type.

services:
    acme.api.do_something:
        tags:
            - { name: oro.api.processor, action: get_list, group: initialize, requestType: rest }
  • A processor is executed for all requests except for the specified one.

services:
    acme.api.do_something:
        tags:
            - { name: oro.api.processor, action: get_list, group: initialize, requestType: !rest }
  • A processor is executed only for REST requests that conform to the JSON:API specification.

services:
    acme.api.do_something:
        tags:
            - { name: oro.api.processor, action: get_list, group: initialize, requestType: rest&json_api }
  • A processor is executed either for REST requests or requests that conform to the JSON:API specification.

services:
    acme.api.do_something:
        tags:
            - { name: oro.api.processor, action: get_list, group: initialize, requestType: rest|json_api }

Please note that a value can contain either & (logical AND) or | (logical OR) operators, but you cannot combine them.

  • A processor is executed for all REST requests, excluding requests that conform to the JSON:API specification.

services:
    acme.api.do_something:
        tags:
            - { name: oro.api.processor, action: get_list, group: initialize, requestType: rest&!json_api }
  • A processor is executed for several specified actions.

services:
    acme.api.do_something:
        tags:
            - { name: oro.api.processor, action: get, group: initialize, priority: 10 }
            - { name: oro.api.processor, action: get_list, group: initialize, priority: 5 }
  • A processor is executed only for a specified entity.

services:
    acme.api.do_something:
        tags:
            - { name: oro.api.processor, action: get_list, group: initialize, class: Oro\Bundle\UserBundle\Entity\User }
  • A processor is executed only for entities that implement a certain interface or extend a certain base class. Currently, two attributes are compared by the instance of instead of the equal operator. These attributes are class and parentClass.

services:
    acme.api.do_something:
        tags:
            - { name: oro.api.processor, action: get_list, group: initialize, class: Oro\Bundle\UserBundle\Entity\AbstractUser }
  • A processor is executed only when someAttribute exists in the context.

services:
    acme.api.do_something:
        tags:
            - { name: oro.api.processor, action: get_list, group: initialize, someAttribute: exists }

Please note that exists operators cannot be used together with & (logical AND) and | (logical OR) operators.

  • A processor is executed only when someAttribute does not exist in the context.

services:
    acme.api.do_something:
        tags:
            - { name: oro.api.processor, action: get_list, group: initialize, someAttribute: '!exists' }

For more examples, see the configuration of existing processors. See processors.*.yml files.

Error Handling 

There are several types of errors that may occur when processing a request:

  • Validation errors. A validation error occurs if a request has some invalid parameters, headers, or data.

  • Security errors. This type of error occurs if access is denied to a requested, updated, or deleted entity.

  • Unexpected errors. These errors will occur if an unforeseen problem happens. For example, when there is no access to the database or a file system, or the requested entity does not exist.

Please note that to validate input data for create and update actions the best solution is to use validation constraints. In most cases, it helps avoid writing any PHP code and configuring the required validation rules in Resources/config/oro/api.yml. For detailed information on adding custom validation constraints, see the Forms and Validators Configuration topic. The following example shows how to add a validation constraint via Resources/config/oro/api.yml:

api:
    entities:
        Acme\Bundle\DemoBundle\Entity\SomeEntity:
            fields:
                primaryEmail:
                    form_options:
                        constraints:
                            # add Symfony\Component\Validator\Constraints\Email validation constraint
                            - Email: ~

If an error occurs in a processor, the main execution flow is interrupted, and the control is passed to a group of processors called normalize_result. This is true for all types of errors. But there are some exceptions to this rule for the errors that occur in any processor of the normalize_result group. The execution flow is interrupted only if any of these processors raises an exception. However, these processors can safely add new errors into the context, and the execution of the next processors will not be interrupted. For implementation details, see RequestActionProcessor.

An error is represented by the Error class. Additionally, you can use the ErrorSource class to specify a source of an error, e.g., the name of a URI parameter or the path to a property in the data. These classes have the following methods:

Error class

  • create(title, detail) static - Creates an instance of the Error class.

  • createValidationError(title, detail) static - Creates an instance of the Error class represents a violation of validation constraint.

  • createByException(exception) static - Creates an instance of the Error class based on a given exception object.

  • getStatusCode() - Retrieves the HTTP status code applicable to this problem.

  • getCode() - Retrieves an application-specific error code.

  • setCode(code) - Sets an application-specific error code.

  • getTitle() - Retrieves a short, human-readable summary of the problem.

  • setTitle(title) - Sets a short, human-readable summary of the problem.

  • getDetail() - Retrieves a human-readable explanation specific to this occurrence of the problem.

  • setDetail(detail) - Sets a human-readable explanation specific to this occurrence of the problem.

  • getSource() - Retrieves the instance of ErrorSource that represents a source of this problem.

  • setSource(source) - Sets the instance of ErrorSource that represents a source of this occurrence of the problem.

  • getInnerException() - Retrieves an exception object that caused this occurrence of the problem.

  • setInnerException(exception) - Sets an exception object that caused this occurrence of the problem.

  • trans(translator) - Translates all attributes that are represented by the Label object.

ErrorSource class

  • createByPropertyPath(propertyPath) static - Creates an instance of the ErrorSource class that represents the path to a property that caused the error.

  • createByPointer(pointer) static - Creates an instance of the ErrorSource class that represents a pointer to a property in the request document that caused the error.

  • createByParameter(parameter) static - Creates an instance of the ErrorSource class that represents URI query parameter caused the error.

  • getPropertyPath() - Retrieves the path to a property that caused the error. For example, title or author.name.

  • setPropertyPath(propertyPath) - Sets the path to a property that caused the error.

  • getPointer() - Retrieves a pointer to a property in the request document that caused the error. For JSON, the pointer conforms RFC 6901. For example, /data for a primary data object, or /data/attributes/title for a specific attribute.

  • setPointer(pointer) - Sets a pointer to a property in the request document that caused the error.

  • getParameter() - Retrieves the URI query parameter that caused the error.

  • setParameter(parameter) - Sets the URI query parameter that caused the error.

Below is an illustration of throwing an exception to demonstrate how a processor informs about an error.

namespace Oro\Bundle\ApiBundle\Processor\Shared;

use Doctrine\ORM\QueryBuilder;

use Oro\Component\ChainProcessor\ContextInterface;
use Oro\Component\ChainProcessor\ProcessorInterface;
use Oro\Component\EntitySerializer\EntitySerializer;
use Oro\Bundle\ApiBundle\Exception\RuntimeException;
use Oro\Bundle\ApiBundle\Processor\Context;
use Oro\Bundle\ApiBundle\Request\ApiActionGroup;

/**
 * Loads entity using the EntitySerializer component.
 * As returned data is already normalized, the "normalize_data" group will be skipped.
 */
class LoadEntityByEntitySerializer implements ProcessorInterface
{
    private EntitySerializer $entitySerializer;

    public function __construct(EntitySerializer $entitySerializer)
    {
        $this->entitySerializer = $entitySerializer;
    }

    /**
     * {@inheritDoc}
     */
    public function process(ContextInterface $context): void
    {
        /** @var Context $context */

        if ($context->hasResult()) {
            // data already retrieved
            return;
        }

        $query = $context->getQuery();
        if (!$query instanceof QueryBuilder) {
            // unsupported query
            return;
        }

        $config = $context->getConfig();
        if (null === $config) {
            // only configured API resources are supported
            return;
        }

        $result = $this->entitySerializer->serialize(
            $query,
            $config,
            $context->getNormalizationContext()
        );
        if (empty($result)) {
            $result = null;
        } elseif (count($result) === 1) {
            $result = reset($result);
        } else {
            throw new RuntimeException('The result must have one or zero items.');
        }

        $context->setResult($result);

        // data returned by the EntitySerializer are already normalized
        $context->skipGroup(ApiActionGroup::NORMALIZE_DATA);
    }
}

For security errors, throw Symfony\Component\Security\Core\Exception\AccessDeniedException). The raised exception will be converted to the Error object automatically by NormalizeResultActionProcessor. The services called exception text extractors automatically fill the meaningful properties of the error objects (like HTTP status code, title, and description) based on the underlying exception object. The default implementation of such extractor is ExceptionTextExtractor. To add a new extractor, create a class that implements ExceptionTextExtractorInterface and tag it with the oro.api.exception_text_extractor in the dependency injection container.

Another way to add an Error object to the context is helpful for validation errors as it allows you to add several errors:

namespace Oro\Bundle\ApiBundle\Processor\Shared;

use Oro\Component\ChainProcessor\ContextInterface;
use Oro\Component\ChainProcessor\ProcessorInterface;
use Oro\Bundle\ApiBundle\Model\Error;
use Oro\Bundle\ApiBundle\Processor\SingleItemContext;
use Oro\Bundle\ApiBundle\Request\Constraint;

/**
 * Makes sure that the identifier of an entity exists in the Context.
 */
class ValidateEntityIdExists implements ProcessorInterface
{
    /**
     * {@inheritDoc}
     */
    public function process(ContextInterface $context): void
    {
        /** @var SingleItemContext $context */

        $entityId = $context->getId();
        if ((null === $entityId || '' === $entityId) && $context->hasIdentifierFields()) {
             $context->addError(
                Error::createValidationError(
                    Constraint::ENTITY_ID,
                    'The identifier of an entity must be set in the context.'
                )
            );
        }
    }
}

Please note that the default HTTP status code for validation errors is 400 Bad Request. If necessary, you can set another HTTP status code, e.g., by passing it as the third argument of the Error::createValidationError method.

The Constraint class contains titles for different kinds of validation errors. All titles end with the word constraint. We recommend using the same template when adding custom types.

All API logs are written into the API channel. To inject the API logger directly to your processors, use the common way. For example:

services:
    acme.api.do_something:
        class: Acme\Bundle\DemoBundle\Api\Processor\DoSomething
        arguments:
            - '@logger'
        tags:
            - { name: oro.api.processor, ... }
            - { name: monolog.logger, channel: api }