How can I pass a dynamic dependency from one registered container definition to another? In this case, a generic Database object wants to inherit from a generic Config object. The twist is config is not static, but loaded depending on a given environment variable.
Config pertinent methods
public function __construct()
{
$configFile = 'example.config.yml';
$yamlParser = new Parser();
$reader = new Config\Reader($yamlParser);
$configYaml = $reader->parse(file_get_contents($configFile));
$config = new Config\Environment(getenv('SITE'), $configYaml);
$this->config = $config;
}
public function getEnvironmentConfig()
{
return $this->config;
}
Registering config is as simple as
$container->register('config', 'Config');
Database is currently added to the container as follows:
$container
->register('database', 'Database')
->addArgument($config->getEnvironmentConfig('Database', 'db.username'))
->addArgument($config->getEnvironmentConfig('Database', 'db.password'))
;
But I want to do something like
$container
->register('database', 'Database')
->addArgument(new Reference('config')->getEnvironmentConfig('Database', 'db.username'))
->addArgument(new Reference('config')->getEnvironmentConfig('Database', 'db.password'))
;
The $config in-PHP variable makes migrating from a PHP-built config impossible. I want to define the services in yaml force the container to:
Instantiate Config
Parse the config yaml file and create an environment-specific version
Return this on a call to getEnvironmentConfig
Is this possible?
This was solved by using the Expression Language Component
So you can easily chain method calls, for example:
use Symfony\Component\ExpressionLanguage\Expression;
$container->register('database', 'Database')
->addArgument(new Expression('service("config").getEnvironmentConfig("Database", "db.username")'));
Related
I'm using Monolog to create my app's logging system. In the core app file, after I create a new Monolog object, I need to select the log level that I want to print in the log file. I want to use a global constant LOG_LEVEL which could be 'DEBUG', 'INFO', etc. I need the Monolog class to treat its value as a class constant.
// content of config.php
// Here I declare the constants in a separate file called 'config.php'
define("LOG_FILE", "patch/to/my/log.log");
define("LOG_LEVEL", "ERROR");
// content of app.php
require 'config.php';
require 'vendor/autoload.php';
$container['logger'] = function($c) {
$logger = new \Monolog\Logger('logger');
error_log('log level ' . LOG_LEVEL); // prints 'log level ERROR'
$fileHandler = new \Monolog\Handler\StreamHandler(LOG_FILE, $logger::LOG_LEVEL); // here I get the error 'Undefined class constant LOG_LEVEL'
//the normal syntax would be '$logger::ERROR' in this case and that works fine
$logger->pushHandler($fileHandler);
return $logger;
};
I need the 'LOG_LEVEL' constant to be used as 'ERROR' by the monolog class, not as 'LOG_LEVEL'. What am I doing wrong here, been searching an answer for hours now without any luck.
You are now doing $logger::LOG_LEVEL, which is taking the 'LOG_LEVEL' out of the class whichever $logger is (in this case a \Monolog\Logger). That doesn't have a static variable named LOG_LEVEL, thus you get the undefined.
You have just have 'LOG_LEVEL' defined, out of any class, so:
$fileHandler = new \Monolog\Handler\StreamHandler(LOG_FILE, LOG_LEVEL);
Fancy solution:
You could do a static class and include that in your main page:
Class CONFIG {
public static $LOG_LEVEL = 'default Value';
}
// Then you can use this anywhere:
CONFIG::$LOG_LEVEL
$fileHandler = new \Monolog\Handler\StreamHandler(LOG_FILE, CONFIG::$LOG_LEVEL);
The advantage of this is having only one file for configs, not scattered across all kinds of files, which'll become very annoying very fast.
Make a static class and include that...
class GLOBALCONF{
public static $VALUE= 'Something in here';
}
// Use it where you want
GLOBALCONF::$VALUE
You're making this more complicated than it needs to be. Monolog has a function to convert an error level as as string to its own internal value. Just change your code to this:
$fileHandler = new \Monolog\Handler\StreamHandler(LOG_FILE, $logger::toMonologLevel(LOG_LEVEL));
You can also use Logger::getLevels() like the following:
$log_level = $logger->getLevels()[LOG_LEVEL];
$fileHandler = new ...StreamHandler(LOG_FILE, $log_level);
I filled a bug but it seams I'm off :p
I just want to replace the service Symfony\Component\Translation\Reader\TranslationReader (translation.reader) with my own class. In fact I want to know how to replace any service of SF4 if I want
translation.reader::addLoader() is normally called by the framework but if I decorate with my own class addLoader is not called.
Can you tell me how I can just drop replace my own service ?
https://github.com/symfony/symfony/issues/28843
Symfony version(s) affected: 4.1.6
Description
Cannot decorate translation.reader (I want to change the default i18n file loading process)
How to reproduce
copy/adapt Symfony\Component\Translation\Reader\TranslationReader to App\Translation\Reader\TranslationReader
Follow https://symfony.com/doc/current/service_container/service_decoration.html
Modify services.yaml
Symfony\Component\Translation\Reader\TranslationReader: ~
App\Translation\Reader\TranslationReader:
decorates: Symfony\Component\Translation\Reader\TranslationReader
#translation.reader: '#App\Translation\Reader\TranslationReader'
Without the alias : the new service is ignored
With the alias : read() is trigger but not addLoader()
Here are the generated injection file getTranslationReaderService.php :
<?php
use Symfony\Component\DependencyInjection\Argument\RewindableGenerator;
use Symfony\Component\DependencyInjection\Exception\RuntimeException;
// This file has been auto-generated by the Symfony Dependency Injection Component for internal use.
// Returns the private 'App\Translation\Reader\TranslationReader' shared autowired service.
include_once $this->targetDirs[3].'/vendor/symfony/translation/Reader/TranslationReaderInterface.php';
include_once $this->targetDirs[3].'/src/Translation/Reader/TranslationReader.php';
return $this->privates['App\Translation\Reader\TranslationReader'] = new \App\Translation\Reader\TranslationReader();
By default it looks like :
use Symfony\Component\DependencyInjection\Argument\RewindableGenerator;
use Symfony\Component\DependencyInjection\Exception\RuntimeException;
// This file has been auto-generated by the Symfony Dependency Injection Component for internal use.
// Returns the private 'translation.reader' shared service.
include_once $this->targetDirs[3].'/vendor/symfony/translation/Reader/TranslationReaderInterface.php';
include_once $this->targetDirs[3].'/vendor/symfony/translation/Reader/TranslationReader.php';
include_once $this->targetDirs[3].'/vendor/symfony/translation/Loader/LoaderInterface.php';
include_once $this->targetDirs[3].'/vendor/symfony/translation/Loader/ArrayLoader.php';
include_once $this->targetDirs[3].'/vendor/symfony/translation/Loader/FileLoader.php';
include_once $this->targetDirs[3].'/vendor/symfony/translation/Loader/PhpFileLoader.php';
include_once $this->targetDirs[3].'/vendor/symfony/translation/Loader/YamlFileLoader.php';
include_once $this->targetDirs[3].'/vendor/symfony/translation/Loader/XliffFileLoader.php';
include_once $this->targetDirs[3].'/vendor/symfony/translation/Loader/PoFileLoader.php';
include_once $this->targetDirs[3].'/vendor/symfony/translation/Loader/MoFileLoader.php';
include_once $this->targetDirs[3].'/vendor/symfony/translation/Loader/QtFileLoader.php';
include_once $this->targetDirs[3].'/vendor/symfony/translation/Loader/CsvFileLoader.php';
include_once $this->targetDirs[3].'/vendor/symfony/translation/Loader/IcuResFileLoader.php';
include_once $this->targetDirs[3].'/vendor/symfony/translation/Loader/IcuDatFileLoader.php';
include_once $this->targetDirs[3].'/vendor/symfony/translation/Loader/IniFileLoader.php';
include_once $this->targetDirs[3].'/vendor/symfony/translation/Loader/JsonFileLoader.php';
$this->privates['translation.reader'] = $instance = new \Symfony\Component\Translation\Reader\TranslationReader();
$a = ($this->privates['translation.loader.yml'] ?? $this->privates['translation.loader.yml'] = new \Symfony\Component\Translation\Loader\YamlFileLoader());
$b = ($this->privates['translation.loader.xliff'] ?? $this->privates['translation.loader.xliff'] = new \Symfony\Component\Translation\Loader\XliffFileLoader());
$instance->addLoader('php', ($this->privates['translation.loader.php'] ?? $this->privates['translation.loader.php'] = new \Symfony\Component\Translation\Loader\PhpFileLoader()));
$instance->addLoader('yaml', $a);
$instance->addLoader('yml', $a);
$instance->addLoader('xlf', $b);
$instance->addLoader('xliff', $b);
$instance->addLoader('po', ($this->privates['translation.loader.po'] ?? $this->privates['translation.loader.po'] = new \Symfony\Component\Translation\Loader\PoFileLoader()));
$instance->addLoader('mo', ($this->privates['translation.loader.mo'] ?? $this->privates['translation.loader.mo'] = new \Symfony\Component\Translation\Loader\MoFileLoader()));
$instance->addLoader('ts', ($this->privates['translation.loader.qt'] ?? $this->privates['translation.loader.qt'] = new \Symfony\Component\Translation\Loader\QtFileLoader()));
$instance->addLoader('csv', ($this->privates['translation.loader.csv'] ?? $this->privates['translation.loader.csv'] = new \Symfony\Component\Translation\Loader\CsvFileLoader()));
$instance->addLoader('res', ($this->privates['translation.loader.res'] ?? $this->privates['translation.loader.res'] = new \Symfony\Component\Translation\Loader\IcuResFileLoader()));
$instance->addLoader('dat', ($this->privates['translation.loader.dat'] ?? $this->privates['translation.loader.dat'] = new \Symfony\Component\Translation\Loader\IcuDatFileLoader()));
$instance->addLoader('ini', ($this->privates['translation.loader.ini'] ?? $this->privates['translation.loader.ini'] = new \Symfony\Component\Translation\Loader\IniFileLoader()));
$instance->addLoader('json', ($this->privates['translation.loader.json'] ?? $this->privates['translation.loader.json'] = new \Symfony\Component\Translation\Loader\JsonFileLoader()));
return $instance;
You can see that loaders are not injected when I do the decorating...
I'm not sure exactly if this is the root of your problem, but here are some remarks. Hopefully this will help you find a solution, even though I'm not actually given a full answer to your question.
1) Some translation services in Symfony are called only during the cache warmup phase. Whenever you change your config, or do a bin/console cache:clear, you'll see these classes are run, and they generate translations in your var/cache/<env>/translations/ folder.
2) You can try to make sure that in your cache, the classe loaded by var/cache/<env>/Container<...>/getTranslation_ReaderService.php is yours and not the default one like this:
$this->privates['translation.reader'] =
new \Symfony\Component\Translation\Reader\TranslationReader();
3) I also encountered a similar issue in the dev environment, where I was trying to replace Symfony\Component\Translation\Translator with my own service, and didn't manage to get my methods to be called at first. Part of the explanation was that when the Symfony Profiler is enabled, Symfony does something like this (in src<env>DebugProjectContainer.php>):
$this->services['translator'] = new \Symfony\Component\Translation\DataCollectorTranslator(
($this->privates['translator.default'] ?? $this->getTranslator_DefaultService())
);
and the DataCollectorTranslator itself is a wrapper for whichever translator it gets as its constructor argument.
I know this is not a perfect answer but hopefully this will help you find your way to a solution.
I've managed to make it work... but please feel free to comment
I had to create a TranslatorPass to add loaders to the decorating service injection file.
<?php
namespace App\Translation\DependencyInjection;
use Symfony\Component\DependencyInjection\Compiler\CompilerPassInterface;
use Symfony\Component\DependencyInjection\ContainerBuilder;
use Symfony\Component\DependencyInjection\Reference;
use App\Translation\Reader\TranslationReader;
class TranslatorPass implements CompilerPassInterface
{
private $readerServiceId;
private $loaderTag;
public function __construct(string $readerServiceId = TranslationReader::class, string $loaderTag = 'translation.loader')
{
$this->readerServiceId = $readerServiceId;
$this->loaderTag = $loaderTag;
}
public function process(ContainerBuilder $container)
{
$loaders = array();
$loaderRefs = array();
foreach ($container->findTaggedServiceIds($this->loaderTag, true) as $id => $attributes) {
$loaderRefs[$id] = new Reference($id);
$loaders[$id][] = $attributes[0]['alias'];
if (isset($attributes[0]['legacy-alias'])) {
$loaders[$id][] = $attributes[0]['legacy-alias'];
}
}
if ($container->hasDefinition($this->readerServiceId)) {
$definition = $container->getDefinition($this->readerServiceId);
foreach ($loaders as $id => $formats) {
foreach ($formats as $format) {
$definition->addMethodCall('addLoader', array($format, $loaderRefs[$id]));
}
}
}
}
}
I've put it in the Kernel.php
protected function build(ContainerBuilder $container)
{
parent::build($container);
$container->addCompilerPass(new TranslatorPass(), PassConfig::TYPE_BEFORE_OPTIMIZATION, 1000);
}
then
bin/console cache:clear
et voilĂ !
I have a long list of dependency injections to display a page with an article, navigation, etc. And currently I put them in a file called index.php to glue them together.
index.php,
use MyCustomVerndorName\Constant\Mapper\ConstantsMapper;
use MyCustomVerndorName\Slug\Mapper\SlugMapper;
.... (more)
$ConstantService = new ConstantService();
$ConstantController = new ConstantController();
$ArticleService = new ArticleService();
$ArticleController = new ArticleController();
// Prepare Nav model.
$NavModel = new NavModel();
$NavMapper = new NavMapper($PdoAdapter);
$NavService->setMapper($NavMapper)->setModel($NavModel);
// Prepare Article model.
$ArticleModel = new ArticleModel();
$ArticleMapper = new ArticleMapper($PdoAdapter);
// Prepare components.
$ArticleContentComponent = new ArticleContentComponent($PdoAdapter);
... (more)
// Inject components.
$ArticleMapper->addComponent($ArticleContentComponent);
... (more)
$NavChildrenComponent = new NavChildrenComponent($PdoAdapter);
... (more)
// Inject components.
$NavMapper->addComponent($NavChildrenComponent);
$NavMapper->addComponent($NavLanguageComponent);
// Controll the slug.
$SlugController->setService($SlugService)->fetchRow([
"url" => $url
]);
// Control the nav.
$NavController->setService($NavService)->fetchRows();
// Controll the article.
$ArticleService->setMapper($ArticleMapper)->setModel($ArticleModel);
$ArticleController->setService($ArticleService)->fetchRow([
"url" => $url
]);
// Prepare template.
$PageTemplate = new PageTemplate();
// Prepare view.
$ArticleView = new ArticleView($PageTemplate, $ArticleModel);
$ArticleView->setSlug($SlugModel);
$ArticleView->setNav($NavModel);
// Render the view.
echo $ArticleView->render('index.php');
in my router.php (I'm using AltoRouter),
use AltoRouter as Router;
$Router = new Router();.
$Router->map('GET', '/', '/article/container');
... (and other routes)
if($match)
{
$target = $match['target'];
$url = isset($match['params']['url']) ? $match['params']['url'] : DEFAULT_HOMEPAGE;
$language = isset($match['params']['language']) ? $match['params']['language'] : null;
include __DIR__ . $target . '.php';
}
I'm thinking to make the list of dependency injections in index.php into a container class so I can call this class whenever it is needed,
For instace in the router.php,
$Router->map('GET', '/', 'MyCustomVerndorName\Article\Container\ArticleContainer');
....
new $target($PdoAdapter, $url, $language);
And this container class,
namespace MyCustomVerndorName\Article\Container;
// Mapper.
use MyCustomVerndorName\Constant\Mapper\ConstantsMapper;
...
class ArticleContainer
{
/*
* Construct dependency.
*/
public function __construct(\MyCustomVerndorName\Adapter\PdoAdapter $PdoAdapter, $url, $language)
{
$ConstantService = new ConstantService();
$ConstantController = new ConstantController();
// Slug.
$SlugService = new SlugService();
$SlugController = new SlugController();
// Nav.
$NavService = new NavService();
$NavController = new NavController();
// Article.
$ArticleService = new ArticleService();
$ArticleController = new ArticleController();
// Prepare Article model.
$ArticleModel = new ArticleModel();
$ArticleMapper = new ArticleMapper($PdoAdapter);
// Prepare components.
$ArticleContentComponent = new ArticleContentComponent($PdoAdapter);
...
// Inject components.
$ArticleMapper->addComponent($ArticleContentComponent);
...
// Control the nav.
$NavController->setService($NavService)->fetchRows();
// Controll the article.
$ArticleService->setMapper($ArticleMapper)->setModel($ArticleModel);
$ArticleController->setService($ArticleService)->fetchRow([
"url" => $url
]);
// Prepare template.
$PageTemplate = new PageTemplate();
// Prepare view.
$ArticleView = new ArticleView($PageTemplate, $ArticleModel);
$ArticleView->setSlug($SlugModel);
$ArticleView->setNav($NavModel);
// Render the view.
echo $ArticleView->render('index.php');
}
}
Basically, I put all the dependency list into __construct,
public function __construct(\MyCustomVerndorName\Adapter\PdoAdapter $PdoAdapter, $url, $language)
{
...
}
So, is this the correct way of doing a DI Container without relying on the well known containers out there for PHP (such as Pimple, Symfony\DependencyInjection, etc)?
Traditionally, a DI container returns services upon demand. What you are calling ArticleContainer is really just a function then renders a view. It does a lot of DI injecting but it's not a container.
Let's assume you had a real container all properly initialized. Your code would look like:
$container = new Container();
// Some initialization
// Now use it
$viewArticle = $container->get('view_article');
echo $viewArticle->render('index.php');
Hopefully you can see the difference. Once the initialization is complete your application just pulls out the view it needs and renders it. No need to worry about pdo or url etc.
So how do we get our article view service into the container?
$container = new Container();
// Some initialization
$container->set('page_template',function($container)
{
return new PageTemplate();
}
$container->set('article_view',function($container)
{
$articleView = new ArticleView(
$container->get('page_template',
$container->get('article_model')
);
$articleView->setSlug($container->get('slug_model');
$articleView->setNav ($container->get('nav_model');
return $articleView;
};
// Now use it
$viewArticle = $container->get('article_view');
echo $viewArticle->render('index.php');
So we defined two service functions for our container. When we do $container->get('article_view');, the container calls the function with the container itself as an argument. The function creates the desired class by pulling dependencies out of the container and then returns the new object.
One assumes that you will probably have more page views in your application and that most of them will need a page_template. The same page_template service we defined can be used by your other views as well. So we start to get some reuse out of the container.
You basically just continue to add services to the container.
You can use Pimple for this but it's also instructive to make your own container. Really not much to it.
Hi,
In Zend Framwork 1, I used to have an application\configs\appsettings.xml, where I used to store params and values like hostnames for Rest API URLs, debug settings and other application specific settings for dev, test and prod environments. This registry was available to me across all controllers and models and was created in index.php
$applicationEnvironment = 'development';
$config = new Zend_Config_Xml( APPLICATION_PATH . '/configs/appsettings.xml',
$applicationEnvironment, true );
Zend_Registry::set( 'config', $config );
How do I achieve similar thing in Zend Framework 2?
Thanks
There is no such thing as a Registry in ZF2 because it is kind of an anti pattern. It's just a fancy substitution for global variables, which can cause all sort of unwanted side effects in your application.
In ZF2 you have the serviceManager and this allow to cleanly inject all your dependencies into your controllers/models/services. All config files in the config/autoload directory are automaticaly merged into one single array by ZF2 and you can retrieve this from the service manager using $serviceLocator->get('Config'). Whenever you need to use configuration in your controller just create a serviceFactory and inject the config.
class FooController
{
protected $config;
public __construct($config)
{
$this->config = $config;
}
public barAction()
{
//use $this->config
}
}
class Module
{
public function getControllerConfig()
{
return array(
'factories' => array(
'fooController' => function(ControllerManager $cm)
{
$sm = $cm->getServiceLocator();
$config = $sm->get('Config');
$controller = new FooController($config);
return $controller;
},
),
);
}
}
For sake of simplicity the factory above is defined as a closure, but I'd suggest to create a seperate factory class. There are many resources which explain how to do that.
In this example we are injecting the complete configuration, but depending on your use case it will generally be better to only inject the config keys you need.
Alternatively you can wrap certain config values into a dedicated config object with explicit getters and setters and inject this into your controller. Zend\StdLib\AbstractOptions can help you.
If you wish to work with config files and you dont have access to the Service Manager or you wish to write content to it, you can use Zend\Config
To read from, you can do something like:
$config = new Config(include 'config/autoload/my_amazing_config.global.php');
$details = $config->get('array_key')->get(sub_key)->toArray();
To write to, you can do:
// Create the config object
$config = new Zend\Config\Config(array(), true);
$config->production = array();
$config->production->webhost = 'www.example.com';
$config->production->database = array();
$config->production->database->params = array();
$config->production->database->params->host = 'localhost';
$config->production->database->params->username = 'production';
$config->production->database->params->password = 'secret';
$config->production->database->params->dbname = 'dbproduction';
$writer = new Zend\Config\Writer\Xml();
echo $writer->toString($config);
The class support ini, xml, phpArray, json, yaml
You can read more at:
http://framework.zend.com/manual/2.2/en/modules/zend.config.introduction.html
As the title says there is a problem accessing variable (associative array) inside class from included file. Here is the source code both class and include file:
require("applications/cw_database.php");
require("config/dbConfig.php");
require("config/appConfig.php");
class APP_ASSESMENTS
{
private $dbObj;
private $DisplayOutput = "";
public function __construct($PageParams)
{
try
{
$dbObj = new CW_DB($dbConfig['hostname'],$dbConfig['username'],$dbConfig['password'],$dbConfig['name'],$dbConfig['port']);
} catch (Exception $e) {
throw new ErrorException($e);
}
}
...
The other part has nothing to do with $dbConfig.
Also this is the included file (config/dbConfig.php):
/*
Testing configuration for MySQL database
*/
$dbConfig['username'] = "phpcoursework"; // changed on demand
$dbConfig['password'] = "phpcoursework"; // changed on demand
$dbConfig['hostname'] = "localhost"; // changed on demand
$dbConfig['name'] = "students"; // changed on demand
$dbConfig['port'] = 3306; // default for MySQL
First, $dbObj will not automatically assume class member scope, it will create a local copy of CW_DB and discard it when __construct returns. You need to explicitly reference the property;
$this->dbObj = ...
Anyway, global state using global as suggested by others will "work", but if you're using OOP practices you're best not to do that. You can actually return from an include(), so an option would be to do the following:
// your config file dbConfig.php
return array(
'username' => "phpcoursework",
'password' => "phpcoursework",
'hostname' => "localhost",
'name' => "students",
'port' => 3306,
);
And inject it into the object, via constructor or method (here's constructor)
class APP_ASSESMENTS
{
private $dbObj;
public function __construct($dbConfig, $PageParams)
{
$dbObj = new CW_DB($dbConfig['hostname'], $dbConfig['username'],
$dbConfig['password'], $dbConfig['name'], $dbConfig['port']);
// ...
}
}
// include() here, will actually return the array from the config file
$appAssesments = new \APP_ASSESMENTS(include('dbConfig.php'), $PageParams);
It would be recommended that you go another level up: instead, inject the database object itself, taking the dependency out of your APP_ASSESSMENTS class.
(Also, PascalCase is the typical convention of class naming, such as AppAssessments and CwDb)
$dbObj = new CwDb(include('dbConfig.php'));
$appAssessments = new AppAssessments($dbObj, $etc, $etc);
This simple change allows you to remove the dependency from AppAssessments on CwDb. That way, if you extend CwDb for some reason, you can just pass in an instance of the extended class without having to change any code in AppAssessments
You can change the AppAssessments constructor like so:
public function __construct(CwDb $db, $etc, $etc){
$this->db = $db;
// ...
}
This takes advantage of PHPs (limited, albeit still useful) type-hinting, ensuring the first argument is always of the correct type.
This plays into part of the open/closed principle: classes should be open to extension but closed for modification.
Includes are used in the scope of access. So, to access the variables you need to include the files within your class. As earlier mentioned global will let you access variables from another scope too. But global should be used with caution! See the documentation.
See the manual for more information.
Edit: I need to make it clear that globals are never a good alternative for handling these kind of critical variables..
public function __construct($PageParams){
global $dbConfig;
try{
$dbObj = new CW_DB($dbConfig['hostname'],$dbConfig['username'],$dbConfig['password'],$dbConfig['name'],$dbC onfig['port']);
} catch (Exception $e) {
throw new ErrorException($e);
}
}
or you could use $GLOBALS['dbConfig'].