DEV Community

OmegaVesko
OmegaVesko

Posted on • Originally published at bulletproofphp.dev

Yes, PHP is Worth Learning/Using in $CURRENT_YEAR

Welcome, dear reader! If you're reading this, you're probably one of the many people who find themselves wondering how much of what they've heard about PHP (a lot of which isn't super positive, I'm sure) is still relevant today. Is PHP a dying language? Should you learn PHP in $CURRENT_YEAR, and/or use it to build your next app? Hopefully, by the end of this post, you'll have the answers to these questions and more.

Rather than yet another generic overview of the language or a point-by-point refutation of the things people say is wrong with it, what I want this post to be more than anything else is kind of a comprehensive list of ✨good things about PHP✨ (or, well, at least things that I think are good).

How We Got Here

If you have any preconceptions about PHP at all, they're probably largely shaped by what the discourse around it was during the PHP 4 and PHP 5 days. This was an era where PHP was increasingly seen as a "legacy" platform in contrast to cool new projects like Ruby on Rails and Node.js, and conventional wisdom was that PHP was simply a "bad" language, or at least a language a lot of people were writing bad code in.

You might also have heard that PHP 7 was a big step forward for PHP, though, and this is true. While the story of how PHP got to where it is today is largely one of many incremental improvements, a lot of people would probably agree that the release of PHP 7 in 2015 was the start of the "modern" PHP era. PHP 7 included, among other things:

  • dramatic performance improvements
  • a significantly expanded type system (scalar types and return types)
  • anonymous classes
  • the null coalescing operator (??)
  • the spaceship operator (<=>)
  • unicode codepoint escape syntax (echo "\u{2764}"; → ❤️)
  • a built-in CSPRNG API

What PHP Code Looks Like Today

Over the last several years, PHP has become a significantly more ergonomic language, as examples like this post illustrate very well. Let's do a rundown of some of the most significant features PHP has gained over the years, some of which you may or may not recognize from other languages (and some you might even wish your favorite language had!).

The null coalescing operator

// equivalent to:
// $username = isset($_GET['user']) ? $_GET['user'] : 'anonymous';

$username = $_GET['user'] ?? 'anonymous';
Enter fullscreen mode Exit fullscreen mode

The nullsafe operator

// equivalent to:
// $dateTime = $event->getDateTime();
// $timestamp = $dateTime ? $dateTime->getTimestamp() : null;

$timestamp = $event->getDateTime()?->getTimestamp();
Enter fullscreen mode Exit fullscreen mode

Constructor property promotion

class WidgetManager
{
    public function __construct(
        public LoggerInterface $logger
    ) {}
}
Enter fullscreen mode Exit fullscreen mode

This code is equivalent to:

class WidgetManager
{
    public LoggerInterface $logger;

    public function __construct(
        LoggerInterface $logger
    ) {
        $this->logger = $logger;
    }
}
Enter fullscreen mode Exit fullscreen mode

The match expression

$error = match ($code) {
    0 = null,
    1 => new SomeError(),
    2, 3, 4 => new OtherError(),
    default => new UnknownError(),
}
Enter fullscreen mode Exit fullscreen mode

Named arguments

function testFunction(
    string $first,
    string $second,
    ?string $third = null,
    ?string $fourth = null
) { /* … */ }

testFunction(
    second: 'second value',
    first: 'first value',
    fourth: 'fourth value',
);
Enter fullscreen mode Exit fullscreen mode

Arrow functions

$collection = new ArrayCollection([1, 2, 3]);
$incremented = $collection->map(fn (int $i) => $i + 1);
// $incremented is [2, 3, 4]
Enter fullscreen mode Exit fullscreen mode

Attributes

#[Route('/greetings')]
class GreetingController
{
    #[Route('/hello')]
    public function hello(): string
    {
        return 'hello';
    }
}
Enter fullscreen mode Exit fullscreen mode

The spaceship operator

The spaceship operator is a little esoteric (and possibly a little controversial, depending on how confident you are that people reading your code will know what it does without looking it up), but the one thing it's very useful for is writing clear and succinct comparison/sorting functions.

echo 1 <=> 1; // 0
echo 1 <=> 2; // -1
echo 2 <=> 1; // 1

echo "a" <=> "a"; // 0
echo "a" <=> "b"; // -1
echo "b" <=> "a"; // 1
Enter fullscreen mode Exit fullscreen mode

The spread operator

$first = ['a' => 1, 'b' => 2];
$second = ['c' => 3, 'd' => 4];

$merged = [...$first, ...$second];

// $merged is ['a' => 1, 'b' => 2, 'c' => 3, 'd' => 4]
Enter fullscreen mode Exit fullscreen mode

The numeric literal separator

$withoutSeparators = 1000000000;
$withSeparators = 1_000_000_000;

echo $withoutSeparators === $withSeparators; // true
Enter fullscreen mode Exit fullscreen mode

Array destructuring

$list = [1, 2]
[$first, $second] = $list

// $first is 1, $second is 2

$array = [
    'a' => 1,
    'b' => 2
];

['a' => $a, 'b' => $b] = $array;

// $a is 1, $b is 2
Enter fullscreen mode Exit fullscreen mode

PHP's Type System

While it's by no means the strictest type system out there (not to mention entirely optional, much like TypeScript), modern PHP has a robust type system that includes features like interfaces, scalar and object types, nullable types, union and intersection types, and more.

Code speaks louder than words, so here's an example of what code that takes full advantage of the type system in PHP 8.1 can look like:

<?php

declare(strict_types=1);

class MyClass
{
    public \DateTimeInterface $dateTime = new \DateTime();

    public function __construct(
        public readonly LoggerInterface $logger
    ) {}

    public function useUnionTypes(int|string $input): void
    {
        // $input is guaranteed to be either an int or a string
    }

    public function useIntersectionTypes(Traversable&Countable $input): void
    {
        // $input is guaranteed to satisfy the constraints
        // of both `Traversable` AND `Countable`
    }
}
Enter fullscreen mode Exit fullscreen mode

Package Management

Composer is the de facto standard package manager for modern-day PHP, and has been for about a decade now. It's strongly inspired by other popular package managers, such as npm for JavaScript, so if you've used a modern package manager in any other language, chances are you'll feel right at home with Composer.

Packagist is the main public package repository for Composer. Like npm, you can also use Packagist to host your private packages for a reasonable monthly fee.

What I'd consider the main difference between Composer and npm is actually one of culture, rather than a technical difference — the PHP community doesn't generally have the preference for micropackages that the JavaScript community does, so for better or worse, the average PHP project is more likely to have dozens of larger dependencies than hundreds of smaller ones.

Frameworks

The current PHP landscape is dominated by two web application frameworks: Laravel and Symfony. While a detailed breakdown of the differences and similarities between these is out of scope for this post, suffice to say that they're both modern, expressive frameworks that aim to make it easier to write robust, fast, and maintainable web applications while reducing the need to write boilerplate code as much as possible.

If you're concerned about frameworks perhaps being "overkill" for what you want to do with PHP, you'll be happy to hear that Symfony is a microframework out of the box (all components outside of the core framework are 100% optional), and Laravel also has a microframework variant called Lumen.

To help you get a bit of a feel for these, here's what some typical modern PHP code written with one of these frameworks might look like:

<?php

namespace App\Controller;

use Symfony\Component\HttpFoundation\Response;
use Symfony\Component\Routing\Annotation\Route;

class ExampleController
{
    // the RandomNumberGenerator is automatically
    // injected by a service container

    public function __construct(
        private RandomNumberGenerator $randomNumberGenerator
    ) {}

    #[Route('/number')]
    public function number(): Response
    {
        $number = $this->randomNumberGenerator->generate(min: 1, max: 100)

        return new Response(
            "<html><body>Your lucky number is: $number</body></html>"
        );
    }
}
Enter fullscreen mode Exit fullscreen mode

The Open-Source Ecosystem

PHP has an incredibly robust open-source ecosystem that I honestly think even a lot of more "respectable" languages can envy.

There are high-quality, well-maintained open-source libraries available for pretty much everything the average PHP application is likely to need, and many of the most popular packages in the ecosystem are maintained by established vendors or projects rather than individual maintainers, though of course there's plenty of those, too.

Anyhow, the following is a very surface-level overview of some of the most significant packages in the PHP ecosystem:

  • symfony/* - the Symfony Components, a set of incredibly popular PHP packages and the foundation for the Symfony framework
    • symfony/cache - a production-ready caching library with support for many different backing stores
    • symfony/console - a CLI library used by many notable PHP projects
    • symfony/dependency-injection - a PSR-11-compatible service container
    • symfony/dotenv - a .env file parser
    • symfony/event-dispatcher - an event dispatcher
    • symfony/form - a library for creating and processing forms (HTML or otherwise)
    • symfony/http-client - an HTTP client library
    • symfony/mailer - a multi-transport library for creating and sending emails
    • symfony/messenger - a message bus with support for sync and async message processing
    • symfony/notifier - a tool for sending notifications with first-party support for email, SMS, Slack, Discord, Telegram, push notifications, and more
    • symfony/routing - the router used by the Symfony framework
    • symfony/security - utilities for authentication, authorization, CSRF protection, and other common security needs
    • symfony/serializer - a serialization/deserialization library with support for JSON, XML, YAML, CSV, and more
    • symfony/validator - a data validation library
  • league/* - The League of Extraordinary Packages, a set of modern, standards-compliant PHP packages developed with the explicit mission of improving the PHP ecosystem
    • league/commonmark - a CommonMark-compliant Markdown parser
    • league/csv - read and write CSV documents
    • league/flysystem - a filesystem abstraction with support for local filesystems, object storage, FTP, and more
    • league/oauth2-server - an OAuth 2.0 authorization server implementation
    • league/oauth2-client - an OAuth 2.0 client library with built-in and community support for many common OAuth 2.0 providers, as well as custom providers
    • league/omnipay - a multi-gateway payment processing library
  • doctrine/* - packages from the Doctrine Project, largely but not exclusively related to working with databases
    • doctrine/collections - utilities for working with arrays of data
    • doctrine/dbal - a data*base **abstraction **l*ayer with support for MySQL, Oracle, Microsoft SQL Server, PostgreSQL, and SQLite databases
    • doctrine/orm - the Doctrine ORM, a popular PHP ORM based on the Data Mapper pattern
    • doctrine/migrations - utilities for database schema versioning (i.e. database migrations)
  • phpoffice/* - packages from the PHPOffice project, a set of libraries for working with file formats produced by Microsoft Office and other office suites
    • phpoffice/phppresentation - read and write presentation file formats (e.g. .pptx)
    • phpoffice/phpspreadsheet - read and write spreadsheet file formats (e.g. .xlsx, .csv)
    • phpoffice/phpword - read and write document file formats (e.g. .docx)
  • guzzlehttp/guzzle - an HTTP client library based on PSR-7
  • monolog/monolog - a very widely-used logging library based on PSR-3
  • phpunit/phpunit - the de facto standard PHP testing framework, based on the xUnit architecture
  • pestphp/pest - a modern testing framework built on top of PHPUnit
  • nesbot/carbon - an extension of the PHP DateTime API
  • phpstan/phpstan - a static analysis tool, the most popular of several active projects
  • twig/twig - a modern template engine, inspired by Python's Jinja template engine

PHP-FIG

PHP-FIG — short for "PHP Framework Interop Group" — is a group of influential projects in the PHP community working to push PHP forward by standardizing a bunch of things every project was doing in its own, ever-so-slightly incompatible way.

Unfortunately, while pretty much all of the most influential projects in the PHP ecosystem were once members of PHP-FIG, many have since left over concerns that the project was headed in the direction of building a "framework-by-committee" rather than working on relatively simple standards everyone could actually implement. Some things never change, I guess.

That being said, PHP-FIG still very much deserves a place in this post, both because it's frankly a pretty unique project I don't think I've seen attempted anywhere else, and also because it's still produced a number of incredibly useful PSRs (PHP Standard Recommendations) over the years. These include:

No Compiling/Transpiling

One thing you'll probably find refreshing about PHP compared to many comparable languages is that it doesn't require a build step (at least, not one that you have to think about). In fact, due to PHP's typical execution model (more on that later), not only is there no build step, but you don't even have to restart your web server when you change your code — hit save, send the request again and the response you get will be from the code you just saved.

One major benefit of this (to me, anyway) is that when you install a package through Composer, you're simply downloading the source code of the package, not compiled artifacts or code mangled by some build tool before publishing. What this means for you is that any time you click into, say, a function that comes from a third-party package, you're going to be looking at the actual source code — formatting, comments and all.

PHP is Fast

While it's hard to make apples-to-apples performance comparisons between programming languages, and you probably shouldn't be worrying about language runtime performance that much anyway (your code is almost always going to be I/O-bound, after all), it's worth pointing out that modern PHP is very fast, handily beating "slow" languages like Ruby in synthetic benchmarks.

More than anything else, this is thanks to lots of hard work put into improving performance by the PHP team over the years, up to and including adding entirely new features, such as the JIT compiler introduced in PHP 8.

The Ups and Downs of PHP's Unique Execution Model

You might have heard people say that PHP was "doing serverless before serverless was a thing", and this is kind of true.

The way people used to write PHP was they'd have a bunch of PHP files where each file corresponded to a page or route (e.g. index.php for a homepage, item.php for another page, and so on) and output some HTML. Then they'd upload this code to their server — often a shared hosting provider where all a developer had to do was FTP the files up to the server, and it'd just work. The web server would handle all the parts external to your code, like starting up a PHP process for the request and giving it the right script based on the path.

There's a lot of sites on the internet that still work this way, and while PHP has since evolved past some parts of this approach, you can probably see how it resembles some patterns that have become popular in recent years, like filesystem-based routing and serverless functions.

The part of this that's most relevant today is the idea that your app gets initialized and torn down for every request. Any variables you set, anything you do to the objects in your app, everything gets wiped out at the end of the request — there's no way to persist data between requests without relying on some sort of external resource, like a database.

There are naturally some drawbacks to this setup, not the least of which being that it means that putting some data in memory to keep it around for a while is less trivial in PHP than it is in something like Node.js. On the other hand, this setup makes whole classes of bugs impossible at the application level (like meaningful memory leaks), and perhaps more importantly, it means you don't have to worry about writing async code nearly as much as other languages, because you're only ever going to be handling one request per process anyway.

For instance, making an HTTP request is as simple as writing a blocking call to an HTTP client:

use Symfony\Component\HttpClient\HttpClient;

$client = HttpClient::create();
$response = $client->request('GET', 'https://example.com');

$statusCode = $response->getStatusCode();
$content = $response->getContent();
Enter fullscreen mode Exit fullscreen mode

In most other languages, code like this would be a huge no-no because you'd be blocking the runtime from handling any other requests while you wait for that call to HttpClient::request() to finish. In PHP, though, because the assumption that you have the process "to yourself" is built into the language, you can write blocking code like this safely, while avoiding the mental overhead that async code can sometimes involve.

I should probably cap this section off by mentioning that there has been a push in recent years to make asynchronous code viable in PHP, and frameworks like Amp have made significant progress in this area. That being said, the vast majority of PHP code is still synchronous, and will be for the foreseeable future.

Conclusion

If you came into this post with the mindset that PHP is a legacy language nobody in their right mind would want to write code for today, well, I hope I've been able to at least somewhat change your mind on that.

PHP, of course, still has its share of warts, but more than anything else, what I wanted to convey with this post is that it's absolutely possible to write reliable, clean, maintainable code in PHP that you and/or your team will be happy with. Not only that, but there are parts of PHP that you might even prefer to how things work in other languages you're familiar with.

Still not convinced? You're more than welcome to peruse the rest of my site (https://bulletproofphp.dev) to perhaps get a better feel for what real-world PHP looks like, and if you have concerns you think I overlooked in this post, I'd be happy to talk about it on Twitter or in the comments here.

Discussion (2)

Collapse
andersbjorkland profile image
Anders Björkland

What a refreshing read! Sure, I'm already a PHP coder but I like this overview that you give of all the modern features we have available at our fingertips.

I have not thought about the open source advantage that we have. When I first learned the basics of coding I was doing it with Java, around the time it got some cool features (Java 8 got lambda), but then Oracle started it's licensing for its compiler and the javax library too, if I remember correctly (while there is OpenJDK, for a newbie it was scary). I don't know how that has impacted the open source fauna of Java, but we haven't had that kind of influence on PHP which may be one of our advantages here.

Anyway, thanks for sharing this article with us. I enjoyed reading it!

Collapse
bdelespierre profile image
Benjamin Delespierre • Edited on

Arrow functions are kewl too

$b = 2;
$f = fn($a) => $a + $b;
$f(1); // 3
Enter fullscreen mode Exit fullscreen mode

Enums are kewl too (PHP8.1)

Fibers are kewl too (PHP8.1) <= BIG game changer here!

Array destructuring in loops are awesome too:

$a = [[1,2,3],[4,5,6]];
foreach ($a as [$b, $c, $d]) {
    var_dump($b, $c, $d); 
}
Enter fullscreen mode Exit fullscreen mode

Iterators & generators are awesome in PHP

function get_php_files(...$paths): Generator
{
    foreach ($paths as $path) {
        if (is_file($path)) {
            yield new SplFileInfo($path);
            continue;
        }

        $it = new RecursiveDirectoryIterator($path);
        $it = new RecursiveIteratorIterator($it);
        $it = new RegexIterator($it, '/\.php$/', RegexIterator::MATCH);

        yield from $it;
    }
}
Enter fullscreen mode Exit fullscreen mode