Rector é uma ferramenta reconstrutora para PHP. Ele pega o código-fonte e as regras de transformação como entradas e modifica o código de acordo com as regras como saída.
Mesmo que não pensemos nisso, costumamos usar ferramentas para transformar o código PHP. Por exemplo, PHP CodeSniffer pode validar se o código obedece aos padrões PSR
e, quando não , executando o comando phpcbf
pode corrigi-lo automaticamente. Ou PHP-Scoper definirá o escopo das dependências no projeto para evitar possíveis conflitos.
O Rector é diferente dessas ferramentas por ser um meta-aplicativo. Não tem um objetivo predeterminado, como fixar estilos ou definir o escopo do projeto. Em vez disso, ele transformará o código seguindo as regras, para quaisquer que sejam as regras fornecidas. Então, Rector pode reproduzir perfeitamente PHP CodeSniffer, PHP-Scoper ou qualquer outra ferramenta de transformação de código.
Neste artigo, compartilharei algumas dicas para criar regras no Rector.
Os pilares do reitor
Rector está sobre os ombros de dois gigantes:
- PHP Parser : uma biblioteca que analisa o código PHP, permitindo a análise e manipulação de código estático
- PHPStan : uma ferramenta de análise estática
Graças ao PHP Parser, Rector pode manipular o código usando nós em um AST (abreviação de Abstract Syntax Tree ). E graças ao PHPStan, Rector pode entender o código, portanto, é capaz de mapear, navegar e validar os relacionamentos entre as entidades no código, como obter o ancestral de uma classe ou todas as suas interfaces implementadas.
É uma boa ideia ter um conhecimento básico dessas duas bibliotecas antes de começar com o Rector e continuar aprendendo com sua documentação enquanto trabalhamos com o Rector. Na verdade, quanto mais complexa for a regra do Reitor, mais importante se torna ter um bom domínio dessas duas bibliotecas.
Quais são as regras do reitor?
Uma regra é uma classe PHP herdada de AbstractRector
, que executa as transformações nos nós do AST (correspondente ao arquivo PHP analisado).
É composto de três métodos principais, que devemos satisfazer:
-
getRuleDefinition
: usado para documentar a regra -
getNodeTypes
: em que tipo de nó a regra será aplicada -
refactor
: lógica para produzir o novo nó AST
Por exemplo, a regra DowngradeNullCoalescingOperatorRector
substitui o operador ??=
introduzido no PHP 7.4 por seu equivalente no PHP 7.3. Ele tem esta implementação: >
use PhpParser \ Node; use PhpParser \ Node \ Expr \ Assign; use PhpParser \ Node \ Expr \ AssignOp \ Coalesce como AssignCoalesce; use PhpParser \ Node \ Expr \ BinaryOp \ Coalesce; use Rector \ Core \ Rector \ AbstractRector; use Symplify \ RuleDocGenerator \ ValueObject \ CodeSample \ CodeSample; use Symplify \ RuleDocGenerator \ ValueObject \ RuleDefinition; classe final DowngradeNullCoalescingOperatorRector estende AbstractRector { public function getRuleDefinition (): RuleDefinition { return new RuleDefinition ('Remover operador de coalescência nulo ??=', [ new CodeSample ( <<<'CODE_SAMPLE' $ array=[]; $ array ['user_id'] ??='valor'; CODE_SAMPLE , <<<'CODE_SAMPLE' $ array=[]; $ array ['user_id']=$ array ['user_id'] ??'valor'; CODE_SAMPLE ), ]); } /** * @return string [] */ public function getNodeTypes (): array { return [AssignCoalesce:: class]; } /** * @param AssignCoalesce $ node */ refator de função pública (Node $ node):? Node { retornar new Assign ($ node-> var, new Coalesce ($ node-> var, $ node-> expr)); } }
Vamos ver como funciona.
getRuleDefinition
Devemos fornecer um exemplo do código antes e depois da transformação. O Rector então usa esses dois estados para documentar as mudanças, usando o formato diff, como feito aqui :
$ array=[];
-$ array ['user_id'] ??='valor';
+ $ array ['user_id']=$ array ['user_id'] ??'valor';
getNodeTypes
Nesta função, indicamos em qual nó da AST a transformação será aplicada. Esses nós vêm diretamente do PHP Parser .
No exemplo acima, a regra é aplicada apenas em nós do tipo Coalesce
(apelidado de AssignCoalesce
), que é o nó que representa ??=
.
Alguns exemplos de outros nós são:
-
FuncCall
: sempre que chamar uma função, comovar_dump("hello")
-
MethodCall
: sempre que chamar um método de uma classe, como$foo->bar()
-
Atribuir
: ao atribuir um valor via=
-
Equal
,NotEqual
,Identical
eNotIdentical
: sempre que usar o operador binário==
,!=
,===
ou!==
, respectivamente
refactor
Esta função realiza a transformação, se necessário. Possui tipo de retorno ? Node
, o que significa:
- Retorne um novo nó, que substituirá o nó antigo; ou
- Retorne
null
, para significar nenhuma mudança
Observe que retornar null
significa “não modifique o nó”; não significa “remover o nó”.
A regra acima visa substituir $ foo ??=$ bar
por seu equivalente $ foo=$ foo ?? $ bar
. A função refactor
então retorna este novo nó:
return new Assign ( $ node-> var, novo Coalesce ( $ node-> var, $ node-> expr ) );
O novo nó é do tipo Atribuir
, que é o =
em $ foo=$ foo ?? $ bar
. Este tipo requer dois elementos:
- A variável
$ foo
, que é recuperada do nó original, como$node->var
- A expressão
$ foo ?? $ bar
Para criar a expressão, aninhamos um novo nó nela, do tipo [Coalesce] ( https://github.com/nikic/PHP-Parser/blob/master/lib/PhpParser/Node/Expr/BinaryOp/Coalesce.php )
, que é o ??
em $ foo ?? $ bar
. O operador de coalescência requer dois elementos:
- A expressão à esquerda
$ foo
, que é recuperada do nó original como$node->var
- A expressão à direita
$ bar
, que é recuperada do nó original como$node->expr
Este exemplo mostra o conceito básico do que envolve a criação de uma regra:
- Descubra qual novo nó satisfaz o código-alvo
- Identifique os dados de que necessita
- Dados de porta (variáveis, expressões) do nó antigo para o novo nó
Reutilizando código de regras existentes
No momento em que este artigo foi escrito, o repo Rector fornece quase 700 regras , envolvendo transformações de muitos tipos. Essas regras existentes são uma fonte maravilhosa para nos ajudar a implementar nossas próprias regras personalizadas.
Portanto, este é o meu conselho: sempre que precisar criar uma regra personalizada, verifique primeiro se uma lógica semelhante já foi codificada em alguma das regras existentes. Provavelmente, haverá.
Por exemplo, implementei a regra Agora, podemos não saber o tipo do parâmetro durante a análise estática. Por exemplo, esta função retorna uma string ou uma matriz: Então, nosso código precisa verificar o tipo do parâmetro durante o tempo de execução: Mas agora temos um problema: a função Portanto, a solução é atribuir o valor de Mas então, não posso escolher aleatoriamente o nome da variável como Eu não tinha ideia de como lidar com isso. Então, eu naveguei na lista de todas as regras no repo , verificando se alguma delas resolveria esse problema, ou seja, criar uma nova variável com um nome não utilizado. E eu encontrei. A regra A variável Depois, copiei/colei a lógica para usar o serviço, daqui : Como uma observação geral, acho o código-fonte no repositório Rector bastante elegante. Eu particularmente gosto que ele faz uso extensivo dos componentes do Symfony , incluindo injeção de dependência, comandos CLI e localizador de arquivos e diretórios. E aprendi um pouco sobre as práticas recomendadas de programação durante a navegação, então recomendo que você também faça. Aqui estão algumas dicas úteis para testar regras conectadas ao PHPUnit . Ao executar Por exemplo, ao executar o seguinte: Eu obteria este erro: Pelo erro, podemos dizer que o teste Isso é útil para depuração, para executar o método rápido e fácil de despejar a saída na tela de modo a visualizar onde pode estar o problema. Se precisarmos despejar o conteúdo de um nó, podemos fazer isso dentro da classe de regra, como este: Devemos usar Rector é uma ferramenta maravilhosa para transformar o código PHP. Estou usando para Transpilar meu aplicativo de PHP 7.4 para 7.1 para que eu possa codificá-lo usando recursos PHP modernos, mas implantá-lo no ambiente com suporte de meus clientes. A postagem Dicas para criar sua primeira regra de reitor para transformar O código PHP apareceu primeiro no LogRocket Blog . -strip_tags ($ string, ['a','p']);
+ strip_tags ($ string,'<'. implode ('> <', ['a','p']).'>');
function getStringOrArray () { if (rand (0, 1)) { return ['a','p']; } return'ap';
}
-strip_tags ($ string, getStringOrArray ());
+ strip_tags ($ string, is_array (getStringOrArray ())? ('<'. implode ('> <', getStringOrArray ()).'>'): getStringOrArray ());
getStringOrArray ()
é executada duas vezes, o que pode ser caro ou, pior ainda, pode produzir efeitos colaterais indesejados (por exemplo, se aumentar um contador global , ele fará isso duas vezes). getStringOrArray ()
a uma variável primeiro: -strip_tags ($ string, getStringOrArray ());
+ $ var=getStringOrArray ();
+ strip_tags ($ string, is_array ($ var)? ('<'. implode ('> <', $ var).'>'): $ var);
$ var
(ou qualquer outra coisa), pois ela pode já existir e eu estaria substituindo seu valor: $ var="blá, blá, blá";
-strip_tags ($ string, getStringOrArray ());
+ $ var=getStringOrArray ();
+ strip_tags ($ string, is_array ($ var)? ('<'. implode ('> <', $ var).'>'): $ var); var_dump ($ var);
//Ele espera"blá, blá, blá". Obteve"ap"
ForRepeatedCountToOwnVariableRector
faz esta transformação: classe SomeClass { execução de função pública ($ items) {
-para ($ i=5; $ i <=count ($ items); $ i ++) {
+ $ itemsCount=count ($ items);
+ para ($ i=5; $ i <=$ itemsCount; $ i ++) { echo $ items [$ i]; } } }
$ itemsCount
está sendo criada do nada. Verificando como isso é feito, descobri o serviço VariableNaming
, que pode identificar se a variável $ itemsCount
já existe. Em caso afirmativo, ele tenta novamente $ itemsCount2
e assim por diante até encontrar um nome de variável que não foi adicionado. $ variableName=$ this-> variableNaming-> resolveFromFuncCallFirstArgumentWithSuffix ( $ node, 'Contar', 'itemsCount', $ forScope
);
Dicas para teste
phpunit
para testar uma regra, se a regra tem muitos testes e apenas um está falhando, podemos executar apenas aquele passando --filter=test # X
, onde X
é o número do pedido do teste de fixação. vendor/bin/phpunit rules/solid/tests/Rector/Class_/FinalizeClassesWithoutChildrenRector/FinalizeClassesWithoutChildrenRectorTest.php
Houve 1 falha: 1) Rector \ DowngradePhp73 \ Tests \ Rector \ List_ \ DowngradeListReferenceAssignmentRector \ DowngradeListReferenceAssignmentRectorTest:: teste com conjunto de dados # 4 (objeto Symplify \ SmartFileSystem \ SmartFileInfo (...))
rules/downgrade-php73/tests/Rector/List_/DowngradeListReferenceAssignmentRector/Fixture/nested_list.php.inc
Falha ao afirmar que a string corresponde à descrição do formato.
nested_list.php.inc
é # 4
, então eu pude executar apenas aquele teste assim: vendor/bin/phpunit rules/solid/tests/Rector/Class_/FinalizeClassesWithoutChildrenRector/FinalizeClassesWithoutChildrenRectorTest.php--filter=test # 4
dump ($ this-> print ($ node));
morrer;
dump
, de componente VarDumper do Symfony , em vez de var_dump
porque:
dump
os identifica e interrompe, mas var_dump
não, então a saída na tela continuaria indefinidamente Conclusão