diff --git a/.editorconfig b/.editorconfig
index 0b666c6..779f99a 100644
--- a/.editorconfig
+++ b/.editorconfig
@@ -1,90 +1,12 @@
-# EditorConfig helps developers define and maintain consistent
-# coding styles between different editors and IDEs
-# editorconfig.org
-
 root = true
 
 [*]
-# Change these settings to your own preference
 indent_style = space
 indent_size = 4
-
-# We recommend you to keep these unchanged
 end_of_line = lf
 charset = utf-8
 trim_trailing_whitespace = true
 insert_final_newline = true
 
-[*.feature]
-indent_style = space
-indent_size = 2
-
-[*.js]
-indent_style = space
-indent_size = 2
-
-[*.json]
-indent_style = space
-indent_size = 2
-
 [*.md]
-indent_style = space
-indent_size = 4
-trim_trailing_whitespace = false
-
-[*.neon]
-indent_style = tab
-indent_size = 4
-
-[*.php]
-indent_style = space
-indent_size = 4
-
-[*.sh]
-indent_style = tab
-indent_size = 4
-
-[*.{yaml,yml}]
-indent_style = space
-indent_size = 4
 trim_trailing_whitespace = false
-
-[.babelrc]
-indent_style = space
-indent_size = 2
-
-[.gitmodules]
-indent_style = tab
-indent_size = 4
-
-[.php_cs{,.dist}]
-indent_style = space
-indent_size = 4
-
-[composer.json]
-indent_style = space
-indent_size = 4
-
-[docker-compose{,.override}.{yaml,yml}]
-indent_style = space
-indent_size = 2
-
-[Dockerfile]
-indent_style = tab
-indent_size = 4
-
-[package.json]
-indent_style = space
-indent_size = 2
-
-[phpspec.yml{,.dist}]
-indent_style = space
-indent_size = 4
-
-[phpstan.neon]
-indent_style = tab
-indent_size = 4
-
-[phpunit.xml{,.dist}]
-indent_style = space
-indent_size = 4
\ No newline at end of file
diff --git a/.gitattributes b/.gitattributes
index f101a62..f3c6333 100644
--- a/.gitattributes
+++ b/.gitattributes
@@ -1,11 +1,13 @@
-/spec                       export-ignore
-/tests                      export-ignore
-/.editorconfig              export-ignore
-/.gitattributes             export-ignore
-/.gitignore                 export-ignore
-/.scrutinizer.yml           export-ignore
-/.travis.yml                export-ignore
-/easy-coding-standard.yml   export-ignore
-/phpspec.yml.dist           export-ignore
-/phpstan.neon               export-ignore
-/README.md                  export-ignore
+* text=auto
+
+/.github                        export-ignore
+/tests                          export-ignore
+/.editorconfig                  export-ignore
+/.gitattributes                 export-ignore
+/.gitignore                     export-ignore
+/composer-require-checker.json  export-ignore
+/ecs.php                        export-ignore
+/infection.json.dist            export-ignore
+/phpunit.xml.dist               export-ignore
+/psalm.xml                      export-ignore
+/README.md                      export-ignore
diff --git a/.github/dependabot.yml b/.github/dependabot.yml
new file mode 100644
index 0000000..f479bb7
--- /dev/null
+++ b/.github/dependabot.yml
@@ -0,0 +1,14 @@
+# https://docs.github.com/en/github/administering-a-repository/configuration-options-for-dependency-updates
+
+version: 2
+
+updates:
+    - commit-message:
+          include: "scope"
+          prefix: "composer"
+      directory: "/"
+      open-pull-requests-limit: 1
+      package-ecosystem: "composer"
+      schedule:
+          interval: "weekly"
+      versioning-strategy: "widen"
diff --git a/.github/workflows/build.yaml b/.github/workflows/build.yaml
index 165cd30..d237b44 100644
--- a/.github/workflows/build.yaml
+++ b/.github/workflows/build.yaml
@@ -1,42 +1,225 @@
-name: build
+name: "build"
 on:
-    push: ~
+    push:
+        branches:
+            - "3.x"
     pull_request: ~
     workflow_dispatch: ~
 
 jobs:
-    checks:
-        name: 'PHP ${{ matrix.php-versions }} with composer args: ${{ matrix.composer-args }}'
-        runs-on: ubuntu-latest
+    coding-standards:
+        name: "Coding Standards"
+
+        runs-on: "ubuntu-latest"
+
+        strategy:
+            matrix:
+                php-version:
+                    - "7.4"
+
+                dependencies:
+                    - "highest"
+
+        steps:
+            -   name: "Checkout"
+                uses: "actions/checkout@v3"
+
+            -   name: "Setup PHP, with composer and extensions"
+                uses: "shivammathur/setup-php@v2"
+                with:
+                    php-version: "${{ matrix.php-version }}"
+                    extensions: "${{ env.PHP_EXTENSIONS }}"
+                    coverage: "none"
+
+            -   name: "Install composer dependencies"
+                uses: "ramsey/composer-install@v2"
+                with:
+                    dependency-versions: "${{ matrix.dependencies }}"
+
+            -   name: "Validate composer"
+                run: "composer validate --strict"
+
+            -   name: "Check composer normalized"
+                run: "composer normalize --dry-run"
+
+            -   name: "Check style"
+                run: "composer check-style"
+
+    dependency-analysis:
+        name: "Dependency Analysis"
+
+        runs-on: "ubuntu-latest"
+
+        strategy:
+            matrix:
+                php-version:
+                    - "7.4"
+                    - "8.0"
+                    - "8.1"
+
+                dependencies:
+                    - "highest"
+
+                symfony:
+                    - "^5.4"
+                    - "^6.0"
+
+                exclude:
+                    -   php-version: "7.4"
+                        symfony: "^6.0"
+
+        steps:
+            -   name: "Checkout"
+                uses: "actions/checkout@v3"
+
+            -   name: "Setup PHP, with composer and extensions"
+                uses: "shivammathur/setup-php@v2"
+                with:
+                    coverage: "none"
+                    extensions: "${{ env.PHP_EXTENSIONS }}"
+                    php-version: "${{ matrix.php-version }}"
+                    tools: "composer-require-checker, composer-unused, flex"
+
+            -   name: "Install composer dependencies"
+                uses: "ramsey/composer-install@v2"
+                with:
+                    dependency-versions: "${{ matrix.dependencies }}"
+                env:
+                    SYMFONY_REQUIRE: "${{ matrix.symfony }}"
+
+            -   name: "Run maglnet/composer-require-checker"
+                run: "composer-require-checker check"
+
+            -   name: "Run composer-unused/composer-unused"
+                run: "composer-unused"
+
+    static-code-analysis:
+        name: "Static Code Analysis"
+
+        runs-on: "ubuntu-latest"
+
+        strategy:
+            matrix:
+                php-version:
+                    - "7.4"
+                    - "8.0"
+                    - "8.1"
+
+                dependencies:
+                    - "highest"
+
+                symfony:
+                    - "^5.4"
+                    - "^6.0"
+
+                exclude:
+                    -   php-version: "7.4"
+                        symfony: "^6.0"
+
+        steps:
+            -   name: "Checkout"
+                uses: "actions/checkout@v3"
+
+            -   name: "Setup PHP, with composer and extensions"
+                uses: "shivammathur/setup-php@v2"
+                with:
+                    php-version: "${{ matrix.php-version }}"
+                    extensions: "${{ env.PHP_EXTENSIONS }}"
+                    coverage: "none"
+                    tools: "flex"
+
+            -   name: "Install composer dependencies"
+                uses: "ramsey/composer-install@v2"
+                with:
+                    dependency-versions: "${{ matrix.dependencies }}"
+                env:
+                    SYMFONY_REQUIRE: "${{ matrix.symfony }}"
+
+            -   name: "Static analysis"
+                run: "vendor/bin/psalm --php-version=${{ matrix.php-version }}"
+
+    unit-tests:
+        name: "Unit tests"
+
+        runs-on: "ubuntu-latest"
+
+        strategy:
+            matrix:
+                php-version:
+                    - "7.4"
+                    - "8.0"
+                    - "8.1"
+
+                dependencies:
+                    - "lowest"
+                    - "highest"
+
+                symfony:
+                    - "^5.4"
+                    - "^6.0"
+
+                exclude:
+                    -   php-version: "7.4"
+                        symfony: "^6.0"
+
+        steps:
+            -   name: "Checkout"
+                uses: "actions/checkout@v3"
+
+            -   name: "Setup PHP, with composer and extensions"
+                uses: "shivammathur/setup-php@v2"
+                with:
+                    php-version: "${{ matrix.php-version }}"
+                    extensions: "${{ env.PHP_EXTENSIONS }}"
+                    coverage: "none"
+                    tools: "flex"
+
+            -   name: "Install composer dependencies"
+                uses: "ramsey/composer-install@v2"
+                with:
+                    dependency-versions: "${{ matrix.dependencies }}"
+                env:
+                    SYMFONY_REQUIRE: "${{ matrix.symfony }}"
+
+            -   name: "Run phpunit"
+                run: "composer phpunit"
+
+    code-coverage:
+        name: "Code Coverage"
+
+        runs-on: "ubuntu-latest"
+
         strategy:
             matrix:
-                php-versions: ['7.4', '8.0', '8.1']
-                composer-args: ['--prefer-lowest --prefer-stable', '']
+                php-version:
+                    - "8.1"
+
+                dependencies:
+                    - "highest"
+
         steps:
-            -   name: Checkout
-                uses: actions/checkout@v1
-            -   name: Setup PHP, with composer and extensions
-                uses: shivammathur/setup-php@v2
-                with:
-                    php-version: ${{ matrix.php-versions }}
-                    extensions: intl
-                    tools: composer-require-checker, composer-unused
-                    coverage: none
-            -   name: Install Composer dependencies
-                run: composer update --no-progress --no-suggest --prefer-dist --no-interaction ${{ matrix.composer-args }}
-            -   name: Validate composer
-                run: composer validate --strict
-            -   name: Check composer normalized
-                run: composer normalize --dry-run
-            -   name: Check style
-                run: composer check-style
-            -   name: Static analysis
-                run: composer analyse
-            -   name: Run phpunit
-                run: composer phpunit
-            -   name: Composer require checker
-                if: matrix.php-versions == '7.4' && matrix.composer-args == ''
-                run: composer-require-checker
-            -   name: Composer unused checker
-                if: matrix.php-versions == '7.4' && matrix.composer-args == ''
-                run: composer-unused
+            -   name: "Checkout"
+                uses: "actions/checkout@v3"
+
+            -   name: "Setup PHP, with composer and extensions"
+                uses: "shivammathur/setup-php@v2"
+                with:
+                    coverage: "pcov"
+                    extensions: "${{ env.PHP_EXTENSIONS }}"
+                    php-version: "${{ matrix.php-version }}"
+
+            -   name: "Set up problem matchers for phpunit/phpunit"
+                run: "echo \"::add-matcher::${{ runner.tool_cache }}/phpunit.json\""
+
+            -   name: "Install composer dependencies"
+                uses: "ramsey/composer-install@v2"
+                with:
+                    dependency-versions: "${{ matrix.dependencies }}"
+
+            -   name: "Collect code coverage with pcov and phpunit/phpunit"
+                run: "vendor/bin/phpunit --coverage-clover=.build/logs/clover.xml"
+
+            -   name: "Send code coverage report to Codecov.io"
+                env:
+                    CODECOV_TOKEN: "${{ secrets.CODECOV_TOKEN }}"
+                run: "bash <(curl -s https://codecov.io/bash)"
diff --git a/LICENSE b/LICENSE
index e987d35..dc9924d 100644
--- a/LICENSE
+++ b/LICENSE
@@ -1,6 +1,6 @@
 MIT License
 
-Copyright (c) 2020 Setono
+Copyright (c) 2022 Setono
 
 Permission is hereby granted, free of charge, to any person obtaining a copy
 of this software and associated documentation files (the "Software"), to deal
diff --git a/README.md b/README.md
index 3222ad0..02265b8 100644
--- a/README.md
+++ b/README.md
@@ -111,4 +111,3 @@ it will be tagged automatically.
 
 [link-packagist]: https://packagist.org/packages/setono/tag-bag-bundle
 [link-github-actions]: https://github.com/Setono/TagBagBundle/actions
-[link-code-coverage]: https://scrutinizer-ci.com/g/Setono/TagBagBundle/code-structure
diff --git a/composer.json b/composer.json
index 2f419cf..15a6f97 100644
--- a/composer.json
+++ b/composer.json
@@ -11,26 +11,25 @@
     ],
     "require": {
         "php": ">=7.4",
-        "setono/tag-bag": "^1.4",
-        "symfony/config": "^4.4 || ^5.4 || ^6.0",
-        "symfony/dependency-injection": "^4.4 || ^5.4 || ^6.0",
-        "symfony/event-dispatcher": "^4.4 || ^5.4 || ^6.0",
-        "symfony/http-foundation": "^4.4 || ^5.4 || ^6.0",
-        "symfony/http-kernel": "^4.4 || ^5.4 || ^6.0",
+        "setono/tag-bag": "2.x-dev",
+        "symfony/config": "^5.4 || ^6.0",
+        "symfony/dependency-injection": "^5.4 || ^6.0",
+        "symfony/event-dispatcher": "^5.4 || ^6.0",
+        "symfony/http-foundation": "^5.4 || ^6.0",
+        "symfony/http-kernel": "^5.4 || ^6.0",
         "twig/twig": "^2.0 || ^3.0",
         "webmozart/assert": "^1.11"
     },
     "require-dev": {
-        "matthiasnoback/symfony-dependency-injection-test": "^4.2",
-        "nyholm/symfony-bundle-test": "^1.7",
+        "matthiasnoback/symfony-dependency-injection-test": "^4.3",
+        "nyholm/symfony-bundle-test": "^2.0",
+        "phpspec/prophecy-phpunit": "^2.0",
         "phpunit/phpunit": "^9.5",
+        "psalm/plugin-phpunit": "^0.17.0",
+        "psalm/plugin-symfony": "^3.1",
         "roave/security-advisories": "dev-latest",
         "setono/code-quality-pack": "^2.2",
-        "setono/php-templates-bundle": "^1.0",
-        "setono/tag-bag-gtag": "^1.0",
-        "setono/tag-bag-php-templates": "^1.2",
-        "setono/tag-bag-twig": "^1.1",
-        "symfony/twig-bundle": "^4.4 || ^5.4 || ^6.0"
+        "symfony/twig-bundle": "^5.4 || ^6.0"
     },
     "prefer-stable": true,
     "autoload": {
@@ -45,16 +44,15 @@
     },
     "config": {
         "allow-plugins": {
-            "phpstan/extension-installer": true,
             "ergebnis/composer-normalize": true,
             "dealerdirect/phpcodesniffer-composer-installer": false
         },
         "sort-packages": true
     },
     "scripts": {
-        "analyse": "phpstan analyse -c phpstan.neon -l max src/",
-        "check-style": "ecs check src/ tests/",
-        "fix-style": "ecs check --fix src/ tests/",
+        "analyse": "psalm",
+        "check-style": "ecs check",
+        "fix-style": "ecs check --fix",
         "phpunit": "phpunit"
     }
 }
diff --git a/ecs.php b/ecs.php
index 2c950d2..716ed1f 100644
--- a/ecs.php
+++ b/ecs.php
@@ -3,7 +3,11 @@
 declare(strict_types=1);
 
 use Symfony\Component\DependencyInjection\Loader\Configurator\ContainerConfigurator;
+use Symplify\EasyCodingStandard\ValueObject\Option;
 
-return static function (ContainerConfigurator $containerConfigurator): void {
-    $containerConfigurator->import(__DIR__ . '/vendor/setono/coding-standard/easy-coding-standard.php');
+return static function (ContainerConfigurator $config): void {
+    $config->import('vendor/sylius-labs/coding-standard/ecs.php');
+    $config->parameters()->set(Option::PATHS, [
+        'src', 'tests'
+    ]);
 };
diff --git a/phpspec.yml.dist b/phpspec.yml.dist
deleted file mode 100644
index e894041..0000000
--- a/phpspec.yml.dist
+++ /dev/null
@@ -1,4 +0,0 @@
-suites:
-    main:
-        namespace: Setono\TagBagBundle
-        psr4_prefix: Setono\TagBagBundle
diff --git a/phpstan.neon b/phpstan.neon
deleted file mode 100644
index 43ce7e5..0000000
--- a/phpstan.neon
+++ /dev/null
@@ -1,3 +0,0 @@
-parameters:
-	reportUnmatchedIgnoredErrors: true
-	checkMissingIterableValueType: false
diff --git a/phpunit.xml.dist b/phpunit.xml.dist
index e83c827..b3b2c8c 100644
--- a/phpunit.xml.dist
+++ b/phpunit.xml.dist
@@ -12,7 +12,7 @@
         </report>
     </coverage>
     <testsuites>
-        <testsuite name="SetonoTagBagBundle Test Suite">
+        <testsuite name="SetonoMetaConversionsApiBundle Test Suite">
             <directory>tests</directory>
         </testsuite>
     </testsuites>
diff --git a/psalm.xml b/psalm.xml
new file mode 100644
index 0000000..9fb9885
--- /dev/null
+++ b/psalm.xml
@@ -0,0 +1,20 @@
+<?xml version="1.0"?>
+<psalm
+    xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
+    xmlns="https://getpsalm.org/schema/config"
+    xsi:schemaLocation="https://getpsalm.org/schema/config vendor/vimeo/psalm/config.xsd"
+    errorLevel="1"
+    phpVersion="8.1"
+>
+    <projectFiles>
+        <directory name="src"/>
+        <directory name="tests"/>
+        <ignoreFiles>
+            <directory name="vendor"/>
+        </ignoreFiles>
+    </projectFiles>
+    <plugins>
+        <pluginClass class="Psalm\SymfonyPsalmPlugin\Plugin"/>
+        <pluginClass class="Psalm\PhpUnitPlugin\Plugin"/>
+    </plugins>
+</psalm>
diff --git a/src/DependencyInjection/Compiler/AddPathsToPhpTemplatesEnginePass.php b/src/DependencyInjection/Compiler/AddPathsToPhpTemplatesEnginePass.php
deleted file mode 100644
index c66971b..0000000
--- a/src/DependencyInjection/Compiler/AddPathsToPhpTemplatesEnginePass.php
+++ /dev/null
@@ -1,48 +0,0 @@
-<?php
-
-declare(strict_types=1);
-
-namespace Setono\TagBagBundle\DependencyInjection\Compiler;
-
-use InvalidArgumentException;
-use ReflectionClass;
-use function Safe\sprintf;
-use Symfony\Component\DependencyInjection\Compiler\CompilerPassInterface;
-use Symfony\Component\DependencyInjection\ContainerBuilder;
-
-final class AddPathsToPhpTemplatesEnginePass implements CompilerPassInterface
-{
-    public function process(ContainerBuilder $container): void
-    {
-        if (!$container->hasDefinition('setono_php_templates.engine.default')) {
-            return;
-        }
-
-        self::addGtagPaths($container);
-    }
-
-    private static function addGtagPaths(ContainerBuilder $container): void
-    {
-        $interface = 'Setono\TagBag\Tag\GtagInterface';
-
-        if (!interface_exists($interface)) {
-            return;
-        }
-
-        $filename = (new ReflectionClass($interface))->getFileName();
-        if (false === $filename) {
-            throw new InvalidArgumentException(sprintf(
-                'The filename of interface %s could not be deduced', $interface
-            ));
-        }
-
-        $path = dirname($filename, 2) . '/templates';
-
-        if (!is_dir($path)) {
-            throw new InvalidArgumentException(sprintf('The path "%s" is not a valid directory', $path));
-        }
-
-        $engine = $container->getDefinition('setono_php_templates.engine.default');
-        $engine->addMethodCall('addPath', [$path]);
-    }
-}
diff --git a/src/DependencyInjection/Compiler/AddPathsToTwigPass.php b/src/DependencyInjection/Compiler/AddPathsToTwigPass.php
deleted file mode 100644
index 63a57bd..0000000
--- a/src/DependencyInjection/Compiler/AddPathsToTwigPass.php
+++ /dev/null
@@ -1,50 +0,0 @@
-<?php
-
-declare(strict_types=1);
-
-namespace Setono\TagBagBundle\DependencyInjection\Compiler;
-
-use InvalidArgumentException;
-use ReflectionClass;
-use function Safe\sprintf;
-use Symfony\Component\DependencyInjection\Compiler\CompilerPassInterface;
-use Symfony\Component\DependencyInjection\ContainerBuilder;
-use Symfony\Component\DependencyInjection\Definition;
-
-final class AddPathsToTwigPass implements CompilerPassInterface
-{
-    public function process(ContainerBuilder $container): void
-    {
-        if (!$container->hasDefinition('twig.loader.filesystem')) {
-            return;
-        }
-
-        $twigLoader = $container->getDefinition('twig.loader.filesystem');
-
-        self::addGtagPaths($twigLoader);
-    }
-
-    private static function addGtagPaths(Definition $twigLoader): void
-    {
-        $interface = 'Setono\TagBag\Tag\GtagInterface';
-
-        if (!interface_exists($interface)) {
-            return;
-        }
-
-        $filename = (new ReflectionClass($interface))->getFileName();
-        if (false === $filename) {
-            throw new InvalidArgumentException(sprintf(
-                'The filename of interface %s could not be deduced', $interface
-            ));
-        }
-
-        $path = dirname($filename, 2) . '/templates/SetonoTagBagGtag';
-
-        if (!is_dir($path)) {
-            throw new InvalidArgumentException(sprintf('The path "%s" is not a valid directory', $path));
-        }
-
-        $twigLoader->addMethodCall('addPath', [$path, 'SetonoTagBagGtag']);
-    }
-}
diff --git a/src/DependencyInjection/Compiler/RegisterRenderersPass.php b/src/DependencyInjection/Compiler/RegisterRenderersPass.php
index bc712f7..67d96af 100644
--- a/src/DependencyInjection/Compiler/RegisterRenderersPass.php
+++ b/src/DependencyInjection/Compiler/RegisterRenderersPass.php
@@ -5,25 +5,23 @@
 namespace Setono\TagBagBundle\DependencyInjection\Compiler;
 
 use Symfony\Component\DependencyInjection\Compiler\CompilerPassInterface;
+use Symfony\Component\DependencyInjection\Compiler\PriorityTaggedServiceTrait;
 use Symfony\Component\DependencyInjection\ContainerBuilder;
-use Symfony\Component\DependencyInjection\Reference;
 
 final class RegisterRenderersPass implements CompilerPassInterface
 {
+    use PriorityTaggedServiceTrait;
+
     public function process(ContainerBuilder $container): void
     {
         if (!$container->hasDefinition('setono_tag_bag.renderer.composite')) {
             return;
         }
 
-        $renderer = $container->getDefinition('setono_tag_bag.renderer.composite');
-
-        foreach ($container->findTaggedServiceIds('setono_tag_bag.renderer') as $id => $tags) {
-            foreach ($tags as $tag) {
-                $priority = $tag['priority'] ?? 0;
+        $composite = $container->getDefinition('setono_tag_bag.renderer.composite');
 
-                $renderer->addMethodCall('addRenderer', [new Reference($id), $priority]);
-            }
+        foreach ($this->findAndSortTaggedServices('setono_tag_bag.renderer', $container) as $service) {
+            $composite->addMethodCall('add', [$service]);
         }
     }
 }
diff --git a/src/DependencyInjection/Configuration.php b/src/DependencyInjection/Configuration.php
new file mode 100644
index 0000000..42726f8
--- /dev/null
+++ b/src/DependencyInjection/Configuration.php
@@ -0,0 +1,32 @@
+<?php
+
+declare(strict_types=1);
+
+namespace Setono\TagBagBundle\DependencyInjection;
+
+use Symfony\Component\Config\Definition\Builder\TreeBuilder;
+use Symfony\Component\Config\Definition\ConfigurationInterface;
+
+final class Configuration implements ConfigurationInterface
+{
+    public function getConfigTreeBuilder(): TreeBuilder
+    {
+        $treeBuilder = new TreeBuilder('setono_tag_bag');
+
+        $rootNode = $treeBuilder->getRootNode();
+
+        /** @psalm-suppress MixedMethodCall, PossiblyUndefinedMethod, PossiblyNullReference */
+        $rootNode
+            ->addDefaultsIfNotSet()
+            ->children()
+                ->arrayNode('renderer')
+                    ->addDefaultsIfNotSet()
+                    ->children()
+                        ->booleanNode('twig')
+                            ->defaultTrue()
+                            ->info('Whether to enable the twig renderer')
+        ;
+
+        return $treeBuilder;
+    }
+}
diff --git a/src/DependencyInjection/SetonoTagBagExtension.php b/src/DependencyInjection/SetonoTagBagExtension.php
index 59f1c57..aaec558 100644
--- a/src/DependencyInjection/SetonoTagBagExtension.php
+++ b/src/DependencyInjection/SetonoTagBagExtension.php
@@ -12,23 +12,26 @@
 
 final class SetonoTagBagExtension extends Extension
 {
-    public function load(array $config, ContainerBuilder $container): void
+    public function load(array $configs, ContainerBuilder $container): void
     {
+        /**
+         * @psalm-suppress PossiblyNullArgument
+         *
+         * @var array{renderer: array{twig: bool}} $config
+         */
+        $config = $this->processConfiguration($this->getConfiguration([], $container), $configs);
+
         $container->registerForAutoconfiguration(RendererInterface::class)
             ->addTag('setono_tag_bag.renderer', [
-                'priority' => 256,
+                'priority' => 128,
             ])
         ;
 
         $loader = new XmlFileLoader($container, new FileLocator(__DIR__ . '/../Resources/config'));
         $loader->load('services.xml');
 
-        if (interface_exists('Setono\TagBag\Tag\TwigTagInterface')) {
-            $loader->load('services/integrations/tag_bag_twig.xml');
-        }
-
-        if (interface_exists('Setono\TagBag\Tag\PhpTemplatesTagInterface')) {
-            $loader->load('services/integrations/tag_bag_php_templates.xml');
+        if (true === $config['renderer']['twig']) {
+            $loader->load('services/integrations/renderer.xml');
         }
     }
 }
diff --git a/src/EventListener/RestoreTagBagSubscriber.php b/src/EventListener/RestoreTagBagSubscriber.php
index 1f867cf..d4325b6 100644
--- a/src/EventListener/RestoreTagBagSubscriber.php
+++ b/src/EventListener/RestoreTagBagSubscriber.php
@@ -11,8 +11,7 @@
 
 final class RestoreTagBagSubscriber implements EventSubscriberInterface
 {
-    /** @var TagBagInterface */
-    private $tagBag;
+    private TagBagInterface $tagBag;
 
     public function __construct(TagBagInterface $tagBag)
     {
@@ -22,18 +21,18 @@ public function __construct(TagBagInterface $tagBag)
     public static function getSubscribedEvents(): array
     {
         /*
-         * The priority needs to be lower than Symfony\Component\HttpKernel\EventListener\SessionListener
-         * which is 128, but still higher than 0 so that people can listen to the request event without
+         * The priority needs to be lower than Symfony\Bundle\SecurityBundle\EventListener\FirewallListener
+         * which is 8, but still higher than 0 so that people can listen to the request event without
          * worrying about priorities
          */
         return [
-            KernelEvents::REQUEST => ['onKernelRequest', 100],
+            KernelEvents::REQUEST => ['onKernelRequest', 7],
         ];
     }
 
     public function onKernelRequest(RequestEvent $event): void
     {
-        if (!$event->isMasterRequest()) {
+        if (!$event->isMainRequest()) {
             return;
         }
 
diff --git a/src/EventListener/StoreTagBagSubscriber.php b/src/EventListener/StoreTagBagSubscriber.php
index b17d7c6..5a41b48 100644
--- a/src/EventListener/StoreTagBagSubscriber.php
+++ b/src/EventListener/StoreTagBagSubscriber.php
@@ -11,8 +11,7 @@
 
 final class StoreTagBagSubscriber implements EventSubscriberInterface
 {
-    /** @var TagBagInterface */
-    private $tagBag;
+    private TagBagInterface $tagBag;
 
     public function __construct(TagBagInterface $tagBag)
     {
@@ -33,7 +32,7 @@ public static function getSubscribedEvents(): array
 
     public function onKernelResponse(ResponseEvent $event): void
     {
-        if (!$event->isMasterRequest()) {
+        if (!$event->isMainRequest()) {
             return;
         }
 
diff --git a/src/Renderer/TwigRenderer.php b/src/Renderer/TwigRenderer.php
new file mode 100644
index 0000000..661cf55
--- /dev/null
+++ b/src/Renderer/TwigRenderer.php
@@ -0,0 +1,39 @@
+<?php
+
+declare(strict_types=1);
+
+namespace Setono\TagBagBundle\Renderer;
+
+use Setono\TagBag\Renderer\RendererInterface;
+use Setono\TagBag\Tag\TagInterface;
+use Setono\TagBag\Tag\TemplateTag;
+use Twig\Environment;
+use Webmozart\Assert\Assert;
+
+final class TwigRenderer implements RendererInterface
+{
+    private Environment $environment;
+
+    public function __construct(Environment $environment)
+    {
+        $this->environment = $environment;
+    }
+
+    /**
+     * @psalm-assert-if-true TemplateTag $tag
+     */
+    public function supports(TagInterface $tag): bool
+    {
+        return $tag instanceof TemplateTag && $tag->getTemplateType() === 'twig';
+    }
+
+    /**
+     * @param TagInterface|TemplateTag $tag
+     */
+    public function render(TagInterface $tag): string
+    {
+        Assert::true($this->supports($tag));
+
+        return $this->environment->render($tag->getTemplate(), $tag->getData());
+    }
+}
diff --git a/src/Resources/config/services/integrations/tag_bag_twig.xml b/src/Resources/config/services/integrations/renderer.xml
similarity index 81%
rename from src/Resources/config/services/integrations/tag_bag_twig.xml
rename to src/Resources/config/services/integrations/renderer.xml
index ee57b47..d14ee8a 100644
--- a/src/Resources/config/services/integrations/tag_bag_twig.xml
+++ b/src/Resources/config/services/integrations/renderer.xml
@@ -3,9 +3,10 @@
 <container xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns="http://symfony.com/schema/dic/services"
            xsi:schemaLocation="http://symfony.com/schema/dic/services http://symfony.com/schema/dic/services/services-1.0.xsd">
     <services>
-        <service id="setono_tag_bag.renderer.twig" class="Setono\TagBag\Renderer\TwigRenderer">
+        <service id="setono_tag_bag.renderer.twig" class="Setono\TagBagBundle\Renderer\TwigRenderer">
             <argument type="service" id="twig"/>
-            <tag name="setono_tag_bag.renderer" priority="256"/>
+
+            <tag name="setono_tag_bag.renderer" priority="64"/>
         </service>
     </services>
 </container>
diff --git a/src/Resources/config/services/integrations/tag_bag_php_templates.xml b/src/Resources/config/services/integrations/tag_bag_php_templates.xml
deleted file mode 100644
index 136ce82..0000000
--- a/src/Resources/config/services/integrations/tag_bag_php_templates.xml
+++ /dev/null
@@ -1,11 +0,0 @@
-<?xml version="1.0" encoding="UTF-8" ?>
-
-<container xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns="http://symfony.com/schema/dic/services"
-           xsi:schemaLocation="http://symfony.com/schema/dic/services http://symfony.com/schema/dic/services/services-1.0.xsd">
-    <services>
-        <service id="setono_tag_bag.renderer.php_templates" class="Setono\TagBag\Renderer\PhpTemplatesRenderer">
-            <argument type="service" id="setono_php_templates.engine.default"/>
-            <tag name="setono_tag_bag.renderer" priority="256"/>
-        </service>
-    </services>
-</container>
diff --git a/src/Resources/config/services/renderer.xml b/src/Resources/config/services/renderer.xml
index 0d0a9c4..17fe5ea 100644
--- a/src/Resources/config/services/renderer.xml
+++ b/src/Resources/config/services/renderer.xml
@@ -11,16 +11,16 @@
 
         <service id="setono_tag_bag.renderer.composite" class="Setono\TagBag\Renderer\CompositeRenderer"/>
 
-        <service id="setono_tag_bag.renderer.content" class="Setono\TagBag\Renderer\ContentRenderer">
-            <tag name="setono_tag_bag.renderer"/>
+        <service id="setono_tag_bag.renderer.content_aware" class="Setono\TagBag\Renderer\ContentAwareRenderer">
+            <tag name="setono_tag_bag.renderer" priority="-64"/>
         </service>
 
         <service id="setono_tag_bag.renderer.script" class="Setono\TagBag\Renderer\ScriptRenderer">
-            <tag name="setono_tag_bag.renderer" priority="128"/>
+            <tag name="setono_tag_bag.renderer"/>
         </service>
 
         <service id="setono_tag_bag.renderer.style" class="Setono\TagBag\Renderer\StyleRenderer">
-            <tag name="setono_tag_bag.renderer" priority="128"/>
+            <tag name="setono_tag_bag.renderer"/>
         </service>
     </services>
 </container>
diff --git a/src/Resources/config/services/storage.xml b/src/Resources/config/services/storage.xml
index ce5fabf..0dcf836 100644
--- a/src/Resources/config/services/storage.xml
+++ b/src/Resources/config/services/storage.xml
@@ -13,8 +13,9 @@
         will automatically be autowired
         -->
         <service id="setono_tag_bag.storage.default" alias="setono_tag_bag.storage.session"/>
+        
         <service id="setono_tag_bag.storage.session" class="Setono\TagBagBundle\Storage\SessionStorage">
-            <argument type="service" id="session"/>
+            <argument type="service" id="request_stack"/>
         </service>
     </services>
 </container>
diff --git a/src/Resources/config/services/tag_bag.xml b/src/Resources/config/services/tag_bag.xml
index 99c019b..4012f76 100644
--- a/src/Resources/config/services/tag_bag.xml
+++ b/src/Resources/config/services/tag_bag.xml
@@ -7,10 +7,16 @@
 
         <service id="setono_tag_bag.tag_bag" class="Setono\TagBag\TagBag">
             <argument type="service" id="setono_tag_bag.renderer.default"/>
-            <argument type="service" id="setono_tag_bag.storage.session"/>
-            <argument type="service" id="event_dispatcher"/>
             <argument type="service" id="setono_tag_bag.generator.value_based_fingerprint"/>
-            <argument type="service" id="logger"/>
+            <call method="setEventDispatcher">
+                <argument type="service" id="event_dispatcher"/>
+            </call>
+            <call method="setStorage">
+                <argument type="service" id="setono_tag_bag.storage.session"/>
+            </call>
+            <call method="setLogger">
+                <argument type="service" id="logger"/>
+            </call>
         </service>
     </services>
 </container>
diff --git a/src/Resources/config/services/twig.xml b/src/Resources/config/services/twig.xml
index 6b4134c..1175ed9 100644
--- a/src/Resources/config/services/twig.xml
+++ b/src/Resources/config/services/twig.xml
@@ -3,9 +3,13 @@
 <container xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns="http://symfony.com/schema/dic/services"
            xsi:schemaLocation="http://symfony.com/schema/dic/services http://symfony.com/schema/dic/services/services-1.0.xsd">
     <services>
-        <service id="setono_tag_bag.twig.tag_bag" class="Setono\TagBagBundle\Twig\TagBagExtension">
-            <argument type="service" id="setono_tag_bag.tag_bag"/>
+        <service id="setono_tag_bag.twig.extension" class="Setono\TagBagBundle\Twig\Extension">
             <tag name="twig.extension"/>
         </service>
+
+        <service id="setono_tag_bag.twig.runtime" class="Setono\TagBagBundle\Twig\Runtime">
+            <argument type="service" id="setono_tag_bag.tag_bag"/>
+            <tag name="twig.runtime"/>
+        </service>
     </services>
 </container>
diff --git a/src/SetonoTagBagBundle.php b/src/SetonoTagBagBundle.php
index 42b444f..a2446de 100644
--- a/src/SetonoTagBagBundle.php
+++ b/src/SetonoTagBagBundle.php
@@ -4,8 +4,6 @@
 
 namespace Setono\TagBagBundle;
 
-use Setono\TagBagBundle\DependencyInjection\Compiler\AddPathsToPhpTemplatesEnginePass;
-use Setono\TagBagBundle\DependencyInjection\Compiler\AddPathsToTwigPass;
 use Setono\TagBagBundle\DependencyInjection\Compiler\RegisterRenderersPass;
 use Symfony\Component\DependencyInjection\ContainerBuilder;
 use Symfony\Component\HttpKernel\Bundle\Bundle;
@@ -15,7 +13,5 @@ final class SetonoTagBagBundle extends Bundle
     public function build(ContainerBuilder $container): void
     {
         $container->addCompilerPass(new RegisterRenderersPass());
-        $container->addCompilerPass(new AddPathsToPhpTemplatesEnginePass());
-        $container->addCompilerPass(new AddPathsToTwigPass());
     }
 }
diff --git a/src/Storage/SessionStorage.php b/src/Storage/SessionStorage.php
index 5aad762..087c055 100644
--- a/src/Storage/SessionStorage.php
+++ b/src/Storage/SessionStorage.php
@@ -4,31 +4,45 @@
 
 namespace Setono\TagBagBundle\Storage;
 
+use Setono\TagBag\Exception\StorageException;
 use Setono\TagBag\Storage\StorageInterface;
+use Symfony\Component\HttpFoundation\Exception\SessionNotFoundException;
+use Symfony\Component\HttpFoundation\RequestStack;
 use Symfony\Component\HttpFoundation\Session\SessionInterface;
 
 final class SessionStorage implements StorageInterface
 {
-    /** @var SessionInterface */
-    private $session;
+    private RequestStack $requestStack;
 
-    public function __construct(SessionInterface $session)
+    public function __construct(RequestStack $requestStack)
     {
-        $this->session = $session;
+        $this->requestStack = $requestStack;
     }
 
     public function store(string $data): void
     {
-        $this->session->set(self::DATA_KEY, $data);
+        $this->getSession()->set(self::DATA_KEY, $data);
     }
 
     public function restore(): ?string
     {
-        return $this->session->get(self::DATA_KEY);
+        /** @var mixed $data */
+        $data = $this->getSession()->get(self::DATA_KEY);
+
+        return is_string($data) ? $data : null;
     }
 
     public function remove(): void
     {
-        $this->session->remove(self::DATA_KEY);
+        $this->getSession()->remove(self::DATA_KEY);
+    }
+
+    private function getSession(): SessionInterface
+    {
+        try {
+            return $this->requestStack->getSession();
+        } catch (SessionNotFoundException $e) {
+            throw new StorageException($e->getMessage(), 0, $e);
+        }
     }
 }
diff --git a/src/Twig/Extension.php b/src/Twig/Extension.php
new file mode 100644
index 0000000..1d9fab6
--- /dev/null
+++ b/src/Twig/Extension.php
@@ -0,0 +1,22 @@
+<?php
+
+declare(strict_types=1);
+
+namespace Setono\TagBagBundle\Twig;
+
+use Twig\Extension\AbstractExtension;
+use Twig\TwigFunction;
+
+final class Extension extends AbstractExtension
+{
+    public function getFunctions(): array
+    {
+        return [
+            new TwigFunction('setono_tag_bag_render_all', [Runtime::class, 'renderAll'], ['is_safe' => ['html']]),
+            new TwigFunction('setono_tag_bag_render_section', [Runtime::class, 'renderSection'], ['is_safe' => ['html']]),
+            new TwigFunction('setono_tag_bag_render_head', [Runtime::class, 'renderHead'], ['is_safe' => ['html']]),
+            new TwigFunction('setono_tag_bag_render_body_begin', [Runtime::class, 'renderBodyBegin'], ['is_safe' => ['html']]),
+            new TwigFunction('setono_tag_bag_render_body_end', [Runtime::class, 'renderBodyEnd'], ['is_safe' => ['html']]),
+        ];
+    }
+}
diff --git a/src/Twig/TagBagExtension.php b/src/Twig/Runtime.php
similarity index 51%
rename from src/Twig/TagBagExtension.php
rename to src/Twig/Runtime.php
index 584373f..13aa1cf 100644
--- a/src/Twig/TagBagExtension.php
+++ b/src/Twig/Runtime.php
@@ -6,30 +6,17 @@
 
 use Setono\TagBag\Tag\TagInterface;
 use Setono\TagBag\TagBagInterface;
-use Twig\Extension\AbstractExtension;
-use Twig\TwigFunction;
+use Twig\Extension\RuntimeExtensionInterface;
 
-final class TagBagExtension extends AbstractExtension
+final class Runtime implements RuntimeExtensionInterface
 {
-    /** @var TagBagInterface */
-    private $tagBag;
+    private TagBagInterface $tagBag;
 
     public function __construct(TagBagInterface $tagBag)
     {
         $this->tagBag = $tagBag;
     }
 
-    public function getFunctions(): array
-    {
-        return [
-            new TwigFunction('setono_tag_bag_render_all', [$this, 'renderAll'], ['is_safe' => ['html']]),
-            new TwigFunction('setono_tag_bag_render_section', [$this, 'renderSection'], ['is_safe' => ['html']]),
-            new TwigFunction('setono_tag_bag_render_head', [$this, 'renderHead'], ['is_safe' => ['html']]),
-            new TwigFunction('setono_tag_bag_render_body_begin', [$this, 'renderBodyBegin'], ['is_safe' => ['html']]),
-            new TwigFunction('setono_tag_bag_render_body_end', [$this, 'renderBodyEnd'], ['is_safe' => ['html']]),
-        ];
-    }
-
     public function renderAll(): string
     {
         return $this->tagBag->renderAll();
diff --git a/tests/DependencyInjection/Compiler/AddPathsToPhpTemplatesEnginePassTest.php b/tests/DependencyInjection/Compiler/AddPathsToPhpTemplatesEnginePassTest.php
deleted file mode 100644
index fe16378..0000000
--- a/tests/DependencyInjection/Compiler/AddPathsToPhpTemplatesEnginePassTest.php
+++ /dev/null
@@ -1,38 +0,0 @@
-<?php
-
-declare(strict_types=1);
-
-namespace Setono\TagBagBundle\Tests\DependencyInjection\Compiler;
-
-use Matthias\SymfonyDependencyInjectionTest\PhpUnit\AbstractCompilerPassTestCase;
-use ReflectionClass;
-use Setono\TagBag\Tag\GtagInterface;
-use Setono\TagBagBundle\DependencyInjection\Compiler\AddPathsToPhpTemplatesEnginePass;
-use Symfony\Component\DependencyInjection\ContainerBuilder;
-use Symfony\Component\DependencyInjection\Definition;
-
-/**
- * @covers \Setono\TagBagBundle\DependencyInjection\Compiler\AddPathsToPhpTemplatesEnginePass
- */
-class AddPathsToPhpTemplatesEnginePassTest extends AbstractCompilerPassTestCase
-{
-    protected function registerCompilerPass(ContainerBuilder $container): void
-    {
-        $container->addCompilerPass(new AddPathsToPhpTemplatesEnginePass());
-    }
-
-    /**
-     * @test
-     */
-    public function service_definitions_should_exist(): void
-    {
-        $this->setDefinition('setono_php_templates.engine.default', new Definition());
-
-        $this->compile();
-
-        $filename = (new ReflectionClass(GtagInterface::class))->getFileName();
-        $path = dirname($filename, 2) . '/templates';
-
-        $this->assertContainerBuilderHasServiceDefinitionWithMethodCall('setono_php_templates.engine.default', 'addPath', [$path]);
-    }
-}
diff --git a/tests/DependencyInjection/Compiler/RegisterRenderersPassTest.php b/tests/DependencyInjection/Compiler/RegisterRenderersPassTest.php
index 1736d10..52cd562 100644
--- a/tests/DependencyInjection/Compiler/RegisterRenderersPassTest.php
+++ b/tests/DependencyInjection/Compiler/RegisterRenderersPassTest.php
@@ -33,6 +33,6 @@ public function service_definitions_should_exist(): void
 
         $this->compile();
 
-        $this->assertContainerBuilderHasServiceDefinitionWithMethodCall('setono_tag_bag.renderer.composite', 'addRenderer', [new Reference('renderer'), 0]);
+        $this->assertContainerBuilderHasServiceDefinitionWithMethodCall('setono_tag_bag.renderer.composite', 'add', [new Reference('renderer')]);
     }
 }
diff --git a/tests/EventListener/EventListenerPrioritiesTest.php b/tests/EventListener/EventListenerPrioritiesTest.php
index 444a2c2..34475c8 100644
--- a/tests/EventListener/EventListenerPrioritiesTest.php
+++ b/tests/EventListener/EventListenerPrioritiesTest.php
@@ -17,6 +17,7 @@ final class EventListenerPrioritiesTest extends TestCase
      */
     public function priorities_are_correct(): void
     {
+        /** @psalm-suppress InternalMethod */
         $sessionListenerEvents = SessionListener::getSubscribedEvents();
         $populateTagBagEvents = RestoreTagBagSubscriber::getSubscribedEvents();
         $populateSessionEvents = StoreTagBagSubscriber::getSubscribedEvents();
diff --git a/tests/EventListener/RestoreTagBagSubscriberTest.php b/tests/EventListener/RestoreTagBagSubscriberTest.php
index a15ebff..59691c7 100644
--- a/tests/EventListener/RestoreTagBagSubscriberTest.php
+++ b/tests/EventListener/RestoreTagBagSubscriberTest.php
@@ -28,7 +28,7 @@ public function it_restores(): void
         $tagBag->restore()->shouldBeCalled();
 
         $event = $this->prophesize(RequestEvent::class);
-        $event->isMasterRequest()->willReturn(true);
+        $event->isMainRequest()->willReturn(true);
 
         $subscriber = new RestoreTagBagSubscriber($tagBag->reveal());
         $subscriber->onKernelRequest($event->reveal());
@@ -43,7 +43,7 @@ public function it_does_not_restore_when_the_request_is_not_a_master_request():
         $tagBag->restore()->shouldNotBeCalled();
 
         $event = $this->prophesize(RequestEvent::class);
-        $event->isMasterRequest()->willReturn(false);
+        $event->isMainRequest()->willReturn(false);
 
         $subscriber = new RestoreTagBagSubscriber($tagBag->reveal());
         $subscriber->onKernelRequest($event->reveal());
@@ -65,6 +65,8 @@ public function it_listens_to_the_correct_event(): void
     public function it_has_the_correct_priority(): void
     {
         $priority = RestoreTagBagSubscriber::getSubscribedEvents()[KernelEvents::REQUEST][1];
+
+        /** @psalm-suppress InternalMethod */
         $sessionListenerPriority = SessionListener::getSubscribedEvents()[KernelEvents::REQUEST][1];
 
         $this->assertLessThan($sessionListenerPriority, $priority);
diff --git a/tests/EventListener/StoreTagBagSubscriberTest.php b/tests/EventListener/StoreTagBagSubscriberTest.php
index 1a7454c..c628761 100644
--- a/tests/EventListener/StoreTagBagSubscriberTest.php
+++ b/tests/EventListener/StoreTagBagSubscriberTest.php
@@ -4,7 +4,6 @@
 
 namespace Setono\TagBagBundle\Tests\EventListener;
 
-use PHPUnit\Framework\MockObject\MockObject;
 use PHPUnit\Framework\TestCase;
 use Prophecy\PhpUnit\ProphecyTrait;
 use Setono\TagBag\TagBagInterface;
@@ -63,20 +62,20 @@ public function it_listens_to_the_correct_event(): void
     public function it_has_the_correct_priority(): void
     {
         $priority = StoreTagBagSubscriber::getSubscribedEvents()[KernelEvents::RESPONSE][1];
+
+        /** @psalm-suppress InternalMethod */
         $sessionListenerPriority = SessionListener::getSubscribedEvents()[KernelEvents::RESPONSE][1];
 
         $this->assertGreaterThan($sessionListenerPriority, $priority);
     }
 
-    /**
-     * @return ResponseEvent|MockObject
-     */
     private function getResponseEvent(bool $masterRequest): ResponseEvent
     {
-        $kernel = $this->createMock(HttpKernelInterface::class);
-        $request = $this->createMock(Request::class);
-        $response = $this->createMock(Response::class);
-
-        return new ResponseEvent($kernel, $request, $masterRequest ? HttpKernelInterface::MASTER_REQUEST : HttpKernelInterface::SUB_REQUEST, $response);
+        return new ResponseEvent(
+            $this->createMock(HttpKernelInterface::class),
+            new Request(),
+            $masterRequest ? HttpKernelInterface::MAIN_REQUEST : HttpKernelInterface::SUB_REQUEST,
+            new Response()
+        );
     }
 }
diff --git a/tests/SetonoTagBagBundleTest.php b/tests/SetonoTagBagBundleTest.php
index 17868ee..306c346 100644
--- a/tests/SetonoTagBagBundleTest.php
+++ b/tests/SetonoTagBagBundleTest.php
@@ -4,39 +4,48 @@
 
 namespace Setono\TagBagBundle\Tests;
 
-use Nyholm\BundleTest\BaseBundleTestCase;
-use Nyholm\BundleTest\CompilerPass\PublicServicePass;
-use Setono\PhpTemplatesBundle\SetonoPhpTemplatesBundle;
+use Nyholm\BundleTest\TestKernel;
 use Setono\TagBag\Generator\ValueBasedFingerprintGenerator;
 use Setono\TagBag\Renderer\CompositeRenderer;
-use Setono\TagBag\Renderer\ContentRenderer;
-use Setono\TagBag\Renderer\PhpTemplatesRenderer;
+use Setono\TagBag\Renderer\ContentAwareRenderer;
+use Setono\TagBag\Renderer\RendererInterface;
 use Setono\TagBag\Renderer\ScriptRenderer;
 use Setono\TagBag\Renderer\StyleRenderer;
-use Setono\TagBag\Renderer\TwigRenderer;
+use Setono\TagBag\Storage\StorageInterface;
 use Setono\TagBag\TagBag;
+use Setono\TagBag\TagBagInterface;
 use Setono\TagBagBundle\EventListener\RestoreTagBagSubscriber;
 use Setono\TagBagBundle\EventListener\StoreTagBagSubscriber;
+use Setono\TagBagBundle\Renderer\TwigRenderer;
 use Setono\TagBagBundle\SetonoTagBagBundle;
 use Setono\TagBagBundle\Storage\SessionStorage;
-use Setono\TagBagBundle\Twig\TagBagExtension;
+use Setono\TagBagBundle\Twig\Extension;
+use Setono\TagBagBundle\Twig\Runtime;
+use Symfony\Bundle\FrameworkBundle\Test\KernelTestCase;
 use Symfony\Bundle\TwigBundle\TwigBundle;
+use Symfony\Component\HttpKernel\KernelInterface;
 
 /**
  * @covers \Setono\TagBagBundle\SetonoTagBagBundle
  */
-final class SetonoTagBagBundleTest extends BaseBundleTestCase
+final class SetonoTagBagBundleTest extends KernelTestCase
 {
-    protected function setUp(): void
+    protected static function getKernelClass(): string
     {
-        parent::setUp();
-
-        $this->addCompilerPass(new PublicServicePass('|^setono_tag_bag.*|'));
+        return TestKernel::class;
     }
 
-    protected function getBundleClass(): string
+    protected static function createKernel(array $options = []): KernelInterface
     {
-        return SetonoTagBagBundle::class;
+        /**
+         * @var TestKernel $kernel
+         */
+        $kernel = parent::createKernel($options);
+        $kernel->addTestBundle(SetonoTagBagBundle::class);
+        $kernel->addTestBundle(TwigBundle::class);
+        $kernel->handleOptions($options);
+
+        return $kernel;
     }
 
     /**
@@ -44,15 +53,12 @@ protected function getBundleClass(): string
      */
     public function it_inits(): void
     {
-        $kernel = $this->createKernel();
-        $kernel->addBundle(TwigBundle::class);
-        $kernel->addBundle(SetonoPhpTemplatesBundle::class);
-
-        $this->bootKernel();
-
-        $container = $this->getContainer();
+        self::bootKernel();
+        $container = self::getContainer();
 
-        // test that all services exist and is the right class
+        /**
+         * @var list<array{id: string, class: class-string}>
+         */
         $services = [
             // event listeners
             ['id' => 'setono_tag_bag.event_listener.restore_tag_bag_subscriber', 'class' => RestoreTagBagSubscriber::class],
@@ -63,26 +69,26 @@ public function it_inits(): void
             ['id' => 'setono_tag_bag.generator.value_based_fingerprint', 'class' => ValueBasedFingerprintGenerator::class],
 
             // renderers
-            //['id' => RendererInterface::class, 'class' => CompositeRenderer::class], // todo why doesn't this work?
+            ['id' => RendererInterface::class, 'class' => CompositeRenderer::class],
             ['id' => 'setono_tag_bag.renderer.default', 'class' => CompositeRenderer::class],
             ['id' => 'setono_tag_bag.renderer.composite', 'class' => CompositeRenderer::class],
-            ['id' => 'setono_tag_bag.renderer.content', 'class' => ContentRenderer::class],
+            ['id' => 'setono_tag_bag.renderer.content_aware', 'class' => ContentAwareRenderer::class],
             ['id' => 'setono_tag_bag.renderer.script', 'class' => ScriptRenderer::class],
             ['id' => 'setono_tag_bag.renderer.style', 'class' => StyleRenderer::class],
             ['id' => 'setono_tag_bag.renderer.twig', 'class' => TwigRenderer::class],
-            ['id' => 'setono_tag_bag.renderer.php_templates', 'class' => PhpTemplatesRenderer::class],
 
             // storage
-            //['id' => StorageInterface::class, 'class' => SessionStorage::class], // todo why doesn't this work?
+            ['id' => StorageInterface::class, 'class' => SessionStorage::class],
             ['id' => 'setono_tag_bag.storage.default', 'class' => SessionStorage::class],
             ['id' => 'setono_tag_bag.storage.session', 'class' => SessionStorage::class],
 
             // tag bag
-            //['id' => TagBagInterface::class, 'class' => TagBag::class], // todo why doesn't this work?
+            ['id' => TagBagInterface::class, 'class' => TagBag::class],
             ['id' => 'setono_tag_bag.tag_bag', 'class' => TagBag::class],
 
             // twig
-            ['id' => 'setono_tag_bag.twig.tag_bag', 'class' => TagBagExtension::class],
+            ['id' => 'setono_tag_bag.twig.extension', 'class' => Extension::class],
+            ['id' => 'setono_tag_bag.twig.runtime', 'class' => Runtime::class],
         ];
 
         foreach ($services as $service) {
diff --git a/tests/Storage/SessionStorageTest.php b/tests/Storage/SessionStorageTest.php
index 0f66404..657edc8 100644
--- a/tests/Storage/SessionStorageTest.php
+++ b/tests/Storage/SessionStorageTest.php
@@ -8,6 +8,7 @@
 use Prophecy\PhpUnit\ProphecyTrait;
 use Setono\TagBag\Storage\StorageInterface;
 use Setono\TagBagBundle\Storage\SessionStorage;
+use Symfony\Component\HttpFoundation\RequestStack;
 use Symfony\Component\HttpFoundation\Session\SessionInterface;
 
 /**
@@ -25,7 +26,10 @@ public function it_stores(): void
         $session = $this->prophesize(SessionInterface::class);
         $session->set(StorageInterface::DATA_KEY, 'data')->shouldBeCalled();
 
-        $storage = new SessionStorage($session->reveal());
+        $requestStack = $this->prophesize(RequestStack::class);
+        $requestStack->getSession()->willReturn($session);
+
+        $storage = new SessionStorage($requestStack->reveal());
         $storage->store('data');
     }
 
@@ -37,7 +41,10 @@ public function it_restores(): void
         $session = $this->prophesize(SessionInterface::class);
         $session->get(StorageInterface::DATA_KEY)->shouldBeCalled();
 
-        $storage = new SessionStorage($session->reveal());
+        $requestStack = $this->prophesize(RequestStack::class);
+        $requestStack->getSession()->willReturn($session);
+
+        $storage = new SessionStorage($requestStack->reveal());
         $storage->restore();
     }
 
@@ -49,7 +56,10 @@ public function it_removes(): void
         $session = $this->prophesize(SessionInterface::class);
         $session->remove(StorageInterface::DATA_KEY)->shouldBeCalled();
 
-        $storage = new SessionStorage($session->reveal());
+        $requestStack = $this->prophesize(RequestStack::class);
+        $requestStack->getSession()->willReturn($session);
+
+        $storage = new SessionStorage($requestStack->reveal());
         $storage->remove();
     }
 }
diff --git a/tests/Twig/ExtensionTest.php b/tests/Twig/ExtensionTest.php
new file mode 100644
index 0000000..48c3ef0
--- /dev/null
+++ b/tests/Twig/ExtensionTest.php
@@ -0,0 +1,58 @@
+<?php
+
+declare(strict_types=1);
+
+namespace Setono\TagBagBundle\Tests\Twig;
+
+use Setono\TagBag\Renderer\CompositeRenderer;
+use Setono\TagBag\Renderer\ContentAwareRenderer;
+use Setono\TagBag\Renderer\ScriptRenderer;
+use Setono\TagBag\Tag\ContentAwareTag;
+use Setono\TagBag\Tag\ScriptTag;
+use Setono\TagBag\Tag\TagInterface;
+use Setono\TagBag\TagBag;
+use Setono\TagBagBundle\Twig\Extension;
+use Setono\TagBagBundle\Twig\Runtime;
+use Twig\RuntimeLoader\RuntimeLoaderInterface;
+use Twig\Test\IntegrationTestCase;
+
+final class ExtensionTest extends IntegrationTestCase
+{
+    public function getRuntimeLoaders(): array
+    {
+        $runtimeLoader = new class() implements RuntimeLoaderInterface {
+            /**
+             * @param string $class
+             */
+            public function load($class): Runtime
+            {
+                $renderer = new CompositeRenderer();
+                $renderer->add(new ScriptRenderer());
+                $renderer->add(new ContentAwareRenderer());
+                $tagBag = new TagBag($renderer);
+
+                $tagBag->add(ScriptTag::create('alert("header")')->withSection(TagInterface::SECTION_HEAD));
+                $tagBag->add(ScriptTag::create('alert("body begin")')->withSection(TagInterface::SECTION_BODY_BEGIN));
+                $tagBag->add(ScriptTag::create('alert("body end")')->withSection(TagInterface::SECTION_BODY_END));
+                $tagBag->add(ContentAwareTag::create('<div class="tag-1">content 1</div>')->withSection('section1'));
+                $tagBag->add(ContentAwareTag::create('<div class="tag-2">content 2</div>')->withSection('section2'));
+
+                return new Runtime($tagBag);
+            }
+        };
+
+        return [$runtimeLoader];
+    }
+
+    public function getExtensions(): array
+    {
+        return [
+            new Extension(),
+        ];
+    }
+
+    public function getFixturesDir(): string
+    {
+        return __DIR__ . '/Fixtures/';
+    }
+}
diff --git a/tests/Twig/Fixtures/functions/render_all.test b/tests/Twig/Fixtures/functions/render_all.test
new file mode 100644
index 0000000..68c6cb3
--- /dev/null
+++ b/tests/Twig/Fixtures/functions/render_all.test
@@ -0,0 +1,8 @@
+--TEST--
+"setono_tag_bag_render_all" function
+--TEMPLATE--
+{{ setono_tag_bag_render_all() }}
+--DATA--
+return []
+--EXPECT--
+<script>alert("header")</script><script>alert("body begin")</script><script>alert("body end")</script><div class="tag-1">content 1</div><div class="tag-2">content 2</div>
diff --git a/tests/Twig/Fixtures/functions/render_section.test b/tests/Twig/Fixtures/functions/render_section.test
new file mode 100644
index 0000000..fa20fd4
--- /dev/null
+++ b/tests/Twig/Fixtures/functions/render_section.test
@@ -0,0 +1,10 @@
+--TEST--
+"setono_tag_bag_render_section" function
+--TEMPLATE--
+{{ setono_tag_bag_render_section('section2') }}
+{{ setono_tag_bag_render_section('section1') }}
+--DATA--
+return []
+--EXPECT--
+<div class="tag-2">content 2</div>
+<div class="tag-1">content 1</div>
diff --git a/tests/Twig/TagBagExtensionTest.php b/tests/Twig/TagBagExtensionTest.php
deleted file mode 100644
index 66a1499..0000000
--- a/tests/Twig/TagBagExtensionTest.php
+++ /dev/null
@@ -1,87 +0,0 @@
-<?php
-
-declare(strict_types=1);
-
-namespace Setono\TagBagBundle\Tests\Twig;
-
-use PHPUnit\Framework\TestCase;
-use Prophecy\PhpUnit\ProphecyTrait;
-use Setono\TagBag\Tag\TagInterface;
-use Setono\TagBag\TagBagInterface;
-use Setono\TagBagBundle\Twig\TagBagExtension;
-use Twig\TwigFunction;
-
-/**
- * @covers \Setono\TagBagBundle\Twig\TagBagExtension
- */
-final class TagBagExtensionTest extends TestCase
-{
-    use ProphecyTrait;
-
-    /**
-     * @test
-     */
-    public function it_renders_head_tags(): void
-    {
-        $tagBag = $this->prophesize(TagBagInterface::class);
-        $tagBag->renderSection(TagInterface::SECTION_HEAD)->willReturn('');
-
-        $extension = new TagBagExtension($tagBag->reveal());
-
-        self::assertSame('', $extension->renderHead());
-    }
-
-    /**
-     * @test
-     */
-    public function it_renders_body_begin_tags(): void
-    {
-        $tagBag = $this->prophesize(TagBagInterface::class);
-        $tagBag->renderSection(TagInterface::SECTION_BODY_BEGIN)->willReturn('');
-
-        $extension = new TagBagExtension($tagBag->reveal());
-
-        self::assertSame('', $extension->renderBodyBegin());
-    }
-
-    /**
-     * @test
-     */
-    public function it_returns_body_end_tags(): void
-    {
-        $tagBag = $this->prophesize(TagBagInterface::class);
-        $tagBag->renderSection(TagInterface::SECTION_BODY_END)->willReturn('');
-
-        $extension = new TagBagExtension($tagBag->reveal());
-
-        self::assertSame('', $extension->renderBodyEnd());
-    }
-
-    /**
-     * @test
-     */
-    public function it_returns_all_tags(): void
-    {
-        $tagBag = $this->prophesize(TagBagInterface::class);
-        $tagBag->renderAll()->willReturn('');
-
-        $extension = new TagBagExtension($tagBag->reveal());
-
-        self::assertSame('', $extension->renderAll());
-    }
-
-    /**
-     * @test
-     */
-    public function it_registers_functions(): void
-    {
-        $tagBag = $this->prophesize(TagBagInterface::class);
-        $tagBag->getAll()->willReturn([]);
-
-        $extension = new TagBagExtension($tagBag->reveal());
-        $functions = $extension->getFunctions();
-
-        self::assertCount(5, $functions);
-        self::assertContainsOnlyInstancesOf(TwigFunction::class, $functions);
-    }
-}
