06 December 2023
13+ PHP code refactoring tips for a meticulous developer
So you know something about code refactoring, huh? But can you create a viable and scalable refactoring process for your PHP application? Read this extensive tutorial and get the best insights from a developer who refactors for a living – from figuring out the system structure, through static code analysis, to updating your libraries, and more. The new December 2023 update even includes tips on how to use AI to improve your skillset!
* The article was updated on December 6th 2023 to reflect the latest developments in PHP code refactoring as well as the new experiences and thoughts of the author. Learn more about the importance of proper naming conventions, extracting and the use of AI-powered tools in code refactoring (will they take his job!? – read on and find out!).
In the course of my career as a PHP developer, I saw and worked on a lot of PHP apps in need of refactoring. They had all sorts of problems, but they all stemmed from a short-sighted approach to development. The first thing to do to refactor them is to change this approach.
But what to do next? After all, each app is different. While there is no one-size-fits-all solution, I’m going to provide you with the next best thing in the form of practical PHP refactoring tips based on my everyday experience.
If you have spent any meaningful time in the world of PHP development, you know that there are applications that almost scream: rewrite me!
Perhaps you’re here because you’ve got (a problem of) an app just like that. Or perhaps it was just curiosity? One way or the other, I’m going to share my experience in PHP code refactoring in the form of practical tips based on real-life projects I took part in.
What’s the problem with refactoring?
Why is it that sometimes it is so difficult for devs to figure out a viable way of refactoring their app?
The primary cause of refactoring problems is fragmentation or lack of proper project knowledge on the application’s business model and more.
When the project knowledge is poor while the app still needs to be maintained and developed further, its codebase can be expected to get more and more out of hand.
Of course, it’s much better when the knowledge is fragmented rather than nonexistent. In the former case, you can gather it and note it all down, using both words and software development diagrams. However, it is quite difficult to make sure that the knowledge fulfills all the requirements of present and future project stakeholders.
When the knowledge is simply not there, the situation gets even worse. Developers need to analyze the app using trial-and-error tactics. It’s messy, and it takes a lot of time.
Luckily, this extreme situation is rare in practice. Typically, we don’t start completely from scratch. After all, gathering business knowledge is a process that takes months or even years.
For optimal results, refactoring efforts should be performed concurrently with producing business knowledge. You can analyze each and every area of a module individually over an extended period of time while shaping a repeatable refactoring process for the whole app.
As you can see, there are hardly any shortcuts. You need to have a viable refactoring plan.
What refactoring techniques in PHP can become a part of such a process?
1. Use the one-step-at-a-time approach
Everyone knows this method, even if they have nothing to do with programming. You use it when you learn how to play the piano, or ride a bike. What does it mean in refactoring?
The gist of it is to try to improve the code by making small changes. It involves bits and pieces of all the other methods. There is no straightforward way of doing this.
If you spot poorly name variables – correct them! If you find out that the structure of a given class is incorrect – change it! Are there missing types somewhere? Add them!
The point is not to spend too much time on it. By making a couple of small positive changes at a time, you gradually improve the codebase. If you get your teammates to do the same thing, the result is even better. Over time, you can even refactor the whole app using the process.
2. Consider the importance of business
Refactoring and business have a troubled relationship.
It’s true that a refactored app clearly works better and has a far more promising future after being made efficient and scalable. On the other hand, refactoring often eats away a lot of programmer’s billable hours and it is not always easy to sell its benefits to non-technical people. After all, the changes made during refactoring have no bearing on how the app looks to the user and its external behavior. To a non-technical person, they are largely invisible.
From the perspective of developers themselves, business knowledge is also crucial. Once you know which modules of the system are most important from a business perspective, you can plan your refactoring process better.
Personally, I like to start with the smallest and least important modules. That way, you can get the hang of the system and the style of its code by working with the least essential parts of it. The probability of breaking the entire system and having to revert entirely to the old codebase shrinks.
3. Take care of your app structure
Learning all there is to know about your system’s code structure is essential. Regardless of how poorly made it is, you need to study it before you get serious about refactoring.
To show you just how important it is, let’s analyze an example app structure inspired by one of my many past projects.
The structure above has two basic parts:
- a proper app structure based on the Symfony framework,
- elements of a subpar app structure inherited from an obsolete part of the app.
If you want to refactor a structure like this, you need to focus on the obsolete part. Often, it includes key elements of the app. This particular obsolete structure contains the CRUD part of the system that has to do with basic data management. Unfortunately, it also contains important business logic, including calculations, and processing rules for form data.
If you work with projects such as this one, you’re bound to come across a situation in which a part of the logic is refactored, while another part is still provided by the old system. If you want to know exactly where every bit of functionality comes from, you need to learn the whole structure.
4. Correct any bug you spot on the fly
This one has to be the shortest piece of advice I will ever give on this subject.
One of the oldest rules of refactoring goes like this: if you change your code in any place of your system, make sure to improve at least the basic version of it.
Picture a function, method, or file you need to change. The code is plain ugly. It has both tabs and spaces, variable names are in multiple languages, naming is non-intuitive, and so on.
Correct at least some of the many problems you are able to find. That way, when another programmer works on that code, they will have fewer problems to deal with. If your team follows that rule, soon the quality of the code will improve significantly.
5. Update your PHP version
One of the first things I do as part of refactoring is update my PHP version. Not only will it speed up the app, but will also make it easier to spot outdated code. This is extremely important, as some of the outdated pieces of your software might lead to security issues that may be exploited. To put it another way, updating your PHP version improves:
- app speed,
- app stability,
- and the safety of your software and your entire business.
Not bad for something that takes a couple of clicks to complete, right?
When you use an IDE such as PHPStorm to do the update, you’re also going to get smart suggestions (including those about the null coalescing operator) without the extra third-party tools.
This simple example shows that making sure to update your PHP version has the potential to not only make your app better but also to change your very approach to writing PHP code.
There are some handy tools that can assist you in upgrading your PHP version. Rector is definitely worth checking out. It may help you move from PHP 5.3 all the way to PHP 8.1.
Once PHP is updated, errors removed, obsolete functions rewritten, and other problematic pieces of code simplified, you can move to another stage of refactoring that has to do with libraries.
6. Update your libraries
When you are in the early stages of PHP refactoring, inspecting all of the libraries in your project is a must.
If your system is large and old, chances are that some of your libraries are not only outdated but entirely abandoned by their authors.
Updating is quite straightforward. The composer tool detects outdated/abandoned ones. Such libraries often recommend how best to update or replace them. The difficult part is finding all the pieces of code affected by the libraries.
Thankfully, our favorite IDEs come again to the rescue. Modern tools make it possible to detect all specific instances of the old library being used in your code. Of course, you need to know enough about the library to recognize its main classes, namespaces, constants, or functions.
Tackling and improving these outdated elements will add a lot of vigor and quality to your code. The performance will be improved as well.
That’s for the pros. Unfortunately, going through the process is not always such a walk in the park even with the best of IDEs. The problem is that not every outdated library has a proper one-to-one replacement. Pieces of code affected by the library cannot be refactored. It is also plausible that a replacement does exist, but it doesn’t allow you to do everything the way it was done previously.
The latter situation makes refactoring possible but costly. A big part of the app needs to be rewritten. Refactoring of this kind poses some risks. The more obscure the original library is, the harder it is for your IDE or even for your own set of eyes to find all the pieces of code affected. On several occasions, I saw some code being triggered by a set of seemingly random signs. There is no way to find and understand such code if neither you nor any of your teammates worked on it originally.
7. Get rid of outdated solutions
This one is less straightforward than you might think at first glance. It’s hard to decide what’s new enough to stay and old enough to get rid of objectively.
One simple refactoring trick that can help is to focus on removing outdated database connections, e.g. those that use the MySQL library phased out in PHP 7.0.
Another good practice is to turn on the error display by switching the “display_errors = true” option in php.ini, the. htaccess file, or the ini_set function. You will also find depreciated solution reports from error_reporting(E_DEPRECATED) useful, so turn them on as well. Learn more about how PHP can help you with error reporting.
Even with that help, this will not be a straightforward job. Often, you will be left frustrated, moving through a mass of code, analyzing it piece by piece. Still, it’s worth doing it if you find:
- a class,
- a function,
- or a whole PHP file in need of a replacement for a newer one.
It’s much easier to correct a bug like this now than suffer the consequences of it in the future together with all of your teammates.
8. Ensure the separation of technologies
The project you inherit might be a weird mix of different technologies and styles within its internal structure. Its readability, extendability, and general developer experience working with it are going to be poor. The code quality may be affected by dead code or duplicate code. Removing dead code and other types of redundant code will be a challenge for you. Here is an example of mixed code in the index.php file
You are bound to come across code like this in some older projects. When you tackle it, the first and best thing you can do is try to separate one technology from another.
Before you get your hands dirty, you need to set up some rules for your code separation efforts. For example, I often move all the PHP code into controllers and dedicated services, while JavaScript, HTML, and CSS land in entirely separate files that use the .js, .html, and .css extensions.
The following example illustrates the solution for separation of technologies:
While the example illustrates the general idea well, you can definitely improve upon it. In a perfect world, the entire HTML section would be moved to a template file such as twig, while all the PHP code would land in the controller. It could end up looking like this:
Although the code here is quite simple, the separation clearly improves its readability and extendability. The more complex your starting code is, the more you will improve it by using this technique.
The key is to set up an acceptable minimal level of readability and continue the refactoring efforts until you reach it. When you do that, you will definitely get to a point where you have an easy-to-work-with system.
9. Maintain your code with static analysis tools
Static code analysis is the mainstay of your refactoring efforts. It holds your code together and makes it easier to eliminate basic errors and misunderstandings (e.g. in case of an indefinite type taken or returned by a function).
In the context of refactoring, a big contribution of static analysis is the way it can smooth out all the inconsistencies in your code. If it was created by many different developers over time, it may include different coding styles. When the code structure is decent, the static analysis tool can even find and correct all of the inconsistencies at the same time.
Even a single space, missing or needlessly added, can mess up the code and give you quite a headache. Static analysis may go a long way to amend such shortcomings.
And if someone you know needs to brush up on PHP coding standards, have them read the latest version of PHP Standards Recommendations (PSR).
Read more on static code analysis in PHP from an article from my colleagues!
10. Try out the Strangler Fig Pattern
There are some people out there who think of the Strangler Fig Pattern as a solution to all refactoring problems.
As you may already suspect, this isn’t quite the case. In software development, there are no one-size-fits-all solutions. Still, the Strangler Fig Pattern may be useful to your refactoring process.
Strangler Fig Pattern – definition
What is the Strangler Fig Pattern? It is all about building a new system around the old one (strangling the old one with the new one) until the latter is not needed anymore.
Strangler Fig Pattern – benefits
This approach has some major benefits.
The system always works
The new system in development shouldn’t collide with the existing one. As the new one matures, its individual modules can gradually replace the old system.
Monitoring is easy
As the tasks are being transferred to the new system, it’s easy to monitor and compare their performance. You can gradually eliminate all the problems should they arise.
In most cases, monitoring is more of a necessity than an optional assignment. It allows your team to learn more about bits and pieces of the old system that you don’t know about either due to incomplete business knowledge or the unusual nature of the legacy code.
How to use the Strangler Fig Pattern?
You should be fine as long as you stick to a couple of basic steps:
Define the interface of what you want to move
It’s an essential step because the interface includes directions on how the new code and modules are supposed to work.
Delegate tasks from the old system to the new one
If there is a piece of code in your new system that can replace a piece of code from the old system, make the switch as soon as possible.
Define a new single source of truth
A well-designed system has a single source of truth (SSOT). It should have all data required by your system’s modules. As you move to the new system, make sure not to compromise your SSOT.
Add all the new functionalities in the new system only
It should go without saying, but unfortunately, it doesn’t always happen. If you continue to expand your old codebase, you delay the moment when the new system takes over completely and risk having to write the same functionality twice.
Turn off the old code altogether when the time comes
Once all the functionalities are moved to the new system, turn off or remove the old code.
The Strangler Fig Patten is a modern solution to the problem of outdated code. It’s worth remembering that you don’t always have to move the entire codebase. Sometimes, you may choose to use the pattern for specific modules or functionalities.
There is no hard-and-fast rule for using the pattern. The best course of action needs to be decided individually for each and every project.
11. Extra tip – testing new code
As cliche as it sounds, testing is a continuous process. Each new testable functionality in your system should have:
- unit tests,
- functional tests,
- integration tests.
In addition to that, you should still test manually. That lets you find bugs that might be missed by the tools you use.
The key word here is “testable” – not every piece of code meets this requirement. Don’t fall into the trap of testing literally everything. It will take more time and it is worth it.
12. Naming
If you want to be thorough with your refactoring efforts, you should strive to improve the names of variables, methods, or functions wherever you can. Their names should be indicative of what they do.
Let’s take the following code that checks for the user type:
You can modify it in three different ways:
The first one is a traditional approach in which the name indicates the user type. In addition, the name of a method changed from type to isType to better reflect what is checked. The type method itself should not check for the user type, but simply return information about it.
The second one is much more interesting. This time, you’re asking specifically if the user is a moderator. The whole operation now takes place in the model itself or the user object.
The third version builds upon the first one. The difference is that you don’t use character strings openly, but only via static values assigned to the model or user object. Note that this version could also be applied to version two – the static values would be checked for inside of the model or user object.
To take the refactoring even further, you can abstract constants such as the aforementioned moderator to an enum. It should make the behavior of user types more orderly and predictable in the future.
Keep in mind that according to the Single Responsibility rule, the user type should not be dependent on the user object. A different object should be responsible for it instead.
13. Extracting
This refactoring technique is all about taking recurring elements from various methods or functions and putting them all in one place. It also involves moving elements that break the Single Responsibility rule to a place that is more descriptive of what a given object or function does.
Let’s look at an example. This code generates data files in two formats. I removed framework-related elements from it to make it easier to analyze:
As you can see, there are some recurring elements – serving an attached file or generating the file’s contents. Let’s take both out.
Here there are three methods instead of two to account for how data is generated for the CSV file. It’s just part of the cleanup. While I don’t recommend it, you could change the match to switch-case to handle data generation in one method.
Now that there are three methods, you can remove generateJsonFile and generateCsvFile and create the following simple method:
It’s worth it to point out that if text formatting was extracted into a different format and injected as a dependency, you would be left with only one method – the generator. In addition, this specific example doesn’t call for the use of the ob_* buffer’s functions.
Of course, there are as many ways to do it as there are programs in the world, but this method is quite practical. You put the disposition method into a different service that is solely responsible for serving files. When the code is divided between different methods and services reasonably, coding becomes much easier because you are always able to locate the source of the problem quickly. The bigger the app, the more of a difference it makes.
And that’s it for manual refactoring tips! Hopefully, you found something useful here to improve your own skill set.
Let’s do something different now! Let’s try some…
Bonus – refactoring with Artificial Intelligence!
Surprised? Nowadays, they use AI-powered technology for just about everything so why not code refactoring?
I’m not going to try to come up with any specific technologies beforehand. I’m simply going to ask ChatGPT version 4 what it can do for me. As for code, I’m going to use the file generator I created in the Extracting section above. I chose the version that does NOT include the csv function (fputcsv).
Prompt:
Hey, could you apply some refactoring methods to this code?
Prompt:
Hey, could you apply some refactoring methods to this code?
{HERE WAS THE CODE}
If what they say is true, in addition to refactoring the code, ChatGPT will share some advice on how to implement these optimizations.
The advice is here indeed. It’s quite general but still helpful. I’m pasting it here as is:
This PHP code can be refactored for better readability and maintainability, and to adhere to single responsibility principles. Here’s how you might refactor it:
- Single Responsibility Principle: Separate the concerns by creating classes that handle one specific thing. For instance, you could have one class for generating JSON, another for CSV, and another one for handling the output to the browser.
- Code Duplication: The headers and output buffering code are duplicated in both methods. This can be extracted into its method.
- String Literals: The code has a few syntax issues with the string literals, using curly quotes instead of straight quotes. This needs to be corrected.
- Magic Strings: The content type strings and file names are “magic strings” and could be extracted to class constants for easier management and changes.
- Complexity in generateCsvFile: The CSV generation logic is complex and could be simplified or broken down into smaller methods.
- Error Handling: There’s no error handling for the JSON_encode or file operations.
Here’s a refactored version of the code:
There’s also a high-level recap of what ChatGPT exactly did to the code:
- Created two separate classes JsonResponseGenerator and CsvResponseGenerator following the Single Responsibility Principle.
- Created a private outputContent method to remove duplication.
- Fixed string literals to use proper quotes.
- Added basic error handling for JSON encoding.
- Extracted magic strings to class constants.
- Broke down the CSV content generation into smaller methods for clarity.
Did ChatGPT do a good job? Overall, I would say that it did. It doesn’t mean that the refactoring couldn’t have been better. For example:
- it didn’t take the opportunity to use two classes in one generator to generate a file according to its type.
- It failed to use a common interface for these two classes.
On the bright side, I’m impressed that the AI included error handling. I didn’t do that in my examples on purpose and I’m very impressed that this AI does it by default.
Naturally, a test as simple as this one cannot reflect the complexity of business logic and the technological diversity of a commercial project.
Despite all of its power, AI still can’t understand the business context of an app. For better or worse, this is still something only us developers can do.
Refactoring PHP code – 3 key takeaways
As you can, there is a lot to consider when refactoring your existing code:
- The business reality of your software may determine just how much you can refactor and where to begin.
- The quality, structure, and age of your code will decide the approach and how much work you really have on hand. And as you know very well, those factors may vary a lot when it comes to the PHP programming language.
- The size of your team and the extent to which you prioritize scalability, as well as the continuous availability of your software, will make a big difference.
Even putting that aside, refactoring is still unpredictable. Once you start working with a large piece of legacy code, you might find out that your changes cause other unexpected changes. It may take a lot of time and PHP examples to figure out everything. Just how much? Not even your project manager dares to guess!
That’s why my final piece of advice is – to spend as much as you can afford on the initial analysis.
Design a custom refactoring process for your project that gets the most of the limited time and resources you have. This is the way to achieve sustainable code!
Are you looking for skilled PHP developers to refactor your system?
The Software House provides access to many skilled PHP devs that participated in large commercially successful projects just like those described in the article.