diff --git a/CHANGELOG.md b/CHANGELOG.md
index 0358a8f..ce8618f 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -1,5 +1,11 @@
CHANGELOG for 1.x
===================
+## v1.7.0 - (2026-09-21)
+### Added
+- PHPUnit extension `DeprecationLoggerExtension` and `DeprecationLoggerSubscriber` to intercept and log unhandled deprecations during test execution
+- `phpunit-deprecated` make target to run the test suite and export formatted deprecation reports to a log file.
+- `docs/front_code_validation.md` standard added and mention on README.md
+
## v1.6.0 - (2026-09-14)
### TODO When updating
Manual steps `composer recipes:install smartbooster/standard-bundle --reset --force` cannot handle by itself when a project upgrades to `1.6` :
diff --git a/make/test.mk b/make/test.mk
index 4dce30b..644dcce 100644
--- a/make/test.mk
+++ b/make/test.mk
@@ -1,10 +1,14 @@
##
## Tests
## -----
-.PHONY: phpunit coverage-text coverage-html coverage jest
+.PHONY: phpunit phpunit-deprecated coverage-text coverage-html coverage jest
phpunit: ## Launch all tests
vendor/bin/simple-phpunit
+phpunit-deprecated:
+ rm -rf var/cache/test # must be done for get all deprecated
+ DEPRECATIONS_LOG_FILE="var/log/deprecations_test.log" vendor/bin/simple-phpunit
+
coverage-text: ## Launch all tests with code coverage text
XDEBUG_MODE=coverage vendor/bin/simple-phpunit --coverage-text
diff --git a/phpunit.xml.dist b/phpunit.xml.dist
index f004356..d192bf9 100644
--- a/phpunit.xml.dist
+++ b/phpunit.xml.dist
@@ -45,6 +45,7 @@
+
diff --git a/recipes.json b/recipes.json
index fd60449..de3f045 100644
--- a/recipes.json
+++ b/recipes.json
@@ -14,7 +14,8 @@
"1.4.1",
"1.4.2",
"1.5",
- "1.6"
+ "1.6",
+ "1.7"
]
},
"branch": "main",
diff --git a/smartbooster.standard-bundle.1.7.json b/smartbooster.standard-bundle.1.7.json
new file mode 100644
index 0000000..a392240
--- /dev/null
+++ b/smartbooster.standard-bundle.1.7.json
@@ -0,0 +1,32 @@
+{
+ "manifests": {
+ "smartbooster/standard-bundle": {
+ "manifest": {
+ "bundles": {
+ "DAMA\\DoctrineTestBundle\\DAMADoctrineTestBundle": ["test"],
+ "Liip\\TestFixturesBundle\\LiipTestFixturesBundle": ["test"],
+ "Smart\\StandardBundle\\SmartStandardBundle": ["dev", "test"]
+ },
+ "copy-from-package": {
+ "config/packages/dama_doctrine_test_bundle.yaml": "%CONFIG_DIR%/packages/test/dama_doctrine_test_bundle.yaml",
+ "config/packages/liip_test_fixtures.yaml": "%CONFIG_DIR%/packages/test/liip_test_fixtures.yaml",
+ "make/dev.mk": "make/dev.mk",
+ "make/qualimetry.mk": "make/qualimetry.mk",
+ "make/security.mk": "make/security.mk",
+ "make/test.mk": "make/test.mk",
+ "dast_scan/build-plan.sh": "dast_scan/build-plan.sh",
+ "dast_scan/scripts/": "dast_scan/scripts/",
+ "tests/bootstrap.php": "tests/bootstrap.php",
+ ".php-cs-fixer.dist.php": ".php-cs-fixer.dist.php",
+ "phpstan.neon": "phpstan.neon",
+ "phpstan-baseline.neon": "phpstan-baseline.neon",
+ "psalm.xml": "psalm.xml",
+ "psalm-taint-stubs.php": "psalm-taint-stubs.php",
+ "psalm-taint-baseline.xml": "psalm-taint-baseline.xml",
+ "phpunit.xml.dist": "phpunit.xml.dist"
+ }
+ },
+ "ref": "3c1c7b7b1b0dd63e1fc6b77bf2549bbd778fb4ac"
+ }
+ }
+}
diff --git a/src/Phpunit/DeprecationLoggerExtension.php b/src/Phpunit/DeprecationLoggerExtension.php
new file mode 100644
index 0000000..47afd5c
--- /dev/null
+++ b/src/Phpunit/DeprecationLoggerExtension.php
@@ -0,0 +1,65 @@
+resolveLogFile($parameters);
+
+ if (null === $logFile) {
+ return;
+ }
+
+ $logDir = \dirname($logFile);
+ if (!is_dir($logDir)) {
+ @mkdir($logDir, 0o777, true);
+ }
+
+ $facade->registerSubscriber(new DeprecationLoggerSubscriber($logFile));
+ }
+
+ private function resolveLogFile(ParameterCollection $parameters): ?string
+ {
+ $path = $_SERVER['DEPRECATIONS_LOG_FILE']
+ ?? $_ENV['DEPRECATIONS_LOG_FILE']
+ ?? getenv('DEPRECATIONS_LOG_FILE')
+ ?: null;
+
+ if (!$path || !is_string($path)) {
+ return null;
+ }
+
+ if ($this->isAbsolutePath($path)) {
+ return $path;
+ }
+
+ $projectRoot = getcwd() ?: \dirname(__DIR__, 3);
+
+ return $projectRoot . DIRECTORY_SEPARATOR . $path;
+ }
+
+ private function isAbsolutePath(string $path): bool
+ {
+ if ('' === $path) {
+ return false;
+ }
+
+ if ('/' === $path[0] || '\\' === $path[0]) {
+ return true;
+ }
+
+ return (bool) preg_match('/^[A-Za-z]:[\\\\\\/]/', $path);
+ }
+}
diff --git a/src/Phpunit/DeprecationLoggerSubscriber.php b/src/Phpunit/DeprecationLoggerSubscriber.php
new file mode 100644
index 0000000..825da2a
--- /dev/null
+++ b/src/Phpunit/DeprecationLoggerSubscriber.php
@@ -0,0 +1,379 @@
+
+ * }>>
+ */
+ private array $deprecations = [];
+
+ /**
+ * @var array>
+ */
+ private array $testsByType = [];
+ private bool $printed = false;
+
+ public function __construct(?string $logFile)
+ {
+ $this->logFile = $logFile;
+
+ if (null !== $this->logFile) {
+ register_shutdown_function(function (): void { // phpcs:ignore
+ $this->writeSummaryToLog();
+ });
+
+ // Catch @trigger_error()
+ set_error_handler(function (int $errno, string $errstr, string $errfile, int $errline): bool {
+ if (E_USER_DEPRECATED === $errno || E_DEPRECATED === $errno) {
+ $this->recordSuppressedDeprecation('user', $errfile, $errline, $errstr);
+
+ return true; // Avoid console display
+ }
+
+ return false;
+ });
+ }
+ }
+
+ public function notify(object $event): void
+ {
+ if (null === $this->logFile) {
+ return;
+ }
+
+ if ($event instanceof TestDeprecationTriggered) {
+ $this->recordDeprecation(
+ 'user',
+ $event->file(),
+ $event->line(),
+ $event->message(),
+ $event->test()
+ );
+
+ return;
+ }
+
+ if ($event instanceof TestPhpDeprecationTriggered) {
+ $this->recordDeprecation(
+ 'php',
+ $event->file(),
+ $event->line(),
+ $event->message(),
+ $event->test()
+ );
+
+ return;
+ }
+
+ if ($event instanceof TestPhpunitDeprecationTriggered) {
+ $this->recordTestOnlyDeprecation(
+ 'phpunit',
+ $event->message(),
+ $event->test()
+ );
+
+ return;
+ }
+
+ if ($event instanceof TestRunnerDeprecationTriggered) {
+ $this->recordRunnerDeprecation('runner', $event->message());
+ }
+ }
+
+ private function recordDeprecation(string $type, string $file, int $line, string $message, Test $test): void
+ {
+ $trace = debug_backtrace(DEBUG_BACKTRACE_IGNORE_ARGS);
+
+ /** @var string $file */
+ /** @var int $line */
+ [$file, $line] = $this->resolveRealFileAndLine($file, $line, $trace);
+
+ $key = hash('sha256', $type . "\n" . $file . "\n" . $line . "\n" . $message);
+
+ if (!isset($this->deprecations[$type][$key])) {
+ $this->deprecations[$type][$key] = [
+ 'file' => $file,
+ 'line' => $line,
+ 'message' => $message,
+ 'count' => 0,
+ 'tests' => [],
+ ];
+ }
+
+ ++$this->deprecations[$type][$key]['count'];
+
+ $testId = $test->id();
+ $testFile = $test->file();
+ $testLine = null;
+ if ($test instanceof TestMethod) {
+ $testLine = $test->line();
+ }
+
+ $this->deprecations[$type][$key]['tests'][$testId] = [
+ 'file' => $testFile,
+ 'line' => $testLine,
+ ];
+ $this->testsByType[$type][$testId] = true;
+ }
+
+ private function recordSuppressedDeprecation(string $type, string $file, int $line, string $message): void
+ {
+ $trace = debug_backtrace(DEBUG_BACKTRACE_IGNORE_ARGS);
+
+ /** @var string $file */
+ /** @var int $line */
+ [$file, $line] = $this->resolveRealFileAndLine($file, $line, $trace);
+
+ $key = hash('sha256', $type . "\n" . $file . "\n" . $line . "\n" . $message);
+
+ if (!isset($this->deprecations[$type][$key])) {
+ $this->deprecations[$type][$key] = [
+ 'file' => $file,
+ 'line' => $line,
+ 'message' => $message,
+ 'count' => 0,
+ 'tests' => [],
+ ];
+ }
+
+ ++$this->deprecations[$type][$key]['count'];
+
+ /** @var string $testId */
+ /** @var string $testFile */
+ /** @var int $testLine */
+ [$testId, $testFile, $testLine] = $this->findTestFromTrace($trace);
+
+ $this->deprecations[$type][$key]['tests'][$testId] = [
+ 'file' => $testFile,
+ 'line' => $testLine,
+ ];
+ $this->testsByType[$type][$testId] = true;
+ }
+
+ private function recordTestOnlyDeprecation(string $type, string $message, Test $test): void
+ {
+ $key = hash('sha256', $type . "\n" . $message);
+
+ if (!isset($this->deprecations[$type][$key])) {
+ $this->deprecations[$type][$key] = [
+ 'file' => '[unknown]',
+ 'line' => 0,
+ 'message' => $message,
+ 'count' => 0,
+ 'tests' => [],
+ ];
+ }
+
+ ++$this->deprecations[$type][$key]['count'];
+
+ $testId = $test->id();
+ $testFile = $test->file();
+ $testLine = null;
+ if ($test instanceof TestMethod) {
+ $testLine = $test->line();
+ }
+
+ $this->deprecations[$type][$key]['tests'][$testId] = [
+ 'file' => $testFile,
+ 'line' => $testLine,
+ ];
+ $this->testsByType[$type][$testId] = true;
+ }
+
+ private function recordRunnerDeprecation(string $type, string $message): void
+ {
+ $key = hash('sha256', $type . "\n" . $message);
+
+ if (!isset($this->deprecations[$type][$key])) {
+ $this->deprecations[$type][$key] = [
+ 'file' => '[test-runner]',
+ 'line' => 0,
+ 'message' => $message,
+ 'count' => 0,
+ 'tests' => [],
+ ];
+ }
+
+ ++$this->deprecations[$type][$key]['count'];
+ }
+
+ /**
+ * @param list $trace
+ *
+ * @return array{0: string, 1: int}
+ */
+ private function resolveRealFileAndLine(string $file, int $line, array $trace): array
+ {
+ $wrappers = [
+ 'vendor/doctrine/deprecations',
+ 'vendor/symfony/deprecation-contracts',
+ ];
+
+ foreach ($wrappers as $wrapper) {
+ if (str_contains($file, $wrapper)) {
+ foreach ($trace as $frame) {
+ if (
+ isset($frame['file'])
+ && !str_contains($frame['file'], 'vendor/doctrine/deprecations')
+ && !str_contains($frame['file'], 'vendor/symfony/deprecation-contracts')
+ && __FILE__ !== $frame['file']
+ ) {
+ return [$frame['file'], $frame['line'] ?? $line];
+ }
+ }
+ break;
+ }
+ }
+
+ return [$file, $line];
+ }
+
+ /**
+ * @param list $trace
+ *
+ * @return array{0: string, 1: string|null, 2: int|null}
+ */
+ private function findTestFromTrace(array $trace): array
+ {
+ $testId = '[kernel / boot]';
+ $testFile = null;
+ $testLine = null;
+
+ foreach ($trace as $frame) {
+ if (isset($frame['class']) && is_subclass_of($frame['class'], TestCase::class)) {
+ $testId = $frame['class'] . '::' . ($frame['function'] ?? '');
+ $testFile = $frame['file'] ?? null;
+ $testLine = $frame['line'] ?? null;
+ break;
+ }
+ }
+
+ return [$testId, $testFile, $testLine];
+ }
+
+ private function writeSummaryToLog(): void
+ {
+ if ($this->printed || null === $this->logFile) {
+ return;
+ }
+
+ $types = [
+ 'php' => 'PHP',
+ 'user' => 'user',
+ 'phpunit' => 'phpunit',
+ 'runner' => 'test runner',
+ ];
+
+ $output = '';
+
+ foreach ($types as $type => $label) {
+ if (empty($this->deprecations[$type])) {
+ continue;
+ }
+
+ $totalEvents = 0;
+ foreach ($this->deprecations[$type] as $data) {
+ $totalEvents += $data['count'];
+ }
+
+ $testCount = isset($this->testsByType[$type]) ? count($this->testsByType[$type]) : 0;
+
+ if ('' === $output) {
+ $output .= $this->buildHeader();
+ } else {
+ $output .= "\n";
+ }
+
+ $output .= sprintf(
+ "%d tests triggered %d %s deprecations:\n\n",
+ $testCount,
+ $totalEvents,
+ $label
+ );
+
+ $index = 1;
+ foreach ($this->deprecations[$type] as $data) {
+ $output .= sprintf(
+ "%d) %s:%d\n%s\n\nTriggered by:\n\n",
+ $index,
+ $data['file'],
+ $data['line'],
+ $data['message']
+ );
+
+ foreach ($data['tests'] as $testId => $testInfo) {
+ $output .= '* ' . $testId . "\n";
+ if (!empty($testInfo['file'])) {
+ $output .= ' ' . $testInfo['file'];
+ if (null !== $testInfo['line']) {
+ $output .= ':' . $testInfo['line'];
+ }
+ $output .= "\n";
+ }
+ }
+
+ if (empty($data['tests'])) {
+ $output .= "* test runner\n";
+ }
+
+ $output .= "\n";
+ ++$index;
+ }
+ }
+
+ if ('' === $output) {
+ $output = $this->buildHeader() . "No deprecations.\n";
+ }
+
+ $this->printed = true;
+ @file_put_contents($this->logFile, $output, LOCK_EX);
+ }
+
+ private function buildHeader(): string
+ {
+ $memoryText = $this->formatBytes(memory_get_peak_usage(true));
+
+ return sprintf("Memory: %s\n\n", $memoryText);
+ }
+
+ private function formatBytes(int $bytes): string
+ {
+ $mb = $bytes / 1024 / 1024;
+
+ return sprintf('%.2f MB', $mb);
+ }
+}