I am trying to use different cache system on my environments. I would like to have, for example, Filesystem for dev and memcached for prod.
I am using symfony 3.3.10.
To achieve this, I would like to autowire the CacheInterface as follow:
use Psr\SimpleCache\CacheInterface;
class Api {
public function __construct(CacheInterface $cache)
{
$this->cache = $cache;
}
}
Here are my configuration files:
config_dev.yml:
framework:
cache:
app: cache.adapter.filesystem
config_prod.yml:
framework:
cache:
app: cache.adapter.memcached
...
Here is the error I get:
The error disappears when the FilesystemCache is declared as a service:
services:
Symfony\Component\Cache\Simple\FilesystemCache: ~
But now I cannot have another cache system for the test environment like NullCache. In fact, I have to declare only one service inheriting from CacheInterface. It is not possible as config_test is using config_dev too.
This is the beginning of services.yml if it can help:
services:
_defaults:
autowire: true
autoconfigure: true
public: false
Any idea on how to autowire different cache system depending on the environment?
EDIT:
Here is the working configuration:
use Psr\Cache\CacheItemPoolInterface;
class MyApi
{
/**
* #var CacheItemPoolInterface
*/
private $cache;
public function __construct(CacheItemPoolInterface $cache)
{
$this->cache = $cache;
}
}
config.yml:
framework:
# ...
cache:
pools:
app.cache.api:
default_lifetime: 3600
services.yml:
# ...
Psr\Cache\CacheItemPoolInterface:
alias: 'app.cache.api'
Even though factory pattern is a good option to solve this kind of problem, normally you don't need to do that for Symfony cache system. Typehints CacheItemPoolInterface instead:
use Psr\Cache\CacheItemPoolInterface;
public function __construct(CacheItemPoolInterface $cache)
It automatically injects the current cache.app service depending on the active environment, so Symfony does the job for you!
Just make sure to configure the framework.cache.app for each environment config file:
# app/config/config_test.yml
imports:
- { resource: config_dev.yml }
framework:
#...
cache:
app: cache.adapter.null
services:
cache.adapter.null:
class: Symfony\Component\Cache\Adapter\NullAdapter
arguments: [~] # small trick to avoid arguments errors on compile-time.
As cache.adapter.null service isn't available by default, you might need to define it manually.
In Symfony 3.3+/4 and 2017/2019 you can omit any config dependency and keep full control of the behavior with factory pattern:
// AppBundle/Cache/CacheFactory.php
namespace AppBundle\Cache;
final class CacheFactory
{
public function create(string $environment): CacheInterface
{
if ($environment === 'prod') {
// do this
return new ...;
}
// default
return new ...;
}
}
And services.yml of course:
# app/config/services.yml
services:
Psr\SimpleCache\CacheInterface:
factory: 'AppBundle\Cache\CacheFactory:create'
arguments: ['%kernel.environment%']
See more about service factory in Symfony Documentation.
You can read more about this in my Why Config Coding Sucks post.
Related
I have a problem with registering a class as a service in services.yaml.
I have created a class MenuBuilder.php in Symfony 6 that looks somewhat like this.
src/Menu/MenuBuilder.php
namespace App\Menu;
class MenuBuilder
{
public function createMainMenu(array $options)
{
// method logic
}
}
Now, when I want to register it as a service: (and use it in twig with KnpMenuBundle)
config/services.yaml
parameters:
services:
_defaults:
autowire: true # Automatically injects dependencies in your services.
autoconfigure: true # Automatically registers your services as commands, event subscribers, etc.
App\:
resource: '../src/'
exclude:
- '../src/DependencyInjection/'
- '../src/Entity/'
- '../src/Kernel.php'
app.menu_builder:
class: App\Menu\MenuBuilder
arguments: [ "#knp_menu.factory" ]
tags:
- { name: knp_menu.menu_builder, method: createMainMenu, alias: main }
I get an error Class "App\Menu\MenuBuilder" not found.
I tried making some other classes (ex. Test/TestClass) and namespaces and it doesn't work with freshly created classes.
On the other hand it i fiddled around with the naming and it recognized my entities and factories.
I tried clearing the cache
I am in development mode
I am getting this message when calling {{ knp_menu_render('main') }} in twig.
My composer autoload:
"autoload": {
"psr-4": {
"App\\": "src/"
}
},
symfony console debug:container app.menubuilder gives me this:
Information for Service "app.menu_builder"
==========================================
---------------- -------------------------------------------------------------
Option Value
---------------- -------------------------------------------------------------
Service ID app.menu_builder
Class App\Menu\MenuBuilder
Tags knp_menu.menu_builder (method: createMainMenu, alias: main)
Public no
Synthetic no
Lazy no
Shared yes
Abstract no
Autowired yes
Autoconfigured yes
Usages knp_menu.menu_provider.lazy
---------------- -------------------------------------------------------------
! [NOTE] The "app.menu_builder" service or alias has been removed or inlined when the container was compiled.
How do i get Symfony to recognize my classses?
I start new project and install fresh copy of Symfony 5 (microservice skeleton), and add first controller HealthCheckController to the default folder src/Controller, at this moment all is fine, I can get access to it from browser.
In next step I change a project name in composer.json and all related namespaces in code to
"autoload": {
"psr-4": {
"Project\\SubProject\\": "src/"
}
},
"autoload-dev": {
"psr-4": {
"Project\\SubProject\\Tests\\": "tests/"
}
},
and in service.yaml
Project\SubProject\:
resource: '../src/'
exclude:
- '../src/DependencyInjection/'
- '../src/Entity/'
- '../src/Kernel.php'
- '../src/Tests/'
# controllers are imported separately to make sure services can be injected
# as action arguments even if you don't extend any base controller class
Project\SubProject\Controller: # assuming you have namespace like that
resource: '../src/Controller/'
tags: [ 'controller.service_arguments' ]
everything still works.
Next step is to change directory structure, add layers and modules. So I move Kernel.php to the Common/Infrastructure/Symfony/ (ofcourse I change path to config files in the kernel) and controller to folder Common/Interfaces/Controller and change configs in the service.yaml
Project\SubProject\:
resource: '../src/'
exclude:
- '../src/Common/Infrastructure/Symfony/DependencyInjection/'
- '../src/Common/Infrastructure/Symfony/Kernel.php'
- '../src/Module1/Infrastructure/Entity/'
- '../src/Module2/Infrastructure/Entity/'
- '../src/Module3/Infrastructure/Entity/'
- '../src/Module1/Test/'
- '../src/Module2/Test/'
- '../src/Module3/Test/'
# controllers are imported separately to make sure services can be injected
# as action arguments even if you don't extend any base controller class
Project\SubProject\Common\Interfaces\Controller\: # assuming you have namespace like that
resource: '../src/Common/Interfaces/Controller/'
tags: [ 'controller.service_arguments' ]
and in routes/annotation/yaml
controllers:
resource: ../../src/Common/Interfaces/Controller
type: annotation
kernel:
resource: ../../src/Common/Infrastructure/Symfony/Kernel.php
type: annotation
and now I'm getting error Project\SubProject\Common\Interfaces\Controller\HealthCheckController" has no container set, did you forget to define it as a service subscriber?
What I'm doing wrong, I forgot to change something???
I know you can tell me you need to add container to controller like this
Project\SubProject\Common\Interfaces\Controller\HealthCheckController:
calls:
- method: setContainer
arguments: ['#service_container']
but it's stupid to configure each controller manually when autowire feature turned on, and when it worked with a default directory structure.
Also I clear cache by CLI command and manually deleting folder.
It was somewhat difficult to follow exactly what your configuration files ended as. I don't have a specific answer for you but I was a bit intrigued at the notion of moving Kernel.php. You did not mention moving the config directory so I chose to leave it where it was and:
namespace Project\SubProject\Common\Infrastructure\Symfony;
class Kernel extends BaseKernel
{
protected function configureContainer(ContainerConfigurator $container): void
{
$base = $this->getProjectDir();
$container->import($base . '/config/{packages}/*.yaml');
$container->import($base . '/config/{packages}/'.$this->environment.'/*.yaml');
$container->import($base . '/config/services.yaml');
$container->import($base . '/config/{services}_'.$this->environment.'.yaml');
}
# same for configureRoutes
# project/config/services.yaml
services:
_defaults:
autowire: true
autoconfigure: true
Project\SubProject\:
resource: '../src/'
exclude:
- '../src/Common/Infrastructure/Symfony/Kernel.php'
Project\SubProject\Common\Interfaces\Controller\:
resource: '../src/Common/Interfaces/Controller/'
tags: ['controller.service_arguments']
# project/config/routes/annotations.yaml
controllers:
resource: ../../src/Common/Interfaces/Controller/
type: annotation
Tweaked index.php and console to use the new Kernel path and it all worked as expected.
I should point out that as long as you are extending from AbstractController then you don't actually need the Controller section in services.yaml at all. It's quite puzzling why you seem to be getting a controller service but setContainer is not being called.
bin/console debug:container HealthCheckController
Class Project\SubProject\Common\Interfaces\Controller\HealthCheckController
Tags controller.service_arguments
container.service_subscriber
Calls setContainer
The Calls setContainer is obviously the important line.
I'm guessing you do have a typo somewhere and I suspect you did not actually start your namespace with Project\SubProject. But again it does work as expected.
Just for my own reference I checked in my test project.
In Symfony 4, I would like to combine different configuration files for services. In the following scenario, my attempt is to import services from php configuration named services.php and then perform the other services configurations in the yaml file that imports the others services..
services.yaml
imports:
- { resource: services.php }
services:
_defaults:
autowire: true
autoconfigure: true
public: false
App\:
resource: '../src/*'
exclude: '../src/{Entity,Migrations,Tests,Kernel.php}'
services.php
<?php
use Symfony\Component\DependencyInjection\Definition;
$definition = new Definition();
$definition
->setAutowired(true)
->setAutoconfigured(true)
->setPublic(false)
;
$this->registerClasses($definition, 'App\\', '../src/*', '../src/{Entity,Migrations,Tests}');
$container->getDefinition(\App\SomeClass::class)
->setArgument('$param', 'someValue');
Class file
class SomeClass
{
public function __construct(string $param)
{
...
}
I get the following error:
Cannot autowire service
"App\SomeClass": argument "$param"
of method "__construct()" is type-hinted "string", you should
configure its value explicitly.
Also, I'm wondering if I have to necessary to overwrite the initial _defaults definition (or others already done in by the files that imports) from the yaml or I can inherit. Not sure how these files are all merged.
The problem is that you registering the classes in src/* twice, once in your services.php and once in your services.yaml.
So in the first run with services.php you correctly define the class and the required argument, then, in the second run with services.yaml the definition is being overwritten and it loses the argument again.
The minimal solution would be to exclude the SomeClass.php in the services.yaml so it won't be registered a second time:
App\:
resource: '../src/*'
exclude: '../src/{Entity,Migrations,Tests,Kernel.php,SomeClass.php}' # <- here I added SomeClass.php
It would be better though to create a separate namespace and exclude the directory in the YAML and only register this directory in the PHP-config.
I do have simple command with constructor requiring LoggerInterface as dependency.
<?php
namespace App\Command;
// use (...)
class ProcessReportCommand extends Command
{
/** #var LoggerInterface */
private $logger;
public function __construct(LoggerInterface $logger)
{
parent::__construct();
$this->logger = $logger;
}
// (...)
}
My configuration in services.yml looks pretty default:
parameters:
services:
_defaults:
autowire: true
autoconfigure: true
public: false
App\:
resource: '../src/*'
exclude: '../src/{Entity,Migrations,Exception,Tests,Kernel.php}'
App\Controller\:
resource: '../src/Controller'
tags: ['controller.service_arguments']
Unfortunately I am getting PHP Error saying that dependency was not injected.
PHP Fatal error: Uncaught
Symfony\Component\Debug\Exception\FatalThrowableError: Type error: Too
few arguments to function
App\Command\ProcessReportCommand::__construct(), 0 passed in
/home/tomasz/project/bin/console on line 40 and exactly 1 expected in
/home/tomasz/project/src/Command/ProcessReportCommand.php:17
Even if I will switch this dependency to any other class which I have under App\ it behaves always like that.
I literally have no idea what should I do more to make it works, all ways even the one with including explicitly the service inside services.yml does not work for me. Any clues?
Did you clear the chache? php bin/console cache:clear.
If that does not work, what are the outputs of php bin/console debug:container Process?
I am using Symfony's Dependency Injection component version 3.4 in my custom PHP project. My project is running on PHP 5.6
"symfony/dependency-injection": "^3.4"
I have defined my services.yaml file to contain following service definitions
logger:
class: Monolog\Logger
arguments: ["application"]
autowire: true
public: true
Monolog\Logger: '#logger'
plugin_context:
class: MyProject\PluginContext
autowire: true
public: true
I can confirm that the autoloading is working and the instance of both classes are present in the definition, but the Logger class is not autowired in PluginContext constructor. the class is defined in the following code
use Monolog\Logger;
class PluginContext
{
private $logger;
function __construct(Logger $logger) {
$this->logger = $logger;
}
}
When the following code is run, PHP throws an exception
$container->get("plugin_context");
Catchable fatal error: Argument 1 passed to MyProject\PluginContext::__construct() must be an instance of Monolog\Logger, none given
Change your FQCN $logger and use this one use Psr\Log\LoggerInterface instead Monolog\Logger
Another thing, thanks to autowiring you don't need to specify anything in service.yaml except this (the default configuration):
_defaults:
autowire: true # Automatically injects dependencies in your services.
autoconfigure: true # Automatically registers your services as commands, event subscribers, etc.
public: false # Allows optimizing the container by removing unused services; this also means
# fetching services directly from the container via $container->get() won't work.
# The best practice is to be explicit about your dependencies anyway.
# makes classes in src/ available to be used as services
# this creates a service per class whose id is the fully-qualified class name
App\:
resource: '../src/*'
exclude: '../src/{Entity,Migrations,Tests,Kernel.php}'
The Doc said : « Aliases are used by the core bundles to allow services to be autowired. For example, MonologBundle creates a service whose id is logger. But it also adds an alias: Psr\Log\LoggerInterface that points to the logger service. This is why arguments type-hinted with Psr\Log\LoggerInterface can be autowired » so in your case the Psr\Log\LoggerInterface is an alias for Monolog https://symfony.com/doc/current/service_container/autowiring.html#using-aliases-to-enable-autowiring
It seems that the either the contents of services.yaml are not full.
Your services file should be like this
services:
logger:
class: Monolog\Logger
arguments: ["application"]
autowire: true
public: true
Monolog\Logger: '#logger'
plugin_context:
class: MyProject\PluginContext
autowire: true
public: true