@@ -298,20 +298,14 @@ public function getCode()
298298 case '} ' :
299299 $ code .= '} ' ;
300300 if (--$ open === 0 && ! $ isShortClosure ) {
301- $ candidates [] = [
302- 'code ' => $ code ,
303- 'use ' => $ use ,
304- 'isShortClosure ' => $ isShortClosure ,
305- 'isUsingThisObject ' => $ isUsingThisObject ,
306- 'isUsingScope ' => $ isUsingScope ,
307- ];
308- $ code = '' ;
309- $ state = 'start ' ;
310- $ open = 0 ;
311- $ use = [];
312- $ isShortClosure = false ;
313- $ isUsingThisObject = false ;
314- $ isUsingScope = false ;
301+ $ reset = $ this ->collectCandidate ($ candidates , $ code , $ use , $ isShortClosure , $ isUsingThisObject , $ isUsingScope );
302+ $ code = $ reset ['code ' ];
303+ $ state = $ reset ['state ' ];
304+ $ open = $ reset ['open ' ];
305+ $ use = $ reset ['use ' ];
306+ $ isShortClosure = $ reset ['isShortClosure ' ];
307+ $ isUsingThisObject = $ reset ['isUsingThisObject ' ];
308+ $ isUsingScope = $ reset ['isUsingScope ' ];
315309 } elseif ($ inside_structure ) {
316310 $ inside_structure = ! ($ open === $ inside_structure_mark );
317311 }
@@ -327,20 +321,14 @@ public function getCode()
327321 case '] ' :
328322 if ($ isShortClosure ) {
329323 if ($ open === 0 ) {
330- $ candidates [] = [
331- 'code ' => $ code ,
332- 'use ' => $ use ,
333- 'isShortClosure ' => $ isShortClosure ,
334- 'isUsingThisObject ' => $ isUsingThisObject ,
335- 'isUsingScope ' => $ isUsingScope ,
336- ];
337- $ code = '' ;
338- $ state = 'start ' ;
339- $ open = 0 ;
340- $ use = [];
341- $ isShortClosure = false ;
342- $ isUsingThisObject = false ;
343- $ isUsingScope = false ;
324+ $ reset = $ this ->collectCandidate ($ candidates , $ code , $ use , $ isShortClosure , $ isUsingThisObject , $ isUsingScope );
325+ $ code = $ reset ['code ' ];
326+ $ state = $ reset ['state ' ];
327+ $ open = $ reset ['open ' ];
328+ $ use = $ reset ['use ' ];
329+ $ isShortClosure = $ reset ['isShortClosure ' ];
330+ $ isUsingThisObject = $ reset ['isUsingThisObject ' ];
331+ $ isUsingScope = $ reset ['isUsingScope ' ];
344332 continue 3 ;
345333 }
346334 $ open --;
@@ -350,20 +338,14 @@ public function getCode()
350338 case ', ' :
351339 case '; ' :
352340 if ($ isShortClosure && $ open === 0 ) {
353- $ candidates [] = [
354- 'code ' => $ code ,
355- 'use ' => $ use ,
356- 'isShortClosure ' => $ isShortClosure ,
357- 'isUsingThisObject ' => $ isUsingThisObject ,
358- 'isUsingScope ' => $ isUsingScope ,
359- ];
360- $ code = '' ;
361- $ state = 'start ' ;
362- $ open = 0 ;
363- $ use = [];
364- $ isShortClosure = false ;
365- $ isUsingThisObject = false ;
366- $ isUsingScope = false ;
341+ $ reset = $ this ->collectCandidate ($ candidates , $ code , $ use , $ isShortClosure , $ isUsingThisObject , $ isUsingScope );
342+ $ code = $ reset ['code ' ];
343+ $ state = $ reset ['state ' ];
344+ $ open = $ reset ['open ' ];
345+ $ use = $ reset ['use ' ];
346+ $ isShortClosure = $ reset ['isShortClosure ' ];
347+ $ isUsingThisObject = $ reset ['isUsingThisObject ' ];
348+ $ isUsingScope = $ reset ['isUsingScope ' ];
367349 continue 3 ;
368350 }
369351 $ code .= $ token [0 ];
@@ -713,16 +695,6 @@ public function getCode()
713695 }
714696 }
715697
716- if ($ isShortClosure ) {
717- $ this ->useVariables = $ this ->getStaticVariables ();
718- } else {
719- $ this ->useVariables = empty ($ use ) ? $ use : array_intersect_key ($ this ->getStaticVariables (), array_flip ($ use ));
720- }
721-
722- $ this ->isShortClosure = $ isShortClosure ;
723- $ this ->isBindingRequired = $ isUsingThisObject ;
724- $ this ->isScopeRequired = $ isUsingScope ;
725-
726698 $ attributesCode = array_map (function ($ attribute ) {
727699 $ arguments = $ attribute ->getArguments ();
728700
@@ -740,107 +712,35 @@ public function getCode()
740712 return "#[ $ name( $ arguments)] " ;
741713 }, $ this ->getAttributes ());
742714
743- $ lastItem = array_pop ($ candidates );
744-
745- foreach ($ candidates as $ candidate ) {
746- $ code = $ candidate ['code ' ];
747- $ use = $ candidate ['use ' ];
748- $ isShortClosure = $ candidate ['isShortClosure ' ];
749- $ isUsingThisObject = $ candidate ['isUsingThisObject ' ];
750- $ isUsingScope = $ candidate ['isUsingScope ' ];
715+ if (count ($ candidates ) > 1 ) {
716+ $ lastItem = array_pop ($ candidates );
751717
752- // Verify Static
753- $ isStaticCode = mb_stripos ($ code , 'static ' ) !== false ;
754- if (parent ::isStatic () !== $ isStaticCode ) {
755- continue ;
756- }
757-
758- // Verify Parameters and Used Variables via parsing
759- $ tokens = token_get_all ('<?php ' .$ code );
760- $ params = [];
761- $ vars = [];
762- $ state = 'start ' ;
763- foreach ($ tokens as $ token ) {
764- if (! is_array ($ token )) {
765- if ($ token === '( ' && $ state === 'start ' ) {
766- $ state = 'params ' ;
767- } elseif ($ token === ') ' && $ state === 'params ' ) {
768- $ state = 'body ' ;
769- }
718+ foreach ($ candidates as $ candidate ) {
719+ if (! $ this ->verifyCandidateSignature ($ candidate )) {
770720 continue ;
771721 }
772- if ($ token [0 ] === T_VARIABLE ) {
773- $ name = substr ($ token [1 ], 1 );
774- if ($ state === 'params ' ) {
775- $ params [] = $ name ;
776- } elseif ($ state === 'body ' ) {
777- if ($ name !== 'this ' ) {
778- $ vars [$ name ] = true ;
779- }
780- }
781- }
782- }
783-
784- // Verify Param Count
785- if (parent ::getNumberOfParameters () !== count ($ params )) {
786- continue ;
787- }
788722
789- // Verify Use Vars (for Short Closures especially)
790- if ($ isShortClosure ) {
791- $ actualVars = array_keys (parent ::getStaticVariables ());
792- $ foundVars = array_keys ($ vars );
723+ $ this ->applyCandidate ($ candidate );
793724
794- $ foundCaptures = array_diff ( $ foundVars , $ params ) ;
725+ $ code = $ candidate [ ' code ' ] ;
795726
796- if (count ( $ foundCaptures ) !== count ( $ actualVars )) {
797- continue ;
727+ if (! empty ( $ attributesCode )) {
728+ $ code = implode ( "\n" , array_merge ( $ attributesCode , [ $ code ])) ;
798729 }
799730
800- if (count (array_diff ($ foundCaptures , $ actualVars )) > 0 ) {
801- continue ;
802- }
803- } else {
804- if (! empty ($ use )) {
805- $ actualStaticVariables = array_keys (parent ::getStaticVariables ());
806- if (count (array_diff ($ use , $ actualStaticVariables )) > 0 ) {
807- continue ;
808- }
809- }
810- if (count ($ use ) !== count (parent ::getStaticVariables ())) {
811- continue ;
812- }
813- }
731+ $ this ->code = $ code ;
814732
815- if ($ isShortClosure ) {
816- $ this ->useVariables = $ this ->getStaticVariables ();
817- } else {
818- $ this ->useVariables = empty ($ use ) ? $ use : array_intersect_key ($ this ->getStaticVariables (), array_flip ($ use ));
733+ return $ this ->code ;
819734 }
820735
821- $ this ->isShortClosure = $ isShortClosure ;
822- $ this ->isBindingRequired = $ isUsingThisObject ;
823- $ this ->isScopeRequired = $ isUsingScope ;
824- $ this ->code = $ code ;
825-
826- return $ this ->code ;
736+ $ candidates [] = $ lastItem ;
827737 }
828738
829- $ code = $ lastItem ['code ' ];
830- $ use = $ lastItem ['use ' ];
831- $ isShortClosure = $ lastItem ['isShortClosure ' ];
832- $ isUsingThisObject = $ lastItem ['isUsingThisObject ' ];
833- $ isUsingScope = $ lastItem ['isUsingScope ' ];
739+ $ lastItem = array_pop ($ candidates );
834740
835- if ($ isShortClosure ) {
836- $ this ->useVariables = $ this ->getStaticVariables ();
837- } else {
838- $ this ->useVariables = empty ($ use ) ? $ use : array_intersect_key ($ this ->getStaticVariables (), array_flip ($ use ));
839- }
741+ $ this ->applyCandidate ($ lastItem );
840742
841- $ this ->isShortClosure = $ isShortClosure ;
842- $ this ->isBindingRequired = $ isUsingThisObject ;
843- $ this ->isScopeRequired = $ isUsingScope ;
743+ $ code = $ lastItem ['code ' ];
844744
845745 if (! empty ($ attributesCode )) {
846746 $ code = implode ("\n" , array_merge ($ attributesCode , [$ code ]));
@@ -1389,4 +1289,135 @@ protected function parseNameQualified($token)
13891289
13901290 return [$ id_start , $ id_start_ci , $ id_name ];
13911291 }
1292+
1293+ /**
1294+ * Collect a closure candidate and reset state for finding the next one.
1295+ *
1296+ * @param array $candidates
1297+ * @param string $code
1298+ * @param array $use
1299+ * @param bool $isShortClosure
1300+ * @param bool $isUsingThisObject
1301+ * @param bool $isUsingScope
1302+ * @return array
1303+ */
1304+ protected function collectCandidate (&$ candidates , $ code , $ use , $ isShortClosure , $ isUsingThisObject , $ isUsingScope )
1305+ {
1306+ $ candidates [] = [
1307+ 'code ' => $ code ,
1308+ 'use ' => $ use ,
1309+ 'isShortClosure ' => $ isShortClosure ,
1310+ 'isUsingThisObject ' => $ isUsingThisObject ,
1311+ 'isUsingScope ' => $ isUsingScope ,
1312+ ];
1313+
1314+ return [
1315+ 'code ' => '' ,
1316+ 'state ' => 'start ' ,
1317+ 'open ' => 0 ,
1318+ 'use ' => [],
1319+ 'isShortClosure ' => false ,
1320+ 'isUsingThisObject ' => false ,
1321+ 'isUsingScope ' => false ,
1322+ ];
1323+ }
1324+
1325+ /**
1326+ * Apply a candidate's properties to this instance.
1327+ *
1328+ * @param array $candidate
1329+ * @return void
1330+ */
1331+ protected function applyCandidate ($ candidate )
1332+ {
1333+ if ($ candidate ['isShortClosure ' ]) {
1334+ $ this ->useVariables = $ this ->getStaticVariables ();
1335+ } else {
1336+ $ this ->useVariables = empty ($ candidate ['use ' ])
1337+ ? $ candidate ['use ' ]
1338+ : array_intersect_key ($ this ->getStaticVariables (), array_flip ($ candidate ['use ' ]));
1339+ }
1340+
1341+ $ this ->isShortClosure = $ candidate ['isShortClosure ' ];
1342+ $ this ->isBindingRequired = $ candidate ['isUsingThisObject ' ];
1343+ $ this ->isScopeRequired = $ candidate ['isUsingScope ' ];
1344+ }
1345+
1346+ /**
1347+ * Verify that a candidate matches the closure's signature.
1348+ *
1349+ * @param array $candidate
1350+ * @return bool
1351+ */
1352+ protected function verifyCandidateSignature ($ candidate )
1353+ {
1354+ $ code = $ candidate ['code ' ];
1355+ $ use = $ candidate ['use ' ];
1356+ $ isShortClosure = $ candidate ['isShortClosure ' ];
1357+
1358+ // Check if code starts with 'static' (more precise than searching anywhere in code)
1359+ $ isStaticCode = strtolower (substr (ltrim ($ code ), 0 , 6 )) === 'static ' ;
1360+ if (parent ::isStatic () !== $ isStaticCode ) {
1361+ return false ;
1362+ }
1363+
1364+ // Parse the candidate to extract parameters and variables
1365+ $ tokens = token_get_all ('<?php ' .$ code );
1366+ $ params = [];
1367+ $ vars = [];
1368+ $ state = 'start ' ;
1369+
1370+ foreach ($ tokens as $ token ) {
1371+ if (! is_array ($ token )) {
1372+ if ($ token === '( ' && $ state === 'start ' ) {
1373+ $ state = 'params ' ;
1374+ } elseif ($ token === ') ' && $ state === 'params ' ) {
1375+ $ state = 'body ' ;
1376+ }
1377+
1378+ continue ;
1379+ }
1380+
1381+ if ($ token [0 ] === T_VARIABLE ) {
1382+ $ name = substr ($ token [1 ], 1 );
1383+
1384+ if ($ state === 'params ' ) {
1385+ $ params [] = $ name ;
1386+ } elseif ($ state === 'body ' && $ name !== 'this ' ) {
1387+ $ vars [$ name ] = true ;
1388+ }
1389+ }
1390+ }
1391+
1392+ // Verify parameter count
1393+ if (parent ::getNumberOfParameters () !== count ($ params )) {
1394+ return false ;
1395+ }
1396+
1397+ // Verify use/captured variables
1398+ if ($ isShortClosure ) {
1399+ $ actualVars = array_keys (parent ::getStaticVariables ());
1400+ $ foundCaptures = array_diff (array_keys ($ vars ), $ params );
1401+
1402+ if (count ($ foundCaptures ) !== count ($ actualVars )) {
1403+ return false ;
1404+ }
1405+
1406+ if (count (array_diff ($ foundCaptures , $ actualVars )) > 0 ) {
1407+ return false ;
1408+ }
1409+ } else {
1410+ $ actualStaticVariables = array_keys (parent ::getStaticVariables ());
1411+
1412+ if (! empty ($ use ) && count (array_diff ($ use , $ actualStaticVariables )) > 0 ) {
1413+ return false ;
1414+ }
1415+
1416+ if (count ($ use ) !== count (parent ::getStaticVariables ())) {
1417+ return false ;
1418+ }
1419+ }
1420+
1421+ return true ;
1422+ }
13921423}
0 commit comments