Our rule in this example is: `Rector\Privatization\Rector\Class_\FinalizeClassesWithoutChildrenRector`
This rule's job is to add `final` to every class that has no children and is not a Doctrine entity = everywhere it can without breaking our code.
-----
```
### B. The Code Should Be Skipped
```bash
```
In this particular case, the code should change - `final` should be added so that the test fixture would look like this:
```php
-----
```
- The closing `?>` is there for slightly better PHPStorm.
- The PSR-4 namespace is there to make each class unique because the test classes are loaded to an analysis by reflection and must be unique
- The file name conventions => class is `add_final.php.inc` => `AddFinal` class
Run PHPUnit with the test file to confirm:
```bash
vendor/bin/phpunit rules-tests/Privatization/Rector/Class_/FinalizeClassesWithoutChildrenRector/FinalizeClassesWithoutChildrenRectorTest.php
```
To run only the single test fixture, add `--filter test#X`, where X is the fixture's order number.
```bash
vendor/bin/phpunit rules-tests/Privatization/Rector/Class_/FinalizeClassesWithoutChildrenRector/FinalizeClassesWithoutChildrenRectorTest.php --filter test#4
```
If PHPUnit fails, you've successfully added a test case! :)
Thank you
-----------------------------------------
## Advanced: How Rector Works
## 1. Finds all files and Load Configured Rectors
- The application finds files in the source code you provide and registered Rectors - from `--config` or local `rector.php`
- Then it iterates all found files and applies relevant Rectors to them.
- A *Rector* in this context is 1 single class that modifies 1 thing, e.g. changes the class name
## 2. Parse and Reconstruct 1 File
The iteration of files, nodes and Rectors respects this lifecycle:
```php
nodes
/** @var Parser $phpParser */
$nodes = $phpParser->parse(file_get_contents($fileInfo->getRealPath()));
// nodes => 1 node
foreach ($nodes as $node) { // rather traverse all of them
/** @var PhpRectorInterface[] $rectors */
foreach ($rectors as $rector) {
foreach ($rector->getNodeTypes() as $nodeType) {
if (is_a($node, $nodeType, true)) {
$rector->refactor($node);
}
}
}
}
}
```
### 2.1 Prepare Phase
- Files are parsed by [`nikic/php-parser`](https://github.com/nikic/PHP-Parser), 4.0 that supports writing modified tree back to a file
- Then nodes (array of objects by parser) are traversed by `StandaloneTraverseNodeTraverser` to prepare their metadata, e.g. the class name, the method node the node is in, the namespace name etc. added by `$node->setAttribute('key', 'value')`.
### 2.2 Rectify Phase
- When all nodes are ready, the application iterates on all active Rectors
- Each node is compared with `$rector->getNodeTypes()` method to see if this Rector should do some work on it, e.g. is this class name called `OldClassName`?
- If it doesn't match, it goes to next node.
- If it matches, the `$rector->refactor($node)` method is called
- Active Rector change everything they have to and return changed nodes
### 2.2.1 Order of Rectors
- Nodes to run rectors are iterated in the node traversal order.
E.g. rectors for `Class_` node always run before rectors for `ClassMethod` in one class.
- Rectors are run by the natural order in the configuration, meaning the first
in the configuration will be run first.
E.g. in this case, first the `@expectedException` annotation will be changed to a method,
then the `setExpectedException` method will be changed to `expectedException`.
```php
withRules([ExceptionAnnotationRector::class])
->withConfiguredRule(Rector\Renaming\Rector\MethodCall\RenameMethodRector::class, [
new MethodCallRename('PHPUnit\Framework\TestClass', 'setExpectedException', 'expectedException'),
new MethodCallRename('PHPUnit\Framework\TestClass', 'setExpectedExceptionRegExp', 'expectedException'),
]);
```
### 2.3 Save File/Diff Phase
- When work on all nodes of 1 file is done, the file will be saved if it has some changes
- Or if the `--dry-run` option is on, it will store the *git-like* diff thanks to [GeckoPackages/GeckoDiffOutputBuilder](https://github.com/GeckoPackages/GeckoDiffOutputBuilder)
- Then Rector will go to the next file
## 3 Reporting
- After this, Rector displays the list of changed files
- Or with `--dry-run` option the diff of these files
-----------------------------------------
## Advanced: Custom Rule
First, make sure it's not covered by [any existing Rectors](/find-rule).
Let's say we want to **change method calls from `set*` to `change*`**.
```diff
setPassword('123456');
+$user->changePassword('123456');
```
💡
Since Rector 0.19.3 you can generate basic structure of your custom rule with this command:
```bash
vendor/bin/rector custom-rule
```
## 1. Create a New Rector and Implement Methods
Create a class that extends [`Rector\Rector\AbstractRector`](https://github.com/rectorphp/rector/blob/main/src/Rector/AbstractRector.php). It will inherit useful methods e.g. to check node type and name. See the source (or type `$this->` in an IDE) for a list of available methods.
```php
>
*/
public function getNodeTypes(): array
{
// what node types are we looking for?
// pick from
// https://github.com/rectorphp/php-parser-nodes-docs/
return [MethodCall::class];
}
/**
* @param MethodCall $node
*/
public function refactor(Node $node): ?Node
{
$methodCallName = $this->getName($node->name);
if ($methodCallName === null) {
return null;
}
// we only care about "set*" method names
if (! str_starts_with($methodCallName, 'set')) {
// return null to skip it
return null;
}
$newMethodCallName = preg_replace(
'#^set#', 'change', $methodCallName
);
$node->name = new Identifier($newMethodCallName);
// return $node if you modified it
return $node;
}
/**
* This method helps other to understand the rule
* and to generate documentation.
*/
public function getRuleDefinition(): RuleDefinition
{
return new RuleDefinition(
'Change method calls from set* to change*.', [
new CodeSample(
// code before
'$user->setPassword("123456");',
// code after
'$user->changePassword("123456");'
),
]
);
}
}
```
## File Structure
This is how the file structure for custom rule in your own project will look like:
```bash
/src
SomeCode.php
/utils
/rector
/src
/Rector
MyFirstRector.php
/tests
/Rector
/MyFirstRector
/Fixture
test_fixture.php.inc
/config
config.php
MyFirstRectorTest.php
rector.php
composer.json
```
## Update `composer.json`
We also need to load Rector rules in `composer.json`:
```json
{
"autoload": {
"psr-4": {
"App\\": "src"
}
},
"autoload-dev": {
"psr-4": {
"Utils\\Rector\\": "utils/rector/src",
"Utils\\Rector\\Tests\\": "utils/rector/tests"
}
}
}
```
After adding this to `composer.json`, be sure to reload Composer's class map:
```bash
composer dump-autoload
```
## 2. Register It
```php
withRules([
MyFirstRector::class
]);
```
## 3. Let Rector Refactor Your Code
The `rector.php` configuration is loaded by default, so we can skip it.
```bash
# see the diff first
vendor/bin/rector process src --dry-run
# if it's ok, apply
vendor/bin/rector process src
```
That's it!
-----------------------------------------
## Advanced: Custom Set Provider
Core sets like [Twig, Symfony, or Doctrine](/documentation/composer-based-sets) are based on specific versions of installed packages in `/vendor`:
```php
use Rector\Config\RectorConfig;
return RectorConfig::configure()
->withComposerBased(twig: true);
```
If you use Twig 2, Rector will load sets for Twig 2. Once you bump to Twig 3 in `composer.json`, Rector will automatically load new rules for Twig 3.
These sets are configured via classes that implement `SetProviderInterface`. Let's say you want to create a custom rule set for your Laravel community. Once set up, let us know, and we can add it to the Rector `withComposerBased()` method.
Let's take it step by step.
## 1. Create a custom `LaravelSetProvider` class that implements `SetProviderInterface`
```php
use Rector\Set\Contract\SetInterface;
use Rector\Set\Contract\SetProviderInterface;
use Rector\Set\ValueObject\Set;
final class LaravelSetProvider implements SetProviderInterface
{
/**
* @return SetInterface[]
*/
public function provide(): array
{
return [];
}
}
```
## 2. Add your `ComposerTriggeredSet` objects to the `provide()` method
What is the `ComposerTriggeredSet` object for? It connects to a specific package + version with a path to the rule set:
```php
namespace Rector\Set\ValueObject;
use Rector\Set\Contract\SetInterface
final class ComposerTriggeredSet implements SetInterface
{
public function __construct(
private string $groupName,
private string $packageName,
private string $version,
private string $setFilePath
) {
}
// ...
}
```
* The `$groupName` is key in `->withComposerBased()`:
* The `$packageName` is the composer package name.
* `$version` is the minimal version to trigger the set
* and `$setFilePath` is the path to the Rector config with rules as we know it
Let's add objects for Laravel 10 and 11:
```php
use Rector\Set\Contract\SetInterface;
use Rector\Set\Contract\SetProviderInterface;
use Rector\Set\ValueObject\ComposerTriggeredSet;
use Rector\Set\ValueObject\Set;
final class LaravelSetProvider implements SetProviderInterface
{
/**
* @return SetInterface[]
*/
public function provide(): array
{
return [
new ComposerTriggeredSet(
'laravel',
'laravel/framework',
'10.0',
__DIR__ . '/../../config/sets/laravel10.php'
),
new ComposerTriggeredSet(
'laravel',
'laravel/framework',
'11.0',
__DIR__ . '/../../config/sets/laravel11.php'
),
// ...
];
}
}
```
## 3. Enable the custom set provider in `rector.php`
```php
use Rector\Config\RectorConfig;
return RectorConfig::configure()
->withComposerBased(laravel: true);
```
If your parameter is missing, send us [a PR to enable it](https://github.com/rectorphp/rector-src/blob/main/src/Config/RectorConfig.php).
And we're done. Now, Rector will load the `laravel10.php` rules for Laravel 10 and `laravel11.php` rules for Laravel 11.
You can be creative with `SetListProvider` and define a rule for more specific packages like `illuminate/container`. That way, we handle complexity for Rector end-users, and all they have to do is add a single parameter to the `rector.php` config.
-----------------------------------------
## Advanced: Writing Tests For Custom Rule
Writing test for your custom rules will save you a lot of time in future debugging.
Rector provides a structured way of running your rules on different snippets of code
so you can validate that your rule works as expected in a variety of cases.
## Requirements
There are 2 composer packages to run tests for your custom rule:
* `phpunit/phpunit`: the testing framework
* `rector/rector`: this contains the `AbstractRectorTestCase` class to simplify test configuration
💡
Since Rector 0.19.3 you can generate basic structure of your custom rule with this command:
```bash
vendor/bin/rector custom-rule
```
## File Structure
Here is an example file structure for testing:
```bash
/src
/Rector
MyFirstRector.php
/tests
/Rector
/MyFirstRector
/Fixture
test_fixture.php.inc
skip_rule_test_fixture.php.inc
/config
config.php
MyFirstRectorTest.php
```
The files in `tests/Rector/MyFirstRector` will be explained below.
### MyFirstRectorTest.php
This class handles the heavy lifting of preparing Rector & running it against your test cases.
The usual structure of the test class is as follows:
```php
doTestFile($filePath);
}
public static function provideData(): Iterator
{
return self::yieldFilesFromDirectory(__DIR__ . '/Fixture');
}
public function provideConfigFilePath(): string
{
return __DIR__ . '/config/configured_rule.php';
}
}
```
You can see that there are 3 functions in this test class:
- `public function test(string $filePath): void`:
- This method is to help PHPUnit detect this test
- For `$filePath`, we use a [PHPUnit DataProvider](https://phpunit.readthedocs.io/en/10.0/writing-tests-for-phpunit.html#data-providers)
- This triggers a run for every test file in your Fixtures directory
- `public static function provideData(): Iterator`:
- As stated above, this is a PHPUnit DataProvider
- Using `self::yieldFilesFromDirectory` it iterates over all test cases you provided
- By default this only picks up files ending on `.php.inc`, see the `AbstractRectorTestCase` to see how you can change this.
- See "Fixtures/*.php.inc" below for the files that are expected in the `/Fixture` directory
- In the example file structure earlier, this would result in `test_fixture.php.inc` and `skip_rule_test_fixture.php.inc`
- `public function provideConfigFilePath(): Iterator`:
- This should return a `rector.php`-styled file configuring the minimal set of rules needed to run the tests (including `MyFirstRectorRule`)
- See "config/config.php" below for an example
### config/config.php
This is a `rector.php`-styled file. If your rule is not configurable, it will look like this:
```php
use Rector\Config\RectorConfig;
use Package\MyFirstRector;
return RectorConfig::configure()
->withRules([
MyFirstRector::class,
]);
```
This essentially reflects how you would use your rule in real life.
### Fixture/*.php.inc
As mentioned in `MyFirstRectorTest.php`, these are the snippets of code on which Rector will run your custom rule.
To prevent automated tools from picking up those snippets, you need to add an extra suffix `.inc` (so `example.php` should be `example.php.inc`).
There are two options for every test file: Either the snippet should be changed by your rule or it should stay the same.
#### Fixture/test_fixture.php.inc
Assuming your rector rule changes `$user->setPassword('123456')` to `$user->changePassword('123456')`,
this is an example snippet:
```php
setPassword($password);
}
}
?>
-----
changePassword($password);
}
}
?>
```
This file contains a "before" and "after" situation, separated by exactly 5 dashes: `-----`.
The `AbstractRectorTestCase` detects the `-----`
and will run the rules configured in `config/config.php` on the snippet of code before the `-----`
and assert that the changed file exactly matches the snippet of code after the `-----`.
#### Fixture/skip_rule_test_fixture.php.inc
There are cases where you might want to check that your rule is **not** applied.
The file structure is very similar to the `Fixture/test_fixture.php.inc` with 1 exception:
It only contains a "before" situation.
It is not necessary to prefix the fixture with `skip`, but doing so makes it easy to see that no changes are expected.
Example snippet:
```php
isCorrectPassword($password);
}
}
?>
```
As you see, there is no `-----`, so `AbstractRectorTestCase` will run the rules in `config/config.php` on the snippet
and assert that there are no changes applied to the snippet.
## Running your tests
You can run your tests with `vendor/bin/phpunit tests`
-----------------------------------------
## Advanced: Creating Node Visitor
You can add new NodeVisitors to decorate nodes with attributes before being used by one or more rules. This is useful if you need to add metadata to nodes, that are known only in higher nodes. E.g. if `Expr` is part of assign:
```php
$value = 100;
```
First we create a node visitor, that implements `Rector\Contract\PhpParser\DecoratingNodeVisitorInterface`:
```php
var->setAttribute(self::IS_PART_OF_ASSIGN, true);
$node->expr->setAttribute(self::IS_PART_OF_ASSIGN, true);
return null;
}
}
```
Never modify any nodes in the visitor as it might break node tree traversal. Only add attributes to nodes.
### Register in Rector
Then, register in the `rector.php` config:
```php
registerDecoratingNodeVisitor(ScopeResolverNodeVisitorInterface::class);
```
If you're adding similar node visitor to Rector codebase, add class to `Rector\DependencyInjection\LazyContainerFactory::DECORATING_NODE_VISITOR_CLASSES` to register it.
Then, you can check the attribute in any Rector rule:
```php
fianl class SomeRector extends AbstractRector
{
public function refactor(Node $node): ?Node
{
if ($node->getAttribute(AssignAwareNodeVisitor::IS_PART_OF_ASSIGN)) {
// we know node is part of assign
}
// ...
return null;
}
}
```
### Use Existing Attribute Keys
Rector provides couple such node visitors out of the box. You can use these attributes already.
To find out list of attributes you can use, see `IS_*` constants in
[`AttributeKey` class](https://github.com/rectorphp/rector-src/blob/main/src/NodeTypeResolver/Node/AttributeKey.php).
-----------------------------------------
## Advanced: Run on PHP 5.3
Rector requires PHP 7.2 as min version. But what if you want to upgrade much older project, like PHP 7.0 or even 5.3?
The trick is in the separate Rector installation in own directory.
## 1. Install Rector
Create a directory parallel to your project, and install Rector there:
```bash
mkdir rector-standalone
cd rector-standalone
composer require rector/rector --dev
```
Now you have following file structure:
```bash
/your-project
/rector-standalone
```
## 2. Setup PHP
Make sure you're using PHP 7.2+, so you can run Rector. It doesn't matter your project can handle only PHP 5.3, as Rector uses static parsing of code and never runs your project.
```bash
php --version
# PHP 7.2
```
## 3. Run Rector
Now move to `/your-project` and run Rector in it:
```bash
cd /your-project
php ../rector-standalone/vendor/bin/rector
```
It will create a `rector.php` config in your project directory:
```bash
/your-project/rector.php
```
Tune the config to fit your needs. We typically add the current project PHP version set, e.g.:
```php
withSets([
SetList::PHP_53,
// and slowly level up
// SetList::PHP_54,
// SetList::PHP_55,
]);
```
Now run Rector and see the diffs it suggests:
```bash
php ../rector-standalone/vendor/bin/rector --dry-run
```
Ready to go? Let's run Rector to refactor your code:
```bash
php ../rector-standalone/vendor/bin/rector
```
That's it!
Next step would be pull-request, merge and then bump to `PHP_54` set.
-----------------------------------------
## Rector Family Tools: Swiss Knife
In 2024 we published an in-house tool that we used only internal for years.
It can help you with various everyday tasks that are outside Rector's scope, but are connected with upgrades and code quality.
### Install
Swiss Knife package is scoped and downgraded. It requires **PHP 7.2+** and can be installed on any legacy project.
Install with Composer:
```bash
composer require rector/swiss-knife --dev
```
## Features
It helps you to:
* **finalize all classes** except entities, mocked classes, classes with children, YAML-defined etc.
* **privatize class constant**, that are never used outside the class
* detect commented code creep
* **convert random namespaces to PSR-4** - updates `composer.json`, class names and use imports
* spot accidental git conflicts in CI
* turn any JSON file to **nice and human-readable format**
* search all PHP files with quick regex
* generate [Symfony 5.3](https://symfony.com/blog/new-in-symfony-5-3-config-builder-classes) configs
* split huge Symfony configs to per-package in `/packages/` folder
* spot fake-traits
...and more
See [Swiss knife Github repository](https://github.com/rectorphp/swiss-knife) for full documentation.
-----------------------------------------
=========================================
# Blog posts
## Upgrade to PHPUnit 12.5 in 7 Diffs
Perex: PHPUnit 12 was released a year ago, but only PHPUnit 12.5 released in December 2025 includes valuable features that are worth it.
The most important change, that will affect your code, is that mocks are now much stricter. There are also stubs, a mock that does nothing. How do you spot them and separate them?
Curious how to get from 4000 notices to under 100 in 7 diffs? Read on.
What is the difference between a mock and a stub? You did not have to care until PHPUnit 12.5, but now you do.
Why? Because PHPUnit now complains about their misuse very verbosely. There is no way to ignore it:
There is more precise definition in the PHPUnit docs, but in plain English:
"What is a difference between a mock and a stub?"
* **A mock** is a fake class that has expectations about being called or not being called,
```php
$someMock = $this->createMock(SomeClass::class);
$someMock->expects($this->once())
->method('someMethod')
->willReturn(100);
```
Here we expect the `someMethod` to be called. PHPUnit will crash with error otherwise.
* **A stub** is also a fake class, but it does not do anything at all.
We can use it to make comply with constructor requirements:
```php
$someClass = new SomeClass($this->createStub(SomeDependency::class));
```
We can also use it to assert the same object is used on a getter call later:
```php
$request = $this->createStub(Request::class);
$requestStack = new RequestStack($request);
$this->assertSame($request, $requestStack->getCurrentRequest());
```
This leads us to the first and simplest change we can make.
## 1. Use `createStub()` instead of `createMock()` in arguments
The first cases are as simple as:
```diff
$someClass = new SomeClass(
- $this->createMock(SomeDependency::class)
+ $this->createStub(SomeDependency::class)
);
```
Also variable assigns:
```diff
-$someDependency = $this->createMock(SomeDependency::class);
+$someDependency = $this->createStub(SomeDependency::class);
$someClass = new SomeClass($someDependency);
```
Or coalesce directly in the argument:
```diff
$someClass = new SomeClass(
- $someInput ?? $this->createMock(SomeDependency::class),
+ $someInput ?? $this->createStub(SomeDependency::class),
);
```
But also property fetches without any expectations:
```diff
protected function setUp()
{
- $this->someDependency = $this->createMock(SomeDependency::class);
+ $this->someDependency = $this->createStub(SomeDependency::class);
}
public function test()
{
$someClass = new SomeClass($this->someDependency);
}
```
## 2. Inline once-used Mocks Property to a Variable
This is not a change in PHPUnit 12.5 itself, but it helps with the changes that come with it. During the upgrade, I've noticed some properties are used just once.
Properties are not variables for one reason: to be used across multiple methods. Let us fix that:
```diff
-private MockObject $someDependency;
protected function setUp()
{
- $this->someDependency = $this->createMock(SomeDependency::class);
}
public function test()
{
- $someClass = new SomeClass($this->someDependency);
+ $someClass = new SomeClass($this->createStub(SomeDependency::class));
}
```
We have less code to read for us and GPT, and also can move to `createStub()` without any doubts.
## 3. Remove never used isolated mocks and dead code
Speaking of dead code, the mocks to stub narrowign also surfaces another issue: never used mocks that live on their own island.
PHPUnit being more stricter now helps us find code that was never evaluated and only taking our reading space.
```php
$this->createMock(SomeClass::class)
->method('someMethod')
->with($this->isInstanceOf(InputArgument::class))
->willReturn(100);
```
What is wrong with this code snippet, apart from being a stub? It is never used. We created it, but we never assigned it to a variable, nor property feath, nor argument of a method call.
It is dead code. Remove it:
```diff
-$this->createMock(SomeClass::class)
- ->method('someMethod')
- ->with($this->isInstanceOf(InputArgument::class))
- ->willReturn(100);
```
Beware, this can be as complex as a well defined and typed property... that is never used. Dead code, remove it:
```diff
-private MockObject $mockProperty;
protected function setUp(): void
{
- $this->mockProperty = $this->createMock(\stdClass::class);
- $this->mockProperty->expects($this->once())
- ->method('someMethod')
- ->willReturn('someValue');
}
```
## 4. From `$this->any()` to explicit expectations
PHPUnit now also deprecated used of `$this->any()` expectations. This is a wise choice, as it effectively says "we expect 0, 1, or any number of occurrences". This code as well could be removed.
Following code snippets have the same meaning:
```php
$someClass = $this->createMock(SomeClass::class);
$someClass->expects($this->any())
->method('someMethod')
->willReturn(100);
// same as
$someClass
->method('someMethod')
->willReturn(100);
```
Both will be most reported by PHPUnit as stubs. They have 0 expectations (among other numbers). So how do we fix that? Change we used before is not enough and will not work here:
```diff
-$someClass = $this->createMock(SomeClass::class);
+$someClass = $this->createStub(SomeClass::class);
```
We have to be honest here, and it might require to understand the code.
* Is it a dummy method defined in `setUp()` method, in case it will be called any further in the codebase?
* Is it implicit `$this->any()`, just because we forgot to add explicit number?
The most common case in codebases I work with was the second one:
```diff
-$someClass->expects($this->any())
+$someClass->expects($this->atLeastOnce())
->method('someMethod')
->willReturn(100);
```
But what about the `setUp()` method? Do we have to now go through all the code and inline all the properties? This hurts just writing it. This gets us to our next change:
## 5. Add `#[AllowMockObjectsWithoutExpectations]` for optional setUp mocks
It's perfectly reasonable to use `setUp()` method to create mock properties that may or may not be used in one of the test method later:
```php
private SomeObject $someDependency;
protected function setUp(): void
{
$this->someDependency = $this->createMock(SomeDependency::class)
// implicit ->expects($this->any())
->method('someMethod')
->willReturn(100);
}
public function testUsing()
{
$someClass = new SomeClass($this->someDependency);
// ...
}
public function testNotUsing()
{
$someClass = new SomeClass(new AnotherDependnecy());
// ...
}
```
Here we have one mocked object as a property with *any* expectations. Then there are two test methods. The first one uses the mock as a mock.
The second test method does not, so from its point of view it is a stub.
(Also, another method can be using the property, but never calling the mocked method, so it's a stub as well).
An attribute to the rescue!
```diff
use PHPUnit\Framework\TestCase;
+use PHPUnit\Framework\Attributes\AllowMockObjectsWithoutExpectations;
+#[AllowMockObjectsWithoutExpectations]
final class SomeTest extends TestCase
{
private SomeObject $someDependency;
// ...
}
```
This attribute will silence the notices about stubs in this test class.
We could use it on every case above, yes. But that would prevent us from obvious fixes and push the technical debt deeper under the rug with a hole under our apartment.
## 6. Cover vendor `*TestCase` classes and data providers
There are two more cases where the `#[AllowMockObjectsWithoutExpectations]` attribute is needed and makes sense.
We use a 3rd party test case class, that defines its "any" expectations for a reason. They might be used, or not. Depends on how we write the test.
```diff
use Symfony\Component\Form\Test\TypeTestCase;
+#[AllowMockObjectsWithoutExpectations]
final class SomeTest extends TypeTestCase
{
// ...
}
```
The next is a test method that uses a data provider. The data provider usually tests edge case values that may or may not trigger a method call:
```diff
use PHPUnit\Framework\TestCase;
+#[AllowMockObjectsWithoutExpectations]
final class SomeTest extends TestCase
{
#[DataProvider('provideData')]
public function test($input)
{
$someClass = $this->createMock(SomeClass::class);
$someClass
// implicit $this->any() here
->expects($this->atLeastOnce())
->method('someMethod')
->willReturn(100);
// ...
}
public static function provideData(): iterable
{
// ...
}
}
```
## 7. Move from object mocking to real objects
"The best mock is no mock at all"
Before we even started the PHPUnit upgrade, we first eliminated the obvious cases that don't need any mocking at all.
We looked for plain objects, DTOs, value objects, entities and documents and replaced them with real, natively typed objects.
It can be as simple as using a simple `Request` directly:
```diff
use PHPUnit\Framework\TestCase;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\HttpFoundation\RequestStack;
final class SomeTest extends TestCase
{
public function test()
{
- $request = $this->createMock(Request::class);
+ $request = new Request();
- $requestStack = $this->createMock(RequestStack::class);
- $requestStack->expects($this->atLeastOnce())
- ->method('getMainRequest')
- ->willReturn($request);
+ $requestStack = new RequestStack($request);
$this->someMethod($requestStack);
}
}
```
Simple as that. Same applies for entity/document objects. Instead of hard-to-read getter mocks, use real objects with real values and types:
```diff
-$user = $this->createMock(User::class);
+$user = new User();
-$user->expects($this->any())
- ->method('getName')
- ->willReturn('Tomas');
+$user->setName('Tomas');
-$user->expects($this->any())
- ->method('getAge')
- ->willReturn($age);
+$user->setAge($age);
$service->process($user);
```
You can get the entity/document PHPStan spotter rule from `symplify/phpstan-rules` [here](https://github.com/symplify/phpstan-rules/blob/4b7aa41072850f9875b45272d263be3f4a183f40/src/Rules/Doctrine/NoDocumentMockingRule.php#L21).
Also, give a go to experimental [Rector rule](https://github.com/rectorphp/rector-phpunit/pull/629) that manages to change these mocks to entities. It is a real time saver.
## Enjoy the Automated Upgrade
We automated most of this work above, so you can let your agent handle the rest of the edge-cases. To get there, first enable the `phpunitCodeQuality` prepared set in your `rector.php`:
```php
use Rector\Config\RectorConfig;
return RectorConfig::configure()
->withPreparedSets(phpunitCodeQuality: true)
```
And run Rector:
```bash
vendor/bin/rector
```
Only then upgrade to PHPUnit 12.5 and run Rector with composer based set:
```php
use Rector\Config\RectorConfig;
return RectorConfig::configure()
->withComposerBased(phpunit: true)
```
And run Rector again:
```bash
vendor/bin/rector
```
It will automatically pick up the PHPUnit version and apply [the 12.0 set](https://github.com/rectorphp/rector-phpunit/blob/main/config/sets/phpunit120.php) and [the 12.5 set](https://github.com/rectorphp/rector-phpunit/blob/main/config/sets/phpunit125.php).
That's all folks. I hope you enjoyed this manually written post. I certainly enjoyed writing it.
As always, if you have improvement or bug report, head to Rector on Github and let us know.
Happy coding!
-----------------------------------------
## Introducing Safe and Progressive Strict Type Adoption Rule
Perex: PHP's `declare(strict_types=1)` directive is a powerful tool for preventing subtle bugs.
Yet most existing, mature projects don't use it consistently, if at all.
Why? Because automatically adding it to all your files at once will cause your application to explode: expect thousands of errors.
That leaves you fixing files by hand, one by one, and relying on team members to remember to add it to new files.
Without a safe and automated process, adoption hardly ever sticks.
Until now. The new `SafeDeclareStrictTypesRector` only adds strict types to files that are **already type-safe**, making safe, progressive adoption finally possible and preventing the file from becoming non-strict in the future.
*This is a guest post by [Caleb White](https://x.com/calebdw), an engineer and open source contributor who has contributed to Rector, PHPStan, the Laravel framework, and many other projects.*
This feature is still experimental. We look for your feedback in real projects.
Did something break when it shouldn't have? Let us know on GitHub.
## What is `strict_types` and Why Should You Care?
By default, PHP will coerce scalar values (`bool`, `float`, `int`, `string`, `null`) if there's a type mismatch.
For example, passing a `bool` to a function expecting a `string` will silently convert it to a string:
```php
Here are some important things to remember about `strict_types`:
- it is only defined for scalar type **declarations**, meaning it is only enforced for:
- arguments passed to functions, methods, callables, and constructors
- return statements
- property assignments
- the only exception is that an `int` value will pass a `float` type declaration
- it is **file-scoped**, meaning a file with `declare(strict_types=1)` can still `include` or `require` a file that is not strict
- it only affects **calling code**, so a function can be called non-strictly even if it is defined in a strict file
## A Real-World Horror Story
At my company, we learned this lesson the hard way.
Someone on our team was arguing against strict types and remarked, *"I've never had a bug that strict types would have prevented"*.
Less than a month later, the same individual refactored some code and accidentally introduced **exactly that kind of bug**.
💥 Our manufacturing numbers were suddenly wrong.
My manager and I spent two full days tracking it down.
No exceptions.
No errors or warnings in the logs.
The diff looked innocent enough, I reviewed the code from the last release multiple times before I finally found the culprit: **one method accepted an `int` instead of a `float`**. 😬
The refactor silently coerced a float length to an integer: e.g., `4.5` inches became `4` inches.
This cascaded through several calculations and ultimately wreaked havoc on our system.
If that one file had `declare(strict_types=1)`, we would have seen a `TypeError` immediately instead of burning four days of engineering time.
PHP has deprecated implicit float to integer type coercion since PHP 8.1 as it results in precision loss.
However, these deprecation warnings can still be ignored or sent to a log file which might not be seen until it is too late.
## The Adoption Problem
If strict types are so valuable, why doesn't everyone use them? The answer is simple: **enabling strict types in an existing, mature codebase is painful**.
Existing tools like the `DeclareStrictTypesRector` or PHP-CS-Fixer's `declare_strict_types` rule simply add `declare(strict_types=1)` to every file *indiscriminately*: an all-or-nothing approach.
On a large codebase, this can introduce **thousands of `TypeError`s** instantly.
No team wants to:
- Fix tons of type errors across hundreds or thousands of files
- Risk introducing production bugs by changing behavior
- Spend weeks or months on a "refactoring" that stakeholders see as delivering zero business value
So strict types adoption stalls.
Teams know they should use it, but the path to get there seems impossible.
And even with the best intentions, teams are rarely unified: some developers champion strict types, others are indifferent, and everyone occasionally forgets.
Relying on manual discipline across a whole team just doesn't scale.
## Enter `SafeDeclareStrictTypesRector`
The new `SafeDeclareStrictTypesRector` takes a fundamentally different approach: **it only adds `declare(strict_types=1)` to files that are already type-safe**.
Instead of blindly adding the declaration everywhere and breaking things, it analyzes each file to ensure that enabling strict types won't change any runtime behavior.
If a file would throw a `TypeError` after adding strict types, Rector skips it entirely.
### How It Works
For every file, the rule examines every place where type declaration coercion could occur:
1. **Function/method/callable/constructor calls**: are all arguments compatible with parameter types?
2. **Return statements**: are all returned values compatible with the declared return type?
3. **Property assignments**: are all assigned values compatible with typed properties?
For each of these, it uses PHPStan's type inference to check whether the actual value types are strictly compatible with the declared types.
Only if **every** type coercion point passes does Rector add the declaration.
```php
### Conservative by Design
The checker is intentionally conservative. If it can't be 100% certain a file is safe, it skips it:
- Dynamic method calls where the reflection can't be resolved? **Skip.**
- Argument unpacking with `...$var`? **Skip.**
- Mixed types that could be anything? **Skip.**
This means you might not get strict types added on every file that *could* have it, but **you'll never wind up with a broken file**.
## Progressive Adoption Made Possible
The real power of `SafeDeclareStrictTypesRector` is that it enables **progressive adoption**:
1. Run Rector: safe files get `declare(strict_types=1)` automatically
2. Those files are now protected from future type coercion bugs
3. PHPStan will catch any new violations, preventing those files from becoming non-strict again
4. Fix remaining files incrementally as time permits
5. Re-run Rector periodically to catch newly-safe files (preferably in a CI pipeline)
This rule also works in conjunction with other Rector rules.
Rector has rules that add parameter types, return types, and even cast arguments to the correct type.
As these rules run, files that were previously unsafe become safe.
Once a file is fully type-safe, `SafeDeclareStrictTypesRector` will automatically add the declaration.
This creates a virtuous cycle: Rector adds types and casts, files become strict-safe, and PHPStan prevents regressions.
Your codebase gradually becomes more type-safe over time with minimal manual effort.
In our enterprise application, the first run added `declare(strict_types=1)` to **over 1,500 files**: completely automatic and with zero risk of breakage.
That's 1,500 files now protected from the exact class of bug that cost us two days of debugging. 💪
## Try It Yourself
Add the rule to your Rector configuration:
```diff
withRules([
+ SafeDeclareStrictTypesRector::class,
]);
```
Then run Rector: watch as safe files automatically get `declare(strict_types=1)` while unsafe files are left untouched.
No breakage. No risk. Just automatic improvement.
## Conclusion
Strict types shouldn't be an all-or-nothing choice.
With `SafeDeclareStrictTypesRector`, you can adopt `declare(strict_types=1)` progressively, automatically, and safely.
Every file that gets strict types added is one more file protected from silent type coercion bugs.
Those bugs might cost you two days of debugging like they did for us, or they might cost you much more.
Start protecting your codebase today. 🛡️
-----------------------------------------
## Make PHPUnit tests Perfect in 15 Diffs
Perex:
Rector helps you improve PHP code, upgrade it to latest PHP version, make use of modern features and faster code structures. But did you know it can make your PHPUnit tests faster and easier to read?
New PHPUnit version have more precise and reliable asserts, but most people don't know about them. They make tests run faster and in case of failure, provide more clear error message you'll understand.
Rector can help you with that!
There are two main ways to keep your PHPUnit tests up-to-date and in perfect shape without any work.
## First: Use latest PHPUnit features
A year ago we introduced [Composer-version based sets](/blog/introducing-composer-version-based-sets). This feature:
* tells Rector to read your `composer.json`,
* detect your installed PHPUnit version
* and automatically pick up the sets that handle the upgrade to new features
To enable it, just add this line to your `rector.php` config:
```php
use Rector\Config\RectorConfig;
return RectorConfig::configure()
->withComposerBased(phpunit: true);
```
Setup once and forget. That's it. No need to list sets everytime new PHPUnit version is out. Rector now automatically applies PHPUnit upgrade sets, everytime you change version in `composer.json`:
```diff
{
"require-dev": {
- "phpunit/phpunit": "^12.5"
+ "phpunit/phpunit": "^13.0"
}
}
```
Run composer to install the new PHPUnit version. Then Rector to apply the upgrade:
```bash
composer update
vendor/bin/rector
```
That's it!
## Second: Use the best PHPUnit asserts and practices
Using new feature is first step to perfection, but there is more. Less code is better, and more precise assertion is better. But PHPUnit has so many different asserts, it's hard to keep up with the best ones to use in that particular situation.
### 1. Exact Assertions
In prehistorical past, there was only `assertTrue()` and `assertFalse()`. Now, PHPUnit has many more precise assertions you can use:
```diff
-$this->assertTrue(isset($anything["foo"]), "message");
+$this->assertArrayHasKey("foo", $anything, "message");
```
```diff
public function test()
{
$result = '...';
- $this->assertSame($result, 'expected');
+ $this->assertSame('expected', $result);
}
```
```diff
-$this->assertSame(true, $value);
+$this->assertTrue($value);
```
### 2. More human-readable mocks
```diff
- ->willReturnCallback(function (...$parameters) use ($matcher) {
- match ($matcher->getInvocationCount()) {
- 1 => $this->assertSame([1], $parameters),
- };
- });
+ ->with(1, $parameters);
```
```diff
$translator = $this->createMock('SomeClass');
$translator->expects($this->any())
->method('trans')
- ->will($this->returnValue('translated max {{ max }}!'));
+ ->willReturnValue('translated max {{ max }}!');
```
```diff
$this->createMock('SomeClass')
->method('someMethod')
- ->with($this->callback(function (array $args): bool {
- return true;
- }))
- ->willReturn(['some item']);
+ ->willReturnCallback(function (array $args): array {
+ return ['some item'];
+ });
```
Why mocks property for a whole tests, if it's used only once?
```diff
use PHPUnit\Framework\TestCase;
class SomeServiceTest extends TestCase
{
- private $someServiceMock;
-
public function setUp(): void
{
- $this->someServiceMock = $this->createMock(SomeService::class);
+ $someServiceMock = $this->createMock(SomeService::class);
}
}
```
### 3. Correct Type Declarations
```diff
use PHPUnit\Framework\TestCase;
class SomeTest extends TestCase
{
public function test()
{
$someClass = new SomeClass();
- $someClass->setPhone(12345);
+ $someClass->setPhone('12345');
}
}
final class SomeClass
{
public function setPhone(string $phone)
{
}
}
```
```diff
use PHPUnit\Framework\TestCase;
final class SomeTest extends TestCase
{
public function test(): \stdClass
{
return new \stdClass();
}
/**
* @depends test
*/
- public function testAnother($someObject)
+ public function testAnother(\stdClass $someObject)
{
}
}
```
There is no better way to start using strict types, than in tests. The safest way to spot, which places use loose types:
```diff
+declare(strict_types=1);
+
use PHPUnit\Framework\TestCase;
final class SomeTestWithoutStrict extends TestCase
{
public function test()
{
}
}
```
### 4. No more call on `null` errors
Sometimes a method can return `null`, and we hope it will not. PHPStan spots these "call on possible null" cases, so Rector can help us fix them:
```diff
use PHPUnit\Framework\TestCase;
final class SomeTest extends TestCase
{
public function test()
{
$someObject = $this->getSomeObject();
+ $this->assertInstanceOf(SomeClass::class, $someObject);
$value = $someObject->getSomeMethod();
}
private function getSomeObject(): ?SomeClass
{
// ...
}
}
```
### 5. More Readable Data Providers
Nobody likes arrays, except legacy projects who have no choice. What's even worse are nested arrays. Array in array in array. That's how data provide methods looks like. But they don't have to!
You can use `yield` with each case on standalone line - and include data just that particular line:
```diff
use PHPUnit\Framework\TestCase;
final class SomeTest implements TestCase
{
public static function provideData()
{
- $value = 'last text, but defined here';
-
- return [
- ['some text'],
- ['another text'],
- ['third text'],
- [$value],
- ];
+ yield ['some text'];
+ yield ['another text'];
+ yield ['third text'];
+ $value = 'last text, but defined here';
+ yield [$value];
}
}
```
If you still fancy `array` for data providers, make sure they're neatly indented:
```diff
- return [['content', 8], ['content123', 11]];
+ return [
+ ['content', 8],
+ ['content123', 11]
+ ];
```
From `@testWith` to data provider method:
```diff
-/**
- * @testWith [0, 0, 0]
- * @testWith [0, 1, 1]
- * @testWith [1, 0, 1]
- * @testWith [1, 1, 3]
- */
+#[DataProvider('dataProviderSum')]
public function testSum(int $a, int $b, int $expected)
{
$this->assertSame($expected, $a + $b);
}
+public static function dataProviderSum(): array
+{
+ return [
+ [0, 0, 0],
+ [0, 1, 1],
+ [1, 0, 1],
+ [1, 1, 3]
+ ];
+}
```
...and much more. These are all part of *PHPUnit Code Quality set* with nearly 50 rules that work for you.
To enable it, just add this line to your `rector.php` config:
```php
use Rector\Config\RectorConfig;
return RectorConfig::configure()
->withPreparedSets(phpunitCodeQuality: true);
```
That's it! If some rule doesn't fit your coding style, you can always [skip it](https://getrector.com/documentation/ignoring-rules-or-paths).
Happy coding!
-----------------------------------------
## Rector 2.2: New rules for Array Docblocks
Perex: As you know, we provide an [upgrade services](https://getrector.com/hire-team) to speed up the modernization of codebases. Part of this service is getting PHPStan to level 8 with no baseline (only edge cases).
Level 6 is known for requesting more detailed types over `array`,
`iterable` or `Iterator` type hints. Bare `mixed` or `array` should be replaced with explicit key/value types, e.g., `string[]` or `array`.
At first, we did this work manually. Later, we made custom Rector rules that we kept private.
Today, we are open-sourcing these rules to help you with the same task.
This feature is experimental. We look for your feedback in real projects. Found a glitch, or do you expect a different output? Let us known on GitHub.
When you're proud that your project passes PHPStan on level 7...
...and then remove that single ignore line ↓ pic.twitter.com/VNKYWEEZoa
— Tomas Votruba (@VotrubaT) September 30, 2025
1573 errors... Rector should be able to help with that, right?
Let's look at a few examples that are missing detailed types and that Rector can improve now:
## 1. Known Return Scalar
```php
function getNames(): array
{
return ['John', 'Jane'];
}
```
Now this is straightforward; it can be improved to:
```diff
+/**
+ * @return string[]
+ */
function getNames(): array
{
// ...
}
```
Why do this manually in 100s of places, if Rector can do it for you?
## 2. Known Return Objects
Let's look at another example:
```php
function getUsers(): array
{
$user = [];
$users[] = new User('John');
$users[] = new User('Jane');
return $users;
}
```
No-brainer:
```diff
+/**
+ * @return User[]
+ */
function getUsers(): array
{
// ...
}
```
## 3. Known Shared Object Type
What if there are multiple different objects that all share a single contract interface?
```php
final class ExtensionProvider
{
public function provide(): array
{
return [
new FirstExtension(),
new SecondExtension(),
];
}
}
```
In a real project, we would have to open all of those classes, check parent classes and interfaces, and try to find the first common one. Now we don't have to, Rector does it for us:
```diff
+ /**
+ * @return ExtensionInterface[]
+ */
public function provide(): array
{
// ...
}
```
## 4. Known `array_map()` return
We can infer the type from functions like `array_map()`:
```diff
+/**
+ * @return string[]
+ */
public function getNames(array $users): array
{
return array_map(fn (User $user): string => $user->getName(), $users);
}
```
## 5. Known private method types
What if the method is private and is called only in a local class? We can now collect all the method calls and learn their type:
```diff
final class IncomeCalculator
{
public function addCompanyTips(): void
{
$this->addTips([100, 200, 300]);
}
public function addPersonalTips(): void
{
$this->addTips([50, 150]);
}
+ /**
+ * @param int[] $tips
+ */
private function addTips(array $tips): void
{
}
}
```
...and many more. Right now, the initial set contains **[15 rules](https://github.com/rectorphp/rector-src/blob/main/src/Config/Level/TypeDeclarationDocblocksLevel.php)**, and we plan to extend it further. Got an idea for an obvious rule that you keep doing manually and is not covered yet? Let us know.
We designed these rules to avoid filling useless types like `mixed`, `mixed[]`, or `array`. If the Rector doesn't know better, it will skip these cases. We want to fill those types they way humans would do to improve code readability and static analysis.
## Smart Override
Rector is smart enough to keep detailed types, but override those dummy ones:
```diff
/**
- * @return mixed[]
+ * @return string[]
*/
function getNames(): array
{
return ['one', 'two']
}
```
## Start with Levels
The best way to start using this set is via [level feature](/documentation/levels). Add this single line to your `rector.php` config:
```php
withTypeCoverageDocblockLevel(0);
```
And take it one level at a time:
```diff
withTypeCoverageDocblockLevel(0);
+ ->withTypeCoverageDocblockLevel(1);
```
In a rush or feeling lucky? Add full set:
```php
withPreparedSets(typeDeclarationDocblocks: true);
```
We've put a lot of work into making rules balanced and reliable, but it's still in the early testing phase. Give it a go, let us know how slim your baseline got after a single Rector run.
Happy coding!
-----------------------------------------
## Introducing Rector Jack: Raise Your Dependencies Safely
Perex: Hey PHP folks! 👋 We're thrilled to share **Jack**, a new experimental CLI tool to help you lift your Composer dependencies one version at a time – safely and steadily. If you've ever dreaded the "oh no, our dependencies are *years* old" moment, Jack's here to make upgrades less painful.
We've tested it internally for a couple months, published silently to **pass 3500 downloads in 20 days**, it's time to share it with the world.
It fits both legacy projects to reach higher faster, and modern projects not to fall behind.
## What's Jack All About?
In the real world, a jack lifts your car to fix issues underneath. In the Composer world, **Jack** lifts your dependency versions, so you can keep your project cruising smoothly. It's built to tackle the chaos of outdated packages with a simple, automated approach.
Why use Jack? Because manual upgrades are a headache. Big version jumps often bring errors, compatibility woes, and wasted time. Jack steps in to:
- Catch outdated dependencies in CI.
- Gradually open up package versions for safe updates.
- Prioritize low-risk changes (like dev dependencies).
It's **open-source** and built with community feedback in mind. We're developing this in the open, so your input shapes Jack's future!
## How Does It Work?
Jack is lightweight, works on **PHP 7.2+**, and slots into any legacy project. Install it with:
```bash
composer require rector/jack --dev
```
From there, you've got three killer commands:
### 1. Catch Too Many Outdated Dependencies in CI
Run `vendor/bin/jack breakpoint` to spot major outdated packages. If you've got more than 5, your CI will fail, keeping upgrades on your radar.
Tweak the limit with `--limit 3` or focus on dev packages with `--dev`.
### 2. Open Up Next Versions
Unsure where to start with upgrades? Let Jack nudge your `composer.json` to the next version (e.g., `symfony/console: 5.1.*` → `5.1.*|5.2.*`).
Just run:
```bash
vendor/bin/jack open-versions
```
It will open your current constraints in `composer.json` to the nearest next version. Then, `composer update` does the heavy lifting.
Want to play it safe?
* Use `--dev` to start with dev packages first,
* `--limit` to handle different number of packages
* or `--dry-run` for a preview.
### 3. Sync `composer.json` with Already Installed Versions
Got a modern project but an outdated `composer.json`? Jack raises your dependency versions to match what's installed:
```bash
vendor/bin/jack raise-to-installed
```
No more signaling old versions you don't even use!
## Try Jack and Share Your Thoughts
Jack is experimental, so we're counting on you to kick the tires. Try it out, report bugs, or suggest features on [GitHub](https://github.com/rectorphp/jack). Want to see it in action? Check the [README](https://github.com/rectorphp/jack#readme) for more examples.
We're excited to turn boring dependency upgrades to more fun game.
Happy coding! 🚀
-----------------------------------------
## How to install old or new PHP on non-LTS Ubuntu
Perex: Legacy projects can be upgraded in 2 directions - PHP-wise and infrastructure-wise. We can get a new Ubuntu 25 but still have to run old PHP.
The same goes for upgrading a legacy project. Locally, we use Ubuntu 25, but we work on a project that uses PHP 7.2.
By default, Ubuntu releases ship only one PHP version. Often we need one of the other ~10 versions. How do we run old or new PHP on the latest Ubuntu?
*This post extends [reply on Github issue](https://github.com/oerdnj/deb.sury.org/issues/1662#issuecomment-2823699313), to give it more visibility. Based on comments and SO struggles, many developers give up after reading too many misleading solutions:*
*Don't fret. There is a way.*
*Disclaimer: do this cautiously, preferably on a local dev machine rather than production, to keep the risk of conflicts low.*
A typical solution is to add an external repository with all the old PHP versions.
This repository is maintained by [Ondrej Sury](https://github.com/sponsors/oerdnj), who has maintained it since the beginning of PHP times.
```bash
sudo add-apt-repository ppa:ondrej/php
sudo apt install ppa-purge
sudo ppa-purge ppa:ondrej/php
```
Then reload dependencies:
```bash
sudo apt-get update
sudo apt-get upgrade
```
And we can install our desired PHP version:
```bash
sudo apt-get install php7.4 php7.4-curl
```
This worked very well for years. Until 2021.
## Ubuntu non-LTS no longer supported
Maintaining these packages comes at a great cost. PHP is released once a year. Ubuntu has 2-3 releases a year, this adds to maintenance cost and time, adding to server costs.
We sponsored (PHP 8.1 release)[https://github.com/oerdnj/deb.sury.org/issues/1439#issuecomment-705552989] to help a bit:
But it wasn't enough. There is still [a way to sponsor Ondrej](https://github.com/sponsors/oerdnj) if you want to help out regularly.
This and probably other reasons lead to narrowing [support only to Ubuntu LTS releases](https://github.com/oerdnj/deb.sury.org/issues/1662).
That means only once every 2 years, without the x.10 improved version:
* 24.04 (released in 2024)
* 26.04 (will be in 2026)
## Downgrade to LTS or wait 2 years?
If you're working on an upgrade, downgrading the server would be a bit counterproductive. In the same way, using old PHP and waiting for 2 years to use another outdated PHP version would not help.
So how do we get the PHP version we want without being limited by the LTS Ubuntu release cycle?
## Fake it till you make it
If we try to install e.g. PHP 8.2 on Ubuntu 25, we get the following error:
```bash
sudo apt-get install php8.2
E: Package 'php-82' has no installation candidate
```
We already added the repository above, but still get errors.
* That's because there is no package list to **match our Ubuntu 25 codename - "plucky"** (it's the first word of the release name, lowercase).
* Yet, there is a package list that matches the LTS Ubuntu 24.04 codename - "noble".
Let's pretend we use the LTS Ubuntu 24.04:
```bash
# edit sources file
nano /etc/apt/sources.list.d/ondrej-ubuntu-php-plucky.sources
```
Change our codename:
```diff
-suites: x
+suites: noble
```
Save, and update packages:
```bash
sudo apt-get update
```
Now, let's give it a try:
```bash
sudo apt-get install php8.2
...
Get:1 https://ppa.launchpadcontent.net/ondrej/php/ubuntu noble/main amd64 php8.2-amqp amd64 2.1.2-5+ubuntu24.04.1+deb.sury.org+1 [59.1 kB]
Selecting previously unselected package php8.2.
(Reading database ... 276942 files and directories currently installed.)
Preparing to unpack .../php8.2.1.2-5+ubuntu24.04.1+deb.sury.org+1_amd64.deb ...
Unpacking php8.2 (2.1.2-5+ubuntu24.04.1+deb.sury.org+1) ...
Setting up php8.2 (2.1.2-5+ubuntu24.04.1+deb.sury.org+1) ...
```
Package installs successfully, yay!
That's it. Now we have PHP 8.2 installed on our local Ubuntu 25. Once PHP 8.5 is out, we can it too.
Happy coding!
-----------------------------------------
## How to Strangle your Project with Strangle Anti-Pattern
Perex: Nearly half of the projects we help upgrade have tried the upgrade before on their own.
They've introduced the infamous "strangle pattern". It's a way to upgrade a project separating one part of the codebase from the rest at a time.
Those companies reach us because while the strangle pattern is great in theory, it rather strangles the project in practice. They're unable to move, they use now 2 frameworks instead of one and the team has to work with complexity squared.
Today we'd like to share why it's a terrible choice and what upgrade strategy to take instead.
## What is "Strangle Pattern"?
In programming, the strangler pattern is an architectural pattern that involves wrapping old code, with the intent of redirecting it to newer code or to log uses of the old code ([taken from Wikipedia](https://en.wikipedia.org/wiki/Strangler_fig_pattern)).
How does it look in practice?
Let's say we have an old CakePHP, CodeIgniter, Yii, or plain PHP project and you want to upgrade to Symfony or Laravel. You start by isolating part of the project, e.g. invoicing, and wrap it with Symfony or Laravel controller.
In practice, it can look something like:
That way, we'll have soon the full invoicing in Symfony or Laravel, right?
No, in practice **this is a terrible idea**. Why?
## Just a single Controller
What is necessary to introduce a single Symfony/Laravel controller into an old project?
* we have to add an HTTP layer for response and request
* we have to set up the routing
* we have to set up the dependency injection container and interconnect it with an old project
* we have to make sure the services used in the controller are correctly autowired
To set up a single invoicing controller, we have to set up the whole Symfony/Laravel HTTP, DI, and Routing layers. This could be months of work without showing any value to the customer.
## The Mythical Man-Month
Have you heard of [The Mythical Man-Month](https://en.wikipedia.org/wiki/The_Mythical_Man-Month) book? Its central theme is that **adding manpower to a software project that is behind schedule delays it even longer**.
Same way as adding a parallel framework
will make upgrading the project even harder.
If we could come to a project in a moment before they put 1 year into a strangle pattern upgrade, we could save them a lot of time and money. It usually takes 3-6 months just to untangle the project to its original state.
Let's say we finished the upgrade of invoicing module to the Symfony/Laravel framework. **Now we've done almost the full framework migration path, but there are still 3 modules we have to upgrade. We have just quadrupled the upgrade costs**. In our experience, there are around 15-30 modules in such PHP projects.
What is the result?
* We now have a legacy project with 2 frameworks instead of 1.
* To upgrade a PHP version, we have to now upgrade 2 frameworks, not just 1.
* To add a new layer, e.g. routing security, we have found a way how both frameworks can work together.
## Pattern Refactoring
Instead of chopping off the project and doubling everything, we should focus on the smallest steps possible to make [project upgrade a success](/blog/7-traits-of-successful-upgrade-companies). We use the [pattern refactoring](https://tomasvotruba.com/blog/2019/04/15/pattern-refactoring) approach.
What does pattern refactoring look like? The gist of this approach is to refactor **single pattern in the whole project at a time**.
That means we always have one framework responsible for one part. There are never 2 frameworks handling the same area e.g. controllers, like in the strangle pattern.
### Step by step
1. Find a pattern that is used in the whole project, e.g. configuration, routing, or dependency injection container
2. Create a fully automated strategy using Rector and symfony/finder + php-parser
3. Have a way to quickly verify if the migration works or not, e.g. using [smoke testing](https://tomasvotruba.com/blog/cost-effective-container-smoke-tests-every-symfony-project-must-have)
4. Would it take more than 2 weeks? Find a simpler pattern
That way we have always one framework responsible for one part of the project.
## Benefits of Pattern Refactoring
* the project is always in a working state
* the project is always improving - even if we decide to pause the upgrade process in 4 months, we'll have a better project than we started with
* the work is always finished - there are no stale pull requests that take a month to review
* the pull-request as small, independent of each other, and easy to review
* great use of CLI automation - 80 % of patterns are repeating in most of projects
We have been using this approach since one of our first upgrades in 2017. We only improve the CLI tooling and automation around it. If we discover a new pattern, we add it to the Rector/tools and use it instantly in the next project.
### Case Study
This way, you can migrate the whole project to Laravel in under a year - [see this Rector case study](/blog/success-story-of-automated-framework-migration-from-fuelphp-to-laravel-of-400k-lines-application).
The pattern refactoring is not just for very complex legacy-framework to Symfony/Laravel migrations. They're also suitable for long framework upgrades like [Symfony 2.8 to 7.2](https://tomasvotruba.com/blog/off-the-beaten-path-to-upgrade-symfony-28-to-72). Once you learn it, you can re-use and benefit from it in many projects.
Happy coding!
-----------------------------------------
## Road to Hell is Paved with Strings
Perex: In [7 Traits of Successful Upgrade Companies](/blog/7-traits-of-successful-upgrade-companies), we wrote about the behavior patterns of companies that [make the impossible upgrade happen](/blog/success-story-of-automated-framework-migration-from-fuelphp-to-laravel-of-400k-lines-application).
Today we look closely at an **anti-pattern that repeats in every framework**, in every custom solution, and even in testing tools. A lot of CLI tools that should help us write better code also suffer from it. Frameworks are trying to get rid of it, but most drag mountain piles of history BC promise and so never do.
In this post, we'll look at spotting this anti-pattern, think about better ways to achieve the same result, and share our strategy to get rid of it in our projects.
First, here are 4 code examples we keep seeing in legacy projects we're hired to upgrade. Maybe you'll know them:
```php
return [
'sync' => 'autoamted',
'aws-region' => 'us-east-1',
'aws-access-key' => 'ENV(AWS_ACCESS_KEY)',
];
```
or
```yaml
parameters:
paths:
- src
- tests
ignoreError:
- "#Missing type#"
```
or
```php
services:
-
class: "App\\Controller\\HomepageController"
autowiret: true
```
or
```php
return [
'connections' => [
'sqlite' => [
'host' => 'url:sqlite:///:memory:',
],
'prefix' => '',
]
];
```
The first snippet is a custom code, but snippets **2-4 are coupled to a project you know**. They're coupled to a documentation you've read, on Stackoverflow or GPT.
All the snippets **include tyop that would break** the functionality. Have you noticed them on the first scan?
"If we have to read documentation to understand the configuration,
it sounds more like hardware than software."
We have to know that:
* "autowired" is a Symfony service configuration keyword
* it can be located in the `services` section under the service definition
* it's not `autowire**t**` but `autowire**d**`
## Why does this Matter?
Imagine we have a flight company that handles flights between the US and the UK. We have quality control that checks all electronics on the plane work, that tires have well-defined pressure, that the fuel is enough, and so on. Everything is fully automated and we get any report on exceptional values across time. Sounds safe enough for you to fly? Also, think about the main business functions of airlines - to maximize profits, we have to cut costs to a minimum.
We have a fuel tank, **sealed tight with dozens of screws**. Should we check manually every screw?
What if they change the fuel for a more effective one, that accidentally speeds up rusting of marginal material that our screws are using?
**What if someone forgets?** We don't want to think about these problems. We already put our attention into [the new software that we've deployed](https://spectrum.ieee.org/how-the-boeing-737-max-disaster-looks-to-a-software-developer) last week.
## Protect your Deep Work
This is called [cognitive load](https://github.com/zakirullin/cognitive-load) and it's the root of many fatal bugs in software. This is how it works:
There is a great book called [Don't Make Me Think](https://www.amazon.com/Dont-Make-Think-Revisited-Usability/dp/0321965515), that hits this problem in a very entertaining way.
## "Too many Cooks spoil the Broth"
Some teams can upgrade their project from PHP 5.3 to 8.3 themselves. Other teams too, but it turns into a costly 3-year project with half-team full-time effort. Some other teams are unable to do it in a timely matter and are stuck with the "if it works, don't touch it approach" that causes ever-growing losses in the software business.
The latter teams have to deal with so many edge cases, and WTFs per day, that they're pushing their abilities to the limit just to keep projects running. There is a short road to burnout from there.
That's why it matters: the code must be readable and easy to understand, even to a fresh developer... or to a fresh GPT that reads the code for the first time too.
## Enable the Power of Static Analysis
Another reason is the power of static analysis - in PHP we have PHPStan, static analysis [for TWIG](https://github.com/twigstan/twigstan) or [for Blade](https://github.com/bladestan/bladestan), static analysis [for Behat](https://github.com/rectorphp/swiss-knife/#7-find-unused-behat-definitions-with-static-analysis---behastan) and more.
This static analysis would warn us about 3 typos in the examples above. We'd know what we should fix before merging the pull request.
But what about other file formats other than PHP? **That's the weak link.**
`YAML`, `ini`, `XML`, `JSON`, `NEON` or Gherkin bellow. These all are parseable formats, they're **all parsed into strings**.
```yaml
Feature: Reading code that looks like a string,
but actually calls PHP method
Scenario: Avoid thinking about code
Given am a lazy human
Then I delegate everything to static analysis
```
We have to change those into PHP format first. Sometimes it's one of the formats the tool already handles.
Sometimes we have to come up with our own format like [in case of Behat PHP Config](https://x.com/VotrubaT/status/1882864670723412438/photo/1) - fortunately, it's pretty easy and straightforward with php-parser.
Now let's say we actually have all configs in PHP syntax. Is that good enough to **avoid legacy forever?**
```php
return [
'sync' => 'autoamted',
'aws-region' => 'us-east-1',
'aws-access-key' => 'ENV(AWS_ACCESS_KEY)',
];
```
No, it's not.
* we still made a typo
* we don't know what is *configuration option* and what is *our written value*
* PHPStan is running, but silent
## Configuration Objects to the Rescue!
I'll share a few examples, so you have a better idea about the shape we want to get in. First, let's convert our 1st example to a configuration object:
```php
return AwsConfig::configure()
->syncAutomated()
->withAwsRegion(RegionList::US_EAST_1)
->withAwsAccessKey(EnumKey::AWS_ACCESS));
```
Now we have:
* typo-proof IDE method autocomplete ✅
* IDE autocomplete for enums ✅
* PHPStan warning us about invalid types ✅
* PHPStan reporting deprecated methods ✅
In 2024, the Laravel 11 introduced [streamlined configs](https://laravel-news.com/laravel-11-directory-structure):
```php
return Application::configure()
->withProviders()
->withRouting(
web: __DIR__.'/../routes/web.php',
commands: __DIR__.'/../routes/console.php',
)
->withMiddleware(function (Middleware $middleware) {})
->withExceptions(function (Exceptions $exceptions) {})
->create();
```
[ECS](https://github.com/easy-coding-standard/easy-coding-standard) uses PHP config object [since 2022](https://tomasvotruba.com/blog/new-in-ecs-simpler-config):
```php
// ecs.php
use PhpCsFixer\Fixer\ListNotation\ListSyntaxFixer;
use Symplify\EasyCodingStandard\Config\ECSConfig;
return ECSConfig::configure()
->withPaths([__DIR__ . '/src', __DIR__ . '/tests'])
->withRules([
ListSyntaxFixer::class,
])
->withPreparedSets(psr12: true);
```
In 2021, Symfony 5.3 shows state of the art [autogenarated-configs](https://symfony.com/blog/new-in-symfony-5-3-config-builder-classes):
```php
// config/packages/security.php
use Symfony\Config\SecurityConfig;
return static function (SecurityConfig $security): void {
$security->firewall('main')
->pattern('^/*')
->lazy(true)
->anonymous();
$security->accessControl(['path' => '^/admin', 'roles' => 'ROLE_ADMIN']);
};
```
## Automate and Forget
Once we have PHP in place, we move to the configuration object and get PHPStan with deprecated rules on board, we can forget **about the configuration syntax**. We can focus on the business logic and let the configuration be handled by the tool.
* Next time you use Symfony - [modernize configs](/blog/modernize-symfony-configs) to best shape the framework provides.
* Upgrade your Laravel project to 11 and use [streamlines configs](https://github.com/laravel/laravel/blob/master/bootstrap/app.php).
* Create a PHP wrapper for the tool that uses that old format and is error-prone. Learn [php-parser](https://github.com/nikic/PHP-Parser).
Happy coding!
-----------------------------------------
## Evolution of New Rector Logo
Perex: After 7 years since the first Rector commit, we have a new logo!
Today, we're proud to share a streamlined, smooth, and modernized version of the Rector logo. Similar to a project upgrade, the final version looks great, but the journey is the destination. Did you know it took 40 commits, 5 sketched paper pages, and 3 months to get there?
Today, I'd like to share the backstory with you.
Rector started as a pet project to ease my daily work. I asked a friend to make a logo, and he helped me.
The job description was "house, left part old, right part new". I wanted to show the main purpose of **rec**onstruc**tor** (original name) = a code reconstruction. I haven't told this to anyone, but it represents the house in Liberec where I grew up with my family. My father used to reconstruct it all the time. The result was the logo we all know:
It was good for the time being. But Rector grew, a pet project turned into full-time work, company, [a book](https://leanpub.com/rector-the-power-of-automated-refactoring) and regular conference talks. A logo with 30 colors+, gradients, and tiny details was impossible to use in printed material. The need for a new version was imminent.
We have been thinking of a new Rector logo since 2020. We wanted a new logo to land on [Rector 1.0 live release in Laracon 2024](/blog/rector-1-0-is-here), Amsterdam. But many of the new features presented in the talk had to be delivered first. After the Rector 1.0 release, we received a few offers from external designers, yet we could not find a good fit with any of them.
We joined [Open Source Pledge](/blog/rector-is-joining-open-source-pledge) this year to spread the "giving back to open-source is normal" message. A couple of weeks after joining the pledge, we got a surprise message: "Oh, your logo will be on Times Square." The only problem was that we had to stitch up a new, temporary, simple logo within a week.
"Just strip down every detail; make it simple". This was the result:
Butter, but still not pretty to watch. It was the last nail in the coffin.
**We've decided to land a new logo before Rector 2.0 is out.**
## It's an Upgrade Time... of Logo
I wanted to keep the house, as it's a part of Rector's DNA. I wanted to keep the left "shady" and the right "modern" side.
The logo should represent modern, streamlined change as we do with legacy projects that are perceived as impossible to upgrade.
The logo must be:
* **simple = 2 colors maximum**
* **modern with curves**
* easy to print and spot in a tiny version (favicon)
* **A/B tested on users**, so we get the idea of how developers see our identity
We've decided to make as many iterations as needed until we're sure it's the right one.
**2nd iteration** came up with an idea: "Let's use CI colors, the left for warnings, the right for a successful pass.":
What was the feedback?
* "The yellow/orange colors do not contrast enough."
* "There are too many lines and details
* "There are basically 4 colors."
* "What are those horizontal lines?"
On **3rd iteration** we've found a great [color palette](https://colorhunt.co/palette/4d46465b56567fcd91f5eaea) we fell in love:
What was the feedback?
* "Colors look much better"
* "When it's small, there are still many details"
* "Is the roof flying?"
* "remove windows, remove doors - people will get it"
We've integrated feedback, and in **4th iteration**, we removed all the details but cracks:
What was the feedback?
* "House looks neat."
* "What are these lines?"
* "Is that the homepage icon?"
We don't want to make another homepage icon. We want to make a logo that represents **R**ector. We've removed the confusing lines and tested various "R" fonts.
On **5th iteration**, we felt we were getting closer.
What was the feedback?
* "2 in the middle"
* "2 and 5"
* "The roof is tiny, though."
We added a roof testing for **6th iteration**
What was the feedback?
* "2 and 3."
* "3 or 4."
* "4 but with bigger chimney."
We went with version 3 and made the chimney slightly bigger. Time to test on production!
## Final Version
**We silently pushed the logo on our homepage to see reactions in the wild**. It was great to see people noticing the change and giving us feedback.
Here is the final version:
## New Rector Book Cover
While at it, we've designed a new cover for the [Rector book](https://leanpub.com/rector-the-power-of-automated-refactoring), so it runs Rector 2.0 not only inside but outside as well.
The 5-iteration journey looked like this:
It's been a long but adventurous journey. **Rector is built on the PHP community, so we've really enjoyed including the community in making the new logo** the best version possible.
We're proud of the final result, and we hope you like it, too.
Happy coding!
-----------------------------------------
## 5 New Features in Rector 2.0
Perex: Rector 2 is out! We've upgraded to PHPStan 2 and PHP-Parser 5. Based on testing on several huge legacy projects, Rector now runs **10-15 %** faster.
We've also managed to fit in a couple of new features.
Let's take a look at what's new.
## 1. The `--only` Option to run 1 Rule
At the moment, the Rector repository has a `rector.php` config that enables the running of 350+ rules. What if we add a single custom rule and want to run only that one? We'd have to comment out all other rules, run Rector, and then uncomment them back. That's a lot of manual work, right?
**Now we can use the `--only` option to run only a single rule**:
```bash
vendor/bin/rector process src --only="Utils\Rector\MoveTraitsToConstructorRector"
```
Making all quotes and slashes in CLI work across all operating systems was a tough challenge. Thanks to [Christian Weiske](https://github.com/rectorphp/rector-src/pull/6441), who's done a great job on this feature.
## 2. Introducing Composer-based sets
In the wild, vendor sets like Symfony, Twig, Laravel, or Doctrine have many sets - each containing a group or rules for a specific version. Symfony has over 20 sets, but let's look at a more straightforward example - Twig:
```php
use Rector\Config\RectorConfig;
return RectorConfig::configure()
->withSets([
\Rector\Symfony\Set\TwigSetList::TWIG_112,
\Rector\Symfony\Set\TwigSetList::TWIG_127,
\Rector\Symfony\Set\TwigSetList::TWIG_134,
\Rector\Symfony\Set\TwigSetList::TWIG_20,
\Rector\Symfony\Set\TwigSetList::TWIG_24,
])
```
This doesn't seem right for many reasons. It can lead to:
* bloated `rector.php` file
* missed new set, as we have to always add new sets as they're published
* conflicting changes as version 2.4 can remove something added in version 1.12, many years apart
Instead, **the Rector should be able to pick up the version from the installed version and provide only relevant rules**.
Fully automated, like the following:
```php
return RectorConfig::configure()
->withComposerBased(twig: true)
```
Currently, we provide `twig`, `doctrine`, and `phpunit` composer-based sets.
If you want to **know how it works behind the scenes**, check [this dedicated post](/blog/introducing-composer-version-based-sets).
## 3. Polyfill Packages by Default
We had a special configuration to enable [polyfill packages](https://github.com/symfony/polyfill). That way we can get PHP 8.x features early on PHP 7.x codebase. Yet, not many people knew about it and missed it. Also, polyfill packages are already defined in the `composer.json`, so it doesn't make sense to "enable" them again `rector.php`.
We've decided to include polyfills by default when `->withPhpSets()` is called. So you can drop the extra method:
```diff
use Rector\Config\RectorConfig;
return RectorConfig::configure()
- ->withPhpPolyfill()
->withPhpSets();
```
It works for all `->withPhp*Sets()` too.
## 4. Smarter Annotations to Attributes sets
We've been providing [annotations to attributes upgrade](https://getrector.com/blog/how-to-upgrade-annotations-to-attributes) since PHP 8.0 day. You can enable them in `rector.php` easily:
```php
use Rector\Config\RectorConfig;
return RectorConfig::configure()
->withAttributesSets(symfony: true, phpunit: true, doctrine: true);
```
In reality, some packages add attribute support a bit later. E.g., Rector 2 ships with [Behat attributes](https://github.com/rectorphp/rector-src/pull/6510) contributed by [Carlos Granados](https://github.com/carlos-granados). To use them in our project, we'd have to change the config:
```diff
use Rector\Config\RectorConfig;
return RectorConfig::configure()
- ->withAttributesSets(symfony: true, phpunit: true, doctrine: true);
+ ->withAttributesSets(symfony: true, phpunit: true, doctrine: true, behat: true);
```
But who has time to check if this or that package has new attribute sets? Instead, **make the method empty**:
```diff
use Rector\Config\RectorConfig;
return RectorConfig::configure()
- ->withAttributesSets(symfony: true, phpunit: true, doctrine: true);
+ ->withAttributesSets();
```
Now, the Rector will pick up all attribute sets automatically.
Also, Rector will now check if the attribute **actually exists before it adds it**. E.g. `#[Route]` attribute was not added until Symfony 5.2. If you're running Symfony 5.1, Rector will not make any changes to your code.
## 5. Leaner Custom Rules
Last but not least, we've collected feedback from custom rule creators. We also create many custom rules for our clients. Some of them are temporary, and others fix simple elements. Still, we always have to fill `getRuleDefinition()` with dummy data to make Rector happy.
In reality, this method is used only by Rector core rules for the [Find rule](https://getrector.com/find-rule) page.
Saying that we no longer need to write tedious `getRuleDefinition()`. Now you can finally drop this method:
```diff
use Rector\Rector\AbstractRector;
-use Symplify\RuleDocGenerator\ValueObject\CodeSample\CodeSample;
-use Symplify\RuleDocGenerator\ValueObject\RuleDefinition;
Final class SimpleRector extends AbstractRector
{
- public function getRuleDefinition(): RuleDefinition
- {
- return new RuleDefinition('// @todo fill the description', [
- new CodeSample('...', '...'),
- ]);
- }
// valuable code starts here
}
```
See the [upgrade guide](https://github.com/rectorphp/rector/blob/main/UPGRADING.md) for full details.
Happy coding!
-----------------------------------------
## Introducing Composer Version-Based Sets
Perex: Packages that ship a lot of versions can have a lot of sets to apply. For, twig/twig has 6 sets in Rector, a couple for v1 and a couple for v2. What about v3? We must always check for our locally installed version and then keep `rector.php` up to date.
This could lead to errors as we run sets with new features from v3 that we don't have yet.
At the moment, we have to add sets manually one by one to `rector.php`:
```php
return RectorConfig::configure()
->withSets([
\Rector\Symfony\Set\TwigSetList::TWIG_112,
\Rector\Symfony\Set\TwigSetList::TWIG_127,
\Rector\Symfony\Set\TwigSetList::TWIG_134,
\Rector\Symfony\Set\TwigSetList::TWIG_20,
\Rector\Symfony\Set\TwigSetList::TWIG_24,
])
```
This doesn't seem right for a couple of reasons:
* we have to always check if there is a new set we should add here
* if we use Twig 2, there is no point in running Twig 1 set - they also may cause an error as syntax evolves
* if we use Twig 1.20, we don't want 1.40 sets to run as they might break the code
### What should happen instead?
* Rector should look into installed `composer.json` version of `twig/twig`
* then check all Twig sets and find those that make sense to apply
* run those
If we upgrade to Twig 3 later on, Rector should pick up sets for Twig 3 for us. So, we don't maintain the `rector.php` at all.
### Introducing `withComposerBased()`
```php
return RectorConfig::configure()
->withComposerBased(twig: true);
```
You can drop all sets from above and use this single parameter.
Currently, we support Twig, PHPUnit, and Doctrine. Support for Symfony and Laravel is coming soon.
## How does it work?
Rector will go through all Twig sets and check our installed version in `vendor/composer/installed.json`. Then, it finds all sets relevant to our specific version.
If you're writing your custom extension for your community, you'll want to create your own `SetProvider` to handle complexity for your users.
Let's look at a real example for [`TwigSetProvider`](https://github.com/rectorphp/rector-symfony/blob/main/src/Set/SetProvider/TwigSetProvider.php) from the `rector-symfony` package:
```php
namespace Rector\Symfony\Set\SetProvider;
use Rector\Set\Contract\SetInterface;
use Rector\Set\Contract\SetProviderInterface;
use Rector\Set\ValueObject\ComposerTriggeredSet;
use Rector\Set\ValueObject\Set;
final class TwigSetProvider implements SetProviderInterface
{
/**
* @return SetInterface[]
*/
public function provide(): array
{
return [
new ComposerTriggeredSet(
'twig',
'twig/twig',
'1.27',
__DIR__ . '/../../../config/sets/twig/twig127.php'
),
new ComposerTriggeredSet(
'twig',
'twig/twig',
'2.0',
__DIR__ . '/../../../config/sets/twig/twig20.php'
),
// ...
];
}
}
```
Setup is straightforward - define the version and path to the set:
```php
namespace Rector\Set\ValueObject;
final class ComposerTriggeredSet
{
public function __construct(
private string $groupName,
private string $packageName,
private string $version,
private string $setFilePath
) {
}
// ...
}
```
* The `$groupName` is key in `->withComposerBased()`:
* The `$packageName` is the composer package name.
* `$version` is the minimal version to trigger the set
* and `$setFilePath` is the path to the Rector config with rules as we know it
In the near future, community packages like [Laravel](https://github.com/driftingly/rector-laravel) will have their own `SetProvider` classes. To get their latest upgrade sets, all you'll have to do is add one param:
```php
return RectorConfig::configure()
->withComposerBased(laravel: true);
```
Happy coding!
-----------------------------------------
## Improving Rector Performance by 20-30 %
Perex: Today I want to talk about how I added an optimization that made Rector 20-30% faster!
*This is a guest post by [Carlos Granados](https://twitter.com/carlos_granados), who uses Rector very frequently and has recently contributed several improvements to this tool.*
This week we've released @rectorphp 1.2.5
with huge performance improvement
by amazing @carlos_granados 👏❤️️
Rector is now 25-30 % faster🚀🚀🚀
Enjoy 🤗 pic.twitter.com/uhT3eyaMks
— Tomas Votruba (@VotrubaT) September 9, 2024
## Always optimizing
I started my development career in the 80s, developing games for home computers like Spectrum, Amstrad or Commodore 64. These were very limited platforms, so you learned that every CPU cycle and every byte counted.
So even nowadays, whenever I am developing software, in the back of my mind I'm always thinking about ways in which things can be optimized so that they need to do less computing and use less CPU or memory. I always try to implement small optimizations, things like assigning the result of a function to a variable and reusing that instead of calling the function several times or moving the calculation of the end condition of a loop out of the loop so that we don't need to calculate the condition on every loop pass.
Most of these optimizations provide very small benefits but, as they say, "every little counts" and hundreds of these little optimizations end up making code that is faster and uses less resources.
There are many cases where you need to opt for code clarity or complexity instead of performance. For example, if you refactor a function to extract some code into another function, performance is going to be a tiny bit worse, but the gains in diminished complexity and improved reusability far outweight this small lose.
If you are really looking into heavily optimizing some software, my advice is: look for the code that is executed more often and concentrate your efforts there. If a function is called thousands of times during the execution of your program, even a tiny improvement there can lead to great gains in the overall performance of the product.
## Optimizing Rector
I was trying to debug a Rector rule, trying to fing out why it provided an incorrect result for some cases, when I realized something: Rector was trying to apply all rules to all AST nodes. But every rule can only work with a limited subset of node types. So the first thing that every rule did was to check if that rule could be applied to that particular kind of node. And for the majority of rules the answer would be "no". So we lost a lot of time checking if every rule could be applied to the particular node that we had in hand. And this got worse as we increased the number of rules that we were applying to our code.
I thought that instead of doing this, we could find out a list of the rules that applied to a particular kind of node and only call these rules when we were dealing with each of them. There was only one obstacle, the `NodeTraverser` class from the `PhpParser` library that Rector used to traverse the AST did not provide any mechanism to only call some visitors (rules) for each kind of node. So I had to patch this class to add this mechanism. This is how the class looks after this patching:
```php
class NodeTraverser implements NodeTraverserInterface
{
...
protected function traverseNode(Node $node) : Node {
...
$visitors = $this->getVisitorsForNode($subNode);
foreach ($visitors as $visitorIndex => $visitor) {
...
}
...
/**
* @return NodeVisitor[]
*/
public function getVisitorsForNode(Node $node)
{
return $this->visitors;
}
...
}
```
As you can see, now before looping for the visitors for each node, we call a function that can provide the list of visitors to apply to that node. The default implementation just returns all available visitors.
Then in our `RectorNodeTraverser` Rector class, which inherits from this base `NodeTraverser`, we implement the `getVisitorsForNode()` function like this:
```php
final class RectorNodeTraverser extends NodeTraverser
{
/**
* @var array,RectorInterface[]>
*/
private array $visitorsPerNodeClass = [];
...
/**
* We return the list of visitors (rector rules) that can be applied to each node class
* This list is cached so that we don't need to continually check if a rule can be applied to a node
*
* @return NodeVisitor[]
*/
public function getVisitorsForNode(Node $node): array
{
$nodeClass = $node::class;
if (! isset($this->visitorsPerNodeClass[$nodeClass])) {
$this->visitorsPerNodeClass[$nodeClass] = [];
foreach ($this->visitors as $visitor) {
assert($visitor instanceof RectorInterface);
foreach ($visitor->getNodeTypes() as $nodeType) {
if (is_a($nodeClass, $nodeType, true)) {
$this->visitorsPerNodeClass[$nodeClass][] = $visitor;
continue 2;
}
}
}
}
return $this->visitorsPerNodeClass[$nodeClass];
}
...
}
```
As you can see, we don't pre-calculate the rules to be used for every single kind of node, instead we calculate this list of the fly for every type of node that we find. This allows us to avoid calculating this list for any kind of node that is not present in our code base. Also, we don't attempt to cache these lists in any way. Calculating them is quite fast and the extra complexity that would have been needed to create and use this cache is not worth the small extra performance gain that we could have obtained.
When I was working on this code, before I tested it I was hoping for a performance gain of 5%-10%, so I was really happy when my tests returned a performance gain of 20-25%. This was later confirmed by Tomas Votruba, Abdul Malik Ikhsan and Markus Staab who measured similar or even greater gains.
I am really proud to have been able to add this improvement to Rector. I have always said that this tool is the best thing that has happened to the PHP ecosystem in the most recent years and I am always happy to see ways to improve it. And now all Rector users will benefit from much shorter runs.
One part of this that makes me specially happy is to think about the carbon footprint implications of this change. Rector is a tool used by thousands of developers which must be run thousands of times a day. This means that this improvement will end up saving many millions of minutes of execution, which means helping to lower the carbon footprint of this tool in a very significant way.
If you like this improvement and would like to support my contributions to open source, please consider [sponsoring me](https://github.com/sponsors/carlos-granados). Thanks!!!
-----------------------------------------
## Rector is joining the Open Source Pledge
Perex: Our business is built on open-source and with open-source software. We're using "free" PHP language, "free" PHP frameworks, and "free" packages. But those are not free to develop and maintain.
That's why we're joining [Open Source Pledge](https://osspledge.com/). To put actual numbers on the table and commit to long-term support.
We want to support not only existing open-source projects but also young, fresh, and aspiring talents. Those who put more work and effort after paid work. At night, after they take care of their family. On the weekend, when they could rest and relax after a challenging week.
"The people who are crazy enough
to think they can change the world
are the ones who do."
We believe that's how amazing projects are created. By someone with a crazy idea to change the world, with little time and money they have, from their tiny room or with a couple of like-minded people.
## What is the Open Source Pledge?
There is an excellent write-up of the state of open-source in 2024 by [Scott Chacon from GitButler](https://blog.gitbutler.com/open-source-pledge-2024/). You probably know the company behind PHP open-source project we use daily. The Private Packagist joined this week, [Nils Adermann sharing their post](https://blog.packagist.com/packagist-is-joining-the-open-source-pledge/).
The idea behind the Open Source Pledge is simple:
* contribute at least **2 000 €/full-time developer**/year to open-source projects
* **share the numbers**
* keep on going every year
In 2023, we've contributed:
* [Symfony](https://symfony.com/sponsor) - 10 000 $
* [junior.guru](https://junior.guru/love/) - 3 530 $
* [phpcsstandards](https://github.com/sponsors/PHPCSStandards) - 1 000 $
* [staabm](https://github.com/sponsors/staabm) - 1 000 $
* [3v4l.org](https://3v4l.org/sponsor) - 250 $
* [gehrisandro](https://github.com/sponsors/gehrisandro) - 200 $
Total of **15 980 $**
In 2024, so far we've contributed:
* [Symfony](https://symfony.com/sponsor) - 10 000 $
* [junior.guru](https://junior.guru/love/) - 3 530 $
* [3v4l.org](https://3v4l.org/sponsor) - 1 000 $
* [janedbal](https://github.com/sponsors/janedbal) - 1 000 $
* [peterfox](https://github.com/sponsors/peterfox) - 1 000 $
* [localheinz](https://github.com/sponsors/localheinz) - 333 $
* [Ondrej Sury](https://github.com/sponsors/oerdnj) - 200 $/month
Total of **18 663 $** to this day.
Is your company building on top of open-source? Do you want to enjoy more tools in the future to help our IT economy?
Here is how [to join Plege](https://osspledge.com/join/).
Thank you
Happy coding!
-----------------------------------------
## How to Migrate CodeIgniter to Symfony or Laravel
Perex: CodeIgniter was created in 2006 and was one of the first MVC PHP frameworks. Yet it never gained traction and got stuck.
Is your project running CodeIgniter, and do your developers want a change?
We receive a few client requests a year for CodeIgniter project upgrades, so we'll share a few tips on migrating it to Symfony/Laravel.
## How Expensive is Framework Migration?
First, we have to ask the tough question: are you afraid framework migration will be expensive? It is often easier than a single framework upgrade. Let's look at an example where an upgrade of a single framework takes more steps than migration from one to another:
* Project A: Symfony 2, we make an upgrade to 3, 4, 5, 6, 7
* Project B: CodeIgniter 1/2/3 to Symfony 7
↓
* Project A has **5 steps**
* Project B has **a single step** - even if the step is quite complex, it's still only single step
## Step 1: Avoid Two Entry Points
Someone asked about ["Incremental migrate Codeigniter to Symfony" on Reddit](https://www.reddit.com/r/symfony/comments/gmldnk/incremental_migrate_codeigniter_to_symfony/) 4 years. The first suggested solution is introducing a few Symfony controllers and creating "a bridge".
**That's not a way to upgrade**. You'll end up with 2 frameworks mess instead, as one comment explains:
"You're going to spend way more time getting the two frameworks to talk to each other than you think. Unless you have some very small, very targeted needs and are highly confident that won't change, setting up a way for them to communicate via events/messages is going to save you time in the long run.
I speak from experience having done something similar with multiple legacy code bases, including one that is Symfony wrapping CI."
Instead, we always use a single framework. We migrate from one to another using [pattern migration](/blog/success-story-of-automated-framework-migration-from-fuelphp-to-laravel-of-400k-lines-application). This allows us to run business features in CodeIgniter while working custom Rector rules to flip to Symfony in parallel. **That way [business is growing](/blog/how-to-migrate-legacy-php-applications-without-stopping-development-of-new-features) and migration is being prepared at the same time**.
Let's dive into it.
## Step 2: Identify Patterns in both Frameworks
First, we have to identify patterns in CodeIgniter and find their equivalent in Symfony. This way, we can map them and create a migration plan.
**CodeIgniter** has:
* models to communicate with a database
* routes map that checks specific URL string, then calls a specific public method in a specific controller class
* controller classes that load services
* PHP and HTML templates to render data
* array configs to store configuration
**Symfony/Laravel** has:
* Doctrine repositories/Eloquent models to communicate with a database
* `@Route()` annotations or `routes.php` that mark specific controller methods to match URL string
* controllers using dependency injection to load services
* TWIG/Blade templates to render data
* PHP configs with fluent API to store configuration
## Step 3: From Models to Repositories
In most PHP frameworks, the database is not tightly coupled to the application. That's what *M* in MVC standards for—**model**. Symfony can work with CodeIgniter models, and CodeIgniter can work with Doctrine repositories. After all, it's only a group of arrays or simple objects.
That's why the database is low hanging fruit that we start with.
At first, we have to focus on basic principles - what do we need to replace?
* call data from the database
* return them in the form of arrays
Let's see how the model class looks in CodeIgniter:
```php
class Coupon_Model extends CI_Model
{
/**
* @return object
*/
public function getCoupon($couponCode)
{
$this->db->where('coupon_code', $couponCode);
$query = $this->db->get('coupons');
return $query->row();
}
}
```
There we can see model class name <=> table name convention. Doctrine has the same convention.
How would it look like in Doctrine?
```php
use Doctrine\ORM\EntityRepository;
class CouponRepository extends EntityRepository
{
public function getCoupon($couponCode)
{
return $this->createQueryBuilder('c')
->andWhere('c.couponCode = :code')
->setParameter('code', $couponCode)
->getQuery()
->getOneOrNullResult();
}
}
```
At first, we focus only on getting data from the database. Result of both CodeIgniter and Symfony methods:
```php
public function getCoupon($couponCode)
```
**must be the same**. For now, it's essential to skip entities, objects, and collections and **focus only on a single pattern at a time**.
Once we've flipped the read model to repositories, we can examine other patterns, such as data storage, modification, and so on.
## Step 4: From file-routing to Controller Annotations
Controllers should be as slim as possible. Their primary function is to delegate request data to specific services and then render results.
Let's check the layers that convert URL to specific controller action - routing. CodeIgniter defines routes in `application/config/routes.php` as follows:
```php
$route['blog'] = "blog/overview";
```
The route "blog" leads to the `BlogController` class, with the `overview()` public method. Once we know the pattern, we create a custom Rector rule to read this file and generate Symfony controller annotations in the right place:
```php
/**
* @Route("/blog", name="blog_overview")
*/
public function overview()
{
// ...
}
```
## Step 5: Migrate Controller externals
Now that we have prepared the route and repository migration let's check the features used in controllers.
To give you a practical example, let's look at a typical CodeIgniter 1.0 controller:
```php
class Products extends CI_Controller
{
public function __construct()
{
parent::__construct();
// loads Product_model to "Product_model" magic property
$this->load->model('Product_model');
}
public function index()
{
$data['products'] = $this->Product_model->get_all_products();
// Loading the view with data
$this->load->view('products/index', $data);
}
}
```
We can extend rules from Step 3 to use our Doctrine repository service:
```diff
-$data['products'] = $this->Product_model->get_all_products();
+$data['products'] = $this->productRepository->get_all_products();
```
We can also see 2 more patterns we haven't covered yet:
* dependency injection
```php
public function __construct()
{
parent::__construct();
// loads Product_model to "Product_model" magic property
$this->load->model('Product_model');
}
```
* template rendering
```php
// Loading the view with data
$this->load->view('products/index', $data);
```
Because our target is Symfony 7, we can work with constructor dependency injection. We create a custom Rector rule to move magic string-based dependencies to type-based dependencies:
```diff
+private ProductRepository $productRepository;
-public function __construct()
+public function __construct(ProductRepository $productRepository)
{
parent::__construct();
- $this->load->model('Product_model');
+ $this->productRepository = $productRepository;
}
```
Add similar migration for `$this->load->helper('...');` that looks like a service locator.
The next step is to add a custom rule for template rendering. We can use Symfony Twig templating engine:
```diff
-$this->load->view('products/index', $data);
+return $this->render('products/index', $data);
```
We respect the original pattern, but we use Symfony services instead.
## Step 6: Migrate Templates from PHP to Twig
Out of the box, CodeIgniter users bare PHP + HTML templates:
```php
load->view('some_header'); ?>
Product: = $product->title ?>
```
We can temporarily use PHP rendering in Symfony, or better finish the job and create PHP to Twig migration instead.
```diff
-load->view('some_header'); ?>
+{{ include('some_header') }}
- Product: = $product->title ?>
+ Product: {{ $product->title }}
```
## Step 7: Prepare for Configs
CodeIgniter has a straightforward way to configure your project:
```php
'value'
];
```
Simple array. Using [Symfony PHP fluent API](/blog/modernize-symfony-configs), this should be easy to migrate.
Either to parameters or bundle configurations:
```php
// config/security.php
use Symfony\Config\SecurityConfig;
return static function (SecurityConfig $securityConfig): void {
$securityConfig->enableAuthenticatorManager(true);
};
```
## Step 8: Migrate Controllers
By now, we have chipped everything we could off controllers:
* routing via annotations
* dependency injection via constructor
* template rendering via TWIG
* database calls via repository
If nothing else is left, we create a custom Rector rule to migrate CodeIgniter controllers to Symfony.
This is the gist of CodeIgniter to Symfony migration. Every project is strictly individual and requires custom work on Rector rules to cover all the patterns.
It's important to note that these rules must **dry-run in CI** on the fly so you can see the progress and fix edge cases. Do not make migration until all the known patterns are covered by Rector rules. That way, you save yourself from manual work and bugs.
Happy coding!
-----------------------------------------
## Introducing Rule Finder
Perex: To this day, Rector provides **over 535 rules spread in 4 repositories** - core, PHPUnit, Symfony, and Doctrine. If you are looking for a rule that does a specific job, you'd have to go through 4 markdown files, find it on a page, and hope to get it right. That is frustrating, especially when you look for a "constant," but rules have "const" in their name.
We heard your feedback and worked on a **single place to search rules past couple of months**. We're proud to share the final page.
Right to the point: next time you look for a rule that does "this or that", open [getrector.com/find-rule](/find-rule).
There are 3 ways you can narrow your search.
## 1. Search by name and description
We have prepared 5 queries for you to try. Click on any of them to see the result.
Click on the rule class input to select it. Then, you can copy it and paste it into your `rector.php` setup.
The search works word by word, so you can type them in any order. You can look for "type constant," and rules with "const" will also match.
You can also see if the rule is *configurable* = used in many sets with specific configurations.
## 2. Filter by node
Are you curious about rules that modify class methods? Do you look for PHP 8 attributes? What about types of closures?
Pick your node in the select:
## 3. Filter by set
Did you know there are over 30 prepared sets in Rector? Not just PHP or framework upgrades, but with a focus on dead code, PHPUnit code quality, or Doctrine improvements.
It would be wild to turn them on unthinkingly. Now you can **see all the rules by a single set** - pick it from the select box:
This is part of our mission to make learning Rector and AST more fun. Like we did with ["Play with AST" page](/blog/introducing-play-with-ast-page). Try it and let us know, [how you like it](https://github.com/rectorphp/getrector-com/issues).
In the future, we **plan to add community packages like Laravel, Sulu, Drupal...** [and more](https://github.com/rectorphp/rector#empowered-by-community-heart).
Now start searching [your following rule](https://getrector.com/find-rule).
Happy coding!
-----------------------------------------
## Introducing Type Perfect for extra Safety
Perex: When dealing with legacy, we first focus on safety by knowing the important types. Code must have reliable type declarations to be refactored safely.
Over time, we've been adding our custom rules to address PHPStan blind spots first. Today, we're proud to publish them in one solid package.
These rules make skipped object types explicit and param types narrow and help you to fill more accurate object type hints. **It's easy to enable, even if your code doesn't pass level 0.**
They're easier to resolve than getting your project to level 1. Their goal is to make your code instantly more solid and reliable. If you care about code quality and type safety, add these 10 rules to your CI.
## Low Hanging Fruit - Exact Instance
There are 2 checks enabled out of the box. The first one makes sure we don't miss a chance to use `instanceof` to make further code know about the exact object type:
```php
$someType = $this->vendorServices->getSomeType();
if (! empty($someType)) {
// ...
}
if (! isset($someType)) {
// ...
}
// here we only know, that $this->someType is not empty/null
```
Why it's dangerous? Because we don't know how reliable the `/vendor` class is. It can be strict:
```php
public function getSomeType(): ?SomeType
{
// ...
}
```
Or it can be loose:
```php
/**
* @return SomeType|null
*/
public function getSomeType()
{
// ...
return 'error message';
}
```
🙅
↓
```php
if (! $this->someType instanceof SomeType) {
return;
}
// here we know $this->someType is exactly SomeType
```
😊
The second rule checks we use explicit object methods over magic array access:
```php
$article = new Article();
$id = $article['id'];
// we have no idea, what the type is
```
🙅
↓
```php
$id = $article->getId();
// we know the type is int
```
😊
### Enable 3 Configured Groups
The following rules can be enabled by configuration in `phpstan.neon`. We take them from the simplest to the most powerful in the same order we apply them to legacy projects.
## 1. Null over False
Enable in `phpstan.neon`:
```yaml
parameters:
type_perfect:
null_over_false: true
```
Bool types are typically used for on/off, yes/no responses. But sometimes, the `false` is misused as a *no-result* response, where `null` would be more accurate:
```php
public function getProduct()
{
if (...) {
return $product;
}
return false;
}
```
🙅
↓
We should use `null` instead, as it enabled strict type declaration in the form of `?Product` since PHP 7.1:
```diff
-public function getProduct()
+public function getProduct(): ?Product
{
if (...) {
return $product;
}
- return false;
+ return null;
}
```
😊
## 2. No Mixed Caller
Enable in `phpstan.neon`:
```yaml
parameters:
type_perfect:
no_mixed: true
```
This group of rules focuses on PHPStan's blind spot. If we have a property/method call with an unknown type, PHPStan cannot analyze it. It silently ignores it.
```php
private $someType;
public function run()
{
$this->someType->someMetho(1, 2);
}
```
It doesn't see a typo in the `someMetho` name or that the second parameter must be `string`.
🙅
↓
```php
private SomeType $someType;
public function run()
{
$this->someType->someMethod(1, 'active');
}
```
This group makes sure all property fetches and methods call know the type they're called on.
😊
## 3. Narrow Param Types
Last but not least, the narrowed param type declarations, the more reliable the code.
Enable in `phpstan.neon`:
```yaml
parameters:
type_perfect:
narrow_param: true
```
In the case of `private` but also `public` method calls, our project knows the exact types that are passed in it:
```php
// in one file
$product->addPrice(100.52);
// another file
$product->addPrice(52.05);
```
But out of fear and "just to be safe," we keep the `addPrice()` param type empty, `mixed`, or in a docblock.
🙅
↓
If, in 100 % of cases, the `float` type is passed, PHPStan knows it can be added and improve further analysis:
```diff
-/**
- * @param float $price
- */
-public function addPrice($price)
+public function addPrice(float $price)
{
$this->price = $price;
}
```
That's where this group comes in. It checks all the passed types and tells us how to narrow the param type declaration.
😊
## Add Type Perfect to your project
First, make sure you have [`phpstan/extension-installer`](https://github.com/phpstan/extension-installer#usage) to load the necessary service configs. Then add the [type-perfect package](https://github.com/rectorphp/type-perfect/) to your project:
```bash
composer require rector/type-perfect --dev
```
Run PHPStan to start improving your code. Add sets one by one on the go, fix what you find helpful, and ignore the rest.
Happy coding!
-----------------------------------------
## 5 Tricks to Write Better Custom Rules
Perex: Rector and its [extensions](https://github.com/rectorphp/rector/?tab=readme-ov-file#empowered-by-community-heart) already consist of many rules for PHP and Framework upgrades, improving code quality and type coverage. However, you may have your own needs - that's when you need to write your own custom rules.
There is documentation for how to [write custom rules](https://getrector.com/documentation/custom-rule), but the following tricks can help you more.
## 1. Decide What `Node` to be Changed **before** vs **after** that is Needed
There are usually 2 kinds of `Node` instances that you can use:
- `PhpParser\Node\Expr`
- `PhpParser\Node\Stmt`
That will ensure the node and its structure are always correctly refreshed after refactored to avoid errors:
```bash
Complete parent node of "PhpParser\Node\Attribute" be a stmt.
```
In some cases, it may be ok not to refresh the node, e.g.:
- `PhpParser\Node\Name`
- `PhpParser\Node\Identifier`
- `PhpParser\Node\Param`
- `PhpParser\Node\Arg`
- `PhpParser\Node\Expr\Variable`
The list is in [`ScopeAnalyzer::NON_REFRESHABLE_NODES` constant](https://github.com/rectorphp/rector-src/blob/650dcc6394c6df206772350e525311f8080e5077/src/NodeAnalyzer/ScopeAnalyzer.php#L19).
To know what `Node` we need to change, you can see the visual [documentation of PHP Parser nodes](https://github.com/rectorphp/php-parser-nodes-docs). You can also use [Play with AST Page](https://getrector.com/ast) with visual and interactive code. We have a blog post at [Introducing with AST Page](https://getrector.com/blog/introducing-play-with-ast-page).
## 2. Utilize `dump_node()` and `print_node()` for Debugging During Writing
When you're on deep `Node` checking, you can directly get the `Node` structure or printed `Node` via Node utility:
```php
dump_node($node); // show AST structure
print_node($node); // print content of Node
```
## 3. Return `null` for no change, the `Node` or array of `Stmt` on Changed
For example:
```php
public function refactor(Node $node): null|Node|array
{
if ( /* some condition */ ) {
return null;
}
if (/* some condition */) {
// make a change to Node
return $node;
}
// return array of nodes, which should be an array of Stmt[],
//e.g., insert a new line before existing stmt
return [
new \PhpParser\Node\Stmt\Nop(),
$node,
];
}
```
## 4. Return `NodeTraverser::REMOVE_NODE` to remove the `Stmt` node
For example, you want to remove `If_` stmt:
```diff
-if (false === true) {
- echo 'dead code';
-}
```
You can return `\PhpParser\NodeTraverser::REMOVE_NODE`, eg:
```php
use PhpParser\Node\Expr\BinaryOp\Identical;
use PhpParser\NodeTraverser;
use PhpParser\Node\Stmt\If_;
use Rector\PhpParser\Node\Value\ValueResolver;
public function __construct(private readonly ValueResolver $valueResolver)
{
}
public function getNodeTypes(): array
{
return [If_::class];
}
/**
* @param If_ $node
*/
public function refactor(Node $node): ?int
{
if (! $node->cond instanceof Identical) {
return null;
}
if (! $this->valueResolver->isFalse($node->cond->left)) {
return null;
}
if (! $this->valueResolver->isTrue($node->cond->right)) {
return null;
}
return NodeTraverser::REMOVE_NODE;
}
```
Then, the `If_` node will be removed.
## 5. Return `NodeTraverser::DONT_TRAVERSE_CHILDREN` to skip `Node` below target `Node`
For example, if you need to check the `Array_` node but don't want to check if the `Array_` is inside `Property` or `ClassConst` `Node`, you can return `\PhpParser\NodeTraverser::DONT_TRAVERSE_CHILDREN`.
That way, Rector [skips below target `Node` on current `Rector` rule](https://github.com/rectorphp/rector-src/blob/6bd2b871c4e9741928fb48df3ca8e899be42be81/src/Rector/AbstractRector.php#L269-L291).
so you have the following target node types:
```php
use PhpParser\Node\Stmt\Property;
use PhpParser\Node\Stmt\ClassConst;
use PhpParser\Node\Expr\Array_;
// ...
public function getNodeTypes(): array
{
return [
Property::class,
ClassConst::class,
Array_::class
];
}
```
Then, you can check the following:
```php
use PhpParser\NodeTraverser;
// ...
/**
* @param Property|ClassConst|Array_ $node
*/
public function refactor(Node $node): null|int|Node
{
if ($node instanceof Property || $node instanceof ClassConst) {
// Array_ below Property and ClassConst won't be processed
return NodeTraverser::DONT_TRAVERSE_CHILDREN;
}
// process Array_ node that is not below Property and ClassConst
}
```
So, it will:
* skip below the current `Node`
* on current Rector rule only
Otherwise, it will be processed.
We hope these tips will give you confidence to experiment with more advanced rules and save even more time with automated work.
Happy coding!
-----------------------------------------
## Migrate DateTime to Carbon
Perex: Carbon is an excellent library for working with dates and times in PHP. It's being used [by Laravel](https://medium.com/@mhmmdtech/datetime-handling-in-laravel-by-carbon-39e032a15a15) as the default date-time library.
But it's not only syntax sugar wrapped around the `DateTime` class. It provides a reliable way to test your code that depends on exact dates and times.
The [Carbon package](https://github.com/briannesbitt/Carbon) brings a practical API to work with dates:
```php
// the DateTime
$date = (new \DateTime('today +20 day'))->format('Y-m-d');
```
```php
// the Carbon way
$date = \Carbon\Carbon::today()->addDays(20)->format('Y-m-d')
```
No need to remember strings like "+days", "-weeks", or "a month". You can use the method API directly:
```php
$date = \Carbon\Carbon::now()->addMonths(2);
```
## Reliable Tests Under Control
Where does the Carbon package bring real value? Tests that depend on the exact date. Native `DateTime` depends on the timezone of the server or commiter. If they get into conflicts, "+1 day" can yield different results and make tests fail. Then, we have to find out if that's a false positive or a real problem. That's not the tests we want to debug.
Instead, we can [mock the "now" directly](https://medium.com/@stefanledin/mock-date-and-time-with-carbon-8a9f72cb843d):
```php
// Don't want this to happen so mock now
Carbon::setTestNow(Carbon::createFromDate(2000, 1, 1));
// comparisons are always done in UTC
if (Carbon::now()->gte($internetWillBlowUpOn)) {
die();
}
// Phew! Return to normal behavior
Carbon::setTestNow();
```
That way, we **make a date constant through our test suite** for any developer or server.
## How to Migrate DateTime to Carbon
To make this work, we have to replace our `DateTime` instances with `Carbon` instances:
```diff
-$start = (new \DateTime('today +1 day'));
+$start = \Carbon\Carbon::today()->addDays(1);
-$end = (new \DateTime('today +30 days'));
+$end = \Carbon\Carbon::today()->addDays(30);
```
That's where Rector comes in. The next release will ship a new carbon set that will handle migration for you:
```php
withPreparedSets(carbon: true);
```
This way, your tests will become reliable, and you won't have to wait for another developer to trigger CI at the "right time" again.
This set is still fresh, so if it misses some cases, [let us know](https://github.com/rectorphp/rector/issues).
Happy coding!
-----------------------------------------
## 7 Traits of Successful Upgrade Companies
Perex: For the past 10 years, we've been working with over 50 companies on legacy PHP upgrades. We've already written about our approach and technical process of upgrades.
We've met many companies we helped turn their projects from legacy projects that were hard to work with to code that is full of joy, safety, and smoothness.
Over time, we've noticed that those companies' traits often repeat and are shared with other similar companies. We'd like to share these observations so you can mimic them for your company and make your upgrade project successful.
## 1. Practical Approach
Companies that are successful with finished upgrades are highly practical. They minimize the number of shallow work, meetings, and paperwork.
We have a first meeting to learn about goals. Then, we do an intro analysis. This is followed by a second meeting to discuss results, and then we jump right in to start. The most successful companies contact us with a clear vision and secured budget. They know [how we work](https://getrector.com/hire-team#process), and they're ready to start.
We'll likely start hands-on upgrade work not even 40 days after first contact. We start with 10 hours/week. In the second month, when they see results, they ask to increase to 40-60 hours/week.
## 2. Space for Deep work focus
Some PHP projects become hard to work with because there is something deeply wrong with them. It's like a traumatized person who needs help out of depression. Their problems will not disappear after watching a few YouTube videos on self-help.
In the same way, legacy code will not be upgraded by reading a few articles about legacy upgrades. Otherwise, these projects would already be fixed by their own developers' team.
Successful companies understand this, and they're willing to invest and give space for deep work exploration. **They know that it takes time to understand the complexity of the problem** and that we need to think about the problem deeply before coming up with a solution.
For example, a couple of projects we worked on used synonymous testing tools to test the same logic—there was PHPUnit, PHPSpec, and Codeception at the same time—all using unit testing of the same code.
It's like having 3 cars to drive to work everyday. This makes tests hard to read, as you have to switch context.
We could quickly delete 2 of them or ignore them (worked so far, right?). None of these would be a sustainable solution, only postponing the problem.
Instead, we took time to think deeply and came up with solution that:
* keeps the value of tests
* and reduces complexity from 3 to 1
We migrated all tests to PHPUnit using custom Rector rules.
## 3. Trust
Companies that hire us and end up with successful upgrades are those that strongly trust us. This doesn't mean they believe everything without critical thinking, but they listen to us. When we agree on an upgrade plan, they give us confidence to execute it.
We don't have meetings to rethink, we don't discuss for weeks in PR, and they don't ask us about a line of code. To be honest, they accept 95 % of PRs with "Approve."
If we feel our clients trust us, we react responsibly and take projects seriously like our own. We come up with solutions that are harder for us to prepare and execute but will bring more value to the project in the future.
## 4. Fast Merging
This is related to trust. If we trust our clients and our clients trust us, we also trust the code. It's our job to increase the trust in the code itself by anyone working with it. If a junior comes to the project, they have to be confident they can make a change and it will not break the project.
Successful clients know this, and they embrace the change by merging it as soon as possible. It doesn't mean we get merged withing an hour without review and hope for the best. It means we work on a responsible CI/CD pipeline that will tell us if something is wrong. **If CI passes, the PR can be merged in 1-2 days**.
If the average time to merge is over 1-2 weeks, this is usually a sign of trust issues that will result in an upgrade stale.
## 5. Focus on Long-term vision
In the intro analysis, we present the upgrade battle plan for the next 6-12 months. It's a fairly comprehensive report with 30-40 areas, but it's not the bullet points they discuss—it's the vision they're interested in.
Successful companies don't want only to improve their code quality. They want to attract talented developers, they want to deliver features fast. They want the **codebase to serve them, not the other way around**. They focus on long-term sustainability in the next 3-5 years.
## 6. Single Responsible Person
The successful companies we work with **have exactly one responsible person in charge of the upgrade**. When we have a meeting, the person is there. If we're proposing a change of direction, we mention the person. If we want to plan for next year, we speak with the person.
That way, we know there is no loss of information, and our client knows they're informed about everything.
## 7. Will to Radical Prune
Every gardener knows that if you want your apple tree to grow, you must prune branches yearly. You have to cut branches that are weak, growing upwards or downwards, or too close to each other. It takes courage to do so—if a tree is neglected for a couple of years, up to 80 % of its branches have to be cut. As a result, the tree will grow juicy, full apples rich in vitamins.
"Perfection is achieved not when there is nothing more to add,
but when there is nothing left to take away."
Successful companies understand this principle. Remember the [Twitter firing 80 % of stuff](https://edition.cnn.com/2023/04/12/tech/elon-musk-bbc-interview-twitter-intl-hnk/index.html) to deliver more features than ever before.
Legacy projects are similar to overgrown trees. In the first couple of months, we radically prune packages that bring marginal value to the project.
For example, some projects we started with had PHPStan, SARB, Psalm, ppm,d, and a colossal baseline. That's 5 different approaches to static analysis with little extra value.
It's like paying taxes in 5 countries for the same income. It takes a lot of pruning to get to a bare PHPStan setup with a single config. Successful companies are willing to do so and actually replicate this approaches in other places themselves.
Last but not least, **people from successful companies care about the company**. They want the best for the project in the future, not to fast-tick the KPI and bonus in a salary. If we look at the headlines of this post, it's like a recipe for good friendship.
Do you recognize yourself in some of those traits? Would you like to learn the other ones? [Let us know](https://getrector.com/contact); we're ready to help.
Happy upgrading!
-----------------------------------------
## Introducing Play with AST page
Perex: Do you want to know what the AST structure from a PHP Source code? The getrector.com will help you with new interactive form.
Even for experienced Rector users, manual trial and error may be needed to decide which `Node` to use when creating a new Rector rule or refactoring an existing one. We are here to help you so that you can play before deciding which `Node` you want to use to refactor your code.
We created a new "Play with AST" page for you: [https://getrector.com/ast](https://getrector.com/ast), that you can:
* Insert a PHP source code
* Get All AST structure from the source code
* Get Only AST structure for part of the code (click a part of the PHP source code)
* Get node types can be used on `Rector::getNodeTypes()`
You can start with open [https://getrector.com/ast](https://getrector.com/ast), and insert a sample code, for example:
```php
Then, you can click the `Show me` button, then you can get the AST structure:
The source code on the first block is clickable; for example, you can click the "return" part:
Then, you will get the AST structure of return and node usage that can be used for creating custom rules:
That's it!
Happy coding!
-----------------------------------------
## Upgrade Legacy Framework or Change it for Another?
Perex: Would you upgrade your Nokia 3310 to a newer Nokia or change it to an iPhone with USB-C? Would you upgrade your old Ford Fiesta to a newer Ford or change it to a Tesla Model 3? Would you upgrade your house's wooden windows for better wood or use plastic 3-layers?
If you use any PHP framework, it doesn't mean you have to stick with it for the end of your project life. The upgrade or change can be both valid options, depending on your project state, PHP community in your country or version.
When we get reached by clients with "we need to upgrade framework X from version 1 to 2", we don't go for the upgrade immediately. Instead, we ask them questions about their particular context:
* How fast does framework X help you deliver business value?
* How easily can you hire enough developers with framework X knowledge?
* What is the most used framework by your dev team in their hobby projects?
* What meetups do your developers go to?
It might sound like we ask technical questions that have nothing to do with productivity and business value. After all, we upgrade mainly to get fast, deliver value, and quickly adapt to beat the competition.
## Newer Technology = Faster Learning
Would you rather have 10 developers with Windows 98 or 3 developers with MacBook M3?
**Tooling + technology + community = productivity**
Those 3 developers go to an IT meetup in your city every month. They share their know-how, attract other talented developers, and can join your team. This talent further improves the technical state of your project, making it faster than the competition and more tolerant of future challenges.
These new developers attend the meetup, share their great experiences with your company, and attract other talents. The circle continues.
This way, you can hire great developers without heavy HR investments by supporting your technical stack. We've seen many startups successfully using this approach, followed by a successful exit to a global fund.
## Active Hiring Pool
The goal of the upgrade is not to have a great codebase at the moment. **The goal is to be competitive in the next 3-5 years, prepare for any unexpected changes, and provide a superior experience for our customers. That's why they buy products or services from us, which keeps the company growing.
There is a lack of developers who bring value to your project. This is obvious for any growing company that needs to hire more developers. That's why focus on a hiring pool—a group of developers you can hire from. These developers meet your conditions, and you meet their payment expectations. Like a fish pool, this pool has to be growing and healthy.
What does this mean in practice? For example, your project uses the latest Symfony 7.x. But your job applicants know only Laravel. Of course, you can teach them different technology than they use daily, but they won't be productive. It's more likely your developer count will head to zero as your hiring pool is dry.
This applies vise versa: if you use the latest Laravel, but all you can hire from are Symfony developers, your project will be unable to grow. **So always consider your specific hiring pool first**.
## Upgrade or Change?
We can move to the technology once we consider your hiring pool.
* There is a group we call "dying frameworks". Those are frameworks that very few people would choose to build a new project in 2024.
* The other group is "living frameworks." Most PHP developers would choose one of them to build a new project—not because they have known it for 10+ years, but because they find it easy to use and fast to build the desired project. For example, we have known Nokia for 25 years, but it would not be the phone to buy as a gift to your friend.
If you're a business owner/CTO and not an active part of the online/offline PHP community, it can be hard to know which is which. Your company has used the *framework X* since the beginning, so it's only reasonable to keep using it.
**There is a quick way to guesstimate whether framework X belongs to the "dying" or "living" category** The download statistics show that if framework X is popular, most developers will know about it, use it, learn it, and improve it.
### How to find out downloads stat numbers?
* Go to https://packagist.org/
* Type your framework name into the search bar and press Enter
* Click on the first package
* Then click on "Installs" in the right column
For example, this year, we wrote about [CakePHP migration](/blog/what-to-expect-when-you-plan-to-migrate-away-from-cakephp-2). Let's see its [download stats](https://packagist.org/packages/cakephp/cakephp/stats):
We also wrote about [Phalcon framework](/blog/how-to-upgrace-phalcon-project). What are [the stats in](https://packagist.org/packages/phalcon/cphalcon/stats) there?
Once we have stats for your *framework X*, we need **a stable reference to compare to**. We use Symfony or Laravel stats. Here, let's check [Laravel stats](https://packagist.org/packages/laravel/framework/stats):
Now we have the data to compare. What to look at?
* **absolute daily/monthly downloads** - this will give you an idea if your framework is popular or not
* **trending line** - how healthy community is, is framework growing or not; look for past 1-2 years
## Examples of Decisions based on data
Now that we have data about the hiring pool and the dying or living framework, the last criterion is the framework's age.
**How old is the version your project uses?**
As we wrote in [How to Upgrade Zend Legacy Project](/blog/how-to-upgrade-zend-legacy-project), it's cheaper to completely change from an ancient framework like Zend 1 to the latest Laravel/Symfony. But if your project uses Zend 3, sticking with it and upgrading is generally cheaper.
Here, we'll share a few practical examples of how to evaluate these data so you can better decide whether to upgrade or change.
## Usecase A
* your framework is CakePHP 2
* the hiring pool is active with Symfony and Zend developers
* Symfony has more downloads than Zend
* your developers use Symfony or Laravel in their hobby projects
Upgrading CakePHP 2 to the CakePHP 4 doesn't make sense, as you need to have new CakePHP developers to hire from.
You can choose Symfony or Zend developers. To decide, we check the download stats. Symfony has more downloads than Zend. Last but not least, we verify with the team. They'd prefer any of Symfony or Laravel.
**Symfony is the best choice.**
## Usecase B
* your framework is Symfony 5.0
* the hiring pool is active with Symfony and Laravel
* Symfony and Laravel have quite similar downloads
* your developers use rather Laravel in their hobby projects
Symfony is a living framework we can stick with. Your hiring pool has Symfony developers, so we're suitable to grow. The latest Symfony version is 7, so we'd only have to upgrade 2 major versions = doable. Despite your developers prefer Laravel, **we'd stick with Symfony**.
## Usecase C
Same as above, but:
* your framework is Symfony 2.x
Here, we want to demonstrate when it might be **cheaper to change frameworks, depending on the age of your current version**. Symfony 2 was released in 2011 and has an entirely different architecture than the current Symfony 7. It's 13 years technology gap and 5 major versions to upgrade.
On the other hand, **your developers developers prefer Laravel**. That means your project could grow further with Laravel developers. The change from Symfony 2 to Laravel 11 would be only a single step:
* Symfony 2 → Laravel 11
Here, we'd choose a framework **change from Symfony to Laravel** as a cheaper and more beneficial solution.
This is how we discuss with our clients about the decision to upgrade or change the framework entirely.
Happy coding!
-----------------------------------------
## How to Upgrade Zend Legacy Project
Perex: Zend is the second most requested project upgrade in our client group and online forums. If you have no idea where to start, how should you approach the Zend upgrade? What criteria should you consider? What are the alternatives?
Zend is one of the oldest PHP frameworks. It's dead and no longer active—in 2024, no new projects will be built on it.
There is a fresh Reddit thread called ["Who migrated codebase from Zend framework?"](https://www.reddit.com/r/PHP/comments/1cibspi/who_migrate_codebase_from_zend_framework/), where authors ask about other people's experience with Zend framework migration. One reply mentions a 5+ year migration, which sounds like a terrible waste of resources.
We want to prevent such a waste of resources in future Zend migration (done with us or on your own), so we'll share our approach to Zend and what turned out to be an important **criteria for a successful upgrade from a business point of view**.
## Main Criteria
If you're running on Zend and want to upgrade your stack, there are 2 most important criteria to consider.
* your Zend version
* your Zend-developer community you're hiring your PHP talents from
Let's look at the Zend version first.
## Zend 3
Congrats! This is the best Zend version you can have. Zend 3 itself was more or less only rebranded in 2018 to Laminas and continues its development. Here is how to [migrate from Zend to Laminas](https://docs.laminas.dev/migration/).
It's also worth reading [Zend Framework 3: A retrospective](https://getlaminas.org/blog/2020-03-09-transferring-zf-to-laminas.html) from 2020 to understand what's ahead of you after you migrate to Laminas.
## Zend 2
If you have Zend 2, deciding what to do next is more complex.
You have 2 options:
First, similar to the above, upgrade your project to Zend 3. Once you're on Zend 3, you can change to Laminas. **The huge downside of this approach** is that there is no Zend 2 to 3 upgrade set. You'd have to do this manually or with Rector's help by writing custom rules.
Zend 2 and 3 are quite different, with some packages removed and some newly added - you can read more about it in the documentation - [Upgrading to Zend 3.0
](https://docs.zendframework.com/zend-mvc/migration/to-v3-0/).
We strongly recommend hiring a senior developer who's done such a migration as a consultant. Otherwise, you'll easily waste a year of resources discovering the differences between those 2 versions.
Second, you can change to another framework like Symfony/Laravel. Surprisingly, such a change is **cheaper to perform**, thanks to the vast Symfony/Laravel ecosystem and well community support.
Instead of a 2-step migration, going Zend 2 to 3, then Laminas, this is only a single step. The work is done in parallel, so feature [delivery keeps running](/blog/success-story-of-automated-framework-migration-from-fuelphp-to-laravel-of-400k-lines-application). The only work that needs to be done is to identify MVC patterns in your specific Zend 2 project. MVC patterns in Symfony/Laravel are very well documented, and Zend 2 features are a major subset of them.
## Zend 1
Zend 1 is the worst-case scenario. Zend 2 is an almost complete rewrite compared to Zend 1, and the architecture has moved from magical autoloading to a solid framework. But if it's planned properly, the upgrade can be the same complexity as the upgrade of Zend 2.
Again, you still have 2 options.
Go from Zend 1 to Zend 2, Zend 2 to Zend 3, and then to Laminas. If you decide to go for this, hire a consultant and prepare a three-year budget.
It's like upgrading from Windows 3.11 to 95, from 95 to 2000, from 200O to NT, then to XP and Windows 11. The difference is so huge that the A to Z jump is cheaper and more effective than going through all the versions.
The second option is to change Zend 1 to Symfony/Laravel. This requires identifying MVC patterns in Zend 1 and then writing custom rules to migrate to Symfony/Laravel. Based on our experience, Zend 1 has fewer features than Zend 2, so mapping to Symfony/Laravel is easier as there are fewer features to migrate.
## What is the community you Hire From
But, before you decide on a project upgrade of framework migration, ask your PHP team and HR team essential but often missed questions:
What is the PHP community like around us?
What developers can we hire at a reasonable price?
What PHP framework is well supported in our city if you want your developer to grow?
I came to Prague in 2014, looking for a PHP community. I only found "Zend meetup", so I went there to talk about PHP and other frameworks. It turned out that nobody there uses Zend anymore. It's only the old name of the meetup. The majority was using Symfony.
It didn't mean the Symfony was a better framework per se. It meant it was easier to find a Symfony developer in that city in that year.
* **If you can hire developers who know your framework in your city**, maybe you can find talented 10x developers who will take your project to the next level.
* **If you struggle to hire a developer who knows the framework you use**, you'll have to spend resources to teach them. They'll most likely misuse the framework in a non-standard way, so other developers who know the framework won't understand it. They'll make wrong design choices that will influence the future, maintainability, and costs of your project.
The PHP community around you is a pool of talent you can hire from. This talent will shape the future of your project in the next 3-5 years.
If your pool is dry, consider changing to another active pool. If your pool is active, stick with it.
Happy coding!
-----------------------------------------
## How to Upgrade Phalcon project
Perex: Phalcon is a PHP framework that is written in C and is known for its speed. It was created in 2012 and it has own PHP-like language - Zephir. After CakePHP, this is the most requested framework to handle. We though we'll share your options if you want to upgrade your project running on Phalcon.
In 2010-2015 there was a boom of PHP frameworks - FuelPHP, CodeIgniter, CakePHP or Yii. While you can still download these packages over composer, they are no longer active nor growing. Phalcon belongs to this group.
## Finished Upgrade != Successful Upgrade
We did a huge Phalcon upgrade to from version 4 in 2019 lead by [Kerrial Newham](https://www.linkedin.com/in/kerrial-newham-030484176/), and we used to provide [upgrade set in Rector](https://github.com/rectorphp/rector/pull/2437). It was quite a challenge, because Phalcon project is not written in PHP, but in slightly different syntax. We had to create a custom parser for it.
Yes, we helped our client to use a newer version... but:
We've completed the upgrade task as we agreed.
but they still struggled with the same issues as before.
Our mission is not only to upgrade projects but also to make them **cheaper and easier to maintain in the future**and help our clients grow in the long term.
They **could not hire new developers that use Phalcon**, because they were so rare they were costly.
If they did hire a junior developer, they could not teach them new technologies like Vue, Redis, RabbitMQ, notifications, or sockets. They were stuck in the past, only with a newer version of the same framework.
## Why Phalcon is Not a Good Choice in 2024
Why was that? The Phalcon framework - like many others - did not get enough **traction to create an active community**. In 2020, there was [announced Phalcon 6](https://en.wikipedia.org/wiki/Phalcon_(framework)) as a native PHP framework like every other, but it's still not released—the composer package [has 77 downloads over the past 3 years](https://packagist.org/packages/phalcon/phalcon/stats).
The same goes for cphalcon package, which has, on average [~ 400 daily downloads](https://packagist.org/packages/phalcon/cphalcon/stats):
That means even if you upgrade to the latest Phalcon 5 version, your next upgrade will be even harder - because of the flip from C extension to native PHP code.
It's like getting a mortgage with the following deal:
when you finally pay it off,
we'll reset it, and you'll start paying again.
We don't mean to sound negative, but we want to help you make the best decision for future of your project. It's better to work with realistic data and make informed decisions.
## What is a Successful Upgrade?
Would you buy a house with a beautiful facade, but the inside is still from the 1950s? There is no internet, poor electricity, and coal heating. Probably not. The same goes for software. The facade is the framework; the inside is the code. If you upgrade the facade but not the inside, you're not getting any extra value.
Our priority is to **get our clients to sustainable and maintainable codebase**that will accelerate the business growth. Even after we're long done, **your code should work for you**, so your company can:
* hire new developers at a reasonable price
* have free resources to educate them - videos, articles, or book
* have a framework that attracts new developers with talent
* have cheap and fast adoption of new technologies
This often depends on the community around the framework. If your developers can't get to a meetup or a conference of your framework, they can't learn an effective way to use it and become stuck in the past. So will your project.
## Should we Rewrite the project?
Saying that, the only option seems to rewrite the project into a modern framework like Symfony or Laravel, right? Not necessarily. Much cheaper than [known "rewrite from scratch" mistake](https://www.joelonsoftware.com/2000/04/06/things-you-should-never-do-part-i/), is to switch the framework.
Various PHP MVC frameworks are sometimes more similar than version 2 or 3 of the same framework. That means switching from Phalcon to Symfony/Laravel is easier than from Phalcon 4 to Phalcon 5.
The reason is that we migrate framework X to Symfony/Laravel so often,
it's getting easier and cheaper every year.
While upgrade of non-active framework X to X+1 is done once in a blue moon,
and is always a challenge from scratch, often costly.
## Migrate to Symfony/Laravel
Since migration to Symfony/Laravel is requested so often that we have a boilerplate ready, and can fill in edge cases specific toto your project.
We mainly handle these migrations behind NDA, but we can share some stories:
* In 2019, we've done a middle-size production project [Nette to Symfony migration](https://tomasvotruba.com/blog/2019/08/26/how-we-migrated-54-357-lines-of-code-nette-to-symfony-in-2-people-under-80-hours) under 80 hours - see [Rector ruleset](https://github.com/deprecated-packages/rector-nette-to-symfony)
* In 2022, Rajyan shared a story of [400 000 lines project migration from FuelPHP to Laravel](https://getrector.com/blog/success-story-of-automated-framework-migration-from-fuelphp-to-laravel-of-400k-lines-application)
* In 2023, we created a ruleset to migrate [Symfony to Laravel](https://github.com/TomasVotruba/laravelize); templates included
Both Symfony and Laravel communities are active with 4-6 conferences every year:
* [Laravel conferences](https://laravel-news.com/events)
* [Symfony conferences](https://live.symfony.com/)
## Migrate your Project to Success
Despite your first initial choice to "only upgrade" a single version, the framework migration is a cheaper and faster solution that brings you long-term success.
That way, your facade and interiore will be modern and attractive to new developers who will be happy to take care of your project.
Happy coding!
-----------------------------------------
## Rector 1.0 is Here
Perex: The stable Rector version is here. It was about time and we've done all planned changes by the end of 2023.
What could be better time and place to release a stable Rector than live on stage during talk:
.@VotrubaT just release 1.0 of Reactor 🙌🙌 #LaraconEU pic.twitter.com/Z0D9omHiZF
— Christoph Rumpel 🤠 (@christophrumpel) February 6, 2024
While this release brings stable API, it will be easier to stay up to date as well. The 1.x versioning behaves as expected with `composer update` (compared to special 0.x).
Our main focus **is on improving developers experience**. **This release brings new features that help with custom rules writing, adding Rector to CI and adding Rector to any legacy project in general**.
Some features are partially available in previous version, but we'd like to highligh them because since 1.0 you can use them all together.
## Zen Config with Autocomplete
If you run Rector for the first time, it will create a `rector.php` config with your project paths for you. In past we used various class constants references to add commonly used rule sets. This required knowledge about these classes and was often missed.
We've changed this to work with single configuration class. It provides autocomplete for available sets, including attributes:
```php
use Rector\Config\RectorConfig;
return RectorConfig::configure()
->withPreparedSets(codeQuality: true, codingStyle: true)
->withAttributesSets(symfony: true, doctrine: true)
->withPaths([
__DIR__ . '/src',
__DIR__ . '/tests',
])
->withRootFiles();
```
## PHP Sets Automated
To keep up to date with you PHP, now you can use single method:
```php
use Rector\Config\RectorConfig;
return RectorConfig::configure()
->withPhpSets();
```
It learns about PHP version from your `composer.json`:
```json
{
"require": {
"php": "^8.0"
}
}
```
...and will always keep sync with your required PHP version. No need to double check `rector.php` configuration anymore.
## Streamline Integration to Projects
We're also adding 2 experimental methods, that make Rector integration to new projects easier. Before, you could run whole type declaration or read code set, see 1000 changed files and rather close it being overwhelmed. Instead, we want to take it slow, as we do with our custom upgrades as well:
```php
use Rector\Config\RectorConfig;
return RectorConfig::configure()
->withTypeCoverageLevel(10)
->withDeadCodeLevel(10)
```
Now you can improve your code base one rule at a time. The rules are sorted from the easiest to integrate, e.g. add `void` to closure, to more complex one. That way you can improve your code base in your own pace. We're collecting feedback on rule order, so the levels will likely change.
## New and Improved Commands
We added a new command to generate bare custom rule files and structure for you:
```bash
vendor/bin/rector custom-rule
```
Call the command, type the rule name and rule, its test and composer autoload is generated for you. So you can focus on the contents of `refactor()` method.
We also improved the `setup-ci` command, that generates Github and Gitlab CI setup files, so you can let Rector work for you:
```bash
vendor/bin/rector setup-ci
```
The command handles generic setup for you and then guides you to register needed access.
Last but not least, we've **updated [the book to run Rector 1.0](https://leanpub.com/rector-the-power-of-automated-refactoring/)** as well, so you can enjoy the latest features and improvements.
Enjoy first major Rector release and let us know how you use it!
Happy coding!
-----------------------------------------
## What to expect when you plan to Migrate Away from CakePHP 2
Perex: What is the most requested project we get from our clients? PHP upgrade, Symfony upgrade, framework switch... yes, these belong to the most common ones. But one of the requests is far beyond the most requested one. From CakePHP 2 to Symfony/Laravel.
*Disclaimer: This post is no rant about any framework. It's about the process of migration that our clients often request.*
Most companies can handle PHP, Laravel, or Symfony upgrades themselves by using bare Rector. But the CakePHP 2 migration is a different story. It's a framework feature-wise similar to Symfony/Laravel, so it's an obvious target to migrate to one of them. We get, on average, 3 requests a year and can only handle some of them. We thought we'd share the process with you so you can start yourself.
So what is the problem with CakePHP 2 then?
## The PHP 5.2-feature-lock
The CakePHP 2 was written around the era of Zend 1, when standardized autoload was not a thing. Some of you remember the `Under_score` approach that was pre-step to namespace separator `\\`. Finally, PHP 5.3 first introduced namespaces which was released in 2009. A lot of frameworks went for a back-ward compatibility approach and didn't use namespaces for a long time.
That's why a typical CakePHP 2 class looks like this:
```php
App::uses('Controller', 'Framework');
class ChatGPTController extends Controller
{
}
```
It's full of `App::uses()` calls.
As I write in [8 Steps You Can Make Before Huge Upgrade to Make it Faster, Cheaper and More Stable](https://tomasvotruba.com/blog/2019/12/16/8-steps-you-can-make-before-huge-upgrade-to-make-it-faster-cheaper-and-more-stable/), the first prerequisite before starting any upgrade is to have classes autoloaded with PSR-4. In some projects, this is a matter of weeks, using [smart tooling](https://github.com/symplify/easy-ci).
In the case of a CakePHP 2, it's a real challenge. We love challenges in the Rector team, so let's dive into it.
What is happening here?
```php
App::uses('Controller', 'Framework');
class ChatGPTController extends Controller
{
}
```
We created a controller class that extends some Controller classes. There is some kind of PHP 5.3 use-import-like call. How would such code look written in PHP 5.3?
```php
use Framework\Controller;
class ChatGPTController extends Controller
{
}
```
The `Framework\Controller` class does not exist; it is not autoloadable and thus [invisible to PHPStan](https://github.com/phpstan/phpstan/discussions/5257) and static analysis. There is a [PHPStan extension](https://github.com/sidz/phpstan-cakephp2) that can help you, but it's not a long-term solution.
## Step 1: Make Classes Autoloadable
The first step is to make all classes autoloadable with PSR-4. -+
Create 2 [custom rules](https://getrector.com/documentation/custom-rule) to handle `App::uses()` and `App::imports()` calls:
```diff
-App::uses('Controller', 'Framework');
+use Framework\Controller;
class ChatGPTController extends Controller
{
}
```
This looks simple enough, right? There is a catch. Now we must find the `Controller` class in a `Framework` "namespace" and actually add the namespace to the class:
```php
// src/Framework/Controller.php
class Controller
{
}
```
CakePHP 2 uses paths to assume the namespace. So, the class `Controller` located in the `Framework` folder is considered to be in the `Framework` namespace. The same class located in the `Admin` directory would have an `Admin` namespace.
Now we create a 3rd rule, that autocompletes the namespace:
```diff
+namespace Framework;
// src/Framework/Controller.php
class Controller
{
}
```
Now we use
## Step 2: Move away from CakePHP 2 autoloader
To make the PSR-4 autoloader work, we have to get rid of the magic CakePHP autoloader that could give us a false sense of security. The CakePHP 2 `App::load()` method states following:
```php
class App
{
/**
* Method to handle the automatic class loading. It will look for each class' package
* defined using App::uses() and with this information, it will resolve the package name to a full path
* to load the class from. The file name for each class should follow the class name. For instance,
* if a class is named `MyCustomClass` the file name should be `MyCustomClass.php`
*
* @param string $className the name of the class to load
* @return bool
*/
public static function load($className)
{
// ...
}
}
```
We want to ensure all our classes are loaded with PSR-4, so we have to cut off any autoloader that string classes could fall back to. We can do that by removing the places where CakePHP registers the autoload. This can be located in various places, depending on the project.
**But we must be careful here**, as removing such code could break something else in CakePHP internals that still depends on the magic autoload.
## Step 3: Make sure CakePHP 2 is Part of Your Project
This is where we come to the next important step. We might have removed the old CakePHP 2 code that fakes the `use` and `namespace` calls, but the internal use of the framework still depends on it.
We may have to visit the internal working of the CakePHP 2 codebase and refactor it.
To enable that, make sure the `CakePHP 2` library is not a dependency in your `composer.json`, but located directly in `/library/cakephp2` directory. This way, we can easily refactor the codebase without breaking the framework.
Look for `ClassRegistry` static calls, and beware the [special `Model` type](https://github.com/sidz/phpstan-cakephp2/blob/fa4edd9fc56b81a28576342b753316b2431a8253/src/ClassRegistryInitExtension.php#L61-L67).
```php
ClassRegistry::init()
```
This piece of work is very individual and depends on the project. It depends on what parts of the framework you use and how you use them. Creating a custom rule would be overly complex and not satisfy the needs of every project.
Take a deep breath and start refactoring. Give special care to plugins.
## Step 4: Prepare Rector Migration rules
We've done much hard work - all classes autoloaded with PSR-4, and the CakePHP 2 autoloader was removed. The class names are unique and use PHP 5.3 namespaces. It's time to prepare custom framework migration rules from CakePHP 2 to Symfony/Laravel.
Depending on your project, this may include a custom rule:
* to refactor the controller to Symfony/Laravel
* to refactor the model to Doctrine/Eloquent
* to refactor validation to Symfony/Laravel validation attributes and rules
* to refactor views to Twig/Blade
Again, this part is strictly individual and depends on the project. It's a good idea to start with a small, isolated model in the project and see how it goes.
We plan to extend this post as we learn more about the process. If you have any experience with this, please share it with us at [@rectorphp](https://twitter.com/rectorphp).
Happy coding!
-----------------------------------------
## Grab Fresh Book Release with Rector 1.0
Perex: We are thrilled to introduce the latest update to our book, along with long-awaited **Rector 1.0** from February 2024. This release includes 2 new commands, brand new configuration with smart IDE autocomplete, brand new chapter and DX improvements to help you master code refactoring with ease.
We've worked on this release back and forth past 3 weeks and we're excited to share it with you.
We've released the [Rector - The Power of Automated Refactoring](https://leanpub.com/rector-the-power-of-automated-refactoring) book with goal of continuous upgrades. It's been a year since last upgrade, so it's time to step up and deliver fresh 2024 book release.
Key Highlights of this update are:
### New Chapter: Explore "Node Type and Refactor Examples"
Featuring typical `refactor()` use-cases, enhancing your refactoring skills.
## Simplified Configuration
Introducing a minimalist `RectorConfig::configure()` config for smoother setup.
```php
use Rector\Config\RectorConfig;
return RectorConfig::configure()
->withPaths([__DIR__ . '/app', __DIR__ . '/tests'])
->withImportNames(removeUnusedImports: true)
->withPreparedSets(codeQuality: true, codingStyle: true, instanceOf: true)
->withPhpSets();
```
## Convenient Commands
New commands, such as `setup-ci` and `custom-rule`, to streamline your workflow.
## Code Examples in Git Repository
Access a complete code repository at [`rectorphp/rector-book-code-examples`](https://github.com/rectorphp/rector-book-code-examples) for comprehensive learning.
## Improved Visuals
* Enhancements in rule and test file visualization for better understanding.
* Dependency Updates: Keeping pace with technology, we've updated dependencies, including Rector (0.15 → 1.0), PHP (8.0 → 8.2), ECS (to 12.1), and PHPUnit (9.5 → 10.5).
* Clarification: Added a section on differentiating between `Stmt` and `Expr` in the "Creating Your First Rector Rule" chapter.
* Refactoring Insights: Discover use of attributes for more efficient refactoring.
This update empowers you to become a code refactoring expert with the latest 2024 Rector features.
It's available immediately for all existing readers.
If you haven't purchased the book yet, **[grab your copy now](https://leanpub.com/rector-the-power-of-automated-refactoring)**!
Happy coding!
-----------------------------------------
## Modernize Symfony Configs
Perex: Symfony configuration is one of the changes that are difficult to spot until they're removed in the next major version. Then you must Google the "invalid option id error message" and hope for a solution. That doesn't sound like an excellent way to spend your weekend, does it?
Symfony actually adds a deprecation message to those options, but they're not easy to spot.
Today, we'll show you how to spot them with the help of Rector, PHPStan, and one other fantastic tool.
Symfony security is known for major changes in almost every Symfony version. What worked in the past...
```yaml
# config/security.yml
security:
enable_authenticator_manager: true
```
...can be changed or removed.
**This actually does not work in Symfony 7**. We want to be warned early, as soon as the change happens, and without running our code.
Other PHP projects use `@deprecation` annotations that highlight deprecated methods, properties, constants, and classes right in code in your favorite IDE.
How about YAML? It does not, so we first have to migrate to PHP.
## 1. Move from YAML to PHP
It is not necessary to bother with a manual flip. Use [symplify/config-transformer](https://github.com/symplify/config-transformer) to automate the process instead:
```bash
composer require symplify/config-transformer --dev
vendor/bin/config-transformer switch-format config/security.yml
```
It parses YAML, maps it into [PHP format](https://symfony.com/blog/new-in-symfony-3-4-php-based-configuration-for-services-and-routes), and prints it out:
```php
# config/security.php
use Symfony\Component\DependencyInjection\Loader\Configurator\ContainerConfigurator;
return static function (ContainerConfigurator $containerConfigurator): void {
$containerConfigurator->extension('security', [
'enable_authenticator_manager' => true,
]);
};
```
Job done!
If it's your first PHP fluent config, don't forget to [update Kernel to load PHP files too](https://tomasvotruba.com/blog/2020/07/27/how-to-switch-from-yaml-xml-configs-to-php-today-with-migrify/).
Now we have PHP configs, but in reality - it's just another form of array full of strings.
**Symfony can do better, at least since Symfony 5.3**.
## 2. Move from an array of strings to typed PHP objects
Instead of arrays, we want something we can use in our IDE - **fully typed objects with methods autocomplete**.
Are you on Symfony 5.3+? If not, upgrade first with Rector. Then, you can use [Config Builder Classes](https://symfony.com/blog/new-in-symfony-5-3-config-builder-classes).
But there is a catch - it's tricky to tell your IDE and PHPStan about them because **they're not part of the Symfony code**. They're generated on the fly based on your current Symfony version.
To streamline the generating, use `tomasvotruba/symfony-config-generator` tool that detects available extension classes and generates config builder classes for us:
```bash
composer require tomasvotruba/symfony-config-generator --dev
vendor/bin/symfony-config-generator
```
Check the generate classes in your Symfony cache directory:
```bash
/var/cache/Symfony
```
We have config builder classes available and PHP with arrays of string.
### How can we flip those arrays to config builder classes?
Rector to the rescue! Symfony rules for Rector contain one little gem that helps us automate 99 % of work - the `StringExtensionToConfigBuilderRector` rule. Add it to your `rector.php` config:
```php
# rector.php
use Rector\Config\RectorConfig;
use Rector\Symfony\CodeQuality\Rector\Closure\StringExtensionToConfigBuilderRector;
return static function (RectorConfig $rectorConfig): void {
$rectorConfig->rules([
StringExtensionToConfigBuilderRector::class,
]);
};
```
Then run Rector on the config file:
```bash
vendor/bin/rector p config/security.php
```
And voilá - here is your state of art Symfony security config:
```php
# config/security.php
use Symfony\Config\SecurityConfig;
return static function (SecurityConfig $securityConfig): void {
$securityConfig->enableAuthenticatorManager(true);
};
```
*Note: some elements like firewall, roles, etc. will need a separate variable call - PHPStan will navigate you.*
You have just **increased the value of your configs by order of magnitude**:
* full IDE autocomplete of any security configuration
* no more copy-pasting `"firewall"` string and reading Symfony docs options
* you get warnings by PHPStan if we ever make a typo
To make PHPStan work and not complain about missing classes located in `/var/cache`, we have to load classes in PHPStan config:
```yaml
# phpstan.neon
parameters:
bootstrapFiles:
- var/cache/Symfony/Config/SecurityConfig.php
- var/cache/Symfony/Config/Security/ProviderConfig.php
- var/cache/Symfony/Config/Security/AccessControlConfig.php
- var/cache/Symfony/Config/Security/AccessDecisionManagerConfig.php
- var/cache/Symfony/Config/Security/FirewallConfig.php
- var/cache/Symfony/Config/Security/PasswordHasherConfig.php
- var/cache/Symfony/Config/Security/ProviderConfig.php
```
## 3. Harvest full power of Static Analysis
We'd love to work with the Symfony project in such a shape, but wait - there is more.
What we really want is to **get early warnings about deprecations right in our CI** before we decide to upgrade. The configs are unavailable in the Symfony Github repository, as they're generated on the fly.
But when we look at the file in our project:
```bash
var/cache/Symfony/Config/SecurityConfig.php
```
We'll see the `enableAuthenticatorManager()` method is actually deprecated in Symfony 6.4:
```php
/**
* @default true
* @param ParamConfigurator|bool $value
* @deprecated The "enable_authenticator_manager" option at "security" is deprecated.
* @return $this
*/
public function enableAuthenticatorManager($value): static
{
// ...
}
```
Now we have:
* configs in PHP methods
* PHPStan available
* we use a method with the `@deprecated` marker
But that's not enough to let CI help us. One piece is missing... care to guess?
PHPStan provides an extension that reports `@deprecated` elements right in the CI:
```bash
composer require phpstan/phpstan-deprecation-rules --dev
```
*Note: include phpstan/extension-installer if you don't have it yet:*
```bash
composer require phpstan/extension-installer --dev
```
Let's run PHPStan on our config to see the result:
```bash
vendor/bin/phpstan a config/security.php
```
The PHPStan fails and let us know:
```bash
1/1 [▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓] 100%
------ -------------------------------------------------------------------------
Line config.php
------ -------------------------------------------------------------------------
8 Call to deprecated method enableAuthenticatorManager()
of class Symfony\Config\SecurityConfig:
The "enable_authenticator_manager" option at "security" is deprecated.
------ -------------------------------------------------------------------------
```
Now you're covered and can start fixing deprecations before they're gone completely.
Setting up can take a while for the first time, but you'll get quickly into it.
The value returns a thousand folds in the long term as you'll get the best Symfony configuration there is.
Happy coding!
-----------------------------------------
## 5 Common Mistakes in Rector Config and How to Avoid Them
Perex: Rector is becoming a standard tool to automate PHP/package upgrades and code quality improvements. Last month, we crossed 60 000 downloads a day.
Past 2 months, we've also improved CPU and memory performance, making Rector a lighter version.
Yet, even fast and lightweight Rector can get stuck on simple config mistakes. We'll talk about the 5 most common ones and how to avoid them.
We used the following tips when [upgrading the Rector config in Mautic](https://github.com/mautic/mautic/pull/12676).
We want to share them with you so you can get the most out of Rector.
## 1. Use explicit paths over `/vendor`
This happens very rarely, but it's worth mentioning. Rector should always **run only on the code you own**. If you run it in the root directory, the memory might bloat on the bare `/vendor` directory.
```php
use Rector\Config\RectorConfig;
return RectorConfig::configure()
->withPaths([
__DIR__ . '/../first-project',
__DIR__ . '/../second-project',
]);
```
* Be sure to **install Rector directly to your project** using composer, like any other dev package, to avoid such accidents on climbing paths up.
* Make sure you use the paths you own - including tests and config directory:
```php
use Rector\Config\RectorConfig;
return RectorConfig::configure()
->withPaths([
__DIR__ . '/config',
__DIR__ . '/src',
__DIR__ . '/tests',
]);
```
## 2. Avoid checking migrations and test fixtures
This mistake is hard to spot but effective in throttling your Rector run. Doctrine migrations, test fixtures, and **any other generated generated PHP files should be excluded**. Why? At the start, you can have 5-10 database migrations - that's fine, but the older the project is, the greater the burden to handle on every run.
We've seen projects with 200-600 migration files carefully hidden in `/src/Migrations` like any other production file.
These files should be excluded not only for performance gains but also to make sure their structure and behavior will persist.
```php
use Rector\Config\RectorConfig;
return RectorConfig::configure()
->withSkip([
__DIR__ '/app/Migrations',
]);
```
Moving those files into the root `/migrations` directory is even better, so Rector and other tools like PHPStan and ECS do not check them.
## 3. Avoid keeping `UP_TO_*` for longer than needed
Symfony, PHPUnit and Twig level sets caused mutually conflicting changes and heavy performance loads. They got deprecated since Rector 0.19.2. Use the last major version set instead.
Rector can handle both code improvements and package upgrades. The upgrade is usually a one-time job to get your codebase to the latest PHP and packages. You can find the following sets in your code:
```php
use Rector\Config\RectorConfig;
return RectorConfig::configure()
->withSets([
LevelSetList::UP_TO_PHP_81,
// deprecated since 0.19.2
PHPUnitLevelSetList::UP_TO_PHPUNIT_100,
SymfonyLevelSetList::UP_TO_SYMFONY_63,
]);
```
These are what we call **low-hit sets**. They contain dozens of rules that will never find any code to upgrade or change, yet they're still run on every Rector run. That's like checking every release of The Time magazine for prime numbers higher than 1,000,000 - it's a waste of your time and resources.
For, the Symfony 6.3 level set itself contains 80 rules - including a rule that renames class and checks class renames in a nested manner.
### How should we use these `UP_TO_*` sets then?
**Enable them just once during the upgrade period**. Let's say we are upgrading to Symfony 6.3 - we keep the set in `rector.php` for the time being, and once we're on Symfony 6.3, we remove it.
Don't worry; any leftovers will be reported again in 6 months when you handle the Symfony 6.4/7 upgrade.
## 4. Instead of long `withRules()` calls, use slim `withPreparedSets()`
During the upgrade period, it's also typical to add one rule at a time, run Rector, and push the fixed cases. Then repeat.
That way, you might end up with 100+ rules listed one by one from a single set:
```php
use Rector\Config\RectorConfig;
return RectorConfig::configure()
->withRules([
\Rector\DeadCode\Rector\BooleanAnd\RemoveAndTrueRector::class,
\Rector\DeadCode\Rector\Stmt\RemoveUnreachableStatementRector::class,
\Rector\DeadCode\Rector\ClassConst\RemoveUnusedPrivateClassConstantRector::class,
\Rector\DeadCode\Rector\ClassMethod\RemoveUnusedPrivateMethodParameterRector::class,
\Rector\DeadCode\Rector\Concat\RemoveConcatAutocastRector::class,
\Rector\DeadCode\Rector\Return_\RemoveDeadConditionAboveReturnRector::class,
\Rector\DeadCode\Rector\For_\RemoveDeadContinueRector::class,
\Rector\DeadCode\Rector\For_\RemoveDeadIfForeachForRector::class,
\Rector\DeadCode\Rector\If_\RemoveDeadInstanceOfRector::class,
]);
```
This makes `rector.php` hard to read and maintain. Instead, use the whole set to keep it slim:
```php
use Rector\Config\RectorConfig;
return RectorConfig::configure()
->withPreparedSets(deadCode: true);
```
Are there rules you don't like from a particular set? You can [skip them](https://getrector.com/documentation/ignoring-rules-or-paths).
## 5. Make use of code quality sets
Last but not least, make sure you're using the most powerful feature of Rector. It's not the upgrade sets but the refactoring sets.
Those sets are the opposite of the low-hit sets discussed in point 3. **They're helping you with everyday coding** - in PHP 7.0, PHP 8.2, Laravel, Symfony, or in plain PHP.
```php
use Rector\Config\RectorConfig;
return RectorConfig::configure()
->withPreparedSets(deadCode: true, codeQuality: true, naming: true, privatization: true);
```
Avoid adding them all at once, as such a PR is impossible to review. Instead, add one set by another and push the fixes in between.
Happy coding!
-----------------------------------------
## Rector 0.17 - Using Scoped Traverse to Locate Specific Node
Perex: Rector [no longer supports parent node lookup](/blog/rector-017-brings-more-robust-and-lighter-node-tree) since version 0.17. To look up a specific node, we'll have to traverse from parent to child node instead.
Here is how we can achieve it.
For example, we want to find all the `return` nodes in the following code. But we want to skip those inside **inner scope** - _anonymous classes_, _inner functions_ or _closures_.
```php
class SomeClass
{
private const LABEL_A = 'A';
public function run()
{
$someClosure = function () {
return 1;
};
if (rand(0, 1)) {
return 'A';
}
return $someClosure() + 2;
}
}
```
Previously, we could hook into the `Return_` node and check if the node is located within the parent node of `PhpParser\Node\Expr\Closure;`. How can we detect the exact location now?
In our rule, we hook into the top shared node - `PhpParser\Node\Stmt\ClassMethod`:
```php
use PhpParser\Node\Stmt\ClassMethod;
// ...
public function getNodeTypes()
{
return [ClassMethod::class];
}
```
We have 2 ways to skip the closure.
## 1. Scoped Node Finder
Then, you can do this in the `refactor()` method:
```php
use PhpParser\Node\Stmt\Return_;
public function refactor(Node $node): ?Node
{
$returns = $this->betterNodeFinder->findInstancesOfInFunctionLikeScoped(
$node, Return_::class
);
if ($returns === []) {
return null;
}
// process $returns nodes here ...
}
```
You're familiar with `findInstanceOf()`, which returns all the nodes it can find of a specific type.
**The `findInstancesOfInFunctionLikeScoped()` is similar, but smarter** - if it enters an anonymous class, inner function, or closure inside, it will skip it.
In the example above, we'll give you only 2 items:
```php
return 'A';
return $someClosure() + 2;
```
👍
## 2. Using Node Traversing
Another option is to use `SimpleCallableNodeTraverser`. E.g., we need to replace:
```diff
-return 'A';
+return false;
```
Here is how we design the Rector rule:
```php
use PhpParser\Node\Expr;
use PhpParser\Node\Expr\Closure;
use PhpParser\Node\Stmt\Class_;
use PhpParser\Node\Stmt\Function_;
use PhpParser\Node\Stmt\Return_;
use PhpParser\Node\Expr\ConstFetch;
use PhpParser\Node\Name;
use PhpParser\NodeTraverser;
use PhpParser\Node\Stmt\ClassMethod;
/**
* @param ClassMethod $node
*/
public function refactor(Node $node): ?Node
{
if ($node->stmts === null) {
return null;
}
$hasChanged = false;
$this->traverseNodesWithCallable(
$node->stmts,
function (Node $subNode) use (&$hasChanged): ?int {
if ($subNode instanceof Class_
|| $subNode instanceof Function_
|| $subNode instanceof Closure
) {
return NodeTraverser::DONT_TRAVERSE_CURRENT_AND_CHILDREN;
}
if (! $subNode instanceof Return_) {
return null;
}
if (! $subNode->expr instanceof Expr) {
return null;
}
if (! $this->valueResolver->isValue($subNode->expr, 'A')) {
return null;
}
$subNode->expr = new ConstFetch(new Name('false'));
$hasChanged = true;
return null;
}
);
if ($hasChanged) {
return $node;
}
return null;
}
```
👍
Pick the solution that fits your situation. That's it ;)
Happy coding!
-----------------------------------------
## Rector 0.18 - From Symfony Container to Laravel and How to Upgrade your Extensions
Perex: Since the first Rector version, we used Symfony container to inject the services. It worked very well. The new PHP 8.0 came with attributes, and Symfony started to use them extensively.
We're downgrading Rector down to PHP 7.2, and this forced us to lock with unmaintained Symfony 6.1. We needed a hacky patch to make Rector config work...
This made us think: Is there a better way?
The container started to cost more maintenance than the features it provided, so we tried an experimental switch to Laravel. To our surprise, this helped us to make **[our tests run 7x faster](/blog/rector-018-how-we-made-tests-seven-times-faster)**.
## It's all about the Downgrade
How is this possible? Apart from the 3 changes we mentioned in the article regarding downgrading and scoping - **the size matters**.
During downgrade and scoping, all the `/vendor` is shipped with the Rector project. That means every dependency is scoped and downgraded - line by line.
What about features? Rector is a CLI app, and it needs a simple dependency injection container:
* autowire service constructor,
* pass tagged services,
* callback to avoid circular dependencies.
If two packages are compatible, the more lines mean only more features we will never use.
**Downgrading is a challenging process** and sometimes it's a dead end - e.g., downgrading PHP code that uses PHP 8.0 attributes and doesn't provide a fallback feature on PHP 7.2 cannot be automated. Fewer lines mean fewer pitfalls to worry about.
## Smaller is Better
Let's compare container package sizes without knowing the package name.
Guess which is which ↓
```bash
Filesystem count
Directories ......................................... 14
Files .............................................. 186
Lines of code count / relative
Code ................................... 17 694 / 79 %
Comments ................................ 4 698 / 21 %
Total .................................. 22 392 / 100 %
```
* **17 694 lines of code**
Then the other:
```bash
Filesystem count
Directories .......................................... 0
Files ................................................ 6
Lines of code count / relative
Code .................................... 1 091 / 56.1 %
Comments .................................. 855 / 43.9 %
Total ................................... 1 946 / 100 %
```
* **1 091 lines of code**
That means one is **16 x size greater** than the other.
Results revealed:
* the symfony/dependency-injection 6.3 ..... **17 694 lines**
* the illuminate/container 10.20 ..................... **1 081 lines**
*We also used symfony/http-kernel for the kernel test case to run, but we skipped transitional packages for the sake of comparison simplicity.*
## How to Upgrade your Extensions?
Rector 0.18 is the first release with a Laravel container. **Now, we're testing in the wild**.
If you've used [bare `RectorConfig` class](/blog/new-in-rector-012-much-simpler-and-safer-rule-configuration) to set up your configuration, **no upgrade is needed**.
Few changes are required if you've used Symfony internal methods or maintain a custom Rector extension.
**The way services are registered**:
* Before, you had to register every single service, even if fully autowired.
* Now, you only register the services requiring a scalar parameter, factory, or tagged services.
```diff
use Rector\Config\RectorConfig;
return static function (RectorConfig $rectorConfig): void {
- $services = $rectorConfig->services();
- $services->set(App\SomeService::class);
+ $rectorConfig->singleton(App\SomeService::class, function () {
+ return new SomeService('some parameter');
+ });
};
```
Tag service:
```diff
use Rector\Config\RectorConfig;
return static function (RectorConfig $rectorConfig): void {
- $services = $rectorConfig->services();
- $services->set(App\SomeService::class)
- ->tag(SomeInterface::class);
+ $rectorConfig->singleton(SomeService::class);
+ $rectorConfig->tag(SomeService::class, SomeInterface::class);
};
```
You can drop the `$services->defaults()` calls ultimately, as this is now included in the core:
```diff
- $services = $rectorConfig->services();
- $services->defaults()
- ->public()
- ->autowire()
- ->autoctonfigure();
```
Also, PSR-4 autodiscovery is not needed anymore, as services are created for you:
```diff
- $services->load('Rector\\', __DIR__ . '/../packages')
- ->exclude([...]);
```
That's it! Find more in [Laravel documentation](https://laravel.com/docs/10.x/container) and [pull-request with container switch in Rector](https://github.com/rectorphp/rector-src/pull/4698).
Is there something missing? Let us know to update this post. Thank you!
Happy coding!
-----------------------------------------
## Rector 0.18 - How we made tests Seven Times Faster
Perex: The developer experience is a priority when it comes to contributing tools, fixing bugs, and delivering merge requests fast. Rector 0.17 tests could **eat up enough memory to crash on 16 GB RAM and took 3-5 minutes to complete**.
This was painful and lead developers to skip test run locally and wait for the CI.
We wanted [fast feedback](https://tomasvotruba.com/blog/2020/01/13/why-is-first-instant-feedback-crucial-to-developers), so **everyone can enjoy fast feedback**. We worked hard past 2 months to make our tests faster than a sip of a good coffee.
## Why so Slow?
At first, we had to identify, *why* are our tests so slow. Let's take it step by step. The code is being parsed by php-parser; we have around 3 300 tests. That means:
* php-parser parses test fixture to nodes, typically PHP class of 15 lines,
* then PHPStan decorates nodes with types,
* then Rector uses registered rules and changes nodes,
* then the printer prints nodes back to the string,
* finally, the printed string is compared to an expected one.
**This happened 3 300 times**. We tried to speed up the process by removing unnecessary iterations, which helped on a single-project run. We're very grateful for [the tremendous work Markus Staab](https://staabm.github.io/2023/05/06/racing-rector.html) has done in this area.
So we were sure the **bottleneck was elsewhere**. But where?
Tests in Rector 0.17 take **73 seconds** and **8.17 GB of memory**.
The idea came from a lucky experiment. A few weeks ago, I flipped [ECS from Symfony to Laravel container](https://tomasvotruba.com/blog/experiment-how-i-replaced-symfony-di-with-laravel-container-in-ecs) because it's much easier to use when it comes to CLI apps - we downgrade whole /vendor to PHP 7.2 and prefix every single class.
Surprisingly, the container switch affected test speed as well. Tests went [from 0,75 s down to 0,17 s](https://twitter.com/VotrubaT/status/1683576139049058304) - that's 77 % faster. These numbers are too small to take seriously, but it gave us a hint - maybe we **could achieve a similar speed-up in Rector**.
Even going down from 80 seconds to 30 would make contributing Rector more joyful.
The speed 7x speed up is **combination of 3 changes**.
## 1. From Compiled contains to Lazy Container
In every test, the container has to load config, register rules as a service, set up parameters and then invoke the cycle above. The Symfony container is compiled, so the fluent PHP config that `RectorConfig` is dumped to a cached PHP file. This brings excellent performance on HTTP requests per second but can put a massive burden on your local on 3300 different tests. Symfony parameter bag is tightly coupled to the container, so to invalidate, e.g., paths or skip parameters, the container cache rebuild is needed.
On the other hand, the Laravel container is *lazy* - it only creates the services you need when you need them. The Rector core contains ~ 400 services. If you need to test a single service, the Laravel container will create a single service with its dependency tree.
We switched **the container from compiled and cached to lazy one**.
* Rector tests now run faster, as typical test runs a single Rector service with the same dependencies
* We create a single shared container for all the 3300 tests.
* This means if a service is injected in each of those tests, it will be created just once and reused.
But that wasn't enough.
## 2. Identify Resettable Services
When we moved from compiled container per test case to a lazy one, we had another problem. Some services kept state and piled up configuration or cached values. E.g., a collector that kept class renames was growing on every run.
We had to identify these services and mark them with `ResetableInterface` to set their state empty:
```php
final class RenamedClassesDataCollector implements ResetableInterface
{
/**
* @var array
*/
private array $oldToNewClasses = [];
public function reset(): void
{
$this->oldToNewClasses = [];
}
// ...
}
```
And reset them on every test run:
```php
protected function setUp()
{
// ...
$renamedClassesDataCollector = $container->make(RenamedClassesDataCollector::class);
$renamedClassesDataCollector->reset();
}
```
Instead of container-coupled parameter services like in a compiled container, we used a static parameter provider that we reset in `tearDown()`. This allowed us to keep the lazy container and parameter configuration separate.
## 3. Avoid new Nodes in Data Providers
Last but not least, we improved speed on bizarre time leaks.
There were 2 test cases related to the doc block parsing/printer on multiline docblocks, like Doctrine many to many.
They were specific by interesting behavior - when we ran them standalone, it took around 80 ms. But the more tests were run before them, the longer it took. E.g., the whole test suite took 2-3 minutes more.
It was not in Rector code, PHPStan code, Laravel container, or any other used dependency. Well, it ones one of the dependencies - the PHPUnit.
The PHPUnit 10 made a change in data providers to require them to be `static`. It's probably related to some caching because in this code, it spiked a 2-3 minute delay:
```php
use PhpParser\Node\Stmt\Class_;
use PhpParser\Node;
#[DataProvider('provideData')]
public function test(string $filePath, Node $node)
{
// ...
}
public static function provideData(): Iterator
{
yield [
__DIR__ . '/some_file.txt',
new Class_(IndexInTable::class),
];
}
```
So instead of 10 seconds, the whole **test suite would run a couple of minutes** even on the Laravel container. It has probably something to do with the [`jsonSerialize()` method of `PhpParser\NodeAbstract`](https://github.com/nikic/PHP-Parser/blob/a6303e50c90c355c7eeee2c4a8b27fe8dc8fef1d/lib/PhpParser/NodeAbstract.php#L172-L177) and PHPUnit caching.
Other value objects like PHPStan type objects can be used in data providers with no performance hit.
So, **how we fixed it**? Simply using nodes outside the data provider:
```diff
use PhpParser\Node\Stmt\Class_;
use PhpParser\Node;
#[DataProvider('provideData')]
-public function test(string $filePath, Node $node)
+public function test(string $filePath, string $className)
{
+ $node = new Class_($className);
// ...
}
public static function provideData(): Iterator
{
yield [
__DIR__ . '/some_file.txt',
- new Class_(IndexInTable::class),
+ IndexInTable::class,
];
}
```
This removed our last and slowest annoying bottleneck.
## And the results?
* From 73 seconds to **10 seconds**
* From 8 170 MB of memory to **633 MB**
* We narrowed 8 parallel test jobs **[to single one](https://github.com/rectorphp/rector-src/pull/4827)** with 20 seconds run.
Job well done!
We know there is still space to improve the container. Do you have some experience with Laravel container performance optimization? Please, roast our [container factory](https://github.com/rectorphp/rector-src/blob/main/src/DependencyInjection/LazyContainerFactory.php).
We want to make it even faster, so any PHP developer can run Rector on any machine worldwide. Thank you!
Happy coding!
-----------------------------------------
## Rector 0.18 - Refocus on PHP
Perex: Before going to Rector 1.0, we need to refocus solely on PHP files. In this release, we're leaving a not-so-well-known feature that could handle some changes in configs and templates.
Like any other tools like ECS, PHPStan, PHP-CS-Fixer, PHPUnit, Pest, etc., Rector focuses on working with PHP files.
Yet, in some cases, it could change YAML, TWIG, or files too. While using PHP classes in templates is a bad practice, it cannot be avoided like in Symfony configs.
That's why I added a "special file processor" that could handle some non-php files too. This is a not-so-known feature that worked in quite a magic way.
* those files are processed only if passed into the explicit paths, e.g.
```bash
# this could process non-PHP files
bin/rector config src
# this could not
bin/rector src
```
* it only renames class names matching the FQN regex pattern
```bash
# This is skipped
use App\SomeType;
class: SomeType
```
The problem is Rector is built on AST to work with any form of PHP class naming - it's 100 % reliable and can handle whatever aliases.
The magic mentioned above was hidden deep, leading to unexpected cases - some classes are renamed but are not.
Instead, these files should be skipped entirely, and **Rector handles PHP files only**. This will show the same behavior as other CLI tools mention above and make you handle non-PHP files yourself in a consistent and aware way.
## What are the essential changes?
* In Rector 0.17.3 we removed `NonPhpFileProcessor` and `NonPhpRectorInterface` - see [PR 4761](https://github.com/rectorphp/rector-src/pull/4761).
* The `FileProcessorInterface` designed to support these magic changes is now deprecated and should be moved away from.
The community usage is very rare - as far as we know, there is only single package using those. But we want to plan ahead and give you time to adjust.
## Potential for a Standalone tool
On the other hand, this shows potential for a standalone tool. In the same way PHPStan core does not handle Blade templates but only pure PHP syntax, but there is an [wrapper package](https://github.com/TomasVotruba/bladestan), the Rector will handle pure PHP and allow reliable extensions to grow.
Happy coding!
-----------------------------------------
## Rector 0.17 brings More Robust and Lighter Node Tree
Perex: Rector has matured enough to start thinking about a stable version. This year, we want to release Rector 1.0. Before that happens, we want to ensure it is available to variety of users and the known splinters are removed.
One of them is **to lower memory consumption** - so Rector runs **faster on any laptop anywhere in the world**.
To achieve this, we are removing the next, previous, and parent nodes connection in the node tree:
```php
public function refactor(Node $node)
{
// is null in Rector 0.17
$parentNode = $node->getAttribute(AttributeKey::PARENT_NODE);
}
```
This will make the node tree more robust, as each node should know about only its child nodes, not about the whole tree. It's like dependency injection, where the service only knows about its dependencies, not the whole container.
## Before and After
PHPStan has done [the same move in April 2022](https://phpstan.org/blog/preprocessing-ast-for-custom-rules), and Rector architecture now follows the same path. We borrow the following example from their blog post to show the change on a `try/catch` node.
Before this change, the node tree looked like this - every node knows about every other node:
Now, it looks simple like this:
With such a simpler architecture, the rules are faster because the node tree doesn't have to remember that many references. But also, **rules are easier to read as we go from top to bottom**.
To land this change in the Rector codebase, [we've done over 100 pull requests](https://github.com/rectorphp/rector/issues/7947) to core and extensions:
## Rule of the Thumb - Use First Relevant Node
How do you **upgrade your custom rules**? Check [the issue](https://github.com/rectorphp/rector/issues/7947) for examples of refactoring.
During refactoring, we noticed some rules were scratching a left ear with its right hand. E.g., hooking to the `Property` node to add a method on a parent `Class_`. Instead, the rule should hook into the lowest node possible and the highest relevant node being changed.
In the example, it should hook into the `Class_` node, find a property in the class and add a method there. This way, the rule will be faster and more robust:
```php
use PhpParser\Node;
use PhpParser\Node\Stmt\Class_;
use PhpParser\Node\Stmt\ClassMethod;
use Rector\Rector\AbstractRector;
final class AddMethodBasedonPropertyRector extends AbstactRector
{
public function getNodeTypes(): array
{
return [Class_::class];
}
/**
* @param Class_ $node
*/
public function refactor(Node $node): ?Node
{
$addMethod = false;
foreach ($node->getProperties() as $property) {
// detect relevant property
if (...) {
$addMethod = true;
}
}
if ($addMethod === false) {
return null;
}
// add class method here
$node->stmts[] = new ClassMethod(...);
return $node;
}
}
```
Update your rules to use the first relevant node, and you'll be ready for Rector 1.0.
Happy coding!
-----------------------------------------
## Faster Rector on 0.15.22
Perex: Correctness has more priority than speed. Since version 0.14.x, Rector has better scope refresh handling for multiple rules and handle more crash that happen on 0.13.x. On 0.15.x, Rector give optimization a chance to raise.
This performance optimization is contributed by [keulinho](https://github.com/keulinho), which with his knowledge on usage of `blackfire`, provided various PRs on performance optimizations:
- [https://github.com/rectorphp/rector-src/pull/3485](https://github.com/rectorphp/rector-src/pull/3485)
- [https://github.com/rectorphp/rector-src/pull/3495](https://github.com/rectorphp/rector-src/pull/3495)
- [https://github.com/rectorphp/rector-src/pull/3501](https://github.com/rectorphp/rector-src/pull/3501)
- [https://github.com/rectorphp/rector-src/pull/3502](https://github.com/rectorphp/rector-src/pull/3502)
- [https://github.com/rectorphp/rector-symfony/pull/381](https://github.com/rectorphp/rector-src/pull/381)
- [https://github.com/rectorphp/rector-symfony/pull/382](https://github.com/rectorphp/rector-src/pull/382)
## What happened?
1) Memoization resolved data
On various use cases, data can be hit in multiple rules, again and again, that's worth cached, for example:
```php
/**
* @var array
*/
private array $skippedFiles = [];
public function shouldSkip(string | object $element, string $filePath): bool
{
if (isset($this->skippedFiles[$filePath])) {
return $this->skippedFiles[$filePath];
}
$skippedPaths = $this->skippedPathsResolver->resolve();
return $this->skippedFiles[$filePath] = $this->fileInfoMatcher->doesFileInfoMatchPatterns($filePath, $skippedPaths);
}
```
Above save data with index `$filePath` to a property, which the service is shared so it won't hit again.
2) Avoid object creation when not needed, for example:
**Before**
```php
if (! $this->isObjectType($methodCall->var, new ObjectType('ReflectionFunctionAbstract'))) {
return false;
}
return $this->isName($methodCall->name, 'getReturnType');
```
**After**
```php
if (! $this->isName($methodCall->name, 'getReturnType')) {
return false;
}
return $this->isObjectType($methodCall->var, new ObjectType('ReflectionFunctionAbstract'));
```
Above, no need to create `new ObjectType()` when the name is not `getReturnType`, which faster.
In the `CodeIgniter 4` project, it already show twice faster:
**Before**
**After**
## Another future improvements effort
1) Moving away from `parent` lookup after node found by `NodeFinder` to `SimpleCallableNodeTraverser`, like this this PR:
- [https://github.com/rectorphp/rector-src/pull/3504](https://github.com/rectorphp/rector-src/pull/3504)
Above:
✔️ We know that we search specific `Node`, which is `Assign` node with local property
✔️ No need to traverse deep when we found anonymous class ( `new class` ) and inner function inside `ClassMethod`
2) Replacing lookup all nodes to only found first node, like this PR:
- [https://github.com/rectorphp/rector-src/pull/3505](https://github.com/rectorphp/rector-src/pull/3505)
Above:
✔️ Instead of get all nodes by instance, and search name later, we find first found instance and directly verify the name.
Start feel the speed. Run composer update!
Happy coding!
-----------------------------------------
## New command to add Rector to your CI in seconds
Perex: We're working hard to make the developer experience as smooth as possible. The fewer steps to your first run and full automation with Rector, the better.
In February, we added improvement for the first run. Now we **add a new command to set up Rector in your CI to work for you**.
When you run Rector for the very first time, it generates the `rector.php` config for you. It suggests directories and the first rule to kick off. Let's take this further.
## Little preview of what we'll talk about today
1) Does your PHPStan fail?
2) No worries, just let Rector fix it:
3) And soon, your CI is passing and ready for merge ✅
## Rector working for you
You often ask, "how can I add Rector to CI to work for me?" In the end, the answer is a short Github Workflow file, but there are **few traps on the way**:
* it must run only for core developers, who have access to the repository = that way Rector can actually contribute
* it must not run on forks
* it must run only on pull-requests
* the Rector commit must re-trigger the CI Workflows so you have its work verified
* it needs access to the Github token to have enough rights to contribute
This road is pretty cumbersome, right?
## Introducing the `setup-ci` command
Now we turned it to single command that:
* generates `.github/workflows/rector.yaml`
* fills your repository name to run workflow only for core contributors
* shows you 2 links: to create a Github token and add the token as a repository secret
## How to use it?
First, make sure you have the latest Rector `0.15.21`, and then run locally:
```bash
vendor/bin/rector setup-ci
```
## Follow instructions
* 1) generate the token
* 2) fill it in the right place
Push a pull-request, make a mistake and see how Rector handles boring work for you.
Happy coding!
-----------------------------------------
## How to Upgrade to PHPUnit 10 in Diffs
Perex: PHPUnit 10 [was released today](https://phpunit.de/announcements/phpunit-10.html). Do you fancy an early upgrade?
We'll show you how to do it with Rector and what other changes you have to handle. Ready?
## What has changed regarding the dependencies?
* PHPUnit 10 now requires PHP 8.1+
* and it requires `Sebastian/diff` 5.0 instead of 4.0
The first is a hard requirement, so if you're stuck on PHP 8.0, do not upgrade.
The second one can be vendor locked by other packages. If the `Sebastian/diff` is a blocker for you, use the composer alias to allow it:
```diff
{
"require": {
- "sebastian/diff": "^4.0"
+ "sebastian/diff": "5.0 as 4.0.4"
}
}
```
## Update the `Differ` class
Do you use the `sebastian/diff` in your code? The `Differ` class now requires an explicit dependency via a constructor. So add it:
```diff
use SebastianBergmann\Diff\Differ;
use SebastianBergmann\Diff\Output\StrictUnifiedDiffOutputBuilder;
+$unifiedDiffOutputBuilder = new UnifiedDiffOutputBuilder();
-$differ = new Differ()
+$differ = new Differ($unifiedDiffOutputBuilder);
```
## Update `.gitignore` paths
New PHPUnit 10 uses the whole directory for caching instead of a single file:
```diff
# .gitignore
-.phpunit.result.cache
+/.phpunit.cache
```
## And the Rest?
* data providers must be static methods now
* the `@annotations` are flipped to `#[attributes]`
* the abstract "test" must have `TestCase` suffix
```diff
use PHPUnit\Framework\TestCase;
+use PHPUnit\Framework\Attributes\DataProvider;
final class SomeTest extends TestCase
{
- /**
- * @dataProvider provideData()
- */
+ #[DataProvider('provideData')]
public function test(int $value)
{
}
- public function provideData()
+ public static function provideData()
{
yield [10_0];
}
}
```
Note: Do your data providers contain dynamic method calls? You'll need to refactor them to static ones first:
```diff
public static function provideData()
{
- yield $this->loadDirectory(__DIR__ . '/Fixtures');
+ yield self::loadDirectory(__DIR__ . '/Fixtures');
}
```
The `abstract` test now has to have `*TestCase` suffix:
```diff
use PHPUnit\Framework\TestCase;
-abstract AbstractTypeTest extends TestCase
+abstract AbstractTypeTestCase extends TestCase
{
}
```
To handle these, add PHPUnit 10 upgrade set to the `rector.php` config:
```php
use Rector\Config\RectorConfig;
use Rector\PHPUnit\Set\PHPUnitSetList;
return static function (RectorConfig $rectorConfig): void {
$rectorConfig->sets([
PHPUnitSetList::PHPUNIT_100,
]);
};
```
Then run Rector on tests to upgrade:
```bash
vendor/bin/rector tests
```
That's it! We made an [upgrade of Rector tests](https://github.com/rectorphp/rector-src/pull/3332) based on this tutorial.
Enjoy your new PHPUnit 10 tests today!
Happy coding!
-----------------------------------------
## New in Rector 0.15: Complete Safe and Known Type Declarations
Perex: Rector is helping with PHP upgrades and framework migrations. It also helps to [rise the type coverage](https://tomasvotruba.com/blog/how-to-measure-your-type-coverage/) of your project.
Rector completes the type declarations for parameters, returns, and properties, with one Rector rule per each case. The only problem was, **these rules use PHPStan type inference that relies on docblocks**, and it could complete strict types that are not true. You burn once, and then you ignore these rules forever.
### We want you to Feel 100 % Safe
Our goal is to make Rector reliable, so it does not guess and does not require your attention on every change. Instead, it must work without flaws and be 100 % reliable.
That's why [we slowly refactored away from these rules](/blog/how-to-automatically-add-return-type-declarations-without-breaking-your-code), **split them into dozens of small ones that work only with strictly known types**.
Next release brings huge improvement in type inferring 💪
We moved many rules from unreliable docblocks
to 100 % sure type declarations ↓
Every project will get the most type coverage they can with guaranteed safety 😎https://t.co/kAuyIbaK48 pic.twitter.com/xQlyxf6vTz
— Rector (@rectorphp) December 3, 2022
## Known and Safe Types ~~First~~ Only
Before, Rector would make this change:
```diff
class SomeProject
{
- private $name;
+ private string $name;
/**
* @param string $name
*/
public function __construct($name)
{
$this->name = $name;
}
}
```
Then we create the `Project` based on the API call:
```php
$projectName = $this->apiCaller->getProjectName(123);
$project = new Project($projectName);
```
and get a crash as pass a `null` to the `Project` constructor. **We don't want that.**
From Rector 0.15, we removed the `ParamTypeDeclarationRector`, `ReturnTypeDeclarationRector`, and `PropertyTypeDeclarationRector` and their array alternatives, so this will not happen.
Now, Rector takes into account **only strict type declarations**:
```diff
class Project
{
- private $name;
+ private string $name;
public function __construct(string $name)
{
$this->name = $name;
}
}
```
or
```diff
class Project
{
- public function getSize()
+ public function getSize(): int
{
return strlen($this->name);
}
}
```
## How to Upgrade to Rector 0.15?
We removed the `TYPE_DECLARATION_STRICT` set and moved reliable rules to the `TYPE_DECLARATION` set. Now you can use a single set to handle the type declarations.
```diff
use Rector\Config\RectorConfig;
use Rector\Set\ValueObject\SetList;
return static function (RectorConfig $rectorConfig): void {
$rectorConfig->sets([
SetList::TYPE_DECLARATION,
- SetList::TYPE_DECLARATION_STRICT,
]);
};
```
## Protip: One by one
Take a single rule from the `TYPE_DECLARATION` set, one by one. Apply the rules slowly on your code base and create a pull request per rule. Soon the strict type declarations will be everywhere they can.
Start with return type declaration rules first. They're the easiest to apply and tolerated thanks to [return type covariance](https://www.php.net/manual/en/language.oop5.variance.php).
-----------------------------------------
## Separating Laravel and CakePHP as Community Packages
Perex: Rector is built for and on the whole PHP community right from the start. But there are also somewhat "local" PHP communities around a specific framework. Each framework has specific needs that are best known to the community member.
That's [why we entirely moved Typo3 and Nette](/blog/separating-typo3-and-nette-as-community-packages) Rector extensions **to their communities**. They know best how to handle rules for the framework.
We want to encourage the community to build their own packages on top of Rector core, so we also decided to move Laravel and CakePHP to the community.
Based on a great experience with other frameworks, it's time to give #laravel @rectorphp package to the community it belongs to 🙏
We look for person, who will take over current package https://t.co/S1rlIiRfZr and builds it further into propper "Laractor"😉
— Rector (@rectorphp) October 26, 2022
## Communities in Control of Their Standards
The significant advantage of community-maintained packages over core ones is that every community has its standards. Those standards are specific to the particular community but not useful for general Rector users. E.g., Laravel uses Blade, a PHP template syntax that can be automated, too.
We want to give these communities the freedom to implement any feature their framework needs. Having these packages in the core, where there are no Laravel and CakePHP developers, only drags those down.
As a side effect, Rector users who do not use particular community packages benefit from this too. Their Rector install load is now smaller and pulls fewer dependencies.
## Community Leaders with Strong Vision
Second, the framework communities are driven by their passionate leaders. There is no Symfony without Fabien, no Laravel without Taylor. Leaders need freedom, responsibility, and power to decide where the project should go. Of course, they discuss their opinions with others before making a move, but in the end, it is their decision to move in this or that direction.
That's why the community package should be in the hands of people who use the framework daily. A new framework version brings new features every year. The person who converts them to Rector rules is a passionate developer with a taste for innovation and a bleeding edge.
That's why we decided to separate Laravel and CakePHP from the core and let their active communities take over. They're not part of `rector/rector` anymore, but they're Rector extensions that you can install standalone.
### How to upgrade to the Laravel community Rector package?
Add [new package](https://github.com/driftingly/rector-laravel) via composer:
```bash
composer require driftingly/rector-laravel --dev
```
**Big thanks to [Anthony Clark, aka driftingly](https://github.com/driftingly)** from Tighten for making this happen promptly and with a smooth swipe 🙏
### How to upgrade to the CakePHP community Rector package?
Add [new package](https://github.com/cakephp/upgrade) via composer:
```bash
composer require cakephp/upgrade --dev
```
That's it!
We believe this brings faster iterations of the packages and focuses on a stronger Rector core to support their growth.
-----------------------------------------
## Support for Nested Doctrine Annotation to Flat Attributes in Rector 0.14
Perex: We added support for [annotation to attribute upgrade](/blog/how-to-upgrade-annotations-to-attributes) in Rector 0.12. Since then, PHP 8.1 has come with nested attributes. Rector supports these, e.g., for Symfony validator.
Yet, Doctrine already took a path of its own and **unwrapped nested annotations to flat attributes** to be exclusively open to PHP 8.0 users.
Next Rector comes with support for these too.
One of the annotations that got unwrapped is `Doctrine\ORM\Mapping\Table`, where `indexes` and `uniqueConstraints` have their own attributes:
```php
use Doctrine\ORM\Mapping as ORM;
#[ORM\Index(name: 'index_key')]
#[ORM\UniqueConstraint(name: 'unique_key')]
```
Adding support for this attribute is pretty straightforward, as every unique annotation class has its unique attribute class.
Then Doctrine came with the next-level challenge. Unwrap array of `JoinColumn` annotations, once to `JoinColumn` attribute, and once to [`InverseJoinColumns` attribute](https://www.doctrine-project.org/projects/doctrine-orm/en/2.13/reference/attributes-reference.html#joincolumn-inversejoincolumn). Based on parent key.
```php
use Doctrine\ORM\Mapping as ORM;
/**
* @ORM\JoinTable(name="join_table_name",
* joinColumns={
* @ORM\JoinColumn(name="target_id"),
* },
* inverseJoinColumns={
* @ORM\JoinColumn(name="another_id")
* }
* )
*/
private $collection;
```
To handle this specific situation, we added brand new [`NestedAnnotationToAttributeRector` rule](https://github.com/rectorphp/rector-src/pull/2781) to cover.
The case above would be handled by such configuration:
```php
use Rector\Config\RectorConfig;
use Rector\Php80\Rector\Property\NestedAnnotationToAttributeRector;
use Rector\Php80\ValueObject\NestedAnnotationToAttribute;
return static function (RectorConfig $rectorConfig): void {
$rectorConfig->ruleWithConfiguration(NestedAnnotationToAttributeRector::class, [
new NestedAnnotationToAttribute('Doctrine\ORM\Mapping\JoinTable', [
'joinColumns' => 'Doctrine\ORM\Mapping\JoinColumn',
'inverseJoinColumns' => 'Doctrine\ORM\Mapping\InverseJoinColumn',
]),
]);
```
This rule will intelligently split the annotations:
```php
use Doctrine\ORM\Mapping as ORM;
#[ORM\JoinTable(name: 'join_table_name')]
#[ORM\JoinColumn(name: 'target_id')]
#[ORM\InverseJoinColumn(name: 'another_id')]
private $collection;
```
## Doctrine Support OnBoard
Do you use the Doctrine upgrade set?
```php
use Rector\Config\RectorConfig;
use Rector\Doctrine\Set\DoctrineSetList;
return static function (RectorConfig $rectorConfig): void {
$rectorConfig->sets([
DoctrineSetList::ANNOTATIONS_TO_ATTRIBUTES,
]);
};
```
We've got you covered. You don't need to fill the particular annotations because [the set is already extended](https://github.com/rectorphp/rector-doctrine/blob/bdf6e7c07b91df02000fa286e30e74c7fb7e5301/config/sets/doctrine-annotations-to-attributes.php#L12-L28).
Just upgrade to Rector 0.14.1, and you're good to go.
Happy coding!
-----------------------------------------
## Tests Made Simpler in Rector 0.14
Perex: In August, we've been working hard to make Rector lighter. As use the count of users grows, developers use Rector on more legacy projects than before, and user experience b becomes a higher priority. The easy use, installation, and writing of custom rules is the key to the success of any project upgrade.
We cut down dependencies that it really does not need, removed a few niche features, and made the test case simpler.
You can benefit from this change if you're using Rector to write your custom rules and test those. What has changed and how?
The Rector rule test case has 3 essential methods:
* test method that PHPUnit invokes
* data provider that provides test fixtures
* and method that provides a path to a config file
```php
use Iterator;
use Rector\Testing\PHPUnit\AbstractRectorTestCase;
use Symplify\SmartFileSystem\SmartFileInfo;
final class RenameClassRectorTest extends AbstractRectorTestCase
{
/**
* @dataProvider provideData()
*/
public function test(SmartFileInfo $fileInfo): void
{
$this->doTestFileInfo($fileInfo);
}
/**
* @return Iterator
*/
public function provideData(): Iterator
{
return $this->yieldFilesFromDirectory(__DIR__ . '/Fixture');
}
public function provideConfigFilePath(): string
{
return __DIR__ . '/config/configured_rule.php';
}
}
```
The main concerns were `SmartFileInfo` wrapping every test fixture, and this class includes a version of Symfony FileSystem to work with relative paths. After internal analysis, we've found that the extra methods are used in about 15 cases. We made a [pull-requests](https://github.com/rectorphp/rector-src/pull/2876) to address this issue and saw about 10 % memory less consumption narrow test suite.
## From Value Object to File Path
Wrapping a string in a value object and then creating a service in every single value object is not cheap. But further we need to ask the critical question: **"do we really need it"?**
No.
We dropped this wrapper, removed the `SmartFileInfo` wrapping from everywhere, and lowered the memory consumption of the files.
In the next Rector 0.14.1, we've also simplified the test to use direct file paths:
```php
use Rector\Testing\PHPUnit\AbstractRectorTestCase;
final class RenameClassRectorTest extends AbstractRectorTestCase
{
/**
* @dataProvider provideData()
*/
public function test(string $filePath): void
{
$this->doTestFile($filePath);
}
public function provideData(): Iterator
{
return $this->yieldFilesFromDirectory(__DIR__ . '/Fixture');
}
public function provideConfigFilePath(): string
{
return __DIR__ . '/config/configured_rule.php';
}
}
```
## How to Upgrade?
The upgrade is effortless. Just use PHPStorm to find the `test()` method like this:
```diff
-public function test(SmartFileInfo $fileInfo): void
+public function test(string $filePath): void
{
- $this->doTestFileInfo($fileInfo);
+ $this->doTestFile($filePath);
}
```
And you're ready to go with less memory and simpler code.
Happy coding!
-----------------------------------------
## Interview: Legacy Code, Javascript Transpilers and Rector Challenges
Perex: I'll be speaking there in October at Paris on [Forum PHP 2022](https://event.afup.org/) about Rector.
I was asked for a simple interview to warmup the talk topic. The 3 questions - each looking at different angle, but going deep.
Contrary to mostly technical content on this blog, this post will give you behind the scenes insights on wider Rector vision.
## 1) Your talk is about legacy code. What made you interested in that subject, and do you think it is an important topic for developers?
What made me interested in legacy code? That's a good question :) During my first 10 years of coding, I strictly avoided legacy codebases. I've heard only traumatic stories from older friends, and I wanted to work only with fresh new technologies. That was the most fun, right? As I grew, I realized it's easy to work with new technology and... also boring. I started to look for a challenge and got hired by the largest Czech Pharmacy company to get a 10-year-old spaghetti code to modern MVC with high-quality standards.
That was a great experience that got me closer to automated tools. Compared to my peers, I've always been extremely lazy and had a weakness for clean code. That got me thinking, "this company is probably not the only one in the world having this problem". If I work all my life, I can help max. 20 companies this way. That's not much :)
I came across automated tools like Code Sniffer, and the idea of "automated upgrades for legacy code" started. If one person writes a rule to add a "void" type where it belongs, anyone in the world can use it for free. The whole world runs on a new version of PHP in a few seconds.
I realized people around me might hate legacy code, but they also love to work with complex systems and improve them. That's where the biggest problem of the PHP community and the solution clicked.
## 2) In the JS ecosystem, downgrading and transpiling code is the norm. Do you think it is the future of the PHP ecosystem too?
When I started working on Rector in 2016, I noticed other languages have similar tools. Yet their focus was rather dialect-based than single-lined. JS ecosystem is rich but also has too many variations. Those variations are easily spread, burn out in a few years, and they are hard to get rid of or change.
I mean, a new JS framework has just been released during writing these answers :). There is no ReactJS <-> Angular migration tool, nor Angular 2 to 4 upgrade (fun factory: I was hired once for that too).
The PHP is very concise in this matter. There is PHP 5.4, then 5.5, then 5.6. There is no 5.6-beta-only-with-this-feature with brand-new syntax. That means the language is deterministic. Every version has strictly defined behavior. You can do an "A → B upgrade" with a computer algorithm. There is no better language than PHP to spark these automated tools, whether coding standard tools, static analyzers, or Rector. There are already discussions on Reddit that Rector should become part of PHP official RFCs.
It will be fantastic. Imagine there is RFC that adds a new "read-only" keyword to the class. The implementation and tests are part of the RFC already. But now, there will also be a Rector upgrade set, tested on top 1000 composer packages.
There is no "it will be so hard to upgrade, don't give us more work, please" discussion. It's zero work for us developers to upgrade. We just run "composer update" and enjoy the new PHP version without effort :)
## 3) There are many rules to maintain on a project like Rector. What's the main difficulty of working on this project?
Thank you for your question about this challenge. I'm proud to say the Rector community gives the tool propper battle testing. By the nature of the tool, Rector faces the worst legacy code there is. Its job is to upgrade the "impossible" legacy projects, after all :). The people then report edge cases that Rector missed.
Now comes the most significant challenge: people send a failing test case to particular Rector rule, but they think the fix is too hard to try. Then we try to explain to them that they can do it. Most often than not, the fix usually adds an "instanceof" check here or changes the bool return. When they try it, they're honestly surprised by how easy it is. The next issue usually comes up with a fix included.
At last, I want to encourage you. For anyone working with a legacy codebase or with 3+ years old project, give this technology a try. Whether Rector or abstract syntax tree, **it will give you incredible power to change "impossible-level problems" in your project in a matter of days**.
If you need a heads-up start, get a [Rector book](https://leanpub.com/rector-the-power-of-automated-refactoring) that explains this from the very first steps. It will show you that anything is possible with your code, whether 1000 lines or 10 000 000 lines.
Good luck and happy coding!
Tomas
-----------------------------------------
## Separating Typo3 and Nette as Community Packages
Perex: When Rector started, it was a small project that handled upgrading a vast amount of PHP packages.
As the project grew and expanded, more local PHP communities joined with community packages that build custom rules on top Rector core.
It makes sense to separate these projects from the core and let the community handle them. Who does a better job at growing the vegetable than farmers themselves, right?
To this day, there are around 10 known community packages that are [maintained by the community](https://github.com/rectorphp/rector#empowered-by-rector-community-heart). These include [CraftCMS](https://github.com/craftcms/rector), [Shopware](https://github.com/FriendsOfShopware/shopware-rector) or famous [Drupal Rector bot](https://www.drupal.org/blog/accelerating-drupal-9-module-and-theme-readiness-with-automated-patches) that automates the upgrades for Drupal 9.
## Communities in Control of Their Standards
The significant advantage of community-maintained packages over core ones is that every community has its standards. Those standards are specific to the particular community but not useful for general Rector users. E.g., Typo3 uses XML files that can be automated, too, and Nette uses NEON files for service registrations, etc.
We want to give these communities the freedom to implement any feature their framework needs. When Typo3 and Nette packages were part of the core, these features were often evaluated with strict questions "how does the Rector community benefit from it"? This approach leads to a collision between two unrelated worlds.
Why have frictions when these 2 projects can cooperate and work better apart?
Also, Rector users who do not use these community packages, can benefits from this change. Their Rector install load is now smaller and pulls less dependencies.
## Community Leaders with Strong Vision
Second, the framework communities are driven by their passionate leaders. There is no Symfony without Fabien, no Laravel without Taylor. Leaders need freedom, responsibility, and power to decide where the project should go. Of course, they discuss their opinions with others before making a move, but in the end, it is their decision to move in this or that direction.
That's why we believe the community package should be in the hands of people who use the framework daily. A new framework version brings new features every year, and the person to convert them to Rector rules is a passionate developer with a taste for innovation and bleeding edge.
That's why we decided to separate typo3 and Nette from the core and let their active communities take over. They're not part of `rector/rector` anymore, but they're Rector extensions that you can install yourself if you need those:
### How to upgrade to Typo3 community Rector package?
Add [new package](https://github.com/sabbelasichon/typo3-rector) via composer:
```bash
composer require sabbelasichon/typo3-rector --dev
```
### How to upgrade to Nette community Rector package?
Add [new package](https://github.com/efabrica-team/rector-nette) via composer:
```bash
composer require efabrica/rector-nette --dev
```
And replace namespace:
```diff
-Rector\\Nette\\
+RectorNette\\
```
That's it!
We believe this brings faster iterations of the packages and focuses on a stronger Rector core to support their growth.
-----------------------------------------
## How to Automatically Add Return Type Declarations without Breaking Your Code
Perex: Code filled with docblocks param, var, and return types is a gold mine. Not in the meaning of valuable resource, but rather as exploding metal covered with a thin piece of gold, so we grab it without thinking. While these docblocks give us much information about the code, they might be nothing more than a wish, dream, or promise.
Have you ever blindly trusted docblocks and switched them to type declarations? Then you know the explosive regression this move brings.
Yet, how can we turn to add strict types to our code without fear of breaking it?
## Docblock Trust is Blind
What can we read from this code if we trust it?
```php
class SweetPromise
{
private $values;
/**
* @param mixed[] $values
*/
public function setValues($values)
{
$this->values = $values;
}
/**
* @return string[]
*/
public function getValues()
{
return $this->values;
}
}
```
The values are always an `array` of strings. Let's tolerate strings and work with the `array` type.
If that is true, these should not be possible:
```php
$sweetPromise = new SweetPromise();
echo $sweetPromise->getValues(); // null or array?
$sweetPromise->setValues('{55}'); // string or array?
echo $sweetPromise->getValues(); // string or array?
```
We have no idea about the value we put in. We cannot make any assumptions unless we're ready to risk the type failure and manual verification of every single type.
The honest filter would show the class above like this:
```php
class SweetPromise
{
private $values;
public function setValues($values)
{
$this->values = $values;
}
public function getValues()
{
return $this->values;
}
}
```
Now it's obvious **we can't trust the docblocks**. Mainly because the docblock types are not based on actual code but ideal state = if every method is 100 % reliable, called in precisely defined order, and with 100 % reliable typed input variables. Which they're not.
## Millions of Lines with Docblocks
This sound a little depressing. So what can we do if we have millions of lines of code with docblocks? We want **to move fast and safe at the same time**.
We have a few options:
* go line by line, try to detect the pattern, and add the first single, strict type declaration
* then [propagate this type](/blog/2021/02/15/how-much-does-single-type-declaration-know) to all calls of this code
* risk the automated docblock conversion and let the user do the testing; 500 on the server means the type was not detected correctly
* go with certain types
## What are "Certain Types"?
Certain types are based on actual values, operations, and logical structures that **have under any circumstances always exactly one type**:
* exact scalar value
* `5` → `int`
* `"hi"` → `string`
* values based on PHP internal functions
* [`strlen()` always returns `int`](https://www.php.net/manual/en/function.strlen.php)
* [`substr()` always returns `string`](https://www.php.net/manual/en/function.substr.php)
* result of binary operators
* `$anything && $somethingElse` always returns `bool`
* `100 - 20` always returns `int`
* `! $anything` always returns `bool`
Not only one-line values but also more complex structures like created and filled array:
```php
function provideDreams()
{
$dreams = [];
foreach ($this->dreamRepository->fetchAll() as $dream) {
$dreams[] = $dream;
}
return $dreams;
}
```
This code always returns `array`, no matter what `$dream` actually is.
Based on these 4 rules, we can complete the following types with 100 % certainty:
```diff
final class Reality
{
- public function getAge()
+ public function getAge(): int
{
return 100;
}
- public function removeTax(int $value)
+ public function removeTax(int $value): int
{
return $value - 200;
}
- public function transform($value)
+ public function transform($value): string
{
if ($value === null) {
return '';
}
return base64_encode($value);
}
}
```
And much more!
Depending on what age your project is coming from, there is one requirement to make this work. You must use PHP 7.0, where [return type declarations](https://www.php.net/manual/en/migration70.new-features.php#migration70.new-features.return-type-declarations) were added.
## Try it Yourself
Rector can handle some cases above in the basic version. Add these rules to your code and see for yourself:
```php
rules([
ReturnTypeFromStrictNativeCallRector::class,
ReturnTypeFromStrictScalarReturnExprRector::class,
]);
};
```
Run Rector and see how many new types were added. In the project we tested these rules on, we had over 40 changed files in the first run. From zero to 40 files, quite impressive. And as you know, strict types [spread exponentially](/blog/2021/02/15/how-much-does-single-type-declaration-know).
## Coming in Rector Enterprise
The rest of the cases and more will be covered in rules of upcoming Rector Enterprise version, with more extra rules and features under private paid license.
-----------------------------------------
## New in Rector 0.13 - Refresh Scope for Changed Nodes
Perex: Rector is using PHPStan to detect types of various expressions. That means every node has access to [PHPStan `Scope`](https://phpstan.org/developing-extensions/scope), e.g., with types or class reflection. From code `$value = 1;` we know, that `$value` is type of int. But what if we change the node?
Let's say the Rector changes the code the following way:
```diff
-$value = 1;
+$value = 'yes';
```
The `$value` was an `int` type. But after the change, the type is gone. We have an old `Scope` object where `$value` is still `int` or even worse. If we create a new `Assign` node, no `Scope` is available anymore.
## Changed Nodes with Lost Scope
This becomes problematic when we rename the variable. Suddenly PHPStan has no idea about the new variable type, and everything is `mixed` for it. When we use `CountOnNullRector` that prevents `count()` on `null` fatal error, we can see Rector changed like these:
```diff
-$items = [];
+$posts = [];
-echo count($items);
+echo is_array($posts) ? count($posts) : 0;
```
Which is obviously wrong.
## Scope Refresh to the Rescue
To solve this problem, we implemented a new feature called "scope refresh" that rebuilds the scope based on the changed node. When a variable name changes, the scope will know about it and keep its type. It's a challenge as PHPStan `Scope` object has few design limitations. It's an immutable object - once you enter a class, you cannot enter it again.
In the end, we made it work so that every new node will be traversed again with a new `Scope` object.
That will allows Rector to be aware of the node type, even if nodes change:
```diff
-$items = [];
+$posts = [];
-echo count($items);
+echo count($posts);
```
This is feature coming in Rector 0.13. In the meantime, don't forget to [upgrade to `RectorConfig`](/blog/new-in-rector-012-introducing-rector-config-with-autocomplete).
Happy coding!
-----------------------------------------
## New in Rector 0.12 - Introducing Rector Config with Autocomplete
Perex: Rector is using Symfony container configuration to build the service model. While it brings automated autowiring, array autowiring, and native container features, the syntax to configure Rector has been complex and talkative.
The hard question is: how can we refactor from Symfony, have a custom Rector config class but keep using its features?
A year ago we switched to [a distribution repository releases](/blog/prefixed-rector-by-default). It now allows us to add own `RectorConfig` class on top of Symfony internals.
**The `RectorConfig` helps you by**:
* full IDE autocomplete,
* isolation from Symfony - useful for Symfony projects
* validation of configuration methods that happens before running Rector itself
Are you curious about the implementation? In the end, [the secret is](https://github.com/rectorphp/rector-src/pull/2019) in a [single patched file](https://raw.githubusercontent.com/rectorphp/vendor-patches/main/patches/symfony-php-config-loader.patch) with 4 changed lines.
So what does it look like?
## Configuration as we Know It Now
```php
use Symfony\Component\DependencyInjection\Loader\Configurator\ContainerConfigurator;
use Rector\Configuration\Option;
use Rector\DowngradePhp81\Rector\Property\DowngradeReadonlyPropertyRector;
// ups, possible conflict with ContainerConfigurator
return static function (ContainerConfigurator $containerConfigurator): void {
$parameters = $containerConfigurator->parameters();
// too verbose params, constants and possible typo in param value
$parameters->set(Option::PATHS, [[ // ups, "[[" typo
__DIR__ . '/src/',
]]);
$services = $containerConfigurator->services();
$services->set(DowngradeReadonlyPropertyRector::class);
};
```
Such complexity is confusing just to read and easy to make error in.
## The new Way to Configure Rector
What if we could single class to configure the Rector?
```php
use Rector\Config\RectorConfig;
return static function (RectorConfig $rectorConfig): void {
$rectorConfig->parallel();
$rectorConfig->paths([
__DIR__.'/src',
]);
$rectorConfig->rule(DowngradeReadonlyPropertyRector::class);
};
```
## How to upgrade to `RectorConfig`?
First, be sure to use at least Rector 0.12.21. If your config has a few lines, you can handle it easily manually. But what if you have dozens of lines or even custom configs in your tests? Rector to the rescue!
```php
use Rector\Config\RectorConfig;
use Rector\Set\ValueObject\SetList;
return static function (RectorConfig $rectorConfig): void {
$rectorConfig->sets([
SetList::RECTOR_CONFIG
]);
};
```
Do you use Symfony? Avoid converting your Symfony `/configs` and only process only your Rector configs:
```bash
vendor/bin/rector process rector.php utils/rector/tests
```
Thanks to this upgrade set, we've [migrated all the Rector packages](https://github.com/rectorphp/rector-src/pull/2063/files) to use the new `RectorConfig` syntax.
## Configured Rules
Do you configure your rules? In previous version [we've added `configure()` method](/blog/new-in-rector-012-much-simpler-and-safer-rule-configuration) to help with validation of configuration:
```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)
->configure([
'App\SomeOldClass' => 'App\SomeNewClass',
]);
```
But it was not enough, was it?
That's why now **you can use dedicated `ruleWithConfiguration()` method** with validated input:
```php
use Rector\Renaming\Rector\Name\RenameClassRector;
use Rector\Config\RectorConfig;
return static function (RectorConfig $rectorConfig): void {
$rectorConfig->ruleWithConfiguration(RenameClassRector::class, [
'App\SomeOldClass' => 'App\SomeNewClass',
]);
```
Why not just use the `rule()` method with a magic 2nd argument? The reason to separate these 2 methods is to give you fast feedback about wrong configuration:
* when you try to rule without configuration, e.g., `ChangeSwitchToMatchRector` with configuration, Rector will tell you the configuration is not needed
* and when you use a rule that requires configuration, e.g., `RenameClassRector`, you'll know about missing configuration too
Happy coding!
-----------------------------------------
## Success Story of Automated Framework Migration from FuelPHP to Laravel of 400k+lines Application
Perex: Today, I'm very excited to talk about the full story of our successful automated framework migration how Rector saved our product by refactoring our 400k+lines PHP web application!
*This is an guest post by [rajyan](https://twitter.com/rajyan_k), who used Rector to migrate an extensive PHP application from FuelPHP to Laravel.*
For some framework switch is impossible.
Others can do it with zero downtime 👏 https://t.co/votwydILzX
— Rector (@rectorphp) January 6, 2022
## The Migration Plan
Our application was a monolithic application consisting of the backend of web service, native app API, intra-company support tools, and batch jobs for web and app.
* **Framework**: FuelPHP → Laravel
* **PHP Version**: PHP 7.0 → PHP 7.4
* **Application**:
* Released in 2015/11
* 2000+ PHP files, 400k+ lines of PHP codes
* **Time Schedule**
* 2021/01-11
* Migrated internal tools at 2021/09 (QA from 2021/07 ~)
* Start canary release of Web and API from 2021/11/1 (QA from 2021/08 ~)
* Switched to Laravel with 100 % release at 2021/11/16
* **Team Members**
* 1~2 engineers + 1 senior engineer for advice
* **Special notes**
* Running the migration and developing new features at the same time
* Zero downtime by releasing old and new environments in the canary release
## Why did we Choose Rector?
Why did we decide to automate the migration? The reason was simple.
Our application was too large to migrate manually, and automation was needed to make the migration successful.
Even more, we estimated that it might take about a year for migration without automation, even if all team members worked for migration.
**If it's the same speed as human work, why not try something new that might be faster?**
## Fully Automated Migration
**At first, we were using Rector only to convert DB query builders of FuelPHP to Laravel** and manually modify controllers, configs, "Facades" (~= "Classes" in FuelPHP).
However, as I wrote custom Rector rules, I noticed AST power and flexibility and realized that full automation might be possible. Also, FuelPHP is a relatively lightweight framework, and automated migration to Laravel, which has more features, was imaginable.
## FuelPHP to Laravel
**99% of the PHP files were converted automatically**, editing 200k+ lines of code.
An automated migration by custom Rector rules of 2000+ files included:
* Fuel Query Builder → Laravel Query Builder
* Non psr-4 → psr-4
* We created a dummy autoloader to run Rector, because we did not install FuelPHP
* Adding namespaces, and moving files to the correct dir
* Converting Config
* `File, Response` Class → Laravel `Response` facades or helpers
* `Input, Upload` Class → Laravel `Request` facades or helpers
* FuelPHP Exceptions → Mapped to Laravel Exceptions
* There were a lot of other ad-hoc rules specific to our code
A manual migration of ~20 files:
* Routes
* There are no routes in FuelPHP
* Some parts of authentication
* Some parts of config
* FuelPHP specific classes.
* ex. `Format`, `Agent`
* wrote a custom facade in Laravel
Let's look into them in detail.
## Let's write a Rule to Migrate a Query Builder
Creating custom Rector rules to migrate the query builder was like creating a piece of a puzzle. We created **many small refactoring rules** and put the pieces together to modify the whole query.
For example, we wanted to convert FuelPHP...
```php
\DB::select_array(['id', 'name'])->from('user');
```
...to Laravel:
```php
\DB::table('user')->select_array(['id', 'name']);
```
For this refactoring, we created two rector rules.
1. Swap `from` and `select_array` and rename `from` to `table`
2. Convert `select_array` to `select`
### 1. Swap `from` and `select_array` and Rename `from` to `table`
The first rule can be written like this:
```php
public function getNodeTypes(): array
{
return [MethodCall::class];
}
public function refactor(Node $fromNode): ?Node
{
if (!$this->isName($fromNode->name, 'from')) {
return null;
}
$selectNode = $fromNode->var;
if (!$selectNode instanceof StaticCall ||
$this->isNames($selectNode->name, ['select', 'select_array'])) {
return null;
}
return new MethodCall(
new StaticCall(
new Node\Name\FullyQualified('DB'),
new Node\Identifier('table'),
$fromNode->args
),
$selectNode->name,
$selectNode->args
);
}
```
The rule goes step by step through conditions:
* Get method calls and check if the name is `from`.
* If the variable node of the method call is a static call of class `DB` named `select_array`
* Then swap the static call and method call and rename the static call to “table”
It's simple, isn't it?
### 2. Convert `select_array` to `select`
Then let's modify `select_array` to `select`. You need to expand the array to args and rename the method:
```php
public function getNodeTypes(): array
{
return [MethodCall::class];
}
public function refactor(Node $selectArrayNode): ?Node
{
if (!$this->isName($selectArrayNode->name, 'select_array')) {
return null;
}
if (count($selectArrayNode->args) !== 1) {
return null;
}
$array = $selectArrayNode->args[0]->value;
if (!$array instanceof Node\Expr\Array_) {
return null;
}
$selectArrayNode->name = new Node\Identifier('select');
$selectArrayNode->args = array_map(
fn(Node\Expr\ArrayItem $item) => new Node\Arg($item->value),
$array->items
);
return $selectArrayNode;
}
```
Great! Now we can convert the whole query running these 2 rules.
## New Features and Migration at the Same Time?
This is the most significant and wonderful benefit of automated migration.
It's explained in detail in the [previous post](/blog/how-to-migrate-legacy-php-applications-without-stopping-development-of-new-features), so take a look if you haven't read it yet!
## What was Important for Automated migration?
### Tests
Migrating tests together with the application code and running them can be a critical indicator that the application works after applying Rector.
Sadly, our project did not have enough tests…
### PHPStan
It was another hero of the project besides Rector.
We created a baseline first and ran them after running Rector. We could find codes broken by running Rector and fix the Rector rules.
### Rector rule Tests
**Rector rule tests gave great confidence** that the modification in the migration itself is working.
We wrote about **80 Rector rules** to migrate the application, and the tests helped us find rules broken by dependencies and breaking changes of Rector's updates.
### Abstract Syntax Tree (AST)
A deep understanding of AST and Rector itself is essential to write custom Rector rules.
The most efficient way for me to learn them was to write the test fixtures of the Rector rules and dump them by nikic/php-parser. Trial and error writing rules and dumping the AST was an excellent way to understand the structure.
Also, I read a lot of codes of Rector, php-parser, PHPStan, and Larastan to understand how they are using, working with AST.
But as a shortcut, there is a [book about Rector](https://leanpub.com/rector-the-power-of-automated-refactoring) that explains AST and other vital things about Rectory. Let's read the Rector book!
## What have we Struggled with?
### Codes too Complicated to Convert by Rector
Sometimes some codes were too complicated to write a Rector rule. In these cases, we refactored the code itself to make it possible to convert by Rector or delete them if we could.
We deleted 100k+ lines of code during the migration!
The important thing was that we were editing these codes in the "Development branch" to refactor and deploy the code in FuelPHP to confirm that the code was working before the migration release.
In some situations, writing custom rules is too tricky and expensive. We edited those in the migration branch and skipped automated migration for those files (about 10-20 files). It is essential to set a boundary, **what should be automated and what should be done manually**.
### Minor Differences between Frameworks
There were minor differences between frameworks, which were difficult to notice while writing custom rules.
For instance,
* FuelPHP return empty array response `response([])` with status code “204 No Contents” while Laravel does not
* FuelPHP `DB::insert` returns array of `['id', 'affected rows']` while Laravel `DB::insertGetId returns just 'id'`
* …etc.
For these differences, QA testing and canary release were crucial. We iterated testing over and over and fixed the custom rules to achieve the complete migration.
### Rector Bugs and Breaking Changes
We started the migration with Rector 0.9.x, and it's 0.12.x now! At 2020-2021, Rector was changing and evolving at a very high speed, and sometimes there were unstable versions with bugs. Also, some of our custom rules relied on Rector core codes, so there were significant breaking changes during the migration.
However, issues were already recognized by the community, and the fixes were extremely fast.
I very much appreciate the hard work of Tomas, other core developers, and the community of Rector!
## How was the Migration in a Brief?
The pros:
* Works on large codebases
* Can decrease human errors of migration
* Could continue developing new features and run migration at the same time with no conflicts
The cons:
* Converted code doesn't use the full functionality of Laravel
* You can refactor them after migration!
* Requires understanding of AST
* Let's read [the Rector book!](/book)
To be honest, **I don't have any big cons for automated migration. It was a great experience**, and I can say that we could not finish our migration without Rector.
Thank you!
-----------------------------------------
## How to Migrate Legacy PHP Applications Without Stopping Development of New Features
Perex: Migrating legacy PHP applications is always a challenging task.
Today I'll introduce one strategy to make these migrations easier by using the power of the Rector. With this strategy, we successfully migrated a legacy PHP application over a period of one year, **without stopping developing new features!**
*This is an introduction post by [rajyan](https://twitter.com/rajyan_k), who used Rector to migrate an extensive PHP application from FuelPHP to Laravel.*
## Difficulty of Migrating Legacy PHP Applications
Migrating requires high knowledge of PHP, a deep understanding of the application, **and time**. Especially in applications running in the production. Migration takes time and man-hours for code modification and testing that the application works after the migration.
While migrating is going on, you have to stop implementing new features. Otherwise, you'll fall into a hell of conflict and outdated branches. Stopping development for a specific time for the migration would be a difficult decision.
## How to Solve it?
Let's see the concept first:
The most important thing is to avoid conflicts.
We have to be careful not to edit the same file in the "Development branch" and the "Migration branch". We continue developing new features as usual in the "Development branch" directory, for example, `old/app`. Then we apply Rector rules to migrate `old/app` and copy the complete result to `new/app` in the "Migration branch", which automated the process in CI.
We are free from conflicts by always editing the app code in the "Development branch" and only adding files in the migration branch. We can also make our "Migration branch" to the "Development branch" by merging them.
You can easily run the new application by changing the namespace in `composer.json` if written in psr-4.
```diff
"autoload": {
"psr-4": {
- "App\\": "old/app"
+ "App\\": "new/app"
}
}
```
We gain enough time to test and fix bugs in the new application and keep developing in different branches with this approach.
## Summary
This article introduced one approach that can make migrating legacy PHP applications easier. We used this method and successfully migrated the framework of our extensive legacy PHP application. It can be helpful for other situations such as upgrading PHP versions or refactoring the codebase by Rector.
I'll talk about the whole story of the migration in the next post, so stay tuned!
-----------------------------------------
## New in Rector 0.12 - Much Simpler and Safer Rule Configuration
Perex: Configurable rules are the most powerful building stone for instant upgrade sets. Do you want to upgrade from Symfony 5 to 6? You'll primarily deal with renamed classes, renamed methods, new default arguments in method, or renamed class constants.
In the end, we have to configure around 10 rules to get the most job done. That's why we focused on developer experience and added a new `configure()` method in Rector 0.12.
Each configurable Rector rule implements own `configure()` method.
To register a rule in `rector.php`, we use [Symfony PHP configs](https://symfony.com/doc/current/service_container/configurators.html#using-the-configurator) syntax.
There we pass arguments that configure our rule, e.g. `RenameClassRector`:
```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',
],
]]);
};
```
While we have full autocomplete support thanks to PHP, this approach has a few downsides.
* we have to be careful about the exact syntax of nested arrays
* we have to use class constants to pass the nested array to
What exactly does it mean? Well, any of the following syntaxes would crash:
```php
->call('configure', [[[
...
]]]);
```
...or...
```php
->call('configure', [
...
]);
```
...or...
```php
->call('configure', [[
[
'App\SomeOldClass' => 'App\SomeNewClass',
],
]]);
```
We think that's unnecessary complexity. Do you agree?
## New `configure()` Method to the Rescue
We don't like to make you think about implementation details **We know there are more important goals you want to achieve**.
That's why **we remove the complexity** with the `configure()` method right in the `rector.php` config.
The goal is clear:
* no constants
* single method
* write only the configuration, nothing else
* Rector validates input for you
```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)
->configure([
'App\SomeOldClass' => 'App\SomeNewClass',
]);
};
```
Nice and clear.
## Say Good-Bye to Value Object Inlining
Symfony does not support value objects for configuration. That's why we had to come up with [own value object more inline](/blog/2020/09/07/how-to-inline-value-object-in-symfony-php-config) when we wanted to use value objects for configuration. It looked like this:
```php
use Rector\Transform\Rector\FuncCall\FuncCallToStaticCallRector;
use Symplify\SymfonyPhpConfig\ValueObjectInliner;
use Symfony\Component\DependencyInjection\Loader\Configurator\ContainerConfigurator;
$services->set(FuncCallToStaticCallRector::class)
->call('configure', [[
FuncCallToStaticCallRector::FUNC_CALLS_TO_STATIC_CALLS => ValueObjectInliner::inline([
new FuncCallToStaticCall('dump', 'Tracy\Debugger', 'dump'),
])
]]);
```
In Rector 0.12, you can use the simple syntax:
```php
use Rector\Transform\Rector\FuncCall\FuncCallToStaticCallRector;
use Symfony\Component\DependencyInjection\Loader\Configurator\ContainerConfigurator;
$services->set(FuncCallToStaticCallRector::class)
->configure([
new FuncCallToStaticCall('dump', 'Tracy\Debugger', 'dump'),
]);
```
## Bonus: Rector now Validates Your Input
One more thing...
While we were at it, we made sure the configuration input was now validated. Is there a better package than `webmozart/assert` to handle it?
So if you ever add however invalid configuration, we'll tell you what to fix the second you run Rector:
```php
use Rector\Rector\AbstractRector;
use Webmozart\Assert\Assert;
final class RenameClassRector extends AbstractRector
{
/**
* @param mixed[] $configuration
*/
public function configure(array $configuration) : void
{
$oldToNewClasses = $configuration[self::OLD_TO_NEW_CLASSES] ?? $configuration;
Assert::isArray($oldToNewClasses);
Assert::allString($oldToNewClasses);
$this->addOldToNewClasses($oldToNewClasses);
}
// ...
}
```
Give your `rector.php` fresh breeze look with Rector 0.12 and the new `configure()` method!
Happy coding!
-----------------------------------------
## New in Rector 0.12 - The Latest PHP in a Single Import
Perex: The most used feature of Rector is to keep you updated with the latest PHP. PHP 8.1 was released almost a month ago, so many projects started to use Rector to upgrade to PHP 8.1. There is a new import in your `rector.php` with every new version.
Soon, your config is cluttered with a list of imports. How can we reduce this complexity to a single line? How can we handle your-favorite-framework upgrade in second?
How do you upgrade to PHP 8.1 in 2 steps?
Register new PHP 8.1 set in `rector.php`:
```diff
use Rector\Set\ValueObject\SetList;
use Rector\Config\RectorConfig;
return function (RectorConfig $rectorConfig): void {
$rectorConfig->import(SetList::PHP_55);
$rectorConfig->import(SetList::PHP_56);
$rectorConfig->import(SetList::PHP_70);
$rectorConfig->import(SetList::PHP_71);
$rectorConfig->import(SetList::PHP_73);
$rectorConfig->import(SetList::PHP_74);
$rectorConfig->import(SetList::PHP_80);
+ $rectorConfig->import(SetList::PHP_81);
};
```
And run Rector:
```bash
vendor/bin/rector
```
That's it! Yet, there is a code smell lurking.
## Error-Prone Complexity
High complexity leads to a high error rate. What could go wrong with such a configuration?
* we can accidentally skip one of the versions - what version do we miss? 7.2
* we skip lower versions - the PHP 5.3 and 5.4 contain many valuable rules, but our code will still run the old syntax
* the more versions we want to upgrade, the longer config gets
## Config made Simple with Level Set
We wanted to lower complexity and remove these errors. That's why we add a new feature in Rector 0.12 - *Level sets*.
Now instead of gazillion lines with set imports, you can use just **the single latest level**:
```php
use Rector\Set\ValueObject\LevelSetList;
use Rector\Config\RectorConfig;
return function (RectorConfig $rectorConfig): void {
$rectorConfig->sets([
LevelSetList::UP_TO_PHP_81,
]);
};
```
That's it! No more place for mistakes.
The `LevelSetList` also includes the PHP target version parameter to ensure all rules are applied.
Next time you'll need to upgrade PHP, you can change only 1 line:
```diff
use Rector\Set\ValueObject\LevelSetList;
use Rector\Config\RectorConfig;
return function (RectorConfig $rectorConfig): void {
$rectorConfig->sets([
- LevelSetList::UP_TO_PHP_81,
+ LevelSetList::UP_TO_PHP_82,
]);
};
```
There are also `Rector\Set\ValueObject\DowngradeSetList` configs that make sure you downgrade with ease too!
Happy coding!
-----------------------------------------
## How all Frameworks can Bump to PHP 8.1 and You can Keep Using Older PHP
Perex: Imagine **hypothetical situation**: new major Symfony and Laravel are released in December 2021. We'll already have PHP 8.1 out by that time. There have been a lot of positive vibes about new PHP versions in the last year, so let's say the frameworks will take a brave leap forward.
[Symfony 6](https://symfony.com/releases/6.0) and [Laravel 9](https://blog.laravel.com/laravel-9-release-date) will require PHP 8.1 as a minimal version in their `composer.json`.
How would you react to such a move? What if you could keep using your current PHP version while using Symfony 6 or Laravel 9?
## Developers: "Please, Don't Force us to Upgrade PHP"
The day has finally come. The new Symfony and Laravel 9 are released. We want to enjoy the latest features, so we bump our `composer.json`:
```diff
{
"require": {
"php": "8.0",
- "symfony/console": "^5.3"
+ "symfony/console": "^6.0"
}
}
```
And run composer to update dependencies:
```bash
composer update
```
What happens?
*"Could not install "symfony/console" packages because it requires PHP 8.1.
It does not meet your constraint of PHP 8.0."*
This doesn't look good. Bumping to PHP 8.1 since day one might be a bit extreme, but **this situation happens in every PHP version bump**. No matter how small it is. Even if you bump from PHP 7.4 to 8.0:
There are always thousands of packages and projects that can't support the new PHP version you've decided to use.
## Maintainers: "We need new PHP Features"
On the other hand, there is no point in maintaining PHP 7.1, 7.2, 7.3, and 7.4. Most of these do not have security updates:
But most importantly, if we support the old PHP version, we can **say goodbye to features** like:
* typed properties
* promoted properties
* `match()`
* enums
* `#[attributes]`
* union types
* intersection types
Every package maintainer wants these features in its code. That's why we want to bump to PHP 8.1 in private projects as soon as possible.
## Developers versus Maintainers
The project maintainers want to bump to PHP 8.1 to enjoy the latest features. On the contrary, developers who use packages want support for their PHP version, so they're not forced to upgrade PHP.
Two sides against each other. Humans versus machines.
Which of them is evil, and which of them is good? It depends on what side of the barricade do you stand on.
## Is Peace Possible?
Disruptive evolution starts with an insane question.
What if we could move from one place to another at a speed of 100 km/h for just a few dollars per hour? Now we have trains, metro, and public transport system and consider such question stupid.
"What if you could keep using your current PHP version
while using Symfony 6 or Laravel 9?"
To start conflict resolution, we should look not at the differences but **at shared goals**. What do both camps want?
* fun to code
* 0 maintenance
We can achieve both of these if we add one step to the release workflow.
## Introducing Release Downgrades
Let's say Symfony 6 or Laravel 9 is released with a minimum of PHP 8.1. How can we change the release process so, in the end, even developers with PHP 8.0 can use them?
Typical release of new tag from maintainer to developer downloading the package has 5-step lifecycle:
* tag locally
* push tag to the remote repository
* let remote repository push the tag to Packagist via git hook
* let user require a new version in `composer.json`
* find the tag in Packagist and download the zip from GitHub
When we require `symfony/console:6.0.0` in our code, we get the same git hash version in `/vendor`.
## PHP-version Tailored Releases
The release process usually runs on GitHub, where also run GitHub Actions. That's where we can add step **that releases different PHP** versions of the same code:
```bash
php --version
# php 8.1
composer require symfony/console 6.0
```
Successfully installed 👍
```bash
php --version
# php 8.0
composer require symfony/console 6.0
```
Successfully installed 👍
Symfony 6.0 requires at least PHP 8.1. How is it possible we installed it on PHP 8? If we look closer, we'll see these are 2 different tags packages:
- Symfony 6.0.0.81 for PHP 8.1
- Symfony 6.0.0.80 for PHP 8.0
The last 2 numbers in the tag stand for the PHP version of the code.
**The goal** is to have the same features in both projects that use different PHP versions. We do 👍
## Downgrade in GitHub Action
Where is the trick here? The PHP 8.1 release is a release as we know it; instead of 6.0.0, we tag it 6.0.0.81.
The PHP 8.0 release has one extra step - downgrade code to PHP 8.0
```diff
* tag locally
+* downgrade to PHP 8.0
* push tag to the remote repository
* let remote repository push the tag to Packagist via git hook
* let user require a new version in `composer.json.
* find the tag in Packagist and download the zip from GitHub
```
We create a `rector-downgrade.php` config with downgrade set:
```php
use Rector\Set\ValueObject\DowngradeLevelSetList;
use Rector\Config\RectorConfig;
return function (RectorConfig $rectorConfig): void {
$rectorConfig->import(DowngradeLevelSetList::DOWN_TO_PHP_80);
};
```
Run Rector on the source code with this config:
```bash
vendor/bin/rector process /src --config rector-downgrade.php
```
After Rector finishes, the code in `/src` will have **PHP 8.0 syntax**. The release workflow will push it and tag it under `6.0.0.80`.
**This way, we can keep using our current PHP version, and maintainers can bump to the latest PHP ever.**
Happy coding!
Do you want to bring the peace to PHP world?
Check September issue of PHP[architekt], where you learn more.
-----------------------------------------
## How to Upgrade Annotations to Attributes
Perex: We used `@annotations` in PHP 7.4 and below. Now we can use native `#[attributes]` in PHP 8. They have better support in PHPStan and Rector, thanks to their native language nature.
The Internet is full of questions ["How can I use PHP 8 attributes instead of annotations in Doctrine?"](https://stackoverflow.com/questions/66769981/how-can-i-use-php8-attributes-instead-of-annotations-in-doctrine) or ["Converting Annotations to Attributes"](https://www.reddit.com/r/symfony/comments/lbvmdx/converting_annotations_into_attributes/).
Do you want to know the answer? Rector has a simple solution for you.
One package that added support for attributes is Doctrine:
```diff
use Doctrine\ORM\Mapping as ORM;
-/**
- * @ORM\Column(type="string")
- */
+#[ORM\Column(type: "string")]
```
Now, let's go to upgrade itself. It's effortless.
## Upgrade from Annotations to Attributes in 3 Steps
### 1. Configure `rector.php` to include all available attributes:
```php
use Rector\Config\RectorConfig;
return RectorConfig::configure()
->withAttributesSets();
```
Or limit to specific group - this option is better when you're addin Rector to a new project:
```php
use Rector\Config\RectorConfig;
return RectorConfig::configure()
->withAttributesSets(symfony: true);
```
### 2. Run Rector to upgrade your code
```bash
vendor/bin/rector process
```
### 3. Handle Manual Steps
* Do you use `doctrine/orm`? Be sure to use [at least version 2.9](https://github.com/doctrine/orm/releases/tag/2.9.0), where attributes were released.
* Do you use Doctrine with Symfony? Update the Symfony bundle mapping parser in your config to read attributes:
```diff
# config/packages/doctrine.yaml
doctrine:
orm:
mappings:
App:
- type: annotation
+ type: attribute
```
This enables new Symfony [auto-detection feature](https://github.com/symfony/symfony/pull/42054).
That's it! Now your codebase uses nice and shiny PHP 8 native attributes.
Happy coding!
-----------------------------------------
## Dropping Docker in Favor of Composer Install for Better Developer Experience
Perex: Some developers see Docker as default-to-use for Rector. Yet they struggle to run it successfully with fundamental changes like [renaming class from underscore to namespace](https://twitter.com/frankdejonge/status/1419298126133927941).
It's very frustrating for them, and they often end-up up deleting the tool with a bad feeling inside.
This cannot happen.
The core team around Rector is not using Docker for running because there is no need for it. The current Docker version is naturally obsolete and lacking behind PHP code development.
## Use Rector like PHPUnit
**With [downgrades](https://getrector.com/blog/2021/03/22/rector-010-released-with-php71-support) and [prefixed release](https://getrector.com/blog/prefixed-rector-by-default) repository, Rector can be easily installed on any project:**
```bash
composer require rector/rector --dev
```
Rector can save you hundreds of hours on automated refactoring and instant upgrades. But the main added features are in the long-term feedback and everyday work it resolves for you. With new rules and sets, you should never upgrade manually ever again. How?
The same way PHPUnit checks your tests in CI, [the same way Rector works in CI for you](https://getrector.com/blog/2020/10/05/how-to-make-rector-contribute-your-pull-requests-every-day).
If the PHP version is the limit, Docker will not help as Rector requires PHP 7.1 to run. There is [PHP 5.6 sponsorware program](https://getrector.com/blog/rector-for-php56-native) for projects who want to make it happen.
Saying that, we're dropping [Docker from native Rector support](https://github.com/rectorphp/rector-src/pull/614). Do you want to keep Docker for Rector alive? See [the issue](https://github.com/rectorphp/rector-src/pull/614) to propose community maintenance.
We believe this choice will radically improve experience for first time users and embrace focus on single codebase to make it strong and reliable.
Thank you and happy coding!
-----------------------------------------
## How to bump Minimal PHP Version without Leaving Anyone Behind?
Perex: Last week we introduced [Prefixed Rector by Default](/blog/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](/blog/prefixed-rector-by-default). 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`:
```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 :)
## 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:
```bash
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`:
```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.
```diff
{
"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
```bash
# 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](https://twitter.com/VotrubaT/status/1391445133405696014).
The question is not "how can we do it",
but "who will be next"?
Happy coding!
-----------------------------------------
## Prefixed Rector by Default
Perex: 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](https://phpstan.org/blog/phpstan-0-12-released#phar-distribution-by-default).*
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:
```bash
composer require symfony/console:2.8
composer require rector/rector --dev
```
❌
That's where [prefixed version](/blog/2020/01/20/how-to-install-rector-despite-composer-conflicts) helps too.
```bash
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:
```bash
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:
- adding [static reflection](/blog/2021/03/15/legacy-refactoring-made-easy-with-static-reflection)
- adding [custom static annotation parser](/blog/from-doctrine-annotations-parser-to-static-reflection)
- [automate downgrade to PHP 7.1](/blog/2021/03/22/rector-010-released-with-php71-support)
- polishing user experience while writing own Rector rules
- working with tests
- installing Rector extensions
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-prefixed` → `rector/rector` - the **distribution repository**
- `rector/rector` → `rector/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:
```bash
composer remove rector/rector-prefixed
composer require rector/rector:^0.11 --dev
```
For normal version:
```bash
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!
-----------------------------------------
## From Doctrine Annotations Parser to Static Reflection
Perex: 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](https://github.com/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:
```php
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:
```php
$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](/blog/2021/03/15/legacy-refactoring-made-easy-with-static-reflection) 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](https://github.com/doctrine/annotations/blob/c66f06b7c83e9a2a7523351a9d5a4b55f885e574/lib/Doctrine/Common/Annotations/DocParser.php#L1155).
This causes bugs like these:
```diff
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](https://github.com/rectorphp/rector/blob/0.10.3/packages/DoctrineAnnotationGenerated/ConstantPreservingDocParser.php#L796-L798) 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:
```diff
-* @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](https://github.com/phpstan/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:
```php
$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](https://github.com/phpstan/phpdoc-parser/issues/11), 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](https://github.com/phpstan/phpdoc-parser/pull/65) 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](https://github.com/doctrine/annotations/blob/1.13.x/lib/Doctrine/Common/Annotations/DocParser.php) 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?**
```php
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!
-----------------------------------------
## Rector 0.10 Released - with PHP 7.1 Support
Perex: 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](https://github.com/rectorphp/rector/pulse/monthly) 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](/blog/2021/03/15/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](/blog/2020/12/28/rector-09-released), 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](https://github.com/rectorphp/rector-prefixed) repository.
Use it like this:
```bash
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](https://github.com/rectorphp/rector/pull/5880/files)
We've reached PHP 7.1 and it's a great success. The next most wanted legacy PHP version is PHP 5.6. Can we downgrade Rector there too?
## 6 Standalone Project Packages
Let's be honest. The Rector repository got a bit fat around the belly over the years. It got out of shape, collecting too much clutter at once. Rector included every framework right in the core. Do you use Symfony? There is also CakePHP, Doctrine and Laravel.
It's like asking for Vietnamese translation for Bum Bo Nam Bo and getting 30 other languages as a bonus.
Grouping those projects together also **created a barrier for contributors**. Too much code to handle if you wanted to add a single Symfony rule.
Contrary to that, we can see [drupal-rector](https://github.com/palantirnet/drupal-rector) or [typo3-rector](https://github.com/sabbelasichon/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**:
- [rector-symfony](https://github.com/rectorphp/rector-symfony)
- [rector-phpunit](https://github.com/rectorphp/rector-phpunit)
- [rector-doctrine](https://github.com/rectorphp/rector-doctrine)
- [rector-cakephp](https://github.com/rectorphp/rector-cakephp)
- [rector-laravel](https://github.com/rectorphp/rector-laravel)
- [rector-phpoffice](https://github.com/rectorphp/rector-phpoffice)
- [rector-downgrade-php](https://github.com/rectorphp/rector-downgrade-php)
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](https://github.com/rectorphp/rector-prefixed) 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:
```php
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!
-----------------------------------------
## Legacy Refactoring made Easy with Static Reflection
Perex: 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?
```php
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](https://phpstan.org/blog/zero-config-analysis-with-static-reflection).
## Configuration Everywhere
In Rector 0.9 and below, you could define files and directories to autoload - with a bit of [RobotLoader help](https://tomasvotruba.com/blog/2020/06/08/drop-robot-loader-and-let-composer-deal-with-autoloading/).
```php
// 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:
```bash
vendor/bin/rector process someFile.php
```
We had to include the file:
```bash
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:
```diff
-$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:
```bash
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](https://getrector.com/demo).
Happy coding!
-----------------------------------------
## How much does Single Type Declaration Know?
Perex: 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](https://tomasvotruba.com/blog/2019/11/11/from-0-doc-types-to-full-type-declaration-with-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:
```php
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:
```php
$person = new Person(1000);
```
Or even this (don't try to imagine it):
```php
$person = new Person(1000);
$anotherPerson = new Person($person);
```
See [3v4l.org](https://3v4l.org/Of6KU).
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:
```diff
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:
```diff
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:
```diff
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?
```diff
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](https://github.com/rectorphp/rector/blob/master/config/set/type-declaration-strict.php) yourself of pick rule by rule, so you can see how your code base becomes strict for each new rule you add:
```php
use Rector\TypeDeclaration\Rector\Param\ParamTypeFromStrictTypedPropertyRector;
use Rector\TypeDeclaration\Rector\ClassMethod\ReturnTypeFromReturnNewRector;
use Rector\TypeDeclaration\Rector\ClassMethod\ReturnTypeFromStrictTypedCallRector;
use Rector\TypeDeclaration\Rector\ClassMethod\ReturnTypeFromStrictTypedPropertyRector;
use Rector\TypeDeclaration\Rector\Property\TypedPropertyFromStrictConstructorRector;
use Rector\Config\RectorConfig;
return function (RectorConfig $rectorConfig): void {
$rectorConfig->rule(ParamTypeFromStrictTypedPropertyRector::class);
$rectorConfig->rule(ReturnTypeFromReturnNewRector::class);
$rectorConfig->rule(ReturnTypeFromStrictTypedPropertyRector::class);
$rectorConfig->rule(ReturnTypeFromStrictTypedCallRector::class);
$rectorConfig->rule(TypedPropertyFromStrictConstructorRector::class);
};
```
Happy coding!
-----------------------------------------
## How to Instantly Decouple Symfony Doctrine Repository Inheritance to Clean Composition
Perex: 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](https://tomasvotruba.com/blog/2017/10/16/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](https://tomasvotruba.com/blog/2017/10/16/how-to-use-repository-with-doctrine-as-service-in-symfony/#how-to-make-this-better-with-symfony-3-3), where the goal is to [turn inheritance to composition](https://github.com/jupeter/clean-code-php#prefer-composition-over-inheritance) - one of SOLID principles.
**Instead of inheritance...**
```php
namespace App\Repository;
use App\Entity\Post;
use Doctrine\ORM\EntityRepository;
final class PostRepository extends EntityRepository
{
}
```
**...we use composition:**
```php
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
```bash
composer install rector/rector --dev
```
### 2. Setup `rector.php`
```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:
```bash
vendor/bin/rector process /app
```
You will see diffs like:
```diff
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!
-----------------------------------------
## How to Instantly Refactor Symfony Action Injects to Constructor Injection
Perex: 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](https://laravel.com/docs/8.x/controllers#method-injection) and [Symfony feature](https://symfony.com/doc/3.4/service_container/3.3-di-changes.html#controllers-are-registered-as-services), that turns Controller action method to injected constructor:
```php
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](https://tomasvotruba.com/blog/2018/04/23/how-to-slowly-turn-your-symfony-project-to-legacy-with-action-injection/):
Action injection makes it confusing whether an object is treated stateful or stateless - a very grey area with, e.g., the Session.
I'm a Symfony trainer, and I'm told to teach people how to use Symfony and talk about this injection pattern. Sob.
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.
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...
```php
final class SomeController
{
public function detail(int $id, Request $request, ProductRepository $productRepository)
{
$this->validateRequest($request);
$product = $productRepository->find($id);
// ...
}
}
```
**...to the constructor injection:**
```php
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
- services
- [`Request` objects](https://symfony.com/doc/current/controller.html#controller-request-argument)
- and [Argument Resolver objects](https://symfony.com/doc/current/controller/argument_value_resolver.html)
- 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
```bash
composer install rector/rector --dev
```
### 2. Prepare Config
Enable the set and configure your Kernel class name in `rector.php` config:
```php
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
```bash
vendor/bin/rector process /app
```
You will see diffs like:
```diff
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!
-----------------------------------------
## Smooth Upgrade to Nette 3.1 in Diffs
Perex: 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
```diff
{
"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](https://sensible.com/dont-make-me-think/) makes it harder to separate classes from interfaces, and it affects over 30 class names:
```diff
-use Nette\Application\UI\ITemplate;
+use Nette\Application\UI\Template;
-final class SomeTemplate implements ITemplate
+final class SomeTemplate implements Template
{
// ...
}
```
```diff
foreach ($rows as $row) {
- /** @var \Nette\Database\IRow $row */
+ /** @var \Nette\Database\Row $row */
$row->...()
}
```
```diff
-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:
```diff
-use Nette\Forms\IControl;
+use Nette\Forms\Control;
use Nette\Applicatdion\UI\Control;
```
```diff
-use Nette\Application\UI\ITemplate;
+use Nette\Application\UI\Template;
use Nette\Bridges\ApplicationLatte\Template;
```
Don't forget about configs:
```diff
services:
nette.mailer:
- class: Nette\Mail\IMailer
+ class: Nette\Mail\Mailer
```
### 3. From Magical `RouteList` to `addRoute()` Method
```diff
$routeList = new RouteList();
-$routeList[] = new Route('/[/]', 'Homepage:default');
+$routeList->addRoute('/[/]', 'Homepage:default');
return $routeList;
```
### 4. Form `addImage()` to `addImageButton()`
```diff
$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](https://doc.nette.org/en/3.0/bootstrap#toc-dynamic-parameters):
```diff
// bootstrap.php
$configurator = new Nette\Configurator();
-$configurator->addParameters([
+$configurator->addStaticParameters([
// ...
]);
$configurator->addDynamicParameters([
// ...
]);
```
### 6. Renamed `DefaultTemplate` class
The ~~i~~nterface rename sometimes took already existing class names.
That's why some classes had to be renamed too:
```diff
-use Nette\Bridges\ApplicationLatte\Template;
+use Nette\Bridges\ApplicationLatte\DefaultTemplate;
```
### 7. New Param in `sendTemplate()` Method
```diff
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](https://github.com/nette/caching/commit/5ffe263752af5ccf3866a28305e7b2669ab4da82#diff-c3abbeb8a7d04b072c4f38e38e10d763123f5503f1719f852ec21e1602c4c4db) resolve data first, then pass it to cache `save()` method:
```diff
-$result = $this->cache->save($key, function () {
- return $this->getSomeHeavyResult();
-});
+$result = $this->getSomeHeavyResult();
+$this->cache->save($key, $result);
```
### 9. Cookie now send Lax by default
```diff
session:
autoStart: false
- cookieSamesite: Lax
```
```diff
$this->response->setCookie(
$key,
$data,
$expire,
null,
null,
$this->request->isSecured(),
- true,
- 'Lax',
);
```
You can read more [detailed post about changes](https://forum.nette.org/cs/34080-novinky-v-nette-3-1-je-venku) 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`
```php
use Rector\Nette\Set\NetteSetList;
use Rector\Config\RectorConfig;
return function (RectorConfig $rectorConfig): void {
$rectorConfig->import(NetteSetList::NETTE_31);
};
```
2. Run Rector:
```bash
vendor/bin/rector process
```
That's it!
Have we missed something? [Create an issue](https://github.com/rectorphp/rector/issues/new?assignees=&labels=feature&template=2_Feature_request.md) so we can complete the set for other developers.
Happy coding!
-----------------------------------------
## Switch Symfony String Route Names to Constants
Perex: Last December, we started to use PHP 8.0 and Symfony 5.2. This exact combination opens [many cool tricks](https://tomasvotruba.com/blog/2020/12/21/5-new-combos-opened-by-symfony-52-and-php-80/) 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](https://getrector.com/blog/2020/11/30/smooth-upgrade-to-php-8-in-diffs).
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".
```php
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:
```php
use Symfony\Component\Routing\Annotation\Route;
final class MissionController
{
/**
* @Route(path="archive", name="mission")
*/
public function __invoke()
{
// ...
}
}
```
We can use PHP-native attribute:
```php
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:
```php
use Symfony\Component\Routing\Annotation\Route;
final class MissionController
{
#[Route(path: 'mission', name: 'mission')]
public function __invoke()
{
// ...
}
}
```
```php
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?
```diff
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()
{
// ...
}
}
```
```diff
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`:
```php
use Rector\SymfonyCodeQuality\Rector\Attribute\ExtractAttributeRouteNameConstantsRector;
use Rector\Config\RectorConfig;
return function (RectorConfig $rectorConfig): void {
$rectorConfig->rule(ExtractAttributeRouteNameConstantsRector::class);
$rectorConfig->importNames();
};
```
Run Rector:
```bash
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](https://github.com/rectorphp/getrector.org/pull/235/files).
## The Future Scope
With invokable controllers, we might get even to this:
```php
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!
-----------------------------------------
## 7 Valuable Lessons We Learned from our Clients in 2020
Perex: 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:
```json
{
"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](https://tomasvotruba.com/blog/2018/11/19/when-you-should-use-monorepo-and-when-local-packages/), 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`:
```diff
{
"reqiure": {
- "nette/application": "^2.4",
+ "nette/application": "^3.0"
}
}
```
Run:
```bash
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](https://github.com/symplify/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](https://tomasvotruba.com/blog/2020/01/13/why-is-first-instant-feedback-crucial-to-developers/) 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](https://tomasvotruba.com/blog/2019/12/16/8-steps-you-can-make-before-huge-upgrade-to-make-it-faster-cheaper-and-more-stable/). 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](https://github.com/symplify/class-presence)
- 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](https://github.com/rectorphp/rector/pull/4447). 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:
```bash
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!
-----------------------------------------
## Rector 0.9 Released ❄️
Perex: 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](https://getrector.com/blog/2020/11/30/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!
```php
use Rector\Set\ValueObject\SetList;
use Rector\Config\RectorConfig;
return function (RectorConfig $rectorConfig): void {
$rectorConfig->import(SetList::PHP_80);
};
```
## Debuggable Rector Prefixed
The PHAR file is a file that contains PHP code in a single RAR file, thus the PH+AR acronym. Working with PHAR in PHP has it's edge cases. Do you use a real path? [realpath in PHAR does not work](https://bugs.php.net/bug.php?id=52769). Everything has to be relative to some PHAR root. Another bunch of bugs happen [in Symfony](https://tomasvotruba.com/blog/2019/12/02/how-to-box-symfony-app-to-phar-without-killing-yourself/), thanks to `glob` and slash juggling. The PHAR itself caused over [143 issues](https://github.com/rectorphp/rector/search?q=prefixed+is%3Aissue&type=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](https://github.com/rectorphp/rector/pull/4559/files).
### Try it Now
Install path remains the same:
```bash
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](https://www.php.net/supported-versions.php) 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](https://blog.logrocket.com/coding-in-php-7-4-and-deploying-to-7-1-via-rector-and-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?
```bash
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](https://github.com/rectorphp/rector/pull/4447). Stay tuned!
## Upgrade from 0.8 to 0.9
Do you want to try a new version but fear changes? Check [`UPGRADE_09.md`](https://github.com/rectorphp/rector/blob/master/UPGRADE_09.md) for specific steps and go for it:
```bash
composer update rector/rector:^0.9
```
## Thank You, Contributors
Last but not least, we'd like to thank [all 0.9 contributors](https://github.com/rectorphp/rector/graphs/contributors?from=2020-11-15&to=2020-12-27&type=c) that put their keystrokes together as one.
Thank you [@samsonasik](https://github.com/samsonasik), [@leoloso](https://github.com/leoloso), [@simivar](https://github.com/simivar), [@staabm](https://github.com/staabm), [@Wirone](https://github.com/Wirone), [@HDVinnie](https://github.com/HDVinnie), [@lulco](https://github.com/lulco) and [@JanMikes](https://github.com/JanMikes).
Happy [rectoring](https://rectoring.com/)!
-----------------------------------------
## 4 Configurable PHPStan rules that Help Rector Merge 188 pull-requests a Month
Perex: Last month, we merged a total [188 pull-requests](https://github.com/rectorphp/rector/pulse/monthly) 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:
```php
use Symfony\Component\DependencyInjection\Variable;
use Symplify\RuleDocGenerator\ValueObject\RuleDefinition;
use PhpParser\Node;
use Rector\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!
```diff
-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**.
```yaml
# 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](https://github.com/symplify/symplify/blob/master/packages/phpstan-rules/src/Rules/PreferredClassRule.php) 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](https://github.com/rectorphp/rector/blob/master/docs/how_to_add_test_for_rector_rule.md), 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:
```yaml
# phpstan.neon
services:
-
class: Symplify\PHPStanRules\Rules\SeeAnnotationToTestRule
tags: [phpstan.rules.rule]
arguments:
requiredSeeTypes:
- Rector\Rector\AbstractRector
```
An unintended side effect of the [SeeAnnotationToTestRule](https://github.com/symplify/symplify/blob/master/packages/phpstan-rules/src/Rules/SeeAnnotationToTestRule.php) 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](/rule-detail/add-see-test-annotation-rector) 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:
```php
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](https://blog.codinghorror.com/the-broken-window-theory/), we've added a custom PHPStan rule.
This [OnlyOneClassMethodRule](https://github.com/symplify/symplify/blob/master/packages/phpstan-rules/src/Rules/OnlyOneClassMethodRule.php) checks classes of specific type(s), and makes sure there **is exactly 1 method** used from defined list:
```yaml
# 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?
```php
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?
```diff
- 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](https://github.com/symplify/symplify/blob/master/packages/phpstan-rules/src/Rules/CheckTypehintCallerTypeRule.php):
```yaml
# phpstan.neon
services:
-
class: Symplify\PHPStanRules\Rules\CheckTypehintCallerTypeRule
tags: [phpstan.rules.rule]
```
Thanks to [`@samsonasik`](https://github.com/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](https://github.com/symplify/phpstan-rules). Get them, use them. Your code will thank you.
That's all for today.
Happy coding!
-----------------------------------------
## Laravel Facades to Constructor Injection: Replace Facade Aliases with Full Classes in 2 hours
Perex: Laravel facades are known as [static service locators](https://sergeyzhuk.me/2016/05/27/laravel-facades/). 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](https://stackoverflow.com/questions/49138428/avoid-laravel-facade-on-controller) [moving away](https://github.com/laravel/ideas/issues/1508) [from](https://programmingarehard.com/2014/01/11/stop-using-facades.html/) [facades](https://www.freecodecamp.org/news/moving-away-from-magic-or-why-i-dont-want-to-use-laravel-anymore-2ce098c979bd/#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](https://github.com/laravel/ideas/issues/1508):
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:
```php
// 'original class', 'alias'
class_alias('Some\LongerClass\App', 'App');
```
That means these 2 lines are identical:
```php
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
```bash
composer require rector/rector --dev
# creates "rector.php"
vendor/bin/rector init
```
2. Configure `rector.php`
Look how Laravel is helping us here. Just copy paste lines from `config/app.php` → `aliases`:
```php
use Rector\Renaming\Rector\Name\RenameClassRector;
use Rector\Config\RectorConfig;
return function (RectorConfig $rectorConfig): void {
$rectorConfig->ruleWithConfiguration(RenameClassRector::class, [
'App' => 'Illuminate\Support\Facades\App',
'Artisan' => 'Illuminate\Support\Facades\Artisan',
'Auth' => 'Illuminate\Support\Facades\Auth',
'Blade' => 'Illuminate\Support\Facades\Blade',
'Broadcast' => 'Illuminate\Support\Facades\Broadcast',
'Bus' => 'Illuminate\Support\Facades\Bus',
'Cache' => 'Illuminate\Support\Facades\Cache',
'Config' => 'Illuminate\Support\Facades\Config',
// ...
]);
};
```
3. Run Rector
```bash
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](https://eonx.com/).
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!
-----------------------------------------
## Smooth Upgrade to PHP 8 in Diffs
Perex: 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](https://stitcher.io/blog/new-in-php-8) 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:
```bash
composer require rector/rector --dev
# create "rector.php"
vendor/bin/rector init
```
2. Update `rector.php` with PHP 8 set:
```diff
+use Rector\Set\ValueObject\SetList;
use Rector\Config\RectorConfig;
return function (RectorConfig $rectorConfig): void {
+ $rectorConfig->import(SetList::PHP_80);
};
```
3. Run Rector:
```bash
vendor/bin/rector process src
```
How does such upgrade look in practise? See one of real pull-requests created with Rector:
- [tomasvotruba.com](https://github.com/TomasVotruba/tomasvotruba.com/pull/1107/files)
- [getrector.com](https://github.com/rectorphp/getrector.org/pull/190/files)
- [friendsofphp.org](https://github.com/TomasVotruba/friendsofphp.org/pull/176/files)
### 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()`
```diff
-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`
```diff
-get_class($object);
+$object::class;
```
### 3. From Dummy Constructor to Promoted Properties
```diff
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
```diff
class SomeClass
{
- final private function getter()
+ private function getter()
{
return $this;
}
}
```
### 5. Replace Null Checks with Null Safe Calls
```diff
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
```diff
final class SomeClass
{
public function run()
{
try {
- } catch (Throwable $notUsedThrowable) {
+ } catch (Throwable) {
}
}
}
```
### 7. New `str_contains()` Function
```diff
-$hasA = strpos('abc', 'a') !== false;
+$hasA = str_contains('abc', 'a');
```
### 8. New `str_starts_with()` Function
```diff
-$isMatch = substr($haystack, 0, strlen($needle)) === $needle;
+$isMatch = str_starts_with($haystack, $needle);
```
### 9. New `str_ends_with()` Function
```diff
-$isMatch = substr($haystack, -strlen($needle)) === $needle;
+$isMatch = str_ends_with($haystack, $needle);
```
### 10. New `Stringable` Interface for
```diff
-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:
```php
function run(string $anyString)
{
// ...
}
$name = new Name('Kenny');
run($name);
```
### 11. From Union docblock types to Union PHP Declarations
```diff
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
```diff
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
```diff
-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:
```diff
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:
```diff
####
## 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:
```diff
jobs:
unit_tests:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- 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:
```diff
{
"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](https://www.youtube.com/watch?v=c3bpTBjhK2Y)** about this by [Nikita Popov](https://twitter.com/nikita_ppv), the most active PHP core developer, and [Nikolas Grekas](https://twitter.com/nicolasgrekas), 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](https://php.watch/articles/composer-ignore-platform-req):
```diff
-composer install
+composer update --ignore-platform-req php
```
Upgrade your CI workflows and Docker build scripts, and you're ready to go.
Happy coding!
-----------------------------------------
## How to make Rector Contribute Your Pull Requests Every Day
Perex: 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](https://packagist.org/packages/rector/rector/dependents?order_by=downloads) 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](http://github.com/rectorphp/rector) since [March 2020](https://github.com/rectorphp/rector/pull/3013/files) when [Jan Mikes](https://github.com/JanMikes) 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](https://github.com/easy-coding-standard/easy-coding-standard) 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](https://tomasvotruba.com/blog/2017/10/16/how-to-use-repository-with-doctrine-as-service-in-symfony/).
**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](https://github.com/TomasVotruba/unused-public/blob/main/.github/workflows/rector.yaml) to handle this process.
**Do you want to try it?** Copy it, create [new access token](https://github.com/settings/tokens), add `ACCESS_TOKEN` env variable to [repository Secrets](https://github.com/TomasVotruba/unused-public/settings/secrets), and you're ready to make your first actively reviewed pull-request.
```yaml
# .github/workflows/rector.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@v4
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: 8.1
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:
```yaml
## 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:
```yaml
- 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](https://github.com/TomasVotruba/unused-public/blob/main/.github/workflows/rector.yaml).
Happy coding!
-----------------------------------------
## How to Inline Value Object in Symfony PHP Config
Perex: Rector uses [PHP Symfony configs](/blog/2020/08/31/rector-is-moving-from-yaml-to-php-configs-what-changes-and-how-to-get-ready) for [many good reasons](https://tomasvotruba.com/blog/2020/07/16/10-cool-features-you-get-after-switching-from-yaml-to-php-configs/).
One of them is the possibility to have control over complex configurations with value objects.
Would you like such features in your configs too? Unfortunately, Symfony does not support it out of the box.
What can we do about it?
Since Rector 0.12 we deal with value objects for you with configure() method. The configuration is simpler and input is validated.
## Why are Value Objects in Configuration Priceless?
Let's say we want to rename `dump()` func call to `Tracy\Debugger::dump()`.
```diff
-dump($value);
+Tracy\Debugger::dump($value);
```
KISS. Why not use a simple array?
Look at our [previous post](/blog/2020/08/31/rector-is-moving-from-yaml-to-php-configs-what-changes-and-how-to-get-ready#value-objects-configuration-ftw) 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:
```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:
```php
// rector.php
$services->set(FuncCallToStaticCallRector::class)
->call('configure', [[
FuncCallToStaticCallRector::FUNC_CALLS_TO_STATIC_CALLS => [
new FuncCallToStaticCall('dump', 'Tracy\Debugger', 'dump'),
]
]]);
```
↓
```bash
[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:
```php
// 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:
```php
new SomeClass('value');
```
- 1 line solution
- Ideally, in Symfony way, so it the least surprise to use
To cover all benefits together, we've created custom `ValueObjectInliner` class:
```php
// rector.php
$services->set(FuncCallToStaticCallRector::class)
->call('configure', [[
FuncCallToStaticCallRector::FUNC_CALLS_TO_STATIC_CALLS => ValueObjectInliner::inline([
new FuncCallToStaticCall('dump', 'Tracy\Debugger', 'dump'),
new FuncCallToStaticCall('d', 'Tracy\Debugger', 'dump'),
])
]]);
```
## 4. Simple `configure()` Method
Since Rector 0.12 you can use `->configure()` method, that handles the complexity for you:
```php
$services->set(FuncCallToStaticCallRector::class)
->configure([
new FuncCallToStaticCall('dump', 'Tracy\Debugger', 'dump'),
// it handles multiple items without duplicated call
new FuncCallToStaticCall('d', 'Tracy\Debugger', 'dump'),
new FuncCallToStaticCall('dd', 'Tracy\Debugger', 'dump'),
]);
```
Instead of complexity keys, constants and inliners, just pass it array of value objects.
And you're set!
✅
This way, **anyone can configure even the most complex Rector rules** without ever looking inside the Rector rule and scan for configuration values.
## And What Rector uses What Value Object?
In convention over configuration principle, all you need to know **the rule name**.
Could you guess the value object naming pattern?
- `FuncCallToStaticCallRector` (rule)
- `FuncCallToStaticCall` (value object)
That's it!
Happy and safe coding!
-----------------------------------------
## Rector is Moving From YAML to PHP Configs - What Changes and How to Get Ready?
Perex: 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
services();
$services->set(RenameClassRector::class)
->configure([
'old_2' => 'new_2',
]);
```
If you have 2 configs with same method call and same argument, **only the latest one is used**:
```php
// another config
$services->set(RenameClassRector::class)
->configure([
'old_1' => 'new_1',
]);
```
**We fixed this behavior from "override" to "merge"** with [custom file loader](https://github.com/rectorphp/rector/pull/4081/files#diff-1f79bb7ffdca1f08c0a6ac35bbb2d928). It collects all the arguments and sets them once in the end before the container is compiled.
Now, even our [demo](/demo) runs on PHP configs:
Saying that yesterday [we dropped YAML config support from Rector core](https://github.com/rectorphp/rector/pull/4081).
## 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](https://tomasvotruba.com/blog/2020/07/27/how-to-switch-from-yaml-xml-configs-to-php-today-with-migrify/) got you covered:
```bash
composer require symplify/config-transformer --dev
```
Then provide files/directories with YAML files you want to switch to PHP:
```bash
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](https://tomasvotruba.com/blog/2020/07/16/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.
```yaml
services:
# rename class
Rector\Renaming\Rector\Name\RenameClassRector:
$oldToNewClasses:
'OldClass': 'NewClass'
```
Be sure to [bother your brain with memorizing](https://tomasvotruba.com/blog/2018/08/27/why-and-how-to-avoid-the-memory-lock/) `$oldToNewClasses`. And this is just `key: value` complexity - the simplest one.
What about **more common nested configuration**, e.g., constant rename?
```yaml
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**.
```php
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?
```php
use Rector\Renaming\Rector\ClassConstFetch\RenameClassConstantRector;
use Rector\Config\RectorConfig;
return function (RectorConfig $rectorConfig): void {
$rectorConfig->ruleWithConfiguration(RenameClassConstantRector::class, [
new ClassConstantRename('Cake\View\View', 'NAME_ELEMENT', 'TYPE_ELEMENT')
]);
};
```
That's it. Use IDE autocomplete and value objects.
**There is no need to look into the rule and look for the parameter name**, the meaning of that particular key or value, if the key/value is required, optional, named, or implicit.
We're thinking of [introducing rule <=> value object naming convention](https://github.com/rectorphp/rector/issues/4086), 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](https://twitter.com/VotrubaT/status/1297974889148813322).
Happy coding!
-----------------------------------------
## How to Migrate From PHPExcel to PHPSpreadsheet with Rector in 30 minutes
Perex: [PHPExcel](https://github.com/PHPOffice/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](https://github.com/PHPOffice/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](https://github.com/PHPOffice/PhpSpreadsheet/blob/master/docs/topics/migration-from-PHPExcel.md) 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()`](https://github.com/PHPOffice/PhpSpreadsheet/blob/87f71e1930b497b36e3b9b1522117dfa87096d2b/src/PhpSpreadsheet/Helper/Migrator.php#L329). Regular expressions are not the best thing to use to modify code, rather contrary.
But there are also extra method calls:
```diff
-$worksheet->getDefaultStyle();
+$worksheet->getParent()->getDefaultStyle();
```
Method call renames:
```diff
-$worksheet->setSharedStyle($sharedStyle, $range);
+$worksheet->duplicateStyle($sharedStyle, $range);
```
Argument switch and extra method calls:
```diff
-$cell = $worksheet->setCellValue('A1', 'value', true);
+$cell = $worksheet->getCell('A1')->setValue('value');
```
Method move to another class:
```diff
-PHPExcel_Cell::absoluteCoordinate()
+PhpOffice\PhpSpreadsheet\Cell\Coordinate::absoluteCoordinate()
```
and [so on](https://github.com/PHPOffice/PhpSpreadsheet/blob/50d78ce7898ee3a540cefd9693085b3636e578e6/docs/topics/migration-from-PHPExcel.md).
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**](https://github.com/rectorphp/rector-phpoffice/blob/main/config/sets/phpexcel-to-phpspreadsheet.php) of configuration and **17 rules**.
## How to Migrate?
1. Install Rector
```bash
composer require rector/rector-phpoffice --dev
```
2. Create `rector.php` config
```bash
vendor/bin/rector init
# on Windows?
vendor\bin\rector init
```
3. Add your set to the `rector.php` config:
```php
use Rector\PHPOffice\Set\PHPOfficeSetList;
use Rector\Config\RectorConfig;
return function (RectorConfig $rectorConfig): void {
$rectorConfig->import(PHPOfficeSetList::PHPEXCEL_TO_PHPSPREADSHEET);
};
```
4. Dry run Rector to see what *would be* changed
```bash
vendor/bin/rector process src --dry-run
```
5. Make the changes happen:
```bash
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](https://github.com/PHPOffice/PhpSpreadsheet/blob/50d78ce7898ee3a540cefd9693085b3636e578e6/docs/topics/migration-from-PHPExcel.md) or [let us know on GitHub](https://github.com/rectorphp/rector/issues).
Happy coding!
-----------------------------------------
## Upgrading Glami to PSR-4, part 1: What and why?
Perex: In April 2019 we upgraded [Glami](https://glami.cz)'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](https://www.php-fig.org/psr/psr-4/) in the PHP world, supported for example by composer or by [PHPStorm since 2014](https://blog.jetbrains.com/phpstorm/2014/04/psr-0-psr-4-and-sourcetest-root-support-in-phpstorm-8-eap/).
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
App\Demo\Controller\DemoController
App
./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](https://github.com/rectorphp/getrector.org/blob/master/composer.json):
```json
{
"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](https://github.com/nette/robot-loader) (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](https://twitter.com/mikes_honza/status/1224818282143809537?s=20) - 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](https://getrector.com/contact) achieve this great success too!
-----------------------------------------
## How to install Rector despite Composer Conflicts
Perex: 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](https://www.tomasvotruba.com/blog/2019/02/28/how-to-upgrade-symfony-2-8-to-3-4/), 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:
```bash
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
- [bamarni/composer-bin-plugin](https://github.com/bamarni/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:
```php
// version 2.8
class YourFavoriteFrameworkClass
{
public function process($name)
{
}
}
```
vs
```php
// 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](https://3v4l.org/Znrnq):
```bash
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](https://janmikes.cz/) Rector also [runs in Docker](https://github.com/rectorphp/rector#run-rector-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:
```php
// your code
namespace Symfony\Component\Console;
class Command
{
}
```
```php
// Rector code
namespace Symfony\Component\Console;
class Command
{
}
```
In short:
```bash
Symfony\Component\Console\Command
Symfony\Component\Console\Command
```
Any ideas?
Well, one of them probably... has to be **named differently**?
```bash
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](https://github.com/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](https://johnlinhart.com/) 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](https://github.com/rectorphp/rector) repository.
You can **install it** despite having conflicting packages:
```bash
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**.
-----------------------------------------