Cleaning Lady Notes: From Class Mess to PSR-4 Step by Step With Confidence

This post was updated at August 2020

Updated Rector YAML to PHP configuration, as current standard.


Today I'm starting a new post series - Cleaning Notes. These posts are for people who are aspiring legacy migrators with a vision to improve private PHP ecosystem and bring joy to coding with gigantic applications again. The same vision we have in the Rector team.

In this series, you can learn about my experience, tricks, tips, and what fucked me up. So you save some frustration, where is not needed, discover hidden shortcuts and cool tools you never saw before.

We start with the most problematic topic in PHP legacy, that every project needs, but almost none has - transition to PSR-4.

Dedicated to Kerrial, my great friend who teaches me so much about not giving a f_ck and just do the stuff.
Thanks, dude!


How to Approach the Migration Itself?

Know Your Enemy

There are many use cases that we have to handle to get to PSR-4. Honestly, I find it easier to switch a framework, where start is clear and goal is clear.

In PSR-4 migration, we have a clear goal:

Start has Many Ugly Forms

if (! class_exists('SomeClass')) {
    final class SomeClass
    {
        // ...
    }
}

A lot to suck in, right? Don't worry; each of them has a guide to follow.

Low Hanging Fruit

Each project is different, some of them has functions mixed with HTML, some is missing composer completely, some needs to switch from custom-framework autoloading.

But you should always apply basic rule:

"Take the low hanging fruit first."

Always go for a simple target first. Don't be a hero. A hero falls from the sky after a massive battle over Atlantic, forgot to charge his smartphone... and dies alone.

Be professional, close quickly, close early. Are there 3 files with 20 classes in them?

Done. You've just made a first small step. Cross one step of your list, 9/10 is left. But all those 9 steps are now 10 % less complicated.

"Even if you die, the code you wrote is merged."
Bus Boy Scout Factor

I love this coding principle. Why? Because it takes minimalism and productivity to the practical world. It narrows our focus, so any developer becomes a 10x programmer effortlessly.


What Exactly to Do? - Case Study

Enough theory. Let's look at the project we've recently migrate to PSR-4 and how exactly we did it.

This is not paid promo, but Amateri are hiriging. We're far enough with the migration, so I'm confident it would be fun to work with such codebase.

1. Split Multiple Classes in 1 File

How does it look?

 // SomeFile.php
 class SomeClass
 {
 }

 class AnotherClass
 {
 }
+// AnotherClass.php
+class AnotherClass
+{
+}


The 1st cool tool we look at today is migrify/psr4-switcher. It doesn't need projects' autoloader so that it can be installed outside the project, e.g., in /var/www/tools, while your project is in /var/www/old_project.

Install it:

composer require migrify/psr4-switcher --dev

It would be great to have a list of all such multi-class files, right?

vendor/bin/psr4-switcher find-multi-classes /src

* SomeFile.php
    * SomeClass
    * AnotherClass

Now we know how big a problem we're dealing with.

Now 1 file has exactly 1 class/interface/trait.

Send pull request, make sure your project's autoloader autoloads them, and tests are passing. Merge it, and you're done.

2. Check Class Short name vs. Filename

 // Cucumber.php
-class Car
+class Cucumber
 {
 }

If we only knew how many such files are there and where... back to PSR4-Switcher:

vendor/bin/psr4-switcher check-file-class-name src

You will get a list of files that don't match. Use PHPStorm refactoring to change the class name everywhere:

Commit, PR, CI passes, merge.

3. Upper-case Directories First Letter

In PSR-4, any non-root directory must start with the first big letter. Root is e.g. /app, /src.

-/app/form/someForm
+/app/Form/SomeForm

Go through directory in the left panel in PHPStorm and rename the directories there:

Commit, PR, CI passes, merge.

4. Check PSR-4 root

We've done 3 steps so far. Now comes the biggest one, actually adding PSR-4 roots to composer.json.

It will not be as pretty as 1 root line, but that's not what we go here now. Our goal is to have all classes loaded with PSR-4, no matter how many lines in composer.json does it need.

{
    "autoload": {
        "psr-4": {
            "Amateri\\Payment\\": "src/somewhere-else/Payment",
            "Amateri\\Delivery\\": "src/another-dir/Delivery"
        }
    }
}

We can guess what namespace roots ("Amateri\\Payment\\") should be loaded from which directory ("src/somewhere-else/Payment")... or we can use science!

vendor/bin/psr4-switcher generate-psr4-paths project/src --composer-json project/composer.json

The command will generate such paths for us, based on existing namespaces and file locations. There may be over 10 or even 50 of those. Don't worry about it now.

If everything passes... Commit, PR, CI passes, merge.

5. Narrow the Namespace Root and Directories in composer.json

Now comes my favorite part. Here we move all directories to use as little namespace root as possible.

It might be a little bit unclear, but give it time and it will fit in. Let's look at the example:

 {
     "autoload": {
         "psr-4": {
-            "Amateri\\Payment\\": "src/somewhere-else/Payment",
-            "Amateri\\Delivery\\": "src/another-dir/Delivery",
+            "Amateri\\": "src"
        }
    }
}

What happens with files?

-src/somewhere-else/Payment
+src/Payment
-src/another-dir/Delivery
+src/Delivery

Here use PHPStorm refactoring on the directory as in step 3.

If everything passes... Commit, PR, CI passes, merge.

6. What if there are No Namespaces or Are Very Very Bad?

In many codebases, there are just random files—no namespace, no fake namespace, etc.

For these, we have help of Rector with these 2 rules:


Register them in rector.php:

<?php

// rector.php

declare(strict_types=1);

use Rector\PSR4\Rector\FileSystem\NormalizeNamespaceByPSR4ComposerAutoloadFileSystemRector;
use Rector\PSR4\Rector\Namespace_\NormalizeNamespaceByPSR4ComposerAutoloadRector;
use Symfony\Component\DependencyInjection\Loader\Configurator\ContainerConfigurator;

return function (ContainerConfigurator $containerConfigurator): void {
    $services = $containerConfigurator->services();
    $services->set(NormalizeNamespaceByPSR4ComposerAutoloadRector::class);
    $services->set(NormalizeNamespaceByPSR4ComposerAutoloadFileSystemRector::class);
};

And manually add the desired namespace to your composer.json:

 {
+   "autoload": {
+        "psr-4": {
+            "Amateri\\": "src"
+        }
+    }
 }

When you run the Rector, it will try to autocomplete all the namespaces to respect your composer.json:

vendor/bin/rector p src

This is one of the most significant changes in your application, so be sure to check it carefully. Not all cases are covered by Rector yet.

If everything passes... Commit, PR, CI passes, merge.


Then we added few manual tweaks here and there, and we were PSR-4 compliant with ~7 lines in PSR-4 in composer.json.


Have you found a case that is not covered or a better way to this? Let me know in the comments.


Happy coding!