Rector Blog https://getrector.org/ Rector Blog about Legacy Code Migrations Wed, 28 Jul 2021 11:07:35 +0000 Wed, 21 Jul 2021 00:00:00 +0000 <![CDATA[ Rector for PHP 5.6 Native ]]> https://getrector.org/blog/rector-for-php56-native Currently, you can install Rector on PHP 7.1-8.0 with native support. There is no need for Docker, syntax hacking, or running Rector from a different PHP version than your project. Rector is now downgraded from PHP 8.0 to PHP 7.1.

To get there, we had to write over 40 downgrade rules. From the first downgrade rule in October 2020 to release in March 2021 it took us 6 months of extensive work.

]]>
Projects Stuck on PHP 5.6

We want to take it a step further and make Rector accessible to those who need it most. Upgrade from PHP 7.1 to 8.0 is a piece of cake compared to 5.6 to 7.0. Projects stuck on 5.6 have huge troubles to upgrade because there are no native automated supports. We want to change it to this:

php --version
# PHP 5.6

composer require rector/rector --dev

vendor/bin/rector process src
# just works... How cool is that?


We'll be honest with you. We need your help to get there!


Sponsorware

We want to release this package as a sponsorware.

Sponsorware is a release strategy for open-source software that enables developers to be compensated for their open-source work with fewer downsides than traditional open-source funding models.

Why sponsorware?

  • Downgrading is not a simple process. To make it work correctly, we have to explore our code for features that we have to downgrade. That is the easy part. The hard part is every single vendor dependency and also their dependencies.

  • Valid downgrade also includes PHP-internal constants, the fallback of newer functions, and syntax order changes. To make this right and provide a Rector that gets you out of PHP 5.6 beyond, we need your help.

  • We also want to make sure there is enough interest in the PHP community worth the effort to introduce this version of Rector.

1. Early-Access Threshold

At the time we publish this, we have 22 sponsors.

When we reach 45 sponsors, we'll release the rector-php56 version in a matter of 4 weeks for testing and give exclusive access to all sponsors at the time of release and onward. Testing and bug fixes included.

2. Open-Source Threshold

When we reach 70 sponsors, we'll release rector-php56 as an open-source package under MIT license.

The repository for release is here: rectorphp/rector-php5.


You can sponsor us here.


Thank you!

]]>
https://getrector.org/blog/rector-for-php56-native Wed, 21 Jul 2021 00:00:00 +0000 Wed, 21 Jul 2021 00:00:00 +0000 https://getrector.org/blog/rector-for-php56-native#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 Nette, CakePHP, 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:

// rector.php
use Symfony\Component\DependencyInjection\Loader\Configurator\ContainerConfigurator;
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;

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

    $services->set(ParamTypeFromStrictTypedPropertyRector::class);
    $services->set(ReturnTypeFromReturnNewRector::class);
    $services->set(ReturnTypeFromStrictTypedPropertyRector::class);
    $services->set(ReturnTypeFromStrictTypedCallRector::class);
    $services->set(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[ Introducing composer.json Rector Rules ]]> https://getrector.org/blog/2021/01/25/introducing-composer-json-rector-rules In the last post we had a closer look at Nette 3.1 changes in diffs. That was the first upgrade with Rector ever, where you don't have to touch the composer.json file.
At all?

]]>
What is the first step you do if you want to upgrade a package? You change composer.json and update it. Easy, right?


What Framework Upgrade means for composer.json?

This simple sentence can develop into multi-step task:

  • What is the next version of this package?
  • Is it a minor or major version change?
  • Should I run Rector first or update composer.json first?
  • What are the exact version of dependencies that support the next version of my favorite framework?

The last step can take a whole week of detailed detective work to figure out.


In 2012, when the Composer created, most frameworks could be updated with one line change:

 {
     "require": {
-         "nette/nette": "^1.0"
+         "nette/nette": "^2.0"
     }
 }

Then we run composer update, and we're done. Well, at least for the composer.json update.


How is it now? Today we are using monorepos, split packages, and small packages with narrow features. One package is handling dependency injection; one package is for forms, another for translations.

What Packages and Versions we need for Nette 3.1?

Let's look at the Nette 3.1 upgrade. Nette package tagging is not standard monorepo-like, where 1 version is the same for all packages at a certain point in time.

Instead, Nette uses per-repository tagging. When nette/application is one version 3.1, the nette/forms can be 2.9.

How do we find out which packages are part of the upgrade for Nette "3.1"?

Trial and error of careful looking into 20+ repositories in Nette GitHub organization. In the end, we end up with something like this:

 {
     "require": {
-        "nette/application": "^3.0",
+        "nette/application": "^3.1",
-        "nette/di": "^2.4",
+        "nette/di": "^3.0",
-        "nette/http": "^3.0",
+        "nette/http": "^3.1",
-        "nette/utils": "^3.0",
+        "nette/utils": "^3.2",
-        "latte/latte": "^2.3"
+        "latte/latte": "^2.9"
     }
 }

Oh, do you use 3rd party packages?

 {
     "require": {
-        "contributte/console": "^0.8",
+        "contributte/console": "^0.9",
-        "contributte/event-dispatcher": "^0.7",
+        "contributte/event-dispatcher": "^0.8",
-        "nettrine/annotations": "^0.5"
+        "nettrine/annotations": "^0.7"
     }
 }

That is so much work that every developer has to figure out over and over again. The combination of packages is different for every project, but the changed composer.json packages always have the same values.


That's when Lulco came to Rector repository with a question:

"How can we automate this?"


Introducing Composer Rector

Many rules in Rector allow configuration via config. There you can set values in an array or value object. E.g., class rename:

// rector.php

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',
            ],
        ]]);
};


What if something similar was possible for composer.json? Something we could configure to do typical work for us:

  • upgrade this package to this version
  • replace this package with another
  • remove this package

Those Rector rules could be named like:

  • ChangePackageVersionComposerRector
  • ReplacePackageAndVersionComposerRector
  • RemovePackageComposerRector

They would be easily configurable:

// rector.php

use Rector\Composer\Rector\ChangePackageVersionComposerRector;
use Rector\Composer\Rector\RemovePackageComposerRector;
use Rector\Composer\ValueObject\PackageAndVersion;
use Symfony\Component\DependencyInjection\Loader\Configurator\ContainerConfigurator;
use Symplify\SymfonyPhpConfig\ValueObjectInliner;

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

    $services->set(ChangePackageVersionComposerRector::class)
        ->call('configure', [[
            ChangePackageVersionComposerRector::PACKAGES_AND_VERSIONS => ValueObjectInliner::inline([
                new PackageAndVersion('nette/application', '^3.1'),
                new PackageAndVersion('nette/di', '^3.0'),
                new PackageAndVersion('nette/http', '^3.1'),
                new PackageAndVersion('nette/utils', '^3.2'),
                new PackageAndVersion('contributte/console', '^0.9'),
                new PackageAndVersion('nettrine/annotations', '^0.7'),
            ]),
        ]]);

    $services->set(RemovePackageComposerRector::class)
        ->call('configure', [[
            RemovePackageComposerRector::PACKAGE_NAMES => ['nette/component-model', 'nette/neon'],
        ]]);
};

Change only Found Packages

They also respect existing composer.json. E.g., if the nette/di is not there, it would not be added with a newer version but skipped.

In the end, you only run Rector and let it upgrade both your PHP code and your composer.json:

vendor/bin/rector process


How does such Set Look for Nette 3.1?

Pretty neat. See NETTE_31 set for full setup.



Happy coding!

]]>
https://getrector.org/blog/2021/01/25/introducing-composer-json-rector-rules Mon, 25 Jan 2021 00:00:00 +0000 Mon, 25 Jan 2021 00:00:00 +0000 https://getrector.org/blog/2021/01/25/introducing-composer-json-rector-rules#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 Symfony\Component\DependencyInjection\Loader\Configurator\ContainerConfigurator;

return static function (ContainerConfigurator $containerConfigurator): void {
    $containerConfigurator->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\Core\Configuration\Option;
use Symfony\Component\DependencyInjection\Loader\Configurator\ContainerConfigurator;
use Rector\SymfonyCodeQuality\Rector\Attribute\ExtractAttributeRouteNameConstantsRector;

return static function (ContainerConfigurator $containerConfigurator): void {
    $services = $containerConfigurator->services();
    $services->set(ExtractAttributeRouteNameConstantsRector::class);

    $parameters = $containerConfigurator->parameters();
    $parameters->set(Option::AUTO_IMPORT_NAMES, true);
 };

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 Symfony\Component\DependencyInjection\Loader\Configurator\ContainerConfigurator;

return static function (ContainerConfigurator $containerConfigurator): void {
    $containerConfigurator->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 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' => '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 Symfony\Component\DependencyInjection\Loader\Configurator\ContainerConfigurator;

 return static function (ContainerConfigurator $containerConfigurator): void {
+    $containerConfigurator->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?

]]>
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 create custom Symplify\SymfonyPhpConfig\ValueObjectInliner class:

// rector.php
use Symplify\SymfonyPhpConfig\ValueObjectInliner;

$services->set(FuncCallToStaticCallRector::class)
    ->call('configure', [[
        FuncCallToStaticCallRector::FUNC_CALLS_TO_STATIC_CALLS => ValueObjectInliner::inline([
            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'),
        ])
    ]]);

Do you want to sure it too?

composer require symplify/symfony-php-config

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)
    ->call('configure', [[
        RenameClassRector::OLD_TO_NEW_CLASSES => [
            '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)
    ->call('configure', [[
        RenameClassRector::OLD_TO_NEW_CLASSES => [
            '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 function Rector\SymfonyPhpConfig\inline_value_objects;
use Symfony\Component\DependencyInjection\Loader\Configurator\ContainerConfigurator;

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

    $services->set(RenameClassConstantRector::class)
        ->call('configure', [[
            RenameClassConstantRector::CLASS_CONSTANT_RENAME => inline_value_objects([
                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 config:
use Rector\Set\ValueObject\SetList;
use Symfony\Component\DependencyInjection\Loader\Configurator\ContainerConfigurator;

return static function (ContainerConfigurator $containerConfigurator): void {
    $containerConfigurator->import(SetList::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