You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

如何在Rector中处理IntersectionType?Laravel组件迁移遇变量类名难题

解决Laravel Collective到Spatie/HTML迁移中含变量的Class属性转换问题

问题背景

我正在编写Rector规则集,将Laravel项目中laravel/collective的Form代码迁移到spatie/html。已完成静态Class属性的转换,示例如下:

// 原Laravel Collective代码
<?php echo \Collective\Html\FormFacade::button('<i class="fa fa-fw fa-close" aria-hidden="true"></i> ' . trans('LaravelLogger::laravel-logger.modals.shared.btnCancel'), array('class' => 'btn btn-outline pull-left btn-flat', 'type' => 'button', 'data-dismiss' => 'modal' )) ?>
<?php echo \Collective\Html\FormFacade::button('<i class="fa ' . $actionBtnIcon . '" aria-hidden="true"></i> ' . $btnSubmitText, array('class' => 'btn btn-' . $modalClass . ' pull-right btn-flat', 'type' => 'button', 'id' => 'confirm' )) ?>

// 转换后的Spatie/HTML代码
<?php echo html()->button('<i class="fa fa-fw fa-close" aria-hidden="true"></i> ' . trans('LaravelLogger::laravel-logger.modals.shared.btnCancel'))->class(['btn', 'btn-outline', 'pull-left', 'btn-flat'])->type(['button']) ?>
<?php echo html()->button('<i class="fa ' . $actionBtnIcon . '" aria-hidden="true"></i> ' . $btnSubmitText)->type(['button']) ?>

卡住的核心问题

当处理包含变量的Class属性时,比如:

array('class' => 'btn btn-' . $modalClass . ' pull-right btn-flat')

$this->valueResolver->getValue($arg->value)无法获取有效内容,需要将其转换为:

->class(['btn', 'btn-'. $modalClass, 'pull-right', 'btn-flat'])

当前自定义ValueResolver的extractConstantArrayTypeValue方法在处理动态字符串拼接时报错,无法解析包含变量的结构。

解决方案

1. 放弃Type解析,直接操作AST节点

动态字符串拼接在PHP AST中是BinaryOp\Concat节点,无法通过PHPStan的类型解析获取完整值,必须直接遍历AST节点处理。

2. 修改Rector规则的核心处理逻辑

在规则中直接识别FormFacade::button调用,遍历其属性数组节点,提取并转换Class属性:

// 识别Laravel Collective的button调用
if ($node instanceof MethodCall && $this->isFormFacadeButtonCall($node)) {
    $attributesArg = $node->args[1] ?? null;
    if (!$attributesArg || !$attributesArg->value instanceof Array_) {
        return null;
    }

    $arrayNode = $attributesArg->value;
    $classElement = null;

    // 遍历数组找到class键值对
    foreach ($arrayNode->items as $item) {
        if (!$item || !$item->key instanceof String_ || $item->key->value !== 'class') {
            continue;
        }
        $classElement = $item;
        break;
    }

    if (!$classElement || !$classElement->value) {
        return null;
    }

    // 解析Class值为数组项
    $classArrayItems = $this->parseClassValueToArrayItems($classElement->value);
    if (!$classArrayItems) {
        return null;
    }

    // 创建->class()方法调用节点
    $classMethodCall = $this->nodeFactory->createMethodCall(
        $node,
        'class',
        [$this->nodeFactory->createArray($classArrayItems)]
    );

    // 移除原数组中的class元素
    $arrayNode->items = array_filter($arrayNode->items, fn($item) => $item !== $classElement);
    // 数组为空则移除整个属性参数
    if (empty($arrayNode->items)) {
        $node->args = array_filter($node->args, fn($arg) => $arg !== $attributesArg);
    }

    return $classMethodCall;
}

3. 实现动态Class值的解析方法

编写递归方法处理字符串拼接节点,保留变量结构并按空格拆分Class项:

private function parseClassValueToArrayItems(Expr $valueExpr): array
{
    $classParts = [];

    // 递归处理字符串拼接节点
    if ($valueExpr instanceof BinaryOp\Concat) {
        $leftParts = $this->parseClassValueToArrayItems($valueExpr->left);
        $rightParts = $this->parseClassValueToArrayItems($valueExpr->right);
        $classParts = array_merge($leftParts, $rightParts);
    }
    // 处理纯字符串节点,按空格拆分
    elseif ($valueExpr instanceof String_) {
        $splitParts = explode(' ', trim($valueExpr->value));
        $classParts = array_filter($splitParts);
        // 转换为String节点
        $classParts = array_map(fn($part) => $this->nodeFactory->createString($part), $classParts);
    }
    // 处理变量或其他动态表达式,直接保留
    else {
        $classParts[] = $valueExpr;
    }

    // 合并相邻的字符串节点,避免拆分"btn-"和$modalClass
    $mergedParts = [];
    $currentString = '';
    foreach ($classParts as $part) {
        if ($part instanceof String_) {
            $currentString .= $part->value;
        } else {
            if (!empty($currentString)) {
                $mergedParts[] = $this->nodeFactory->createString($currentString);
                $currentString = '';
            }
            $mergedParts[] = $part;
        }
    }
    if (!empty($currentString)) {
        $mergedParts[] = $this->nodeFactory->createString($currentString);
    }

    // 再次拆分合并后的字符串,确保按空格分割
    $finalParts = [];
    foreach ($mergedParts as $part) {
        if ($part instanceof String_) {
            $splitParts = explode(' ', trim($part->value));
            foreach ($splitParts as $splitPart) {
                if (!empty($splitPart)) {
                    $finalParts[] = $this->nodeFactory->createString($splitPart);
                }
            }
        } else {
            $finalParts[] = $part;
        }
    }

    return $finalParts;
}

4. 效果验证

对于'btn btn-' . $modalClass . ' pull-right btn-flat',解析后会生成:

  • String_('btn')
  • BinaryOp\Concat(String_('btn-'), Variable('modalClass'))
  • String_('pull-right')
  • String_('btn-flat')

最终生成的->class()调用参数就是目标格式:

['btn', 'btn-' . $modalClass, 'pull-right', 'btn-flat']

关键注意事项

  • 动态内容无法通过Type解析获取,必须直接操作AST节点
  • 递归处理拼接节点,保留变量的原有结构
  • 合并相邻字符串节点,避免破坏btn-与变量的拼接关系

内容的提问来源于stack exchange,提问作者Dmitry Vasilyev

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.06.14 17:17:03