Adds the complete project foundation: - BMAD BMM workflow tooling (_bmad/) - Claude slash commands, skills, and project memories (.claude/) - ESP32 firmware scaffold (PlatformIO + Waveshare e-ink driver) - .gitignore excluding _bmad-output/ and .pio/ build artifacts Planning artifacts (PRD, architecture, epics) are intentionally not tracked — they live in _bmad-output/ per project convention. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
17 KiB
name, description
| name | description |
|---|---|
| oro-backend-dev | OroCommerce backend development reference covering entities, CRUD, datagrids, message queue, bundles, security, system configuration, workflows, cron, and integrations. Use when writing PHP code for Oro bundles, creating entities, building controllers, configuring datagrids, or implementing backend business logic. |
OroCommerce Backend Developer
Reference for backend PHP development on OroCommerce/OroPlatform. Full docs: https://doc.oroinc.com/backend/
Bundle Structure
All custom code lives in bundles under src/Acme/Bundle/{Name}Bundle/.
Bundle priority > 210 to load after Oro core bundles.
AcmeExampleBundle/
AcmeExampleBundle.php # extends Bundle
DependencyInjection/
AcmeExampleExtension.php # loads services.yml
Configuration.php # config tree (if needed)
Resources/
config/
oro/
bundles.yml # { name: FQCN, priority: 255 }
datagrids.yml
navigation.yml
workflows.yml
features.yml
actions.yml # operations/action buttons
processes.yml # entity lifecycle automation
search.yml # search index config
placeholders.yml # back-office UI injection
dashboards.yml # dashboard widgets
api.yml # API entity config
acls.yml # YAML-based ACL definitions
services.yml
translations/messages.en.yml
Entity/
Controller/
Form/Type/
EventListener/
Migrations/Schema/
Tests/Unit/
Auto-registration via Resources/config/oro/bundles.yml:
bundles:
- { name: Acme\Bundle\ExampleBundle\AcmeExampleBundle, priority: 255 }
Entities
Use PHP 8 ORM attributes. Oro entities need #[Config] for ACL, routing, and extend support.
#[ORM\Entity]
#[ORM\Table(name: 'acme_example')]
#[Config(
routeName: 'acme_example_index',
routeView: 'acme_example_view',
defaultValues: ['entity' => ['icon' => 'fa-cube']]
)]
class Example implements ExtendEntityInterface
{
use ExtendEntityTrait;
#[ORM\Id]
#[ORM\GeneratedValue]
#[ORM\Column(type: 'integer')]
private ?int $id = null;
}
#[ConfigField] Attribute
Use #[ConfigField] on individual properties for field-level config:
#[ORM\Column(name: 'subject', type: 'string', length: 255)]
#[ConfigField(defaultValues: [
'dataaudit' => ['auditable' => true],
'importexport' => ['identity' => true],
])]
private $subject;
Config commands: oro:entity-config:update, oro:entity-config:cache:clear,
oro:entity-config:debug "FQCN".
Extend Existing Oro Entities
Add fields to Oro entities via migrations with oro_options:
$table = $schema->getTable('oro_order');
$table->addColumn('custom_field', 'string', [
'oro_options' => [
'extend' => ['is_extend' => true, 'owner' => ExtendScope::OWNER_CUSTOM],
'entity' => ['label' => 'Custom Field'],
]
]);
Never modify Oro entity classes directly. Clear extend cache after:
php bin/console oro:entity-extend:cache:clear
Migrations
Implement Oro\Bundle\MigrationBundle\Migration\Migration (incremental) or
Installation (full schema). Place in Migrations/Schema/v{N}/.
Run: php bin/console oro:migration:load
Datagrids
Configure in Resources/config/oro/datagrids.yml. Key sections:
source: ORM query (select, from, join); protect withacl_resourcecolumns: display config with labels andfrontend_typesorters: sortable columns withdata_namefilters: filter widgets (string, datetime, entity, boolean)properties+actions: row links (view, edit, delete); hide withacl_resource
Set extended_entity_name if entity supports dynamic fields.
Customizing Existing Datagrids
Two approaches:
- YAML override: add columns/sorters/filters in your bundle's
datagrids.yml(Oro merges configs from all bundles by priority). - Event listener: listen to
oro_datagrid.datagrid.build.before.{grid-name}andoro_datagrid.orm_datasource.result.after.{grid-name}for complex modifications (joins, computed columns, post-fetch decoration).
Custom filter types: implement filter class, register with
oro_filter.extension.orm_filter.filter tag.
CRUD Controllers
Pattern: FormType + Controller + Twig templates.
- Controller uses
#[Acl]attributes for permissions UpdateHandlerFacadehandles form submission- Templates extend
@OroUI/actions/{index,view,update}.html.twig - Routing in
Resources/config/oro/routing.yml
Doctrine Entity Listeners
For entity-specific event hooks (postPersist, preUpdate, postUpdate), use Doctrine entity listeners instead of generic event subscribers. Register via service tags:
services:
acme.entity_listener.customer_sync:
class: Acme\Bundle\EclipseBundle\EventListener\CustomerSync
arguments: ['@oro_message_queue.client.message_producer', '@logger']
tags:
- { name: doctrine.orm.entity_listener, entity_manager: default, entity: Oro\Bundle\CustomerBundle\Entity\Customer, event: preUpdate }
- { name: doctrine.orm.entity_listener, entity_manager: default, entity: Oro\Bundle\CustomerBundle\Entity\Customer, event: postUpdate }
- { name: oro_featuretoggle.feature, feature: acme_eclipse_sync }
calls:
- [ addFieldToListenChanges, [ 'name' ] ]
- [ addFieldToListenChanges, [ 'email' ] ]
Abstract base pattern for field-level change tracking:
abstract class AbstractEntityModificationListener
{
protected array $entitiesIdToSync = [];
protected array $fieldsToListenChanges = [];
public function __construct(
protected MessageProducerInterface $messageProducer,
protected LoggerInterface $logger
) {}
public function addFieldToListenChanges(string $value): void
{
if (!in_array($value, $this->fieldsToListenChanges)) {
$this->fieldsToListenChanges[] = $value;
}
}
public function preUpdate(object $entity, PreUpdateEventArgs $event): void
{
foreach ($this->fieldsToListenChanges as $fieldName) {
if ($event->hasChangedField($fieldName)) {
$this->entitiesIdToSync[$entity->getId()] = $entity->getId();
break;
}
}
}
}
Message Queue
Processors implement MessageProcessorInterface + TopicSubscriberInterface.
Return self::ACK, self::REJECT, or self::REQUEUE.
Tag service: oro_message_queue.client.message_processor.
Use JobRunner::createUnique() for non-duplicate jobs.
Transport: DBAL (default, polls DB) or RabbitMQ (EE, via ORO_MQ_DSN).
Topic Registration Shorthand
Use _defaults in mq_topics.yml to auto-tag all topic classes:
services:
_defaults:
tags:
- { name: oro_message_queue.topic }
Acme\Bundle\EclipseBundle\Async\Topic\SyncOrderStatusTopic: ~
Acme\Bundle\EclipseBundle\Async\Topic\SyncOriginOrdersTopic: ~
Acme\Bundle\CpnBundle\Async\Topic\SyncCpnFromEclipseTopic: ~
Processor Auto-Tagging via TopicSubscriberInterface
Alternative to specifying topicName in the service tag -- implement TopicSubscriberInterface
and use _instanceof in mq_processors.yml:
services:
_instanceof:
Oro\Component\MessageQueue\Client\TopicSubscriberInterface:
tags:
- { name: oro_message_queue.client.message_processor }
acme.eclipse.processor.sync_order_status:
class: Acme\Bundle\EclipseBundle\Async\SyncOrderStatusProcessor
parent: acme.async.abstract_processor
The processor class must implement getSubscribedTopics() returning the topic name.
Abstract Processor Pattern
For processors sharing common patterns (job uniqueness, redelivery limits, feature toggle):
abstract class AbstractAcmeProcessor implements MessageProcessorInterface, TopicSubscriberInterface
{
use FeatureCheckerHolderTrait;
public const MAX_REDELIVERY_COUNT = 5;
public function __construct(
protected JobRunner $jobRunner,
protected ManagerRegistry $registry,
protected LoggerInterface $logger
) {}
public function process(MessageInterface $message, SessionInterface $session): string
{
if (!$this->isFeaturesEnabled()) {
return self::REJECT;
}
$redeliveryCount = $message->getProperty(RedeliveryMessageExtension::PROPERTY_REDELIVER_COUNT);
if ($redeliveryCount !== null && $redeliveryCount >= static::MAX_REDELIVERY_COUNT) {
$this->logger->critical('Redelivery limit reached', ['message' => $message]);
return self::REJECT;
}
$result = $this->jobRunner->runUnique(
$message->getMessageId(),
$this->getJobName($message->getBody()),
fn() => $this->processMessage($message->getBody())
);
return $result ? self::ACK : self::REJECT;
}
abstract protected function processMessage(array $body): bool;
abstract protected function getJobName(array $body): string;
}
Security & ACL
Entity ACL via #[Config(defaultValues: ['security' => [...], 'ownership' => [...]])].
Controller ACL via #[Acl(id: '...', type: 'entity', class: '...', permission: 'VIEW')].
Permissions: VIEW, CREATE, EDIT, DELETE, ASSIGN.
Also available:
#[AclAncestor('existing_acl_id')]to reuse ACL definitions on controllers.acls.ymlfor YAML-based ACL definitions.acl_resourceon navigation items to hide unauthorized menus.is_granted('acl_id', entity)in Twig for conditional rendering.AclHelper::apply($queryBuilder)for ACL-filtered ORM queries.
System Configuration
Define settings via SettingsBuilder::append() in Configuration.php.
UI form in Resources/config/oro/system_configuration.yml (groups, fields, tree).
Access: $configManager->get('acme_bundle.setting_name').
Feature Toggles
Define in Resources/config/oro/features.yml with label, toggle (config key),
routes, navigation_items, api_resources, mq_topics.
Services implement FeatureToggleableInterface + FeatureCheckerHolderTrait.
Twig: {% if feature_enabled('feature_name') %}.
Workflows
YAML-based state machines in Resources/config/oro/workflows.yml.
Define steps, attributes, transitions, conditions, and actions.
Triggers: user-initiated, cron-based, or entity-event-based.
Load: php bin/console oro:workflow:definitions:load
Cron Jobs
Commands implement CronCommandScheduleDefinitionInterface.
Namespace: oro:cron:acme:{name}.
getDefaultDefinition() returns crontab string.
Load schedule: php bin/console oro:cron:definitions:load
Navigation
Resources/config/oro/navigation.yml defines menu items and tree placement.
Trees: application_menu (main bar), usermenu, shortcuts.
Import/Export
Register processors tagged oro_importexport.processor (type: export/import).
Provide template fixtures for download-template feature.
Buttons: {% include '@OroImportExport/ImportExport/buttons_from_configuration.html.twig' %}
Logging
Use Monolog (PSR-3 LoggerInterface). Inject @logger via constructor.
Use {placeholders} in messages, pass variables in context array.
Pass exceptions as ['exception' => $e]. Never log sensitive data.
Custom channels: tag with { name: monolog.logger, channel: acme_{bundle} }.
Email Templates
Create Twig templates in Resources/emails/ with metadata comments
(@entityName, @subject, @name, @isSystem).
Load via fixture extending AbstractEmailFixture.
Send via EmailModelSender::send().
Commerce Extensions
- Payment methods: Settings entity + Transport + PaymentMethod + Provider + ViewProvider (https://doc.oroinc.com/backend/extend-commerce/payment/)
- Shipping methods: Settings entity + Transport + ShippingMethod + Types + Provider (https://doc.oroinc.com/backend/extend-commerce/shipping/)
- Checkout: extend or override the
b2b_flow_checkoutworkflow (https://doc.oroinc.com/backend/extend-commerce/checkout-customization-methods/)
Testing
| Level | Base class | Location |
|---|---|---|
| Unit | PHPUnit TestCase | Tests/Unit/ |
| Functional | WebTestCase | Tests/Functional/ |
| API | RestJsonApiTestCase | Tests/Functional/Api/ |
| BDD | Behat features | Tests/Behat/Features/ |
Operations (Actions)
Define custom operations in Resources/config/oro/actions.yml for entity
buttons, datagrid mass actions, and route-based actions. Operations support
preconditions, conditions, form dialogs, and inline actions.
Disable default CRUD operations with exclude_entities; extend/substitute
with extends and substitute_operation.
Validate: php bin/console oro:action:configuration:validate
Scopes
Scopes provide context-dependent behavior (customer, website, customer group).
Use ScopeManager to find/create scopes. Implement ScopeCriteriaProviderInterface
and register with oro_scope.provider tag. Apply scope criteria to queries with
ScopeCriteria::applyToJoinWithPriority(). Used by visibility, pricing, content.
Dashboards
Create widgets in Resources/config/oro/dashboards.yml. Widget template extends
@OroDashboard/Dashboard/widget.html.twig. Route: oro_dashboard_widget with
bundle and name parameters (maps to {Bundle}:Dashboard:{name}).
Data Audit
Enable change logging via 'dataaudit' => ['auditable' => true] in #[Config]
on entity class and #[ConfigField] on individual fields.
Implements AuditAdditionalFieldsInterface for extra fields in audit log.
History accessible at /audit/history/{entity}/{id} and via REST API.
Data Fixtures
Place in Migrations/Data/ORM/ (main) or Migrations/Data/Demo/ORM/ (demo).
Implement FixtureInterface::load(ObjectManager $manager).
Use OrderedFixtureInterface for ordering, DependentFixtureInterface for deps.
Load: php bin/console oro:migration:data:load (add --fixtures-type=demo).
File Storage
Oro uses KnpGaufretteBundle for filesystem abstraction (local FS or GridFS).
Two adapter types: public (direct URL via /media/) and private (no web access).
Register filesystems in Resources/config/oro/app.yml, create FileManager
services as children of oro_gaufrette.file_manager.
GridFS configured via oro_gridfs adapter or gaufrette_adapter.* parameters.
Reports & Segments
Reports are datagrid-based. Configure in datagrids.yml with source.acl_resource: oro_report_view, aggregations, totals, and options.export: true.
Add to Reports menu via navigation.yml.
Segments: dynamic (real-time) or static (snapshot in oro_segment_snapshot).
Search Index
Configure Resources/config/oro/search.yml to make entities searchable.
Define alias, fields (target_type: text/integer/double/datetime), relation_fields,
route for result links, and search_template for custom rendering.
Reindex: php bin/console oro:search:reindex --class="FQCN".
Processes
Automated entity lifecycle tasks in Resources/config/oro/processes.yml.
Triggers fire on Doctrine events (create/update/delete) or cron schedules.
Can run immediately or deferred via MQ with priority and time_shift.
Use exclude_definitions to prevent recursion.
Load: php bin/console oro:process:configuration:load.
Access Rules
Custom query-level access restrictions beyond standard ACL.
Implement AccessRuleInterface (isApplicable + process methods).
Register with oro_security.access_rule tag (type, entityClass, permission).
Modifies ORM queries via AclHelper::apply() + AccessRuleWalker.
Field ACL
Field-level permissions (VIEW, CREATE, EDIT per field).
Enable with 'security' => ['field_acl_supported' => true] in #[Config].
Limit per-field permissions via #[ConfigField(defaultValues: ['security' => ['permissions' => 'VIEW;CREATE']])].
Check: isGranted('VIEW', new FieldVote($entity, 'fieldName')).
Twig: {% if is_granted('VIEW', entity, 'fieldName') %}.
Notification Alerts
Log integration/sync errors visible in admin UI (System > Alerts).
Register NotificationAlertManager service with source and resource type.
Alerts track: source, resource, alert type, operation, step, item ID, external ID.
CLI: oro:notification:alerts:list, oro:notification:alerts:cleanup.
Auto-cleanup of resolved alerts older than 30 days via cron.
Placeholders
Inject content into back-office page regions via Resources/config/oro/placeholders.yml.
Define items with template, order, and optional applicable filter.
Common targets: oro_view_additional_data, oro_widget_side_bar.
Different from Layout Updates (storefront only).
Organization Types (EE)
Define in Resources/config/oro/organization_types.yml. Assign feature
restrictions per organization. Strategies: exclude_list (default) or
include_list. Verify: php bin/console oro:organization-type:config:debug.
Additional Resources
- For detailed entity extension patterns, see reference-entities.md
- For integration patterns, see reference-integrations.md
- For cloud deployment and operations, see the
oro-cloudskill