
The quality of the code is not only how it works, but also how it looks. The fact that a single code style within a campaign is a very important thing is no longer necessary to convince anyone nowadays. The code must be not only written, but also decorated. In terms of the design of the PHP code, the
php-cs-fixer utility has long become a standard. It is quite simple to use it, there are a lot of rules and you can comfortably start it up on any combination of keys in a storm or on a pre-commit hook in the git. All this is easily googled and versed in hundreds of articles. And today we will talk about something else. Although php-cs-fixer has a large number of different fixers, but what if we need one that is not there? How to write your own fixer?
Fixer?
What is a fixer? Fixer, this is a small class that fixes your code, leads it to some kind of form. I did not invent stupid or complicated cases for a new fixer, and decided to take some very real one. For example, reducing all keywords in code to lowercase. The fixer
LowercaseKeywordsFixer is responsible for
this . Let's use his example to learn how to create your own fixers.
Fixim
So, you have already done
git clone https://github.com/FriendsOfPHP/PHP-CS-Fixer.git composer install
Our experimental fixer consists of two parts:
Fixer itself:
src/Fixer/Casing/LowercaseKeywordsFixer.php
And the test:
tests/Fixer/Casing/LowercaseKeywordsFixerTest.php
LowercaseKeywordsFixer.php is the file that contains the fixer class. Each fixer should be inherited from the abstract class
PhpCsFixer \ AbstractFixer, and therefore contain methods:
getDefinition(); isCandidate(Tokens $tokens); applyFix(\SplFileInfo $file, Tokens $tokens);
We will return to these methods. Let's now consider a very important concept for us: Token.
')
Token in php
If you are familiar with PHP, then the concept of tokens is not new to you. In Russian, they are sometimes called “tags”. Tokens are PHP language tokens. For example, if you take such a simple code:
<?php foreach ($a as $B) { try { new $c($a, isset($b)); } catch (\Exception $e) { exit(1); } }
and break it into tokens, then we get an array of 54 elements. The second element will be:
Array ( [0] => 334 [1] => foreach [2] => 3 )
Where 334 is the token identifier. That is, not this particular token, but this type of token. In other words, all tokens that represent the foreach construct will have identifier 382. This identifier corresponds to the constant
T_FOREACH . A list of all constants can be found
in the documentation .
A very important point.
Identifiers change from version to version of PHP interpreter , your code should never depend on specific digits, just
constants !
You can read more about tokens in the
documentation .
Token in php-cs-fixer
In php-cs-fixer there are two classes for working with tokens:
PhpCsFixer \ Tokenizer \ Tokens for working with an array of tokens, and
PhpCsFixer \ Tokenizer \ Token to work with one token.
Consider some useful methods.
Token: equals($other, $caseSensitive = true)
Verifies that the token passed by the first parameter is equivalent to the current one. This is the most correct way to verify that tokens are equal.
equalsAny(array $others, $caseSensitive = true);
Checks that one of the tokens passed in the first parameter is equal to the current one.
getContent();
Get the contents of the token.
setContent($content);
Set the contents of the token.
isChanged();
Whether the token has already been modified.
isKeyword(); isNativeConstant(); isMagicConstant(); isWhitespace();
The names speak for themselves.
Read more Tokens: findBlockEnd($type, $searchIndex, $findEnd = true);
Find the end of a block of type $ type (braces, square or parentheses), starting from the token with the index $ searchIndex. If the third parameter is true, the method will search for the beginning of the block, not the end.
findGivenKind($possibleKind, $start = 0, $end = null);
Find the tokens of the specified type (types, if you pass an array) starting from the token under the $ start index and to the token under the $ end index.
generateCode();
Generate PHP code from a set of tokens.
generatePartialCode($start, $end);
Generate PHP code from a set of tokens between $ start and $ end
getNextTokenOfKind($index, array $tokens = array(), $caseSensitive = true);
Find the next token of a certain type
getNextMeaningfulToken($index); getPrevMeaningfulToken($index);
Find the next / previous token containing something other than spaces and comments.
insertAt($index, $items);
Add a new token to the collection, after $ index
overrideAt($index, $token);
Replace the token with the index $ index with the one passed by the second parameter.
Read more Write fixer
Now to the fixer itself.
Let me remind you that we write a fixer that brings all PHP keywords to lower case. The fixer class will be in the file.
src/Fixer/Casing/LowercaseKeywordsFixer.php
First we need to determine if the code falls under our case. In our case, we need to process any code that contains php keywords. Define the
isCandidate method.
public function isCandidate(Tokens $tokens) { return $tokens->isAnyTokenKindsFound(Token::getKeywords()); }
Now we need to describe our fixer. To do this, we define the method:
public function getDefinition() { return new FixerDefinition( 'PHP keywords MUST be in lower case.', array( new CodeSample( '<?php FOREACH($a AS $B) { TRY { NEW $C($a, ISSET($B)); WHILE($B) { INCLUDE "test.php"; } } CATCH(\Exception $e) { EXIT(1); } } ' ), ) ); }
This method returns a
FixerDefinition object, the constructor of which takes two parameters: a short description of the fixer (it will be in the documentation in the
README.rst file) and a small sample code for the fix (it will not be displayed anywhere but participates in tests).
We can also implement the method
public function getPriority() { return 0; }
Which returns a fixer priority if we need to run our fixer before or after other fixers. In our case, our fixer does not depend on the rest, so you can not implement the method, leaving the value 0 from the parent class.
All preparations are finished, let's implement the method that will fix the code.
We need to run through the entire code, if the token is a keyword, then bring it to lowercase:
protected function applyFix(\SplFileInfo $file, Tokens $tokens) { foreach ($tokens as $token) { if ($token->isKeyword()) { $token->setContent(strtolower($token->getContent())); } } }
In the end, should get something
like this file .
What's next
We have a working fixer. It's great. Left just a little bit. Let's write a test for it. Our test will be in the file
tests/Fixer/Casing/LowercaseKeywordsFixerTest.php
This is the usual PHPUnit test, unless it has its own method.
doTest($expected, $input = null, \SplFileInfo $file = null)
which first parameter takes the expected result, and the second - the original code. Test Method:
public function testFix($expected, $input = null) { $this->doTest($expected, $input); }
Write the data provider:
public function provideExamples() { return array( array('<?php $x = (1 and 2);', '<?php $x = (1 AND 2);'), array('<?php foreach(array(1, 2, 3) as $val) {}', '<?php FOREACH(array(1, 2, 3) AS $val) {}'), array('<?php echo "GOOD AS NEW";'), array('<?php echo X::class ?>', '<?php echo X::ClASs ?>'), ); }
As a result, we get this
code .
The test works, and if you run only it, then everything will be successful. But the general test fails, because data about our fixer is not in the documentation. The documentation in php-cs-fixer is auto-generated, so it’s enough to run:
php php-cs-fixer readme > README.rst
And information about our fixer will be added to the documentation.
Now we need to check both our files for matching the code to the style:
php ./php-cs-fixer fix
Well, in the end run a general test:
phpunit ./tests
If everything went well, then your own fixer is ready. Then you can make a
pull request and after some time your creation will appear in php-cs-fixer.