I know that i is possible to add optional service dependency for a service. The syntax is
arguments: [#?my_mailer]
But how do i add optional parameter dependency for a service?
arguments: [%my_parameter%]
I tried
arguments: [%?my_parameter%]
arguments: [?%my_parameter%]
But neither of them work, is this feature implemented in sf2?
From Symfony 2.4 you can use expression for this:
arguments: ["#=container.hasParameter('some_param') ?
parameter('some_param') : 'default_value'"]
More at http://symfony.com/doc/current/book/service_container.html#using-the-expression-language
I think that if you don't pass/set the parameter, Symfony will complain about the service dependency. You want to make the parameter optional so that it is not required to always set in the config.yml file. And you want to use that parameter whenever it is set.
There is my solution:
# src/Acme/HelloBundle/Resources/config/services.yml
parameters:
my_parameter:
services:
my_mailer:
class: "%my_mailer.class%"
arguments: ["%my_parameter%"]
And then
# you-bundle-dir/DependencyInjection/Configuration.php
public function getConfigTreeBuilder()
{
$treeBuilder = new TreeBuilder();
$rootNode = $treeBuilder->root('you_bundle_ns');
// This is for wkhtmltopdf configuration
$rootNode
->children()
->scalarNode('my_parameter')->defaultNull()->end()
->end();
// Here you should define the parameters that are allowed to
// configure your bundle. See the documentation linked above for
// more information on that topic.
return $treeBuilder;
}
And then
# you-bundle-dir/DependencyInjection/YourBundleExtension.php
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');
$container->setParameter(
'you_bundle_ns.your_parameter',
isset($$config['you_bundle_ns']['your_parameter'])?$$config['you_bundle_ns']['your_parameter']:null
);
}
You make the your parameter optional by giving default value to the '%parameter%'
Please let me know if you have better alternatives.
Did you try to set a default value for a parameter? Like so:
namespace Acme\FooBundle\Services;
class BarService
{
public function __construct($param = null)
{
// Your login
}
}
and not injecting anything.
Related
I have a growing number of service classes that share a common interface (let's say BarService and BazService, that implement FooInterface).
All of these need to be decorated with the same decorator. Reading the docs, I know that I can do:
services:
App\BarDecorator:
# overrides the App\BarService service
decorates: App\BarService
Since I have to use the same decorator for different services I guess I would need to do:
services:
bar_service_decorator:
class: App\BarDecorator
# overrides the App\BarService service
decorates: App\BarService
baz_service_decorator:
class: App\BarDecorator
# overrides the App\BazService service
decorates: App\BazService
Problem is: this gets repetitive, quickly. And every time a new implementation of FooInterface is created, another set needs to be added to the configuration.
How can I declare that I want to decorate all services that implement FooInterface automatically, without having to declare each one individually?
A compiler pass allows to modify the container programmatically, to alter service definitions or add new ones.
First you'll need a way to locate all implementations of FooInterface. You can do this with the help of autoconfigure:
services:
_instanceof:
App\FooInterface:
tags: ['app.bar_decorated']
Then you'll need to create the compiler pass that collects all FooServices and creates a new decorated definition:
// src/DependencyInjection/Compiler/FooInterfaceDecoratorPass.php
namespace App\DependencyInjection\Compiler;
use App\BarDecorator;
use Symfony\Component\DependencyInjection\Compiler\CompilerPassInterface;
use Symfony\Component\DependencyInjection\ContainerBuilder;
class FooInterfaceDecoratorPass implements CompilerPassInterface
{
public function process(ContainerBuilder $container)
{
if (!$container->has(BarDecorator::class)) {
// If the decorator isn't registered in the container you could register it here
return;
}
$taggedServices = $container->findTaggedServiceIds('app.bar_decorated');
foreach ($taggedServices as $id => $tags) {
// skip the decorator, we do it's not self-decorated
if ($id === BarDecorator::class) {
continue;
}
$decoratedServiceId = $this->generateAliasName($id);
// Add the new decorated service.
$container->register($decoratedServiceId, BarDecorator::class)
->setDecoratedService($id)
->setPublic(true)
->setAutowired(true);
}
}
/**
* Generate a snake_case service name from the service class name
*/
private function generateAliasName($serviceName)
{
if (false !== strpos($serviceName, '\\')) {
$parts = explode('\\', $serviceName);
$className = end($parts);
$alias = strtolower(preg_replace('/[A-Z]/', '_\\0', lcfirst($className)));
} else {
$alias = $serviceName;
}
return $alias . '_decorator';
}
}
Finally, register the compiler pass in the kernel:
// src/Kernel.php
use App\DependencyInjection\Compiler\FooInterfaceDecoratorPass;
class Kernel extends BaseKernel
{
// ...
protected function build(ContainerBuilder $container)
{
$container->addCompilerPass(new FooInterfaceDecoratorPass());
}
}
Interesting! I think that's going to be tricky... but maybe with some hints here you might come up with a solution that fits your needs
find all Decorators... not sure if there's an easier way in that case but I use tags for that. So create a DecoratorInterface add auto tag it...
loop through the definitions and and modify and set the decorated service
e. g. in your Kernel or AcmeAwesomeBundle do
protected function build(ContainerBuilder $container)
{
$container->registerForAutoconfiguration(DecoratorInterface::class)
->addTag('my.decorator.tag');
$decoratorIds = $container->findTaggedServiceIds('my.decorator.tag');
foreach ($decoratorIds as $decoratorId) {
$definition = $container->getDefinition($decoratorId);
$decoratedServiceId = $this->getDecoratedServiceId($definition);
$definition->setDecoratedService($decoratedServiceId);
}
}
private function getDecoratedServiceId(Definition $decoratorDefinition): string
{
// todo
// maybe u can use the arguments here
// e.g. the first arg is always the decoratedService
// might not work because the arguments are not resolved yet?
$arg1 = $decoratorDefinition->getArgument(0);
// or use a static function in your DecoratorInterface like
// public static function getDecoratedServiceId():string;
$class = $decoratorDefinition->getClass();
$decoratedServiceId = $class::getDecoratedServiceId();
return 'myDecoratedServiceId';
}
I'm pretty sure this is not complete yet but let us know how you solved it
Using Symfony 3.4, I just try a simple case for a new bundle (name: APiToolBundle).
Here is the content of src/ApiToolBundle/Resources/config/config.yml :
imports:
- { resource: parameters.yml }
api_tool:
api_url: %myapi_url%
api_authorization_name: 'Bearer'
This file is loaded by ApiToolBundleExtension :
public function load(array $configs, ContainerBuilder $container)
{
$configuration = new Configuration();
$config = $this->processConfiguration($configuration, $configs);
$loader = new Loader\YamlFileLoader($container, new FileLocator(__DIR__.'/../Resources/config'));
$loader->load('services.yml');
}
I have set the Configuration file too :
class Configuration implements ConfigurationInterface
{
public function getConfigTreeBuilder()
{
$treeBuilder = new TreeBuilder();
$rootNode = $treeBuilder->root('production_tool');
$rootNode
->children()
->scalarNode('api_url')->defaultValue('')->end()
->scalarNode('api_authorization_name')->defaultValue('')->end()
->end()
;
return $treeBuilder;
}
}
But then I just want to bind a config parameter to one of my service :
# src/ApiToolBundle/Resources/config/services.yml
ApiToolBundle\Service\MyApi:
bind:
$apiUrl: '%api_tool.api_url%'
I am based on this doc : https://symfony.com/doc/3.4/bundles/configuration.html
But I am not sure to understand everything, since they talk about mergin in other way ... Do I need to do something else to load my own bundle config file ?
This is indeed a bit tricky to grasp. The bundle configuration is distinct from the parameters you use in your service configuration (even though inside your config you can also define services, which seems a bit odd at first). This is one of the reasons why Symfony in Version 4 discouraged using the semantic configuration inside applications, not use bundles & configuration, and instead directly work with parameters and services instead.
You will need to map the configuration to parameters or directly inject them into the service where you need them, in case you don't want them to be publicly available to other services or to be pulled from the container using getParameter. You can do this in the Extension where you have access to the ContainerBuilder.
See for example the FrameworkExtension where you have large configs that change the container: https://github.com/symfony/symfony/blob/v3.4.30/src/Symfony/Bundle/FrameworkBundle/DependencyInjection/FrameworkExtension.php#L194-L196
In your case it could look something like this:
public function load(array $configs, ContainerBuilder $container)
{
$configuration = new Configuration();
$config = $this->processConfiguration($configuration, $configs);
$loader = new Loader\YamlFileLoader($container, new FileLocator(__DIR__.'/../Resources/config'));
$loader->load('services.yml');
# Below is the new part, the rest is copied from your question
# If you only want to use the configuration values internally in your services:
$container->getDefinition('ApiToolBundle\Service\MyApi')->setArgument(
0 # Offset of the argument where you want to use the api url
$config['api_url']
);
# If you want to make the values publicly available as parameters:
$container->setParameter('api_tool.api_url', $config['api_url']);
}
Hello Ther i try to connenct to ES indet that is located on AWS, but i still get he Error.
[Symfony\Component\DependencyInjection\Exception\ServiceNotFoundException]
You have requested a non-existent service "es.managers.default".
I installed the bundle using Conposer, as described on the Docs.
then added a config part tom my config.ylm
ongr_elasticsearch:
managers:
default:
index:
index_name: contents
hosts:
- https://search-***.es.amazonaws.com:443
mappings:
- StatElasticBundle
i have a awsaccesskey and a awssecretkey, but i don't now where i have to put them, so i created a aws_connection section in the parameters.yml and try to load it
Then i try to establish a connection in my SymfonyBundle and created a class in my Bundle->DepandencyInjection folder to extend my bundle, this is where i get the error. Mayby someone of you struggled with the same error?
Thanks for help.
Class StatElasticExtension extends Extension
{
const YAML_FILES = [
'parameters.yml',
'config.yml',
'services.yml'
];
/**
* {#inheritdoc}
*/
public function load(array $configs, ContainerBuilder $container)
{
$loader = new Loader\YamlFileLoader($container, new FileLocator(__DIR__.'/../Resources/config'));
foreach (self::YAML_FILES as $yml) {
$loader->load($yml);
}
$configuration = new Configuration();
$config = $this->processConfiguration($configuration, $configs);
$serviceDefinition = $container->getDefinition('es.managers.default');
$awsConnections = $container->getParameter('aws_connections');
$elasticsearchClient = $this->getClient($awsConnections);
$serviceDefinition->replaceArgument(2, $elasticsearchClient);
}
The correct service name is es.manager.default.
I'm writing my own project and want to use some of Symfony components.
I'm implementing Sf Router to my project and I don't understand how to call controller in router.
For ex I have a class:
<?php
namespace App;
use Symfony\Component\Config\FileLocator;
use Symfony\Component\Routing\Loader\YamlFileLoader;
use Symfony\Component\Routing\Matcher\UrlMatcher;
use Symfony\Component\Routing\RequestContext;
use Symfony\Component\HttpFoundation as Http;
use Symfony\Component\Routing\RouteCollection;
use Symfony\Component\Routing\Route;
class AppRouter
{
public function match() {
$context = new RequestContext();
$request = Http\Request::createFromGlobals();
$context->fromRequest($request);
$matcher = new UrlMatcher($this->getRoutes(), $context);
return $matcher->matchRequest($request);
}
private function getRoutes() {
$fileLocator = new FileLocator([__DIR__ . '/../config']);
$loader = new YamlFileLoader($fileLocator);
$routes = $loader->load('routes.yml');
return $routes;
}
}
and I have some defined routes in my routes.yml so if I trying to go to registred route match() method returns to me an array like below:
[
"_controller" => "Controller\\MyController::testAction"
"_route" => "test"
]
So, how now can I call the controller for matched URL? I'm read a documentation but can't understand how should I do this.
This is a much more involved question then it might look.
The easiest approach is to simply explode the _controller string, new the controller and call the action.
// Untested but I think the syntax is correct
$matched = $router->match();
$parts = explode(':',$matched['_controller']);
$controller = new $parts[0]();
$controller->$parts[2]();
Of course there is a great deal of error checking to be added.
The reason I say it can be more complicated is that there is actually a lot you more you can do. Take a look at the HTTP Component. HttpKernel::handle($request) which uses a ControllerResolver class to create a controller as well as an ArgumentResolver to handle many of the controller's arguments. Fun stuff.
Create Your Own Framework is an excellent resource.
And the fairly new Symfony Flex approach gives you a bare bones framework which is worth studying as well.
The HTTP Kernel handles the Route to Controller invokation. You can find the full documentation here
The Controller Resolver and argument resolver will help you to match pretty easily a route to a controller method. This is the easy and robust way in order to get the router up and running on a legacy project without having to invoke the fully-fledge framework.
Think about it once more: our framework is more robust and more flexible than ever and it still has less than 50 lines of code.
require_once __DIR__.'/../vendor/autoload.php';
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\HttpFoundation\Response;
use Symfony\Component\Routing;
use Symfony\Component\HttpKernel;
function render_template(Request $request)
{
extract($request->attributes->all(), EXTR_SKIP);
ob_start();
include sprintf(__DIR__.'/../src/pages/%s.php', $_route);
return new Response(ob_get_clean());
}
$request = Request::createFromGlobals();
$routes = include __DIR__.'/../src/app.php';
$context = new Routing\RequestContext();
$context->fromRequest($request);
$matcher = new Routing\Matcher\UrlMatcher($routes, $context);
$controllerResolver = new HttpKernel\Controller\ControllerResolver();
$argumentResolver = new HttpKernel\Controller\ArgumentResolver();
try {
$request->attributes->add($matcher->match($request->getPathInfo()));
$controller = $controllerResolver->getController($request);
$arguments = $argumentResolver->getArguments($request, $controller);
$response = call_user_func_array($controller, $arguments);
} catch (Routing\Exception\ResourceNotFoundException $exception) {
$response = new Response('Not Found', 404);
} catch (Exception $exception) {
$response = new Response('An error occurred', 500);
}
$response->send();
`
My solution for are next:
change a little my routes.yml parameters: no class name contain only slash
use call_user_func_array($this->controller, $this->parameters)
I have a bundle named: "ApiBundle". In this bundle I have the class "ServiceManager", this class is responsible for retrieving a specific Service object. Those Service objects needs to be created based on some configuration, so after this piece of code in my bundle extension class:
$configuration = new Configuration();
$config = $this->processConfiguration($configuration, $configs);
$loader = new Loader\YamlFileLoader($container, new FileLocator(__DIR__.'/../Resources/config'));
$loader->load('services.yml');
// Create Service objects...
I create those Service objects right after I have processed the configuration, something like this:
foreach ($services as $name => $service) {
$service = new Service();
$service->setName($name);
$manager = $container->get($this->getAlias() . '.service_manager');
$manager->add($service);
}
Unfortunately, this does not work, probably because the container isn't compiled yet. So I tried to add those Service objects the following way:
$manager = $container->getDefinition($this->getAlias() . '.service_manager');
$manager->addMethodCall('add', array($service));
But again, this throws the following exception: RuntimeException: Unable to dump a service container if a parameter is an object or a resource.
I can't seem to get a grasp on how to use the service container correctly. Does someone knows how I can add those Service objects to the ServiceManager (which is a service) inside the bundle extension class?
This is how the configuration of the bundle looks like:
api_client:
services:
some_api:
endpoint: http://api.yahoo.com
some_other_api:
endpoint: http://api.google.com
Every 'service' will be a seperate Service object.
I hope I explained it well enough, my apologies if my english is incorrect.
Steffen
EDIT
I think I may have solved the problem, I made a Compiler Pass to manipulate the container there with the following:
public function process(ContainerBuilder $container)
{
$services = $container->getParameter('mango_api.services');
foreach ($services as $name => $service) {
$clientManager = $container->getDefinition('mango_api.client_manager');
$client = new Definition('Mango\Bundle\ApiBundle\Client\Client', array($name, 'client', 'secret'));
$container->setDefinition('mango_api.client.' .$name, $client);
$clientManager->addMethodCall('add', array($client));
}
}
Is this appropriate?
To create services based on configuration you need to create compiler pass and enable it.
Compiler passes give you an opportunity to manipulate other service
definitions that have been registered with the service container.
I think I may have solved the problem, I made a Compiler Pass to manipulate the container there with the following:
public function process(ContainerBuilder $container)
{
$services = $container->getParameter('mango_api.services');
foreach ($services as $name => $service) {
$clientManager = $container->getDefinition('mango_api.client_manager');
$client = new Definition('Mango\Bundle\ApiBundle\Client\Client', array($name, 'client', 'secret'));
$client->setPublic(false);
$container->setDefinition('mango_api.client.' .$name, $client);
$clientManager->addMethodCall('add', array($client));
}
}