I'm working with symfony 2.8, I'm facing a situation where a service must be instanced even if it is not requested somewhere.
Why ? Because this Core service configure tagged services that are transfered by method calling into Core ( I did this in a compiler pass ).
And then, if I request one of these tagged services without request Core, it will not be configured and then be unusable.
Here is the compiler pass :
$coreDefinition = $container->findDefinition(
'app.improvements.core'
);
$taggedAppliers = $container->findTaggedServiceIds('app.improvement.applier');
foreach ($taggedAppliers as $id => $tags) {
$coreDefinition->addMethodCall(
'registerApplier',
[new Reference($id)]
);
}
$taggedImprovements = $container->findTaggedServiceIds(
'app.improvement'
);
// Only method found to initialize improvements.
foreach ($taggedImprovements as $id => $tags) {
$coreDefinition->addMethodCall(
'registerImprovement',
[new Reference($id)]
);
}
To sum up, the Appliers registers Improvement and Core registers Appliers. The core associate improvements with appliers because each improvement must be registered in a specific applier that the core stores.
The problem I that when I only request a Applier, its improvements are not registered into it because the core isn't instancied.
Thank you.
Design problems aside, the easiest way to instantiate a service is to use an event listener. In your case you would listen for the kernel.request and pull your service from the container.
class RequestListener
{
public function onKernelRequest(GetResponseEvent $event)
{
$event->getKernel()->getContainer()->get('service_id');
}
}
Related
Symfony docs show a very neat way to create a stack of decorators:
namespace Symfony\Component\DependencyInjection\Loader\Configurator;
return function(ContainerConfigurator $container) {
$container>stack('decorated_foo_stack', [
inline_service(\Baz::class),
inline_service(\Bar::class),
inline_service(\Foo::class),
])
;
};
And show this as an alternative to doing:
// config/services.php
namespace Symfony\Component\DependencyInjection\Loader\Configurator;
return function(ContainerConfigurator $configurator) {
$services = $configurator->services();
$services->set(\Foo::class);
$services->set(\Bar::class)
->decorate(\Foo::class, null, 5)
->args([service('.inner')]);
$services->set(\Baz::class)
->decorate(\Foo::class, null, 1)
->args([service('.inner')]);
};
Problem is, the "neater" approach leaves service Foo::class undecorated. Applications that use the original definition do not go through the stack, but access the original service.
In my case, I have to decorate a service called api_platform.serializer.context_builder. Doing this works in creating a decorated stack:
$services->stack(
'decorated_context_builder',
[
inline_service(SupportTicketMessageContextBuilder::class),
inline_service(LeadContextBuilder::class),
inline_service(BidContextBuilder::class),
inline_service(PartnerContextBuilder::class),
inline_service(WebProfileContextBuilder::class),
service('api_platform.serializer.context_builder'),
]
);
The service is provided by a vendor dependency, and it's used by that dependency. When it uses the injected api_platform.serializer.context_builder it completely ignores my newly created decorated_context_builder stack.
Instead, if I create the stack manually:
$services->set(LeadContextBuilder::class)
->decorate('api_platform.serializer.context_builder', priority: 4)
;
$services->set(BidContextBuilder::class)
->decorate('api_platform.serializer.context_builder', priority: 3)
;
// etc, etc, etc
... it works as expected.
How can I use a decoration stack to decorate an existing service definition, so that the existing definition gets decorated?
Apparently this is not possible with stacking decorators, as they are not currently compatible with decorating other services, just to create stand-alone stacks.
I setup a Unit Test in a Shopware custom (static) Plugin following this guide:
Shopware documentation
Everything runs fine and I'm able to run a unit test
class ProductReturnsTest extends TestCase
{
use IntegrationTestBehaviour;
use StorefrontPageTestBehaviour;
public function testConfirmPageSubscriber(): void
{
$container = $this->getKernel()->getContainer();
$dd = $container->get(CustomDataService::class); <== IT BREAKS HERE ServiceNotFoundException: You have requested a non-existent service
$dd = $container->get('event_dispatcher'); // WORKS WITH SHOPWARE ALIASES NOT WITH PLUGINS
}
}
I can make container->get on any shopware alias but as soon I try to recall and get from the container any service decleared in any xml of any 3th party plugin, i get
ServiceNotFoundException: You have requested a non-existent service "blabla"
What is wrong ?
Take a look at the answer given here: https://stackoverflow.com/a/70171394/10064036.
Probably your plugin is not marked as active in the DB your tests run against.
The test environment has a mostly unpopulated database to allow tests to to run unaffected with their own fixtures only. Therefore after each test there should be a rollback to all transactions made within the test. This principle also includes plugin installations and database transactions they may execute in their lifecycle events.
You may want to install your plugin properly before your tests, so you get a representative state of the environment with the plugins lifecycle events getting dispatched and thereby caused possible changes.
public function setUp(): void
{
$this->installPlugin();
}
private function installPlugin(): void
{
$application = new Application($this->getKernel());
$installCommand = $application->find('plugin:install');
$args = [
'--activate' => true,
'--reinstall' => false,
'plugins' => ['YourPluginName'],
];
$installCommand->run(new ArrayInput($args, $installCommand->getDefinition()), new NullOutput());
}
I have asked this question yesterday as well, but this one includes code.
Issue
My application have multiple modules and 2 types of user accounts, Some modules are loaded always which are present in application.config.php some of them are conditional i.e. some are loaded for user type A and some for user type B
After going through documentations and questions on Stack Overflow, I understand some of ModuleManager functionalities and started implementing the logic that I though might work.
Some how I figured out a way to load the modules that are not present in application.config.php [SUCCESS] but their configuration is not working [THE ISSUE] i.e. if in onBootstrap method I get the ModuleManager service and do getLoadedModules() I get the list of all the modules correctly loaded. Afterwards if I try to get some service from that dynamically loaded module, it throws exception.
Zend\ServiceManager\ServiceManager::get was unable to fetch or create an instance for jobs_mapper
Please note that, the factories and all other stuff are perfectly fine because if I load the module from application.config.php it works fine
Similarly when I try to access any route from the dynamically loaded module it throws 404 Not Found which made it clear that the configuration from module.config.php of these modules are not loading even though the module is loaded by ModuleManager.
Code
In Module.php of my Application module I implemented InitProviderInterface and added a method init(ModuleManager $moduleManager) where I catch the moduleManager loadModules.post event trigger and load modules
public function init(\Zend\ModuleManager\ModuleManagerInterface $moduleManager)
{
$eventManager = $moduleManager->getEventManager();
$eventManager->attach(\Zend\ModuleManager\ModuleEvent::EVENT_LOAD_MODULES_POST, [$this, 'onLoadModulesPost']);
}
Then in the same class I delcare the method onLoadModulesPost and start loading my dynamic modules
public function onLoadModulesPost(\Zend\ModuleManager\ModuleEvent $event)
{
/* #var $serviceManager \Zend\ServiceManager\ServiceManager */
$serviceManager = $event->getParam('ServiceManager');
$configListener = $event->getConfigListener();
$authentication = $serviceManager->get('zfcuser_auth_service');
if ($authentication->getIdentity())
{
$moduleManager = $event->getTarget();
...
...
$loadedModules = $moduleManager->getModules();
$configListener = $event->getConfigListener();
$configuration = $configListener->getMergedConfig(false);
$modules = $modulesMapper->findAll(['is_agency' => 1, 'is_active' => 1]);
foreach ($modules as $module)
{
if (!array_key_exists($module['module_name'], $loadedModules))
{
$loadedModule = $moduleManager->loadModule($module['module_name']);
//Add modules to the modules array from ModuleManager.php
$loadedModules[] = $module['module_name'];
//Get the loaded module
$module = $moduleManager->getModule($module['module_name']);
//If module is loaded succesfully, merge the configs
if (($loadedModule instanceof ConfigProviderInterface) || (is_callable([$loadedModule, 'getConfig'])))
{
$moduleConfig = $module->getConfig();
$configuration = ArrayUtils::merge($configuration, $moduleConfig);
}
}
}
$moduleManager->setModules($loadedModules);
$configListener->setMergedConfig($configuration);
$event->setConfigListener($configListener);
}
}
Questions
Is it possible to achieve what I am trying ?
If so, what is the best way ?
What am I missing in my code ?
I think there is some fundamental mistake in what you are trying to do here: you are trying to load modules based on merged configuration, and therefore creating a cyclic dependency between modules and merged configuration.
I would advise against this.
Instead, if you have logic that defines which part of an application is to be loaded, put it in config/application.config.php, which is responsible for retrieving the list of modules.
At this stage though, it is too early to depend on any service, as service definition depends on the merged configuration too.
Another thing to clarify is that you are trying to take these decisions depending on whether the authenticated user (request information, rather than environment information) matches a certain criteria, and then modifying the entire application based on that.
Don't do that: instead, move the decision into the component that is to be enabled/disabled conditionally, by putting a guard in front of it.
What you're asking can be done, but that doesn't mean you should.
Suggesting an appropriate solution without knowing the complexity of the application you're building is difficult.
Using guards will certainly help decouple your code, however using it alone doesn't address scalability and maintainability, if that's a concern?
I'd suggest using stateless token-based authentication. Instead of maintaining the validation logic in every application, write the validation logic at one common place so that every request can make use of that logic irrespective of application. Choosing a reverse proxy server (Nginx) to maintain the validation logic (with the help of Lua) gives you the flexibility to develop your application in any language.
More to the point, validating the credentials at the load balancer level essentially eliminates the need for the session state, you can have many separate servers, running on multiple platforms and domains, reusing the same token for authenticating the user.
Identifying the user, account type and loading different modules then becomes a trivial task. By simply passing the token information via an environment variable, it can be read within your config/application.config.php file, without needing to access the database, cache or other services beforehand.
I'm wondering what is the best way to inject dynamic configuration(retrieved from db for instance) into configuration array in Zend Framework 2? In Module.php I have:
public function onBootstrap(MvcEvent $e) {
$eventManager = $e->getApplication()->getEventManager();
$moduleRouteListener = new ModuleRouteListener();
$moduleRouteListener->attach($eventManager);
$eventManager->attach('route', array($this, 'mergeDynamicConfig'));
}
public function mergeDynamicConfig(EventInterface $e) {
$application = $e->getApplication();
$sm = $application->getServiceManager();
$configurationTable = $sm->get('DynamicConfiguration\Model\Table\ConfigurationTable');
$dynamicConfig = $configurationTable->fetchAllConfig();
//Configuration array from db
//Array
//(
// [config] => 'Test1',
// [config2] => 'Test2',
// [config3] => 'Test3',
//)
//What to do here?
//I want to use the configurations above like $sm->get('Config')['dynamic_config']['config3'];
}
There is a section in the documentation that explains how to manipulate the merged configuration using the specific event ModuleEvent::EVENT_MERGE_CONFIG
Zend\ModuleManager\Listener\ConfigListener triggers a special event, Zend\ModuleManager\ModuleEvent::EVENT_MERGE_CONFIG, after merging all configuration, but prior to it being passed to the ServiceManager. By listening to this event, you can inspect the merged configuration and manipulate it.
The problem with this is that the service manager is not available at this point as the listener's event is one of the first events triggered by the module manager at priority 1000).
This means that you cannot execute your query and merge the config prior to the configuration being passed to the service manager, you would need to do so after.
Perhaps I have misunderstood your requirements, however I would approach this differently.
You could replace any calls where you need config $serviceManager->get('config') with $serviceManager->get('MyApplicationConfig'); which would be you own configuration service that uses the merged application config and then adds to it.
For example, you could register this configuration service in module.config.php.
return [
'service_manager' => [
'factories' => [
'MyApplicationConfig' => 'MyApplicationConfig\Factory\MyApplicationConfigFactory',
]
],
];
And create a factory to do the loading of merged module configuration, making any database calls or caching etc.
class MyApplicationConfigFactory implements FactoryInterface
{
public function createService(ServiceLocatorInterface $sm)
{
$config = $sm->get('config');
$dbConfig = $this->getDatabaseConfigAsArray($sm);
return array_replace_recursive($config, $dbConfig);
}
protected function getDatabaseConfigAsArray(ServiceLocatorInterface $sm)
{}
}
You also have the added benefit that the service is lazy loaded.
I would not use this approuch, for a few reasons.
Putting SQL queries in your Module.php means that they will get executed on EVERY request for every user thus making your application slow, very slow.
If your database is compromised all the config will be stolen as well.
Solution would be to move all the config in your config/autoload/my_custom_config.local.php via array with keys. From there you can always load it without making a single database request. It will be way faster and secure, because the file will be outside your root folder and hacking a server is always alot harder than hacking a database.
If you still want to allow users to eit the options you can simply include the file in an action and show it with a foreach for example. To save the information you can do this:
file_put_contents("my_custom_config.local.php", '<?php return ' . var_export($config, true).';');
One other plus is that if you load your config the way discribe above you can also retrive the config like you want via $sm->get('Config')['dynamic_config']['config3']
Can someone explain what a compilerpass is?
CompilerPass implementations are some kind of listeners that are executed after dependency injection container is built from configuration files and before it is saved as plain PHP in cache. They are used to build some structures that requires access to definitions from outer resources or need some programming that is not available in XML/YAML configuration. You can consider them as "final filters" that can modify entire DIC.
Let's consider a TwigBundle and its TwigEnvironmentPass. What it does is quite simple:
Fetch a reference to twig service (defined as <service id="twig" class="..." ...>)
Find all services that has been tagged with twig.extension tag. To do that you have work on complete DIC (built from XML configuration files) as those services might be defined in any bundle.
Build a custom code for service creation method.
As a final result the following code will be generated:
protected function getTwigService()
{
$this->services['twig'] = $instance = new \Twig_Environment($this->get('twig.loader'), ...);
// THIS HAS BEEN ADDED THANKS TO THE TwigEnvironmentPass:
$instance->addExtension(new \Symfony\Bundle\SecurityBundle\Twig\Extension\SecurityExtension($this->get('security.context')));
$instance->addExtension(new \Symfony\Bundle\TwigBundle\Extension\TransExtension($this->get('translator')));
$instance->addExtension(new \Symfony\Bundle\TwigBundle\Extension\TemplatingExtension($this));
$instance->addExtension(new \Symfony\Bundle\TwigBundle\Extension\FormExtension(array(0 => 'TwigBundle::form.html.twig', 1 => 'SiteBundle::widgets.html.twig')));
$instance->addExtension(new \MyProject\SiteBundle\Twig\Extension\MyVeryOwnExtensionToTwig($this));
return $instance;
}