77
88namespace Magento \Framework \ObjectManager \Resetter ;
99
10- use Magento \Framework \App \ObjectManager ;
10+ use Magento \Framework \Component \ComponentRegistrar ;
11+ use Magento \Framework \Component \ComponentRegistrarInterface ;
12+ use Magento \Framework \Exception \LocalizedException ;
1113use Magento \Framework \ObjectManager \ResetAfterRequestInterface ;
1214use Magento \Framework \ObjectManagerInterface ;
1315use WeakMap ;
1719 */
1820class Resetter implements ResetterInterface
1921{
20- public const RESET_PATH = '/app/etc/reset.php ' ;
22+ public const RESET_PATH = 'reset.json ' ;
23+ private const RESET_STATE_METHOD = '_resetState ' ;
2124
2225 /** @var WeakMap instances to be reset after request */
2326 private WeakMap $ resetAfterWeakMap ;
@@ -28,40 +31,56 @@ class Resetter implements ResetterInterface
2831 /** @var WeakMapSorter|null Note: We use temporal coupling here because of chicken/egg during bootstrapping */
2932 private ?WeakMapSorter $ weakMapSorter = null ;
3033
31- /**
32- * @var array
33- *
34- */
35- private array $ classList = [
36- //phpcs:disable Magento2.PHP.LiteralNamespaces
37- 'Magento\Framework\GraphQl\Query\Fields ' => true ,
38- 'Magento\Store\Model\Store ' => [
39- "_baseUrlCache " => [],
40- "_configCache " => null ,
41- "_configCacheBaseNodes " => [],
42- "_dirCache " => [],
43- "_urlCache " => []
44- ]
45- ];
34+ /** @var array */
35+ private array $ reflectionCache = [];
36+
37+ /** @var array */
38+ private array $ isObjectInClassListCache = [];
39+
40+ /** @var array */
41+ private readonly array $ classList ;
42+
43+ /** @var array */
44+ private array $ sortedClassListsByClass = [];
4645
4746 /**
48- * @var array
47+ * @param ComponentRegistrarInterface|null $componentRegistrar
48+ * @param array $classList
49+ * @return void
50+ * @phpcs:disable Magento2.Functions.DiscouragedFunction
4951 */
50- private array $ reflectionCache = [];
52+ public function __construct (
53+ private ?ComponentRegistrarInterface $ componentRegistrar = null ,
54+ array $ classList = [],
55+ ) {
56+ if (null === $ this ->componentRegistrar ) {
57+ $ this ->componentRegistrar = new ComponentRegistrar ();
58+ }
59+ foreach ($ this ->getPaths () as $ resetPath ) {
60+ if (!\file_exists ($ resetPath )) {
61+ continue ;
62+ }
63+ $ resetData = \json_decode (\file_get_contents ($ resetPath ), true );
64+ if (!$ resetData ) {
65+ throw new LocalizedException (__ ('Error parsing %1 ' , $ resetPath ));
66+ }
67+ $ classList += $ resetData ;
68+ }
69+ $ this ->classList = $ classList ;
70+ $ this ->resetAfterWeakMap = new WeakMap ;
71+ }
5172
5273 /**
53- * Constructor
74+ * Get paths for reset json
5475 *
55- * @return void
56- * @phpcs:disable Magento2.Functions.DiscouragedFunction
76+ * @return \Generator<string>
5777 */
58- public function __construct ()
78+ private function getPaths (): \ Generator
5979 {
60- if ( \file_exists ( BP . self ::RESET_PATH )) {
61- // phpcs:ignore Magento2.Security.IncludeFile.FoundIncludeFile
62- $ this -> classList = array_replace ( $ this -> classList , ( require BP . self ::RESET_PATH )) ;
80+ yield BP . ' /app/etc/ ' . self ::RESET_PATH ;
81+ foreach ( $ this -> componentRegistrar -> getPaths (ComponentRegistrar:: MODULE ) as $ modulePath ) {
82+ yield $ modulePath . ' /etc/ ' . self ::RESET_PATH ;
6383 }
64- $ this ->resetAfterWeakMap = new WeakMap ;
6584 }
6685
6786 /**
@@ -72,7 +91,10 @@ public function __construct()
7291 */
7392 public function addInstance (object $ instance ) : void
7493 {
75- if ($ instance instanceof ResetAfterRequestInterface || isset ($ this ->classList [\get_class ($ instance )])) {
94+ if ($ instance instanceof ResetAfterRequestInterface
95+ || \method_exists ($ instance , self ::RESET_STATE_METHOD )
96+ || $ this ->isObjectInClassList ($ instance )
97+ ) {
7698 $ this ->resetAfterWeakMap [$ instance ] = true ;
7799 }
78100 }
@@ -116,37 +138,124 @@ public function setObjectManager(ObjectManagerInterface $objectManager) : void
116138 }
117139
118140 /**
119- * State reset without reflection
141+ * Checks if the object is in the class list uses inheritance (via instanceof)
142+ *
143+ * @param object $object
144+ * @return bool
145+ * @SuppressWarnings(PHPMD.UnusedLocalVariable)
146+ */
147+ public function isObjectInClassList (object $ object )
148+ {
149+ $ className = \get_class ($ object );
150+ $ isObjectInClassListCachedValue = $ this ->isObjectInClassListCache [$ className ] ?? null ;
151+ if (null !== $ isObjectInClassListCachedValue ) {
152+ return $ isObjectInClassListCachedValue ;
153+ }
154+ foreach ($ this ->classList as $ key => $ value ) {
155+ if ($ object instanceof $ key ) {
156+ $ this ->isObjectInClassListCache [$ className ] = true ;
157+ return true ;
158+ }
159+ }
160+ $ this ->isObjectInClassListCache [$ className ] = false ;
161+ return false ;
162+ }
163+
164+ /**
165+ * State reset using reflection (or RESET_STATE_METHOD instead if it exists)
120166 *
121167 * @param object $instance
122168 * @return void
123169 * @throws \ReflectionException
170+ * @SuppressWarnings(PHPMD.UnusedLocalVariable)
124171 */
125172 private function resetStateWithReflection (object $ instance )
126173 {
127- $ className = \get_class ($ instance );
174+ if (\method_exists ($ instance , self ::RESET_STATE_METHOD )) {
175+ $ instance ->{self ::RESET_STATE_METHOD }();
176+ return ;
177+ }
178+ $ className = get_class ($ instance );
179+ if (!array_key_exists ($ className , $ this ->sortedClassListsByClass )) {
180+ $ temporaryClassList = [];
181+ foreach ($ this ->classList as $ key => $ value ) {
182+ if ($ instance instanceof $ key ) {
183+ $ temporaryClassList [] = $ key ;
184+ }
185+ }
186+ $ this ->sortClasses ($ temporaryClassList );
187+ $ this ->sortedClassListsByClass [$ className ] = $ temporaryClassList ;
188+ }
189+ foreach ($ this ->sortedClassListsByClass [$ className ] as $ currentClassName ) {
190+ $ this ->resetStateWithReflectionByClassName ($ instance , $ currentClassName );
191+ }
192+ }
193+
194+ /**
195+ * Sorts an array of strings that are class names and interface names
196+ *
197+ * Note: This sorting algorithm only takes arrays that are keyed by contiguous numbers starting at zero.
198+ * Note: This sorting algorithm works with comparators that return false on unrelated items.
199+ *
200+ * @param array $array
201+ * @return void
202+ */
203+ private function sortClasses (array &$ array ) : void
204+ {
205+ $ i = 0 ;
206+ $ count = count ($ array );
207+ while ($ i + 1 < $ count ) {
208+ for ($ j = $ i + 1 ; $ j < $ count ; $ j ++) {
209+ if ($ this ->sortClassesComparitor ($ array [$ i ], $ array [$ j ])) {
210+ $ swapTemp = $ array [$ i ];
211+ $ array [$ i ] = $ array [$ j ];
212+ $ array [$ j ] = $ swapTemp ;
213+ continue 2 ;
214+ }
215+ }
216+ $ i ++;
217+ }
218+ }
219+
220+ /**
221+ * Comparator for class/interface sorter that returns true if $b should come before $a.
222+ *
223+ * @param string $a
224+ * @param string $b
225+ * @return bool
226+ */
227+ private function sortClassesComparitor (string $ a , string $ b ) : bool
228+ {
229+ if (is_a ($ a , $ b , true )) {
230+ return true ;
231+ }
232+ if (is_a ($ b , $ a , true )) {
233+ return false ;
234+ }
235+ if (interface_exists ($ a ) && class_exists ($ b )) {
236+ return true ; // Note: If they aren't related, classes should come before interfaces
237+ }
238+ return false ; // No relation
239+ }
240+
241+ /**
242+ * State reset using reflection using specific className
243+ *
244+ * @param object $instance
245+ * @param string $className
246+ * @return void
247+ */
248+ private function resetStateWithReflectionByClassName (object $ instance , string $ className )
249+ {
250+ $ classResetValues = $ this ->classList [$ className ] ?? [];
128251 $ reflectionClass = $ this ->reflectionCache [$ className ]
129252 ?? $ this ->reflectionCache [$ className ] = new \ReflectionClass ($ className );
130253 foreach ($ reflectionClass ->getProperties () as $ property ) {
131- $ type = $ property ->getType ()?->getName();
132- if (empty ($ type ) && preg_match ('/@var\s+([^\s]+)/ ' , $ property ->getDocComment (), $ matches )) {
133- $ type = $ matches [1 ];
134- if (\str_contains ($ type , '[] ' )) {
135- $ type = 'array ' ;
136- }
137- }
138254 $ name = $ property ->getName ();
139- if (!in_array ( $ type , [ ' bool ' , ' array ' , ' null ' , ' true ' , ' false ' ], true )) {
255+ if (!array_key_exists ( $ name , $ classResetValues )) {
140256 continue ;
141257 }
142- $ value = $ this ->classList [$ className ][$ name ] ??
143- match ($ type ) {
144- 'bool ' => false ,
145- 'true ' => true ,
146- 'false ' => false ,
147- 'array ' => [],
148- 'null ' => null ,
149- };
258+ $ value = $ classResetValues [$ name ];
150259 $ property ->setAccessible (true );
151260 $ property ->setValue ($ instance , $ value );
152261 $ property ->setAccessible (false );
0 commit comments