When discussing PHP, there’s no point in avoiding the elephant in the room; PHP used to be a really mediocre tool, and that’s probably an understatement. In fact, I saw an article recently that addressed the issue of why people don’t like PHP. However, in my personal experience, developers often confuse what they think PHP is with what it actually is.
The fact is that PHP got its reputation from somewhere. But the old, dark days of PHP v4 and earlier are way behind us. If you hadn’t run off screaming in horror, you’d have seen PHP evolve, and did it evolve.
In this article, I’ll cover what a modern PHP development environment looks like, reviewing the tools that are available both within the language itself and as complements. With the complexities involved in web development nowadays, you can’t judge the language in isolation. You need to be aware of all the satellite technologies that help you create high-quality software.
By the end of the article, you’ll hopefully have second thoughts about your certainty of how PHP sucks. However, if you’re a fan of PHP, you’ll have more reasons to defend your choice. Let’s get started!
Before we jump into the details of what makes PHP great, let’s first establish a definition for modern PHP. At the time of writing, PHP v8.1.0 has just seen the light of day, the PHP Foundation is about to become a reality, and PHP v5.6 is reaching its end-of-life. So, when I refer to modern PHP, I’m referring to v7 and later versions.
Since being rewritten in v5.0, the evolution of the language and its tools has been impressive. PHP v5.0 marked an inflection point in PHP’s history, bringing it to the realm of actual object-oriented languages.
Another discrete leap was the introduction of Composer, a PHP dependency manager, which certainly drew the line between amateur and professional development. But, I’m getting a little ahead of myself, we’ll cover this in-depth later. Let’s review some major improvements made to PHP within the last few versions.
Since PHP v7.0, which was released on December 3, 2015, several exciting new features have been introduced, like type declarations, built-in cryptography, support for complex data structures, named arguments, and attributes.
The syntax also experienced some powerful improvements, like arrow functions, the spaceship operator, and null coalescing. Each new release came packed with major performance improvements over the previous one.
Each of these new features might be pretty shocking to someone who left PHP three or four versions ago. To get the most of these great features, you’d probably have to be a heavy PHP user, however, for those of us who use PHP more casually, PHP has introduced additional new features tailored towards everyday use cases.
Now that we have a grasp on the features the latest PHP versions have introduced, let’s build our toolbox. In the following sections, I’ll discuss some tools I consider to be indispensable when it comes to professional software development in PHP. They are presented in incremental order, meaning I believe this will be the easiest path to adoption.
Before the introduction of debuggers like XDebug and ZendDebugger, developers were forced to spend excessive time understanding the root cause of an application’s misbehavior.
In practice, debugging involves looking at the contents of variables during a program’s execution.
In general, PHP is used in batch mode, meaning that the output is only visible once the script has run to completion, making it hard for developers to guess what the context was when the error occurred.
Additionally, the tools available for this task like var_dump
, echo
, and print_r
pose a high risk of leaving traces behind, potentially exposing sensitive information and lowering the bar for malicious attackers.
Both XDebug and ZendDebugger work well with modern IDEs like PhpStorm and VS Code to solve the issues mentioned above. If you prefer to go straight through the command line, phpdbg
comes bundled with PHP since v5.6.
Importing external libraries as dependencies used to be a real pain in PHP. However, one of the most prominent changes in PHP’s maturity came with the release of Composer. Before Composer, PHP used PEAR, which solved the same problem in a more primitive way.
For example, it’s complex to have individual project dependencies using PEAR. Dependency management with PEAR is an all-or-nothing situation, so running several projects on the same server is difficult, especially if each depends on a different or conflicting set of dependencies.
On the other hand, dependency management is much simpler with Composer. Every project has its own composer.json
and vendor
folders, keeping everything self-contained.
Another great advantage of Composer is its versioning system, which has built-in intelligence to determine the best fit for a dependency tree; think of dependencies that have dependencies of their own. PEAR, on the other hand, does a very poor job in this area.
Nowadays, PHP best practice requires familiarity with Composer. Most of the tools we’ll cover require its availability in your working environment.
If you’re building any non-trivial application, chances are you’ll have to create a lot of boilerplate code before you can actually solve your client’s problem. Think of issues like authentication, routing, and database management.
In the older days of PHP, these were a real challenge. Nowadays, there are many MVC frameworks available, most notably Symfony and Laravel, which you can use as a basis for your task. Symfony and Laravel both boast of large community support and widespread usage.
Automated testing tools have become a standard throughout the software development industry. Every language has its own tools, and the biggest player for PHP is definitely phpUnit.
phpUnit was originally designed as a unit testing framework, but other tools have helped expand it to provide other types of testing like end-to-end and integration testing.
Using phpUnit is pretty simple. Say you have a class like the following:
<?php namespace LeewayAcademy; class Calculator { public function add(int $a, int $b): int { return $a + $b; } }
Reading the code, you probably assume it will work. But with phpUnit, you can define a set of repeatable tests that will help you build and justify your confidence level. For example, a test case will look like the following:
<?php use PHPUnit\Framework\TestCase; use LeewayAcademy\Calculator; class CalculatorTest extends TestCase { public function testAdd() { $sut = new Calculator(); $this->assertEquals(3, $sut->add(1, 2)); $this->assertEquals(10, $sut->add(5, 5)); $this->assertEquals(10, $sut->add(0, $sut->add(4, 6))); } }
The code above runs the add
method with different sets of inputs, then validates that the output matches what was expected. You can run tests with phpUnit using the following command:
php vendor/phpunit/phpunit/phpunit --no-configuration --filter CalculatorTest --test
The code above will produce an output like the following:
Testing started at 10:07 ... PHPUnit 9.5.11 by Sebastian Bergmann and contributors. Time: 00:00.006, Memory: 4.00 MB OK (1 test, 3 assertions)
You can run this kind of test as many times as you want. If all of them pass, you’ll have some actual proof that your application is doing what it’s supposed to do. Of course, these tools are only as good as the tests you write, but that’s another discussion altogether.
Other tools worth mentioning include Codeception and behat. Both use phpUnit underneath, but have different approaches to writing tests.
A lack of static analysis used to be a big drawback for PHP and other non-compiled languages. Some bugs were so well-hidden in obscure execution paths, it was very hard to find them under normal testing situations. We now have phpstan, Psalm, and Exakat, just to name a few. For example, consider the following bug:
<?php function f(int $p): int { return $p * 2; } $a = 'M'; echo f($a);
With static analysis tools, a type
mismatch bug like the one above can be detected without running the code, simply by issuing a command like the one below:
vendor/bin/phpstan analyse test.php --level 5
The code above will produce the following output:
1/1 [â–“â–“â–“â–“â–“â–“â–“â–“â–“â–“â–“â–“â–“â–“â–“â–“â–“â–“â–“â–“â–“â–“â–“â–“â–“â–“â–“â–“] 100% ------ ---------------------------------------------------------- Line test.php ------ ---------------------------------------------------------- 10 Parameter #1 $p of function f expects int, string given. ------ ---------------------------------------------------------- [ERROR] Found 1 error
Now, you have very precise information on errors that might have otherwise been overlooked. By including these tools in a continuous integration pipeline or running them as part of Git Hooks, you can more easily improve the quality of your codebase.
A developer’s job doesn’t end once they’ve written their last line of code. To reach an audience, your application must first reach a production server.
With older versions of PHP, deploying your application required pushing the new files to a remote location. However, nowadays, it’s a bit more complicated. You’ll probably have to handle database updates, directory permissions, and an abundance of other small tasks to get everything up and running. Oftentimes, missing one of these actions or running them in a different order makes the whole deployment fail.
Just like automated testing tools, the PHP ecosystem provides fantastic tools for taking your application to production and keeping it updated as needed, preventing tremendous headaches. Some of these include Deployer, Rocketeer, Pomander, and easydeploy. As an example, here’s a configuration for Deployer I used for a client’s project:
<?php namespace Deployer; require 'recipe/codeigniter.php'; // Project name set('application', 'APP'); // Project repository set('repository', '[email protected]:maurochojrin/REPO.git'); set('branch', 'master'); set('default_stage', 'prod'); // [Optional] Allocate tty for git clone. Default value is false. set('git_tty', true); // Shared files/dirs between deploys add('shared_files', [ 'application/config/database.php', 'app_env.php', ]); add('shared_dirs', [ 'application/sessions', 'application/logs', 'assets/uploads/excel', 'logs', ]); // Writable dirs by web server add('writable_dirs', [ 'application/sessions', 'assets/uploads', 'application/logs', ]); // Hosts host('THE_HOST') ->stage('prod') ->identityFile('~/.ssh/MauroChojrin.pem') ->set('user', 'ubuntu') ->set('deploy_path', '~/{{application}}'); // Tasks task('build', function () { run('cd {{release_path}} && build'); }); task('pwd', function () { $result = run('pwd'); writeln("Current dir: $result"); }); // [Optional] if deploy fails automatically unlock. after('deploy:failed', 'deploy:unlock');
With this configuration in place, whenever I push a new version to production, I just have to run the command below:
dep deploy
The script will remotely run every task required to make the app available for users. If you’re still pushing files over FTP, you probably want to check these tools out.
Another common complaint when it comes to PHP is its lack of asynchronous execution support. There are a couple of projects aimed in that direction like Swoole and ReactPHP. Take a look at the following code pulled from the Swoole By Examples repository:
#!/usr/bin/env php <?php declare(strict_types=1); /** * How to run this script: * docker exec -t $(docker ps -qf "name=client") bash -c "time ./io/blocking-io.php" * * This script takes about 3 seconds to finish, and prints out "12". * * Here the PHP function sleep() is used to simulate blocking I/O. The non-blocking version takes about 2 seconds to * finish, as you can see in script "non-blocking-io.php". */ (function () { sleep(2); echo '1'; })(); (function () { sleep(1); echo '2'; })();
Compare it to its non-blocking counterpart:
#!/usr/bin/env php <?php declare(strict_types=1); /** * How to run this script: * docker exec -t $(docker ps -qf "name=client") bash -c "time ./io/non-blocking-io.php" * * This script takes about 2 seconds to finish, and prints out "21". * * Here the Swoole function co:sleep() is used to simulate non-blocking I/O. If we update the code to make it work in * blocking mode, it takes about 3 seconds to finish, as you can see in script "blocking-io.php". * * To see how the code is executed in order, please check script "non-blocking-io-debug.php". */ go(function () { co::sleep(2); echo '1'; }); go(function () { co::sleep(1); echo '2'; });
Syntactically, they look pretty similar, but underneath, the second version is leveraging Swoole’s power for parallel processing, decreasing the time required to achieve the end result.
In PHP v8.1, Fibers were introduced as an out-of-the-box feature, so if async is your goal, there’s nothing stopping you from achieving it without leaving PHP.
PHP has come a long way. Sadly, not every PHP developer has followed along with these best practices, so you can still find a lot of spaghetti code out there. However, this reflects more of an individual’s responsibility rather than a tool’s shortcomings.
On the bright side, there are many excellent resources to level up with PHP if you want to. I hope you enjoyed this article. If you weren’t already, I hope you’re now a fan of PHP, or at least willing to give it a shot. Let’s turn PHP’s reputation around.
Install LogRocket via npm or script tag. LogRocket.init()
must be called client-side, not
server-side
$ npm i --save logrocket // Code: import LogRocket from 'logrocket'; LogRocket.init('app/id');
// Add to your HTML: <script src="https://cdn.lr-ingest.com/LogRocket.min.js"></script> <script>window.LogRocket && window.LogRocket.init('app/id');</script>
Hey there, want to help make our blog better?
Join LogRocket’s Content Advisory Board. You’ll help inform the type of content we create and get access to exclusive meetups, social accreditation, and swag.
Sign up nowLearn how to manage memory leaks in Rust, avoid unsafe behavior, and use tools like weak references to ensure efficient programs.
Bypass anti-bot measures in Node.js with curl-impersonate. Learn how it mimics browsers to overcome bot detection for web scraping.
Handle frontend data discrepancies with eventual consistency using WebSockets, Docker Compose, and practical code examples.
Efficient initializing is crucial to smooth-running websites. One way to optimize that process is through lazy initialization in Rust 1.80.
2 Replies to "Modern tools for PHP developers"
2015 wants it’s article back.. “PHP v5.6 is reaching its end-of-life” reaching it’s end of life? 5.6 is so buried that the graves of 7.0, 7.1, 7.2 and 7.3 are buried on top of it. All these tools have been in production for years now… you might as well write an article about javascript where you have this cool thing called npm and you can use it on the backend as well!
A dark theme would be nice