Created
June 10, 2026 17:32
-
-
Save saade/2c04fba7ab0ef7eccc839c39d7ebef87 to your computer and use it in GitHub Desktop.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| <?php | |
| namespace Filament\Support\Rector; | |
| use Filament\Actions\Action; | |
| use Filament\Forms\Components\Concerns\CanBeValidated; | |
| use Filament\Notifications\Notification; | |
| use Filament\Schemas\Components\Component as SchemaComponent; | |
| use Filament\Schemas\Components\Concerns\HasState; | |
| use Filament\Schemas\Schema; | |
| use PhpParser\Node; | |
| use PhpParser\Node\Expr; | |
| use PhpParser\Node\Expr\MethodCall; | |
| use PhpParser\Node\Expr\StaticCall; | |
| use PHPStan\Type\Type; | |
| use Rector\Rector\AbstractRector; | |
| use Symplify\RuleDocGenerator\ValueObject\CodeSample\CodeSample; | |
| use Symplify\RuleDocGenerator\ValueObject\RuleDefinition; | |
| class AlphabetizeFluentMethodsRector extends AbstractRector | |
| { | |
| /** | |
| * @var array<string, ?string> | |
| */ | |
| private array $methodTraitCache = []; | |
| public function getRuleDefinition(): RuleDefinition | |
| { | |
| return new RuleDefinition( | |
| 'Alphabetize methods in fluent builder chains for Filament classes with specific ordering rules', | |
| [ | |
| new CodeSample( | |
| <<<'CODE_SAMPLE' | |
| TextInput::make('name') | |
| ->default('test') | |
| ->sortable() | |
| ->searchable() | |
| ->required() | |
| ->label('Name'); | |
| CODE_SAMPLE | |
| , | |
| <<<'CODE_SAMPLE' | |
| TextInput::make('name') | |
| ->label('Name') | |
| ->searchable() | |
| ->sortable() | |
| ->required() | |
| ->default('test'); | |
| CODE_SAMPLE | |
| ), | |
| ] | |
| ); | |
| } | |
| public function getNodeTypes(): array | |
| { | |
| return [MethodCall::class]; | |
| } | |
| /** | |
| * @param MethodCall $node | |
| */ | |
| public function refactor(Node $node): ?Node | |
| { | |
| // Get the root of the chain | |
| $rootNode = $this->getRootNode($node); | |
| // Check if this is a Filament class | |
| if (! $this->isFilamentClass($rootNode)) { | |
| return null; | |
| } | |
| // Only process chains that start with a ::make() static call | |
| if (! $this->isChainStartingWithMake($rootNode)) { | |
| return null; | |
| } | |
| // Only process the outermost method call in the chain | |
| if (! $this->isOutermostMethodCall($node)) { | |
| return null; | |
| } | |
| // Get the class name early for terminal method detection | |
| $className = $this->getClassName($rootNode); | |
| // Check if the outermost method is non-fluent or terminal | |
| $rootType = $this->nodeTypeResolver->getType($rootNode); | |
| $nonFluentTail = null; | |
| $terminalMethods = []; | |
| $chainToProcess = $node; | |
| // Collect non-fluent and terminal methods from the end | |
| $current = $node; | |
| // @phpstan-ignore-next-line instanceof.alwaysTrue - First iteration is always true, but $current is reassigned in loop | |
| while ($current instanceof MethodCall) { | |
| $methodName = $this->getName($current->name); | |
| if (! $this->isFluentBuilderMethod($current, $rootType)) { | |
| // Non-fluent method, save it | |
| $nonFluentTail = $current; | |
| $chainToProcess = $current->var; | |
| if (! $chainToProcess instanceof MethodCall) { | |
| return null; | |
| } | |
| $current = $chainToProcess; | |
| } elseif ($this->isTerminalMethod($methodName, $className)) { | |
| // Terminal method, save it | |
| array_unshift($terminalMethods, ['name' => $methodName, 'call' => $current]); | |
| $chainToProcess = $current->var; | |
| if (! $chainToProcess instanceof MethodCall) { | |
| // If only terminal methods, nothing to sort | |
| return null; | |
| } | |
| $current = $chainToProcess; | |
| } else { | |
| // Regular fluent method, stop collecting | |
| break; | |
| } | |
| } | |
| // Collect all method calls in the chain (excluding the make() call itself and terminal methods) | |
| $methodCalls = $this->collectMethodCalls($chainToProcess); | |
| // If there are less than 2 methods, no need to sort | |
| if (count($methodCalls) < 2) { | |
| return null; | |
| } | |
| // Split the chain at clone() boundaries and sort each segment independently | |
| $sortedCalls = $this->sortMethodCallsWithCloneBoundaries($methodCalls, $className); | |
| // Check if the order changed | |
| if ($sortedCalls === $methodCalls) { | |
| return null; | |
| } | |
| // Rebuild the chain with sorted methods | |
| $rebuiltChain = $this->rebuildChain($rootNode, $sortedCalls); | |
| // Sort and reattach terminal methods in the correct order | |
| $sortedTerminalMethods = $this->sortTerminalMethods($terminalMethods, $className); | |
| foreach ($sortedTerminalMethods as $terminalData) { | |
| $newTerminal = clone $terminalData['call']; | |
| $newTerminal->var = $rebuiltChain; | |
| $rebuiltChain = $newTerminal; | |
| } | |
| // If there was a non-fluent tail, reattach it | |
| if ($nonFluentTail !== null) { | |
| $newTail = clone $nonFluentTail; | |
| $newTail->var = $rebuiltChain; | |
| return $newTail; | |
| } | |
| return $rebuiltChain; | |
| } | |
| /** | |
| * Check if a method is a terminal method that should stay at the end | |
| */ | |
| protected function isTerminalMethod(?string $methodName, ?string $className): bool | |
| { | |
| if ($methodName === null || $className === null) { | |
| return false; | |
| } | |
| if ($this->isAction($className)) { | |
| return $this->isActionTerminalMethod($methodName); | |
| } | |
| if ($this->isNotification($className)) { | |
| return $this->isNotificationTerminalMethod($methodName); | |
| } | |
| return false; | |
| } | |
| /** | |
| * Check if a method is a terminal method for Actions | |
| */ | |
| protected function isActionTerminalMethod(string $methodName): bool | |
| { | |
| // Action setup methods (run before lifecycle) | |
| $setupMethods = [ | |
| 'fillForm', | |
| 'mountUsing', | |
| ]; | |
| // Action lifecycle methods | |
| $lifecycleMethods = [ | |
| 'before', | |
| 'action', | |
| 'using', | |
| 'after', | |
| ]; | |
| if (in_array($methodName, $setupMethods, true)) { | |
| return true; | |
| } | |
| if (in_array($methodName, $lifecycleMethods, true)) { | |
| return true; | |
| } | |
| // success* and failure* methods for Actions | |
| if (str_starts_with($methodName, 'success') || str_starts_with($methodName, 'failure')) { | |
| return true; | |
| } | |
| return false; | |
| } | |
| /** | |
| * Check if a method is a terminal method for Notifications | |
| */ | |
| protected function isNotificationTerminalMethod(string $methodName): bool | |
| { | |
| // Methods that execute/send notifications and should remain at the end | |
| $terminalMethods = [ | |
| 'send', | |
| 'sendToDatabase', | |
| 'broadcast', | |
| 'dispatch', | |
| 'sendToQueue', | |
| ]; | |
| return in_array($methodName, $terminalMethods, true); | |
| } | |
| /** | |
| * Sort terminal methods according to class-specific ordering | |
| * | |
| * @param array<array{name: string, call: MethodCall}> $terminalMethods | |
| * @return array<array{name: string, call: MethodCall}> | |
| */ | |
| protected function sortTerminalMethods(array $terminalMethods, ?string $className): array | |
| { | |
| if ($className === null) { | |
| return $terminalMethods; | |
| } | |
| if ($this->isAction($className)) { | |
| return $this->sortActionTerminalMethods($terminalMethods); | |
| } | |
| if ($this->isNotification($className)) { | |
| return $this->sortNotificationTerminalMethods($terminalMethods); | |
| } | |
| return $terminalMethods; | |
| } | |
| /** | |
| * Sort terminal methods for Actions according to their lifecycle order | |
| * | |
| * @param array<array{name: string, call: MethodCall}> $terminalMethods | |
| * @return array<array{name: string, call: MethodCall}> | |
| */ | |
| protected function sortActionTerminalMethods(array $terminalMethods): array | |
| { | |
| usort($terminalMethods, function ($a, $b) { | |
| $priorityA = $this->getActionTerminalMethodPriority($a['name']); | |
| $priorityB = $this->getActionTerminalMethodPriority($b['name']); | |
| return $priorityA <=> $priorityB; | |
| }); | |
| return $terminalMethods; | |
| } | |
| /** | |
| * Get the priority order for Action terminal methods (lower = earlier in chain) | |
| */ | |
| protected function getActionTerminalMethodPriority(string $methodName): int | |
| { | |
| // Define the order of terminal methods | |
| $terminalMethodOrder = [ | |
| 'fillForm', | |
| 'mountUsing', | |
| 'before', | |
| 'action', | |
| 'using', | |
| 'after', | |
| ]; | |
| // Check exact match first | |
| $index = array_search($methodName, $terminalMethodOrder, true); | |
| if ($index !== false) { | |
| return $index; | |
| } | |
| // Success methods come after lifecycle | |
| if (str_starts_with($methodName, 'success')) { | |
| return count($terminalMethodOrder); | |
| } | |
| // Failure methods come after success methods | |
| if (str_starts_with($methodName, 'failure')) { | |
| return count($terminalMethodOrder) + 1; | |
| } | |
| // Default priority for unknown terminal methods | |
| return 999; | |
| } | |
| /** | |
| * Sort terminal methods for Notifications | |
| * | |
| * @param array<array{name: string, call: MethodCall}> $terminalMethods | |
| * @return array<array{name: string, call: MethodCall}> | |
| */ | |
| protected function sortNotificationTerminalMethods(array $terminalMethods): array | |
| { | |
| usort($terminalMethods, function ($a, $b) { | |
| $priorityA = $this->getNotificationTerminalMethodPriority($a['name']); | |
| $priorityB = $this->getNotificationTerminalMethodPriority($b['name']); | |
| return $priorityA <=> $priorityB; | |
| }); | |
| return $terminalMethods; | |
| } | |
| /** | |
| * Get the priority order for Notification terminal methods (lower = earlier in chain) | |
| */ | |
| protected function getNotificationTerminalMethodPriority(string $methodName): int | |
| { | |
| // Define the order of notification terminal methods | |
| $terminalMethodOrder = [ | |
| 'send', | |
| 'sendToDatabase', | |
| 'broadcast', | |
| ]; | |
| $index = array_search($methodName, $terminalMethodOrder, true); | |
| return $index !== false ? $index : 999; | |
| } | |
| protected function getRootNode(MethodCall $node): Expr | |
| { | |
| $current = $node; | |
| while ($current->var instanceof MethodCall) { | |
| $current = $current->var; | |
| } | |
| return $current->var; | |
| } | |
| protected function getClassName(Node $node): ?string | |
| { | |
| if ($node instanceof StaticCall) { | |
| $callerType = $this->nodeTypeResolver->getType($node->class); | |
| if ($callerType->isObject()->yes()) { | |
| $classNames = $callerType->getObjectClassNames(); | |
| return $classNames[0] ?? null; | |
| } | |
| } | |
| return null; | |
| } | |
| protected function isFilamentClass(Node $node): bool | |
| { | |
| // For StaticCall, check the class name | |
| if ($node instanceof StaticCall) { | |
| $callerType = $this->nodeTypeResolver->getType($node->class); | |
| if ($callerType->isObject()->yes()) { | |
| $classNames = $callerType->getObjectClassNames(); | |
| foreach ($classNames as $className) { | |
| // Exclude classes that have order-dependent builder methods | |
| if ($this->isExcludedClass($className)) { | |
| return false; | |
| } | |
| if (str_starts_with($className, 'Filament\\')) { | |
| return true; | |
| } | |
| } | |
| } | |
| } | |
| // For other expressions, check their type | |
| $nodeType = $this->nodeTypeResolver->getType($node); | |
| if ($nodeType->isObject()->yes()) { | |
| $classNames = $nodeType->getObjectClassNames(); | |
| foreach ($classNames as $className) { | |
| // Exclude classes that have order-dependent builder methods | |
| if ($this->isExcludedClass($className)) { | |
| return false; | |
| } | |
| if (str_starts_with($className, 'Filament\\')) { | |
| return true; | |
| } | |
| } | |
| } | |
| return false; | |
| } | |
| protected function isExcludedClass(string $className): bool | |
| { | |
| // Classes with order-dependent builder methods | |
| $excludedClasses = [ | |
| Schema::class, | |
| ]; | |
| foreach ($excludedClasses as $excludedClass) { | |
| if ($className === $excludedClass || is_subclass_of($className, $excludedClass)) { | |
| return true; | |
| } | |
| } | |
| return false; | |
| } | |
| protected function isChainStartingWithMake(Node $node): bool | |
| { | |
| if ($node instanceof StaticCall) { | |
| return $this->isName($node->name, 'make'); | |
| } | |
| return false; | |
| } | |
| protected function isOutermostMethodCall(MethodCall $node): bool | |
| { | |
| // Check if this method call is not part of a larger method call chain | |
| $parent = $node->getAttribute('parent'); | |
| return ! ($parent instanceof MethodCall && $parent->var === $node); | |
| } | |
| /** | |
| * @return array<array{name: string, call: MethodCall}> | |
| */ | |
| protected function collectMethodCalls(MethodCall $node): array | |
| { | |
| $calls = []; | |
| $current = $node; | |
| $rootType = $this->nodeTypeResolver->getType($this->getRootNode($node)); | |
| while ($current instanceof MethodCall) { | |
| $methodName = $this->getName($current->name); | |
| if ($methodName !== null && $this->isFluentBuilderMethod($current, $rootType)) { | |
| // Add to the beginning of the array to maintain original order | |
| array_unshift($calls, [ | |
| 'name' => $methodName, | |
| 'call' => $current, | |
| ]); | |
| } | |
| $current = $current->var; | |
| } | |
| return $calls; | |
| } | |
| protected function isFluentBuilderMethod(MethodCall $methodCall, Type $rootType): bool | |
| { | |
| // Get the return type of this method call | |
| $returnType = $this->nodeTypeResolver->getType($methodCall); | |
| // Root must be an object to have a fluent interface | |
| if (! $rootType->isObject()->yes()) { | |
| return false; | |
| } | |
| // If return type is definitely NOT an object (void, bool, string, int, array, etc.) | |
| if ($returnType->isObject()->no()) { | |
| return false; | |
| } | |
| $rootClassNames = $rootType->getObjectClassNames(); | |
| // Try to check if the return type matches using isSuperTypeOf | |
| // This handles cases where getObjectClassNames() might not work | |
| if ($returnType->isSuperTypeOf($rootType)->yes() || $rootType->isSuperTypeOf($returnType)->yes()) { | |
| return true; | |
| } | |
| // Try to get class names from the return type | |
| $returnClassNames = $returnType->getObjectClassNames(); | |
| // If we got class names from both, check if they match | |
| if (! empty($rootClassNames) && ! empty($returnClassNames)) { | |
| return ! empty(array_intersect($rootClassNames, $returnClassNames)); | |
| } | |
| // If we couldn't get return class names, the type is unknown | |
| // We cannot determine if it's fluent based on types alone | |
| return false; | |
| } | |
| protected function getMethodDefiningTrait(string $className, string $methodName): ?string | |
| { | |
| $cacheKey = $className . '::' . $methodName; | |
| if (isset($this->methodTraitCache[$cacheKey])) { | |
| return $this->methodTraitCache[$cacheKey]; | |
| } | |
| // Try to get the class reflection | |
| if (! class_exists($className) && ! trait_exists($className)) { | |
| $this->methodTraitCache[$cacheKey] = null; | |
| return null; | |
| } | |
| try { | |
| $reflection = new \ReflectionClass($className); | |
| // Check if the method exists in the class or its traits | |
| if (! $reflection->hasMethod($methodName)) { | |
| $this->methodTraitCache[$cacheKey] = null; | |
| return null; | |
| } | |
| $method = $reflection->getMethod($methodName); | |
| $declaringClass = $method->getDeclaringClass(); | |
| // Check if it's from the validation trait | |
| if ($declaringClass->getName() === CanBeValidated::class) { | |
| $this->methodTraitCache[$cacheKey] = CanBeValidated::class; | |
| return CanBeValidated::class; | |
| } | |
| // Check if it's from the state trait | |
| if ($declaringClass->getName() === HasState::class) { | |
| $this->methodTraitCache[$cacheKey] = HasState::class; | |
| return HasState::class; | |
| } | |
| // Check if the declaring class uses these traits | |
| $traits = $this->getAllTraits($declaringClass); | |
| if (in_array(CanBeValidated::class, $traits, true)) { | |
| // Check if method is defined in the validation trait | |
| if (trait_exists(CanBeValidated::class)) { | |
| $traitReflection = new \ReflectionClass(CanBeValidated::class); | |
| if ($traitReflection->hasMethod($methodName)) { | |
| $this->methodTraitCache[$cacheKey] = CanBeValidated::class; | |
| return CanBeValidated::class; | |
| } | |
| } | |
| } | |
| if (in_array(HasState::class, $traits, true)) { | |
| // Check if method is defined in the state trait | |
| if (trait_exists(HasState::class)) { | |
| $traitReflection = new \ReflectionClass(HasState::class); | |
| if ($traitReflection->hasMethod($methodName)) { | |
| $this->methodTraitCache[$cacheKey] = HasState::class; | |
| return HasState::class; | |
| } | |
| } | |
| } | |
| } catch (\ReflectionException $e) { | |
| // Class doesn't exist or can't be reflected | |
| } | |
| $this->methodTraitCache[$cacheKey] = null; | |
| return null; | |
| } | |
| /** | |
| * @return array<string> | |
| */ | |
| protected function getAllTraits(\ReflectionClass $class): array | |
| { | |
| $traits = []; | |
| do { | |
| $traits = array_merge($traits, $class->getTraitNames()); | |
| } while ($class = $class->getParentClass()); | |
| // Also get traits of traits | |
| $traitTraits = []; | |
| foreach ($traits as $trait) { | |
| if (trait_exists($trait)) { | |
| $traitReflection = new \ReflectionClass($trait); | |
| $traitTraits = array_merge($traitTraits, $this->getAllTraits($traitReflection)); | |
| } | |
| } | |
| return array_unique(array_merge($traits, $traitTraits)); | |
| } | |
| /** | |
| * @param array<array{name: string, call: MethodCall}> $methodCalls | |
| * @return array<array{name: string, call: MethodCall}> | |
| */ | |
| protected function sortMethodCallsWithCloneBoundaries(array $methodCalls, ?string $className): array | |
| { | |
| // Split the chain into segments at clone*() boundaries | |
| $segments = []; | |
| $currentSegment = []; | |
| foreach ($methodCalls as $callData) { | |
| if (str_starts_with($callData['name'], 'clone')) { | |
| // Sort the current segment before adding clone | |
| if (! empty($currentSegment)) { | |
| $segments[] = $this->sortMethodCalls($currentSegment, $className); | |
| } | |
| // Add clone method as its own segment (don't reorder it) | |
| $segments[] = [$callData]; | |
| $currentSegment = []; | |
| } else { | |
| $currentSegment[] = $callData; | |
| } | |
| } | |
| // Sort the last segment | |
| if (! empty($currentSegment)) { | |
| $segments[] = $this->sortMethodCalls($currentSegment, $className); | |
| } | |
| // Flatten the segments back into a single array | |
| return array_merge(...$segments); | |
| } | |
| /** | |
| * @param array<array{name: string, call: MethodCall}> $methodCalls | |
| * @return array<array{name: string, call: MethodCall}> | |
| */ | |
| protected function sortMethodCalls(array $methodCalls, ?string $className): array | |
| { | |
| if (! $className) { | |
| return $this->sortDefaultMethods($methodCalls); | |
| } | |
| // Determine which sorting behavior to use based on class type | |
| if ($this->isAction($className)) { | |
| return $this->sortActionMethods($methodCalls, $className); | |
| } | |
| if ($this->isNotification($className)) { | |
| return $this->sortNotificationMethods($methodCalls, $className); | |
| } | |
| // Check if this is a Component (forms/schemas/tables components) | |
| if ($this->isSchemaComponent($className)) { | |
| return $this->sortSchemaComponentMethods($methodCalls, $className); | |
| } | |
| return $this->sortDefaultMethods($methodCalls); | |
| } | |
| protected function isAction(string $className): bool | |
| { | |
| if (! class_exists($className)) { | |
| return false; | |
| } | |
| return is_a($className, Action::class, true); | |
| } | |
| protected function isNotification(string $className): bool | |
| { | |
| if (! class_exists($className)) { | |
| return false; | |
| } | |
| return is_a($className, Notification::class, true); | |
| } | |
| protected function isSchemaComponent(string $className): bool | |
| { | |
| if (! class_exists($className)) { | |
| return false; | |
| } | |
| return is_a($className, SchemaComponent::class, true); | |
| } | |
| /** | |
| * @param array<array{name: string, call: MethodCall}> $methodCalls | |
| * @return array<array{name: string, call: MethodCall}> | |
| */ | |
| protected function sortActionMethods(array $methodCalls, string $className): array | |
| { | |
| $labelMethods = []; | |
| $regularMethods = []; | |
| foreach ($methodCalls as $callData) { | |
| $methodName = $callData['name']; | |
| if ($methodName === 'label') { | |
| $labelMethods[] = $callData; | |
| } else { | |
| $regularMethods[] = $callData; | |
| } | |
| } | |
| // Sort regular methods alphabetically | |
| usort($regularMethods, fn ($a, $b) => strcmp($a['name'], $b['name'])); | |
| return array_merge($labelMethods, $regularMethods); | |
| } | |
| /** | |
| * @param array<array{name: string, call: MethodCall}> $methodCalls | |
| * @return array<array{name: string, call: MethodCall}> | |
| */ | |
| protected function sortNotificationMethods(array $methodCalls, string $className): array | |
| { | |
| $titleMethods = []; | |
| $bodyMethods = []; | |
| $regularMethods = []; | |
| foreach ($methodCalls as $callData) { | |
| $methodName = $callData['name']; | |
| if ($methodName === 'title') { | |
| $titleMethods[] = $callData; | |
| } elseif ($methodName === 'body') { | |
| $bodyMethods[] = $callData; | |
| } else { | |
| $regularMethods[] = $callData; | |
| } | |
| } | |
| // Sort regular methods alphabetically | |
| usort($regularMethods, fn ($a, $b) => strcmp($a['name'], $b['name'])); | |
| // Order: title, body, then alphabetically sorted regular methods | |
| return array_merge($titleMethods, $bodyMethods, $regularMethods); | |
| } | |
| /** | |
| * @param array<array{name: string, call: MethodCall}> $methodCalls | |
| * @return array<array{name: string, call: MethodCall}> | |
| */ | |
| protected function sortSchemaComponentMethods(array $methodCalls, string $className): array | |
| { | |
| $labelMethods = []; | |
| $regularMethods = []; | |
| $validationMethods = []; | |
| $stateMethods = []; | |
| foreach ($methodCalls as $callData) { | |
| $methodName = $callData['name']; | |
| if ($methodName === 'label') { | |
| $labelMethods[] = $callData; | |
| } elseif ($this->getMethodDefiningTrait($className, $methodName) === CanBeValidated::class) { | |
| $validationMethods[] = $callData; | |
| } elseif ($this->getMethodDefiningTrait($className, $methodName) === HasState::class) { | |
| $stateMethods[] = $callData; | |
| } else { | |
| $regularMethods[] = $callData; | |
| } | |
| } | |
| // Sort regular methods alphabetically | |
| usort($regularMethods, fn ($a, $b) => strcmp($a['name'], $b['name'])); | |
| // Sort validation methods with 'required' first, then alphabetically | |
| usort($validationMethods, function ($a, $b) { | |
| if ($a['name'] === 'required') { | |
| return -1; | |
| } | |
| if ($b['name'] === 'required') { | |
| return 1; | |
| } | |
| return strcmp($a['name'], $b['name']); | |
| }); | |
| // Sort state methods alphabetically | |
| usort($stateMethods, fn ($a, $b) => strcmp($a['name'], $b['name'])); | |
| // Order: label, regular, validation, state | |
| return array_merge($labelMethods, $regularMethods, $validationMethods, $stateMethods); | |
| } | |
| /** | |
| * @param array<array{name: string, call: MethodCall}> $methodCalls | |
| * @return array<array{name: string, call: MethodCall}> | |
| */ | |
| protected function sortDefaultMethods(array $methodCalls): array | |
| { | |
| $sortedMethods = $methodCalls; | |
| // Sort all methods alphabetically | |
| usort($sortedMethods, fn ($a, $b) => strcmp($a['name'], $b['name'])); | |
| return $sortedMethods; | |
| } | |
| /** | |
| * @param array<array{name: string, call: MethodCall}> $sortedCalls | |
| */ | |
| protected function rebuildChain(Expr $rootNode, array $sortedCalls): MethodCall | |
| { | |
| $chain = null; | |
| foreach ($sortedCalls as $callData) { | |
| $originalCall = $callData['call']; | |
| if ($chain === null) { | |
| // First method in chain - clone the original to preserve attributes | |
| $newCall = clone $originalCall; | |
| $newCall->var = $rootNode; | |
| $chain = $newCall; | |
| } else { | |
| // Subsequent methods - clone to preserve attributes | |
| $newCall = clone $originalCall; | |
| $newCall->var = $chain; | |
| $chain = $newCall; | |
| } | |
| // Preserve original attributes for formatting | |
| foreach ($originalCall->getAttributes() as $key => $value) { | |
| $chain->setAttribute($key, $value); | |
| } | |
| } | |
| return $chain; | |
| } | |
| } |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment