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;
}
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 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've managed to get a stable load balanced front end servers that can scale horizontally quite well however the next bottle neck would be the db. There was a blog post discussing scaling dbs horizontally however very little detail on it. I'm currently using PostgreSQL and so the only plugin I've found wouldn't work.
Are my only options creating my own HAProxy or rewriting the PostgreSQL plugin to allow connections with read replicas?
I'm using AWS for all my hosting
Firstly - I'd love to be corrected on this!
Having only had a quick look through some of the ORM classes in a SilverStripe 3.5 site, it looks like while the ORM does support multiple database connections (see DB::get_conn with argument for name) it is designed for specific use cases in mind. That is to say, you may have a module that needs to write to a specific database, so this would allow it to.
What you want is native and automatic support for this within the framework, so that all reads go to your slave(s) and writes go to your master. Unfortunately, it doesn't look like this comes out of the box. You might be able to achieve it by overloading a couple of the core SQL classes using the injector.
If you were to try it, this answer outlines how you could separate select statements out from the rest and run them through a different database connector.
As a quick example of how you might go at achieving this with SQLSelect, you will notice that it is injectable, which means you can easily overload it.
File: mysite/_config/injector.yml
Injector:
SQLSelect:
class: ReadOnlySQLSelect
You need to register a new database connection with the DB class:
File: mysite/_config.php
$readDatabaseConfig = array(/** define your DB credentials here, as with the default $databaseConfig **/);
if (!DB::connect($readDatabaseConfig, 'default_read')) {
user_error('Failed to connect to read replica DB!', E_USER_ERROR);
}
Now, overload the SQLSelect class and replace the parts of it that call the DB class methods. This class inherits from SQLExpression which is the class the contains the methods you actually care about in this instance:
File: mysite/code/ReadOnlySQLSelect.php
class ReadOnlySQLSelect extends SQLSelect
{
public function sql(&$parameters = array())
{
// Changed from SQLExpression: third parameter passed as connection name
$sql = DB::build_sql($this, $parameters, 'default_read');
if (empty($sql)) {
return null;
}
if ($this->replacementsOld) {
$sql = str_replace($this->replacementsOld, $this->replacementsNew, $sql);
}
return $sql;
}
public function execute()
{
$sql = $this->sql($parameters);
// Changed from SQLExpression: skip DB::prepared_query since it doesn't allow
// you to provide the connection name - replace it with its contents instead.
$conn = DB::get_conn('default_read');
return $conn->preparedQuery($sql, $parameters);
}
}
Note: SQLSelect::unlimitedRowCount should technically be replaced where it calls DB::prepared_query, since the prepared query method calls DB::get_conn with no arguments, so will always return the default connection. You could replace the DB::prepared_query line the same as used above:
$conn = DB::get_conn('default_read');
$result = $conn->preparedQuery($sql, $innerParameters);
If you implement the above method, also change new SQLSelect() to SQLSelect::create(), otherwise you'll end up with some queries that still hit the master server because it'll bypass your class by not using the injector.
There's also an instance in SQLConditionalExpression that you should replace too (::toSelect) but that is likely to affect query transformations from other child implementations of that class, and you won't be able to do much about it without either (A) PRing a fix to the framework or (B) overloading all the other SQL* classes.
At this point you should have everything you need to route select queries to your default_read connection.
Infrastructure
On the infrastructure side, you should be able to set up read replicas through the RDS console. When you do so it will provide you with a DNS endpoint for your replica node(s), which you can use in your _config.php to configure the connection to the read replica database.
If this works for you, you should create a module for it and put it up on GitHub - this would definitely be useful for others in future!
You may also consider making pull requests to the framework to add additional arguments to methods like DB::prepared_query to accept a connection name.
Also worth noting is that if you're using the mysqlnd database adapter you may be able to take advantage of read/write splitting, implemented with some sort of injector overloading but all handled at a lower level than the application layer.
I have a Yaml loader that loads additional config items for a "profile" (where one application can use different profiles, e.g. for different local editions of the same site).
My loader is very simple:
# YamlProfileLoader.php
use Symfony\Component\Config\Loader\FileLoader;
use Symfony\Component\Yaml\Yaml;
class YamlProfileLoader extends FileLoader
{
public function load($resource, $type = null)
{
$configValues = Yaml::parse($resource);
return $configValues;
}
public function supports($resource, $type = null)
{
return is_string($resource) && 'yml' === pathinfo(
$resource,
PATHINFO_EXTENSION
);
}
}
The loader is used more or less like this (simplified a bit, because there is caching too):
$loaderResolver = new LoaderResolver(array(new YamlProfileLoader($locator)));
$delegatingLoader = new DelegatingLoader($loaderResolver);
foreach ($yamlProfileFiles as $yamlProfileFile) {
$profileName = basename($yamlProfileFile, '.yml');
$profiles[$profileName] = $delegatingLoader->load($yamlProfileFile);
}
So is the Yaml file it's parsing:
# profiles/germany.yml
locale: de_DE
hostname: %profiles.germany.host_name%
At the moment, the resulting array contains literally '%profiles.germany.host_name%' for the 'hostname' array key.
So, how can I parse the % parameters to get the actual parameter values?
I've been trawling through the Symfony 2 code and docs (and this SO question and can't find where this is done within the framework itself. I could probably write my own parameter parser - get the parameters from the kernel, search for the %foo% strings and look-up/replace... but if there's a component ready to be used, I prefer to use this.
To give a bit more background, why I can't just include it into the main config.yml: I want to be able to load app/config/profiles/*.yml, where * is the profile name, and I am using my own Loader to accomplish this. If there's a way to wildcard import config files, then that might also work for me.
Note: currently using 2.4 but just about ready to upgrade to 2.5 if that helps.
I've been trawling through the Symfony 2 code and docs (and this SO question and can't find where this is done within the framework itself.
Symfony's dependency injection component uses a compiler pass to resolve parameter references during the optimisation phase.
The Compiler gets the registered compiler passes from its PassConfig instance. This class configures a few compiler passes by default, which includes the ResolveParameterPlaceHoldersPass.
During container compilation, the ResolveParameterPlaceHoldersPass uses the Container's ParameterBag to resolve strings containing %parameters%. The compiler pass then sets that resolved value back into the container.
So, how can I parse the % parameters to get the actual parameter values?
You'd need access to the container in your ProfileLoader (or wherever you see fit). Using the container, you can recursively iterate over your parsed yaml config and pass values to the container's parameter bag to be resolved via the resolveValue() method.
Seems to me like perhaps a cleaner approach would be for you to implement this in your bundle configuration. That way your config will be validated against a defined structure, which can catch configuration errors early. See the docs on bundle configuration for more information (that link is for v2.7, but hopefully will apply to your version also).
I realise this is an old question, but I have spent quite a while figuring this out for my own projects, so I'm posting the answer here for future reference.
I tried a lot of options to resolve %parameter% to parameters.yml but no luck at all. All I can think of is parsing %parameter% and fetch it from container, no innovation yet.
On the other hand I don't have enough information about your environment to see the big picture but I just come up with another idea. It can be quite handy if you declare your profiles in your parameters.yml file and load it as an array in your controller or service via container.
app/config/parameters.yml
parameters:
profiles:
germany:
locale: de_DE
host_name: http://de.example.com
uk:
locale: en_EN
host_name: http://uk.example.com
turkey:
locale: tr_TR
host_name: http://tr.example.com
You can have all your profiles as an array in your controller.
<?php
namespace Acme\DemoBundle\Controller;
use Symfony\Bundle\FrameworkBundle\Controller\Controller;
class DefaultController extends Controller
{
public function indexAction()
{
$profiles = $this->container->getParameter('profiles');
var_dump($profiles);
return $this->render('AcmeDemoBundle:Default:index.html.twig');
}
}
With this approach
you don't have to code a custom YamlLoader
you don't have to worry about importing parameters into other yml files
you can have your profiles as an array anytime you have the $container in your hand
you don't have to load/cache profile files one by one
you don't have to find a wildcard file loading solution
If I got your question correctly, this approach can help you.
I would like to use Doctrine (v2.4) in my ZendFramework (v1.11) application, which I am starting from scratch. There are some articles describing such integration, but they seem quite complicated and a little out of date. Is there any fairly simple way to connect ZF1 and Doctrine2?
I've implemented this as an application resource (extending \Zend_Application_Resource_ResourceAbstract)
The code is quite long so below is a top level check list of the requirements.
Create a doctrine entity manager configuration instance (Doctrine\ORM\Configuration).
$config = new Doctrine\ORM\Configuration();
Populate the configuration with the required data (metadata driver, cache config etc). Doctrine's documentation is a good reference here to what would be required (http://docs.doctrine-project.org/en/latest/reference/configuration.html)
Example here uses the Annotation driver:
$driver = new Driver\AnnotationDriver(
new Annotations\CachedReader(new Annotations\AnnotationReader(), new Cache\ArrayCache()),
$entityDirs
);
$config->setMetadataDriverImpl($driver);
Lastly pass this new config instance to the static entity manager EntityManager::create
E.G. ($options here is the database connection info as exampled in the above link)
$entityManager = EntityManager::create($options['database'], $config);
Take a look at my full source, at the very least it will give you a head start:
https://github.com/alex-patterson-webdev/Multiverse/blob/master/lib/Multiverse/Application/Resource/Entitymanager.php