Skip to content

Commit c783970

Browse files
stronk7gsherwood
authored andcommitted
Fix PHP 7.4 regression, changed behavior of get_declared_classes()
Since PHP 7.4 get_declared_classes() does not guarantee any order. That implies that parent classes aren't the first any more, rendering the array_reverse() technique futile for the loop & break code that follows. So, additionally, let's try to reduce the list of candidates by removing all the classes known to be "parents". That way, at the end, only the "main" class just included with remain. Source: https://raw.githubusercontent.com/php/php-src/PHP-7.4/UPGRADING Text: Previously get_declared_classes() always returned parent classes before child classes. This is no longer the case. No particular order is guaranteed for the get_declared_classes() return value.
1 parent 8b0922d commit c783970

File tree

7 files changed

+185
-12
lines changed

7 files changed

+185
-12
lines changed

autoload.php

Lines changed: 50 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -160,14 +160,55 @@ public static function loadFile($path)
160160
return self::$loadedClasses[$path];
161161
}
162162

163-
$classes = get_declared_classes();
164-
$interfaces = get_declared_interfaces();
165-
$traits = get_declared_traits();
163+
$classesBeforeLoad = [
164+
'classes' => get_declared_classes(),
165+
'interfaces' => get_declared_interfaces(),
166+
'traits' => get_declared_traits(),
167+
];
166168

167169
include $path;
168170

169-
$className = null;
170-
$newClasses = array_reverse(array_diff(get_declared_classes(), $classes));
171+
$classesAfterLoad = [
172+
'classes' => get_declared_classes(),
173+
'interfaces' => get_declared_interfaces(),
174+
'traits' => get_declared_traits(),
175+
];
176+
177+
$className = self::determineLoadedClass($classesBeforeLoad, $classesAfterLoad);
178+
179+
self::$loadedClasses[$path] = $className;
180+
self::$loadedFiles[$className] = $path;
181+
return self::$loadedClasses[$path];
182+
183+
}//end loadFile()
184+
185+
186+
/**
187+
* Determine which class was loaded based on the before and after lists of loaded classes.
188+
*
189+
* @param array $classesBeforeLoad The classes/interfaces/traits before the file was included.
190+
* @param array $classesAfterLoad The classes/interfaces/traits after the file was included.
191+
*
192+
* @return string The fully qualified name of the class in the loaded file.
193+
*/
194+
public static function determineLoadedClass($classesBeforeLoad, $classesAfterLoad)
195+
{
196+
$className = null;
197+
198+
$newClasses = array_diff($classesAfterLoad['classes'], $classesBeforeLoad['classes']);
199+
200+
// Since PHP 7.4 get_declared_classes() does not guarantee any order, making
201+
// it impossible to use order to determine which is the parent an which is the child.
202+
// Let's reduce the list of candidates by removing all the classes known to be "parents".
203+
// That way, at the end, only the "main" class just included will remain.
204+
$newClasses = array_reduce(
205+
$newClasses,
206+
function ($remaining, $current) {
207+
return array_diff($remaining, class_parents($current));
208+
},
209+
$newClasses
210+
);
211+
171212
foreach ($newClasses as $name) {
172213
if (isset(self::$loadedFiles[$name]) === false) {
173214
$className = $name;
@@ -176,7 +217,7 @@ public static function loadFile($path)
176217
}
177218

178219
if ($className === null) {
179-
$newClasses = array_reverse(array_diff(get_declared_interfaces(), $interfaces));
220+
$newClasses = array_reverse(array_diff($classesAfterLoad['interfaces'], $classesBeforeLoad['interfaces']));
180221
foreach ($newClasses as $name) {
181222
if (isset(self::$loadedFiles[$name]) === false) {
182223
$className = $name;
@@ -186,7 +227,7 @@ public static function loadFile($path)
186227
}
187228

188229
if ($className === null) {
189-
$newClasses = array_reverse(array_diff(get_declared_traits(), $traits));
230+
$newClasses = array_reverse(array_diff($classesAfterLoad['traits'], $classesBeforeLoad['traits']));
190231
foreach ($newClasses as $name) {
191232
if (isset(self::$loadedFiles[$name]) === false) {
192233
$className = $name;
@@ -195,11 +236,9 @@ public static function loadFile($path)
195236
}
196237
}
197238

198-
self::$loadedClasses[$path] = $className;
199-
self::$loadedFiles[$className] = $path;
200-
return self::$loadedClasses[$path];
239+
return $className;
201240

202-
}//end loadFile()
241+
}//end determineLoadedClass()
203242

204243

205244
/**

phpcs.xml.dist

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,7 @@
1010
<file>tests</file>
1111

1212
<exclude-pattern>*/src/Standards/*/Tests/*\.(inc|css|js)$</exclude-pattern>
13-
<exclude-pattern>*/tests/Core/*/*Test\.(inc|css|js)$</exclude-pattern>
13+
<exclude-pattern>*/tests/Core/*/*\.(inc|css|js)$</exclude-pattern>
1414

1515
<arg name="basepath" value="."/>
1616
<arg name="colors"/>
Lines changed: 118 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,118 @@
1+
<?php
2+
/**
3+
* Tests for the \PHP_CodeSniffer\Util\Common::isCamelCaps method.
4+
*
5+
* @author Greg Sherwood <gsherwood@squiz.net>
6+
* @copyright 2006-2015 Squiz Pty Ltd (ABN 77 084 670 600)
7+
* @license https://github.com/squizlabs/PHP_CodeSniffer/blob/master/licence.txt BSD Licence
8+
*/
9+
10+
namespace PHP_CodeSniffer\Tests\Core\Autoloader;
11+
12+
use PHPUnit\Framework\TestCase;
13+
14+
class DetermineLoadedClassTest extends TestCase
15+
{
16+
17+
18+
/**
19+
* Load the test files.
20+
*
21+
* @return void
22+
*/
23+
public static function setUpBeforeClass(): void
24+
{
25+
include __DIR__.'/TestFiles/Sub/C.inc';
26+
27+
}//end setUpBeforeClass()
28+
29+
30+
/**
31+
* Test for when class list is ordered.
32+
*
33+
* @return void
34+
*/
35+
public function testOrdered()
36+
{
37+
$classesBeforeLoad = [
38+
'classes' => [],
39+
'interfaces' => [],
40+
'traits' => [],
41+
];
42+
43+
$classesAfterLoad = [
44+
'classes' => [
45+
'PHP_CodeSniffer\Tests\Core\Autoloader\A',
46+
'PHP_CodeSniffer\Tests\Core\Autoloader\B',
47+
'PHP_CodeSniffer\Tests\Core\Autoloader\C',
48+
'PHP_CodeSniffer\Tests\Core\Autoloader\Sub\C',
49+
],
50+
'interfaces' => [],
51+
'traits' => [],
52+
];
53+
54+
$className = \PHP_CodeSniffer\Autoload::determineLoadedClass($classesBeforeLoad, $classesAfterLoad);
55+
$this->assertEquals('PHP_CodeSniffer\Tests\Core\Autoloader\Sub\C', $className);
56+
57+
}//end testOrdered()
58+
59+
60+
/**
61+
* Test for when class list is out of order.
62+
*
63+
* @return void
64+
*/
65+
public function testUnordered()
66+
{
67+
$classesBeforeLoad = [
68+
'classes' => [],
69+
'interfaces' => [],
70+
'traits' => [],
71+
];
72+
73+
$classesAfterLoad = [
74+
'classes' => [
75+
'PHP_CodeSniffer\Tests\Core\Autoloader\A',
76+
'PHP_CodeSniffer\Tests\Core\Autoloader\Sub\C',
77+
'PHP_CodeSniffer\Tests\Core\Autoloader\C',
78+
'PHP_CodeSniffer\Tests\Core\Autoloader\B',
79+
],
80+
'interfaces' => [],
81+
'traits' => [],
82+
];
83+
84+
$className = \PHP_CodeSniffer\Autoload::determineLoadedClass($classesBeforeLoad, $classesAfterLoad);
85+
$this->assertEquals('PHP_CodeSniffer\Tests\Core\Autoloader\Sub\C', $className);
86+
87+
$classesAfterLoad = [
88+
'classes' => [
89+
'PHP_CodeSniffer\Tests\Core\Autoloader\A',
90+
'PHP_CodeSniffer\Tests\Core\Autoloader\C',
91+
'PHP_CodeSniffer\Tests\Core\Autoloader\Sub\C',
92+
'PHP_CodeSniffer\Tests\Core\Autoloader\B',
93+
],
94+
'interfaces' => [],
95+
'traits' => [],
96+
];
97+
98+
$className = \PHP_CodeSniffer\Autoload::determineLoadedClass($classesBeforeLoad, $classesAfterLoad);
99+
$this->assertEquals('PHP_CodeSniffer\Tests\Core\Autoloader\Sub\C', $className);
100+
101+
$classesAfterLoad = [
102+
'classes' => [
103+
'PHP_CodeSniffer\Tests\Core\Autoloader\Sub\C',
104+
'PHP_CodeSniffer\Tests\Core\Autoloader\A',
105+
'PHP_CodeSniffer\Tests\Core\Autoloader\C',
106+
'PHP_CodeSniffer\Tests\Core\Autoloader\B',
107+
],
108+
'interfaces' => [],
109+
'traits' => [],
110+
];
111+
112+
$className = \PHP_CodeSniffer\Autoload::determineLoadedClass($classesBeforeLoad, $classesAfterLoad);
113+
$this->assertEquals('PHP_CodeSniffer\Tests\Core\Autoloader\Sub\C', $className);
114+
115+
}//end testUnordered()
116+
117+
118+
}//end class

tests/Core/Autoloader/TestFiles/A.inc

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
<?php
2+
namespace PHP_CodeSniffer\Tests\Core\Autoloader;
3+
class A {}

tests/Core/Autoloader/TestFiles/B.inc

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
<?php
2+
namespace PHP_CodeSniffer\Tests\Core\Autoloader;
3+
require 'A.inc';
4+
class B extends A {}

tests/Core/Autoloader/TestFiles/C.inc

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
<?php
2+
namespace PHP_CodeSniffer\Tests\Core\Autoloader;
3+
require 'B.inc';
4+
class C extends B {}
Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
<?php
2+
namespace PHP_CodeSniffer\Tests\Core\Autoloader\Sub;
3+
require __DIR__.'/../C.inc';
4+
use PHP_CodeSniffer\Tests\Core\Autoloader\C as ParentC;
5+
class C extends ParentC {}

0 commit comments

Comments
 (0)