DIC and Parameters - symfony

There's something that I'm missing.
Let's say I have a database from wich I start a new project. This database will be created after restoring a .dump file so it will also contain some "static" (static inserts from scratch db to populate it with "unchangeable" data) information inside it.
As this information will never change and as I need some of them (i.e.: an id for retrieve exactly a specific entity) I've thought to place them into a configuration file that is processed with the DIC & co.
First question: is this approach right or is better to create a specific .php file with some configuration values?
Second question: Why parameters I've defined through DIC aren't available?
I've create a folder called DependencyInjection inside my bundle, I've created MyCompanyMyBundleNameExtension file and create a Configuration file. I'm sure that files are placed in the right place as If I "remove" them all the things will mess up (just for people who will comment with "are you sure that ..." yes, I'm sure) I've also included inside my bundle "main" file the following
public function build(ContainerBuilder $container)
{
parent::build($container);
}
but if I dump (I use this approach as inside controller the exception "The parameter "xxxxx" must be defined. is raised") the config file inside Extension I can notice that is empty. Have I missing something?
UPDATE
This is Configuration.php file
namespace Foo\FooBundle\DependencyInjection;
use Symfony\Component\Config\Definition\ConfigurationInterface;
use Symfony\Component\Config\Definition\Builder\TreeBuilder;
class Configuration implements ConfigurationInterface
{
/**
* Generates the configuration tree.
*
* #return TreeBuilder
*/
public function getConfigTreeBuilder()
{
$tree_builder = new TreeBuilder();
$root_node = $tree_builder->root('foo_bundle');
$root_node
->children()
->arrayNode('text_type')
->children()
->integerNode('title')->defaultValue(1)->end()
->integerNode('description')->defaultValue(2)->end()
->end()
->end()
->end()
;
return $tree_builder;
}
}
and this is the Extension
<?php
namespace Foo\BookingEngineBundle\DependencyInjection;
use Symfony\Component\HttpKernel\DependencyInjection\Extension;
use Symfony\Component\DependencyInjection\ContainerBuilder;
use Symfony\Component\Config\Definition\Processor;
class FooFooBundleExtension extends Extension
{
public function load(array $configs, ContainerBuilder $container)
{
$processor = new Processor();
$configuration = new Configuration();
$config = $processor->processConfiguration($configuration, $configs);
error_log(print_r($config, true)); //empty
$this->remapParametersNamespaces($config, $container, array(
'' => array(
'text_type' => 'foo_bundle.text_type.%s',
),
));
}
protected function remapParametersNamespaces(array $config, ContainerBuilder $container, array $namespaces)
{
foreach ($namespaces as $ns => $map) {
if ($ns) {
if (!array_key_exists($ns, $config)) {
continue;
}
$namespaceConfig = $config[$ns];
} else {
$namespaceConfig = $config;
}
if (is_array($map)) {
$this->remapParameters($namespaceConfig, $container, $map);
} else {
foreach ($namespaceConfig as $name => $value) {
$container->setParameter(sprintf($map, $name), $value);
}
}
}
}
protected function remapParameters(array $config, ContainerBuilder $container, array $map)
{
foreach ($map as $name => $paramName) {
if (array_key_exists($name, $config)) {
$container->setParameter($paramName, $config[$name]);
}
}
}
UPDATE 2
If I run from command line the following php app/console config:dump-reference foo_bundle I can see configuration printed out. I'm pretty confused.
UPDATE 3
I got it finally:
it seems that you need to specify at least one parameter into your config.yml main file, otherwise the "merge" option performed into YourBundleNameExtension will fail and return an empty array.
Subquestion: there isn't a method that I can follow to free me from write parateter into parameters.yml?

I got it finally:
it seems that you need to specify at least one parameter (related to your Config D.I. file) into your config.yml main file, otherwise the "merge" option performed into YourBundleNameExtension will fail and return an empty array.
Looking at my Configuration.php file, in order to make the code works without specify parameter directly into parameters.yml file, modify Configuration.php that way:
<?php
namespace Koobi\BookingEngineBundle\DependencyInjection;
use Symfony\Component\Config\Definition\ConfigurationInterface;
use Symfony\Component\Config\Definition\Builder\TreeBuilder;
class Configuration implements ConfigurationInterface
{
/**
* Generates the configuration tree.
*
* #return TreeBuilder
*/
public function getConfigTreeBuilder()
{
$tree_builder = new TreeBuilder();
$root_node = $tree_builder->root('koobi_booking_engine');
$root_node
->children()
->arrayNode('text_type')
->cannotBeOverwritten()
->addDefaultsIfNotSet()
->children()
->integerNode('title')->defaultValue(1)->end()
->integerNode('description')->defaultValue(2)->end()
->end()
->end()
->end()
;
return $tree_builder;
}
}
the key is ->addDefaultsIfNotSet() builder's method
How do I went to solution?
Look at Extension file: you should notice the
$config = $processor->processConfiguration($configuration, $configs);
snippet of code.
If you open use Symfony\Component\Config\Definition\Processor; and analyze processConfiguration() method, you'll see that $configs - that represent config.yml content - will be merged to $configuration (that represent Configuration.php, so the DIC file that extenion will manage).
So the idea that a common key should be present somewhere came in my mind: after that successful check I've started to search for some methodology that helps me avoid the explicit process of writing parameters into config.yml (that wasn't what i want) and now all works like a charm
SIDE NOTE
I still don't know if this is a valid approach so I'm not gonna check this as correct answer.

Related

How to provide default values for Symfony bundle parameters / config values?

I have created a Symfony 5.3+ bundle which should be used to add common code to different projects. The bundle contains some services which should be configurable using parameters / options as described in the Symfony docs.
How to provide default values for these options? Defaults set in the bundles Configuration.php do not have any effect.
Details:
I have created a bundle project using the following structure and added it to my Symfony project using composer:
path/to/bundles/XYCommonsBundle/
config/
services.yaml
src/
Service/
SomeService.php
DependencyInjection
Configuration.php
XYCommensExtension.php
XYCommensBundle.php
composer.json
...
// src/DependencyInjection/XYCommensExtension.php
<?php
namespace XY\CommensBundle\DependencyInjection;
use ...
class XYCommensExtension extends Extension {
public function load(array $configs, ContainerBuilder $container) {
$configuration = new Configuration();
$config = $this->processConfiguration($configuration, $configs);
// make config available as parameters. Necessary?
foreach ($config as $key => $value) {
$container->setParameter('xy_commons.' . $key, $value);
}
$loader = new YamlFileLoader($container, new FileLocator(__DIR__.'/../../config'));
$loader->load('services.yaml');
}
}
// src/DependencyInjection/Configuration.php
class Configuration implements ConfigurationInterface {
public function getConfigTreeBuilder() {
$treeBuilder = new TreeBuilder('xy_commons');
$treeBuilder->getRootNode()
->children()
->arrayNode('params')
->children()
->integerNode('paramA')->defaultValue(100)->end()
->integerNode('ParamB')->defaultValue(200)->end()
->end()
->end()
->end()
;
return $treeBuilder;
}
}
// config/services.yaml
services:
xy_commons.service.some_service:
class: XY\CommonsBundle\Service\SomeService
arguments:
- $paramA: '%xy_commons.params.paramA%'
- $paramB: '%xy_commons.params.paramB%'
// src/Service/SomeService.php
<?php
namespace XY\CommensBundle\Service;
use ...
class SomeService {
public function __construct(LoggerInterface $logger, $paramA, $paramB) {
}
Problem: How to use default values of parameters?
paramA and paramB are defined in the bundles Configuration.php with default values of 100 and 200. I would like to use these defaults in the project without specifying custom values. However, I do not create a config/packages/xy_commons.yaml file in the project and explicitly specify values, I get the following error:
You have requested a non-existent parameter
"xy_commons.params.paramA".
When creating a config/packages/xy_commons.yaml file, I cannot use ~ to use the default value:
xy_commons:
params:
paramA: ~
paramB: ~
Invalid type for path "xy_commons.params.paramA". Expected "int", but
got "null".
Only when explicitly specifying a value it works:
xy_commons:
params:
paramA: 300
paramB: 400
How to use the default values defined in Configuration.php?
There are three problems here. The first deals with what is possibly my least favorite class in Symfony. The dreaded configuration object. In particular, you need to use addDefaultsIfNotSet when dealing with arrays:
class Configuration implements ConfigurationInterface
{
public function getConfigTreeBuilder()
{
$treeBuilder = new TreeBuilder('my');
$rootNode = $treeBuilder->getRootNode();
$rootNode
->children()
->arrayNode('params')->addDefaultsIfNotSet() # ADD THIS
->children()
->integerNode('paramA')->defaultValue(100)->end()
->integerNode('paramB')->defaultValue(200)->end()
->end()
->end()
->end()
;
return $treeBuilder;
}
}
The second problem is that you are not defining your parameters correctly. In particular the parameters will be grouped in an array called params. You almost had it:
class MyExtension extends Extension
{
public function load(array $configs, ContainerBuilder $container)
{
$configuration = new Configuration();
$config = $this->processConfiguration($configuration, $configs);
foreach($config['params'] as $key => $value) {
$container->setParameter('xy_commons.params.' . $key, $value);
}
# Use bin/console dump:container --parameters to verify
The final thing is the use of tilde to indicate default values. I think this might be a works as designed issue. Technically, tilde on yaml means null and it is up to the processor to get it a meaning. The ArrayNode works as expected but the IntegerNode does not. So this works:
# config/packages/my.yaml
my:
params: ~
# paramA: ~
# paramB: 666

How to load, process and use custom parameters from Yaml configuration files in DI Extension class?

I'm trying to import a yaml configuration file in my App following the documentation provided here http://symfony.com/doc/current/bundles/extension.html but I always have the error message:
There is no extension able to load the configuration for "app"
My file is located here : config/packages/app.yaml and has the following structure :
app:
list:
model1:
prop1: value1
prop2: value2
model2:
...
As this is a simple App, all the files are in src/. So I have src/DependencyInjection/AppExtension.php
<?php
namespace App\DependencyInjection;
use Symfony\Component\DependencyInjection\ContainerBuilder;
use Symfony\Component\Config\FileLocator;
use Symfony\Component\HttpKernel\DependencyInjection\Extension;
use Symfony\Component\DependencyInjection\Loader;
class AppExtension extends Extension
{
public function load(array $configs, ContainerBuilder $container)
{
$configuration = new Configuration();
$config = $this->processConfiguration($configuration, $configs);
}
}
And src/DependencyInjection/Configuration.php
<?php
namespace App\DependencyInjection;
use Symfony\Component\Config\Definition\Builder\TreeBuilder;
use Symfony\Component\Config\Definition\ConfigurationInterface;
class Configuration implements ConfigurationInterface
{
public function getConfigTreeBuilder()
{
$treeBuilder = new TreeBuilder();
$rootNode = $treeBuilder->root('app');
// Node definition
$rootNode
->children()
->arrayNode('list')
->useAttributeAsKey('name')
->requiresAtLeastOneElement()
->prototype('array')
->children()
->requiresAtLeastOneElement()
->prototype('scalar')
->end()
->end()
->end()
->end()
->end();
return $treeBuilder;
}
}
I'm not able to access my parameters :(
Any idea?
If you want to load a custom configuration file to process it's parameters using an Extension class (like in Symfony bundle extension but without to create a bundle), to eventually "create" and add one or more of it to the "container" (before it will be compiled) you can register your Extension class manually in the configureContainer method contained in the Kernel.php file:
protected function configureContainer(ContainerBuilder $container, LoaderInterface $loader)
{
// to avoid the same error you need to put this line at the top
// if your file is stored under "$this->getProjectDir().'/config'" directory
$container->registerExtension(new YourAppExtensionClass());
// ----- rest of the code
}
Then you can use your params as usual registering a Compiler Pass.
Hope this helps.

Symfony2: addMethodCall doesn't work in Extension

I must be missing something. I have a "ConfigBundle" inside of which I have this services.yml (in Acme/ConfigBundle/Resources/config):
services:
config.manager:
class: Acme\ConfigBundle\Manager\ConfigManager
arguments: ["#doctrine.orm.entity_manager", "#config.storage"]
This service works as expected.
Next I have a Configuration.php:
class Configuration implements ConfigurationInterface
{
/**
* {#inheritdoc}
*/
public function getConfigTreeBuilder()
{
$treeBuilder = new TreeBuilder();
$rootNode = $treeBuilder->root('config');
$rootNode
->children()
->scalarNode('storage')->isRequired()->cannotBeEmpty()->end()
->variableNode('defaults')->end()
->end();
;
return $treeBuilder;
}
}
Which also works as expected. And at the end I have ConfigExtensions.php:
class ConfigExtension extends ConfigurableExtension
{
/**
* {#inheritdoc}
*/
protected function loadInternal(array $mergedConfig, ContainerBuilder $container)
{
// Set storage service.
$container->setAlias('config.storage', $mergedConfig['storage']);
if (!empty($mergedConfig['defaults'])) {
$loader = new YamlFileLoader($container, new FileLocator(__DIR__.'/../Resources/config'));
$loader->load('services.yml');
//$container->getDefinition('config.manager')->addArgument('test');
$container->getDefinition('config.manager')->addMethodCall('setDefaults', [$mergedConfig['defaults']]);
var_dump('It is being invoked!');
}
}
}
Which doesn't work as expected because method addMethodCall doesn't work. addMethodCall executes, I checked this, but the call is never invoked and is not visible in appDevDebugProjectContainer.xml in cache. When I add:
calls:
- [setDefaults, ['test']]
To my service definition in services.yml it gets invoked, but I cannot achieve it by using addMethodCall in Extension. I'm not getting any errors, I cleared cache directory, more than once, when trying to figure this out. Any ideas why this doesn't work?

How to get web directory path from inside Entity?

I researched the How to Handle File Uploads with Doctrine and I don't want to hard-code the __DIR__.'/../../../../web/'.$this->getUploadDir(); path because maybe in future I will change the web/ directory. How to do it more flexible? I found this but it doesn't answer the question how to do it more flexible from inside the Entity
You shouldn't use entity class as a form model here. It's simply not suitable for that job. If the entity has the path property, the only valid values it can stores are: null (in case lack of the file) and string representing the path to the file.
Create a separate class, that's gonna be a model for your form:
class MyFormModel {
/** #Assert\File */
private $file;
/** #Assert\Valid */
private $entity;
// constructor, getters, setters, other methods
}
In your form handler (separate object configured through DIC; recommended) or the controller:
...
if ($form->isValid()) {
/** #var \Symfony\Component\HttpFoundation\File\UploadedFile */
$file = $form->getData()->getFile();
/** #var \Your\Entity\Class */
$entity = $form->getData()->getEntity();
// move the file
// $path = '/path/to/the/moved/file';
$entity->setPath($path);
$someEntityManager->persist($entity);
return ...;
}
...
Inside form handler/controller you can access any dependencies/properties from DIC (including path to the upload directory).
The tutorial you've linked works, but it's an example of bad design. The entities should not be aware of file upload.
To access the root directory from outside the controller you can simply inject '%kernel.root_dir%' as an argument in your services configuration.
service_name:
class: Namespace\Bundle\etc
arguments: ['%kernel.root_dir%']
Then you can get the web root in the class constructor:
public function __construct($rootDir)
{
$this->webRoot = realpath($rootDir . '/../web');
}
You can use a variable in your parameters.yml.
Like this you'll can change path when you want.
for example :
# app/config/parameters.yml
# Upload directories
upload_avatar_dir: /uploads/avatars
upload_content_dir: /uploads/content
upload_product_offer_dir: /uploads/product-offer
...
I handled this by creating an abstract class that Entities may extend if they are handling file uploads as described in the Symfony Documentation. I created the files array so I could create a copy of the existing file path in the set methods so it could be deleted off the file system on a successful update or delete without defining any additional properties in the Entity proper.
use Symfony\Component\HttpFoundation\File\File;
abstract class FileUploadEntity
{
private $files;
public function __set($name, File $value)
{
$this->files[$name] = $value;
}
public function __get($name)
{
if (!is_array($this->files)) $this->files = array();
if (!array_key_exists($name, $this->files)) {
return null;
}
return $this->files[$name];
}
public function getUploadRootDirectory()
{
return $this->getWebDirectory() . $this->getUploadDirectory();
}
public function getWebDirectory()
{
return __DIR__ . "/../../../../web/";
}
public function getUploadDirectory()
{
$year = date("Y");
$month= date("m");
return "images/uploads/$year/$month/";
}
public function getEncodedFilename($name)
{
return sha1($name . uniqid(mt_rand(), true));
}
// this should be a PrePersist method
abstract public function processImages();
// This should be defined as a Doctrine PreUpdate Method
abstract public function checkImages();
// this should be a PostPersist method
abstract public function upload();
// this should be a PostUpdate method and delete old files
abstract public function checkUpload();
// This should be a PostRemove method and delete files
abstract public function deleteFile();
}

How do I parse a yaml file from within a service in Symfony2

I want to get an array from a yaml file inside one of my services, and I am a little confused of how to inject the file to use in my services.yml.
# /path/to/app/src/Bundle/Resources/config/services.yml
parameters:
do_something: Bundle\DoSomething
yaml.parser.class: Symfony\Component\Yaml\Parser
yaml.config_file: "/Resources/config/config.yml" # what do I put here to win!
services:
yaml_parser:
class: %yaml.parser.class%
do_parsing:
class: %do_something%
arguments: [ #yaml_parser, %yaml.config_file% ]
In my service I have
# /path/to/app/src/Bundle/DoSomething.php
<?php
namespace Bundle;
use \Symfony\Component\Yaml\Parser;
class DoSemething
{
protected $parser;
protected $parsed_yaml_file;
public function __construct(Parser $parser, $file_path)
{
$this->parsed_yaml_file = $parser->parse(file_get_contents(__DIR__ . $file_path));
}
public function useParsedFile()
{
foreach($parsed_yaml_file as $k => $v)
{
// ... etc etc
}
}
}
This may be the completely wrong approach, if I should be doing something else please let me know!
First I'll explain why I implemented my solution for you to decide if this case is right for you.
I needed a way to easily load custom .yml files in my bundle (for lots of bundles) so adding a separate line to app/config.yml for every file seemed like a lot of hassle for every setup.
Also I wanted most of the configs to be already loaded by default so end-user wouldn't even need to worry about configuring most of the time, especially not checking that every config file is setup correctly.
If this seems like a similar case for you, read on. If not, just use Kris solution, is a good one too!
Back when I encountered a need for this feature, Symfony2 didnt't provide a simple way to achieve this, so here how I solved it:
First I created a local YamlFileLoader class which was basically a dumbed down Symfony2 one:
<?php
namespace Acme\DemoBundle\Loader;
use Symfony\Component\Yaml\Yaml;
use Symfony\Component\Config\Loader\FileLoader;
/**
* YamlFileLoader loads Yaml routing files.
*/
class YamlFileLoader extends FileLoader
{
/**
* Loads a Yaml file.
*
* #param string $file A Yaml file path
*
* #return array
*
* #throws \InvalidArgumentException When config can't be parsed
*/
public function load($file, $type = null)
{
$path = $this->locator->locate($file);
$config = Yaml::parse($path);
// empty file
if (null === $config) {
$config = array();
}
// not an array
if (!is_array($config)) {
throw new \InvalidArgumentException(sprintf('The file "%s" must contain a YAML array.', $file));
}
return $config;
}
/**
* Returns true if this class supports the given resource.
*
* #param mixed $resource A resource
* #param string $type The resource type
*
* #return Boolean True if this class supports the given resource, false otherwise
*
* #api
*/
public function supports($resource, $type = null)
{
return is_string($resource) && 'yml' === pathinfo($resource, PATHINFO_EXTENSION) && (!$type || 'yaml' === $type);
}
}
Then I updated DIC Extension for my bundle (it's usually generated automatically if you let Symfony2 create full bundle architecture, if not just create a DependencyInjection/<Vendor&BundleName>Extension.php file in your bundle directory with following content:
<?php
namespace Acme\DemoBundle\DependencyInjection;
use Symfony\Component\DependencyInjection\ContainerBuilder;
use Symfony\Component\Config\FileLocator;
use Symfony\Component\HttpKernel\DependencyInjection\Extension;
use Symfony\Component\DependencyInjection\Loader;
use Acme\DemoBundle\Loader\YamlFileLoader;
/**
* This is the class that loads and manages your bundle configuration
*
* To learn more see {#link http://symfony.com/doc/current/cookbook/bundles/extension.html}
*/
class AcmeDemoExtension extends Extension
{
/**
* {#inheritDoc}
*/
public function load(array $configs, ContainerBuilder $container)
{
$configuration = new Configuration();
$config = $this->processConfiguration($configuration, $configs);
$loader = new Loader\XmlFileLoader($container, new FileLocator(__DIR__.'/../Resources/config'));
$loader->load('services.xml');
// until here everything is default config (for your DIC services)
$ymlLoader = new YamlFileLoader(new FileLocator(__DIR__.'/../Resources/config'));
$container->setParameter('param_name', $ymlLoader->load('yaml_file_name'))); // load yml file contents as an array
}
}
And now you can access/pass your yaml config as simple service parameter (i.e. %param_name% for services.yml)
I solved it this way:
Services.yml
#/path/to/app/src/Bundle/Resources/config/services.yml
parameters:
example.class: Path\To\Bundle\Service\Class
example.yaml_config_file: "%kernel.root_dir%/../src/Path/To/Bundle/Resources/config/config.yml"
services:
example_service:
class: %example.class%
arguments: [%example.yaml_config_file% ]
Service class
# /path/to/app/src/Bundle/Service/Example.php
<?php
namespace Bundle\Service;
use \Symfony\Component\Yaml\Yaml;
class Example
{
private $parsed_yaml_file;
public function __construct($yaml_config_file)
{
$this->parsed_yaml_file = Yaml::parse($yaml_config_file);
}
}
You can use the kernel.root_dir parameter:
parameters:
yaml.config_file: "%kernel.root_dir%/../src/Path/To/MyBundle/Resources/config/config.yml"
If you're using Symfony 3.3 or higher, you can now also use the new kernel.project_dir parameter.
This parameter points to the highest level directory containing a composer file.

Resources