diff --git a/composer.json b/composer.json
index 93d1acf3..a5cd5923 100644
--- a/composer.json
+++ b/composer.json
@@ -44,6 +44,7 @@
"symfony/http-kernel": "^5.4 || ^6.0 || ^7.0",
"symfony/mailer": "^5.4 || ^6.0 || ^7.0",
"symfony/mime": "^5.4 || ^6.0 || ^7.0",
+ "symfony/routing": "^5.4 || ^6.0 || ^7.0",
"symfony/security-core": "^5.4 || ^6.0 || ^7.0",
"symfony/service-contracts": "^1.1 || ^2.0 || ^3.0",
"symfony/translation-contracts": "^2.0 || ^3.0",
diff --git a/config/listeners.php b/config/listeners.php
index d09a0fdb..ec611d56 100644
--- a/config/listeners.php
+++ b/config/listeners.php
@@ -8,6 +8,7 @@
use Codefog\HasteBundle\Formatter;
use Symfony\Contracts\Translation\TranslatorInterface;
use Terminal42\NotificationCenterBundle\Backend\AutoSuggester;
+use Terminal42\NotificationCenterBundle\BulkyItem\BulkyItemStorage;
use Terminal42\NotificationCenterBundle\Config\ConfigLoader;
use Terminal42\NotificationCenterBundle\EventListener\AdminEmailTokenListener;
use Terminal42\NotificationCenterBundle\EventListener\Backend\BackendMenuListener;
@@ -17,6 +18,7 @@
use Terminal42\NotificationCenterBundle\EventListener\Backend\DataContainer\MessageListener;
use Terminal42\NotificationCenterBundle\EventListener\Backend\DataContainer\ModuleListener;
use Terminal42\NotificationCenterBundle\EventListener\Backend\DataContainer\NotificationListener;
+use Terminal42\NotificationCenterBundle\EventListener\BulkyItemsTokenListener;
use Terminal42\NotificationCenterBundle\EventListener\DbafsMetadataListener;
use Terminal42\NotificationCenterBundle\EventListener\DisableDeliveryListener;
use Terminal42\NotificationCenterBundle\EventListener\DoctrineSchemaListener;
@@ -100,6 +102,14 @@
])
;
+ $services->set(BulkyItemsTokenListener::class)
+ ->args([
+ service(BulkyItemStorage::class),
+ service(TokenDefinitionFactoryInterface::class),
+ service('twig'),
+ ])
+ ;
+
$services->set(DisableDeliveryListener::class);
$services->set(NotificationTypeForModuleListener::class);
diff --git a/config/services.php b/config/services.php
index 9390a619..af0e39f6 100644
--- a/config/services.php
+++ b/config/services.php
@@ -10,6 +10,7 @@
use Terminal42\NotificationCenterBundle\BulkyItem\BulkyItemStorage;
use Terminal42\NotificationCenterBundle\BulkyItem\FileItemFactory;
use Terminal42\NotificationCenterBundle\Config\ConfigLoader;
+use Terminal42\NotificationCenterBundle\Controller\DownloadBulkyItemController;
use Terminal42\NotificationCenterBundle\Cron\PruneBulkyItemStorageCron;
use Terminal42\NotificationCenterBundle\DependencyInjection\Terminal42NotificationCenterExtension;
use Terminal42\NotificationCenterBundle\Gateway\GatewayRegistry;
@@ -18,6 +19,8 @@
use Terminal42\NotificationCenterBundle\Token\Definition\Factory\ChainTokenDefinitionFactory;
use Terminal42\NotificationCenterBundle\Token\Definition\Factory\CoreTokenDefinitionFactory;
use Terminal42\NotificationCenterBundle\Token\Definition\Factory\TokenDefinitionFactoryInterface;
+use Terminal42\NotificationCenterBundle\Twig\NotificationCenterExtension;
+use Terminal42\NotificationCenterBundle\Twig\NotificationCenterRuntime;
return static function (ContainerConfigurator $container): void {
$services = $container->services();
@@ -31,6 +34,14 @@
])
;
+ $services->set(DownloadBulkyItemController::class)
+ ->args([
+ service('uri_signer'),
+ service(BulkyItemStorage::class),
+ ])
+ ->public()
+ ;
+
$services->set(GatewayRegistry::class)
->args([
tagged_iterator(Terminal42NotificationCenterExtension::GATEWAY_TAG),
@@ -57,6 +68,8 @@
$services->set(BulkyItemStorage::class)
->args([
service('contao.filesystem.virtual.'.Terminal42NotificationCenterExtension::BULKY_ITEMS_VFS_NAME),
+ service('router'),
+ service('uri_signer'),
])
;
@@ -72,6 +85,13 @@
])
;
+ $services->set(NotificationCenterExtension::class);
+ $services->set(NotificationCenterRuntime::class)
+ ->args([
+ service(BulkyItemStorage::class),
+ ])
+ ;
+
$services->set(NotificationCenter::class)
->args([
service('database_connection'),
diff --git a/contao/templates/.twig-root b/contao/templates/.twig-root
new file mode 100644
index 00000000..e69de29b
diff --git a/contao/templates/notification_center/file_token.html.twig b/contao/templates/notification_center/file_token.html.twig
new file mode 100644
index 00000000..ab7b3b34
--- /dev/null
+++ b/contao/templates/notification_center/file_token.html.twig
@@ -0,0 +1,13 @@
+{% if format is same as 'html' %}
+
+{% endif %}
+
+{% if format is same as 'text' %}
+ {% for voucher, file in files %}
+ - [{{ file.name }} ({{ file.size|format_bytes }})]({{ notification_center_file_url(voucher) }})
+ {% endfor %}
+{% endif %}
\ No newline at end of file
diff --git a/src/BulkyItem/BulkyItemStorage.php b/src/BulkyItem/BulkyItemStorage.php
index f9401b5e..2e8323c3 100644
--- a/src/BulkyItem/BulkyItemStorage.php
+++ b/src/BulkyItem/BulkyItemStorage.php
@@ -7,12 +7,19 @@
use Contao\CoreBundle\Filesystem\ExtraMetadata;
use Contao\CoreBundle\Filesystem\VirtualFilesystemException;
use Contao\CoreBundle\Filesystem\VirtualFilesystemInterface;
+use Symfony\Component\HttpFoundation\UriSigner;
+use Symfony\Component\Routing\Generator\UrlGeneratorInterface;
+use Symfony\Component\Routing\RouterInterface;
use Symfony\Component\Uid\Uuid;
class BulkyItemStorage
{
+ public const VOUCHER_REGEX = '^\d{8}/[a-f0-9]{8}-[a-f0-9]{4}-4[a-f0-9]{3}-[89ab][a-f0-9]{3}-[a-f0-9]{12}$';
+
public function __construct(
private readonly VirtualFilesystemInterface $filesystem,
+ private readonly RouterInterface $router,
+ private readonly UriSigner $uriSigner,
private readonly int $retentionPeriodInDays = 7,
) {
}
@@ -95,12 +102,16 @@ public function prune(): void
}
}
- public static function validateVoucherFormat(string $voucher): bool
+ public function generatePublicUri(string $voucher, int|null $ttl = null): string
{
- if (!preg_match('@^\d{8}/@', $voucher)) {
- return false;
- }
+ return $this->uriSigner->sign(
+ $this->router->generate('nc_bulky_item_download', ['voucher' => $voucher], UrlGeneratorInterface::ABSOLUTE_URL),
+ time() + $ttl,
+ );
+ }
- return Uuid::isValid(substr($voucher, 9));
+ public static function validateVoucherFormat(string $voucher): bool
+ {
+ return 1 === preg_match('@'.self::VOUCHER_REGEX.'@', $voucher);
}
}
diff --git a/src/ContaoManager/Plugin.php b/src/ContaoManager/Plugin.php
index f23ea343..64257a63 100644
--- a/src/ContaoManager/Plugin.php
+++ b/src/ContaoManager/Plugin.php
@@ -8,9 +8,12 @@
use Contao\ManagerPlugin\Bundle\BundlePluginInterface;
use Contao\ManagerPlugin\Bundle\Config\BundleConfig;
use Contao\ManagerPlugin\Bundle\Parser\ParserInterface;
+use Contao\ManagerPlugin\Routing\RoutingPluginInterface;
+use Symfony\Component\Config\Loader\LoaderResolverInterface;
+use Symfony\Component\HttpKernel\KernelInterface;
use Terminal42\NotificationCenterBundle\Terminal42NotificationCenterBundle;
-class Plugin implements BundlePluginInterface
+class Plugin implements BundlePluginInterface, RoutingPluginInterface
{
public function getBundles(ParserInterface $parser): array
{
@@ -20,4 +23,12 @@ public function getBundles(ParserInterface $parser): array
->setLoadAfter([ContaoCoreBundle::class]),
];
}
+
+ public function getRouteCollection(LoaderResolverInterface $resolver, KernelInterface $kernel)
+ {
+ return $resolver
+ ->resolve(__DIR__.'/../Controller/DownloadBulkyItemController.php', 'attribute')
+ ->load(__DIR__.'/../Controller/DownloadBulkyItemController.php')
+ ;
+ }
}
diff --git a/src/Controller/DownloadBulkyItemController.php b/src/Controller/DownloadBulkyItemController.php
new file mode 100644
index 00000000..d038ef09
--- /dev/null
+++ b/src/Controller/DownloadBulkyItemController.php
@@ -0,0 +1,51 @@
+ BulkyItemStorage::VOUCHER_REGEX])]
+class DownloadBulkyItemController
+{
+ public function __construct(
+ private readonly UriSigner $uriSigner,
+ private readonly BulkyItemStorage $bulkyItemStorage,
+ ) {
+ }
+
+ public function __invoke(Request $request, string $voucher): Response
+ {
+ if (!$this->uriSigner->checkRequest($request)) {
+ throw new NotFoundHttpException();
+ }
+
+ if (!$bulkyItem = $this->bulkyItemStorage->retrieve($voucher)) {
+ throw new NotFoundHttpException();
+ }
+
+ $stream = $bulkyItem->getContents();
+
+ $response = new StreamedResponse(
+ static function () use ($stream): void {
+ while (!feof($stream)) {
+ echo fread($stream, 8192); // Read in chunks of 8 KB
+ flush();
+ }
+ fclose($stream);
+ },
+ );
+
+ $response->headers->set('Content-Type', 'application/octet-stream');
+ $response->headers->set('Cache-Control', 'no-cache, no-store, must-revalidate');
+
+ return $response;
+ }
+}
diff --git a/src/DependencyInjection/Terminal42NotificationCenterExtension.php b/src/DependencyInjection/Terminal42NotificationCenterExtension.php
index 464f03ba..951b1430 100644
--- a/src/DependencyInjection/Terminal42NotificationCenterExtension.php
+++ b/src/DependencyInjection/Terminal42NotificationCenterExtension.php
@@ -61,7 +61,7 @@ public function load(array $configs, ContainerBuilder $container): void
}
$container->findDefinition(BulkyItemStorage::class)
- ->setArgument(1, $config['bulky_items_storage']['retention_period'])
+ ->setArgument(3, $config['bulky_items_storage']['retention_period'])
;
}
diff --git a/src/EventListener/BulkyItemsTokenListener.php b/src/EventListener/BulkyItemsTokenListener.php
new file mode 100644
index 00000000..4ff252ea
--- /dev/null
+++ b/src/EventListener/BulkyItemsTokenListener.php
@@ -0,0 +1,105 @@
+addTokenDefinition($this->tokenDefinitionFactory->create(AnythingTokenDefinition::class, 'file_item_html_*', 'file_item_html_*'))
+ ->addTokenDefinition($this->tokenDefinitionFactory->create(AnythingTokenDefinition::class, 'file_item_text_*', 'file_item_text_*'))
+ ;
+ }
+
+ #[AsEventListener]
+ public function onCreateParcel(CreateParcelEvent $event): void
+ {
+ if (!$event->getParcel()->hasStamp(TokenCollectionStamp::class) || !$event->getParcel()->getStamp(BulkyItemsStamp::class)) {
+ return;
+ }
+
+ $tokenCollection = $event->getParcel()->getStamp(TokenCollectionStamp::class)->tokenCollection;
+
+ foreach ($tokenCollection as $token) {
+ $items = $this->extractFileItems($token, $event->getParcel()->getStamp(BulkyItemsStamp::class));
+
+ if ([] === $items) {
+ continue;
+ }
+
+ $tokenCollection->addToken($this->createFileToken($event->getParcel(), $token, $items, 'html', HtmlTokenDefinition::class));
+ $tokenCollection->addToken($this->createFileToken($event->getParcel(), $token, $items, 'text', TextTokenDefinition::class));
+ }
+ }
+
+ /**
+ * @param array $items
+ */
+ private function createFileToken(Parcel $parcel, Token $token, array $items, string $format, string $tokenDefinitionClass): Token
+ {
+ $content = $this->twig->render('@Contao/notification_center/file_token.html.twig', [
+ 'files' => $items,
+ 'parcel' => $parcel,
+ 'format' => $format,
+ ]);
+
+ $tokenName = 'file_item_'.$format.'_'.$token->getName();
+
+ return $this->tokenDefinitionFactory->create($tokenDefinitionClass, $tokenName, $tokenName)
+ ->createToken($tokenName, $content)
+ ;
+ }
+
+ /**
+ * @return array
+ */
+ private function extractFileItems(Token $token, BulkyItemsStamp $bulkyItemsStamp): array
+ {
+ $possibleVouchers = StringUtil::trimsplit(',', $token->getParserValue());
+ $items = [];
+
+ foreach ($possibleVouchers as $possibleVoucher) {
+ // Shortcut: Not a possibly bulky item voucher anyway - continue
+ if (!BulkyItemStorage::validateVoucherFormat($possibleVoucher)) {
+ continue;
+ }
+
+ if (!$bulkyItemsStamp->has($possibleVoucher)) {
+ continue;
+ }
+
+ if ($item = $this->bulkyItemStorage->retrieve($possibleVoucher)) {
+ $items[$possibleVoucher] = $item;
+ }
+ }
+
+ return $items;
+ }
+}
diff --git a/src/Gateway/AbstractGateway.php b/src/Gateway/AbstractGateway.php
index 0f979f06..10c58adc 100644
--- a/src/Gateway/AbstractGateway.php
+++ b/src/Gateway/AbstractGateway.php
@@ -16,6 +16,7 @@
use Terminal42\NotificationCenterBundle\Parcel\Stamp\StampInterface;
use Terminal42\NotificationCenterBundle\Parcel\Stamp\TokenCollectionStamp;
use Terminal42\NotificationCenterBundle\Receipt\Receipt;
+use Terminal42\NotificationCenterBundle\Token\TokenCollection;
abstract class AbstractGateway implements GatewayInterface
{
@@ -80,20 +81,26 @@ abstract protected function doSealParcel(Parcel $parcel): Parcel;
abstract protected function doSendParcel(Parcel $parcel): Receipt;
- protected function replaceTokens(Parcel $parcel, string $value): string
+ /**
+ * You can provide an optional TokenCollection if you want to force a certain token collection. Otherwise, they
+ * will be taken from the TokenCollectionStamp.
+ */
+ protected function replaceTokens(Parcel $parcel, string $value, TokenCollection|null $tokenCollection = null): string
{
- if (!$parcel->hasStamp(TokenCollectionStamp::class)) {
+ if (!$simpleTokenParser = $this->getSimpleTokenParser()) {
return $value;
}
- if ($simpleTokenParser = $this->getSimpleTokenParser()) {
- return $simpleTokenParser->parse(
- $value,
- $parcel->getStamp(TokenCollectionStamp::class)->tokenCollection->forSimpleTokenParser(),
- );
+ $tokenCollection ??= $parcel->getStamp(TokenCollectionStamp::class)?->tokenCollection;
+
+ if (!$tokenCollection instanceof TokenCollection) {
+ return $value;
}
- return $value;
+ return $simpleTokenParser->parse(
+ $value,
+ $tokenCollection->forSimpleTokenParser(),
+ );
}
protected function replaceInsertTags(string $value): string
@@ -105,9 +112,13 @@ protected function replaceInsertTags(string $value): string
return $value;
}
- protected function replaceTokensAndInsertTags(Parcel $parcel, string $value): string
+ /**
+ * You can provide an optional TokenCollection if you want to force a certain token collection. Otherwise, they
+ * will be taken from the TokenCollectionStamp.
+ */
+ protected function replaceTokensAndInsertTags(Parcel $parcel, string $value, TokenCollection|null $tokenCollection = null): string
{
- return $this->replaceInsertTags($this->replaceTokens($parcel, $value));
+ return $this->replaceInsertTags($this->replaceTokens($parcel, $value, $tokenCollection));
}
protected function isBulkyItemVoucher(Parcel $parcel, string $voucher): bool
diff --git a/src/Twig/NotificationCenterExtension.php b/src/Twig/NotificationCenterExtension.php
new file mode 100644
index 00000000..9382e89f
--- /dev/null
+++ b/src/Twig/NotificationCenterExtension.php
@@ -0,0 +1,18 @@
+bulkyItemStorage->generatePublicUri($voucher, $ttl);
+ }
+}
diff --git a/tests/BulkyItem/BulkItemStorageTest.php b/tests/BulkyItem/BulkItemStorageTest.php
index 131314df..2e1db797 100644
--- a/tests/BulkyItem/BulkItemStorageTest.php
+++ b/tests/BulkyItem/BulkItemStorageTest.php
@@ -9,6 +9,8 @@
use Contao\CoreBundle\Filesystem\FilesystemItemIterator;
use Contao\CoreBundle\Filesystem\VirtualFilesystemInterface;
use PHPUnit\Framework\TestCase;
+use Symfony\Component\HttpFoundation\UriSigner;
+use Symfony\Component\Routing\RouterInterface;
use Terminal42\NotificationCenterBundle\BulkyItem\BulkyItemStorage;
use Terminal42\NotificationCenterBundle\BulkyItem\FileItem;
@@ -72,7 +74,7 @@ function (ExtraMetadata $meta) {
)
;
- $storage = new BulkyItemStorage($vfs);
+ $storage = new BulkyItemStorage($vfs, $this->createMock(RouterInterface::class), $this->createMock(UriSigner::class));
$voucher = $storage->store($this->createFileItem());
$this->assertTrue(BulkyItemStorage::validateVoucherFormat($voucher));
@@ -88,7 +90,7 @@ public function testHas(): void
->willReturn(true)
;
- $storage = new BulkyItemStorage($vfs);
+ $storage = new BulkyItemStorage($vfs, $this->createMock(RouterInterface::class), $this->createMock(UriSigner::class));
$this->assertTrue($storage->has('a10aed4d-abe1-498f-adfc-b2e54fbbcbde'));
}
@@ -118,7 +120,7 @@ public function testRetrieve(): void
->willReturn($this->createStream())
;
- $storage = new BulkyItemStorage($vfs);
+ $storage = new BulkyItemStorage($vfs, $this->createMock(RouterInterface::class), $this->createMock(UriSigner::class));
$item = $storage->retrieve('a10aed4d-abe1-498f-adfc-b2e54fbbcbde');
$this->assertInstanceOf(FileItem::class, $item);
@@ -154,7 +156,7 @@ public function testPrune(): void
->with('20220101')
;
- $storage = new BulkyItemStorage($vfs);
+ $storage = new BulkyItemStorage($vfs, $this->createMock(RouterInterface::class), $this->createMock(UriSigner::class));
$storage->prune();
}
diff --git a/tests/EventListener/BulkyItemsTokenListenerTest.php b/tests/EventListener/BulkyItemsTokenListenerTest.php
new file mode 100644
index 00000000..87c54dab
--- /dev/null
+++ b/tests/EventListener/BulkyItemsTokenListenerTest.php
@@ -0,0 +1,115 @@
+createMock(TokenDefinitionFactoryInterface::class);
+ $tokenDefinitionFactory
+ ->expects($this->exactly(2))
+ ->method('create')
+ ->willReturnCallback(static fn (string $definitionClass, string $tokenName, string $translationKey) => new $definitionClass($tokenName, $translationKey))
+ ;
+
+ $event = new GetTokenDefinitionsForNotificationTypeEvent($this->createMock(NotificationTypeInterface::class));
+ $listener = new BulkyItemsTokenListener(
+ $this->createMock(BulkyItemStorage::class),
+ $tokenDefinitionFactory,
+ $this->createMock(Environment::class),
+ );
+
+ $listener->onGetTokenDefinitions($event);
+
+ $this->assertSame(['file_item_html_*', 'file_item_text_*'], array_keys($event->getTokenDefinitions()));
+ }
+
+ public function testOnCreateParcelSkipsIfStampsAreMissing(): void
+ {
+ $parcel = new Parcel(MessageConfig::fromArray([]));
+ $event = new CreateParcelEvent($parcel);
+
+ $listener = new BulkyItemsTokenListener(
+ $this->createMock(BulkyItemStorage::class),
+ $this->createMock(TokenDefinitionFactoryInterface::class),
+ $this->createMock(Environment::class),
+ );
+
+ $listener->onCreateParcel($event);
+
+ $this->addToAssertionCount(1); // Ensure no exceptions or errors
+ }
+
+ public function testOnCreateParcelProcessesTokens(): void
+ {
+ $bulkyItemStorage = $this->createMock(BulkyItemStorage::class);
+ $bulkyItemStorage
+ ->method('retrieve')
+ ->willReturn($this->createMock(BulkyItemInterface::class))
+ ;
+
+ $tokenDefinitionFactory = $this->createMock(TokenDefinitionFactoryInterface::class);
+ $tokenDefinitionFactory
+ ->method('create')
+ ->willReturnCallback(static fn (string $definitionClass, string $tokenName, string $translationKey) => new $definitionClass($tokenName, $translationKey))
+ ;
+
+ $twig = $this->createMock(Environment::class);
+ $twig
+ ->method('render')
+ ->willReturnCallback(
+ function (string $template, array $context) {
+ $this->assertSame('@Contao/notification_center/file_token.html.twig', $template);
+
+ return match ($context['format']) {
+ 'html' => 'rendered_html_content',
+ 'text' => 'rendered_text_content',
+ default => $this->fail('Invalid format!'),
+ };
+ },
+ )
+ ;
+
+ $tokenCollection = new TokenCollection();
+ $tokenCollection->addToken(new Token('form_upload', '20221228/a10aed4d-abe1-498f-adfc-b2e54fbbcbde', '20221228/a10aed4d-abe1-498f-adfc-b2e54fbbcbde'));
+
+ $parcel = new Parcel(MessageConfig::fromArray([]));
+ $parcel = $parcel->withStamp(new TokenCollectionStamp($tokenCollection));
+ $parcel = $parcel->withStamp(new BulkyItemsStamp(['20221228/a10aed4d-abe1-498f-adfc-b2e54fbbcbde']));
+ $event = new CreateParcelEvent($parcel);
+
+ $listener = new BulkyItemsTokenListener(
+ $bulkyItemStorage,
+ $tokenDefinitionFactory,
+ $twig,
+ );
+
+ $listener->onCreateParcel($event);
+
+ $tokenCollection = $event->getParcel()->getStamp(TokenCollectionStamp::class)?->tokenCollection;
+ $this->assertInstanceOf(TokenCollection::class, $tokenCollection);
+ $this->assertTrue($tokenCollection->has('file_item_html_form_upload'));
+ $this->assertTrue($tokenCollection->has('file_item_text_form_upload'));
+ $this->assertSame('rendered_html_content', $tokenCollection->getByName('file_item_html_form_upload')->getParserValue());
+ $this->assertSame('rendered_text_content', $tokenCollection->getByName('file_item_text_form_upload')->getParserValue());
+ }
+}
diff --git a/tests/Gateway/MailerGatewayTest.php b/tests/Gateway/MailerGatewayTest.php
index d450fd1f..991572e1 100644
--- a/tests/Gateway/MailerGatewayTest.php
+++ b/tests/Gateway/MailerGatewayTest.php
@@ -15,8 +15,10 @@
use League\Flysystem\InMemory\InMemoryFilesystemAdapter;
use Symfony\Component\DependencyInjection\Container;
use Symfony\Component\ExpressionLanguage\ExpressionLanguage;
+use Symfony\Component\HttpFoundation\UriSigner;
use Symfony\Component\Mailer\MailerInterface;
use Symfony\Component\Mime\Email;
+use Symfony\Component\Routing\RouterInterface;
use Terminal42\NotificationCenterBundle\BulkyItem\BulkyItemStorage;
use Terminal42\NotificationCenterBundle\Config\LanguageConfig;
use Terminal42\NotificationCenterBundle\Config\MessageConfig;
@@ -88,7 +90,7 @@ static function (Email $email) use ($parsedTemplateHtml, $expectedAttachmentsCon
$mailer,
);
$container = new Container();
- $container->set(AbstractGateway::SERVICE_NAME_BULKY_ITEM_STORAGE, new BulkyItemStorage($vfsCollection->get('bulky_item')));
+ $container->set(AbstractGateway::SERVICE_NAME_BULKY_ITEM_STORAGE, new BulkyItemStorage($vfsCollection->get('bulky_item'), $this->createMock(RouterInterface::class), $this->createMock(UriSigner::class)));
$container->set(AbstractGateway::SERVICE_NAME_SIMPLE_TOKEN_PARSER, new SimpleTokenParser(new ExpressionLanguage()));
$gateway->setContainer($container);