Symfony 2 - how to parse %parameter% in my own Yaml file loader? - php

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.

Related

Yii2: How to force using fallback MessageFormatter method?

My website is with a hosting provider that has the MessageFormatter class available on the server (Linux, PHP 7.0.27) but it is an old ICU version (4.2.1) that doesn't support my message {number,plural,=0{# available} =1{# available} other{# available}} and gives the error:
Message pattern is invalid: Constructor failed
msgfmt_create: message formatter creation failed: U_ILLEGAL_CHARACTER
...because of the =1 and =2 notation.
I'm not able to make changes to the server so how can I force using the fallback method provided by Yii2 which works just fine?
There is this hacky way you can try.
Copy the yii\i18n\MessageFormatter code to a new file. Name it MessageFormatter.php and place somewhere in your application (but not in vendor folder).
In this new file change the format() method to:
public function format($pattern, $params, $language)
{
$this->_errorCode = 0;
$this->_errorMessage = '';
if ($params === []) {
return $pattern;
}
return $this->fallbackFormat($pattern, $params, $language);
}
Don't change anything else (including namespace).
Now let's use Yii mapping.
Find a place in your application when you can put code that will be run every time in bootstrapping phase. Good place for this is common/config/bootstrap.php if you are using "Advanced Template"-like project.
Add there this line:
Yii::$classMap['yii\i18n\MessageFormatter'] = 'path/to/your/MessageFormatter.php';
Obviously change the path to the one you've chosen. Now Yii autoloader will load this class from your file instead of the original Yii vendor folder (as mentioned in Class Autoloading section of the Guide).
In the modified file MessageFormatter method presence of intl library is never checked so fallback is used as default.
The downside of this trick is that you need to update manually your file every time original Yii file is changed (so almost every time you upgrade Yii version).
Another approach is to configure I18N component in your application to use your custom MessageFormatter where you can extend the original file and just override format() method inside without modifying class map.

Testing Laravel Service Providers

I'm (we're) creating a package that acts as a core component for our future CMS and of course that package needs some unit tests.
When the package registeres, the first thing it does is set the back/frontend context like this:
class FoundationServiceProvider extends ServiceProvider
{
// ... stuff ...
public function register()
{
// Switch the context.
// Url's containing '/admin' will get the backend context
// all other urls will get the frontend context.
$this->app['build.context'] = request()->segment(1) === 'admin'
? Context::BACKEND
: Context::FRONTEND;
}
}
So when I visit the /admin url, the app('build.context') variable will be set to backend otherwise it will be set to `frontend.
To test this I've created the following test:
class ServiceProviderTest extends \TestCase
{
public function test_that_we_get_the_backend_context()
{
$this->visit('admin');
$this->assertEquals(Context::BACKEND, app('build.context'));
}
}
When I'm running the code in the browser (navigating to /admin) the context will get picked up and calling app('build.context') will return backend, but when running this test, I always get 'frontend'.
Is there something I did not notice or some incorrect code while using phpunit?
Thanks in advance
Well, this is a tricky situation. As I understand it, laravel initiates two instances of the framework when running tests - one that is running the tests and another that is being manipulated through instructions. You can see it in tests/TestCase.php file.
So in your case you are manipulating one instance, but checking the context of another (the one that did not visit /admin and is just running the tests). I don't know if there's a way to access the manipulated instance directly - there's nothing helpful in documentation on this issue.
One workaround would be to create a route just for testing purposes, something like /admin/test_context, which would output the current context, and the check it with
$this->visit('admin/test_context')->see(Context::BACKEND);
Not too elegant, but that should work. Otherwise, look around in laravel, maybe you will find some undocumented feature.

Symfony2 - something other than bundle

I'm currenly working on the project where i need something orther than bundle. Something i call "Module".
It should be different from the bundle in that when project is starting system doesn't know which "Modules" will be used
and how they will be configured.
Also i'm going to use these modules similar to bundles
$response = $this->forward('AcmeHelloModule:Hello:fancy');
OR
$response = $this->forward('Acme/Hello:Hello:fancy');
Here HelloController->fancyAction(); would be executed. And this controller described say in file /src/modules/Acme/Hello/Controller/HelloController.php
So the question is how to implement this ?
a solution would be to implement a PluginBundle that can dynamicly install, load and run your so called modules.
the PluginBundle would not contain specific plugin code at all, but the runtime environment for you modules/plugins. you may then save in the database which plugins/modules are enabled and load them dynamicly at runtime.
with this sollution it should be possible to create a dynamic plugin mechanism as in wordpress. modifying the AppKernel at runtime is not a good solution because you'd also have to clear the cache when en- disabeling bundles.
In AppKernel add the following method:
public function getBundle($name, $first = true)
{
if (substr($name, -6) == 'Module')) {
return $this->getBundle('ModuleBundle')->getModule($name, $first);
}
return parent::getBundle($name, $first);
}
and all the logic runs in ModuleBundle.
But make sure the type of response is the same as Kernel->getBundle();

How to enable ORM annotation prefix outside of Symfony2?

I'm converting an old PHP project to the Symfony2 framework. Some of the pages are now handled by my Symfony2 front controller (index.php), but many pages have not yet been converted.
The problem is that, within Symfony, all of my Doctrine entity annotations must begin with the ORM\ prefix, but outside of Symfony, that prefix does not appear to be enabled, and so I get the following error:
Class MyProject\MyBundle\Entity\MyClass is not a valid entity or mapped super class.
I've tried to duplicate whatever magic Symfony does to set this up, including following these instructions [doctrine-project.org], and actually including app/autoload.php entirely into my legacy bootstrap process. But nothing works.
Does anyone know how I can manually replicate whatever it is that Symfony does to enable the ORM\ prefix for my Doctrine annotations?
I got the answer from the Symfony2 Google group. The problem is that the Doctrine configuration shown in the documentation uses SimpleAnnotationReader behind the scenes, but you need regular AnnotationReader to use the ORM\ namespace prefix. I got it to work by replacing this:
$config = new Doctrine\ORM\Configuration();
$driver = $config->newDefaultAnnotationDriver('/path/to/my/entities');
with this:
use Doctrine\Common\Annotations\AnnotationReader;
use Doctrine\ORM\Mapping\Driver\AnnotationDriver;
// ...
$config = new Doctrine\ORM\Configuration();
$reader = new AnnotationReader();
$driver = new AnnotationDriver($reader, '/path/to/my/entities');
I ended up with:
Doctrine\ORM\Tools\Setup::createAnnotationMetadataConfiguration($paths, $devMode, null, null, false);`
The 3rd and 4th null arguments are default. The 5th false argument tells it to make a standard AnnotationReader rather than a basic one.
I'm using Doctrine 2.5.6.
Explanation
I found I couldn't get Ian's solution working without calling Doctrine\ORM\Tools\Setup::createAnnotationMetadataConfiguration before making my own config. I was getting this error:
'[Semantical Error] The annotation "#Doctrine\ORM\Mapping\Entity" in class My\Class does not exist, or could not be auto-loaded.'
I was really confused so I took a look at the source code.
It turns out createAnnotationMetadataConfiguration calls Doctrine\ORM\Configuration::newDefaultAnnotationDriver rather than creating the annotation driver directly. This calls AnnotationRegistry::registerFile(__DIR__ . '/Mapping/Driver/DoctrineAnnotations.php'); which seems to be critical. After that, newDefaultAnnotationDriver just creates a new AnnotationDriver().

Symfony2 explanation of CompilerPass?

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;
}

Categories