In the JavaScript world converting modern code to an older syntax is quite common. In the PHP world you don't see that happen often. Symfony provides a few polyfills, but a full fledged conversion isn't available. At the meetup of our local PHP user group Jens Segers, Hannes Van de Vreken and I were toying around with the idea of converting PHP 7 code to equivalent PHP 5 code automatically.

Today our little hobby project called 7to5 was tagged 1.0.0. You can view the repo on GitHub.

What does it do?

removing scalar type hints

removing return type hints

removing the strict type declaration

replacing the spaceship operator by an equivalent PHP 5 code

replacing null coalesce statements by equivalent PHP 5 code

replacing group use declarations by equivalent PHP 5 code

replacing defined arrays by equivalent PHP 5 code

converting anonymous classes to regular classes

The tool will convert PHP 7 to PHP 5 by:

Because there are a lot of small things that cannot be detected and/or converted properly it is not guaranteed that the converted code will work. It's highly recommended to run your automated tests against the converted code to determine if it works.

Using the tool

composer install global spatie/ 7 to5

7to5 can be installed globally by running:

Once that's done you can use php7to5 to convert a whole directory in one go:

php7to5 convert {$directoryWithPHP7Code} {$destinationWithPHP5Code}

Behind the curtains

Removing some of the features of PHP 7 like scalar type hints and return type hints seems quite easy. Replacing anonymous classes, spaceship operators and null coaleasance operators is al little harder.

Image how you would convert this:

class Test { public function test () { $class = new class () { public function method (string $parameter = '' ) : string { return $parameter ?? 'no parameter set' ; } }; $class->method(); $anotherClass = new class () { public function anotherMethod (int $integer) : int { return $integer > 3 ; } }; } }

to this:

class AnonymousClass0 { public function method ($parameter = '' ) { return isset ($parameter) ? $parameter : 'no parameter set' ; } } class AnonymousClass1 { public function anotherMethod ($integer) { return $integer < 3 ? -1 : ($integer == 3 ? 0 : 1 ); } } class Test { public function test () { $class = new AnonymousClass0(); $class->method(); $anotherClass = new AnonymousClass1(); } }

After the meetup of our usergroup the idea lay dormant for a while, but at this year's PHPUKConference Hannes decided to just do it™. I was immediately pulled in. We ran over a few options. It quickly became apparent that using regex was out of the question. Using a state machine would become very unwieldy fast too. We settled on using the PHP parser tool created by Nikita Popov. This tool can convert PHP code to an abstract syntax tree. It's very similar to a domtree for html code. Here's an example (taken from the php parser docs). This code:

echo 'Hi' , 'World' ; hello\world( 'foo' , 'bar' . 'baz' );

will get converted to this tree:

[ 0 : Stmt_Echo( exprs: [ 0 : Scalar_String( value: Hi ) 1 : Scalar_String( value: World ) ] ) 1 : Expr_FuncCall( name: Name( parts: [ 0 : hello 1 : world ] ) args: [ 0 : Arg( value: Scalar_String( value: foo ) byRef: false ) 1 : Arg( value: Expr_Concat( left: Scalar_String( value: bar ) right: Scalar_String( value: baz ) ) byRef: false ) ] ) ]

Here's the code to create the abstract syntax tree.

$parser = ( new PhpParser\ParserFactory())->create(PhpParser\ParserFactory::PREFER_PHP7); $php7code = file_get_contents($pathToPhp7File); $syntaxTree = $parser->parse($php7code);

Now that we have created the abstract syntax tree, let's manipulate it. The tree can be traversed by a Traverser . The manipulation can be done by one or more Visitors objects. Let's view some code that traverses the tree and converts it back to regular PHP code.

$traverser = new PhpParser\NodeTraverser(); $traverser->addVisitor( new NullCoalesceReplacer()); $manipulatedTree = $traverser->traverse($syntaxTree); $code = ( new \PhpParser\PrettyPrinter\Standard())->prettyPrintFile($manipulatedTree); file_put_contents($pathToFileWithManipulatedPhpCode, $code);

The real magic happens in the NullCoalesceReplacer class. It will convert all usages of the null coalesce operator, which is exclusive to PHP 7, to equivalent PHP 5 code. This class is not a part of Nikita's package. Hannes and I created it together with all other visitors.

Before taking a look at the code of that class, we'll first examine how we would manually replace a null coalesce operator.

$result = $input ?? 'fixed-value' ; $result = isset ($input) ? $input : 'fixed-value' ;

Let's describe the needed conversion in plain English. We'll need to create a ternary statement. The left hand side of the '??' needs to put in an isset statement. The true branch of the ternary statement needs to use the left hand side of the '??'-operator, the false branch needs the right hand side.

Here's the code of the NullCoalesceReplacer -visitor.

namespace Spatie \ Php7to5 \ NodeVisitors ; use PhpParser \ Node ; use PhpParser \ Node \ Expr \ BinaryOp \ Coalesce ; use PhpParser \ NodeVisitorAbstract ; class NullCoalesceReplacer extends NodeVisitorAbstract { public function leaveNode (Node $node) { if (!$node instanceof Coalesce) { return ; } $issetCall = new Node\Expr\FuncCall( new Node\Name( 'isset' ), [$node->left]); return new Node\Expr\Ternary($issetCall, $node->left, $node->right); } }

Let's review another visitor. This one will remove all scalar type hints:

namespace Spatie \ Php7to5 \ NodeVisitors ; use PhpParser \ Node ; use PhpParser \ Node \ Param ; use PhpParser \ NodeVisitorAbstract ; class ScalarTypeHintsRemover extends NodeVisitorAbstract { public function leaveNode (Node $node) { if (!$node instanceof Param) { return ; } if ( $this ->isScalar($node->type)) { $node->type = null ; } } protected function isScalar ($type) { return in_array($type, [ 'int' , 'integer' , 'float' , 'string' , 'bool' , 'boolean' ]); } }

Using an abstract syntax tree sure makes manipulating code fairly readable. There are quite a number of other visitors. You can take a look at them on GitHub.

In closing

Honestly, I don't think I will ever use this tool myself. I'd rather upgrade a server to PHP 7 that converting code to PHP 5. That being said, it was a fun project to work on. It has a certain coolness factor. I learned that manipulating code using PHP parser is not that hard. Maybe I can use that knowledge on another project someday.

Meanwhile a more featured rich project that converts PHP 7 code has popped up (so if you do want to convert some PHP 7 code take a look at that one too).