Skip to content

Commit 45bd19f

Browse files
committed
Feat: Allow Dependency Injection from Symfony into Corto
Prior to this change, it was not possible to inject dependencies into Corto in a modern way. Corto pulled all dependencies from the Symfony service container itself. This caused issues, because it is no longer possible to pull twig from the service container in Symfony 6.4. This change introduces a DiContainerRuntime, which can be used to inject dependencies into Corto. This was not possible using the DiContainer, because the DiContainer is constructed during bundle bootstrapping, where DI is not available. It is not possible to move it from there, as it is required by various constructions in the bootstapping process. The DiContainerRuntime is injected into Corto after bootstrapping, but before the request is handled. #1874
1 parent b9a3f19 commit 45bd19f

9 files changed

Lines changed: 122 additions & 17 deletions

File tree

config/services/services.yml

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,12 @@ services:
22
_defaults:
33
public: true
44

5+
OpenConext\EngineBlockBundle\Bridge\EngineBlockBootstrapper:
6+
autowire: true
7+
autoconfigure: true
8+
tags:
9+
- { name: kernel.event_subscriber }
10+
511
OpenConext\EngineBlock\Xml\:
612
resource: '../../src/OpenConext/EngineBlock/Xml'
713
exclude: '../../src/OpenConext/EngineBlock/Xml/ValueObjects'

library/EngineBlock/Application/DiContainer.php

Lines changed: 5 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -30,8 +30,12 @@
3030
use OpenConext\EngineBlockBundle\Sbs\SbsClientInterface;
3131
use Symfony\Component\DependencyInjection\ContainerInterface as SymfonyContainerInterface;
3232
use Symfony\Component\Mailer\MailerInterface;
33-
use Twig\Environment;
3433

34+
/**
35+
* This DiContainer relies on fetching services from the Symfony service container. This is no longer the way to go.
36+
* Instead, going forward, inject new dependencies into \OpenConext\EngineBlockBundle\Bridge\DiContainerRuntime
37+
* This way we no longer need to worry about Symfony purging services and Symfony can properly optimize it's service container.
38+
*/
3539
class EngineBlock_Application_DiContainer extends \Pimple\Container
3640
{
3741
const ATTRIBUTE_METADATA = 'attributeMetadata';
@@ -417,14 +421,6 @@ public function getEncryptionKeysConfiguration()
417421
return $this->container->getParameter('encryption_keys');
418422
}
419423

420-
/**
421-
* @return Environment
422-
*/
423-
public function getTwigEnvironment()
424-
{
425-
return $this->container->get('twig');
426-
}
427-
428424
/**
429425
* @return array
430426
*/

library/EngineBlock/ApplicationSingleton.php

Lines changed: 17 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -17,12 +17,11 @@
1717
*/
1818

1919
use OpenConext\EngineBlock\Logger\Handler\FingersCrossed\ManualOrDecoratedActivationStrategy;
20-
use OpenConext\EngineBlock\Metadata\Entity\IdentityProvider;
2120
use OpenConext\EngineBlock\Metadata\MetadataRepository\EntityNotFoundException;
2221
use OpenConext\EngineBlock\Request\RequestId;
22+
use OpenConext\EngineBlockBundle\Bridge\DiContainerRuntime;
2323
use OpenConext\EngineBlockBundle\Exception\Art;
2424
use Psr\Log\LoggerInterface;
25-
use SAML2\XML\saml\Issuer;
2625
use Symfony\Component\DependencyInjection\ContainerInterface;
2726

2827
define('ENGINEBLOCK_FOLDER_ROOT' , realpath(__DIR__ . '/../../') . '/');
@@ -80,6 +79,8 @@ class EngineBlock_ApplicationSingleton
8079
*/
8180
protected $_diContainer;
8281

82+
protected ?DiContainerRuntime $_diContainerRuntime = null;
83+
8384
/**
8485
* @var ManualOrDecoratedActivationStrategy
8586
*/
@@ -109,6 +110,20 @@ public static function getInstance()
109110
return self::$s_instance;
110111
}
111112

113+
public function setDiContainerRuntime(DiContainerRuntime $diContainerRuntime): void
114+
{
115+
$this->_diContainerRuntime = $diContainerRuntime;
116+
}
117+
118+
public function getDiContainerRuntime(): DiContainerRuntime
119+
{
120+
if ($this->_diContainerRuntime === null) {
121+
throw new RuntimeException('The DiContainerRuntime is not ready yet!');
122+
}
123+
124+
return $this->_diContainerRuntime;
125+
}
126+
112127
/**
113128
* Get THE Log instance.
114129
*

library/EngineBlock/Corto/Adapter.php

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -439,7 +439,7 @@ protected function _processProxyServerResponseBody(EngineBlock_Http_Response $re
439439

440440
protected function _getCoreProxy()
441441
{
442-
$twig = EngineBlock_ApplicationSingleton::getInstance()->getDiContainer()->getTwigEnvironment();
442+
$twig = EngineBlock_ApplicationSingleton::getInstance()->getDiContainerRuntime()->twig;
443443
return new EngineBlock_Corto_ProxyServer($twig);
444444
}
445445

library/EngineBlock/Corto/Module/Bindings.php

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -116,7 +116,7 @@ public function __construct(EngineBlock_Corto_ProxyServer $server)
116116
$diContainer = EngineBlock_ApplicationSingleton::getInstance()->getDiContainer();
117117
$this->_featureConfiguration = $diContainer->getFeatureConfiguration();
118118
$this->_logger = EngineBlock_ApplicationSingleton::getLog();
119-
$this->twig = $diContainer->getTwigEnvironment();
119+
$this->twig = EngineBlock_ApplicationSingleton::getInstance()->getDiContainerRuntime()->twig;
120120
$this->acsLocationSchemeValidator = $diContainer->getAcsLocationSchemeValidator();
121121
$this->stepupIdentityProvider = $diContainer->getStepupIdentityProvider($this->_server);
122122
}

library/EngineBlock/Corto/Module/Services.php

Lines changed: 5 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -75,6 +75,7 @@ public function serve($serviceName)
7575
private function factoryService($className, EngineBlock_Corto_ProxyServer $server)
7676
{
7777
$diContainer = EngineBlock_ApplicationSingleton::getInstance()->getDiContainer();
78+
$diContainerRuntime = EngineBlock_ApplicationSingleton::getInstance()->getDiContainerRuntime();
7879

7980
switch($className) {
8081
case EngineBlock_Corto_Module_Service_ProvideConsent::class :
@@ -84,7 +85,7 @@ private function factoryService($className, EngineBlock_Corto_ProxyServer $serve
8485
$diContainer->getConsentFactory(),
8586
$diContainer->getConsentService(),
8687
$diContainer->getAuthenticationStateHelper(),
87-
$diContainer->getTwigEnvironment(),
88+
$diContainerRuntime->twig,
8889
$diContainer->getProcessingStateHelper(),
8990
$diContainer->getDiscoverySelectionService()
9091
);
@@ -128,19 +129,19 @@ private function factoryService($className, EngineBlock_Corto_ProxyServer $serve
128129
return new EngineBlock_Corto_Module_Service_SingleSignOn(
129130
$server,
130131
$diContainer->getXmlConverter(),
131-
$diContainer->getTwigEnvironment(),
132+
$diContainerRuntime->twig,
132133
$diContainer->getServiceProviderFactory(),
133134
$diContainer->getDiscoverySelectionService()
134135
);
135136
case EngineBlock_Corto_Module_Service_ContinueToIdp::class :
136137
return new EngineBlock_Corto_Module_Service_ContinueToIdp(
137138
$server,
138139
$diContainer->getXmlConverter(),
139-
$diContainer->getTwigEnvironment(),
140+
$diContainerRuntime->twig,
140141
$diContainer->getServiceProviderFactory()
141142
);
142143
default :
143-
return new $className($server, $diContainer->getXmlConverter(), $diContainer->getTwigEnvironment());
144+
return new $className($server, $diContainer->getXmlConverter(), $diContainerRuntime->twig);
144145
}
145146
}
146147
}
Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,35 @@
1+
<?php
2+
3+
/**
4+
* Copyright 2025 SURFnet B.V.
5+
*
6+
* Licensed under the Apache License, Version 2.0 (the "License");
7+
* you may not use this file except in compliance with the License.
8+
* You may obtain a copy of the License at
9+
*
10+
* http://www.apache.org/licenses/LICENSE-2.0
11+
*
12+
* Unless required by applicable law or agreed to in writing, software
13+
* distributed under the License is distributed on an "AS IS" BASIS,
14+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
15+
* See the License for the specific language governing permissions and
16+
* limitations under the License.
17+
*/
18+
19+
namespace OpenConext\EngineBlockBundle\Bridge;
20+
21+
use Twig\Environment;
22+
23+
/**
24+
* In contrast to DiContainer, this provider is constructed 'runtime'.
25+
* As in, the current DiContainer is needed very early in the bootstrapping process, so it's not possible to inject dependencies into it.
26+
*
27+
* This provider should be used for all new dependencies.
28+
*/
29+
final readonly class DiContainerRuntime
30+
{
31+
32+
public function __construct(public Environment $twig)
33+
{
34+
}
35+
}
Lines changed: 47 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,47 @@
1+
<?php
2+
3+
/**
4+
* Copyright 2025 SURFnet B.V.
5+
*
6+
* Licensed under the Apache License, Version 2.0 (the "License");
7+
* you may not use this file except in compliance with the License.
8+
* You may obtain a copy of the License at
9+
*
10+
* http://www.apache.org/licenses/LICENSE-2.0
11+
*
12+
* Unless required by applicable law or agreed to in writing, software
13+
* distributed under the License is distributed on an "AS IS" BASIS,
14+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
15+
* See the License for the specific language governing permissions and
16+
* limitations under the License.
17+
*/
18+
19+
namespace OpenConext\EngineBlockBundle\Bridge;
20+
21+
use Symfony\Component\EventDispatcher\EventSubscriberInterface;
22+
use Symfony\Component\HttpKernel\KernelEvents;
23+
use Twig\Environment;
24+
25+
class EngineBlockBootstrapper implements EventSubscriberInterface
26+
{
27+
private readonly DiContainerRuntime $diContainerRuntime;
28+
29+
public function __construct(
30+
Environment $twig,
31+
) {
32+
$this->diContainerRuntime = new DiContainerRuntime($twig);
33+
}
34+
35+
public function onKernelRequest(): void
36+
{
37+
$engineBlock = \EngineBlock_ApplicationSingleton::getInstance();
38+
$engineBlock->setDiContainerRuntime($this->diContainerRuntime);
39+
}
40+
41+
public static function getSubscribedEvents(): array
42+
{
43+
return [
44+
KernelEvents::REQUEST => ['onKernelRequest', 100],
45+
];
46+
}
47+
}

tests/library/EngineBlock/Test/Corto/Module/BindingsTest.php

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@
1919
use Mockery as m;
2020
use Mockery\Adapter\Phpunit\MockeryPHPUnitIntegration;
2121
use OpenConext\EngineBlock\Metadata\Entity\ServiceProvider;
22+
use OpenConext\EngineBlockBundle\Bridge\DiContainerRuntime;
2223
use PHPUnit\Framework\TestCase;
2324
use SAML2\Assertion;
2425
use SAML2\Assertion\Validation\ConstraintValidator\NotBefore;
@@ -48,6 +49,10 @@ public function setUp(): void
4849
new EngineBlock_X509_PrivateKey(__DIR__.'/test.pem.key')
4950
)
5051
);
52+
53+
$engineBlock = \EngineBlock_ApplicationSingleton::getInstance();
54+
$engineBlock->setDiContainerRuntime(new DiContainerRuntime(Phake::mock(Twig\Environment::class)));
55+
5156
$this->bindings = new EngineBlock_Corto_Module_Bindings($proxyServer);
5257
}
5358

0 commit comments

Comments
 (0)