diff --git a/.github/workflows/test-application.yaml b/.github/workflows/test-application.yaml
new file mode 100644
index 0000000..1d8b5ea
--- /dev/null
+++ b/.github/workflows/test-application.yaml
@@ -0,0 +1,95 @@
+name: Test application
+
+on:
+ pull_request:
+ push:
+ branches:
+ - '[0-9]+.x'
+ - '[0-9]+.[0-9]+'
+
+jobs:
+ test:
+ name: 'PHP ${{ matrix.php-version }} (${{ matrix.dependency-versions }}, Coverage ${{ matrix.coverage }})'
+ runs-on: ubuntu-latest
+
+ env:
+ DATABASE_URL: mysql://root:root@127.0.0.1:3306/sulu_messenger_test?serverVersion=5.7&charset=utf8mb4
+
+ strategy:
+ fail-fast: false
+ matrix:
+ include:
+ - php-version: '8.0'
+ coverage: false
+ dependency-versions: 'lowest'
+ env:
+ SYMFONY_DEPRECATIONS_HELPER: weak
+
+ - php-version: '8.1'
+ coverage: true
+ dependency-versions: 'highest'
+ env:
+ SYMFONY_DEPRECATIONS_HELPER: weak
+
+ services:
+ mysql:
+ image: mysql:5.7
+ env:
+ MYSQL_ROOT_PASSWORD: root
+ ports:
+ - 3306:3306
+ options: --health-cmd="mysqladmin ping" --health-interval=10s --health-timeout=5s --health-retries=5
+
+ steps:
+ - name: Checkout project
+ uses: actions/checkout@v2
+
+ - name: Install and configure PHP
+ uses: shivammathur/setup-php@v2
+ with:
+ php-version: ${{ matrix.php-version }}
+ extensions: ctype, iconv, mysql
+ coverage: pcov
+
+ - name: Install composer dependencies
+ uses: ramsey/composer-install@v2
+ with:
+ dependency-versions: ${{ matrix.dependency-versions }}
+ composer-options: ${{ matrix.composer-options }}
+
+ - name: Bootstrap test environment
+ run: composer bootstrap-test-environment
+
+ - name: Execute test cases
+ if: matrix.coverage == false
+ run: composer test
+
+ - name: Execute test cases with coverage
+ if: ${{ matrix.coverage }}
+ run: composer test-with-coverage
+
+ - name: Check code coverage
+ if: ${{ matrix.coverage }}
+ run: composer check-coverage
+
+ lint:
+ name: "PHP Lint"
+ runs-on: ubuntu-latest
+
+ steps:
+ - name: Checkout project
+ uses: actions/checkout@v2
+
+ - name: Install and configure PHP
+ uses: shivammathur/setup-php@v2
+ with:
+ php-version: 8.1
+ extensions: ctype, iconv, mysql
+
+ - name: Install composer dependencies
+ uses: ramsey/composer-install@v1
+ with:
+ dependency-versions: highest
+
+ - name: Lint Code
+ run: composer lint
diff --git a/.gitignore b/.gitignore
new file mode 100644
index 0000000..20bcd78
--- /dev/null
+++ b/.gitignore
@@ -0,0 +1,12 @@
+.idea
+*.iml
+
+/vendor/
+composer.lock
+
+.php-cs-fixer.cache
+.phpunit.result.cache
+.deptrac.cache
+
+/tests/Application/var
+/tests/Application/secrets
diff --git a/.php-cs-fixer.dist.php b/.php-cs-fixer.dist.php
new file mode 100644
index 0000000..b31feeb
--- /dev/null
+++ b/.php-cs-fixer.dist.php
@@ -0,0 +1,43 @@
+in(__DIR__)
+ ->exclude(['tests/Application/var'])
+ ->notName('bundles.php');
+
+return (new PhpCsFixer\Config())
+ ->setRiskyAllowed(true)
+ ->setRules([
+ '@Symfony' => true,
+ '@Symfony:risky' => true,
+ 'ordered_imports' => true,
+ 'concat_space' => ['spacing' => 'one'],
+ 'array_syntax' => ['syntax' => 'short'],
+ 'phpdoc_align' => ['align' => 'left'],
+ 'class_definition' => [
+ 'multi_line_extends_each_single_line' => true,
+ ],
+ 'linebreak_after_opening_tag' => true,
+ 'declare_strict_types' => true,
+ 'method_argument_space' => ['on_multiline' => 'ensure_fully_multiline'],
+ 'native_constant_invocation' => true,
+ 'native_function_casing' => true,
+ 'native_function_invocation' => ['include' => ['@internal']],
+ 'no_php4_constructor' => true,
+ 'no_superfluous_phpdoc_tags' => ['allow_mixed' => true, 'remove_inheritdoc' => true],
+ 'no_unreachable_default_argument_value' => true,
+ 'no_useless_else' => true,
+ 'no_useless_return' => true,
+ 'php_unit_strict' => true,
+ 'phpdoc_order' => true,
+ 'semicolon_after_instruction' => true,
+ 'strict_comparison' => true,
+ 'strict_param' => true,
+ 'array_indentation' => true,
+ 'multiline_whitespace_before_semicolons' => true,
+ 'single_line_throw' => false,
+ 'visibility_required' => ['elements' => ['property', 'method', 'const']],
+ ])
+ ->setFinder($finder);
diff --git a/README.md b/README.md
index b5e3f64..62d7ccc 100644
--- a/README.md
+++ b/README.md
@@ -1 +1,73 @@
-# messenger
\ No newline at end of file
+
Sulu Messenger
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+This library provides the stamps and middlewares which configures the sulu message bus.
+It can be used independently in any symfony installation.
+
+## Installation
+
+Make sure Composer is installed globally, as explained in the
+[installation chapter](https://getcomposer.org/doc/00-intro.md)
+of the Composer documentation.
+
+Open a command console, enter your project directory and execute:
+
+```bash
+composer require sulu/messenger
+```
+
+Then, enable the bundle by adding it to the list of registered bundles
+in the `config/bundles.php` file of your project:
+
+```php
+// config/bundles.php
+
+return [
+ // ...
+ Sulu\Messenger\Infrastructure\Symfony\HttpKernel\SuluMessengerBundle::class => ['all' => true],
+];
+```
+
+## Middlewares
+
+### UnpackExceptionMiddleware
+
+The `UnpackExceptionMiddleware` will unpack the `HandlerFailedException` which
+is created by the Symfony [`HandleMessageMiddleware`](/~https://github.com/symfony/symfony/blob/c7dbcc954366f92f66360f3960a10dc1ef5f2584/src/Symfony/Component/Messenger/Middleware/HandleMessageMiddleware.php#L129).
+This way we make sure that the real exception is thrown out by this message
+bus, and a controller can catch or convert it to a specific http status code.
+This middleware is always activated in the sulu message bus.
+
+### DoctrineFlushMiddleware
+
+The `DoctrineFlushMiddleware` is a Middleware which let us flush the Doctrine
+EntityManager by an opt-in flag via the `EnableFlushStamp`. It can be used this way:
+
+```php
+use Sulu\Messenger\Infrastructure\Symfony\Messenger\FlushMiddleware\EnableFlushStamp;
+
+$this->handle(new Envelope(new YourMessage(), [new EnableFlushStamp()]));
+```
+
+This middleware is always activated in the sulu message bus.
diff --git a/composer.json b/composer.json
new file mode 100644
index 0000000..eac7612
--- /dev/null
+++ b/composer.json
@@ -0,0 +1,105 @@
+{
+ "name": "sulu/messenger",
+ "description": "This library provides the stamps, middlewares and the sulu message bus.",
+ "license": "proprietary",
+ "type": "sulu-bundle",
+ "require": {
+ "php": "8.0.* || 8.1.*",
+ "doctrine/dbal": "^3.3",
+ "doctrine/doctrine-bundle": "^2.5",
+ "doctrine/orm": "^2.11",
+ "symfony/config": "^5.4 || ^6.0",
+ "symfony/dependency-injection": "^5.4 || ^6.0",
+ "symfony/doctrine-bridge": "^5.4 || ^6.0",
+ "symfony/framework-bundle": "^5.4 || ^6.0",
+ "symfony/http-kernel": "^5.4 || ^6.0",
+ "symfony/messenger": "^5.4 || ^6.0"
+ },
+ "require-dev": {
+ "coduo/php-matcher": "^6.0",
+ "friendsofphp/php-cs-fixer": "^3.6",
+ "handcraftedinthealps/code-coverage-checker": "^0.2.5",
+ "jangregor/phpstan-prophecy": "^1.0",
+ "nunomaduro/collision": "^5.0 || ^6.0",
+ "phpspec/prophecy-phpunit": "^2.0",
+ "phpstan/phpstan": "^1.4",
+ "phpstan/phpstan-doctrine": "^1.2",
+ "phpstan/phpstan-phpunit": "^1.0",
+ "phpstan/phpstan-symfony": "^1.1",
+ "phpstan/phpstan-webmozart-assert": "^1.0",
+ "phpunit/phpunit": "^9.5",
+ "qossmic/deptrac-shim": "^0.19.3",
+ "rector/rector": "^0.12.16",
+ "schranz/test-generator": "^0.3",
+ "symfony/browser-kit": "^5.4 || ^6.0",
+ "symfony/css-selector": "^5.4 || ^6.0",
+ "symfony/debug-bundle": "^5.4 || ^6.0",
+ "symfony/error-handler": "^5.4 || ^6.0",
+ "symfony/phpunit-bridge": "^5.4 || ^6.0",
+ "thecodingmachine/phpstan-strict-rules": "^1.0",
+ "symfony/dotenv": "^5.4 || ^6.0",
+ "symfony/yaml": "^5.4 || ^6.0"
+ },
+ "autoload": {
+ "psr-4": {
+ "Sulu\\Messenger\\": "src/"
+ }
+ },
+ "autoload-dev": {
+ "psr-4": {
+ "Sulu\\Messenger\\Tests\\": "tests/"
+ }
+ },
+ "scripts": {
+ "bootstrap-test-environment": [
+ "@php tests/Application/bin/console cache:clear --env test",
+ "@php tests/Application/bin/console doctrine:database:drop --force --if-exists --env test",
+ "@php tests/Application/bin/console doctrine:database:create --env test",
+ "@php tests/Application/bin/console doctrine:schema:create --env test"
+ ],
+ "test": "@phpunit",
+ "test-with-coverage": "@phpunit --coverage-php Tests/Application/var/reports/coverage.php --coverage-cobertura=Tests/Application/var/cobertura-coverage.xml --coverage-html Tests/Application/var/reports/html --log-junit Tests/Application/var/reports/junit.xml",
+ "check-coverage": [
+ "@php vendor/bin/code-coverage-checker \"Tests/Application/var/reports/coverage.php\" \"line\" \"100.00\" \"src\""
+ ],
+ "lint": [
+ "@php-cs",
+ "@phpstan",
+ "@lint-rector",
+ "@lint-twig",
+ "@lint-yaml",
+ "@lint-container",
+ "@lint-composer",
+ "@lint-doctrine",
+ "@lint-deptrac"
+ ],
+ "phpunit": "@php vendor/bin/phpunit",
+ "phpstan": [
+ "@php tests/Application/bin/console cache:warmup --env=dev",
+ "@php vendor/bin/phpstan analyze"
+ ],
+ "lint-rector": [
+ "@php tests/Application/bin/console cache:warmup --env=dev",
+ "@php vendor/bin/rector process --dry-run"
+ ],
+ "rector": [
+ "@php vendor/bin/rector process"
+ ],
+ "php-cs": "@php vendor/bin/php-cs-fixer fix --verbose --diff --dry-run",
+ "php-cs-fix": "@php vendor/bin/php-cs-fixer fix",
+ "lint-composer": "@composer validate --no-check-publish --strict",
+ "lint-yaml": "@php tests/Application/bin/console lint:yaml tests/Application/config --parse-tags",
+ "lint-deptrac": "@php vendor/bin/deptrac",
+ "lint-container": [
+ "@php tests/Application/bin/console lint:container --env dev",
+ "@php tests/Application/bin/console lint:container --env test",
+ "@php tests/Application/bin/console lint:container --env prod"
+ ],
+ "lint-doctrine": [
+ "@php tests/Application/bin/console doctrine:schema:validate --skip-sync --env prod"
+ ]
+ },
+ "config": {
+ "sort-packages": true
+ }
+}
diff --git a/config/services.php b/config/services.php
new file mode 100644
index 0000000..1c33c75
--- /dev/null
+++ b/config/services.php
@@ -0,0 +1,19 @@
+services();
+
+ $services->set('sulu_messenger.doctrine_flush_middleware')
+ ->class(DoctrineFlushMiddleware::class)
+ ->args([service('doctrine.orm.entity_manager')]);
+
+ $services->set('sulu_messenger.unpack_exception_middleware')
+ ->class(UnpackExceptionMiddleware::class);
+};
diff --git a/deptrac.yaml b/deptrac.yaml
new file mode 100644
index 0000000..ce4c8c1
--- /dev/null
+++ b/deptrac.yaml
@@ -0,0 +1,66 @@
+parameters:
+ paths:
+ - src
+
+ layers:
+ # Infrastructure
+ - name: Infrastructure
+ collectors:
+ - type: className
+ regex: ^Redis
+ - type: className
+ regex: .*EntityManager.*
+ - type: className
+ regex: .*EntityRepository.*
+ - type: className
+ regex: .*ObjectManager.*
+ - type: className
+ regex: .*ObjectRepository.*
+
+ # Layers
+ - name: UserInterface
+ collectors:
+ - type: directory
+ regex: UserInterface/.*
+ - name: Infrastructure
+ collectors:
+ - type: directory
+ regex: Infrastructure/.*
+ - name: Application
+ collectors:
+ - type: directory
+ regex: Application\/((?!ProcessManager).*)/.*
+ - name: ProcessManager
+ collectors:
+ - type: directory
+ regex: Application/ProcessManager/.*
+ - name: Domain
+ collectors:
+ - type: directory
+ regex: Code\/Domain\/((?!Event).*)\/.* # events are excluded because they are accessible by all
+ - name: Event
+ collectors:
+ - type: directory
+ regex: Code\/Domain\/Event\/.*
+
+ ruleset:
+ # Ruleset
+ UserInterface:
+ - Application
+ - Domain
+ - Infrastructure
+ Infrastructure:
+ - Infrastructure
+ - Application
+ - Domain
+ Application:
+ - Domain
+ - Event
+ ProcessManager:
+ - Domain
+ - Application
+ # All Events:
+ #- OtherContext Event
+ Domain: ~
+ Event:
+ - Domain
diff --git a/phpstan.neon b/phpstan.neon
new file mode 100644
index 0000000..f6b4b5a
--- /dev/null
+++ b/phpstan.neon
@@ -0,0 +1,24 @@
+includes:
+ - vendor/jangregor/phpstan-prophecy/extension.neon
+ - vendor/phpstan/phpstan-symfony/extension.neon
+ - vendor/phpstan/phpstan-doctrine/extension.neon
+ - vendor/phpstan/phpstan-doctrine/rules.neon
+ - vendor/phpstan/phpstan-phpunit/extension.neon
+ - vendor/phpstan/phpstan-phpunit/rules.neon
+ - vendor/phpstan/phpstan-webmozart-assert/extension.neon
+ - vendor/thecodingmachine/phpstan-strict-rules/phpstan-strict-rules.neon
+
+parameters:
+ paths:
+ - src
+ - tests
+ - config
+ level: max
+ excludePaths:
+ - %currentWorkingDirectory%/vendor/*
+ - %currentWorkingDirectory%/tests/Application/var/*
+ doctrine:
+ objectManagerLoader: tests/phpstan/object-manager.php
+ symfony:
+ container_xml_path: %currentWorkingDirectory%/tests/Application/var/cache/dev/Sulu_Messenger_Tests_Application_KernelDevDebugContainer.xml
+ console_application_loader: tests/phpstan/console-application.php
diff --git a/phpunit.xml.dist b/phpunit.xml.dist
new file mode 100644
index 0000000..d88a1b4
--- /dev/null
+++ b/phpunit.xml.dist
@@ -0,0 +1,47 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ tests/Unit
+
+
+
+ tests/Functional
+
+
+
+
+
+ src
+
+
+
+
+
+
+
+
+
+
diff --git a/rector.php b/rector.php
new file mode 100644
index 0000000..eb75dca
--- /dev/null
+++ b/rector.php
@@ -0,0 +1,43 @@
+parameters();
+ $services = $containerConfigurator->services();
+
+ $parameters->set(Option::PATHS, [__DIR__ . '/src', __DIR__ . '/tests']);
+ $parameters->set(Option::SKIP, [
+ __DIR__ . '/tests/Application/var',
+ __DIR__ . '/tests/Application/config',
+ ]);
+ $parameters->set(Option::PHPSTAN_FOR_RECTOR_PATH, __DIR__ . '/phpstan.neon');
+
+ // basic rules
+ $parameters->set(Option::AUTO_IMPORT_NAMES, true);
+ $parameters->set(Option::IMPORT_DOC_BLOCKS, true);
+ $parameters->set(Option::IMPORT_SHORT_CLASSES, false);
+
+ $containerConfigurator->import(SetList::CODE_QUALITY);
+ $containerConfigurator->import(LevelSetList::UP_TO_PHP_80);
+
+ // symfony rules
+ $parameters = $containerConfigurator->parameters();
+ $parameters->set(
+ Option::SYMFONY_CONTAINER_XML_PATH_PARAMETER,
+ __DIR__ . '/tests/Application/var/cache/dev/Sulu_Messenger_Tests_Application_KernelDevDebugContainer.xml'
+ );
+ $containerConfigurator->import(SymfonySetList::SYMFONY_CODE_QUALITY);
+ $containerConfigurator->import(SymfonySetList::SYMFONY_CONSTRUCTOR_INJECTION);
+ $containerConfigurator->import(SymfonyLevelSetList::UP_TO_SYMFONY_54);
+
+ $containerConfigurator->import(DoctrineSetList::DOCTRINE_CODE_QUALITY);
+};
diff --git a/src/Infrastructure/Symfony/HttpKernel/SuluMessengerBundle.php b/src/Infrastructure/Symfony/HttpKernel/SuluMessengerBundle.php
new file mode 100644
index 0000000..2dfd709
--- /dev/null
+++ b/src/Infrastructure/Symfony/HttpKernel/SuluMessengerBundle.php
@@ -0,0 +1,90 @@
+ $config
+ */
+ public function getConfiguration(array $config, ContainerBuilder $container): ConfigurationInterface
+ {
+ return $this;
+ }
+
+ public function prepend(ContainerBuilder $container): void
+ {
+ if (!$container->hasExtension('framework')) {
+ throw new \RuntimeException(\sprintf('The "%s" bundle requires "framework" bundle.', self::ALIAS));
+ }
+
+ $container->prependExtensionConfig(
+ 'framework',
+ [
+ 'messenger' => [
+ 'default_bus' => 'sulu_message_bus',
+ 'buses' => [
+ 'sulu_message_bus' => [
+ 'middleware' => [
+ 'sulu_messenger.unpack_exception_middleware',
+ 'sulu_messenger.doctrine_flush_middleware',
+ ],
+ ],
+ ],
+ ],
+ ]
+ );
+ }
+
+ public function getConfigTreeBuilder(): TreeBuilder
+ {
+ return new TreeBuilder(self::ALIAS);
+ }
+
+ /**
+ * @param array $configs
+ */
+ public function load(array $configs, ContainerBuilder $container): void
+ {
+ $loader = new PhpFileLoader($container, new FileLocator(\dirname(__DIR__, 4) . '/config'));
+
+ $loader->load('services.php');
+ }
+}
diff --git a/src/Infrastructure/Symfony/Messenger/FlushMiddleware/DoctrineFlushMiddleware.php b/src/Infrastructure/Symfony/Messenger/FlushMiddleware/DoctrineFlushMiddleware.php
new file mode 100644
index 0000000..bebc6ff
--- /dev/null
+++ b/src/Infrastructure/Symfony/Messenger/FlushMiddleware/DoctrineFlushMiddleware.php
@@ -0,0 +1,30 @@
+next()->handle($envelope, $stack);
+
+ // flush unit-of-work to the database after the root message was handled successfully
+ if (!empty($envelope->all(EnableFlushStamp::class))) {
+ $this->entityManager->flush();
+ }
+
+ return $envelope;
+ }
+}
diff --git a/src/Infrastructure/Symfony/Messenger/FlushMiddleware/EnableFlushStamp.php b/src/Infrastructure/Symfony/Messenger/FlushMiddleware/EnableFlushStamp.php
new file mode 100644
index 0000000..6b59dc7
--- /dev/null
+++ b/src/Infrastructure/Symfony/Messenger/FlushMiddleware/EnableFlushStamp.php
@@ -0,0 +1,14 @@
+next()->handle($envelope, $stack);
+ } catch (HandlerFailedException $exception) { // @phpstan-ignore-line
+ throw $exception->getPrevious(); // @phpstan-ignore-line
+ }
+
+ return $envelope;
+ }
+}
diff --git a/tests/Application/.env b/tests/Application/.env
new file mode 100644
index 0000000..8de577b
--- /dev/null
+++ b/tests/Application/.env
@@ -0,0 +1,6 @@
+# env
+APP_ENV=test
+APP_SECRET='s$cretf0rt3st'
+SYMFONY_DEPRECATIONS_HELPER=999999
+KERNEL_CLASS='Sulu\Messenger\Tests\Application\Kernel'
+DATABASE_URL=mysql://root:ChangeMe@127.0.0.1:3306/sulu_messenger?serverVersion=8.0&charset=utf8mb4
diff --git a/tests/Application/Kernel.php b/tests/Application/Kernel.php
new file mode 100644
index 0000000..9a59db5
--- /dev/null
+++ b/tests/Application/Kernel.php
@@ -0,0 +1,47 @@
+ ['all' => true],
+ DebugBundle::class => ['dev' => true, 'test' => true],
+ DoctrineBundle::class => ['all' => true],
+ SuluMessengerBundle::class => ['all' => true],
+ ];
+
+ foreach ($bundles as $class => $envs) {
+ if ($envs[$this->environment] ?? $envs['all'] ?? false) {
+ yield new $class();
+ }
+ }
+ }
+}
diff --git a/tests/Application/bin/console b/tests/Application/bin/console
new file mode 100755
index 0000000..a32f095
--- /dev/null
+++ b/tests/Application/bin/console
@@ -0,0 +1,4 @@
+#!/usr/bin/env php
+getParameterOption(['--env', '-e'], null, true)) {
+ \putenv('APP_ENV=' . $_SERVER['APP_ENV'] = $_ENV['APP_ENV'] = $env);
+}
+
+if ($input->hasParameterOption('--no-debug', true)) {
+ \putenv('APP_DEBUG=' . $_SERVER['APP_DEBUG'] = $_ENV['APP_DEBUG'] = '0');
+}
+
+require \dirname(__DIR__, 2) . '/bootstrap.php';
+
+if ($_SERVER['APP_DEBUG']) {
+ \umask(0000);
+
+ if (\class_exists(Debug::class)) {
+ (new Debug())->enable();
+ }
+}
+
+$kernel = new Kernel($_SERVER['APP_ENV'], (bool) $_SERVER['APP_DEBUG']);
+$application = new Application($kernel);
+$application->run($input);
diff --git a/tests/Application/config/packages/doctrine.yaml b/tests/Application/config/packages/doctrine.yaml
new file mode 100644
index 0000000..bde8bb1
--- /dev/null
+++ b/tests/Application/config/packages/doctrine.yaml
@@ -0,0 +1,11 @@
+doctrine:
+ dbal:
+ url: '%env(resolve:DATABASE_URL)%'
+
+ # IMPORTANT: You MUST configure your server version,
+ # either here or in the DATABASE_URL env var (see .env file)
+ #server_version: '8.0'
+ orm:
+ auto_generate_proxy_classes: true
+ naming_strategy: doctrine.orm.naming_strategy.underscore_number_aware
+ auto_mapping: true
diff --git a/tests/Application/config/packages/framework.yaml b/tests/Application/config/packages/framework.yaml
new file mode 100644
index 0000000..e40ab1e
--- /dev/null
+++ b/tests/Application/config/packages/framework.yaml
@@ -0,0 +1,25 @@
+# see https://symfony.com/doc/current/reference/configuration/framework.html
+framework:
+ secret: '%env(APP_SECRET)%'
+ #csrf_protection: true
+ #http_method_override: false
+
+ # Enables session support. Note that the session will ONLY be started if you read or write from it.
+ # Remove or comment this section to explicitly disable session support.
+ session:
+ handler_id: null
+ cookie_secure: auto
+ cookie_samesite: lax
+ storage_factory_id: session.storage.factory.native
+ name: SULUSESSID # This avoids conflicts with other applications running on the same domain
+
+ #esi: true
+ #fragments: true
+ php_errors:
+ log: true
+
+when@test:
+ framework:
+ test: true
+ session:
+ storage_factory_id: session.storage.factory.mock_file
diff --git a/tests/Application/public/.gitkeep b/tests/Application/public/.gitkeep
new file mode 100644
index 0000000..e69de29
diff --git a/tests/Functional/.gitkeep b/tests/Functional/.gitkeep
new file mode 100644
index 0000000..e69de29
diff --git a/tests/Traits/PrivatePropertyTrait.php b/tests/Traits/PrivatePropertyTrait.php
new file mode 100644
index 0000000..b4d7b8a
--- /dev/null
+++ b/tests/Traits/PrivatePropertyTrait.php
@@ -0,0 +1,45 @@
+getProperty($propertyName);
+ $propertyReflection->setAccessible(true);
+
+ return $propertyReflection->getValue($object);
+ }
+
+ /**
+ * @param mixed $value
+ */
+ protected static function setPrivateProperty(object $object, string $propertyName, $value): void
+ {
+ $reflection = new \ReflectionClass($object);
+ try {
+ $propertyReflection = $reflection->getProperty($propertyName);
+ self::setValue($propertyReflection, $object, $value);
+ } catch (\ReflectionException) {
+ $parent = $reflection->getParentClass();
+ if ($parent) {
+ $propertyReflection = $parent->getProperty($propertyName);
+ self::setValue($propertyReflection, $object, $value);
+ }
+ }
+ }
+
+ private static function setValue(\ReflectionProperty $propertyReflection, object $object, mixed $value): void
+ {
+ $propertyReflection->setAccessible(true);
+
+ $propertyReflection->setValue($object, $value);
+ }
+}
diff --git a/tests/Unit/Infrastructure/Symfony/Messenger/FlushMiddleware/DoctrineFlushMiddlewareTest.php b/tests/Unit/Infrastructure/Symfony/Messenger/FlushMiddleware/DoctrineFlushMiddlewareTest.php
new file mode 100644
index 0000000..b719780
--- /dev/null
+++ b/tests/Unit/Infrastructure/Symfony/Messenger/FlushMiddleware/DoctrineFlushMiddlewareTest.php
@@ -0,0 +1,76 @@
+
+ */
+ private ObjectProphecy $entityManager;
+
+ protected function setUp(): void
+ {
+ $this->entityManager = $this->prophesize(EntityManagerInterface::class);
+ $this->middleware = new DoctrineFlushMiddleware(
+ $this->entityManager->reveal()
+ );
+ }
+
+ public function testHandleWithoutStamp(): void
+ {
+ $envelope = $this->createEnvelope();
+ $stack = $this->createStack();
+
+ $this->entityManager->flush()
+ ->shouldNotBeCalled();
+
+ $this->assertSame(
+ $envelope,
+ $this->middleware->handle($envelope, $stack)
+ );
+ }
+
+ public function testHandleWithStamp(): void
+ {
+ $envelope = $this->createEnvelope();
+ $envelope = $envelope->with(new EnableFlushStamp());
+ $stack = $this->createStack();
+
+ $this->entityManager->flush()
+ ->shouldBeCalled();
+
+ $this->assertSame(
+ $envelope,
+ $this->middleware->handle($envelope, $stack)
+ );
+ }
+
+ private function createEnvelope(): Envelope
+ {
+ return new Envelope(new \stdClass());
+ }
+
+ private function createStack(): StackMiddleware
+ {
+ return new StackMiddleware([]);
+ }
+}
diff --git a/tests/Unit/Infrastructure/Symfony/Messenger/UnpackExceptionMiddleware/UnpackExceptionMiddlewareTest.php b/tests/Unit/Infrastructure/Symfony/Messenger/UnpackExceptionMiddleware/UnpackExceptionMiddlewareTest.php
new file mode 100644
index 0000000..0cd4782
--- /dev/null
+++ b/tests/Unit/Infrastructure/Symfony/Messenger/UnpackExceptionMiddleware/UnpackExceptionMiddlewareTest.php
@@ -0,0 +1,84 @@
+middleware = new UnpackExceptionMiddleware();
+ }
+
+ public function testHandleDefault(): void
+ {
+ $envelope = $this->createEnvelope();
+ $stack = $this->createStack();
+
+ $this->assertSame(
+ $envelope,
+ $this->middleware->handle($envelope, $stack)
+ );
+ }
+
+ public function testHandleException(): void
+ {
+ $this->expectException(\LogicException::class);
+ $this->expectExceptionMessage('Specific unpacked exception.');
+
+ $envelope = $this->createEnvelope();
+ $stack = $this->createStack(function () use ($envelope) {
+ throw new HandlerFailedException($envelope, [new \LogicException('Specific unpacked exception.')]);
+ });
+
+ $this->assertSame(
+ $envelope,
+ $this->middleware->handle($envelope, $stack)
+ );
+ }
+
+ private function createEnvelope(): Envelope
+ {
+ return new Envelope(new \stdClass());
+ }
+
+ private function createStack(callable $handler = null): StackMiddleware
+ {
+ if (!$handler) {
+ return new StackMiddleware([]);
+ }
+
+ $middleware = new class($handler) implements MiddlewareInterface {
+ /**
+ * @param callable $handler
+ */
+ public function __construct(private $handler)
+ {
+ }
+
+ public function handle(Envelope $envelope, StackInterface $stack): Envelope
+ {
+ $handler = $this->handler;
+ $handler();
+
+ return $envelope;
+ }
+ };
+
+ return new StackMiddleware($middleware);
+ }
+}
diff --git a/tests/bootstrap.php b/tests/bootstrap.php
new file mode 100644
index 0000000..0a418b9
--- /dev/null
+++ b/tests/bootstrap.php
@@ -0,0 +1,21 @@
+bootEnv(__DIR__ . '/Application/.env');
+
+if (\class_exists(\Locale::class)) {
+ \Locale::setDefault('en');
+}
diff --git a/tests/generator-config.php b/tests/generator-config.php
new file mode 100644
index 0000000..192961a
--- /dev/null
+++ b/tests/generator-config.php
@@ -0,0 +1,11 @@
+hooks[] = 'vendor/bin/rector process %s';
+$config->hooks[] = 'vendor/bin/php-cs-fixer fix %s';
+
+return $config;
diff --git a/tests/phpstan/console-application.php b/tests/phpstan/console-application.php
new file mode 100644
index 0000000..eb14451
--- /dev/null
+++ b/tests/phpstan/console-application.php
@@ -0,0 +1,13 @@
+boot();
+
+return new Application($kernel);
diff --git a/tests/phpstan/object-manager.php b/tests/phpstan/object-manager.php
new file mode 100644
index 0000000..24e0e21
--- /dev/null
+++ b/tests/phpstan/object-manager.php
@@ -0,0 +1,33 @@
+boot();
+
+/** @var ContainerInterface $container */
+$container = $kernel->getContainer();
+
+/** @var EntityManager $objectManager */
+$objectManager = $container->get('doctrine')->getManager();
+
+// remove ResolveTargetEntityListener from returned EntityManager to not resolve SuluPersistenceBundle classes
+// this is a workaround for the following phpstan issue: /~https://github.com/phpstan/phpstan-doctrine/issues/98
+$resolveTargetEntityListener = \current(\array_filter(
+ $objectManager->getEventManager()->getListeners('loadClassMetadata'),
+ static fn ($listener) => $listener instanceof ResolveTargetEntityListener
+));
+
+if ($resolveTargetEntityListener) {
+ $objectManager->getEventManager()->removeEventListener([Events::loadClassMetadata], $resolveTargetEntityListener);
+}
+
+return $objectManager;
diff --git a/translations/.gitkeep b/translations/.gitkeep
new file mode 100644
index 0000000..e69de29