<?php | |
namespace Tests\Integration; | |
use Symfony\Bundle\FrameworkBundle\Test\KernelTestCase; | |
use Symfony\Component\Config\Util\XmlUtils; | |
class ContainerTest extends KernelTestCase | |
{ | |
private const FILTER_LIST = [ | |
// some services can exist only in dev or prod (thus not in test env) | |
// or some services are behind some features flags | |
// or some services are static (thus they are not real service) | |
]; | |
public function testContainer() | |
{ | |
static::bootKernel(); | |
$container = static::getContainer(); | |
$projectDir = $container->getParameter('kernel.project_dir'); | |
$xml = file_get_contents("{$projectDir}/var/cache/test/AppKernelTestDebugContainer.xml"); | |
$servicesNode = XmlUtils::parse($xml)->getElementsByTagName('services')[0]; | |
$services = XmlUtils::convertDomElementToArray($servicesNode)['service']; | |
$count = 0; | |
foreach ($services as $service) { | |
if ($this->isSkipped($service, $projectDir)) { | |
continue; | |
} | |
$container->get($service['id']); | |
++$count; | |
} | |
$this->addToAssertionCount($count); | |
} | |
private function isSkipped(array $service, string $projectDir): bool | |
{ | |
if (str_starts_with($service['id'], '.instanceof.') || str_starts_with($service['id'], '.abstract.') || str_starts_with($service['id'], '.errored.')) { | |
return true; // Symfony internal stuff | |
} | |
if ($service['abstract'] ?? false) { | |
return true; // Symfony internal stuff | |
} | |
if (!\array_key_exists('class', $service)) { | |
return true; // kernel, or alias, or abstract | |
} | |
$class = $service['class']; | |
if (\in_array($class, self::FILTER_LIST)) { | |
return true; | |
} | |
try { | |
$rc = new \ReflectionClass($class); | |
} catch (\ReflectionException) { | |
// service class not found, it may be an interface | |
return true; | |
} catch (\Error $e) { | |
// service class not found, it may be an interface | |
return true; | |
} | |
$filename = $rc->getFileName(); | |
if (!str_starts_with($filename, "{$projectDir}/src")) { | |
return true; // service class not in tests/Integration | |
} | |
if ($rc->isAbstract()) { | |
return true; | |
} | |
return false; | |
} | |
} |
Thank you. I'm testing this script right now. The first problem I see is that it throws on unused services i.e. the services that are auto registered via resource: '../src/*'
. AFAIR such services are removed during container compilation because they are not dependencies of any other service (exception classes, Models, ValueObjects and so on). I know those classes can be excluded via exclude
option but this looks like some additional work that we don't do on a daily basis, just to satisfy your script. WDYT?
Second problem: private services will throw here. Do you assume that all the services are public in the test env?
Symfony\Component\DependencyInjection\Exception\ServiceNotFoundException: The "App\Security\MyJwtAuthenticator" service or alias has been removed or inlined when the container was compiled. You should either make it public, or stop using the container directly and use dependency injection instead.
Yes! indeed. But it's better to exclude from the DIC theses service (less CPU/time consumed for nothing)
I use this pattern all the time:
AppBundle\Crawler\:
resource: '../../../src/Crawler/*'
exclude:
- '../../../src/Crawler/**/Exception/*'
- '../../../src/Crawler/**/Model/*'
- '../../../src/Crawler/Messenger/Message/*'
About the 2nd problem, I do have lot of private service (almost all of them) and it works nicely.
Symfony, in test env, have a special container where all service are kinda public
if (!str_starts_with($filename, "{$projectDir}/src")) {
return true; // service class not in tests/Integration
}
Why do we check only classes from src/? This is common that services from vendor packages are configured by the application via a bundle config or custom container extension or compiler pass. We want to check instantiating those as well (to verify we have configured them correctly).
@javaDeveloperKid you're right. I coded that only for my use case. But feel free to adapt it!
What's the difference between this and
lint:container
command?