The set up

When developing software, you sometimes come up with ideas that in the end don’t really work out. Depending on the idea and on the time spent finding out it’s a bad idea, it can be challenging to turn back the changes.

One of those ideas is splitting up your code into multiple different codebases, so you can deploy them separately. If done well and for the right reasons, this can be a sensible decision. But if executed poorly or for the wrong reasons, you may end up in a development hell.

Imagine a web app. Somewhere in the past, it was decided to split it up into 4 different Laravel codebases. First of all, there is a user facing part. It provides the user interface, contains some business logic, and includes some code that connects to an API.

This API is the second part. It has no user interface (UI) and can not be reached by the users directly. It contains most of the business logic.

There is also an admin application. It has a separate UI, which is not as pretty, and contains some classic administrative functionality. This functionality overlaps partly with the business logic in the API and front-end facing project.

Last, but not least, we have a common codebase of shared resources. It’s not a Laravel application, but it contains Models, ValueObjects, and other classes that can be used by the other 3 projects.

After some time, we found out that this approach doesn’t work very well. Most features we developed required changing code in at least 3 of these codebases and sometimes in all of them. Working with git became very cumbersome and deploying the new feature required multiple pipelines running next to each other. Frustration was growing rapidly.

To keep growing, we needed to fix this problem. We needed to merge everything into one Laravel application instead of 3 plus a common library. This would improve the developer experience drastically, but also allow us to start working on a new structure, based on the principles of hexagonal architecture and domain driven design.

Problem

But, there is always a but. There is one big problem we need to fix before we can go ahead and move our code into the same application. Each application has the same namespace.

Following traditional Laravel defaults, each code base lives inside the App namespace. This means that when merging all code together, there will be conflicting classes. There will be classes that have the same fully qualified name, but are in a different location, and which have different logic.

Solution

The solution is simple. Each sub project will receive its own namespace. The front-end facing subproject will get Web, API will get Api, etc. The execution would prove a bit more challenging than coming up with the idea, however.

After some research, Rector seemed to be the right tool for the job. It’s a tool that automates certain refactorings for you. It does this by defining so-called “rectors”, or rules for refactoring. There are rectors for making your code compatible with certain PHP versions, rectors for removing dead code, rectors for switching docblock type hints to actual type annotations and many more. And much to my joy at that point, there is also a rector for renaming a namespace.

Next problem

But life is never that easy. Remember that each subproject uses code from the common library. This would turn out to be a problem when using this specific rector. Take the following example code:

<?php

namespace App\Example; // inside web project

use App\Models\Api\Beneficiary; //Class from 'web' project
use App\Models\Database\Parking\Parking; //Class from 'core' project

class Example
{
	public function method(Beneficiary $beneficiary, Parking $parking)
	{
		// Imagine code here
	}

}

This class is defined in the Web project and uses another class from the Web project and one from the common library. This is a very common sight in the code. After the ideal refactor, this class should look like this. Notice how the namespace and use statements are updated.

<?php

namespace Web\Example;

use Web\Models\Api\Beneficiary;
use Core\Models\Database\Parking\Parking;

class Example
{
	public function method(Beneficiary $beneficiary, Parking $parking)
	{
		// Imagine code here
	}

}

However, the code didn’t look as expected after running Rector. Notice how every namespace was changed to Web, instead of only the relevant ones.

<?php

namespace Web\Example;

use Web\Models\Api\Beneficiary;
use Core\Models\Database\Parking\Parking;

class Example
{
	public function method(Beneficiary $beneficiary, Parking $parking)
	{
		// Imagine code here
	}

}

Every problem has a solution

This happened because of the way Rector works. Rector uses nikic/php-parser in the background. It will parse every class in your codebase into an abstract syntax tree and iterate over each node in that tree. If the type of node is relevant for the rector, it will be passed to the rector which will then do its job.

For our use case, the relevant node’s are Namespace, Use, and Name. Namespace and Use speak for themselves; Name is every occurence of a classname (fully qualified name (FQN) or not) in the code, for example, when type hinting.

When it encounters one of these nodes, it will basically do a str_replace on the classname. The rector isn’t very smart or at least not smart in the way we need. It does not detect that a class does not originate in the same project.

Luckily Rector allows you to write your own rectors. So off we go into the interesting realm of writing rectors for automatic refactors. Below is the code I ended up with. It’s based on the original RenameNamespaceRector, but now with added “smartness.” To keep the text somewhat structured, I first give the outline of the class and go deeper on each method afterwards.

<?php

declare(strict_types=1);

namespace Madewithlove\Rector;

final class RenameNamespaceRector extends AbstractRector implements ConfigurableRectorInterface
{
	/**
	 * @return array<class-string<Node>>
	 */
	public function getNodeTypes(): array
	{
	}

	/**
	 * @param Name|Namespace_|Use_ $node
	 */
	public function refactor(Node $node): ?Node
	{
}

	private function isCommon(string $name, string $newName): bool
	{
	}
}

The getNodeTypes method will return an array with all the node types the rector is interested in. Rector uses this method to know which nodes are relevant for your rector and will only pass in nodes with a type defined in this method. That way we don’t try to refactor every single code snippet in each class.

/**
	 * @return array<class-string<Node>>
	 */
	public function getNodeTypes(): array
	{
		return [Namespace_::class, Use_::class, Name::class];
	}

The refactor method is the entry method for starting a refactor and it receives the node that could be refactored. I started with the code from the original rector and added smartness to fit our use case. It will either return the new node (with updated namespace) or return null (indicating nothing should change).

When handling a Namespace node, we will always rename the namespace if it fits the pattern we configured (e.g. App). This works because we only refactor the classes inside the Web project and are not iterating over the classes in the common project.

This is something we will do after the Web, Admin and API projects have been refactored. Every namespace statement we encounter here will automatically point to a namespace inside the Web project, so it’s safe to change.

When handling a Use or Name node, it’s possible we are working with a class from the common library. We check for that using the isCommon method and if that is the case, we return null. Otherwise, we will do the renaming if relevant.

/**
* @param Name|Namespace_|Use_ $node
*/public function refactor(Node $node): ?Node
{
	$name = $this->getName($node);
	if ($name === null) {
		return null;
	}
	$renamedNamespaceValueObject = $this->namespaceMatcher->matchRenamedNamespace($name, $this->oldToNewNamespaces);

	if (!$renamedNamespaceValueObject instanceof RenamedNamespace){
		return null;
	}
	if ($this->isClassFullyQualifiedName($node)){
		return null;
	}

	if ($node instanceof Namespace_){
		$newName = $renamedNamespaceValueObject->getNameInNewNamespace();
		$node->name = new Name($newName);

		return $node;
	}

	$newName = ($node instanceof Name && $this->isPartialNamespace($node)) ? $this->resolvePartialNewName($node, $renamedNamespaceValueObject) : $renamedNamespaceValueObject->getNameInNewNamespace();
	if ($this->isCommon($name, $newName)){
		return null;
	}
	if ($node instanceof Use_){
		$newName = $renamedNamespaceValueObject->getNameInNewNamespace();
		$node->uses[0]->name = new \PhpParser\Node\Name($newName);

		return $node;
	}
	$parent = $node->getAttribute(AttributeKey::PARENT_NODE);
	// already resolved above
	if ($parent instanceof Namespace_){
		return null;
	}
	if ($parent instanceof UseUse && $parent->type === Use_::TYPE_UNKNOWN){
		return null;
	}
	$newName = $this->isPartialNamespace($node) ? $this->resolvePartialNewName($node, $renamedNamespaceValueObject) : $renamedNamespaceValueObject->getNameInNewNamespace();

	return new FullyQualified($newName);
}

The isCommon method will check whether a class belongs to the ‘core’ project or not. It takes the old and new name as arguments. It uses reflection to find out the full filename of the class, and when that contains ‘/common/’, we know the class comes from the common library. If it can’t find a class with the old name, it will look for a class with the new name, because it’s possible the class has already been renamed.

private function isCommon(string $name, string $newName): bool
{
	try
	{
		$reflection = new \ReflectionClass($name);
	}
	catch (\Exception $e)
	{
		try
		{
			$reflection = new \ReflectionClass($newName);
		}
		catch (\Exception $e)
		{
			// this should not really happen, but ok...
			return true;
		}
	}

	return str_contains($reflection->getFileName(), '/common/');
}

We run the rector and everything looks as it should be. Nice!

Rector without templates and linters

One thing to keep in mind is that Rector will only parse PHP files, so Blade templates with class name references can not be automatically refactored. The same goes for class or namespace names in strings. Rector will not detect those. I’m pretty sure that making an automated solution for this is possible, but in our case, a good old fashioned search and replace did the trick.

Another small gotcha is that after the refactor, you will have to run your linter again, since it won’t automatically import your classes. It will by default replace classnames with FQNs.

Go forth and refactor (automatically)

I knew Rector existed for some time now, but this is the first time I played with it. I’m very happy with how it turned out and I’m also looking forward to seeing what else it can automate for me. I think this is a tool I will definitely keep in my belt.