Introduction
Shopware 6.7 introduces significant enhancements to its state machine system, providing developers with more flexibility and control over order and payment processing workflows. One of the most powerful features is the ability to create custom payment statuses that integrate seamlessly with the existing state machine infrastructure. This blog post will walk you through the technical implementation of adding a custom payment status using Shopware's state machine capabilities.
Understanding State Machines in Shopware 6.7
Before diving into the implementation, it's crucial to understand how state machines work in Shopware 6.7. The state machine system is built around three core concepts:
- States: Represent the current condition of an entity
- Transitions: Define how entities move between states
- Transitions: Events that trigger state changes
In the context of payment processing, states represent different payment statuses (pending, paid, cancelled, etc.), while transitions define the valid paths between these statuses.
Prerequisites and Setup
To implement a custom payment status, you'll need:
- Shopware 6.7+ installation
- Basic understanding of Symfony DI container
- Knowledge of Shopware's entity system
- Access to plugin development environment
Creating the Custom Payment Status
Step 1: Define the New State in the Payment State Machine
First, we need to define our custom payment status within the existing payment state machine configuration. Create a new file config/state_machine/payment.xml in your plugin:
<?xml version="1.0" encoding="UTF-8"?>
<state-machine xmlns="http://symfony.com/schema/dic/state-machine"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://symfony.com/schema/dic/state-machine https://symfony.com/schema/dic/state-machine/state_machine-1.0.xsd">
<state-machine name="payment">
<states>
<state name="open" color="#FF9800"/>
<state name="paid" color="#4CAF50"/>
<state name="cancelled" color="#F44336"/>
<state name="refunded" color="#2196F3"/>
<state name="pending_review" color="#9C27B0"/> <!-- Custom state -->
</states>
<transitions>
<transition name="process">
<from>open</from>
<to>pending_review</to>
</transition>
<transition name="approve">
<from>pending_review</from>
<to>paid</to>
</transition>
<transition name="reject">
<from>pending_review</from>
<to>cancelled</to>
</transition>
</transitions>
</state-machine>
</state-machine>
Step 2: Register the State Machine Configuration
In your plugin's Resources/config/config.xml, register the state machine:
<?xml version="1.0" encoding="UTF-8"?>
<config xmlns="http://symfony.com/schema/dic/services"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://symfony.com/schema/dic/services https://symfony.com/schema/dic/services/services-1.0.xsd">
<services>
<service id="Shopware\Core\Checkout\Order\Aggregate\OrderTransaction\OrderTransactionDefinition">
<tag name="shopware.state_machine"/>
</service>
<service id="Shopware\Core\Checkout\Order\OrderDefinition">
<tag name="shopware.state_machine"/>
</service>
</services>
</config>
Step 3: Implement the Custom State Machine Handler
Create a custom state machine handler to manage our new payment status:
<?php declare(strict_types=1);
namespace YourPlugin\Handler;
use Shopware\Core\Checkout\Order\Aggregate\OrderTransaction\OrderTransactionEntity;
use Shopware\Core\Checkout\Order\OrderEvents;
use Shopware\Core\Framework\DataAbstractionLayer\Search\Criteria;
use Shopware\Core\Framework\DataAbstractionLayer\Search\Filter\EqualsFilter;
use Symfony\Component\EventDispatcher\EventSubscriberInterface;
use Symfony\Contracts\Translation\TranslatorInterface;
class CustomPaymentStatusHandler implements EventSubscriberInterface
{
private TranslatorInterface $translator;
public function __construct(TranslatorInterface $translator)
{
$this->translator = $translator;
}
public static function getSubscribedEvents(): array
{
return [
OrderEvents::ORDER_TRANSACTION_STATE_CHANGED => 'onTransactionStateChanged',
];
}
public function onTransactionStateChanged(OrderTransactionStateChangedEvent $event): void
{
$transaction = $event->getTransaction();
if ($transaction->getStateMachineState()->getName() === 'pending_review') {
$this->handlePendingReviewStatus($transaction);
}
}
private function handlePendingReviewStatus(OrderTransactionEntity $transaction): void
{
// Custom logic for pending review status
$this->logTransactionStatusChange($transaction, 'pending_review');
// Send notification to merchant
$this->sendMerchantNotification($transaction);
// Update order status if needed
$this->updateOrderStatus($transaction);
}
private function logTransactionStatusChange(OrderTransactionEntity $transaction, string $status): void
{
// Implementation for logging
\Shopware\Core\Checkout\Order\Aggregate\OrderTransaction\OrderTransactionDefinition::class;
}
private function sendMerchantNotification(OrderTransactionEntity $transaction): void
{
// Implementation for sending notifications
}
private function updateOrderStatus(OrderTransactionEntity $transaction): void
{
// Implementation for updating order status
}
}
Step 4: Create Custom Payment Method
To fully integrate our custom payment status, we need to create a custom payment method:
<?php declare(strict_types=1);
namespace YourPlugin\Core\Checkout\Payment;
use Shopware\Core\Checkout\Payment\PaymentMethodDefinition;
use Shopware\Core\Framework\DataAbstractionLayer\EntityDefinition;
use Shopware\Core\Framework\DataAbstractionLayer\Field\BoolField;
use Shopware\Core\Framework\DataAbstractionLayer\Field\FkField;
use Shopware\Core\Framework\DataAbstractionLayer\Field\Flag\PrimaryKey;
use Shopware\Core\Framework\DataAbstractionLayer\Field\Flag\Required;
use Shopware\Core\Framework\DataAbstractionLayer\Field\IdField;
use Shopware\Core\Framework\DataAbstractionLayer\Field\StringField;
use Shopware\Core\Framework\DataAbstractionLayer\FieldCollection;
class CustomPaymentMethodDefinition extends EntityDefinition
{
public function getEntityName(): string
{
return 'custom_payment_method';
}
public function getCollectionClass(): string
{
return CustomPaymentMethodCollection::class;
}
public function getEntityClass(): string
{
return CustomPaymentMethodEntity::class;
}
protected function defineFields(): FieldCollection
{
return new FieldCollection([
(new IdField('id', 'id'))->setFlags(new PrimaryKey(), new Required()),
(new StringField('name', 'name'))->setFlags(new Required()),
(new StringField('description', 'description')),
(new BoolField('active', 'active'))->setDefaultValue(true),
(new StringField('payment_method_id', 'payment_method_id'))->setFlags(new Required()),
]);
}
}
Step 5: Configure Payment Method with Custom Status
Create a service to handle the payment method configuration:
<?php declare(strict_types=1);
namespace YourPlugin\Core\Checkout\Payment;
use Shopware\Core\Framework\DataAbstractionLayer\Search\Criteria;
use Shopware\Core\Framework\DataAbstractionLayer\Search\Filter\EqualsFilter;
use Symfony\Component\DependencyInjection\ContainerInterface;
class CustomPaymentMethodService
{
private ContainerInterface $container;
public function __construct(ContainerInterface $container)
{
$this->container = $container;
}
public function createCustomPaymentMethod(): void
{
$paymentMethodRepository = $this->container->get('payment_method.repository');
$criteria = new Criteria();
$criteria->addFilter(new EqualsFilter('handlerIdentifier', 'your_custom_payment_handler'));
$existing = $paymentMethodRepository->search($criteria, Context::createDefaultContext());
if ($existing->count() === 0) {
$paymentMethodRepository->create([
[
'name' => 'Custom Payment Method',
'description' => 'Payment method with custom status handling',
'active' => true,
'handlerIdentifier' => 'your_custom_payment_handler',
'position' => 10,
'pluginId' => $this->getPluginId(),
]
], Context::createDefaultContext());
}
}
private function getPluginId(): string
{
// Implementation to retrieve plugin ID
return '';
}
}
Step 6: Implement State Machine Transition Service
Create a dedicated service for managing state transitions:
<?php declare(strict_types=1);
namespace YourPlugin\Core\Checkout\Order;
use Shopware\Core\Checkout\Order\OrderEntity;
use Shopware\Core\Checkout\Order\OrderEvents;
use Shopware\Core\Checkout\Order\OrderStateHandler;
use Shopware\Core\Framework\Context;
use Symfony\Contracts\EventDispatcher\EventDispatcherInterface;
class CustomOrderStateHandler
{
private OrderStateHandler $orderStateHandler;
private EventDispatcherInterface $eventDispatcher;
public function __construct(
OrderStateHandler $orderStateHandler,
EventDispatcherInterface $eventDispatcher
) {
$this->orderStateHandler = $orderStateHandler;
$this->eventDispatcher = $eventDispatcher;
}
public function transitionToPendingReview(string $orderId, Context $context): void
{
// First, check if the transition is valid
$this->validateTransition($orderId, 'pending_review', $context);
// Perform the actual transition
$this->orderStateHandler->process($orderId, 'pending_review', $context);
// Dispatch custom event for further processing
$this->eventDispatcher->dispatch(new CustomPaymentStatusEvent($orderId, 'pending_review'));
}
private function validateTransition(string $orderId, string $targetState, Context $context): void
{
// Implementation for validating state transitions
// This could include checking business rules, permissions, etc.
}
}
Database Integration and Migration
To properly support our custom payment status, we need to create database migrations:
<?php declare(strict_types=1);
namespace YourPlugin\Core\Migration;
use Doctrine\DBAL\Connection;
use Shopware\Core\Framework\Migration\MigrationStep;
class Migration1234567890CustomPaymentStatus extends MigrationStep
{
public function getCreationTimestamp(): int
{
return 1234567890;
}
public function update(Connection $connection): void
{
$connection->executeStatement('
CREATE TABLE IF NOT EXISTS `custom_payment_status_log` (
`id` BINARY(16) NOT NULL,
`order_transaction_id` BINARY(16) NOT NULL,
`old_status` VARCHAR(255) NOT NULL,
`new_status` VARCHAR(255) NOT NULL,
`created_at` DATETIME NOT NULL,
`updated_at` DATETIME DEFAULT NULL,
PRIMARY KEY (`id`),
CONSTRAINT `fk.custom_payment_status_log.order_transaction_id`
FOREIGN KEY (`order_transaction_id`) REFERENCES `order_transaction` (`id`)
ON DELETE CASCADE
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
');
}
public function updateDestructive(Connection $connection): void
{
// No destructive operations needed for this migration
}
}
Testing the Implementation
Create a comprehensive test to ensure our custom payment status works correctly:
<?php declare(strict_types=1);
namespace YourPlugin\Tests\Unit\Core\Checkout\Order;
use PHPUnit\Framework\TestCase;
use Shopware\Core\Checkout\Order\OrderEntity;
use Shopware\Core\Checkout\Order\OrderStateHandler;
use Shopware\Core\Framework\Context;
use Shopware\Core\Framework\DataAbstractionLayer\EntityRepositoryInterface;
use Shopware\Core\Framework\Test\TestCaseBase\IntegrationTestBehaviour;
class CustomPaymentStatusTest extends TestCase
{
use IntegrationTestBehaviour;
public function testCustomPaymentStatusTransition(): void
{
$context = Context::createDefaultContext();
$orderRepository = $this->getContainer()->get('order.repository');
// Create a test order
$orderId = $this->createTestOrder();
// Test transition to pending_review status
$orderStateHandler = $this->getContainer()->get(OrderStateHandler::class);
$orderStateHandler->process($orderId, 'pending_review', $context);
// Verify the status change
$order = $orderRepository->search(new Criteria([$orderId]), $context)->first();
static::assertEquals('pending_review', $order->getStateMachineState()->getName());
}
private function createTestOrder(): string
{
// Implementation for creating test order
return '';
}
}
Performance Considerations
When implementing custom payment statuses, consider these performance aspects:
- Caching: Implement proper caching strategies for state machine configurations
- Batch Operations: Optimize bulk state transitions using batch processing
- Database Indexes: Ensure proper indexing on status-related fields
- Event Handling: Use async event handling for non-critical operations
Security Best Practices
- Validation: Always validate state transitions before execution
- Permissions: Implement proper access control for state changes
- Logging: Maintain detailed audit logs of all status changes
- Input Sanitization: Sanitize all input data before processing
Conclusion
Adding custom payment statuses via state machine in Shopware 6.7 provides a robust and flexible way to extend payment processing workflows. By following the implementation steps outlined in this guide, you can create sophisticated payment status handling that integrates seamlessly with Shopware's existing infrastructure.
The key advantages of this approach include:
- Modularity: Custom payment statuses are cleanly separated from core functionality
- Extensibility: Easy to add new statuses without modifying core code
- Maintainability: Clear separation of concerns makes code easier to maintain
- Performance: Optimized state machine handling with proper caching
This implementation provides a solid foundation for building complex payment workflows while maintaining compatibility with Shopware 6.7's state machine architecture. Remember to thoroughly test your custom implementation and consider the performance implications of your specific use case.
The flexibility offered by Shopware 6.7's state machine system allows developers to create highly customized payment processing experiences that meet specific business requirements while maintaining the reliability and scalability that Shopware is known for.