Description
bin/cake i18n extract warns about classes that do not exist. On a stock install the warnings come from CakePHP's own source, which is scanned to build cake.pot:
Could not reflect class/enum Cake\Core\TestSuite\Router in file src/Core/TestSuite/ContainerStubTrait.php: Class "Cake\Core\TestSuite\Router" does not exist
Could not reflect class/enum Cake\Event\true in file src/Event/EventListenerRegistrationTrait.php: Class "Cake\Event\true" does not exist
Cake\Event\true gives it away: a true literal is being read as a class name. Extraction still completes and the output is correct, so this is cosmetic — just noisy, especially in CI.
Cause
I18nExtractCommand::parseClassName() starts looking for a name as soon as it sees T_ENUM or T_CLASS:
if ($type === T_ENUM || $type === T_CLASS) {
$waitingForName = true;
continue;
}
if ($waitingForName && $type === T_STRING) {
return $namespace !== '' ? $namespace . '\\' . $value : $value;
}
But T_CLASS is also the token for the ::class magic constant — Foo::class tokenizes as T_STRING, T_DOUBLE_COLON, T_CLASS. So in is_a($listener, EventListenerInterface::class, true) the parser hits T_CLASS, then takes the next T_STRING, which is true.
The second condition is that both files declare a trait. parseClassName() does not know T_TRAIT, so there is no real declaration to match first and the first ::class in the body wins:
| file |
source |
parsed as |
Event/EventListenerRegistrationTrait.php |
is_a($listener, EventListenerInterface::class, true) |
Cake\Event\true |
Core/TestSuite/ContainerStubTrait.php |
class_exists(Router::class) |
Cake\Core\TestSuite\Router |
The second also shows imports are not followed: the file has use Cake\Routing\Router;, but the name is glued to the file's own namespace. When such a guess happens to exist, no warning appears and the wrong class is reflected silently — harmless today only because extractFileReflection() drops anything that is not an enum.
Steps to reproduce
In any 5.4.1 app:
bin/cake i18n extract --extract-core yes --overwrite --paths src/,templates/ --output resources/locales/
--extract-core yes is the part that matters: it puts vendor/cakephp/cakephp/src/ in the scanned paths, and both offending files are there. Nothing in the app itself is needed to trigger it.
Expected
No warnings; a file declaring no class or enum is skipped.
Suggested fix
Track the previous significant token and ignore T_CLASS that follows T_DOUBLE_COLON:
- if ($type === T_ENUM || $type === T_CLASS) {
+ // `Foo::class` produces T_CLASS as well, and is not a declaration
+ if (($type === T_ENUM || $type === T_CLASS) && $previous !== T_DOUBLE_COLON) {
$waitingForName = true;
continue;
}
with $previous assigned on every branch that reaches the end of the loop body.
Running both versions over the two offending files plus a real enum and class:
| file |
current |
patched |
EventListenerRegistrationTrait.php |
Cake\Event\true |
null |
ContainerStubTrait.php |
Cake\Core\TestSuite\Router |
null |
| an app enum |
App\Model\Enum\FirstSeenSource |
unchanged |
| an app table class |
App\Model\Table\HistoricalConnectionsTable |
unchanged |
Optionally, class_exists($fqn, false) before new ReflectionClass($fqn) would stop an unresolvable name from becoming an exception at all.
Environment
CakePHP 5.4.1, PHP 8.4.24. Still present on 5.x as of this writing.
CakePHP Version
5.4.1
PHP Version
8.4.24
Description
bin/cake i18n extractwarns about classes that do not exist. On a stock install the warnings come from CakePHP's own source, which is scanned to buildcake.pot:Cake\Event\truegives it away: atrueliteral is being read as a class name. Extraction still completes and the output is correct, so this is cosmetic — just noisy, especially in CI.Cause
I18nExtractCommand::parseClassName()starts looking for a name as soon as it seesT_ENUMorT_CLASS:But
T_CLASSis also the token for the::classmagic constant —Foo::classtokenizes asT_STRING,T_DOUBLE_COLON,T_CLASS. So inis_a($listener, EventListenerInterface::class, true)the parser hitsT_CLASS, then takes the nextT_STRING, which istrue.The second condition is that both files declare a trait.
parseClassName()does not knowT_TRAIT, so there is no real declaration to match first and the first::classin the body wins:Event/EventListenerRegistrationTrait.phpis_a($listener, EventListenerInterface::class, true)Cake\Event\trueCore/TestSuite/ContainerStubTrait.phpclass_exists(Router::class)Cake\Core\TestSuite\RouterThe second also shows imports are not followed: the file has
use Cake\Routing\Router;, but the name is glued to the file's own namespace. When such a guess happens to exist, no warning appears and the wrong class is reflected silently — harmless today only becauseextractFileReflection()drops anything that is not an enum.Steps to reproduce
In any 5.4.1 app:
--extract-core yesis the part that matters: it putsvendor/cakephp/cakephp/src/in the scanned paths, and both offending files are there. Nothing in the app itself is needed to trigger it.Expected
No warnings; a file declaring no class or enum is skipped.
Suggested fix
Track the previous significant token and ignore
T_CLASSthat followsT_DOUBLE_COLON:with
$previousassigned on every branch that reaches the end of the loop body.Running both versions over the two offending files plus a real enum and class:
EventListenerRegistrationTrait.phpCake\Event\truenullContainerStubTrait.phpCake\Core\TestSuite\RouternullApp\Model\Enum\FirstSeenSourceApp\Model\Table\HistoricalConnectionsTableOptionally,
class_exists($fqn, false)beforenew ReflectionClass($fqn)would stop an unresolvable name from becoming an exception at all.Environment
CakePHP 5.4.1, PHP 8.4.24. Still present on
5.xas of this writing.CakePHP Version
5.4.1
PHP Version
8.4.24