Rector Blog https://getrector.org/ Rector Blog about Legacy Code Migrations Wed, 05 Oct 2022 02:59:43 +0000 Fri, 09 Sep 2022 00:00:00 +0000 <![CDATA[ Support for Nested Doctrine Annotation to Flat Attributes in Rector 0.14 ]]> https://getrector.org/blog/support-for-nested-doctrine-annotation-to-flat-attributes-in-rector-014 We added support for annotation to attribute upgrade in Rector 0.12. Since then, PHP 8.1 has come with nested attributes. Rector supports these, e.g., for Symfony validator.

Yet, Doctrine already took a path of its own and unwrapped nested annotations to flat attributes to be exclusively open to PHP 8.0 users. Next Rector comes with support for these too.

]]>
One of the annotations that got unwrapped is Doctrine\ORM\Mapping\Table, where indexes and uniqueConstraints have their own attributes:

use Doctrine\ORM\Mapping as ORM;

#[ORM\Index(name: 'index_key')]
#[ORM\UniqueConstraint(name: 'unique_key')]

Adding support for this attribute is pretty straightforward, as every unique annotation class has its unique attribute class.


Then Doctrine came with the next-level challenge. Unwrap array of JoinColumn annotations, once to JoinColumn attribute, and once to InverseJoinColumns attribute. Based on parent key.

    use Doctrine\ORM\Mapping as ORM;

    /**
     * @ORM\JoinTable(name="join_table_name",
     *     joinColumns={
     *          @ORM\JoinColumn(name="target_id"),
     *     },
     *     inverseJoinColumns={
     *          @ORM\JoinColumn(name="another_id")
     *     }
     * )
     */
    private $collection;


To handle this specific situation, we added brand new NestedAnnotationToAttributeRector rule to cover.

The case above would be handled by such configuration:

use Rector\Config\RectorConfig;
use Rector\Php80\Rector\Property\NestedAnnotationToAttributeRector;
use Rector\Php80\ValueObject\NestedAnnotationToAttribute;

return static function (RectorConfig $rectorConfig): void {
    $rectorConfig->ruleWithConfiguration(NestedAnnotationToAttributeRector::class, [
        new NestedAnnotationToAttribute('Doctrine\ORM\Mapping\JoinTable', [
            'joinColumns' => 'Doctrine\ORM\Mapping\JoinColumn',
            'inverseJoinColumns' => 'Doctrine\ORM\Mapping\InverseJoinColumn',
        ]),
    ]);


This rule will intelligently split the annotations:

    use Doctrine\ORM\Mapping as ORM;

    #[ORM\JoinTable(name: 'join_table_name')]
    #[ORM\JoinColumn(name: 'target_id')]
    #[ORM\InverseJoinColumn(name: 'another_id')]
    private $collection;


Doctrine Support OnBoard

Do you use the Doctrine upgrade set?

use Rector\Config\RectorConfig;
use Rector\Doctrine\Set\DoctrineSetList;

return static function (RectorConfig $rectorConfig): void {
    $rectorConfig->sets([
        DoctrineSetList::ANNOTATIONS_TO_ATTRIBUTES,
    ]);
};

We've got you covered. You don't need to fill the particular annotations because the set is already extended.

Just upgrade to Rector 0.14.1, and you're good to go.


Happy coding!

]]>
https://getrector.org/blog/support-for-nested-doctrine-annotation-to-flat-attributes-in-rector-014 Fri, 09 Sep 2022 00:00:00 +0000 Fri, 09 Sep 2022 00:00:00 +0000 https://getrector.org/blog/support-for-nested-doctrine-annotation-to-flat-attributes-in-rector-014#disqus_thread
<![CDATA[ Tests Made Simpler in Rector 0.14 ]]> https://getrector.org/blog/tests-made-simpler-in-rector-014 In August, we've been working hard to make Rector lighter. As use the count of users grows, developers use Rector on more legacy projects than before, and user experience b becomes a higher priority. The easy use, installation, and writing of custom rules is the key to the success of any project upgrade.

We cut down dependencies that it really does not need, removed a few niche features, and made the test case simpler.

You can benefit from this change if you're using Rector to write your custom rules and test those. What has changed and how?

]]>
The Rector rule test case has 3 essential methods:

  • test method that PHPUnit invokes
  • data provider that provides test fixtures
  • and method that provides a path to a config file
use Iterator;
use Rector\Testing\PHPUnit\AbstractRectorTestCase;
use Symplify\SmartFileSystem\SmartFileInfo;

final class RenameClassRectorTest extends AbstractRectorTestCase
{
    /**
     * @dataProvider provideData()
     */
    public function test(SmartFileInfo $fileInfo): void
    {
       $this->doTestFileInfo($fileInfo);
    }

    /**
     * @return Iterator<SmartFileInfo>
     */
    public function provideData(): Iterator
    {
        return $this->yieldFilesFromDirectory(__DIR__ . '/Fixture');
    }

    public function provideConfigFilePath(): string
    {
        return __DIR__ . '/config/configured_rule.php';
    }
}

The main concerns were SmartFileInfo wrapping every test fixture, and this class includes a version of Symfony FileSystem to work with relative paths. After internal analysis, we've found that the extra methods are used in about 15 cases. We made a pull-requests to address this issue and saw about 10 % memory less consumption narrow test suite.

From Value Object to File Path

Wrapping a string in a value object and then creating a service in every single value object is not cheap. But further we need to ask the critical question: "do we really need it"?

No.


We dropped this wrapper, removed the SmartFileInfo wrapping from everywhere, and lowered the memory consumption of the files.

In the next Rector 0.14.1, we've also simplified the test to use direct file paths:

use Rector\Testing\PHPUnit\AbstractRectorTestCase;

final class RenameClassRectorTest extends AbstractRectorTestCase
{
    /**
     * @dataProvider provideData()
     */
    public function test(string $filePath): void
    {
       $this->doTestFile($filePath);
    }

    public function provideData(): Iterator
    {
        return $this->yieldFilesFromDirectory(__DIR__ . '/Fixture');
    }

    public function provideConfigFilePath(): string
    {
        return __DIR__ . '/config/configured_rule.php';
    }
}

How to Upgrade?

The upgrade is effortless. Just use PHPStorm to find the test() method like this:

-public function test(SmartFileInfo $fileInfo): void
+public function test(string $filePath): void
 {
-    $this->doTestFileInfo($fileInfo);
+    $this->doTestFile($filePath);
 }

And you're ready to go with less memory and simpler code.


Happy coding!

]]>
https://getrector.org/blog/tests-made-simpler-in-rector-014 Fri, 02 Sep 2022 00:00:00 +0000 Fri, 02 Sep 2022 00:00:00 +0000 https://getrector.org/blog/tests-made-simpler-in-rector-014#disqus_thread
<![CDATA[ Interview: Legacy Code, Javascript Transpilers and Rector Challenges ]]> https://getrector.org/blog/interview-legacy-code-javascript-transpilers-and-rector-challenges I'll be speaking there in October at Paris on Forum PHP 2022 about Rector. I was asked for a simple interview to warmup the talk topic. The 3 questions - each looking at different angle, but going deep.

Contrary to mostly technical content on this blog, this post will give you behind the scenes insights on wider Rector vision.

]]>
1) Your talk is about legacy code. What made you interested in that subject, and do you think it is an important topic for developers?

What made me interested in legacy code? That's a good question :) During my first 10 years of coding, I strictly avoided legacy codebases. I've heard only traumatic stories from older friends, and I wanted to work only with fresh new technologies. That was the most fun, right? As I grew, I realized it's easy to work with new technology and... also boring. I started to look for a challenge and got hired by the largest Czech Pharmacy company to get a 10-year-old spaghetti code to modern MVC with high-quality standards.

That was a great experience that got me closer to automated tools. Compared to my peers, I've always been extremely lazy and had a weakness for clean code. That got me thinking, "this company is probably not the only one in the world having this problem". If I work all my life, I can help max. 20 companies this way. That's not much :)

I came across automated tools like Code Sniffer, and the idea of "automated upgrades for legacy code" started. If one person writes a rule to add a "void" type where it belongs, anyone in the world can use it for free. The whole world runs on a new version of PHP in a few seconds.

I realized people around me might hate legacy code, but they also love to work with complex systems and improve them. That's where the biggest problem of the PHP community and the solution clicked.


2) In the JS ecosystem, downgrading and transpiling code is the norm. Do you think it is the future of the PHP ecosystem too?

When I started working on Rector in 2016, I noticed other languages have similar tools. Yet their focus was rather dialect-based than single-lined. JS ecosystem is rich but also has too many variations. Those variations are easily spread, burn out in a few years, and they are hard to get rid of or change.

I mean, a new JS framework has just been released during writing these answers :). There is no ReactJS <-> Angular migration tool, nor Angular 2 to 4 upgrade (fun factory: I was hired once for that too).

The PHP is very concise in this matter. There is PHP 5.4, then 5.5, then 5.6. There is no 5.6-beta-only-with-this-feature with brand-new syntax. That means the language is deterministic. Every version has strictly defined behavior. You can do an "A → B upgrade" with a computer algorithm. There is no better language than PHP to spark these automated tools, whether coding standard tools, static analyzers, or Rector. There are already discussions on Reddit that Rector should become part of PHP official RFCs.

It will be fantastic. Imagine there is RFC that adds a new "read-only" keyword to the class. The implementation and tests are part of the RFC already. But now, there will also be a Rector upgrade set, tested on top 1000 composer packages.

There is no "it will be so hard to upgrade, don't give us more work, please" discussion. It's zero work for us developers to upgrade. We just run "composer update" and enjoy the new PHP version without effort :)


3) There are many rules to maintain on a project like Rector. What's the main difficulty of working on this project?

Thank you for your question about this challenge. I'm proud to say the Rector community gives the tool propper battle testing. By the nature of the tool, Rector faces the worst legacy code there is. Its job is to upgrade the "impossible" legacy projects, after all :). The people then report edge cases that Rector missed.

Now comes the most significant challenge: people send a failing test case to particular Rector rule, but they think the fix is too hard to try. Then we try to explain to them that they can do it. Most often than not, the fix usually adds an "instanceof" check here or changes the bool return. When they try it, they're honestly surprised by how easy it is. The next issue usually comes up with a fix included.

At last, I want to encourage you. For anyone working with a legacy codebase or with 3+ years old project, give this technology a try. Whether Rector or abstract syntax tree, it will give you incredible power to change "impossible-level problems" in your project in a matter of days.

If you need a heads-up start, get a Rector book that explains this from the very first steps. It will show you that anything is possible with your code, whether 1000 lines or 10 000 000 lines.

Good luck and happy coding!

Tomas

]]>
https://getrector.org/blog/interview-legacy-code-javascript-transpilers-and-rector-challenges Sat, 20 Aug 2022 00:00:00 +0000 Sat, 20 Aug 2022 00:00:00 +0000 https://getrector.org/blog/interview-legacy-code-javascript-transpilers-and-rector-challenges#disqus_thread
<![CDATA[ Separating Typo3 and Nette as Community Packages ]]> https://getrector.org/blog/separating-typo3-and-nette-as-community-packages When Rector started, it was a small project that handled upgrading a vast amount of PHP packages.

As the project grew and expanded, more local PHP communities joined with community packages that build custom rules on top Rector core.

It makes sense to separate these projects from the core and let the community handle them. Who does a better job at growing the vegetable than farmers themselves, right?

]]>
To this day, there are around 10 known community packages that are maintained by the community. These include CraftCMS, Shopware or famous Drupal Rector bot that automates the upgrades for Drupal 9.

Communities in Control of Their Standards

The significant advantage of community-maintained packages over core ones is that every community has its standards. Those standards are specific to the particular community but not useful for general Rector users. E.g., Typo3 uses XML files that can be automated, too, and Nette uses NEON files for service registrations, etc.

We want to give these communities the freedom to implement any feature their framework needs. When Typo3 and Nette packages were part of the core, these features were often evaluated with strict questions "how does the Rector community benefit from it"? This approach leads to a collision between two unrelated worlds.

Why have frictions when these 2 projects can cooperate and work better apart?

Also, Rector users who do not use these community packages, can benefits from this change. Their Rector install load is now smaller and pulls less dependencies.

Community Leaders with Strong Vision

Second, the framework communities are driven by their passionate leaders. There is no Symfony without Fabien, no Laravel without Taylor. Leaders need freedom, responsibility, and power to decide where the project should go. Of course, they discuss their opinions with others before making a move, but in the end, it is their decision to move in this or that direction.

That's why we believe the community package should be in the hands of people who use the framework daily. A new framework version brings new features every year, and the person to convert them to Rector rules is a passionate developer with a taste for innovation and bleeding edge.


That's why we decided to separate typo3 and Nette from the core and let their active communities take over. They're not part of rector/rector anymore, but they're Rector extensions that you can install yourself if you need those:

You can find those at:


How to upgrade to Typo3 community Rector package?

Add package via composer:

composer require sabbelasichon/typo3-rector --dev


How to upgrade to Nette community Rector package?

Add package via composer:

composer require efabrica/rector-nette --dev

And replace namespace:

-Rector\\Nette\\
+RectorNette\\

That's it!


We believe this brings faster iterations of the packages and focuses on a stronger Rector core to support their growth.

]]>
https://getrector.org/blog/separating-typo3-and-nette-as-community-packages Fri, 05 Aug 2022 00:00:00 +0000 Fri, 05 Aug 2022 00:00:00 +0000 https://getrector.org/blog/separating-typo3-and-nette-as-community-packages#disqus_thread
<![CDATA[ How to Automatically Add Return Type Declarations without Breaking Your Code ]]> https://getrector.org/blog/how-to-automatically-add-return-type-declarations-without-breaking-your-code Code filled with docblocks param, var, and return types is a gold mine. Not in the meaning of valuable resource, but rather as exploding metal covered with a thin piece of gold, so we grab it without thinking. While these docblocks give us much information about the code, they might be nothing more than a wish, dream, or promise.

Have you ever blindly trusted docblocks and switched them to type declarations? Then you know the explosive regression this move brings.

Yet, how can we turn to add strict types to our code without fear of breaking it?

]]>
Docblock Trust is Blind

What can we read from this code if we trust it?

class SweetPromise
{
    private $values;

    /**
     * @param mixed[] $values
     */
    public function setValues($values)
    {
        $this->values = $values;
    }

    /**
     * @return string[]
     */
    public function getValues()
    {
        return $this->values;
    }
}

The values are always an array of strings. Let's tolerate strings and work with the array type.


If that is true, these should not be possible:

$sweetPromise = new SweetPromise();
echo $sweetPromise->getValues(); // null or array?

$sweetPromise->setValues('{55}'); // string or array?
echo $sweetPromise->getValues(); // string or array?

We have no idea about the value we put in. We cannot make any assumptions unless we're ready to risk the type failure and manual verification of every single type.


The honest filter would show the class above like this:

class SweetPromise
{
    private $values;

    public function setValues($values)
    {
        $this->values = $values;
    }

    public function getValues()
    {
        return $this->values;
    }
}


Now it's obvious we can't trust the docblocks. Mainly because the docblock types are not based on actual code but ideal state = if every method is 100 % reliable, called in precisely defined order, and with 100 % reliable typed input variables. Which they're not.


Millions of Lines with Docblocks

This sound a little depressing. So what can we do if we have millions of lines of code with docblocks? We want to move fast and safe at the same time.

We have a few options:

  • go line by line, try to detect the pattern, and add the first single, strict type declaration
  • then propagate this type to all calls of this code
  • risk the automated docblock conversion and let the user do the testing; 500 on the server means the type was not detected correctly
  • go with certain types


What are "Certain Types"?

Certain types are based on actual values, operations, and logical structures that have under any circumstances always exactly one type:


Not only one-line values but also more complex structures like created and filled array:

function provideDreams()
{
    $dreams = [];

    foreach ($this->dreamRepository->fetchAll() as $dream) {
        $dreams[] = $dream;
    }

    return $dreams;
}

This code always returns array, no matter what $dream actually is.


Based on these 4 rules, we can complete the following types with 100 % certainty:

 final class Reality
 {
-    public function getAge()
+    public function getAge(): int
     {
         return 100;
     }

-    public function removeTax(int $value)
+    public function removeTax(int $value): int
     {
         return $value - 200;
     }

-    public function transform($value)
+    public function transform($value): string
     {
         if ($value === null) {
             return '';
         }

         return base64_encode($value);
    }
}

And much more!


Depending on what age your project is coming from, there is one requirement to make this work. You must use PHP 7.0, where return type declarations were added.

Try it Yourself

Rector can handles some case above in the basic version. Add these rules to your code and see for yourself:

use Rector\Config\RectorConfig;
use Rector\CodeQuality\Rector\ClassMethod\ReturnTypeFromStrictScalarReturnExprRector;
use Rector\TypeDeclaration\Rector\ClassMethod\ReturnTypeFromStrictNativeCallRector;

return static function (RectorConfig $rectorConfig): void {
    $rectorConfig->rules([
        ReturnTypeFromStrictNativeCallRector::class,
        ReturnTypeFromStrictScalarReturnExprRector::class,
    ]);

Run Rector and see how many new types were added. In the project we tested these rules on, we had over 40 changed files in the first run. From zero to 40 files, quite impressive. And as you know, strict types spread exponentially.

Coming in Rector Enterprise

The rest of the cases and more will be covered in rules of upcoming Rector Enterprise version, with more extra rules and features under private paid license.

]]>
https://getrector.org/blog/how-to-automatically-add-return-type-declarations-without-breaking-your-code Mon, 04 Jul 2022 00:00:00 +0000 Mon, 04 Jul 2022 00:00:00 +0000 https://getrector.org/blog/how-to-automatically-add-return-type-declarations-without-breaking-your-code#disqus_thread
<![CDATA[ New in Rector 0.13 - Refresh Scope for Changed Nodes ]]> https://getrector.org/blog/new-in-rector-013-refresh-scope-for-changed-nodes Rector is using PHPStan to detect types of various expressions. That means every node has access to PHPStan Scope, e.g., with types or class reflection. From code $value = 1; we know, that $value is type of int. But what if we change the node?

]]>
Let's say the Rector changes the code the following way:

-$value = 1;
+$value = 'yes';

The $value was an int type. But after the change, the type is gone. We have an old Scope object where $value is still int or even worse. If we create a new Assign node, no Scope is available anymore.

Changed Nodes with Lost Scope

This becomes problematic when we rename the variable. Suddenly PHPStan has no idea about the new variable type, and everything is mixed for it. When we use CountOnNullRector that prevents count() on null fatal error, we can see Rector changed like these:

-$items = [];
+$posts = [];
-echo count($items);
+echo is_array($posts) ? count($posts) : 0;

Which is obviously wrong.

Scope Refresh to the Rescue

To solve this problem, we implemented a new feature called "scope refresh" that rebuilds the scope based on the changed node. When a variable name changes, the scope will know about it and keep its type. It's a challenge as PHPStan Scope object has few design limitations. It's an immutable object - once you enter a class, you cannot enter it again.

In the end, we made it work so that every new node will be traversed again with a new Scope object.

That will allows Rector to be aware of the node type, even if nodes change:

-$items = [];
+$posts = [];
-echo count($items);
+echo count($posts);

This is feature coming in Rector 0.13. In the meantime, don't forget to upgrade to RectorConfig.


Happy coding!

]]>
https://getrector.org/blog/new-in-rector-013-refresh-scope-for-changed-nodes Thu, 12 May 2022 00:00:00 +0000 Thu, 12 May 2022 00:00:00 +0000 https://getrector.org/blog/new-in-rector-013-refresh-scope-for-changed-nodes#disqus_thread
<![CDATA[ New in Rector 0.12 - Introducing Rector Config with Autocomplete ]]> https://getrector.org/blog/new-in-rector-012-introducing-rector-config-with-autocomplete Rector is using Symfony container configuration to build the service model. While it brings automated autowiring, array autowiring, and native container features, the syntax to configure Rector has been complex and talkative.

The hard question is: how can we refactor from Symfony, have a custom Rector config class but keep using its features?

]]>

A year ago we switched to a distribution repository releases. It now allows us to add own RectorConfig class on top of Symfony internals.

The RectorConfig helps you by:

  • full IDE autocomplete,
  • isolation from Symfony - useful for Symfony projects
  • validation of configuration methods that happens before running Rector itself


Are you curious about the implementation? In the end, the secret is in a single patched file with 4 changed lines.

So what does it look like?

Configuration as we Know It Now

use Symfony\Component\DependencyInjection\Loader\Configurator\ContainerConfigurator;
use Rector\Core\Configuration\Option;
use Rector\DowngradePhp81\Rector\Property\DowngradeReadonlyPropertyRector;

// ups, possible conflict with ContainerConfigurator
return static function (ContainerConfigurator $containerConfigurator): void {
    $parameters = $containerConfigurator->parameters();

    // too verbose params, constants and possible typo in param value
    $parameters->set(Option::PATHS, [[ // ups, "[[" typo
        __DIR__ . '/src/',
    ]]);

    $services = $containerConfigurator->services();
    $services->set(DowngradeReadonlyPropertyRector::class);
};

Such complexity is confusing just to read and easy to make error in.

The new Way to Configure Rector

What if we could single class to configure the Rector?

use Rector\Config\RectorConfig;

return static function (RectorConfig $rectorConfig): void {
    $rectorConfig->parallel();

    $rectorConfig->paths([
        __DIR__.'/src',
    ]);

    $rectorConfig->rule(DowngradeReadonlyPropertyRector::class);
};

How to upgrade to RectorConfig?

First, be sure to use at least Rector 0.12.21. If your config has a few lines, you can handle it easily manually. But what if you have dozens of lines or even custom configs in your tests? Rector to the rescue!

use Rector\Config\RectorConfig;
use Rector\Set\ValueObject\SetList;

return static function (RectorConfig $rectorConfig): void {
    $rectorConfig->sets([
        SetList::RECTOR_CONFIG
    ]);
};


Do you use Symfony? Avoid converting your Symfony /configs and only process only your Rector configs:

vendor/bin/rector process rector.php utils/rector/tests

Thanks to this upgrade set, we've migrated all the Rector packages to use the new RectorConfig syntax.

Configured Rules

Do you configure your rules? In previous version we've added configure() method to help with validation of configuration:

use Rector\Renaming\Rector\Name\RenameClassRector;
use Symfony\Component\DependencyInjection\Loader\Configurator\ContainerConfigurator;

return static function (ContainerConfigurator $containerConfigurator): void {
    $services = $containerConfigurator->services();
    $services->set(RenameClassRector::class)
        ->configure([
            'App\SomeOldClass' => 'App\SomeNewClass',
        ]);


But it was not enough, was it?

That's why now you can use dedicated ruleWithConfiguration() method with validated input:

use Rector\Renaming\Rector\Name\RenameClassRector;
use Rector\Config\RectorConfig;

return static function (RectorConfig $rectorConfig): void {
    $rectorConfig->ruleWithConfiguration(RenameClassRector::class, [
        'App\SomeOldClass' => 'App\SomeNewClass',
    ]);


Why not just use the rule() method with a magic 2nd argument? The reason to separate these 2 methods is to give you fast feedback about wrong configuration:

  • when you try to rule without configuration, e.g., ChangeSwitchToMatchRector with configuration, Rector will tell you the configuration is not needed
  • and when you use a rule that requires configuration, e.g., RenameClassRector, you'll know about missing configuration too


Happy coding!

]]>
https://getrector.org/blog/new-in-rector-012-introducing-rector-config-with-autocomplete Tue, 26 Apr 2022 00:00:00 +0000 Tue, 26 Apr 2022 00:00:00 +0000 https://getrector.org/blog/new-in-rector-012-introducing-rector-config-with-autocomplete#disqus_thread
<![CDATA[ Success Story of Automated Framework Migration from FuelPHP to Laravel of 400k+lines Application ]]> https://getrector.org/blog/success-story-of-automated-framework-migration-from-fuelphp-to-laravel-of-400k-lines-application Today, I'm very excited to talk about the full story of our successful automated framework migration how Rector saved our product by refactoring our 400k+lines PHP web application!

]]>
This is an guest post by rajyan, who used Rector to migrate an extensive PHP application from FuelPHP to Laravel.


The Migration Plan

Our application was a monolithic application consisting of the backend of web service, native app API, intra-company support tools, and batch jobs for web and app.

  • Framework: FuelPHP → Laravel
  • PHP Version: PHP 7.0 → PHP 7.4
  • Application:
    • Released in 2015/11
    • 2000+ PHP files, 400k+ lines of PHP codes
  • Time Schedule
    • 2021/01-11
    • Migrated internal tools at 2021/09 (QA from 2021/07 ~)
    • Start canary release of Web and API from 2021/11/1 (QA from 2021/08 ~)
    • Switched to Laravel with 100 % release at 2021/11/16
  • Team Members
    • 1~2 engineers + 1 senior engineer for advice
  • Special notes
    • Running the migration and developing new features at the same time
    • Zero downtime by releasing old and new environments in the canary release

Why did we Choose Rector?

Why did we decide to automate the migration? The reason was simple.

Our application was too large to migrate manually, and automation was needed to make the migration successful. Even more, we estimated that it might take about a year for migration without automation, even if all team members worked for migration.

If it's the same speed as human work, why not try something new that might be faster?

Fully Automated Migration

At first, we were using Rector only to convert DB query builders of FuelPHP to Laravel and manually modify controllers, configs, "Facades" (~= "Classes" in FuelPHP).

However, as I wrote custom Rector rules, I noticed AST power and flexibility and realized that full automation might be possible. Also, FuelPHP is a relatively lightweight framework, and automated migration to Laravel, which has more features, was imaginable.

FuelPHP to Laravel

99% of the PHP files were converted automatically, editing 200k+ lines of code.

An automated migration by custom Rector rules of 2000+ files included:

  • Fuel Query Builder → Laravel Query Builder
  • Non psr-4 → psr-4
    • We created a dummy autoloader to run Rector, because we did not install FuelPHP
    • Adding namespaces, and moving files to the correct dir
    • Converting Config
  • File, Response Class → Laravel Response facades or helpers
  • Input, Upload Class → Laravel Request facades or helpers
  • FuelPHP Exceptions → Mapped to Laravel Exceptions
  • There were a lot of other ad-hoc rules specific to our code


A manual migration of ~20 files:

  • Routes
    • There are no routes in FuelPHP
  • Some parts of authentication
  • Some parts of config
  • FuelPHP specific classes.
    • ex. Format, Agent
    • wrote a custom facade in Laravel

Let's look into them in detail.

Let's write a Rule to Migrate a Query Builder

Creating custom Rector rules to migrate the query builder was like creating a piece of a puzzle. We created many small refactoring rules and put the pieces together to modify the whole query.

For example, we wanted to convert FuelPHP...

\DB::select_array(['id', 'name'])->from('user');


...to Laravel:

\DB::table('user')->select_array(['id', 'name']);


For this refactoring, we created two rector rules.

  1. Swap from and select_array and rename from to table
  2. Convert select_array to select

1. Swap from and select_array and Rename from to table

The first rule can be written like this:

public function getNodeTypes(): array
{
    return [MethodCall::class];
}

public function refactor(Node $fromNode): ?Node
{
    if (!$this->isName($fromNode->name, 'from')) {
        return null;
    }

    $selectNode = $fromNode->var;
    if (!$selectNode instanceof StaticCall ||
        $this->isNames($selectNode->name, ['select', 'select_array'])) {
        return null;
    }

    return new MethodCall(
        new StaticCall(
            new Node\Name\FullyQualified('DB'),
            new Node\Identifier('table'),
            $fromNode->args
        ),
        $selectNode->name,
        $selectNode->args
    );
}

The rule goes step by step through conditions:

  • Get method calls and check if the name is from.
  • If the variable node of the method call is a static call of class DB named select_array
  • Then swap the static call and method call and rename the static call to “table”

It's simple, isn't it?


2. Convert select_array to select

Then let's modify select_array to select. You need to expand the array to args and rename the method:

public function getNodeTypes(): array
{
    return [MethodCall::class];
}

public function refactor(Node $selectArrayNode): ?Node
{
    if (!$this->isName($selectArrayNode->name, 'select_array')) {
        return null;
    }

    if (count($selectArrayNode->args) !== 1) {
        return null;
    }

    $array = $selectArrayNode->args[0]->value;
    if (!$array instanceof Node\Expr\Array_) {
        return null;
    }

    $selectArrayNode->name = new Node\Identifier('select');
    $selectArrayNode->args = array_map(
        fn(Node\Expr\ArrayItem $item) => new Node\Arg($item->value),
        $array->items
    );

    return $selectArrayNode;
}

Great! Now we can convert the whole query running these 2 rules.

New Features and Migration at the Same Time?

This is the most significant and wonderful benefit of automated migration. It's explained in detail in the previous post, so take a look if you haven't read it yet!

What was Important for Automated migration?

Tests

Migrating tests together with the application code and running them can be a critical indicator that the application works after applying Rector. Sadly, our project did not have enough tests…

PHPStan

It was another hero of the project besides Rector. We created a baseline first and ran them after running Rector. We could find codes broken by running Rector and fix the Rector rules.

Rector rule Tests

Rector rule tests gave great confidence that the modification in the migration itself is working.

We wrote about 80 Rector rules to migrate the application, and the tests helped us find rules broken by dependencies and breaking changes of Rector's updates.

Abstract Syntax Tree (AST)

A deep understanding of AST and Rector itself is essential to write custom Rector rules.

The most efficient way for me to learn them was to write the test fixtures of the Rector rules and dump them by nikic/php-parser. Trial and error writing rules and dumping the AST was an excellent way to understand the structure.

Also, I read a lot of codes of Rector, php-parser, PHPStan, and Larastan to understand how they are using, working with AST.

But as a shortcut, there is a book about Rector that explains AST and other vital things about Rectory. Let's read the Rector book!

What have we Struggled with?

Codes too Complicated to Convert by Rector

Sometimes some codes were too complicated to write a Rector rule. In these cases, we refactored the code itself to make it possible to convert by Rector or delete them if we could.

We deleted 100k+ lines of code during the migration!

The important thing was that we were editing these codes in the "Development branch" to refactor and deploy the code in FuelPHP to confirm that the code was working before the migration release.

In some situations, writing custom rules is too tricky and expensive. We edited those in the migration branch and skipped automated migration for those files (about 10-20 files). It is essential to set a boundary, what should be automated and what should be done manually.

Minor Differences between Frameworks

There were minor differences between frameworks, which were difficult to notice while writing custom rules.

For instance,

  • FuelPHP return empty array response response([]) with status code “204 No Contents” while Laravel does not
  • FuelPHP DB::insert returns array of ['id', 'affected rows'] while Laravel DB::insertGetId returns just 'id'
  • …etc.

For these differences, QA testing and canary release were crucial. We iterated testing over and over and fixed the custom rules to achieve the complete migration.

Rector Bugs and Breaking Changes

We started the migration with Rector 0.9.x, and it's 0.12.x now! At 2020-2021, Rector was changing and evolving at a very high speed, and sometimes there were unstable versions with bugs. Also, some of our custom rules relied on Rector core codes, so there were significant breaking changes during the migration.

However, issues were already recognized by the community, and the fixes were extremely fast.

I very much appreciate the hard work of Tomas, other core developers, and the community of Rector!

How was the Migration in a Brief?

The pros:

  • Works on large codebases
  • Can decrease human errors of migration
  • Could continue developing new features and run migration at the same time with no conflicts

The cons:

  • Converted code doesn't use the full functionality of Laravel
    • You can refactor them after migration!
  • Requires understanding of AST


To be honest, I don't have any big cons for automated migration. It was a great experience, and I can say that we could not finish our migration without Rector.

Thank you!

]]>
https://getrector.org/blog/success-story-of-automated-framework-migration-from-fuelphp-to-laravel-of-400k-lines-application Mon, 07 Feb 2022 00:00:00 +0000 Mon, 07 Feb 2022 00:00:00 +0000 https://getrector.org/blog/success-story-of-automated-framework-migration-from-fuelphp-to-laravel-of-400k-lines-application#disqus_thread
<![CDATA[ How to Migrate Legacy PHP Applications Without Stopping Development of New Features ]]> https://getrector.org/blog/how-to-migrate-legacy-php-applications-without-stopping-development-of-new-features Migrating legacy PHP applications is always a challenging task.

Today I'll introduce one strategy to make these migrations easier by using the power of the Rector. With this strategy, we successfully migrated a legacy PHP application over a period of one year, without stopping developing new features!

]]>
This is an introduction post by rajyan, who used Rector to migrate an extensive PHP application from FuelPHP to Laravel.

Difficulty of Migrating Legacy PHP Applications

Migrating requires high knowledge of PHP, a deep understanding of the application, and time. Especially in applications running in the production. Migration takes time and man-hours for code modification and testing that the application works after the migration.

While migrating is going on, you have to stop implementing new features. Otherwise, you'll fall into a hell of conflict and outdated branches. Stopping development for a specific time for the migration would be a difficult decision.

How to Solve it?

Let's see the concept first:

The most important thing is to avoid conflicts.

We have to be careful not to edit the same file in the "Development branch" and the "Migration branch". We continue developing new features as usual in the "Development branch" directory, for example, old/app. Then we apply Rector rules to migrate old/app and copy the complete result to new/app in the "Migration branch", which automated the process in CI.

We are free from conflicts by always editing the app code in the "Development branch" and only adding files in the migration branch. We can also make our "Migration branch" to the "Development branch" by merging them.


You can easily run the new application by changing the namespace in composer.json if written in psr-4.

 "autoload": {
     "psr-4": {
-        "App\\": "old/app"
+        "App\\": "new/app"
     }
 }

We gain enough time to test and fix bugs in the new application and keep developing in different branches with this approach.

Summary

This article introduced one approach that can make migrating legacy PHP applications easier. We used this method and successfully migrated the framework of our extensive legacy PHP application. It can be helpful for other situations such as upgrading PHP versions or refactoring the codebase by Rector.

I'll talk about the whole story of the migration in the next post, so stay tuned!

]]>
https://getrector.org/blog/how-to-migrate-legacy-php-applications-without-stopping-development-of-new-features Fri, 21 Jan 2022 00:00:00 +0000 Fri, 21 Jan 2022 00:00:00 +0000 https://getrector.org/blog/how-to-migrate-legacy-php-applications-without-stopping-development-of-new-features#disqus_thread
<![CDATA[ New in Rector 0.12 - Much Simpler and Safer Rule Configuration ]]> https://getrector.org/blog/new-in-rector-012-much-simpler-and-safer-rule-configuration Configurable rules are the most powerful building stone for instant upgrade sets. Do you want to upgrade from Symfony 5 to 6? You'll primarily deal with renamed classes, renamed methods, new default arguments in method, or renamed class constants.

In the end, we have to configure around 10 rules to get the most job done. That's why we focused on developer experience and added a new configure() method in Rector 0.12.

]]>
Each configurable Rector rule implements own configure() method.

To register a rule in rector.php, we use Symfony PHP configs syntax.


There we pass arguments that configure our rule, e.g. RenameClassRector:

use Rector\Renaming\Rector\Name\RenameClassRector;
use Symfony\Component\DependencyInjection\Loader\Configurator\ContainerConfigurator;

return static function (ContainerConfigurator $containerConfigurator): void {
    $services = $containerConfigurator->services();
    $services->set(RenameClassRector::class)
        ->call('configure', [[
            RenameClassRector::OLD_TO_NEW_CLASSES => [
                'App\SomeOldClass' => 'App\SomeNewClass',
            ],
        ]]);
};

While we have full autocomplete support thanks to PHP, this approach has a few downsides.

  • we have to be careful about the exact syntax of nested arrays
  • we have to use class constants to pass the nested array to


What exactly does it mean? Well, any of the following syntaxes would crash:

->call('configure', [[[
    ...
]]]);

...or...

->call('configure', [
    ...
]);

...or...

->call('configure', [[
    [
        'App\SomeOldClass' => 'App\SomeNewClass',
    ],
]]);


We think that's unnecessary complexity. Do you agree?


New configure() Method to the Rescue

We don't like to make you think about implementation details We know there are more important goals you want to achieve.

That's why we remove the complexity with the configure() method right in the rector.php config.


The goal is clear:

  • no constants
  • single method
  • write only the configuration, nothing else
  • Rector validates input for you
use Rector\Renaming\Rector\Name\RenameClassRector;
use Symfony\Component\DependencyInjection\Loader\Configurator\ContainerConfigurator;

return static function (ContainerConfigurator $containerConfigurator): void {
    $services = $containerConfigurator->services();
    $services->set(RenameClassRector::class)
        ->configure([
            'App\SomeOldClass' => 'App\SomeNewClass',
        ]);
};

Nice and clear.


Say Good-Bye to Value Object Inlining

Symfony does not support value objects for configuration. That's why we had to come up with own value object more inline when we wanted to use value objects for configuration. It looked like this:

use Rector\Transform\Rector\FuncCall\FuncCallToStaticCallRector;
use Symplify\SymfonyPhpConfig\ValueObjectInliner;
use Symfony\Component\DependencyInjection\Loader\Configurator\ContainerConfigurator;

$services->set(FuncCallToStaticCallRector::class)
    ->call('configure', [[
        FuncCallToStaticCallRector::FUNC_CALLS_TO_STATIC_CALLS => ValueObjectInliner::inline([
            new FuncCallToStaticCall('dump', 'Tracy\Debugger', 'dump'),
        ])
    ]]);


In Rector 0.12, you can use the simple syntax:

use Rector\Transform\Rector\FuncCall\FuncCallToStaticCallRector;
use Symfony\Component\DependencyInjection\Loader\Configurator\ContainerConfigurator;

$services->set(FuncCallToStaticCallRector::class)
    ->configure([
        new FuncCallToStaticCall('dump', 'Tracy\Debugger', 'dump'),
    ]);


Bonus: Rector now Validates Your Input

One more thing...

While we were at it, we made sure the configuration input was now validated. Is there a better package than webmozart/assert to handle it?

So if you ever add however invalid configuration, we'll tell you what to fix the second you run Rector:

use Rector\Core\Rector\AbstractRector;
use Webmozart\Assert\Assert;

final class RenameClassRector extends AbstractRector
{
    /**
     * @param mixed[] $configuration
     */
    public function configure(array $configuration) : void
    {
        $oldToNewClasses = $configuration[self::OLD_TO_NEW_CLASSES] ?? $configuration;

        Assert::isArray($oldToNewClasses);
        Assert::allString($oldToNewClasses);

        $this->addOldToNewClasses($oldToNewClasses);
    }

    // ...
}


Give your rector.php fresh breeze look with Rector 0.12 and the new configure() method!


Happy coding!

]]>
https://getrector.org/blog/new-in-rector-012-much-simpler-and-safer-rule-configuration Fri, 07 Jan 2022 00:00:00 +0000 Fri, 07 Jan 2022 00:00:00 +0000 https://getrector.org/blog/new-in-rector-012-much-simpler-and-safer-rule-configuration#disqus_thread
<![CDATA[ New in Rector 0.12 - The Latest PHP in a Single Import ]]> https://getrector.org/blog/new-in-rector-012-the-latest-php-in-a-single-import The most used feature of Rector is to keep you updated with the latest PHP. PHP 8.1 was released almost a month ago, so many projects started to use Rector to upgrade to PHP 8.1. There is a new import in your rector.php with every new version.

Soon, your config is cluttered with a list of imports. How can we reduce this complexity to a single line? How can we handle your-favorite-framework upgrade in second?

]]>
How do you upgrade to PHP 8.1 in 2 steps?

Register new PHP 8.1 set in rector.php:

 use Rector\Set\ValueObject\SetList;
 use Rector\Config\RectorConfig;

 return function (RectorConfig $rectorConfig): void {
     $rectorConfig->import(SetList::PHP_55);
     $rectorConfig->import(SetList::PHP_56);
     $rectorConfig->import(SetList::PHP_70);
     $rectorConfig->import(SetList::PHP_71);
     $rectorConfig->import(SetList::PHP_73);
     $rectorConfig->import(SetList::PHP_74);
     $rectorConfig->import(SetList::PHP_80);
+    $rectorConfig->import(SetList::PHP_81);
};


And run Rector:

vendor/bin/rector

That's it! Yet, there is a code smell lurking.

Error-Prone Complexity

High complexity leads to a high error rate. What could go wrong with such a configuration?

  • we can accidentally skip one of the versions - what version do we miss? 7.2
  • we skip lower versions - the PHP 5.3 and 5.4 contain many valuable rules, but our code will still run the old syntax
  • the more versions we want to upgrade, the longer config gets

Config made Simple with Level Set

We wanted to lower complexity and remove these errors. That's why we add a new feature in Rector 0.12 - Level sets.


Now instead of gazillion lines with set imports, you can use just the single latest level:

use Rector\Set\ValueObject\LevelSetList;
use Rector\Config\RectorConfig;

return function (RectorConfig $rectorConfig): void {
    $rectorConfig->import(LevelSetList::UP_TO_PHP_81);
};

That's it! No more place for mistakes.

The LevelSetList also includes the PHP target version parameter to ensure all rules are applied.


Next time you'll need to upgrade PHP, you can change only 1 line:

 use Rector\Set\ValueObject\LevelSetList;
 use Rector\Config\RectorConfig;

 return function (RectorConfig $rectorConfig): void {
-    $rectorConfig->import(LevelSetList::UP_TO_PHP_81);
+    $rectorConfig->import(LevelSetList::UP_TO_PHP_82);
 };

Frameworks or Downgrades?

You might think that's a very nice improvement for PHP, but do we support something similar for the framework packages?

See for yourself:

use Rector\Symfony\Set\SymfonyLevelSetList;
use Rector\Set\ValueObject\LevelSetList;
use Rector\Config\RectorConfig;

return function (RectorConfig $rectorConfig): void {
    $rectorConfig->import(LevelSetList::UP_TO_PHP_82);
    $rectorConfig->import(SymfonyLevelSetList::UP_TO_SYMFONY_60);
};

There are also Rector\Set\ValueObject\DowngradeSetList configs that make sure you downgrade with ease too!


Happy coding!

]]>
https://getrector.org/blog/new-in-rector-012-the-latest-php-in-a-single-import Fri, 31 Dec 2021 00:00:00 +0000 Fri, 31 Dec 2021 00:00:00 +0000 https://getrector.org/blog/new-in-rector-012-the-latest-php-in-a-single-import#disqus_thread
<![CDATA[ How all Frameworks can Bump to PHP 8.1 and You can Keep Using Older PHP ]]> https://getrector.org/blog/how-all-frameworks-can-bump-to-php-81-and-you-can-use-older-php Imagine hypothetical situation: new major Symfony and Laravel are released in December 2021. We'll already have PHP 8.1 out by that time. There have been a lot of positive vibes about new PHP versions in the last year, so let's say the frameworks will take a brave leap forward.

Symfony 6 and Laravel 9 will require PHP 8.1 as a minimal version in their composer.json.

How would you react to such a move? What if you could keep using your current PHP version while using Symfony 6 or Laravel 9?

]]>
Developers: "Please, Don't Force us to Upgrade PHP"

The day has finally come. The new Symfony and Laravel 9 are released. We want to enjoy the latest features, so we bump our composer.json:

 {
     "require": {
        "php": "8.0",
-       "symfony/console": "^5.3"
+       "symfony/console": "^6.0"
     }
 }


And run composer to update dependencies:

composer update


What happens?

"Could not install "symfony/console" packages because it requires PHP 8.1.
It does not meet your constraint of PHP 8.0."

This doesn't look good. Bumping to PHP 8.1 since day one might be a bit extreme, but this situation happens in every PHP version bump. No matter how small it is. Even if you bump from PHP 7.4 to 8.0:

There are always thousands of packages and projects that can't support the new PHP version you've decided to use.

Maintainers: "We need new PHP Features"

On the other hand, there is no point in maintaining PHP 7.1, 7.2, 7.3, and 7.4. Most of these do not have security updates:

But most importantly, if we support the old PHP version, we can say goodbye to features like:

  • typed properties
  • promoted properties
  • match()
  • enums
  • #[attributes]
  • union types
  • intersection types

Every package maintainer wants these features in its code. That's why we want to bump to PHP 8.1 in private projects as soon as possible.

Developers versus Maintainers

The project maintainers want to bump to PHP 8.1 to enjoy the latest features. On the contrary, developers who use packages want support for their PHP version, so they're not forced to upgrade PHP.

Two sides against each other. Humans versus machines.


Which of them is evil, and which of them is good? It depends on what side of the barricade do you stand on.

Is Peace Possible?

Disruptive evolution starts with an insane question.

What if we could move from one place to another at a speed of 100 km/h for just a few dollars per hour? Now we have trains, metro, and public transport system and consider such question stupid.

"What if you could keep using your current PHP version
while using Symfony 6 or Laravel 9?"

To start conflict resolution, we should look not at the differences but at shared goals. What do both camps want?

  • fun to code
  • 0 maintenance

We can achieve both of these if we add one step to the release workflow.

Introducing Release Downgrades

Let's say Symfony 6 or Laravel 9 is released with a minimum of PHP 8.1. How can we change the release process so, in the end, even developers with PHP 8.0 can use them?

Typical release of new tag from maintainer to developer downloading the package has 5-step lifecycle:

  • tag locally
  • push tag to the remote repository
  • let remote repository push the tag to Packagist via git hook
  • let user require a new version in composer.json
  • find the tag in Packagist and download the zip from GitHub

When we require symfony/console:6.0.0 in our code, we get the same git hash version in /vendor.

PHP-version Tailored Releases

The release process usually runs on GitHub, where also run GitHub Actions. That's where we can add step that releases different PHP versions of the same code:

php --version
# php 8.1
composer require symfony/console 6.0

Successfully installed 👍


php --version
# php 8.0
composer require symfony/console 6.0

Successfully installed 👍


Symfony 6.0 requires at least PHP 8.1. How is it possible we installed it on PHP 8? If we look closer, we'll see these are 2 different tags packages:

  • Symfony 6.0.0.81 for PHP 8.1
  • Symfony 6.0.0.80 for PHP 8.0

The last 2 numbers in the tag stand for the PHP version of the code.

The goal is to have the same features in both projects that use different PHP versions. We do 👍

Downgrade in GitHub Action

Where is the trick here? The PHP 8.1 release is a release as we know it; instead of 6.0.0, we tag it 6.0.0.81.

The PHP 8.0 release has one extra step - downgrade code to PHP 8.0

 * tag locally
+* downgrade to PHP 8.0
 * push tag to the remote repository
 * let remote repository push the tag to Packagist via git hook
 * let user require a new version in `composer.json.
 * find the tag in Packagist and download the zip from GitHub


We create a rector-downgrade.php config with downgrade set:

use Rector\Set\ValueObject\DowngradeLevelSetList;
use Rector\Config\RectorConfig;

return function (RectorConfig $rectorConfig): void {
    $rectorConfig->import(DowngradeLevelSetList::DOWN_TO_PHP_80);
};


Run Rector on the source code with this config:

vendor/bin/rector process /src --config rector-downgrade.php


After Rector finishes, the code in /src will have PHP 8.0 syntax. The release workflow will push it and tag it under 6.0.0.80.

This way, we can keep using our current PHP version, and maintainers can bump to the latest PHP ever.


Happy coding!


]]>
https://getrector.org/blog/how-all-frameworks-can-bump-to-php-81-and-you-can-use-older-php Mon, 18 Oct 2021 00:00:00 +0000 Mon, 18 Oct 2021 00:00:00 +0000 https://getrector.org/blog/how-all-frameworks-can-bump-to-php-81-and-you-can-use-older-php#disqus_thread
<![CDATA[ How to Upgrade Annotations to Attributes ]]> https://getrector.org/blog/how-to-upgrade-annotations-to-attributes We used @annotations in PHP 7.4 and below. Now we can use native #[attributes] in PHP 8. They have better support in PHPStan and Rector, thanks to their native language nature.

The Internet is full of questions "How can I use PHP 8 attributes instead of annotations in Doctrine?" or "Converting Annotations to Attributes".

Do you want to know the answer? Rector has a simple solution for you.

]]>
One package that added support for attributes is Doctrine:

 use Doctrine\ORM\Mapping as ORM;

-/**
- * @ORM\Column(type="string")
- */
+#[ORM\Column(type: "string")]

Now, let's go to upgrade itself. It's effortless.

Upgrade from Annotations to Attributes in 3 Steps

1. Configure rector.php to include the packages you use:

use Rector\Doctrine\Set\DoctrineSetList;
use Rector\Symfony\Set\SymfonySetList;
use Rector\Symfony\Set\SensiolabsSetList;
use Rector\Nette\Set\NetteSetList;
use Rector\Config\RectorConfig;

return function (RectorConfig $rectorConfig): void {
    $rectorConfig->sets([
        DoctrineSetList::ANNOTATIONS_TO_ATTRIBUTES,
        SymfonySetList::ANNOTATIONS_TO_ATTRIBUTES,
        NetteSetList::ANNOTATIONS_TO_ATTRIBUTES,
        SensiolabsSetList::FRAMEWORK_EXTRA_61,
    ]);
};


2. Run Rector to upgrade your code

vendor/bin/rector process


3. Handle Manual Steps

  • Do you use doctrine/orm? Be sure to use at least version 2.9, where attributes were released.

  • Do you use Doctrine with Symfony? Update the Symfony bundle mapping parser in your config to read attributes:

 # config/packages/doctrine.yaml
 doctrine:
     orm:
         mappings:
             App:
-                type: annotation

This enables new Symfony auto-detection feature.

That's it! Now your codebase uses nice and shiny PHP 8 native attributes.


Happy coding!

]]>
https://getrector.org/blog/how-to-upgrade-annotations-to-attributes Mon, 11 Oct 2021 00:00:00 +0000 Mon, 11 Oct 2021 00:00:00 +0000 https://getrector.org/blog/how-to-upgrade-annotations-to-attributes#disqus_thread
<![CDATA[ Dropping Docker in Favor of Composer Install for Better Developer Experience ]]> https://getrector.org/blog/dropping-docker-in-favor-of-composer-install-for-better-developer-experince Some developers see Docker as default-to-use for Rector. Yet they struggle to run it successfully with fundamental changes like renaming class from underscore to namespace.

It's very frustrating for them, and they often end-up up deleting the tool with a bad feeling inside.

This cannot happen.

]]>
The core team around Rector is not using Docker for running because there is no need for it. The current Docker version is naturally obsolete and lacking behind PHP code development.

Use Rector like PHPUnit

With downgrades and prefixed release repository, Rector can be easily installed on any project:

composer require rector/rector --dev

Rector can save you hundreds of hours on automated refactoring and instant upgrades. But the main added features are in the long-term feedback and everyday work it resolves for you. With new rules and sets, you should never upgrade manually ever again. How?

The same way PHPUnit checks your tests in CI, the same way Rector works in CI for you.


If the PHP version is the limit, Docker will not help as Rector requires PHP 7.1 to run. There is PHP 5.6 sponsorware program for projects who want to make it happen.


Saying that, we're dropping Docker from native Rector support. Do you want to keep Docker for Rector alive? See the issue to propose community maintenance.


We believe this choice will radically improve experience for first time users and embrace focus on single codebase to make it strong and reliable.


Thank you and happy coding!

]]>
https://getrector.org/blog/dropping-docker-in-favor-of-composer-install-for-better-developer-experince Mon, 09 Aug 2021 00:00:00 +0000 Mon, 09 Aug 2021 00:00:00 +0000 https://getrector.org/blog/dropping-docker-in-favor-of-composer-install-for-better-developer-experince#disqus_thread
<![CDATA[ How to bump Minimal PHP Version without Leaving Anyone Behind? ]]> https://getrector.org/blog/how-to-bump-minimal-version-without-leaving-anyone-behind Last week we introduced Prefixed Rector by Default. The main advantage of this release is that you have a single package to install, with no conflicts and minimal PHP version.

Rector can be used on PHP 7.1+ platforms. Yet, we bumped a minimal version to PHP 8. Is that a BC break?

]]>
Isn't that an irony that a tool that focuses on instant upgrades of legacy codebase wants to require the latest PHP as a minimum in composer.json? Aren't developers using PHP 8 the group of people that would never use Rector? Why upgrade to PHP 8 if we already have it?

In last post, we shared a new architecture of development and distribution repository. In short, this is the current situation:

1. Development Repository

This is a repository where you can send pull-request and change the code. We can use development dependencies to change the code and test Rector rules with PHPUnit, Rector, ECS, and PHPStan.

Its current composer.json:

{
    "name": "rector/rector-src",
    "require": {
        "php": ">=7.3"
    }
}

This package cannot be installed through composer require rector/rector-src, as it's not designed for end-users. If we would make a cake, it's a <těsto> :)

2. Release Repository

This repository is read-only and published from rector/rector-src. It's for developers who want to use Rector in their projects like:

composer require rector/rector --dev

How is it different from rector/rector-src? It's prefixed and downgraded to PHP 7.1. It includes the dependencies from /vendor, which are prefixed and downgraded too. Do you know PHAR? This repository is similar, just unpacked.

It's current composer.json:

{
    "name": "rector/rector",
    "require": {
        "php": ">=7.1"
    }
}

Single PHP Version bump Leads to Chain of BC Breaks

Bumping a minimal version in a package usually means that every package user must upgrade their PHP version to use the new package.

E.g., if/when Symfony 6 will require at least PHP 8 and you will still have PHP 7.4, you need to upgrade your PHP first to PHP 8, and only then can you install Symfony 6. At first, it seems like one step task, but there a fractal of traps behind the first corner.

If you upgrade the PHP version, all the packages in your vendor have to work on the new version. The new PHP version is usually connected to a major package release (e.g., Symfony 5 → 6). And what does major package release mean for changes? There will be BC breaks.

One package changed single PHP version requirement and soon you have to take a 3 months windows to upgrade your whole project and its dependencies.

Require min. PHP 8?

Despite this, we decided to bump the min PHP version for the Rector code base to PHP 8.

 {
     "name": "rector/rector-src",
     "require": {
-        "php": ">=7.3"
+        "php": ">=8.0"
     }
 }

"What? Are you serious?"

Yes. Now look more carefully at the composer.json change. It's not rector/rector, but rector/rector-src.

"So no changed for us end-users? We will still be able to use Rector on PHP 7.1. Even you build it in PHP 8?"

Exactly!

"That's amazing!"

Yes, it is.

How can Symfony 6 require PHP 8 and run PHP 7.1?

How can the PHP community benefit from this release model? Let's look at about use case we talked about before.

Symfony 6 will require PHP 8. But some people are stuck on the PHP 7.x version because their project is on a single server with 50 other projects (or any other valid reason).

Yet, they would love to use Symfony 6 features. Why? One reason for all - every single minor release has few super cool DX improvements.

So how can we do that for both groups?

Monorepo Combo!

We're lucky here, as the Symfony repository is using monorepo architecture. That means all the developments happen in symfony/symfony, and packages are split into smaller read-only repositories:

  • symfony/console
  • symfony/event-dispatcher
  • ...

Can you see the pattern Symfony, PHPStan, and Rector share?

We can add a "middleware" operation, thanks to the way the split happens on release. This middleware operation will downgrade code from PHP 8 to PHP 7.1, and the tag will be published with code on PHP 7.1.

Per-PHP Versions

"So if Symfony 6 is out, there will be only PHP 7.1 version? What if I have 2 projects and I want to make use of PHP 8 native code?"

No worries. Each release should have per PHP version tag.

  • Do you want to install Symfony 6 with PHP 8? Not a problem.
  • Do you want to install Symfony 6 with PHP 7.1? Yes, we can do that

"How do we have to install a package that fits our version? Will there be symfony/console-php71?"

That would be a way to go, but it would only bother users with learning new packages names for implementation detail they don't care about. That's why we merged rector/rector and rector/rector-prefixed.

Composer Semver to the Rescue

Instead, we can use simple tagging, adding the last 2 digits to the state PHP version.

  • 6.0.0 - native release without downgrade - no need to version
  • 6.0.0.71 - downgraded release to PHP 7.1
# on PHP 8
composer require symfony/console:^6.0
...installing symfony/console:6.0.0

# on PHP 7.1
composer require symfony/console:^6.0
...installing symfony/console:6.0.0.71

This way, if any package will require symfony/console:^6.0, they will always match the current set of features.

For now, it's just an idea. Yet we already can see working prototype examples in the PHP world. PHPStan is using this release approach since 0.12. This week Rector and ECS has joined.

The question is not "how can we do it",
but "who will be next"?


Happy coding!

]]>
https://getrector.org/blog/how-to-bump-minimal-version-without-leaving-anyone-behind Mon, 10 May 2021 00:00:00 +0000 Mon, 10 May 2021 00:00:00 +0000 https://getrector.org/blog/how-to-bump-minimal-version-without-leaving-anyone-behind#disqus_thread
<![CDATA[ Prefixed Rector by Default ]]> https://getrector.org/blog/prefixed-rector-by-default Today we're introducing a big step up in making Rector developer experience more smooth and intuitive. It will also ease development for Rector contributors. We won't have to think about dependencies in composer.json anymore.

Are these goals in contradiction? Quite the contrary.

]]>
This move was inspired by PHPStan 0.12 development and release repository setup.

The prefixed version allows to use of Rector on an older version than Rector is being developed. E.g., if you need to refactor your project on 7.1.

If you have symfony/console 2.8 and wanted to install rector/rector on your project, it would fail:

composer require symfony/console:2.8
composer require rector/rector --dev

That's where prefixed version helps too.

composer require symfony/console:2.8
composer require rector/rector-prefixed --dev

The ultimate problem with this setup is a terrible user experience [with hidden knowledge](@todo memory lock post). As a user, I don't want to think about different names for the same package. Would you install symfony/console or symfony/console-prefixed based on conflicts on install? No.

Single Distribution Package

We knew this must be a single way to install Rector:

composer require symfony/console:2.8
composer require rector/rector --dev

In April and May we've been working hard to make rector/rector-prefixed experience identical to rector/rector. It included:

Last big change was a repository switch. The original rector/rector repository will become development only and will be replecated with distribution rector/rector-prefixe repository:

  • rector/rector-prefixedrector/rector - the distribution repository
  • rector/rectorrector/rector-src - the development repository
  • deprecate rector/rector-prefixed and suggest rector/rector as replacement

How to Upgrade?

The next version is still in progress and will be released in May 2021. We're now testing the dev version to ensure there are no glitches when the stable tag is out.

There are 2 ways to upgrade, depending on which version you use.

For prefixed version:

composer remove rector/rector-prefixed
composer require rector/rector:^0.11 --dev

For normal version:

composer update rector/rector:^0.11 --dev

From now on, every next Rector release will be under rector/rector package name. One less thing to worry about before you instantly upgrade your code.


Happy coding!

]]>
https://getrector.org/blog/prefixed-rector-by-default Mon, 03 May 2021 00:00:00 +0000 Mon, 03 May 2021 00:00:00 +0000 https://getrector.org/blog/prefixed-rector-by-default#disqus_thread
<![CDATA[ From Doctrine Annotations Parser to Static Reflection ]]> https://getrector.org/blog/from-doctrine-annotations-parser-to-static-reflection Until recently, we used doctrine/annotations to parse class annotations that you know @ORM\Entity or @Route. Last 2 weeks, we rewrote this parser from scratch to our custom solution to improve spaces, constants and use static reflection.

During refactoring, the parser got reduced from 6700 lines to just 2700.
What we changed, why, and how can we benefit from a static reflection in annotations?

]]>
The doctrine/annotations package has been an excellent help for Rector for the past couple of years. Symfony, Doctrine, JMS, or Gedmo use the same package. We used it to parse the following code to a custom value object:

use Doctrine\ORM\Mapping as ORM;

// ...

/**
 * @ORM\Column(type="text")
 */
private $config;

Here the object was Rector\Doctrine\PhpDoc\Node\Property_\ColumnTagValueNode. This object provided data about all inner values:

$columnTagValueNode->getType(); // "text"

That way, we could modify the content, get the value type to add @var string etc. Straightforward object API with method IDE auto-complete. So far, so good?


Ups and Downs of Doctrine Annotations

The problem was that for every such annotation, we had to have a custom object. That means lot of classes:

Also, each class had its factory service that mapped annotation class to our custom *TagValueNode. Phew.


No Static Reflection

Doctrine parser uses class_exists() and native reflection to load the Column class annotation properties:

That means the static reflection we added in Rector 0.10 cannot be used here. That means you have to include the annotation classes in your autoloader. It's very confusing.


Constants are Replaced by their Values

The Doctrine parser is used only for reading the values. In Rector, we need to print the docblock back, e.g., change the type from "text" to "number".

That worked most of the time, but what if there was a constant? The constants are replaced by their values right here.

This causes bugs like these:

public const VALUES = [
    '4star' => FiveStar::class,
];

 /**
- * @Assert\Choice(choices=self::VALUES)
+ * @Assert\Choice({"4star":"App\Entity\Rating\FourStar"})
  */

Instead, we need to keep the original value of "self::VALUES", a bare constant reference. To overcome this, we had to create a set of Rector rules that will copy-paste the Doctrine parser class code from /vendor, replace the constant() lines with preserving identifier + value collector and few more ugly hacks.

This solution was terrible, but it did the job.

Broken Spaces on Reprint

Last but not least, spaces were completely removed on re-print:

-* @ORM\Table(name = "my_entity", indexes = {@ORM\Index(
-*     name = "my_entity_xxx_idx", columns = {
-*         "xxx"
-*     }
-* )})
+* @ORM\Table(name="my_entity", indexes={@ORM\Index(name="my_entity_xxx_idx", columns={"xxx"})})

We tried to compensate for this with regular expressions, but it was a very crappy solution.


Why we Used doctrine/annotations?

You may be wondering why we even used doctrine/annotations if it causes so many troubles?

"There are no solutions,
only trade-offs"

The next other solution was using phpdoc-parser from PHPStan. The most advanced docblock parser we now have in PHP. The first downside is that it parses Doctrine annotations as GenericTagValueNode with all values connected to a long string:

Do you need to change "my_entity" to "our_entity"? Use regular expression and good luck.


1. Nodes with Attributes

To make it work, we had to do 2 things: add attributes to the PhpDoc nodes, the same way nikic/php-parser does:

$phpDocNode->setAttibute('key', 'value');
$phpDocNode->getAttibute('key'); // "value"

That would enable format-preserving and nested values juggling, which Doctrine Annotations are known.

We proposed the attributes in phpdoc-parser 2 years ago, but it didn't get any traction as phpdoc-parser was also a read-only tool like Doctrine Annotations.

Luckily, it got revived and we contributed attributes on each node a month ago and was released under phpdoc-parser 0.5!

2. Rewrite Doctrine/Annotation in phpdoc-parser

We also needed values of annotation values using a custom lexer based on phpdoc-parser. This parser should:

  • keep constants
  • cover nested values, like annotation in an annotation
  • cover nested spaces, quotes, : or =
  • keep the original format


To make it happen, we had to rewrite DocParser from Doctrine to phpdoc-parser syntax. That included parsing values, arrays, curly arrays with keys, constants, brackets, quotes, and newlines between them.


11 days later, the final result is here:


Now every Doctrine-like annotation has:

  • single object to work with
  • annotation class with fully qualified class name
  • way to modify its values, quoted and silent
  • way to modify nested annotations
  • automated reprint on a modified node, e.g., if we change string to int

👍

How does it help the Rector Community?

With static reflection in annotations, now you can refactor old projects that use Doctrine Annotations without loading them.

Refactoring php doc tag nodes is now super easy. E.g., if we wanted to modify @Method from Sensio before this refactoring, we had to create a node class, a factory class, register it, autoload the doctrine annotation in a stub and prepare custom methods for custom properties of that specific class.

And now?

use Rector\BetterPhpDocParser\PhpDoc\DoctrineAnnotationTagValueNode;

$methodTagValueNode = $phpDocInfo->getByAnnotationClass(
    'Sensio\Bundle\FrameworkExtraBundle\Configuration\Method'
);

if ($methodTagValueNode instanceof DoctrineAnnotationTagValueNode) {
    $values = $methodTagValueNode->getValues();
    // ...
    $methodTagValueNode->changeValue('key', ['value']);
}


Happy coding!

]]>
https://getrector.org/blog/from-doctrine-annotations-parser-to-static-reflection Mon, 05 Apr 2021 00:00:00 +0000 Mon, 05 Apr 2021 00:00:00 +0000 https://getrector.org/blog/from-doctrine-annotations-parser-to-static-reflection#disqus_thread
<![CDATA[ Rector 0.10 Released - with PHP 7.1 Support ]]> https://getrector.org/blog/2021/03/22/rector-010-released-with-php71-support Today we're releasing Rector that brings the most significant improvement for usability yet. It took 2 months of hard work of our team and Rector community, but we're here.

What is new, and what makes your life easier?

]]>
February and March took the Rector repository by storm. The average pulse is around ~100 PRs, but this month it was amazing 188 PRpM.


Today we're happy to share the fruits of this extensive work with you in Rector 0.10 release.


What is in the package?


Static Reflection

This is one of the most extensive improvements we had in years. So big we've dedicated its post Legacy Refactoring made Easy with Static Reflection.

Rector on PHP 7.1 and 7.2 without Docker

Exactly three months ago, we've released Rector 0.9, which bumped the min PHP version from 7.1 to 7.3. That forced lot of legacy projects to go on the Docker path. If the project had Docker already, it probably wouldn't be in a state of legacy in the first place.

To compensate that, we introduced "downgrade sets". Sets that you can turn your PHP 8 project into PHP 7.1.


Three months ago, it was only an idea. Today, we're happy to eat our own dog food. We've downgraded Rector from 7.3 to 7.1. The downgraded code is published in rectorphp/rector-prefixed repository.

Use it like this:

php --version
# PHP 7.1

composer require symfony/console:^2.8

composer require rector/rector-prefixed:^0.10 --dev

The big advantage of this approach is that code is still editable in the vendor. Do you have any troubles or debugging your own Rector rule? Edit vendor/rector/rector-prefixed code like your own.


Are you interested in workflow implementation? See pull-request


We've reached PHP 7.1 and it's a great success. The next most wanted legacy PHP version is PHP 5.6. Can we downgrade Rector there too?

6 Standalone Project Packages

Let's be honest. The Rector repository got a bit fat around the belly over the years. It got out of shape, collecting too much clutter at once. Rector included every framework right in the core. Do you use Symfony? There is also CakePHP, Doctrine and Laravel.

It's like asking for Vietnamese translation for Bum Bo Nam Bo and getting 30 other languages as a bonus.

Grouping those projects together also created a barrier for contributors. Too much code to handle if you wanted to add a single Symfony rule. Contrary to that, we can see drupal-rector or typo3-rector Rector community packages that focus on single project.

Saying that, we've decided to make this simpler for you and created per-project packages:

Now it's easier to contribute, e.g., to Nette package, because you only have to work with Nette-specific code, nothing else.


This will also lead to:

  • more stable core Rector API, as many packages depend on it now
  • faster testing
  • inspiration for community-packages


These packages are included in rector/rector for now, so the prefixed and downgraded version can be used for any project.

Simpler Test Case

We collected feedback from Rector community developers about testing - custom methods were setting a parameter, for setting a php version, etc. It wasn't apparent how to use a test. We made this simple in Rector 0.10:

use Rector\Testing\PHPUnit\AbstractRectorTestCase;

final class SomeRectorTest extends AbstractRectorTestCase
{
    // test provided fixtures

    // provide fixtures

    public function provideConfigFilePath(): string
    {
        return __DIR__ . '/config/configured_rule.php';
    }
}

What you put in config/configured_rule.php, will happen. A single test case, single config, the syntax you know from rector.php is there.


Happy coding!

]]>
https://getrector.org/blog/2021/03/22/rector-010-released-with-php71-support Mon, 22 Mar 2021 00:00:00 +0000 Mon, 22 Mar 2021 00:00:00 +0000 https://getrector.org/blog/2021/03/22/rector-010-released-with-php71-support#disqus_thread
<![CDATA[ Legacy Refactoring made Easy with Static Reflection ]]> https://getrector.org/blog/2021/03/15/legacy-refactoring-made-easy-with-static-reflection Properly configured class autoloading have been a big requirement problem for many projects that do not use flawless PSR-4 autoload. It took two months of hard work of our team and Rector community, but we're here.

What is a static reflection, and how can you use it?

]]>
What is Static Reflection?

To perform static analysis of code in vendor, Rector needs to use reflection over the classes. The class must be autoloaded so we can use reflection on it. For classes, we can use PSR-4 or a class map to make that happen. Some projects don't use autoload at all as the framework handles it for them. Not only frameworks but also some tools have their autoloader - e.g., PHPUnit, so tests are in non-PSR-4 namespace structure too.

What about functions?

function hi()
{
    echo 'hello';
}

hi();

To autoload functions, we had to use include 'function_file.php. Including this file causes PHP to run it and shows "hello" during vendor/bin/rector run.

Simple "hello" is ok, but what about connecting to the database? Removing files etc.? "I've just run a CLI tool a few files were removed" is not a problem we want to deal with.


So what is static reflection? Instead of including a file with the hi() function, we parse the file AST and analyze the file without running it. We can ask a particular service, ReflectionProvider, for the function - if it's autoloaded, we'll get a native reflection. If not, we'll get the AST-based reflection.

You can read more in a great post on PHPStan blog - Zero Config Analysis with Static Reflection.


Configuration Everywhere

In Rector 0.9 and bellow, you could define files and directories to autoload - with a bit of RobotLoader help.

// rector.php
$parameters->set(Option::AUTOLOAD_PATHS, [
    __DIR__ . '/project-without-composer',
]);


This was very frustrating as every PSR-4 incompatible file had to be included. Even simple single file run like these:

vendor/bin/rector process someFile.php

We had to include the file:

vendor/bin/rector process someFile.php --autoload-file someFile.php


Rector Switch to Static Reflection

Rector is using PHPStan to analyse types for couple of years now. PHPStan implemented Static Reflection last summer. It was about time to give this feature to Rector users too:


How to Include Custom Autoload?

Static reflection now only parses the files found in AUTOLOAD_PATHS. That means those files are not executed and not included anymore. In some cases like custom autoload or files with defined constant, you still need to include them.

To keep this working, move these files to BOOTSTRAP_FILES parameter:

-$parameters->set(Option::AUTOLOAD_PATHS, [
+$parameters->set(Option::BOOTSTRAP_FILES, [
     __DIR__ . '/constants.php',
     __DIR__ . '/project/special/autoload.php',
 ]);


Do you want to try it out? This week the Rector 0.10 is released with static reflection on-board:

composer require rector/rector:^0.10 --dev

# or prefixed version
composer require rector/rector-prefixed:^0.10 --dev


Independent Low Maintenance Tests

In the past, every single test fixture was included so Rector could analyse it. With a project with 3192 test fixture, that often lead to conflicts on same-named classes or functions. One of the positive externalities is that test fixtures don't have to be unique classes anymore. Every fixture file is loaded independently on another. Now contributing Rector became easier with single click from demo.


Happy coding!

]]>
https://getrector.org/blog/2021/03/15/legacy-refactoring-made-easy-with-static-reflection Mon, 15 Mar 2021 00:00:00 +0000 Mon, 15 Mar 2021 00:00:00 +0000 https://getrector.org/blog/2021/03/15/legacy-refactoring-made-easy-with-static-reflection#disqus_thread
<![CDATA[ How much does Single Type Declaration Know? ]]> https://getrector.org/blog/2021/02/15/how-much-does-single-type-declaration-know When it comes to completing type declaration from docblocks, we rely on trust and hopes in commented code. One way out of is dynamic analysis that works with real data that enter the method. But we have to log it, wait for it, and update our codebase based on logged data.

Is there a faster, simpler solution we can just plugin?

]]>
Let's say we have a Person object:

final class Person
{
    /**
     * @var string
     */
    public $name;

    /**
     * @param string $name
     */
    public function __construct($name)
    {
        $this->name = $name;
    }

    /**
     * @return string
     */
    public function getName()
    {
        return $this->name;
    }
}

How sure are you about the name being a string? 80-95 %? Every percent under 100 % is a probability of a bug behind the corner.

We can do this:

$person = new Person(1000);

Or even this (don't try to imagine it):

$person = new Person(1000);
$anotherPerson = new Person($person);

See 3v4l.org.

Rector has a TYPE_DECLARATION set to autocomplete types based PHPStan types that rely heavily on docblocks even in strict mode. This set is useful for keeping high code quality but might break some older code.

Single Type Declaration

The way out of legacy is to completely type declaration right in PHP to every single place it can appear:

  • param types
  • return types
  • property types

Such work has enormous benefits, as we can rely 100 % on the types and move our focus on more critical parts. But it is tedious and prolonged work.

When it comes to a single type of declaration, there is more than meets the eye. More encoded knowledge is not visible to the human eye, but it is there.

Let's say we add a single type we are sure off:

 final class Person
 {
     /**
      * @var string
      */
     public $name;

-    /**
-     * @param string $name
-     */
-    public function __construct($name)
+    public function __construct(string $name)
     {
         $this->name = $name;
     }

     /**
      * @return string
      */
     public function getName()
     {
         return $this->name;
     }
}

Causality

The param $name in a constructor is always a string. What does it mean for the rest of the code? Assign in the constructor to property means that property uses identical type:

 final class Person
 {
-    /**
-     * @var string
-     */
-    public $name;
+    public string $name;

     public function __construct(string $name)
     {
         $this->name = $name;
     }

     /**
      * @return string
      */
     public function getName()
     {
         return $this->name;
     }
}


The $name property is now the string type right from the object construction. In effect, any getter inherits the same type:

 class Person
 {
     public string $name;

     public function __construct(string $name)
     {
         $this->name = $name;
     }

-    /**
-     * @return string
-     */
-    public function getName()
+    public function getName(): string
     {
         return $this->name;
     }
}

Now we have an object fully typed, and all we had to do is complete a single type in constructor.

What about places that are using the Person object?

 final class PersonScanner
 {
-    public function getPersonName(Person $person)
+    public function getPersonName(Person $person): string
     {
         return $person->getName();
     }
 }

And all methods using PersonScanner->getPersonName()? They know the string too. This healthy immunity is now spreading through our code base with every single type of declaration we add.

From single manually added type declaration Rector can autocomplete:

  • property type
  • getter return type
  • getter base on method call return type
  • every method call in the chain using typed property or getter return type

Rector watch will save you so much detailed detective work on types that are already in the code but hard to spot.

Try it Yourself

Add TYPE_DECLARATION_STICT set yourself of pick rule by rule, so you can see how your code base becomes strict for each new rule you add:

use Rector\TypeDeclaration\Rector\ClassMethod\ParamTypeFromStrictTypedPropertyRector;
use Rector\TypeDeclaration\Rector\ClassMethod\ReturnTypeFromReturnNewRector;
use Rector\TypeDeclaration\Rector\ClassMethod\ReturnTypeFromStrictTypedCallRector;
use Rector\TypeDeclaration\Rector\ClassMethod\ReturnTypeFromStrictTypedPropertyRector;
use Rector\TypeDeclaration\Rector\Property\TypedPropertyFromStrictConstructorRector;
use Rector\Config\RectorConfig;

return function (RectorConfig $rectorConfig): void {
    $rectorConfig->rule(ParamTypeFromStrictTypedPropertyRector::class);
    $rectorConfig->rule(ReturnTypeFromReturnNewRector::class);
    $rectorConfig->rule(ReturnTypeFromStrictTypedPropertyRector::class);
    $rectorConfig->rule(ReturnTypeFromStrictTypedCallRector::class);
    $rectorConfig->rule(TypedPropertyFromStrictConstructorRector::class);
};


Happy coding!

]]>
https://getrector.org/blog/2021/02/15/how-much-does-single-type-declaration-know Mon, 15 Feb 2021 00:00:00 +0000 Mon, 15 Feb 2021 00:00:00 +0000 https://getrector.org/blog/2021/02/15/how-much-does-single-type-declaration-know#disqus_thread
<![CDATA[ How to Instantly Decouple Symfony Doctrine Repository Inheritance to Clean Composition ]]> https://getrector.org/blog/2021/02/08/how-to-instantly-decouple-symfony-doctrine-repository-inheritance-to-clean-composition Do your Doctrine repositories extend a parent Symfony service? Do you use magic methods of parent Doctrine\ORM\EntityRepository? Would you like switch to decoupled service design and use composition over inheritance?

If you're looking for "why", read How to use Repository with Doctrine as Service in Symfony.

If you know why and look for "how", keep reading this post.

]]>
The Single Class Fallacy of The Best Practise

It's always very simple to show an example of one polished class, with final, constructor injection, SOLID principles, design patterns and modern PHP 8.0 features. That's why it's easy to write such posts as the one above :)

But what about real-life projects that have 50+ repositories? Would you read a post about how someone refactored 50 repositories to services one by one? Probably not, because it would take dozens of hours just to write the post.

Turn Fallacy to Pattern Refactoring with Rector

What if you could change just 1 case and it would be promoted to the rest of your application? From many cases, to just one. That's exactly what Rector help you with.

Let's see how it works. We'll use the example from the original post, where the goal is to turn inheritance to composition - one of SOLID principles.


Instead of inheritance...

namespace App\Repository;

use App\Entity\Post;
use Doctrine\ORM\EntityRepository;

final class PostRepository extends EntityRepository
{
}

...we use composition:

namespace App\Repository;

use App\Entity\Post;
use Doctrine\ORM\EntityManager;
use Doctrine\ORM\EntityRepository;

final class PostRepository
{
    private EntityRepository $repository;

    public function __construct(EntityManager $entityManager)
    {
        $this->repository = $entityManager->getRepository(Post::class);
    }
}

4 Steps to Instant Refactoring of All Repositories

1. Install Rector

composer install rector/rector --dev

2. Setup rector.php

use Symfony\Component\DependencyInjection\Loader\Configurator\ContainerConfigurator;
use Rector\Doctrine\Rector\MethodCall\ReplaceParentRepositoryCallsByRepositoryPropertyRector;
use Rector\Doctrine\Rector\Class_\MoveRepositoryFromParentToConstructorRector;

return function (ContainerConfigurator $containerConfigurator): void {
    $services = $containerConfigurator->services();

    // order matters, this needs to be first to correctly detect parent repository

    // this will replace parent calls by "$this->repository" property
    $services->set(ReplaceParentRepositoryCallsByRepositoryPropertyRector::class);

    // this will move the repository from parent to constructor
    $services->set(MoveRepositoryFromParentToConstructorRector::class);
};

3. Run Rector on Your Code

Now the fun part:

vendor/bin/rector process /app

You will see diffs like:

 use App\Entity\Post;
 use Doctrine\ORM\EntityRepository;

-final class PostRepository extends EntityRepository
+final class PostRepository
 {
+    private \Doctrine\ORM\EntityRepository $repository;
+    public function __construct(\Doctrine\ORM\EntityManager $entityManager)
+    {
+        $this->repository = $entityManager->getRepository(\App\Entity\Post::class);
+    }
     /**
      * Our custom method
      *
      * @return Post[]
@@ -14,7 +22,7 @@
      */
     public function findPostsByAuthor(int $authorId): array
     {
-        return $this->findBy([
+        return $this->repository->findBy([
             'author' => $authorId
         ]);
     }

And your code is now both refactored to more the cleanest version possible. That's it!



Happy instant refactoring!

]]>
https://getrector.org/blog/2021/02/08/how-to-instantly-decouple-symfony-doctrine-repository-inheritance-to-clean-composition Mon, 08 Feb 2021 00:00:00 +0000 Mon, 08 Feb 2021 00:00:00 +0000 https://getrector.org/blog/2021/02/08/how-to-instantly-decouple-symfony-doctrine-repository-inheritance-to-clean-composition#disqus_thread
<![CDATA[ How to Instantly Refactor Symfony Action Injects to Constructor Injection ]]> https://getrector.org/blog/2021/02/01/how-to-instantly-refactor-symfony-action-injects-to-constructor-injection Action Injections are much fun a first, but they turn your fresh project into legacy code very fast. With PHP 8 and promoted properties, there is no reason to pollute method arguments with services.

How to refactor out of the legacy back to constructor injection today?

]]>
Action Injection or Method Injection is Laravel and Symfony feature, that turns Controller action method to injected constructor:

final class SomeController
{
    public function actionDetail(int $id, User $user, PostRepository $postRepository)
    {
        $post = $postRepository->get($id);
        if (! $user->hasAccess($post)) {
            // ...
        }

        // ...
    }
}


It looks sexy and fun at first, but in few months, it will reveal its true face as an ugly code smell:


Action injection makes it confusing whether an object is treated stateful or stateless - a very grey area with, e.g., the Session.
Iltar van der Berg
I'm a Symfony trainer, and I'm told to teach people how to use Symfony and talk about this injection pattern. Sob.
Alex Rock
I work on a project that uses action injection, and I hate it. The whole idea about action injection is broken. Development with this pattern is a total nightmare.
A


It's natural to try new patterns with an open heart and validate them in practice, but what if you find this way as not ideal and want to go to constructor injection instead?

How would you change all your 50 controllers with action injections...

final class SomeController
{
    public function detail(int $id, Request $request, ProductRepository $productRepository)
    {
        $this->validateRequest($request);
        $product = $productRepository->find($id);
        // ...
    }
}

...to the constructor injection:

final class SomeController
{
    public function __construct(
        private ProductRepository $productRepository
    ) {
    }

    public function detail(int $id, Request $request)
    {
        $this->validateRequest($request);
        $product = $this->productRepository->find($id);
        // ...
    }
}

How to Waste a Week in one Team?

Let's say your project is fairly small, e.g. 50 controllers, there are four action methods per each. So you have to refactor 200 service arguments to constructor injection. You decided to do it manually.

  • some of the services them are duplicated

  • you have to identify 3 parts

  • there will be code-reviews and discussions that might take up to 5-10 days

  • and of course, rebase on new merged PRs... you have another 4-10 hours of team-work wasted ahead of you


We find the time of our client's team very precious, don't you? So we Let Rector do the work.

3 Steps to Instant Refactoring

1. Install Rector

composer install rector/rector --dev

2. Prepare Config

Enable the set and configure your Kernel class name in rector.php config:

use Rector\Set\ValueObject\SetList;
use Symfony\Component\DependencyInjection\Loader\Configurator\ContainerConfigurator;

return function (ContainerConfigurator $containerConfigurator): void {
    $containerConfigurator->import(SetList::ACTION_INJECTION_TO_CONSTRUCTOR_INJECTION);

    // the default value
    $parameters = $containerConfigurator->parameters();
    $parameters->set('kernel_class', 'App\Kernel');
};

3. Run Rector on Your Code

vendor/bin/rector process /app


You will see diffs like:

 final class SomeController
 {
+    public function __construct(
+         private ProductRepository $productRepository
+    ) {
+    }
+
-    public function detail(int $id, Request $request, ProductRepository $productRepository)
+    public function detail(int $id, Request $request)
     {
         $this->validateRequest($request);
-        $product = $productRepository->find($id);
+        $product = $this->productRepository->find($id);
         // ...
     }
 }


And your code is now both refactored and clean. That's it!



Happy instant refactoring!

]]>
https://getrector.org/blog/2021/02/01/how-to-instantly-refactor-symfony-action-injects-to-constructor-injection Mon, 01 Feb 2021 00:00:00 +0000 Mon, 01 Feb 2021 00:00:00 +0000 https://getrector.org/blog/2021/02/01/how-to-instantly-refactor-symfony-action-injects-to-constructor-injection#disqus_thread
<![CDATA[ Smooth Upgrade to Nette 3.1 in Diffs ]]> https://getrector.org/blog/2021/01/18/smooth-upgrade-to-nette-31-in-diffs Nette 3.1 was released almost a month ago. Packages using it had enough time to give support to small BC breaks and now it's ready to run on your project. Let's look at what has changed and how to upgrade today.

]]>
If you're not on Nette 3.1, hurry up. Why? There is a security issue in 3.0.x version:


But that's not the only reason to upgrade. Nette is rising to more active half of PHP frameworks and is right behind Laravel. Give Nette core developers your appreciation by upgrading to the new version as soon as possible.


What has Changed in Nette 3.1?

1. Minimal PHP Version Bumped to PHP 7.2

 {
     "require": {
-        "php": "^7.1"
+        "php": "^7.2"
     }
 }


2. All Interfaces lost their I Prefix

This is the most significant change of Nette 3.1. Removing visual clues makes it harder to separate classes from interfaces, and it affects over 30 class names:

-use Nette\Application\UI\ITemplate;
+use Nette\Application\UI\Template;

-final class SomeTemplate implements ITemplate
+final class SomeTemplate implements Template
 {
    // ...
 }
 foreach ($rows as $row) {
-    /** @var \Nette\Database\IRow $row */
+    /** @var \Nette\Database\Row $row */
     $row->...()
 }
-use Nette\Localization\ITranslator;
+use Nette\Localization\Translator;


Watch Out For Name Conflicts

This will create a naming conflict with already existing class short names, so be careful about "found & replace" upgrades:

-use Nette\Forms\IControl;
+use Nette\Forms\Control;
 use Nette\Applicatdion\UI\Control;
-use Nette\Application\UI\ITemplate;
+use Nette\Application\UI\Template;
 use Nette\Bridges\ApplicationLatte\Template;


Don't forget about configs:

 services:
     nette.mailer:
-        class: Nette\Mail\IMailer
+        class: Nette\Mail\Mailer


3. From Magical RouteList to addRoute() Method

 $routeList = new RouteList();
-$routeList[] = new Route('<presenter>/<action>[/<id>]', 'Homepage:default');
+$routeList->addRoute('<presenter>/<action>[/<id>]', 'Homepage:default');

 return $routeList;


4. Form addImage() to addImageButton()

 $form = new Form;
-$input = $form->addImage('image');
+$input = $form->addImageButton('image');


5. New addStaticParameters() Method

Now its more clear what is a static parameter and what dynamic one:

 // bootstrap.php
 $configurator = new Nette\Configurator();

-$configurator->addParameters([
+$configurator->addStaticParameters([
    // ...
]);

 $configurator->addDynamicParameters([
     // ...
 ]);


6. Renamed DefaultTemplate class

The interface rename sometimes took already existing class names. That's why some classes had to be renamed too:

-use Nette\Bridges\ApplicationLatte\Template;
+use Nette\Bridges\ApplicationLatte\DefaultTemplate;


7. New Param in sendTemplate() Method

 use Nette\Application\UI\Template;

 final class SomePresenter extends Presenter
 {
-    public function sendTemplate(): void
+    public function sendTemplate(?Template $template = null): void
     {
         // ...
     }
 }


8. Cache save() with Callable is Deprecated

After this change you have to resolve data first, then pass it to cache save() method:

-$result = $this->cache->save($key, function () {
-    return $this->getSomeHeavyResult();
-});
+$result = $this->getSomeHeavyResult();
+$this->cache->save($key, $result);


 session:
     autoStart: false
-    cookieSamesite: Lax
 $this->response->setCookie(
     $key,
     $data,
     $expire,
     null,
     null,
     $this->request->isSecured(),
-    true,
-    'Lax',
);


You can read more detailed post about changes on the Czech forum.


2 Steps to Make your Project Nette 3.1

Eager to try it on your project? This time try it without any composer.json modification - Rector will handle it too.

  1. Add NETTE_31 set your rector.php
use Rector\Nette\Set\NetteSetList;
use Rector\Config\RectorConfig;

return function (RectorConfig $rectorConfig): void {
    $rectorConfig->import(NetteSetList::NETTE_31);
 };
  1. Run Rector:
vendor/bin/rector process

That's it!


Have we missed something? Create an issue so we can complete the set for other developers.


Happy coding!

]]>
https://getrector.org/blog/2021/01/18/smooth-upgrade-to-nette-31-in-diffs Mon, 18 Jan 2021 00:00:00 +0000 Mon, 18 Jan 2021 00:00:00 +0000 https://getrector.org/blog/2021/01/18/smooth-upgrade-to-nette-31-in-diffs#disqus_thread
<![CDATA[ Switch Symfony String Route Names to Constants ]]> https://getrector.org/blog/2021/01/11/switch-symfony-string-route-names-to-constants Last December, we started to use PHP 8.0 and Symfony 5.2. This exact combination opens many cool tricks we could never use before. One of those tricks is using constants for route name in #[Route] attribute.

]]>
If you're not on PHP 8, switch with Rector today.

Ready now? Good.

We've all been there. Our memory is full of repeated strings with typos, and we have to type very slowly and carefully. IDE and CI is going the other directory - they automate our coding process, so we only type the bare minimum, so the program understands our intention. IDE autocompletes full class structures, method names, and pre-defined live templates.

Back to the strings. The only way to avoid typos is not to write at all. We're not there yet, so we go for the next minimum. Type them just once. In programming, we call it "constants".

class CompanyInfo
{
    public const COMPANY_NAME = 'Edukai, s. r. o.';

    public const VAT_IT = 'CZ07237626';

    public const CARD_NUMBER = '0239583290850928';
}

In this way, We can avoid using an incorrect card number.

Symfony #[Route] Reborn

Now back to Symfony. With Symfony 5.2, we have a new option use Routes. Instead of the old comment annotation way:

use Symfony\Component\Routing\Annotation\Route;

final class MissionController
{
    /**
     * @Route(path="archive", name="mission")
     */
    public function __invoke()
    {
        // ...
    }
}

We can use PHP-native attribute:

use Symfony\Component\Routing\Annotation\Route;

final class MissionController
{
    #[Route(path: 'mission', name: 'mission')]
    public function __invoke()
    {
        // ...
    }
}

What's the advantage of the route attribute?

  • It's native PHP
  • ECS can apply all PHP rules, Rector and PHPStan too
  • it won't break as wobbly PHP comments do

Use Constants in Attributes

Now that we've stated the benefits of constants and attributes, we should be ready to spot the problem they bring together:

use Symfony\Component\Routing\Annotation\Route;

final class MissionController
{
    #[Route(path: 'mission', name: 'mission')]
    public function __invoke()
    {
        // ...
    }
}
use Symfony\Component\Routing\Annotation\Route;

final class ContactController
{
    #[Route(path: 'contact', name: 'contact')]
    public function __invoke()
    {
        // ...
        return $this->redirectToRoute('mision');
    }
}

Can you see it? The route used has a "mission".

You're probably thinking, the IDE plugin would handle this, right? Well, you might be right, but on Symfony 5.2, it's broken and does not collect routes.

IDE plugin should only compensate missing PHP features,
not duplicate them with code smell approach.

When we see a string, we assume it's a unique string we can change without affecting anything else, like an error message or headline title.

So here is the suggestion - what if we constants used instead of strings for route names?

 use Symfony\Component\Routing\Annotation\Route;
+use App\ValueObject\Routing\RouteName;

 final class MissionController
 {
-    #[Route(path: 'mission', name: 'mission')]
+    #[Route(path: 'mission', name: RouteName::MISSION)]
    public function __invoke()
    {
        // ...
    }
}
 use Symfony\Component\Routing\Annotation\Route;
+use App\ValueObject\Routing\RouteName;

 final class ContactController
 {
-    #[Route(path: 'contact', name: 'contact')]
+    #[Route(path: 'contact', name: RouteName::CONTACT)]
     public function __invoke()
     {
         // ...
-        return $this->redirectToRoute('mision');
+        return $this->redirectToRoute(RouteName::MISSION);
     }
 }

Looks nice, right? But applying this to your 200 routes... well, not so lovely.

2 Steps to Convert Route Names to Constants

It is very nice to Rector because it will:

  • replace string routes with constants in your attributes
  • generate RouteName value object with all the route constants
  • tidy up the constant class import to short ones

Update your rector.php:

use Rector\SymfonyCodeQuality\Rector\Attribute\ExtractAttributeRouteNameConstantsRector;
use Rector\Config\RectorConfig;

return function (RectorConfig $rectorConfig): void {
    $rectorConfig->rule(ExtractAttributeRouteNameConstantsRector::class);

    $rectorConfig->importNames();
 };

Run Rector:

vendor/bin/rector process

That's it!


We run this rule on our website while writing this post. How did it go? See for yourself.


The Future Scope

With invokable controllers, we might get even to this:

use Symfony\Component\Routing\Annotation\Route;

final class ContactController
{
    #[Route(path: 'contact', name: ContactController::class)]
    public function __invoke()
    {
        return $this->redirectToRoute(MissionController::class);
    }
}


Happy coding!

]]>
https://getrector.org/blog/2021/01/11/switch-symfony-string-route-names-to-constants Mon, 11 Jan 2021 00:00:00 +0000 Mon, 11 Jan 2021 00:00:00 +0000 https://getrector.org/blog/2021/01/11/switch-symfony-string-route-names-to-constants#disqus_thread
<![CDATA[ 7 Valuable Lessons We Learned from our Clients in 2020 ]]> https://getrector.org/blog/2021/01/04/7-valuable-lessons-we-learned-from-our-clients-in-2020 2020 was a big year for us. We had 4 large projects with only tests in CI. Adding ECS with 10 basic sets, PHPStan to level 8, PSR-4 to all classes. In the end, we successfully upgraded Nette 2.2 to 3.0, Symfony 2.7 to 3.4 and Laravel 5.5 to 5.8, to Symfony, and from PHP 5.6 to 7.4. Oh, we also migrated Phalcon to Symfony.

The secret of a successful migration is speed and fast merges. During these 8 months of intense work, sometimes even 200 hours a month, we failed a lot. We try to learn from our mistakes.

Today we want to share what we've learned from our clients in 2020.

]]>
"Hindsight Is 20/20."

...moreover, in 2020. We want to share our failures so that you can prepare better prepared for your own.

1. Every Client is Different

Our focus groups are CTOs of projects that have decided to make a giant leap forward. That's a common trait that defines a good client. From that onward, it's subjective. Each person is different. While one client wants to be part of the migration and knows about each change, another client is happy for CI green checkbox in the CI. While one client wants to outsource the whole project to an external company, another client wants to contribute to your pull-requests.

Ask, communicate, set boundaries, and respect them mutually.

2. Communicate Possible Temporary Hacks

Nothing is at it seems at first sight. Developers use available framework features to their profit, regardless of what documentation or everyday use case it. E.g., the project has composer.json with a couple of internal packages:

{
    "require": {
        "company-name/some-package": "^1.1",
        "company-name/another-package": "^1.2"
    }
}

These are internal private packages that are only available to the project. The developer usually creates internal packages to extract a package that is used by many company projects. E.g., an agency creates a package to work with payments. To make it simpler, they move it to their repository, and every new project is using it.

When you upgrade the main project from PHP 5.6 to 7.0, you have to upgrade the "another-name/some-package". It takes time, testing, tagging, and fixing bugs found during this cycle. So we started this cycle and worked on it for 5 days.

Guess what. The package "another-name/some-package" is used only by the main project and it was decoupled because "it seemed like a good idea". There is no actual reason to keep it separated. We find this out too late when our client suggested, "we can move this package to a local vendor. It's used only by this project". We've just wasted 5 days of work.

Of course, the client knows better, as we don't have access to their repositories, but we should have asked:

"Why is this package in a standalone repository?
How many other repositories does it use?
Just one? Can we inline it here?
It would speed up the migration and save a lot of work."

Now we know. The correct approach for a package that people tend to refactor to their repository, but it is used only by the project they're trying to refactor it from is... local package. But it's not very known information, so instead of using the local package, a new repository with huge maintenance cost is added.

We were afraid to doubt our client about a lack of this knowledge. Next time we will politely ask to save both sides the troubles.

3. Create probe PRs

Sometimes it's to upgrade than it seems. Honestly, it rarely is, but it's worth giving a "blitzkrieg" push a try. What does that mean? E.g., change composer.json:

 {
     "reqiure": {
-        "nette/application": "^2.4",
+        "nette/application": "^3.0"
     }
 }

Run:

composer update

Then try to run an application and try to process as many exceptions as possible. One by one in a specific time frame, e.g., 2-4 hours. In the end, the project could be upgraded (not likely), or you'll end up with un-mergeable broken pull-request.

What now? Revert and give up? Continue with the frustration of end out of sight?


Don't worry. This work has its value, just not in being merged.

3 Valuable Takeaways

  • some of these commits can be applied even to older version
  • some of these commits can be turned into Rector rule and automated next time
  • some of these commits can be automated in some other way, e.g. Latte to Twig converter
  • some of these commits have to be done manually - we take note of them so we don't forget them

Now, we automated steps 1, 2, and 3. When we're done, we'll check out from the master branch and repeat the same probe process. But now we have 1, 2, and 3 automated, so we have a lot of extra time and work for manual-only work. We'll get much further in every iteration.

In the end, it might take 2-5 of such iterations. In the end, we have 30 commits before the upgrade even started, 10 new Rector rules, 2 new packages. And the project is migrated.

4. Go for as small PRs a Possible

This is very important, very. It's easy to create a big pull-request with 30 commits. The power is in small, independent pull-request.

  • If PR can be split, we have to do it
  • If this commit can be cherry-picked to the pre-migration phase, we have to do it
  • If one of 5 commits is not passing it, drop it and make pull-request pass

The confidence is the key here. The project owner has to feel they're in control, the pull-request will not break anything, and that CI is passing. If these conditions are met, pull-request can be merged quickly, and you can have a fast feedback loop. We managed to create such a feedback loop with one of our clients - then it was a standard to create 7-8 PRs a day, and they've merged within an hour. After a week of such cooperation we pushed the project further than we could do in the last month.

5. Have a Regular Calls with Face to Face

It was quite a challenge to start 4 new cooperations during times of corona. It was forbidden to meet, everyone worked from home with their spouse, dogs, cats, and families, and chaos was everywhere. That's why we often met in random cases. We had to agree on time each week or two. The time was different for each meeting, once 11 AM, then 3 PM. Sometimes we had a call over the phone, sometimes just emails.

This worked when everything went by expectations. But where there were frictions, the lack of communication was a problem. We got stuck over a problem with one client that we could've solved in a short call. But it would mean to organize a meeting, settle on a date that might be next week, and we both wanted to solve it fast. It created confusion on both sides, which was purely organizational.

We're very grateful that our clients were understanding and open to corrections. After this mistake, we decided to have a week-call based on a specific time, as short as 15 minutes, to catch up and have a space for trouble management.

Even if there was 0 work done, we knew we could talk to each other, see each other and mainly share updates out of our control, like 3rd party client requests, upcoming vacations, hot fixing server failure priority, or budget/time changes.

6. Get CI First, Even if it takes a Long Time

How do you know your project works? And how do you know the project you've just opened for the first time works? For us, it always a first time, and fast feedback is crucial. When we run Rector upgrade that changes 10 000 lines (which we do a lot), the fast feedback loop is essential also to our clients.

With the first project, we usually tried to deliver the visual upgrade first. That means something changed in composer.json and a lot of changed files. But without proper CI setup, this ends up with a couple of bugs that we wasted days or weeks on.

After this mistake, we started to CI setup first before any migration. First 4-6 weeks, there is no upgrade. Even though we risk losing a client because "they only want the upgrade", we stand behind this priority. We only focus on CI and preparation steps that make the rest of migration cheaper and faster. Consider it a year training program before going to war. Would you go to was right away or get training first?

We apply the same to coding. After we:

  • setup ECS with 10 basic sets
  • switch classes completely to PSR-4
  • add PHPStan to max level
  • run class-existence checks
  • automated tests running on CI

The migration is ready to go. Then it's fast as there are just little pitfalls to overcome.

7. Scoped and Downgraded Rector is Must Have

It's easy to upgrade a project running PHP 7.4 to 8.0. I bet we could upgrade any such project under a day (if we exclude dependency PHP vendor locks). But the lower PHP version is, the more difficult it gets. Rector 0.9 itself requires at least PHP 7.3. What if your project has PHP 7.1 or 7.2 or even 5.6? Then we have to switch to Rector in a Docker.

Developers can handle elementary upgrades without our help, so most of our clients can be found between PHP 5.6 and 7.1. Docker is very problematic to set up because projects that are legacy and need our help are legacy for a reason.

That's why we started a focus on downgrades in the Autumn 2020. The PHP 7.1 version is almost ready. Then we plan to continue to PHP 7.0 and PHP 5.6. The goal is to allow more accessible composer-like installation even on lower PHP:

composer require rector/rector-php56 --dev

This way, even the oldest project could upgrade from PHP 5.6 to 7.2 only with composer.


The year 2020 was a challenge for all of us. We learned a lot, thanks to our client and their patience with our upgrade process.

Without mistake, there is no learning. We hope you've learned a lot from our failures that are not related to migrations only. Good luck with yours!


Happy new year 2021!

]]>
https://getrector.org/blog/2021/01/04/7-valuable-lessons-we-learned-from-our-clients-in-2020 Mon, 04 Jan 2021 00:00:00 +0000 Mon, 04 Jan 2021 00:00:00 +0000 https://getrector.org/blog/2021/01/04/7-valuable-lessons-we-learned-from-our-clients-in-2020#disqus_thread
<![CDATA[ Rector 0.9 Released ❄️ ]]> https://getrector.org/blog/2020/12/28/rector-09-released More than 45 days have passed since the last Rector release. Since then, we pushed 292 commits in over 220 pull-requests. No wonder the most common question in issues was "when will the next Rector be released?".

Today, we're proud to finally tag and launch Rector 0.9!

]]>

PHP 8 Upgrade Set

The most awaited feature is upgrade set to PHP 8. We already wrote Smooth Upgrade to PHP 8 in Diffs - with promoted properties, annotations to attributes, union types, and more.

We tested this set for the last 30 days, solved bugs one by one, and applied in CI on 5 PHP projects.

How does it perform?

That feeling when you go to make coffee,
while @rectorphp upgrades your project in PHP 8...

Do you want to upgrade to PHP 8? It's ready!

use Rector\Set\ValueObject\SetList;
use Rector\Config\RectorConfig;

return function (RectorConfig $rectorConfig): void {
    $rectorConfig->import(SetList::PHP_80);
};

Debuggable Rector Prefixed

The PHAR file is a file that contains PHP code in a single RAR file, thus the PH+AR acronym. Working with PHAR in PHP has it's edge cases. Do you use a real path? realpath in PHAR does not work. Everything has to be relative to some PHAR root. Another bunch of bugs happen in Symfony, thanks to glob and slash juggling. The PHAR itself caused over 143 issues so far.

Dump in PHAR? Forget it

Rector is designed to work with the worst code possible. It's improving on every release but still fails with a fatal error on new edge cases that we haven't thought of.

How would you debug a line in prefixed Rector? Simple and fast dump() on the broken line? But, how can we edit a RAR file? It's not possible to do directly. First, we need to unpack the file, edit it, then pack it again. The same rules apply for PHAR.

We want to make working with Rector in legacy projects more accessible. That's why we're moving to scoped version.

Try it Now

Install path remains the same:

composer require rector/rector-prefixed --dev

But now you can also:

  • debug with dump()
  • modify the code in /vendor
  • work with absolute paths in Rector rules

Bump min version to PHP 7.3

PHP 7.2 is out of maintenance November 30th, and dangerous to use. That's why the minimal version for Rector was bumped to 7.3.

Downgrade Sets

Why would anyone want to downgrade code? What a silly question, right?

There is one use case that every package maintainer is thinking about. We want to develop in the latest PHP - PHP 8. But our composer.json also allows PHP 7.3 and 7.4. Why? Because we don't want to leave the PHP community behind.

This will soon be possible on a CI basis. Today, you can code in PHP 7.4 and deploy to 7.1 via GitHub Actions.

PHP-version Releases

Now the fun part comes. We have downgrade sets, and we moved from PHAR to Scoped. This switch not only solves edge cases and Docker mashup with relative/absolute paths but also opens the door to per-PHP version releases. Do you need Rector for PHP 7.1 without Docker?

composer require rector/rector-php71 --dev
composer require rector/rector-php70 --dev
composer require rector/rector-php56 --dev

The floor is the limit.

We're working on it. Stay tuned!

Upgrade from 0.8 to 0.9

Do you want to try a new version but fear changes? Check UPGRADE_09.md for specific steps and go for it:

composer update rector/rector:^0.9

Thank You, Contributors

Last but not least, we'd like to thank all 0.9 contributors that put their keystrokes together as one.

Thank you @samsonasik, @leoloso, @simivar, @staabm, @Wirone, @HDVinnie, @lulco and @JanMikes.


Happy rectoring!

]]>
https://getrector.org/blog/2020/12/28/rector-09-released Mon, 28 Dec 2020 00:00:00 +0000 Mon, 28 Dec 2020 00:00:00 +0000 https://getrector.org/blog/2020/12/28/rector-09-released#disqus_thread
<![CDATA[ 4 Configurable PHPStan rules that Help Rector Merge 188 pull-requests a Month ]]> https://getrector.org/blog/2020/12/14/4-configurable-phpstan-rules-that-help-rector-merge-188-pull-request-a-month Last month, we merged a total 188 pull-requests to Rector code. We could not afford such a high rate without having a robust CI setup we trust. Dozens of custom PHPStan rules help us on every commit.

Today we'll share with you 4 of them. You can use them in your code to save time and let robots work for you.

]]>
Following PHPStan rules saves us from pushing bugs to GitHub. They also save us precious time in code-reviews commenting obvious errors - that are easy to miss in huge pull-requests. They allow us not to think about code details but about business logic or architecture design.

1. When PHPStorm Imports the Wrong Short Class

Rector is using php-parser and it's nodes, e.g. PhpParser\Node\Expr\Variable.

A Rector rule that should change $something would look like this:

use Symfony\Component\DependencyInjection\Variable;
use Symplify\RuleDocGenerator\ValueObject\RuleDefinition;
use PhpParser\Node;
use Rector\Core\Rector\AbstractRector;

final class ChangeSomethignRector extends AbstractRector
{
    public function getNodeTypes() : array
    {
        return [Variable::class];
    }

    /**
     * @param Variable $node
     */
    public function refactor(Node $node) : ?Node
    {
        // ...
    }

    // ...
}

Now look at the code again, and try to find the bug.

Found it!

-use Symfony\Component\DependencyInjection\Variable;
+use PhpParser\Node\Expr\Variable;

There is more than 2 option actually:

PHPStorm has no idea which class you want to use. When we code, we don't have time or thought about which imports are right. 99 % it's the first one. Of course, PHPStorm can learn from our manual choices and prefer the most selected one. But that means every contributor has to teach their PHPStorm, to avoid these bugs. That's non-sense.

Instead, we added known miss-types that appeared in pull-requests to PHPStan checks. Not only the wrong type but neatly with a suggestion for the right node class.

# phpstan.neon
services:
    -
        class: Symplify\PHPStanRules\Rules\PreferredClassRule
        tags: [phpstan.rules.rule]
        arguments:
            oldToPreferredClasses:
                'PHPUnit\TextUI\Configuration\Variable': 'PhpParser\Node\Expr\Variable'
                'Symfony\Component\DependencyInjection\Variable': 'PhpParser\Node\Expr\Variable'
                'phpDocumentor\Reflection\Types\Expression': 'PhpParser\Node\Stmt\Expression'
                'phpDocumentor\Reflection\DocBlock\Tags\Param': 'PhpParser\Node\Param'
                'phpDocumentor\Reflection\DocBlock\Tags\Return_': 'PhpParser\Node\Stmt\Return_'
                'SebastianBergmann\Type\MixedType': 'PHPStan\Type\MixedType'
                'Hoa\Protocol\Node\Node': 'PhpParser\Node'

The PreferredClassRule works in 100 % of our cases. We never had to use the left type in our code.

2. From Class to its Test and Back Again

When we work with Rector, we either create new rules or fix bug in existing rule. To fix a bug, we add failing test fixture, then we switch to the rule class and fixed the bug. Add a test fixture, fix it... Add a test fixture, fix it...

That means constant jumping between rule and its tests. To save cognitive overload, we add a @see annotation above every rule class:

This way we avoid a long search for the correct class and just jump right to it with one click.

Does every new rule has this annotation? No need to bother each other during code-reviews, because PHPStan has our back:

# phpstan.neon
services:
    -
        class: Symplify\PHPStanRules\Rules\SeeAnnotationToTestRule
        tags: [phpstan.rules.rule]
        arguments:
            requiredSeeTypes:
                - Rector\Core\Rector\AbstractRector


An unintended side effect of the SeeAnnotationToTestRule is that every Rector rule is tested.

Do you want to add this PHPStan rule yourself but don't like hundreds of PHPStan errors in your CI? Use this Rector rule to complete @see annotations for you.

3. Only one Optional Method

Rector test cases can provide configuration in 3 different methods:

  • only Rector class in getRectorClass() method
  • Rector class with configuration in getRectorsWithConfiguration() method
  • full path to config.php with services in provideConfigFileInfo() method

At least that was the idea. In reality, it was possible to mix these methods:

use Rector\Testing\PHPUnit\AbstractRectorTestCase;

final class SomeRectorTest extends AbstractRectorTestCase
{
    public function getRectorClass()
    {
        return SomeRector::class;
    }

    // WTF? why is this here?
    // it only duplicates the previous method
    public function getRectorsWithConfiguration(): array
    {
        return [
            SomeRector::class => [],
        ];
    }
}

It didn't cause any runtime troubles, but it was a code smell. To avoid more broken windows, we've added a custom PHPStan rule.

This OnlyOneClassMethodRule checks classes of specific type(s), and makes sure there is exactly 1 method used from defined list:

# phpstan.neon
services:
    -
        class: Symplify\PHPStanRules\Rules\OnlyOneClassMethodRule
        tags: [phpstan.rules.rule]
        arguments:
            onlyOneMethodsByType:
                Rector\Testing\PHPUnit\AbstractRectorTestCase:
                    - 'getRectorClass'
                    - 'getRectorsWithConfiguration'
                    - 'provideConfigFileInfo'

4. Avoid Known Types Re-Checks

This rule is not configurable, but it saved us so much duplicated work we have to mention it.

Look at the following code. What would you improve type-wise?

use PhpParser\Node;
use PhpParser\Node\Expr\MethodCall;

class SomeClass
{
    public function run(Node $node)
    {
        if ($node instanceof MethodCall) {
            $this->isCheck($node);
        }
    }

    private function isCheck(Node $node)
    {
        if (! $node instanceof MethodCall) {
            return;
        }

        // ...
    }
}


What about this?

-   private function isCheck(Node $node)
+   private function isCheck(MethodCall $methodCall)
    {
-       if (! $node instanceof MethodCall) {
-           return;
-       }

        // ...
    }

Yes, why would you check MethodCall that it's MethodCall again? It might be evident in the post example with ten lines and nothing else. But in real life, we can easily miss it in any pull-request of 30+ lines. Now PHPStan has again our back with the CheckTypehintCallerTypeRule:

# phpstan.neon
services:
    -
        class: Symplify\PHPStanRules\Rules\CheckTypehintCallerTypeRule
        tags: [phpstan.rules.rule]

Thanks to @samsonasik for contributing and further improving this last rule. It's a real time-saver.


You can find all mentioned rules in symplify/phpstan-rules. Get them, use them. Your code will thank you.

That's all for today.


Happy coding!

]]>
https://getrector.org/blog/2020/12/14/4-configurable-phpstan-rules-that-help-rector-merge-188-pull-request-a-month Mon, 14 Dec 2020 00:00:00 +0000 Mon, 14 Dec 2020 00:00:00 +0000 https://getrector.org/blog/2020/12/14/4-configurable-phpstan-rules-that-help-rector-merge-188-pull-request-a-month#disqus_thread
<![CDATA[ Laravel Facades to Constructor Injection: Replace Facade Aliases with Full Classes in 2 hours ]]> https://getrector.org/blog/2020/12/07/laravel-facades-to-constructor-injection-replace-facade-aliases-with-full-classes-in-2-hours Laravel facades are known as static service locators. The idea is get any service anywhere, which comes very handy for project bootstrapping.

Around Laravel 6, released in March 2019, the Laravel community started moving away from facades towards clearly typed constructor injection.

Today we'll take 1st step to make it happen.

]]>
It was a big surprise for us that it's not only external critics of Laravel but also from inside the community itself. There is a proposal to remove facades completely in Laravel 6:


But as we well know, programmers are lazy:

"Give me the tools to solve my problem, and I'll consider it.
Give me an extra work, and I'll pass."

So, in the end, the discussion was closed, and nothing has changed. Yet, the spark that started a fire...

What the... Alias?

Aliases are defined in config/app.php under alias key. They're basically converted to:

// 'original class', 'alias'
class_alias('Some\LongerClass\App', 'App');

That means these 2 lines are identical:

App::run();
Some\LongerClass\App::run();

2 ways to do 1 thing is a code smell. It's also the low handing fruit we can make colossal leap to make our code base less magical.

As a bonus, we'll save few in config/app.php:

3 Steps to Remove Facade Aliases

Class alias is basically inverted class rename. So all we need to do si rename short classes (App) to long ones (Some\LongerClass\App), e.g.:

  1. Install Rector
composer require rector/rector --dev

# creates "rector.php"
vendor/bin/rector init
  1. Configure rector.php

Look how Laravel is helping us here. Just copy paste lines from config/app.phpaliases:

use Rector\Renaming\Rector\Name\RenameClassRector;
use Rector\Config\RectorConfig;

return function (RectorConfig $rectorConfig): void {
    $rectorConfig->ruleWithConfiguration(RenameClassRector::class, [
        'App' => 'Illuminate\Support\Facades\App',
        'Artisan' => 'Illuminate\Support\Facades\Artisan',
        'Auth' => 'Illuminate\Support\Facades\Auth',
        'Blade' => 'Illuminate\Support\Facades\Blade',
        'Broadcast' => 'Illuminate\Support\Facades\Broadcast',
        'Bus' => 'Illuminate\Support\Facades\Bus',
        'Cache' => 'Illuminate\Support\Facades\Cache',
        'Config' => 'Illuminate\Support\Facades\Config',
        // ...
    ]);
};
  1. Run Rector
vendor/bin/rector process src

That's it!


Blade Templates?

What are Blade templates in simple words? PHP files with syntax sugar.

Most Rector rules or static analysis would fail here, but Rector can rename classes even in non-standard PHP files like Blade, TWIG, Latte, Neon, or YAML. We rarely want to rename a class in PHP code and keep the old name in configs and templates.

Blade templates are covered too!


Real Case Study with Eonx

You're probably thinking: "Demo always looks nice. It's designed to look nice. But what about a real private project?" We're glad you doubt.

Recently, we applied this refactoring on a project of size 250 000-750 000 lines of code, in cooperation with Australian company, Eonx.

Here are all the pull-requests, we had to make:

The biggest pull-request has changed 45 600 lines.


The Numbers

  • we changed ~65 000 lines of code
  • it took ~15 hours of Rector configuration work and research (= the insights in this post)
  • and 4 hours of coding work and running Rector

Now the same work on the same project would take 2 hours in total. And we believe you can do it for your project too!


Happy refactoring!

]]>
https://getrector.org/blog/2020/12/07/laravel-facades-to-constructor-injection-replace-facade-aliases-with-full-classes-in-2-hours Mon, 07 Dec 2020 00:00:00 +0000 Mon, 07 Dec 2020 00:00:00 +0000 https://getrector.org/blog/2020/12/07/laravel-facades-to-constructor-injection-replace-facade-aliases-with-full-classes-in-2-hours#disqus_thread
<![CDATA[ Smooth Upgrade to PHP 8 in Diffs ]]> https://getrector.org/blog/2020/11/30/smooth-upgrade-to-php-8-in-diffs PHP 8 was released more than 2 weeks ago. Do you want to know what is new? Check colorful post series about PHP 8 news by Brent.

Do you want to upgrade your project today? Continue reading...

]]>
In a Rush to Private Jet?

3 Steps to Upgrade in 5 mins

  1. Do it in 5 minutes:
composer require rector/rector --dev
# create "rector.php"
vendor/bin/rector init
  1. Update rector.php with PHP 8 set:
+use Rector\Set\ValueObject\SetList;
 use Rector\Config\RectorConfig;

 return function (RectorConfig $rectorConfig): void {
+    $rectorConfig->import(SetList::PHP_80);
 };
  1. Run Rector:
vendor/bin/rector process src


How does such upgrade look in practise? See one of real pull-requests created with Rector:


Smooth Upgrade?

This tutorial aims to prepare you for the expected required steps so that the upgrade will require the least effort possible. Follow the guide and get to PHP 8 like a walk in the park.

What Rector handles for You?

1. From switch() to match()

-switch ($this->lexer->lookahead['type']) {
-    case Lexer::T_SELECT:
-        $statement = $this->SelectStatement();
-        break;
-
-    default:
-        $this->syntaxError('SELECT, UPDATE or DELETE');
-        break;
-}
+$statement = match ($this->lexer->lookahead['type']) {
+    Lexer::T_SELECT => $this->SelectStatement(),
+    default => $this->syntaxError('SELECT, UPDATE or DELETE'),
+};


2. From get_class() to Faster X::class

-get_class($object);
+$object::class;


3. From Dummy Constructor to Promoted Properties

 class SomeClass
 {
-    public float $alcoholLimit;
-
-    public function __construct(float $alcoholLimit = 0.0)
+    public function __construct(public float $alcoholLimit = 0.0)
     {
-        $this->alcoholLimit = $alcoholLimit;
     }
 }


4. Private Final Methods are Not Allowed Anymore

 class SomeClass
 {
-    final private function getter()
+    private function getter()
     {
         return $this;
     }
 }


5. Replace Null Checks with Null Safe Calls

 class SomeClass
 {
     public function run($someObject)
     {
-        $someObject2 = $someObject->mayFail1();
-        if ($someObject2 === null) {
-            return null;
-        }
-
-        return $someObject2->mayFail2();
+        return $someObject->mayFail1()?->mayFail2();
     }
 }


6. Unused $variable in catch() is not Needed Anymore

 final class SomeClass
 {
     public function run()
     {
         try {
-        } catch (Throwable $notUsedThrowable) {
+        } catch (Throwable) {
         }
     }
 }


7. New str_contains() Function

-$hasA = strpos('abc', 'a') !== false;
+$hasA = str_contains('abc', 'a');


8. New str_starts_with() Function

-$isMatch = substr($haystack, 0, strlen($needle)) === $needle;
+$isMatch = str_starts_with($haystack, $needle);


9. New str_ends_with() Function

-$isMatch = substr($haystack, -strlen($needle)) === $needle;
+$isMatch = str_ends_with($haystack, $needle);


10. New Stringable Interface for

-class Name
+class Name implements Stringable
 {
-    public function __toString()
+    public function __toString(): string
     {
         return 'I can stringz';
     }
 }

Class that implements Stringable can now be used in places, where string type is needed:

function run(string $anyString)
{
   // ...
}

$name = new Name('Kenny');
run($name);


11. From Union docblock types to Union PHP Declarations

 class SomeClass
 {
-    /**
-     * @param array|int $number
-     * @return bool|float
-     */
-    public function go($number)
+    public function go(array|int $number): bool|float
     {
         // ...
     }
 }


12. Symfony Annotations to Attributes

 use Symfony\Component\Routing\Annotation\Route;

 class SomeController
 {
-   /**
-    * @Route(path="blog/{postSlug}", name="post")
-    */
+    #[Route(path: 'blog/{postSlug}', name: 'post')]
     public function __invoke(): Response
     {
         // ...
     }
 }


13. From Doctrine Annotations to Attributes

-use Doctrine\Common\Annotations\Annotation\Target;
+use Attribute;
 use Symfony\Component\Validator\Constraint;

-/**
- * @Annotation
- * @Target({"PROPERTY", "ANNOTATION"})
- */
+#[Attribute(Attribute::TARGET_PROPERTY)]
 final class PHPConstraint extends Constraint
 {
 }

Then use in code with attributes:

 final class DemoFormData
 {
-    /**
-     * @PHPConstraint()
-     */
+    #[PHPConstraint]
     private string $content;
-
-    /**
-     * @PHPConstraint()
-     */
+    #[PHPConstraint]
     private string $config;

    // ...


Don't bother with any of the steps above. Let Rector handle it.

Update Dockerfile

Do you use Docker? Upgrade images to new PHP version:

 ####
 ## Base stage, to empower cache
 ####
-FROM php:7.4-apache as base
+FROM php:8.0-apache as base

GitHub Actions

Update shivammathur/setup-php@v2 in your workflows:

 jobs:
     unit_tests:
         runs-on: ubuntu-latest
         steps:
             -   uses: actions/checkout@v2
             -   uses: shivammathur/setup-php@v2
                 with:
+                    php-version: 7.4
-                    php-version: 8.0

Skip incompatible Coding Standard rules

These 3 rules are not compatible with PHP 8 yet. So better skip them in ecs.php:

  • PhpCsFixer\Fixer\ClassNotation\ClassAttributesSeparationFixer
  • PhpCsFixer\Fixer\Operator\BinaryOperatorSpacesFixer
  • PhpCsFixer\Fixer\Operator\TernaryOperatorSpacesFixer
  • SlevomatCodingStandard\Sniffs\Classes\DisallowMultiPropertyDefinitionSniff

Resolve Hard Conflicts With composer install

Some packages didn't get to update their composer.json. Be nice and help your fellow developers with a small pull-request:

 {
     "require": {
-        "php": "^7.3"
+        "php": "^7.3|^8.0"
     }
 }

Other packages block PHP 8 upgrades from their own maintainers' ideology, even though the code runs on PHP 8. Watch an excellent 15-min video about this by Nikita Popov, the most active PHP core developer, and Nikolas Grekas, the same just for Symfony.

But ideology is not why we're here. We want to upgrade our project to PHP 8. Thanks to Composer 2, this can be easily solved:

-composer install
+composer update --ignore-platform-req php

Upgrade your CI workflows and Docker build scripts, and you're ready to go.


Happy coding!

]]>
https://getrector.org/blog/2020/11/30/smooth-upgrade-to-php-8-in-diffs Mon, 30 Nov 2020 00:00:00 +0000 Mon, 30 Nov 2020 00:00:00 +0000 https://getrector.org/blog/2020/11/30/smooth-upgrade-to-php-8-in-diffs#disqus_thread
<![CDATA[ How to make Rector Contribute Your Pull Requests Every Day ]]> https://getrector.org/blog/2020/10/05/how-to-make-rector-contribute-your-pull-requests-every-day Rector can upgrade legacy code to a modern one. But in reality, that's ~5 % of usage. On the other hand, more than 300 projects use Rector daily, on every commit in Github Actions, Travis, and Gitlab CI.

And that's only open-source projects. The number of private projects using Rector would be much higher.

]]>
Using Rector in CI is a huge help to each member of the team. Rector reports weak parts and suggests better code.

But Rector's primary goal is not to give you more work and steal your attention. Rector handles repeated, and mundane work for you and let you focus on essential problems.

Actually, Rector is pushing commits to itself on GitHub since March 2020 when Jan Mikes added it. What does it mean? If you contribute a rule that belongs, e.g., to code quality set that is part of rector.php, its change will propagate to all Rector's code automatically before it gets merged. You don't have to do anything - maintenance zero.

When does that happen?

Make part Rector of your Code Review

We all know a passive code review. It's a classic code review, with comments from colleagues, reports from failed test cases, invalid coding standards, or maybe call on missing class. It only tells us something is wrong, but we have to fix it. It's passive feedback that only complains and adds us extra work.

That is not sustainable. The bigger code-base you have, the more features you add, the more passive code-review you get from all your colleagues and tools from CI. That isn't very pleasant, and it's clear why most developers don't like code-reviews. I don't mean those who give feedback, but those who need to invest the energy to resolve the feedback.

From Passive to Active Code Review

You already know coding standard tools like ECS that fix code for you. It fixes all the code style rules your team agreed on. E.g., each file must contain declare(strict_types=1);. If a new developer joins the team, he or she don't have to handle it, the coding standard tool will do it for them.

The same works for more complicated cases, like making every class final, using service constructor injection, and using repository service.

Do you enjoy making code-reviews with hundreds of rules in your head and adding extra work to the pull-request author?

We don't, so we let Rector for us in active code review.

How to make Rector Active Contributor in GitHub Actions

We've been testing this workflow for the last 7 months and that saving work and attention is addictive.

The workflow is simple:

  • you commit to a new branch
  • Rector runs through the code, changes it
  • changes are committed to your pull-request
  • then you can either merge it or continue pushing (with force to override it, because Rector will change the new code again - he's restless)

Let's talk real code now.

We have a dedicated GitHub action to handle this process.

Do you want to try it? Copy it, create new access token, add ACCESS_TOKEN env variable to repository Secrets, and you're ready to make your first actively reviewed pull-request.

# .github/workflows/rector_ci.yaml
name: Rector CI

on:
    pull_request: null

jobs:
    rector-ci:
        runs-on: ubuntu-latest
        # run only on commits on main repository, not on forks
        if: github.event.pull_request.head.repo.full_name == github.repository
        steps:
            -
                uses: actions/checkout@v2
                with:
                    # Solves the not "You are not currently on a branch" problem, see https://github.com/actions/checkout/issues/124#issuecomment-586664611
                    ref: ${{ github.event.pull_request.head.ref }}
                    # Must be used to trigger workflow after push
                    token: ${{ secrets.ACCESS_TOKEN }}

            -
                uses: shivammathur/setup-php@v2
                with:
                    php-version: 7.4
                    coverage: none

            -   run: composer install --no-progress --ansi

            ## First run Rector without --dry-run, it would stop the process with exit 1 here
            -   run: vendor/bin/rector process --ansi

            -
                name: Check for Rector modified files
                id: rector-git-check
                run: echo ::set-output name=modified::$(if git diff --exit-code --no-patch; then echo "false"; else echo "true"; fi)

            -   name: Git config
                if: steps.rector-git-check.outputs.modified == 'true'
                run: |
                    git config --global user.name 'rector-bot'
                    git config --global user.email 'your-name@your-domain.com'
                    echo ::set-env name=COMMIT_MESSAGE::$(git log -1 --pretty=format:"%s")

            -   name: Commit Rector changes
                if: steps.rector-git-check.outputs.modified == 'true'
                run: git commit -am "[rector] ${COMMIT_MESSAGE}"


Then, the coding standard fixes all design nuances that Rector made:

            ## Now, there might be coding standard issues after running Rector
            -
                if: steps.rector-git-check.outputs.modified == 'true'
                run: vendor/bin/ecs check src --fix

            -
                name: Check for CS modified files
                if: steps.rector-git-check.outputs.modified == 'true'
                id: cs-git-check
                run: echo ::set-output name=modified::$(if git diff --exit-code --no-patch; then echo "false"; else echo "true"; fi)

            -   name: Commit CS changes
                if: steps.cs-git-check.outputs.modified == 'true'
                run: git commit -am "[cs] ${COMMIT_MESSAGE}"


Last, we push the commits into the branch:

            -   name: Push changes
                if: steps.rector-git-check.outputs.modified == 'true'
                run: git push

Congrats! Now you delegate active code-reviews to Rector.

Make the most of Rector CI

To make the most of it, notice the most repeated comments in passive code-reviews and make Rector rules out of it. You'll save time, work and code-reviews become more joyful and lighter. As a side effect, you can now focus on topics that computers can't automate (yet) - architecture and design.

That's all, folks - now go and try out the Github Action for yourself.


Happy coding!

]]>
https://getrector.org/blog/2020/10/05/how-to-make-rector-contribute-your-pull-requests-every-day Mon, 05 Oct 2020 00:00:00 +0000 Mon, 05 Oct 2020 00:00:00 +0000 https://getrector.org/blog/2020/10/05/how-to-make-rector-contribute-your-pull-requests-every-day#disqus_thread
<![CDATA[ How to Inline Value Object in Symfony PHP Config ]]> https://getrector.org/blog/2020/09/07/how-to-inline-value-object-in-symfony-php-config Rector uses PHP Symfony configs for many good reasons.

One of them is the possibility to have control over complex configurations with value objects. Would you like such features in your configs too? Unfortunately, Symfony does not support it out of the box.

What can we do about it?

]]>
Since Rector 0.12 we deal with value objects for you with configure() method. The configuration is simpler and input is validated.

Why are Value Objects in Configuration Priceless?

Let's say we want to rename dump() func call to Tracy\Debugger::dump().

-dump($value);
+Tracy\Debugger::dump($value);

KISS. Why not use a simple array?

Look at our previous post about why the road to hell is paved with good intentions.


Now that we know why and when we want to use value objects, the question is How can we use them in Symfony PHP Config?

1. Direct Object

First obvious idea is to just use is like we would in PHP:

// very dummy PHP approach
$funcCallToStaticCallRector = new FuncCallToStaticCallRector();

$configuration = [
    FuncCallToStaticCallRector::FUNC_CALLS_TO_STATIC_CALLS => [
         new FuncCallToStaticCall('dump', 'Tracy\Debugger', 'dump'),
    ]
];

$funcCallToStaticCallRector->configure($configuration);

This should work, right? It's just passing a bunch of scalar values:

// rector.php

$services->set(FuncCallToStaticCallRector::class)
    ->call('configure', [[
        FuncCallToStaticCallRector::FUNC_CALLS_TO_STATIC_CALLS => [
            new FuncCallToStaticCall('dump', 'Tracy\Debugger', 'dump'),
        ]
    ]]);

 [ERROR] Cannot use values of type
         "Rector\Transform\ValueObject\FuncCallToStaticCall" in service
         configuration files.


2. Native inline_service() Function

Fortunately, Symfony gives us a little trick. The inline_service() basically registers a local service, just for the configuration:

// rector.php

$services->set(FuncCallToStaticCallRector::class)
    ->call('configure', [[
        FuncCallToStaticCallRector::FUNC_CALLS_TO_STATIC_CALLS => [
            inline_service(FuncCallToStaticCall::class)
                ->args(['dump', 'Tracy\Debugger', 'dump']),
        ]
    ]]);

This works!

There is just little detail. The IDE autocomplete we want from value objects:

...is gone.

  • How can we know there are precisely 3 arguments?
  • What is their order?
  • What is their name?
  • When does __construct() type validation happens?

This approach shuts us back to coding in the dark with Notepad.


3. Best of Both Worlds - inline_value_objects()

What we need here?

  • A value object configuration that looks like a value object:

    new SomeClass('value');
  • 1 line solution

  • Ideally, in Symfony way, so it the least surprise to use


To cover all benefits together, we've created custom ValueObjectInliner class:

// rector.php
$services->set(FuncCallToStaticCallRector::class)
    ->call('configure', [[
        FuncCallToStaticCallRector::FUNC_CALLS_TO_STATIC_CALLS => ValueObjectInliner::inline([
            new FuncCallToStaticCall('dump', 'Tracy\Debugger', 'dump'),
            new FuncCallToStaticCall('d', 'Tracy\Debugger', 'dump'),
        ])
    ]]);


4. Simple configure() Method

Since Rector 0.12 you can use ->configure() method, that handles the complexity for you:

$services->set(FuncCallToStaticCallRector::class)
    ->configure([
        new FuncCallToStaticCall('dump', 'Tracy\Debugger', 'dump'),
        // it handles multiple items without duplicated call
        new FuncCallToStaticCall('d', 'Tracy\Debugger', 'dump'),
        new FuncCallToStaticCall('dd', 'Tracy\Debugger', 'dump'),
    ]);

Instead of complexity keys, constants and inliners, just pass it array of value objects.

And you're set!


This way, anyone can configure even the most complex Rector rules without ever looking inside the Rector rule and scan for configuration values.

And What Rector uses What Value Object?

In convention over configuration principle, all you need to know the rule name.

Could you guess the value object naming pattern?

  • FuncCallToStaticCallRector (rule)
  • FuncCallToStaticCall (value object)

That's it!


Happy and safe coding!

]]>
https://getrector.org/blog/2020/09/07/how-to-inline-value-object-in-symfony-php-config Mon, 07 Sep 2020 00:00:00 +0000 Mon, 07 Sep 2020 00:00:00 +0000 https://getrector.org/blog/2020/09/07/how-to-inline-value-object-in-symfony-php-config#disqus_thread
<![CDATA[ Rector is Moving From YAML to PHP Configs - What Changes and How to Get Ready? ]]> https://getrector.org/blog/2020/08/31/rector-is-moving-from-yaml-to-php-configs-what-changes-and-how-to-get-ready In July 2020, we started to move from the configuration in YAML to one defined in PHP. The YAML configuration is now deleted in Rector core and won't be supported next 0.8 release.

What benefits PHP brings, how the rule configuration changes, and how to prepare yourself?

]]>

You might have noticed the warning on Rector run:

This happens when you're using rector.yaml config in your project.

If you're already on rector.php, this message is gone.

Testing YAML to PHP in the Wild

It took around 3 weeks to switch configs, test out practical impacts, and resolve bugs related to method call values merging.

What is method call values merging? Rector uses multiple Symfony configs to keep sets separated, e.g., symfony40, symfony41, symfony42, etc. Each of them renames some classes, so they call configure() method with their values.

<?php

use Rector\Renaming\Rector\Name\RenameClassRector;

// first config
$services = $containerConfigurator->services();
$services->set(RenameClassRector::class)
    ->configure([
        'old_2' => 'new_2',
    ]);


If you have 2 configs with same method call and same argument, only the latest one is used:

// another config
$services->set(RenameClassRector::class)
    ->configure([
            'old_1' => 'new_1',
    ]);

We fixed this behavior from "override" to "merge" with custom file loader. It collects all the arguments and sets them once in the end before the container is compiled.


Now, even our demo runs on PHP configs:

Saying that yesterday we dropped YAML config support from Rector core.


How to Switch from rector.yaml to rector.php

What if you have a lot of custom setup in rector.yaml? Is there a list of changes that you need to do manually?

Don't worry. We're all lazy here. Symplify toolkit got you covered:

composer require symplify/config-transformer --dev

Then provide files/directories with YAML files you want to switch to PHP:

vendor/bin/config-transformer switch-format rector.yaml

What are Instant Benefits of rector.php?

There are 2 groups of benefits: one is related to general format switch - read about them in 10 Cool Features You Get after switching from YAML to PHP Configs.

The other is related to the smarter configuration of Rector rules.

Before, the configuration was done in an array-random-like manner.

services:
    # rename class
    Rector\Renaming\Rector\Name\RenameClassRector:
        $oldToNewClasses:
            'OldClass': 'NewClass'

Be sure to bother your brain with memorizing $oldToNewClasses. And this is just key: value complexity - the simplest one.

What about more common nested configuration, e.g., constant rename?

services:
    Rector\Renaming\Rector\ClassConstFetch\RenameClassConstantRector:
        $oldToNewConstantsByClass:
            'SomeClass::OLD_CONSTANT': 'NEW_CONSTANT'
            # or...?
            'SomeClass':
                'OLD_CONSTANT': 'NEW_CONSTANT'
            # or...?
            'SomeClass':
                'OLD_CONSTANT': ['NEW_CONSTANT']
            # or...?
            ['SomeClass', 'OLD_CONSTANT']: 'NEW_CONSTANT'

This is a perfect example of "have an n-guesses, then rage quit" developer experience—freedom at its worst.

Value Objects Configuration FTW

There is a simple solution with PHP that could never be used in YAML - value objects.

final class ClassConstantRename
{
    // ...
    public function __construct(string $oldClass, string $oldConstant, string $newConstant)
    {
        // ...
    }
}

What Value is in Value Objects?

By only making single object, we got instantly high code quality for free:

  • type validation, string/int/constants
  • IDE autocompletes names of parameters, so we know what arguments are needed
  • a single line of configuration, no nesting to nesting to nesting to nesting


How do value objects look like in practice?

use Rector\Renaming\Rector\ClassConstFetch\RenameClassConstantRector;
use Rector\Config\RectorConfig;

return function (RectorConfig $rectorConfig): void {
    $rectorConfig->ruleWithConfiguration(RenameClassConstantRector::class, [
        new ClassConstantRename('Cake\View\View', 'NAME_ELEMENT', 'TYPE_ELEMENT')
    ]);
};

That's it. Use IDE autocomplete and value objects.

There is no need to look into the rule and look for the parameter name, the meaning of that particular key or value, if the key/value is required, optional, named, or implicit.


We're thinking of introducing rule <=> value object naming convention, so it gets even simpler:

  • RenameClassConstantRector for rule
  • RenameClassConstant for value object

Thanks to PHP, now all these optimizations are possible, and PHPStorm powers help us. And this is just a start.


Happy coding!

]]>
https://getrector.org/blog/2020/08/31/rector-is-moving-from-yaml-to-php-configs-what-changes-and-how-to-get-ready Mon, 31 Aug 2020 00:00:00 +0000 Mon, 31 Aug 2020 00:00:00 +0000 https://getrector.org/blog/2020/08/31/rector-is-moving-from-yaml-to-php-configs-what-changes-and-how-to-get-ready#disqus_thread
<![CDATA[ How to Migrate From PHPExcel to PHPSpreadsheet with Rector in 30 minutes ]]> https://getrector.org/blog/2020/04/16/how-to-migrate-from-phpexcel-to-phpspreadsheet-with-rector-in-30-minutes PHPExcel is a package for working with Excel files in PHP. The last version was released in 2015, and it was deprecated in 2017. Still, it has over 27 000 daily downloads - that's tons of legacy code.

Do you use it too? Do you want to switch to PHPSpreadsheet? You can do it today.

]]>
PHPSpreadsheet is direct follower of PHPExcel with the same maintainers, just with dozens of BC breaks. There is the official migration tutorial that describes step by step 24 different changes that you need to do.

Only 1 of those changes - class renames - is automated with preg_replace(). Regular expressions are not the best thing to use to modify code, rather contrary.


But there are also extra method calls:

-$worksheet->getDefaultStyle();
+$worksheet->getParent()->getDefaultStyle();

Method call renames:

-$worksheet->setSharedStyle($sharedStyle, $range);
+$worksheet->duplicateStyle($sharedStyle, $range);

Argument switch and extra method calls:

-$cell = $worksheet->setCellValue('A1', 'value', true);
+$cell = $worksheet->getCell('A1')->setValue('value');

Method move to another class:

-PHPExcel_Cell::absoluteCoordinate()
+PhpOffice\PhpSpreadsheet\Cell\Coordinate::absoluteCoordinate()

and so on.


Most people use PHPExcel in the past, and didn't touch the code ever since. If it works, why touch it, right?

That why for last 3 years the download rate decreased just very poorly - from ~25 000 daily downlaod to ~20 000 daily downloads:

We need Migration - Fast

We got into a project that used PHPExcel and wanted to switch to PHP 7.4 and Symfony 5. PHP upgrade and framework migration is half of the work. The other half are these small packages, that vendor locks your project to old PHP or another old dependency.

To get rid of old PHPExcel, we prepare a set phpexcel-to-phpspreadsheet to help us.

It took 3 days to make and now has 230 lines of configuration and 17 rules.

How to Migrate?

  1. Install Rector
composer require rector/rector --dev
  1. Create rector.php config
vendor/bin/rector init

# on Windows?
vendor\bin\rector init
  1. Add your set to the rector.php config:
use Rector\PHPOffice\Set\PHPOfficeSetList;
use Rector\Config\RectorConfig;

return function (RectorConfig $rectorConfig): void {
    $rectorConfig->import(PHPOfficeSetList::PHPEXCEL_TO_PHPSPREADSHEET);
};
  1. Dry run Rector to see what would be changed
vendor/bin/rector process src --dry-run
  1. Make the changes happen:
vendor/bin/rector process src


That's it! Rector just migrated your code from PHPExcel to PHPSpreadsheet.


If you have any issues, look at official migration tutorial or let us know on GitHub.


Happy coding!

]]>
https://getrector.org/blog/2020/04/16/how-to-migrate-from-phpexcel-to-phpspreadsheet-with-rector-in-30-minutes Thu, 16 Apr 2020 00:00:00 +0000 Thu, 16 Apr 2020 00:00:00 +0000 https://getrector.org/blog/2020/04/16/how-to-migrate-from-phpexcel-to-phpspreadsheet-with-rector-in-30-minutes#disqus_thread
<![CDATA[ Upgrading Glami to PSR-4, part 1: What and why? ]]> https://getrector.org/blog/2020/03/10/upgrading-glami-to-psr-4-what-why-how In April 2019 we upgraded Glami's big codebase to follow PSR-4.

It was a great success! In this part, we will go through what PSR-4 is and it's benefits.

]]>
What is PSR-4?

PSR-4 is standard for autoloading classes in the PHP world, supported for example by composer or by PHPStorm since 2014.

In short, you must register namespace prefix to a directory and every class fully qualified name must follow certain standards. As well only having a single class per file is allowed and it must match (without the .php extension).

Example is worth 1000 words here:

Fully Qualified Class Name Namespace Prefix Base Directory Resulting File Path
Acme\Log\Writer\File_Writer Acme\Log\Writer ./acme-log-writer/lib/ ./acme-log-writer/lib/File_Writer.php
Symfony\Core\Request Symfony\Core ./vendor/Symfony/Core/ ./vendor/Symfony/Core/Request.php
Rector\Website\Demo\Controller\DemoController Rector\Website ./src ./src/Controller/DemoController.php

Following PSR-4 + using composer for autoloading allows you to rapidly create classes and use them instantly, without bothering about loading them manually.

It is really simple, just check autoloading definition in composer.json of this website:

{
  "autoload": {
        "psr-4": {
            "Rector\\Website\\": "src",
            "Rector\\Website\\Blog\\": "packages/Blog/src"
        }
    }
}

Why should you want it?

Without PSR-4 it's a mess!

Most of the popular tools (like Rector, PHPStan or Psalm) expects you to have solved the question of autoloading already or they most likely will not be able to process your code.

In Glami, for autoloading classes, they were originally using a combination of Nette RobotLoader (a great tool for autoloading, if you do not mind about PSR-4 at all) and on some places, for performance reasons, including the files manually.

 

Having 5 classes in the same file was a common thing, making the need for finding a specific class for the developer much more difficult.

We found "dead" PHPUnit test cases - not running, because of not following the standard! For example class MySomeClassTest in file MySomeClass.php and because of the default *Test.php file filter in PHPUnit, these tests were simply ignored.

Glami is very focused on performance. Because there are no publicly documented performance outcomes of switching to PSR-4 autoloading, we did not know what to expect.

And we were quite surprised the after first tests! Glami was globally faster by 2-4ms!

In one of the most business-critical parts, response time lowered from 8ms to 6ms. That is an incredible 25% performance gain for this specific scenario just by following a PSR-4 standard!

In the next parts, we will look more into the migrated codebase and migration process itself.

Don't you have a PSR-4 compatible application yet? Let us help you achieve this great success too!

]]>
https://getrector.org/blog/2020/03/10/upgrading-glami-to-psr-4-what-why-how Tue, 10 Mar 2020 00:00:00 +0000 Tue, 10 Mar 2020 00:00:00 +0000 https://getrector.org/blog/2020/03/10/upgrading-glami-to-psr-4-what-why-how#disqus_thread
<![CDATA[ How to install Rector despite Composer Conflicts ]]> https://getrector.org/blog/2020/01/20/how-to-install-rector-despite-composer-conflicts Rector is a composer package. If you install it, it has to meet install requirements conditions.
But how can you upgrade your Symfony 2.8, when Rector needs at least Symfony 4.4?

]]>
Do you have the most modern code base on PHP 7.2 and Symfony 4.4? No?

Then you've probably experienced this before:

composer install rector/rector --dev

That's sad :(

Rector needs the same access to your code as PHPStan, so they can use reflection to get metadata and vendor class analysis. Classes must be unique and autoloaded. Few alternatives might help with version install config.

Alternative Solutions that Do Not Work

1. composer-bin-plugin

This composer plugin will help install the dependency to own directory with own dependencies - e.g. vendor-bin/rector/vendor/bin/rector (instead of vendor/bin/rector). It will allow us to install Rector, but not to use it. Why?

There will be conflicts between same-named class in your project and the one in Rector's vendor:

// version 2.8

class YourFavoriteFrameworkClass
{
    public function process($name)
    {
    }
}

vs

// version 4.4

class YourFavoriteFrameworkClass
{
    public function process(string $name, $value)
    {
    }
}

Now it depends on luck - which classes get loaded first.

Often you run out of luck and get incompatible class error:

Warning: Declaration of Implementer::process($name, $newArgument) should be compatible with
YourFavoriteFrameworkClass44::process(string $name) in ...

Also, Rector now doesn't know, if you're using version 2.8 (yours) or 4.4 (its), and if what versions and types it should prefer.

2. Docker

Thanks to Jan Mikes Rector also runs in Docker. Docker is promoted as a tool to isolate dependencies, right?

Well, the truth is somewhere half-way. Do you have PHP 5.6? Rector needs at least 7.1 at version 0.5 and 7.2 at version 0.6. Docker allows you to run Rector on older PHP. That's a good thing if you need to upgrade PHP.

But does it solve the same-named classes problem in your project and Rector? No.


Super Hard but only Working Solution

Now we know, the real problem is same-named classes.

The question is, how can we make name difference between these 2 classes:

// your code

namespace Symfony\Component\Console;

class Command
{

}
// Rector code

namespace Symfony\Component\Console;

class Command
{

}

In short:

Symfony\Component\Console\Command
Symfony\Component\Console\Command

Any ideas?


Well, one of them probably... has to be named differently?

Symfony\Component\Console\Command
RectorsVendor\Symfony\Component\Console\Command

Yes. That's the only way (we know of), how to make your code autoloadable and Rector's code unique. So every time Rector uses Symfony\Component\Console\Command internally, e.g. to call ProcessCommand, it will actually use prefixed RectorsVendor\Symfony\Component\Console\Command.

This way, there will never be two same-named classes anymore. Problem solved... well, in theory.

Community Effort

How do we prefix all these classes in Rector's vendor and replace their names in Rector's code?

Luckily, there is a humbug/php-scoper tool to help us. It doesn't have much clear API and provides unclear error messages like "missing index.php", but if you find the magical combination, it does the right job.

The main pain point was the Symfony application. Symfony was never designed to be prefixed and downgraded. And we don't mean one Symfony class, but Symfony application with dependency injection, PSR-4 autodiscovery, YAML, configs, and local packages.

Thanks to friendly kick from John Linhart from Mautic and 3 co-work weekend on this issue only, we got this working in the early December 2019.

Introducing rector/rector 0.11

After a month of testing and fixing bugs, we're proud to announce prefixed and downgraded rector/rector.

Rector is now prefixed and published as per-commit into distributoin repository repository.


You can install it despite having conflicting packages:

composer require rector/rector --dev


This opens migrations of legacy projects to brand new opportunities.

Now, even Symfony 1.4, Nette 0.9, or Laravel 3 on PHP 5.3 can be instantly upgraded.

]]>
https://getrector.org/blog/2020/01/20/how-to-install-rector-despite-composer-conflicts Mon, 20 Jan 2020 00:00:00 +0000 Mon, 20 Jan 2020 00:00:00 +0000 https://getrector.org/blog/2020/01/20/how-to-install-rector-despite-composer-conflicts#disqus_thread