Buy Access to Course
04.

Post-Rector Cleanups & Tweaks

|

Share this awesome video!

|

Rector just automated several changes to our app that are needed to remove deprecations on Symfony 5.4. Plus it did some bonus refactoring, like adding the optional Response return type on our controllers.

But as nice as that is, it's not perfect. All the class names are inlined, instead of having a use statement. And even though it renamed some interfaces, it didn't rename the methods that we call on those objects to reflect the change. No need to worry, though. Rector gave us a great start and helped highlight several changes that we need to make. Now, let's finish the job.

Installing php-cs-fixer

First, for these long class names with no use statement and, in general for coding styles, Rector doesn't know what coding style we prefer, so it doesn't even try to format things correctly. The official recommendation is to use a tool in your project like PHP CS Fixer to reformat the code after running Rector. PHP CS Fixer is a great tool anyway... so let's get it installed so it can help us along our journey.

You can install PHP CS Fixer a few different ways, but oddly enough, the recommended way - and the way that I like - is to install it via Composer into its own directory. Run:

mkdir -p tools/php-cs-fixer

There's nothing special here: just a new tools/ directory with php-cs-fixer/ inside. Now install it into that directory by running composer require --working-dir=tools/php-cs-fixer - that tells Composer to behave like I'm running it from inside of tools/php-cs-fixer - and then friendsofphp/php-cs-fixer.

If you're wondering why we're not just installing this directly into our main composer.json dependencies, well... that's a bit tricky. PHP CS Fixer is a standalone executable tool. If I install it into our app's dependencies, then it could cause problems if some of its dependencies don't match versions that we have already in our app. Well, really, this is a potential problem whenever you install any library. But since all we need is a standalone binary... there's no reason to mix it into our app. We could have done the same thing with Rector.

This gives us, in that directory, composer.json and composer.lock files. And in its vendor/bin directory... yes: php-cs-fixer. That's the executable.

And because we have a new vendor/ directory, open up the root .gitignore file and, at the bottom, ignore that: /tools/php-cs-fixer/vendor. And while we're here, let's also ignore /.php-cs-fixer.cache. That's a cache file that PHP CS Fixer will create when it does its work.

26 lines | .gitignore
// ... lines 1 - 23
/tools/php-cs-fixer/vendor/
/.php-cs-fixer.cache

Adding php-cs-fixer Config

The last thing we need to do is add a config file. Up here, create a new file called .php-cs-fixer.php. Inside, I'm going to paste about 10 lines of code. This is pretty simple. It tells PHP CS Fixer where to find our src/ files... then, below, which rules to apply. I'm using a pretty standard Symfony set of rules.

14 lines | .php-cs-fixer.php
// ... lines 1 - 2
$finder = PhpCsFixer\Finder::create()
->in(__DIR__.'/src')
;
$config = new PhpCsFixer\Config();
return $config->setRules([
'@Symfony' => true,
'yoda_style' => false,
])
->setFinder($finder)
;

And... we're ready to run this! To see what it does, over at the command line, add all the changes to git with:

git add .

Then check on them:

git status

But don't commit them yet. I still want to be able to review the changes that Rector made before we finally commit. But at least, now, we'll be able to see what PHP CS Fixer does.

Let's run it:

./tools/php-cs-fixer/vendor/bin/php-cs-fixer fix

And... nice! It modified 6 files. Let's check them out!

git diff

Awesome! It removed the long class names for Response across our entire codebase! It also deleted a few old use statements that we don't need. So the code from Rector still isn't perfect, but that was a nice step towards making it better!

Fixing the Password Hasher Code

For the final fixes, we'll do them manually by digging into the changes that Rector made, one by one. I'll help out by zooming us into the places that need updates.

The first is RegistrationController: src/Controller/RegistrationController.php. This is one of the places where it changed UserPasswordEncoderInterface to UserPasswordHasherInterface. Notice that PHP CS Fixer did fix a lot of the long, inlined class names... but not all of them. It depends on if there was already a use statement for that class or not.

So let's fix this by hand. Hover over the class, hit "alt" + "enter" and then go to "Simplify FQN". That shortens it and adds the use statement on top.

101 lines | src/Controller/RegistrationController.php
// ... lines 1 - 11
use Symfony\Component\PasswordHasher\Hasher\UserPasswordHasherInterface;
// ... lines 13 - 16
class RegistrationController extends AbstractController
{
// ... lines 19 - 21
public function register(Request $request, UserPasswordHasherInterface $userPasswordHasher, VerifyEmailHelperInterface $verifyEmailHelper): Response
// ... lines 23 - 99
}

But there's another problem. If we trace down to where this is used, previously we were calling ->encodePassword(). But... that method doesn't exist on the new interface! We need to call ->hashPassword().

I'm also going to rename the argument. Go to "Refactor" then "Rename" and call it $userPasswordHasher... just because that's a more fitting name.

101 lines | src/Controller/RegistrationController.php
// ... lines 1 - 21
public function register(Request $request, UserPasswordHasherInterface $userPasswordHasher, VerifyEmailHelperInterface $verifyEmailHelper): Response
{
// ... lines 24 - 27
if ($form->isSubmitted() && $form->isValid()) {
// ... line 29
$user->setPassword(
$userPasswordHasher->hashPassword(
// ... lines 32 - 34
);
// ... lines 36 - 55
}
// ... lines 57 - 60
}
// ... lines 62 - 101

Next up is src/Factory/UserFactory.php for the same change. Scroll down and... once again, we have a long class name. Hit "alt" + "enter" and go to "Simplify FQN" to add that use statement. Then... let's "Refactor" and "Rename" the argument to $passwordHasher... good... and "Refactor", "Rename" the property also to $passwordHasher.

70 lines | src/Factory/UserFactory.php
// ... lines 1 - 6
use Symfony\Component\PasswordHasher\Hasher\UserPasswordHasherInterface;
// ... lines 8 - 29
final class UserFactory extends ModelFactory
{
private UserPasswordHasherInterface $userPasswordHasher;
public function __construct(UserPasswordHasherInterface $userPasswordHasher)
{
// ... lines 36 - 37
$this->userPasswordHasher = $userPasswordHasher;
}
// ... lines 40 - 68
}

Finally, below, we need to call ->hashPassword() instead of ->encodePassword().

70 lines | src/Factory/UserFactory.php
// ... lines 1 - 50
protected function initialize(): self
{
// ... line 53
return $this
->afterInstantiate(function (User $user) {
if ($user->getPlainPassword()) {
$user->setPassword(
$this->userPasswordHasher->hashPassword($user, $user->getPlainPassword())
);
}
})
;
}
// ... lines 64 - 70

Done!

Just one more spot where we need this same change: src/Security/LoginFormAuthenticator.php. We're going to refactor this class later to use the new security system... but let's at least get it working. Find the UserPasswordHasherInterface argument, shorten that with "Simplify FQN"... then rename the argument to $passwordHasher... and rename the property to $passwordHasher.

121 lines | src/Security/LoginFormAuthenticator.php
// ... lines 1 - 8
use Symfony\Component\PasswordHasher\Hasher\UserPasswordHasherInterface;
// ... lines 10 - 24
class LoginFormAuthenticator extends AbstractFormLoginAuthenticator implements PasswordAuthenticatedInterface
{
// ... lines 27 - 34
private $userPasswordHasher;
public function __construct(SessionInterface $session, EntityManagerInterface $entityManager, UrlGeneratorInterface $urlGenerator, CsrfTokenManagerInterface $csrfTokenManager, UserPasswordHasherInterface $userPasswordHasher)
{
// ... lines 39 - 42
$this->userPasswordHasher = $userPasswordHasher;
}
// ... lines 45 - 119
}

Then we check to see where this is used... I'll search for "hasher"... there we go! Down on line 84, the ->isPasswordValid() actually does exist on the new interface, so this is one case where we don't need to change anything else.

121 lines | src/Security/LoginFormAuthenticator.php
// ... lines 1 - 14
use Symfony\Component\Security\Core\Exception\UserNotFoundException;
// ... lines 16 - 24
class LoginFormAuthenticator extends AbstractFormLoginAuthenticator implements PasswordAuthenticatedInterface
{
// ... lines 27 - 66
public function getUser($credentials, UserProviderInterface $userProvider)
{
// ... lines 69 - 75
if (!$user) {
throw new UserNotFoundException('Email could not be found.');
}
// ... lines 79 - 80
}
// ... lines 82 - 119
}

Oh, but while we're in here, the UserNotFoundException is another long class name. Hit "Simplify FQN" again.

Beautiful! That should be everything.

The big question now is: does our app work? If we go back to the Homepage... it doesn't. We're back on the Welcome to Symfony page? That's weird...

Spin back over to your terminal and run:

php bin/console debug:router

Wow. In fact, all of our routes are gone. This is due to one other change that Rector made that we need to pay close attention to. It's inside of our Kernel class. We're going to talk more about this class later when we upgrade our recipes. Rector changed the argument to RoutingConfigurator, but it didn't update the code below.

54 lines | src/Kernel.php
// ... lines 1 - 10
class Kernel extends BaseKernel
{
// ... lines 13 - 44
protected function configureRoutes(\Symfony\Component\Routing\Loader\Configurator\RoutingConfigurator $routes): void
{
$confDir = $this->getProjectDir().'/config';
$routes->import($confDir.'/{routes}/'.$this->environment.'/*'.self::CONFIG_EXTS, '/', 'glob');
$routes->import($confDir.'/{routes}/*'.self::CONFIG_EXTS, '/', 'glob');
$routes->import($confDir.'/{routes}'.self::CONFIG_EXTS, '/', 'glob');
}
}

So again, Rector is really good for finding some of these changes, but you should always double-check the final result.

Fortunately, the entire configureRoutes() method has been moved into this MicroKernelTrait - a fact I'll talk about more soon. So we don't need this method in our class at all anymore. As soon as we delete it, the correct version from the trait is used... our routes are back.... and the page works! Woohoo!

And hopefully we have a few less deprecations than before. I now see 58. Progress!

So what's next? We've upgraded our dependencies and automated some of the changes we need with Rector. Well, there's still one more thing we can do before we start going through each deprecation manually: updating our recipes. And this has gotten a whole heck of a lot easier than the last time you upgraded. Let's find out how next.