RectorでSymfony4への変換

こんにちは、岡野です。

最近、Symfony2.8/PHP7.0で実装されたサービスをSymfony4.4/PHP7.4へバージョンアップしました。その際ソースコードの自動変換に使用したRectorというソフトウェアを紹介します。
詳しくは https://getrector.org/ を見ていただくとし、実際の変換結果を挙げていきます。    

変換結果

Symfony2.8 -> 2.8

元々Symfony2.8で実装していますが念のためRectorを実行します。

最初に設定ファイルを準備します。
$ vi rector.php
...
$parameters->set(Option::PATHS, [
    __DIR__.'/app',
    __DIR__.'/src',
]);
$parameters->set(Option::EXCLUDE_PATHS, [
    __DIR__.'/app/SymfonyRequirements.php',
    __DIR__.'/app/cache/',
    __DIR__.'/src/AppBundle/Tests/',
]);
$parameters->set(Option::PHP_VERSION_FEATURES, '7.0'); // 適宜変更
$parameters->set(
    Option::SYMFONY_CONTAINER_XML_PATH_PARAMETER,
    __DIR__.'/appDevDebugProjectContainer.xml'
);
そしてRectorを実行します。
$ vendor/bin/rector process --set symfony28
以下変換されました(以降、変換結果の一部を抜粋します)。
/**
 * @Route(defaults={"foo": ""}, ...)
 */
↓ ↓ ↓
/**
 * @Route(defaults={"foo"= ""}, ...)
 */
 

Symfony2.8 -> 3.0

$ vendor/bin/rector process --set symfony30
$form = $this->createForm(new Foo());
↓ ↓ ↓
$form = $this->createForm(\AppBundle\Form\Foo::class);
 

Symfony3.0 -> 3.4

(symfony31~symfony33は差分が発生しませんでした)
$ vendor/bin/rector process --set symfony34
/**
 * @Route(...)
 * @Method("POST")
 */
↓ ↓ ↓
/**
 * @Route(..., methods={"POST"})
 */
 

PHP7.0 -> 7.0

$ vendor/bin/rector process --set php70
isset($foo[$bar]) ? $foo[$bar]: 0
↓ ↓ ↓
$foo[$bar] ?? 0

rand();
↓ ↓ ↓
random_int(0, mt_getrandmax());
 

PHP7.0 -> 7.1

$ vendor/bin/rector process --set php71
list($a, $b) = $this->foo();
↓ ↓ ↓
[$a, $b] = $this->foo();

count($foo)
↓ ↓ ↓
is_array($foo) || $foo instanceof \Countable ? count($foo) : 0 // php73実行時に改善される
 

Symfony3.4 -> 4.2

(symfony40,symfony41は差分が発生しませんでした)
$ vendor/bin/rector process --set symfony42
use Symfony\Bundle\FrameworkBundle\Controller\Controller;
class FooController extends Controller
↓ ↓ ↓
use Symfony\Bundle\FrameworkBundle\Controller\AbstractController;
class FooController extends AbstractController
 

PHP7.1 -> 7.3

(php72は差分が発生しませんでした)
$ vendor/bin/rector process --set php73
is_array($foo) || $foo instanceof \Countable ? count($foo) : 0
↓ ↓ ↓
is_countable($foo) ? count($foo) : 0

json_encode($foo)
↓ ↓ ↓
json_encode($foo, JSON_THROW_ON_ERROR)

json_decode($foo)
↓ ↓ ↓
json_decode($foo, false, 512, JSON_THROW_ON_ERROR)
 

Symfony4.2 -> 4.3

$ vendor/bin/rector process --set symfony43
$event->getDispatcher()->dispatch(
    FOSUserEvents::SECURITY_IMPLICIT_LOGIN,
    new UserEvent($event->getUser(), $event->getRequest()));
↓ ↓ ↓
$event->getDispatcher()->dispatch(
    new UserEvent($event->getUser(), $event->getRequest()),
    FOSUserEvents::SECURITY_IMPLICIT_LOGIN);

use Symfony\Component\HttpKernel\Event\GetResponseEvent;
public function foo(GetResponseEvent $event)
↓ ↓ ↓
use Symfony\Component\HttpKernel\Event\RequestEvent;
public function foo(RequestEvent $event)
 

Symfony4.3 -> 4.4

$ vendor/bin/rector process --set symfony44
// Command class
public final function execute(InputInterface $input, OutputInterface $output)
↓ ↓ ↓
public final function execute(InputInterface $input, OutputInterface $output): int
 

PHP7.3 -> 7.4

$ vendor/bin/rector process --set php74
/**
 * @var integer
 * @ORM\Column(name="id", type="integer")
 * @ORM\Id
 * @ORM\GeneratedValue(strategy="AUTO")
 */
private $id;
↓ ↓ ↓
/**
 * @ORM\Column(name="id", type="integer")
 * @ORM\Id
 * @ORM\GeneratedValue(strategy="AUTO")
 */
private int $id;

/**
 * @var integer
 * @ORM\Column(name="foo", type="integer", nullable=true)
 */
private $foo;
↓ ↓ ↓
/**
 * @ORM\Column(name="foo", type="integer", nullable=true)
 */
private ?int $foo = null;

/** @var EntityManager */
private $em;
↓ ↓ ↓
private \Doctrine\ORM\EntityManager $em; // use形式への変更は手作業が必要

1024
↓ ↓ ↓
1_024 // この変換は微妙
 

Twig1.x -> 2.0

$ vendor/bin/rector process --set twig20
new \Twig_SimpleFilter(...)
↓ ↓ ↓
new \Twig_Filter(...)
 

Twig2.0 -> 2.4.0

$ vendor/bin/rector process --set twig240
new \Twig_Filter(...)
↓ ↓ ↓
new \Twig\TwigFilter(...)
 

まとめ

正規表現による置換よりもソフトウェアで一括変換した方が抜けも誤りも少ないでしょう。

だし誤変換もごく一部あります。未定義の配列へ要素追加しているコードが $foo = (array)$foo; $foo[] = $bar; と変換されるなどです(元々未定義なことが良くないのですが)。

って変換結果にざっと目を通すことは必要ですが、ほとんどはPHP/Symfony文法エラーのため容易に気付けます。変換後にPHPStormのInspect Code機能で確認することも有効でした。

当然
ながらRectorで変換できない点もありますので手動対応は必要です(src/AppBundle -> src/など)。

その他

­ 上記以外にもRectorのルールセットは多数ありますので興味ありましたら以下をご覧ください。
https://github.com/rectorphp/rector/tree/master/config/set