I am migrating our project to Symfony 4. In my test suites, we used PHPUnit for functional tests (I mean, we call endpoints and we check result). Often, we mock services to check different steps.
Since I migrated to Symfony 4, I am facing this issue: Symfony\Component\DependencyInjection\Exception\InvalidArgumentException: The "my.service" service is already initialized, you cannot replace it.
when we redefine it like this : static::$container->set("my.service", $mock);
Only for tests, how can I fix this issue?
Thank you
Replacing is deprecated since Symfony 3.3. Instead of replacing service you should try using aliases.
http://symfony.com/doc/current/service_container/alias_private.html
Also, you can try this approach:
$this->container->getDefinition('user.user_service')->setSynthetic(true);
before doing $container->set()
Replace Symfony service in tests for php 7.2
Finally, I found a solution. Maybe not the best, but, it's working:
I created another test container class and I override the services property using Reflection:
<?php
namespace My\Bundle\Test;
use Symfony\Bundle\FrameworkBundle\Test\TestContainer as BaseTestContainer;
class TestContainer extends BaseTestContainer
{
private $publicContainer;
public function set($id, $service)
{
$r = new \ReflectionObject($this->publicContainer);
$p = $r->getProperty('services');
$p->setAccessible(true);
$services = $p->getValue($this->publicContainer);
$services[$id] = $service;
$p->setValue($this->publicContainer, $services);
}
public function setPublicContainer($container)
{
$this->publicContainer = $container;
}
Kernel.php :
<?php
namespace App;
use Symfony\Component\HttpKernel\Kernel as BaseKernel;
class Kernel extends BaseKernel
{
use MicroKernelTrait;
public function getOriginalContainer()
{
if(!$this->container) {
parent::boot();
}
/** #var Container $container */
return $this->container;
}
public function getContainer()
{
if ($this->environment == 'prod') {
return parent::getContainer();
}
/** #var Container $container */
$container = $this->getOriginalContainer();
$testContainer = $container->get('my.test.service_container');
$testContainer->setPublicContainer($container);
return $testContainer;
}
It's really ugly, but it's working.
I've got a couple of tests like this (the real code performs some actions and returns a result, the test-version just returns false for every answer).
If you create and use a custom config for each environment (eg: a services_test.yaml, or in Symfony4 probably tests/services.yaml), and first have it include dev/services.yaml, but then override the service you want, the last definition will be used.
app/config/services_test.yml:
imports:
- { resource: services.yml }
App\BotDetector\BotDetectable: '#App\BotDetector\BotDetectorNeverBot'
# in the top-level 'live/prod' config this would be
# App\BotDetector\BotDetectable: '#App\BotDetector\BotDetector'
Here, I'm using an Interface as a service-name, but it will do the same with '#service.name' style as well.
As I understood it, it means that class X was already injected(because of some other dependency) somewhere before your code tries to overwrite it with self::$container->set(X:class, $someMock).
If you on Symfony 3.4 and below you can ovverride services in container regardless it privite or public. Only deprication notice will be emmited, with content similar to error message from question.
On Symfony 4.0 error from the question was thown.
But on Symfony 4.1 and above you can lean on special "test" container. To learn how to use it consider follow next links:
https://symfony.com/blog/new-in-symfony-4-1-simpler-service-testing
https://dev.to/nikolastojilj12/symfony-5-mocking-private-autowired-services-in-controller-functional-tests-24j4
Related
maybe i try the impossible thing, but i try so archive a logging system where i can log anywhere with something like Logger::info('some info') or plain LogInfo(...).
So no pre-initialisation with new .. or as parameter/injection (have to us it in some functions with "dynamic" count of parameters). I need to use inside a controller/command and outside in smaller custom made classes and functions, called by some way by the controllers/commands but not extending any containers.
for example i use the MessageGenerator Example from the Symfony Docs
// srcMessageGenerator.php
namespace App\Library\Util;
use Doctrine\ORM\EntityManagerInterface;
class MessageGenerator
{
private $em;
public function setEM(EntityManagerInterface $em){
$this->em = $em;
var_dump('a');
}
public function getHappyMessage(){
var_dump($this->em);
$messages = [
'You did it! You updated the system! Amazing!',
'That was one of the coolest updates I\'ve seen all day!',
'Great work! Keep going!',
];
$index = array_rand($messages);
return $messages[$index];
}
}
I already tried to define it as service
# config/services.yaml
services:
App\Library\Util\MessageGenerator:
arguments: ['#doctrine.orm.entity_manager']
public: true
problem:
i have to inject it to every function i wanna use it - not possible
i tried use it as an static function - services can't be static
i tried to isolate it as static function without beeing a service, but then i don't know how to get the config/package/doctrine settings depending on my env.
i have looked into monolog too, but here is the same problem, i cant use it without declaration outside a controller/command.
anyone have a hint how i can access the default (env based) doctrine connection, or how i can use a service inside/outside a controller/command without declaring it (static like) and giving the entity manager to it.
(a solution to second way would be lovely, so i can "upgrade" to monolog some day using the same solution)
You can use Injecting Services/Config into a Service in Symfony by passing your service to the function in your controller.
Your service:
class MessageGeneratorService
{
private $em;
public function __construct(EntityManagerInterface $em){
$this->em = $em;
}
public function getHappyMessage(){
// the job
}
}
In an other service:
class MyService
{
private $messageGeneratorService;
public __construct(MessageGeneratorService $mGService) {
$this->messageGeneratorService = $mGService
}
// You can use this service like you use MessageGeneratorService
public makeMeHappy() {
$messageGeneratorService->getHappyMessage();
}
}
In a controller:
class MyController
{
/**
* #Route("/example", name="page_example")
*/
public index(MessageGeneratorService $mGService) {
$mGService->getHappyMessage(); // Now, you can use it
return $this->render('example/index.html.twig');
}
}
Now, you can use an instance of MessageGeneratorService in a controller or in a service:
The container will automatically know to pass the logger service when
instantiating the MessageGenerator
Trying to upgrade a project from Symfony 3.3 to 3.4. I've done composer update symfony/symfony --with-depdencies and added public: false to my services.yml file.
Now when I run my PHPUnit tests, I get this error:
Symfony\Component\DependencyInjection\Exception\ServiceNotFoundException : The service "templating.loader.cache" has a dependency on a non-existent service "templating.loader.wrapped".
Any ideas why this happens? I can't find any Google results or any Symfony documentation references for this at all...
Problem was found to be caused by overriding the definition of templating.loader.cache to public in a compiler pass class to allow access during functional tests.
Based off code here: https://github.com/symfony/symfony-docs/issues/8097
tl;dr do not do this:
final class TestCompilerPass implements CompilerPassInterface
{
/** {#inheritdoc} */
public function process(ContainerBuilder $container)
{
foreach ($container->getDefinitions() as $id => $definition) {
$definition->setPublic(true);
}
}
}
Instead limit the services you make public to the ones you actually require.
Unless you prepared your code for private services you shouldn't use the public: false tag. That is used to mark services as private. Probably somewhere in your code you have something like $var = $container->get('example'); which calls a public service. You can read more here.
I posted another question trying to find a way to statically access a repository class outside of a controller in a custom "helper" class.
So far the only way I have figured out how to achieve this is using the code below. If anyone wants to chime into the other question about "best practice" or "design patterns" please do.
I opened this question to seek the best method on having a singleton service (?) loaded when symfony boots so other classes can access it statically without any dependency injection. I haven't had much luck on finding any official docs or common practices. I know singleton is anti practice, but is the method below the best way, or is there a more ideal solution?
services.yml
parameters:
entity.device: Asterisk\DbBundle\Entity\Device
services:
asterisk.repository.device:
class: Asterisk\DbBundle\Entity\Repositories\DeviceRepository
factory: ["#doctrine.orm.asterisk_entity_manager", getRepository]
arguments:
- %entity.device%
tags:
- {name: kernel.event_listener, event: kernel.request, method: onKernelRequest}
DeviceRepository
class DeviceRepository extends \Doctrine\ORM\EntityRepository
{
/** #var ExtendedEntityRepository */
protected static $instance;
public function __construct(EntityManager $entityManager, ClassMetadata $class)
{
parent::__construct($entityManager, $class);
if(static::$instance instanceof static == false)
static::$instance = $this;
}
public static function getInstance()
{
return static::$instance;
}
public function onKernelRequest($event)
{
return;
}
}
Glad to see you are not running around anymore.
Your approach is not going to work unless someone grabs the repository out of the container first so self::$instance is initialized. But you really don't want to do this anyways. Super hacky.
You want to inject the repository service into your kernel listener. Trying to make the repository act as a kernel listener is just not a good design. So just make a service for your repository and then a second one for the listener. It may seem a bit strange at first but it really does work well in practice and it's the way S2 is designed.
If for some reason you are stuck with the notion that you have to be able to access the container globally then be aware that your kernel is defined globally(take a look at app.php) and it has a getContainer method in it.
$repo = $_GLOBAL['kernel']->getContainer()->get('asterisk.repository.device');
But again, there should be no need to do this.
==============================
Update - It looks like you are trying to use the listener functionality just to setup singletons. You should try to avoid singletons but if you really think you need them then the global access to the kernel can be used:
class DeviceRepository extends \Doctrine\ORM\EntityRepository
{
/** #var ExtendedEntityRepository */
protected static $instance;
public static function getInstance()
{
if (!static::$instance) {
static::$instance = $_GLOBAL['kernel']->getContainer()->get('asterisk.repository.device');
}
return static::$instance;
}
Poor design but at least it get's rid of the listener hack and it avoids creating the repository until it's actually needed. It aslo means you can access the repository from commands (listeners are not setup when commands are called).
I do not understand what the profit will be about this method. The idea of the servicecontainer is to make just one instance of each class and give a reference (or pointer if you like) to any method who asks to use this same instance. Let me proof it:
Service definition:
// app/config.yml
services:
app.test:
class: Vendor\AppBundle\Service\Test
and a custom class:
// src/AppBundle/Service/Test.php
namespace AppBundle/Service;
class Test {
public $test = 0;
}
and a controller:
// src/AppBundle/Controller/DefaultController
namespace AppBundle/Controller;
use Sensio\Bundle\FrameworkExtraBundle\Configuration\Route;
use Symfony\Bundle\FrameworkBundle\Controller\Controller;
use Symfony\Component\HttpFoundation\Request;
class DefaultController extends Controller
{
/**
* #Route("/", name="homepage")
*/
public function indexAction()
{
$instance1 = $this->get('app.test');
$instance2 = $this->get('app.test');
$instance1->test = 1;
echo $instance2->test; // RETURNS 1 !!!
exit;
}
Currently I'm working on testing some services in Symfony2 and I'm trying to use Guzzle MockPlugin for controlling CURL responses. Symfony version 2.3.8 is used. I've got to an interesting behaviour and I'm not sure if this is a Symfony2 bug or not.
I have these services in services.yml:
lookup_service_client:
class: FOO
public: false
factory_service: lookup_client_builder
factory_method: build
lookup_repository_auth_type:
class: AuthType
arguments: ["#lookup_service_client"]
lookup_repository_cancel_reason:
class: CancelReason
arguments: ["#lookup_service_client"]
payment_service_client:
class: FOO
public: false
factory_service: payment_client_builder
factory_method: build
payment_repository:
class: Payment
arguments: ["#payment_service_client"]
The name of the classes are not important. You can see that both "lookup_service_client" and "lookup_service_client" are PRIVATE services.
I have a test class, which extends Symfony\Bundle\FrameworkBundle\Test\WebTestCase. In one test I need to do something like:
$lookup = $this->client->getContainer()->get('lookup_service_client');
$payment = $this->client->getContainer()->get('payment_service_client');
I expected that, setting those services as PRIVATE, will not let me retrieve the services from container in tests, but the actual result is:
$lookup = $this->client->getContainer()->get('lookup_service_client'); => returns the service instance
$payment = $this->client->getContainer()->get('payment_service_client'); => returns an exception saying: "You have requested a non-existent service"
The only difference between those tow service_client services is that "lookup_service_client" is injected in several other services, while "payment_service_client" is injected in only one other service.
So, the questions are:
Why I can retrieve from container "lookup_service_client", since I've set it to private?
Why I can retrieve "lookup_service_client", but cannot retrieve "payment_service_client" since the only difference is presented above?
Is it a Symfony2 bug that I can access private service?
There were some new changes regarding this in Symfony 4.1:
In Symfony 4.1, we did the same and now tests allow fetching private services by default.
In practice, tests based on WebTestCase and KernelTestCase now access to a special container via $client->getContainer() or the static::$container property that allows to fetch non-removed private services.
You can read more about it in the news post.
While this is not a bug, it is definitely counter intuitive. The manual specifically says:
Now that the service is private, you should not fetch the service
directly from the container:
$container->get('foo');
This may or may not work, depending on how the container has optimized
the service instanciation and, even in the cases where it works, is
deprecated. Simply said: A service can be marked as private if you do
not want to access it directly from your code.
Which is why the core team has decided to make this behavior more consistent and intuitive in Symfony 4:
Setting or unsetting a private service with the Container::set() method is deprecated in Symfony 3.2 and no longer supported in 4.0;
Checking the existence of a private service with the Container::has() will always return false in Symfony 4.0;
Requesting a private service with the Container::get() method is deprecated in Symfony 3.2 and no longer returns the service in 4.0.
2018+ and Symfony 3.4/4.0+ solution
This approach with all its pros/cons is described in this post with code examples.
The best solution to access private services is to add a Compiler Pass that makes all services public for tests. That's it. How does it look in practice?
1. Update Kernel
use Symfony\Component\HttpKernel\Kernel;
+use Symplify\PackageBuilder\DependencyInjection\CompilerPass\PublicForTestsCompilerPass;
final class AppKernel extends Kernel
{
protected function build(ContainerBuilder $containerBuilder): void
{
$containerBuilder->addCompilerPass('...');
+ $containerBuilder->addCompilerPass(new PublicForTestsCompilerPass());
}
}
2. Require or create own Compiler Pass
Where PublicForTestsCompilerPass looks like:
use Symfony\Component\DependencyInjection\Compiler\CompilerPassInterface;
use Symfony\Component\DependencyInjection\ContainerBuilder;
final class PublicForTestsCompilerPass implements CompilerPassInterface
{
public function process(ContainerBuilder $containerBuilder): void
{
if (! $this->isPHPUnit()) {
return;
}
foreach ($containerBuilder->getDefinitions() as $definition) {
$definition->setPublic(true);
}
foreach ($containerBuilder->getAliases() as $definition) {
$definition->setPublic(true);
}
}
private function isPHPUnit(): bool
{
// defined by PHPUnit
return defined('PHPUNIT_COMPOSER_INSTALL') || defined('__PHPUNIT_PHAR__');
}
}
To use this class, just add the package by:
composer require symplify/package-builder
But of course, the better way is to use own class, that meets your needs (you might Behat for tests etc.).
Then all your tests will keep working as expected!
Let me know, how that works for you.
Check them in the container:
container:debug lookup_service_client
container:debug payment_service_client
in your example they both have class "FOO", maybe that's the case
I would like to extend Laravels Router class (Illuminate\Routing\Router) to add a method I need a lot in my application.
But sadly I can't get this to work. I already extended other classes successfully so I really have no idea where my wrong thinking comes from.
Anyway, right into the code:
<?php
namespace MyApp\Extensions;
use Illuminate\Routing\Router as IlluminateRouter;
class Router extends IlluminateRouter
{
public function test()
{
$route = $this->getCurrentRoute();
return $route->getParameter('test');
}
}
So as you see I want to get the parameter set by {test} in routes.php with a simple call like:
Router::test();
Not sure how to go on now. Tried to bind it to the IOC-Container within my ServiceProvider in register() and boot() but I got no luck.
Whatever I try I get either a constructor error or something else.
All solutions I found are too old and the API has changed since then.
Please help me!
edit:
I already tried binding my own Router within register() and boot() (as said above) but it doesn't work.
Here is my code:
<?php
namespace MyApp;
use Illuminate\Support\ServiceProvider;
use MyApp\Extensions\Router;
class MyAppServiceProvider extends ServiceProvider {
public function register()
{
$this->app['router'] = $this->app->share(function($app)
{
return new Router(new Illuminate\Events\Dispatcher);
}
// Other bindings ...
}
}
When I try to use my Router now I have the problem that it needs an Dispatcher.
So I have to do:
$router = new Router(new Illuminate\Events\Dispatcher); // Else I get an exception :(
Also it simply does nothing, if I call:
$router->test();
:(
And if I call
dd($router->test());
I get NULL
Look at: app/config/app.php and in the aliases array. You will see Route is an alias for the illuminate router via a facade class.
If you look at the facade class in Support/Facades/Route.php of illuminate source, you will see that it uses $app['router'].
Unlike a lot of service providers in laravel, the router is hard coded and cannot be swapped out without a lot of work rewiring laravel or editing the vendor source (both are not a good idea). You can see its hardcoded by going to Illuminate / Foundation / Application.php and searching for RoutingServiceProvider.
However, there's no reason i can think of that would stop you overriding the router class in a service provider. So if you create a service provider for your custom router, which binds to $app['router'], that should replace the default router with your own router.
I wouldn't expect any issues to arise from this method, as the providers should be loaded before any routing is done. So overriding the router, should happen before laravel starts to use the router class, but i've not this before, so be prepared for a bit of debugging if it doesn't work straight away.
So I was asking in the official Laravel IRC and it seems like you simply can't extend Router in 4.1 anymore. At least that's all I got as a response in a pretty long dialogue.
It worked in Laravel 4.0, but now it doesn't. Oh well, maybe it will work in 4.2 again.
Other packages suffer from this as well: https://github.com/jasonlewis/enhanced-router/issues/16
Anyway, personally I'll stick with my extended Request then. It's not that much of a difference, just that Router would've been more dynamic and better fitting.
I'm using Laravel 4.2, and the router is really hard coded into the Application, but I extended it this way:
Edit bootstrap/start.php, change Illuminate\Foundation\Application for YourNamespace\Application.
Create a class named YourNamespace\Application and extend \Illuminate\Foundation\Application.
class Application extends \Illuminate\Foundation\Application {
/**
* Register the routing service provider.
*
* #return void
*/
protected function registerRoutingProvider()
{
$this->register(new RoutingServiceProvider($this));
}
}
Create a class named YourNamespace\RoutingServiceProvider and extend \Illuminate\Routing\RoutingServiceProvider.
class RoutingServiceProvider extends \Illuminate\Routing\RoutingServiceProvider {
protected function registerRouter()
{
$this->app['router'] = $this->app->share(function($app)
{
$router = new Router($app['events'], $app);
// If the current application environment is "testing", we will disable the
// routing filters, since they can be tested independently of the routes
// and just get in the way of our typical controller testing concerns.
if ($app['env'] == 'testing')
{
$router->disableFilters();
}
return $router;
});
}
}
Finally, create YourNamespace\Router extending \Illuminate\Routing\Router and you're done.
NOTE: Although you're not changing the name of the class, like Router and RoutingServiceProvider, it will work because of the namespace resolution that will point it to YourNamespace\Router and so on.