Half a year ago, I wrote this:
Ok, so, I've started working on something — probably the biggest project I've worked on in a while — and I'm having a blast. I'm building… A framework 😳
If you haven't heard of that framework before — it's called Tempest — you could check out me building the foundations of it during several livestreams.
I had a break working on Tempest for a couple of months because of other projects, but recently I started diving into it again. I've actually used it to build a small website, and let me tell you: I really enjoyed it.
That probably shouldn't come as a surprise since I built it the way I like to build things. But still, I was very happy seeing all pieces fit together so perfectly. Granted: there is a lot more fine-tuning to do, but I do think there's some potential here.
To be clear: Tempest doesn't aim to replace any frameworks like Laravel or Symfony; but for getting smaller PHP websites up and running? I feel like there's a lot of potential for a small and modern framework.
I especially like how Tempest gets out of your way. There's no need for config files or any setup, Tempest understands your code. I'm not kidding, it works really well.
So, I'd say go check out a very pre-alpha version of what undoubtedly will become something great in the future: https://github.com/tempestphp/tempest-framework.
Much to my frustration, I've been sick since mid-December. Nothing serious, just a very annoying cough that doesn't want to go away. I've seen my doctor numerous times, a specialist, I'm using an inhaler, nasal spray, tried antacids, nothing seems to work. I actually have had the same issue for multiple years now: periods of coughing without any clear reason or solution.
Like I said, it's not too serious, just an annoying cough that doesn't want to go away, though it is very limiting when trying to record videos or audio. The more I talk, the worse it gets, so proper recording sessions aren't really an option right now. That three-minute video about Tempest I linked a couple paragraphs ago took waaaaay too much time and editing to get all the coughs out…
That's why I've been rather quiet on PHP Annotated, and why I've mostly focussed on writing content for PhpStorm lately. Last week, the first post in a series about JetBrains' AI Assistant for Unit Testing went live, and I've got a couple more scheduled soon.
I really hope my coughing gets over soon, and I really hope we'll find a proper solution. I've got another doctor's appointment scheduled next week and hope new medication will have a better impact.
Anyway, I mainly wanted to share this with you in case people were wondering why PHP Annotated is so quiet lately. I really want to get back into it a soon as possible though!
That's it for today's newsletter, do check out Tempest and give it a star while you're there 😉
Until next time
Brent
PS: I feel real inspired to livestream today. I'm still coughing, but I hope I'll manage. Come say hi:
]]>Keep in mind note that I'm working with the data available. That means that these charts are not a 100% accurate representation of the PHP community as a whole, but they are an accurate representation of one of the most prominent parts of PHP: the packagist ecosystem.
Let's start with the percentage of PHP versions being used today, and compare it to the previous three editions, note that I've omitted all versions that don't have more than 1% usage:
Version | 2022-07 | 2023-01 | 2023-07 | 2024-01 |
7.1 | 1.9% | 1.8% | 1.3% | 1.0% |
7.2 | 5.1% | 4.3% | 4.3% | 2.5% |
7.3 | 8.0% | 5.3% | 4.2% | 3.2% |
7.4 | 38.4% | 27.7% | 19.9% | 13.6% |
8.0 | 20.6% | 16.2% | 12.3% | 7.2% |
8.1 | 24.5% | 38.8% | 39.3% | 35.2% |
8.2 | 0.0% | 4.7% | 17.2% | 29.4% |
8.3 | 0.0% | 0.0% | 0.2% | 6.4% |
Visualizing this data looks like this:
There seems to be a slightly faster adoption of PHP 8.3 compared to PHP 8.2: 6.4% of projects are using PHP 8.3 within the first two months of its release, for PHP 8.2 it was 4.7%.
Furthermore, the PHP 7.* share continues to shrink — a good thing given that support for the 7.* series ended more than a year ago. Right now PHP 8.1 is the oldest supported version, only receiving security updates until November 25 this year. I can't help it, I keep saying the same thing over and over again, it's important update your PHP installations!
Moving on to the all-time overview chart, here you can see the evolution of version usage across time:
Next, I used Nikita's popular package analyzer to download the 1000 most popular composer packages. I use a script that scans these packages to determine their minimum required version. Here are the results:
Version | 2022-07 | 2023-01 | 2023-07 | 2024-01 |
5.2 | 10 | 10 | 7 | 7 |
5.3 | 77 | 78 | 65 | 58 |
5.4 | 40 | 40 | 31 | 28 |
5.5 | 35 | 37 | 21 | 16 |
5.6 | 42 | 43 | 32 | 30 |
7.0 | 29 | 30 | 24 | 24 |
7.1 | 153 | 159 | 125 | 100 |
7.2 | 130 | 144 | 133 | 123 |
7.3 | 104 | 106 | 56 | 49 |
7.4 | 86 | 98 | 97 | 87 |
8.0 | 94 | 103 | 144 | 126 |
8.1 | 125 | 129 | 107 | 154 |
8.2 | - | - | 94 | 135 |
8.3 | - | - | - | 0 |
There are two important notes to make here.
Instead of comparing absolute numbers, it's best to plot this data into a chart for a relative comparison, so that we can see changes over time:
Minimal PHP requirement over time
Talking about progression, I'd like to remind open source maintainers about the power and responsibility they hold. Image if all modern open source packages only supported PHP versions that were actively worked on. My suspicion is that many more projects would be encouraged to update faster; eventually leading to a healthier, more performant, and more secure ecosystem. Open source maintainers yield quite a lot of power in this regard.
Also keep in mind that forcing a new minimal PHP requirement doesn't automatically block older projects from using your code: outdated projects can still download older versions of your packages, so there's really no good argument not to do it from a package maintainer's point of view.
That's all data I have to share for this edition of PHP's version stats. You can always reach me via email if you want to share your thoughts or have questions.
You can probably relate. That moment when you were writing code because you wanted to, no, because you desired to. No deadlines, no pressure. Only that feeling of power, being able to create something from nothing with code. It could be something as simple as an HTML web page, a program on your calculator to — hmm — cheat on your maths exam, or hacking something together for a game you're playing.
For me, at least, those are some of the examples that resonate.
I'm writing this as a reminder to myself and whoever needs to hear it: don't forget that time when your programming passion sparked. You're older now, you've got other things to do, you have responsibilities now. You might never experience the same amount of joy compared to that first year of programming, behind the computer in the living room while your siblings were playing, making lots of noise and distracting you.
But you can still find passion projects. Keep an open mind. Write a game, a website, a book, whatever. Not because you must, not to impress your social media following, not to make a living out of it — but because you can, because it gives you joy.
]]>I've always assumed this mentality existed mostly thanks to "being online", and that when it came to face-to-face interactions, people would be more forgiving towards each other.
Apparently, that assumption is wrong 😅
Last week, I went to SymfonyCon, and a year ago I went to Laracon. It turns out people are as harsh towards the other community in real life as they are online.
I'm baffled by this sentiment. There's no denying that people are building successful things with Symfony, just like there's no denying people are building successful things with Laravel. Why does anyone feel the need to convince me or others that their choice is superior? Why do we feel the need to pick sides? Why does it matter that someone else uses another solution to the same problem, if they manage to solve that problem equally well? Why do we feel the need to identify so strongly with a framework, that we need to dismiss the other?
I don't know.
Maybe we should say that more often: "I don't know", especially when it comes to the other side, and give them the benefit of the doubt.
What are your thoughts? Have you had the same experience? Maybe completely the opposite? Let me know!
PS: there were also a bunch of nice people at both SymfonyCon and Laracon, maybe that's good to mention as well 😁
]]>Two weeks ago, I had a chat with my colleague Roman about how PHP is evolving: we both want to get a better understanding of how the community feels about RFCs. While technically, the internals mailing list is open to everyone, I know only a very small group of PHP developers who actually participate. So we decided to build a small app that could help us and the community to lower the bar for participation.
Its temporary name is "RFC Vote" — but please share your ideas if you have better names! It's a small project that allows everyone to vote for RFCs. The most interesting part though is how you vote: if you vote directly, you must give argumentation to explain why you voted yes or no. On top of that: everyone can vote for existing arguments individually. Say someone already shared the same reasons as you for voting yes or no, then you can simply vote for that argument, instead of writing something on your own — argument votes are added to the total result as well.
We hope that by using this vote mechanic, we'll get better insights into why people vote the way they do, and not just if people would like a feature or not. And, maybe — we can even inspire internal developers to take a look at the community results, before casting their own vote for upcoming RFCs.
Here's an except from our about page:
This project is dedicated to providing a platform for the PHP community to express their thoughts and feelings about the proposals for the PHP language in an easier way.
Our main goal is to visualize the diverse opinions and arguments surrounding PHP's proposed features, making it easier to understand the benefits and downsides of each proposal. By doing so, we hope to foster a greater understanding of how PHP developers feel about these changes.
While official voting on RFCs is limited to internal qualified developers and a specific group of contributors, RFC Vote offers a space for everyone in the PHP community to share their voice. Your votes and comments won't directly influence the official PHP RFC outcomes, but they can serve as valuable insights for those involved in the decision-making process.
In addition to casting a vote, you are encouraged to share your reasoning behind your choices on each RFC. By explaining why you voted yes or no, we can collectively gain better insights into the popularity or concerns associated with an RFC. This collaborative approach allows us to learn from one another and contributes to a more informed and connected PHP community.
So, next thing I knew, I was programming like a madman, did some livestreams to show off the progress, and got help from 9 other developers.
After two weeks, we're nearing a "version 1", and so I wanted to share it with you as well. Maybe you have some ideas, questions, or just want to take a look? Well, the site is already live here: https://rfc.stitcher.io/, and you can check out the code on GitHub.
Oh, by the way, I'll be doing another live stream at 11 CEST today (less than 2 hours from when this post was published), so you might want to tune in? I'll see you there!
]]>PHP 8.3 will be released on November 23, 2023; it has improvements to readonly classes, the new json_validate()
function, additions to the recently added Randomizer
class, stack overflow detection, and more.
In this post, we'll go through all features, performance improvements, changes and deprecations one by one. If you want to stay up to date, you can subscribe to my newsletter, follow me on Twitter, or subscribe to my RSS feed.
This RFC proposed two changes, only one was accepted: being able to reinitialize readonly properties while cloning. That might sound like a very big deal, but this RFC only addresses a very specific (but important) edge case: overwriting property values within __clone()
, in order to allow deep cloning readonly properties.
readonly class Post
{
public function __construct(
public DateTime $createdAt,
) {}
public function __clone()
{
$this->createdAt = new DateTime();
// This is allowed,
// even though `createdAt` is a readonly property.
}
}
You can read an in-depth post about this RFC and some sidenotes here.
You can now typehint class constants:
class Foo
{
const string BAR = 'baz';
}
#[Override]
attribute RFCThe new #[Override]
attribute is used to show a programmer's intent. It basically says "I know this method is overriding a parent method. If that would ever change, please let me know".
Here's an example:
abstract class Parent
{
public function methodWithDefaultImplementation(): int
{
return 1;
}
}
final class Child extends Parent
{
#[Override]
public function methodWithDefaultImplementation(): int
{
return 2; // The overridden method
}
}
Now, let's imagine at one point the parent method changes its method name:
abstract class Parent
{
public function methodWithNewImplementation(): int
{
return 1;
}
}
Thanks to the #[Override]
attribute, PHP will be able to detect that Child::methodWithDefaultImplementation()
doesn't override anything anymore, and it will throw an error.
You can read more about the #[Override]
attribute here.
If you have an empty array, add an item with a negative index, and then add another item, that second item would always start at index 0
:
$array = [];
$array[-5] = 'a';
$array[] = 'b';
var_export($array);
//array (
// -5 => 'a',
// 0 => 'b',
//)
Starting from PHP 8.3, the next item will be added at index -4
:
//array (
// -5 => 'a',
// -4 => 'b',
//)
Previously, you weren't able to mark anonymous classes as readonly. That's fixed in PHP 8.3:
$class = new readonly class {
public function __construct(
public string $foo = 'bar',
) {}
};
json_validate()
function RFCPreviously, the only way to validate whether a string was valid JSON, was to decode it and detect whether any errors were thrown. This new json_validate()
function is beneficial if you only need to know whether the input is valid JSON, since it uses less memory compared to decoding the string.
json_validate(string $json, int $depth = 512, int $flags = 0): bool
Randomizer
additions RFCPHP 8.2 added the new Randomizer class. This update brings some small additions:
Randomizer::getBytesFromString(string $string, int $length): string
This method allows you to generate a string with a given length that consists of randomly selected bytes from a given string.
Randomizer::getFloat(
float $min,
float $max,
IntervalBoundary $boundary = IntervalBoundary::ClosedOpen
): float
getFloat()
returns a float between $min
and $max
. You can define whether $min
and $max
should be included thanks to the IntervalBoundary
enum. Closed
means the value is included, while Open
means excluded.
Randomizer::nextFloat(): float {}
Finally, nextFloat()
is a shorthand for getFloat(0, 1, IntervalBoundary::ClosedOpen)
, in other words: it'll give you a random float between 0 and 1, where 1 is excluded.
PHP 8.3 allows you to fetch constants with a more dynamic syntax:
class Foo
{
const BAR = 'bar';
}
$name = 'BAR';
// Instead of this:
constant(Foo::class . '::' . $name);
// You can now do this:
Foo::{$name};
In many cases, PHP would simply throw an Exception
or Error
object; or emit a warning or error when something went wrong in dealing with dates and times. This RFC goes through all those edge cases and adds proper, dedicated exceptions for them.
We now have exceptions like DateMalformedIntervalStringException
, DateInvalidOperationException
, and DateRangeError
.
In general, these additions won't break any code, since these newly added exceptions and errors subclass the generic Exception
and Error
classes. However, there are three small breaking changes that come with this RFC:
Epoch doesn't fit in a PHP integer
now returns a new DateRangeError
instead of a generic ValueError
, which it does not subclass. This is only an issue for 32-bit platforms.Only non-special relative time specifications are supported for subtraction
warning with DateTime::sub()
and date_sub()
becomes a new DateInvalidOperationException
.Unknown or bad format (%s) at position %d (%c): %s
and String '%s' contains non-relative elements
warnings that are created while parsing wrong/broken DateInterval
strings will now throw a new DateMalformedIntervalStringException
when used with the OO interface, instead of showing a warning and returning false.unserialize()
error handling RFCunserialize()
will now always emit a E_WARNING
when running into problems instead of sometimes an E_NOTICE
.
This RFC also proposed adding more exceptions when running unserialize()
, but that part didn't get accepted.
range()
function breakingFrom the changelog:
TypeError
is now thrown when passing objects, resources, or arrays as the boundary inputsValueError
is thrown when passing 0 for $step
ValueError
is now thrown when using a negative $step
for increasing ranges$step
is a float that can be interpreted as an int, it is now done soValueError
is now thrown if any argument is infinity
or NAN
E_WARNING
is now emitted if $start
or $end
is the empty string. The value continues to be cast to the value 0.E_WARNING
is now emitted if $start
or $end
has more than one byte, only if it is a non-numeric string.E_WARNING
is now emitted if $start
or $end
is cast to an integer because the other boundary input is a number. (e.g. range(5, 'z');
)E_WARNING
is now emitted if $step is a float when trying to generate a range of characters, except if both boundary inputs are numeric strings (e.g. range('5', '9', 0.5);
does not produce a warning)range()
now produce a list of characters if one of the boundary inputs is a string digit instead of casting the other input to int (e.g. range('5', 'z');
)From the changelog:
Uses of traits with static properties will now redeclare static properties inherited from the parent class. This will create a separate static property storage for the current class. This is analogous to adding the static property to the class directly without traits.
PHP 8.3 adds two new ini directives called zend.max_allowed_stack_size
and zend.reserved_stack_size
. Programs that are close to overflowing the call stack may now throw an Error
when using more than the difference between zend.max_allowed_stack_size
and zend.reserved_stack_size
.
The benefit of this feature is that stack-overflow-induced segmentation faults won't result in segfaults anymore, making debugging a lot easier.
The default for zend.max_allowed_stack_size
is 0
, meaning PHP will automatically determine a value. You can also provide -1
to indicate there isn't a limit, or a specific number of bytes. The zend.reserved_stack_size
directive is used to determine the "buffer zone", so that PHP is able to still throw an error instead of actually running out of memory. The value here should be a number of bytes, but PHP will determine a reasonable default for you, so you don't necessarily need to set it, unless you're running into edge cases for specific programs.
On a final note, for fibers, the existing fiber.stack_size
directive is used as the max allowed stack size.
zend.max_allowed_stack_size=128K
mb_str_pad
function RFCFrom the RFC:
In PHP, various string functions are available in two variants: one for byte strings and another for multibyte strings. However, a notable absence among the multibyte string functions is a
mbstring
equivalent ofstr_pad()
. Thestr_pad()
function lacks multibyte character support, causing issues when working with languages that utilize multibyte encodings like UTF-8. This RFC proposes the addition of such a function to PHP, which we will callmb_str_pad()
.
The function looks like this:
function mb_str_pad(
string $string,
int $length,
string $pad_string = " ",
int $pad_type = STR_PAD_RIGHT,
?string $encoding = null,
): string {}
Let's say you have a class that supports magic methods:
class Test {
public function __call($name, $args)
{
var_dump($name, $args);
}
public static function __callStatic($name, $args) {
var_dump($name, $args);
}
}
PHP 8.3 allows you to create closures from those methods, and then pass named arguments to those closures. That wasn't possible before.
$test = new Test();
$closure = $test->magic(...);
$closure(a: 'hello', b: 'world');
Previously, visibility for constants weren't checked when implementing an interface. PHP 8.3 fixes this bug, but it might lead to code breaking in some places if you weren't aware of this behaviour.
interface I {
public const FOO = 'foo';
}
class C implements I {
private const FOO = 'foo';
}
As is usual with every release, there's a single RFC that adds a bunch of small deprecations. Keep in mind that deprecations are no errors, and these are generally a good thing for the language to move forward. These are the deprecations that passed, you can read more details about them in the RFC:
$widths
to mb_strimwidth()
NumberFormatter::TYPE_CURRENCY
constantMT_RAND_PHP
)ldap_connect()
with 2 parameters $host
and $port
Not every change in PHP passes the RFC process. In fact, the majority of changes include maintenance and bugfixing, and don't require an RFC. All of these changes are listed in the UPGRADING document. I'll list some of the most prominent ones, but you should definitely read throughout the whole list if you want to know about the tiniest details.
void
now return null
instead of returning FFI\CData:void
posix_getrlimit()
now takes an optional $res
parameter to allow fetching a single resource limit.gc_status()
has four new fields: running
, protected
, full
, and buffer_size
.class_alias()
now supports creating an alias of an internal class.mysqli_poll()
now raises a ValueError
when the read nor error arguments are passed.array_pad()
is now only limited by the maximum number of elements an array can have. Before, it was only possible to add at most 1048576 elements at a time.posix_sysconf()
, posix_pathconf()
, posix_fpathconf()
, and posix_eaccess()
proc_get_status()
multiple times will now always return the right value on posix systems.opcache.consistency_checks
ini directive was removedarray_sum()
and array_product()
That's it for now, but this list will grow over time. If you want to stay up to date, you can subscribe to my newsletter, follow me on Twitter, or subscribe to my RSS feed.
]]>They are, of course, right, but only half. Let's talk about object relations.
You can think of the classic class/interface relation as an Is A
relation: you can say Item Is A Purchasable Item
, or, in technical terms: class Item implements Purchasable
.
I don't think there's any disagreement here amongst developers, this is the classic definition of an interface. It allows us to write code that works with all types of purchasables, without worrying which concrete implementation we're dealing with.
function createPayment(Purchasable $purchasable): Payment
{
$price = $purchasable->getPrice();
// …
}
This is where people who argue against interface default methods stop. If this is the only way you're using interfaces, then yes, you're right: interface default methods aren't necessary.
However, there is another way interfaces are used. And mind you: I'm not saying "there is another way interfaces can be used", no, I'm saying this is happening in modern PHP code, today, in many projects.
Here goes. Interfaces can be used to model an Acts As
relation — I have a perfect example, thanks to Larry.
Here's the so called LoggerInterface
, part of PSR-3:
interface LoggerInterface
{
public function emergency(
string|\Stringable $message,
array $context = []
): void;
public function alert(
string|\Stringable $message,
array $context = []
): void;
public function critical(
string|\Stringable $message,
array $context = []
): void;
public function error(
string|\Stringable $message,
array $context = []
): void;
public function warning(
string|\Stringable $message,
array $context = []
): void;
public function notice(
string|\Stringable $message,
array $context = []
): void;
public function info(
string|\Stringable $message,
array $context = []
): void;
public function debug(
string|\Stringable $message,
array $context = []
): void;
public function log(
$level,
string|\Stringable $message,
array $context = []
): void;
}
As you can see, most methods are essentially shortcuts for the log
method: they are convenience methods so that you don't have to manually provide a logging level. It's a great design choice to include in the interface, as it forces all implementations to have better accessibility.
However, let's be honest, no one is ever going to need a logger with a different implementation of debug
or info
or any of the other shorthands. These methods will always look the same:
public function debug(
string|\Stringable $message,
array $context = []
): void
{
$this->log(LogLevel::DEBUG, $message, $context);
}
In essence, this LoggerInterface
is not only describing an Is A
relation — if that were the case we'd only need the log
method. No, it also describes an Acts As
relation: a concrete logger implementation can Act As
as proper logger, including the convenience methods associated with it. FileLogger Acts As LoggerInterface
, Monolog Acts As LoggerInterface
.
It all boils down to the question: is this a valid way of writing and using interfaces? I would say, yes.
I'd encourage you to take a close look at your own projects, and be totally honest for a moment: are any of your interfaces describing an Acts As
relationship? If they do, then you cannot make the case against interface default methods.
As always, it's important to note that I'm working with the data available to us. That means that these charts are not a 100% accurate representation of the PHP community as a whole, but they are an accurate representation of one of the most prominent parts of PHP: the packagist ecosystem.
Let's start with the percentage of PHP versions being used today, and compare it to the previous three editions, note that I've omitted all versions that don't have more than 1% usage:
Version | 2022-01 | 2022-07 | 2023-01 | 2023-07 |
7.1 | 2.4% | 1.9% | 1.8% | 1.3% |
7.2 | 6.6% | 5.1% | 4.3% | 4.3% |
7.3 | 12.0% | 8.0% | 5.3% | 4.2% |
7.4 | 43.9% | 38.4% | 27.7% | 19.9% |
8.0 | 23.9% | 20.6% | 16.2% | 12.3% |
8.1 | 9.1% | 24.5% | 38.8% | 39.3% |
8.2 | 0.0% | 0.0% | 4.7% | 17.2% |
Visualizing this data looks like this:
It's important to know which PHP versions are currently still supported: PHP 8.2 and PHP 8.1 are still receiving updates. PHP 8.0 is still getting security updates until the end of November, this year. That means that PHP 7.4 and below don't receive any updates more, and should be considered end of life.
In total, that's around 30% of packagist downloads by outdated and insecure version of PHP. At the beginning of this year, that number was closer to 40%, meaning we see a steady decline — a good thing!
Moving on to the all-time overview chart, here you can see the evolution of version usage across time:
It seems that PHP 8.1 saw the biggest growth over time since PHP 7.4 and PHP 5.5. PHP 8.2, in comparison, seems to make a slower start. It's also interesting to note a relative high percentage of PHP 8.1 two years in a row. Granted, PHP 8.1 was a pretty solid release with features like enums and readonly properties. It'll be interesting to see how this graph evolves next year, when PHP 8.1 moves in security fixes only mode.
Next, I used Nikita's popular package analyzer to download the 1000 most popular composer packages. I wrote a script that scans these packages to determine their minimum required version. Here are the results:
Version | 2022-01 | 2022-07 | 2023-01 | 2023-07 |
5.2 | 10 | 10 | 10 | 7 |
5.3 | 83 | 77 | 78 | 65 |
5.4 | 43 | 40 | 40 | 31 |
5.5 | 42 | 35 | 37 | 21 |
5.6 | 49 | 42 | 43 | 32 |
7.0 | 29 | 29 | 30 | 24 |
7.1 | 190 | 153 | 159 | 125 |
7.2 | 133 | 130 | 144 | 133 |
7.3 | 116 | 104 | 106 | 56 |
7.4 | 69 | 86 | 98 | 97 |
8.0 | 160 | 94 | 103 | 144 |
8.1 | - | 125 | 129 | 107 |
8.2 | - | - | - | 94 |
There are two important notes to make here.
Instead of comparing absolute numbers, it's best to plot this data into a chart for a relative comparison, so that we can see changes over time:
Minimal PHP requirement over time
There seems to be a pretty big leap in PHP 8.0 and PHP 8.1 being the minimal versions — a good thing. After all, the open source community plays a big part in pushing the community forward by increasing their minimal required version.
That's all data I have to share for this edition of PHP's version stats. You can always reach me via email if you want to share your thoughts or have questions. You can also subscribe to my newsletter if you want to receive updates about this blog in the future.
#[Override]
attribute. It's a feature already known in other languages, but let me summarize in case you're unaware of what it does.
Marking a method with the #[Override]
attributes signifies that you know this method is overriding a parent method. So the only thing it does, is show intent.
Why does that matter? You already know you're overriding a method, don't you? Well, let's imagine these two classes:
abstract class Parent
{
public function methodWithDefaultImplementation(): int
{
return 1;
}
}
final class Child extends Parent
{
#[Override]
public function methodWithDefaultImplementation(): int
{
return 2; // The overridden method
}
}
Now, let's imagine at one point the parent method changes its method name:
abstract class Parent
{
public function methodWithNewImplementation(): int
{
return 1;
}
}
Before the #[Override]
attribute, there was no way of knowing that Child::methodWithDefaultImplementation()
doesn't override the renamed method anymore, which could lead to unforeseen bugs.
Thanks to #[Override]
though, PHP now knows something is wrong, thanks to that attribute. It basically says "I know this method should override a parent method. If that would ever change, please let me know".
What strikes me most about this RFC, is how irrelevant it could be. Once again we're adding runtime checks for something that could be determined by static analysers.
I don't want to repeat every argument I made in the past, so I'll just link to my previous thoughts on the topic, and summarise: we're missing out. PHP internals should either come with an official spec for static analysers, or with a first-party static analyser. Why? Because so much more would be possible, and it would drive PHP forward tremendously.
Anyway, it seems like many people have tried to make the same argument about this RFC in particular on the Internals mailing list, to no avail.
I don't mind this feature, although I'll probably never use it myself (I use an IDE that prevents me from making these kinds of mistakes, I don't need PHP's runtime to double-check it for me). What I am sad about, is how the PHP community is divided between the static-analysis and non-static-analysis camps, and I don't know if there will ever be someone who'll be able to unify and steer the language into a next decade of progression.
]]>It was a beautifully complex, abstract class that integrated with Doctrine (the ORM that I was using back then). It had every possible CRUD operation you could think of — not just for entities, but also for child- and has many relations; there were automatic overviews, filtering, pagination, validation, data persistence, routing, and whatnot.
And the only thing I had to do was to create a new controller, extend from my amazing CRUDController™, provide an entity class, and be done.
class TimesheetController extends CRUDController
{
public function getEntity(): string
{
return Timesheet::class;
}
}
Such a blast!
Except, of course: exceptions started to emerge. Not the programming kind, but the business kind. Some controllers had to do some things a little differently. It was small things at first: different URL schemes, different kinds of validation; but soon, things grew more and more complex: support for nested entities or complex filtering, to name a few.
And young me? I just kept going. Adding the proverbial knobs and pulls to my abstract class (which was growing into a set of classes by now).
In the end, I created a monster; and — ironically — it had taken more time than if I had simply copied code between controllers over and over again. On top of that: I was leaving the startup, and no one really understood how more than 50 controllers actually worked.
You might assume I understood, but let me be clear: I didn't really know how much of it worked anymore.
Yes, I had failed to see the proper solution: a class generator — so that I didn't have to manually copy code again. It actually existed back then, I simply didn't know about it, no one told me about it, and I wasn't smart enough to question myself once I started going down a certain path.
It's such a classic example, many of you probably recognise it. I wouldn't say my coding skills were bad, by the way. No, I didn't question myself enough, I lacked self-reflection and wasn't able to critically look at my own ideas.
I wish I could show you The CRUDController™'s source code; but I don't have access to it anymore, unfortunately. Luckily, the memory still exists. And it's a memory I remind myself of very often when I'm going down the path of abstractions and complications. It's often enough to hold me back and look for better solutions.
]]>So, how do we go from plain PHP to a map like this? It all starts with noise.
Let's imagine we have grid of 150 by 100 pixels. Each pixel makes up a point of our map.
$pixels = [];
for($x = 0; $x < 150; $x++) {
for($y = 0; $y < 100; $y++) {
$pixels[$x][$y] = drawPixel($x, $y, 0);
}
}
For now, the drawPixel
function will generate a div
per pixel, which can be laid out on a CSS grid. We can refactor to use canvas
later, but being able to use CSS's built-in grid saves a lot of time.
function drawPixel(int $x, int $y): string
{
return <<<HTML
<div style="--x: {$x}; --y: {$y};"></div>
HTML;
}
This is the template file:
<style>
:root {
--pixel-size: 9px;
--pixel-gap: 1px;
--pixel-color: #000;
}
.map {
display: grid;
grid-template-columns: repeat({{ count($pixels) }}, var(--pixel-size));
grid-auto-rows: var(--pixel-size);
grid-gap: var(--pixel-gap);
}
.map > div {
width: var(--pixel-size);
height: 100%;
grid-area: var(--y) / var(--x) / var(--y) / var(--x);
background-color: var(--pixel-color);
}
</style>
<div class="map">
@foreach($pixels as $x => $row)
@foreach($row as $y => $pixel)
{!! $pixel !!}
@endforeach
@endforeach
</div>
This is the result:
By the way, I will get rid of the gaps between pixels, I only added them to show that these are indeed separate grid cells.
Let's play around with our grid. We'll start by assigning a value between 0 and 1 for each individual pixel. We'll use a class called Noise
that takes a pixel's point (X/Y coordinates), and returns a value for that point. First, we'll return a random value.
final readonly class Noise
{
public function __construct(
private int $seed,
) {}
public function generate(Point $point): float
{
return rand(1, 100) / 100;
}
}
// …
drawPixel($x, $y, $noise->generate($x, $y));
Here's the result:
Relying on randomness won't get us very far though. We want a given seed to generate the same map over and over again. So instead of randomness, let's write a hash function: a function that, for any given point and seed, will generate the same value over and over again. You could make it as simple as multiplying the seed with the x and y coordinates, and turning that into a fraction:
public function generate(Point $point): float
{
$hash = $this->seed * $point->x * $point->y;
return floatval('0.' . $hash);
}
The result, however, doesn't seem random enough. Remember that we want to generate a world map: we want some randomness, but also some cohesion. So our hash function will need to be a bit more complex.
Let's try an existing hash function that we don't need to invent ourselves. We probably want a performant one, since we're generating a hash for thousands of pixels. PHP has support for xxHash, which is an "extremely fast hashing algorithm". Let's give it a try.
private function hash(Point $point): float
{
$hash = bin2hex(
hash(
algo: 'xxh32',
data: $this->seed * $point->x * $point->y,
)
);
$hash = floatval('0.' . $hash);
return $hash;
}
This noise looks promising: it's quite random, but always yields the same result for a given seed. But going from this to a cohesive world map still seems like a leap. Let's change our hash function, so that it'll return the same color within a square of 10 pixels:
private function hash(Point $point): float
{
$baseX = ceil($point->x / 10);
$baseY = ceil($point->y / 10);
$hash = bin2hex(
hash(
algo: 'xxh32',
data: $this->seed * $baseX * $baseY,
)
);
$hash = floatval('0.' . $hash);
return sqrt($hash);
}
Here's the result:
Oh, by the way: I take the square root of the hash, just to increase all values a little bit. That'll be useful in the future, but not necessary. Without the square root, the map looks like this:
Let's imagine something for a second. Let's say all pixels with a value higher than 0.6 are considered land, and all pixels with a lower value are considered water. Let's make some changes to our drawPixel
method to reflect that behaviour:
function drawPixel(int $x, int $y, float $value): string
{
$hexFromNoise = hex($value);
$color = match(true) {
$noise < 0.6 => "#0000{$hexFromNoise}", // blue
default => "#00{$hexFromNoise}00", // green
};
return <<<HTML
<div style="--x: {$x}; --y: {$y}; --pixel-color: {$color}"></div>
HTML;
}
By the way, that hex
function converts a value between 0 and 1 to a two-digit hexadecimal. It looks like this:
function hex(float $value): string
{
if ($value > 1.0) {
$value = 1.0;
}
$hex = dechex((int) ($value * 255));
if (strlen($hex) < 2) {
$hex = "0" . $hex;
}
return $hex;
}
The result is looking a lot more like a map already:
Ok, pretty nice! But these sharp edges don't really look realistic. Can we find a way to make transitions between the edges more smooth?
Time for some maths. Let's say we have two values: 0.34
and 0.78
. We want to know the value exactly in the middle between these two. How do we do that?
Well, there's a simple mathematical formula for that. It's called "Linear Interpolation" — "LERP" for short:
function lerp(float $a, float $b, float $fraction): float
{
return $a + $fraction * ($b - $a);
}
lerp(0.34, 0.78, 0.5); // 0.56
So, given a number $a
(0.34
), a number $b
(0.78
), and a fraction (0.5
, also known as: "half"); we get 0.56
— the number exactly in the middle between 0.34
and 0.78
.
Thanks to the fraction part in our lerp formula, we can determine the value at any place between these points, not just the middle:
lerp(0.34, 0.78, 0.25); // 0.45
Ok, so why is this important? Well, we can use our lerp function to smooth edges! Let go back to our noise pattern and explain:
Let's say that, instead of coloring each pixel in this grid, we only color a pixel when it's exactly on a 10x10 lattice. In other words: when its x and y coordinates are divisible by 10.
final readonly class Noise
{
public function generate(Point $x): float
{
if ($point->x % 10 === 0 && $point->y % 10 === 0) {
return $this->hash($point);
} else {
return 0.0;
}
}
// …
}
Here's the lattice:
Let's imagine that these pixels are the $a
and $b
boundaries we pass into our lerp function. For any given pixel, it's trivial to determine these surrounding "fixed" points (they are on a fixed 10x10 grid) and we can also calculate the pixel's relative distance to those points. We can use the hash of these fixed points and the distance from any pixel to these points as input values for our lerp function. The result will be a value that's somewhere between the values of our two edge points — in other words: a smooth transition.
First, we'll use our lerp function on the y-axis (whenever x is divisible by 10). We'll determine the relative top and bottom points on our "lattice", calculate the distance between our current point and the top point, and then we'll use our lerp function to determine the right value between the top and bottom point, with that distance fraction:
if ($point->x % 10 === 0 && $point->y % 10 === 0) {
$noise = $this->hash($point);
} elseif ($point->x % 10 === 0) {
$topPoint = new Point(
x: $point->x,
y: (floor($point->y / 10) * 10),
// The closest point divisible by 10, above our current pixel
);
$bottomPoint = new Point(
x: $point->x,
y: (ceil($point->y / 10) * 10)
// The closest point divisible by 10, below our current pixel
);
$noise = lerp(
// The hash value (or color) of that top point:
a: $this->hash($topPoint),
// The hash value (or color) of that bottom point:
b: $this->hash($bottomPoint),
// The distance between our current point and the top point
// — the fraction
fraction: ($point->y - $topPoint->y) / ($bottomPoint->y - $topPoint->y),
);
}
Here's the result, you can already see the smooth transition within the lines:
Next, let's add the same functionality in the other direction, when y is divisible by 10:
if ($point->x % 10 === 0 && $point->y % 10 === 0) {
// …
} elseif ($point->x % 10 === 0) {
// …
} elseif ($point->y % 10 === 0) {
$leftPoint = new Point(
x: (floor($point->x / 10) * 10),
y: $point->y,
);
$rightPoint = new Point(
x: (ceil($point->x / 10) * 10),
y: $point->y,
);
$noise = lerp(
$this->hash($leftPoint),
$this->hash($rightPoint),
($point->x - $leftPoint->x) / ($rightPoint->x - $leftPoint->x),
);
}
No surprises:
Finally, for the remainder of the pixels, we won't be able to do a simple lerp function (which only works in one dimension). We'll have to use Bilinear Interpolation: we'll first make two lerp values for both x-axes, and then one final lerp value for the y-axis. We'll also need four edge points instead of two, because these pixels aren't aligned with our lattice.
if ($point->x % 10 === 0 && $point->y % 10 === 0) {
// …
} elseif ($point->x % 10 === 0) {
// …
} elseif ($point->y % 10 === 0) {
// …
} else {
$topLeftPoint = new Point(
x: (floor($point->x / 10) * 10),
y: (floor($point->y / 10) * 10),
);
$topRightPoint = new Point(
x: (ceil($point->x / 10) * 10),
y: (floor($point->y / 10) * 10),
);
$bottomLeftPoint = new Point(
x: (floor($point->x / 10) * 10),
y: (ceil($point->y / 10) * 10)
);
$bottomRightPoint = new Point(
x: (ceil($point->x / 10) * 10),
y: (ceil($point->y / 10) * 10)
);
$a = lerp(
$this->hash($topLeftPoint),
$this->hash($topRightPoint),
($point->x - $topLeftPoint->x) / ($topRightPoint->x - $topLeftPoint->x),
);
$b = lerp(
$this->hash($bottomLeftPoint),
$this->hash($bottomRightPoint),
($point->x - $bottomLeftPoint->x) / ($bottomRightPoint->x - $bottomLeftPoint->x),
);
$noise = lerp(
$a,
$b,
($point->y - $topLeftPoint->y) / ($bottomLeftPoint->y - $topLeftPoint->y),
);
}
Note that there's some repetition in our code that we could get rid of. But I prefer explicitly making all four edge points for clarity. Take a look at the result though:
This looks much smoother! Let's apply our colours:
Hm. You can probably see where we're going, but I still think these lines are far too… rough. Luckily, there are two more tricks we can apply! First, instead of using a plain lerp function, we can apply a so-called "shaping function" to our fraction. With this shaping function, we can manipulate our fraction before passing it to the lerp function. By default, our fraction will have a linear value — it's the distance from any given point to the starting edge:
But by applying a function to our fraction, we can manipulate it so that the values closer to the edges are even more smooth.
We could use whatever function we'd want. For our case I'll use a shaping function called smoothstep
, which smooths edges.
function smooth(float $a, float $b, float $fraction): float
{
$smoothstep = function (float $fraction): float {
$v1 = $fraction * $fraction;
$v2 = 1.0 - (1.0 - $fraction) * (1.0 -$fraction);
return lerp($v1, $v2, $fraction);
};
return lerp($a, $b, $smoothstep($fraction));
}
The difference is subtle, but it's a little bit better.
The second trick is to apply a new layer of noise. This one shouldn't be as random as our first one though. We'll use a simple circular pattern, and apply it as a height map on our existing noise. The further a pixel is from the center, the smaller its value:
private function circularNoise(int $totalWidth, int $totalHeight, Point $point): float
{
$middleX = $totalWidth / 2;
$middleY = $totalHeight / 2;
$distanceFromMiddle = sqrt(
pow(($point->x - $middleX), 2)
+ pow(($point->y - $middleY), 2)
);
$maxDistanceFromMiddle = sqrt(
pow(($totalWidth - $middleX), 2)
+ pow(($totalHeight - $middleY), 2)
);
return 1 - ($distanceFromMiddle / $maxDistanceFromMiddle) + 0.3;
}
This is the pattern on its own:
And now we combine this pattern with our existing noise, which is as easy as multiplying them:
final readonly class Noise
{
public function __construct(
private int $seed,
) {}
public function generate(Point $point): float
{
return $this->baseNoise($point)
* $this->circularNoise($point);
}
}
Here's the result:
This looks a lot better! Thanks to our circular pattern, the middle portion of our map is raised, while the outer portions are lowered. It creates a neat island look. Let's try out some seeds to see the difference between them:
Pretty nice! But we're far from done: we'll want to add different areas on our map: forests, plains, mountains, vegetation, …. Simply using a match
in our drawPixel
method won't suffice anymore.
Let's make an interface Biome
, which will determine our pixel color, and can determine what kind of vegetation should be added. We'll also represent pixels as a proper value object.
interface Biome
{
public function getPixelColor(Pixel $pixel): string;
}
Let's add seas and plains first.
final readonly class SeaBiome implements Biome
{
public function getPixelColor(Pixel $pixel): string
{
$base = $pixel->value;
while ($base < 0.25) {
$base += 0.01;
}
$r = hex($base / 3);
$g = hex($base / 3);
$b = hex($base);
return "#{$r}{$g}{$b}";
}
}
final readonly class PlainsBiome implements Biome
{
public function getPixelColor(Pixel $pixel): string
{
$g = hex($pixel->value);
$b = hex($pixel->value / 4);
return "#00{$g}{$b}";
}
}
Depending on a pixel's biome, we'll use its noise to generate a different kind of color.
In our drawPixel
function, we can now make some changes:
function drawPixel(Pixel $pixel): string
{
$biome = BiomeFactory::for($pixel);
$color = $biome->getPixelColor($pixel);
return <<<HTML
<div style="
--x: {$x};
--y: {$y};
--pixel-color: {$color};
"></div>
HTML;
}
For now, our
final readonly class BiomeFactory
{
public static function for(Pixel $pixel): Biome
{
return match(true) {
$pixel->value < 0.6 => new SeaBiome(),
default => new PlainsBiome(),
};
}
}
It still works:
Let's go ahead and add all biomes now:
final readonly class BiomeFactory
{
public static function make(Pixel $pixel): Biome
{
return match(true) {
$pixel->value < 0.4 => new SeaBiome(),
$pixel->value >= 0.4 && $pixel->value < 0.44 => new BeachBiome(),
$pixel->value >= 0.6 && $pixel->value < 0.8 => new ForestBiome(),
$pixel->value >= 0.8 => new MountainBiome(),
default => new PlainsBiome(),
};
}
}
Note that I also decided to change the sea level: from 0.6 to 0.4, so the map looks a bit different now. Take a look:
Not bad, right? But this is far from a finished game — in fact, this is only the very first step: we'll need a way of interacting with this map, we'll need to define some form of gameplay. Maybe you remember the intro? I mentioned resource gathering. I imagine this game to be some kind of cookie-clicker style game: the more resources you gather, the more you can advance in the tech tree, the more resources you can gather, …
Anyway, I've done a lot more work on this game already: I actually started this project as an experiment to explore the limits of Laravel Livewire, so interactivity and gameplay were the primary focus. I've explained the basic concepts already on my YouTube channel in two videos. I'm also working on a third video where I discuss how I added interaction to this particular game board, as well as the problems I ran in to (there are a bunch of caveats I haven't mentioned yet in this blog post).
So, if you want to follow along, make sure to subscribe on YouTube — I hope to make the third and final video of this series soon. Alternatively, you can subscribe to my mailing list, where I'll send you an update as well.
In the meantime, you can watch the first two parts of this series; and don't forget to subscribe!
]]>
I do want to write about the underlying mindset of these kinds of posts, and of the people upvoting — that mindset of calling something "harmful". I can understand that someone doesn't like a framework, an ecosystem, the community or some individuals of that community. But calling something "harmful"? As if it's proven that Laravel hurts? Hurts what exactly? The PHP community? PHP's reputation? Laravel's competing frameworks? A programmer's "best practices"?
For what it's worth, I'd say that Laravel has done mostly good to the PHP community: it seems that a lot of people are able to make a living thanks to it; it seems that Laravel is widely known and praised within the programming community; in what way could it be considered "harmful"?
"It creates unmaintainable projects and doesn't teach best practices" — people say.
The argument of "best practices" baffles me. What defines a "best practice"? Aren't "best practices" meant to help you in building successful software? And hasn't Laravel proven to be able to do exactly that: build successful software? Shouldn't our assumptions of what defines a "best practice" be redefined if the most popular PHP framework doesn't seem to fit those assumptions?
Unmaintainable? To what extent? There are huge projects running Laravel in production — they have been for years? Some people will point towards failed Laravel projects, trying to prove their point. Well, let me show you success stories. Now what? Let me show you some failed Symfony projects. Who's right? Who's wrong?
This kind of mindset isn't uncommon in the programming world: we're used to doing things a certain way, and we're comfortable doing so. Whenever something comes along that doesn't fit our way of doing things, we experience friction. However, instead of questioning ourselves, too often do we point towards the thing that's causing friction, blaming it for everything that's wrong in the world. The safety of the internet makes it even more profound: we can write whatever we want with little to no consequences.
I would say that the inability to self-reflect, to adapt, and to question ourselves, is probably way more harmful than Laravel will ever be.
]]>It looks like someone combined a horse with a giraffe, and kept adding stuff until they met the requirement of "being able to survive in the desert". Who came up with that? Don't get me wrong: camels do work, so they must have gotten something right. But honestly, the finishing touches are far from ideal:
PHP is like a camel: there's little to say when it comes to beauty or elegance, but it does seem to survive many harsh conditions. So, who am I to judge? Coincidentally, PHP was originally inspired by PERL, whose logo is a camel; so maybe there is a deeper connection still? But let's not go too deep down the camel rabbit hole; instead, let's talk about how programming languages are designed.
PHP is one of those programming languages that's not backed by a big company or a group of people whose job it is to work on it. PHP is mostly driven by a group of volunteers. A year ago, we got the PHP Foundation which employs a handful of developers to work on the language, but it's not like they have any ownership. They have to submit their ideas, and are at the mercy of "PHP Internals" to decide what goes in the language, and what not.
So about that group of volunteers: PHP Internals are a group of people who discuss and vote on what gets added into the language. Part of that group consists of core developers, and then there are some prominent PHP community members, release managers, past contributors, docs maintainers, and probably a bunch of other people. We're talking somewhere around 200 members — probably, but there aren't any public numbers, as far as I know.
That group is the perfect example of a committee: they all come together to decide on how PHP should evolve next. They don't have a unified plan or vision, but all of them bring their own agenda. While a core programmer might favor cleaning up PHP's internal code (even when it brings some breaking changes), a prominent community member might push forward ideas that help with the development of a PHP framework or package.
This phenomenon — designing a language with such a large group — is called "design by committee". At first glance, it might sound like a very democratic approach to software design where everyone can pitch an idea, and if enough people vote for it, it gets added. However, because there's such a large diversity of opinions, a committee rarely achieves excellence.
Say you want to add a feature in PHP that benefits you — a prominent framework developer. Then you'll have to convince enough people to vote for it. First, you request Internals to share their comments on your idea — an RFC (request for comments). Naturally, you'll make compromises, trying to get as many people on your side as possible before voting starts. On top of that: people can lobby others to influence their vote and, in the end, the result. Suddenly you're playing a game of politics, instead of proper software design.
And so, design by committee often leads to average results at best; or (in many cases) no result, because people couldn't find consensus. Camels were probably designed by a committee as well. Sure, they work, but they are far from excellence and full of compromise.
The alternative to design by committee is called "design by dictator" or having a "benevolent dictator for life". It might sound rather negative at first, but hear me out. A benevolent dictator (or group of benevolent dictators) don't want to push their own agenda; they want the best for their product. It's their product — they have full ownership. And exactly because it is their product, they will listen to "the masses". Because, their product is nothing, without their people.
In the end though, a dictator will make decisions, even when those decisions aren't agreed upon within the community as a whole. There's much less need for compromise, and so, more room for excellence.
Maybe you are skeptical of the idea? Well, don't take my word for it, there are quite a lot of open source projects applying this technique. You might even recognise a couple:
I think it's fair to say that all of these projects are a huge success. I believe much of their success comes from that one person in charge, leading the way, having a clear vision. These leaders still listen to their audience, and they often allow for voting on features. But, in the end, there's one person carrying the ownership. There's one leader to look up to.
When people trust a benevolent dictator, much more becomes possible than when they settle with a committee.
PHP will never change its leadership model; the committee would have to give up its power which, of course, they don't want to. I think this is one of the reasons I'm excited about the idea of a superset for PHP: it could be an independent project, led by one person or a company, regardless of what the committee wants. If that person or company gained my trust, I would be fine with them make the decisions to move the language forward, rather than settling with average.
]]>Let's take a look!
In an ideal world, we'd be able to clone classes with readonly properties, based on a user-defined set of values. The so called clone with
syntax (which doesn't exist):
readonly class Post
{
public function __construct(
public string $title,
public string $author,
public DateTime $createdAt,
) {}
}
$post = new Post(
title: 'Hello World',
// …
);
// This isn't possible!
$updatedPost = clone $post with {
title: 'Another One!',
};
Reading the title of the current RFC: "Readonly properties can be reinitialized during cloning" — you might think something like clone with
this is now possible. However… it isn't. The RFC allows only allows for one specific operation: to overwrite readonly values in the magic __clone
method:
readonly class Post
{
public function __construct(
public string $title,
public string $author,
public DateTime $createdAt,
) {}
public function __clone()
{
$this->createdAt = new DateTime();
// This is allowed,
// even though `createdAt` is a readonly property.
}
}
Is this useful? It is! Say you want to clone objects with nested objects — a.k.a. making "deep clones"; then this RFC allows you to clone those nested objects as well, and overwrite them in your newly created clone — even when they are readonly properties.
readonly class Post
{
public function __clone()
{
$this->createdAt = clone $this->createdAt;
// Creates a new DateTime object,
// instead of reusing the reference
}
}
Without this RFC, you'd be able to clone $post
, but it would still hold a reference to the original $createdAt
object. Say you'd make changes to that object (which is possible since readonly
only prevents the property assigned from changing, not from its inner values being changed):
$post = new Post(/* … */);
$otherPost = clone $post;
$post->createdAt->add(new DateInterval('P1D'));
$otherPost->createdAt === $post->createdAt; // true :(
Then you'd end up with the $createdAt
date changed on both objects!
Thanks to this RFC, we can make real clones, with all their nested properties cloned as well, even when these properties are readonly:
$post = new Post(/* … */);
$otherPost = clone $post;
$post->createdAt->add(new DateInterval('P1D'));
$otherPost->createdAt === $post->createdAt; // false :)
I think it's good that PHP 8.3 makes it possible deep cloning readonly properties. However, I have mixed feelings about this implementation. Imagine for a second that clone with
existed in PHP, then all of the above would have been unnecessary. Take a look:
// Again, this isn't possible!
$updatedPost = clone $post with {
createdAt: clone $post->createdAt,
};
Now imagine that clone with
gets added in PHP 8.4 — pure speculation, of course. It means we'd have two ways of doing the same thing in PHP. I don't know about you, but I don't like it when languages or frameworks offer several ways of doing the same thing. As far as I'm concerned, that's suboptimal language design at best.
This is, of course, assuming that clone with
would be able to automatically map values to properties without the need of manually implementing mapping logic in __clone
. I'm also assuming that clone with
can deal with property visibility: only able to change public properties from the outside, but able to change protected and private properties when used inside a class.
Awhile ago, I wrote about how PHP internals seem to be divided, one group comes up with one solution, while another group wants to take another approach. To me, it's a clear drawback of designing by committee.
Full disclosure — the RFC mentions clone with
as a future scope:
None of the envisioned ideas for the future collide with the proposals in this RFC. They could thus be considered separately later on.
But I tend to disagree with this statement, at least assuming that clone with
would work without having to implement any userland code. If we'd follow the trend of this current RFC, I could imagine someone suggesting to add clone with
only as a way to pass data into __clone
, and have users deal with it themselves:
readonly class Post
{
public function __clone(...$properties)
{
foreach ($properties as $name => $value) {
$this->$name = $value;
}
}
}
However, I really hope this isn't the way clone with
gets implemented; because you'd have to add a __clone
implementation on every readonly class.
So, assuming the best case where clone with
gets added, and where it is able to automatically map values; then the functionality of this current RFC gets voided, and makes it so that we have two ways of doing the same thing. It will confuse users because it faces them with yet another a decision when coding. I think PHP has grown confusing enough as is, and I'd like to see that change.
On the other hand, I do want to mention that I don't oppose this RFC on its own. I think Nicolas and Máté did a great job coming up with a solid solution to a real-life problem.
PS: in case someone wants to make the argument for the current RFC because you only need to implement __clone
once per object and not worry about it on call-site anymore. There's one very important details missing from these examples in isolation: deep copying doesn't happen with a simple clone
call. Most of the time, packages like deep-copy are used, and thus, the potential overhead that comes with my clone with
example is already taken care off by those packages and don't bother end users.
Today I want to thank Kinsta in particular for stepping in and helping out! They've committed to a generous monthly sponsorship, which is getting me closer to achieving that break-even goal.
If you haven't heard of Kinsta before, they are a cloud platform targeted towards companies and dev teams to help them ship and manage their web projects. What I like about companies like Kinsta is that they are always on the lookout for helping out the community members like they did with my sponsor issue. They contacted me out of the blue saying they wanted to help, which I really appreciate!
So big thanks to Kinsta for helping me work on stitcher.io in my free time!
]]>/-
. You can use it to mark code as comments, but it is scope aware.
For example, you can use it to comment out individual keywords and statements like final
or implements X
:
/-final class Foo /-implements Bar
But you could also use the /--
variant to comment out a whole section based on its scope:
/--public function fromInterface(int $i): int {
return $i + 1;
}
It's just a small thing, but when I saw it, it immediately clicked. Especially in a language like PHP where "dump and die debugging" is the de-facto standard, it would be nice to have a slightly shorter and more convenient way to comment out code.
/-final class Foo /-implements Bar
{
public /-readonly string $prop;
/--public function fromInterface(int $i): int {
return $i + 1;
}
}
]]>I’ve been laughed at and ridiculed. But don’t worry, I won’t give in. I will keep using a light colour scheme.
I know I’m a minority, and people do ask me why I keep using light colours schemes. Well, I’ve got three reasons to do so, and I think they make a pretty good point for everyone to switch.
First, people have been doing research into text readability. Studies show that our eyes can more easily distinguish a dark foreground from a light background, than the other way around. I will leave some links in the footnotes if you want to read about it. I find it a compelling first reason for light colour schemes: they are easier to read.
Second: the majority of websites and UIs are still made with a light colour scheme first, so switching between dark code and light applications becomes tiring after a while, especially if you’re doing it day in, day out. Granted, more and more applications support dark mode, but then we’re back to reason one: they are more difficult to read.
Third, and this is probably the thing most people think about: there’s this misconception that “light colour schemes” hurt your eyes, especially in dark rooms. Let me tell you: sitting in an all dark room programming isn’t good for your eyes anyways, regardless of the colour scheme you’re using. If you feel that a light colour scheme hurts your eyes, it might be better to lower the brightness of your screen and make sure your room is properly lit because — a dark theme isn’t the solution.
When I first learned about the real arguments for light colour schemes, I couldn’t do anything else but to use them. I’ve got decades to go in front of a screen, and I really need to do all I can to make it as easy as possible for my eyes.
Here’s my challenge: use a light colour scheme for a week, but do it in a properly lit room, and with proper screen brightness. Come back here after a week, and let me know your thoughts.
]]>I know it looks strange the first time you see it, but hear me out for a minute: I am a code folder.
class TwitterSyncCommand extends Command
{
protected $signature = 'twitter:sync {--clean}';
/** @var \Illuminate\Database\Eloquent\Collection<Mute> */
private Collection $mutes;
public function handle(Twitter $twitter) { /* … */ }
public function syncFromList(Twitter $twitter): void { /* … */ }
public function syncFromSearch(Twitter $twitter): void { /* … */ }
private function storeTweets(array $tweets, TweetFeedType $feedType): void { /* … */ }
private function shouldBeRejected(Tweet $tweet): ?RejectionReason { /* … */ }
}
I hide most of my code, most of the time. I have keyboard shortcuts to easily show and hide blocks of code; and when I open a file, all method and function bodies are collapsed by default.
The reason? I’m not a superhuman speed reader that understands dozens of lines of code at one glance. And… I also don’t have a two-metre-high screen.
I just can’t read and understand all of this — you know?
class TwitterSyncCommand extends Command
{
protected $signature = 'twitter:sync {--clean}';
/** @var \Illuminate\Database\Eloquent\Collection<Mute> */
private Collection $mutes;
public function handle(Twitter $twitter)
{
$this->mutes = Mute::query()->select('text')->get();
if ($this->option('clean')) {
$this->error('Truncating tweets!');
Tweet::truncate();
}
$this->syncFromSearch($twitter);
$this->syncFromList($twitter);
$this->info('Done');
}
public function syncFromList(Twitter $twitter): void
{
do {
$lastTweet = Tweet::query()
->where('feed_type', TweetFeedType::LIST)
->orderByDesc('tweet_id')
->first();
$tweets = $twitter->request('lists/statuses.json', 'GET', [
'list_id' => config('services.twitter.list_id'),
'since_id' => $lastTweet?->tweet_id,
'count' => 200,
'tweet_mode' => 'extended',
]);
$count = count($tweets);
if ($count === 0) {
$this->comment('No more new tweets');
} else {
$this->comment("Syncing {$count} tweets from list");
$this->storeTweets($tweets, TweetFeedType::LIST);
}
} while ($tweets !== []);
}
public function syncFromSearch(Twitter $twitter): void
{
do {
$lastTweet = Tweet::query()
->where('feed_type', TweetFeedType::SEARCH)
->orderByDesc('tweet_id')
->first();
$tweets = $twitter->request('/search/tweets.json', 'GET', [
'q' => 'phpstorm',
'since_id' => $lastTweet?->tweet_id,
'count' => 200,
'tweet_mode' => 'extended',
])->statuses;
$count = count($tweets);
if ($count === 0) {
$this->comment('No more new tweets');
} else {
$this->comment("Syncing {$count} tweets from search");
$this->storeTweets($tweets, TweetFeedType::SEARCH);
}
} while ($tweets !== []);
}
private function storeTweets(array $tweets, TweetFeedType $feedType): void
{
foreach ($tweets as $tweet) {
$subject = $tweet->retweeted_status ?? $tweet;
$tweet = Tweet::updateOrCreate([
'tweet_id' => $tweet->id,
], [
'state' => TweetState::PENDING,
'feed_type' => $feedType,
'text' => $subject->full_text ,
'user_name' => $subject->user->screen_name,
'retweeted_by_user_name' => isset($tweet->retweeted_status)
/** @phpstan-ignore-next-line */
? $tweet->user->screen_name
: null,
'created_at' => Carbon::make($subject->created_at),
'payload' => json_encode($tweet),
]);
if ($reason = $this->shouldBeRejected($tweet)) {
$tweet->update([
'state' => TweetState::REJECTED,
'rejection_reason' => $reason->message,
]);
}
(new ParseTweetText)($tweet);
}
}
private function shouldBeRejected(Tweet $tweet): ?RejectionReason
{
if ($tweet->isRetweet() && $tweet->feed_type === TweetFeedType::SEARCH) {
return RejectionReason::retweetedFromSearch();
}
// Reject tweets containing a specific word
foreach ($this->mutes as $mute) {
if ($tweet->containsPhrase($mute->text)) {
return RejectionReason::mute($mute->text);
}
}
// Reject replies
if ($tweet->getPayload()->in_reply_to_status_id) {
return RejectionReason::isReply();
}
// Reject mentions
if (str_starts_with($tweet->text, '@')) {
return RejectionReason::isMention();
}
// Reject non-english tweets
$language = $tweet->getPayload()->lang;
if ($language !== 'en') {
return RejectionReason::otherLanguage($language);
}
return null;
}
}
I feel overwhelmed looking at all that code, especially when I’m searching for one specific method.
Now, when I create a new file, it’s all fine, it’s a blank slate, and I’ve got control. But code grows rapidly, and real life projects more often than not include work in existing files instead of blank slates. So I really need a way to reduce cognitive load, I can’t “take it all in at once”. And that’s why I’ve grown to like code folding so much.
Give yourself a week to get used to it. Configure your IDE to automatically fold method bodies and functions, and assign proper key bindings to show and hide blocks. I promise you, you’ll like it.
]]>I'm suspicious of acronyms because there's always someone, somewhere, making compromises to make them work. Whether it is by omitting equally (or more) important elements because they don't fit the acronym; whether less important elements are included to make it work; or whether names are changed, their meaning obfuscated, so that they'd fit perfectly.
Acronyms are of course a great choice to market your ideas because, well, people tend to remember them. SOLID is the perfect example.
]]>I like to think of my code as a book. Not just any book, I think of it as a precious, beautifully designed work of art. Something I want to WANT to read. You know why? Because programming is so much more about reading and understanding code, than it is about writing.
I would say that “writing code” is only the lesser part of my programming life. So naturally, I have much to gain by making the “reading part” as pleasant as possible.
So, let's work from an example:
final class CodeController
{
public function __construct(private MarkdownConverter $markdown) {}
public function __invoke(string $slug)
{
$code = $this->markdown->convert(file_get_contents(__DIR__ . "/code/{$slug}.md"))->getContent();
return view('code', [
'code' => $code,
]);
}
}
First things first, I choose a large font. My brain can only read so many characters per second, so I don’t need to try and fit as much code as possible on screen, at all times.
final class CodeController
{
public function __construct(private MarkdownConverter $markdown) {}
public function __invoke(string $slug)
{
$code = $this->markdown->convert(file_get_contents(__DIR__ . "/code/{$slug}.md"))->getContent();
return view('code', [
'code' => $code,
]);
}
}
I choose a font that’s pleasant to read, modern fonts suit me better than the ones that originated back in the 80s or 90s.
final class CodeController
{
public function __construct(private MarkdownConverter $markdown) {}
public function __invoke(string $slug)
{
$code = $this->markdown->convert(file_get_contents(__DIR__ . "/code/{$slug}.md"))->getContent();
return view('code', [
'code' => $code,
]);
}
}
I increase the line height, because it gives my code some room to breathe, and makes it even easier to read.
final class CodeController
{
public function __construct(private MarkdownConverter $markdown) {}
public function __invoke(string $slug)
{
$code = $this->markdown->convert(file_get_contents(__DIR__ . "/code/{$slug}.md"))->getContent();
return view('code', [
'code' => $code,
]);
}
}
Finally, I make sure that my code isn’t too wide. The less I need to move my eyes from left to right, the easier it is.
final class CodeController
{
public function __construct(
private MarkdownConverter $markdown,
) {}
public function __invoke(string $slug)
{
$path = file_get_contents(__DIR__ . "/code/{$slug}.md");
$code = $this->markdown
->convert($path)
->getContent();
return view('code', [
'code' => $code,
]);
}
}
Looking at typography guidelines, the maximum advised length is somewhere between 60 and 80 characters. I think somewhere between 80 and 100 works well, because code also includes lots of tabs.
Have you considered typography when programming? Give it a try, it’ll make a lasting impression.
]]>"Levenshtein" is the name of an algorithm to determine the difference — aka "distance" — between two strings. The name comes — unsurprisingly — from its inventor: Vladimir Levenshtein.
It's a pretty cool function to determine how similar two related words or phrases are. For example: passing in "PHP is awesome"
twice, will result in a "distance" of 0
:
levenshtein("PHP is awesome", "PHP is awesome"); // 0
However, passing in two different phrases will result in a larger distance:
levenshtein("Dark colour schemes", "are awesome"); // 13
Unsurprisingly, given how incompatible above two statements are 😉
PHP has — believe it or not — a built-in function to determine the date of Easter for any given year. Given that Easter's date is determined by "the first Sunday after the full Moon that occurs on or after the spring equinox", I'm in awe of PHP being able to calculate it for me.
Or maybe it simply is hard coded?
date('Y-m-d', easter_date(2023)); // 2023-04-08
Did you know PHP can be async? CLI versions of PHP have access to the pcntl
functions, including the pcntl_fork
function. This function is basically a wrapper for creating process forks, allowing one PHP process to spawn and manage several!
Here's a simple example using sockets to create an async child process in PHP:
function async(Process $process): Process {
socket_create_pair(AF_UNIX, SOCK_STREAM, 0, $sockets);
[$parentSocket, $childSocket] = $sockets;
if (($pid = pcntl_fork()) == 0) {
socket_close($childSocket);
socket_write($parentSocket, serialize($process->execute()));
socket_close($parentSocket);
exit;
}
socket_close($parentSocket);
return $process
->setStartTime(time())
->setPid($pid)
->setSocket($childSocket);
}
I actually wrote a little package that wraps everything in an easy-to-use API: spatie/async.
Similar to levenshtein
, methaphone
can generate a phonetic representation of a given string:
metaphone("Light color schemes!"); // LFTKLRSXMS
metaphone("Light colour schemes!"); // LFTKLRSXMS
PHP understands DNS, apparently. It has a built-in function called dns_get_record
, which does as its name implies: it gets a DNS record.
dns_get_record("stitcher.io");
{
["host"] => "stitcher.io"
["class"] => "IN"
["ttl"] => 539
["type"] => "NS"
["target"] => "ns1.ichtushosting.com"
}
// …
I mainly wanted to include array_merge_recursive
because, for a long time, I misunderstood what it did. I used to think you'd have to use it for merging multidimensional arrays, but that's not true!
It might be better to let past-me explain it but, in summary, it works like this:
$first = [
'key' => 'original'
];
$second = [
'key' => 'override'
];
array_merge_recursive($first, $second);
{
["key"] => {
"original",
"override",
}
}
PHP has a mail function. A function to send mail. I wouldn't use it, but it's there:
mail(
string $to,
string $subject,
string $message,
array|string $additional_headers = [],
string $additional_params = ""
): bool
Apparently, there's a function in PHP that allows you to dynamically load extensions, while your script is running!
if (! extension_loaded('sqlite')) {
if (strtoupper(substr(PHP_OS, 0, 3)) === 'WIN') {
dl('php_sqlite.dll');
} else {
dl('sqlite.so');
}
}
glob
is a seriously awesome function: it finds pathnames according to a pattern. It's pretty easy to explain, but it's oh so useful:
glob(__DIR__ . '/content/blog/*.md');
glob(__DIR__ . '/content/*/*.md');
{
/path/to/content/blog/foo.md,
/path/to/content/other/bar.md,
…
}
Finally, PHP not only knows about Easter, it also knows about when the sun rises and sets, for any given date! It also requires a longitude and latitude, which of course makes sense because the sunrise and sunset times depend on your location:
date_sun_info(
timestamp: strtotime('2023-01-27'),
latitude: 50.278809,
longitude: 4.286095,
)
{
["sunrise"] => 1674804140
["sunset"] => 1674836923
["transit"] => 1674820532
["civil_twilight_begin"] => 1674802111
["civil_twilight_end"] => 1674838952
["nautical_twilight_begin"] => 1674799738
["nautical_twilight_end"] => 1674841325
["astronomical_twilight_begin"] => 1674797441
["astronomical_twilight_end"] => 1674843622
}
What's your favourite PHP function? Let me know on Twitter!
]]>This is a curly bracket, or brace: {
.
It’s rarely used as a punctuation mark, but it is one of the most used symbols in programming languages, where they are used to group code and create scopes. It’s also one of the more debated topics when it comes to code styles.
The question is simple: should an opening brace go on a new line or not? You might think: it’s all personal preference; but I would say: it’s not.
Take a look at this code snippet:
public function __construct(string $publicDirectory, string $configurationFile, PageParser $pageParser, PageRenderer $pageRenderer) {
// ...
}
It’s a constructor that takes a couple of arguments. So what's wrong with this code? Well first of all, you probably have to scroll sideways to read it. That's a bad thing. Scrolling requires an extra interaction with your code. You'll have to consciously search for information about the arguments of this method. That time distracts you from focusing on the application code.
Second, if you're into web development, you probably know that people don't read text, they scan. Usually from left to right and top to bottom. This is especially true for websites, but the same goes for reading code. Putting important information to the right makes it more difficult to find.
In case of this argument list, all arguments are equally important; yet a lot of useful information is pushed to that right, blurry side, where our focus isn’t by default.
So how do we pull useful information more to the left?
public function __construct(string $publicDirectory,
string $configurationFile,
PageParser $pageParser,
PageRenderer $pageRenderer) {
// ...
}
This could be the first solution you think about. But it doesn't really scale. As soon as you're refactoring a method name, the alignment breaks. Say we want to make this a static constructor instead of a normal one.
public static function create(string $publicDirectory,
string $configurationFile,
PageParser $pageParser,
PageRenderer $pageRenderer) {
// ...
}
See the alignment breaking?
Another issue is that arguments are still pushed rather far to the right; so let's take a look at another approach.
public function __construct(
string $publicDirectory, string $configurationFile,
PageParser $pageParser, PageRenderer $pageRenderer) {
// ...
}
The advantage here is that our alignment issue is solved. However, how will you decide how many arguments should go on one line? Will you make some styling guidelines about this? How will you enforce them? This example has four arguments, but what if it had three or five?
public function __construct(
string $publicDirectory, string $configurationFile,
string $cachePath, PageParser $pageParser,
PageRenderer $pageRenderer) {
// ...
}
Consistency is key. If we can find a consistent rule, we don't have to think about it anymore. And like I said before, if we don't have to think about it, there's room in our heads for more important things.
So let's continue our search for consistency.
public function __construct(
string $publicDirectory,
string $configurationFile,
PageParser $pageParser,
PageRenderer $pageRenderer) {
$this->publicDirectory = rtrim($publicDirectory, '/');
$this->configurationFile = $configurationFile;
$this->pageParser = $pageParser;
$this->pageRenderer = $pageRenderer;
}
By giving each argument its own line, we solve all our problems. But we also created a new one: it's now more difficult to distinguish between the argument list and the method body.
I can illustrate it for you. Let's replace all characters in this code with X's:
XXXXXX XXXXXXXX __XXXXXXXXX(
XXXXXX XXXXXXXXXXXXXXXX,
XXXXXX XXXXXXXXXXXXXXXXXX,
XXXXXXXXXX XXXXXXXXXXX,
XXXXXXXXXXXX XXXXXXXXXXXXX) {
XXXXXXXXXXXXXXXXXXXXXX = XXXXXXXXXXXXXXXXXXXXXXXXXXXX;
XXXXXXXXXXXXXXXXXXXXXXXX = XXXXXXXXXXXXXXXXXX;
XXXXXXXXXXXXXXXXX = XXXXXXXXXXX;
XXXXXXXXXXXXXXXXXXX = XXXXXXXXXXXXX;
}
Can you see how difficult it becomes to spot where the argument list ends and the method body starts?
You might say "there's still the curly bracket on the right indicating the end". But that’s not where our focus is! We want to keep the important information to the left. How do we solve it? It turns out there is one true place where to put your curly brackets:
XXXXXX XXXXXXXX __XXXXXXXXX(
XXXXXX XXXXXXXXXXXXXXXX,
XXXXXX XXXXXXXXXXXXXXXXXX,
XXXXXXXXXX XXXXXXXXXXX,
XXXXXXXXXXXX XXXXXXXXXXXXX
) {
XXXXXXXXXXXXXXXXXXXXXX = XXXXXXXXXXXXXXXXXXXXXXXXXXXX;
XXXXXXXXXXXXXXXXXXXXXXXX = XXXXXXXXXXXXXXXXXX;
XXXXXXXXXXXXXXXXX = XXXXXXXXXXX;
XXXXXXXXXXXXXXXXXXX = XXXXXXXXXXXXX;
}
On a new line. Placing curly brackets on new lines gives our code space to breathe. It creates a visual boundary between argument lists and method bodies, it helps us to focus on things that matter.
public function __construct(
string $publicDirectory,
string $configurationFile,
PageParser $pageParser,
PageRenderer $pageRenderer
) {
$this->publicDirectory = rtrim($publicDirectory, '/');
$this->configurationFile = $configurationFile;
$this->pageParser = $pageParser;
$this->pageRenderer = $pageRenderer;
}
]]>I work on stitcher.io and my newsletters solely in my free time. I used to have a large company sponsoring me for two years, but that partnership has ended. I'm now looking into other ways to get stitcher.io to break even again, and one of the options is to try out GitHub sponsors. If you're a regular reader and if my content helps you, I kindly want to ask you to consider sponsoring me on GitHub.
Finally, if you're a company looking into dedicated ad placements on my blog or newsletters, you can contact me via email.
I've been working on stitcher.io as a hobby project for over five years now, and I've been enjoying it tremendously. In the beginning, stitcher.io was just a static blog that I hosted on a cheap $5 digital ocean droplet (in fact, my previous employer provided it for free, thanks Spatie!).
Over time though, costs began to grow. I got a long-term sponsor contract, meaning I had a couple of hundred euros extra monthly income. It also meant I was now legally required to start a side business in my name, which of course, increased my monthly costs: paying an accountant, social security fees, paying taxes on that newly created income, …
I also started sending out a regular newsletter, meaning I had to upscale my cheap droplet, had to deal with mailing providers, etc. I managed to always break even thanks to that long-term sponsor contract, but I always knew that if they decided to pull the plug, I'd have to search for other solutions. I am still running carbon ads, but honestly, they earn too little to rely on: $50 a month on average.
So here we are: I've got a monthly cost of around 250 euros for hosting, mailing, accounting, and software, which doesn't take into account any yearly or biyearly costs such as social security costs, taxes, domain names, and of course all the time I spend on it.
Now, to be clear: I don't want to turn my blog and newsletters into a profitable business. And I will keep working on it regardless of whether it's making any profit. Though, it would be nice to be able to break even again. I also did consider other ways to finance this project, like, for example, by writing a new book, but I have to admit that my family situation right now (2 kids, a third is on the way) doesn't permit such a time investment in something that I don't know will work.
So, long story short: stitcher.io right now is making a loss, I'm fine with it, but I would like to see it break even. I feel a bit awkward writing about it, but on the other hand: lots of other people do it, so I figured I'd give it a chance. If you benefit from my work and want to help out on a one-time or recurring basis, you can check out my GitHub Sponsors page. If you're a company looking into dedicated ad placements on my blog or newsletters, you can contact me via email.
Thanks!
]]>Do you want to know something dangerous?
Habits.
Not the kind where you practise long and hard to make something a habit. No, I’m talking about the kind where you’ve taught yourself something without noticing. The kind of habit that has been “there” for years, but you can’t remember how it came to be.
I’m not impeccable myself. I have several such habits, and I even have a couple that I know are wrong and should be changed.
For example: I use spaces instead of tabs for indentation.
We could talk about preference all day, but in the end we should ask: what are the rational arguments for tabs and spaces?
For tabs, there’s one big argument: accessibility. First and foremost, the size of a tab is configurable, it doesn’t have to be a fixed width of x spaces. Say there are two visually impaired people working on the same codebase. One person has to use a really large font size, so their tab length should be shorter than the default; and the other one prefers much larger tabs on a very wide monitor so that the difference between nested levels of code is more clear. Just like colour schemes or fonts, each developer can choose what style fits them best.
On top of that: blind people also code. They often use braille displays. These displays show a fixed number of characters, and every space wastes one. Say you’ve got 4 spaces per indentation level, and want to read something that’s nested 3 levels deep; then we’re talking about 12 braille characters being wasted on an already limited display. Compare that to only three when you’d use tabs.
But let’s not jump to early conclusions and look at the rational arguments for using spaces as well: Someone decided it would be the best. That’s it. “Consistency” is often coined as an argument because different editors might show different tab lengths, and some programmers… don’t like that?
A huge part of the programming community — including myself — has gotten used to spaces. Not because there are any good arguments for them, but because it has grown into a habit — a habit so big that popular code styles force users to write spaces, without any good reason.
So maybe we should rethink that habit? I would say that accessibility and writing more inclusive code is a very good argument. But unfortunately, as we all know: breaking habits is hard.
]]>It's a language that has seen an astonishing transformation over the course of almost three decades. Even within the last 10 years, PHP has transformed in ways we couldn't imagine.
Every year, I write a post about the current state of PHP, where I look back and forward. Let's begin!
I usually start these posts with a summary of the latest PHP version, but this time I want focus on the PHP Foundation first. It's been a little over a year since the foundation was created: a collective of 10 volunteers and 6 developers being paid to work on PHP, the language.
Last year, I wrote this:
I'm a little worried now that Nikita has stepped down. He's definitely not the only person capable of working on PHP's core, but he did a tremendous amount of work these past years with PHP 8.0 and 8.1. I hope the PHP Foundation will be up to speed soon and that there are enough core developers who have time to work on PHP next year. PHP 8.2 is already in development, although there aren't many RFCs drafted yet.
I don't think 2022 will be the most mind blowing year for PHP, but rather a year of adding stability. Nothing wrong with that.
I think it's fair to say that the Foundation has delivered. They recently published their 2022 report, and it shows some pretty impressive numbers:
I think the Foundation is one of the best things to happen to PHP in a long time, and I hope they'll be able to improve the language even more in 2023. If you're working at a company that uses PHP, I'd highly recommend you consider donating.
Moving on to PHP 8.2. It's generally regarded as a smaller release, but nevertheless it has a bunch of nice features. Just to name a couple:
readonly class PostData
{
public function __construct(
public string $title,
public string $author,
public string $body,
public DateTimeImmutable $createdAt,
public PostState $state,
) {}
}
A brand new randomizer:
$rng = $is_production
? new Random\Engine\Secure()
: new Random\Engine\Mt19937(1234);
$randomizer = new Random\Randomizer($rng);
$randomizer->shuffleString('foobar');
Standalone null
, true
and false
:
function alwaysFalse(): false
{
return false;
}
Disjunctive normal form types:
function generateSlug((HasTitle&HasId)|null $post)
{ /* … */ }
Redacted parameters:
function connect(
string $user,
#[\SensitiveParameter] string $password
) {
// …
}
It's kind of crazy to realise how much PHP has evolved over the years. I made a little video comparison that clearly shows the difference:
Just like every year, I should mention Packagist, PHP's package manager: it now lists 361,000 packages; 60,000 more than last year:
One impressive number is the total amount of installations. Last year I mentioned this:
Oh, by the way, just recently Packagist passed the milestone of having handled over more than 50 billion installs. Congrats Packagist!
I just checked, and we're now at 74,492,061,634 installs. That's an increase of 24 billion installs in one year, 2 billion installs per month. All of that to say: the PHP ecosystem is growing a lot.
Twice a year, I publish my version stats post. In these posts, I analyze PHP version usage across the community based on Packagist's data. I wanted to share a graph from that post again: the timeline between 2013 and now, showing the per-version usage history.
While it's great to see PHP 8.* usage rise steeply, there's also a big chunk of people still stuck on older, slow and insecure PHP versions. My hope for 2023 is to see those old version numbers decline even more rapidly. I wrote it like this in my version stats post:
This data beautifully visualizes the division within the PHP community: one part is keeping up with modern PHP, while another one stays helplessly behind.
Speaking of upgrades, I want to mention one tool in particular: Rector. Rector is a free automation tool that helps upgrade your PHP codebase. All it takes is a tiny amount of configuration, and it'll do a huge amount of work for you.
I recently used it to update my community-driven content aggregator, Aggregate to PHP 8.2, and it was really fun and easy to use.
When, after publishing my version stats post, several people told me they hadn't updated yet and were stuck on PHP 7.*, I asked them why. They told me it was simply too much manual work. Interestingly enough, no one had even tried to use tools like Rector to help them…
I firmly believe that a "programming language" is so much more than a compiler: it's the tools and ecosystem that play an equal part in defining that "programming language", and I really think lots of people, projects and businesses would benefit if they looked into using automation tools like Rector.
Since I'm talking about the ecosystem, I can't go without mentioning PHP's two largest frameworks: Laravel and Symfony.
Over the past years, Laravel has grown tremendously. They now employ 8 full time developers to work on the framework and its ecosystem. On top of that, JetBrains' dev survey reports that 67% of PHP developers work with Laravel.
While Symfony as a framework might be less popular compared to Laravel these days, it's still one of the most mature and stable frameworks within the PHP community. It's more often used for enterprise app development, but its standalone components are popular throughout the whole PHP ecosystem — Laravel also has a couple of dependencies on Symfony components. No surprise that more than a handful of Symfony packages make it into Packagist's top package list.
I should also mention WordPress. I'll be honest, I have a love/hate relationship with it. As a user, WordPress is great. It's so easy to install and use, and I think it has earned every bit of its popularity over the years. As a developer though, WordPress makes me sad. The inability to stay up to date with modern and safe PHP versions casts a shadow on the whole PHP community.
Right now, WordPress only has beta support for PHP 8.0. Now, to be clear: PHP 8.0 was released in 2020, and is now end of life, three years later — and WordPress doesn't yet support it…
Of course, there are reasons for not properly supporting newer PHP versions. Up to you to decide whether they are good or not. My personal opinion is that the decision to hold on to backwards compatibility as much as WordPress does is mostly business driven: a big part of WordPress is the commercial part, and a big part of their customer base is running old PHP versions. It's a vicious circle where both parties are holding each other back and, by extent, hold back the PHP community as a whole.
On the other hand, we should recognise the fact that not many software projects are able to stay as popular and relevant as WordPress after almost 20 years, so maybe their strategy about backwards compatibility is the right one?
Finally, I can't help but mention my long-term dream for PHP. I write about it, I speak about it, and I hope that one day it'll become true: a superset of PHP, with proper IDE and static analyser support.
There are a bunch of reasons why I want it to happen, you can read and hear about them if you want to; but I really hope it'll become a reality. I doubt we'll get to see a widely accepted and supported superset in 2023, but some baby steps are already being made. I'm definitely keeping a close eye on PXP, which might take things in the right direction.
With all of that being said, I hope that you'll enjoy 2023! And just in case you're new here: I'm Brent, developer advocate at JetBrains, I write and vlog about PHP; and I'd really appreciate it if you checked out the YouTube channel I've been working on lately. Take a look, and maybe consider subscribing? Thanks!
If you're not into videos but still want to follow me, you can join 15k newsletter subscribers instead, I hope to see you there!
]]>As always, it's important to note that I'm working with the data available to us. That means that these charts are not a 100% accurate representation of the PHP community as a whole, but they are an accurate representation of one of the most prominent parts of PHP: the packagist ecosystem.
Let's start with the percentage of PHP versions being used today, and compare it to the previous three editions, note that I've omitted all versions that don't have more than 1% usage:
Version | 2021-07 | 2022-01 | 2022-07 | 2023-01 |
8.2 | 0.0% | 0.0% | 0.0% | 4.7% |
8.1 | 0.1% | 9.1% | 24.5% | 38.8% |
8.0 | 14.7% | 23.9% | 20.6% | 16.2% |
7.4 | 46.8% | 43.9% | 38.4% | 27.7% |
7.3 | 19.2% | 12.0% | 8.0% | 5.3% |
7.2 | 10.4% | 6.6% | 5.1% | 4.3% |
7.1 | 3.8% | 2.4% | 1.9% | 1.8% |
Visualizing this data looks like this:
We can see a decent growth for PHP 8.* versions, I think that's great news! It's also great to see PHP 8.0 usage already declining: PHP 8.0 went into security fixes only mode at the end of last year, and I hope to see its usage decline a lot more this year. Keep in mind that PHP 8.0 will reach end of life on November 26, 2023. So it's crucial that projects start preparing to upgrade in the coming months.
PHP 7.4 reached end of life last year, so at the same time it's worrying that more than 25% of projects are still using it! Let's hope to see this number decline soon.
This data beautifully visualizes the division within the PHP community: one part is keeping up with modern PHP, while another one stays helplessly behind. I know there are many reasons to stay behind — often driven by business requirements and constraints — but it's crucial to realise that a lot of PHP projects are in fact running insecure and slow versions in production because of it.
Moving on to the all-time overview chart, here you can see the evolution of version usage across time:
Like I said: the decline of 7.4 is going too slow to my liking. Compare it to the much steeper decline of PHP 5.5 back in 2015 when PHP 7.0 was released: I would have liked to see the same happen with PHP 7.4 and have people move to PHP 8.0, but unfortunately that's not the case.
I feel like I'm repeating myself every year, but I really hope that people upgrade their projects sooner in the future. I'm curious to learn how this part of the PHP community can be helped. I feel that tools like Rector to automate upgrades have so much potential, if only people started using it.
Next, I used Nikita's popular package analyzer to download the 1000 most popular composer packages. I used a little script to get their minimum required version. Here are the results:
Version | 2021-07 | 2022-01 | 2022-07 | 2023-01 |
8.2 | - | - | - | - |
8.1 | - | - | 125 | 129 |
8.0 | 117 | 160 | 94 | 103 |
7.4 | 56 | 69 | 86 | 98 |
7.3 | 133 | 116 | 104 | 106 |
7.2 | 142 | 133 | 130 | 144 |
7.1 | 182 | 190 | 153 | 159 |
7.0 | 31 | 29 | 29 | 30 |
5.6 | 61 | 49 | 42 | 43 |
5.5 | 43 | 42 | 35 | 37 |
5.4 | 41 | 43 | 40 | 40 |
5.3 | 97 | 83 | 77 | 78 |
5.2 | 12 | 10 | 10 | 10 |
There are two important notes to make here.
Instead of comparing absolute numbers, it's best to plot this data into a chart for a relative comparison, so that we can see changes over time:
Minimal PHP requirement over time
You can see there's a slight increase in PHP 7.* requirements. It's a good evolution, but still very slow compared to how fast PHP is moving forward.
In my opinion, package authors should push more to require only supported PHP versions. I think it's the only way for PHP to keep moving forward at a decent rate, and for the community to keep up. On top of that: yearly upgrades are much easier to do than to stay on, for example, PHP 7.4 and try to make the jump directly to PHP 8.2.
In closing, if you take one thing away from this post, I hope it's that it's time to upgrade to at least PHP 8.1, preferably PHP 8.2. It's not as difficult as you might think, and it's definitely worth your time.
What are your thoughts on these stats? Are you using PHP 8.2? Let me know your thoughts on Twitter and subscribe to my newsletter if you want to be kept up-to-date about these posts!
Let's set aside all practical concerns for a moment — it's Christmas, after all. If you could choose — freely choose: what would you change about PHP?
Would you want generics or the pipe operator? Maybe you'd like to see consistent function signatures or get rid of the dollar sign. Type aliases, scalar objects, autoloading for namespaced functions, improved performance, less breaking changes, more breaking changes — the list goes on.
But what if I told you, you had to pick one, and only one. What would it be?
My number one feature isn't in this list. Even worse: my number one wish for PHP will probably never happen. But like I said at the beginning: let's set aside all practical concerns. Let's dream for a moment, not because we believe all dreams come true; but because dreams, in themselves, are valuable and give hope — it is Christmas, after all.
Let me tell you the story of another programming language. A language that, just like PHP, had been the underdog for decades. It didn't have sexy syntax, it was a poor performer, it had messy methods and confusing classes. It was… JavaScript.
It was a language that — legend says — was written in two weeks. Yet it grew to be the most popular programming language people had ever seen, almost by accident.
Indeed, for years, developers were stuck with JavaScript: it was the only option to do any kind of frontend programming at all. And despite everyone mocking poor JavaScript, there simply were no other alternatives, so they had to use it.
That is: until a bunch of smart people started thinking out of the box. The idea was simple: what if we don't write JavaScript anymore, but instead write a program in another language, and convert that code to JavaScript? That way our programs could still work in the browser, but we didn't have to put up with JavaScript's ugliness and quirkiness.
And so history was written: we compiled languages like C to a subset of JavaScript — an optimised subset that was much more performant. It was called asm.js. It allowed us to run existing game engines in the browser. We compiled JavaScript to JavaScript with Babel: newer syntax that wasn't available in browsers was "transpiled" to older JavaScript versions. Supersets like CoffeeScript came into existence. They added better and more convenient syntax.
More and more, the JavaScript community transformed into a host of languages, with JavaScript simply being a compilation target.
Sure, compiling JavaScript meant adding a build step, but it seemed like developers got used to it. First and foremost: build steps were optimised for performance, and second: you could suddenly add a lot of static functionality to your language: tasks that were performed only when compiling your program but not while running it. TypeScript was created, and static type checking became a thing. It turned out: computers were awfully good at detecting mistakes, as long as we provide them with just enough information — types.
And everyone marvelled at JavaScript: it grew from a small frontend scripting language to the number 1 programming language in the world.
Let's talk about PHP. I've been writing it for more than a decade now. I love the PHP community, I love the ecosystem, I love how the language has managed to evolve over the years. At the same time I believe there's room for PHP to grow — lots of room. And so I dream.
I dream of a world where PHP follows in JavaScript footsteps. Where it becomes more than just PHP. I dream of a TypeScript for PHP: a language that's still 100% compatible with all PHP code out there, but one that adds generics and proper static type checking — without having to worry about runtime performance costs. I dream of a language that has all (or most) modern-day language features, that are compiled to plain, old, boring — but working — PHP.
But dreams seldom come true.
Someone once said: "For JavaScript to become as popular as it is today, there had to be two things: it had to suck, and it had to be the only viable option available". And here's the thing: unlike JavaScript, PHP isn't the only option available for backend programming. I also dare to say that it's way better than JavaScript back in the day. These two main components that were needed for JavaScript to grow into something more than itself, aren't equally present in PHP.
And that's ok. It means my dream is probably unrealistic, but it also means something much more important.
My realisation recently is that PHP is already awesome. People are already building great things with it. Sure, maybe PHP is boring compared to the latest and greatest programming languages, and sure you might need to use another language if you're building something for those 0.1% of edge cases that need insane performance. My dream of a superset of PHP might be one of many approaches, but it sure isn't the only viable path forward.
Even without that dream of mine: PHP is doing great. It's not because of how its designed, it's not because of its syntax. It's not because of its top-notch performance and it's not because of its incredible type system. It's because people like you are building amazing things with it. Whether you're using PHP 8.2 or not; whether your running it serverless or asynchronous or not; whether you're writing OOP, FP, DDD, ES, CQRS, Serverless or whatever term you want to throw at it — you are building awesome things. It turns out a language is rarely the bottleneck, because it's merely a tool. But "PHP" is so much more than just a language, it's so much more than just a tool. That's because of you.
Thank you for being part of what makes PHP great. Happy holidays.
]]>I've never felt any regrets for that decision. On the contrary: I believe that if I stuck with Medium, I would have never been able to grow my blog to where it is today. It's because of my blog that I was able to join my current and previous job — it's safe to say this has been life defining for me and my family. Full control meant taking ownership and responsibility, I started caring for my content in another way and that paid off.
It's been five years since I took ownership of my content, but I didn't do the same with my audience. My audience is spread across different places on the web (Reddit, RSS, other blogs, Twitter, HackerNews, …), with Twitter being the largest by far.
With all the Twitter drama these past weeks, I realised how dependent I am — once again — on a platform that's totally out of my control.
I've managed to build a following of 15k people on Twitter. That's small for many people but it's larger than what I imagined a couple of years ago. And while I have 15k followers (that's maybe 1k of real people actually seeing my tweets), if Twitter shuts down tomorrow, those people are gone. To me that would mean all the time and effort I put into my Twitter audience will simply be… gone.
Now, I'm not making any predictions about Twitter's future, and I don't believe anyone talking about it actually has a clue. But, I also won't move to the next platform anymore. At least not for building an audience like I did on Twitter. Instead, I've managed to build a decent newsletter audience of 17k people by now. That are around 5k actual readers, despite double opt-in (email is weird 🙄). But at least it's in my control: my newsletter is self hosted and I keep full control over my audience. No one can pull the plug but me.
Now the Mastodon fans will tell me how Mastodon is decentralized and safe and how they are in full control, but that's simply not true. You're not owning your Mastodon servers — at least not the ones that are actually large enough to matter: phpc.social for example (that's where my audience is), is hosted on masto.host.
Remember how WhatsApp was considered the "safe and encrypted messaging app" once? Remember how everyone and their mother encouraged you to migrate to Signal or Telegram a couple of years later, which also turned out to have privacy and security flaws on their own? You're never in control.
I'm not saying I won't use any of those platforms — if Twitter goes down I probably will join the PHP community on Mastodon. But I won't build and rely on it like I did with Twitter. I'll take full control of my audience from this point forward, meaning you can either follow me via my newsletter or via RSS, whatever works best for you.
]]>Readonly properties were introduced in PHP 8.1. This RFC builds on top of them, and adds syntactic sugar to make all class properties readonly at once. Instead of writing this:
class Post
{
public function __construct(
public readonly string $title,
public readonly Author $author,
public readonly string $body,
public readonly DateTime $publishedAt,
) {}
}
You can now write this:
readonly class Post
{
public function __construct(
public string $title,
public Author $author,
public string $body,
public DateTime $publishedAt,
) {}
}
Functionally, making a class readonly is entirely the same as making every property readonly; but it will also prevent dynamic properties being added on a class:
$post = new Post(/* … */);
$post->unknown = 'wrong';
Uncaught Error: Cannot create dynamic property Post::$unknown
Note that you can only extend from readonly classes if the child class is readonly as well.
PHP has changed quite a lot, and readonly classes are a welcome addition. You can take a look at my video about PHP's evolution if you want to as well:
I'd say this is a change for the better, but it will hurt a little bit. Dynamic properties are deprecated in PHP 8.2, and will throw an ErrorException
in PHP 9.0:
class Post
{
public string $title;
}
// …
$post->name = 'Name';
Keep in mind that classes implementing __get
and __set
will still work as intended:
class Post
{
private array $properties = [];
public function __set(string $name, mixed $value): void
{
$this->properties[$name] = $value;
}
}
// …
$post->name = 'Name';
If you want to learn more about why deprecations are useful and how to deal with them, you can read this followup post on how to deal with deprecations, or you can check out my vlog:
PHP 8.2 adds a new random number generator that fixes a lot of problems with the previous one: it’s more performant, more secure, it’s easier to maintain, and doesn’t rely on global state; eliminating a range of difficult to detect bugs when using PHP’s random functions.
There’s a new class called Randomizer
, which accepts a randomizer engine. Now you can change that engine, depending on your needs. For example, to differentiate between a production and testing environment.
$rng = $is_production
? new Random\Engine\Secure()
: new Random\Engine\Mt19937(1234);
$randomizer = new Random\Randomizer($rng);
$randomizer->shuffleString('foobar');
null
, true
, and false
as standalone types RFCPHP 8.2 adds three new types — or something that looks like it. We'll avoid going down the rabbit hole of type safety in this post, but technically null
, true
, and false
could be considered valid types on their own. Common examples are PHP's built-in functions, where false
is used as the return type for when an error occurs. For example in file_get_contents
:
file_get_contents(/* … */): string|false
Before PHP 8.2, you could already use false
together with other types as a union; but now it can be used as a standalone type as well:
function alwaysFalse(): false
{
return false;
}
The same now also goes for true
and null
.
DNF types allow us to combine union and intersection types, following a strict rule: when combining union and intersection types, intersection types must be grouped with brackets. In practice, that looks like this:
function generateSlug((HasTitle&HasId)|null $post)
{
if ($post === null) {
return '';
}
return
strtolower($post->getTitle())
. $post->getId();
}
In this case, (HasTitle&HasId)|null
is the DNF type.
It's a nice addition, especially since it means that we can now have nullable intersection types, which is probably the most important use case for this feature.
You can now use constants in traits:
trait Foo
{
public const CONSTANT = 1;
public function bar(): int
{
return self::CONSTANT;
}
}
You won't be able to access the constant via the trait's name, either from outside the trait, or from inside it.
trait Foo
{
public const CONSTANT = 1;
public function bar(): int
{
return Foo::CONSTANT;
}
}
Foo::CONSTANT;
You can however access the constant via the class that uses the trait, given that it's public:
class MyClass
{
use Foo;
}
MyClass::CONSTANT; // 1
A common practice in any codebase is to send production errors to a service that keeps track of them, and will notify developers when something goes wrong. This practice often involves sending stack traces over the wire to a third party service. There are cases however where those stack traces can include sensitive information such as environment variables, passwords or usernames.
PHP 8.2 allows you to mark such "sensitive parameters" with an attribute, so that you don't need to worry about them being listed in your stack traces when something goes wrong. Here's an example from the RFC:
function login(
string $user,
#[\SensitiveParameter] string $password
) {
// …
throw new Exception('Error');
}
login('root', 'root');
Fatal error: Uncaught Exception: Error in login.php:8
Stack trace:
#0 login.php(11): login('root', Object(SensitiveParameterValue))
#1 {main}
thrown in login.php on line 8
From the RFC:
This RFC proposes to allow the use of
->
/?->
to fetch properties of enums in constant expressions. The primary motivation for this change is to allow fetching the name and value properties in places where enum objects aren't allowed, like array keys
That means that the following code is now valid:
enum A: string
{
case B = 'B';
const C = [self::B->value => self::B];
}
DateTime::createFromImmutable()
and DateTimeImmutable::createFromMutable()
breakingPreviously, these methods looked like this:
DateTime::createFromImmutable(): DateTime
DateTimeImmutable::createFromMutable(): DateTimeImmutable
In PHP 8.2 those method signatures are changed like so:
DateTime::createFromImmutable(): static
DateTimeImmutable::createFromMutable(): static
This change makes a lot more sense, as it improves static insight possibilities for classes extending from DateTime
and DateTimeImmutable
. However, technically, this is a breaking change that might affect custom implementations that extend from either of those two classes.
utf8_encode()
and utf8_decode()
deprecations RFCIn PHP 8.2, using either utf8_encode()
or utf8_decode()
will trigger these deprecation notices:
Deprecated: Function utf8_encode() is deprecated
Deprecated: Function utf8_decode() is deprecated
The RFC argues that these functions have a inaccurate name that often causes confusion: these functions only convert between ISO-8859-1
and UTF-8
, while the function name suggest a more broader use. There's a more detailed explanation about the reasoning in the RFC.
The alternative? The RFC suggests using mb_convert_encoding()
instead.
strtolower()
and strtoupper()
breaking RFCBoth strtolower()
and strtoupper()
are no longer locale-sensitive. You can use mb_strtolower()
if you want localized case conversion.
Several methods of SPL classes have been changed to properly enforce their correct type signature:
SplFileInfo::_bad_state_ex()
SplFileObject::getCsvControl()
SplFileObject::fflush()
SplFileObject::ftell()
SplFileObject::fgetc()
SplFileObject::fpassthru()
SplFileObject::hasChildren()
SplFileObject::getChildren()
n
modifier in PCRE upgradingYou can now use the n
modifier (NO_AUTO_CAPTURE
) in pcre*
functions.
From the UPGRADING guide:
The
ODBC
extension now escapes the username and password for the case when both a connection string and username/password are passed, and the string must be appended to.
The same applies to PDO_ODBC
.
${}
string interpolation RFCPHP has several ways of embedding variables in strings. This RFC deprecates two ways of doing so, since they are rarely used, and often lead to confusion:
"Hello ${world}";
Deprecated: Using ${} in strings is deprecated
"Hello ${(world)}";
Deprecated: Using ${} (variable variables) in strings is deprecated
To be clear: the two popular ways of string interpolation still work:
"Hello {$world}";
"Hello $world";
Another change, although one with a slightly smaller impact, is that partially supported callables are now deprecated as well. Partially supported callables are callables which can be called using call_user_func($callable)
, but not by calling $callable()
directly. The list of these kinds of callables is rather short, by the way:
"self::method"
"parent::method"
"static::method"
["self", "method"]
["parent", "method"]
["static", "method"]
["Foo", "Bar::method"]
[new Foo, "Bar::method"]
The reason for doing this? It's a step in the right direction towards being able to use callable
for typed properties. Nikita explains it very well in the RFC:
all of these callables are context-dependent. The method that "self::method" refers to depends on which class the call or callability check is performed from. In practice, this usually also holds for the last two cases, when used in the form of [new Foo, "parent::method"].
Reducing the context-dependence of callables is the secondary goal of this RFC. After this RFC, the only scope-dependence still left is method visibility: "Foo::bar" may be visible in one scope, but not another. If callables were to be limited to public methods in the future (while private methods would have to use first-class callables or Closure::fromCallable() to be made scope-independent), then the callable type would become well-defined and could be used as a property type. However, changes to visibility handling are not proposed as part of this RFC.
That's all there is for now, I'll keep this list updated throughout the year. You can subscribe to my newsletter if you want to receive occasional updates!
]]>Today, these two blend together! My personal channel will now be known as "PHP Annotated", and I'll be able to work on it as part of my job!
I'm really excited about this, especially since the goal of the channel will stay exactly the same: creating quality content, focussed on the PHP community. So don't worry: the channel won't become a "marketing channel", it'll still be me, doing the same things I was already doing — but now more and better!
If you're curious what I've been doing, you can watch my brand new "What's new in PHP 8.2" video right now!
I hope you're excited, and would love for you to spread the word! Also make sure to subscribe if you want to be kept in the loop. If you've got any questions, you can reach me — as always — via Twitter or e-mail.
]]>Back then, PHP 7.3 was just around the corner and the package started out as a way to add complex runtime type checks for class properties. It gave programmers certainty about whether they were actually dealing with the right data in a typed way:
class PostData extends DataTransferObject
{
/**
* Built in types:
*
* @var string
*/
public $property;
/**
* Classes with their FQCN:
*
* @var \App\Models\Author
*/
public $property;
/**
* Lists of types:
*
* @var \App\Models\Author[]
*/
public $property;
/**
* Iterator of types:
*
* @var iterator<\App\Models\Author>
*/
public $property;
/**
* Union types:
*
* @var string|int
*/
public $property;
/**
* Nullable types:
*
* @var string|null
*/
public $property;
/**
* Mixed types:
*
* @var mixed|null
*/
public $property;
/**
* Any iterator:
*
* @var iterator
*/
public $property;
/**
* No type, which allows everything
*/
public $property;
}
Fast forward a year to PHP 7.4, and typed properties were added. From the very start I said that one of the package's goal was to become obsolete in the future, and it seemed like we were one step closer to achieving it.
However, another (accidental) feature of the package started to gain popularity: the ability to cast raw data into nested DTOs. So a DTO like this:
class PostData extends DataTransferObject
{
public AuthorData $author;
}
Could be created from this input:
$postData = new PostData([
'author' => [
'name' => 'Foo',
],
]);
So while typed properties were now a thing, we decided to continue the package, albeit in a slightly other form.
Next came along PHP 8.0 with attributes and named properties, allowing for even more functionality to be added:
class MyDTO extends DataTransferObject
{
public OtherDTO $otherDTO;
public OtherDTOCollection $collection;
#[CastWith(ComplexObjectCaster::class)]
public ComplexObject $complexObject;
public ComplexObjectWithCast $complexObjectWithCast;
#[NumberBetween(1, 100)]
public int $a;
#[MapFrom('address.city')]
public string $city;
}
At this point though, we were far away from the problem the package initially set out to solve. It did no more runtime type checking: in part because of PHP's improved type system, in part because I believe static analysis is a much better approach to solving type-related problems these days.
On top of that, there are better solutions to "data mapping" than what this package does: there's spatie/laravel-data with an incredible Laravel-specific approach to mapping data between requests, databases, views, etc; there's cuyz/valinor which offers much more functionality than our package; and there is symfony/serializer which is a little more bare-bones, but more powerful as well.
And so, the question that has been on my mind for two years, has been answered: it is time to deprecate spatie/data-transfer-object
.
I of course discussed the matter with my ex-colleagues at Spatie, as well as with Aidan who has helped maintaining the package for a couple of years now. We all agreed that this is a good time:
Now, keep in mind that a deprecation doesn't mean the package is gone! The code is still here for you to use, and I don't foresee any issue for the near future.
If you were to have any serious concerns though: don't hesitate to let me know on Twitter!
]]>Start by making sure brew is up-to-date:
brew update
Next, upgrade PHP. You can either use the built-in php recipe, but I recommend to use the shivammathur/homebrew-php
tap.
brew upgrade php
shivammathur/homebrew-php
brew tap shivammathur/php
brew install shivammathur/php/php@8.2
To switch between versions, use the following command:
brew link --overwrite --force php@8.2
You can read more in the repository.
Check the current version by running php -v
:
php -v
Restart Nginx or Apache, if you're using Laravel Valet you can skip to the next section; you need some extra steps in order for the web server to properly work.
sudo nginx -s reload
sudo apachectl restart
And make sure that your local web server also uses PHP 8.2 by visiting this script:
# index.php, accessible to your web server
phpinfo();
The version should show 8.2.x
.
If you're using Laravel Valet, you should do the following steps to upgrade it:
composer global update
You can use valet use
to switch between PHP versions:
valet use php@8.2
valet use php@8.1
PHP extensions are installed using pecl. I personally use Redis and Xdebug. They can be installed like so:
pecl install redis
pecl install xdebug
You can run pecl list
to see which extensions are installed:
pecl list
# Installed packages, channel pecl.php.net:
# =========================================
# Package Version State
# redis 5.3.4 stable
# xdebug 3.1.1 stable
You can search for other extensions using pecl search
:
pecl search pdf
# Retrieving data...0%
# ..
# Matched packages, channel pecl.php.net:
# =======================================
# Package Stable/(Latest) Local
# pdflib 4.1.4 (stable) Creating PDF on the fly with the PDFlib library
Make sure to restart your web server after installing new packages:
sudo nginx -s reload
sudo apachectl restart
valet restart
Make sure all extensions are correctly installed and loaded by checking both your PHP webserver and CLI installs:
php -i | grep redis
var_dump(extension_loaded('redis'));
If extensions aren't properly loaded, there are two easy fixes.
First, make sure the extensions are added in the correct ini file. You can run php --ini
to know which file is loaded:
Configuration File (php.ini) Path: /opt/homebrew/etc/php/8.2
Loaded Configuration File: /opt/homebrew/etc/php/8.2/php.ini
Scan for additional .ini files in: /opt/homebrew/etc/php/8.2/conf.d
Additional .ini files parsed: /opt/homebrew/etc/php/8.2/conf.d/error_log.ini,
/opt/homebrew/etc/php/8.2/conf.d/ext-opcache.ini,
/opt/homebrew/etc/php/8.2/conf.d/php-memory-limits.ini
Now check the ini file:
extension="redis.so"
zend_extension="xdebug.so"
Note that if you're testing installed extensions via the CLI, you don't need to restart nginx, apache or Valet when making changes to ini settings.
The second thing you can do, if you're updating from an older PHP version which also used pecl to install extension; is to reinstall every extension individually.
pecl uninstall redis
pecl install redis
Finally you should test and upgrade your projects for PHP 8.2 compatibility.
]]>In other words, instead of writing this:
class BlogData
{
public function __construct(
public readonly string $title,
public readonly Status $status,
public readonly ?DateTimeImmutable $publishedAt = null,
) {}
}
You can now write this:
readonly class BlogData
{
public function __construct(
public string $title,
public Status $status,
public ?DateTimeImmutable $publishedAt = null,
) {}
}
I've written about readonly properties before, so let's quickly summarise first:
readonly
flag cannot be changed during inheritance; and finallySince readonly classes are merely syntactic sugar for making all properties of that class readonly, it means that the same rules apply to readonly classes as well.
All properties of a readonly class can only be written once, and can not be unset:
readonly class BlogData { /* … */ }
$blogData = new BlogData(/* … */);
$blogData->title = 'other';
unset($blogData->title);
A readonly class can only have typed properties:
readonly class BlogData
{
public string $title;
public $mixed;
}
Since readonly properties cannot be static, readonly classes cannot have any static properties:
readonly class BlogData
{
public static string $title;
}
Properties of a readonly class can not have a default value unless you're using promoted properties:
readonly class BlogData
{
public string $title = 'default';
}
readonly class BlogData
{
public function __construct(
public string $title = 'default', // This works
) {}
}
You cannot change the readonly
class flag during inheritance:
readonly class BlogData { /* … */ }
class NewsItemData extends BlogData { /* … */ }
Readonly classes also don't allow dynamic properties. This won't have a big impact since dynamic properties are deprecated in PHP 8.2 anyway, but means that you cannot add the #[AllowDynamicProperties]
attribute to readonly classes:
#[AllowDynamicProperties]
readonly class BlogData { /* … */ }
Finally, there's a new reflection method to determine whether a class is readonly: ReflectionClass::isReadOnly()
. You can also use ReflectionClass::getModifiers()
, which will include the ReflectionClass::IS_READONLY
flag.
readonly class PostData
{
public function __construct(
public string $title,
public string $author,
public string $body,
public DateTimeImmutable $createdAt,
public PostState $state,
) {}
}
$rng = $is_production
? new Random\Engine\Secure()
: new Random\Engine\Mt19937(1234);
$randomizer = new Random\Randomizer($rng);
$randomizer->shuffleString('foobar');
function alwaysFalse(): false
{
return false;
}
null
, true
, and false
as standalone types
function generateSlug((HasTitle&HasId)|null $post)
{ /* … */ }
trait Foo
{
public const CONSTANT = 1;
public function bar(): int
{
return self::CONSTANT;
}
}
function connect(
string $user,
#[\SensitiveParameter] string $password
) {
// …
}
class Post {}
$post = new Post();
$post->title = 'Name';
// Deprecated: Creation of dynamic property is deprecated
enum A: string
{
case B = 'B';
const C = [self::B->value => self::B];
}
Enum properties in const expressions
It looks something like this:
final class Post
{
public function __construct(
public private(set) string $title,
) {}
}
There are a couple of things going on here:
$title
can only be set (and thus overwritten) within the private
scope of Post
.In other words, this is allowed:
final class Post
{
public function __construct(
public private(set) string $title,
) {}
public function changeTitle(string $title): void
{
// Do a bunch of checks
if (strlen($title) < 30) {
throw new InvalidTitle('Title length should be at least 30');
}
// Change the title from within the private scope
$this->title = $title;
}
}
While this isn't:
$post = new Post('Title');
// Setting $title from the public scope isn't allowed:
$post->title = 'Another title';
I would say it's a pretty decent proposal, I can come up with a bunch of use cases where you want public readonly access to an object's properties (without the overhead of implementing getters), while still being able to change a property's value from within its class. That class could for example add internal checks to ensure the value adheres to any number of business rules — as a simplified example: ensuring the title of our post is at least 30 characters long.
So all things good, here's hoping the RFC passes.
Right?
Well, I have a problem with it. Actually, not with the RFC itself — I think it's a very good proposal. No, my concern is not with this RFC on its own, but rather with how it's closely related to readonly properties.
Here we have an RFC which scope overlaps with readonly properties (albeit not entirely the same), with a future possibility to replace readonly properties altogether. Here's a quote from the RFC:
At this time, there are only two possible operations to scope: read and write. In concept, additional operations could be added with their own visibility controls. Possible examples include:
- …
- once - Allows a property to be set only once, and then frozen thereafter. In this case, public private(once) would be exactly equivalent to readonly, whereas public protected(once) would be similar but also allow the property to be set from child classes.
It's clear that this RFC and its successors have the potential to replace readonly properties entirely. Readonly properties — a feature that only has been added one year ago in PHP 8.1, not to mention readonly classes, which are coming to PHP 8.2 later this year.
Despite asymmetric visibility being a great proposal, I'm afraid of what PHP will become if we're adding features only to make them irrelevant three or four years later, as could potentially happen here with readonly properties. We should be very careful and deliberate about how we're changing PHP, and not dismiss existing features too quickly.
If we did, we'd contribute to a lot of uncertainty and instability within the community. Imagine someone adopting readonly properties today, only to hear a year later that by PHP 9.0, they'll probably be deprecated in favor of asymmetric visibility.
Even if readonly properties would stay and coexist with asymmetric visibility, there would be so much room for confusion: when could you use readonly properties? Should you always use asymmetric visibility instead? I would say it's bad language design if a language allows room for these kinds of questions and doubts.
Furthermore, I totally agree with Marco's sentiment on the matter:
I use readonly properties aggressively, and I try to make the state as immutable as possible.
In the extremely rare cases where
public get
andprivate set
are needed, I rely on traditional getters and setters, which are becoming extremely situational anyway, and still work perfectly fine.[…]
In fact, I'm writing so few getters and setters these days, that I don't see why I'd need getter and setter semantics to creep into the language, especially mutable ones, not even with the reflection improvements.
Now to be clear: I'm very well aware that asymmetric visibility and readonly properties aren't the same thing. Asymmetric visibility covers a much larger scope and offers much more flexibility. However: Nikita already coined a very similar idea to asymmetric visibility last year, which wasn't pursued in favour of readonly properties. The discussion about whether we want more flexibility has already been had, and the conclusion back then was: no; readonly properties cover 95% of the use cases, and that's good enough.
I would be sad to see PHP become a language that throws out core features every couple of years, for the sake of a little more flexibility. If we wanted more flexibility in this case, we should have decided on that two years ago when readonly properties were discussed in depth; now — in my opinion — is too late.
On a final note, if you are worried about cloning objects with new values (a problem this RFC would solve and readonly properties don't): people are already working on an RFC to allow rewriting readonly properties while cloning. I'd say it's better to focus our efforts in that area, instead of coming up with an entirely different approach.
Even more: the original example I showed with asymmetric visibility allowing for more functionality (internally guarding business rules) wasn't entirely correct. The same is possible with readonly properties, given that we have a way to overwrite readonly values when cloning them:
final class Post
{
public function __construct(
public readonly string $title,
) {}
public function changeTitle(string $title): self
{
// Do a bunch of checks
if (strlen($title) < 30) {
throw new InvalidTitle('Title length should be at least 30');
}
return clone $this with {
title: $title,
}
}
}
Oh, and while the above syntax isn't available yet, it's already possible to overwrite readonly properties while cloning today with some additional userland code:
final class Post
{
use Cloneable;
public function __construct(
public readonly string $title,
) {}
public function changeTitle(string $title): self
{
// Do a bunch of checks
if (strlen($title) < 30) {
throw new InvalidTitle('Title length should be at least 30');
}
return $this->with(
title: $title,
);
}
}
In summary: I think asymmetric visibility is a great feature for some use cases, although there are alternatives as well. All in all, I don't think it's worth adding asymmetric visibility now that we have readonly properties. We decided on readonly properties, we'll have to stick with them for the sake of our users and to prevent ambiguous features from making a mess.
I think a unified vision and direction for PHP is lacking these days, and this RFC — great as it is on its own — is a good example of that lacking in practice. I hope that we (PHP internals, that is) can come up with a solution, maybe the PHP Foundation has an important role to play in all of this, in the future?
I like thinking about these topics. Not because I believe all of these things necessarily need be added to PHP, but because it's good to challenge our own views and critically think about language design.
I'll keep this list short and to the point, but I'll always link to more in-depth content where relevant. Let's take a look!
The obvious one first. If I'd have to order my wishlist in descending priority, generics are on places one to five, and the rest follows afterwards.
function app(classString<ClassType> $className): ClassType
{
// …
}
It's pretty clear however that generics aren't coming to PHP. That is: as long as we want to validate them at runtime. That's actually what my second wishlist-item is about.
It's not just because of generics, but because it would allow so much more cool stuff, without having a runtime impact: switching to an opt-in, statically analysed compiler.
I actually wrote an in-depth article about not needing runtime type checks. Static analysis is incredibly powerful, and it would benefit PHP's growth a lot if we could get rid of "trying to do everything at runtime".
Keep in mind: it's my opinion, you don't have to agree 😉.
Like TypeScript for JavaScript. Imagine what would be possible if we'd be able to compile a superset language to plain PHP: complex static type checking, lots of cool syntax that would be too difficult to implement with PHP's runtime constraints, generics 🥺, …
There are a lot of caveats and sidenotes to be made here. I made a little vlog about the topic a while ago where I discuss some of the cons, but still I hope that one day this dream may become reality.
This one might actually be doable: an easy way of getting attributes from parent classes.
So instead of writing this:
$attributes = [];
do {
$attributes = $reflection->getAttributes(
name: RouteAttribute::class,
flags: ReflectionAttribute::IS_INSTANCEOF
);
$reflection = $reflection->getParentClass();
} while ($attributes === [] && $reflection);
We could do this:
$attributes = $reflection->getAttributes(
name: RouteAttribute::class,
flags: ReflectionAttribute::IS_INSTANCEOF
| ReflectionAttribute::CASCADE
);
It's not that high up on my list since there are userland implementations available, although it would be nice if scalar objects could have proper names. So String
instead of StringHelper
(which is how Laravel solves the problem of String
being a reserved word).
$string = new String(' hello, world ')->trim()->explode(',');
There has been an RFC for it a while ago: the pipe operator. However, it has been inactive for a while now. I actually think Larry Garfield still wants to do it some day, but I'm not sure how concrete the plans are currently.
Anyway, a pipe operator would be cool:
$result = "Hello World"
|> htmlentities(...)
|> str_split(...)
|> array_map(strtoupper(...), $$)
|> array_filter($$, fn($v) => $v != 'O');
Note that I'm taking some creative liberties in how argument placeholders would work with the $$
syntax.
"It would be such a breaking change 😱!!!" to finally make all PHP functions follow the same naming convention. No more str_replace
and strlen
, but rather string_replace
and string_length
.
It wouldn't be that big of a breaking change though: we could automate the upgrade process with tools like Rector, and static analysers would tell us about the correct function names while writing code. It would take some getting used to, but at least there would be a consistent API.
If you weren't yelling at me by now, many of you probably will after reading this section. I'd make PHP much stricter overall. Why? Because I like clarity in my code. A stricter language means less room for interpretation or behind-the-scenes-magic that leads to lots of confused moments. So that would include:
This will probably never happen, and that's ok; I can enforce most of these things by using PHP CS anyway.
On a final note, I made a similar list to this one in the past, and I'm actually happy to see that some of the things I wished for back then are implemented in PHP today:
So, who knows! Many more things of this list might end up in PHP after all? Please let it be generics though 🥺!
What would you change about PHP? Let me know on Twitter or send me e-mail!
]]>So first things first, what are dynamic properties exactly? Well, they are properties that aren't present on a class' definition, but are set on objects of those classes dynamically, at runtime.
For example this Post
class doesn't have a name
property, but nevertheless we set it at runtime:
class Post
{
}
// …
$post = new Post();
$post->name = 'Name';
var_dump($post->name); // 'Name'
As of PHP 8.2, these dynamic properties will be deprecated:
// …
$post->name = 'Name';
You'll see this message: Deprecated: Creation of dynamic property Post::$name is deprecated
.
__get
and __set
still works!You might be panicking at this point, because dynamic properties are a big part of meta programming in PHP — many frameworks rely on it!
Not to worry: this new deprecation won't affect any class that implements __get
and __set
. Classes that implement these magic functions will keep working as intended:
class Post
{
private array $properties = [];
public function __set(string $name, mixed $value): void
{
$this->properties[$name] = $value;
}
// …
}
// …
$post->name = 'Name';
The same goes for objects of stdClass
, they will support dynamic properties just as before:
$object = new stdClass();
$object->name = 'Name'; // Works fine in PHP 8.2
Now some clever readers might wonder: if stdClass
still allows dynamic properties, what would happen if you'd extend from it?
Indeed, it is possible to extend from stdClass
to prevent the deprecation notice from being shown. However, I'd say this solution is far from ideal:
// Don't do this
class Post extends stdClass
{
}
$post = new Post();
$post->name = 'Name'; // Works in PHP 8.2
If you really want to use dynamic properties without implementing __get
and __set
, there is a much better alternative than to extend from stdClass
.
The PHP core team has provided a built-in attribute called AllowDynamicProperties
. As its name suggests, it allows dynamic properties on classes, without having to rely on sketchy extends:
#[\AllowDynamicProperties]
class Post
{
}
$post = new Post();
$post->name = 'Name'; // All fine
PHP used to be a very dynamic language, but has been moving away from that mindset for a while now. Personally I think it's a good thing to embrace stricter rules and rely on static analysis wherever possible, as I find it leads to writing better code.
I can imagine developers who relied on dynamic properties, who are less happy with this change. If you're in that group, you might find it useful to take a closer look into static analysis. You can check out my Road to PHP: Static Analysis series if you want to learn more!
If you're willing to invest some time in figuring out static analysis, I'm fairly certain that most of you won't ever want to return back to the mess that is a dynamic programming language. With PHP we're lucky that both options are available and that you can migrate gradually towards a stricter type system.
So, yes: this deprecation might be a little painful, but I believe it's for the best of the language to do so. And remember that it won't be a fatal error until PHP 9.0, so there's plenty of time to deal with it.
Simply switching to a light colour scheme isn't enough though, you need to take some extra steps. And that's exactly the point of this video, to explain what those steps are. I hope you give it a look, and let me know in the comments whether you're taking up the one-week light theme challenge!
]]>I did some casual benchmarks on a WordPress installation from PHP 5.6 to PHP 8.1, here are the results:
]]>If you're unfamiliar with the concept: the Road to PHP is a newsletter series where you'll receive a daily email teaching you about a new feature of PHP. This time around we'll look at PHP 8.2 — in previous editions we looked at PHP 8.1 and static analysis.
In total, almost 7000 people have followed one of my Road to PHP series, and many have told me they like the format where they can start their day with a 5-minute read, straight to their inbox.
By the way: you won't receive any followup every afterwards: you're automatically unsubscribed, unless you specifically decide to subscribe to my main newsletter afterwards.
I'd say: give it a try, and join the Road to PHP 8.2!
]]>You can see this trend all throughout the community:
While I think this is a good evolution, I also realise there is a large group within the PHP community that don't want to use a stricter type system or rely on static analysis.
I've had several discussions with that group over the years, and it seems that we cannot get on the same page. I lay out my arguments in favour of stricter type systems and static analysis and as a response I get something like this: sure, but it's way too verbose to write all those types, it makes my code too strict to my liking, and I don't get enough benefit from it.
So when working on my latest video about the problem with null, I came up with yet another way to phrase the argument, in hopes to convince some people to at least consider the possibility that types and static analysis — despite their overhead — can still benefit them.
So, here goes. Attempt number I-lost-count:
My main struggle with writing and maintaining code isn't with what patterns to use or which performance optimizations to apply, it isn't about clean code, project structure or what not; it is about uncertainty and doubt. Here's what that looks like:
X
?
It is those kinds of questions and doubts that I'm bothered by, and it is those kinds of questions that a static analyser answers for me — most of the time.
So no, using a stricter type system and relying on static analysis doesn't slow you down. It increases productivity tenfold, it takes away so much uncertainty and doubt, it's liberating, and I cannot code without it anymore.
This is just a fun little post I wrote because I wanted to visualise how my data transfer objects have evolved over the years.
If you prefer, you can watch my 2-minute as well:
Let's start with PHP 5.6, this is what most people without modern-day PHP knowledge probably think PHP code still looks like. I'll just give you the code, and I'll mention what changes in future versions.
class BlogData
{
/** @var string */
private $title;
/** @var State */
private $state;
/** @var \DateTimeImmutable|null */
private $publishedAt;
/**
* @param string $title
* @param State $state
* @param \DateTimeImmutable|null $publishedAt
*/
public function __construct(
$title,
$state,
$publishedAt = null
) {
$this->title = $title;
$this->state = $state;
$this->publishedAt = $publishedAt;
}
/**
* @return string
*/
public function getTitle()
{
return $this->title;
}
/**
* @return State
*/
public function getState()
{
return $this->state;
}
/**
* @return \DateTimeImmutable|null
*/
public function getPublishedAt()
{
return $this->publishedAt;
}
}
PHP 7.0 introduced some major new syntax features: scalar types and return types being the most notable here. Nullable types aren't a thing yet, so we still need to use doc block types for our nullable $publishedAt
:
class BlogData
{
/** @var string */
private $title;
/** @var State */
private $state;
/** @var \DateTimeImmutable|null */
private $publishedAt;
/**
* @param \DateTimeImmutable|null $publishedAt
*/
public function __construct(
string $title,
State $state,
$publishedAt = null
) {
$this->title = $title;
$this->state = $state;
$this->publishedAt = $publishedAt;
}
public function getTitle(): string
{
return $this->title;
}
public function getState(): State
{
return $this->state;
}
/**
* @return \DateTimeImmutable|null
*/
public function getPublishedAt()
{
return $this->publishedAt;
}
}
With PHP 7.1 finally came nullable types, so we could remove some more doc blocks:
class BlogData
{
/** @var string */
private $title;
/** @var State */
private $state;
/** @var \DateTimeImmutable|null */
private $publishedAt;
public function __construct(
string $title,
State $state,
?DateTimeImmutable $publishedAt = null
) {
$this->title = $title;
$this->state = $state;
$this->publishedAt = $publishedAt;
}
public function getTitle(): string
{
return $this->title;
}
public function getState(): State
{
return $this->state;
}
public function getPublishedAt(): ?DateTimeImmutable
{
return $this->publishedAt;
}
}
While there were some exciting features in 7.2 like parameter type widening and the object
type, there's nothing we could do to clean up our specific DTO in this release.
The same goes for PHP 7.3, nothing to see here.
PHP 7.4 is a different story though! There now are typed properties — finally!
class BlogData
{
private string $title;
private State $state;
private ?DateTimeImmutable $publishedAt;
public function __construct(
string $title,
State $state,
?DateTimeImmutable $publishedAt = null
) {
$this->title = $title;
$this->state = $state;
$this->publishedAt = $publishedAt;
}
public function getTitle(): string
{
return $this->title;
}
public function getState(): State
{
return $this->state;
}
public function getPublishedAt(): ?DateTimeImmutable
{
return $this->publishedAt;
}
}
Another game changer: PHP 8 adds promoted properties; also, trailing commas in parameter lists are now a thing!
class BlogData
{
public function __construct(
private string $title,
private State $state,
private ?DateTimeImmutable $publishedAt = null,
) {}
public function getTitle(): string
{
return $this->title;
}
public function getState(): State
{
return $this->state;
}
public function getPublishedAt(): ?DateTimeImmutable
{
return $this->publishedAt;
}
}
Next, we arrive at PHP 8.1. Readonly properties are a thing, and allow us to write our DTO like so:
class BlogData
{
public function __construct(
public readonly string $title,
public readonly State $state,
public readonly ?DateTimeImmutable $publishedAt = null,
) {}
}
And finally, we arrive at PHP 8.2 — not released yet. Whenever a class only has readonly properties, the class itself can be marked as readonly, instead of every individual property:
readonly class BlogData
{
public function __construct(
public string $title,
public State $state,
public ?DateTimeImmutable $publishedAt = null,
) {}
}
That's quite the difference, don't you think?
It's interesting to see how the language has evolved over the course of almost a decade. If you had proposed the 8.2 syntax 10 years ago, you'd probably be called a madman. The same is true today, and I'm sure we'll look back at this point, ten years from now and wonder "how did we ever put up with that?".
As always, it's important to note that I'm working with the data available to us. That means that these charts are no 100% accurate representation of the PHP community as a whole, but they are an accurate representation of one of the most prominent parts of PHP: the packagist ecosystem.
Let's start with the percentage of PHP versions being used today, and compare it to the previous two editions:
Version | July, 2021 (%) | January, 2022 (%) | July, 2022 (%) |
8.1 | 0.1 | 9.1 | 24.5 |
8.0 | 14.7 | 23.9 | 20.6 |
7.4 | 46.8 | 43.9 | 38.4 |
7.3 | 19.2 | 12.0 | 8.0 |
7.2 | 10.4 | 6.6 | 5.1 |
7.1 | 3.8 | 2.4 | 1.9 |
Note that I've omitted all versions that don't have more than 1% usage. Visualizing this data looks something like this:
As expected during a year with a minor release instead of a major one: PHP 8.1 is growing, and PHP 8.0's usage is already declining. A good sign that developers are updating! Keep in mind that PHP 8.0 is still actively supported for another four months. So if you kept off on updating to PHP 8.1, now is a good time.
Less good news — although not unexpected: more than 50% of developers are still on PHP 7.4 or lower. That's not an insignificant number, considering that PHP 7.4 only receives security updates for 5 more months, and all older versions simply aren't supported anymore.
I did hope to see PHP 8.X adoption to be climbing more rapidly, I've shared some of my thoughts about it here, if you want some more reading.
Moving on the the all-time overview chart, here you can see the evolution of version usage across time:
It's interesting to compare the 5.5 peak in 2014 to the 7.4 peak two years ago. PHP 5.5 and subsequent versions saw a much faster decline as soon as PHP 7.0 became available, compared to PHP 7.4's decline when PHP 8.0 was released. I'm a little worried that PHP 8.0 wasn't as exciting as PHP 7.0 back in the day.
Fear for upgrading shouldn't be a blocker these days compared to eight years ago: we now have mature tools like Rector and PHP CS that take care of almost the whole upgrade path for you.
So why aren't people upgrading to PHP 8.0? Why are more people staying with PHP 7.4 compared to the 5.5 and 5.6 days? I don't have a definitive answer.
Part of the answer though (I think) lies with the open source community: what are packages requiring as their minimal version? Are they encouraging their users to update, or not?
I used Nikita's popular package analyzer to download the 1000 most popular composer packages. Next, I used a little script to get the lowest version each package supports from their composer.json
file. Here are the results:
Version | July, 2021 (#) | January, 2022 (#) | July, 2022 (#) |
8.1 | - | - | 125 |
8.0 | 117 | 160 | 94 |
7.4 | 56 | 69 | 86 |
7.3 | 133 | 116 | 104 |
7.2 | 142 | 133 | 130 |
7.1 | 182 | 190 | 153 |
7.0 | 31 | 29 | 29 |
5.6 | 61 | 49 | 42 |
5.5 | 43 | 42 | 35 |
5.4 | 41 | 43 | 40 |
5.3 | 97 | 83 | 77 |
5.2 | 12 | 10 | 10 |
5.0 | 2 | 2 | 1 |
I have mixed feelings about this data. On the one hand it's good to see PHP 8.1 as the minimum required version for 125 packages. However, look at how many packages still require a version lower than PHP 8.0: 707 out of 926 packages analysed. That's more than 75%!
Oh, as a side note: there only are 926 packages because some of the 1000 most popular packages don't specifically require a PHP version.
Let's plot this data into a chart:
Minimal PHP requirement over time
I won't say that the open source community is the only responsible factor here, but I do want to encourage you to think carefully about your responsibilities if you are an open source maintainer. We're not just talking about new and shiny PHP features here: we're talking about performance, software security for the most popular programming language on the web, and even about the impact of old PHP versions on electricity usage and server requirements, in Rasmus' words we can help save the planet.
What are your thoughts on these stats? Are you already using PHP 8.1? Let me know your thoughts on Twitter and subscribe to my newsletter if you want to be kept up-to-date about these posts!
Before explaining how you can get an elephpant, let me say how much I appreciate it that you're here. I never imagined my blog to turn out the way it did when I first started it 5 years ago, and that's in large part thanks to you!
So, thank you!
Now, how to get an elephpant: I'm giving them away on Twitter, my newsletter and YouTube. By the time you're reading this, my newsletter is probably already sent, but you can still follow me on Twitter and check out YouTube if you want one.
Here's the YouTube video with more instructions:
Thanks for reading, and thank you for being here!
Brent
I only use keyboard shortcuts and the actions menu, so I hide all toolbars.
Light colour schemes are better. That's not just an opinion, it is a fact. I prefer a colour scheme that might be less cool and fancy, but forces me to properly light my room and put less strains on my eyes.
Similar to light colour schemes, pick a font size and line height that works on your monitor, make sure you don't have to squint your eyes in order to read code.
I enable code folding for method bodies by default. It gives me a proper overview of classes, without too much noise. I've learned to use my keyboard for navigation and managing folds, so that it doesn't impact my performance.
For some reason, placing tabs at the bottom of my screen feels way more natural and less intrusive.
Instead of moving sidebars to the right, I undock it, so that it slides on top of my code without moving that code around.
If you prefer an even cleaner look, you can hide the sidebar altogether and use the navigation bar instead.
Distraction free mode centers my code, making it easier to read on large displays. By default it also hides tabs and line numbers, but you can show them again using the actions menu.
I use scopes to group and colour my code, allowing for easier navigation.
That's it! Take a look at my YouTube channel if you like these kinds of videos!
You must use uppercase notation for enum cases:
enum PostState
{
case PENDING;
case PUBLISHED;
case STARRED;
case DENIED;
}
Enums are very close to "constant values". Even though the RFC showed them using PascalCase (inspired by other languages), it doesn't give a good reason for doing so.
Some people argue to use PascalCase for enums specifically to visually distinguish between constants and enums. I don't believe that relying on naming conventions to distinguish between concepts is a good idea. It's better to rely on your IDE's or editor's insights to know what something is, instead of holding onto a naming convention.
Ironically, uppercase constants exist because of the same reason: to distinguish them from normal variables. I want to acknowledge that fact, however there's little we can do to change decades of programming history. I think it's fair to say that the majority of PHP projects use uppercase constants, and so I would like to keep following that convention for closely related concepts, despite how the convention originated.
Nevertheless, I have tried using PascalCase for enums, but it always felt unnatural to me: I wrote my enum cases uppercase automatically, only to notice I did afterwards; meaning I had to go back and refactor them. I decided not to fight my instincts and just go with uppercase notation which feels the most natural to me, because of how constant values have been written for years, and how close enums are to constants anyway.
You should only use backed enums when you actually have a need for them. A common use case is serialization. If an enum is only going to be used within code, you shouldn't make it a backed enum.
enum PostState: string
{
case PENDING = 'pending';
case PUBLISHED = 'published';
case STARRED = 'starred';
case DENIED = 'denied';
}
Manually assigning values to enums introduces maintenance overhead: you're not only maintaining the enum cases itself, but also their values. It's important to take into consideration when to add overhead, and when not.
On top of that: enums have their built-in name
property that's always accessible in case you need a textual representation:
Status::PUBLISHED->name; // PUBLISHED
When in doubt, apply this rule: do you need to create this enum from scalar values (from a database, from user input, …): use a backed enum. If the answer is "no" however, hold off assigning values until your use case requires it.
Enums shouldn't contain complex methods. Ideally, they only contain methods using the match
expression that provide richer functionality for specific enum cases:
enum Status
{
case DRAFT;
case PUBLISHED;
case ARCHIVED;
public function label(): string
{
return match($this) {
Status::DRAFT => 'work in progress',
Status::PUBLISHED => 'published',
Status::ARCHIVED => 'archived',
};
}
}
Enums shouldn't provide complex functionality, if you find yourself writing too much logic in your enums, you probably want to look into using the state pattern instead.
A good rule of thumb is that if it can fit into a match
expression (without returning closures or callables or what not), you're good to go. Otherwise consider alternatives.
A backed enum's value should only be used as a value for (de)serialization, not for functionality like providing labels.
You might be tempted to rewrite the example from the previous point as follows:
enum Status
{
case DRAFT = 'work in progress';
case PUBLISHED = 'published';
case ARCHIVED = 'archived';
}
While shorter, you're now mixing two concerns into one: representing labels in a user-readable format, combined with their serialization value. The first one is very likely to change over time, the second one should stay unchanged as much as possible to prevent complex data migrations.
You should only use integer backed enums when their values actually are integers, not to assign some notion of "order" or "indexing" to your enums.
enum VatPercentage: int
{
case SIX = 6;
case TWELVE = 12;
case TWENTY_ONE = 21;
}
enum Status: int
{
case DRAFT = 1;
case PUBLISHED = 2;
case ARCHIVED = 3;
}
It might be tempting to assign integer values to, for example, status enums where DRAFT
comes before PUBLISHED
, comes before ARCHIVED
.
While such sequential indexing works in a limited amount of cases, it's a mess to maintain. What happens when you're required to add another state called DENIED
? It should probably get the value 2
or 3
, meaning that you're stuck with data migrations for all published and archived entries.
When in doubt, you can again use the state pattern to model complex transitions between states and their order instead.
Do you agree? Disagree? Let me know on Twitter or via email!
However: they didn't share their raw data or absolute numbers, and they also didn't mention which packages were analysed exactly and how.
I think we can do better. So let's take a closer look at how attributes are used in the PHP open source community.
I used Nikita's Popular Package Analyser: it downloads the top-1000 packages from Packagist, and allows you to run analysis on them by writing simple PHP scripts.
In this case I looped over all PHP files, and scanned whether they contained attributes. You can check out the full script here.
A couple of remarks:
The first metric that stood out is that #[\ReturnTypeWillChange]
outnumbers all others by far: out of 2786 attributes in total, 1668 are either #[\ReturnTypeWillChange]
or #[ReturnTypeWillChange]
, that's almost 60% being ReturnTypeWillChange
attributes.
This attribute is the first built-in attribute in PHP, introduced in PHP 8.1, and meant to deal with a newly added deprecation notice. It was a design goal of the original attribute RFC to use them for these kinds of cases where we want userland code to communicate information to PHP's interpreter. In this case: to suppress a deprecation notice.
Speaking of the two variants (with and without a leading \
): some developers import their attributes and others use the fully qualified class name: the latter option is by far the preferred one: out of 1668 ReturnTypeWillChange
usages, only 524 imported the attribute — that's only 31%. It seems that most developers like to use the FQCN variant.
Out of 997 packages, only 200 packages are using attributes. Out of those 200, Symfony, Drupal and Laravel have large pieces of the pie. Symfony is the leader here with 9.6%: not unlikely caused by the fact that Symfony has been using docblock annotations for years.
Another point of interest is the use of #[Attribute]
and #[\Attribute]
: representing custom attributes provided by packages themselves. In total there are 561 custom attributes provided by these packages.
Looking on a per-package basis: Symfony provides 88 custom attributes, with PHPUnit providing 49, and doctrine's mongodb implementation providing 42. Again a clear example of how Symfony is an early adopter, thanks to them being used to docblock annotations for years. Interestingly, Laravel provides no custom attributes.
It's remarkable that there are no vendors using multiline attributes:
#[
AsCommand,
ReturnTypeWillChange,
]
It makes sense that vendors opt for the single-line notation, since it's compatible with older PHP versions because these lines are treated like comments before PHP 8.0:
#[AsCommand]
#[ReturnTypeWillChange]
ReturnTypeWillChange
attribute is by far the most used oneAsCommand
attribute, which is provided by SymfonyOn a personal note: I'd say there's room for improvement. I think Laravel should start embracing custom attributes, especially since they now require PHP 8.0 as their minimum version.
What's your opinion? Let me know via Twitter or email!
Deprecated: Creation of dynamic property Post::$name
is deprecated in /in/4IreV on line 10
Deprecation notices 🤢
Despite them being annoying and frustrating to many developers, they actually serve a purpose. I would even say you will appreciate them once you understand their goal, and how to deal with them. Let's take a look!
Oh, by the way: you can watch this blog post as a vlog if you prefer that:
It's a common complaint: "why do my PHP scripts break with minor version updates??". And quite right: PHP has a tendency to add deprecation notices in minor releases, which tend to be audibly present when upgrading a project. Take for example PHP 8.1, where suddenly logs were filled with these kinds of warnings:
Return type should either be compatible with
IteratorAggregate::getIterator(): Traversable,
or the #[ReturnTypeWillChange] attribute should be used
to temporarily suppress the notice
It's important to understand what deprecations are about: they aren't errors, they are notices. They are a way of notifying PHP developers about a breaking change in the future. They want to warn you up front, to give you plenty of time to deal with that upcoming breaking change.
Of course, one could ask: are these breaking changes and fancy features really necessary? Do we really need to change internal return types like IteratorAggregate::getIterator(): Traversable
, do we really need to disallow dynamic properties?
In my opinion — and it's shared by the majority of PHP internal developers — yes. We need to keep improving PHP, it needs to grow up further. And that sometimes means introducing a breaking change, like for example when internals add return types to built-in class methods: if you're extending IteratorAggregate
in userland code, you will need to make some changes. The language needs to evolve.
Overall I'd say that, despite some of the annoyances that come with such an evolving language, it's for the better.
And luckily we have a mechanic like deprecation notices: they tell us that something will break in the future, but that we can still use it today. We can incrementally make changes and updates around our codebase.
Second, PHP internals go to great lengths to help userland developers in dealing with deprecations. Thanks to the addition of attributes in PHP 8.0, we now have a much better and standardized way of communication between our code and PHP's interpreter.
For example: you can tag userland code with the ReturnTypeWillChange
attribute in order to prevent deprecation notices being shown.
final class MyIterator implements \IteratorAggregate
{
#[\ReturnTypeWillChange]
public function getIterator()
{
// …
}
}
Of course, this code will break in PHP 9.0, so while silencing deprecation notices is a short-term solution; you will need to fix them if you ever want to upgrade to the next major version:
final class MyIterator implements \IteratorAggregate
{
public function getIterator(): \Traversable
{
}
}
One more example maybe? With dynamic properties being deprecated in PHP 8.2, you can mark classes with the AllowDynamicProperties
attribute, making it so that they allow dynamic properties again and suppress the deprecation notice:
#[\AllowDynamicProperties]
class Post
{
}
// …
$post->title = 'Name';
PHP code will keep working just fine, even when parts of it trigger deprecation notices. Of course, you know by now that it's in your best interest to fix them if you ever want to upgrade to the next major version, but you don't need to do it right now. It's perfectly ok to upgrade your production projects and deal with deprecations notices over time.
I'd even recommend disabling deprecation notices on production altogether, or at least not show them to your end users:
error_reporting(E_ALL ^ E_DEPRECATED);
Maybe you can keep track of them using an external error tracker for the first months, to get a clear image of the places you'll need to fix those deprecations. But above all: deprecation notices shouldn't be blockers when upgrading to the latest minor PHP version.
Lastly, keep in mind that you don't need to do the boring tasks by hand. There are tools like Rector and phpcs that can take care of many upgrading issues for you. Usually it's a matter of running a script that takes a couple of minutes at most to scan and fix your codebase. That's work that you might need days for if you'd do it by hand.
It's not difficult or time consuming anymore to deal with PHP upgrades. In fact, deprecations help tremendously to create a smoother upgrade path and prepare your codebase for the future.
I like deprecations, you should too.
]]>You've probably used the strategy pattern before: a behavioral pattern that enables selecting an algorithm at runtime.
Let's consider a classic example: the user provides some input either in the form of XML, JSON or an array; and we want that input to be parsed to a pretty JSON string.
So all these inputs:
'{"title":"test"}'
'<title>test</title>'
['title' => 'test']
Would convert to this:
{
"title": "test"
}
Oh, there's one more requirement: we need these strategies to be extensible. Developers should be allowed to add their own strategies for dealing with other kinds of inputs: YAML, interfaces, iterable objects, whatever they need.
Let's take a look at the classic solution, and its problems.
Usually, we start by introducing some kind of interface that all strategies should implement:
interface ParserInterface
{
public function canParse(mixed $input): bool;
public function parse(mixed $input): mixed;
}
Each strategy must define whether it can run on a given input, and provide its actual implementation.
Next we can provide several implementations of that interface:
final class ArrayParser implements ParserInterface
{
public function canParse(mixed $input): bool
{
return is_array($input);
}
public function parse(mixed $input): mixed
{
return json_encode($input, JSON_PRETTY_PRINT);
}
}
final class JsonParser implements ParserInterface
{
public function canParse(mixed $input): bool
{
return
is_string($input)
&& str_starts_with(trim($input), '{')
&& str_ends_with(trim($input), '}');
}
public function parse(mixed $input): mixed
{
return json_encode(
json_decode($input),
JSON_PRETTY_PRINT
);
}
}
final class XmlParser implements ParserInterface
{
public function canParse(mixed $input): bool
{
return
is_string($input)
&& str_starts_with(trim($input), '<')
&& str_ends_with(trim($input), '>');
}
public function parse(mixed $input): mixed
{
return json_encode(
simplexml_load_string(
$input,
"SimpleXMLElement",
LIBXML_NOCDATA
),
JSON_PRETTY_PRINT
);
}
}
Full disclosure: these are very naive implementations. The strategy detection in the canParse
method simply looks at the first and last character of the input string, and probably isn't fool-proof. Also: the XML decoding doesn't properly work; but it's good enough for the sake of this example.
The next step is to provide a class that developers can use as the public API, this one will use our different strategies underneath. It's configured by adding a set of strategy implementations, and exposes one parse
method to the outside:
final class Parser
{
/** @var ParserInterface[] */
private array $parsers = [];
public function __construct() {
$this
->addParser(new ArrayParser)
->addParser(new JsonParser)
->addParser(new XmlParser);
}
public function addParser(ParserInterface $parser): self
{
$this->parsers[] = $parser;
return $this;
}
public function parse(mixed $input): mixed
{
foreach ($this->parsers as $parser) {
if ($parser->canParse($input)) {
return $parser->parse($input);
}
}
throw new Exception("Could not parse given input");
}
}
And we're done, right? The user can now use our Parser
like so:
$parser = new Parser();
$parser->parse('{"title":"test"}');
$parser->parse('<title>test</title>');
$parser->parse(['title' => 'test']);
And the output will always be a pretty JSON string.
Well… let's take a look at it from the other side: a developer who wants to extend the existing parser with their own functionality: an implementation that transforms a Request
class to a JSON string. We designed our parser with the strategy pattern for this exact reason; so, easy enough:
final class RequestParser implements ParserInterface
{
public function canParse(mixed $input): bool
{
return $input instanceof Request;
}
public function parse(mixed $input): mixed
{
return json_encode([
'method' => $input->method,
'headers' => $input->headers,
'body' => $input->body,
], JSON_PRETTY_PRINT);
}
}
And let's assume our parser is registered somewhere in an IoC container, we can add it like so:
Container::singleton(
Parser::class,
fn () => (new Parser)->addParser(new RequestParser);
);
And we're done!
Except… have you spotted the one issue? If you've used the strategy pattern in this way before (many open source packages apply it), you might already have an idea.
It's in our RequestParser::parse
method:
public function parse(mixed $input): mixed
{
return json_encode([
'method' => $input->method,
'headers' => $input->headers,
'body' => $input->body,
], JSON_PRETTY_PRINT);
}
The problem here is that we have no clue about the actual type of $input
. We know it should be a Request
object because of the check in canParse
, but our IDE of course doesn't know that. So we'll have to help it a little bit, either by providing a docblock:
/**
* @var mixed|Request $input
*/
public function parse(mixed $input): mixed
{
return json_encode([
'method' => $input->method,
'headers' => $input->headers,
'body' => $input->body,
], JSON_PRETTY_PRINT);
}
Or by doing the instanceof
check again:
public function parse(mixed $input): mixed
{
if (! $input instanceof Request) {
// error?
}
return json_encode([
'method' => $input->method,
'headers' => $input->headers,
'body' => $input->body,
], JSON_PRETTY_PRINT);
}
So because of how we designed our ParserInterface
, developers who want to implement it, will have to do double work:
final class RequestParser implements ParserInterface
{
public function canParse(mixed $input): bool
{
return $input instanceof Request;
}
public function parse(mixed $input): mixed
{
if (! $input instanceof Request) {
// error?
}
// …
}
}
This kind of code duplication isn't the end of the world, at most it's a minor inconvenience. Most developers won't even bat an eye.
But I do. As a package maintainer, I want my public APIs to be as intuitive and frictionless as possible. To me, that means that static insights are a crucial part of the developer experience, and I don't want the users of my code to be hindered because of how I designed this parser.
So, let's discuss a couple of ways to fix this problem.
If the problem of duplication happens because we've split our canParse
and parse
methods, maybe the easiest solution is to simply… not split them?
What if we design our strategy classes in such a way that they will throw an exception if they can't parse it, instead of using an explicit conditional?
interface ParserInterface
{
/**
* @throws CannotParse
* When this parser can't parse
* the given input.
*/
public function parse(mixed $input): mixed;
}
final class RequestParser implements ParserInterface
{
public function parse(mixed $input): mixed
{
if (! $input instanceof Request) {
throw new CannotParse;
}
// …
}
}
Our generic parser class would change like so:
final class Parser
{
// …
public function parse(mixed $input): mixed
{
foreach ($this->parsers as $parser) {
try {
return $parser->parse($input);
} catch (ParseException) {
continue;
}
}
throw new Exception("Could not parse given input");
}
}
Of course, now we're opening up the rabbit hole of "what an exception is" and whether we're allowed to use exceptions to control our program flow in this way. My personal opinion is "yes, definitely"; because passing a string to a method that can only work with a Request
object is in fact, an exception to the rule. At least, that's my definition.
Some people might opt for returning null
instead of throwing an exception, although that feels more wrong to me: null
doesn't communicate that this particular method wasn't able to handle the input. In fact, null
could very well be a valid result from this parser, depending on its requirements. So no, no null
for me.
However, I share the opinion that probably a couple of people have when reading this: either returning null
or throwing an exception doesn't feel like the cleanest solution. If we're embarking on this journey for the sole purpose of fixing a detail that only a handful of developers might be bothered about, we might explore other options as well, and dive even deeper into the rabbit hole.
We've written this manual check to guard against invalid input: $input instanceof Request
; but did you know there's an automated way for PHP to do these kinds of checks? Its built-in type system! Why bother rewriting stuff manually that PHP can do for us behind the scenes? Why not simply type hint on Request
?
final class RequestParser implements ParserInterface
{
public function parse(Request $input): mixed
{
// …
}
}
Well we can't, because of two problems:
mixed
to Request
according to the Liskov Substitution Principle, which is enforced by PHP and our ParserInterface
; andSo, end of story? Well… we're already so deep into the rabbit hole, we might as well give it a shot.
Let's start by imagining that the two problems mentioned aren't an issue: could we in fact design our parser in such a way that it's able to detect each strategy's accepted input, and select the proper strategy based on that information?
We sure can! The most simple solution is to loop over all strategies, try to pass them some input and continue if they can't handle it; let PHP's type system handle the rest:
final class Parser
{
public function handle(mixed $input): mixed
{
foreach ($this->parsers as $parser) {
try {
return $parser->parse($input);
} catch (TypeError) {
continue;
}
}
throw new Exception("Could not parse given input");
}
}
I actually prefer this approach over any kind of runtime reflection trying to determine which method can accept which input. Let's not try to recreate PHP's type checker at runtime. The only real requirement for this approach to work is that your strategy methods won't have any side effects and that they'll always properly type hint their input. That's one of my personal cornerstones when programming, and so I have no problems writing code that assumes this principle.
Ok so it is possible to match any given input to its correct strategy based on its method signature. But we still need to deal with our two initial problems.
The first one is that we're not allowed to write this:
final class RequestParser implements ParserInterface
{
public function parse(Request $input): mixed
{
// …
}
}
Because we defined the signature of parse
in our ParserInterface
like so:
interface ParserInterface
{
public function parse(mixed $input): mixed;
}
We can't narrow down parameter types, we can only widen them; that's called contravariance.
So on the one hand we have an interface that says that our strategies can take any type of input (mixed
); but on the other hand we have our strategy classes that tell us they can only work with a specific type of input.
If we want to go further into the rabbit hole, then there's no other conclusion to make than that our interface isn't actually telling the truth: we're not making strategies that work with any kind of input, and so it doesn't make sense to have an interface tell us that we do. This interface is essentially telling a lie, and there's no reason to keep it.
Well, actually: there is a reason to have this interface: it guides a developer in understanding how they can add their own strategies, without having to rely on documentation. When a developer sees this method signature:
final class Parser
{
// …
public function addParser(ParserInterface $parser): self
{
$this->parsers[] = $parser;
return $this;
}
}
It's clear to them that they'll need to implement ParserInterface
for their custom strategies to work. So I'd say that getting rid of this interface might do more harm than good, because without it, developers are operating in the dark.
There is one solution that I can think of that can counter this problem: accepting callables.
public function addParser(callable $parser): self
{
$this->parsers[] = $parser;
return $this;
}
callable
is a special type in PHP, because it doesn't only cover functions and closures, but also invokable objects. The only real thing missing here is that we can't tell — with certainty — from our code what our callables should look like.
We've established a rule saying that it should accept any kind of input that it can work with, but there's no way we can tell developers extending our code that, without providing an additional docblock. This is definitely a downside of this approach, and might be reason enough for you not to go with it.
I personally don't mind, I think the code duplication we had in the beginning and manual type validation annoys me more than having to read a docblock:
/**
* @param callable $parser A callable accepting one typed parameter.
* This parameter's type is used to match
* the input given to the parser to the
* correct parser implementation.
*/
public function addParser(callable $parser): self
{
$this->parsers[] = $parser;
return $this;
}
Then there's our second problem: not everything can be represented by a type. For example: both JSON and XML parsers should match on a string
of either JSON or XML, and we can't type hint those. I can think of two solutions.
parse
method for these edge cases, and throw an TypeError
when they don't match; orJsonString
and XmlString
as custom classes, and have a factory first convert those raw strings to their proper types.The first option would look like this:
final class JsonParser
{
public function __invoke(string $input): string
{
if (
! str_starts_with(trim($input), '{')
|| ! str_ends_with(trim($input), '}')
) {
throw new TypeError("Not a valid JSON string");
}
return json_encode(
json_decode($input),
JSON_PRETTY_PRINT
);
}
}
final class XmlParser
{
public function __invoke(string $input): string
{
if (
! str_starts_with(trim($input), '<')
|| ! str_ends_with(trim($input), '>')
) {
throw new TypeError("Not a valid XML string");
}
return json_encode(
simplexml_load_string(
$input,
"SimpleXMLElement",
LIBXML_NOCDATA
),
JSON_PRETTY_PRINT
);
}
}
The second one, having a custom class for JsonString
and XmlString
, would look something like this:
final class JsonParser
{
public function __invoke(JsonString $input): string
{
return json_encode(
json_decode($input),
JSON_PRETTY_PRINT
);
}
}
final class XmlParser
{
public function __invoke(XmlString $input): string
{
return json_encode(
simplexml_load_string(
$input,
"SimpleXMLElement",
LIBXML_NOCDATA
),
JSON_PRETTY_PRINT
);
}
}
But don't forget that we'd also need to introduce a factory to convert a string to its proper type, which means quite a lot of overhead.
On a final note, callable
has another advantage: users aren't bound to using invokable classes. Depending on their needs and how they test, they could get away with simply adding closures:
Container::singleton(
Parser::class,
fn () => (new Parser)->addParser(
fn (Request $request) => json_encode([
'method' => $request->method,
'headers' => $request->headers,
'body' => $request->body,
], JSON_PRETTY_PRINT)
);
);
Are there downsides to this approach? Definitely. Just like there are downsides to the original solution where we had lots of code duplication. I personally think that, from a developer experience point of view; it's worth considering alternatives to the original way of how we implement dynamic strategies; and I can imagine some projects benefiting from it.
What do you think? Let me know via Twitter or email; don't hesitate to say I'm slowly going crazy if you think so!
I've got some important news: today marked my last day working at Spatie and I will start working at JetBrains in the PhpStorm team on Monday. I want to talk about that.
You might be wondering what happened. I actually didn't plan on leaving Spatie originally; I wasn't looking for a new job. It was back in September last year, that I was told about the PhpStorm team looking for a developer advocate; and that felt like an opportunity I couldn't pass.
If you'd ask me to describe my dream job, I would probably tell you it would focus on the PHP community, content creation and education; and this is everything my new job at JetBrains is focussed on. So, I don't think this comes as a surprise to anyone, and I think you'll understand why I made this decision.
I'll of course miss my colleagues at Spatie — I spent almost 5 years with them — but I'm also ready and very excited about this new adventure; I'll keep you posted!
Nothing much will change: I'll keep working on my blog and other personal projects in my free time; like I have been doing for the past years. I do find more creative energy in making videos over blog posts at the moment though, but I don't have any long term plans. I'll just keep doing what I like at the moment.
I've written quite a lot of open source code at Spatie, and I'm happy to help maintain it. It's actually an important part of my new job to keep programming, just to keep in touch with the language. So while I won't work any more on client projects like I used to, I will keep writing PHP.
It’s up to you to decide after this video whether you agree or not. So, your honor, I’d like to start my closing statements.
Adding monomorphized or reified generics won’t happen. At least not according to Nikita who did an extensive amount of research on the topic. Both options either pose performance problems or simply require too much core code refactoring of PHP’s runtime type checker to be achievable within a reasonable amount of time.
However, if we think about the true value that generics bring, it’s not about runtime type checks. By the time PHPs runtime type checker kicks in and possibly throws a type error, we’re already running our code. Our program will crash. And I’ve never heard any user of my programs say “oh, it’s a type error, it’s ok”. No. The program crashed, and that’s the end of story.
Runtime type checks in PHP are a very useful debugging tool, I give you that, and in some cases required for type juggling. But most of the value of PHP’s type system comes from static analysis.
So, if we want generics in PHP we need a mind shift:
First, developers need to embrace static analysis. The irony here is that developers who want generics and understand their value, also understand the value of static type checkers. So while there is a group of PHP developers who don’t care about static analysis, they also shouldn’t care about the value that generics bring. Because, these two: generics and static type checking, simply cannot be separated.
Second, if PHP internals decide that statically checked generics have their place in PHP; they should wonder whether static analysis should be left as a responsibility with the community, or whether they should play a role in it. Either by creating a specification that every static analyser should follow, or by shipping their own static type checker. The second one would definitely be preferable, but you can imagine what an undertaking that would be. I don’t think that relying on proven third part tools should be an issue.
Third, type juggling simply wouldn’t be possible anymore, at least not when using generics. You’d have to trust your static type checker. This is a way of programming that PHP developers aren’t really used to, but many other languages do exactly this, and it works fine. A static type checker is incredibly powerful and accurate. I can imagine it’s difficult for PHP developers to understand the power of statically typed languages without having used one before. It’s worth looking into a language like Rust, Java, or even TypeScript, just to appreciate the power of static type systems. Or you could start using one of PHP’s third party static analysers: Psalm or PHPStan.
To summarize: if we want generics in PHP, with all the benefits they bring to static analysis, we need to accept the fact that runtime erased generics are the only viable path.
In closing, a few more remarks I’d address.
First there’s the argument that what I’m describing is already possible with docblocks. If you go back to the second post in this series, you’ll find me explaining the differences in detail, but let me quickly summarize:
A second remark is that, even with type erasure, we could still expose generic type information via the reflection API. I’m not saying that the type information should be completely gone at runtime, my foremost concern is that PHP shouldn’t check generic types at runtime. I’m not sure what the impact would be on PHP’s core to have generic type information available via reflection; so I’m just putting it out there that I’m not against that idea.
And finally, there is of course another solution. One that anyone could pursue in theory. One that has proven itself in the past: TypeScript. It could be possible for a superset of PHP to exist that compiles to normal PHP, and while compiling doing lots of type checks and other cool stuff. TypeScript is immensely popular, and I think that if there’s room for a similar approach in serverside languages, PHP is probably a good candidate. However, TypeScript didn’t just magically appear overnight. It was created by experienced language designers, it’s magnitudes larger than adding runtime-ignored generics in PHP. But who knows, maybe one day.
With all of that being said, I hope that you found this series useful and educational; I said everything I wanted to about generics. I’d appreciate it if you shared this series with your colleagues and followers — I believe it’s an important topic and I want to see things change.
I rest my case.
]]>Let’s get started.
Generics aren’t coming to PHP. That was Nikita’s conclusion last year. It simply wasn’t doable.
To understand why Nikita said that, we need to look at how generics could be implemented. In general, there are three possible ways to do so; programming languages that do support generics mostly use one of these three methods.
The first one is called Monomorphized Generics. Let’s go back to the first post of this series where I showed this collection example:
class StringCollection extends Collection
{
public function offsetGet(mixed $key): string
{ /* … */ }
}
class UserCollection extends Collection
{
public function offsetGet(mixed $key): User
{ /* … */ }
}
I explained that we could manually create implementations of the collection class for each type that we needed a collection for. It would be lots of manual work, there’d be lots of code, but it would work.
Monomorphized generics do exactly this, but in an automated way, behind the scenes. At runtime, PHP would not know about the generic Collection class, but rather about two or more specific implementations:
$users = new Collection<User>();
// Collection_User
$slugs = new Collection<string>();
// Collection_string
Monomorphized generics are a totally valid approach. Rust, for example, uses them. One advantage is that there are a bunch of performance gains, because there are no more generic type checks at runtime, it’s all split apart before running the code.
But that immediately brings us to the problem with monomorphized generics in PHP. PHP doesn’t have an explicit compilation step like Rust to split one generic class into several specific implementations; and, on top of that: monomorphized generics do require quite a lot of memory, because you’re making several copies of the same class with a few differences. That might not be as big an issue for a compiled Rust binary, but it is a serious concern for PHP code being run from a central point, the server; maybe serving hundreds or thousands of requests per second.
The next option is Reified Generics. This is an implementation where the generic class is kept as-is, and type information is evaluated on the fly, at runtime. C# and Kotlin have reified generics, and it’s the closest to PHP’s current type system, because PHP does all its type checks at runtime. The problem here is that it would require an immense amount of core code refactoring for reified generics to work, and you can imagine some performance overhead creeping in, as we’re doing more and more type checks at runtime.
That brings us to the last option: completely ignore generics at runtime. Act like they are not there; after all, a generic implementation of, for example, a collection class would work with every kind of input anyway.
So if we ignore all generic type checks at runtime, there aren’t any problems.
Well, not so fast. Ignoring generic types at runtime — it’s called type erasure by the way, Java and Python do it — it poses some problems for PHP.
For one: PHP not only uses types for validation, it also uses type information to convert values on the fly from one type to another — that’s the type juggling I mentioned in the first post of this series:
function add(int $a, int $b): int
{
return $a + $b;
}
add('1', '2') // 3;
If PHP ignored the generic type of this “string” collection, and we’d accidentally add an integer to it, it wouldn’t be able to warn us about that, if the generic type was erased:
$slugs = new Collection<string>();
$slugs[] = 1; // 1 won't be cast to '1'
The second, and more important problem with type erasure — maybe you’re already yelling it at your screen by now — is that the types are gone. Why would we add generic types, if they are erased at runtime?
It makes sense in Java and Python, because all type definitions are checked before running the code using a static analyser. Java for example runs a built-in static analyser when compiling code; something that PHP simply doesn’t do: there is no compilation step, and there certainly isn’t a built-in static type checker.
On the other hand… all the advantages of type checking, the ones we discussed in the previous posts; they don’t come from PHP’s built-in runtime typechecker. By the time PHP’s type checker tells us something is wrong, we’re already running the code. A type error essentially crashes our program.
Instead, most of the added value of type checks comes from static analysers that don’t require us to run our code. They are pretty good at making sure there can be no runtime type errors, as long as you, the programmer, provide enough type information. That doesn’t mean there can’t be any bugs in your code, but it is possible to write PHP code that’s completely statically checked and doesn’t produce any type errors while running. And on top of that: there are all the static insights that we get while writing code; that’s by far the most valuable part of any type system, and has nothing to do with runtime type checks.
So do we actually need runtime type checks? Because that’s the main reason why generics can’t be added in PHP today: it’s either too complex or too resource intensive for PHP to validate generic types at runtime.
That’s next time, in the last post of this series.
]]>$users = new Collection<User>();
$slugs = new Collection<string>();
Collections; they are probably the easiest way to explain what generics are about, but they also are the example that everyone talks about when discussing generics. It’s actually not uncommon for people to think that “generics” and “collections with a type” are the same thing. That’s definitely not the case.
So let’s take a look at two more examples.
Here’s a function called app
— if you work with a framework like Laravel, it might look familiar: this function takes a class name, and will resolve an instance of that class using the dependency container:
function app(string $className): mixed
{
return Container::get($className);
}
Now, you don’t need to know how the container works, what’s important is that this function will give you an instance of the class that you request.
So, basically, it’s a generic function; one whose return type will depend on what kind of class name you gave it. And it would be cool if our IDE and other static analysers also understand that if I give the classname “UserRepository” to this function, I expect an instance of UserRepository to be returned, and nothing else:
function app(string $className): mixed
{ /* … */ }
app(UserRepository::class); // ?
Well, generics allow us to do that.
And I guess this is a good time to mention that I’ve been keeping a secret, kind of: I previously said that generics don’t exist in PHP; well, that’s not entirely true. All static analysers out there — the tools that read your code without running it, tools like your IDE — they have agreed to use doc block annotation for generics:
/**
* @template Type
* @param class-string<Type> $className
* @return Type
*/
function app(string $className): mixed
{ /* … */ }
Granted: it’s not the most pretty syntax, and all static analysers are relying on a simple agreement that this is the syntax — there’s no official specification; but nevertheless: it works. PhpStorm, Psalm and PhpStan — those are the three largest static analysers in the PHP world — understand this syntax to some degree.
IDEs like PhpStorm use it to give the programmer feedback when they are writing code, and tools like Psalm and PhpStan use it to analyse your codebase in bulk and detect potential bugs, mostly based on type definitions.
So actually, we can build this app
function in such a way that our tools aren’t operating in the dark anymore. Of course, there’s no guarantee by PHP itself that the return type will be the correct one — PHP won’t do any runtime type checks for this function; but if we can trust our static analysers to be right, there’s very little — or even no chance of this code breaking when running it.
This is the incredible power of static analysis: we can actually be sure that, without running our code; most of it will work as intended. All of that thanks to types — including generics.
Let’s look at an even more complex example:
Attributes::in(MyController::class)
->filter(RouteAttribute::class)
->newInstance()
->
Here we have a class that can “query” attributes and instantiate them on the fly. If you’ve worked with attributes before you know that their reflection API is rather verbose, so I find this kind of helper class very useful.
When we use the filter
method, we give it an attribute’s class name; and afterwards calling the newInstance
method, we know that the result will be an instance of our filtered class. And again: it would be nice if our IDE understood what we’re talking about.
You guessed it: generics allow us to do that:
/** @template AttributeType */
class Attributes
{
/**
* @template InputType
* @param class-string<InputType> $className
* @return self<InputType>
*/
public function filter(string $className): self
{ /* … */ }
/**
* @return AttributeType
*/
public function newInstance(): mixed
{ /* … */ }
// …
}
I hope you start to see how powerful simple type information can be. A couple of years ago, I would have needed an IDE plugin for these kinds of insights to work, now I just need to add some type information.
This latest example doesn’t only rely on generics though, there’s another equally important part that’s in play. Type inference: the ability of a static analyser to “guess” — or reliably determine — a type without the user specifying it. That’s what’s happening with that class-string annotation over there. Our IDE is able to recognise the input we give this function as a class name, and infer that type as the generic type.
So, everything’s solved, right: generics are available in PHP and all major static analysers know how to work with them. Well… there’s a couple of caveats.
First of, there’s no official spec of what generics should look like, right now every static analyser could push its own syntax; they happen to have agreed on one, for now; but there are little future guarantees.
Second: doc blocks are, in my opinion, suboptimal. They feel like a less important part of our codebase. And granted: generic annotations only provide static insights and no runtime functionality, but we’ve seen how powerful static analysis can be, even without runtime type checks. I think it’s unfair to treat type information as “doc comments”, it doesn’t communicate the importance of those types within our code. That’s why we got attributes in PHP 8: all functionality that attributes provide, was already possible with docblock annotations, but that just didn’t feel good enough. The same goes for generics.
And finally: without a proper specification, all three major static analysers have differences between their generics implementations. PhpStorm being the one most lacking at the moment. Ideally, there would be an official specification coming from PHP’s internals. Right now, there isn’t.
These are the main reasons why I believe that it’s worth investing time in a more permanent and sustainable solution. So why doesn’t PHP have proper generics yet? Why do we rely on doc blocks without a clear specification?
That’s for the next post!
]]>I’m going to do a series on this blog about generics and PHP. We’ll start from the beginning, but quickly work our way to the more complex topics. We’ll talk about what generics are, why PHP doesn’t support them, and what’s possible in the future.
Let’s get started.
Every programming language has some kind of type system. Some languages have a very strict implementation, while others — PHP falls in this category — are much more lenient.
Now, type systems are used for a variety of reasons; the most obvious one is type validation.
Let’s imagine we have a function that takes two numbers, two integers; and does some kind of maths operation on them:
function add($a, $b)
{
return $a + $b;
}
PHP will happily allow you to pass any kind of data to that function, numbers, strings, booleans, doesn’t matter. PHP will try its best to convert a variable whenever it makes sense, like for example adding them together.
add('1', '2');
But those conversions — type juggling — often lead to unexpected results, if not to say: bugs and crashes.
add([], true); // ?
Now, we could manually write code to check whether our maths addition will work with any given input:
function add($a, $b)
{
if (!is_int($a) || !is_int($b)) {
return null;
}
return $a + $b;
}
Or we could make use of PHPs built-in type hints — built-in shorthands for what we’d otherwise do manually:
function add(int $a, int $b): int
{
return $a + $b;
}
Many developers in the PHP community say they don’t really care about these type hints because they know they should only pass integers to this function — they wrote it, after all.
However, that kind of reasoning quickly falls apart: you’re often not the only one working in that codebase, you’re also using code that you haven’t written yourself — think about how many packages you’re pulling in with composer. And so, while this example in isolation might not seem to be that big a deal, type checking does come in handy once your code starts to grow.
Besides that, adding type hints not only guards against invalid state, but they also clarify what kind of input is expected from us, programmers. Types often make it so that you don’t need to read external documentation, because much of what a function does is already encapsulated by its type definition.
IDEs make heavy use of this principle: they can tell the programmer what kind of input is expected by a function or what fields and methods are available on an object — because it belongs to a class. IDEs make our code writing so much more productive, in large part because they can statically analyse type hints across our codebase.
Keep that word in mind: static analysis — it’s going to be very important later in this series. It means that programs, IDEs or other kinds of “static analysers” can look at our code, and without running it tell us whether it will work or not — at least, to some degree. If we’re passing a string to our function that takes an integer, our IDE will tell us we’re doing something wrong — something that would lead to a crashing program at runtime; but our IDE is able to tell us without having to actually run the code.
On the other hand, type systems have their limitations. A common example is a “list of items”:
class Collection extends ArrayObject
{
public function offsetGet(mixed $key): mixed
{ /* … */ }
public function filter(Closure $fn): self
{ /* … */ }
public function map(Closure $fn): self
{ /* … */ }
}
A collection has a bunch of methods that work with any kind of input: looping, filtering, mapping, you name it; a collection implementation shouldn’t care about whether it’s dealing with strings or integers.
But let’s look at it from an outsider’s perspective. What happens if we want to be sure that one collection only contains strings, and another one only contains User
objects. The collection itself doesn’t care when looping over its items, but we do. We want to know whether this item in a loop is a User or a string — that’s quite the difference. But without proper type information, our IDE is operating in the dark.
$users = new Collection();
// …
foreach ($users as $user) {
$user-> // ?
}
Now, we could create separate implementations for each collection: one that only works with strings, and another that only works with User
objects:
class StringCollection extends Collection
{
public function offsetGet(mixed $key): string
{ /* … */ }
}
class UserCollection extends Collection
{
public function offsetGet(mixed $key): User
{ /* … */ }
}
But what if we need a third implementation? A fourth? Maybe ten or twenty. It becomes quite painful to manage all that code.
That’s where generics come in.
Now, to be clear: PHP doesn’t have generics. That’s a bold statement cutting quite a lot of corners, and we’re coming back to that later in this series. But for now it’s sufficient to say that what I’m showing next isn’t possible in PHP. But it is in many other languages.
Instead of creating a separate implementation for every possible type, many programming languages allow developers to define a “generic” type on the collection class:
class Collection<Type> extends ArrayObject
{
public function offsetGet(mixed $key): Type
{ /* … */ }
// …
}
Basically we’re saying that the implementation of the collection class will work for any kind of input, but when we create an instance of a collection, we should specify a type. It’s a generic implementation, but it’s made specific depending on the programmer’s needs:
$users = new Collection<User>();
$slugs = new Collection<string>();
It might seem like a small thing to do: adding a type. But that type alone opens a world of possibilities. Our IDE now knows what kind of data is in a collection, it can tell us whether we’re adding an item with the wrong type; it can tell us what we can do with items when iterating over a collection, it can tell us whether we’re passing the collection to a function that knows how to work with those specific items.
And while we could technically achieve the same by manually implementing a collection for every type we need; a generic implementation would be a significant improvement for you and me, developers who are writing and maintaining code.
So, why don’t we have generics in PHP? What other things can we do with them besides a boring collection? Can we add support for them? We’re going to answer all those questions in this mini series. And to be clear up front: my goal with this series is to teach you about generics, but equally important is that I want to create awareness about how we’re missing out with PHP. I want that to change.
]]>It’s the end of 2021 and I still hear some people say they think PHP is dead. They are wrong, let’s talk about that.
Let’s start with the language itself. It’s been actively developed for the past decade, with a new release every year. And just to name a few features that have been added recently:
And these are just my favourites, there’s much more. All of that to say that PHP is a rich language these days. On top of that, static analysis has grown in popularity over the years, allowing for generics, closure definitions and quite a lot more; and we see more and more people and projects adopting static analysis tools like Psalm and PHPStan. It’s quite powerful.
Finally, when it comes to the language itself: just recently the PHP foundation was announced. A non-profit with the goal to support, promote, and advance PHP. They are backed by companies like
just to name a few. Their only focus is to be able to pay core developers so that they can work on PHP full time. The foundation was announced less than a month ago, and has raised Two hundred fourteen thousand dollars to date, with a yearly goal of slightly less than three hundred thousand.
Besides the language itself — which I can assure you is stable and very much alive — PHP has a large and active community. Packagist is the most popular package manager, and has a total of almost fifty billion downloads. With around one point five billion new downloads each month.
Next there are the immensely popular web frameworks live Symfony and Laravel, and CMSs like WordPress and Craft.
There’s also the async community — don’t be mistaken: PHP can perfectly run asynchronously like for example you’d do with node. There’s Swoole and RoadRunner which are low-level components that optimize PHP’s runtime for asynchronous programming, but there also are high-level packages that you can plug into any project: packages like Amp and ReactPHP.
Finally, PHP is just a fun language to work with. It’s mature, performant, has a huge community, and is actively developed. I like working with PHP. You might prefer other languages which is totally fine, but there’s absolutely no reason to say that PHP is dead. It’s very much alive.
]]>I've written a lot about PHP 8.1 these past months. Not just about the release itself, but also about how important it is to keep up-to-date with PHP versions.
I don't want to leave you with empty words though, so I want to share some actionable advice on how to deal with updating external dependencies. Because, let's face it, you want to update your project to PHP 8.1, but some of your dependencies don't support it. So, end of story, right?
Here's what I do in such cases.
First off, I don't wait for the official PHP release to start testing my code. This goes for client and private projects, as well as open source projects. You can safely start testing once PHP's release candidates arrive. You might even discover a bug or two in PHP's core, helping them along the way.
Next, if you encounter an external dependency that doesn't support the latest PHP version yet, try and send a pull request to fix it yourself. Open source maintainers will often be very thankful for your contribution. It might be best to check the open issues, pull requests and discussions though, maybe it's already being worked on.
Of course, it's not always possible to send a PR on your own. Either you don't know the codebase well enough or don't have the time. The next step is to reach out to the maintainers, politely ask if they can provide PHP X.X support and whether you can help in any way. Even when you're not able to actually code the required changes, you might be able to test those changes in your project, and provide early feedback.
The benefit of starting early, is that you're not as much pressured for time. Maybe it'll take maintainers a bit longer than you anticipated, so starting early is only beneficial.
Let's fast-forward in time. PHP 8.1 is released, and one of your dependencies still hasn't been updated. Either the maintainer seems to be inactive, or there's a roadblock that can't be solved in a reasonable amount of time. Start looking for alternatives. This is the reason why you shouldn't pull in any dependency into your project without doing your research first. You should look into who and how many people are maintaining it, whether their work is funded, whether there's recent activity in the repository or not, and how many open and stale issues there are. It's best to do this kind of research before, instead of having to deal with it right when want to upgrade your project.
It's impossible to predict the future though, so you'll have to deal with inactive projects one day or another.
If you can't find any alternative dependencies, or none that can be implemented in a reasonable amount of time; you can consider forking the package and pulling it into your project. This should always be a last-resort option, but it might be worth it. Realise that you're adding tons of technical debt to your project by doing so though, so carefully consider the pros and cons. Also avoid trying to maintain a public fork, unless you're really motivated to do so.
Now, this strategy seems to work fine in client projects; but what about open source projects that have dependencies themselves? Things get a little more difficult when your open source code turns out to be dependant on other code, code that doesn't support the latest PHP version. Our last resort — forking — isn't as trivial when hundreds or thousands of projects depend on that code as well.
If you're an open source maintainer, I'd say the rule about picking your dependencies is even more important for your open source code. Forking "in the open" comes with a lot of headaches that you want to avoid at all costs.
While it's still a valid strategy in some cases, it might be worth to look at it from a different angle.
Being active in the Laravel community, I actually think we're rather fortunate. There are companies that pay employees to work on open source code. Laravel itself is the best example. I spoke with Dries in preparation for this post, one of the maintainers of Laravel. I asked him about the most difficult things when it comes to dealing with external dependencies from Laravel's point of view. He said it's mostly a waiting game. On top of that there's lots of communication between them and external package maintainers to get everything working.
So if you're in open source, the best thing you can do is to carefully consider what packages you depend on. Keep a good relation with those maintainers and try to help where you can. If, on the other hand, you're an open source user; you or your company should consider supporting the people you depend on. They are doing incredibly important work; most of the time, for free.
That's all I've got for you today. Though I'm wondering: are you running up-to-date PHP versions? If not, what's keeping you back? Are external dependencies the problem, or maybe you're dealing with server constraints? Maybe it has to do with time or budget issues or something else? Let me know via email or Twitter, I'm curious!
]]>There used to be a function in Windows Kernel that didn't do anything. Yet, the docs told programmers that this function had to be called after calling another one: if you called GetEnvironmentStrings
, you also needed to call FreeEnvironmentStrings
. However, many programmers didn't bother to do so because it was pointless: FreeEnvironmentStrings
didn't do anything, it was literally an empty function. A couple of years later, that function actually got an implementation, and many applications started to break because their programmers never bothered to call it in the first place.
The article summarises it as follows:
If the documentation says that you have to call a function, then you have to call it. It may be that the function doesn’t do anything, but that doesn’t prevent it from doing something in the future.
Or, how I like to phrase it: some of the most crappy software design possible.
There was a whole debate in the comment section on whether Microsoft or the developers — the users of Microsoft's code — messed up. After all: the docs explicitly told their users they needed to call that function, so if they didn't follow the rules, they were on their own.
There were a couple of commentators calling out Microsoft though:
SourceWhy not instead design the API such that the programmer cannot fail to use it correctly and still have their program compile?
And:
SourceAs a sometimes systems-programmer myself, I'm dismayed that it took until just a couple of comments ago before somebody pointed out that it was ALSO a dumb thing to stick in a do-nothing function call and then assume people would call it by contract.
Uh, did that REALLY seem like a good idea to anybody? By Windows NT 4, had we not all figured out a long time ago that many application developers are not going to do things exactly the way you tell them?
This is MS's error first; the app developer's second. Neither one is right, but MS was more wrong.
In my opinion, these are some sensible arguments. Users in general can't be trusted to, one, read the docs and two, follow rules that don't seem to make sense at that point in time. That's just not how to world works.
But ok, this was 2008 and we've learned to do better now, haven't we? Well… it's actually still very common to write code that's going to be used by others, and assume those users will know how to use that code responsibly. I personally know plenty of people who follow this mindset. I respect them very much, and they also know I disagree with them on this opinion.
Looking at it from the user perspective though, this mindset is suboptimal as well: if the code itself isn't clear on how it should be used, there's a level of uncertainty introduced in the user's mind. "Am I missing something here?" "Should I read every docs page available before actually using this code?"
I think it's better software design, for vendors and users alike, to make our code as explicit and robust as possible, with as little room for interpretation and uncertainty as possible.
In my opinion, that means:
final
by defaultreadonly
by defaultprivate
by defaultIt's better software design, for vendors and users alike, to make our code as explicit and robust as possible, with as little room for interpretation and uncertainty.
To me, it's not a matter of distrust, it's about writing code that's clear about what it does, without having to dig through a set of rules written in the docs somewhere far away from your IDE.
There's only very little code around these days that doesn't need extra explanation, and I think we can do better than that.
]]>Two months ago, I decided to give Twitter Home another chance: the timeline that Twitter fills using their algorithm instead of just chronologically showing tweets of people you follow. I wanted to discover interesting new people to follow and I figure Twitter would fill my feed with tweets related to my interests; based on what and who I liked, retweeted and followed over the years.
I was wrong.
After two months, I now only see tweets of:
And just to be clear, I'm not mad at any of those people; most of them actually tweet interesting stuff as well; but Twitter simply didn't show those more interesting tweets in my Home feed.
It turns out their algorithm is rather picky, especially when it comes to tweeting external sources:
On average, 51% of tweets in chronological timelines contained an external link, compared to just 18% in the algorithmic timelines
Unfortunately for me, I find external sources (blog posts, news articles, etc) often the most relevant and insightful; and Twitter deliberately filters them out when discovering new people.
So after using Twitter Home for two months, I felt genuinely miserable every time I opened my feed. I knew that almost no tweet would interest me, and I'm sad that the Twitter algorithm doesn't work better for my case. I'm sure Twitter is well aware of how their algorithm works, and I'm sure it yields the best results for the majority of their users, but I apparently don't belong in that group.
So, here's where I need your help: I really want to discover more interesting people online; people who write about PHP, webdev, and programming; people who dare to challenge ideas that we take for granted; content that makes us think outside our box.
But how do I find those people? Twitter clearly isn't the best platform and I find Reddit and HackerNews either too focussed or too broad. So what's left? Any ideas? I would very much appreciate your input!
]]>Here's what he asked:
I struggle in one area and I would like your help in that regard. I wonder how you plan your week, month, year and 5 years? Developers like me can learn coding and programming techniques; but struggle to set a goal, set proper time and follow a plan.
It only rarely happens, but this time it did: this question actually made me think really hard about my own progress, and I think it's worth sharing it.
First off, I should make a distinction between doing client work, and what I like to call "creative" work. Client work pays the bills, but creative work is what really gives me energy. I have a very different approach in handling them both.
Let's start with client work (the most boring part of the answer): I've been working on the same project for over three years now. I work on it together with a team of wonderful colleagues. I'm the one who has been communicating directly with the client though: analyzing the problem space, setting goals and deadlines, etc.
There's only one thing I can think of that I've come to find incredibly important: clear communication. Check and double check that the work we're doing is what the client is expecting, and clearly communicate about how we're doing in terms of time and budget. We have regular meetings just to talk about how it's going. We both understand that things can change, and we both are adaptable. But we also know that change comes at a price, and we respect that.
So: honest communication and building a meaningful relation between us and our clients are what I consider to be the key. Everything planning related goes rather smoothly once there's a level of trust between both parties.
That's about all I wanted to say about client work, now I want to look at the exciting part — the part that really got me thinking when Muhammad asked his question. How do I plan what I write about on this blog? What kind of podcasts I make? What kind of videos or tweets or newsletters I post?
I don't.
Whenever I try to shove my creative energy into a some kind of system, I find that it utterly fails. That's why, on some weeks, I write 3 or 4 blog posts, and while sometimes I go a month without writing anything.
Sometimes I feel inspired to make a video or podcast. When I do I usually sit down whenever I find the time, write a script, record it and edit it as soon as possible. I've got no "long term media engagement strategy" like some people do. I tried it, and it simply doesn't work.
I used to plan everything to the minute though. Both my wife and I did.
I remember us sitting in the car visiting my wife's gynaecologist — she had been pregnant with our first child for about 3 months — and we were discussing how we wanted the delivery to go. Ideally my wife wanted to stay less then 24 hours in the hospital (you can do that in Belgium if your baby is healthy, it's called an "outpatient delivery" if Google Translate can be trusted).
Fast forward two months and we're sitting in that same car driving urgently to the hospital. It turns out there were some serious complications and my wife needed to be hospitalized. Eventually our son was born at around eight months with an emergency C-section — not what we had planned for. Almost one year later the pandemic hit and we suddenly were in lockdown for the next couple of months with a 9-month old baby.
We learned that no single plan can ever guarantee it'll succeed. So why bother with the stress, disappointment and frustration that comes with it when things turn out to be different than expected. Sure, we still plan short-term things, but there's definitely not a long-term plan we hold on to. There are dreams and hopes, but we don't expect all of them to become a reality.
I do the same with my creative work. Whether it's a blog post, newsletter, tweet, video or podcast; I don't make any long term plans. At most I keep a list of ideas that pop up into my head, but I'll have to admit I remove most of them after a couple of months if I didn't find the energy to work on them by then. Only a handful of ideas make it, and those are the ones that I simply start working on whenever I feel like it.
I'll see where this whole online adventure takes me step by step, and I'm ok with that. I actually find it very liberating. The same goes for learning (for me at least). When I'm truly passionate about something, I don't need a schedule to force me to spend time digging into it. On the contrary: I'd almost need a schedule to tell me when to pause because I need to take care of the kids or do house chores or whatever. After all, aren't only the things we're truly passionate about worth spending so much time on?
I'm not sure this was the answer Muhammad expected, but at least it's the most accurate one I could give. If you want to share your thoughts as well, just send me an email!
Thanks for reading! This post is part of my "Dev Diaries" series where I write about my own and personal experiences as a developer. Would you like to read some more?
If you want to stay up to date about what's happening on this blog, you can follow me on Twitter or subscribe to my newsletter:
Exciting times are ahead, let's take a look at modern day PHP!
I can't help but start this list with the newest PHP version, released just a little more than one month ago. My main projects are already being prepared to run on PHP 8.1 in production, which I must admit I'm very excited about. You might not expect it from a minor release — there are no major breaking changes and only deprecation notices added — but PHP 8.1 brings some very cool features. here's my personal top three.
Enums are now built-into the language:
enum Status
{
case draft;
case published;
case archived;
public function color(): string
{
return match($this)
{
Status::draft => 'grey',
Status::published => 'green',
Status::archived => 'red',
};
}
}
We're able to use new
in initializers:
class PostStateMachine
{
public function __construct(
private State $state = new Draft(),
) {
}
}
And, of course, readonly properties:
class PostData
{
public function __construct(
public readonly string $title,
public readonly PostState $state,
public readonly DateTimeImmutable $publishedAt,
) {}
}
Which, combined with PHP 8.0's promoted properties, make for some very clean data classes. Just to visualise the difference, here's that same class, with the same functionality, written in PHP 5.6:
class PostData
{
/** @var string */
private $title;
/** @var State */
private $state;
/** @var \DateTimeImmutable|null */
private $publishedAt;
/**
* @param string $title
* @param State $state
* @param \DateTimeImmutable|null $publishedAt
*/
public function __construct(
$title,
$state,
$publishedAt = null
) {
$this->title = $title;
$this->state = $state;
$this->publishedAt = $publishedAt;
}
/**
* @return string
*/
public function getTitle()
{
return $this->title;
}
/**
* @return State
*/
public function getState()
{
return $this->state;
}
/**
* @return \DateTimeImmutable|null
*/
public function getPublishedAt()
{
return $this->publishedAt;
}
}
Can you see why I'm excited? PHP is getting these awesome syntax improvements with every release. It's only getting more and more fun to write it! Of course, there are a lot more features added in modern PHP, check out my 3-minute video if you want a quick rundown, or you can scroll to keep reading — there's much more exciting about PHP in 2022!
I've already briefly mentioned it: static analysis in PHP is growing significantly.
If you just want a quick read about why static analysis matters in PHP, and why I'm so excited about it, you could check out this blog post: "We don't need runtime type checks".
Two months ago, the PHP world got some pretty big news, maybe even the biggest news of 2021: Nikita, one of most active core maintainers is stepping down to work on LLVM, but at the same time there's also a new initiative backed by several big companies to finally make core development sustainable.
In short, there's the PHP Foundation, a non-profit with the only goal to fund PHP core development. The initiative is driven by JetBrains, who have already pledged $100,000 to the project. Alongside many others, they have now raised $329,920.75; a good start!
That money is used to fund core development, and opens doors for people to work on PHP who were previously unable. You can read more about the Foundation's mission and goals in JetBrains' blog post.
Just like every year, I can't go without mentioning Packagist, now with over 3 million registered versions and more than 300.000 packages. As you can see, the ecosystem just keeps growing and growing, 2022 won't be any different.
Oh, by the way, just recently Packagist passed the milestone of having handled over more than 50 billion installs. Congrats Packagist!
One exciting development in the async community, is that the developers from both Amp and ReactPHP — the two major async players — have come together to make a fiber-compatible event loop implementation called Revolt PHP.
In comparison to the community at large, async PHP is only used by a small portion of it; but nevertheless it's still good to see the async community is going strong and embracing modern PHP.
An interesting development I've been following along the sideline, is serverless PHP. My buddy Matthieu Napoli is making it his mission to educate PHP developers about this relatively new way of using PHP, and he seems to be doing pretty well. You can check out Bref, his open source project to make serverless PHP easy, or check out his course about serverless PHP in 2022.
I haven't had any use cases for it myself, but I know more and more people running serverless PHP in production, so it's definitely something worth keeping an eye on.
So what about this year? I'm looking forward to see where static analysis is going, and am especially curious about even better integration with PhpStorm. I find that realtime static analysis — static analysis in your code while you're writing it — offers much more value during the development phase itself.
I'm a little worried now that Nikita has stepped down. He's definitely not the only person capable of working on PHP's core, but he did a tremendous amount of work these past years with PHP 8.0 and 8.1. I hope the PHP Foundation will be up to speed soon and that there are enough core developers who have time to work on PHP next year. PHP 8.2 is already in development, although there aren't many RFCs drafted yet.
I don't think 2022 will be the most mind blowing year for PHP, but rather a year of adding stability. Nothing wrong with that.
These were the things that stood out the most to me personally in 2021, and that make me excited for PHP in 2022: awesome new syntax, stability for core development thanks to the PHP Foundation, static analysis is growing stronger and better, and lots of interesting developments across the community.
What are you most excited about? Let me know on Twitter or via email; I'd love to hear your thoughts!
]]>As always, it's important to note that I'm working with the data available to us. That means that these charts are no 100% accurate representation of the PHP community as a whole, but they are an accurate representation of one of the most prominent parts of PHP: the packagist ecosystem.
Let's start with the raw numbers: the percentage of PHP versions being used, today and six months ago.
Version | July, 2021 (%) | January, 2022 (%) |
8.1 | 0.1 | 9.1 |
8.0 | 14.7 | 23.9 |
7.4 | 46.8 | 43.9 |
7.3 | 19.2 | 12.0 |
7.2 | 10.4 | 6.6 |
7.1 | 3.8 | 2.4 |
7.0 | 1.3 | 0.8 |
Note that I've omitted all versions that didn't have more than 1% usage back in July, 2021. Visualizing this data looks something like this:
It's good to see PHP 8.1 being used in almost 10% of all composer installs, only one month after its release. It makes sense that it's easier picked up on for projects already on PHP 8.0, since it's fairly easy to upgrade from PHP 8.0 to PHP 8.1.
I'm also happy to see PHP 8.0's growth, although PHP 8.0 and 8.1 combined only account for one third of all installs. That means that two out of three composer installs use a PHP versions that isn't actively supported any more.
Over the past years, I've been making it my goal to educate the people around me about the importance of keeping software up to date. I believe we — you and I — all have a responsibility to carry. If you want some hands-on tips on how to start, you can read this post about the why and how of managing PHP version ugrades.
two out of three composer installs use a PHP versions that isn't actively supported any more
Moving on the the all-time overview chart, here you can see the evolution of version usage across time.
Despite PHP 7.4 starting its downward trajectory, it's clear that it still has a way to go. Let's hope we'll see a steeper decline in six months.
Another interesting metric is the minimum required version across packages. I've used Nikita's popular package analyzer to download the 1000 most popular composer packages, and wrote a little script to get the lowest version they support from their composer.json
. Here are the results:
Version | July, 2021 (#) | January, 2022 (#) |
8.0 | 117 | 160 |
7.4 | 56 | 69 |
7.3 | 133 | 116 |
7.2 | 142 | 133 |
7.1 | 182 | 190 |
7.0 | 31 | 29 |
5.6 | 61 | 49 |
5.5 | 43 | 42 |
5.4 | 41 | 43 |
5.3 | 97 | 83 |
5.2 | 12 | 10 |
5.0 | 2 | 2 |
You'll notice there are a little less than 1000 packages included, that's because some of the 1000 most popular packages don't specifically require a PHP version.
For visual learners, here's the same data visualised in a chart:
Minimal PHP requirement over time
You might be surprised not to see PHP 8.1 here yet, but keep in mind that this data shows the minimum required version. It doesn't mean that no packages support PHP 8.1, it only means they also support PHP 8.0 or lower versions. That makes a lot of sense given that PHP 8.1 has only been released for a little more than a month.
By the way, I'm talking about PHP 8.0 and 8.1 as if you're all already familiar with these newer versions. If that's not the case, or if you want 3-minute refresher about all the cool things that are happening in the PHP world these days, check out my video about modern day PHP!
So, in summary: I see similar trends as previous years, with the majority of composer installs still running outdated versions. I know many have good reasons why they can't or won't upgrade, but I also believe it has never been easier to stay up-to-date with modern PHP than it is today. Investing a couple of hours or days per year to keep your codebase healthy and up-to-date shouldn't be an issue for anyone. I hope you want to help create awareness for this issue. You can start by sharing this blog post or my recap video.
What are your thoughts on these stats? Are you already using PHP 8.1? Let me know your thoughts on Twitter and subscribe to my newsletter if you want to be kept up-to-date about these posts!
Start by making sure brew is up-to-date:
brew update
Next, upgrade PHP. You can either use the built-in php recipe, but I recommend to use the shivammathur/homebrew-php
tap.
brew upgrade php
shivammathur/homebrew-php
brew tap shivammathur/php
brew install shivammathur/php/php@8.1
To switch between versions, use the following command:
brew link --overwrite --force php@8.1
You can read more in the repository.
Check the current version by running php -v
:
php -v
Restart Nginx or Apache, if you're using Laravel Valet you can skip to the next section; you need some extra steps in order for the web server to properly work.
sudo nginx -s reload
sudo apachectl restart
And make sure that your local web server also uses PHP 8.1 by visiting this script:
# index.php, accessible to your web server
phpinfo();
The version should show 8.1.x
.
If you're using Laravel Valet, you should do the following steps to upgrade it:
composer global update
You can use valet use
to switch between PHP versions:
valet use php@8.1
valet use php@8.0
PHP extensions are installed using pecl. I personally use Redis and Xdebug. They can be installed like so:
pecl install redis
pecl install xdebug
You can run pecl list
to see which extensions are installed:
pecl list
# Installed packages, channel pecl.php.net:
# =========================================
# Package Version State
# redis 5.3.4 stable
# xdebug 3.1.1 stable
You can search for other extensions using pecl search
:
pecl search pdf
# Retrieving data...0%
# ..
# Matched packages, channel pecl.php.net:
# =======================================
# Package Stable/(Latest) Local
# pdflib 4.1.4 (stable) Creating PDF on the fly with the PDFlib library
Make sure to restart your web server after installing new packages:
sudo nginx -s reload
sudo apachectl restart
valet restart
Make sure all extensions are correctly installed and loaded by checking both your PHP webserver and CLI installs:
php -i | grep redis
var_dump(extension_loaded('redis'));
If extensions aren't properly loaded, there are two easy fixes.
First, make sure the extensions are added in the correct ini file. You can run php --ini
to know which file is loaded:
Configuration File (php.ini) Path: /opt/homebrew/etc/php/8.1
Loaded Configuration File: /opt/homebrew/etc/php/8.1/php.ini
Scan for additional .ini files in: /opt/homebrew/etc/php/8.1/conf.d
Additional .ini files parsed: /opt/homebrew/etc/php/8.1/conf.d/error_log.ini,
/opt/homebrew/etc/php/8.1/conf.d/ext-opcache.ini,
/opt/homebrew/etc/php/8.1/conf.d/php-memory-limits.ini
Now check the ini file:
extension="redis.so"
zend_extension="xdebug.so"
Note that if you're testing installed extensions via the CLI, you don't need to restart nginx, apache or Valet when making changes to ini settings.
The second thing you can do, if you're updating from an older PHP version which also used pecl to install extension; is to reinstall every extension individually.
pecl uninstall redis
pecl install redis
Finally you should test and upgrade your projects for PHP 8 compatibility.
]]>These benchmarks were taken on my local machine, and only meant to measure the relative difference between PHP 8.0 and 8.1. I benchmarked a page that showed lots of data, with lots of classes loaded, as I expected that inheritance cache would have the largest performance impact.
Metric | PHP 8.0 | PHP 8.1 |
Requests per second (#/sec) | 32.02 | 34.75 |
Time per request (ms) | 31.232 | 28.777 |
The results seem to be in line what what Dmitry originally reported when he added this feature: a 5-8% performance increase.
]]>enum Status
{
case draft;
case published;
case archived;
public function color(): string
{
return match($this)
{
Status::draft => 'grey',
Status::published => 'green',
Status::archived => 'red',
};
}
}
class PostData
{
public function __construct(
public readonly string $title,
public readonly string $author,
public readonly string $body,
public readonly DateTimeImmutable $createdAt,
public readonly PostState $state,
) {}
}
class PostStateMachine
{
public function __construct(
private State $state = new Draft(),
) {
}
}
$fiber = new Fiber(function (): void {
$valueAfterResuming = Fiber::suspend('after suspending');
// …
});
$valueAfterSuspending = $fiber->start();
$fiber->resume('after resuming');
Fibers, a.k.a. "green threads"
$array1 = ["a" => 1];
$array2 = ["b" => 2];
$array = ["a" => 0, ...$array1, ...$array2];
var_dump($array); // ["a" => 1, "b" => 2]
Array unpacking also supports string keys
function foo(int $a, int $b) { /* … */ }
$foo = foo(...);
$foo(a: 1, b: 2);
function generateSlug(HasTitle&HasId $post) {
return strtolower($post->getTitle()) . $post->getId();
}
$list = ["a", "b", "c"];
array_is_list($list); // true
$notAList = [1 => "a", 2 => "b", 3 => "c"];
array_is_list($notAList); // false
$alsoNotAList = ["a" => "a", "b" => "b", "c" => "c"];
array_is_list($alsoNotAList); // false
The new array_is_list
function
Generics. We all want them, they are probably not going to be built-into PHP any time soon. BUT there is proper support for generics using docblocks; both by static analysers like PHPStan and Psalm, but also — and this is a big one — by PhpStorm.
You see, only a few months ago, PhpStorm added basic support for generics using docblocks. And it does in fact work with quite a lot of cases, PhpStorm can tell us — in real time, while coding — what kind of generic type we're dealing with. It's even smart enough to infer generic types in some occasions.
If you have no clue what I'm talking about right now, I would suggest doing some reading on my blog — I've added some useful links in the description for you. But I also want to give an example.
Here we have a kind of "attribute query" class: a class that can filter and instantiate attributes:
$routeAttributes = Attributes::new(MyController::class)
->instanceOf(Route::class)
->first();
It provides a slightly cleaner API compared to straight up using PHP's built-in reflection classes.
Now, after running this query here, I want my IDE to know, in real time, that I have an instance of the Route attribute here. And this is exactly the kind of complex example that PhpStorm is now able to detect, and it's a huge time saver.
Let me show you the query itself, or at least: the interesting parts of it.
/**
* @template AttributeType
*/
class Attributes
{
/**
* @return AttributeType
*/
public function first(): mixed
{ /* … */ }
}
Here we have our attribute class, with a generic AttributeType
, and that's the type that's returned by the first method, in this example. The problem here: how do we actually set that generic type? It's actually pretty straight forward when you know about generic type inference.
/**
* @template AttributeType
*/
class Attributes
{
/**
* @template InstanceOfType
*
* @param class-string<InstanceOfType> $className
*
* @return self<InstanceOfType>
*/
public function instanceOf(string $className): self
{ /* … */ }
}
Here we have our instanceOf
method, and you can see it defines another generic type, it's called InstanceOfType
. Now, PhpStorm, and other static analysers; are smart enough to detect — or infer the type of the input that's passed to this function — that's the name of the attribute we want to filter on — and use that type as the generic type for our attributes class, when we return it. This kind of generic type inference is an incredibly powerful tool.
You might need to read this example a few times before getting it, but it essentially allows us to create classes like this attribute query class where the end user still has lots of information available to them while using this package.
Now, this is actually not what I wanted to talk about today. It's all pretty cool, and I'm very excited about it; but if you know me, you know that I like to think about the details. And one of those details that we need to talk about, now that generics in PHP are much more accessible; is how to name them.
I used AttributeType
and InstanceOfType
as the generic type placeholders, but that's kind of not the convention in most programming languages.
I looked at quite a lot of them, and by far, the most popular convention — I think that's thanks to Java — is to use a single letter for generic types; so T, or E or V or K or U — those are some of the popular choices. And that are quite a lot of languages that follow this convention: Java, Kotlin, Rust, Go, also C# and Swift.
And I don't know about you, but I find that this makes my code so much harder to read. And the reasoning behind using these single letters, is to make it clear, by their name, that these are generic types and not real types. But as you can see in my screenshots, you could very well just use a different colour for generic types to differentiate them.
I need to mention though that PhpStorm doesn't support that yet, but I hope that they'll change it after seeing this video.
Another convention that I've seen around — especially in the Psalm and PHPStan's documentation is to prefix the generic name with an uppercase letter T. But is that really more readable than suffixing it with "Type"? TAttribute
or AttributeType
; TInstanceOf
or InstanceOfType
? The second one sounds much more like we'd say it in human language, no?
So yeah, these are the kinds of details I'm thinking about, because I feel like they genuinely affect the readability of my code, and code of others that I need to work in.
So, what I would like to know: what's your opinion?
I made a poll on Twitter, and was surprised that so many people — more than 50% — preferred the single letter approach. That's terrible for code readability — especially if you're working with multiple generic types within the same context.
Anyway; I'll probably stick with what feels best for me and what I think is the most readably; but do share your opinions in the comments or on reddit or twitter wherever you're watching this; maybe someone is able to change my mind; or maybe I just changed yours?
As with every release, PHP 8.1 adds some nice new features. Keep in mind that this list will grow over the year.
Enums will be added in PHP 8.1! If you're unsure what they can be used for, you can read about them here.
Adding enums would be a significant improvement in PHP, so I for one am very much looking forward seeing enums arrive in PHP 8.1. To give you a quick preview of what they will look like, here's a code sample:
enum Status {
case Pending;
case Active;
case Archived;
}
And this is how they will be used:
class Post
{
public function __construct(
private Status $status = Status::Pending;
) {}
public function setStatus(Status $status): void
{
// …
}
}
$post->setStatus(Status::Active);
You can find an in-depth analysis of how to use enums in this post.
Fibers — aka "green threads" — are a low level mechanism to manage parallelism. You probably won't use them directly in your applications, but frameworks like Amphp and ReactPHP will make extensive use of them.
Here's a simple example of using fibers:
$fiber = new Fiber(function (): void {
$valueAfterResuming = Fiber::suspend('after suspending');
// …
});
$valueAfterSuspending = $fiber->start();
$fiber->resume('after resuming');
If you want to read some more about fibers, what they can and can't do, you can read this post.
Dmitry Stogov has added some improvements to opcache, he calls it "inheritance cache". This feature allows links between classes to be cached, much like linked classes can be preloaded as of PHP 7.4.
Dmitry reports between a 5% and 8% performance increase thanks to this change, a nice little detail to look out for in PHP 8.1.
Array unpacking was already allowed in PHP 7.4, but it only worked with numeric keys. The reason string keys weren't supported before is because there wasn't any consensus on how to merge array duplicates. The RFC cleanly solves this by following the semantics of array_merge
:
$array1 = ["a" => 1];
$array2 = ["b" => 2];
$array = ["a" => 0, ...$array1, ...$array2];
var_dump($array); // ["a" => 1, "b" => 2]
new
in initializers RFCThis RFC allows you to use the new
keyword in function definitions as a default parameter, as well as in attribute arguments and other places.
class MyController {
public function __construct(
private Logger $logger = new NullLogger(),
) {}
}
You can read all about this feature in this dedicated post.
Class properties can be marked as readonly, meaning they can only be written once.
class PostData {
public function __construct(
public readonly string $title,
public readonly DateTimeImmutable $date,
) {}
}
Trying to change a readonly property after it has been initialized will result in an error:
$post = new Post('Title', /* … */);
$post->title = 'Other';
Error: Cannot modify readonly property Post::$title
If you want to learn more about readonly properties in depth, you can read my followup post.
You can now make a closure from a callable by calling that callable and passing it ...
as its argument:
function foo(int $a, int $b) { /* … */ }
$foo = foo(...);
$foo(a: 1, b: 2);
You already know about union types in PHP 8.0, and intersection types are a similar feature. Where union types require the input to be one of the given types, intersection types require the input to be all of the specified types. Intersection types are especially useful when you're working with lots of interfaces:
function generateSlug(HasTitle&HasId $post) {
return strtolower($post->getTitle()) . $post->getId();
}
If you like this style of programming, you'd need to create a new interface Sluggable
and implement it in $post
, intersection types get rid of that overhead.
never
type RFCThe never
type can be used to indicated that a function will actually stop the program flow. This can be done either by throwing an exception, calling exit
or other similar functions.
function dd(mixed $input): never
{
// dump
exit;
}
never
differs from void
in that void
still allows the program to continue. This might seem like a novelty feature but it's actually a quite useful one for static analysers.
array_is_list
function RFCYou've probably had to deal with this once in a while: determine if an array's keys are in numerical order, starting from index 0. Just like json_encode
decides whether an array should be encoded as an array or object.
PHP 8.1 adds a built-in function to determine whether an array is a list with those semantics, or not:
$list = ["a", "b", "c"];
array_is_list($list); // true
$notAList = [1 => "a", 2 => "b", 3 => "c"];
array_is_list($notAList); // false
$alsoNotAList = ["a" => "a", "b" => "b", "c" => "c"];
array_is_list($alsoNotAList); // false
Do you want to stay up-to-date about PHP 8.1's development? Subscribe to my newsletter and receive occasional updates:
Class constants in PHP can be overridden during inheritance:
class Foo
{
public const X = "foo";
}
class Bar extends Foo
{
public const X = "bar";
}
As of PHP 8.1, you can mark such constants as final
in order to prevent this:
class Foo
{
final public const X = "foo";
}
class Bar extends Foo
{
public const X = "bar";
Fatal error: Bar::X cannot override final constant Foo::X
}
fsync
function RFCPHP 8.1 adds the fsync
and fdatasync
functions to force synchronization of file changes to disk and ensure operating system write buffers have been flushed before returning.
$file = fopen("sample.txt", "w");
fwrite($file, "Some content");
if (fsync($file)) {
echo "File has been successfully persisted to disk.";
}
fclose($file);
Because disk synchronization is a file system operation, the fsync
function will only work on plain file streams. Attempting to sync non-file streams will emit a warning.
You can now use 0o
and 0O
to denote octal numbers. The previous notation by prefixing a number with 0
still works as well.
016 === 0o16; // true
016 === 0O16; // true
While PHP 8.1 is a minor version, there will be some changes that might technically be a breaking change, and deprecations as well. Let's discuss them one by one.
Chances are you might run into this deprecation notice when upgrading to PHP 8.1:
Return type should either be compatible with IteratorAggregate::getIterator(): Traversable,
or the #[ReturnTypeWillChange] attribute should be used to temporarily suppress the notice
You might notice this error pop up when using phpunit/phpunit
, symfony/finder
and some other popular open source packages. What's happened is that internal functions are starting to use proper return types. If you're extending a class from the standard library (like IteratorAggregate
), you'll need to add those return types as well.
The fix is simple: update your vendor code if the error occurs in a third-party package (most of those are already fixed with their newest releases). If the error occurs in your code you can either add the ReturnTypeWillChange
attribute, suppressing the error until PHP 9.0. Here's an example of a class extending DateTime
:
class MyDateTime extends DateTime
{
/**
* @return DateTime|false
*/
#[ReturnTypeWillChange]
public function modify(string $modifier)
{
return false;
}
}
Or you can simply add the return type:
class MyDateTime extends DateTime
{
public function modify(string $modifier): DateTime|false
{
return false;
}
}
$GLOBALS
usage RFCA small change to how $GLOBALS
is used will have a significant impact on the performance of all array operations. Nikita does a fine job explaining the problem and solution in the RFC. The change means that some edge cases aren't possible to do any more with $GLOBALS
. "What is no longer supported are writes to $GLOBALS taken as a whole. All the following will generate a compile-time error":
$GLOBALS = [];
$GLOBALS += [];
$GLOBALS =& $x;
$x =& $GLOBALS;
unset($GLOBALS);
On top of that, passing $GLOBALS
by reference will generate a runtime error:
by_ref($GLOBALS); // Run-time error
Nikita analysed the top 2000 packages on Packagist, and only found 23 cases that will be affected by this change. We can conclude the impact of this — technically breaking — change will be low, which is why internals decided to add it in PHP 8.1. Remember that most of us will win by this change, given the positive performance impact it has everywhere in our code.
These changes are part of the long-term vision to convert all resources to dedicated objects. You can read more about it here.
Fileinfo functions with finfo
objects
Functions like finfo_file
and finfo_open
used to accept and return resources. As of PHP 8.1, they work with finfo
objects.
IMAP functions with IMAPConnection
objects
Just like the fileinfo change, IMAP functions like imap_body
and imap_open
no longer work with resources
This change is simple: internal functions currently accept null
for arguments that are non-nullable, this RFC deprecates that behaviour. For example, this is currently possible:
str_contains("string", null);
In PHP 8.1, these kinds of errors will throw a deprecation warning, in PHP 9 they will be converted to type errors.
false
RFCFrom the RFC:
PHP natively allows for autovivification (auto-creation of arrays from falsey values). This feature is very useful and used in a lot of PHP projects, especially if the variable is undefined. However, there is a little oddity that allows creating an array from a false and null value.
You can read the details on the RFC page. In summary, this behaviour is deprecated:
$array = false;
$array[] = 2;
Automatic conversion of false to array is deprecated
With every release, there's a bunch of very minor changes to the language. All of them are listed in the UPGRADING guide on GitHub and the small deprecations RFC, make sure to check it out if you want to know every little detail.
Here's a summary of the most significant changes:
MYSQLI_STMT_ATTR_UPDATE_MAX_LENGTH
no longer has an effectMYSQLI_STORE_RESULT_COPY_DATA
no longer has an effecthtmlspecialchars
and htmlentities
now also escape '
by default to '
; malformed UTF-8 will also be replaced with a unicode character, instead of resulting in an empty stringhash
, hash_file
and hash_init
have an extra argument added to them called $options
, it has a default value of []
so it won't affect your codeMurmurHash3
and xxHash
That's it for now, keep in mind I'll regularly update this post during the year, so make sure to subscribe if you want to be kept in the loop. Are you excited for PHP 8.1? Let me know on Twitter!
]]>Lately, I've done quite a lot of thinking about a specific use case for them. I've talked with a bunch of people about it and I've tried to approach the question as rationally as possible: are attributes used for routing, a good or bad idea?
After months of thoughts and discussions, I've come to (what I think to be as objectively as possible) a conclusion: they are worth giving a try, albeit with some side notes attached. In this post, I'll share my thought process, as well as address all counterarguments I've heard against route attributes over these past years.
Let's get started.
To make sure we're on the same page, route attributes in their most basic form would look something like this:
class PostAdminController
{
#[Get('/posts')]
public function index() {}
#[Get('/posts/{post}')]
public function show(Post $post) {}
// …
#[Post('/posts/{post}')]
public function store(Post $post) {}
}
There are a lot of issues with such a simplified example, so let's go through them one by one.
First of all, there's the issue of duplication. It might not seem like a problem in this example, but most projects definitely have more than "a few routes". I've counted them in two of the larger projects I'm working on: 470 and 815 routes respectively.
Both Symfony and Laravel have a concept called "route groups" to deal with these kinds of scaling issues. And the same thinking can be applied when using route attributes.
I'm sure you can come up with quite a lot of different approaches to modeling such attribute route groups; I'm going to share two that I think are robust and qualitative solutions, but it's by no means a definitive list.
You could manage "shared route configuration", stuff like prefixes and middlewares, on the controller level:
#[Prefix('/posts')]
#[Middleware(AdminMiddleware::class)]
class PostController
{
#[Get('/posts')]
public function index() {}
#[Get('/posts/{post}')]
public function show(Post $post) {}
}
Or, taking it a step further; have a generic Route
attribute that can be used like so:
#[Route(
prefix: '/post',
middleware: [AdminMiddleware::class]
)]
class PostController
{
#[Get('/posts')]
public function index() {}
#[Get('/posts/{post}')]
public function show(Post $post) {}
}
But also make it extensible:
#[Attribute]
class AdminRoute extends Route
{
public function __construct(
string $prefix,
array $middleware,
) {
parent::__construct(
prefix: "/admin/{$prefix}",
middleware: [
AdminMiddleware::class,
...$middleware
],
)
}
}
And be used like so:
#[AdminRoute]
class PostController
{
#[Get('/posts')]
public function index() {}
#[Get('/posts/{post}')]
public function show(Post $post) {}
}
This last approach is definitely my favourite, but feel free to differ in that opinion. The main point here is: excessive duplication doesn't have to be a problem with route attributes.
The second-biggest argument against route attributes comes from people who say that they prefer to keep their routes in a single file, so that they can easily search them, instead of spreading them across potentially hundreds of controller files.
Let's take a look at a real life example though. Here we have a contacts controller with an edit
method:
class ContactsController
{
public function edit(Contact $contact)
{ /* … */ };
}
People arguing for "a central place to manage their routes", in other words: against route attributes; say that a central route file makes it easier to find what they are looking for. So, ok, let's click through to our routes file (in my case the Laravel IDEA plugin allows you to click the edit
method and go straight to the route definition), and take a look at what's there:
Route::get('{contact}', [ContactsController::class, 'edit']);
So, what's the URI to visit this page? Is it /{contactId}
? Of course not, this route is part of a route group:
Route::prefix('people')->group(function (): void {
// …
Route::get('{contact}', [ContactsController::class, 'edit']);
});
So, it's /people/{contactId}
? Nope, because this group is part of another group:
Route::prefix('crm')
// …
->group(function (): void {
Route::prefix('people')->group(function (): void {
// …
Route::get('{contact}', [ContactsController::class, 'edit']);
});
}
Which is part of another group:
Route::middleware('can:admin,' . Tenant::class)
->group(function (): void {
Route::prefix('crm')
// …
->group(function (): void {
Route::prefix('people')->group(function (): void {
// …
Route::get('{contact}', [ContactsController::class, 'edit']);
});
}
Which is part of another group, defined in Laravel's route service provider:
Route::middleware(['web', 'auth', /* … */])
->prefix('admin/{currentTenant}')
->group(base_path('routes/admin_tenant.php'));
So, in fact, the full URI to this controller is /admin/{tenantId}/crm/people/edit/{contactId}
. And now remember our route file actually contains somewhere between 700 and 1500 lines of code, not just the snippets I shared here.
I'd argue that using dedicated route attributes like CrmRoute
extending AdminRoute
would be much easier to work with, since you can simply start from the controller and click your way one level up each time, without manually looking through group configurations.
Furthermore, adding a route to the right place in such a large route file poses the same issue: on what line exactly should my route be defined to fall in the right group? I'm not going to step through the same process again in reverse, I'm sure you can see the problem I'm pointing at.
Finally, some people mention splitting their route files into separate ones to partially prevent these problems. And I'd agree with them: that's exactly what route attributes allow you to do on a controller-based level.
In short, dedicated route files do not improve discoverability and route attributes definitely don't worsen the situation.
With the two main arguments against route attributes refuted, let's consider whether they have additional benefits compared to separated route files.
Spoiler: they do.
In the vast majority of cases, from my own experience and based on other's testimonies, controller methods and URIs almost always map one to one together. So why shouldn't they be kept together?
When I'm writing a new controller method, the last thing I want to be bothered about is to create the controller method, and then having to think about "ok, which route file should I now go to that has the correct middleware groups setup, and where in that file should I register this particular method". There's so much unnecessary cognitive overload introduced because of separate route files, because they pull apart two concepts they tightly belong together.
Let's just keep them together, so that we can focus on more important stuff.
Furthermore, any framework worth its salt will provide you with the tools necessary to generate any URI based on a controller method:
action([PostController::class, 'show'], $post);
If you're already working with controller methods as the "entry point" into your project's URI scheme, then why not keep relevant meta data right with them as well?
So yes, route attributes do add value compared to route files: they reduce cognitive load while programming.
One of the only arguments against route attributes that I kind of agree with, is how we deal with collisions. You've probably dealt with a situation like this one, where two route definitions collide with each other:
Route::get('/contacts/{id}', …);
Route::get('/contacts/list', …);
Here we have a classic collision: when visiting /contacts/list
, your router could detect it as matching /contacts/{id}
, and in turn runs the wrong action for that route.
Such problems occur rarely, but I've had to deal with them myself on the odd occasion. The solution, when using a single route file, is to simply switch their order:
Route::get('/contacts/list', …);
Route::get('/contacts/{id}', …);
This makes it so that /contacts/list
is the first hit, and thus prevents the route collision. However, you don't have any control over the route order when using attributes since they are directly coupled to controller methods and not grouped together; so what then?
First of all, there are a couple of ways to circumvent route collisions, using route files or attributes, all the same; that don't require you to rely on route ordering:
/contacts/show/{id}
or /contacts/{id}/show
; or/contacts/{id:\d+}
.However, there still might be some edge cases where collisions are unavoidable. How to handle those? The most obvious solution is to simply allow some kind of "order" key on route attributes, so that you can carefully control their order yourself:
#[Get('/contacts/list', order: 'contacts-1')]
public function index() {}
#[Get('/contacts/{id}', order: 'contacts-2')]
public function show(Contact $contact) {}
I agree that this approach isn't ideal, but I'd say that solving route collisions never is. On top of that, these kinds of collisions only rarely happen, so I only consider it a very minor problem that can be solved when needed.
Finally a short one, but one that needs mentioning: some people are afraid of attributes because of performance issues.
First of all: reflection in PHP is pretty fast, all major frameworks use reflection extensively, and I bet you never noticed those parts being a performance bottleneck.
And, secondly: attribute discovery and route registration is something that is very easily cacheable in production: Laravel already does this with event listeners and blade components, just to name two examples.
In fact, the concept of "a route cache" is already present in both Symfony and Laravel, and Symfony even already supports route attributes.
So no, performance isn't a concern when using route attributes.
So, what's left? The only argument I've heard that I didn't address here is that "people just don't like attributes".
There's very little to say against that. I think it mostly means that "people don't like change" in general. I've been guilty of this attitude myself. The only advice I can give, if you're in that situation, is to just try it out. Get out of your comfort zone, it's a liberating thing to do.
Now, maybe you want to tell me I'm wrong, or share your own thoughts on the matter. I'd love for my opinion to be challenged, so feel free to share your thoughts on Twitter or via email!
]]>class MyStateMachine
{
public function __construct(
private ?State $state = null,
) {
$this->state ??= new InitialState();
}
}
In this state machine example, we'd like to construct our class in two ways: with and without an initial state. If we construct it without an initial state, we want a default one to be set. PHP of course supports setting initial values directly in the parameter list, but only for primitive types. For example, if our state machine used strings instead of objects internally, we'd be able to write its constructor like so:
class MyStateMachine
{
public function __construct(
private string $state = 'initial',
) {
}
}
So with PHP 8.1 we're able to use that same "default value" syntax for objects as well. In other words: you can use new
for default arguments (which are one example of "initializers"):
class MyStateMachine
{
public function __construct(
private State $state = new InitialState(),
) {
}
}
"Initializers" are more than parameter default values though, here's a simple explanation from the RFC:
This RFC proposes to allow use of new expressions inside parameter default values, attribute arguments, static variable initializers and global constant initializers
You read it right: attributes are in this list as well! Imagine a simple validation library that uses attributes to validate input on properties. Maybe it should be able to validate array elements, something like this:
class CreateEmailsRequest extends FormRequestData
{
#[ValidArray(
email: [new Required, new ValidEmail],
name: [new Required, new ValidString],
)]
public array $people;
}
Before PHP 8.1, you wouldn't be able to write this kind of code, because you weren't allowed to use new
in attributes, due to the way they are evaluated, but now you can!
Let's take a look at some important details worth mentioning.
These kinds of "new values" will only be constructed when actually needed. That means that, in our first example, PHP will only create a new object of InitialState
if no argument is given:
class MyStateMachine
{
public function __construct(
private State $state = new InitialState(),
) {
}
}
new MyStateMachine(new DraftState()); // No InitialState is created
new MyStateMachine(); // But now it is
In case of attributes, for example, the objects will only be created when newInstance
is called on the reflection attribute.
You should also know that you cannot use new
as a default value in class properties. Supporting this functionality would introduce lots of unforeseen side effects when, for example, serializing and unserializing objects.
class MyStateMachine
{
private State $state = new InitialState();
}
Luckily we have promoted properties which do allow a default value, since PHP will transpile the property promotion syntax, keeping the default value in the constructor argument, but not in the actual property.
Here's what the transpiled version looks like:
class MyStateMachine
{
private State $state;
public function __construct(
State $state = new InitialState(),
) {
$this->state = $state;
}
}
You might have already guessed it, but you can only pass a limited set of input when constructing new objects in initializers. For example, you can't use variables, the spread operator, anonymous classes, etc. Still, it's a very welcome addition!
PHP is getting better and better with every update. Some people argue that these changes aren't strictly necessary since they don't add any new functionality to the language; but they do make our day-by-day developer lives just a little more easy, I really like that about PHP these days!
This post was first released on my newsletter. Feel free to subscribe if you want to be the first to see these kinds of posts, and want to talk about about them with me directly via email.
Every once in a while, maybe every couple of years, someone has an idea that revolutionises the tech industry. All popular frameworks that we use day by day once started out as such a small and insignificant idea: React, TypeScript, Tailwind, Electron, Laravel.
It's hard to imagine a world without those frameworks. And yet: if you told me 15 years ago that the JavaScript ecosystem would be where it is today, I'd have a hard time believing you.
Eric Evans once gave a talk about trying to imagine alternatives to frameworks and libraries that we're used to today. He looked at a popular Java time library — Joda Time — and wondered if it was the best solution out there. It certainly was the most used one, but did that also mean "the best"? What followed was a thought experiment about dealing with time, thinking outside the box and solving domain problems from scratch without being influenced by prior knowledge and legacy expectations.
What if we'd start from a blank slate, Eric wondered?
I think that's exactly the spot where potential revolutionary ideas are born. What if we don't take our current solutions for granted but start from scratch instead?
I find Laravel to be a good example. You could easily say that, 10 years ago, the problem of "MVC frameworks for PHP" had already been solved by several frameworks; why would we need another one? And yet, Laravel managed to outgrow every other framework in just a few years time. There's quite a few data sources confirming that:
It's true that there's no single data set with a 100% accurate representation of the real world, though I think these sources are the most accurate available, and they all confirm the same: the massive growth of a framework that didn't really solve any new problems compared to its competitors, and yet skyrocketed in popularity.
Ironically though, it's that same popularity, that will most likely mean its end in the long run.
What I like about Eric's thought experiment, is that there weren't any alternative goals: he simply wanted to take an honest look at the software he had at hand and wonder "could it be better?".
There's a hidden caveat with Eric's approach though: he can come up with the best solution in the world, all the while ignoring legacy code and backwards compatibility. If you don't have to worry about an existing user base then yes, you can come up with a better solution to almost any given problem.
Joda Time wasn't popular because it was the best, but because it grew with its users for many, many years. It was a trusted solution — sure it had its quirks, but it had proven itself more than enough.
The irony of managing popular software is that once it becomes popular, you can't promise the same quality you did at the start. You can't create the same disruption over and over again, because you need to accommodate your existing user base. In the end, you inevitably end up making compromises.
I feel like this is happening to Laravel today, 10 years after its birth. That's not a bad thing, by the way; it's a sign of maturity and stability, which are two key components in creating long lasting, valuable software.
On top of that, Laravel has solved most, if not all, of the problems there are when writing a web application. There aren't many more ground-breaking features that are missing. Every release brings some niceties, but nothing that's absolutely life critical. Meanwhile though, it takes a very long time to get up to speed with the latest PHP additions, because there's a legacy user base to keep in mind.
By satisfying their users and guaranteeing stability, any framework must stop being as disruptive as they were at the start. They must create a void that will be filled by another framework somewhere in the future.
That's exactly why Laravel grew so popular: it filled the void created by other popular frameworks. Ten years ago, that void in the PHP community was simplicity and a low-level entry barrier. That's the void that Laravel filled, and turned out to be extremely successful.
Laravel is in the same place today, where other frameworks were a decade ago. There's a subtle void being created, and it'll grow steadily for the years to come. I believe there will be a point in time where that void is large enough to spark the birth of a new framework. And while Laravel will keep being relevant for many more years — there's quite a lot of production code dependant on it — there will be another "best thing". Laravel, just like any other framework, will reach a tipping point; just like jQuery, Rails, Bootstrap, Symfony, Angular.
So the question at hand: who and what will fill that void?
I think it's possible for any framework to fill its own void, but it requires groundbreaking changes. In Laravel's case, I can come up with a few things I'd expect from a framework if it were created from scratch today:
It'll be interesting to see whether Laravel will be able to fill its own void the next decade or so. I wouldn't be surprised if it did, or at least partially.
Important note: PHP 8.2 adds a way of making whole classes readonly at once: readonly classes.
Writing data transfer objects and value objects in PHP has become significantly easier over the years. Take for example a look at a DTO in PHP 5.6:
class BlogData
{
/** @var string */
private $title;
/** @var Status */
private $status;
/** @var \DateTimeImmutable|null */
private $publishedAt;
/**
* @param string $title
* @param Status $status
* @param \DateTimeImmutable|null $publishedAt
*/
public function __construct(
$title,
$status,
$publishedAt = null
) {
$this->title = $title;
$this->status = $status;
$this->publishedAt = $publishedAt;
}
/**
* @return string
*/
public function getTitle()
{
return $this->title;
}
/**
* @return Status
*/
public function getStatus()
{
return $this->status;
}
/**
* @return \DateTimeImmutable|null
*/
public function getPublishedAt()
{
return $this->publishedAt;
}
}
And compare it to its PHP 8.0's equivalent:
class BlogData
{
public function __construct(
private string $title,
private Status $status,
private ?DateTimeImmutable $publishedAt = null,
) {}
public function getTitle(): string
{
return $this->title;
}
public function getStatus(): Status
{
return $this->status;
}
public function getPublishedAt(): ?DateTimeImmutable
{
return $this->publishedAt;
}
}
That's already quite the difference, though I think there's still one big issue: all those getters. Personally, I don't use them anymore since PHP 8.0 with its promoted properties. I simply prefer to use public properties instead of adding getters:
class BlogData
{
public function __construct(
public string $title,
public Status $status,
public ?DateTimeImmutable $publishedAt = null,
) {}
}
Object oriented purists don't like this approach though: an object's internal status shouldn't be exposed directly, and definitely not be changeable from the outside.
In our projects at Spatie, we have an internal style guide rule that DTOs and VOs with public properties shouldn't be changed from the outside; a practice that seems to work fairly well, we've been doing it for quite some time now without running into any problems.
However, yes; I agree that it would be better if the language ensured that public properties couldn't be overwritten at all. Well, PHP 8.1 solves all these issues by introducing the readonly
keyword:
class BlogData
{
public function __construct(
public readonly string $title,
public readonly Status $status,
public readonly ?DateTimeImmutable $publishedAt = null,
) {}
}
This keyword basically does what its name suggests: once a property is set, it cannot be overwritten anymore:
$blog = new BlogData(
title: 'PHP 8.1: readonly properties',
status: Status::PUBLISHED,
publishedAt: now()
);
$blog->title = 'Another title';
Error: Cannot modify readonly property BlogData::$title
Knowing that, when an object is constructed, it won't change anymore, gives a level of certainty and peace when writing code: a whole range of unforeseen data changes simply can't happen anymore.
Of course, you still want to be able to copy data over to a new object, and maybe change some properties along the way. We'll discuss how to do that with readonly properties later in this post. First, let's look at them in depth.
Readonly properties can only be used in combination with typed properties:
class BlogData
{
public readonly string $title;
public readonly $mixed;
}
You can however use mixed
as a type hint:
class BlogData
{
public readonly string $title;
public readonly mixed $mixed;
}
The reason for this restriction is that by omitting a property type, PHP will automatically set a property's value to null
if no explicit value was supplied in the constructor. This behaviour, combined with readonly, would cause unnecessary confusion.
You've already seen examples of both: readonly
can be added both on normal, as well as promoted properties:
class BlogData
{
public readonly string $title;
public function __construct(
public readonly Status $status,
) {}
}
Readonly properties can not have a default value:
class BlogData
{
public readonly string $title = 'Readonly properties';
}
That is, unless they are promoted properties:
class BlogData
{
public function __construct(
public readonly string $title = 'Readonly properties',
) {}
}
The reason that it is allowed for promoted properties, is because the default value of a promoted property isn't used as the default value for the class property, but only for the constructor argument. Under the hood, the above code would transpile to this:
class BlogData
{
public readonly string $title;
public function __construct(
string $title = 'Readonly properties',
) {
$this->title = $title;
}
}
You can see how the actual property doesn't get assigned a default value. The reason for not allowing default values on readonly properties, by the way, is that they wouldn't be any different from constants in that form.
You're not allowed to change the readonly flag during inheritance:
class Foo
{
public readonly int $prop;
}
class Bar extends Foo
{
public int $prop;
}
This rule goes in both directions: you're not allowed to add or remove the readonly
flag during inheritance.
Once a readonly property is set, you cannot change it, not even unset it:
$foo = new Foo('value');
unset($foo->prop);
There's a new ReflectionProperty::isReadOnly()
method, as well as a ReflectionProperty::IS_READONLY
flag.
So, if you can't change readonly properties, and if you can't unset them, how can you create a copy of your DTOs or VOs and change some of its data? You can't clone
them, because you wouldn't be able to overwrite its values. There's actually an idea to add a clone with
construct in the future that allows this behaviour, but that doesn't solve our problem now.
Well, you can copy over objects with changed readonly properties, if you rely on a little bit of reflection magic. By creating an object without calling its constructor (which is possible using reflection), and then by manually copying each property over — sometimes overwriting its value — you can in fact "clone" an object and change its readonly properties.
I made a small package to do exactly that, here's what it looks like:
class BlogData
{
use Cloneable;
public function __construct(
public readonly string $title,
) {}
}
$dataA = new BlogData('Title');
$dataB = $dataA->with(title: 'Another title');
I actually wrote a dedicated blogpost explaining the mechanics behind all of this, you can read it here.
Finally, I should also mention the addition of readonly classes in PHP 8.2. In cases where all properties of your class are readonly (which often happens with DTOs or VOs), you can mark the class itself as readonly. This means you won't have to declare every individual property as readonly — a nice shorthand!
readonly class BlogData
{
public function __construct(
public string $title,
public Status $status,
public ?DateTimeImmutable $publishedAt = null,
) {}
}
So, that's all there is to say about readonly properties. I think they are a great feature if you're working on projects that deal with lots of DTOs and VOs, and require you to carefully manage the data flow throughout your code. Immutable objects with readonly properties are a significant help in doing so.
I'm looking forward to using them, what about you? Let me know on Twitter or via e-mail!
]]>I've had this IKEA clock for 14 years now — that's half my life. And it's one of those things I'll be really sad about the day it stops working.
Why? This clock is a beautiful example of perfect design. Not because it's pretty, but because it's simple, has a clear goal, combined with extremely good UX.
We should design our software more like this clock. No nonsense, focussed on a clear goal and with the simplest UX possible. It just works.
Unfortunately, IKEA doesn't make this kind of clock any more (it's called "Slabang", by the way). I really hope mine will last a few more decades; I've already been looking for replacements but nothing seems to match the excellence of this clock.
]]>I want to make clear up front that I mean no disrespect to any individual and I've tried very hard to convey that in this post. If something however still comes across as disrespectful or hurtful, please reach out to me to let me know and I'm happy to revisit it.
Let's start at the beginning.
Several years ago, when the PHP-FIG created its first PSRs they started some big changes in the PHP ecosystem
They definitely did. I'd say the FIG has done an amazing job in modernizing the PHP ecosystem.
PSRs for coding standards were defined, which I'm sure helped a lot of teams to leave coding standard discussions behind.
I think it's fair to say that most professional developers are using at least some PSR, with or without their knowledge.
Next up were the PSRs that aimed for the big goal: framework interoperability. […] The idea […] was that frameworks could provide implementation packages for the proposed interfaces. So you could eventually use the Symfony router, the Zend container, a Laravel security component, and so on.
While framework interoperability is useful to some extent, it's unrealistic trying to make everything work together: you'd end up with a single framework in the end. There are only so many ways to implement a router or container, if there's one common set of interfaces shared among different frameworks then there'll be very little differences to the users of those frameworks. Sure there might be some implementation differences — but the goal of a framework is for users not to care about those, and to be able to focus on building applications instead.
Matthias shares this concern:
One of the concerns I personally had about PSR abstractions is that once you have a good abstraction, you don't need multiple implementation packages
The beauty of the two or three (or four or five, depending how you count) major frameworks in our community is that they each have their unique way of tackling problems. Each framework has its own identity that attracts a different group of developers.
Aiming for "full framework interoperability" is not only an unrealistic goal, but also something we simply don't need.
But fair enough, maybe Matthias and the FIG aren't talking about full framework interoperability, just about some parts. So let's talk about abstractions. Matthias says there's value in PSRs because they are tried and tested abstractions and you can trust them.
Take, for example PSR-18, the HTTP client interface:
interface ClientInterface
{
/**
* @throws \Psr\Http\Client\ClientExceptionInterface
*/
public function sendRequest(
RequestInterface $request
): ResponseInterface;
}
Granted, this is as simple as an HTTP client can get. It should be able to send a request and return a response. Matthias says the following about it:
It is a great abstraction already: it does what you need, nothing more, nothing less. After all, what you need is to send an HTTP request and do something with the returned HTTP response
However, there's of course that infamous RequestInterface
. Here's Matthias again:
The only problem about this interface is maybe: how can you create a RequestInterface instance?
I can resonate with that thought. Whenever I encounter a PSR-7 compliant library, I need to stop and think and search which package allows me to easily create — what I would think should be — a simple request object:
Every time I need an HTTP client I struggle with this again: what packages to install, and how to get a hold of these objects?
Matthias contemplates the idea that maybe your own, simpler abstraction is a better option?
interface HttpClient
{
public function get(
string $uri,
array $headers,
array $query
): string;
public function post(
string $uri,
array $headers,
string $body
): string;
}
"Unfortunately", he says:
[…] by creating my own abstraction I lose the benefits of using an established abstraction, being:
- You don't have to design a good abstraction yourself.
- You can use the interface and rely on an implementation package to provide a good implementation for it. If you find another package does a better job, it will be a very easy switch.
We're almost arriving at the core of my problem with the FIG these days. Sure, there's value in using tried and tested code, in not reinventing the wheel for every project. Matthias warns about the danger of doing that:
If you wrap PSR interfaces with your own classes you lose these benefits. You may end up creating an abstraction that just isn't right, or one that requires a heavy implementation that can't be easily replaced.
The benefit of using PSRs in comparison to running your own implementation, is that your own implementation raises tons of questions that have been answered by the FIG before:
- What is the structure of the $headers array: header name as key, header value as value? All strings?
- Same for $query; but does it support array-like query parameters? Shouldn't the query be part of the URI?
- Should $uri contain the server hostname as well?
- What if we want to use other request methods than get or post?
- What if we want to make a POST request without a body?
- What if we want to add query parameters to a POST request?
- How can we deal with failure? Do we always get a string? What kind of exceptions do these methods throw?
So why spend time and money on creating another abstraction while we already have one?
Well, have you considered the fact that… maybe the FIG doesn't always come up with the best abstractions? That maybe the process that takes months of discussion by a small group of developers, to finally come up with an interface that contains a few methods, might not actually solve the problems that developers are dealing with in real life?
Sure the question of creating HTTP requests and responses has been answered by the FIG. It's an answer. Symfony and Laravel both have their own answer as well which are simpler if you ask me. Moreover, possibly better answers for my use cases.
We've been talking about HTTP abstractions, but Matthias gives another example: the container interface. He agrees that the FIG doesn't always come up with an abstraction that's relevant to the community's needs:
Without meaning to discredit the effort that went into it, nor anyone involved, there will always be standards that end up being outdated, like in my opinion PSR-11: Container interface.
Matthias carefully uses the word "outdated" here, though I want to say it's a plain irrelevant abstraction. The same way PSR-7 is irrelevant for most of the work I — and many others — are doing in Laravel or Symfony projects. I'm more than happy to ditch "interoperability" — what does that even mean when you're building a project closely tied to a framework and never intend to change it — and just use a simpler, straight forward, opinionated solution. It's also an abstraction, a good one, just not one that has an "official" name backed by the FIG.
Now some people tell me "you can't predict the future, maybe you do want interoperability somewhere in the next few years". I don't know about others but we actually outline the scope of projects in contracts with clients. They pay us to make an application specifically in one framework. There's no need for this level of interoperability.
Take a look, for example at both the container interface implementations of Symfony and Laravel, both implement \Psr\Container\ContainerInterface
, and yet both add so many more methods. Implementing PSR-11 is merely a gimmick here for frameworks to be able to say "yes, we're PSR compliant"; because there's no real interoperability between these two.
Another example: PSR-7, the HTTP messages PSR. Both Symfony and Laravel don't implement PSR-7, because it simply doesn't solve their use cases. Oh and, just to be able to say "yes we're PSR compliant", there's the PSR-7 bridge, which basically applies the adapter pattern.
Do you realise that applying the adapter pattern is exactly the opposite of what the FIG is trying to achieve? The FIG wants a common abstraction to be used across frameworks, while the adapter pattern allows one interface to be used as another interface.
I want to end with making a slight change to Matthias' last sentence:
At the same time, we should also use PSR abstractions whenever it makes sense, since they will save us a lot of design work and will make our code less sensitive to changes in vendor packages.
I'd phrase it like this: "we should use abstractions whenever it makes sense". Whether it's the tried and tested Laravel or Symfony implementation of the container, HTTP client, caching, queuing, … These implementations work. They are valid abstractions, even if they don't carry the "PSR" name.
Yes, we should use abstractions; but no, we shouldn't use irrelevant and outdated abstractions. It's not because an abstraction carries the name "PSR" that it's suddenly better than others.
I figure there's a chance of some people getting angry by this post. You're allowed to, I’m open for that feedback. Please reach out to me via mail to tell me your thoughts. I don't question the sincerity and efforts of the FIG. I just genuinely believe they are trying to solve a problem that doesn't exist.
Let me end with how I started: the FIG has had a great impact on the PHP community, I'm very thankful for the early work they did as pioneers and the whole community needs to acknowledge that. I also think the FIG has reached its goal, and the project should be called complete.
As an addendum, I want to address one more point. I shared Matthias' original post on /r/php before publishing this one. There were some insightful discussions about it, and Matthieu Napoli, the author of PSR-11, pitched in.
I want to address one of the things he said, because I reckon it might be a counterargument that people bring up after reading this post as well. He said:
PSR-11 is great for libraries that want to interoperate with containers, for example:
- Phinx to allow loading seed classes from your framework's container
- Behat for doing dependency injection in feature contexts
- Tactician command bus: load from your container (https://tactician.thephpleague.com/plugins/container/)
- Faker for dealing with extensions
- schmittjoh/serializer for lazy loading handlers from your container
In other words: I mainly look at PSRs from a framework's perspective, frameworks like Symfony or Laravel, while Matthieu is thinking about smaller, standalone, packages.
Here's one of the examples Matthieu gave in practice. Phinx optionally supports to set a container instance, which it'll use to resolve seed classes.
if ($this->container !== null) {
$seed = $this->container->get($class);
}
And, fair enough: there seems to be some adoption at least in small, standalone packages. But what about the broader context? Are those features actually used by end users? Would it be worse if those package provided an adapter layer instead of relying on a third-party abstraction? There are a lot of ifs here, and I'd like to hear from users who actually have real-life experience with using these kinds of packages.
I mainly look at the FIG from a framework's point of view, I think that's relevant since it's the PHP framework interoperability group. I'm not entirely dismissing the merits of proper abstractions, I hope that was clear throughout this post.
]]>I want to nuance those arguments a little bit.
The main fear is that supporting named arguments in, for example, a framework or open source package will increase the risk of breaking changes.
Imagine a package or framework exposing this class:
class QueryBuilder
{
public function join(
string $table,
string $leftColumn,
string $rightColumn,
string $type
) { /* … */ }
}
The problem with named arguments is that if users call this function with them, the framework now needs to treat parameter name changes as possible breaking ones:
$query->join(
type: 'left',
table: 'table_b',
leftColumn: 'table_a.id',
rightColumn: 'table_b.table_a_id',
)
If the framework wants to rename leftColumn
and rightColumn
to simply left
and right
, the above code — userland code — would break.
Here's the thing: no framework or package can prevent users from using named arguments, there simply isn't a way to disallow them. So either, as an open source maintainer, you:
Being an open source maintainer myself: I choose option three. First of all: argument name changes only rarely happen; and second: I trust my users to be professional developers and know the consequences of using named arguments. They are smart grown ups, it's their responsibility.
So in summary for this first part: there's nothing the framework can do to prevent this kind of backwards compatibility issues besides making a note in the README on how they deal with argument name changes. Be consistent with whatever policy you choose, and you're fine.
The second way named arguments can be used, is in combination with variadic functions, essentially becoming a — in my opinion cleaner — shorthand for passing arrays of data:
$user = User::create(
name: 'Brent',
email: 'brendt@stitcher.io',
company_id: 1,
);
This is possible thanks to named arguments playing well together with variadic functions:
class User
{
public function create(...$props) { /* … */ }
}
Passing a named argument list into this variadic create
function will result in an array like this:
[
'name' => 'Brent',
'email' => 'brendt@stitcher.io',
'company_id' => 1,
]
Rewriting the above example without named arguments but arrays instead, would look like this:
$user = User::create([
'name' => 'Brent',
'email' => 'brendt@stitcher.io',
'company_id' => 1,
]);
I know which one of these two approaches I prefer. Disclaimer: it's the one that has better syntax highlighting and is shorter to write.
Here's the kicker: there isn't any possibility for breaking changes, because there aren't any hard coded argument names to begin with!
We really need to be more thoughtful about claiming that we cannot support named arguments in our open source packages because of backwards compatibility issues. In the first case there's nothing you can do either way, and the second case doesn't pose any danger of breaking changes.
Don't you agree? Send me an email or tweet and we can further discuss it. I'm open to be proven wrong.
/** @var \App\Models\Foo[] */
$arrayOfFoo = …
Or like this:
/** @var Foo[] */
$arrayOfFoo = …
Of course, the second example assumes you've imported the full class name at the top of your file.
use \App\Models\Foo;
// …
/** @var Foo[] */
$arrayOfFoo = …
I've asked this question maybe three or four times over the years, and I consistently get the same answer from a large group of respondents: they use the full class name, so that they don't have to scroll to the top of the file to know what they are dealing with.
My response, time and time again, has been: so what about real types? Property types, argument types, return types? Do you use the full class name in those cases as well?
class Bar
{
public function baz(\App\Models\Foo $foo): \App\Models\Foo
{
// …
}
}
I've actually had one person say "yes" to that question, and fair enough, they are consistent. But all the others say they don't. They write it like this:
use \App\Models\Foo;
class Bar
{
public function baz(Foo $foo): Foo
{
// …
}
}
So what's the difference between doc block types (which are required in some cases because PHP's type system is limited), or real types? Why do you want to import one, but not the other; and how does "not scrolling to the top" make a good argument when it isn't consistent?
Programmers often take pride in their rational thinking. We look at problems from a slightly different angle than non-programmers do. It's probably this personality trait that got many people into programming to begin with.
I don't mind that people aren't consistent in how they write types in or out of doc blocks. But I am always surprised with how difficult it is to defend that opinion, once you start asking deeper questions, once people are forced to think about it a little more.
It makes me wonder, could it be that there are more such opinions that we think we're sure of; but that, in reality, we haven't actually thought through all that well? Tabs or spaces, light or dark colour schemes, active record or entity mapper, dependency injection or service location, …
How many "rational opinions" do we have that turn out to be irrational after all? Opinions that are habit-driven; that we think of as "the best option" — not because they are, but because they've worked in the past and we're comfortable using them.
Are we — the "rational thinkers" — in fact, just like everyone else, influenced by emotion, sometimes without even knowing it?
The real reason to always use full class names in doc blocks, by the way, is because PHP doesn't have a reflection API for import statements. So if you want to map a class name from a doc block to its FQCN, you'd need to manually parse that PHP file. Mind you: import statements are quite difficult to parse: there are aliases, partial namespace imports, grouped imports, function imports, and more.
Fortunately, the problem I describe here isn't really a problem anymore. There's a package called phpdocumentor/type-resolver
that supports exactly this kind of parsing.
So the only real argument against importing doc block types, turns out to be not so relevant anymore.
How sure are we of our opinions? How fiercely and emotionally do we defend those opinions, even when we haven't thought them through all that well? And can we admit it when we're wrong, maybe apologise and move on?
I don't want to start a fight over tabs or spaces, importing types or not; but I do want to encourage you to critically look at your own opinions. Wonder whether you thought them through well enough; and if you'd be willing to change them, if they turn out to be more biased than you thought.
Are you angry right now? Do you want to tell me I'm wrong? Send me an email or a tweet and we can have a proper internet fight! No really, I would very much appreciate you challenging these thoughts if you don't agree, I'm looking forward to hearing from you!
Oh and if you want to stay up-to-date about my content and these kinds of posts, consider subscribing to my newsletter.
This is going to be a slightly different style of newsletter than you're used to. I'm not going to blog or tweet about this, I just want to share these thoughts with you, because you can hit the reply button and let me know your feedback, one on one.
So: why do I write? "Being a content creator" has been a hobby of mine for many years now (I actually made Minecraft videos before writing a blog, believe it or not) and I've always wanted to stay true to myself. My number one rule is that I only write about what I want, when I want to.
Yesterday though, my colleague Sebastian sent out a new edition of his newsletter that made me stop and think about where I am today. You should totally subscribe to Sebastian's newsletter by the way, I'm always happy to receive one of his mails in my inbox.
Anyway, Seb wrote about how we live in a culture where it seems that everyone and their grandma is working on a successful side-project, and how that affects him. He called it the the monetization trap — a must read!
I immediately told him I felt the same way he described in his post, and we had a small conversation about it. During our conversation, he shared an awesome blog post — another must read — and I must admit it struck a nerve with me. Here's a quote from that post:
I see a lot of ["fast food content"] on the web sadly. People who once had something to say that are now trapped in an endless cycle of recycled content, month after month, year after year, saying the same thing, over and over and over again.
I felt at least a little shame: I've done this myself. Maybe not to extremes, but I have written blog posts in the past, because I knew people like to read them, because I knew they have a high chance of going viral and giving me an adrenaline rush for a day, maybe two.
I was suddenly reminded of my number one rule, and came to realise that I might not always have followed it, even though I thought I did.
Now, I don't regret writing those kinds of posts, in fact I like to believe some of them have had a significant impact on people's professional developer's life (some people have told me it did). And yet, those aren't the posts I'm most proud of.
In fact, the ones I'm most proud of are actually the least popular ones.
Looking back, not following that one rule might have caused some levels of added stress these past few months, and I'm thankful for Seb's and Manu's writings to make me realise that.
Having named the problem and knowing it's there is actually very comforting, it means I can now work on it. It'll be good to focus on things that I want to, instead of things I must. We'll see what the future brings.
Having written all of this, I'm actually thinking of reposting it on my blog one day, because maybe there are more people out there experiencing the same pressure — consciously or not. I want to see your responses first though, so feel free hit the reply button if you have any thoughts! I'll try my best to answer soon.
Kind regards
Brent
I'd like to ask you — the blog reader — the same: if you have any thoughts to share about this topic, please send me an email (if the mailto link doesn't work: brendt@stitcher.io). I'll try my best to answer all mails, it might take a while!
If this post resonates with you, please consider sharing it with your audience. I think there's an important message here, but I'll let you be the judge of that.
Thanks for reading! This post is part of my "Dev Diaries" series where I write about my own and personal experiences as a developer. Would you like to read some more?
If you want to stay up to date about what's happening on this blog, you can follow me on Twitter or subscribe to my newsletter:
Worried about spam or followup mails? You'll be automatically unsubscribed after those 10 days, I'm not keeping your email address!
You can subscribe here, and see whether it's your thing or not.
]]>TypeError
in one of my projects? To be honest, I can't remember, so it's probably a few years. Coincidentally, I also started relying on static analysis around the same time.
I'm fairly certain that I could disable PHP's runtime type checking altogether — if that was a thing — and have a perfectly working codebase.
Because, here's the thing about runtime type checks: they are a debugging device, not so much a safety net. Runtime type errors make it easier to detect and fix bugs, but the reality is that whenever a type error is triggered, our code still crashed at runtime. When type errors occur in production, the end result is the program crashing, nothing you can do about it.
Now, I've written about type systems before, you'll find some references at the end of this post; so if you want more background information about them be sure to do some followup reading. Today, I want to discuss how static analysis has the power to revolutionize the way we write PHP code much more than it already does today, and how it can open doors to many new possibilities.
The tradeoff? We need a community-wide mind shift: there are still many PHP developers (including internal developers) who are taken aback by the idea of static type checking. My only goal today is to encourage you to think outside your box, to imagine what would be possible if PHP shifted towards a built-in, statically type-checked model.
Whether you're into static type systems or not, I promise it'll be interesting nevertheless. Let's dive in!
A while back, it became clear that generics in PHP are probably not coming any time soon. One of the main reasons being that there are two ways to implement them, and both have significant problems. Either there's too large an impact on runtime performance, or the implementation is just way too complex to get right.
Both approaches did assume a runtime type-checked implementation though. So I shared a thought experiment with internals: what if we only need to support the syntax for generics, and have static analysers do all the checks? I called them transpiled generics back then, but runtime-erased or runtime-ignored generics is probably a better name.
My thinking was that adding support for generic syntax shouldn't be all that hard, and without runtime type checks, there shouldn't be any performance impact. It makes sense if you think about it from a static analysis point of view: your code has already been analysed, and it's been proven to work correctly, so there's no more need for runtime type checks. On top of that, most developers only want generics as a way to get better code insights while coding; does the "I want to know what items are in an array" argument ring a bell?
Of course there could still be some type information exposed at runtime via reflection, but we can't deny that having types ignored at runtime is a major paradigm shift that most PHP developers aren't used to. Here's Sara's response on my runtime-ignored generics idea (which Hack already does) and how it requires a mind-shift:
Oh, I agree that there's real value in HackLang's approach. It's just that there is a mountain of inertia around "The PHP Way" and it's going to take an equal and opposite mountain to alter course. Entirely possible, even probable, but we won't see that level of shift in the next five years.
It'll take a few more years, but it's probably the way PHP will evolve anyway, according to Sara.
Now, before I get an angry mob chasing me: I'm not suggesting we bundle a static analyser in PHP that you're required to run (that would mean a "compilation" step in practice). What I am suggesting is that we can disable PHP's runtime type checks if we want to, and deal with the consequences ourselves. If you want to use generics in such a scenario then, yes, you'll have to use a static analyser. If you don't want to, that's fine, but you won't be able to use generics.
Of course, in an ideal world, PHP would ship with such a built-in, opt-in static analyser; instead of users having to rely on third party tools. Because the main problem with third party tools is consistency between them. Case in point: PhpStorm will support a basic form a generic-type doc blocks in their next release, years after Psalm and PHPStan added support for them.
If there was an official spec supported by internals, static analysis vendors wouldn't have any choice but to follow that spec. That's the major problem with doc block type checks at the moment: there are no rules, so every vendor does whatever they want.
The idea of a centralised static analyser isn't new, by the way, but you can imagine it's a massive undertaking to get right. Here's Rasmus on the matter a few years ago:
Now if the RFC was a plan for baking a compile-time static analysis engine into PHP itself, that would be interesting. But that is a massive project.
When I asked Nikita about the idea of runtime-ignored types and generics, he described the main problem with generics that have a runtime implementation like so:
Complexity is a pretty big problem for us, and I think severely underestimated by non-contributors. Feature additions that seem simple on the surface tend to interact with other existing features in ways that balloon the complexity.
He also called runtime-erased generics "the cowards way out". The reason Nikita says that is because, if runtime-erased generics were supported, it would mean there's a huge inconsistency within PHP's type system where some parts are checked at runtime, and other parts are checked statically.
So to be clear: I don't think runtime-erased or runtime-ignored generics are where we should start. We first need PHP without runtime type checks whatsoever, and then we can think about building on top of that.
I asked Nikita if he thought such a version of PHP would have merit, he said this:
I think that would be a good thing... but then again, lots of things would be different in PHP if we'd do a clean-slate redesign now. We have to work within the constraints we have, somehow.
From a userland-developer point of view, I think we can work with the given constraints, as long as there's a large enough user base supporting these ideas.
So what if internals don't think of it as achievable to optionally step away from runtime type checks? Or what if such a mind shift won't happen within the next decade?
Well, there is another approach, one that actually has been tried and proven before in another language: TypeScript. The power and immense popularity of TypeScript comes from its static type checker. Sure, there's an extra compilation step to transform TypeScript code to regular JavaScript, but developers seem to manage that just fine — because they know how much they gain from proper static analysis.
The same has been tried in PHP before, by the way: there was Hack that at one point did compile to PHP, and there was preprocess, a project by Christopher Pitt. Unfortunately, Hack took another direction, and preprocess halted; not because of implementation problems, but because of lack of support in IDEs and the wider community.
If transpiling PHP gains more traction again, it'll definitely need proper IDE support if we ever want a chance for it to succeed. That's the benefit of an internals-backed implementation: when it's in PHP core, IDEs and other external tooling can't do anything but to follow along. A community-driven transpiler wouldn't have that benefit.
So, this is where we are today:
I see much more potential for static analysis. It has made my code more stable and easier to write, and I couldn't do without it anymore. On the other hand, the community and toolset has still a long way to go, and we're all playing a part in that journey.
I'd love for internals to further explore the static analysis side of PHP: it's more than just a userland addon to the language, and will only continue to grow tighter to PHP in the future.
I'd want to see these changes to the language today, though I know that's an unrealistic expectation. I hope Sara's assessment is right in that this is the form PHP is evolving to, but unfortunately it'll take a few more years to get there. This blog post is just an attempt to give one more little push in the right direction.
What's your opinion? Let me know.
I'll probably add some more information to this section when people read this post and share their feedback, though I could already think of a couple of things.
Someone mentioned the idea about using PHP without runtime type checks by using a combination of Psalm and Rector: Psalm first analysed the codebase, and Rector removed all type hints afterwards to generate a "compiled" production build.
It's an interesting step to further explore the problem space, and while it doesn't mean there's support for custom syntax, there might be potential in the Psalm + Rector combo.
The classic question that gets asked by skeptics: why not use Java or C# or whatever other language if you want to rely on static analysis so much?
Well, the answer is simple: the ecosystem.
The main problem with doc block types is one of consistency, which maybe the FIG could solve?
I'm not sure about the relevance of the FIG these days: I can't imagine the FIG having an influence over the development of, for example, PhpStorm; and there are very little significant frameworks still following PSRs to the rule.
So, yes, maybe? I'd love to be proven wrong.
Python's whole type system is built on type-erasure. So it's interesting to note that what I'm proposing here isn't anything new.
]]>Over time, some kind of dynamic has grown between our client and I. They ask me "Brent, would it take much work to build … ?", to which I always reply: "everything is possible, it just depends on how much you want to pay"; our client in turn always says "I know! But I need an accurate estimate first!".
Our client has told me several times that my estimates are actually pretty accurate, so I guess I must be doing something right, and I figure I'd share my process — based on nothing but my own experience — with you. Maybe it can help someone out there.
First things first, everything that's between 2 and 4 hours of work, I feel comfortable estimating with a buffer in mind. I usually multiply my original idea by a factor of 2, and that seems to result in accurate, small-scale estimates almost every time.
The problems start with features that take longer than a few hours. I don't think the human brain is capable of fully comprehending the scope of work at these scales, and this is a problem that's only growing more significant the larger the scope becomes.
Personally, I have a tendency to approach my estimates very optimistically: "if everything goes right, it'll probably take three weeks". And that's what I used to tell my clients. The problem with that approach is that they only hear "three weeks", and forget about the "if everything goes right" part.
I've now learned that nothing ever goes "just right"; and that there will always be unforeseen changes. I should account for those as well.
So I ask my client: would you like to have an optimistic or realistic estimate? In practice, my realistic estimate is simply the optimistic one, but multiplied by two or three, and represented as a range. For example: if my optimistic estimate is 2 weeks, I'll tell my client it'll take somewhere between 2 weeks and 4 or 5 weeks. If the optimistic estimate already spans months, I prefer to multiply by three, because the larger the estimate, the more things that can change.
I've been using this tactic for years now, and it seem to actually work pretty well.
What I think is key here, is that there's an open communication between my client and myself. There's a level of trust, and I feel safe to just tell them the truth, even though I know sometimes they might have liked a more optimistic version.
This level of trust and communication comes from working together for a couple of years though. It might be lacking at the start of a new project. That's why so many starting projects are under-estimated, and why there's almost always a point in the project where expectations need to be adjusted (with all the consequences that come with it, these usually aren't happy times in a project's lifespan).
So I try to ask clients up front: do you want an optimistic or realistic estimate? I tell them I know the realistic one will be better in the long run.
A long-awaited feature, enums are coming! There's little to be said about this one, besides that I'm looking forward to not having to use spatie/enum or myclabs/php-enum anymore. Thanks for all the years of enum support to those packages, but they are the first I'll ditch when PHP 8.1 arrives and when I change this:
/**
* @method static self draft()
* @method static self published()
* @method static self archived()
*/
class StatusEnum extends Enum
{
}
PHP 8.0
To this:
enum Status
{
case draft;
case published;
case archived;
}
PHP 8.1
This one might seem like a small one, but it has bothered me more than once: only list arrays could be unpacked before PHP 8.1:
$a = [1, 2, 3];
$b = [4, 5, 6];
// This is allowed
$new = [...$a, ...$b];
PHP 8.0
While arrays with string keys cannot:
$a = ['a' => 1, 'b' => 2, 'c' => 3];
$b = ['d' => 4, 'e' => 5, 'f' => 6];
$new = [...$a, ...$b];
// You'd need to use array_merge in this case
$new = array_merge($a, $b);
PHP 8.0
And so, one of the great features of PHP 8.1 that will make my life easier, is that arrays with string keys can now be unpacked as well!
$a = ['a' => 1, 'b' => 2, 'c' => 3];
$b = ['d' => 4, 'e' => 5, 'f' => 6];
// :)
$new = [...$a, ...$b];
PHP 8.1
Another stunning new feature is once again such a quality-of-life improvement that I've struggled with for years: default arguments in function parameters. Imagine you'd want to set a default state class for a BlogData
object. Before PHP 8.1 you'd have to make it nullable and set it in the constructor:
class BlogData
{
public function __construct(
public string $title,
public ?BlogState $state = null,
) {
$this->state ??= new Draft();
}
}
PHP 8.0
PHP 8.1 allows that new
call directly in the function definition. This will be huge:
class BlogData
{
public function __construct(
public string $title,
public BlogState $state = new Draft(),
) {
}
}
PHP 8.1
Speaking of huge, have I mentioned yet that readonly properties are a thing now?!?
class BlogData
{
public function __construct(
public readonly string $title,
public readonly BlogState $state = new Draft(),
) {
}
}
PHP 8.1
Oh and don't worry about cloning, by the way: I've got you covered.
As if that wasn't enough, there's now also the first-class callable syntax, which gives you a clean way of creating closures from callables.
Previously you'd had to write something like this:
$strlen = Closure::fromCallable('strlen');
$callback = Closure::fromCallable([$object, 'method']);
PHP 8.0
In PHP 8.1, you can do… this:
$strlen = strlen(...);
$callback = $object->method(...);
PHP 8.1
There are even more features in PHP 8.1, but these are the ones I'm most excited about. What's your favourite one? Let me know on Twitter!
]]>Here's how it works: every time a user watches one of our course videos, every time they complete a video series or a pull request gets merged on GitHub, they'll receive experience points. On top of that, there are also some achievements to reward users for their efforts. For example, there's a "10 pull requests" achievement, as well as a "100 XP" achievement and a few others.
Getting an achievement will reward the user with a digital badge, and in some cases they'll get some kind of certificate as well.
As you can see, it's a small, contained system, with well-defined boundaries. And I'd like to discuss one aspect of it with you today.
Let's zoom in on one of the possible flows: the pull request reward system. There are a few steps to it:
Writing this flow down in bullets makes it feel like the pull request reward system is a linear process. In fact, we could write it like so:
class RegisterPullRequest
{
public function __invoke(PullRequestData $data): void
{
// Persist the pull request
$pullRequest = PullRequest::create(...$data);
$user = $pullRequest->user;
// Award XP
$user->award(10);
$pullRequestCount = count($user->pullRequests);
// Determine whether an achievement should be triggered
if (in_array($pullRequestCount, [10, 50, 100])) {
$achievement = new PullRequestAchievement($pullRequestCount);
// Persist the achievement
$user->unlock($achievement);
// Notify the user
$user->notify(new AchievementNotification($achievement));
}
}
}
You might want to refactor this code to separate methods or to use injected actions or service classes if you prefer that style of programming; but I wanted to keep this example concise, to easily clarify a flaw with this approach.
This code clearly represents the ordered steps we listed at first, but there are some hidden costs that come with it; costs that might not be apparent at the time of writing it. I can see two problems hidden within this implementation:
Let's consider "awarding XP" and "unlocking achievements" for a moment. These are two equally important parts of our system. In fact, there's also an achievement for XP being awarded, which means that our current implementation is either lacking, or that there's some added functionality in $user->award(10);
that we don't know about. Let's assume the latter for now.
Even though these two parts are equally important and not directly dependent on each other, we've combined them into one process because it seems like they belong together. However, an unfortunate side effect of doing so, is that our RegisterPullRequest
class is growing larger and more complex. Making a change to how pull request achievements are handled, will inevitably take us to the same place where XP rewards are handled.
While you might find it still relatively easy to reason about this isolated (simplified) example, I think most of us can agree that yes, in fact, we're mixing several processes together into one: we're creating some kind of "god-class" that manages and oversees a complex process. We've created a single point of failure. And the more complex our business becomes, this code has the potential to grow larger, more complex and more difficult to reason about.
Speaking for myself, I've written these kinds of classes more than I'd like to admit, and I've seen it applied in many other code bases as well. And from experience, I can tell you they grow much larger than the example we're working with today.
I understand why we get to this point: we'll always need some kind of entry-point in our code, no? A complex process will need to be tied together somehow; we can't avoid that, right?
When I first learned about event-driven systems, I was hesitant, maybe even skeptical about them. Events introduce an unavoidable layer of indirectness to our code that makes it more difficult to follow the "flow of our program". However, keeping everything tightly coupled together also makes it difficult to understand our program flow, just in another way.
The indirectness of event-driven systems is actually the solution to our problem. While event-driven architecture might feel overly complex at first glance, it offers exactly the kind of flexibility we need to model our processes in a clean way — and better yet: in a way that's scalable and maintainable for years to come, much more than our current solution.
In an event-driven system, both "XP rewards" and "achievement unlocks" are treated as two standalone systems. They don't need to know of each other. The only thing they need to know is when a pull request is merged — when an event happens.
Both our systems are now event listeners that will act whenever a PullRequestMerged
event is dispatched:
class AchievementManager
{
public function __invoke(PullRequestMerged $event): void
{
$pullRequestCount = User::find($event->userId)
->pullRequests
->count();
if (! in_array($pullRequestCount, [10, 50, 100])) {
return;
}
$achievement = new PullRequestAchievement($pullRequestCount);
$user->unlock($achievement);
$user->notify(new AchievementNotification($achievement));
}
}
class ExperienceManager
{
public function __invoke(PullRequestMerged $event): void
{
$user = User::find($event->userId);
$user->award(10);
}
}
Now that these two systems are separated, it's much easier to reason about them because they live in isolation.
It doesn't stop there, by the way. What about that "achievement for a given amount of XP" I mentioned at the beginning of this post? ExperienceEarned
could be an event itself that our AchievementManager
listens for as well:
class AchievementManager
{
public function onPullRequestMerged(PullRequestMerged $event): void
{ /* … */ }
public function onExperienceEarned(ExperienceEarned $event): void
{
$user = User::find($event->userId);
$currentCount = $user->experience;
$previousCount = $currentCount - $event->amount;
if ($previousCount >= 100) {
return;
}
if ($currentCount < 100) {
return;
}
$achievement = new ExperienceAchievement('100 XP!');
$user->unlock($achievement);
$user->notify(new AchievementNotification($achievement));
}
}
You might even begin to see some opportunities yourself: what about sending a mail after an achievement was unlocked? That could also be driven by an event, so that AchievementManager
doesn't need to think about it — we could add a listener that handles mails. What about persisting the pull request to the database? That could be event-driven as well. Achievements that earn experience? The list goes on.
This is the beauty of event-driven systems: by removing tightly-coupled components, we allow room for much more flexibility, while keeping our individual components small and sustainable. Besides that, events are an excellent way of handling micro-service messaging, horizontal scaling and more — though, discussing all these benefits would be too much to cover in one blog post.
Of course, I'm also glossing over some important details: what about eventual consistency? Or what about persisting events themselves? There's much more to event-driven systems than what I showed today, but I did show you the power of thinking with events. That idea alone has revolutionized the way I look at code, and I hope you will give it some more thought as well.
If you're really interested in the topic, I'd like to share my course on event sourcing in Laravel with you. It's an in-depth course that covers many topics related to event-driven systems, and you don't need any prior Laravel or PHP knowledge to learn tons of stuff from it. You can read a sample chapter or two if you'd like to know more.
With all of that being said, let me know your thoughts on Twitter and leave a like if you appreciated this post, thanks!
]]>In PHP 8.1, readonly properties aren't allowed to be overridden as soon as they are initialized. That also means that cloning an object and changing one of its readonly properties isn't allowed. It's likely that PHP will get some kind of clone with
functionality in the future, but for now we'll have to work around the issue.
Let's imagine a simple DTO class with readonly properties:
class Post
{
public function __construct(
public readonly string $title,
public readonly string $author,
) {}
}
PHP 8.1 would throw an error when you'd clone a post object and tried to override one of its readonly properties:
$postA = new Post(title: 'a', author: 'Brent');
$postB = clone $postA;
$postB->title = 'b';
Error: Cannot modify readonly property Post::$title
The reason why this happens is because the current readonly implementation will only allow a value to be set as long as it's uninitialized. Since we're cloning an object that already had a value assigned to its properties, we cannot override it.
It's very likely PHP will add some kind of mechanism to clone objects and override readonly properties in the future, but with the feature freeze for PHP 8.1 coming up, we can be certain this won't be included for now.
So, at least for PHP 8.1, we'll need a way around this issue. Which is exactly what I did, and why I created a package that you can use as well: https://github.com/spatie/php-cloneable.
Here's how it works. First you download the package using composer, and next use the Spatie\Cloneable\Cloneable
trait in all classes you want to be cloneable:
use Spatie\Cloneable\Cloneable;
class Post
{
use Cloneable;
public function __construct(
public readonly string $title,
public readonly string $author
) {}
}
Now our Post
objects will have a with
method that you can use to clone and override properties with:
$postA = new Post(title: 'a', author: 'Brent');
$postB = $postA->with(title: 'b');
$postC = $postA->with(title: 'c', author: 'Freek');
There are of course a few caveats:
with
method will be a shallow clone, meaning that nested objects aren't cloned as well.I imagine this package being useful for simple data-transfer and value objects; which are exactly the types of objects that readonly properties were designed for to start with.
For my use cases, this implementation will suffice. And since I believe in opinion-driven design, I'm also not interested in added more functionality to it: this package solves one specific problem, and that's good enough.
Such a request/dto setup usually looks something like this. Here's the request class handling validation of raw incoming data:
class PostEditRequest extends Request
{
public function rules(): array
{
return [
'title' => ['required', 'string', 'unique:posts,title', 'max:255'],
'status' => ['required', 'string'],
'body' => ['required', 'string'],
'date' => ['required', 'date_format:Y-m-d'],
'author_id' => ['nullable', 'exists:authors,id'],
'tags' => [new CollectionRule(Tag::class)],
];
}
}
And here's the DTO that represents that data in a way so that PHP, our IDE and external static analysers can understand it (note that I'm using our data-transfer-object package here):
class PostEditData extends DataTransferObject
{
public string $title;
public PostStatus $status;
public string $body;
public Carbon $date;
public ?string $authorId;
#[CastWith(ArrayCaster::class, itemType: Tag::class)]
public array $tags;
}
Finally, there's the controller in between that converts the validated request data to a DTO and passes it to an action class to be used in our business processes:
class PostEditController
{
public function __invoke(
UpdatePostAction $updatePost,
Post $post,
PostEditRequest $request,
) {
return $updatePost(
post: $post,
data: new PostEditData(...$request->validated()),
);
}
}
I've been thinking about the overhead that's associated with this two-step request/dto transformation. In the end, we only really care about a valid, typed representation of the data that's sent to our server, we don't really care about working with an array of raw request data.
So why not do exactly that: have a way for our request classes to be properly typed, without the overhead of having to transform it manually to a DTO?
I could build up some suspense here to get you all excited about it, but I trust my readers to be able to draw their own, informed conclusions, so I'm just going to show you what it would look like in the end:
class PostEditRequest extends Request
{
#[Rule(UniquePostRule::class)]
#[Max(255)]
public string $title;
public PostStatus $status;
public string $body;
#[Date('Y-m-d')]
public Carbon $date;
public ?string $authorId;
#[Rule(CollectionRule::class, type: Tag::class)]
public array $tags;
}
Some people might say we're combining two responsibilities in one class: validation and data representation. They are right, but I'd say the old approach wasn't any different. Take a look again at the rules
method in our old request:
class PostEditRequest extends Request
{
public function rules(): array
{
return [
'title' => ['required', 'string', 'unique:posts,title', 'max:255'],
'status' => ['required', 'string'],
'body' => ['required', 'string'],
'date' => ['required', 'date_format:Y-m-d'],
'author_id' => ['nullable', 'exists:authors,id'],
'tags' => [new CollectionRule(Tag::class)],
];
}
}
We're also validating type information here, it's just more hidden and can't be interpreted by an IDE or other static analyser. The only thing I suggest we do different is to properly use PHP's built-in type system to its full extent, and fill the gaps for more complex validation rules with attributes.
Finally, our controller could be refactored like so:
class PostEditController
{
public function __invoke(
UpdatePostAction $updatePost,
Post $post,
PostEditRequest $data,
) {
return $updatePost(
post: $post, data: new PostEditData(...$request->validated()), data: $data,
);
}
}
I didn't just come up with this idea by the way, there are a number of modern web frameworks doing exactly this:
And, finally: I don't think implementing this in Laravel would be all that difficult. We could even create a standalone package for it. All we need to do is build the request rules dynamically based on the public properties of the request, and fill them whenever a request comes in. I reckon the biggest portion of work is in creating the attributes to support all of Laravel's validation rules.
Anyway, I'm just throwing the idea out there to see what people think of it. Feel free to share your thoughts on Twitter with me.
]]>I made sure to let Jordi know how much I appreciated all the work he put into this, but also told him I felt kind of sad the series would end. While real-time stats are definitely useful, I did enjoy the occasional post that popped up every 6 months or so. They were a good interpretation of the data, and always sparked interesting discussions.
So I asked Jordi if he'd be ok with me continuing the series, which he was. And so, here we are, with a brand new version stats update!
I will keep a slightly different schedule though: I'll post in July and January, because I'm interested in seeing early adaption of the newest PHP version a month or so after it's released (which is usually at the end of November). I admit I'm two days early with this one, that's because of some upcoming holiday plans. And one more disclaimer: the data used in this post only looks at the composer ecosystem, and while composer represents a large part of the PHP community, it doesn't represent the whole.
With all of that being said, let's dive in!
Let's start with the raw numbers: the percentage of PHP versions being used:
Version | % |
8.0 | 14.7 |
7.4 | 46.8 |
7.3 | 19.2 |
7.2 | 10.4 |
7.1 | 3.8 |
7.0 | 1.3 |
5.6 | 1.1 |
Note that I've excluded versions with less than 1% usage, in practice this means everything older than 5.6, as well as 8.1 (which hasn't been released yet) is excluded. Let's visualise this year's data and compare it to last year's:
Evolution of version usage, now and in May, 2020
PHP 7.4 has seen significant growth compared to last year. In fact, it looks like many people went straight from older versions to 7.4. Let's hope we'll be able to observe the same trend next year for PHP 8. Major version updates tend to take a little longer though; I assume that's because developers are wary of the breaking changes associated with them. Good news though: the update from PHP 7.4 to PHP 8 is actually surprisingly easy, thanks to the deprecation work that has been done in the 7.x series. It might be worth considering updating soon, not only because active support for PHP 7.4 ends in less than half a year, but also because PHP 8 brings tons of new and useful features.
Next, let's take a look at the all-time evolution chart, which also clearly visualizes the growth of PHP 7.4:
I want to emphasize once more that the update from PHP 7.4 to 8 isn't all that hard, and truly worth investing a little time in. There are tools like Rector and PHP CS Fixer that can help make the upgrade even more smooth.
I've used Nikita's popular package analyzer to download the 1000 most popular composer packages, and wrote a little script to get the lowest version they support from their composer.json
. Here are the results:
Version | # |
8.0 | 117 |
7.4 | 56 |
7.3 | 133 |
7.2 | 142 |
7.1 | 182 |
7.0 | 31 |
5.6 | 61 |
5.5 | 43 |
5.4 | 41 |
5.3 | 97 |
5.2 | 12 |
5.0 | 2 |
You'll notice there are a little less than 1000 packages included, that's because some of the 1000 most popular packages don't specifically require a PHP version.
It's interesting how 7.1 is still the minimum required version for almost 20% of the analysed packages. 7.2 adds another 15%. In fact, only 18% of the most popular packages require an actively supported version as their minimum.
I'm an open source maintainer myself, and I believe we have a responsibility to the community to help keep their software stack safe and secure. This starts with only supporting active PHP versions. So I hope to see this number shift more towards PHP 8 in the near future because, remember, PHP 7.4 is already nearing its end!
That being said, it's clear that many users are moving towards a supported PHP version, despite lower package requirements. I am a little worried about the move to PHP 8 though, and I hope we'll see broader adoption soon.
What are your thoughts on these stats? Are you looking forward to PHP 8.1? Let me know your thoughts on Twitter and subscribe to my newsletter if you want to be kept up-to-date about these posts!
]]>I once worked at a company that wrote and maintained an in-house framework. Over the course of ten years, they probably made around 250 websites and applications with it. Despite many shortcomings, the company kept using their framework for a simple reason: they were in control.
Thanks to that control, they were able to tailor their framework to their own needs without any overhead. And, while I would argue that using popular, community-backed frameworks is almost always the better choice over writing your own , I can appreciate some of their reasoning.
Except, in the end, they utterly failed in creating what they originally set out to do. Instead of a tool shaped specifically for their needs, the framework's core developers often wanted flexibility: they dreamt of open sourcing their framework and it growing popular, so it needed to handle as many cases as possible to reach as wide an audience as possible. And thus, flexibility and configuration were often prioritized, even though they rarely added much — if any — value to company projects. The core developers were always able to convince the not-so-technical managers; while in reality, the design and flexibility of this framework was often just a burden for me and my colleagues to deal with.
This mindset of "high configurability and flexibility" is, unfortunately, common in software design. I'm not sure why, but somehow programmers — myself included — often think they need to account for every possible outcome, even when those outcomes aren't relevant to their use cases. Many of us deal with some kind of fear of losing our audience if the code we're writing isn't able to handle the most specific of specific edge cases. A very counter-productive thought.
Lately I've come to appreciate an opinion-driven approach to software design. Especially in the open source world, where you're writing code for others to use. I used to tell myself I'd need to write more code for more flexibility "if I want this package to grow popular".
I don't believe that anymore.
These days, I prefer one way of solving a problem, instead of offering several options. As an open source maintainer, I realise that not everyone might like the solutions I come up with as much as I do; but in the end, if the job gets done, if my code is reliable, clear and useful; there rarely are any complaints. So I started to prefer opinion-driven design when I realised that flexibility comes with a price that is often not worth paying.
I'm not the only one benefiting by the way. When users of my open source code only get one way of doing something, they don't have to be worried about micro-decisions that wouldn't affect the end result. And that, for me, is good software design: allowing programmers to focus on decisions that really matter and offer value to their projects and clients; instead of wasting time on unnecessary details.
Thanks for reading! This post is part of my "Dev Diaries" series where I write about my own and personal experiences as a developer. Would you like to read some more?
If you want to stay up to date about what's happening on this blog, you can follow me on Twitter or subscribe to my newsletter:
Most of these ideas are born from my daily experience with Laravel, and more specifically from the little annoyances I have with it. Now, don't get me wrong: I think Laravel is one of the best frameworks out there when it comes to modern PHP development and it's only natural that it has a quirk here and there, after a decade of development.
So this definitely isn't a Laravel-rant, rather it's just a thought experiment in dealing with one of those little annoyances.
So, Laravel has these PHP config files, right. Here's one example (this one is auth.php
, if you're wondering):
<?php
return [
/*
|--------------------------------------------------------------------------
| Authentication Defaults
|--------------------------------------------------------------------------
|
| This option controls the default authentication "guard" and password
| reset options for your application. You may change these defaults
| as required, but they're a perfect start for most applications.
|
*/
'defaults' => [
'guard' => 'web',
'passwords' => 'users',
],
/*
|--------------------------------------------------------------------------
| Authentication Guards
|--------------------------------------------------------------------------
|
| Next, you may define every authentication guard for your application.
| Of course, a great default configuration has been defined for you
| here which uses session storage and the Eloquent user provider.
|
| All authentication drivers have a user provider. This defines how the
| users are actually retrieved out of your database or other storage
| mechanisms used by this application to persist your user's data.
|
| Supported: "session", "token"
|
*/
'guards' => [
'web' => [
'driver' => 'session',
'provider' => 'users',
],
'api' => [
'driver' => 'token',
'provider' => 'users',
'hash' => false,
],
],
/*
|--------------------------------------------------------------------------
| User Providers
|--------------------------------------------------------------------------
|
| All authentication drivers have a user provider. This defines how the
| users are actually retrieved out of your database or other storage
| mechanisms used by this application to persist your user's data.
|
| If you have multiple user tables or models you may configure multiple
| sources which represent each model / table. These sources may then
| be assigned to any extra authentication guards you have defined.
|
| Supported: "database", "eloquent"
|
*/
'providers' => [
'users' => [
'driver' => 'eloquent',
'model' => App\Models\User::class,
],
// 'users' => [
// 'driver' => 'database',
// 'table' => 'users',
// ],
],
/*
|--------------------------------------------------------------------------
| Resetting Passwords
|--------------------------------------------------------------------------
|
| You may specify multiple password reset configurations if you have more
| than one user table or model in the application and you want to have
| separate password reset settings based on the specific user types.
|
| The expire time is the number of minutes that the reset token should be
| considered valid. This security feature keeps tokens short-lived so
| they have less time to be guessed. You may change this as needed.
|
*/
'passwords' => [
'users' => [
'provider' => 'users',
'table' => 'password_resets',
'expire' => 60,
'throttle' => 60,
],
],
/*
|--------------------------------------------------------------------------
| Password Confirmation Timeout
|--------------------------------------------------------------------------
|
| Here you may define the amount of seconds before a password confirmation
| times out and the user is prompted to re-enter their password via the
| confirmation screen. By default, the timeout lasts for three hours.
|
*/
'password_timeout' => 10800,
];
While I think PHP configuration files are far superior to, for example, YAML or XML configuration; there's one thing that annoys me quite a lot with it: there are no IDE insights in what kind of config values are available in this array, let alone which type of values they require.
To counteract this problem, the official Laravel config files have these inline doc blocks explaining each config entry. Most serious third party packages also provide such doc blocks.
With PHP 8 and named arguments though, there's a better solution available: config objects that know exactly what kind of data they'd need.
Here's what I'd imagine the auth.php
would look like with it:
return AuthConfig::make()
->defaults(
guard: 'web',
passwords: 'users',
)
->guards(
web: GuardConfig::make()
->driver('session')
->provider('users'),
api: GuardConfig::make()
->driver('token')
->provider('users')
->hash(false),
)
->providers(
users: AuthProviderConfig::make()
->driver('eloquent')
->model(User::class),
)
->passwords(
users: PasswordConfig::make()
->provider('users')
->table('password_resets')
->expire(60)
->throttle(60)
)
->passwordTimeout(10800);
Thanks to named arguments and their support for variadic functions, we end up with a conciser syntax, while still having all documentation available to us: it's added as property types and doc blocks in these config objects, instead of being hard coded in the config files as text.
To me that's the most important value: your IDE tells you what you need to do, instead of having to read documentation — inline or external:
The only thing needed for this to work is some kind of interface that requires these config builders, as I like to call them, to implement a toArray
method. You could go one step further and turn things around by always using config objects instead of arrays, which would allow you to also make use of their built-in documentation when reading config, and not only when initializing it. That's a bit more of a aggressive change though.
Here's what a config builder implementation would look like:
class AuthConfig extends ConfigBuilder
{
public function defaults(
?string $guard = null,
?string $password = null,
// …
): self {
$this->config['defaults']['guard'] =
$guard
?? $this->config['defaults']['guard']
?? null;
$this->config['defaults']['password'] =
$password
?? $this->config['defaults']['password']
?? null;
return $this;
}
public function guards(GuardConfig ...$guardConfigs): self
{
foreach ($guardConfigs as $name => $guardConfig) {
$this->config['guards'][$name] = $guardConfig->toArray();
}
return $this;
}
// …
public function passwordTimeout(int $timeout): self
{
$this->config['password_timeout'] = $timeout;
return $this;
}
}
Another improvement I can come up with is by using enums instead of string values. They will be natively available in PHP 8.1, but there are also alternatives out there for older PHP versions.
Let's just assume we're already running PHP 8.1, we could write parts of it like so:
->guards(
web: GuardConfig::make()
->driver(Driver::Session)
->provider(Provider::Users),
api: GuardConfig::make()
->driver(Driver::Token)
->provider(Provider::Users)
->hash(false),
)
These ideas aren't new, by the way. We've been using config objects in some of our packages at Spatie, but we always start from a simple array and convert it when booting the application. There's also PHP CS that uses the same approach. Also, Symfony just recently added support for a fluent interface to build their config files; great!
There's one advantage to tinkering with your own framework: you're not constrained by backwards compatibility and legacy. I imagine it might not be trivial to properly support config builders in Laravel, at least not as the default approach. They also become much less useful if you cannot use named arguments, which require PHP 8.
But, who knows? Maybe something similar might get added in the future in Laravel? Or maybe some third-party packages start doing it on their own first. Anyway, I'm going to tinker some more with my custom framework, just for fun!
What's your opinion on config builders? Let me know your thoughts, via Twitter or by subscribing to my newsletter.
]]>Credit where credit is due, many new features were inspired by Axon,
a popular event sourcing framework in the Java world, and several people pitched in during the development process.
In this post, I'll walk you through all significant changes, but first I want to mention the course that we've built at Spatie over the last months about event sourcing. If you're working on an event sourced project or thinking about starting one, this course will be of great help. Check it out on https://event-sourcing-laravel.com/!
If you've used previous versions of our package, you might have struggled with how event handlers were registered across classes. Aggregate roots required you to write applyEventName
functions, while projectors and reactors had an explicit event mapping.
Whatever class you're writing will now register event handlers the same way: by looking at the type of the event. You don't need any more configuration or naming conventions anymore.
class CartAggregateRoot extends AggregateRoot
{
// …
public function onCartAdded(CartAdded $event): void
{
// Any `CartAdded` event will automatically be matched to this handler
}
}
class CartProjector extends Projector
{
public function onCartAdded(CartAdded $event): void
{
// The same goes for projectors and reactors.
}
}
Event queries are a new feature that allow you to easily query an event stream without building database projections. You can think of them as in-memory projections that are rebuilt every time you call them.
Here's an example of it in action:
class EarningsForProductAndPeriod extends EventQuery
{
private int $totalPrice = 0;
public function __construct(
private Period $period,
private Collection $products
) {
EloquentStoredEvent::query()
->whereEvent(OrderCreated::class)
->whereDate('created_at', '>=', $this->period->getStart())
->whereDate('created_at', '<=', $this->period->getEnd())
->cursor()
->each(
fn (EloquentStoredEvent $event) => $this->apply($event)
);
}
protected function applyOrderCreated(OrderCreated $orderCreated): void
{
$orderLines = collect($orderCreated->orderData->orderLineData);
$totalPriceForOrder = $orderLines
->filter(function (OrderLineData $orderLineData) {
return $this->products->first(
fn(Product $product) => $orderLineData->productEquals($product)
) !== null;
})
->sum(
fn(OrderLineData $orderLineData) => $orderLineData->totalPriceIncludingVat
);
$this->totalPrice += $totalPriceForOrder;
}
}
Note that these examples come from the Event Sourcing in Laravel book.
Aggregate partials allow you to split large aggregate roots into separate classes, while still keeping everything contained within the same aggregate. Partials can record and apply events just like an aggregate root, and can share state between them and their associated aggregate root.
Here's an example of an aggregate partial that handles everything related to item management within a shopping cart:
class CartItems extends AggregatePartial
{
// …
public function addItem(
string $cartItemUuid,
Product $product,
int $amount
): self {
$this->recordThat(new CartItemAdded(
cartItemUuid: $cartItemUuid,
productUuid: $product->uuid,
amount: $amount,
));
return $this;
}
protected function applyCartItemAdded(
CartItemAdded $cartItemAdded
): void {
$this->cartItems[$cartItemAdded->cartItemUuid] = null;
}
}
And this is how the cart aggregate root would use it:
class CartAggregateRoot extends AggregateRoot
{
protected CartItems $cartItems;
public function __construct()
{
$this->cartItems = new CartItems($this);
}
public function addItem(
string $cartItemUuid,
Product $product,
int $amount
): self {
if (! $this->state->changesAllowed()) {
throw new CartCannotBeChanged();
}
$this->cartItems->addItem($cartItemUuid, $product, $amount);
return $this;
}
Aggregate partials come with the same testing capabilities as aggregate roots, and are a useful way of keeping aggregate-related code maintainable.
We've added a command bus that can automatically map commands to handlers on aggregate roots:
namespace Spatie\Shop\Cart\Commands;
use Spatie\Shop\Support\EventSourcing\Attributes\AggregateUuid;
use Spatie\Shop\Support\EventSourcing\Attributes\HandledBy;
use Spatie\Shop\Support\EventSourcing\Commands\Command;
#[HandledBy(CartAggregateRoot::class)]
class AddCartItem implements Command
{
public function __construct(
#[AggregateUuid] public string $cartUuid,
public string $cartItemUuid,
public Product $product,
public int $amount,
) {
}
}
Whenever this command is dispatched, it will automatically be captured and handled by the associated aggregate root. It even works with aggregate partials:
class CartItems extends AggregatePartial
{
// …
public function addItem(AddCartItem $addCartItem): self
{
// …
}
public function removeItem(RemoveCartItem $removeCartItem): self
{
// …
}
}
Besides these new features, there are also some quality-of-life changes across the board:
All in all, I'm very exited for this new release. All the new features are also used in our real-life projects, so we know from experience how useful they are in complex applications. Of course, a blog post can't discuss all the details and the thought process behind this new version, so make sure to read the book if you want in-depth knowledge about all of these features, and more.
]]>If you're on an actively supported PHP version, you already know about short closures — a.k.a. "arrow functions". And, most importantly, one of their biggest drawbacks: they only support one-line expressions, which are also used as the return value.
The multi-line short closures RFC by Nuno and Larry aims to solve that problem, in — what I think is — an elegant way.
Some people are skeptical about this RFC though, and I want to address their arguments and share why I think it's still a worthwhile addition. Though first, I'll show you what the RFC is about. Keep in mind this is a proposal, and not added to PHP yet!
A quick refresher, this is what arrow functions in PHP look like today:
$getTitle = fn (Post $post) => $post->title;
And this is what the RFC proposes:
$date = now();
$getSlug = fn (Post $post) {
$slug = Str::slug($post->title);
return $date . $slug;
}
There are two important things to note about multi-line short closures:
use
; andYou might have already noticed it, but the RFC introduces an elegant kind of symmetry in how you can create closures:
function
and fn
, the first one doesn't auto-capture variables from the outside, the second one does; and{ … }
or =>
, using curly brackets allow you to write multiple lines, while =>
only accepts one expression, but also returns it.Because of this symmetry, all of the following code samples are possible and all of them behaving a little differently.
Here's a closure that doesn't auto-capture outer scope, with multiple lines:
$date = now();
$getSlug = function (Post $post) use ($date) {
$slug = Str::slug($post->title);
return $date . $slug;
}
Next, a closure that does capture outer scope and immediately returns the result:
$date = now();
$getSlug = fn (Post $post) => $date . Str::slug($post->title);
And finally — proposed by the RFC — a closure that does capture outer scope, but still allows multiple lines:
$date = now();
$getSlug = fn (Post $post) {
$slug = Str::slug($post->title);
return $date . $slug;
}
If you're counting, you know that one option is still missing: a closure that doesn't auto-capture outer scope and immediately returns a value:
$date = now();
$getSlug = function (Post $post) use ($date)
=> $date . Str::slug($post->title);
The RFC lists this last one as a possible future addition, but doesn't cover it right now.
With the background info out of the way, let's look at some counter arguments as to why some people don't like this change.
Some people voice their fear about auto-capturing variables from the outer scope, especially that it can lead to unclear code in the long run and thus, bugs.
I've got a few arguments against that fear.
First, auto-capturing is already supported by PHP in the current short closures, there's nothing about this RFC that changes that. The arrow function RFC passed with 51 votes for, and 8 against after a thorough discussion, and has been used extensively in lots of projects since — take a look at some popular OSS frameworks if you want some examples. Clear signs that this particular behaviour is wanted by the majority of people, whether you're afraid of it or not.
Nikita also shares this opinion:
I'm generally in favor of supporting auto-capture for multi-line closures. I think that extensive experience in other programming languages has shown that auto-capture does not hinder readability of code, and I don't see any particular reason why the effects in PHP would be different. — https://externals.io/message/113740#114239
Auto-capturing outer scope might not be your preferred way of coding, but that doesn't mean the idea should be dismissed, as there are many people who do prefer this style.
Fear should never be the driving force in a programming language's design, we should look at facts instead.
I believe the RFC gets it right when it says that outer-scope variables are always captured by-value, and not by-reference. This means that you won't be able to change the value of an outer-scope variable from within your closure — a good thing.
People might argue that this will cause confusion, because what happens if you do want to change the outer-scope variable? We could discuss about whether this would ever be a good idea or not, but it doesn't even matter since PHP already has the answer — remember that symmetry I talked about earlier?
If you want by-reference passing, you'll need to specifically say which variables you want access to — which is exactly what function
allows:
$date = now();
$getSlug = function (Post $post) use (&$date) {
$date = now()->addDay();
// Please don't do this…
}
I'd argue that this RFC creates clear boundaries between what's possible with function
and fn
. It doesn't cause confusion; on the contrary: it creates consistency and clarity within PHP's syntax.
Some people argue that there's no need for adding multi-line short closures because there's little to be gained when it comes to the amount of characters you're writing. That might be true if you're not relying on outer-scope values, but to be honest it's not about how many characters you write.
This RFC makes the language's syntax more consistent, and a consistent language allows for easier programming.
When writing code, I don't want to be bothered with having to change fn
to function
when refactoring a closure that suddenly does need to be written on two lines instead of the prior one.
It might seem like such a small detail, but it's those details that impact our day-by-day developer life. And isn't that what a maturing language should be about? Look at features like property promotion, named arguments, enums, attributes, the match operator, etc. You could argue that none of those feature are "necessary" to write working PHP code, but still they do improve my day-by-day life significantly — and I hope yours too.
Finally, some people might find it difficult to deal with change, and you really need to ask yourself that question if you're voting on RFCs. Yes, you might not see a need for a given feature but you're not only voting for yourself, you have a responsibility to the PHP community; there's more to this than just your projects and your team.
Do you know why closures right now don't auto-import variables from the outer scope? You might have gotten used to it, but do you know why they were designed this way 12 years ago? Larry did some digging in the mailing list archives, and discovered there were three reasons why use
was introduced in the first place:
function
and fn
.You might have gotten used to closures not auto-importing variables for the past decade, but keep in mind this behaviour was only added as a necessity back in the day. All arguments for only using explicit capture have been nullified by time, a great sign that PHP is maturing even more.
With all of that being said, I'm looking forward to Nuno and Larry opening the vote on their RFC. PHP 8.1's feature freeze is planned for the 20th of July, 2021; so there's still some time to finalize the details. If you're voting on RFCs, I truly hope you can see the big picture, as this is one of the RFCs that will have a significant impact on many people's developer life.
]]>There are a few good and robust solutions to run PHP code in parallel already; and yet, we've made our own implementation. I want to explain why. First, let's set the scene: I want to run PHP code in parallel. Here are some of my use cases:
My use cases have two requirements in common: run a arbitrary amount of functions in parallel, and wait until all of them are finished. Let's look at the solutions available today.
AmpPHP has a package called parallel-functions. It looks like this:
use Amp\Promise;
use function Amp\ParallelFunctions\parallelMap;
$values = Promise\wait(
parallelMap([1, 2, 3], function ($time) {
\sleep($time);
return $time * $time;
})
);
For my use cases, I've got a few problems with this implementation:
Moving on to ReactPHP, they don't have an out-of-the-box solution like Amp, but they do offer the low-level components:
$loop = React\EventLoop\Factory::create();
$process = new React\ChildProcess\Process('php child-process.php');
$process->start($loop);
$process->stdout->on('data', function ($chunk) {
echo $chunk;
});
$process->on('exit', function($exitCode, $termSignal) {
echo 'Process exited with code ' . $exitCode . PHP_EOL;
});
$loop->run();
A few caveats with this implementation:
Finally, there's Guzzle with its concurrent requests:
use GuzzleHttp\Client;
use GuzzleHttp\Promise;
$client = new Client(['base_uri' => 'http://httpbin.org/']);
$promises = [
'image' => $client->getAsync('/image'),
'png' => $client->getAsync('/image/png'),
'jpeg' => $client->getAsync('/image/jpeg'),
'webp' => $client->getAsync('/image/webp')
];
$responses = Promise\Utils::unwrap($promises);
Of all of the above, Amp's approach would have my preference, were it not that it still has quite a lot of overhead for my simple use cases. Honestly, all I wanted to do was to run some functions in parallel and wait until all of them are finished. I don't want to be bothered by looking up documentation about the particular API a framework is using. Did I have to import a function here? How to unwrap promises? How to wait for everything to finish?
All of the above examples are great solutions for the 10% cases that require people to have lots of control, but what about the 90% of cases where you just want to do one thing as simply as possible?
Less is more. We often forget that in software design. We overcomplicate our solution "just in case" someone might need it, and forget about the 90% use case. It leads to frustration because developers have to look up documentation in order to understand how to use a framework, or they have to write lots of boilerplate to get their generic case to work.
So with all of that being said, you now know why I decided to make another library that has one simple goal: run functions in parallel and wait for the result. Here's what it looks like:
$rssFeeds = Fork::new()
->run(
fn () => file_get_contents('https://stitcher.io/rss'),
fn () => file_get_contents('https://freek.dev/rss'),
fn () => file_get_contents('https://spatie.be/rss'),
);
And that's it. It does one job, and does it well. And don't be mistaken: it's not because there's a simple API that it only offers simple functionality! Let me share a few more examples.
Parallel functions are able to return anything, including objects:
$dates = Fork::new()
->run(
fn () => new DateTime('2021-01-01'),
fn () => new DateTime('2021-01-02'),
);
They use process forks instead of fresh processes, meaning you don't need to manually boot your framework in every child process:
[$users, $posts, $news] = Fork::new()
->run(
fn () => User::all(),
fn () => Post::all(),
fn () => News::all(),
);
They allow before and after bindings, just in case you need to do a little more setup work. In the previous example, Laravel actually needs to reconnect to the database in the child processes before it would work:
[$users, $posts, $news] = Fork::new()
->before(fn () => DB::connection('mysql')->reconnect())
->run(
fn () => User::all(),
fn () => Post::all(),
fn () => News::all(),
);
And finally, before and after bindings can be run both in the child process and parent process; and also notice how individual function output can be passed as a parameter to these after
callbacks:
Fork::new()
->after(
child: fn () => DB::connection('mysql')->close(),
parent: fn (int $amountOfPages) =>
$this->progressBar->advance($amountOfPages),
)
->run(
fn () => Pages::generate('1-20'),
fn () => Pages::generate('21-40'),
fn () => Pages::generate('41-60'),
);
There are of course a few things this package doesn't do:
In other words: it's the perfect solution for the 90% case where you just want to run some functions in parallel and be done with it. If you need anything more than that, then the solutions listed above are a great start. There's also another package of ours called spatie/async
that doesn't work with promises but does offer pool configuration and extensive exception handling.
If you want to know more or want to try the package yourself, you can check it out on GitHub: spatie/fork
.
Less is more. That's one of my core principles when coding. I prefer code that forces me to do something one way but always works, instead of a highly configurable framework that makes me wonder how to use it every time I look at it. I feel that many developers often get lost in a maze of high configurability and extensibility and forget their original end goal by doing so.
I hope this package can be of help for that group of people who fall in the 90% category.
]]>Like I said time and time again: PHP isn't the same language it was ten years ago, and we're very thankful for that. It's a fast and reliable language, used to build large applications at scale. So let's discuss some of the most notable changes of PHP in the last year, to the language and the community.
The new major version, PHP 8, arrived late last year. I've written an extensive amount on the topic, and I won't copy/paste all of that here. As always, performance is only improving, as shown by the benchmarks done by Kinsta.
There's also the JIT that does seem to improve performance of some projects here and there, as well as preloading that has an overall positive impact if you're not using shared hosting.
I think that features like attributes (a.k.a. "annotations"), named arguments and promoted properties also deserve a mention, as they have definitely contributed towards PHP 8 being such a great release.
Meanwhile, the core team is already working on the next version, PHP 8.1, which will be released somewhere by the end of 2021. For now, the most significant features are enums and fibers, I'll mention both of them again later in this post.
Year after year, the core team succeeds in bringing a new stable release to the community, packed with a bunch of features and quality-of-life improvements. The upgrade path also isn't all that difficult anymore. I upgraded some of my own projects from PHP 7.4 to PHP 8, and it took only an hour or so per project. There really isn't any good reason to stay behind!
There's actually some very exciting news when it comes to types: enums will be added in PHP 8.1. On top of that we've also seen some of the maintainers of static analysis tools contributing to PHP's source code by landing their first RFC. This one adds the never
type, a useful addition for static analysis.
Speaking of static analysis tools, PhpStorm has added built-in support for Psalm and PhpStan, which is a great step towards broader adaptation; our favourite IDE now also supports generics as docblocks, which makes them a lot more useful.
Still no support for first-class generics, unfortunately. There are some major road blockers, especially since we're still dealing with a dynamically typed language. Nikita has outlined the problems here. Personally, my approach would be the easy way out: only support runtime-erased generics and rely on static analysis. This requires more than a technical solution though, it also requires a mind shift in the PHP community as a whole. Maybe one day it becomes a viable option, but not as of yet.
There was some big news recently: PHP is getting coroutines — aka. green threads — in PHP 8.1! Although, fibers — that's what they are called — might not be as big a game-changer as you might think.
Even though fibers themselves might only be a small cog in what is the large async machine, the RFC has spiked interest in the async community again, which we can only be happy about. Async frameworks like Amphp and ReactPHP are growing in popularity, and recently Laravel announced built-in support for Swoole.
I can't go on without mentioning Composer, the de-facto standard package manager. It has had a new major release in October, 2020: Composer 2.0. This version comes with several UX improvements, but most importantly sees extreme performance improvements, sometimes even tripling its speed on clean installs.
Speaking of composer, I like to measure the current state of PHP's ecosystem by looking at the available packages over time. Last year I spoke about ±25 million downloads a day, today that number has more than doubled and we're looking at ±60 million daily downloads.
Finally, take a look at this graph, listing the amount of packages and versions over time. It can also be found on their website. You can clearly see a healthy ecosystem growing, and there's no end in sight.
Let's end with a reminder of everything that's been added to PHP these recent years. If you haven't kept up with its development, you really want to check this list out. I think it shows the growth of the community and core development team within recent years, and I'm confident there's more to come.
All of that to say: PHP is alive and doing very well. Every year I feel happier with the direction the language is going, and am looking forward to using it for many years to come!
If you're excited as well, you probably want to subscribe to my newsletter to stay up-to-date about PHP's development, as well as follow me on Twitter. Let me know your thoughts via Twitter or email, and share this post with your audience if you found it useful, thanks!
]]>I've created an audio version of this post. You can listen on YouTube or by subscribing to my podcast feed on Apple Podcasts, Stitcher or Spotify
So I was going to write an in-depth blogpost about using fibers in PHP 8.1. We were going to start with a basic example to explain them from the ground up. The idea was to send async HTTP requests and process them in parallel using fibers.
But playing around with them, I learned that the RFC wasn't kidding when it said "The Fiber API is not expected to be used directly in application-level code. Fibers provide a basic, low-level flow-control API to create higher-level abstractions that are then used in application code".
So instead of going down this path and make things way too complicated, we'll discuss what fibers are conceptually, why they are barely usable in application code, and how you can make use of async PHP after all.
First, a little bit of theory.
Imagine you want to send three HTTP requests and process their combined result. The synchronous way of doing so is by sending the first one, waiting for the response, then sending the second one, waiting, etc.
Let's represent such a program flow with as easy a chart as possible. You need to read this chart from the top down, and time progresses the further down you go. Each colour represents one HTTP request. The coloured pieces of each request represent PHP code actually running, where the CPU on your server is doing work, the transparent blocks represent waiting times: the request needs to be sent over the wire, the other server needs to process it and send it back. It's only when the response arrives that we can work again.
This is a synchronous execution flow: send, wait, process, repeat.
In the world of parallel processing, we send the request but don't wait. Then we send the next request, followed by another. Only then do we wait for all requests. And while waiting we periodically check whether one of our requests is already finished. If that's the case we can process it immediately.
You can see how such an approach reduces execution time because we're using the waiting time more optimally.
Fibers are a new mechanism in PHP 8.1 that allow you to manage those parallel execution paths more efficiently. It was already possible by using generators and yield
, but fibers are a significant improvement, since they are specifically designed for this use case.
You would create one fiber for each request, and pause the fiber after the request is sent. After you've created all three fibers, you'd loop over them, and resume them one by one. By doing so, the fiber checks whether the request is already finished, if not it pauses again, otherwise it can process the response and eventually finish.
You see, fibers are a mechanism to start, pause and resume the execution flow of an isolated part of your program. Fibers are also called "green threads": threads that actually live in the same process. Those threads aren't managed by the operating system, but rather the runtime — the PHP runtime in our case. They are a cost efficient way of managing some forms of parallel programming.
But note how they don't add anything truly asynchronous: all fibers live in the same PHP process, and only one can run at a time. It's the main process that loops over them and checks them while waiting, and that loop is often called the "event loop".
The difficult part about parallelism isn't about how you loop over fibers or generators or whatever mechanism you want to use; it's about being able to start an operation, hand it over to an external service and only check the result when you want to, in a non-blocking way.
See, in the previous examples, we assumed that we could just send off a request and check its response later when we want to, but that actually isn't as easy as it sounds.
That's right: most of PHP's functions that deal with I/O don't have this non-blocking functionality built-in. In fact, there's only a handful of functions that do, and using them is quite cumbersome.
There's the example of sockets, which can be set to be non-blocking, like so:
[$read, $write] = stream_socket_pair(
STREAM_PF_UNIX,
STREAM_SOCK_STREAM,
STREAM_IPPROTO_IP
);
stream_set_blocking($read, false);
stream_set_blocking($write, false);
By using stream_socket_pair()
, two sockets are created that can be used for bidirectional communication. And as you can see, they can be set to be non-blocking using stream_set_blocking()
.
Say we'd want to implement our example, sending three requests. We could use sockets to do so, but we'd need to implement the HTTP protocol ourselves on top of it. That's exactly what nox7 did, a user who shared a small proof of concept on Reddit to show how to send HTTP GET requests using fibers and sockets. Do you really want to be concerned with doing so in your application code?
The answer, for me at least, is "no". Which is exactly what the RFC warned about; I'm not mad about that. Instead, we're encouraged to use one of the existing async frameworks: Amp or ReactPHP.
With ReactPHP, for example, we could write something like this:
$loop = React\EventLoop\Factory::create();
$browser = new Clue\React\Buzz\Browser($loop);
$promises = [
$browser->get('https://example.com/1'),
$browser->get('https://example.com/2'),
$browser->get('https://example.com/3'),
];
$responses = Block\awaitAll($promises, $loop);
That's a better example compared to manually creating socket connections. And that's what the RFC means: application developers shouldn't need to worry about fibers, it's an implementation detail for frameworks like Amp or ReactPHP.
That brings us to the question though: what are the benefits of fibers compared to what we already could do with generators? Well the RFC explains it this way:
Unlike stack-less Generators, each Fiber has its own call stack, allowing them to be paused within deeply nested function calls. A function declaring an interruption point (i.e., calling Fiber::suspend()) need not change its return type, unlike a function using yield which must return a Generator instance.
Fibers can be suspended in any function call, including those called from within the PHP VM, such as functions provided to array_map or methods called by foreach on an Iterator object.
It's clear that fibers are a significant improvement, both syntax-wise and in flexibility. But they are nothing yet compared to, for example, Go, with its "goroutines".
There's still lots of functionality missing for async PHP to become mainstream without the overhead of frameworks, and fibers are a good step in the right direction, but we're not there yet.
So there's that. There actually isn't much to say about fibers if you're not a maintainer of either Amp, ReactPHP or a smaller async PHP framework. Maybe even more frameworks or libraries will start incorporating them?
Meanwhile, there's also Swoole — a PHP extension that actually modifies several core functions to be non-blocking. Swoole itself is a Chinese project and often not very well documented when it comes to English, but recently Laravel announced first party-integration with it. Maybe this is the better strategy when it comes to moving PHP towards a more async model: optionally integrate Swoole or other extensions with frameworks like Laravel and Symfony?
It'll be interesting to see what the future will bring!
]]>Words like "modularized", "distributed", "scalable" and "versatile" come to mind to describe it; characteristics you can't do without if you're building applications at scale. Think about a popular webshop or a bank, handling maybe thousands if not millions of transactions per second. Those are the kinds of projects event sourcing most often is associated with.
And thus, event sourcing is rarely every used in smaller projects, the ones many developers deal with, the ones many of my regular blog readers work on.
I think the reason for this, is because of a fundamental mistake of what we think event sourcing is.
When I discuss event sourcing, people often assume that it comes with significant overhead, and that that overhead isn't justified in smaller projects. What they say is that there's some kind of pivot point, determined by the scope of the project, where event sourcing actually reduces costs, while in smaller projects it would introduce a cost overhead.
Something like this:
And, of course, this is an oversimplification, but it visualizes the argument very well: we should only use event sourcing in projects where we're sure it'll be worth it.
The problem with this statement is that it doesn't talk about just event sourcing, it talks about event sourcing with all its associated patterns as well: aggregates, sagas, snapshots, serialization, versioning, commands, …
Which is why I'd say our graph should look something more like this:
But does it even make sense to use event sourcing without the patterns that build on top it? There's a good reason why those patterns exist. That are the questions I want to answer today.
Here's Martin Fowler's vision on what event sourcing is:
"We can query an application's state to find out the current state of the world, and this answers many questions. However there are times when we don't just want to see where we are, we also want to know how we got there.
Event Sourcing ensures that all changes to application state are stored as a sequence of events. Not just can we query these events, we can also use the event log to reconstruct past states, and as a foundation to automatically adjust the state to cope with retroactive changes.
The fundamental idea of Event Sourcing is that of ensuring every change to the state of an application is captured in an event object, and that these event objects are themselves stored in the sequence they were applied for the same lifetime as the application state itself."
In other words: event sourcing is about storing changes, instead of their result. It's those changes that make up the final state of a project.
With event sourcing, the question of "is a cart checked out" should be answered by looking at the events that happened related to that cart, and not by looking at a cart's status.
That sounds like overhead indeed, so what are the benefits of such an approach? Fowler lists three:
There's one important thing missing in Fowler's list though: events model time extremely well. In fact, they are much closer to how we humans perceive the world than CRUD is.
Think of some kind of process of your daily life. It could be your morning routine, it could be you doing groceries, maybe you attended a class or had a meeting at work; anything goes, as long as there were several steps involved.
Now try to explain that process in as much detail as possible. I'll take the morning routine as an example:
Thinking with events comes natural to us, much more natural than having a table containing the state of what's happening right now, as with a CRUD approach.
If there's already a "flow of time" to be discovered in something as mundane as my morning routine, what about any kind of process for our client projects? Making bookings, sending invoices, managing inventories, you name it; "time" is very often a crucial aspect, and CRUD isn't all that good in managing it since it only shows the current state.
I'm going to give you an example of how extremely simple event sourcing can be, without the need for any kind of framework or infrastructure, and where there's no overhead compared to CRUD, in fact, there's less.
I have a blog, this one; and I use Google Analytics to anonymously track visitors, page views, etc. Of course I know Google isn't the most privacy-focussed so I was tinkering with alternatives. One day I wondered if, instead of relying on client side tracking, I could just rely on my server logs to determine how many pages and which were visited.
So, I wrote a little script that monitors my nginx access log; it filters out traffic like bots, crawlers etc, and stores each visit as a line in a database table. Such a visit has some data associated with it: the URL, the timestamp, the user agent, etc.
And that's it.
Oh were you expecting more? Well, I did end up writing a little more code to make my life easier, but in essence, what we have here already is event sourcing: I keep a chronological log of everything that happened in my application and I can use SQL queries to aggregate the data, for example, to show visits per day.
Now, of course, with millions of visits over time, running raw SQL queries can become tedious, so I added one pattern that builds on event sourcing: projections; also known as the "read model" in CQRS.
Every time I store a visit in the table, I also dispatch it as an event. There are several subscribers that handle them, for example there's a subscriber that groups visits per day, it keeps track of them in a table with two columns: day
and count
. It's literally only a few lines of code:
class VisitsPerDay
{
public function __invoke(PageVisited $event): void
{
DB::insert(
'INSERT INTO visits_per_day (`day`, `count`)
VALUES (?, ?)
ON DUPLICATE KEY UPDATE `count` = `count` + 1',
[
$event->date->format('Y-m-d'),
1,
]
);
}
}
Do you want visits per month? Per URL? I'll just make new subscribers. Here's the kicker though: I can add them after my application is deployed, and replay all previously stored events on them. So even when I make a change to my projectors or add new ones, I can always replay them from the point I started storing events, and not just from when I deployed a new feature.
This was especially useful in the beginning: there was lots of data coming in that were bots or traffic that weren't real users. I let the script run for a few days, observed the results, added some extra filtering, threw away all projection data and simply replayed all visits again. This way I wouldn't need to start all over again every time I made a change to the data.
And can make a guess how long it took to set up this event sourced project? 2 hours, from start to a working production version. Of course I used a framework for a DBAL and an event bus, but nothing specifically event sourcing related. Over the next days I did some fine tuning, added some charts based on my projection tables etc; but the event sourcing setup was absolutely easy to build.
And so, here's the point I'm trying to make: the event sourcing mindset is extremely powerful in many kinds of projects. Not just the ones that require a team of 20 developers to work on for five years. There are many problems where "time" plays a significant role, and most of those problems can be solved using a very simple form of event sourcing, without any overhead at all.
In fact, a CRUD approach would have cost me way more time to build this analytics project. Every time I made a change I would have to wait a few days to ensure this change was effective with real life visits. Event sourcing allowed me, a single developer, to actually be much more productive; which is the opposite of what many people believe event sourcing can achieve.
Now, don't get me wrong. I'm not saying event sourcing will simplify complex domain problems. Complex projects will take lots of time and resources, with or without event sourcing. Event sourcing simplifies some problems within those projects, and makes others a little harder.
But remember I'm not trying to make any conclusions about the absolute cost of using event sourcing or not. I only want people to realise that event sourcing in itself doesn't have to be extremely complex, and might indeed be a better solution for some projects, even when they are relatively small ones.
Greg Young, the one who came up with the term "event sourcing" more than 10 years ago; said that if your starting point for event sourcing is a framework, you're doing it wrong. It's a state of mind first, without needing any infrastructure. I actually hope that more developers can see it this way, that they remove the mental layer of complexity at first, and only add it back when actually needed. You can start with event sourcing today, without any special framework, and it might actually improve your workflow.
If you've made it this far, I assume you're intrigued by what I wrote — or very angry because of it, that's fine too. If you want to stay up-to-date about my content, consider subscribing to my newsletter, I occasionally send a mail about stuff I'm working on.
Finally, if you're interested in more, you can check out the footnotes!
]]>I wrote about my experience with Event Sourcing, and how it's a shift in "mental framework" for all of us who were taught proper MVC and CRUD to build web applications with.
I hope you like it: https://www.eventstore.com/blog/php-and-event-sourcing.
If you want to listen to the audio version, you can head over here: https://www.youtube.com/watch?v=LXDnjtgfl2k.
]]>If you prefer listening over reading, you can listen to this post on YouTube or by subscribing to my podcast feed on Apple Podcasts, Stitcher or Spotify
Do you sometimes lie?
Perhaps that's a too confrontational question. Do you sometimes bend the truth a little for the benefit of others or yourself? Do you still consider it a lie if all parties involved benefit from it?
Studies show that all humans lie to a certain extent. It's a natural part of how we interact with each other. Still there's a category of lies that's not accepted by society: lies that intentionally deceive others for your own benefit, sometimes even causing harm.
This is a story about those kinds of lies. Not the ones you and I tell, but the ones people close to us might. The ones that, when witnessing them makes you feel uncomfortable, makes you want to step in and tell the truth. But you don't, because if you do, it might cause you harm instead.
I was a junior developer, straight out of school. I was working at a company that made websites for small to medium sized businesses. Think your average marketing site with some added complexity like CRM or lead management, stuff like that.
I'd been working with a team of developers on one of the larger projects for half a year, maybe eight months. The project was already in production by this time, but there still was lots of new functionality to build.
One day, the client makes a somewhat angry call to my project manager — the one who's directly above me, who I work with every day. They tell her there's a bug in a recently deployed feature and they ask her to get it fixed immediately. My project manager tells them she'll review the matter internally and get back to them within the hour.
Fast forward an hour; I can see her nervously picking up the phone. By the way, we're in an open office, so everyone's able to hear whatever others say.
She tells our client that the colleague who built that feature is sick for the rest of the week but we'll put another developer on the issue. It might take a bit longer and probably won't get done until tomorrow, maybe even the day after that.
I gave a puzzled look to the guy sitting next to me, that's the colleague our PM is talking about and he's just fine, not sick at all.
Let me paint you the broader picture for a moment. The relation with this client, remember: our largest up until then; it had been a little… tense. Ever since the application was deployed to production — or maybe ever since it became clear that the project estimates weren't as accurate as the client had hoped; they were noticeably frustrated.
We did those estimates together as a team, with our PM and boss. The boss was the one who actually made the sales pitch and had contact with the client before the project started.
We roughly estimated — if memory serves me right — that we'd need a year to deliver the project. Of course, it's a rough estimate on this timescale, no one is able to accurately estimate a project this size. So naturally we told our boss that we'd need to consider enough margins.
That answer wasn't satisfying though. Our boss knew the expectations of the client, they were half of what we proposed. They would surely go with another party if we kept to this estimate. So our boss promised what we, developers and project manager said was impossible: he cut the estimate in half, and gave it to the client. Same features, just half the available time.
We knew from day one this wasn't anywhere near achievable. But "we'll see", our boss said when we voiced our concerns. "Right now it's most important to win this pitch, we'll worry about the practical details later."
Naturally, our client wasn't happy when we told them it would take a little longer than "anticipated" to get an initial version running on production. I'd say we did a pretty good job after all: only 2 months over time instead of the 6 we expected.
But you can imagine the toll these things take on the team. There was a constant atmosphere of stress, and we — as a company — were piling lie upon lie upon lie.
I talked with our PM in private after that phone call, actually I stumbled upon her crying in the hallway. After the client initially called, she went to discuss the situation with our boss.
The colleague in question was currently working on another project (which was also under-estimated by the way) and it would have a significant impact if he'd need to spend an extra day back on the other project again. The boss told our PM to simply say the colleague is out sick, and that it would take more time than strictly required to fix the issue because of this. In his mind it was a win: the colleague can continue working on the other project, we get a few more days to fix the issue and we could invoice more work than actually needed — making up for the losses we made because of our wrong estimation in the beginning.
This situation, together with other reasons, is why I quit that job shortly after. The PM handed in her resignation as well, a month after I left.
These days, I'm fortunate to have found a workplace that does value honesty between them and their clients. I now realise this is not the case for every company. In the end, that company I used to work for did take a turn for the better; I believe they realised there's little to no value in relationships based on lies. Even though a lie might serve you right now, it's never a long lasting solution.
Thanks for reading! This post is part of my "Dev Diaries" series where I write about my own and personal experiences as a developer. Would you like to read some more?
If you want to stay up to date about what's happening on this blog, you can follow me on Twitter or subscribe to my newsletter:
If you prefer listening over reading, you can listen to this post on YouTube or by subscribing to my podcast feed on Apple Podcasts, Stitcher or Spotify
We were sitting with 5 or 6 backend developers around the large meeting table. It was 10 in the morning on a Monday, and we were all silently working on our laptops. There was a hasty atmosphere, and everyone tried to concentrate on the task ahead.
Less than 2 hours before, I walked into the office, not yet aware of any harm. I was immediately called to the meeting room at the back, there was no time to sit at my desk. Still I quickly grabbed a coffee, and went to the back where a few other colleagues already gathered.
With them was our boss, a nice guy; there wasn't any "upper management" culture or anything, we were just colleagues. The other people in the room already knew what was going on, so he explained to me personally.
There was a bug, in our in-house framework.
"It sure isn't the first one" — I remember thinking.
At this point we'd used this custom-built framework for several years; 200 websites were affected, more or less.
The bug was a stupid mistake — after all, which bug isn't?
Our framework router would take a URL and filter out repeated slashes, so //admin
would become /admin
. This is, I believe, part of some HTTP spec; at least I was told so, I never double checked. The problem however, was in the authorisation layer: /admin
was a protected URL, but //admin
was not. So the router would resolve //admin
and all its underlying pages to the admin section, and the authoriser wouldn't recognise it as a location you'd need admin privileges for.
In other words: the admin section of all our websites could be entered without any login, by simply replacing /admin
with //admin
.
I don't remember drinking my coffee after that.
So we did the only thing we could do: manually update all of our websites, some running a very outdated version of our framework. It took us 3 days to do this with 5 or 6 developers. You can do the math on how much it cost.
In the end we never actually got to know whether the bug had been exploited: it was discovered by accident by one of our colleagues over the weekend, and we didn't keep access logs longer than a few days. So nobody could tell whether someone had unauthorised access to one of our sites over the past years; let alone know if, and which data had been leaked.
Don't write your own framework, at least not when you're building websites for paying clients; who trust your work to be professional and secure. Whatever framework you use, make sure it's backed by a large community.
Want to share your thoughts? Let's discuss them on HN.
Thanks for reading! This post is part of my "Dev Diaries" series where I write about my own and personal experiences as a developer. Would you like to read some more?
If you want to stay up to date about what's happening on this blog, you can follow me on Twitter or subscribe to my newsletter:
As usual with my PHP feature posts, we start with a high level overview of what enums look like:
enum Status
{
case DRAFT;
case PUBLISHED;
case ARCHIVED;
}
The benefit of enums is that they represent a collection of constant values, but most importantly those values can be typed, like so:
class BlogPost
{
public function __construct(
public Status $status,
) {}
}
In this example, creating an enum and passing it to a BlogPost
looks like this:
$post = new BlogPost(Status::DRAFT);
That's the basics out of the way, as you can see there's nothing complex at all about them. There are lots of side notes to be made though, let's look at enums in depth!
Enums can define methods, just like classes. This is a very powerful feature, especially in combination with the match
operator:
enum Status
{
case DRAFT;
case PUBLISHED;
case ARCHIVED;
public function color(): string
{
return match($this)
{
Status::DRAFT => 'grey',
Status::PUBLISHED => 'green',
Status::ARCHIVED => 'red',
};
}
}
Methods can be used like so:
$status = Status::ARCHIVED;
$status->color(); // 'red'
Static methods are allowed as well:
enum Status
{
// …
public static function make(): Status
{
// …
}
}
And you can also use self
within an enum:
enum Status
{
// …
public function color(): string
{
return match($this)
{
self::DRAFT => 'grey',
self::PUBLISHED => 'green',
self::ARCHIVED => 'red',
};
}
}
Enums can implement interfaces, just like normal classes:
interface HasColor
{
public function color(): string;
}
enum Status implements HasColor
{
case DRAFT;
case PUBLISHED;
case ARCHIVED;
public function color(): string { /* … */ }
}
Enum values are represented by objects internally, but you can assign a value to them if you want to; this is useful for eg. serializing them into a database.
enum Status: string
{
case DRAFT = 'draft';
case PUBLISHED = 'published';
case ARCHIVED = 'archived';
}
Note the type declaration in the enum definition. It indicates that all enum values are of a given type. You could also make it an int
. Take note that only int
and string
are allowed as enum values.
enum Status: int
{
case DRAFT = 1;
case PUBLISHED = 2;
case ARCHIVED = 3;
}
The technical term for typed enums is called "backed enums" since they are "backed" by a simpler value. If you decide to assign enum values, all cases should have a value. You cannot mix and match them. Enums that aren't "backed" are called "pure enums".
If you're combining backed enums and interface, the enum type must come directly after the enum name, before the implements
keyword.
enum Status: string implements HasColor
{
case DRAFT = 'draft';
case PUBLISHED = 'published';
case ARCHIVED = 'archived';
// …
}
If you're assigning values to enum cases, you probably want a way to serialize and deserialize them. Serializing them means you need a way to access the enum's value. That's done with a readonly public property:
$value = Status::PUBLISHED->value; // 2
Restoring an enum from a value can be done by using Enum::from
:
$status = Status::from(2); // Status::PUBLISHED
There's also a tryFrom
that returns null
if an unknown value is passed. If you'd use from
there would be an exception.
$status = Status::from('unknown'); // ValueError
$status = Status::tryFrom('unknown'); // null
Note that you can also use the built-in serialize
and unserialize
functions on enums. Furthermore, you can use json_encode
in combination with backed enums, its result will be the enum value. This behaviour can be overridden by implementing JsonSerializable
.
You can use the static Enum::cases()
method to get a list of all available cases within an enum:
Status::cases();
/* [
Status::DRAFT,
Status::PUBLISHED,
Status::ARCHIVED
] */
Note that this array contains the actual enum objects:
array_map(
fn(Status $status) => $status->color(),
Status::cases()
);
I already mentioned that enums values are represented as objects, in fact those are singleton objects. That means that you can do comparisons with them like so:
$statusA = Status::PENDING;
$statusB = Status::PENDING;
$statusC = Status::ARCHIVED;
$statusA === $statusB; // true
$statusA === $statusC; // false
$statusC instanceof Status; // true
Because enums values are actually objects, it's currently not possible to use them as array keys. The following will result in an error:
$list = [
Status::DRAFT => 'draft',
// …
];
There is an RFC to change this behaviour, but it hasn't been voted yet.
This means you can only use enums as keys in SplObjectStorage
and WeakMaps
.
Enums can use traits just like classes, but with some more restrictions. You're not allowed to override built-in enum methods, and they can't contain class properties — those are prohibited on enums.
As expected, there are a few reflection classes added for dealing with enums: ReflectionEnum
, ReflectionEnumUnitCase
and ReflectionEnumBackedCase
. There's also a new enum_exists
function which does what its name suggests.
Just like normal classes and properties, enums and their cases can be annotated using attributes. Note that TARGET_CLASS
filter will also include enums.
One last thing: enums also have a read only property $enum->name
, which the RFC mentions is an implementation detail and should probably only be used for debugging purposes. It's still worth mentioning though.
That's about all there is to say about enums, I'm very much looking forward to using them as soon as PHP 8.1 arrives, and also to sunset my own userland implementation.
]]>You can read all about built-in enums in PHP 8.1 in this post. If you're looking for more information on userland implementations of enums before PHP 8.1, you can continue to read this post.
If you came here looking for our enum implementation in PHP, that's this way. If you're interested in the design philosophy behind it, read on!
An enumeration type, "enum" for short, is a data type to categorise named values. Enums can be used instead of hard coded strings to represent, for example, the status of a blog post in a structured and typed way.
PHP doesn't have a native enum type. It offers a very basic SPL implementation, but this really doesn't cut the mustard.
There's a popular package written by Matthieu Napoli called myclabs/php-enum. It's a package I and many others have been using in countless projects. It's really awesome.
Today I want to explore some of the difficulties we encounter when solving problems like enums in userland. I'll talk about my personal take on enums, and we'll ponder on core support.
One last note: I will assume that you know what enums are, and that you know on how to use them in real life projects.
We could write something like this in PHP…
class Post
{
public function setStatus(PostStatus $status): void
{
$this->status = $status;
}
}
… and be sure that the value of Post::$status
is always one of three strings:
draft
, published
or archived
.
Say we'd save this Post
in a database, its status would automatically be represented as a string.
The myclabs/php-enum
package allows us to write this:
class PostStatus extends Enum
{
const DRAFT = 'draft';
const PUBLISHED = 'published';
const ARCHIVED = 'archived';
}
We could use the constant values directly like so:
class Post
{
public function setStatus(string $status): void
{
$this->status = $status;
}
}
// …
$post->setStatus(PostStatus::DRAFT);
But this prevents us to do proper type checking, as every string could be passed to Post::setStatus()
.
A better approach is to use a little magic introduced by the library:
class PostStatus extends Enum
{
private const DRAFT = 'draft';
private const PUBLISHED = 'published';
private const ARCHIVED = 'archived';
}
$post->setStatus(PostStatus::DRAFT());
Using the magic method __callStatic()
underneath, an object of the class PostStatus
is constructed,
with the 'draft'
value in it.
Now we can type check for PostStatus
and ensure the input is one of the three things defined by the "enum".
Here's the problem with the myclabs/php-enum
package though:
by relying on __callStatic()
, we lose static analysis benefits like auto completion and refactoring:
As you can see in this case, your IDE is unaware of the PostsStatus::DRAFT()
method.
Luckily, this problem is solvable with docblock type hints:
/**
* @method static self DRAFT()
* @method static self PUBLISHED()
* @method static self ARCHIVED()
*/
class PostStatus extends Enum
{
private const DRAFT = 'draft';
private const PUBLISHED = 'published';
private const ARCHIVED = 'archived';
}
$post->setStatus(PostStatus::DRAFT());
But now we're in trouble when refactoring an enum's value.
Say we want to rename DRAFT
to NEW
:
Also we're maintaining duplicate code: there's the constant values, and the doc blocks.
At this point it's time to stop and think. In an ideal world, we'd have built-in enums in PHP:
enum PostStatus {
DRAFT, PUBLISHED, ARCHIVED;
}
Since that's not the case right now, we're stuck with userland implementations.
Extending PHP's type system in userland most likely means two things: magic and reflection.
If we're already relying on these two elements, why not go full-out and make our lives as simple as possible?
Here's how I write enums today:
/**
* @method static self DRAFT()
* @method static self PUBLISHED()
* @method static self ARCHIVED()
*/
class PostStatus extends Enum
{
}
Opinionated, right? It's less code to maintain though, with more benefits.
I know this is far from an ideal situation. It would be amazing to see built-in support for enums in PHP one day. But until then, this has to do.
If you want to, you can try out my implementation here.
So, what's your take on enums? Do you want them in core PHP? Let's talk about it on Twitter!
]]>With a fitting title "A Perfect Storm", the author voices their concerns about how upgrading to PHP 8 isn't an easy path, and how open source maintainers have to struggle to be able to support PHP 8 on top of their existing codebase.
There's no denying that PHP 8 introduces some breaking changes. Especially the addition of union types, named arguments and consequently the changes made to the reflection API might feel like a pain.
What the author seems to forget when calling PHP 8 "a nightmare" and claiming it'll take years before being able to use it, is that PHP 8 is a major version, and things are allowed to break. Better yet: it's the entire purpose of a major release.
I don't blame the author for this sentiment, it lives in lots of people's mind. I think it's a side effect from a long and stable period of releases during the 7.x era. We've got used to easy updates, and we find it difficult when our code breaks because of one.
The problem however isn't with PHP growing and maturing, it's with developers and companies not being able to adapt quickly enough. Here's the brutal truth: PHP 7.4 has less than one year of active support to go. By the end of 2021, PHP 8 will be the only version (together with the future PHP 8.1) that's still actively worked on. You need to get on board, or you risk becoming stuck in legacy land. Believe it or not, but PHP 7.4 will one day be what we perceive PHP 5.4 or PHP 5.6 to be: completely outdated, insecure and slow.
Instead of shifting the blame on the perfectly healthy and stable release cycle of PHP, we should look at ourselves and our companies.
If you're still here and wanting to ride along, let's discuss a few things that you can do to prevent ending up in legacy land.
First: learn and use the proper tools. A craftsman is nothing without proper knowledge of his tools. These tools include static analysers, code formatters, IDEs, test frameworks and automatic updaters. Most problems you face during upgrades are actually solved automatically if only you'd use the proper tools.
While you're at it, consider buying the people maintaining those projects a drink, they are going to save you hours upon hours of manual and boring labour; they are worth a few bucks. We recently upgraded a large project to PHP 8. It took a few hours of preparation and research, and only 2 hours to do the actual upgrade; it's doable.
Next: if you're an open source maintainer. Don't bother supporting all of PHP 7 and 8 in the same version. Embrace PHP 8 as your main target and support 7.4 if it doesn't cause any problems. If you really need to actively support older versions, you'll have to support separate branches.
What baffles me about the "open source argument" made by many against PHP 8, is that they seem to forget that their old tags will keep working just fine. It's not because code isn't actively maintained any more that you're prohibited from using it. If your users really need to support an older PHP version, have them use an older version of your packages. If there's a crucial feature missing from those older versions, they are free to fork the package and do whatever they want with it. There shouldn't be anything holding you back from only supporting the active PHP versions. If anything, you're encouraging the majority of your users to upgrade faster, you're doing the PHP community a favour.
Finally, if you're in management or running a company: sticking with older PHP versions will always make you lose money in the end. Every year you hold off on updating your codebase, it becomes more difficult and time-consuming to catch up. Can you imagine what will happen when a critical security issue is discovered in your old version? On top of that: employees really worth their money won't stick with your legacy project forever. One day they'll seize a new opportunity if you don't keep them happy. If you're not keeping up to date, you're loosing in the end.
Part of being a professional developer is to be able to deal with these kinds of situations. Sure, I'd rather spend those hours spent updating on something else, but I know it's a small investment for a lot of joy in the long run.
Don't get dragged along the negativity, embrace the maturing language that is PHP and follow along. You won't regret it.
]]>This is an updated version of an older post of mine, since PHP is growing and evolving year by year. Let's dive in!
A common misconception about OO programming is that it's all about inheritance. Inheritance and polymorphism have their place, but OO is way more than that.
Because these principles are more often than not abused by programmers who claim they write "OO" code, I think the language should help prevent us making these mistakes.
That's why I would make all classes final by default:
final class Foo
{
}
class Bar extends Foo
{
}
Going even one step further: classes are only allowed to extend from abstract classes or implement interfaces. This way we can prevent deep inheritance chains of concrete classes.
Void is a strange thing when you think about it: it a "type", indicating the lack of a type. Why not go with the obvious way: no return type, means nothing is returned.
class Foo
{
public function bar(): void
{
// …
}
}
class Foo
{
public function bar()
{
return false;
}
}
Now you might be thinking: what if a function wants to return two types, that's the next point.
mixed
typeThe mixed
type basically means:
"you've got no idea what this function needs or will return, figure it out on your own".
Such a loose type system can be the source of many bugs. If you feel the need to use two different types in the same function, you should either make two implementations — this is where polymorphism has its place; or you should program to an interface.
Either way, there's always a better solution then relying on mixed
.
In my version of PHP, the language would ensure we always choose the better solution.
We already established that my version of PHP would make return types required. It's no surprise that the same goes for function parameters.
public function handle($bar)
{
}
public function handle(Bar $bar)
{
}
The same rules apply to class properties. Luckily for us, PHP 7.4 will introduce typed properties. I'd make them required though.
class Foo
{
public $bar;
}
class Foo
{
public Bar $bar;
}
Explicitness eliminates room for confusion. That's why all methods and class variables must have a visibility modifier.
class Foo
{
function bar()
{
// …
}
}
class Foo
{
public function bar()
{
// …
}
}
I started this list by saying I'd drop the final
keyword, that is on classes and methods.
final
would be a valid keyword to mark class variables as "read only".
A final variable may be set on construct, and not be changed afterwards.
class Foo
{
public final Bar $bar;
public __construct(Bar $bar)
{
$this->bar = $bar;
}
}
$foo = new Foo($bar);
$foo->bar = new Bar();
Right now, Typed properties can be initialized after construction. This is valid PHP
class Foo
{
public string $bar;
public function __construct() {
// Don't initialize bar
}
}
$foo = new Foo();
$foo->bar = 'abc';
PHP only throws an error when the property is accessed before it's initialised.
$foo = new Foo();
echo $foo->bar; // Error
I'd say to get rid rid of this behaviour. If a typed property isn't initialised after the object was constructed, you get an error.
Named parameters were originally on this list, but fortunately are possible as of PHP 8!
I originally listed "multiline short closures" here, but I think it's a little bit more complex. What I'd like to see is a combination of function/fn
and =>/{
. I'd make all combinations possible:
function a() {
return /* … */;
}
function b() => 1;
fn c() {
return /* … */;
}
fn d() => 1;
Here's the difference: when using the function
keyword, there's no automatic access to the outer scope, in other words you'll have to use use
to access variables outside the closure. Using fn
doesn't have this restriction.
If you're using the bracket notation for the closure's body {}
, you'll be allowed to write multiline functions, but there's no magic return statement. =>
on the other hand only allows a single expression, which is immediately returned.
One of the few things I think that we're all in agreement about: the current PHP function names and definitions are inconsistent and kind of sucky.
Let's treat all scalar types as objects, allowing them to contain what otherwise would be standalone functions.
public function handle(): string
{
return "a, b, c";
}
$this->handle()->explode(',');
You may have noticed a trend in the above changes. Most of them relate to PHP's type system. If all them were added, we'd also need to make the current type system more flexible.
Luckily again, PHP 7.4 already introduces improvements regarding type variance.
class Bar extends Foo { /* … */ }
interface A
{
public function handle(Bar $bar): Foo;
}
class B implements A
{
public function handle(Foo $bar): Bar
{
// …
}
}
Strict type checking is done by default, you should never declare(strict_types=1);
anymore.
After several improvements to the type system, I'd add some more improved ways to actually use it.
First a feature that probably most of the PHP world is waiting for: generics.
class List<T>
{
public function current(): T
{
// …
}
}
Next up: built-in enums. Based on the several userland implementations it's clear that the community would benefit from a built-in enum type.
enum Status
{
DRAFT, STATUS, PUBLISHED;
}
class Bar
{
public Status $status;
}
$bar->status = Status::DRAFT;
It's interesting to note that a new RFC popped up that might add enums in PHP 8.1. It's still being discussed though, so nothing concrete yet.
To end this list: structs. One of my own packages I use all the time is the data transfer object package. It allows us to define strongly typed objects. In essence, they are a userland implementation of what structs are meant to solve.
struct Point {
int $x;
int $y;
}
$point = Point {1, 2}
Let me know what's on your PHP wishlist! If you want to be kept in the loop, feel free to subscribe to my newsletter.
]]>Here's what I mean with that: whenever a door slides open or a non-human character speaks, whenever a droid walks or moves; those movements feel a little unnatural to me. It makes the characters feel like puppets, and it makes objects like sliding doors feel like they have no actual weight.
Here's an example:
Baby Yoda feels like it's a hand played puppet, its movements don't seem natural at all. There is, of course, a good reason that Star Wars uses this approach: it's a style they continued to use ever since the oldest movies which, back in the day, had to deal with much more technical limitations and where the puppet-like feeling was unavoidable. They decided to continue to use the same style of movement, not because it was necessary, but because of the charm and nostalgia feeling.
So what does that have to do with websites?
I was a toddler when the internet began to grow, so I'm too young to have witnessed its rise first hand. Still I got into web development at an early age, starting with HTML when I was 12. These were times when the web was still shaping itself to what we know today. Just like Star Wars, the web had to deal with lots of technical limitations but with those limitations came creativity.
When I browse the web today, there's little creativity left to find. Sure, most websites are as polished as can be, but compare their design to ten others in the same niche, and you'll quickly start to see pattern emerging. While so many websites these days are pixel-perfect, they lack the soul and personality the web used to have.
Test it yourself: think about a topic and do a simple google search. Whether it's email campaigns, travel blogs, social media or news sites; there's little distinction between them.
I wish more websites aimed to be more like Star Wars: not sticking to the modern day rules and instead hold on to the things that give character, that make your website unique. We're allowed to break free from best practices, we're allowed to experiment.
The web still allows you to do so, the only thing holding us back is us.
]]>Arguments and fights on the internet can make you feel exhausted, often having an effect on your mood in real life. So today I'd like to share a few guidelines with you on managing such situations. You can apply these rules to every situation: whether you've found yourself unwillingly dragged into a conversation that's quickly spiralling out of control, or whether you're the one who's getting a little heated yourself.
Finally, before diving in, I want to mention a very popular book on the topic of "how to deal with people": The 7 Habits of Highly Effective People by Stephen Covey. It's one of the most popular resources on people management, and part of this post was inspired by Covey's principles.
First things first: I use the term "conflict" and I want to make sure we're all on the same page on what it means. Let's look at the Oxford Dictionary's definition:
a situation in which people, groups or countries disagree strongly or are involved in a serious argument
Note how it doesn't say anything about the feelings of those people, groups or countries. This is the definition I'll assume when I use the term "conflict" throughout this post. It's a situation where a tension between people exists, but it doesn't mean that that situation needs to deteriorate into one where feelings are hurt and frustration arises.
When I say "conflict", it means there is a situation where people don't agree, but it doesn't mean they are fighting over it.
Instead of the seven habits described by Covey (which cover more than conflict resolving), we'll stick with five rules.
I've managed several online communities in the past years and most recently I've been moderating /r/php on Reddit. I wouldn't say I have to deal with conflicts every day, but they are very real and very abundant indeed. That's why I always try to follow these five rules when having a conversation, and I find they work very well.
The first four rules focus on conflict resolving in a constructive way. They have in common that they focus on a positive outcome. They assume a level of empathy and understanding. The fifth rule is for dealing with conflicts when there's no other way out. It's the emergency break when you notice the conversation is derailing. It's the most drastic one, but in some cases it's better to prevent further escalation.
Also note that you can use these rules, even when the other party doesn't agree to them. That's truly the power of these rules: you can apply them yourself even when the other side doesn't have the same constructive intentions. You'll notice that in most cases they'll even start copying your way of communication throughout a conflict, and start being constructive themselves, without even knowing.
Let's look at these rules one by one.
Rule number one: there seldom is one absolute truth. Most times there's only our personal interpretation of any given situation. All of us have a background and context; things like education, programming languages, frameworks, projects and workplace have a tangible impact on our opinion, our view of the world.
There's a lot of power in realizing that both you and the other party are not neutral. I even think this offers a significant learning opportunity: a conflict where both parties are open to the context of the other one can result in valuable lessons for both.
We're only into rule one, and we've already identified a secondary goal: we can not only prevent conflicts from devolving into chaos, but we can seize the given opportunities to learn and grow. Even when the other party isn't interested in your context, it's still possible for you to grow.
You're on the internet, written text can — and will — be interpreted in other ways than intended. I'd say that written text is one of the worst mediums to resolve conflicts with, since there's very little nuance you can make with dry text.
This is why people on the internet often come across as more rude than they'd be in real life. While many simply forget about the downsides of text, some know it very well and abuse that "freedom" the internet gives. The way some people act online is mind-boggling to me: surely they can't be like this in real life? Realizing this reality will help you put heated conflicts into perspective.
But there's also something to say the other way around: you have to make sure the other party doesn't misinterpret what you wrote. That's why I try to explain my perspective clearly enough when dealing with a conflict. It often looks something like this:
"I want to make sure this doesn't come across as rude. I genuinely believe my point of view is the correct one, but I don't want to insult you by phrasing it in a rude or hurtful way."
Addressing your perspective and acknowledging that there can be differences is often enough to defuse tensions entirely.
When reading someone's comments, you might be inclined to assume an undertone, an emotion, maybe even an unspoken opinion. These assumptions might trigger you in writing a more offensive response. You should be aware that all of this can happen unconsciously, and protect yourself from it.
The most effective way of doing so is by describing how you interpreted any given kind of text. Next you should allow the other party to correct your assumptions.
"I interpreted what you've written here as such and such, and want to make it clear that my answer is based on that interpretation. If you didn't mean it the way I interpreted it, please let me know."
From your point of view, you can be one step ahead and clarify your emotions and possible underlying feelings within the text. Furthermore, by reviewing what you've written, you can often identify parts that might be misconstrued and rewrite them. In some cases it even helps identify that you as well were driven by emotions without knowing and help prevent further escalation by rephrasing before posting.
One important thing to keep in mind at all times is that resolving a conflict doesn't mean all parties have to agree. It's perfectly fine to step away without agreement. There are conflicts that can't be resolved, and you'll have to find a way of dealing with such situations. Sometimes this means that you both agree to disagree, but other times it can also result in one of the parties stepping away from a project or community. Conflict resolving can be drastic indeed, but it should always be done in a respectful and civilized manner.
If you keep the goal of resolving a conflict in mind, you can steer the conversation in the right way.
Like I said before, the fifth rule is the emergency break. There are conflicts that you don't want to engage in or where the other party doesn't seem to be open to any kind of other opinion. You'll always have the power to simply step away from those conversations.
In such cases, it would be most polite to let the other party know you won't be engaging this conversation any further, but there might be cases where even such a message wouldn't be welcome. Just step away. You don't have to reply, you don't have to engage. Decide what's worth your time and what's not.
You shouldn't consider "stepping away" as failing: in some cases it's actually the better and smarter thing to do.
Thanks for reading! This post is part of my "Dev Diaries" series where I write about my own and personal experiences as a developer. Would you like to read some more?
If you want to stay up to date about what's happening on this blog, you can follow me on Twitter or subscribe to my newsletter:
Some readers have reached out to me and talked about solutions to the technical problems described in this post. I'd like to make clear that this post isn't about how to deal with or prevent bugs. It's about how I felt during a rather bad and stressful time, to talk about emotions and how to self-reflect and improve. With that out of the way, I hope you find value in this post.
I went home that evening anxious and stressed out. I'm not ashamed to admit I had to fight back a few tears on the train during my commute back home. Those weren't tears of sadness, mind you, rather tears of fear.
It had only been a few hours since my manager came to me saying that one of our clients complained that someone called them, saying they expected to have gotten a call after filling in the contact form and they never did. They asked me to look into it.
See, this client sold a particular service to people, and they could fill in a form on the website to ask for a quote. Depending on their residence the closest office would call them back after they'd filled in that form. This wasn't a super complex form, it had three steps, and asked some information about people's project, some check boxes, radio buttons and drop downs — you've seen such forms before. Every request submitted is called "a lead" (a potential client), hence the name we internally gave the form: "the lead form".
When one of those leads called our client's office to complain about never receiving a phone call, the office manager was clever enough to ask for the lead's email address, making my job easier. All I had to do was go in the database, find the form submission for that lead's email address, and look at what went wrong.
As a junior developer, my first thought was to look for external problems. Every time a lead submitted the form, an email would be sent to the corresponding office with all information, my guess was that either that email ended up in spam for some reason, or that maybe there had been a problem with our mailing service.
I couldn't find an entry in our database for that email address though.
"The address is probably wrong" — I remember thinking. See, the first thing that happens when the form is submitted is that its contents are saved into the database. That way we always have a copy of the data, in case something went wrong with mailing the office. So I told my manager to contact our client, telling them the email address they gave me is probably wrong, and went on with my day.
A few minutes later and my manager tells me that the email address is absolutely correct.
Ok, that means that the form was never submitted then? I sigh out loud: "it probably has to do with all third party javascript trackers and marketing tools we're using on the site". Team marketing was quite into using A/B testing libraries or tools like Mouseflow to record a user's screen so that they can analyse how people interact with the website. I wouldn't be surprised if one of those tools messed up the form submission.
So I call their marketing team and ask if they know anything more about this lead. They tell me they'll look into it and I, again, went on with my day. An hour goes by and I've already forgotten about the whole affair thanks to my "not my problem" mentality. Suddenly my manager's back again: team marketing wants to speak to me and he hands me the phone.
They have found the screen recording for that particular lead. Around 50% of all sessions were recorded, so we were pretty lucky finding it. It turns out the lead actually submitted the form, they'll send me the recording now.
This is the point where I start to consider that it might be my problem after all, but let's not jump to conclusions. I watch the recording, and yes indeed: the lead fills in step one, step two, step three, submits it and… that's not the right page. He should see a "thank you" page after submitting the form. Instead he sees a page with the header and footer of the website, but without any content on it.
That's not good.
I go into the test environment and submit the form with exactly the same data. I end up on the same blank page, and sure enough my submission isn't saved into the database. is this page? I've never seen it before. So I look at the logs. An error: null passed instead of boolean
. That's a type error deep down in my code.
Before moving on, let me tell you about the error, why it happened and why no one knew of it.
So this form had a bunch of input fields, including check boxes. Whenever a lead request was submitted, we captured the data and prepared to save it into the database. It looked something like this:
$formSubmission = new FormSubmission();
$formSubmission->name = $form->get('name');
$formSubmission->email = $form->get('email');
// …
$entityManager->save($formSubmission);
The bug, simple as it may sound, was a simple typo:
$formSubmission->more_feedback_required =
$form->get('more_feedback_requierd');
Can you spot the requierd
when calling $form->get()
?
As it turns out, our form library — written by us for our in-house framework — wouldn't give an error when you're trying to get an unknown property, it simply returns null
.
The database however expected a boolean value, and thus an error was thrown.
This isn't that big of a problem, albeit not that no one knew of it. The second part of the bug was the empty page: this one was served directly from nginx whenever an error would occur — we didn't want visitors to be bothered by error pages. It turned out that one of my colleagues had set it up a few months ago, and I didn't know of it. When a lead saw the empty page, they probably thought the form was submitted just fine, they didn't know there should be a "thank you" page instead.
Next, we never saw the error happening during testing, because the erroneous code only ran in specific cases, and no one thought about testing those. The form itself was unit tested, but not all edge cases were.
Finally we weren't automatically notified of errors in production, which meant no one knew about it until the client complained. At the time, the company didn't do any error monitoring on production, that might seem completely bonkers, but yes: it happens.
I deployed a fix for the typo (which I wrote to begin with), I told my manager about the problem and that it's fixed. In turn he asks me how many people were affected by it. I tell him to check up on this. I go back to my desk and check when this bug was committed and deployed to production. A month ago.
I remember a sickening feeling starting to rise from this point on.
I check the log files, and scan for the error. I'm stunned for a few moments. It's not just a few errors. It's hundreds. Hundreds of leads that we lost over the past month. I started to shake a little. After a few minutes I reported back to my manager. I again explained the bug itself, why it had been undetected for a month, and finally gave him the hard numbers. There was nothing we could do: those leads were lost.
The following days were probably the most stressful in my career up until today. I'm thankful that no one blamed me directly: we all agreed that this was the team's responsibility and not my own. Still I was the one who wrote the typo, I was the one tasked with testing and deploying the changes to the form a month earlier.
I can honestly tell you that I had a hard time dealing with this feeling of guilt, even though my manager and colleagues were understanding.
I could of course point fingers to others: we shouldn't have used our own framework with its quirky form validation; devops should have installed some kind of error tracking; the empty error page should have been more clear. I admit I thought all these thoughts the days after, I even felt anger towards some of my colleagues for a while. But I learned that anger didn't get me anywhere. I better spent my time looking at how I could learn from all of this, instead of pointing fingers at others.
So, after a few weeks of processing, what felt to me like a trauma; I found the courage to look at what I could have done differently. What I could learn from this.
First of: ask my colleagues for reviews. It doesn't matter if I'm a junior or senior; a fresh look on my code is always valuable. I've learned that most people are busy themselves and won't help you unless you specifically ask them to. To this day I often ask my colleagues to review a PR. Even when I might have more years of experience, I highly value my colleagues' input. No matter how long I've been doing this, I still make mistakes, and I shouldn't be too proud to admit that.
Next: I don't use tools I don't know or trust. This wasn't the first time I stumbled upon the limitations of a custom, in-house framework. I've learned that it's better to use tools that are supported by a large open source community. It wasn't my decision which framework to use on that particular project, but it was a requirement for my next job: I only want to use well-known, community supported frameworks when it comes to client projects in production.
Third: I should test better myself, I shouldn't assume that a checkbox is just a checkbox. I should be skeptical towards every line of code I write. This incident is one of the reasons I came to love strongly typed systems more and more. I don't want a language that tries to be smart and juggle types here and there. I want a language that crashes hard when something happens that shouldn't happen, the stronger the type system, the better.
Next: I tend to code more defensively. I constantly ask myself: are null values possible? Anything unexpected that could happen? I prefer to add an extra check rather than assuming it'll work fine.
And finally: I learned not to blame others first. I think it's a trait of many junior developers to think the problem happened somewhere else. It was an important and humbling lesson to learn, and I tend to be more cautious when blaming an external party these days.
It's strange writing this blogpost. I was a young developer when this happened to me, and it's something that I think has had a lasting impact on my programming career. I also think we don't talk about this often enough. I lay awake at night thinking about how angry the client would be, what the impact would be on the company I worked for. Even though I realise most of my feelings were exaggerated, they still were there. I wasn't able to just turn them off. I felt fear and shame, and had little people to talk about it.
We write about our success stories, but what about our mistakes? What about the impact of these kinds of experiences on our mental health? Many of us in the software industry are introverts, or feel like we can't talk about our deepest emotions.
Let's keep an eye out for each other instead.
Thanks for reading! This post is part of my "Dev Diaries" series where I write about my own and personal experiences as a developer. Would you like to read some more?
If you want to stay up to date about what's happening on this blog, you can follow me on Twitter or subscribe to my newsletter:
When I started my blog in 2017 I never imagined it'd be as successful as now. I never seriously invested in SEO or did any aggressive marketing campaigns, and yet here we are: closing in to 2 million sessions, and the end is no where near.
Cumulated amount of sessions per week
I did of course do something to put my content out there. Over the years I found ways to reach people who might be interested in reading what I wrote. But still, there wasn't much effort involved: I'd share a link from time to time; I'd listen to feedback (a thick skin is an asset if you're writing a blog, people on the internet love to tell you you're wrong); I tried to improve my writing.
I guess the reason it feels like little effort is because it's something I actually really enjoy doing. Even though I'm making some money with my blog now, I still consider it a hobby project. There has never been any pressure to write when I didn't want to. I ignored most of the "blogging best practices" because they asked way too much of my free time. I managed to find my own way of doing things, and it turns out it works.
Throughout this series I'll share everything I learned during my blogging journey with you: how to start, how to grow, how to analyse, how to engage with your audience, how to make money by writing on the internet. I'm a developer so some topics will be technical, but even if you've got no programming experience, you'll be able to learn lots — I'll keep things as simple as possible.
So, are you a blogger? Has it been your long-time dream of starting a blog? I'm happy to share my experience with you!
I write about much more technical and blog related topics, if you want to you can subscribe to stay up to date about my content.
You can also subscribe via RSS.Start by making sure brew is up-to-date:
brew update
Next, upgrade PHP. You can either use the built-in php recipe, use tap shivammathur/homebrew-php
. I'd recommend the second approach, since it allows you to easily install several PHP versions and switch between them.
brew upgrade php
shivammathur/homebrew-php
brew tap shivammathur/php
brew install shivammathur/php/php@8.0
To switch between versions, use the following command:
brew link --overwrite --force php@8.0
You can read more in the repository.
Check the current version by running php -v
:
php -v
Restart Nginx or Apache:
sudo nginx -s reload
sudo apachectl restart
And make sure that your local web server also uses PHP 8 by visiting this script:
# index.php, accessible to your web server
phpinfo();
The version should show 8.0.x
.
Note: if you're using Laravel Valet, please keep on reading, you need some extra steps in order for the web server to properly work.
If you're using Laravel Valet, you should do the following steps to upgrade it:
composer global update
You can use valet use
to switch between PHP versions:
valet use php@8.0
valet use php@7.4
Note that if you're using an older Valet version (prior to v2.13.18), when switching from PHP 8 to PHP 7.4 there was a bug that didn't properly update the changes. This was fixed in Valet 2.13.18 so that it now automatically removes the valet socket after having run valet use php@7.4
. If you need to do this manually, you can run:
cd ~/.config/valet
rm valet.sock
valet restart
PHP extensions are installed using pecl. I personally use Imagick, Redis and Xdebug. They can be installed like so:
pecl install imagick
pecl install redis
pecl install xdebug
You can run pecl list
to see which extensions are installed:
pecl list
# Installed packages, channel pecl.php.net:
# =========================================
# Package Version State
# imagick 3.4.4 stable
# redis 5.1.1 stable
# xdebug 2.8.0 stable
You can search for other extensions using pecl search
:
pecl search pdf
# Retrieving data...0%
# ..
# Matched packages, channel pecl.php.net:
# =======================================
# Package Stable/(Latest) Local
# pdflib 4.1.2 (stable) Creating PDF on the fly with the PDFlib library
Make sure to restart your web server after installing new packages:
sudo nginx -s reload
sudo apachectl restart
If you're using Laravel Valet, you should restart it as well.
valet restart
Make sure all extensions are correctly installed and loaded by checking both your PHP webserver and CLI installs:
php -i | grep redis
var_dump(extension_loaded('redis'));
If extensions aren't properly loaded, there are two easy fixes.
First, make sure the extensions are added in the correct ini file. You can run php --ini
to know which file is loaded:
Configuration File (php.ini) Path: /usr/local/etc/php/7.4</hljs>
Loaded Configuration File: /usr/local/etc/php/7.4/php.ini
Scan for additional .ini files in: /usr/local/etc/php/7.4/conf.d
Additional .ini files parsed: /usr/local/etc/php/7.4/conf.d/ext-opcache.ini,
/usr/local/etc/php/7.4/conf.d/php-memory-limits.ini
Now check the ini file:
extension="redis.so"
extension="imagick.so"
zend_extension="xdebug.so"
Note that if you're testing installed extensions via the CLI, you don't need to restart nginx, apache or Valet when making changes to ini settings.
The second thing you can do, if you're updating from an older PHP version which also used pecl to install extension; is to reinstall every extension individually.
pecl uninstall imagick
pecl install imagick
Finally you should test and upgrade your projects for PHP 8 compatibility.
]]>Because of the breaking changes, there's a higher chance you'll need to make some changes in your code to get it running on PHP 8. If you've kept up to date with the latest releases though, the upgrade shouldn't be too hard, since most breaking changes were deprecated before in the 7.* versions. And don't worry, all these deprecations are listed in this post.
Besides breaking changes, PHP 8 also brings a nice set of new features such as the JIT compiler, union types, attributes, and more.
Let's start with all new features, it's quite a list!
Given the dynamically typed nature of PHP, there are lots of cases where union types can be useful. Union types are a collection of two or more types which indicate that either one of those can be used.
public function foo(Foo|Bar $input): int|float;
Note that void
can never be part of a union type, since it indicates "no return value at all". Furthermore, nullable
unions can be written using |null
, or by using the existing ?
notation:
public function foo(Foo|null $foo): void;
public function bar(?Bar $bar): void;
The JIT — just in time — compiler promises significant performance improvements, albeit not always within the context of web requests. I've done my own benchmarks on real-life web applications, and it seems like the JIT doesn't make that much of a difference, if any, on those kinds of PHP projects.
If you want to know more about what the JIT can do for PHP, you can read another post I wrote about it here.
If you're familiar with the null coalescing operator you're already familiar with its shortcomings: it doesn't work on method calls. Instead you need intermediate checks, or rely on optional
helpers provided by some frameworks:
$startDate = $booking->getStartDate();
$dateAsString = $startDate ? $startDate->asDateTimeString() : null;
With the addition of the nullsafe operator, we can now have null coalescing-like behaviour on methods!
$dateAsString = $booking->getStartDate()?->asDateTimeString();
You can read all about the nullsafe operator here.
Named arguments allow you to pass in values to a function, by specifying the value name, so that you don't have to take their order into consideration, and you can also skip optional parameters!
function foo(string $a, string $b, ?string $c = null, ?string $d = null)
{ /* … */ }
foo(
b: 'value b',
a: 'value a',
d: 'value d',
);
You can read about them in-depth in this post.
Attributes, commonly known as annotations in other languages, offers a way to add meta data to classes, without having to parse docblocks.
As for a quick look, here's an example of what attributes look like, from the RFC:
use App\Attributes\ExampleAttribute;
#[ExampleAttribute]
class Foo
{
#[ExampleAttribute]
public const FOO = 'foo';
#[ExampleAttribute]
public $x;
#[ExampleAttribute]
public function foo(#[ExampleAttribute] $bar) { }
}
#[Attribute]
class ExampleAttribute
{
public $value;
public function __construct($value)
{
$this->value = $value;
}
}
Note that this base Attribute
used to be called PhpAttribute
in the original RFC, but was changed with another RFC afterwards. If you want to take a deep dive in how attributes work, and how you can build your own; you can read about attributes in depth on this blog.
You could call it the big brother of the switch
expression: match
can return values, doesn't require break
statements, can combine conditions, uses strict type comparisons and doesn't do any type coercion.
It looks like this:
$result = match($input) {
0 => "hello",
'1', '2', '3' => "world",
};
You can read up on the match expression in detail, over here.
This RFC adds syntactic sugar to create value objects or data transfer objects. Instead of specifying class properties and a constructor for them, PHP can now combine them into one.
Instead of doing this:
class Money
{
public Currency $currency;
public int $amount;
public function __construct(
Currency $currency,
int $amount,
) {
$this->currency = $currency;
$this->amount = $amount;
}
}
You can now do this:
class Money
{
public function __construct(
public Currency $currency,
public int $amount,
) {}
}
There's a lot more to tell about property promotion, you can read about them in this dedicated post.
static
return type RFCWhile it was already possible to return self
, static
wasn't a valid return type until PHP 8. Given PHP's dynamically typed nature, it's a feature that will be useful to many developers.
class Foo
{
public function test(): static
{
return new static();
}
}
mixed
type RFCSome might call it a necessary evil: the mixed
type causes many to have mixed feelings. There's a very good argument to make for it though: a missing type can mean lots of things in PHP:
Because of the reasons above, it's a good thing the mixed
type is added. mixed
itself means one of these types:
array
bool
callable
int
float
null
object
resource
string
Note that mixed
can also be used as a parameter or property type, not just as a return type.
Also note that since mixed
already includes null
, it's not allowed to make it nullable. The following will trigger an error:
// Fatal error: Mixed types cannot be nullable, null is already part of the mixed type.
function bar(): ?mixed {}
This RFC changes throw
from being a statement to being an expression, which makes it possible to throw exception in many new places:
$triggerError = fn () => throw new MyError();
$foo = $bar['offset'] ?? throw new OffsetDoesNotExist('offset');
Previously, PHP used to apply the same inheritance checks on public, protected and private methods. In other words: private methods should follow the same method signature rules as protected and public methods. This doesn't make sense, since private methods won't be accessible by child classes.
This RFC changed that behaviour, so that these inheritance checks are not performed on private methods anymore. Furthermore, the use of final private function
also didn't make sense, so doing so will now trigger a warning:
Warning: Private methods cannot be final as they are never overridden by other classes
Built upon the weakrefs RFC that was added in PHP 7.4, a WeakMap
implementation is added in PHP 8. WeakMap
holds references to objects, which don't prevent those objects from being garbage collected.
Take the example of ORMs, they often implement caches which hold references to entity classes to improve the performance of relations between entities. These entity objects can not be garbage collected, as long as this cache has a reference to them, even if the cache is the only thing referencing them.
If this caching layer uses weak references and maps instead, PHP will garbage collect these objects when nothing else references them anymore. Especially in the case of ORMs, which can manage several hundreds, if not thousands of entities within a request; weak maps can offer a better, more resource friendly way of dealing with these objects.
Here's what weak maps look like, an example from the RFC:
class Foo
{
private WeakMap $cache;
public function getSomethingWithCaching(object $obj): object
{
return $this->cache[$obj]
??= $this->computeSomethingExpensive($obj);
}
}
::class
on objects RFCA small, yet useful, new feature: it's now possible to use ::class
on objects, instead of having to use get_class()
on them. It works the same way as get_class()
.
$foo = new Foo();
var_dump($foo::class);
Whenever you wanted to catch an exception before PHP 8, you had to store it in a variable, regardless whether you used that variable or not. With non-capturing catches, you can omit the variable, so instead of this:
try {
// Something goes wrong
} catch (MySpecialException $exception) {
Log::error("Something went wrong");
}
You can now do this:
try {
// Something goes wrong
} catch (MySpecialException) {
Log::error("Something went wrong");
}
Note that it's required to always specify the type, you're not allowed to have an empty catch
. If you want to catch all exceptions and errors, you can use Throwable
as the catching type.
Already possible when calling a function, trailing comma support was still lacking in parameter lists. It's now allowed in PHP 8, meaning you can do the following:
public function(
string $parameterA,
int $parameterB,
Foo $objectfoo,
) {
// …
}
As a sidenote: trailing commas are also supported in the use
list of closures, this was an oversight and now added via a separate RFC.
DateTime
objects from interfaceYou can already create a DateTime
object from a DateTimeImmutable
object using DateTime::createFromImmutable($immutableDateTime)
, but the other way around was tricky. By adding DateTime::createFromInterface()
and DatetimeImmutable::createFromInterface()
there's now a generalised way to convert DateTime
and DateTimeImmutable
objects to each other.
DateTime::createFromInterface(DateTimeInterface $other);
DateTimeImmutable::createFromInterface(DateTimeInterface $other);
Stringable
interface RFCThe Stringable
interface can be used to type hint anything that implements __toString()
. Whenever a class implements __toString()
, it automatically implements the interface behind the scenes and there's no need to manually implement it.
class Foo
{
public function __toString(): string
{
return 'foo';
}
}
function bar(string|Stringable $stringable) { /* … */ }
bar(new Foo());
bar('abc');
str_contains()
function RFCSome might say it's long overdue, but we finally don't have to rely on strpos()
anymore to know whether a string contains another string.
Instead of doing this:
if (strpos('string with lots of words', 'words') !== false) { /* … */ }
You can now do this
if (str_contains('string with lots of words', 'words')) { /* … */ }
str_starts_with()
and str_ends_with()
functions RFCTwo other ones long overdue, these two functions are now added in the core.
str_starts_with('haystack', 'hay'); // true
str_ends_with('haystack', 'stack'); // true
fdiv()
function PRThe new fdiv()
function does something similar as the fmod()
and intdiv()
functions, which allows for division by 0. Instead of errors you'll get INF
, -INF
or NAN
, depending on the case.
get_debug_type()
function RFCget_debug_type()
returns the type of a variable. Sounds like something gettype()
would do? get_debug_type()
returns more useful output for arrays, strings, anonymous classes and objects.
For example, calling gettype()
on a class \Foo\Bar
would return object
. Using get_debug_type()
will return the class name.
A full list of differences between get_debug_type()
and gettype()
can be found in the RFC.
get_resource_id()
function PRResources are special variables in PHP, referring to external resources. One example is a MySQL connection, another one a file handle.
Each one of those resources gets assigned an ID, though previously the only way to know that id was to cast the resource to int
:
$resourceId = (int) $resource;
PHP 8 adds the get_resource_id()
functions, making this operation more obvious and type-safe:
$resourceId = get_resource_id($resource);
Traits can specify abstract methods which must be implemented by the classes using them. There's a caveat though: before PHP 8 the signature of these method implementations weren't validated. The following was valid:
trait Test {
abstract public function test(int $input): int;
}
class UsesTrait
{
use Test;
public function test($input)
{
return $input;
}
}
PHP 8 will perform proper method signature validation when using a trait and implementing its abstract methods. This means you'll need to write this instead:
class UsesTrait
{
use Test;
public function test(int $input): int
{
return $input;
}
}
token_get_all()
RFCThe token_get_all()
function returns an array of values. This RFC adds a PhpToken
class with a PhpToken::tokenize()
method. This implementation works with objects instead of plain values. It consumes less memory and is easier to read.
From the RFC: "the Uniform Variable Syntax RFC resolved a number of inconsistencies in PHP's variable syntax. This RFC intends to address a small handful of cases that were overlooked."
Lots of people pitched in to add proper type annotations to all internal functions. This was a long standing issue, and finally solvable with all the changes made to PHP in previous versions. This means that internal functions and methods will have complete type information in reflection.
ext-json
always available RFCPreviously it was possible to compile PHP without the JSON extension enabled, this is not possible anymore. Since JSON is so widely used, it's best developers can always rely on it being there, instead of having to ensure the extension exist first.
As mentioned before: this is a major update and thus there will be breaking changes. The best thing to do is take a look at the full list of breaking changes over at the UPGRADING document.
Many of these breaking changes have been deprecated in previous 7.* versions though, so if you've been staying up-to-date over the years, it shouldn't be all that hard to upgrade to PHP 8.
User-defined functions in PHP will already throw TypeError
, but internal functions did not, they rather emitted warnings and returned null
. As of PHP 8 the behaviour of internal functions have been made consistent.
Lots of errors that previously only triggered warnings or notices, have been converted to proper errors. The following warnings were changed.
DivisionByZeroError
exception instead of warningError
exception instead of warningError
exception instead of warningError
exception instead of warningError
exception instead of warningError
exception instead of warningError
exception instead of warningError
exception instead of warningTraversables
can be unpacked: TypeError
exception instead of warningTypeError
exception instead of warningTypeError
exception instead of warningTypeError
exception instead of warningTypeError
exception instead of warningError
exception instead of warningTypeError
exception instead of warningIt's possible that this change might reveal errors that again were hidden before PHP 8. Make sure to set display_errors=Off
on your production servers!
It's now E_ALL
instead of everything but E_NOTICE
and E_DEPRECATED
. This means that many errors might pop up which were previously silently ignored, though probably already existent before PHP 8.
From the RFC: The current default error mode for PDO is silent. This means that when an SQL error occurs, no errors or warnings may be emitted and no exceptions thrown unless the developer implements their own explicit error handling.
This RFC changes the default error will change to PDO::ERRMODE_EXCEPTION
in PHP 8.
While already deprecated in PHP 7.4, this change is now taken into effect. If you'd write something like this:
echo "sum: " . $a + $b;
PHP would previously interpret it like this:
echo ("sum: " . $a) + $b;
PHP 8 will make it so that it's interpreted like this:
echo "sum: " . ($a + $b);
TypeError
:
[] % [42];
$object + 4;
PHP used to interpret each part of a namespace (separated by a backslash \
) as a sequence of tokens. This RFC changed that behaviour, meaning reserved names can now be used in namespaces.
PHP's type system tries to do a lot of smart things when it encounters numbers in strings. This RFC makes that behaviour more consistent and clear.
This RFC fixes the very strange case in PHP where 0 == "foo"
results in true
. There are some other edge cases like that one, and this RFC fixes them.
A few reflection methods have been deprecated:
ReflectionFunction::isDisabled()
ReflectionParameter::getClass()
ReflectionParameter::isCallable()
You should now use ReflectionType
to get information about a parameter's type:
$reflectionParameter->getType()->allowsNull();
If the type is a single type, ReflectionParameter::getType()
returns an instance of ReflectionNamedType
, which you can get its name from and whether it's built-in:
$reflectionParameter->getType()->getName();
$reflectionParameter->getType()->isBuiltin();
If the type is a union type however, you'll get an instance of ReflectionUnionType
, which can give you an array of ReflectionNamedType
like so:
$reflectionParameter->getType()->getTypes();
Checking whether a type is a union or not can be done with an instanceof
check:
if ($reflectionParameter->getType() instanceof ReflectionNamedType) {
// It's a single type
}
if ($reflectionParameter->getType() instanceof ReflectionUnionType) {
// It's a union type
}
Next up, three method signatures of reflection classes have been changed:
ReflectionClass::newInstance($args);
ReflectionFunction::invoke($args);
ReflectionMethod::invoke($object, $args);
Have now become:
ReflectionClass::newInstance(...$args);
ReflectionFunction::invoke(...$args);
ReflectionMethod::invoke($object, ...$args);
The upgrading guide specifies that if you extend these classes, and still want to support both PHP 7 and PHP 8, the following signatures are allowed:
ReflectionClass::newInstance($arg = null, ...$args);
ReflectionFunction::invoke($arg = null, ...$args);
ReflectionMethod::invoke($object, $arg = null, ...$args);
Before PHP 8, sorting algorithms were unstable. This means that the order of equal elements wasn't guaranteed. PHP 8 changes the behaviour of all sorting functions to stable sorting.
From the RFC: Inheritance errors due to incompatible method signatures currently either throw a fatal error or a warning depending on the cause of the error and the inheritance hierarchy.
During the PHP 7.* development, several deprecations were added that are now finalised in PHP 8.
optional
helpers provided by some frameworks:
$startDate = $booking->getStartDate();
$dateAsString = $startDate ? $startDate->asDateTimeString() : null;
The nullsafe operator provides functionality similar to null coalescing, but also supports method calls. Instead of writing this:
$country = null;
if ($session !== null) {
$user = $session->user;
if ($user !== null) {
$address = $user->getAddress();
if ($address !== null) {
$country = $address->country;
}
}
}
PHP 8 allows you to write this:
$country = $session?->user?->getAddress()?->country;
Let's take a look at what this new operator can and cannot do!
Let's start by addressing the most important question: what exactly is the difference between the null coalescing operator and the nullsafe operator?
Let's take a look at this example:
class Order
{
public ?Invoice $invoice = null;
}
$order = new Order();
Here we have an Order
object which has an optional relation to an Invoice
object. Now imagine we'd want to get the invoice's number (if the invoice isn't null). You could do this both with the null coalescing operator and the nullsafe operator:
var_dump($order->invoice?->number);
var_dump($order->invoice->number ?? null);
So what's the difference? While you could use both operators to achieve the same result in this example, they also have specific edge cases only one of them can handle. For example, you can use the null coalescing operator in combination with array keys, while the nullsafe operator can't handle them:
$array = [];
var_dump($array['key']->foo ?? null);
var_dump($array['key']?->foo);
Warning: Undefined array key "key"
The nullsafe operator, on the other hand, can work with method calls, while the null coalescing operator can't. Imagine an Invoice
object like so:
class Invoice
{
public function getDate(): ?DateTime { /* … */ }
// …
}
$invoice = new Invoice();
You could use the nullsafe operator to call format
on the invoice's date, even when it's null
:
var_dump($invoice->getDate()?->format('Y-m-d'));
// null
While the null coalescing operator would crash:
var_dump($invoice->getDate()->format('Y-m-d') ?? null);
Fatal error: Uncaught Error: Call to a member function format() on null
Sometimes you could use either the null coalescing or nullsafe operator, and other times you'd need to use a specific one. The difference is that the nullsafe operator uses a form of "short circuiting": writing ?->
will cause PHP to look at whats on the lefthand side of this operator, if it's null
then the righthand side will simply be discarded. The null coalescing operator is actually an isset
call in disguise on its lefthand operand, which doesn't support short circuiting.
Short circuiting also means that when writing something like this:
$foo?->bar(expensive_function());
expensive_function
would only be executed if $foo
is actually not null
.
It's possible to nest several nullsafe operator calls like so:
$foo?->bar?->baz()?->boo?->baa();
You cannot use the nullsafe operator to write data to objects:
$offer?->invoice?->date = new DateTime();
The nullsafe operator is definitely a missing piece of the puzzle finally added in PHP. Given its dynamic nature, it feels good to have a smooth way of dealing with null
. The difference and overlap between the nullsafe operator and null coalescing operator feels a bit confusing at first, but I'm sure we'll get used to it.
I wanted to dedicate a blog post on how to setup the JIT as well, since there's a few things to talk about.
Honestly, setting up the JIT is one of the most confusing ways of configuring a PHP extension I've ever seen. Luckily there are some configuration shorthands available so that it's more easy to set up. Still it's good to know about the JIT config in depth, so here goes.
First of all, the JIT will only work if opcache is enabled, this is the default for most PHP installations, but you should make sure that opcache.enable
is set to 1 in yourphp.ini
file. Enabling the JIT itself is done by specifying opcache.jit_buffer_size
in php.ini
.
Note that if you're running PHP via the commandline, you can also pass these options via the -d
flag, instead of adding them to php.ini
:
php -dopcache.enable=1 -dopcache.jit_buffer_size=100M
If this directive is excluded, the default value is set to 0, and the JIT won't run. If you're testing the JIT in a CLI script, you'll need to use opcache.enable_cli
instead to enable opcache:
php -dopcache.enable_cli=1 -dopcache.jit_buffer_size=100M
The difference between opcache.enable
and opcache.enable_cli
is that the first one should be used if you're running, for example, the built-in PHP server. If you're actually running a CLI script, you'll need opcache.enable_cli
.
Before continuing, let's ensure the JIT actually works, create a PHP script that's accessible via the browser or the CLI (depending on where you're testing the JIT), and look at the output of opcache_get_status()
:
var_dump(opcache_get_status()['jit']);
The output should be something like this:
array:7 [
"enabled" => true
"on" => true
"kind" => 5
"opt_level" => 4
"opt_flags" => 6
"buffer_size" => 4080
"buffer_free" => 0
]
If enabled
and on
are true, you're good to go!
Next, there's several ways to configure the JIT (and this is where we'll get into the configuration mess). You can configure when the JIT should run, how much it should try to optimise, etc. All of these options are configured using a single (!) config entry: opcache.jit
. It could look something like this:
opcache.enable=1
opcache.jit=1255
Now, what does that number mean? The RFC lists the meaning of each of them. Mind you: this is not a bit mask, each number simply represents another configuration option. The RFC lists the following options:
0 | don't JIT |
1 | minimal JIT (call standard VM handlers) |
2 | selective VM handler inlining |
3 | optimized JIT based on static type inference of individual function |
4 | optimized JIT based on static type inference and call tree |
5 | optimized JIT based on static type inference and inner procedure analyses |
0 | JIT all functions on first script load |
1 | JIT function on first execution |
2 | Profile on first request and compile hot functions on second request |
3 | Profile on the fly and compile hot functions |
4 | Compile functions with @jit tag in doc-comments |
5 | Tracing JIT |
0 | don't perform register allocation |
1 | use local liner-scan register allocator |
2 | use global liner-scan register allocator |
0 | none |
1 | enable AVX instruction generation |
One small gotcha: the RFC lists these options in reverse order, so the first digit represents the C
value, the second the R
, and so on. Why there simply weren't four configuration entries added is beyond my comprehension, probably to make configuring the JIT faster… right?
Anyways, internals propose 1255
as the best default, it will do maximum jitting, use the tracing JIT, use a global liner-scan register allocator — whatever that might be — and enables AVX instruction generation.
So your ini settings (or -d
flags) should have these values:
opcache.enable=1
opcache.jit_buffer_size=100M
opcache.jit=1255
Keep in mind that opcache.jit
is optional by the way. The JIT will use a default value if that property is omitted.
Which default, you ask? That would be opcache.jit=tracing
.
Hang on, that's not the strange bitmask-like structure we saw earlier? That's right: after the original RFC passed, internals recognised that the bitmask-like options weren't all that user-friendly, so they added two aliases which are translated to the bitmask under the hood. There's opcache.jit=tracing
and opcache.jit=function
.
The difference between the two is that the function JIT will only try to optimise code within the scope of a single function, while the tracing JIT can look at the whole stack trace to identify and optimise hot code. Internals recommends to use the tracing JIT, because it'll probably almost always give the best results. You can read about those results in the benchmarks I've done.
So the only option you actually need to set to enable the JIT with its optimal configuration is opcache.jit_buffer_size
, but if you want to be explicit, listing opcache.jit
wouldn't be such a bad idea:
opcache.enable=1
opcache.jit_buffer_size=100M
opcache.jit=tracing
]]>The last year and a half I've been focussing on more specific topics: the "What's new in PHP" series has been really popular, and the in-depth PHP feature posts have sparked many great discussions and conversations. With those PHP-related topics, I always envisioned them to be part of a greater whole and not separate posts. I've been trying to tell the story of modern PHP for a while now, and I'm trying to make it as focussed and of the highest quality as possible.
So that's why I'm introducing the next chapter, or better said: a bundle of chapters. I'm working on a book that teaches you modern PHP, its best practices and the community surrounding it — it's called "Front Line PHP". Some parts of it will be based on what I've written throughout the years on this blog, but large parts will be brand new. I'll cover the language itself, patterns and principles, frameworks, and most importantly: the mindset of a professional web developer. I'm really looking forward being able to share everything I want as a unified whole.
Like my previous book, I'm working together with Spatie — the company I work at — to ensure it'll be the greatest product possible. You can expect Front Line PHP to launch in December 2020, right around the time PHP 8 arrives. It'll be accompanied by a free video series, teaching you all about PHP 8 in practice.
I'm really looking forward to sharing all of this with you, so make sure you subscribe to the dedicated newsletter to receive all relevant updates. If you want to know a little more, head over to the Front Line PHP website, which will receive regular updates over the coming weeks. I hope you're as excited as I am. If you have any questions or remarks, you're of course free to reach out to me via Twitter or e-mail.
Thanks!
]]>So to help maintainers and contributors in creating quality open source, here are a few tips!
Even though most PRs are well meant, it's important to first look around the repo for existing issues or PRs addressing the same problem. Someone else might already be working on a similar PR or there might be issues preventing the PR from being made. If you want to help out, start by participating in the discussions already happening, instead of working on your own.
If nothing has been said about your feature, it might be a good idea to discuss it first, instead of going ahead and potentially changing hundreds of lines of code. I'll be the one who has to maintain your PR for the foreseeable future, so it's best to first discuss it with me! I can probably give you some tips about the code base, as well as tell you about our expectations regarding implementation.
You can choose to open an issue first, or submit a draft PR with a minimal implementation, a proof of concept. I actually really like those draft PRs: it visualises what you want to change, but also indicates that you realise there's more work to do.
Speaking of expectations, check whether the repository you're committing to has any code style guide or linter set up. Follow those rules. Don't submit a PR that deliberately uses other styling rules. You're submitting code to another codebase, and that style guide has to be followed.
A note to maintainers: you can help out here by using GitHub actions to automatically run your linters on PRs.
I'm going to review your code, so I'd like it to be as clear as possible. This starts by writing clean code: using proper variable names, maybe even add a doc block here and there; but it's equally important to explain your thought process as well. You could use review comments to clarify specific parts of the code, or you could add some general comments in the PR.
Please try to avoid "wip"
commit messages, please? You don't have to write a book for every commit, and I realise that you might not want to think about them while in the zone; but something like "Refactor XX to YY"
is already infinitely better than "wip"
. If you really want a good commit message, try to have your commits only do one thing, and try to explain why this commit is necessary.
Only submit changes that are relevant within the scope of the PR. You might be tempted to fix another thing or two while at it — and you're allowed to — but keep those changes in a separate PR.
You can always base a new branch on your PR branch if necessary, and mention in the second PR that it depends on the first one to be merged. I'm happy to merge them for you, in the right order.
It might take a while for maintainers to merge your PR. I even confess to have lost track of a few PRs over the past years. A friendly "bump" after a few days is always appreciated, but it still might take some time. Remember that most OSS maintainers are doing this on a voluntary basis, so don't be mad when a PR takes a little longer to merge.
This goes for both maintainers and contributors: don't be a jerk. Sometimes a PR gets declined, sometimes a maintainers looses their patience. Stop and breath, realise it's not the end of the world and move on. Be friendly and respectful.
This one is for the maintainers: one of the most frustrating things is a PR getting accepted, and than having to wait another month for a release to be tagged. You shouldn't be afraid of high version numbers, that's what semver is for. Tag the release as soon as possible, and please don't wait another week!
Do you have any more tips? Let them know via Twitter or email. Here's already another good read by my Colleague Sebastian, on how to write a good issue.
]]>Before looking at theory, grab a pair of sunglasses if you have any laying around. With both eyes open, cover only one eye with one of the sunglass glasses. Make it so you're looking through your sunglasses with one eye, and use the other one like you're used to.
With that setup in place, have fun watching this video in 3D!
Did you see the 3D effect? It might not work as well for parts of the video and some of you might not notice the 3D effect at all. That's fine, this post isn't about 3D, but it is about the reason why you can watch that video in 3D with only a pair of sunglasses: the Pulfrich effect.
The Pulfrich effect is a psychophysical percept wherein lateral motion of an object in the field of view is interpreted by the visual cortex as having a depth component, due to a relative difference in signal timings between the two eyes.
All sources are listed in the footnotes by the way, you'll find them at the end of this post.
To clarify, the 3D effect in the video is indeed your brain tricking you; it thinks there's depth in a moving flat image because there's a slight difference in timing between your left and right eye. What's interesting though is what causes that timing difference. Can you guess? It's because you covered one eye with sunglasses, making the image darker. It turns out that dark images take longer to process than light ones.
The Pulfrich effect […] yields about a 15 ms delay for a factor of ten difference in average retinal illuminance
By only covering one eye with sunglasses, you add a few milliseconds of delay to that one. The exact delay will depend on the brightness of the screen and the darkness of the sunglasses, which might explain why some people see the 3D effect better than others. The timing difference between your eyes causes your brain to interpret that image as having depth, hence 3D.
Now on to programming. If you're using a dark colour scheme, you're deliberately adding extra delay, so says the Pulfrich effect. Sure the difference seems negligible, it's only a few milliseconds. Actually, it's a few milliseconds every time you "rescan" your screen; that's between 10 or 50 times a second, depending on what research you want to believe. Still you probably won't notice any real-time difference, but over time this adds up, and the extra effort needed by your eyes can become to feel exhausting.
Besides the Pulfrich effect, there are other reasons that make light colour schemes superior. First of there's what human eyes are used to, what they are built for. Most of us are awake during the day and asleep at night. The human eye is better adapted to interpreting light scenes with dark points of focus, instead of the other way around.
On the other hand there's the case of astigmatism, which is caused by an imperfection of your corneas or lenses. It's estimated that between 30% and 60% of adults in Europe and Asia have it (I actually have it myself, which is why I wear glasses). For people with astigmatism, a bright display with dark text is easier to read, because the iris closes a little more given the additional light; which decreases the impact of the defect in your cornea or lens.
As a sidenote: if you often experience headaches after a day of programming, you might want to test for astigmatism. Glasses make a world of difference
Lastly, there have been extensive studies about the readability of computer screens, one example is a study by Etienne Grandjean, called "Ergonomic Aspects of Visual Display Terminals". You can't read it online; if you manage to find it in a library you should check out pages 137-142. Its conclusion, like several other studies is that it's indeed easier to read dark text on a light background, then the other way around.
Often when I share these arguments with someone who clings to the dark side, they tell me light colour schemes hurt their eyes because they are too bright; you might be thinking the same right now. I've got two answers for you.
First: you don't need to use a white #fff
background with black #000
text. There's lots of light colour schemes that don't go to the extreme ends. The important thing is that there's enough contrast between fore- and background, and that the background is lighter than the foreground.
Second: you can always adjust the brightness of your screen. You don't need to turn it up to a 100%! You'd only do that if the text is otherwise unreadable, and guess when that happens? If you'd use a dark scheme!
I don't want to end with theory though. Over the past three years, I've put light themes to the test: I've challenged myself and dozens of others to switch to a light theme for one week. I wanna do the same with you: try it for one week, and let me know whether you're switching back to a dark theme or not. Based on my past experiments I can tell you that only a few people decide to switch back. The majority stays with a light scheme because, guess what, it's actually better.
Now I reckon there are people who can't use a light colour scheme because of an eye illness. There are legitimate cases when dark colour schemes are better for some people's health, the exceptions to the rule.
So try it out, and let me know your findings via Twitter or e-mail!
PS: if you're using PhpStorm or any other JetBrain IDE, you can check these two light colour schemes:
]]>This is a mail I sent to PHP's internals, these are my thoughts, and you can follow the internals discussion here, or share your own thoughts on Reddit.
Hello internals
Today I'd like to hear your thoughts on what might be a controversial topic, though I think it's worth having this discussion. I want to make the case for adding generic syntax, without actually enforcing any additional type checks at runtime. Please hear me out.
We've been discussing generics for years now [1][2], all without any result. Nikita's latest attempt [3] stalled because, from what I gathered and amongst other things, doing generic type checks at runtime has a significant impact on performance.
On the other hand, static analysers have been making their rise for a few years now. Granted: not the whole community might like this kind of type strictness, and PHP doesn't force them to; but still projects like PhpStorm acknowledge their significance — they will add built-in support for both psalm and PHPStan later this year [4]. Rasmus Lerdorf also showed interest in the idea of improving PHP's static analysis capabilities two years ago [5].
That all to say that there's a significant part of the PHP community who's interested in embracing the benefits of static analysis.
If we look outside of our PHP bubble, we can see the same thing happening in JavaScript: the core benefit that TypeScript adds is its robust static analysis. Sure those developers need an extra compilation step to transpile their code to plain old JavaScript, but it seems that they are… fine with that?
I'd like to discuss a similar idea for PHP. If runtime generics aren't possible because of performance issues, why not explore the other option: adding generic syntax that is ignored by the interpreter, but can be used by static analysis tools — third party of built-into PHP, that's another discussion. I realise this thought goes against the "PHP mindset" we've been programming with for more than 20 years, but we shouldn't ignore what's happening in the PHP- and wider programming community: static analysis is relevant, whether you want to use it or not, and a stricter type system is preferred by many.
Now I know there are alternatives we can use today. Static analysers already support generics, using doc blocks. I'm not trying to argue that it's impossible to achieve the same results with the toolset we have, but rather that there's room for improvement from the developer's point of view. History has shown that such convenience additions to PHP have been a difficult pill to swallow for some, but on the other hand those kind of changes have been happening more and more often anyway: property promotion, short closures, named arguments, attributes, yes even types themselves: you can write the same working PHP program without any of those features, and yet they have been proven so useful and wanted over the last years.
As a sidenote: the idea of transpiling is already present in PHP. Looking at constructor property promotion: a purely syntactical feature, which is transformed to simpler PHP code at runtime. Nikita called this principle "desugaring" in the constructor property promotion RFC [6].
So here's my case for transpiled generics summarized:
So with all that being said, I'm looking forward to hearing your thoughts.
Kind regards
Brent
Both managers were looking at me in disbelief, they seemed to be stunned for a few seconds. I just told them I had decided to resign. One of them quickly recovered, smiled, and said he regretted my decision; though he also realised it's expected in our line of work: people usually don't spend more than a few years at the same company.
I didn't want to chit-chat much longer, so I nodded, apologised (I'm not sure why), and told them I'd be checking out the paperwork with the office manager later that week. I left the room, went back to my desk.
I received a mail from the other manager over the weekend, the one who didn't say much during our conversation. He wrote he was perplexed, didn't see this coming, and regretted the decision tremendously. He told me if there was anything he could do to change my mind, I should tell him.
My decision to leave was final though.
Mind you, it wasn't a financial one; in fact I was just about to get a significant raise and more responsibilities. It also wasn't a relational one, I really appreciated all of my colleagues back then; I'd even call some of them good friends.
I decided to leave because I got stuck and there wasn't any room to grow anymore. The perspective of being a developer who's 5 years behind of modern day practices made me miserable.
Even though I'd been advocating within the company to make significant changes, both on a technical level, as well as on the management side; it didn't seem achievable. We were still struggling to deliver the quality we promised our clients, we were using out of date technologies, I went home feeling down and depressed almost every day.
I wasn't the only one, by the way. During the 3 years I worked for this company, 8 out of 25 people left, and around the same amount were hired; always young developers, straight from school, just like I three years before. Every time this happened, it was described as "the normal flow" a web development company has to deal with, just like our manager told me during my resignation.
I've since realised that this manager was wrong: it's not normal to switch jobs every 5 years, to have a third of your company come and go, and be replaced with inexperienced developers. And mind you: there should always be room for those juniors to grow, but who will teach them if most of the experienced developers left?
No, these kinds of things are only "normal" when your company fails to invest in people the way it should. And that, it turns out, is very common indeed.
I don't regret having worked for that company: I did learn valuable lessons there. It's ok to be at a place where there's little or no room for growth, as long as it's not too long. You've got to watch out, and critically assess your situation from time to time; you might get stuck without even knowing it.
My advice? Either try to change your position and responsibilities within the company or, if that doesn't work, change jobs. I realise that's easy to write, like I also realise it's not as easy as it sounds. Personally, I noticed being stuck after two years and it took me another whole year to find a place where I believed there was enough room to grow.
Whatever turns your path takes, knowing you want to go somewhere and not stand idle is the most important first step.
Thanks for reading! This post is part of my "Dev Diaries" series where I write about my own and personal experiences as a developer. Would you like to read some more?
If you want to stay up to date about what's happening on this blog, you can follow me on Twitter or subscribe to my newsletter:
I wanted to share these talks here on my blog, because I figured some of you might be interested in them, and this way I can revisit them in the future.
Starting with Martin Fowler, who explains the basics of event driven development, the pros and cons, as well as the different patterns that can be applied on top of EDD, one of them event sourcing.
Next Greg Young, one of the founding fathers of event sourcing and CQRS. What's most interesting in this talk is the misconceptions Greg talks about. The one that stood out to me most, is that event sourcing isn't a top-level architecture, it's a pattern that should be applied in parts of your projects where relevant. A great insight, one that has guided us throughout our latest project.
Next up an old talk be Eric Evans. I know the video and sound quality is crap, but the way he talks about splitting large systems in small pieces is awesome.
The greatest insight for me is how he explains the concept of micro services within the context of one large system. Eric explains concrete ways of dealing with such a split system, which directly ties in the point made by Greg Young earlier: event sourcing should only be applied in parts of your system. Eric gives us concrete strategies of doing that.
Finally, putting everything into practice with code: Freek shows a hands-on integration of event sourcing into a Laravel projects, the framework I also work with daily.
setcookie(
name: 'test',
expires: time() + 60 * 60 * 2,
);
Named arguments used on a built-in PHP function
class CustomerData
{
public function __construct(
public string $name,
public string $email,
public int $age,
) {}
}
$data = new CustomerData(
name: $input['name'],
email: $input['email'],
age: $input['age'],
);
A DTO making use of promoted properties, as well as named arguments
$data = new CustomerData(...$customerRequest->validated());
Named arguments also support array spreading
You might have guessed it from the examples: named arguments allow you to pass input data into a function, based on their argument name instead of the argument order.
I would argue named arguments are a great feature that will have a significant impact on my day-to-day programming life. You're probably wondering about the details though: what if you pass a wrong name, what's up with that array spreading syntax? Well, let's look at all those questions in-depth.
Let's say this feature was a highly debated one, and there were some counter arguments to not adding them. However, I'd say their benefit far outweigh the fear of backwards compatibility problems or bloated APIs. The way I see it, they will allow us to write cleaner and more flexible code.
For one, named arguments allow you to skip default values. Take a look again at the cookie example:
setcookie(
name: 'test',
expires: time() + 60 * 60 * 2,
);
Its method signature is actually the following:
setcookie (
string $name,
string $value = "",
int $expires = 0,
string $path = "",
string $domain = "",
bool $secure = false,
bool $httponly = false,
) : bool
In the example I showed, we didn't need to set the a cookie $value
, but we did need to set an expiration time. Named arguments made this method call a little more concise:
setcookie(
'test',
'',
time() + 60 * 60 * 2,
);
setcookie
without named arguments
setcookie(
name: 'test',
expires: time() + 60 * 60 * 2,
);
setcookie
with named arguments
Besides skipping arguments with default values, there's also the benefit of having clarity about which variable does what; something that's especially useful in functions with large method signatures. Now we could say that lots of arguments are usually a code smell; we still have to deal with them no matter what, so it's better to have a sane way of doing so, than nothing at all.
With the basics out of the way, let's look at what named arguments can and can't do.
First of all, named arguments can be combined with unnamed — also called ordered — arguments. In that case the ordered arguments must always come first.
Take our DTO example from before:
class CustomerData
{
public function __construct(
public string $name,
public string $email,
public int $age,
) {}
}
You could construct it like so:
$data = new CustomerData(
$input['name'],
age: $input['age'],
email: $input['email'],
);
However, having an ordered argument after a named one would throw an error:
$data = new CustomerData(
age: $input['age'],
$input['name'],
email: $input['email'],
);
Next, it's possible to use array spreading in combination with named arguments:
$input = [
'age' => 25,
'name' => 'Brent',
'email' => 'brent@stitcher.io',
];
$data = new CustomerData(...$input);
If, however, there are missing required entries in the array, or if there's a key that's not listed as a named argument, an error will be thrown:
$input = [
'age' => 25,
'name' => 'Brent',
'email' => 'brent@stitcher.io',
'unknownProperty' => 'This is not allowed',
];
$data = new CustomerData(...$input);
It is possible to combine named and ordered arguments in an input array, but only if the ordered arguments follow the same rule as before: they must come first!
$input = [
'Brent',
'age' => 25,
'email' => 'brent@stitcher.io',
];
$data = new CustomerData(...$input);
If you're using variadic functions, named arguments will be passed with their key name into the variadic arguments array. Take the following example:
class CustomerData
{
public static function new(...$args): self
{
return new self(...$args);
}
public function __construct(
public string $name,
public string $email,
public int $age,
) {}
}
$data = CustomerData::new(
email: 'brent@stitcher.io',
age: 25,
name: 'Brent',
);
In this case, $args
in CustomerData::new
will contain the following data:
[
'age' => 25,
'email' => 'brent@stitcher.io',
'name' => 'Brent',
]
Attributes — also known as annotations — also support named arguments:
class ProductSubscriber
{
#[ListensTo(event: ProductCreated::class)]
public function onProductCreated(ProductCreated $event) { /* … */ }
}
It's not possible to have a variable as the argument name:
$field = 'age';
$data = CustomerData::new(
$field: 25,
);
And finally, named arguments will deal in a pragmatic way with name changes during inheritance. Take this example:
interface EventListener {
public function on($event, $handler);
}
class MyListener implements EventListener
{
public function on($myEvent, $myHandler)
{
// …
}
}
PHP will silently allow changing the name of $event
to $myEvent
, and $handler
to $myHandler
; but if you decide to use named arguments using the parent's name, it will result in a runtime error:
public function register(EventListener $listener)
{
$listener->on(
event: $this->event,
handler: $this->handler,
);
}
Runtime error in case $listener
is an instance of MyListener
This pragmatic approach was chosen to prevent a major breaking change when all inherited arguments would have to keep the same name. Seems like a good solution to me.
That's most there is to tell about named arguments. If you want to know a little more backstory behind some design decisions, I'd encourage you to read the RFC.
Are you looking forward to using named arguments? Let me know via Twitter or via e-mail!
]]>I'm going to try not to abuse attributes, but I think configuring event listeners is an example of an annotation I'll be using extensively.
You might know that I've been working on event sourced systems lately, and I can tell you: there's lots of event configuration to do. Take this simple projector, for example:
// Before
class CartsProjector implements Projector
{
use ProjectsEvents;
protected array $handlesEvents = [
CartStartedEvent::class => 'onCartStarted',
CartItemAddedEvent::class => 'onCartItemAdded',
CartItemRemovedEvent::class => 'onCartItemRemoved',
CartExpiredEvent::class => 'onCartExpired',
CartCheckedOutEvent::class => 'onCartCheckedOut',
CouponAddedToCartItemEvent::class => 'onCouponAddedToCartItem',
];
public function onCartStarted(CartStartedEvent $event): void
{ /* … */ }
public function onCartItemAdded(CartItemAddedEvent $event): void
{ /* … */ }
public function onCartItemRemoved(CartItemRemovedEvent $event): void
{ /* … */ }
public function onCartCheckedOut(CartCheckedOutEvent $event): void
{ /* … */ }
public function onCartExpired(CartExpiredEvent $event): void
{ /* … */ }
public function onCouponAddedToCartItem(CouponAddedToCartItemEvent $event): void
{ /* … */ }
}
PHP 7.4
There are two benefits attributes will give me:
Luckily, PHP 8 solves these problems:
class CartsProjector implements Projector
{
use ProjectsEvents;
#[SubscribesTo(CartStartedEvent::class)]
public function onCartStarted(CartStartedEvent $event): void
{ /* … */ }
#[SubscribesTo(CartItemAddedEvent::class)]
public function onCartItemAdded(CartItemAddedEvent $event): void
{ /* … */ }
#[SubscribesTo(CartItemRemovedEvent::class)]
public function onCartItemRemoved(CartItemRemovedEvent $event): void
{ /* … */ }
#[SubscribesTo(CartCheckedOutEvent::class)]
public function onCartCheckedOut(CartCheckedOutEvent $event): void
{ /* … */ }
#[SubscribesTo(CartExpiredEvent::class)]
public function onCartExpired(CartExpiredEvent $event): void
{ /* … */ }
#[SubscribesTo(CouponAddedToCartItemEvent::class)]
public function onCouponAddedToCartItem(CouponAddedToCartItemEvent $event): void
{ /* … */ }
}
PHP 8
A smaller one, but this one will have a day-by-day impact. I often find myself still needing doc blocks because of two things: static return types and generics. The latter one can't be solved yet, but luckily the first one will in PHP 8!
When I'd write this in PHP 7.4:
/**
* @return static
*/
public static function new()
{
return new static();
}
PHP 7.4
I'll now be able to write:
public static function new(): static
{
return new static();
}
PHP 8
If you read my blog, you know I wrote quite a bit about the use of PHP's type system combined with data transfer objects. Naturally, I use lots of DTOs in my own code, so you can imagine how happy I am, being able to rewrite this:
class CustomerData extends DataTransferObject
{
public string $name;
public string $email;
public int $age;
public static function fromRequest(
CustomerRequest $request
): self {
return new self([
'name' => $request->get('name'),
'email' => $request->get('email'),
'age' => $request->get('age'),
]);
}
}
$data = CustomerData::fromRequest($customerRequest);
PHP 7.4
As this:
class CustomerData
{
public function __construct(
public string $name,
public string $email,
public int $age,
) {}
}
$data = new CustomerData(...$customerRequest->validated());
PHP 8
Note the use of both constructor property promotion, as well as named arguments. Yes, they can be passed using named arrays and the spread operator!
Do you sometimes find yourself using an enum with some methods on it, that will give a different result based on the enum value?
/**
* @method static self PENDING()
* @method static self PAID()
*/
class InvoiceState extends Enum
{
private const PENDING = 'pending';
private const PAID = 'paid';
public function getColour(): string
{
return [
self::PENDING => 'orange',
self::PAID => 'green',
][$this->value] ?? 'gray';
}
}
PHP 7.4
I would argue that for more complex conditions, you're better off using the state pattern, yet there are cases where an enum does suffice. This weird array syntax already is a shorthand for a more verbose conditional:
/**
* @method static self PENDING()
* @method static self PAID()
*/
class InvoiceState extends Enum
{
private const PENDING = 'pending';
private const PAID = 'paid';
public function getColour(): string
{
if ($this->value === self::PENDING) {
return 'orange';
}
if ($this->value === self::PAID) {
return 'green'
}
return 'gray';
}
}
PHP 7.4 — alternative
But with PHP 8, we can use the match
expression instead!
/**
* @method static self PENDING()
* @method static self PAID()
*/
class InvoiceState extends Enum
{
private const PENDING = 'pending';
private const PAID = 'paid';
public function getColour(): string
{
return match ($this->value) {
self::PENDING => 'orange',
self::PAID => 'green',
default => 'gray',
};
}
}
PHP 8
When I mentioned the static
return type before, I forgot another use case where docblock type hints were required: union types. At least, they were required before, because PHP 8 supports them natively!
/**
* @param string|int $input
*
* @return string
*/
public function sanitize($input): string;
PHP 7.4
public function sanitize(string|int $input): string;
PHP 8
Before PHP 8, you couldn't use throw
in an expression, meaning you'd have to do explicit checks like so:
public function (array $input): void
{
if (! isset($input['bar'])) {
throw BarIsMissing::new();
}
$bar = $input['bar'];
// …
}
PHP 7.4
In PHP 8, throw
has become an expression, meaning you can use it like so:
public function (array $input): void
{
$bar = $input['bar'] ?? throw BarIsMissing::new();
// …
}
PHP 8
If you're familiar with the null coalescing operator you're already familiar with its shortcomings: it doesn't work on method calls. Instead you need intermediate checks, or rely on optional
helpers provided by some frameworks:
$startDate = $booking->getStartDate();
$dateAsString = $startDate ? $startDate->asDateTimeString() : null;
PHP 7.4
With the addition of the nullsafe operator, we can now have null coalescing-like behaviour on methods!
$dateAsString = $booking->getStartDate()?->asDateTimeString();
PHP 8
What's your favourite PHP 8 feature? Let me know via Twitter or via e-mail!
]]>?:
, the null coalescing ??
and the spaceship <=>
operators.
But do you really know how they work?
Understanding these operators makes you use them more, resulting in a cleaner codebase.
Before looking at each operator in depth, here's a summary of what each of them does:
The ternary operator is a shorthand for the if {} else {}
structure. Instead of writing this:
if ($condition) {
$result = 'foo'
} else {
$result = 'bar'
}
You can write this:
$result = $condition ? 'foo' : 'bar';
If this $condition
evaluates to true
, the lefthand operand will be assigned to $result
.
If the condition evaluates to false
, the righthand will be used.
Interesting fact: the name ternary operator actually means "an operator which acts on three operands".
An operand is the term used to denote the parts needed by an expression.
The ternary operator is the only operator in PHP which requires three operands:
the condition, the true
and the false
result. Similarly, there are also binary and unary operators.
You can read more about it here.
Back to ternary operators: do you know which expressions evaluate to true
, and which don't?
Take a look at the boolean
column of this table.
The ternary operator will use its lefthand operand when the condition evaluates to true
.
This could be a string, an integer, a boolean etc.
The righthand operand will be used for so called "falsy values".
Examples would be 0
or '0'
, an empty array or string, null
, an undefined or unassigned variable, and of course false
itself.
All these values will make the ternary operator use its righthand operand.
Since PHP 5.3, it's possible to leave out the lefthand operand, allowing for even shorter expressions:
$result = $initial ?: 'default';
In this case, the value of $result
will be the value of $initial
, unless $initial
evaluates to false
,
in which case the string 'default'
is used.
You could write this expression the same way using the normal ternary operator:
$result = $condition ? $condition : 'default';
Ironically, by leaving out the second operand of the ternary operator, it actually becomes a binary operator.
The following, even though it seems logical; doesn't work in PHP:
$result = $firstCondition
? 'truth'
: $elseCondition
? 'elseTrue'
: 'elseFalse';
The reason because is that the ternary operator in PHP is left-associative, and thus parsed in a very strange way.
The above example would always evaluate the $elseCondition
part first, so even when $firstCondition
would be true
, you'd never see its output.
I believe the right thing to do is to avoid nested ternary operators altogether. You can read more about this strange behaviour in this Stack Overflow answer.
Furthermore, as PHP 7.4, the use of chained ternaries without brackets is deprecated.
Did you take a look at the types comparison table earlier?
The null coalescing operator is available since PHP 7.0.
It similar to the ternary operator, but will behave like isset
on the lefthand operand instead of just using its boolean value.
This makes this operator especially useful for arrays and assigning defaults when a variable is not set.
$undefined ?? 'fallback'; // 'fallback'
$unassigned;
$unassigned ?? 'fallback'; // 'fallback'
$assigned = 'foo';
$assigned ?? 'fallback'; // 'foo'
'' ?? 'fallback'; // ''
'foo' ?? 'fallback'; // 'foo'
'0' ?? 'fallback'; // '0'
0 ?? 'fallback'; // 0
false ?? 'fallback'; // false
The null coalescing operator takes two operands, making it a binary operator. "Coalescing" by the way, means "coming together to form one mass or whole". It will take two operands, and decide which of those to use based on the value of the lefthand operand.
This operator is especially useful in combination with arrays, because of its acts like isset
.
This means you can quickly check for the existence of keys, even nested keys, without writing verbose expressions.
$input = [
'key' => 'key',
'nested' => [
'key' => true
]
];
$input['key'] ?? 'fallback'; // 'key'
$input['nested']['key'] ?? 'fallback'; // true
$input['undefined'] ?? 'fallback'; // 'fallback'
$input['nested']['undefined'] ?? 'fallback'; // 'fallback'
null ?? 'fallback'; // 'fallback'
The first example could also be written using a ternary operator:
$output = isset($input['key']) ? $input['key'] : 'fallback';
Note that it's impossible to use the shorthand ternary operator when checking the existence of array keys. It will either trigger an error or return a boolean, instead of the real lefthand operand's value.
// Returns `true` instead of the value of `$input['key']`
$output = isset($input['key']) ?: 'fallback'
// The following will trigger an 'undefined index' notice
// when $input is no array or has no 'key'.
//
// It will trigger an 'undefined variable' notice
// when $input doesn't exist.
$output = $input['key'] ?: 'fallback';
The null coalescing operator can easily be chained:
$input = [
'key' => 'key',
];
$input['undefined'] ?? $input['key'] ?? 'fallback'; // 'key'
It's possible to use the null coalescing operator on nested object properties, even when a property in the chain is null
.
$a = (object) [
'prop' => null,
];
var_dump($a->prop->b ?? 'empty');
// 'empty'
In PHP 7,4, we can expect an even shorter syntax called the "null coalescing assignment operator".
// This operator will be available in PHP 7.4
function (array $parameters = []) {
$parameters['property'] ??= 'default';
}
In this example, $parameters['property']
will be set to 'default'
, unless it is set in the array passed to the function.
This would be equivalent to the following, using the current null coalescing operator:
function (array $parameters = []) {
$parameters['property'] = $parameters['property'] ?? 'default';
}
The spaceship operator, while having quite a peculiar name, can be very useful.
It's an operator used for comparison.
It will always return one of three values: 0
, -1
or 1
.
0
will be returned when both operands are equals,
1
when the left operand is larger, and -1
when the right operand is larger.
Let's take a look at a simple example:
1 <=> 2; // Will return -1, as 2 is larger than 1.
This simple example isn't all that exiting, right? However, the spaceship operator can compare a lot more than simple values!
// It can compare strings,
'a' <=> 'z'; // -1
// and arrays,
[2, 1] <=> [2, 1]; // 0
// nested arrays,
[[1, 2], [2, 2]] <=> [[1, 2], [1, 2]]; // 1
// and even casing.
'Z' <=> 'z'; // -1
Strangely enough, when comparing letter casing, the lowercase letter is considered the highest. There's a simple explanation though. String comparison is done by comparing character per character. As soon as a character differs, their ASCII value is compared. Because lowercase letters come after uppercase ones in the ASCII table, they have a higher value.
The spaceship operator can almost compare anything, even objects. The way objects are compared is based on the kind of object. Built-in PHP classes can define their own comparison, while userland objects are compared based on their attributes and values.
When would you want to compare objects you ask? Well, there's actually a very obvious example: dates.
$dateA = DateTime::createFromFormat('Y-m-d', '2000-02-01');
$dateB = DateTime::createFromFormat('Y-m-d', '2000-01-01');
$dateA <=> $dateB; // Returns 1
Of course, comparing dates is just one example, but a very useful one nevertheless.
One great use for this operator, is to sort arrays.
There are quite a few ways to sort an array in PHP,
and some of these methods allow a user defined sort function.
This function has to compare two elements, and return 1
, 0
, or -1
based on their position.
An excellent use case for the spaceship operator!
$array = [5, 1, 6, 3];
usort($array, function ($a, $b) {
return $a <=> $b;
});
// $array = [1, 3, 5, 6];
To sort descending, you can simply invert the comparison result:
usort($array, function ($a, $b) {
return -($a <=> $b);
});
// $array = [6, 5, 3, 1];
Hi there, thanks for reading! I hope this blog post helped you! If you'd like to contact me, you can do so on Twitter or via e-mail. I always love to chat!
]]>If you're eligible to vote, or know someone who can: I want to ask you to take five minutes to read this, and to be clear up front: I want you to vote yes.
Here's why from the point of view of a userland developer, both for client projects and open source.
The main argument against named arguments — the PHP 8 puns continue — is that they would make maintaining open source software a pain: changing the name of an argument would become a breaking change.
Here's what that means. Imagine an open source package which has this function as part of its public API:
public function toMediaCollection(string $collection, string $disk = null);
Named parameters would allow to call this function like so:
$pendingMedia
->toMediaCollection(collection: 'downloads', disk: 's3');
If, for some reason, the open source maintainer would want to change the name of the $collection
or $disk
variables, they would have to tag a major release, because named arguments would make that a breaking change.
Now, let me tell you something from my point of view as an open source maintainer: this rarely happens.
As a matter of fact, I can only think of a handful occurrences. And the only reason we decided to do renames on those occurrences, was because we were already working on a new major version and we figured we might as well improve the naming a little bit while we were at it.
I'm not the only one with that opinion by the way, Nicolas Grekas is amongst the people voting yes, and he knows a thing or two about OSS development. Oh and here's Mohamed Said, one of the core maintainers of Laravel:
I've been working at Laravel for 4 years now and I rarely find us wanting to change argument names in a refactor. We either add or remove arguments, but I'd say we never had to change names.
Actually that's an interesting thought: argument lists already are prone to breaking backwards compatibility: changing the order of arguments already is a breaking change! And we're dealing with that just fine now, aren't we?
Now even if you, as an open source maintainer, don't want to take the responsibility of making sure argument names don't change between major releases, here's what you do: tell your users you won't actively support named arguments, and using them is at their own risk. Just put this in the README:
**Heads up**: this package doesn't actively support named arguments.
This means that argument names might change in minor and patch releases.
You can use them, but at your own risk.
Don't deny all PHP developers this flexibility, because you're afraid of a slight chance it might break something somewhere in the far far future. Don't be afraid.
Another argument is that this RFC would encourage bad API design. It would encourage people to write large method definitions, which in turn often indicates a code smell.
I as well can come up with lots of examples that aren't a good fit for named parameters. But that doesn't mean there are no use cases for them at all! Have you heard of data transfer objects or value objects before? If you're following this blog, chances are you have.
I'm not going to copy my writing on them in this post, but I can summarise the main thought behind them: treat data as a first class citizen of your application, model them with objects. For example: an address has a street, number, postal code, city, country, sometimes even more than that. That data should be represented by a strongly typed object in PHP, and not passed between contexts as an array full of random stuff, its constructor would look like this:
class Address
{
public function __construct(
string $street,
string $number,
string $postal,
string $city,
string $country,
) { /* … */ }
}
DTOs and VOs are valid cases where these kinds of large constructors are allowed, it's no code smell at all. I had a quick look at an old project of ours, at the time of tracking its stats, it already had 63 DTO classes, and the project was far from finished at that point!
Large constructors happen, and named parameters would not only add more clarity, but also offer the flexibility of changing the parameter order after the fact, without having to worry about fixing the order at all.
Take our Address
object, for example. Let's say we need to support number suffixes. We can add that argument without having to worry about the order that other places called it:
class Address
{
public function __construct(
string $street,
string $number,
string $numberSuffix,
string $postal,
string $city,
string $country,
) { /* … */ }
}
Sure the calling site still need to add it, but at least you don't have to worry about micro managing the parameter order anymore.
But what if users decide to use ordered arguments instead? You'd need some way to ensure named arguments are used in these cases. The answer is surprisingly dull: establish conventions with your team, and optionally enforce them with tools like phpcs.
Yes, ideally, we'd want the language to prevent us from any possible misstep; but that simply isn't a realistic expectation. To me, that still isn't an argument for voting against this RFC. I've been working with teams of developers for years now, and project conventions need to be established anyway. They work just fine.
Pop quiz! How to set a cookie without a value, which expires two hours from now?
Did you look up the docs or consult your IDE?
That's fine, it's a confusing function after all. Named arguments can offer a little more clarity though. Compare the two following notations:
setcookie(
'test',
'',
time() + 60 * 60 * 2
);
Or:
setcookie(
name: 'test',
expires: time() + 60 * 60 * 2,
);
I know which I would pick, everyone benefits from named arguments in this case. And since chances are slim that PHP's internal functions will change anytime soon, it's another very good reason to add them.
Lastly, let's face the simple facts: several other languages — many also focused on web development — already support named arguments. Some deal with them in slightly different ways, but the base concept is known to many other programmers, and it's a good thing.
Here are a few examples:
So let's not spread fear about harder-to-maintain code, or open source software that will become a nightmare to maintain. Named arguments are a known feature in the larger software community, and have proven their worth. No need for hypothetical problems, we will manage.
By the way the vote will pass at this point! You can read about named arguments in depth on this blog!
]]>match
expression. A powerful feature that will often be the better choice to using switch
. So what exactly are the differences?
Let's start by comparing the two. Here's a classic switch
example:
switch ($statusCode) {
case 200:
case 300:
$message = null;
break;
case 400:
$message = 'not found';
break;
case 500:
$message = 'server error';
break;
default:
$message = 'unknown status code';
break;
}
Here's its match
equivalent:
$message = match ($statusCode) {
200, 300 => null,
400 => 'not found',
500 => 'server error',
default => 'unknown status code',
};
First of all, the match
expression is significantly shorter:
break
statementThat's already quite a lot, but there's even more to it!
match
will do strict type checks instead of loose ones. It's like using ===
instead of ==
.
People will probably disagree whether that's a good thing or not, but that's a topic on its own.
$statusCode = '200';
$message = match ($statusCode) {
200 => null,
default => 'unknown status code',
};
// $message = 'unknown status code'
If you forget to check for a value, and when there's no default
arm specified, PHP will throw an UnhandledMatchError
exception. Again more strictness, but it will prevent subtle bugs from going unnoticed.
$statusCode = 400;
$message = match ($statusCode) {
200 => 'perfect',
};
// UnhandledMatchError
Just like short closures, you can only write one expression. Expression blocks will probably get added at one point, but it's still not clear when exactly.
You already noticed the lack of break
? This also means match
doesn't allow for fallthrough conditions, like the two combined case
lines in the first switch
example. On the other hand though, you can combine conditions on the same line, separated by commas.
So you have the same functionality as switch in this regards, but with less writing, and less ways to screw up. Win-win!
$message = match ($statusCode) {
200, 300, 301, 302 => 'combined expressions',
};
During the RFC discussion, some people suggested the following pattern as an argument against adding the match
expression:
$message = [
$this->matchesRegex($line) => 'match A',
$this->matchesOtherRegex($line) => 'match B',
][$line] ?? 'no match';
There's one big caveat though: this technique will execute all regex functions first, decreasing performance. A good argument for match
.
Finally, because of throw expressions in PHP 8, it's also possible to directly throw from an arm, if you'd like to.
$message = match ($statusCode) {
200 => null,
500 => throw new ServerError(),
default => 'unknown status code',
};
Ok, there's one more thing: pattern matching. It's a technique used in other programming languages, to allow complexer matching than simple values. Think of it as regex, but for variables instead of text.
Pattern matching isn't supported right now, because it's quite a complex feature, but Ilija Tovilo, the RFC author did mention it as a possible future feature. Something to look out for!
If I'd need to summarise the match
expression in one sentence, I'd say it's the stricter and more modern version of it's little switch
brother.
There are some cases — see what I did there? — where switch
will offer more flexibility, especially with multiline code blocks. However, the strictness of the match
operator is appealing, and the perspective of pattern matching would be a game-changer for PHP.
I admit I never wrote a switch
statement in the past years because of its many quirks; quirks that match
actually solve. So while it's not perfect yet, there are use cases that I can think of, where match
would be a good… match.
What's your opinion?
]]>Let's set the scene first. These benchmarks were run on my local machine. As so, they don't say anything about absolute performance gains, I'm only interested in making conclusions about the relative impact the JIT has on real-life code.
I'll be running PHP FPM, configured to spawn 20 child processes, and I'll always make sure to only run 20 concurrent requests at once, just to eliminate any extra performance hits on the FPM level. Sending these requests is done using the following command, with ApacheBench:
ab -n 100 -c 20 -l http://aggregate.stitcher.io.test:8081/discover
With the project in place, let's configure the JIT itself. The JIT is enabled by specifying the opcache.jit_buffer_size
option in php.ini
. If this directive is excluded, the default value is set to 0, and the JIT won't run.
opcache.jit_buffer_size=100M
You'll also want to set a JIT mode, which will determine how the JIT will monitor and react to hot parts of your code. You'll need to use the opcache.jit
option. Its default is set to tracing
, but you can override it using function
:
opcache.jit=function
; opcache.jit=tracing
In our real-life benchmarks, I'll compare both modes with each other. So let's start benchmarking!
First it's best to establish whether the JIT is working properly or not. We know from the RFC that it does have a significant impact on calculating a fractal. So let's start with that example. I copied the mandelbrot example from the RFC, and accessed it via the same HTTP application I'll run the next benchmarks on:
public function index()
{
for ($y = -39; $y < 39; $y++) {
printf("\n");
for ($x = -39; $x < 39; $x++) {
$i = $this->mandelbrot(
$x / 40.0,
$y / 40.0
);
if ($i == 0) {
printf("*");
} else {
printf(" ");
}
}
}
printf("\n");
}
private function mandelbrot($x, $y)
{
$cr = $y - 0.5;
$ci = $x;
$zi = 0.0;
$zr = 0.0;
$i = 0;
while (1) {
$i++;
$temp = $zr * $zi;
$zr2 = $zr * $zr;
$zi2 = $zi * $zi;
$zr = $zr2 - $zi2 + $cr;
$zi = $temp + $temp + $ci;
if ($zi2 + $zr2 > 16) {
return $i;
}
if ($i > 5000) {
return 0;
}
}
}
After running ab
for a few hundred requests, we can see the results:
requests/second (more is better) | |
Mandelbrot without JIT | 3.60 |
Mandelbrot with tracing JIT | 41.36 |
Great, it looks like the JIT is working! That's even a ten times performance increase! Having verified it works as expected, let's move on to our first real-life comparison. We're going to compare no JIT with the function and tracing JIT; using 100MB of memory. The page we're going to benchmark shows an overview of posts, so there's some recursion happening. We're also touching several core parts of Laravel as well: routing, the dependency container, as well as the ORM layer.
If you want to verify whether the JIT is running, you can use opcache_get_status()
, it has a jit
entry which lists all relevant information:
dd(opcache_get_status()['jit']);
// array:7 [▼
// "enabled" => true
// "on" => true
// "kind" => 5
// "opt_level" => 4
// "opt_flags" => 6
// "buffer_size" => 104857584
// "buffer_free" => 104478688
// ]
requests/second (more is better) | |
No JIT | 63.56 |
Function JIT | 66.32 |
tracing JIT | 69.45 |
Here we see the results: enabling the JIT only has a slight improvement. In fact, running the benchmarks over and over, the results differ slightly every time: I've even seen cases where a JIT enabled run performs worse than the non JIT'ed version. Before drawing final conclusions, let's bump the memory buffer limit. We'll give the JIT a little more room to breathe with 500MB of memory instead of 100MB.
requests/second (more is better) | |
No JIT | 71.69 |
Function JIT | 72.82 |
Tracing JIT | 70.11 |
As you can see: a case of the JIT performing worse. Like I said at the beginning of this post: I want to measure the relative impact the JIT has on real-life web projects. It's clear from these tests that sometimes there might be benefits, but it's in no way as noticeable as the fractal example we started out with. I admit I'm not really surprised by that. Like I wrote before: there's very little hot code to be optimised in real-life applications, we're only rarely doing fractal-like computations.
So am I saying there's no need for the JIT? Not quite, I think the JIT can open up new areas for PHP: areas where complex computations do benefit from JIT'ed code. I'm thinking about machine learning, AI, stuff like that. The JIT might give opportunities to the PHP community that didn't exist yet, but it's unclear to say anything with certainty at this point.
So, that concludes my JIT testing. As expected: the JIT probably won't have a significant impact on web applications, at least not right now.
I won't discuss my thoughts on whether the JIT itself is a good addition or not in this post, let's have those discussions together over here!
]]>Besides managing the school, he was also a translator for the King of France, Louis XVI. From time to time, he received fancy invitations from the royal family, which had embossed letters on them. It didn't take long before Haüy wondered whether these raised letters could be a way of teaching his pupils to read, via touch.
In 1786, he printed his first book with raised letters: it was readable by touch and by sight, so both blind and seeing people could read it.
It was a revolutionary idea, but with its downsides: the fancy and curly fonts of that time made it very hard for blind people to distinguish between letters; furthermore, it was also rather expensive to print such books.
Fast forward to 1821, when a French army captain came to the Institute for Blind Youth; now called National Institute for the Young Blind, because of the French revolution. This captain gave a lecture about a technique used in the army for nocturnal writing, in other words: blind writing. They used a 12-dot system to pass messages during the night.
In that lecture was a 12-year old boy, named Louis Braille.
You can probably guess what came next: inspired by the army's nocturnal writing style, Louis came up with a 6-dot system to represent the letters of the alphabet.
It turned out these dots were significantly more easy to distinguish by touch than Haüy's embossed letters. It was a system built upon previous iterations of the same idea, but most importantly it was invented by someone who was blind himself, who understood the problem that needed to be solved first hand.
It shouldn't be a surprise: this technique grew rapidly in popularity among the blind.
While Braille was still developing his system, an American named Samuel Howe came to visit France. He was going to start the first school for blind children in the U.S. and wanted to do practical research. He took the original idea of Haüy's embossed letters, and made a new font for it; a font that would be less "artsy" and more "practical" to read. It was called Boston Line Type.
By the time the system was introduced in the U.S., blind U.S. citizens also heard of braille. Upper management resisted though: it was a European invention, for sure Boston Line Type would be the superior system. It was still readable by both blind and seeing people, and was more clear than Haüy's method.
Despite this reasoning, perhaps even a bit of American pride, it was clear that Boston Line Type would never be able to surpass braille.
Still, schools in the U.S. couldn't live with using the European standard. So what did they do instead? Two or three variations of braille were created over a course of 50 years; all based on the same principle, but all different in their implementation.
This went on until 1932, until braille finally became the accepted standard, probably what most blind people wanted all along. A period that took more than a century; it was later poetically called "The war of the Dots".
Let's talk about software development, shall we? There are one or two parallels with the history of braille that I think we can learn valuable lessons from.
First of all: we shouldn't always aim to please everyone. Both Haüy's method and Boston Line Type aimed to write text that was accessible both for blind and seeing people. The end result was something suboptimal for both groups.
Sometimes it's better, also in software development, to focus on that one specific problem, and find the best solution for it, instead of trying to solve everything. It's better to focus on a specific problem, instead of a generalised one.
Second: it took braille more than 100 years to become the standard, even though it was clear from the start that it was a great system. Software development is still in its infancy. I don't think there will still be 200 different frameworks solving the same problem in 50 years.
Maybe there will even be only a handful of languages, one or two per field: web development, mobile apps, desktop apps, machine learning, etc. Each field will evolve towards the best solution, and we might already know that solution today, but aren't ready to accept it yet.
Braille is just one example of how much time it takes to optimise a process or system. Let's not fool ourselves thinking we already know the best solutions for our problems in software development. Let's keep that in mind when were advocating for the next big thing. Let's stay humble and realise we're only playing a small part in history.
]]>Before starting by the way, if you want deep dive in Nova, I suggest you subscribe to updates in my upcoming course Mastering Nova that will be released this mid-summer!
An abstract resource class will inherit the base Resource
class. This allows you to
override specific methods to add functionality on your real resource classes.
In the end, any method that you improve in your custom base class, will be available on your model resources. I'll show you how to create the abstract resource, and then we'll at concrete improvements.
We start by creating a file AbstractResource.php inside app/Nova
, like this:
app/
Nova/
AbstractResource.php
At first, the AbstractResource
looks like this:
namespace App\Nova;
abstract class AbstractResource extends Resource
{
}
Next, in your Resource classes just inherit from this abstract Resource instead of the Nova Resource one:
namespace App\Nova;
use App\Nova\AbstractResource;
class Review extends AbstractResource
{
//
}
So, let's look at some examples of improvements you can add to your new abstract resource.
On your abstract Resource write this code:
public static function indexQuery(NovaRequest $request, $query)
{
$uriKey = static::uriKey();
if (($request->orderByDirection ?? null) !== null) {
return $query;
}
if (! empty(static::$indexDefaultOrder)) {
$query->getQuery()->orders = [];
return $query->orderBy(
key(static::$indexDefaultOrder),
reset(static::$indexDefaultOrder)
);
}
}
Then on your model resource:
public static $indexDefaultOrder = ['email' => 'asc'];
This will sort your index query by "email, asc" in case there is not a pre-selected sorting order.
If you have a relationship field, you might have seen that you cannot use it
to search on your Resource search field. In that case, you can use the
titasgailius/search-relations
package.
To install it, just import it via Composer:
composer require titasgailius/search-relations
Then in your Abstract Resource, you can add it like:
use Titasgailius\SearchRelations\SearchesRelations;
abstract class AbstractResource extends Resource
{
use SearchesRelations;
}
Henceforth, on your model resources, you can simply add:
public static $searchRelations = [
'user' => ['username', 'email'],
];
where the key is your relationship name, and then an array of searchable column values. Therefore you can now search on your relationship columns!
Let's say you would like to have a generic card that shows information about when
was the last time your current resource was updated, and some other extra information
regarding your resource; or an action that actually will change status on models
that share a status_type
column.
All of this functionality can be shared between model resources.
As an example, let's say you want to add a new Card to all of the model resources that share your abstract resource, you can do it like:
public function cards(Request $request){
return [
new ResourceInformation(
$this->getCurrentResourceInstance($this->getModelInstance())
),
];
}
protected function getModelInstance()
{
$resourceKey = explode('/', request()->path())[1];
$resourceClass = Nova::resourceForKey($resourceKey);
return new $resourceClass::$model;
}
and in your model Resource:
public function cards(Request $request)
{
return array_merge(
[/* your cards */],
parent::cards($request),
);
}
The BelongsTo
field already has an option to remove the checkbox 'With Trashed'
(basically not to show trashed items), but what if want to remove it from any
other relationship operation (e.g.: BelongsToMany
)?
You just need to apply the following code in your abstract resource:
use Illuminate\Support\Facades\Gate;
/**
* Based the trashed behavior on a new policy called trashedAny()
*
* @return boolean
*/
public static function softDeletes()
{
// Is this resource authorized on trashedAny?
if (static::authorizable()) {
if (! method_exists(
Gate::getPolicyFor(static::newModel()),
'trashedAny'
)) {
return true;
}
return Gate::check('trashedAny', static::class));
};
return parent::softDeletes();
}
in this example, all you have to do is to define a policy for your model, and then
create a new method called trashedAny(User $user)
, as example:
public function trashedAny(User $user)
{
return false;
}
These were examples that can trigger your thoughts about how to leverage Abstract Resources on your Nova projects.
And if I was able to convince you :) I suggest you subscribe to updates in my upcoming course Mastering Nova that will be released this mid-summer!
Best, Bruno
]]>Naturally, I'm very happy with the constructor property promotion RFC, it's passed and will be added in PHP 8. You see, this feature reduces a lot of boilerplate code when constructing simple objects such as VOs and DTOs.
In short: property promotion allows you to combine class fields, constructor definition and variable assignments all into one syntax, in the construct parameter list.
So instead of doing this:
class CustomerDTO
{
public string $name;
public string $email;
public DateTimeImmutable $birth_date;
public function __construct(
string $name,
string $email,
DateTimeImmutable $birth_date
) {
$this->name = $name;
$this->email = $email;
$this->birth_date = $birth_date;
}
}
You would write this:
class CustomerDTO
{
public function __construct(
public string $name,
public string $email,
public DateTimeImmutable $birth_date,
) {}
}
Let's look at how it works!
The basic idea is simple: ditch all the class properties and the variable assignments, and prefix the constructor parameters with public
, protected
or private
. PHP will take that new syntax, and transform it to normal syntax under the hood, before actually executing the code.
So it goes from this:
class MyDTO
{
public function __construct(
public string $name = 'Brent',
) {}
}
To this:
class MyDTO
{
public string $name;
public function __construct(
string $name = 'Brent'
) {
$this->name = $name;
}
}
And only executes it afterwards.
Note by the way that the default value is not set on the class property, but on the method argument in the constructor.
So let's look at what promoted properties can and can't do, there's quite a lot of little intricacies worth mentioning!
Promoted properties can only be used in constructors. That might seem obvious but I thought it was worth mentioning this, just to be clear.
You're not able to declare a class property and a promoted property with the same name. That's also rather logical, since the promoted property is simply transpiled to a class property at runtime.
class MyClass
{
public string $a;
public function __construct(
public string $a,
) {}
}
You're allowed to promote untyped properties, though I'd argue that these days with modern PHP, you're better off typing everything.
class MyDTO
{
public function __construct(
public $untyped,
) {}
}
Promoted properties can have default values, but expressions like new …
are not allowed.
public function __construct(
public string $name = 'Brent',
public DateTimeImmutable $date = new DateTimeImmutable(),
) {}
Not all constructor properties should be promoted, you can mix and match.
class MyClass
{
public string $b;
public function __construct(
public string $a,
string $b,
) {
$this->b = $b;
}
}
I'd say: be careful mixing the syntaxes, if it makes the code less clear, consider using a normal constructor instead.
You're allowed to read the promoted properties in the constructor body. This can be useful if you want to do extra validation checks. You can use both the local variable and the instance variable, both work fine.
public function __construct(
public int $a,
public int $b,
) {
assert($this->a >= 100);
if ($b >= 0) {
throw new InvalidArgumentException('…');
}
}
You can add doc comments on promoted properties, and they are still available via reflection.
class MyClass
{
public function __construct(
/** @var string */
public $a,
) {}
}
$property = new ReflectionProperty(MyClass::class, 'a');
$property->getDocComment(); // "/** @var string */"
Just like doc blocks, attributes are allowed on promoted properties. When transpiled, they will be present both on the constructor parameter, as well as the class property.
class MyClass
{
public function __construct(
#[MyAttribute]
public $a,
) {}
}
Will be transpiled to:
class MyClass
{
#[MyAttribute]
public $a;
public function __construct(
#[MyAttribute]
$a,
) {
$this->a = $a;
}
}
I didn't even know abstract constructors were a thing, but here goes! Promoted properties are not allowed in them.
abstract class A
{
abstract public function __construct(
public string $a,
) {}
}
On the other hand, they are allowed in traits. This makes sense, since the transpiled syntax is also valid in traits.
trait MyTrait
{
public function __construct(
public string $a,
) {}
}
var
is not supportedOld, I mean, experienced PHP developers might have used var
in a distant past to declare class variables. It's not allowed with constructor promotion. Only public
, protected
and private
are valid keywords.
public function __construct(
var string $a,
) {}
Since you can't convert to a type that's array of type
, it's not possible to promote variadic parameters.
public function __construct(
public string ...$a,
) {}
Still waiting for generics…
isPromoted
Both ReflectionProperty
and ReflectionParameter
have a new isPromoted
method to check whether the class property or method parameter is promoted.
Since PHP constructors don't need to follow the declaration of their parent constructor, there's little to be said: inheritance is allowed. If you need to pass properties from the child constructor to the parent constructor though, you'll need to manually pass them:
class A
{
public function __construct(
public $a,
) {}
}
class B extends A
{
public function __construct(
$a,
public $b,
) {
parent::__construct($a);
}
}
That's about it for property promotion! I for sure will use them, what about you? Let me know via Twitter or e-mail!
It made me wonder if and how the team we work in, and the kind of projects we work on, might influence our view of type system usage.
I decided to do a little survey, and gather some actual insights in the topic.
Fair warning: I'm no neutral player in this discussion, but I want to make clear that it wasn't my intention to make my own case. I wanted to see whether our seemingly pointless discussions might be caused by the difference in context; I don't want to prove there's one and only true answer.
With all that being said, let's look at the results.
First of all I'd like to thank all 686 people who participated in this survey. I realise this is a small group, though I hope it's representative enough to draw some conclusions. If you think that the results aren't accurate enough, please reach out to me to discuss whether and how we can redo this survey on a larger scale.
Based on the answers in the survey, I made five groups of profiles: A
, B
, C
, D
and E
. A
and B
lean (strongly) towards a stricter type system, C
is somewhat neutral, and D
and E
lean (strongly) towards not using type systems.
This "type profile" was determined by mapping the answers to relevant questions to a score: 1 and 2 points were given to answers favorable to strict type systems, 0 to neutral answers and -1 and -2 to answers leaning towards no type systems.
Type profile of all participants
One thing that immediately stood out is the large amount of people who lean towards the use of a strict type system. I did not expect this. From discussions I had on Twitter, I had the feeling that more people would be in group C
, D
or E
.
These are some of the most popular arguments against the use of PHP's type system, at least the ones I heard in my discussions:
Of course this survey wanted to examine whether there's a correlation between personal preference and team- and project size. Let's look at team size first.
This chart shows the average team size, and for each group the distribution of type profiles within that group.
Type profile distribution, grouped by team size
We'd need to look at relative results to test whether there's a correlation or not. So here goes, but keep in mind that the group with 2-10 people
, is by far the largest.
Relative type profile distribution, grouped by team size
As I expected, based on discussions: profiles D
and E
are more present in smaller teams. Yet I admit I expected that group to be larger again.
Next I looked at project size. I asked participants to describe the size of an average project they work on: small, medium, large or extra large.
Relative type profile distribution, grouped by project size
This chart shows a growth of type A
and B
, related to the size of the project. Most times, "project size" also translates to "project duration", which is why I also asked participants to rate the project duration of such an average project.
Relative type profile distribution, grouped by project duration
Again we see a preference for stricter type systems in longer projects, but we should of course be aware that there were less participants in these groups. Furthermore, I found it interesting that in this case, there' no linear pattern to discover, as with the previous charts.
Unfortunately, I think there weren't enough participants distributed across all kinds of projects and team sizes to draw final conclusions here.
Up front, I assumed that the group who preferred not to use type systems would have been larger; but maybe it's simply a more vocal group, even though smaller? I can't say that for sure though.
I do think that, even with a small amount of participants, we can assume there is a correlation between type system usage and project- and team size; but ideally, we'd need a larger participant pool.
My personal takeaway is that when entering type system discussions, we should be wary to compare each others preference: there might be a good case that you're simply working in a completely different kind of project, and there's no way of telling who's right or wrong.
]]>The concept of attributes isn't new at all, we've been using docblocks to simulate their behaviour for years now. With the addition of attributes though, we now have a first-class citizen in the language to represent this kind of meta data, instead of having to manually parse docblocks.
So what do they look like? How do we make custom attributes? Are there any caveats? Those are the questions that will be answered in this post. Let's dive in!
First things first, here's what attribute would look like in the wild:
use \Support\Attributes\ListensTo;
class ProductSubscriber
{
#[ListensTo(ProductCreated::class)]
public function onProductCreated(ProductCreated $event) { /* … */ }
#[ListensTo(ProductDeleted::class)]
public function onProductDeleted(ProductDeleted $event) { /* … */ }
}
I'll be showing other examples later in this post, but I think the example of event subscribers is a good one to explain the use of attributes at first.
Also yes, I know, the syntax might not be what you wished or hoped for. You might have preferred @
, or @:
, or docblocks or, … It's here to stay though, so we better learn to deal with it. The only thing that's worth mentioning on the syntax is that all options were discussed, and there are very good reasons why this syntax was chosen. You can read the whole discussion about the RFC on the internals list.
That being said, let's focus on the cool stuff: how would this ListensTo
work under the hood?
First of all, custom attributes are simple classes, annotated themselves with the #[Attribute]
attribute; this base Attribute
used to be called PhpAttribute
in the original RFC, but was changed with another RFC afterwards.
Here's what it would look like:
#[Attribute]
class ListensTo
{
public string $event;
public function __construct(string $event)
{
$this->event = $event;
}
}
That's it — pretty simple right? Keep in mind the goal of attributes: they are meant to add meta data to classes and methods, nothing more. They shouldn't — and can't — be used for, for example, argument input validation. In other words: you wouldn't have access to the parameters passed to a method within its attributes. There was a previous RFC that allowed this behaviour, but this RFC specifically kept things more simple.
Back to the event subscriber example: we still need to read the meta data and register our subscribers based somewhere. Coming from a Laravel background, I'd use a service provider as the place to do this, but feel free to come up with other solutions.
Here's the boring boilerplate setup, just to provide a little context:
class EventServiceProvider extends ServiceProvider
{
// In real life scenarios,
// we'd automatically resolve and cache all subscribers
// instead of using a manual array.
private array $subscribers = [
ProductSubscriber::class,
];
public function register(): void
{
// The event dispatcher is resolved from the container
$eventDispatcher = $this->app->make(EventDispatcher::class);
foreach ($this->subscribers as $subscriber) {
// We'll resolve all listeners registered
// in the subscriber class,
// and add them to the dispatcher.
foreach (
$this->resolveListeners($subscriber)
as [$event, $listener]
) {
$eventDispatcher->listen($event, $listener);
}
}
}
}
Note that if the [$event, $listener]
syntax is unfamiliar to you, you can get up to speed with it in my post about array destructuring.
Now let's look at resolveListeners
, which is where the magic happens.
private function resolveListeners(string $subscriberClass): array
{
$reflectionClass = new ReflectionClass($subscriberClass);
$listeners = [];
foreach ($reflectionClass->getMethods() as $method) {
$attributes = $method->getAttributes(ListensTo::class);
foreach ($attributes as $attribute) {
$listener = $attribute->newInstance();
$listeners[] = [
// The event that's configured on the attribute
$listener->event,
// The listener for this event
[$subscriberClass, $method->getName()],
];
}
}
return $listeners;
}
You can see it's easier to read meta data this way, compared to parsing docblock strings. There are two intricacies worth looking into though.
First there's the $attribute->newInstance()
call. This is actually the place where our custom attribute class is instantiated. It will take the parameters listed in the attribute definition in our subscriber class, and pass them to the constructor.
This means that, technically, you don't even need to construct the custom attribute. You could call $attribute->getArguments()
directly. Furthermore, instantiating the class means you've got the flexibility of the constructor the parse input whatever way you like. All in all I'd say it would be good to always instantiate the attribute using newInstance()
.
The second thing worth mentioning is the use of ReflectionMethod::getAttributes()
, the function that returns all attributes for a method. You can pass two arguments to it, to filter its output.
In order to understand this filtering though, there's one more thing you need to know about attributes first. This might have been obvious to you, but I wanted to mention it real quick anyway: it's possible to add several attributes to the same method, class, property or constant.
You could, for example, do this:
#[
Route(Http::POST, '/products/create'),
Autowire,
]
class ProductsCreateController
{
public function __invoke() { /* … */ }
}
With that in mind, it's clear why Reflection*::getAttributes()
returns an array, so let's look at how its output can be filtered.
Say you're parsing controller routes, you're only interested in the Route
attribute. You can easily pass that class as a filter:
$attributes = $reflectionClass->getAttributes(Route::class);
The second parameter changes how that filtering is done. You can pass in ReflectionAttribute::IS_INSTANCEOF
, which will return all attributes implementing a given interface.
For example, say you're parsing container definitions, which relies on several attributes, you could do something like this:
$attributes = $reflectionClass->getAttributes(
ContainerAttribute::class,
ReflectionAttribute::IS_INSTANCEOF
);
It's a nice shorthand, built into the core.
Now that you have an idea of how attributes work in practice, it's time for some more theory, making sure you understand them thoroughly. First of all, I mentioned this briefly before, attributes can be added in several places.
In classes, as well as anonymous classes;
#[ClassAttribute]
class MyClass { /* … */ }
$object = new #[ObjectAttribute] class () { /* … */ };
Properties and constants;
#[PropertyAttribute]
public int $foo;
#[ConstAttribute]
public const BAR = 1;
Methods and functions;
#[MethodAttribute]
public function doSomething(): void { /* … */ }
#[FunctionAttribute]
function foo() { /* … */ }
As well as closures;
$closure = #[ClosureAttribute] fn() => /* … */;
And method and function parameters;
function foo(#[ArgumentAttribute] $bar) { /* … */ }
They can be declared before or after docblocks;
/** @return void */
#[MethodAttribute]
public function doSomething(): void { /* … */ }
And can take no, one or several arguments, which are defined by the attribute's constructor:
#[Listens(ProductCreatedEvent::class)]
#[Autowire]
#[Route(Http::POST, '/products/create')]
As for allowed parameters you can pass to an attribute, you've already seen that class constants, ::class
names and scalar types are allowed. There's a little more to be said about this though: attributes only accept constant expressions as input arguments.
This means that scalar expressions are allowed — even bit shifts — as well as ::class
, constants, arrays and array unpacking, boolean expressions and the null coalescing operator. A list of everything that's allowed as a constant expression can be found in the source code.
#[AttributeWithScalarExpression(1 + 1)]
#[AttributeWithClassNameAndConstants(PDO::class, PHP_VERSION_ID)]
#[AttributeWithClassConstant(Http::POST)]
#[AttributeWithBitShift(4 >> 1, 4 << 1)]
By default, attributes can be added in several places, as listed above. It's possible, however, to configure them so they can only be used in specific places. For example you could make it so that ClassAttribute
can only be used on classes, and nowhere else. Opting-in this behaviour is done by passing a flag to the Attribute
attribute on the attribute class.
It looks like this:
#[Attribute(Attribute::TARGET_CLASS)]
class ClassAttribute
{
}
The following flags are available:
Attribute::TARGET_CLASS
Attribute::TARGET_FUNCTION
Attribute::TARGET_METHOD
Attribute::TARGET_PROPERTY
Attribute::TARGET_CLASS_CONSTANT
Attribute::TARGET_PARAMETER
Attribute::TARGET_ALL
These are bitmask flags, so you can combine them using a binary OR operation.
#[Attribute(Attribute::TARGET_METHOD|Attribute::TARGET_FUNCTION)]
class ClassAttribute
{
}
Another configuration flag is about repeatability. By default the same attribute can't be applied twice, unless it's specifically marked as repeatable. This is done the same way as target configuration, with a bit flag.
#[Attribute(Attribute::IS_REPEATABLE)]
class ClassAttribute
{
}
Note that all these flags are only validated when calling $attribute->newInstance()
, not earlier.
Once the base RFC had been accepted, new opportunities arose to add built-in attributes to the core. One such example is the #[Deprecated]
attribute, and a popular example has been a #[Jit]
attribute — if you're not sure what that last one is about, you can read my post about what the JIT is.
I'm sure we'll see more and more built-in attributes in the future.
As a final note, for those worrying about generics: the syntax won't conflict with them, if they ever were to be added in PHP, so we're safe!
I've got some use-cases already in mind for attributes, what about you? If you've got some thoughts to share about this awesome new feature in PHP 8, you can reach me on Twitter or via e-mail, or we can discuss it over on Reddit.
]]>array_merge
, and not array_merge_recursive
. Confused? So was I. Let's explain what's happening.
Let's first explain what array_merge_recursive
does, take for example these two arrays:
$first = [
'key' => 'original'
];
$second = [
'key' => 'override'
];
Using array_merge_recursive
will result in the following:
array_merge_recursive($first, $second);
// [
// 'key' => [
// 'original',
// 'override',
// ],
// ]
Instead of overriding the original key
value, array_merge_recursive
created an array, with the original and new value both in it.
While that looks strange in this simple example, it's actually more useful in cases where one of the values already is an array, and you want to merge another item in that array, instead of overriding it.
$first = [
'key' => ['original']
];
$second = [
'key' => 'override'
];
In this case, array_merge_recursive
will yield the same result as the first example: it takes the value from the $second
array, and appends it to the value in the $first
array, which already was an array itself.
array_merge_recursive($first, $second);
// [
// 'key' => [
// 'original',
// 'override',
// ],
// ]
So if you want to merge multidimensional arrays, you can simply use array_merge
, it can handle multiple levels of arrays just fine:
$first = [
'level 1' => [
'level 2' => 'original'
]
];
$second = [
'level 1' => [
'level 2' => 'override'
]
];
array_merge($first, $second);
// [
// 'level 1' => [
// 'level 2' => 'override'
// ]
// ]
All of that being said, you could also use the +
operator to merge multidimensional arrays, but it will work slightly different compared to array_merge
.
array_merge
.
Let's imagine these two arrays:
$first = [
'a',
'b',
];
$second = [
'c',
];
Merging them using +
would result in the following:
$first + $second;
// ['a', 'b']
While using array_merge
, would result in this:
array_merge($first, $second);
// ['a', 'b', 'c']
What's happening here is that array_merge
will override existing keys, while +
will not. In other words: when a key exists in the first array, +
will not merge an item with the same key from another array into the first one.
In our example, both arrays actually had numerical keys, like so:
$first = [
0 => 'a',
1 => 'b',
];
$second = [
0 => 'c',
];
Which explains why $first + $second
doesn't add 'c' as an element: there already is an item with index 0
in the original.
The same applies for textual keys:
$first = [
'a' => 'a',
'b' => 'b',
];
$second = [
'a' => 'a - override',
];
$first + $second;
// ['a' => 'a', 'b' => 'b']
And finally, +
also works with nested arrays:
$first = [
'level 1' => [
'level 2' => 'original'
],
];
$second = [
'level 1' => [
'level 2' => 'override'
],
];
Using +
will keep the original
value, while array_merge
would override
it.
One more thing to mention is that +
will apply the same behaviour when merging multidimensional arrays.
Once this survey has gathered enough results, I will publish my conclusions on this blog. If you want stay updated on that, you can subscribe to my newsletter, follow me on Twitter or subscribe to my RSS feed.
A small update, I've gathered enough results, you can read about them here.
]]>Laravel Nova was launched on 22nd August 2018, as the official administration panel for Laravel web applications.
That was Taylor Otwell making the presentation at the Laracon US 2018, and since then Nova has evolved a lot, offering a better a UI experience, faster performance, and at the end giving backend creators a true experience on how to have a seamless integration between a Laravel application and the respective Resources (or Eloquent empowered models) integrated into a nice CRUD interface.
When Nova was created, a lot of expectations were already present in the Laravel community in the way that Laravel Nova should be a real administration back-office system, since this was a niche that was already covered by very nice alternative CRUD platforms like QuickAdminPanel or Laravel BackPack but they weren't an official Laravel product. So, when Nova was launched right after Laracon it went like a sales hit, everybody was talking about it, and everybody that bought a license experienced challenges to using it too. But those are things from the past :)
Nova evolved since then and evolved a lot! It's no longer a simple Resource Management tool and I want to share with you 6 must-have features that might help you when you are developing your Nova projects. Also, if you want deep dive in Nova, I suggest you subscribe to updates in my upcoming course Mastering Nova that will be released this mid-summer!
The first versions of Nova you weren't able to customize your Theme and to tweak it in a way without having your CSS code being overridden each time a new version of Nova was being published.
Now you can create your CSS theme, like this:
php artisan nova:theme brunocfalcao/masteringnova-theme
After that, you have a CSS class in your new package at resources/css/theme.css
where
you can then apply all the new Tailwind classes that you want
to use in your Nova instance.
If you want to even fully customize the entire Nova classes you can enable it using a custom package, then use the Nova::enableThemingClasses() to fully brand it to your needs.
This feature will prefix the Vue components with the string nova-
.
For example, Nova will
add the class name nova-heading to the top-level of the Heading component so you can
then style it from there.
// NovaServiceProvider.php
public function boot()
{
Nova::enableThemingClasses()
}
// app.js
/**
* If configured, register a global mixin to add theming-friendly CSS
* classnames to Nova's built-in Vue components. This allows the user
* to fully customize Nova's theme to their project's branding.
*/
if (window.config.themingClasses) {
Vue.mixin(ThemingClasses)
}
in the version 1.x of Nova you would control your Fields visibility using 8 methods:
hideFromIndex()
hideFromDetail()
hideWhenCreating()
hideWhenUpdating()
onlyOnIndex()
onlyOnDetail()
onlyOnForms()
exceptOnForms()
Now, you have the show*()
methods that allow you to show your Resource in the
respective display context without the dependency of other display contexts. For instance
you can have a showOnIndex()
and a showOnCreating()
, using a callback on the method
that should return true
.
showOnIndex()
showOnDetail()
showOnCreating()
showOnUpdating()
Since version 1.x that we see being added new field types to Nova. Let me highlight you some of the ones I consider the best additions:
See it a Chart "on-the-fly" directly in your Resource index or detail contexts.
Sparkline::make('Total devices Per Week')
->data($data)
->asBarChart()
->width(300),
Key-value fields are ways for you to interact with JSON data type columns, providing a way to manage Key-Value entries in a CRUD way.
KeyValue::make('Server Data', 'server_data')
->keyLabel('Parameter')
->valueLabel('Value')
->actionText('Add Server Parameter')
->rules('json')
->nullable(),
// In your model:
protected $casts = [
'server_data' => 'json'
];
At first, it might not be useful, but believe me, it's great to have it since you can apply data computations to be sent to your UI components.
Hidden::make('User', 'user_id')->default(
fn($request) => $request->user()->id
);
These are the newest kids on the block since they allow you to upload files or images into your Laravel Vapor instance. They will generate a temporary upload URL for Amazon S3 and will immediately upload the file.
VaporFile::make('Filename'),
VaporImage::make('Avatar')->maxWidth(80)->rounded(false),
On the latest Nova version 3.6.0 you can now have a Searchable Select field.
Select::make('Tags', 'tag_id')
->searchable()
->options(\App\Tag::all()->pluck('name', 'id'))
->displayUsingLabels(),
Since version 3.3.0 it's possible to publish the Nova stubs so you can change them to your own needs.
php artisan nova:stubs [--force]
The stubs are published directly in your app folder in a directory called "stubs".
This one I think it's undocumented but you can sort your Resources given a specific attribute in your Resource.
// In NovaServiceProvider.php
Nova::sortResourcesBy(function ($resource) {
return $resource::$priority ?? 9999;
});
// In your Resource
public static $priority = 10; // Or any other number.
The Sidebar Resources will then be sorted by this priority. Neat!
In specific cases, you might want to have your Global Search targeted Resource to go to Edit and not to Detail, or vice-versa. All you have to do is to add this static property on your Resource:
public static $globalSearchLink = 'detail';
Hope you enjoyed, and in case you want to continue learning Laravel Nova you can pre-subscribe my Mastering Nova Course anytime!
Once again thanks to Bruno for writing this post!
]]>array_map(
fn(User $user) => $user->id,
$users
);
Arrow functions, a.k.a. short closures. You can read about them in depth in this post.
class A
{
public string $name;
public ?Foo $foo;
}
Type properties. There's quite a lot to tell about them.
$data['date'] ??= new DateTime();
The null coalescing assignment operator. If you're unfamiliar with the null coalescing operator, you can read all about shorthand operators in this blog.
class ParentType {}
class ChildType extends ParentType {}
class A
{
public function covariantReturnTypes(): ParentType
{ /* … */ }
}
class B extends A
{
public function covariantReturnTypes(): ChildType
{ /* … */ }
}
Improved type variance. If you're not sure what that's about, you should take a look at this post about Liskov and type safety.
$result = [...$arrayA, ...$arrayB];
The array spread operator. There are a few sidenotes to be made about them.
$formattedNumber = 107_925_284.88;
The numeric literal separator, which is only a visual aid.
[preloading]
opcache.preload=/path/to/project/preload.php
Preloading improves PHP performance across requests. It's a complicated topic, but I wrote about it here.
]]>A few days ago while I was browsing the forums, I stumbled across a discussion that was the spark for this article. In short the OP was having problems with an expensive query and trying his best to optimize it. The thread had a lot of responses suggesting ways of optimizing the query like, index this column, use sub selects instead of joins, chunk the results etc…
What was weird for me was the fact that the OP was trying to load 13k options into a select element. I tried to suggest him that even if he manages to optimize the query to a few milliseconds the user experience would be less than poor. I spent time making my case presenting him with alternatives, articles with ux best practices and mobile device optimizations. The focus didn't change. The dedication to improve his existing solution was absolute. Needless to say that the thread is still open to this day…
The whole thing made me think how many times I encountered this behavior in my working environment including myself. I tried to find a real life example that simulates this behavior while drinking my early morning coffee. I stretched and carefully rested my cup to my poorly constructed coffee table. I purchased this table 2 years ago from IKEA and spend hours trying to build it only to realise at the end that I was left with one extra screw. I messed up and skipped a part of the instructions but I was done. I wasn't gonna build the whole thing again for one screw.
To this day my coffee table is supported by a string attached to the legs and all my friends visiting know that the table needs to be treated gently.
I immediately came up with the name "The IKEA effect" and I congratulated myself for the coolness of it. Sure enough, after googling it I realized that yep that was a thing: there is a very interesting paper from Michael I. Norton, a professor in Harvard, titled The “IKEA Effect”: When Labor Leads to Love and to sum it up here is the wikipedia definition:
The IKEA effect is a cognitive bias in which consumers place a disproportionately high value on products they partially created
In my career as a programmer I noticed this phenomenon's negative side effects quite often. It manifests itself in different ways and in different levels of an organization.
From the top level management dedicating time and effort to define the next big thing only to get grounded by the technical constrains or the level of effort required to build it. From the designer spending days to come up with a high fidelity design that at the end gets negative feedback and has to go back to the drawing board. Finally for the programmer who gets a code review after spending days working on a feature that basically requires rewriting the whole thing!
When working on a solution to a problem, the deeper you go, your focus switches to specifics parts of the problem. Many times those parts might require a disproportional amount of effort in order to solve them.
How many times I found myself searching and furiously going through stackoverflow to make this one thing work! When I finally got this little piece of the solution working, I was so attached to it that I wouldn't even consider the thought that it might not be the right way to do it.
I was ready to defend its honor against anyone who dared to challenge it! At this stage the feedback in any kind or form — either this comes from code review or over the water cooler — the result is tension. The worst part is that the feedback is probably right, but you are already too blind to see or accept it.
So how do we stop investing more and more into what we have already worked on, rather than striving for better or more efficient alternatives?
This are my takeaways.
Read the instructions: don't jump into the solution without understanding the problem. Invest time and ask for clarifications when needed.
Early feedback: write a summary of your proposed solution and communicate it to your colleagues, make sure that everybody that needs to be involved is aware.
Build a draft: code it, make it work, don't think about abstractions and future maintainability, just make it as simple as possible. Again ask for feedback and iterate.
Rebuild it: you've got some feedback that requires you to heavily refactor? Big deal you were going to rewrite the thing anyhow, no hard feelings.
Know when to stop: if something doesn't feel right, gets weirdly complicated to model it in your solution, then it smells bad. Stop and revisit your approach better now than later. Do you find yourself lost? Struggling to find a way to make it work? Maybe the problem is too hard for your level of expertise. There is no shame in this. Communicate it early with your senior experts ask for tips, pair coding, anything that can help you grow your game to be able to fight the beast.
Wrap it up: don't try to make every little part perfect. Pay attention to the core of the solution and learn to live with the rest. Ship it and after some time when you are emotionally detached revisit and refactor.
Once again thanks to Dimitris, be sure to check him out! If you're interested in discussing your thoughts in this topic, you can share them on HN.
]]>I, too, deal with these feelings on a regular basis. I read a tweet and think to myself: they are so wrong. I get angry because of the misinformation that's spread, the anti-patterns that are promoted, the lack of understanding.
I've had some insights in these kinds of situations though, and I want to share these with you today. I won't be able to tell you who's wrong or right — if there even is such a thing — and it will also take an effort on your part to succeed.
The payoff? A better understanding of why your colleague or that one guy on Reddit thinks such different thoughts than you, as well as a better grip on those circumstances. It allows you to rise above conflicts, become a problem solver instead of a troublemaker.
In this post I'll share a mental framework for programmers to manage these kinds of conflicts. To do that, we'll need to start with a simplified example.
Let's imagine two types of programmers: builders and architects.
If you ever had to work in teams — in school, as your job or in hobby projects — you're probably familiar with these two types of people.
The first ones, the builders, are the programmers who get things done. They work efficiently. Besides their daytime job, they come up with these amazing side projects and to the outside world it looks like writing code comes naturally to them.
On the other hand there are architects. They are concerned about sturdy and structurally sound code. They spend hours, sometimes days, debating the right way to solve a problem.
These two types of programmers seem like opposites. They themselves look at the other group and often think exactly that. They have a different way of handling the same problems. The other group's solutions are so different, they can't possibly be right, or so it seems.
Builders often find architects too rigid and conservative. They follow the rules for the sake of following them, without any practical benefit. Architects in turn, think of builders as careless, too focused on results, instead of thinking about long-term maintainability.
This tension often leads to confrontations between the groups, at the office or online. Interestingly enough, these kinds of discussions focus on trying to change the other person's point of view. I've been guilty of this myself more than I can count.
What's fascinating about these situations though, is that people from both sides almost never realise there's strength in their differences.
Instead of focusing on what they disagree on, let's try another approach: let's, for a minute, try to support the other party. Mind you: not just agree with them for the sake of ending a discussion, but actually trying to help them solve their problem, with your skill set.
Here's how that looks: while architects enjoy abstract thinking, they often have difficulties putting their thoughts into practice: they run into problems they didn't anticipate, or the actual implementation is too complex and they lose their drive. On the other hand, builders enjoy working towards a clear goal, they find energy in a project taking shape. So let's sit them down, together, and combine their strengths.
The illustration is easy enough — right? Though professional programmers still fall in the trap of endless discussions way too often. In fact, many human beings assume that the world around them thinks like they do. When the reality proves that's not the case, conflicts arise.
When you become aware of this personality trait, both in yourself and in others, you can use that knowledge to both your advantage. The reality is that most people won't have the same vision as you do, it's better to embrace that reality instead of fighting it.
One framework which differentiates between all kinds of personality types is called the Myers-Briggs Type Indicator — MBTI for short. It differentiates between 16 types of personalities. Obviously every person is unique, but those 16 types give an indication of how you and your colleagues think, how you'll react to situations, how you'll handle problems.
MBTI will map your personality into four different aspects:
Like I said, every person is unique, so you're not either introvert or extrovert, you're not only thinking or feeling. It's a scale that balances one way or the other. Based on those aspects though, you can get a better understanding of why other people have such a different approach to the same problem as you do.
For example, builders would lean more towards the intuitive and prospective side, while architects are observant and judging. Builders are imaginative and flexible, architects are organized and practical. One isn't better than the other, rather they should support each other.
Having this knowledge about yourself and your co-workers allows for better team dynamics. At my previous job we mapped our MBTI profiles with around 25 colleagues. We sat down one afternoon to talk about the results. Not only did the majority of people recognise themselves in their profile, it also exposed struggles and hidden frustrations that were never shared before, because no one knew how to deal with them. Now we did.
One thing I want to emphasize at the end of this post, is that my goal was not to promote MBTI. Rather it's to make you think about how people's personalities differ from each other, and how this can result both in conflicts as well as opportunities. MBTI is just an example of how one could visualise these differences.
Understanding there are different kinds of people who think differently than you do, seems like such a simple realisation. Yet the internet and suboptimal or even broken teams show that a principle so easy to understand, is difficult to bring into practice.
If you want to, you can truly change the way you handle inter-personal conflicts. Even though you probably are right, try to think about ways to support your adversary, yes even leveraging them to a higher level. You'll be surprised that you're the one who benefits the most.
If you do want to know your own MBTI profile, you can test it for free online. Again this profile won't tell you who you really are, but it has to power to address the differences in personalities between you and other programmers. There's a great website called 16personalities.com providing a 20-minutes test. I'm interested to see the results! Let me know your profile and check up on others in this poll.
Want to share your own thoughts on the topic? You can send me an email or we can discuss it on HN.
Thanks for reading, until next time!
]]>use Support\Attributes\ListensTo;
class ProductSubscriber
{
#[ListensTo(ProductCreated::class)]
public function onProductCreated(ProductCreated $event) { /* … */ }
#[ListensTo(ProductDeleted::class)]
public function onProductDeleted(ProductDeleted $event) { /* … */ }
}
Attributes — aka annotations — you can read about them in depth in this post.
public function foo(Foo|Bar $input): int|float;
public function bar(mixed $input): mixed;
Union types allows for type hinting several types. There's also a new mixed
type which represents several types at once.
interface Foo
{
public function bar(): static;
}
The static
return type is built-in.
[JIT]
opcache.jit=1225
The just-in-time compiler for PHP.
$triggerError = fn() => throw new MyError();
$foo = $bar['offset'] ?? throw new OffsetDoesNotExist('offset');
throw
can be used in expressions.
try {
// Something goes wrong
} catch (MySpecialException) {
Log::error("Something went wrong");
}
Non-capturing catches: no need to specify an exception variable if you don't need it.
setcookie(
name: 'test',
expires: time() + 60 * 60 * 2,
);
$result = match($input) {
0 => "hello",
'1', '2', '3' => "world",
};
The match
expression as an improvement to the switch
expression.
There's even more. If you want a full list, you can find it on this blog.
What feature are you looking forward to the most? Let me know on Twitter or via email.
]]>array_chunk
is certainly one of these functions for me personally. In this article, we will discover what array_chunk
is, what it does, and see it used in action.
array_chunk
and how it worksarray_chunk
is a pretty nifty name for slicing an array into chunks. This is essentially what the function does, it reads an array, and splits it into chunks.
Let me explain this in developer terms: we have an array, [1, 2, 3, 4, 5, 6, 7, 8]
, we use array_chunk
on the array, specifying we want chunks of 2 items. The output would look similar to below.
$input = [1, 2, 3, 4, 5, 6, 7, 8];
array_chunk($input, 2);
[
0 => [1, 2],
1 => [3, 4],
2 => [5, 6],
3 => [7, 8],
]
Pretty cool eh? With this function, you can quickly see how it can be utilized for statistical purposes, especially with segment averaging.
This function accepts 3 parameters as follows:
array
int
boolean
to instruct the functions to preserve the keys of the original array or to not. Note: the default is false
.Walk with me now through this scenario: my boss wants to know for each working week, the average profits from his shop. Each working week has 5 days.
So, let's say for argument's sake say we have just queried the last 20 days of shop sales from the shop's database. The data returned populate our array with 20 entries and therefore has 4 working weeks.
Now this leaves us with the problem at hand, we need to calculate the average sales across every 5 days for 4 weeks. Follow me through this next section to achieve the result.
array_chunk
We know our data array has 20 entries, and we know that we need an average of each week of sales (5 entries). Let's utilize array_chunk
with a little bit of extra native PHP to do the calculations.
$sales = [
250.70, 220.10, 233, 243.50, 255,
200, 300, 234, 350, 222,
237.99, 200.30, 150.98, 201, 209,
200, 300, 240, 203, 280,
];
// Split the array into groups of five,
// representing a 5 days working week.
$salesPerWeek = array_chunk($sales, 5);
// Map all items to their averages, week by week.
$averageSales = array_map(
fn(array $items) => array_sum($items) / count($items),
$salesPerWeek
);
Now, if we print the contents of $averageSales
we will get something like the following:
[
240.46,
261.2,
199.854,
244.6,
]
Let's break down the code for complete transparency:
array_chunk
to split it into groups of five.array_map
on each chunk, and use array_sum
to divide by the count of the chunk to give us the average.And that is it!
This type of functionality could be used in many statistical applications that require segmentation. The example in this article tries to show you how array_chunk
works in layman terms with a bit of a pretend use-case behind it. I hope this was interesting, if you would like to see any more of my content, please check out my blog, https://www.codewall.co.uk/
Let's set the scene.
This project is one of the larger ones we've worked on. In the end it will serve hundreds of thousands of users, handle large amounts of financial transactions and standalone tenant-specific installations will need to be created on the fly.
One key requirement is that the product ordering flow — the core of the business — can be easily reported on, as well as tracked throughout history.
Besides this front-facing client process, there's also a complex admin panel to manage products. Within this context, there's little to no need for reporting or tracking history of the admin activities; the main goal here is to have an easy-to-use product management system.
I hope you understand that I deliberately am keeping these terms a little vague because obviously this isn't an open-source project, though I think the concepts of "product management" and "orders" is clear enough for you to understand the design decisions we've made.
Let’s first discuss an approach of how to design this system based on my Laravel beyond CRUD series.
In such a system there would probably be two domain groups: Product
and Order
, and two applications making use of both these domains: an AdminApplication
and a CustomerApplication
.
A simplified version would look something like this:
Having used this architecture successfully in previous projects, we could simply rely on it and call it a day. There are a few downsides with it though, specifically for this new project: we have to keep in mind that reporting and historical tracking are key aspects of the ordering process. We want to treat them as such in our code, and not as a mere side effect.
For example: we could use our activity log package to keep track of "history messages" about what happened with an order. We could also start writing custom queries on the order and history tables to generate reports.
However, these solutions only work properly when they are minor side effects of the core business. In this case, they are not. So Freek and I were tasked with figuring out a design for this project that made reporting and historical tracking an easy-to-maintain and easy-to-use, core part of the application.
Naturally we looked at event sourcing, a wonderful and flexible solution that fulfills the above requirements. Nothing comes for free though: event sourcing requires quite a lot of extra code to be written in order to do otherwise simple things. Where you'd normally have simple CRUD actions manipulating data in the database, you now have to worry about dispatching events, handling them with projectors and reactors, whilst always keeping versioning in mind.
While it was clear that an event sourced system would solve many of the problems, it would also introduce lots of overhead, even in places where it wouldn't add any value.
Here's what I mean with that: if we decide to event source the Orders
module, which relies on data from the Products
module, we also need to event source that one, because otherwise we could end up with an invalid state. If Products
weren't event sourced, and one was deleted, we couldn't rebuild the Orders
state anymore, since it's missing information.
So either we event source everything, or find a solution for this problem.
From playing around with event sourcing in some of our hobby projects, we were painfully aware that we shouldn't underestimate the complexity it adds. Furthermore, Greg Young stated that event sourcing a whole system is often a bad idea — he has a whole talk on misconceptions about event sourcing and is worth a watch!
It was clear to us that we did not want to event source the whole application; it simply wouldn't make sense to do so. The only alternative was to find a way to combine a stateful system, together with an event sourced system, but surprisingly, we couldn't find many resources on this topic.
Nevertheless, we did some labour intensive research, and managed to find an answer to our question. The answer didn't come from the event sourcing community though, but rather from well-established DDD practices: bounded contexts.
If we wanted the Products
module to be an independent, stateful system, we had to clearly respect the boundaries between Products
and Orders
. Instead of one monolithic application, we would have to treat these two modules as two separate contexts — separate services, which were only allowed to speak with each other in such a way that it be could guaranteed the Order
context would never end up in an invalid state.
If the Order
context is built whereby it doesn't rely on the Product
context directly, it wouldn't matter how that Product
context was built.
When discussing this with Freek, I phrased it like this: think of Products
as a separate service, accessed via a REST API. How would we guarantee our event sourced application would still work, even if the API goes offline, or makes changes to its data structure.
Obviously we wouldn't actually build an API to communicate between our services, since they would live within the same codebase on the same server. Still it was a good mindset to start designing the system.
The boundary would look like this, where each service has its own internal design.
If you read my Laravel beyond CRUD series, you're already familiar with how the Product
context works. There's nothing new going on over there. The Order
context deserves a little more background information though.
So let's look at the event sourced part. I assume you that if you're reading this post, you have at least an interest in event sourcing, so I won't explain everything in detail.
The OrderAggregateRoot
will keep track of everything that happens within this context and will be the entry point for applications to talk with. It will also dispatch events, which are stored and propagated to all reactors and projectors.
Reactors will handle side effects which will never be replayed and projectors will make projections. In our case these are simple Laravel models. These models can be read from any other context, though they can only be written to from within projectors.
One design decision we made here was to not split our read and write models, for now we rely on a spoken and written convention that these models are only written to via their projectors. One example of such a projection model would be an Order
.
The most important rule to remember is that the whole state of the Order
context should be able to be rebuilt only from its stored events.
So how do we pull in data from other contexts? How can the Order
context be notified when something happens within the Product
context that's relevant to it? One thing is for sure: all relevant information regarding Products
will need to be stored as events within the Order
context; since within that context, events are the only source of truth.
To achieve this, we introduced a third kind of event listener. There already are projectors and reactors; now we add the concept of subscribers. These subscribers are allowed to listen to events from other contexts, and handle them accordingly within their current context. Most likely, they will almost always convert external events to internal, stored ones.
From the moment events are stored within the Order
context, we can safely forget about any dependency on the Product
context.
Some readers might think that we're duplicating data by copying events between these two contexts. We're of course storing an Orders
specific event, based on when a Product
was created
, so yes, some data will be copied. There are, however, more benefits to this than you might think.
First of all: the Product
context doesn't need to know anything about which other contexts will use its data. It doesn't have to take event versioning into account, because its events will never be stored. This allows us to work in the Product
context as if it was any normal, stateful application, without the complexity event sourcing adds.
Second: there will be more than just the Order
context that's event sourced, and all of these contexts can individually listen to relevant events triggered within the Product
context.
And third: we don't have to store a full copy of the original Product
events since each context can cherry-pick and store the data that's relevant for its own use case.
A new question arose.
Say this system has been in production for a year, and we decide to add a new context that's event sourced; one which also requires knowledge about the Product
context. The original Product
events weren't stored — because of the reasons listed above — so how can we build an initial state for our new context?
The answer is this: at the time of deployment, we'll have to read all product data, and send relevant events to the newly added context, based on the existing products. This one-time migration is an added cost, though it gives us the freedom to work within the Product
context without ever having to worry about the outside. For this project that's a price worth paying.
Finally we're able to consume data in our applications gathered from all contexts, by using readonly models. Again, in our case and as of now, these models are readonly by convention; we might change that in the future.
Communication from applications to the Product
context is done like any normal stateful application would do. Communication between applications and event sourced contexts such as Orders
is done via its aggregate root.
Now, here's a final overview. Some arrows are still missing from this diagram, but I hope that the relevant flow between and inside contexts and applications is clear.
The key in solving our problem was to look at DDD's bounded contexts. They describe strict boundaries within our codebase - ones that we cannot simply cross whenever we want. Sure this adds a layer of complexity, though it also adds the freedom to build each context whatever way we want, without having to worry about supporting others.
The final piece of the puzzle was to solely rely on events as a means of communication between contexts. Once again it adds a layer of complexity, but also a means of decoupling and flexibility.
Now it's time to take a deep dive into how we programmed this within a Laravel project. Here's my colleague Freek with part two.
]]>I can't anymore. At least, not with how composer works today.
See yesterday, we stumbled upon a breaking change. And yet we didn't do any major version upgrades. Luckily another colleague of mine, Ruben, discovered the issue before pushing it to production.
Here's what happened.
One of our projects is now nearing its two-year anniversary mark, so suffice to say it has had its fair share of version bumps. After two years of development, these were some packages we used:
{
"laravel/framework": "^6.5",
"league/commonmark": "^0.17.5",
"spatie/laravel-view-components": "^1.2",
// …
}
First of all, laravel/framework:^6.5
. We usually wait a month or two before updating to the next major Laravel version. As of today we're running 7 — which was needed to fix what went wrong.
Next there's league/commonmark:^0.17.5
; a very specific dependency, added in May, 2018. At the time, this specific dependency was needed: according to the changelog it Fixed incorrect version constant value (again)
. If we didn't use this version, it would conflict with other packages.
Two years went by, and league/commonmark
has since tagged a first stable release. This is something they should have done way more early — but that's a topic for another day.
Finally there's spatie/laravel-view-components:^1.2
. A package that has been archived recently, in favour of Laravel's blade components in version 7. Again, back in the day it made lots of sense to use this package. We might remove it at one point in the future, but this of course requires time, and costs money for our client. It isn't something we can "just do".
With the stage being set, it's time to look into the issue. Yesterday we ran a composer update
, and things broke.
More specifically, our spatie/laravel-view-components
package simply stopped working. Instead of rendering the view component, it only showed the render tag instead. It turned out it was a known issue as of version 1.3.0
, and fixed in 1.3.1
. This fix already existed when we ran our disastrous composer update
, yet we never received it. Our spatie/laravel-view-components
seemed to be locked on 1.3.0
, and didn't want to update to 1.3.1
.
Whenever you find yourself in such a pickle, don't panic and keep calm: composer can help you. Simply use composer why-not spatie/laravel-view-components:1.3.1
and it will tell you exactly what's wrong.
It turned out that with spatie/laravel-view-components:1.3.1
, its laravel/framework
dependency version was bumped from ^6.0
to ^6.18
.
That in itself shouldn't be a problem, we require laravel/framework:^6.5
in our project, so we should be able to load ^6.18
just fine.
Unfortunately that didn't happen. You see, laravel/framework
added a dependency on league/commonmark:^1.1
in version 6.10
. In practice, this addition has the same effect as updating the major version of a dependency: from nothing to ^1.1
.
Again, that change in itself isn't a breaking change, yet it did prevent laravel/framework
in our project from updating higher than 6.9
, because of our requirement on league/commonmark:^0.17.5
. That in turn prevented spatie/laravel-view-components
updating from 1.3.0
to 1.3.1
, which contained a much needed bugfix.
So who's to blame? Let's point the finger at myself first: we should have updated league/commonmark
sooner. You could also say that combining a bugfix and a dependency version bump, like spatie/laravel-view-components
did with 1.3.1
, should be avoided. Yet if the version bump is needed for a fix to work, there's little you can do.
You could say that laravel/framework
shouldn't have updated one of its (implicit) dependencies, yet it's a perfectly normal thing to do, especially if the update fixes a security issue.
The solution, by the way, consisted of updating laravel/framework
to ^7.0
— we had to do this anyway sooner or later — and removing the league/commonmark
dependency. So I don't think this change should be avoided by any open source vendors. Open source code should push us towards regular updates, I encourage that myself regularly.
The real problem though, is that composer never notified us that laravel/framework
wasn't able to update further than 6.9
because of underlying conflicts. If you're not carefully managing each dependency, you're in danger of getting stuck on an outdated dependency, which might prevent much needed bug fixes being installed.
As far as I know, there's no option that can be passed to composer update
which can notify you about such situations and I think that would be a good future addition.
My colleague Freek pointed out that there is an external library that does exactly this: https://github.com/Soullivaneuh/composer-versions-check. It'd be nice to have this functionality built-into composer.
]]>In this post I'll go through the architecture step-by-step and address its benefits as well as its downsides — at least, the ones I can think of right now. I do have a proof-of-concept codebase open sourced, and I'll share insights from it throughout this post.
So, first things first, what the architecture is about. It's a long-running PHP server, with its entire state loaded in memory, built from stored events. In other words: it's event sourcing as we know it in PHP, but all aggregates and projections are loaded in-memory and never stored on disk.
Let's break it down!
The first pillar of this architecture is a long running server. The modern PHP landscape offers several battle-tested solutions for managing these kinds of processes: frameworks like ReactPHP, Amphp and Swoole allowed the PHP community to venture into another, unexplored world, while day-to-day PHP was most often related to its characterizing fast request/response cycle.
This fast request/response cycle is of course one of the things that made PHP great: you never had to worry about leaking state or keeping everything in sync: when a request comes in, a clean PHP process is started, and your application boots from 0. After the response is sent, the application gets completely destroyed.
I'm not proposing we ditch this battle-tested technique altogether; the fast request/response cycle is actually a critical part of the architecture I'll be describing. On the other hand, always booting the whole application from scratch has its downsides.
In the architecture I'm describing, an application is split into two parts: one part is a regular PHP app, accepting HTTP requests and generating responses, while the other part is a behind-the-scenes backend server that's always running. A server that always has the whole application state loaded in memory, which allows the clients — our regular PHP apps — to connect with it, read data and store events.
Because the whole application state is always loaded in memory, you never need to perform database queries, spending resources on mapping data from the database to objects, or performance issues like circular references between ORM entities.
This sounds nice in theory, but we probably still need to be able to perform complex queries - something that databases are highly optimised for. It's clear that this architecture will require us to rethink certain aspects we're used to in regular PHP applications. I'll come back to this later.
First, let's look at the second pillar: event sourcing.
Why would I suggest to make event sourcing part of the core of this architecture? You could very well have a long running server with all data loaded in-memory from a normal database.
Let's go down that road for a moment: say a client performs an update and sends it to the backend server. The server will need to store the data in the database, as well as refresh its in-memory state. Such systems will need to take care of updating the application state properly so that everything is correct after an update.
The most naive approach would be to perform the updates in the database and reload the whole application state, which in practice isn't possible due to performance issues. Another approach could be to keep track of everything that needs to happen when an update is received, and the most flexible way to do that is by using events.
If we're naturally leaning towards an event-driven system to keep the in-memory state synchronised, why then add the overhead of storing everything in a database and require an ORM to map the data back to objects? That's why event sourcing is the better approach: it solves all state syncing problems automatically, and offers a performance gain since you don't have to communicate with a database and work with an ORM.
What about complex queries though? How would you search, for example, a product store containing millions of items, when everything is loaded in memory. PHP doesn't particularly excel at these kinds of tasks. But again, event sourcing offers a solution: projections. You're perfectly able to make an optimised projection for a given task, and even store it in a database! This could be a lightweight in-memory SQLite database, or a full-blown MySQL or PostgreSQL server.
Most importantly, these databases aren't part of the application core anymore. No longer are they the source of truth, but rather useful tools living on the edge of the application's core and very much comparable to building optimised search indices like ElasticSearch or Algolia. You can destroy these data sources at any point in time, and rebuild them from the stored events.
That brings us to the final reason why event sourcing is such a great match for this architecture. When the server requires a reboot — because of a server crash or after a deploy — event sourcing offers you a way to rebuild the application's state much faster: snapshots.
In this architecture, a snapshot of the whole application state would be stored once or twice a day. It's a point where the server can be rebuilt from, without needing to replay all events.
As you can see, there are several benefits by building an event sourced system within this architecture. Now we're moving on to the last pillar: the clients.
I've mentioned this before: with "clients" I mean server-side PHP applications communicating with the centralised backend server. They are normal PHP applications, only living a short time within the typical request/response cycle.
You can use whatever existing framework you want for these clients, as long as there's a way to use the event-server instead of directly communicating with eg. a database. Instead of using an ORM like Doctrine in Symfony or Eloquent in Laravel, you'd be using a small communication layer to communicate via sockets with the backend server.
Also keep in mind that the backend server and clients can share the same codebase, which means that from a developer's point of view, you don't need to worry about communication between a client and the server, it's done transparently.
Take the example of bank accounts with a balance. With this architecture, you'd write code like this:
final class AccountsController
{
public function index(): View
{
$accounts = Account::all();
return new View('accounts.index', [
'accounts' => $accounts,
]);
}
}
Keep in mind that I mainly work in a Laravel context and I'm used to the Eloquent ORM. If you prefer to use a repository pattern, that's also fine.
Behind the scenes, Account::all()
or $accountRepository->all()
will not perform database queries, rather they will send a small message to the backend server, which will send the accounts, from memory, back to the client.
If we're making a change to the accounts balance, that's done like so:
final class BalanceController
{
public function increase(Account $account, int $amount): Redirect
{
$aggregateRoot = AccountAggregateRoot::find($account);
$aggregateRoot->increaseBalance($amount);
return new Redirect(
[AccountsController::class, 'index'],
[$account]
);
}
}
Behind the scenes, AccountAggregateRoot::increaseBalance()
will send an event to the server, which will store it and notify all relevant subscribers.
If you're wondering what such an implementation of AccountAggregateRoot
might look like, here's a simplified version:
final class AccountAggregateRootRoot extends AggregateRoot
{
public function increaseBalance(int $amount): self
{
$this->event(new BalanceIncreased($amount));
return $this;
}
}
And finally this is what the Account
entity looks like. Notice the lack of ORM-style configuration; these are simple in-memory PHP objects!
final class Account extends Entity
{
public string $uuid;
public string $name;
public int $balance = 0;
}
One final note to make: remember that I mentioned PHP's fast request/response cycle would actually be critical? Here's why: if we're sending updates to the server, we don't need to worry about broadcasting those updates back to the clients. Every client generally only lives for a second or two, so there's little to worry about keeping them in sync.
All of this sounds interesting in theory, but what about in practice? What about performance? How much RAM will you need to store everything in memory? Will we be able to optimise reading the state by performing complex queries? How will snapshots be stored? What about versioning?
Lots of questions are still unanswered. The goal of this post was not to provide all answers, but rather share some thoughts and questions with you, the community. Who knows what you can come up with?
I mentioned that the code for this is open source, you can take a look at it over here. I'm looking forward to hearing your feedback, on Reddit, via Twitter or e-mail.
]]>In that post, I gave the example of a "date range boundaries" enum, one that represents which boundaries are included in the range, and which are not. It had four possible values:
Boundaries::INCLUDE_NONE();
Boundaries::INCLUDE_START();
Boundaries::INCLUDE_END();
Boundaries::INCLUDE_ALL();
To represent these boundaries, I stored two boolean flags on the enum value classes: $startIncluded
and $endIncluded
.
In this post, I want to show another way to store these two boolean flags, using bitmasks.
Here's a quick recap of what (part of) our enum class looked like:
abstract class Boundaries
{
private bool $startIncluded = false;
private bool $endIncluded = false;
public function startIncluded(): bool
{
return $this->startIncluded;
}
public function endIncluded(): bool
{
return $this->endIncluded;
}
// …
}
In this case, we're using two variables to store two boolean values.
Them being booleans though, means they can only have one of two values: true
or false
; 1
or 0
. Instead of using a whole byte, we only need one bit to store this value.
Hang on though, a whole byte? — It's actually a lot more: 16 bytes to be exact. PHP stores all variables in a structure called a zval
, which reserves memory not only for the payload, but also type information, bit flags and what not. You can take a look at it here.
Of those 16 bytes, there's 8 reserved per zval
to store a payload in; that's 64 bits!
Now, as a precursor, let's make clear that you probably will never need these kinds of micro-optimisations. Boolean bitmasks are often used in game development, compilers and the like, because they are very memory-efficient. In web applications, though, you can be assured you will probably never need them.
Nevertheless, it's a cool, geeky thing to know, and possible in PHP.
So let's store these two flags in one variable.
abstract class Boundaries
{
protected int $inclusionMask = 0b00;
}
What's happening here? We're making use of the binary notation of integers to easily work with individual bits. If you ever learned about binary systems in school or somewhere else, you know that 0b00
equals 0, 0b01
equals 1, 0b10
equals 2 and 0b11
equals 3. 0b
is a prefix that PHP uses to know you're writing binary, and 00
are the two actual bits.
Now that we've got two bits to work with, it's easy to store two boolean values in them. Let's say that the rightmost bit represents endIncluded
, and the leftmost bit represents startIncluded
.
So 0b01
means that the start boundary is not included, while the end boundary is; 0b11
means both are included — you get the gist.
Now that we know how to store data in bits, we still need a way of reading the information in our startIncluded()
and endIncluded()
methods: we don't want to program everything in binary.
Here's where bitwise operators come into play, more specifically the and
operator.
Take the following two binary values:
0b0100101;
0b1010101;
What happens when we apply an and
operation on both of these values? The result will have all bits set to 1
wherever both bits were 1
in the two original values:
0b0100101;
0b1010101;
This is the end result:
0b0000101;
Back to our boundaries example. How can we know whether the start is included or not? Since the start boundary is represented by the leftmost bit, we can apply a bitmask on our inclusion variable. If we want to know whether the start bit is set, we simply need to do an and
operation between the inclusion mask, and the binary value 0b10
.
How so? Since we're only interested in knowing the value of the start boundary, we'll make a mask for that bit only. If we apply an and
operation between these two values, the result will always be 0b00
, unless the start bit was actually set.
Here's an example where the start bit is 0
:
0b10; // The mask we're applying
0b01; // The inclusion mask
0b00; // The result
And here's one where the start bit is 1
:
0b10; // The mask we're applying
0b10; // The inclusion mask
0b10; // The result
The end bit will always be 0
in this case, because the mask we're applying has it set to 0
. Hence, whatever value is stored for the end boundary in the inclusion mask, will always result in 0
.
So how to do this in PHP? By using the binary and
operator, which is a single &
:
public function startIncluded(): bool
{
return $this->inclusionMask & 0b10;
}
public function endIncluded(): bool
{
return $this->inclusionMask & 0b01;
}
PHP's dynamic type system will automatically cast the result, 0
or a numeric value, to a boolean. If you want to be more explicit though, you can write it like so:
public function startIncluded(): bool
{
return ($this->inclusionMask & 0b10) !== 0;
}
public function endIncluded(): bool
{
return ($this->inclusionMask & 0b01) !== 0;
}
Let's make clear that you shouldn't be doing this for performance motivations in PHP. There might even be edge cases where this approach would be less optimal, because our inclusion mask can't be garbage collected unless there are no reference anymore to any of the boolean flags.
However, if you're working with several boolean flags at once, it might be useful to store them in one variable instead of several, to reduce cognitive load. You could think of "storing the boolean values" as a behind-the-scenes implementation detail, while the public API of a class still provides a clear way of working with them.
So, who knows, there might be cases where this technique is useful. If you have some real-life use cases, be sure to let me know on Twitter or via e-mail.
]]>Boundaries
enum would be used:
$dateRange = DateRange::make(
'2020-02-01',
'2020-03-01',
Boundaries::INCLUDE_ALL()
);
This is what the constructor signature of DateRange
looks like:
public function __construct($start, $end, Boundaries $boundaries);
That's the first requirement: we want to use the type system to ensure only valid enum values are used.
Next, we want to be able to ask the enum which boundaries are included, like so:
$dateRange->boundaries->startIncluded();
$dateRange->boundaries->endIncluded();
This means that each enum value should support its own implementation of startIncluded
and endIncluded
.
That's the second requirement: we want our enums to support value-specific behaviour.
On first sight, the easiest solution is to have a Boundaries
class, and implement startIncluded
and endIncluded
like so:
final class Boundaries
{
private const INCLUDE_NONE = 'none';
private const INCLUDE_START = 'start';
private const INCLUDE_END = 'end';
private const INCLUDE_ALL = 'all';
private string $value;
public static function INCLUDE_START(): self
{
return new self(self::INCLUDE_START);
}
private function __construct(string $value)
{
$this->value = $value;
}
public function startIncluded(): bool
{
return $this->value === self::INCLUDE_START
|| $this->value === self::INCLUDE_ALL;
}
public function endIncluded(): bool
{
return $this->value === self::INCLUDE_END
|| $this->value === self::INCLUDE_ALL;
}
}
In short: using conditionals on an enum's value to add behaviour.
For this example, it's a clean enough solution. However: it doesn't scale that well. Imagine our enum needs more complex value-specific functionality; you often end up with large functions containing large conditional blocks.
The more conditionals, the more paths your code can take, the more complex it is to understand and maintain, and the more prone to bugs.
That's the third requirement: we want to avoid using conditionals on enum values.
In summary, we want our enums to match these three requirements:
Polymorphism can offer a solution here: each enum value can be represented by its own class, extending the Boundaries
enum. Therefore, each value can implement its own version of startIncluded
and endIncluded
, returning a simple boolean.
Maybe we'd make something like this:
abstract class Boundaries
{
public static function INCLUDE_NONE(): IncludeNone
{
return new IncludeNone();
}
// …
abstract public function startIncluded(): bool;
abstract public function endIncluded(): bool;
}
And have a concrete implementation of Boundaries
like this — you can imagine what the other three would look like:
final class IncludeNone extends Boundaries
{
public function startIncluded(): bool
{
return false;
}
public function endIncluded(): bool
{
return false;
}
}
While there's more initial work to program these enums, we now meet all requirements.
There's one more improvement to be made. There's no need to use dedicated classes for specific values; they will never be used on their own. So instead of making four classes extending Boundaries
, we could use anonymous classes:
abstract class Boundaries
{
abstract public function startIncluded(): bool;
abstract public function endIncluded(): bool;
public static function INCLUDE_NONE(): Boundaries
{
return new class extends Boundaries
{
public function startIncluded(): bool {
return false;
}
public function endIncluded(): bool {
return false;
}
};
}
public static function INCLUDE_START(): Boundaries
{
return new class extends Boundaries
{
public function startIncluded(): bool {
return true;
}
public function endIncluded(): bool {
return false;
}
};
}
public static function INCLUDE_END(): Boundaries
{
return new class extends Boundaries
{
public function startIncluded(): bool {
return false;
}
public function endIncluded(): bool {
return true;
}
};
}
public static function INCLUDE_ALL(): Boundaries
{
return new class extends Boundaries
{
public function startIncluded(): bool {
return true;
}
public function endIncluded(): bool {
return true;
}
};
}
}
Ok, I was mistaken: there were two more improvements to be made. This is a lot of repeated code! But again there's a solution for that! Let's simply define two properties on each value-specific class ($startIncluded
and $endIncluded
) and let's implement their getters on the abstract Boundaries
class instead!
abstract class Boundaries
{
protected bool $startIncluded;
protected bool $endIncluded;
public function startIncluded(): bool
{
return $this->startIncluded;
}
public function endIncluded(): bool
{
return $this->endIncluded;
}
public static function INCLUDE_NONE(): Boundaries
{
return new class extends Boundaries
{
protected bool $startIncluded = false;
protected bool $endIncluded = false;
};
}
public static function INCLUDE_START(): Boundaries
{
return new class extends Boundaries
{
protected bool $startIncluded = true;
protected bool $endIncluded = false;
};
}
public static function INCLUDE_END(): Boundaries
{
return new class extends Boundaries
{
protected bool $startIncluded = false;
protected bool $endIncluded = true;
};
}
public static function INCLUDE_ALL(): Boundaries
{
return new class extends Boundaries
{
protected bool $startIncluded = true;
protected bool $endIncluded = true;
};
}
}
The above is my favourite approach to implement enums in PHP. If there's one downside I can think of, it's that they require a little setup work, though I find that this is a small, one-off cost, that pays off highly in the long run.
]]>While many of the arguments against PHP still stand today, there's also a bright side: you can write clean and maintainable, fast and reliable applications in PHP.
In this post, I want to look at this bright side of PHP development. I want to show you that, despite its many shortcomings, PHP is a worthwhile language to learn. I want you to know that the PHP 5 era is coming to an end. That, if you want to, you can write modern and clean PHP code, and leave behind much of the mess it was 10 years ago.
So let's look at how the language has changed, matured even, over the past few years. I want to ask you to set aside any prejudice for just a few minutes, and possibly be surprised by what PHP is today.
Let's dive in.
Before diving into details, let's review how PHP, the language, is developed these days. We're at version 7.4 now, and PHP 8 will be the next version after that, at the end of 2020.
Ever since the late 5.* era, the core team tries to keep a consistent yearly release cycle, and have succeeded in doing so for the past four years.
In general, every new release is actively supported for two years, and gets another year of "security fixes only". The goal is to motivate developers to stay up-to-date as much as possible: small upgrades every year are easier than making the jump between 5.4 to 7.0, for example.
Lastly, PHP 5.6 was the latest 5.* release, with 7.0 being the next one. If you want to know what happened to PHP 6, you can listen to this episode of the PHP Roundtable podcast.
PHP's development these days is done by a group of volunteers, some of them are paid by their employers to work on the core full time. Most discussion of how the language is evolved happens on a mailing list.
With all of that out of the way, let's debunk some common misconceptions about modern PHP.
PHP started out as a very weakly and dynamically typed language, which had its benefits at the time. Ever since people started to use PHP for larger projects though, the shortcomings of its type system became clear, and the need for stronger type support arose.
Today, PHP is a rather unique language: it still allows you to write completely dynamically and weakly typed code, but also has a much stronger, opt-in type system. Combined with static analysis, tools like Psalm, Phan and PHPStan, you can write secure, strongly typed and statically analysed code.
Take, for example, a look at this snippet of PHP code, using its modern type system in full:
<?php
declare(strict_types=1);
final class Foo
{
public int $intProperty = 2;
public ?string $nullableString = null;
private Bar $bar;
public function __construct(Bar $bar) {
$this->bar = $bar;
}
public function withInt(int $value): self
{
$clone = clone $this;
$clone->intProperty = $value;
return $clone;
}
public function unionTypes(int|float $input): void
{
// Union types will be added in PHP 8
}
}
Truth be told, there's one important feature still missing in PHP's type system: generics. There's hope they will be added, but there's nothing concrete yet. In case of typed arrays, you'll need to rely on docblocks to get proper IDE support:
/** @var int[] */
public array $arrayOfInts = [];
And while typed arrays are a common use case for generics, solvable with docblocks, there's a lot more functionality we're missing out on because they are not in the language… yet.
The 7.* era has done many good things in terms of making PHP a more mature language when it comes to syntax. To illustrate this I've made a non-exhaustive list of new things in PHP.
Array destructuring:
[$a, $b] = $array;
The null coalescing operator:
$value = $object->property ?? 'fallback if null';
$value = $array['foo'] ?? "fallback if key doesn't exists";
The null coalescing assignment operator:
public function get(string $input): string
{
return $this->cache[$input] ??= $this->sanitize($input);
}
Array spreading:
$a = [/* … */];
$b = [/* … */];
$mergedArray = [...$a, ...$b];
Variadic functions:
public function get(Foo ...$foos): void
{
foreach($foos as $foo) {
// …
}
}
Argument unpacking:
$this->get(...$arrayOfFoo);
public int $intProperty;
Arrow functions, also called short closures:
$ids = array_map(fn(Post $post): int => $post->id, $posts);
Generators:
function make(array $input): Generator
{
foreach ($input as $item) {
yield $this->doSomethingWith($item);
}
}
And quite a lot more. I hope that it's clear from this list that PHP is still evolving today, and you can be sure there's more good stuff to come.
Back in the 5.* days, PHP's performance was… average at best. With 7.0 though, large parts of PHP's core were rewritten from the ground up, resulting in two or three times performance increases. Furthermore, each 7.* release has had a positive impact on performance.
Words don't suffice though. Let's look at benchmarks. Luckily other people have spent lots of time in benchmarking PHP performance. I find that Kinsta has a good updated list.
The latest performance related feature is called preloading, which basically allows you to store compiled parts of your PHP code in memory. You can look at some benchmarks over here.
When PHP 8 arrives, we'll also have a JIT compiler at our disposal, promising interesting performance improvements, and allowing PHP to enter new areas besides web development.
Moving on to what's done by the community with PHP. Let's be clear: PHP isn't just WordPress anymore, on the contrary.
In general there are two major web application frameworks, and a few smaller ones: Symfony and Laravel. Sure there's also Laminas, Yii, Cake, Code Igniter etc. — but if you want to know what modern PHP development looks like, you're good with one of the first two.
Both frameworks have a large ecosystem of packages and products. Ranging from admin panels and CRMs to standalone packages, CI to profilers, numerous services like web sockets servers, queuing managers, payment integrations; honestly there's too much to list.
These frameworks are meant for actual development; if you're instead in need of pure content management, platforms like WordPress, CraftCMS and Statamic are improving more and more.
One way to measure the current state of PHP's ecosystem is to look at Packagist, the main package repository for PHP. It has seen exponential growth. With ±25 million downloads a day, it's fair to say that the PHP ecosystem isn't the small underdog it used to be.
Take a look at this graph, listing the amount of packages and versions over time. It can also be found on the Packagist website.
Besides application frameworks and CMSs, we've also seen the rise of asynchronous frameworks the past years. These are frameworks and servers, written in PHP or other languages, that allow users to run truly asynchronous PHP code. Some major players are Swoole, Amp and ReactPHP.
Since we've ventured into the async world, stuff like web sockets and applications with lots of IO have become actually relevant in the PHP world.
There has also been talk on the internals mailing list to add libuv to the core. For those unaware of libuv: it's the same library Node.js uses to allow all its asynchronicity. Who knows? PHP 8 might be the version adding it to the core!
I hope I was able to show you that PHP has evolved tremendously over the past years, and you're perfectly able to write clean and maintainable code with it.
If you're interested in what PHP code looks like in the wild these days, you can check out the source code of one of my own projects, as well as many open source packages we personally maintain.
So while the language definitely has its drawbacks and 20 years of legacy to carry with it; I can say with confidence that I enjoy working with it.
In my experience, I'm able to create reliable, maintainable and quality software. The clients I work for are happy with the end result, as am I. While it's still possible to do lots of messed up things with PHP, I'd say it's a great choice for web development if used wisely and correctly.
Don't you agree? Let me know why! You can reach me via Twitter or e-mail.
]]>Because I want to know whether preloading will have a practical impact on my projects, I'll be running benchmarks on a real project, on the homepage of my hobby project aggregate.stitcher.io.
This project is a Laravel project, and will obviously do some database calls, view rendering etc. I want to make clear that these benchmarks don't tell anything about the performance of Laravel projects, they only measure the relative performance gains preloading could offer.
Let me repeat that again, just to make sure no one draws wrong conclusions from these results: my benchmarks will only measure whether preloading has a relative performance impact compared to not using it. These benchmarks say nothing about how much performance gain there is. This will depend on several variables: server load, the code being executed, what page you're on, etc.
Let's set the stage.
Since I don't want to measure how much exactly will be gained by using preloading or not, I decided to run these benchmarks on my local machine, using Apache Bench. I'll be sending 5000 requests, with 50 concurrent requests at a time. The webserver is nginx, using php-fpm. Because there were some bugs in early versions of preloading, we're only able to successfully run our benchmarks as early as PHP 7.4.2.
I'll be benchmarking three scenarios: one with preloading disabled, one with all Laravel and application code preloaded, and one with an optimised list of preloaded classes. The reasoning for that latter one is that preloading also comes with a memory overhead, if we're only preloading "hot" classes — classes that are used very often — we might be able to find a sweet spot between performance gain and memory usage.
We start php-fpm and run our benchmarks:
./php-7_4_2/sbin/php-fpm --nodaemonize
ab -n 5000 -c 50 -l http://aggregate.stitcher.io.test:8080/discover
These were the results: we're able to process 64.79
requests per second, with an average time of 771ms
per request.
This is our baseline scenario, we can compare the next results to this one.
Next we'll preload all Laravel and application code. This is the naive approach, because we're never using all Laravel classes in a request. Because we're preloading many more files than strictly needed, we'll have to pay a penalty for it. In this case 1165 classes and their dependencies were preloaded, resulting in a total of 1366 functions and 1256 classes to be preloaded.
Like I mentioned before,you can read that info from opcache_get_status
:
opcache_get_status()['preload_statistics'];
Another metric we get from opcache_get_status
is the memory used for preloaded scripts. In this case it's 17.43 MB.
Even though we're preloading more code than we actually need, naive preloading already has a positive impact on performance.
requests/second | time per request | |
No preloading | 64.79 | 771ms |
Naive preloading | 79.69 | 627ms |
You can already see a performance gain: we're able to manage more requests per second, and the average amount of time to process one request has dropped with ±20%.
Finally we want to compare the performance gain when we're using an optimised preloading list. For testing purposes I started the server without preloading enabled, and dumped all classes that are used within that request:
get_declared_classes();
Next, I only preloaded these classes, 427 in total. Together with all their dependencies this makes for 643 classes and 1034 functions being preloaded, occupying about 11.76 MB of memory.
These are the benchmark results for this setup:
requests/second | time per request | |
No preloading | 64.79 | 771ms |
Naive preloading | 79.69 | 627ms |
Optimised preloading | 86.12 | 580ms |
That's around a 25% performance gain compared to not using preloading, and an 8% gain compared to using the naive approach. There's a flaw with this setup though, since I generated an optimised preloading list for one specific page. In practice you would probably need to preload more code, if you want all your pages covered.
Another approach could be to monitor which classes are loaded how many times over the period of several hours or days on your production server, and compile a preload list based on those metrics It's safe to say that preloading — even using the naive "preload everything" approach — has a positive performance impact, also on real-life projects built upon a full-blown framework. How much exactly there is to be gained will depend on your code, your server and the framework you're using. I'd say go try it out!
]]>Start by making sure brew is up-to-date:
brew update
Next, upgrade PHP:
brew upgrade php
Check the current version by running php -v
:
php -v
Restart Nginx or Apache:
sudo nginx -s reload
sudo apachectl restart
And make sure that your local web server also uses PHP 7.4 by visiting this script:
# index.php, accessible to your web server
phpinfo(); die();
The version should show 7.4.x
.
Note: if you're using Laravel Valet, please keep on reading, you need some extra steps in order for the web server to properly work.
If you're using Laravel Valet, you should do the following steps to upgrade it:
composer global update
Now run valet install
:
valet install
Homebrew doesn't support the installation of PHP extensions anymore, you should use pecl instead. I personally use Imagick, Redis and Xdebug.
They can be installed like so:
pecl install imagick
pecl install redis
pecl install xdebug
You can run pecl list
to see which extensions are installed:
pecl list
# Installed packages, channel pecl.php.net:
# =========================================
# Package Version State
# imagick 3.4.4 stable
# redis 5.1.1 stable
# xdebug 2.8.0 stable
You can search for other extensions using pecl search
:
pecl search pdf
# Retrieving data...0%
# ..
# Matched packages, channel pecl.php.net:
# =======================================
# Package Stable/(Latest) Local
# pdflib 4.1.2 (stable) Creating PDF on the fly with the PDFlib library
Make sure to restart your web server after installing new packages:
sudo nginx -s reload
sudo apachectl restart
If you're using Laravel Valet, you should restart it as well.
valet restart
Make sure all extensions are correctly installed and loaded by checking both your PHP webserver and CLI installs:
php -i | grep redis
var_dump(extension_loaded('redis'));
If extensions aren't properly loaded, there are two easy fixes.
First, make sure the extensions are added in the correct ini file. You can run php --ini
to know which file is loaded:
Configuration File (php.ini) Path: /usr/local/etc/php/7.4</hljs>
Loaded Configuration File: /usr/local/etc/php/7.4/php.ini
Scan for additional .ini files in: /usr/local/etc/php/7.4/conf.d
Additional .ini files parsed: /usr/local/etc/php/7.4/conf.d/ext-opcache.ini,
/usr/local/etc/php/7.4/conf.d/php-memory-limits.ini
Now check the ini file:
extension="redis.so"
extension="imagick.so"
extension="xdebug.so"
Note that if you're testing installed extensions via the CLI, you don't need to restart nginx, apache or Valet.
The second thing you can do, if you're updating from an older PHP version which also used pecl to install extension; is to reinstall every extension individually.
pecl uninstall imagick
pecl install imagick
Finally you should test and upgrade your projects for PHP 7.4 compatibility.
]]>PHP 7.4 is the latest stable version of PHP. It was released on November 28, 2019 and it's the last version before PHP 8. It brings lots of new features, syntax additions and fixes. In this post you'll find a list with everything that's new and changed to help you prepare for the upgrade. Let's start though with a few highlights, included in PHP 7.4:
PHP 7.4 comes with a remarkable amount of new features. We'll start with a list of all new features, and then look at changes and deprecations.
A note before we dive in though: if you're still on a lower version of PHP, you'll also want to read what's new in PHP 7.3.
Arrow functions, also called "short closures", allow for less verbose one-liner functions.
While you'd previously write this:
array_map(function (User $user) {
return $user->id;
}, $users)
You can now write this:
array_map(fn (User $user) => $user->id, $users)
There are a few notes about arrow functions:
use
keyword.$this
is available just like normal closures.You can read about them in depth here.
Class variables can be type hinted:
class A
{
public string $name;
public ?Foo $foo;
}
There's lots to tell about this feature, so I wrote a dedicated post about them.
I also wrote about PHP's type system in the past, so it's good to see some improvements are actually arriving in PHP's core.
Type variance is another topic worth its own blog post, but in short: you'll be able use covariant return types –
class ParentType {}
class ChildType extends ParentType {}
class A
{
public function covariantReturnTypes(): ParentType
{ /* … */ }
}
class B extends A
{
public function covariantReturnTypes(): ChildType
{ /* … */ }
}
– and contravariant arguments.
class A
{
public function contraVariantArguments(ChildType $type)
{ /* … */ }
}
class B extends A
{
public function contraVariantArguments(ParentType $type)
{ /* … */ }
}
Next is the null coalescing assignment operator, a shorthand for null coalescing operations. Instead of doing this:
$data['date'] = $data['date'] ?? new DateTime();
You can do this:
$data['date'] ??= new DateTime();
You can read more about this operator in my post about PHP shorthands.
Next up, it's now possible to use the spread operator in arrays:
$arrayA = [1, 2, 3];
$arrayB = [4, 5];
$result = [0, ...$arrayA, ...$arrayB, 6 ,7];
// [0, 1, 2, 3, 4, 5, 6, 7]
Note that this only works with arrays with numerical keys, as of PHP 8.1 it's also possible to unpack arrays with string keys.
PHP 7.4 allows for underscores to be used to visually separate numeric values. It looks like this:
$unformattedNumber = 107925284.88;
$formattedNumber = 107_925_284.88;
The underscores are simply ignored by the engine.
Moving on to some more core-level features: foreign function interface or "FFI" in short, allows us to call C code from userland. This means that PHP extensions could be written in pure PHP and loaded via composer.
It should be noted though that this is a complex topic. You still need C knowledge to be able to properly use this feature.
Another lower-level feature is preloading. It's is an amazing addition to PHP's core, which can result in some significant performance improvements.
In short: if you're using a framework, its files have to be loaded and linked on every request. Preloading allows the server to load PHP files in memory on startup, and have them permanently available to all subsequent requests.
The performance gain comes of course with a cost: if the source of preloaded files are changed, the server has to be restarted.
Do you want to know more? I wrote a dedicated post about setting up and using preloading and have also done some preloading benchmarks.
Two new magic methods have been added: __serialize
and __unserialize
.
The difference between these methods and __sleep
and __wakeup
is discussed in the RFC.
Libraries like Symfony's var dumper rely heavily on the reflection API to reliably dump a variable. Previously it wasn't possible to properly reflect references, resulting in these libraries relying on hacks to detect them.
PHP 7.4 adds the ReflectionReference
class which solves this issue.
Weak references are references to objects, which don't prevent them from being destroyed.
mb_str_split
added rfcThis function provides the same functionality as str_split
, but on multi-byte strings.
Internal changes have been made to how hashing libraries are used, so that it's easier for userland to use them.
More specifically, a new function password_algos
has been added which returns a list of all registered password algorithms.
The RFC was a little unclear about the benefits, luckily Sara was able to provide some more context:
It means that ext/sodium (or anyone really) can register password hashing algorithms dynamically. The upshot of which is that argon2i and argon2id will be more commonly available moving forward
Besides new features, there are also lots of changes to the language. Most of these changes are non-breaking, though some might have an effect on your code bases.
Note that deprecation warnings aren't per definition "breaking", but merely a notice to the developer that functionality will be removed or changed in the future. It would be good not to ignore deprecation warnings, and to fix them right away; as it will make the upgrade path for PHP 8.0 more easy.
The ternary operator has some weird quirks in PHP. This RFC adds a deprecation warning for nested ternary statements. In PHP 8, this deprecation will be converted to a compile time error.
1 ? 2 : 3 ? 4 : 5; // deprecated
(1 ? 2 : 3) ? 4 : 5; // ok
__toString
rfcPreviously, exceptions could not be thrown in __toString
.
They were prohibited because of a workaround for some old core error handling mechanisms,
but Nikita pointed out that this "solution" didn't actually solve the problem it tried to address.
This behaviour is now changed, and exceptions can be thrown from __toString
.
If you'd write something like this:
echo "sum: " . $a + $b;
PHP would previously interpret it like this:
echo ("sum: " . $a) + $b;
PHP 8 will make it so that it's interpreted like this:
echo "sum: " . ($a + $b);
PHP 7.4 adds a deprecation warning when encountering an unparenthesized expression containing a .
before a +
or -
sign.
array_merge
without arguments upgradingSince the addition of the spread operator, there might be cases where you'd want to use array_merge
like so:
$merged = array_merge(...$arrayOfArrays);
To support the edge case where $arrayOfArrays
would be empty, both array_merge
and array_merge_recursive
now allow an empty parameter list.
An empty array will be returned if no input was passed.
It was possible to access arrays and string offsets using curly brackets:
$array{1};
$string{3};
This has been deprecated.
If you were to use the array access syntax on, say, an integer; PHP would previously return null
.
As of PHP 7.4, a notice will be emitted.
$i = 1;
$i[0]; // Notice
proc_open
improvements upgradingChanges were made to proc_open
so that it can execute programs without going through a shell. This is done by passing an array instead of a string for the command.
strip_tags
also accepts arrays upgradingYou used to only be able to strip multiple tags like so:
strip_tags($string, '<a><p>')
PHP 7.4 also allows the use of an array:
strip_tags($string, ['a', 'p'])
ext-hash
always enabled rfcThis extension is now permanently available in all PHP installations.
Because PEAR isn't actively maintained anymore, the core team decided to remove its default installation with PHP 7.4.
This RFC bundles lots of small deprecations, each with their own vote. Be sure to read a more detailed explanation on the RFC page, though here's a list of deprecated things:
real
typearray_key_exists()
with objectsFILTER_SANITIZE_MAGIC_QUOTES
filterexport()
methodsmb_strrpos()
with encoding as 3rd argumentimplode()
parameter order mix$this
from non-static closureshebrevc()
functionconvert_cyr_string()
functionmoney_format()
functionezmlm_hash()
functionrestore_include_path()
functionallow_url_include
ini directiveYou should always take a look at the full UPGRADING document when upgrading PHP versions.
Here are some changes highlighted:
parent::
in a class without a parent is deprecated.var_dump
on a DateTime
or DateTimeImmutable
instance will no longer
leave behind accessible properties on the object.openssl_random_pseudo_bytes
will throw an exception in error situations.PDO
or PDOStatement
instance will generate
an Exception
instead of a PDOException
.get_object_vars()
on an ArrayObject
instance will return
the properties of the ArrayObject
itself, and not the values of the wrapped array or object.
Note that (array)
casts are not affected.ext/wwdx
has been deprecated.This is technically not an update related to PHP 7.4, though it's worth mentioning: the voting rules for RFC's have been changed.
In a nutshell, this is how it works:
Let's look at it in depth.
While preloading is built on top of opcache, it's not exactly the same. Opcache will take your PHP source files, compile it to "opcodes", and store those compiled files on disk.
You can think of opcodes as a low-level representation of your code, that can be easily interpreted at runtime. So opcache skips the translation step between your source files and what the PHP interpreter actually needs at runtime. A huge win!
But there's more to be gained. Opcached files don't know about other files. If you've got a class A
extending from class B
, you'd still need to link them together at runtime. Furthermore, opcache performs checks to see whether the source files were modified, and will invalidate its caches based on that.
So this is where preloading comes into play: it will not only compile source files to opcodes, but also link related classes, traits and interfaces together. It will then keep this "compiled" blob of runnable code — that is: code usable by the PHP interpreter — in memory.
When a request arrives at the server, it can now use parts of the codebase that were already loaded in memory, without any overhead.
So, what "parts of the codebase" are we talking about?
For preloading to work, you — developers — have to tell the server which files to load. This is done with a simple PHP script, there really isn't anything difficult to it.
The rules are simple:
opcache.preload
opcache_compile_file()
or be required once, from within the preload scriptSay you want to preload a framework, Laravel for example. Your script will have to loop over all PHP files in the vendor/laravel
directory, and include them one by one.
Here's how you'd link to this script in php.ini:
opcache.preload=/path/to/project/preload.php
And here's a dummy implementation:
$files = /* An array of files you want to preload */;
foreach ($files as $file) {
opcache_compile_file($file);
}
Hang on though, there's a caveat! In order for files to be preloaded, their dependencies — interfaces, traits and parent classes — must also be preloaded.
If there are any problems with the class dependencies, you'll be notified of it on server start up:
Can't preload unlinked class
Illuminate\Database\Query\JoinClause:
Unknown parent
Illuminate\Database\Query\Builder
See, opcache_compile_file()
will parse a file, but not execute it. This means that if a class has dependencies that aren't preloaded, itself can also not be preloaded.
This isn't a fatal problem, your server will work just fine; but you won't have all the preloaded files you actually wanted.
Luckily, there's a way to ensure linked files are loaded as well: instead of using opcache_compile_file
you can use require_once
, and let the registered autoloader (probably composer's) take care of the rest.
$files = /* All files in eg. vendor/laravel */;
foreach ($files as $file) {
require_once($file);
}
There are some caveats still. If you're trying to preload Laravel for example, there are some classes within the framework that have dependencies on other classes that don't exist yet. For example, the filesystem cache class \Illuminate\Filesystem\Cache
has a dependency on \League\Flysystem\Cached\Storage\AbstractCache
, which might not be installed in your project if you're never using filesystem caches.
You might run into "class not found" errors trying to preload everything. Luckily, in a default Laravel installation, there's only a handful of these classes, which can easily be ignored. For convenience, I wrote a little preloader class to make ignoring files more easy, here's what it looks like:
class Preloader
{
private array $ignores = [];
private static int $count = 0;
private array $paths;
private array $fileMap;
public function __construct(string ...$paths)
{
$this->paths = $paths;
// We'll use composer's classmap
// to easily find which classes to autoload,
// based on their filename
$classMap = require __DIR__ . '/vendor/composer/autoload_classmap.php';
$this->fileMap = array_flip($classMap);
}
public function paths(string ...$paths): Preloader
{
$this->paths = array_merge(
$this->paths,
$paths
);
return $this;
}
public function ignore(string ...$names): Preloader
{
$this->ignores = array_merge(
$this->ignores,
$names
);
return $this;
}
public function load(): void
{
// We'll loop over all registered paths
// and load them one by one
foreach ($this->paths as $path) {
$this->loadPath(rtrim($path, '/'));
}
$count = self::$count;
echo "[Preloader] Preloaded {$count} classes" . PHP_EOL;
}
private function loadPath(string $path): void
{
// If the current path is a directory,
// we'll load all files in it
if (is_dir($path)) {
$this->loadDir($path);
return;
}
// Otherwise we'll just load this one file
$this->loadFile($path);
}
private function loadDir(string $path): void
{
$handle = opendir($path);
// We'll loop over all files and directories
// in the current path,
// and load them one by one
while ($file = readdir($handle)) {
if (in_array($file, ['.', '..'])) {
continue;
}
$this->loadPath("{$path}/{$file}");
}
closedir($handle);
}
private function loadFile(string $path): void
{
// We resolve the classname from composer's autoload mapping
$class = $this->fileMap[$path] ?? null;
// And use it to make sure the class shouldn't be ignored
if ($this->shouldIgnore($class)) {
return;
}
// Finally we require the path,
// causing all its dependencies to be loaded as well
require_once($path);
self::$count++;
echo "[Preloader] Preloaded `{$class}`" . PHP_EOL;
}
private function shouldIgnore(?string $name): bool
{
if ($name === null) {
return true;
}
foreach ($this->ignores as $ignore) {
if (strpos($name, $ignore) === 0) {
return true;
}
}
return false;
}
}
By adding this class in the same preload script, we're now able to load the whole Laravel framework like so:
// …
(new Preloader())
->paths(__DIR__ . '/vendor/laravel')
->ignore(
\Illuminate\Filesystem\Cache::class,
\Illuminate\Log\LogManager::class,
\Illuminate\Http\Testing\File::class,
\Illuminate\Http\UploadedFile::class,
\Illuminate\Support\Carbon::class,
)
->load();
That's of course the most important question: were all files correctly loaded? You can simply test it by restarting the server, and dump the output of opcache_get_status()
in a PHP script. You'll see it has a key called preload_statistics
, which will list all preloaded functions, classes and scripts; as well as the memory consumed by the preloaded files.
One promising feature is probably an automated preloading solution based on composer, which is used by most modern day PHP projects already.
People are working to add a preload configuration option in composer.json
, which in turn will generate the preload file for you! At the moment, this feature is still a work in progress, but you can follow it here.
Update 2019-11-29: composer support has stopped, as can be read by Jordi's answer.
There's two more important things to mention about the devops side when using preloading.
You already know that you need to specify an entry in php.ini in order for preloading to work. This means that if you're using shared hosting, you won't be able to freely configure PHP however you want. In practice, you'll need a dedicated (virtual) server to be able to optimise the preloaded files for a single project. So keep that in mind.
Also remember you'll need to restart the server (php-fpm
is sufficient if you're using it) every time you want to reload the in-memory files. This might seem obvious for most, but still worth the mention.
Now to the most important question: does preloading actually improve performance?
The answer is yes, of course: Ben Morel shared some benchmarks, which can be found in the same composer issue linked to earlier. I also did my own benchmarks within a real-life Laravel project. You can read about them here.
Interestingly enough, you could decide to only preload "hot classes" — classes that are used often in your codebase. Ben's benchmarks shows that only loading around 100 hot classes, actually yields better performance gains than preloading everything. It's a difference of a 13% and 17% performance increase.
Which classes should be preloaded relies of course on your specific project. It would be wise to simply preload as much as possible at the start. If you really need the few percentage increases, you would have to monitor your code while running.
All of this can of course also be automated, and will probably be done in the future.
For now, most important to remember is that composer will add support, so that you don't have to make preload files yourself, and that this feature is very easy to setup on your server, given that you've got full control over it.
Will you be using preloading once PHP 7.4 arrives? Any remarks or thoughts after reading this post? Let me know via Twitter or e-mail.
]]>array_map
or array_filter
.
This is what they look like:
// A collection of Post objects
$posts = [/* … */];
$ids = array_map(fn($post) => $post->id, $posts);
Previously, you'd had to write this:
$ids = array_map(function ($post) {
return $post->id;
}, $posts);
Let's summarize how short closures can be used.
fn
keywordreturn
keyword allowedA more strictly typed way of writing the example above could be this:
$ids = array_map(fn(Post $post): int => $post->id, $posts);
Two more things to mention:
If you want to return a value by reference, the following syntax should be used:
fn&($x) => $x
In short, short closures allow the same functionality you'd expect from normal closures, with the exception of only allowing one expression.
You read it right: short closures can only have one expression; that one expression may be spread over multiple lines for formatting, but it must always be one expression.
The reasoning is as follows: the goal of short closures is to reduce verbosity.
fn
is of course shorter than function
in all cases.
Nikita Popov, the creator of the RFC, however argued that if you're dealing with multi-line functions,
there is less to be gained by using short closures.
After all, multi-line closures are by definition already more verbose;
so being able to skip two keywords (function
and return
) wouldn't make much of a difference.
Whether you agree with this sentiment is up to you. While I can think of many one-line closures in my projects, there are also plenty of multi-line ones, and I'll personally miss the short syntax in those cases.
There's hope though: it is possible to add multi-line short closures in the future, but that's an RFC on its own.
Another significant difference between short and normal closures is that the short ones don't
require the use
keyword to be able to access data from the outer scope.
$modifier = 5;
array_map(fn($x) => $x * $modifier, $numbers);
It's important to note that you're not allowed to modify variables from the outer scope.
Values are bound by value and not by reference.
This means that you could change $modifier
within the short closure,
though it wouldn't have effect on the $modifier
variable in the outer scope.
One exception is of course the $this
keyword, which acts exactly the same as normal closures:
array_map(fn($x) => $x * $this->modifier, $numbers);
I already mentioned multi-line short closures, which is still a future possibility. Another idea floating around is allowing the short closure syntax in classes, for example for getters and setters:
class Post {
private $title;
fn getTitle() => $this->title;
}
All in all, short closures are a welcome feature, though there is still room for improvement. The biggest one probably being multi-line short closures.
Do you have any thoughts you'd like to share? Feel free to send a tweet or an email my way!
]]>In this post we'll look at the feature in-depth, but first let's start by summarising the most important points:
public
, protected
or private
; or var
void
and callable
This is what they look like in action:
class Foo
{
public int $a;
public ?string $b = 'foo';
private Foo $prop;
protected static string $static = 'default';
}
If you're unsure about the added benefit of types, I'd recommend you reading this post first.
Before looking at the fun stuff, there's an important aspect about typed properties that's essential to talk about first.
Despite what you might think on first sight, the following code is valid:
class Foo
{
public int $bar;
}
$foo = new Foo;
Even though the value of $bar
isn't an integer after making an object of Foo
, PHP will only throw an error when $bar
is accessed:
var_dump($foo->bar);
Fatal error: Uncaught Error: Typed property Foo::$bar
must not be accessed before initialization
As you can read from the error message, there's a new kind of "variable state": uninitialized.
If $bar
didn't have a type, its value would simply be null
.
Types can be nullable though, so it's not possible to determine whether a typed nullable property was set, or simply forgotten.
That's why "uninitialized" was added.
There are four important things to remember about uninitialized:
unset
on a typed property will make it uninitialized, while unsetting an untyped property will make it null
.Especially note that the following code, where an uninitialised, non-nullable property is set after constructing the object, is valid
class Foo
{
public int $a;
}
$foo = new Foo;
$foo->a = 1;
While uninitialized state is only checked when reading the value of a property, type validation is done when writing to it. This means that you can be sure that no invalid type will ever end up as a property's value.
Let's take a closer look at how typed values can be initialized. In case of scalar types, it's possible to provide a default value:
class Foo
{
public int $bar = 4;
public ?string $baz = null;
public array $list = [1, 2, 3];
}
Note that you can only use null
as a default if the type is actually nullable.
This might seem obvious, but there's some legacy behaviour with parameter defaults where the following is allowed:
function passNull(int $i = null)
{ /* … */ }
passNull(null);
Luckily this confusing behaviour is not allowed with typed properties.
Also note that it's impossible to have default values with object
or class types.
You should use the constructor to set their defaults.
The obvious place to initialize typed values would of course be the constructor:
class Foo
{
private int $a;
public function __construct(int $a)
{
$this->a = $a;
}
}
But also remember what I mentioned before: it's valid to write to an uninitialized property, outside of the constructor. As long as there are nothing is reading from a property, the uninitialized check is not performed.
So what exactly can be typed and how? I already mentioned that typed properties will only work in classes (for now),
and that they need an access modifier or the var
key word in front of them.
As of available types, almost all types can be used, except void
and callable
.
Because void
means the absence of a value, it makes sense that it cannot be used to type a value.
callable
however is a little more nuanced.
See, a "callable" in PHP can be written like so:
$callable = [$this, 'method'];
Say you'd have the following (broken) code:
class Foo
{
public callable $callable;
public function __construct(callable $callable)
{ /* … */ }
}
class Bar
{
public Foo $foo;
public function __construct()
{
$this->foo = new Foo([$this, 'method'])
}
private function method()
{ /* … */ }
}
$bar = new Bar;
($bar->foo->callable)();
In this example, $callable
refers to the private Bar::method
, but is called within the context of Foo
.
Because of this problem, it was decided not to add callable
support.
It's no big deal though, because Closure
is a valid type, which will remember the $this
context where it was constructed.
With that out of the way, here's a list of all available types:
PHP, being the dynamic language we love and hate, will try to coerce or convert types whenever possible. Say you pass a string where you expect an integer, PHP will try and convert that string automatically:
function coerce(int $i)
{ /* … */ }
coerce('1'); // 1
The same principles apply to typed properties. The following code is valid and will convert '1'
to 1
.
class Bar
{
public int $i;
}
$bar = new Bar;
$bar->i = '1'; // 1
If you don't like this behaviour you can disabled it by declaring strict types:
declare(strict_types=1);
$bar = new Bar;
$bar->i = '1'; // 1
Fatal error: Uncaught TypeError:
Typed property Bar::$i must be int, string used
Even though PHP 7.4 introduced improved type variance, typed properties are still invariant. This means that the following is not valid:
class A {}
class B extends A {}
class Foo
{
public A $prop;
}
class Bar extends Foo
{
public B $prop;
}
Fatal error: Type of Bar::$prop must be A (as in class Foo)
If the above example doesn't seem significant, you should take a look at the following:
class Foo
{
public self $prop;
}
class Bar extends Foo
{
public self $prop;
}
PHP will replace self
behind the scenes with the concrete class it refers to, before running the code.
This means that the same error will be thrown in this example.
The only way to handle it, is by doing the following:
class Foo
{
public Foo $prop;
}
class Bar extends Foo
{
public Foo $prop;
}
Speaking of inheritance, you might find it hard to come up with any good use cases to overwrite the types of inherited properties.
While I agree with that sentiment, it's worth noting that it is possible to change the type of an inherited property, but only if the access modifier also changes from private
to protected
or public
.
The following code is valid:
class Foo
{
private int $prop;
}
class Bar extends Foo
{
public string $prop;
}
However, changing a type from nullable to non-nullable or reverse, is not allowed.
class Foo
{
public int $a;
public ?int $b;
}
class Bar extends Foo
{
public ?int $a;
public int $b;
}
Fatal error: Type of Bar::$a must be int (as in class Foo)
Like I said at the start of this post, typed properties are a major addition to PHP. There's lots more to say about them. I'd suggest you reading through the RFC to know all the neat little details.
If you're new to PHP 7.4, you probably want to read the full list of changes made and features added. To be honest, it's one of the best releases in a long time, and worth your time!
Finally, if you have any thoughts you want to share on the topic, I'd love to hear from you! You can reach me via Twitter or e-mail.
Until next time!
]]>Or in other words, dealing with complex database relations and Laravel models.
Recently I had to deal with a complex performance issue in one of our larger Laravel projects. Let me quickly set the scene.
We want an admin user to see an overview of all people in the system in a table, and we want a column in that table to list which contracts are active at that moment for each person.
The relation between Contract
and Person
is as follows:
Contract > HabitantContract > Habitant > Person
I don't want to spend too much time going into details as to how we came to this relationship hierarchy. It's important for you to know that, yes, this hierarchy is important for our use cases: a Contract
can have several Habitants
, which are linked via a pivot model HabitantContract
; and each Habitant
has a relation to one Person
.
Since we're showing an overview of all people, we'd like to do something like this in our controller:
class PeopleController
{
public function index()
{
$people = PersonResource::collection(Person::paginate());
return view('people.index', compact('people'));
}
}
Let's make clear that this is an oversimplified example, though I hope you get the gist. Ideally, we'd want our resource class to look something like this:
/** @mixin \App\Domain\People\Models\Person */
class PersonResource extends JsonResource
{
public function toArray($request): array
{
return [
'name' => $this->name,
'active_contracts' => $this->activeContracts
->map(function (Contract $contract) {
return $contract->contract_number;
})
->implode(', '),
// …
];
}
}
Notice especially the Person::activeContracts
relation. How could we make this work?
A first thought might be by using a HasManyThrough
relation, but remember that we're 4 levels deep in our relation hierarchy. Besides that, I find HasManyThrough
to be very confusing.
We could query the contracts on the fly, one-by-one per person. The issue with that is that we're introducing an n+1 issue since there'll be an extra query per person. Imagine the performance impact if you're dealing with more than just a few models.
One last solution that came to mind was to load all people, all contracts, and map them together manually. In the end that's exactly what I ended up doing, though I did it in the cleanest possible way: using custom relations.
Let's dive in.
Since we want our $person->activeContracts
to work exactly like any other relation, there's little work to be done here: let's add a relation method to our model, just like any other.
class Person extends Model
{
public function activeContracts(): ActiveContractsRelation
{
return new ActiveContractsRelation($this);
}
}
There's nothing more to do here. Of course we're only starting, since we haven't actually implemented ActiveContractsRelation
!
Unfortunately there's no documentation on making your own relation classes. Luckily you don't need much to learn about them: some code-diving skills and a little bit of time gets you pretty far. Oh an IDE also helps.
Looking at the existing relation classes provided by Laravel, we learn that there's one base relation that rules them all: Illuminate\Database\Eloquent\Relations\Relation
. Extending it means you need to implement some abstract methods.
class ActiveContractsRelation extends Relation
{
/**
* Set the base constraints on the relation query.
*
* @return void
*/
public function addConstraints() { /* … */ }
/**
* Set the constraints for an eager load of the relation.
*
* @param array $models
*
* @return void
*/
public function addEagerConstraints(array $models) { /* … */ }
/**
* Initialize the relation on a set of models.
*
* @param array $models
* @param string $relation
*
* @return array
*/
public function initRelation(array $models, $relation) { /* … */ }
/**
* Match the eagerly loaded results to their parents.
*
* @param array $models
* @param \Illuminate\Database\Eloquent\Collection $results
* @param string $relation
*
* @return array
*/
public function match(array $models, Collection $results, $relation) { /* … */ }
/**
* Get the results of the relationship.
*
* @return mixed
*/
public function getResults() { /* … */ }
}
The doc blocks get us on the way, though it's not always entirely clear what needs to happen. Again we're in luck, Laravel still has some existing relation classes where we can look to.
Let's go through building our custom relation class step by step. We'll start by overriding the constructor and adding some type hints to the existing properties. Just to make sure, the type system will prevent us from making stupid mistakes.
The abstract Relation
constructor requires both specifically for an eloquent Builder
class, as well as the parent model the relationship belongs to. The Builder
is meant to be the base query object for our related model, Contract
, in our case.
Since we're building a relation class specifically for our use case, there's no need to make the builder configurable. Here's what the constructor looks like:
class ActiveContractsRelation extends Relation
{
/** @var \App\Domain\Contract\Models\Contract|Illuminate\Database\Eloquent\Builder */
protected $query;
/** @var \App\Domain\People\Models\Person */
protected $parent;
public function __construct(Person $parent)
{
parent::__construct(Contract::query(), $parent);
}
// …
}
Note that we type hint $query
both with the Contract
model as well as the Builder
class. This allows IDEs to provide better autocompletion, such as custom scopes defined on the model class.
We've got our relation constructed: it will query Contract
models, and use a Person
model as its parent. Moving on to building our query.
This is where the addConstraints
method come in. It will be used to configure the base query. It will set up our relation query specifically to our needs. This is the place where most business rules will be contained:
$parent
of our relation)Here's what addConstraints
looks like, for now:
class ActiveContractsRelation extends Relation
{
// …
public function addConstraints()
{
$this->query
->whereActive() // A query scope on our `Contract` model
->join(
'contract_habitants',
'contract_habitants.contract_id',
'=',
'contracts.id'
)
->join(
'habitants',
'habitants.id',
'=',
'contract_habitants.habitant_id'
);
}
}
Now I do assume that you know how basic joins work. Though I will summarize what's happening here: we're building a query that will load all contracts
and their habitants
, via the contract_habitants
pivot table, hence the two joins.
One other constraint is that we only want active contracts to show up; for this we can simply use an existing query scope provided by the Contract
model.
With our base query in place, it's time to add the real magic: supporting eager loads. This is where the performance wins are: instead of doing one query per person to load its contracts, we're doing one query to load all contracts, and link these contracts to the correct people afterwards.
This is what addEagerConstraints
, initRelation
and match
are used for. Let's look at them one by one.
First the addEagerConstraints
method. This one allows us to modify the query to load in all contracts related to a set of people. Remember we only want two queries, and link the results together afterwards.
class ActiveContractsRelation extends Relation
{
// …
public function addEagerConstraints(array $people)
{
$this->query->whereIn(
'habitants.contact_id',
collect($people)->pluck('id')
);
}
}
Since we joined the habitants
table before, this method is fairly easy: we'll only load contracts that belong to the set of people provided.
Next the initRelation
. Again this one is rather easy: its goal is to initialise the empty activeContract
relationship on every Person
model, so that it can be filled afterwards.
class ActiveContractsRelation extends Relation
{
// …
public function initRelation(array $people, $relation)
{
foreach ($people as $person) {
$person->setRelation(
$relation,
$this->related->newCollection()
);
}
return $people;
}
}
Note that the $this->related
property is set by the parent Relation
class and it's a clean model instance of our base query so in other words, an empty Contract
model:
abstract class Relation
{
public function __construct(Builder $query, Model $parent)
{
$this->related = $query->getModel();
// …
}
// …
}
Finally we arrive at the core function that will solve our problem: linking all people and contracts together.
class ActiveContractsRelation extends Relation
{
// …
public function match(array $people, Collection $contracts, $relation)
{
if ($contracts->isEmpty()) {
return $people;
}
foreach ($people as $person) {
$person->setRelation(
$relation,
$contracts->filter(function (Contract $contract) use ($person) {
return $contract->habitants->pluck('person_id')->contains($person->id);
})
);
}
return $people;
}
}
Let's walk through what's happening here: on the one hand we've got an array of parent models, the people; on the other hand we've got a collection of contracts, the result of the query executed by our relation class. The goal of the match
function is to link them together.
How to do this? It's not that difficult: loop over all people, and search all contracts that belong to each one of them, based on the habitants linked to that contract.
Almost done? Well… there's one more issue. Since we're using the $contract->habitants
relation, we need to make sure it is also eagerly loaded, otherwise we just moved the n+1 issue instead of solving it. So it's back to the addEagerConstraints
method for a moment.
class ActiveContractsRelation extends Relation
{
// …
public function addEagerConstraints(array $people)
{
$this->query
->whereIn(
'habitants.contact_id',
collect($people)->pluck('id')
)
->with('habitants')
->select('contracts.*');
}
}
We're adding the with
call to eagerly load all habitants, but also note the specific select
statement. We need to tell Laravel's query builder to only select the data from the contracts
table, because otherwise the related habitant data will be merged on the Contract
model, causing it to have the wrong ids and what not.
Finally we need to implement the getResults
method, which simply executes the query:
class ActiveContractsRelation extends Relation
{
// …
public function getResults()
{
return $this->query->get();
}
}
And that's it! Our custom relation can now be used like any other Laravel relation. It's an elegant solution to solving a complex problem the Laravel way.
]]>- The current model Country has a relation to Post via User
- The intermediate model is linked to the current model via users.country_id
- The target model is linked to the intermediate model via posts.user_id
- users.country_id maps to countries.id
- posts.user_id maps to users.id
countries
id - integer
name - string
users
id - integer
country_id - integer
name - string
posts
id - integer
user_id - integer
title - string
class Country extends Model
{
public function posts()
{
return $this->hasManyThrough(
'App\Post',
'App\User',
'country_id', // Foreign key on users table...
'user_id', // Foreign key on posts table...
'id', // Local key on countries table...
'id' // Local key on users table...
);
}
}
]]>Your translations should be hosted on your own blog. Maybe it's helpful for you to have access to the markdown source files, they can be found here.
You're required to add a link to the original post at the start of your translation. Furthermore, please add a link tag in the of your page to also point to the original post.
<head>
<!-- … -->
<link
rel="alternate"
hreflang="en"
href="https://stitcher.io/blog/can-i-translate-your-blog" />
</head>
I'd like to know about when you put a translation online. I plan on listing all translations on my blog somewhere at some point in the future, so it'd be handy if I could add yours.
You can notify me via Twitter, e-mail or by making a GitHub issue
I expect translations to be as close as possible to the original. You can add your own thoughts, but only if properly addressed. For example you can add a final section with additions to the original post, as long as you make it clear that it's not part of the original.
One final remark: I appreciate it very much when people want to translate my content, so thank you very much!
]]>I regularly get requests from people to write a guest post on this blog, but these requests often involve low-quality content just to boost SEO stats. This is something I want to stay far away from.
I realised though that there might be people out there who have a great idea for a blog post, but don't want to spend time maintaining a blog, and don't want to post to a network like Medium. Quality content might never be written because of this, and I realised that independent blogs like mine can offer a solution.
I think of it as a win-win: content creators get an outlet for their ideas, my audience gets more quality content to read, and this blog can keep on growing.
Regular readers of my blog know that I try my best to only write quality content. I don't keep a schedule which forces me to write every week or month, I only write when I feel like I've got something to say that's worth sharing.
I want guest posts on this blog to be the same. That's why I will personally review every one of them, and will only post them when I think they meet a quality level my audience would appreciate.
I believe a set of rules and agreements is in place. If you're thinking about pitching a guest post, please read these carefully.
Guest posts will always stay your property, not mine. This means that if you ever want to move your content to your own blog, you're free to do so.
This of course also means that you're responsible for writing quality content. I'll be happy to review and help improve your content, but you'll be the one who has to do it.
This also means that guest posts will always be properly attributed. You'll be able to provide links to your social media or personal website.
Lastly, every one of my own blogposts has one ad in it, which helps keep this blog going. You're free to ask me to remove it in your content.
I will help you in reviewing your content and provide feedback to improve it. I'm by no means an expert writer or copy editor myself, but I have been doing this for a few years now, so I might be able to help you if you're a beginning writer.
Reviewing your content will either happen via mail or directly on GitHub, whichever you prefer. I will make sure to always discuss any changes I want to be made to the post, and will never alter your content without you knowing it.
Finally, I will also help promote your content once it's launched, and provide feedback regarding traffic afterwards. I cannot guarantee any hard numbers on what kind of traffic your post will get, but I can give you some numbers on the monthly traffic on my blog:
Since this is new for me too, I've got no idea where it'll go from here on out. If you're interested in the idea and would like to talk about something concrete, feel free to send me an email and we'll see whether we're a good match. Make sure to provide the following information:
Your reply started by addressing the P++ shenanigans:
A lot of the discussion is based on an assertion made during the P++ discussion that there are two camps of developers
As a matter of fact, I started writing my letter before the whole P++ galaxy discussion. Life got in the way though (my son was born at the beginning of August), which is why it took almost a month to publish it.
I just wanted to make clear that I already had many thoughts on the topic before P++ was ever mentioned.
You can tell by looking at the history of RFCs that these factions do not in fact exist
Whether we call it factions or not, if you take a look at recent RFCs, they have almost always gone off topic for to discuss the future of PHP on a broader scale. Sure there might only a few people, but loud voices are heard nevertheless.
A few examples:
This is how most of these discussion go:
The rules were written many years ago - arguably for a totally different, pre social coding world - we mostly do a good job of following the rules as they are written.
I think you addressed the essence of the problem: the way PHP internals work is outdated and inefficient in these modern times.
It's important to point out that the rules are not exhaustive, a lot of how we behave is determined by convention. You can argue against this and say that we should try to exhaustively enumerate every possible action
I'd argue we need a sensible and modern day rule set, which can be flexible.
Recently an RFC was conducted to deprecate and remove short PHP tag syntax
While I do have my opinions on maintaining backwards compatibility — which I addressed in the first part of my letter — I think the most important thing to take away from the short syntax RFC is that the process is clearly broken, and needs fixing.
What we have here is a failing of our processes and nothing more. I and likely others are considering how we might avoid this very same failure in the future. It seems desirable at this time to introduce a formal deprecation policy, this both achieves the goal of avoiding this very same failure, and can potentially increase confidence when it comes to adopting new versions of PHP.
Glad we're on the same page on this one, and I know from your comments on internals that you're also looking for a balanced solution. My question is whether this is possible within the current system. It feels like we're going around in circles and very little progress is made.
First, for the sake of clarity. You must be careful how you determine something to be controversial. Loud, is not the same as controversial
That's true, though a few loud voices can impact the development of PHP significantly. There aren't many core contributors, and they have to spend a lot of time reading and replying through the same discussions. I try to keep up-to-date with the internals list myself, so I know this is an exhaustive task.
The time and effort it takes to change our processes is considerable, and only becomes a priority when it's obvious that our processes are failing, or have the potential to fail and do damage.
I think PHP is only slowly evolving because there's so much needless discussions happening over and over again, I'd call that a failing process.
I'm sure that you have a sample of data that shows you this, or you surely wouldn't have made this claim.
Yes, I linked some recent examples previously.
It's a matter of fact that some people can't seem to behave themselves on the internet, while I'm sure (read: must believe) they are reasonable people in real life. These people make themselves obvious very quickly and prove they have nothing much to say.
I think some kind of moderation would be in place, as these people keep coming back, and there's no way to stop them. This is where a mailing list just doesn't suffice.
I can't argue that mailing lists are a good way to communicate, but it's what we have. However, it's not all we have:
True, though the channels you list still don't seem to bridge the gap between core- and userland developers. This is evident by the amount of userland developers voicing their opinions on all kinds of social media. I think a public forum is the way to go here.
This is regularly mentioned, and I think I'll be the first one to point out that to the extent which that is true, it is the communities fault.
There are two groups here:
The wiki isn't very clear on this:
People with php.net VCS accounts that have contributed code to PHP
Representatives from the PHP community, that will be chosen by those with php.net VCS accounts Lead developers of PHP based projects (frameworks, cms, tools, etc.) regular participant of internals discussions
Personally, I think of myself in this second group, though it's unclear to me whether my perception is correct. And I can think of several other developers who would be an accurate representation of the PHP community.
Should I ask? As a matter of fact, I did ask a core member personally, but didn't get a reply. Should I ask this question on the internals list? I'm unsure.
This is again an example of failing communication between core- and userland developers. There's a barrier that's perceived as uncrossable.
Now I'm not saying there is an actual barrier, I just say that it's perceived by many userland developers this way, myself included.
Once again, thank you very much for your reply Joe. I highly appreciate it and am looking forward to read your comments on mine.
Kind regards
Brent
Let me start by thanking those who actively work on the PHP project. Those who contribute to the core, extensions, maintain the docs or vote on RFCs: thank you for a language that I can use every day both in my professional and personal life. PHP has been a very useful tool to me for many years, and it's good to see lots of contributors help making it better every day.
I also want to mention that I, as everyone, am subject to confirmation bias. When I address one or two thoughts in this letter, I'll try my best to be as objective as possible, though I realise I'm looking through my lens, and not someone else's.
Since the goal of this letter is to start a conversation, I'm open to hear your thoughts. Also if they don't align with mine, please feel free to disagree.
I could continue by listing lots of good things — there are many. Though because I want to keep this letter on topic, I won't be doing that. Don't take this as me being a disgruntled developer, I simply want to be efficient in conveying what I want to say.
I want to write about how PHP is shaped and developed these days. I feel that I, as a userland developer, know a thing or two about using PHP in real projects. I believe I have an informed and relevant opinion on the matter.
Recently we've seen several discussions regarding the RFC voting process. Besides recent changes to the voting rules, there have also been a few controversial RFCs which passed the vote, and caused some — in some cases, lots of — controversy.
Two recent RFCs come to mind: the deprecation of the short open tags, as well as several small deprecations for PHP 7.4.
Both RFCs caused discussion on whether these changes are actually beneficial to the language, whether they should be allowed with only a 2/3 majority vote, and whether they should be considered harmful to the PHP community.
The basis for most of these discussions is the fact that PHP tries to maintain backwards compatibility as much as possible. One of the main thoughts behind this is that we want users to stay up-to-date with modern PHP versions, so we should give them as little problems as possible to upgrade.
Lessons were, rightfully, learned from the 5.* era. I too share the opinion that all PHP developers and ecosystems should strive to stay up-to-date. It's a message that companies and developers should tell their clients at the start of every project: keeping it secure and up-to-date will take time, cost money, and there's no responsible way to avoid it.
It's a characteristic of professionalism.
On the other hand: if we want to achieve this professionalism with our clients, we are also allowed to spend reasonable amounts of time on upgrades. It is not the end of the world if there's a backwards incompatible change. We can deal with it.
As a day-by-day user of PHP, I have also had my share of legacy projects that needed updating. Let me tell you this: I much more prefer PHP to move forward and mature, rather than me spending less time on upgrades.
In a maturing language, it's evident that some old legacy stuff is cleaned up. It means that the language sometimes removes two ways to do the same thing. It means that, for example, short open tags are deprecated and removed. It means that sometimes my code will break. And as long as the language evolves in a good and healthy way, I don't mind.
If you're one of the enthusiastic guards of backwards compatibility: I know you mean well. But I don't think it's that big a deal you make out of it. The world will not end because there's a breaking change. We, userland developers, will manage.
Let's not waste too much time with seemingly endless discussion over and over again. Let's move forward in balanced way.
Speaking of how we spend time. Internals have been discussing voting mechanics and what to do with controversial RFCs for months now.
Shouldn't we start looking at how other communities do this? For sure PHP can't be the only open source language out there?
Let's call the current way of PHP's development for what it really is: the same discussions happen over and over again on a weekly or monthly basis without any progress; people are personally attacking others regularly; an insignificant RFC takes months of discussion and requires a re-vote after being accepted; there aren't any good ways to share constructive feedback apart from the big mailing list; the group of voters doesn't seem to be an accurate representation the actual PHP community.
Am I fair to call this system, at least partially, broken?
I believe our system should be thoroughly evaluated, and I think we should look at how open source communities outside of PHP manage to keep moving their project forward in a healthy way.
One example is TC39, the committee that manages ECMAScript, aka JavaScript. Dr. Axel Rauschmayer wrote a great post about how the TC39 process works. Now, you might love or hate JavaScript, but it's clear that they are doing something right over there, given the lasting success of the language over the years.
One of the things they get right is an open communication channel with their community. Communication that is transparent and accessible between contributors and users via GitHub. Another language that does this is Rust, which provides an open forum to discuss how the language is shaped.
An open place like GitHub or a forum diminishes the barrier most userland developers experience with the internals mailing list. Many of us read it, though very little userland developers actually feel they can voice their opinion on it. I think there's two reasons for this:
Better communication will close the current disconnect between the two groups, it will allow PHP to become what the actual majority of PHP users want it to be.
Besides communication, there's the matter of what features should be added to the language. TC39 provides a clear framework on how the language can evolve; it's a system that's superior to and less confusing than PHP's current RFC process.
I already mentioned that the RFC process has been an on-and-off hot debate for the past months; it's at the point where many RFC proposals on the mailing list spark the same discussion over and over again, with no results. Let's again look at committees like TC39, and fix it once and for all.
There's more things to do to fix the current broken process of PHP's development, but I can't possibly list everything here today. So I think it'd be good to keep the conversation going. My suggestion would be to go over to Reddit where we can discuss it further, or send me an email.
Kind regards
Brent
Update August 29: Joe Watkins was kind enough to write a reply. You can read it here.
You can read my reply to Joe's here.
]]>While every modern framework ships with a dependency container — a big box that knows how to construct objects for you — it doesn't guarantee you'll actually be using the dependency injection pattern the way it's supposed to be.
The container can make it much easier to have dependencies injected into a class, but it can also be abused quite a lot.
One way to (ab)use the container is to pull objects from it, instead of having them injected into the current context. This pattern is called "service location", and is the opposite of dependency injection. It looks like this:
class MyController
{
public function indexAction()
{
$service = app(Service::class);
// …
}
}
Service location will ask the container for a specific object. This makes the context you're pulling this service from a difficult point to test, as well as a black box to the outside: you're unable to know what kind of external dependencies MyController
uses, without looking at all of the code.
Some frameworks promote this use of the container, because it can be simple and fast at the start of a project. In projects with hundreds, maybe even thousands of classes registered in the container, the use of service location can and will become a mess; one that proper use of dependency injection would solve.
I also recommend you to read my post on why service location is an anti-pattern.
Moving on to some more positive vibes: making use of the container in a good way.
When dependency injection is properly used, the outside context — in many cases the container — has control over the concrete dependency that it's injecting into a class. This means that the same object can be injected into several other contexts, without those contexts having to know anything about them being "singletons" or "shared dependencies".
Even though sharing dependencies can be a good and powerful thing to do, it is still not what dependency injection is about, but rather a beneficial side effect.
Finally, another useful feature that, again, isn't what dependency injection is about: autowiring.
To give developers more flexibility, some containers allow for smart, automatically determined, class definitions. This means you don't have to manually describe how every class should be constructed. These containers will scan your code, and determine which dependencies are needed by looking at type hints and doc blocks.
A lot of magic happens here, but auto wiring can be a useful tool for rapid application development.
If by now, you want a refresher on the basics of what dependency injection is about. You can go read up on it here.
]]>rgbToHex
.
It takes three arguments, integers between 0 and 255; and converts it to a hexadecimal string.
Here's what this function's definition might look like in a dynamic, weakly typed language:
rgbToHex(red, green, blue) {
// …
}
I think we all agree that "program correctness" is essential. We don't want any bugs, so we write tests.
assert(rgbToHex(0, 0, 0) == '000000')
assert(rgbToHex(255, 255, 255) == 'ffffff')
assert(rgbToHex(238, 66, 244) == 'ee42f4')
Because of our tests, we can be sure our implementation works as expected. Right?
Well… We're actually only testing three out of the 16,777,216 possible colour combinations. But human reasoning tells us that if these three cases work, all probably do.
What happens though if we pass doubles instead of integers?
rgbToHex(1.5, 20.2, 100.1)
Or numbers outside of the allowed range?
rgbToHex(-504, 305, -59)
What about null
?
rgbToHex(null, null, null)
Or strings?
rgbToHex("red", "green", "blue")
Or the wrong amount of arguments?
rgbToHex()
rgbToHex(1, 2)
rgbToHex(1, 2, 3, 4)
Or a combination of the above?
I can easily think of five edge-cases we need to test, before there's relative certainty our program does what it needs to do. That's at least eight tests we need to write — and I'm sure you can come up with a few others given the time.
These are the kind of problems a type system aims to partially solve. And note that word partially, we'll come back to it.
If we filter input by a type — you can think of it as a subcategory of all available input — many of the tests become obsolete.
Say we'd only allow integers:
rgbToHex(Int red, Int green, Int blue)
{
// …
}
Let's take a look at the tests that aren't necessary anymore thanks to the Int
type:
To be honest, we can do better than this: we still need to check whether the input number is between 0 and 255.
Unfortunately at this point, we run against the limitations of many type systems.
Sure we can use Int
, though in many cases (as with ours)
the category described by this type is still too large for our business logic.
Some languages have a UInt
or "unsigned integer" type;
yet this still too large a subset of "numeric data".
Luckily, there are ways to address this issue.
One approach could be to use "configurable" or generic types, for example Int
.
The concept of generics is known in many programming languages,
though I'm unaware of any language that let's you configure scalar types such as integers.
Edit: one of my readers let me know this is possible in Ada. Thanks, Adam!
Nevertheless in theory, a type could be preconfigured in such a way that it's smart enough to know about your business logic.
Languages that lack these kinds of generic types, often need to build custom types. Being an OO programmer myself, I would use classes to do this.
class MinMaxInt
{
public MinMaxInt(Int min, Int max, Int value)
{
assert(min <= value <= max)
this.value = value
}
}
If we're using an instance of MinMaxInt
, we can be sure its value is constrained within a subset of integers.
Still, this MinMaxInt
class is too generic for our case.
If we were to type rgbToHex
with it, we're still not sure what the exact boundaries are:
rgbToHex(MinMaxInt red, MinMaxInt green, MinMaxInt blue)
{
// …
}
We need a more specific type: RgbValue
.
Adding it depends, again, on the programming language and personal preference.
I would extend MinMaxInt
, but feel free to do whatever fits you best.
class RgbValue extends MinMaxInt
{
public RgbValue(Int value)
{
parent(0, 255, value)
}
}
Now we've arrived at a working solution.
By using the RgbValue
type, most of our tests become redundant.
rgbToHex(RgbValue red, RgbValue green, RgbValue blue)
{
// …
}
We can now have one test to test the business logic: "given three RGB-valid colors, does this function return the correct HEX value?" — a great improvement!
Close readers can already think of one or two counter arguments. Let's address them.
If we're building custom types, we still have to test those. That's true in my example, which is influenced by the languages I work in.
It depends on the capabilities of the language though. Given a language that allows this:
rgbToHex(
Int<0, 255> red,
Int<0, 255> green,
Int<0, 255> blue
) {
// …
}
You'd need zero extra tests, as the features are baked into the language itself.
But even if we're stuck with having to build custom types and testing them: don't forget they are reusable throughout the code base.
Chances are you'll be able to re-use most of the types you're making; as these custom categories most likely apply to your business, and are used throughout it.
Next, many would consider my solution too verbose when actually using it:
rgbToHex(
new RgbValue(60),
new RgbValue(102),
new RgbValue(79)
);
While I personally don't mind this verbosity — I know the benefits of a stronger type system — I'd like to ask you to think out of your box for a moment. This argument isn't against stronger types, it's one against your programming language.
The verbosity is caused by the lack of proper syntax provided by the language. Fortunately I can think of ways the problem could be solved.
One solution is type juggling.
Dynamic languages are actually pretty good at it.
Say you'd pass a simple integer as the input,
the compiler can try and cast that integer to an object of RgbValue
.
It could even be aware of possible types which could be cast to RgbValue
,
so you'd still have compile-time error detection.
Another objection might be that your real-life code base obviously differs from a simple rgbToHex
function.
I want to argue the opposite though: the reasoning behind this example can be applied to any part of your code. The actual difficulty lies in the languages and frameworks used: if strong types aren't built in from the ground up, you're gonna have a hard time getting the most use out of them.
This is where I should recommend you to watch this talk by Gary Bernhardt, it's less than 30 minutes long. In it, he takes the topic of type systems and confronts us with our own prejudices and ideologies about them.
Afterwards you can apply this thinking on the current frameworks and languages you're using.
While my example is an example in isolation, the underlying problems solved by stronger types can easily be scaled, if the infrastructure supports it.
So am I suggesting you should ditch your whole stack, or that you're a bad programmer for using a weakly typed language? Definitely not!
I myself program in PHP every day, it's not as bad as it used to be. PHP introduced an opt-in type system, so it's possible to write fairly strongly typed code, even though the language wasn't originally built for it. Another example coming to mind is JavaScript with TypeScript.
So it is possible to leverage a type system, even in many languages that weren't originally built for it. But it will require a mind shift from your side. In my experience, it's worth the effort.
Finally, let's address the elephant in the room when it comes to type systems. I hope it's clear that, while many tests may be omitted thanks to a strong type system, some still need to be written.
People claiming you don't have to write tests in strongly typed languages are wrong.
Remember the partially I mentioned earlier?
In an ideal world, the perfect type system would be able to account for all specific categories required by your business. This is impossible to do though, as computers and programming languages only have limited resources to work with.
So while strong types can help us to ensure program correctness, some tests will always be a necessity to ensure business correctness. It's a matter of "both and", not "either or".
I mentioned several concepts in this posts, but I also mentioned I didn't know of programming languages using some of the concepts I described. I'd love to give some concrete examples though.
So if you're working in a language that should be mentioned in this post, please let me know via Twitter, e-mail, or wherever you read this post on social media.
You can of course also reach out to share other thoughts on this topic, I'd love to hear from you!
]]>You can download the episode here or listen on Apple Podcasts, Stitcher Radio and Spotify
Let's start with a general overview. The project, a web application, features an admin interface to manage inventories, contacts and contracts; bookings, automatic invoicing and about ten third party integrations.
In the future we'll be exposing several of these features to the outside via an API, its main goal to power a mobile app for clients of the platform. The admin panel is already in use in production.
The project is built in Laravel, a PHP framework. We use Blade as a templating engine in combination with many VueJS components. Tailwind is the CSS framework used.
So, how much code have we written the past year? Here's a summary, gathered with the phploc package:
Let's zoom into statistics about the backend code, my area:
The amount of lines split per file type looks like this:
Let's further dive into how the backend code is structured, by using Stefan's laravel-stats package.
To start with, I should explain something about our big Laravel projects. Instead of using the default Laravel project structure, our code is split into two namespaces: "application code" and "domain code".
Domain code holds all business logic and is used by the application layer. If you want to dive further into this topic, you can read about it here.
The following graph shows how application and domain code relate to each other:
By splitting business and application code, we're able to provide a flexible, maintainable and highly testable core. Application code makes use of this core and looks very much like your average Laravel project.
The bulk of our domain code consists of three types of classes:
While the application layer mostly consists of:
Because of the lifecycle of the project, there's room for improvement. For example, we're not using DTOs everywhere, as they were added at a later time.
As with all things: we learn as we go. After a year, it's normal that some parts of the code can be considered "old". We have a rule that states that when we work in these old parts of the codebase, we refactor them.
A big advantage of moving code into domains is testability. While our domain code is heavily unit tested, our application code is mostly only integration tested. In our experience, it's a workable balance between highly tested code and being able to meet deadlines.
At the moment we have 840 tests doing 1,728 assertions. Our test suite could always be improved, but I am very confident deploying new features and refactors without the fear of breaking stuff — thanks to our test suite.
I'm a big proponent of clean code. We try to keep our code clean and maintainable, by setting a few rules of thumb:
You probably noticed that we don't always keep these rules. There are some classes that are longer and more complex.
These classes are the result of making choices: sometimes some technical debt is allowed to meet deadlines — as long as we're aware of it.
I've made a little tool in the past which I use to generate "heat maps" of the codebase. It will take all code in a folder, and generate an image by overlaying the code structure on top of it.
I can use this tool to locate large files, and refactor them when there's time. We have done this in the past, and it works very well.
Here's part of this image of a subdomain in our project:
The darker the image, the more code across all files in that position. You can see that while some files are longer, most of the code lives in the upper 50 lines, something we strive for.
We ensure these short classes and consistent code by using a few tools and methods:
Like I said before: I'm a firm proponent of clean code. When you're working with several people in the same codebase, it's a must to keep your code clean and clear, to secure its future.
Finally, I'd like to show the GIT history of the project visualised with Gource. We've been working on this project with, in total, 7 contributors, and now have more than 4,000 commits listed.
You can clearly see the different "branches" I talked about earlier: application- and domain code; but this overview also includes Blade, JavaScript and CSS files.
So what about your projects? Are you able to share your own stats? Feel free to send me a tweet or an email!
]]>Whether it's your own code or that of others, when you open a file you have to take it all in. You need to wrap your head around what's going on, before you're able to write your code, do your thing.
Having to deal with code almost every day, it's important to find ways to make this process easy. To try and reduce the cognitive load it puts on your brain as much as possible.
Making code easier to read will allow you to work faster and better, and also improve your mental state and mood.
In cognitive psychology, cognitive load refers to the total amount of mental effort being used in the working memory - wikipedia
Today I want to share some techniques that can help you reduce cognitive load while coding. In contrast to some recent advocates of "visual debt", I won't talk about stripping away pieces of your codebase. We'll look purely into the visual aspect: what makes code hard to read and reason about, and how to make it easier.
Fonts have an influence on our mood. The people at Crew wrote an interesting piece about how fonts make us feel. The font you choose has a big impact on how much load is put on your brain to process the text on your screen. It's not just the font by the way, but also its size, line height and letter spacing plays a role.
Typography itself is a topic books are written about. I encourage you to think about your current font choice, and how it influences the way you read code.
Here's a comparison between a not-so-good and better font configuration, in my experience.
Ever worked with a controller providing some CRUD actions? A class with a few methods? Folding your method bodies by default gives you a much clearer overview of the class when opening a file. It makes it easier to decide where you want to go to, instead of scrolling and searching. Take a look at the following example.
IDEs like IntelliJ can fold code by default: Settings > Editor > General > Code Folding
.
I was a bit hesitant to enable it by default, but I can assure you this is an amazing feature once you're used to it.
It's also more convenient compared to the file structure navigator many IDEs and editors provide. This approach allows you to see the visual structure, color and indentation of the class.
You'll probably want to learn the keybinds associated with folding too. On Mac with IntelliJ, these are the defaults: ⌘⇧+
, ⌘⇧-
, ⌘+
and ⌘-
.
Documentation and comments are good tool to clarify what code actually does. Furthermore, some languages and IDEs rely on comment meta data to provide proper static analysis. We shouldn't overdo it though: docblocks and comments often state the obvious things, which are already known by reading the code.
After several years of programming, I can safely say that about 80-90% of comments are redundant. They should be removed to clear visual overload, but you also need to provide something in their place:
Self documented code is better than relying on comments. My rule of thumb when adding a comment is asking the following question: "Does this comment actually add more information than already available through the code?". If the answer is no, the comment shouldn't be there.
Removing these comment frees up your code, giving you visual "space to breath".
Another important thing to keep in mind: how to name things? It's better to give a variable a longer, descriptive name; rather than make them as short as possible.
Short, cryptic names make sense at the moment of writing the code; but even a few days after you've written them, they already become vague and meaningless.
Better to write a little more, than to read ten times as much to understand what you've written in the past.
Here are a few examples from an project of mine:
createPage()
to createPaginatedPage()
.$process
became $pageRenderProcess
.testStitcher()
changed to multiple methods, one of which called test_stitch_multiple_routes()
.This might be a sensitive topic for many people; though I want to ask you to give me the benefit of the doubt for a minute. We all like our dark, cool looking colour schemes, but there's a problem with them.
Research shows that the human eye is better equipped to read dark text on light backgrounds, than the other way around. Back in the 80's, when personal computing was growing in popularity, a guy called Etienne Grandjean did an extensive study on how text is best read on screens.
It turned out that light colour schemes are, for almost all people, the better choice.
Now your first thought might be that light colour schemes actually hurt your eyes, but this has more to do with the brightness of your screen, than the colour scheme itself.
It's true that a light colour scheme can cause headaches and painful eyes if you don't turn down the brightness of your screen. On the other hand, a less bright screen in combination with a light colour scheme will put less load on your eyes, which makes reading code for long periods of time less exhaustive.
I can say what I want of course, but the best thing I could do is challenge you to a one-week tryout. I've challenged many programmers before, and most of them were actually convinced of a light theme after a few days.
It's up to you! Here's how my code looks like these days:
Be sure to let me know how the challenge went! You can reach me on Twitter or via e-mail.
The points I listed today have almost nothing to do with how you write real code (programming logic, patterns used, etc.). But they have an impact on the cognitive load put on your brain day by day.
They take away some of the pain points when writing code. They allow you to enjoy programming more.
]]>People who know me IRL know I listen to lots of podcasts every day. I did some internet radio a few years ago, and since then always wanted to pick it up again.
Ever since my wife and I bought our house, I've slowly been working on my amateur "studio", and I've come to the point where I feel comfortable recording in it. Here's my view when recording:
Anyway, I just wanted to let you know that I'm starting this thing. Just like the newsletter it'll be sporadic. I prefer quality over quantity, so don't expect anything weekly.
I called it "Rant With Brent", and if you'd ask me to summarize, I'd say it's a no-nonsense, straight to the point podcast about programming. I don't want to waste 30 minutes of your time, when the same could be said in 10.
I do my best to make an episode highly informative and worth your time. If you want to give it a chance, you can find the podcast on iTunes, Stitcher and Spotify.
]]>Luckily for me I was able to switch jobs shortly thereafter and, more importantly, PHP managed to evolve quite a bit since the 5.* days. Today I'm addressing the people who are either not programming in PHP anymore, or are stuck in legacy projects.
Spoiler: some things still suck today, just like almost every programming language has its quirks. Many core functions still have their inconsistent method signatures, there are still confusing configuration settings, there are still many developers out there writing crappy code — because they have to, or because they don't know better.
Today I want to look at the bright side: let's focus on the things that have changed and ways to write clean and maintainable PHP code. I want to ask you to set aside any prejudice for just a few minutes.
Afterwards you're free to think exactly the same about PHP as you did before. Though chances are you will be surprised by some of the improvements made to PHP in the last few years.
Update: people asked me to show some actual code. I'm happy to say that's possible! Here's the source code of one of my hobby projects, written in PHP and Laravel; and here is a list of a few hundred OSS packages we maintain at our office. Both are good examples of what modern PHP projects look like.
Let's start.
For good measure, let's quickly review PHP's release cycle today. We're at PHP 7.4 now, and PHP 8.0 will be the next version after that.
Ever since the late 5.* era, the core team tries to keep a yearly release cycle, and have succeeded in doing so for the past four years.
In general, every new release is actively supported for two years, and gets one more year of "security fixes only". The goal is to motivate PHP developers to stay up-to-date as much as possible: small upgrades every year are way more easy than making the jump between 5.4 to 7.0, for example.
An active overview of PHP's timeline can be found here.
Lastly, PHP 5.6 was the latest 5.* release, with 7.0 being the next one. If you want to know what happened to PHP 6, you can listen to the PHP Roundtable podcast.
With that out of the way, let's debunk some common misconceptions about modern PHP.
Back in the 5.* days, PHP's performance was… average at best. With 7.0 though, large parts of PHP's core were rewritten from the ground up, resulting in two or three times performance increases.
Words don't suffice though. Let's look at benchmarks. Luckily other people have spent lots of time in benchmarking PHP performance. I find that Kinsta has a good updated list.
Ever since the 7.0 upgrade, performance only increased. So much that PHP web applications have comparable — in some cases better — performance than web frameworks in other languages. Take a look at this extensive benchmark suite.
Sure PHP frameworks won't outperform C and Rust, but they are equally performant as Django, Rails or Express.
Speaking of frameworks: PHP isn't just WordPress anymore. Let me tell you something as a professional PHP developer: WordPress isn't in any way representative of the contemporary ecosystem.
In general there are two major web application frameworks, and a few smaller ones: Symfony and Laravel. Sure there's also Zend, now called Laminas; Yii, Cake, Code Igniter etc. — but if you want to know what modern PHP development looks like, you're good with one of the first two.
Both frameworks have a large ecosystem of packages and products. Ranging from admin panels and CRMs to standalone packages, CI to profilers, numerous services like web sockets servers, queuing managers, payment integrations; honestly there's too much to list.
These frameworks are meant for actual development though. If you're in need of pure content management, platforms like WordPress and CraftCMS are only improving more and more.
One way to measure the current state of PHP's ecosystem is to look at Packagist, the main package repository for PHP. It has seen exponential growth. With ±25 million downloads a day, it's fair to say that the PHP ecosystem isn't the small underdog it used to be.
Take a look at this graph, listing the amount of packages and versions over time. It can also be found on the Packagist website.
Besides application frameworks and CMSs, we've also seen the rise of asynchronous frameworks the past years. These are frameworks and servers, written in PHP or other languages, that allow users to run truly asynchronous PHP. A few examples include Swoole, Amp and ReactPHP.
Since we've ventured into the async world, stuff like web sockets and applications with lots of IO have become actually relevant in the PHP world.
There has also been talk on the internals mailing list — the place where core developers discuss the development of the language — to add libuv to the core. For those unaware of libuv: it's the same library Node.js uses to allow all its asynchronicity.
While async
and await
are not available yet, lots of improvements to the language itself have been made over the past years.
Here's a non-exhaustive list of new features in PHP:
While we're on the topic of language features, let's also talk about the process of how the language is developed today. There's an active core team of volunteers who move the language forward, though the community is allowed to propose RFCs.
Next, these RFCs are discussed on the "internals" mailing list, which can also be read online. Before a new language feature is added, there must be a vote. Only RFC with at least a 2/3 majority are allowed in the core.
Recently there has been an effort to move RFC discussions to GitHub, where the code actually lives. It seems like the small group of developers who's actively invested in PHP, are on board. You can read about the process here. It's still a work in progress because some people don't like GitHub. It's clear though that the most active contributors are on board with the decision, so it's fair to say the RFC discussion process will be moved to GitHub entirely within the next year or so.
Once an RFC is discussed, it goes into the voting phase. There are probably around 100 people allowed to vote, though you're not required to vote on each RFC. Members of the core team are of course allowed to vote, they have to maintain the code base. Besides them, there's a group of people who have been individually picked from the PHP community. These people include maintainers of the PHP docs, contributors to the PHP project as a whole, and prominent developers in the PHP community.
While most of core development is done on a voluntary basis, one of the core PHP developers, Nikita Popov, has recently been employed by JetBrains to work on the language full time. Another example is the Linux foundation who recently decided to invest into Zend framework, now called Laminas. Employments and acquisitions like these ensure stability for the future development of PHP.
Besides lots of features and improvements added to PHP's core, we've seen an increase in tools around it the past few years. What comes to mind are static analysers like Psalm, created by Vimeo; Phan and PHPStan.
These tools will statically analyse your PHP code and report any type errors, possible bugs etc. In some way, the functionality they provide can be compared to TypeScript, though for now the language isn't transpiled, so no custom syntax is allowed.
Even though that means we need to rely on docblocks, Rasmus Lerdorf, the original creator of PHP, did mention the idea of adding a static analysis engine to the core. While there would be lots of potential, it is a huge undertaking.
Speaking of transpiling, and inspired by the JavaScript community; there have been efforts to extend PHPs syntax in user land. A project called Pre does exactly that: allow new PHP syntax which is transpiled to normal PHP code. While the idea has proven itself in the JavaScript world, it could only work in PHP if proper IDE- and static analysis support was provided. It's a very interesting idea, but has to grow before being able to call it "mainstream".
All of that being said, feel free to still think of PHP as a crappy language. While the language definitely has its drawbacks and 20 years of legacy to carry with it; I can say in confidence that I enjoy working with it.
In my experience, I'm able to create reliable, maintainable and quality software. The clients I work for are happy with the end result, as am I. While it's still possible to do lots of messed up things with PHP, I'd say it's a great choice for web development if used wise and correct.
Don't you agree? Let me know why! You can reach me via Twitter or e-mail.
]]>In this episode I talk about why I think final helps you write better maintainable code.
You can download the episode here or listen in iTunes, Stitcher and Spotify
What to expect if you subscribe? First of all I'll try to send out one newsletter every month, more or less. I won't spam you every day or week. This also gives me ample time to write, review and rewrite the mail. That's the same way I write my blog posts, and works very well for me.
My goal is to write a mini-blog post for every mail, about a topic I've encountered the past month. Next I'll also give some useful php-related tips and tricks. Finally I'll share one or two, maybe three; links to interesting reads on the web.
That's what I envision the newsletter to look like, at least for now.
So, if you're interested, here's the signup link: https://stitcher.io/signup. The first edition will be sent sometime in the next two weeks.
]]>The past few days it became clear that there is little community knowledge about these unsafe functions. Many developers assume, as did I, that the Laravel query builder completely prevents SQL injection attacks.
This blog post aims to raise awareness about what's safe, and what's not.
Let's start by mentioning that this vulnerability has been fixed as of Laravel 5.8.11. While technically we could call this a "vulnerability", Laravel developers should know that they also play a role in preventing these kinds of issues.
Let's examine the issue.
Laravel has the ability to manually specify which columns to select on a query. It also offers the shorthand notation to query JSON data:
<hljs type>Blog</hljs>::<hljs prop>query</hljs>()
-><hljs prop>addSelect</hljs>('<hljs green>title</hljs>-><hljs blue>en</hljs>');
<hljs keyword>SELECT</hljs> <hljs prop>json_extract</hljs>(`<hljs green>title</hljs>`, '$."<hljs blue>en</hljs>"') <hljs keyword>FROM</hljs> blogs;
Instead of manually writing json_extract
, we can use the simplified ->
syntax,
which Laravel will convert to the correct SQL statement.
Be careful though: Laravel won't do any escaping during this conversion. Consider the following example:
<hljs type>Blog</hljs>::<hljs prop>query</hljs>()
-><hljs prop>addSelect</hljs>('<hljs green>title</hljs>-><hljs blue>en</hljs><hljs red>'#</hljs>');
By inserting '#
in our input, we can manually close the json_extract
function,
and ignore the rest of the query:
<hljs keyword>SELECT</hljs> <hljs prop>json_extract</hljs>(`<hljs green>title</hljs>`, '$."<hljs blue>en</hljs><hljs red>'#</hljs><hljs textgrey>"') FROM blogs;</hljs>
This query will fail because of syntax errors, but what about the next one?
<hljs keyword>SELECT</hljs> <hljs prop>json_extract</hljs>(
`<hljs green>title</hljs>`,
'$."<hljs blue>en</hljs><hljs red>"'))
FROM blogs RIGHT OUTER JOIN users ON users.id <> null
#</hljs>
<hljs textgrey>"') FROM blogs;</hljs>
We're adding an outer join on the users
table.
Essentially selecting all data in it.
For reference, this is the URL encoded version of the malicious code:
%22%27%29%29+FROM+blogs+RIGHT+OUTER+JOIN+users+ON+users.id+%3C%3E+null%23
Say we have the following endpoint in our application, to query blog posts from a public API:
Route::get('/posts', function (Request $request) {
$fields = $request->get('fields', []);
$users = Blog::query()->addSelect($fields)->get();
return response()->json($users);
});
Consumers of this API might only be interested in a few fields,
that's why we added a fields
filter.
Something similar to sparse fieldsets from the JSON api spec.
The endpoint can now be used like this:
/blog?fields[]=url&fields[]=title
Now we insert our malicious code instead:
/blog?fields[]=%22%27%29%29+FROM+blogs+RIGHT+OUTER+JOIN+users+ON+users.id+%3C%3E+null%23
It will be added to the query. And by returning the query result as JSON, we'll see the full contents of the users table.
Blog::query()->addSelect([
'%22%27%29%29+FROM+blogs+RIGHT+OUTER+JOIN+users+ON+users.id+%3C%3E+null%23'
])->get();
Two things need to be in place for this attack to be possible:
select
or addSelect
.
Chances are you're not doing this manually in your project.
Though there are popular packages which provide this functionality for easy API endpoints and URL filtering.json_extract
function will fail, stopping our query.
From the entry point though, you can access all data.As mentioned before, this particular vulnerability has been fixed as of Laravel 5.8.11. It's always good to keep up to date with the latest Laravel version.
More importantly though, developers should never allow user input directly to specify columns, without a whitelist. In our previous example, you could prevent this attack by only allowing certain fields to be requested, this would prevent the issue completely.
Next, one of our widely-used packages, spatie/laravel-querybuilder
,
opened up addSelect
by design.
This meant that websites using our package, were vulnerable to the underlying issue.
We immediately fixed it and Freek wrote about it in depth.
If you're using our package and unable to update to the latest Laravel version,
you should immediately update the package.
Finally, the Laravel docs have also been updated to warn developers not to pass user input directly to columns when using the query builder.
]]>In PHP, list
or []
is a so called "language construct", just like array()
.
This language construct is used to "pull" variables out of an array.
In other words: it will "destructure" the array into separate variables.
Note that the word is "destructure", not "destruction" — that's something different 😉
Here's what that looks like:
$array = [1, 2, 3];
// Using the list syntax:
list($a, $b, $c) = $array;
// Or the shorthand syntax:
[$a, $b, $c] = $array;
// $a = 1
// $b = 2
// $c = 3
Whether you prefer list
or its shorthand []
is up to you.
People might argue that []
is ambiguous with the shorthand array syntax,
and therefor prefer list
.
I'll be using the shorthand version in code samples though.
So what more can list
do?
Say you only need the third element of an array, the first two can be skipped by simply not providing a variable.
[, , $c] = $array;
// $c = 3
Also note that list
will always start at index 0.
Take for example the following array:
$array = [
1 => 'a',
2 => 'b',
3 => 'c',
];
The first variable pulled out with list
would be null
,
because there's no element with index 0
.
This might seem like a shortcoming, but luckily there are more possibilities.
PHP 7.1 allows list
to be used with arrays that have non-numerical keys.
This opens a world of possibilities.
$array = [
'a' => 1,
'b' => 2,
'c' => 3,
];
['c' => $c, 'a' => $a] = $array;
As you can see, you can change the order however you want, and also skip elements entirely.
One of the uses of list
are functions like parse_url
and pathinfo
.
Because these functions return an array with named parameters,
we can use list
to pull out the information we'd like:
[
'basename' => $file,
'dirname' => $directory,
] = pathinfo('/users/test/file.png');
As you can see, the variables don't need the same name as the key. Also note that destructuring an array with an unknown key will trigger a notice:
[
'path' => $path,
'query' => $query,
] = parse_url('https://stitcher.io/blog');
// PHP Notice: Undefined index: query
In this case, $query
would be null
.
One last detail: trailing commas are allowed with named destructs, just like you're used to with arrays.
You can also use the list construct in loops:
$array = [
[
'name' => 'a',
'id' => 1
],
[
'name' => 'b',
'id' => 2
],
];
foreach ($array as ['id' => $id, 'name' => $name]) {
// …
}
This could be useful when parsing for example a JSON or CSV file. Be careful though that undefined keys will still trigger a notice.
In summary, there are some pretty good cases in which list
can be of help!
Start by making sure brew is up-to-date:
brew update
Next, upgrade PHP:
brew upgrade php
Check the current version by running php -v
:
php -v
# PHP 7.3.3 (cli) (built: Mar 8 2019 16:42:07) ( NTS )
# Copyright (c) 1997-2018 The PHP Group
# Zend Engine v3.3.3, Copyright (c) 1998-2018 Zend Technologies
# with Zend OPcache v7.3.3, Copyright (c) 1999-2018, by Zend Technologies
Restart Nginx or Apache:
sudo nginx -s reload
sudo apachectl restart
And make sure that your local web server also uses PHP 7.3 by visiting this script:
# index.php, accessible to your web server
phpinfo(); die();
The version should show 7.3.x
.
Note: if you're using Laravel Valet, please keep on reading, you need some extra steps in order for the web server to properly work.
JIT compilation failed
errorYou might notice this error showing up when running PHP scripts, for example: composer global update
.
PHP Warning: preg_match(): JIT compilation failed
This is due to a PHP 7.3 bug, and can easily be solved by making a change in your PHP ini file.
If you don't know which ini file is used, you can run the following:
php --ini
# Configuration File (php.ini) Path: /usr/local/etc/php/7.3
# Loaded Configuration File: /usr/local/etc/php/7.3/php.ini
# Scan for additional .ini files in: /usr/local/etc/php/7.3/conf.d
# Additional .ini files parsed: /usr/local/etc/php/7.3/conf.d/ext-opcache.ini,
# /usr/local/etc/php/7.3/conf.d/php-memory-limits.ini
Solving the above error can be done by manually disabling the pcre.jit
option in our ini file.
# /usr/local/etc/php/7.3/php.ini
- ;pcre.jit=1
+ pcre.jit=0
You may have heard of Homebrew dropping support for PHP extensions, this should now be done with PECL. I personally use Imagick, Redis and Xdebug.
They can be installed like so:
pecl install imagick
pecl install redis
pecl install xdebug
You can run pecl list
to see which extensions are installed:
pecl list
# Installed packages, channel pecl.php.net:
# =========================================
# Package Version State
# imagick 3.4.3 stable
# redis 4.3.0 stable
# xdebug 2.7.0 stable
You can search for other extensions using pecl search
:
pecl search pdf
# Retrieving data...0%
# ..
# Matched packages, channel pecl.php.net:
# =======================================
# Package Stable/(Latest) Local
# pdflib 4.1.2 (stable) Creating PDF on the fly with the PDFlib library
Make sure to restart your web server after installing new packages:
sudo nginx -s reload
sudo apachectl restart
If you're using Laravel Valet, you should do the following steps to upgrade it:
composer global update
Now run valet install
:
valet install
Note that if you're upgrading Valet from 2.0 to 2.1, your Valet config folder will automatically be moved from
~/.valet
to ~/.config/valet
.
If you have any paths pointing to this folder, you'll have to update them. I, for example, have a custom Nginx config file for one of my local sites. This config file contained absolute paths to the Valet socket. These had to be manually changed.
If you're running into problems with Nginx, you can check out the errors in the logs:
cat /usr/local/var/log/nginx/error.log
If any changes were made to your Valet config, you should restart it:
valet restart
Finally you should test and upgrade your projects for PHP 7.3 compatibility.
]]>You can read up on how we structure projects by domains and actions here, and find examples of actions in the code of my aggregate project.
Let's give one example using actions: creating a contract. A contract creation not only saves a model in the database, but also generates a PDF of that contract.
Here's how we'd program this action:
class CreateContractAction
{
public function __construct(
GeneratePdfAction $generatePdfAction
) { /* … */ }
public function execute(
ContractData $contractData
): Contract {
$contract = Contract::createFromDTO($contractData);
$this->generatePdfAction->execute($contract);
return $contract->refresh();
}
}
If you know DDD, actions can be thought of as a command and its handler combined. There are projects where this approach doesn't suffice, but there are also cases where they are very helpful.
We use this pattern a lot, because of the three benefits it offers:
One detail: there are some cases where we want actions to be executed asynchronously.
In the case of our example: we want to create the contract immediately, but we don't want our users to wait until the PDF is generated. This should be done asynchronously.
In the past, we used to wrap actions into jobs. It would look something like this:
class GeneratePdfJob implements ShouldQueue
{
use Dispatchable,
InteractsWithQueue,
Queueable,
SerializesModels;
public function __construct(
Contract $contract
) { /* … */ }
public function handle(
GeneratePdfAction $generatePdfAction
) {
$generatePdfAction
->execute($this->contract);
}
}
Instead of directly calling the action within another action, we dispatch a new job.
class CreateContractAction
{
public function execute(
ContractData $contractData
): Contract {
// …
dispatch(new GeneratePdfJob($contract));
// …
}
}
This works fine, but manually wrapping an action in a job started to be kind of tedious in our larger projects.
That's why we started looking into ways of automating this. And sure thing: we can!
Here's what the GeneratePdfAction
would look like, using our package:
use Spatie\QueueableAction\QueueableAction;
class GeneratePdfAction
{
use QueueableAction;
public function __construct(
Renderer $renderer,
Browsershot $browsershot
) { /* … */ }
public function execute(Pdfable $pdfable): void
{
$html = $this->renderer->render($pdfable);
$this->browsershot
->html($html)
->save($pdfable->getPath());
}
}
By using QueueableAction
, this action can now be executed asynchronously.
Here's how it's used:
class CreateContractAction
{
// …
public function execute(
ContractData $contractData
): Contract {
// …
$this->generatePdfAction
->onQueue()
->execute($contract);
// …
}
}
It's important to note that the above will still have auto completion of the execute
method,
as well as DI support; just like normal actions:
Actions allow for constructor injection, which means you can use actions within actions within actions, and so forth.
Jobs on the other hand get container injection in their handle
method.
This means you cannot compose jobs of of other jobs via the dependency container.
It's obvious why Laravel cannot provide constructor injection in jobs: job-specific data, like our contract, needs to be serialised on order for jobs to be queueable, and the constructor is required to ensure the job has valid data.
By introducing the concept of actions, we're able to separate responsibilities between classes better: jobs are used for data serialisation and executing tasks asynchronously; but they are not concerned with business logic anymore.
If you're concerned with difficult to debug actions when they're queued,
you can put your mind at ease.
ActionJob
classes that are dispatched to for example, Horizon,
have their name changed to the action class they wrap:
The underlying details of making actions queueable are hidden to the developer using them, making it very easy to work with them, even in an asynchronous context.
Like I said: we made this into a small package.
It consists of a simple trait and an ActionJob
.
If you want to give it a try, you can check it out here: spatie/laravel-queueable-action.
Let's make a comparison.
My younger brother is an apprentice wood worker, here's a picture of his shop:
To me this looks like chaos, but my brother manages to find his way around. Even better: he actually makes pretty and useful things.
Granted, he is still an apprentice, but I've been happily surprised by the things he has crafted up until now. I can imagine his skills will improve only more and more over time.
For example, take a look at this display thingy he made:
Or this clock:
He also made me some nice sound absorbing panels for my podcast studio at home:
The same way my brother is crafting things with wood, we are building websites and programs with code. I wonder if we have mastered our toolset like he has.
See, he isn't building all this stuff with his bare hands. He's using the right tools for the job. Have a look at some of the machines he's working with:
Or this one:
I've got no clue what these monstrosities do or how to use them — but my brother does.
He had to learn to work these tools. Not only did he need to know which button does what; he had learn how to apply specific techniques to these machines in order to cut and saw the wood as he wants.
He has a mentor and spends a considerate amount of time learning and improving his tools and techniques. It takes patience: he couldn't just dive in at the beginning, there is a learning curve to creating quality.
What is a programmer's equivalent? As we call ourselves craftsmen and artisans, can I assume we also know our tools inside and out?
Can I assume we dedicate time to properly learn how to work with those tools, how to improve our skillset on a day-by-day basis?
Does it seem like a detail to us? Do we think a simple saw gets us equally far as the big monster machine my brother is using?
Can I assume we know the value of our tools? Do we realise they help us be efficient; they simplify mundane tasks so that we can focus on core problems; they are a safeguard when we tend to make stupid mistakes?
Are those fair assumptions when we call ourselves craftsmen?
All these questions pop into my head when I hear people complain about how learning to use an IDE, setting up your local environment, properly learning version control or learning a framework; takes too much time.
What did you expect? In order to write good code, maintainable code, code you can be proud of; in order to do that, you need to master the right tools.
There are no shortcuts to this, you need to spend time learning.
I want to challenge you to think about how you use your tools. There might be opportunities, areas you can improve on. Be willing to spend time improving your skillset.
It's time well invested.
PS: if you're into wood working, be sure to check out my brother's Instagram!
]]>Heads up! I've done some benchmarks on the JIT in real-life web applications. Be sure to check them out.
Dmitry Stogov recently opened an RFC to add a JIT compiler to PHP. So, what is that about? Does "JIT" mean "instantly better PHP", or is this a more nuanced topic? Today we'll briefly look at what the "JIT" actually does, and more importantly: the difficulties and opportunities it brings to the PHP world.
"JIT" stands for "just in time". You probably know that PHP is an interpreted language: it's not compiled like a C, Java or Rust program. Instead it is translated to machine code — stuff the CPU understands — at runtime.
"JIT" is a technique that will compile parts of the code at runtime, so that the compiled version can be used instead.
Think of it like a "cached version" of the interpreted code, generated at runtime.
How does that work, you ask?
There's a so called "monitor" that will look at the code as it's running. When this monitor detects parts of your code that are re-executed, it will mark those parts as "warm" or "hot", depending on the frequency. These hot parts can be compiled as optimised machine code, and used on the fly instead of the real code.
You can imagine there's a lot more complexity to this topic. If you want to know a little more, you can check out Mozilla's crash course in JIT compilers. For the purpose of this blog post, it's enough to understand that a JIT compiler may improve the performance of your program significantly, but it's a difficult thing to get right.
Zeev, one of the PHP core developers, showed a demo with fractal generation a while back:
Fancy, right? Hold on though…
Let's address the elephpant in the room: PHP is seldom used to generate fractal animations.
Knowing that the JIT compiler tries to identify hot parts of your code, you can guess why it has such an impact on the fractal example: there's a lot of the same calculations happening over and over again. However, since PHP is most often used in a web context, we should also measure the JIT's impact there.
It turns out that, unfortunately, there's a lot less hot code while handling a web request. This doesn't mean the JIT couldn't improve web performance at all, but we also won't see similar improvements like with the fractal example.
Is this a reason to ditch the JIT? Definitely not! There are good arguments to add it, even though it might not have the performance impact we'd hope for.
These are valid arguments in favour of the JIT. Unfortunately though, there are also more arguments against it.
Because the JIT generates machine code, you can imagine it's complex material for a "higher level programmer" to understand.
For example: having machine code as output, it will be harder to debug possible bugs in PHP's JIT compiler. Luckily there are tools to help debugging. But still, it is machine code. Say there is a bug in the JIT compiler, you need a developer who knows how to fix it. Dmitry is the one who did most of the coding up until now, and remember that PHP core development is done on a voluntary basis.
With just a few people being able to maintain such a code base today, the question whether the JIT compiler can be maintained properly seems justified. Of course people can learn how the compiler works. But it is complex material nevertheless. The pull request right now counts around 50k lines of added code. And mind you: this is not your average client-web-application-codebase. This is almost-as-close-as-you-can-get-to-the-CPU-programming.
Again this should not be a reason to ditch the JIT, but the cost of maintenance should be carefully considered. In first place by the ones who have to maintain the code; but also by the userland community, who should also be aware that some bugfixes or version updates might take longer than what we're used to right now.
As of newer versions of the JIT, it now also works Windows and Mac! A big step forward, and definitely worth mentioning.
If right now you're thinking that the JIT offers little short-term benefits for your web applications, you might be right. It's difficult to tell what impact it will have on production applications, before actually using it.
The JIT RFC proposed to enable it in PHP 8, but also to add an experimental version in PHP 7.4. Unfortunately the RFC has passed for PHP 8, but not for 7.4. This means we'll have to wait until PHP 8 before being able to try it out on real projects. You can of course compile PHP 8 from source, if you already want to take a look.
Even though the JIT might not offer any significant short-term improvements, we should remember that it will open a lot of possibilities for PHP to grow, both as a web language and a more generally purposed language. So the question that needs answering: is this possibly bright future worth the investment today?
What do you, the userland programmer think? Let's discuss it on Hacker News. If you want to personally reach out, you can find me on Twitter or via e-mail!
]]>I know how much you want to be able to share your content with an audience, but it's so difficult to do. Twitter is very momentarily; Reddit suffers from spam and angry people; and Facebook is, well, Facebook.
I want a platform where I don't have to feel guilty about sharing my content, and where readers can easily discover new things to read.
So I took one of the oldest blogging concepts and made a community driven platform out of it: RSS.
I realise that starting a project on my own, won't get me far. We live in a day and age where every possible app has been invented at least twice. Trying to do this myself won't get me far.
That's why I decided on two things.
The code is open source. I want to encourage people to contribute, help build the platform they want it to be.
The content is provided by the community, and the platform will always redirect to the source of origin. It will never try to host your content, it's merely a portal.
Content creators can add their RSS feed to the platform. Their posts are synced and tagged according to the content and RSS meta data.
Readers can explore new content daily in their feed. I've decided to not try and do anything fancy while building this feed. Platforms like Facebook and Twitter try to be smart and build a feed based on "your interests". They never succeed.
So the feed is a simple chronological list, which you can filter on tags. That's it. There are no votes, no comments. You're encouraged to go the blogs themselves, and share your thoughts over there.
The goal of the platform is simple: help readers discover new content, and get them to that original content as fast as possible. Meanwhile, content creators get a platform where they are allowed to share their own stuff, and an audience hungry for more.
Even though the basic concept is stable and up and running, there's still lots of things to do. There's improvements to be made to the tagging system, there are some convenience features that need to be added and more.
If you're a web programmer yourself who's interested in open source, feel free to take a look around the project board.
Oh and, of course, here's a link to the platform: aggregate.stitcher.io.
]]>I'm no marketeer, just a simple developer. Today I want to share from my technical experience, how I use traffic data and react to it.
First and foremost, the boring part. It takes less than five minutes to set up, but will improve the correctness of your data: filters.
As you know, Google Analytics works with a tracking code which you can place everywhere. That tracking code can easily be used somewhere else, polluting your data. I'm thinking of local development environments or, more malicious: people who steal your tracking code as a deliberate attack.
Luckily this can easily be prevented with filters. So take five minutes to set up some filters in the admin section:
With filters, you can build a whitelist of traffic you want to include. I usually make a separate view called "Filtered", and keep the "All Web Site Data" view as is, for testing.
Here's an example of a filter I set up, to only include traffic from the "stitcher.io" domain:
You can also filter out local domains, whitelist IP addresses and more. If you want to be really safe, you could whitelist the IP address of your host, in combination with the host name.
With the correct filters set up, it's time to interpret data.
Here's a good question to start with: "should I want to decrease my bounce rate"? The corporate marketeer will of course scream "YES".
I say: it depends. Take for example this blog, or almost any other blog. When I share a post on social media, most visitors come to read that post, and that post only.
It's not that weird when you think about it: don't we all do the same when browsing?
A high bounce rate means that not a lot of visitors click through after their first page visit. In case of a blog, there's nothing wrong with that: many of these people will return the next time you share a link on social media.
Let's take a look at the traffic in November of 2018, on this blog.
That's a rather high bounce rate. Though over the last year, I see a consistent 20-ish percent of returning visitors. These people also add to the bounce rate, though they are the ones who visit this site over and over again, albeit only one page at a time.
You can see how relative this data is, and how you cannot simply say "decrease the bounce rate".
But what can we learn from it though?
If you're a regular reader of my blog, first of all: thanks, you're in the 20% ! Secondly: you know that I place an "up next" link down at the bottom of each post.
In the past, these links were automatically generated and just showed the post before the current one.
When analysing the bounce rate of individual pages though, I noticed that some pages had a way lower bounce rate than others.
Looking at these low bounce pages, they were the pages where I deliberately put another link at the bottom, one to a post of which I thought was a little related to the current one.
So how do you analyse this?
The "Content Drilldown" page is great to analyse per-page statistics.
You can find it under Behavior > Site Content
.
Also note how the "advanced filter" is applied to only show pages with lower bounce rates. We can use this data to learn what we're doing right, and target pages that might need optimisation.
I know most of my traffic comes from links that are shared. I've got some posts that show up high in Google, but more about that later.
The "Referrals" page under Acquisition > All Traffic
is an important one to know where your traffic is coming from.
You can see peaks in traffic during the day, by using the real time overview. I often check the referrals at these moments, to know where traffic is coming from.
I believe that, as an author, it's important to engage with your readers. When content is shared on social media, reactions often show up there; so it's only natural that you reply to them there.
A quick Google search on the blog title and website of the referral, often gets me to the right place in no time.
Now that we know people are visiting our blog, we also want to know whether they are actually reading the content.
Again, the "Content Drilldown" page gets us this data.
By adding a simple regex filter ^\/[a-z\-]+$
, we filter out pages with query parameters.
We're also not interested in pages with very low pageviews.
What we're actually interested in, are the pages with the lowest session duration, to see whether some things can be improved.
Some of these blog posts are very short ones, so nothing strange there. Though it also shows what posts were less interesting to my audience. It's a good metric to know what kind of content I shouldn't focus on.
The "Behavior Flow" chart under the Behavior
menu is one
that helps visualising how visitors browse your site.
This is what it looks like.
As with the bounce rate optimisations, this overview can help identifying pages that encourage people to click through, and pages that don't.
I use this overview in combination with the "Content Drilldown" page, to analyse where people come from, where they go, and whether I can improve my content to help them read what they are actually interested in.
I know that most of my traffic comes from links that are shared over the internet, though I also want to know how much comes through search sites like Google. These pages get a more constant amount of monthly visitors, and might pose a good opportunity to introduce readers to my blog.
By going to Acquisition > All Traffic > Channels
,
you can click through to the "Organic Search" channel.
By default, this overview shows what keywords were searched on, which isn't very useful to us right now. You'll also notice that most of the time, this keyword data is simply missing.
You can however specify a "Secondary Dimension", on the "Landing Page". Now the overview will be grouped per page the visitor landed on, which is exactly what we want to know!
Now we know what pages are good candidates to optimise, but we still don't know what keywords people actually searched for.
If you link your Search Console to Analytics, you'll get the data you need.
On Acquisition > Search Console > Queries
, you'll see an overview of these keywords.
Here's an example, for this past week in January, 2019:
Lastly, something I'm very proud of: my blog's performance. When building this blog I really wanted it to be fast, everywhere.
Analytics also helps with that. Under Behavior > Site Speed > Page Timings
,
you can monitor the performance of individual pages.
Take, for example, the most visited pages of November, 2018.
The red lines are the interesting ones: these pages load slower than the site's average. This can be because of many reasons: lots of visitors with a bad connection to the host, images that should be better optimised, maybe a problem with a script I wrote for a post?
This view allows me to find performance problems early on, and fix them.
This was of course nothing close to all the features Analytics offers. But I hope that I did show you the mindset I have when analysing data. These are real people visiting my site, and I want to do good for my audience.
Google Analytics is a great tool to help you with that, but in the end, it all starts with good content.
]]>He writes and shares content about web development, design and programming in general. In short: a good blog for every programmer to follow!
]]>Does the date range "2019-01-01 – 2019-01-31" contain the date "2019-01-31"?
The answer is yes, right?
… Right?
What if the range ends at 10 o'clock, while our test date starts at 11 o'clock? Now they don't overlap.
How can we reliably compare dates, if there's always a smaller unit of time we might not know about? There's two solutions.
Here a little mathematics refresher, ranges can be written like so:
[start, end]
Obviously, this notation can be applied to date periods:
[2019-01-01, 2019-01-31]
The square brackets indicate that the boundary is included in the range, round brackets mean a boundary is excluded:
(0, 3)
This notation tells us this range contains all numbers between 0 and 3, namely 1
and 2
.
Using exclusive boundaries, we can compare dates with 100% certainty of correctness.
Instead of testing whether [2019-01-01, 2019-01-31]
contains the date 2019-01-31
,
why don't we test whether [2019-01-01, 2019-02-01)
contains it?
An excluded end boundary allows us to say that "all dates before 2019-02-01" are contained within this range. The times of our date and period don't matter anymore, we're always sure that a date before 2019-02-01 will fall within our range.
While the above solution mathematically works, it gets awkward in a real world context. Say we want to note "the whole month of January, 2019" as a range. It looks like this:
[2019-01-01, 2019-02-01)
This is a little counter intuitive, at least it's not the way we humans think about "January". We'd never say "from January 1, until February 1, with February 1 excluded".
As it goes in programming, we often sacrifice the "common way of human thinking" to ensure correctness.
But there is a way to ensure program correctness, with the notation that makes sense to humans:
[2019-01-01, 2019-01-31]
Our problem originated because we weren't sure about the time of the dates we're working with. My suggestion is to not work around the problem by excluding boundaries, but to eliminate it for good.
Let's fix the root of the problem instead of working our way around it. Shouldn't that always be the mindset of every programmer?
Let me say that again, because it's oh so important:
Let's fix the root of the problem instead of working our way around it.
The solution? When you're comparing days, make sure you're only comparing days; not hours, minutes or seconds.
When programming, this means you'll have to store the precision of a date range within that range. It also means you'll have to disallow comparing dates who have different precisions.
]]>I've made the argument many times before: programmers shouldn't underestimate the value of code readability. Today I'll argue once more against a common practice we all seem to take for granted: casing.
It seems like such a minor detail, right? Using camels or snakes or kebabs to string words together, who cares? There's more to this question though.
Code readability shouldn't be neglected. It impacts how easy you can navigate and understand your code. We should talk about the casing conventions we use.
Early programming languages didn't have the same conventions we have today. Languages like Lisp and COBOL originated before widespread ASCII support, explaining the difference. Upper- and lower case, and special characters like underscores simply weren't supported by compilers back in the 50s and early 60s.
Both Lisp and COBOL allowed for hyphens to split words. Lisp's parser was smart enough to detect whether a hyphen was between two words, or whether it should be used as the subtraction operator. COBOL only has full words as operators, eliminating the problem altogether. Here's a subtraction in COBOL:
SUBTRACT data-item-1 FROM data-item-2
Because the hyphen isn't a reserved keyword, it can be used to split words.
When programming languages matured in the 80s and 90s, it became clear that the hyphen should be reserved for mathematical operations. Another issue with Lisp's smart approach was that it didn't scale in modern languages, it slowed down tokenisation significantly.
Spaces obviously could never be used, as almost every programming language uses them as the boundary between tokens. So what's left? How could we write multiple words as one, while keeping these words readable?
This is why we're left with two major conventions today: camel case, either lower- or upper; and snake case. As a sidenote: upper camel case is also called pascal case.
Most of the time, a language tends to favourite one of the two casings. We could say it's a matter of community guidelines, and be done with it.
It's my opinion that there's more to it. There is one better, more readable way of writing words. You can probably guess which one, based on the start of this blog post. camel case makes text more difficult to read, compared to snake case.
Given the word user id
, compare the two ways of writing it:
userId
user_id
It's true: camel case is more compact: you don't have to write as much. But which style is the closest to how the human brain actually reads a text?
This is the only argument that matters to me in this discussion:
How can we make it as easy as possible for our brain to read and understand code?
Readable code, reduces cognitive load. Less cognitive load means more memory space for humans to think about other things, things like writing business logic.
"All of that just by using underscores?" No, not just because of underscores. There's much more to writing readable code than naming conventions. But all small things help in getting a bigger solution.
]]>This course broadened the way I think about fonts, and it's the primary reason my own blog, this blog; looks the way it does. Whether you're a frontend or backend dev, everyone should have basic knowledge about typography, and I'd recommend following the course over at betterwebtype.com to do so.
]]>is_countable
functionarray_key_first
and array_key_last
are two new array helper functionsis_countable
rfcPHP 7.2 added a warning when counting uncountable objects.
The is_countable
function can help prevent this warning.
$count = is_countable($variable) ? count($variable) : null;
array_key_first
and array_key_last
rfcThese two functions basically do what the name says.
$array = [
'a' => '…',
'b' => '…',
'c' => '…',
];
array_key_first($array); // 'a'
array_key_last($array); // 'c'
The original RFC also proposed array_value_first
and array_value_last
,
but these were voted against by the majority of people.
Another idea for array_first
and array_last
was proposed which would return a tuple [$key => $value]
,
but opinions were mixed.
For now we only have two functions to get the first and last key of an array.
Heredoc can be a useful tool for larger strings, though they had an indentation quirk in the past.
// Instead of this:
$query = <<<SQL
SELECT *
FROM `table`
WHERE `column` = true;
SQL;
// You can do this:
$query = <<<SQL
SELECT *
FROM `table`
WHERE `column` = true;
SQL;
This is especially useful when you're using Heredoc in an already nested context.
The whitespaces in front of the closing marker will be ignored on all lines.
An important note: because of this change, some existing Heredocs might break, when they are using the same closing marker in their body.
$str = <<<FOO
abcdefg
FOO
FOO;
// Parse error: Invalid body indentation level in PHP 7.3
What was already possible with arrays, can now also be done with function calls. Note that it's not possible in function definitions!
$compacted = compact(
'posts',
'units',
);
TypeErrors
for integers and booleans used to print out their full name,
it has been changed to int
and bool
, to match the type hints in the code.
Argument 1 passed to foo() must be of the type int/bool
In comparison to PHP 7.2:
Argument 1 passed to foo() must be of the type
integer/boolean
Previously, JSON parse errors were a hassle to debug.
The JSON functions now accept an extra option to make them throw an exception on parsing errors.
This change obviously adds a new exception: JsonException
.
json_encode($data, JSON_THROW_ON_ERROR);
json_decode("invalid json", null, 512, JSON_THROW_ON_ERROR);
// Throws JsonException
While this feature is only available with the newly added option, there's a chance it'll be the default behaviour in a future version.
list
reference assignment rfcThe list()
and its shorthand []
syntax now support references.
$array = [1, 2];
list($a, &$b) = $array;
$b = 3;
// $array = [1, 3];
compact
rfcUndefined variables passed to compact
will be reported with a notice, they were previously ignored.
$a = 'foo';
compact('a', 'b');
// Notice: compact(): Undefined variable: b
There were a few edge cases were case-insensitive constants were allowed. These have been deprecated.
This change not only adds a new parameter,
it also changes the way the setcookie
, setrawcookie
and session_set_cookie_params
functions work in a non-breaking manner.
Instead of one more parameters added to already huge functions, they now support an array of options, whilst still being backwards compatible. An example:
bool setcookie(
string $name
[, string $value = ""
[, int $expire = 0
[, string $path = ""
[, string $domain = ""
[, bool $secure = false
[, bool $httponly = false ]]]]]]
)
bool setcookie (
string $name
[, string $value = ""
[, int $expire = 0
[, array $options ]]]
)
// Both ways work.
PCRE — short for "Perl Compatible Regular Expressions" — has been updated to v2.
The migration had a focus on maximum backwards compatibility, though there are a few breaking changes. Be sure to read the RFC to know about them.
You can no longer pass a non-string needle to string search functions. These are the affected functions:
strpos()
strrpos()
stripos()
strripos()
strstr()
strchr()
strrchr()
stristr()
MBString
is PHP's way of handling complex strings.
This module has received some updates in this version of PHP.
You can read about it here.
Several small things have been deprecated, there's a possibility errors can show up in your code because of this.
mbstring
function aliasesfgetss()
function and string.strip_tags
filterassert()
functionFILTER_FLAG_SCHEME_REQUIRED
and FILTER_FLAG_HOST_REQUIRED
flagspdo_odbc.db2_instance_name
php.ini directivePlease refer to the RFC for a full explanation of each deprecation.
]]>To me, blogging is more than just shouting my opinion in some corner of the web; I hope people may learn, the same way I do, by reading other people's blogs. Writing good content is essential, and I was happy to discover a blog focused on this exact topic.
]]>…worked with an array in PHP that was actually more than just an array? Did you use the array keys as fields? And did you feel the pain of not knowing exactly what was in that array? Not being sure whether the data in it is actually what you expect it to be, or what fields are available?
Let's visualise what I'm talking about:
$line = // Get a line from a CSV file
import($line['id'], $line['name'], $line['amount']);
Another example: what about validated request data?
function store(PostRequest $request, Post $post)
{
$data = $request->validated();
$post->title = $data['title'];
$post->author_id = $data['author_id'];
// …
}
Arrays in PHP are a powerful and versatile data structure. At some point though, one should wonder whether there are better solutions for their problems.
Regular readers of this blog may know that I've written about type theory in the past. I won't revisit the pros and cons on strong type systems; but I do want to say that arrays are a terrible choice if they are meant to be used as anything else but lists.
Here's a simple question for you: what's in this array?
function doSomething(array $blogPost)
{
$blogPost[/* Now what?? */];
}
In this case, there are several ways of knowing what data we're dealing with:
$blogPost
to inspect it.I simply wanted to use this data, but next thing I know, I'm deep into debugging what kind of data I'm actually dealing with. Are these really the things a programmer should be focused on?
Eliminating this uncertainty can reduce your cognitive load significantly. This means you can focus on things that really matter: things like application and business logic. You know, that's what most clients pay you to do.
It turns out that strongly typed systems can be a great help in understanding what exactly we're dealing with. Languages like Rust, for example, solve this problem cleanly:
struct BlogPost {
title: String,
body: String,
active: bool,
}
A struct is what we need! Unfortunately PHP doesn't have structs. It has arrays and objects, and that's it.
However, we can do something like this:
class BlogPost
{
public string $title;
public string $body;
public bool $active;
}
Hang on, I know; we can't really do this, not yet. PHP 7.4 will add typed properties, but they are still a long way away.
Imagine for a minute though that typed properties are already supported; we could use the previous example like so, which our IDE could auto complete:
function doSomething(BlogPost $blogPost)
{
$blogPost->title;
$blogPost->body;
$blogPost->active;
}
We could even support relations:
class BlogPost
{
public Author $author;
// …
}
function doSomething(BlogPost $blogPost)
{
$blogPost->author->name;
}
Our IDE would always be able to tell us what data we're dealing with. But of course, typed properties don't exist in PHP yet. What does exist… are docblocks.
class BlogPost
{
/** @var string */
public $title;
/** @var string */
public $body;
/** @var bool */
public $active;
/** @var \Author */
public $author;
}
Docblocks are kind of a mess though: they are quite verbose and ugly; but more importantly, they don't give any guarantees that the data is of the type they say it is!
Luckily, PHP has its reflection API. With it, a lot more is possible, even today. The above example can actually be type validated with a little reflection magic, as long as we don't write to the properties directly.
$blogPost = new BlogPost([
'title' => 'First',
'body' => 'Lorem ipsum',
'active' => false,
'author' => new Author()
]);
That seems like a lot of overhead, right? Remember the first example though! We're not trying to construct these objects manually, we're reading them from a CSV file, a request or somewhere else:
$blogPost = new BlogPost($line);
That's not bad, right? And remember: a little reflection magic will ensure the values are of the correct type. I'll show you how that works later.
I prefer this approach. It enables auto completion on what would otherwise be a black box. While it requires a little more setup: you'll have to write definitions of data; the benefits in the long run are worth it.
Sidenote: when I say "in the long run", I mean that this approach is especially useful in larger projects, where you're working in the same code base with multiple developers, over a longer timespan.
So, how can we assure that our properties are of the correct type?
Simple: read the @var
docblock declaration, validate the value against that type,
and only then set it.
If the value is of a wrong type, we simply throw a TypeError
.
Doing this extra check means we cannot write to the properties directly. At least not if they are declared public. And in our case public properties are something we really want, because of when we're using these objects. We want to be able to easily read data from them; we don't care as much on making writes easy, because we should never write to them after the object is constructed.
So we need a "hook" to validate a value against its type, before setting it. There are two ways to do this in PHP. Actually there are more, but these two are relevant.
A magic setter in combination with private or protected properties would allow us to run type validation before setting the value.
However, as mentioned before, we want a clean and public API to read from; so magic setters are, unfortunately, a no go.
Like in the previous example, we pass an array of data to the constructor, and the constructor will map that data onto the properties of its class. This is the way to go.
Here's a simplified way of doing this:
public function __construct(array $parameters)
{
$publicProperties = $this->getPublicProperties();
foreach ($publicProperties as $property) {
$value = $parameters[$property->getName()];
if (! $this->isValidType($property, $value) {
throw new TypeError("…");
}
$this->{$property->getName()} = $value;
}
}
Maybe you're curious as to what isValidType
exactly does?
Here is, again a simplified, implementation:
protected function isValidType(ReflectionProperty $property, $value): bool
{
$type = $this->getTypeDeclaration($property);
return $value instanceof $type
|| gettype($value) === $type;
}
}
Of course, there are some things missing here:
@var string|int
@var mixed
support@var \Foo[]
@var int|null
But it is very easy to add these checks to our isValidType
method.
And that's exactly what we did by the way, we made this into a package: spatie/data-transfer-object.
How to handle immutability is the last question to answer. If we use these objects to represent data from the outside, are there any valid use cases for changing these objects once they are constructed?
In 98% of the cases, the answer should be plain and simple: no. We'll never be able to change the data source, hence we shouldn't be able to change the object representing that source.
Real life projects are often not as black and white as I portray it here. While there might be some use cases, I think the mindset of "construct once, and never change" is a good one.
So how to enforce this in PHP?
Unfortunately: we don't. There has been talk of so called "read only" properties in PHP's core, but it's a difficult thing to get right. Then what about our userland type system? Unless we're giving up the ease of reading, the auto completion part; there will be no way to achieve this goal in PHP.
See, we need magic getters to support this behaviour; at the same time we don't want them. They would negate one of the goals we're trying to achieve: easy discoverability.
So for now, unfortunately, our package will allow writes to an object's properties after it is constructed. We are just careful not to do it.
I hope this post inspired you to think about your own code bases, and that you might be prompted to try this pattern out in your projects; with our package or your own implementation.
If there are any thoughts that come to your mind or if you want to discuss this further, I'd love to here from you! You can reach me via Twitter or e-mail.
]]>I've written about these issues before, but it's good to keep a regularly updated list of what's going on. So without further ado: if you're on OSX (Sierra, High Sierra or Mojave); if you're experiencing PhpStorm performance issues, this post might help you.
Do you have an external monitor plugged into your MacBook? There's an issue in Java Swing, the UI framework that PhpStorm uses under the hood. In short: if you're using a non-default resolution, Java has to do a lot of calculations to deal with half pixels and such.
In case of 4k monitors, we've seen good results with 1080p and 4k resolutions, as they are natively supported. All other resolutions can cause massive performance issues.
Default resolutions work fine.
Scaled resolutions not so much…
In your settings, under Editor > Appearance & Behaviour > Appearance
,
you'll find the editor font antialiasing options.
By default, antialiasing is set to subpixel
, to render very smooth fonts.
Again, because of Java graphical issues, there can be a big performance hit.
It's better to set the antialiasing setting to greyscale
, or disable it altogether.
Your font choice might also impact performance. I know this might take some time to get used to, but try using another font. I always used Ubuntu Mono, but switched to Monaco, and had noticeable improvements.
Some plugins make use of JavaFX, that may cause rendering issues. As an easy way to know if you're running such plugins, you can do the following.
Get the PID of the running PhpStorm process:
> top | grep phpstorm
82912 phpstorm …
Next, run jstack
with PhpStorm's process ID, and grep for "quantum":
> jstack 82912 | grep quantum
at com.sun.javafx.tk.quantum.QuantumRenderer$PipelineRunnable.run(QuantumRenderer.java:125)
If you see any output (as above), it means that plugins are using JavaFX. Using these plugins will increase performance issues over time, especially if you're running PhpStorm as a maximized window.
The only way to know which plugins are using JavaFX is by disabling plugins, one by one; restarting PhpStorm and doing the above jstack
test again.
One very popular plugin depending on JavaFX is the Markdown plugin.
The last thing you can do is download a new Java JDK, another version, and use that one to run PhpStorm.
You can configure the JDK PhpStorm is using by opening the command palette and search for Switch Boot JDK…
.
Boot JDK
It's important to note that IntelliJ products won't run on all JDKs! At the time of writing, Java 10 won't work yet.
If you've configured a JDK that broke PhpStorm, you can still fix it though. There's a file in your preferences folder which contains the JDK you're using:
~/Library/Preferences/IntelliJIdea<VERSION>/idea.jdk
You can change the JDK path there. More information on switching JDKs can be found here.
Software development is hard.
It's understandable why JetBrains chooses Java as a platform for their IDEs. Unfortunately Java Swing, an older UI framework, doesn't play well with modern OSX platforms.
Whose fault is this? Should JetBrains fix it? Will they be able to? There's no clear answer to those questions. There's an active issue here, where you can follow the progress; though I doubt there will be any solutions soon.
For now, we'll have to deal with these performance issues, because — even though they are annoying — PhpStorm is still the best PHP IDE out there, by far.
]]>I'd highly recommend adding his blog to your RSS reader, or bookmark it somewhere: assertchris.io. There's lots of good content to look forward to!
]]>array_merge
or the +
operator.
There's a subtle difference between these two methods though, a difference worth knowing.
Let's take a look at how these two methods compare:
array_merge($first, $second);
// vs.
$first + $second;
Let's say these are the two arrays we're working with:
$first = [
'a',
'b',
];
$second = [
'c',
];
This would be the result of a simple array_merge
call:
array_merge($first, $second);
[
'a',
'b',
'c',
]
While the +
operator gives us this result:
$first + $second;
[
'a',
'b',
]
Switching the operands while using the +
operator, gives a different result:
$second + $first;
[
'c',
'b',
]
Confused? So was I.
Let's write out the $first
and $second
arrays in full, with their indices.
This will make things more clear:
$first = [
0 => 'a',
1 => 'b',
];
$second = [
0 => 'c',
];
By now you can probably guess what's going on:
the +
operator will only add the elements of the rightside operand, if their key
doesn't exist in the leftside operand, while array_merge
will override existing keys.
By that definition, we can also determine that +
can never be used to recursively merge arrays,
as it will leave existing elements untouched:
$first = [
'A' => [
'B' => true,
'C' => true,
],
];
$second = [
'A' => [
'B' => false,
'C' => false,
],
];
$first + $second;
Here's the result:
[
'A' => [
'B' => true,
'C' => true,
],
]
While using array_merge
, would give this result:
[
'A' => [
'B' => false,
'C' => false,
],
]
"Hang on", I hear you say, "isn't that what array_merge_recursive
is supposed to do?".
Here we have a case of unfortunate naming. Please don't be surprised — it's PHP after all.
See, array_merge
will merge matching elements by overriding them.
array_merge_recursive
on the other hand will keep both elements, and merge them in a new array, keeping both values.
This is what our previous example would look like, using array_merge_recursive
:
[
'A' => [
'B' => [
true,
false,
],
'C' => [
true,
false,
],
],
]
What about merging multiple arrays? You can probably guess the outcome by now:
$first = ['a'];
$second = ['b'];
$third = ['c'];
Here's what array_merge
results in:
array_merge($first, $second, $third)
[
'a',
'b',
'c',
]
Chaining the +
operator also works, with the following result:
$first + $second + $third
[
'a',
]
With this little refresher, I hope that you won't find yourself confused anymore when you're deep into your code and need to merge arrays.
I found it to be a cognitive burden when I had to stop and think about "hang on, what is the correct way to do this?". Luckily now, we know!
]]>Last month I wrote about view models in Laravel. I received a lot of good reactions on the post, but also the same question over and over again: how do view models differ from view composers in Laravel?
Time to clarify this question once and for all.
Let's look at how view composers are used in Laravel. View composers are a way of binding data to a view from global configuration.
The Laravel documentation explains it like this:
View composers are callbacks or class methods that are called when a view is rendered. If you have data that you want to be bound to a view each time that view is rendered, a view composer can help you organize that logic into a single location.
View composers are registered like this, the example is taken from the Laravel docs.
class ComposerServiceProvider extends ServiceProvider
{
public function boot()
{
View::composer(
'profile', ProfileComposer::class
);
View::composer('dashboard', function ($view) {
// …
});
}
// …
}
As you can see you can both use a class and a closure which you can use to add variables to a view.
Here's how view composers are used in controllers.
class ProfileController
{
public function index()
{
return new view('profile');
}
}
Can you see them? Nope, of course not: view composers are registered somewhere in the global state, and you don't know which variables are available to the view, without that implicit knowledge.
Now I know that this isn't a problem in small projects. When you're the single developer and only have 20 controllers and maybe 20 view composers, it'll all fit in your head.
But what about a project with three or four developers, with hundreds of controllers? What if you're taking over a legacy project where you don't have this implicit knowledge?
This is why at Spatie, we use view models in our larger projects. They make everything much more explicit, which helps us keep the code maintainable.
Here's what we do:
class ProfileController
{
public function index(User $user)
{
return new view(
'profile',
new ProfileViewModel($user)
);
}
}
Now it's clear now from the controller itself what variables are available to the view. We can also re-use the same view for multiple contexts. An example would be the same form view used in the create and edit actions.
One last added benefit, one you might not have thought about, is that we can pass data into the view model explicitly. If you want to use a route argument or bound model to determine data passed to the view, it is done explicitly.
In conclusion: managing global state is a pain in large applications, especially when you're working with multiple developers on the same project. Also remember that just because two means have the same end result, that doesn't mean that they are the same!
I hope this quick writeup answers all the questions about the difference between view models and -composers. If you want to know more about view models in particular, be sure to read the blog post about them here.
]]>While many concepts in this post are inspired by DDD principles, they don't strictly follow domain driven design. In our context, "domain" could also be named "module". A "domain" simply refers to a category of related stuff, that's it.
It's also important to note is that this approach isn't a silver bullet. At Spatie we choose a different project structure based on the needs of that specific project. It is possible that your project isn't a good fit for what we'll be reviewing today.
In our experience, today's principles are mostly beneficial in larger projects:
If you've worked on these kinds of large projects before, you know that "the business logic" never is just one thing. Often during development, you'll identify "sub-systems" within the larger domain; that is: the collection of problems you're trying to solve with your code.
To name a few examples: user management, inventory management, invoicing and contracts. I'm sure you can think of many others.
Most likely, every sub-system has one or several models. But it doesn't stop there: models can be interacted with, actions can be performed with them, there can be system-specific validation rules, ways of passing data between systems, and more.
Looking at a standard Laravel application, the code describing a single system is often spread across multiple directories:
app/
├── Enums/
│ ├── ContractDurationType.php
│ └── ContractType.php
├── Exceptions/
│ └── InvalidContractDate.php
├── Models/
│ └── Contract.php
└── Rules/
├── ContractAvailabilityRule.php
└── ContractDurationRule.php
This structure was the first struggle that prompted me to look for a better solution. I often found myself searching through several places in order to work one thing, one system.
So why not group sub-systems together? It looks something like this:
Domain/
├── Contracts/
├── Invoicing/
└── Users/
You can see the name Domain
here.
According to Oxford Dictionary, a "domain" can be described like so:
A specified sphere of activity or knowledge.
We're grouping code together based on their sphere of activity, their domain. Let's zoom into one specific domain folder:
Contracts/
├── Actions/
├── Enums/
├── Exceptions/
├── Models/
├── Rules/
├── Status/
└── ValueObjects/
Modern PHP developers are most likely familiar with most of these folder names. Though some deserve a little more attention.
Actions are probably the most powerful tool within this whole setup. An action is a class that performs an operation within its domain. This might be a simple task like creating or updating a model, or something more complex following one or several business rules like approving a contract.
Because a single action only focuses on one task, they are extremely flexible: an action can be composed out of other actions and they can be injected wherever you want.
Here's an example of two actions working together: CreateOrUpdateContractLine
and ResolveContractLines
.
The first one will do as its name says: create or update a single contract line.
The second one will loop over a collection of user input, and resolve the lines one by one.
Here's what ResolveContractLines
will do:
Here's the code:
class ResolveContractLines
{
public function __construct(
CreateOrUpdateContractLine $createOrUpdateContractLine,
RemoveContractLine $removeContractLine
) { /* … */ }
public function execute(
Contract $contract,
ContractLinesCollection $contractLinesCollection
) {
$lineIds = [];
foreach ($contractLinesCollection as $contractLineData) {
$contractLine = $this->createOrUpdateContractLine
->execute($contractLineData);
$lineIds[] = $contractLine->id;
}
$contractLinesToRemove = ContractLine::query()
->whereContract($contract)
->whereNotIn('id', $lineIds)
->get();
foreach ($contractLinesToRemove as $contractLine) {
$this->removeContractLine->execute($contractLine);
}
}
}
Besides composing actions together, they are also great for testing. Because of an action's small size and single responsibility, it can be unit tested very efficiently.
Actions also encapsulate most of the business logic for the app: generating contract numbers, changing statuses, handling side-effects in an explicit way,… This makes it easier for developers to reason about what the application does, as most of its business is encapsulated as actions.
If you're into DDD, you're probably thinking of commands right now. Actions are a simpler version of them. There's no command bus and actions may directly return values. For the scope of our projects, it's a very manageable approach.
You're probably wondering how this domain stuff ties together with controllers or CLI commands. That's of course the place where you'll use them. There's one more abstraction we need to understand though: value objects.
Update: since writing this blog post there has been an interesting discussion on the name of "value object". We've changed the name to "data transfer object". You can read more about this naming here.
Have you noticed the ContractLinesCollection
passed to the ResolveContractLines
action in the previous example?
That's a value object.
Working with user input isn't always straight forward. For example, in Laravel applications you'll get an array of form data or an array of CLI arguments, the rest is up to you.
Value objects are a representation of that user data, in a structured way. Because we want don't want to concern our actions with input validation, we pass them a value object. There's one rule applied to value objects: if they exist, they are valid.
Most of the time, value objects are a simple mapping between validated request data, and properties that can be used by actions.
Here's an example of a value object:
class ContractLineData
{
public $price;
public $dateFrom;
public $dateTo;
public $article;
public static function fromArray(
array $input
): ContractLineData {
return new self(
$input['price'],
Carbon::make($input['date_from']),
Carbon::make($input['date_to']),
Article::find($input['article_id'])
);
}
public function __construct(
int $price,
Carbon $dateFrom,
Carbon $dateTo,
Article $article
) { /* … */ }
}
Because of convenience, we're using public properties. You can imagine why we're looking forward to strongly typed and readonly properties in PHP.
Value objects allow actions to only focus on the actual action, and not be concerned whether input is valid or not. Furthermore, it's easy to fake a value object, making tests simpler once more.
Up until this point, I've said almost nothing about controllers or CLI commands, and how they fit into this picture. That's intentional.
See, because our domains are split into separate areas, we're able to develop a whole domain, without ever writing a single controller or view. Everything in the domain is easily testable, and almost every domain can be be developed side by side with other domains.
In larger projects, this is a highly efficient approach. We've got two or three backend developers working on one project, and each of them has a domain they are working on next to each other.
Also, because every domain is tested, we're very certain that all business logic required by the client works as intended, before writing a single form and integration tests.
Once a domain is done, it can be consumed. The domain itself doesn't care when or where it is used, its usage rules are clear to the outside.
This means we're able to build one or more applications, using the existing domains. In one of our projects, there's an admin HTTP application and a REST API. Both of them use the same domains; their actions, models, rules, etc. You can see how this approach is not only efficient during development, but also enables for much better scaling.
Here's an example of how a controller in the admin HTTP application looks:
class ContractsController
{
public function index() { /* … */ }
public function edit(Contract $contract) { /* … */ }
public function update(
Contract $contract,
UpdateContract $updateContract,
UpdateContractRequest $updateContractRequest
) {
$contract = $updateContract->execute(
$contract,
ContractData::fromRequest($updateContractRequest)
);
return new ContractViewModel($contract);
}
}
Almost all our controllers actions are as simple as this:
Structuring code in domains increases efficiency between developers on a single project. Furthermore, it decreases the complexity of maintenance, because sub-systems are separated and well tested.
By using actions and value objects, you're able to communicate with the domain in a controlled and testable way. While it takes longer to initially write, this approach pays off very quickly, even during early development.
Maybe the most important reason for structuring our code this way, is that it's easier to understand. We humans don't think in abstracts like "models", "actions" and "rules"; we categorize complex business processes into sub-systems. Things like "contracts" and "invoicing".
I've been structuring complex code bases like this for two years, and can say from experience that it's significantly more easy to reason about them now. In end, I believe developer experience is equally important as theoretical knowledge and paradigms to succeed.
👋 Hi, thanks for reading! I hope this post can help you in one way or another.
If you want to talk more about this topic –I do– you can always send me a Tweet or e-mail. Here's my Twitter, and here my e-mail.
]]>View models are an abstraction to simplify controller and model code. View models are responsible for providing data to a view, which would otherwise come directly from the controller or the model. They allow a better separation of concerns, and provide more flexibility for the developer.
In essence, view models are simple classes that take some data, and transform it into something usable for the view. In this post I'll show you the basic principles of the pattern, we'll take a look at how they integrate in Laravel projects, and finally I'll show you how we use the pattern in one of Spatie's, our company, projects.
Let's get started. Say you have a form to create a blog post with a category. You'll need a way to fill the select box in the view with category options. The controller has to provide those.
public function create()
{
return view('blog.form', [
'categories' => Category::all(),
]);
}
The above example works for the create method, but let's not forget we should also be able to edit existing posts.
public function edit(Post $post)
{
return view('blog.form', [
'post' => $post,
'categories' => Category::all(),
]);
}
Next there's a new business requirement: users should be restricted in which categories they are allowed to post in. In other words: the category selection should be restricted based on the user.
return view('blog.form', [
'categories' => Category::allowedForUser(
current_user()
)->get(),
]);
This approach doesn't scale.
You'll have to change code both in the create
and edit
method.
Can you imagine what happens when you need to add tags to a post?
Or if there's another special admin form for creating and editing posts?
The next solution is to have the post model itself provide the categories, like so:
class Post extends Model
{
public static function allowedCategories(): Collection
{
return Category::query()
->allowedForUser(current_user())
->get();
}
}
There are numerous reasons why this is a bad idea, though it happens often in Laravel projects. Let's focus on the most relevant problem for our case: it still allows for duplication.
Say there's a new model News
which also needs the same category selection.
This causes again duplication, but on the model level instead of in the controllers.
Another option is to put the method on the User
model.
This makes the most sense, but also makes maintenance harder.
Imagine we're using tags as mentioned before.
They don't rely on the user.
Now we need to get the categories from the user model, and tags from somewhere else.
I hope it's clear that using models as data providers for views also isn't the silver bullet.
In summary, wherever you try to get the categories from, there always seems to be some code duplication. This makes it harder to maintain and reason about the code.
This is where view models come into play. They encapsulate all this logic so that it can be reused in different places. They have one responsibility and one responsibility only: providing the view with the correct data.
class PostFormViewModel
{
public function __construct(
User $user,
Post $post = null
) {
$this->user = $user;
$this->post = $post;
}
public function post(): Post
{
return $this->post ?? new Post();
}
public function categories(): Collection
{
return Category::allowedForUser($this->user)->get();
}
}
Let's name a few key features of such a class:
post
method,
depending on whether your creating or editing a post.This is what the controller looks like:
class PostsController
{
public function create()
{
$viewModel = new PostFormViewModel(
current_user()
);
return view('blog.form', compact('viewModel'));
}
public function edit(Post $post)
{
$viewModel = new PostFormViewModel(
current_user(),
$post
);
return view('blog.form', compact('viewModel'));
}
}
And finally, it can be used in the view like so:
<input value="{{ $viewModel->post()->title }}" />
<input value="{{ $viewModel->post()->body }}" />
<select>
@foreach ($viewModel->categories() as $category)
<option value="{{ $category->id }}">
{{ $category->name }}
</option>
@endforeach
</select>
These are the two benefits of using view models:
The previous example showed a simple class with some methods. This is enough to use the pattern, but within Laravel projects, there are a few more niceties we can add.
For example, you can pass a view model directly to the view
function if the view model implements Arrayable
.
public function create()
{
$viewModel = new PostFormViewModel(
current_user()
);
return view('blog.form', $viewModel);
}
The view can now directly use the view model's properties like $post
and $categories
.
The previous example now looks like this:
<input value="{{ $post->title }}" />
<input value="{{ $post->body }}" />
<select>
@foreach ($categories as $category)
<option value="{{ $category->id }}">
{{ $category->name }}
</option>
@endforeach
</select>
You can also return the view model itself as JSON data, by implementing Responsable
.
This can be useful when you're saving the form via an AJAX call,
and want to repopulate it with up-to-date data after the call is done.
public function update(Request $request, Post $post)
{
// Update the post…
return new PostFormViewModel(
current_user(),
$post
);
}
You might see a similarity between view models and Laravel resources. Remember that resources map one-to-one on a model, when view models may provide whatever data they want.
In one of our projects, we're actually using resources in view models!
class PostViewModel
{
// …
public function values(): array
{
return PostResource::make(
$this->post ?? new Post()
)->resolve();
}
}
Finally, in this project we're working with Vue form components, which require JSON data. We've made an abstraction which provides this JSON data instead of objects or arrays, when calling the magic getter:
abstract class ViewModel
{
// …
public function __get($name): ?string
{
$name = Str::camel($name);
// Some validation…
$values = $this->{$name}();
if (! is_string($values)) {
return json_encode($values);
}
return $values;
}
}
Instead of calling the view model methods, we can call their property and get a JSON back.
<select-field
label="{{ __('Post category') }}"
name="post_category_id"
:options="{{ $postViewModel->post_categories }}"
></select-field>
I hear you! There's a whole separate blog post on that topic. You can read it here.
In summary, view models can be a viable alternative to working with the data directly in a controller. They allow for better reusability and encapsulate logic that doesn't belong in the controller.
You're also not confined to forms when using them. At Spatie we also use them to populate facet filter options, based on a complex context the user is currently working in.
I'd recommend trying this pattern out. You don't need anything to get started by the way. All Laravel gimmicks listed above are optional and can be added depending on your use case.
And just in case you'd like to use Laravel gimmicks, we've got a package for it: spatie/laravel-view-models 🤗.
]]>If you're already convinced of the power of MySQL views, or just want to know how to implement them in Laravel, you're free to skip ahead.
A view in MySQL stores the result of a query in a table-like structure. You're able to query this view just like you would query a normal table.
The power of views is twofold:
There's also a caveat to using views though.
Depending on the kind of query, MySQL will need to construct an "in memory" table representing the view, at runtime.
This operation is called table materialization and happens when using certain keywords like GROUP BY
, or aggregated functions.
The takeaway is that views might actually hurt query performance, depending on the kind of query you're executing. As with all things, views are a good solution for some problems, but a terrible idea for others. Use them wisely, and read up on their restrictions here.
Let's look at a real-life example, to demonstrate how we could solve a given problem.
We've got a model MeterReading
which logs a meter reading done in an apartment building.
Every unit in the building has its own electricity, water and gas meters.
Every reading is listed in the database with a reference to the unit, the date,
the user doing the reading, the type, and the actual meter value.
Type in this example is electricity
, water
or gas
.
This is what a simplified migration of this table looks like:
Schema::create('meter_readings', function (Blueprint $table) {
$table->unsignedInteger('unit_id');
$table->unsignedInteger('user_id');
$table->string('type');
$table->dateTime('date');
$table->unsignedInteger('value');
});
Now the client asks us to generate reports based on this raw data. He wants to see an overview of the units, where every row represents the readings for that unit, on that day, and whether all readings were done or not.
In short, he wants to see this:
+---------+---------+------------+-------------+-------+-----+
| unit_id | user_id | date | electricity | water | gas |
+---------+---------+------------+-------------+-------+-----+
| 14 | 72 | 2018-08-19 | 0 | 1 | 0 |
| 59 | 61 | 2018-08-06 | 0 | 0 | 1 |
| 41 | 64 | 2018-08-02 | 1 | 1 | 1 |
| 41 | 45 | 2018-08-02 | 1 | 1 | 1 |
...
| 41 | 51 | 2018-08-02 | 1 | 1 | 1 |
+---------+---------+------------+-------------+-------+-----+
The report show a data set that is grouped by unit, user and day; and the corresponding readings done for at the time.
Here are a few ways of generating this report.
We always query all the data, and group it in our code. This is the most easy way of doing it, but has some downsides:
We can of course skip PHP and build the raw query to fully use the power of MySQL. While this solves the performance issue, we're still working with a custom data set which can't make use of standard pagination. Also, you're now maintaining a big SQL query somewhere in your code. It's probably a string somewhere in PHP, or –slightly better– a separate sql file.
We could make a separate model called MeterReadingReport
,
and use event hooks on MeterReading
to manage these reports.
Every time a reading is added, we can get or create a report for that unit, day and user; and update the data accordingly.
Now there's a separate model that's simple to query. There's no more performance impact and the pagination issue is also solved.
But on the other hand, there's a lot more code to manage these event hooks. Creating reports is one thing, but what if a reading is updated or deleted? That's a lot of complexity we need to manage.
Projecting events into other models isn't a bad idea though. It's one of the key features in event sourcing. If you've got the right setup, making projectors would definitely be an option.
While we do have a package that handles this exact use case (laravel-event-projector), it seemed overkill for this use case; especially since there are a lot of other "normal" models in this project.
Looking at all the possible solutions, we can make a simple list of requirements:
MySQL views are this perfect middle ground. Let's look at how they are implemented.
To work with a view, we'll have to first create a query that can build this view. While many people are scared of SQL –modern ORMs made us way too lazy– I find it a lot of fun.
Beware that I'm no SQL master, so there might be things that could be done better. I also won't explain what this query does exactly, as it'll be different for your use case.
In this case, it generates the table listed above. This is it:
SELECT
unit_id
, user_id
, DATE_FORMAT(`date`, '%Y-%m-%d') AS day
, COUNT(CASE WHEN type = 'electricity' THEN type END)
AS `electricity`
, COUNT(CASE WHEN type = 'water' THEN type END)
AS `water`
, COUNT(CASE WHEN type = 'gas' THEN type END)
AS `gas`
FROM
meter_readings
GROUP BY
unit_id
, user_id
, day
;
It's very easy to build this query in your favourite SQL browser, and afterwards plug it into your project.
How to plug it in, you ask? Very simple, with a migration.
public function up()
{
DB::statement($this->dropView());
DB::statement($this->createView());
}
First of all, dropView
is required, because Laravel only drops tables when doing a fresh migration.
It's as simple as this:
private function dropView(): string
{
return <<<SQL
DROP VIEW IF EXISTS `meter_reading_reports`;
SQL;
}
You notice I prefer Heredoc in these cases, a separate SQL file is of course equally good.
Michael Dyrynda pointed out to me that there's a --drop-views
flag you can pass to the migrate command.
So, technically, this manual dropping isn't required.
I prefer this way though, because now we don't have to remember to add the extra flag.
Next up, the createView
method returns the query, with some added syntax.
I've shortened the sample a bit, but you get the point.
private function createView(): string
{
return <<<SQL
CREATE VIEW `meter_reading_reports` AS
SELECT /* … The query */
SQL;
}
Sidenote: I'm very much looking forward to PHP 7.3 and flexible Heredoc syntax.
Now that we have a migration in place, all else just works like normal Laravel!
class MeterReadingReport extends Model
{
protected $casts = [
'day' => 'date',
];
public function unit(): BelongsTo
{
return $this->belongsTo(Unit::class);
}
public function user(): BelongsTo
{
return $this->belongsTo(User::class);
}
}
We're using a simple model, without any workarounds whatsoever. Relations work just like normal, casting like you're used to, pagination works like it should be, and there no more performance impact.
The only thing that's not possible is of course writing to a view. It is actually possible to do it in MySQL, but completely irrelevant to our use case.
Maybe you can already see some use cases where MySQL views might be useful? Maybe you have a followup question or remark? I'd love to hear from you! You can reach me on Twitter or via e-mail.
]]>What better way to know the future, than to look at the past? There's an excellent blog called "The History Of The Web". Thanks to Jay Hoffman, the writer of the blog, I've been fascinated by the roots of the web.
On August 6, 1991 Tim Berners-Lee puts the first website online. A few months later, on October 29 of the same year, the first HTML draft is posted on the www-talk mailing list.
Three years went by until Netscape Navigator, the most popular browser at the time, was released on October 13, 1994. It took Microsoft almost a year longer to release their own Internet Explorer 1 on August 15 of 1995. In December of 1996, the first iteration for CSS was conceptualised.
Mozilla released Firefox more than ten years after the beginning of the web in 2004. Only a few years later, Apple flipped the web to its mobile side by presenting the very first iPhone on January 8, 2007.
Twenty-three years after the first mention of HTML modern websites found their identity as we know it today, when HTML5 was made the formal recommendation by the W3C on October 28, 2014.
It's amazing to see how the web has evolved in less than three decades. Not only its technical boundaries were pushed; people also kept finding innovating ways for what the technology was used for. Think about sites like GeoCities or NeoPets; the first blog post written in 1997; or some recent examples like /r/place on Reddit and an experience beyond words crafted by SB Nation.
While the web's primary focus was to distribute content, its users have shaped it into a completely different, broader platform.
It's only in more recent years that we've been able to observe – and be part of – the unique phenomenon of the JavaScript world. While the language has been around since the nineties, it were frameworks like Ember, Backbone and Angular that opened a whole new area the web could grow in.
It's impossible to pinpoint an exact date on when people started looking at JavaScript as something more than a simple scripting language. But there are a few milestones worth mentioning.
Especially with the modern frameworks, better syntax was required; and projects like Babel came into view. This is where it starts to get real interesting: JavaScript in the browser becoming a compilation target, for other languages and supersets.
Gary Bernhardt, a well known public speaker; said that, in order for JavaScript to become as successful as it is today, it had to really suck. It's only then that people start investing in alternatives. And when JavaScript is the only thing that runs in the browsers, people are really forced to think out of the box.
With the arrival of frontend frameworks in JavaScript, people needed to start investing in performance. On the one hand, browser vendors are doing amazing things with their JavaScript engines. On the other hand, one of the most creative, out of the box thinking solutions; must have been asm.js.
Here you have an optimised subset of JavaScript; being able to run, for example, a 3D game engine—in the browser. With asm.js, and Web Assembly following; web technologies can be used for yet another, completely different goal.
It begs the question: "what is the web?"
The technologies the web was built upon: HTTP, HTML, CSS and JavaScript, became technologies to make applications and games; some of the programs we use to build the web, are built on these same technologies themselves.
Does "the web today" refer to all these technologies and creations, or just a collection of connected documents, built on top of the same technologies? Is the browser's goal still just that: browsing documents; or has it become a platform for all kinds of things, whatever we can imagine?
With the web advancing so fast, one can only wonder what it will look like a few decades from now.
One of the key changes could be the sandbox we're all using today: the browser. Will applications stay confined to that browser window, or will they break free and live as first-class programs on your operating system?
The mobile world is already moving in this direction with progressive web apps. Chrome OS was completely built on top of the web, independent of the browser. But, some might argue, it was too ahead of its time.
Imagine a world where web apps can be "installed" via an app store; where you don't need bookmarks or URLs anymore, but simply open an app, like we open the browser today. Obviously, being built on top of the web; these apps don't need to be installed, they just work, everywhere.
Imagine JavaScript and DOM engines baked into operating systems. No more Electron or Java for easy cross-platform programming. All programs can be shared, everything will be interconnected.
And once all that is achieved, someone will come along, claiming a new invention: a simple web app for browsing and sharing content…
Hi, thanks for reading! What's your view for the web in 2045? Feel free to share your thoughts on Twitter or via e-mail, I'd love to chat!
And if you're reading this in 2045: what's your opinion on this blog post, looking back? Where will the web be in, say, another 27 years?
]]>Let's take a look at a common facade call: Auth::user()
.
The Auth
facade will reach into Laravel's service container, grab the registered component,
and forward the static call to that component.
In this case, it'll return the logged in user.
During a discussion with my colleagues, I found it difficult to put into words
what exactly is wrong with grabbing things out of the container –a service locator– so I decided to write my thoughts down,
with an example: a class CreatePostAction
which is tasked to create a blog post,
based on a set of parameters.
class CreatePostAction
{
public function __invoke(
string $title,
string $body
): Post
{
return Post::create([
'title' => $title,
'body' => $body,
'author_id' => Auth::user()->id,
]);
}
}
I want to highlight three problems with this approach, directly caused by the use of a service locator.
Let's look at these problems, one by one.
Before even looking into this first problem, there's one assumption I'll make. That is that, as a developer, you prefer to know bugs in your code as early as possible, so that you can fix them as early as possible.
I'll assume that you don't like a situation where a client tells you a production project is broken, and the issue can only be reproduced by taking several steps.
As the name says, runtime errors can only be discovered by running the program. Truth be told: PHP, being an interpreted language; highly leans towards these kind of errors. You cannot know if a PHP program will work before running it.
There's nothing wrong with that, but my argument here is that every place we can avoid these errors, we should.
Compile time errors are errors that can be detected without running the code. For example: in your IDE or using a static analysis tool. The benefit is that you know a piece of code will absolutely work, even without testing it.
Let's put that into practice. What does Auth::user()
return?
A logged in User
—most of the time.
Our action class doesn't know anything about the system it lives in,
except the things we tell it.
This means that, when calling Auth::user()->id
,
we assume that the surrounding system has a logged in user, with an id
.
Of course, your first thought is that we know there's a user, because this action is called within a controller that requires a logged in user. I'll come back to that argument later on.
For now, speaking from a mathematical point of view,
it's impossible to prove whether Auth::user()->id
will work, without running it.
There are two ways to fix it, from the action's perspective.
By doing a runtime check:
class CreatePostAction
{
public function __invoke(
string $title,
string $body
): Post
{
if (! Auth::user()) {
throw new Exception('…');
}
// ...
}
}
Or by requiring a valid user, before executing:
class CreatePostAction
{
public function __invoke(
string $title,
string $body,
User $author
): Post
{
// ...
}
}
I know you have arguments why this will never happen and I shouldn't be worried about it; I'll address those arguments soon.
Before looking at the biggest problem, how service locators affect cognitive load; there's the issue with obfuscated classes. Let's look at our action's definition.
class CreatePostAction
{
public function __invoke(
string $title,
string $body
): Post
{ /* ... */ }
}
I've blogged and spoken about this a lot already: developers don't read every line of code, they scan it.
At the time of writing the code, it all seems obvious: you know a blog post requires a logged in user. However, for the developer working in your legacy code, that intent is not clear. Not unless he's reading every single line of code.
Imagine being that person: having to work in a legacy project where you need to read every line of code, in order to get the general idea of what's happening.
You might as well not be interested in the specifics of how a post is created, you just want to know what's required to do so. There's two ways to solve this issue.
Either be using docblocks; meaning a lot more work for both the author and reader, and it clutters your code:
class CreatePostAction
{
/**
* This action will create a post,
* and attach the logged in user as its author.
*
* @param string $title
* @param string $body
*
* @return Post
*/
public function __invoke(
string $title,
string $body
): Post
{ /* ... */ }
}
Or by injecting the user:
class CreatePostAction
{
public function __invoke(
string $title,
string $body,
User $author
): Post
{ /* ... */ }
}
Which one do you prefer? Remember: from the perspective of the person working in a legacy project, and it's not just one class, there are dozens and dozens.
This all leads up to the final, and major, problem: cognitive load. I already wrote a lot on this topic, and I'll share some links at the end of this post.
The important question, which counters most of the pro-arguments for service locators; is how much brain effort you, the developer, has to spend on trivial questions like:
How sure am I this code will actually work?
Let's look at the most basic example: Auth::user()->id
.
I work on Laravel projects and admit to have used this piece of code numerous times.
Here's a non-exhaustive list of questions popping into my head when writing this code:
These are all such trivial questions, and I need to think about them every time I use a facade. How much more easy is it to simply say:
I need the logged in user to do this action, and the context which is calling this action can figure it out from there.
class CreatePostAction
{
public function __invoke(
string $title,
string $body,
User $author
): Post
{ /* ... */ }
}
Sure, compile time errors and less code are niceties, but my main problem is this cognitive load. I don't want to ask all these questions every time I use a facade.
Seasoned Laravel developers will tell me this is the way the framework works and we should embrace it. They are right, of course. But making the assumption that "it will work" isn't good enough for me. At least, it's no argument against increased cognitive load, as you're still left with a lot of questions about the surrounding context.
Dependency injection, of course; fixes this. It's a pattern which allows for inversion of control and clarifies intent. It's also perfectly possible to do proper DI in Laravel; and, in my opinion, we should do it more.
I've written about DI before, feel free to read up on it here. I also recently gave a talk about cognitive load, from a visual perspective. You can find it here.
]]>In this talk, I gave six pointers to improve this visual perception, to make it easier to read your own code: fonts, code folding, colours, patterns, documentation and names.
These are the links mentioned in the video:
The colour scheme used is a port of Mozilla's Photon Light theme. Here's the PHPStorm version.
I you have any thoughts coming to mind, if you want to discuss this further or tell me I'm wrong; you can reach me on Twitter or via e-mail.
]]>For example, this is what I'm talking about:
A tree view configured with coloured scopes
These colours allow you to easily recognise files,
and that in turn allow you to think more freely about things that really matter when coding.
First you'll want to configure one or more scopes.
A scope is a set of textual filters that are applied on your files, you can configure them by going to Settings > Scopes
.
You can use the buttons to include and exclude folders and files, or you can write the filters yourself. There's a special syntax to do that, you can read about it described here. Don't forget you can expand the text area for easier configuration:
Every scope can be applied a specific colour. This makes it easy to easily spot files.
By applying colours to a scope, you'll see them in the tree view, in file tabs and when using file navigation.
Besides colours, scopes also allow for easy filtering. For example, in the tree view:
File colours
But also in the finder:
File colours
Setting up scopes shouldn't take longer than 10 minutes every project,
and saves a lot of time in the long run.
There's also the possibility to set default options though,
which will be used every every time you create a new project.
Go to File > New Project Settings > Preference for New Projects
and configure your default scopes and colours over there, the same way you'd do as explained before.
And just in case you'd need some inspiration, these are my default scopes:
App
file:app//*||file:config//*||file:routes//*||file:app||file:config||file:routes||file:src//*||file:src
Resources
file:resources//*||file:resources
Database
file:database//*||file:database
]]>I say many, because GitHub isn't by far Microsoft's first big acquisition. Think of Skype and Nokia a few years back, or Linkedin and Minecraft more recently. Who can blame them? It's only natural that a company continues to look for ways to improve their market footprint, especially in the tech world.
But now what? GitHub becomes one of the many products swallowed by tech giants. It will become even more difficult for smaller products to stand a competing chance. Should we, the tech industry, be worried about big monopolies growing larger and larger?
What better way to look at the future, than to look at the past? Many will say Microsoft isn't deploying their "embrace, extend, and extinguish" strategy anymore; but are they?
Here's what this strategy means, from wikipedia.
It sounds like a smart strategy. It may not be called like this anymore but it seems reasonable that, if Microsoft still wants to grow; they must look at how to outgrow their competitors.
We've seen them embracing a lot of technologies and platforms recently. Think of open sourcing .NET, developing VSCode, embedding Linux etc. Microsoft is doing a lot of good for the open source community lately, but you can't help but think they are also serving their own agenda while at it.
It's only guessing what Microsoft will do to extend GitHub, but we've already seen a lot of nice features added to other acquired products. Growing them to, indeed, part of the standard.
Let's not forget the obvious example though: VSCode, a direct competitor to GitHub's Atom editor. Both are built on the same technology: Electron, managed by GitHub.
So will the tech community allow a giant to extinguish all its competitors? Time will tell.
Thinking back 20 years ago, the browser wars were raging. There was a lot of competition, and with that competition came innovation. We must not forget that it was this period of rivalry between many different players, that lead to the web platform we have today. And it's a platform that grew beyond the wildest expectations of these first pioneers.
So we must wonder: what if there's less and less competition in the tech world? What if the giants, Microsoft, Apple, Amazon, Google and Facebook; only grow larger and larger? What does it mean for innovation?
It's worrisome to see that in the startup culture, it's actually one of the biggest questions right at the beginning: "what's our exit strategy?" The goal of almost every startup, from the start, is to be absorbed by a bigger company, in just a few years.
The drive for innovation gets replaced by the need for money. But we cannot afford stagnation. We must be aware that this, still very young, industry has very large odds of becoming the same as so many others.
To me, these are the thoughts behind GitHub's acquisition. And yes, they are scary.
]]>Not only did it clarify type variance, I also understood what the Liskov substitution principle actually is about. Today, I'm going to share these insights with you.
I'll be writing pseudo code to make clear what I'm talking about. So let's make sure you know what the syntax of this pseudo code will be.
A function is defined like so.
foo(T) : void
bar(S) : T
First comes the function name, second the argument list with types as parameters,
and finally the return type.
When a function returns nothing, it's indicated as void
.
A function can extend — overwrite — another function, as can types. Inheritance is defined like so.
bar > baz(S) : T
T > S
In this example, baz
extends bar
, and S
is a subtype of T
.
The last step is being able to invoke the function, which is done like so.
foo(T)
a = bar(S)
Once again: it's all pseudo code and I'll use it to show what types are, how they can and cannot be defined in combination with inheritance, and how this results in type-safe systems.
Let's look at the official definition of the LSP.
If
S
is a subtype ofT
, then objects of typeT
may be replaced with objects of typeS
—Wikipedia
Instead of using S
and T
, I'll be using more concrete types in my examples.
Organism > Animal > Cat
These are the three types we'll be working with.
Liskov tells us that wherever objects of type Organism
appear in our code,
they must be replaceable by subtypes like Animal
or Cat
.
Let's say there's a function used to feed
an Organism
.
feed(Organism) : void
It must be possible to call it like so:
feed(Animal)
feed(Cat)
Try to think of function definition as a contract, a promise; for the programmer to be used. The contract states:
Given an object of the type
Organism
, I'll be able to execute andfeed
thatOrganism
.
Because Animal
and Cat
are subtypes of Organism
,
the LSP states that this function should also work when one of these subtypes are used.
This brings us to one of the key properties of inheritance.
If Liskov states that objects of type Organism
must be replaceable by objects of type Animal
,
it means that Animal
may not change the expectations we have of Organism
.
Animal
may extend Organism
, meaning it may add functionality,
but Animal
may not change the certainties given by Organism
.
This is where many OO programmers make mistakes. They see inheritance more like "re-using parts of the parent type, and overriding other parts in the sub-type", rather than extending the behaviour defined by its parent. This is what the LSP guards against.
Before exploring the details of type safety with inheritance, we should stop and ask ourselves what's to gain by following this principle. I've explained what Barbara Liskov meant when she defined it, but why is it necessary? Is it bad to break it?
I mentioned the idea of a "promise" or "contract".
If a function or type makes a promise about what it can do,
we should be able to blindly trust it.
If we can't rely on function feed
being able to feed all Organisms
,
there's a piece of undocumented behaviour in our code.
If we know that the LSP is respected, there's a level of security. We may trust that this function will do the thing we expect; even without looking at the implementation of that function. When the contract is breached, however; there's a chance of runtime errors that both the programmer and the compiler could not –or did not– anticipate for.
In the above examples, we looked at respecting the LSP form the developer's point of view. There's another party involved though: a language's type system. A language can be designed in a type-safe way or not. Types are the building blocks to mathematically proof whether a function will do the thing you want it to do.
So, next up; we're going to look at the other side: type-safety on the language level.
To understand how type safety can –or cannot– be guaranteed by a language, let's look at these functions.
take_care(Animal) : void
take_care > feed(Animal) : void
As you can see, feed
extends take_care
and follows its parent signature one-to-one.
Some programming languages don't allow children to change the type signature of their parent.
This is called type invariance.
It's the easiest approach to handle type safety with inheritance, as types are not allowed to vary when inheriting.
But when you think back at how our example types are related to each other,
we know that Cat
extends Animal
.
Let's see whether the following is possible.
take_care(Animal) : void
take_care > feed(Cat) : void
The LSP only defines rules about objects, so on first sight, the function definition itself doesn't break any rules. The real question is: does this function allow for proper use of the LSP when it's called?
We know that feed
extends from take_care
, and thus provides at least the same contract as its parent.
We also know that take_care
allows Animal
and its sub-types to be used.
So feed
should also be able to take an Animal
type.
feed(Animal)
// Type error
Unfortunately, this is not the case. There's a type error occurring. Can you see what we're doing here? Instead of applying the LSP only to the parameters of a function, we're also applying the same principles to the function itself.
Wherever an invocation of
take_care
is used, we must be able to replace it with an invocation offeed
.
This especially makes sense in an OO language where a function is no standalone entity in your code, but rather part of a class, which represents a type itself.
To keep a system type-safe, it may not allow children to make the parameter types more specific. This breaks the promises given by the parent.
However, take a look at the following definition:
take_care(Animal) : void
take_care > feed(Organism) : void
Does this definition ensures type safety?
It may seem backwards at first, but it does.
feed
still follows the contract specified by take_care
.
It can take Animal
as an argument, and work just fine.
In this case, feed
widens the parameter types allowed,
while still respecting the parent's contract.
This is called contravariance.
Types in argument lists should be contravariant for a type system to be safe.
Moving on to return types. There are a few more types we'll have to define, in order for the examples to make sense. I'm sorry in advance for the choice of words!
Excretion > Poop
And these are the functions we're working with.
take_care(Animal) : Excretion
take_care > feed(Animal) : Poop
The question now: is the overridden return type safe? In contrast to the contravariance for the argument list, this example actually is type safe!
The parent definition take_care
tells us that this function will always return
an object of type Excretion
.
excretion = take_care(Animal)
excretion = feed(Animal)
Because Poop
is a subtype of Excretion
, we can be a 100% sure that whatever feed
returns,
it will be within the category of Excretion
.
You see the opposite rule applies for return types compared to function parameters. In the case of return types, we're calling it covariance, or covariant types.
There' no guarantee that a type-safe language will always write a bug-free program. We've seen that the language design only carries half the responsibility of respecting the LSP. The other half is the programmer's task.
Languages differ though, all have their own type system, and each will have a different level of type safety.
Eiffel, for example, allows for parameter covariance. By now you know this means there's an area of wrong behaviour possible that's undetectable by the compiler. Hence there's the possibility of runtime errors.
PHP allows for constructors of child classes to have another signature, while keeping an invariant type system for all other functions. As with many things PHP, this inconsistency increases the confusion for developers.
Some languages like Java, C# and Rust have a concept that I didn't cover today: generics. Type variance also plays a big role there. That topic is out of scope for this blog post, but I might cover it in the future.
With all these differences, there's one thing to keep in mind. The safety of a type system doesn't mean a language is better or worse. I think it's fair to say that some cases would benefit from a very strong type system, while others need the exact opposite. The key takeaway is that every programmer should learn more than just the concepts and paradigms of the languages they are used to the most. A broadened view will be beneficial, now and in the future.
So what's your opinion on type safety? If you're up for it, I'd love to talk about it even more: you can reach me on Twitter or e-mail.
]]>class Car
{
public function drive()
{
// ...
}
}
For this car to work, it needs an engine and wheels. Now, there are several approaches to achieve that goal. You could, for example, do the following:
class Car
{
public function __construct()
{
$this->engine = new Engine();
$this->wheels = [
new Wheel(), new Wheel(),
new Wheel(), new Wheel(),
];
}
public function drive() { ... }
}
There's the blueprint for every car you'll make! Next up, your boss comes to you and says there's a new client and he wants an electric car.
So you end up doing this.
class ElectricCar extends Car
{
public function __construct()
{
parent::__construct();
$this->engine = new ElectricEngine();
}
}
"Beautifully solved"—you think.
There's of course that redundant normal engine that's created when calling parent::__construct()
,
but at least you could re-use the wheels!
I think you can see where this is going.
The next client wants a car with some fancy wheel covers,
another one would like a diesel engine with those same wheel covers,
another one requests a race car,
and the last one wants a self driving car.
Oh—there also was a client who wanted to buy an engine to build a boat with himself,
but you told your boss that wouldn't be possible.
After a while, there's a ton of blueprints in your office, each describing a very specific variation of a car. You started with a neatly ordered pile of blueprints. But after a while you had to group them in different folders and boxes, because it was taking too long to find the blueprint you're looking for.
Object oriented programmers often fall into this trap of inheritance, ending in a completely messed up codebase. So let's look at a better approach. Maybe you've heard about "composition over inheritance" before?
Composition over inheritance is the principle that classes should achieve polymorphic behavior and code reuse by their composition rather than inheritance from a base or parent class—Wikipedia
That's a lot of buzzwords. Let's just look at our car example.
The principle states that Car
should achieve its polymorphic behaviour
by being composed of other classes.
The word polymorphic literally means "many shapes"
and implies that Car
should be able to do drive
in many different ways,
depending on the context it's used in.
With code reuse, we're trying to make code reusable; so that we don't end up with tens of classes doing almost exactly the same thing.
Instead of making a unique blueprint that describes every single possible variation of a car,
we'd rather have Car
do one thing, and do it good: drive.
This means it shouldn't be the car's concern how its engine is built, what wheels it has attached. It should only know the following thing:
Given a working engine and four wheels, I'm able to drive!
We could say that in order for Car
to work, it needs an engine and wheels.
In other words: Car
depends on Engine
and a collection of Wheels
.
Those dependencies should be given to the car. Or, said otherwise: injected.
class Car
{
public function __construct(
Engine $engine,
array $wheels
) {
$this->engine = $engine;
$this->wheels = $wheels;
}
public function drive()
{
$this->engine->connectTo($this->wheels);
$this->engine->start();
$this->engine->accelerate();
}
}
Would you like a race car? No problem!
$raceCar = new Car(new TurboEngine(), [
new RacingWheel(), new RacingWheel(),
new RacingWheel(), new RacingWheel(),
]);
That client who wanted special wheel covers? You've got that covered!
$smugCar = new Car(new Engine(), [
new FancyWheel(), new FancyWheel(),
new FancyWheel(), new FancyWheel(),
]);
You've got a lot more flexibility now!
Dependency injection is the idea of giving a class its requirements from the outside, instead of having that class being responsible for them itself.
Built upon this simple principle, there are frameworks and tools that take it to the next level. You might, for example, have heard about the following things before.
One of the most beneficial side effects of injecting dependencies, is that the outside context can control them. This means that you can give the same instance of a class to several others that have a dependency on that class.
Shared- or reusable dependencies are the ones most often getting the label "dependency injection". Though it's certainly a very good practice, sharing a dependency is not actually the core meaning of dependency injection.
Sometimes it's also called "inversion of control" container, though that's not an accurate name.
Whatever the exact name, the container is a set of class definitions. It's a big box that knows how objects in your application can be constructed with other dependencies. While such a container definitely has a lot of use cases, it's not necessary to do dependency injection.
To give developers even more flexibility, some containers allow for smart, automatically determined, class definitions. This means you don't have to manually describe how every class should be constructed. These containers will scan your code, and determine which dependencies are needed by looking at type hints and doc blocks.
A lot of magic happens here, but auto wiring can be a useful tool for rapid application development.
Instead of injecting dependencies into a class, there are some tools and frameworks that allow a class to ask the container to "give it an instance of another class".
This might seem beneficial at first, because our class doesn't need to know how to construct a certain dependency anymore. However: by allowing a class to ask for dependencies on its own account, we're back to square one.
For service location to work, our class needs to know about the systems on the outside.
It doesn't differ a lot from calling new
in the class itself.
This idea is actually the opposite of what dependency injection tries to achieve.
It's a misuse of what the container is meant to do.
As it goes in real-life projects, you'll notice that dependency injection is not always the solution for your problem.
It's important to realise that there are limits to the benefits of everything. You should always be alert that you're not taking this to the extreme, as there are valid cases in which a pragmatic approach is the better solution.
The core idea behind dependency injection is very simple, yet allows for better maintainable, testable and decoupled code to be written.
Because it's such a powerful pattern, it's only natural that lots of tools emerge around it. I believe it's a good thing to first understand the underlying principle, before using the tools built upon it. And I hope this blog post has helped with that.
If there are any thoughts coming to your mind that you want to share, feel free to reach out to me on via Twitter or e-mail.
Also special thanks to /u/ImSuperObjective2 on Reddit and my colleague Sebastian for proof reading this post.
]]>It turns out, the solution might be rather unexpected. Instead of disabling plugins, inspections and what not; it seems like there's an issue with font rendering in the JRE for Mac.
This means that on certain resolutions, for certain fonts and for certain kinds of antialiasing, PHPStorm will need a lot of CPU power just to render fonts. So how to fix it? There are a few options.
Subpixel
antialiasing. Go to Preferences > Appearance & Behavior > Appearance
to configure antialiasing in your editor to Greyscale
instead.
Your fonts won't look as good, but you'll notice a huge performance improvement.If you're looking for even more performance improvements that can be made in PHPStorm, take a look over here.
]]>Let's take, for example, the debate about strong types in PHP. A lot of people, including myself, would like a better type system. Strong types in PHP would definitely have an impact on my daily work. Not just strong types, I also want generics, better variance and variable types. Improvements to PHP's type system in general would have quite the impact on my programming life.
So what's stopping us from reaching a solution?
Not everyone agrees on the vocabulary used when talking about type systems. So let's clarify a few terms in the way that I will use them here.
Strong or weak types define whether a variable can change its type after it's defined.
A simple example: say there's a variable $a = 'test';
, which is a string;
you are able to re-assign that variable to another type, for example $a = 1;
, an integer.
PHP is a weakly typed language, and I can illustrate this with a more real-life example:
$id = '1'; // An ID retrieved as a URL parameter.
function find(int $id): Model
{
// ...
}
find($id);
You might think that in modern PHP, you can avoid these problems with strict types, but that's not completely true. Declaring strict types prevents other types being passed into a function, but you can still change the value of the variable in the function itself.
declare(strict_types=1);
function find(int $id): Model
{
$id = '' . $id;
// This is perfectly allowed in PHP: `$id` is a string now.
}
find('1'); // This would trigger a TypeError.
find(1); // This would be fine.
Like I said: PHP's type system is weak. Type hints only ensure a variable's type at that point in time, without a guarantee about any future value that variable might have.
Am I saying that strong types are better than weak ones? No. But there's an interesting property to strong types, they come with a few guarantees. If a variable has a type that's unchangeable, a whole range of unexpected behaviour simply cannot happen anymore.
You see, it's mathematically provable that if a strongly typed program compiles, it's impossible for that program to have a range of bugs which can exist in weakly typed languages. In other words, strong types give the programmer a stronger insurance that the code actually behaves how it's supposed to.
This doesn't mean that a strongly typed language cannot have bugs! You're perfectly able to write a buggy implementation. But when a strongly typed program compiles successfully, you're sure a certain set of bugs and errors can't occur in that program.
If you want to further explore the topic on strong and weak types, I'd recommend starting with this video by Gary Bernhardt. Not only does it go further into detail on types, Gary also discusses an important mindset in the whole types debate.
We talked about strong and weak types, what about static and dynamic types? – This is where it starts to get truly interesting.
As you're probably aware, PHP is an interpreted language.
This means a PHP script is compiled at runtime.
When you send a request to a server running PHP,
it will take those plain .php
files, and parse the text in it to something the processor can execute.
This is one of PHP's strong points by the way: the simplicity on how you can write a script, refresh your webpage and everything is there. That's a big difference compared to a language that has to be compiled before it can be run.
There is a downside though: performance. And it's not hard to pinpoint this down: the more tasks there are to do at runtime, the more impact there is on performance. One of those many tasks the PHP engine has to take care of? Type checking.
Because PHP checks the type of variables at runtime, it is often described as a dynamically typed language. A statically typed language on the other hand, will have all its type checks done before the code is executed.
Hang on – I can hear you say – what does this have to do with what PHP can be?
—We'll get to that.
Now we know what we're talking about, let's take a look at PHP's type system today.
I hope that after the theory, it's clear to you that PHP is a dynamic, weakly typed language. And there's nothing wrong with that!
On the other hand, it's interesting to note that many people are asking for a better type system in PHP. This doesn't mean we understand the implications of such a type system on PHP, yet but many of us feel that natural urge for a better type system. I'm sure that a lot of developers can point to real-life, daily situations where a better type system would actually benefit them.
To give one obvious example: the question for generics. Whether it is to ensure an array only contains one type of elements or to improve ORM abstractions, lots of people are asking for generics in PHP.
The question than becomes: is creating a more complicated type system feasible with PHP's current type paradigm? And the answer is, in part, yes—for sure. There are parts that could be improved in the current, dynamic weak type system.
Type hints for one, added in PHP 7.0 and 7.1 are useful to many PHP developers; Levi Morrison is working on generics in traits; also, there are very active discussions about the type system on the internals mailing list.
However: we're missing a very important point. As long as we're striving to improve PHP's runtime type system, we'll always be dealing with the huge performance cost it will take.
This is what Rasmus Lerdorf has to say on the topic.
Now if the RFC was a plan for baking a compile-time static analysis engine into PHP itself, that would be interesting. But that is a massive project.
— Rasmus
Imagine the possibilities when you can write PHP code that can be statically type checked before running the code. Tools like PHPStan and Psalm already do a lot of static analysis, but in my opinion it could go a step further. Say we could do this.
class List<T>
{
private array $list;
// ...
}
What if this was valid PHP code? And what if the runtime engine would just plain ignore it, and a part of PHP engine could do all the type checks, before runtime?
That's –in my opinion– a better solution than standalone tools which rely on docblocks and can't benefit from the core PHP engine, as they are written, in the case of Psalm and PHPStan, in PHP.
Don't get me wrong: tools like these are the first important step towards a bigger goal. I just think we shouldn't stop here.
The need for a better type system is clear. Lots of programmers experience a natural longing for something more than what's possible now. This doesn't only happen in the PHP community, look at modern languages like Rust, or supersets like TypeScript for JavaScript.
So maybe the answer for PHP lies into baked-in features in the core, maybe it lies in a superset that compiles to PHP with extra type checking. That last one by the way, has already been tried: Hack on HHVM.
There even is a third option, a question every programmer should ask themselves from time to time. Should we want PHP to change dramatically to match our needs, or should we change our frame of reference, and maybe look at other languages that might fit those needs better?
There's no shame in using another tool for the job, if that tools fit your needs better. And after all, isn't a programming language just that? A tool.
]]>Every pane in PHPStorm has several modes and can be configured either by hand or via key bindings.
docked
: makes a pane not overlap with other panes or the code screen.pinned
: automatically hides a pane when not pinned.floating
: makes the pane float.windowed
: makes the pane a full-blown window.split
: to allow multiple panes in one area.Working with non-pinned panes will allow for a much cleaner editor view. Binding certain panes to a key combination will show them at will.
By default, PHPStorm will only auto-import namespaces if you're already in a namespaced file.
Auto imports can be configured to also work in normal PHP files
in Settings > Editor > General > Auto Import
.
You can change almost every template of auto-generated code in Settings > Editor > File and Code Templates
For example: generate getters and setters without docblocks, generate test functions in another format and others.
-
When pressing alt + enter
(Show Intention Actions
) on a string, you'll get multiple useful actions.
Things like replace quotes
to toggle between single- and double quotes,
split string
to split the string, and more.
Two very useful commands:
Copy Paths
to copy the full path to the current file.Copy Reference
to copy the relative project path and line number to the current file.This "current file" can be the file you're editing, but could also be the selected file in the tree view or navigation bar.
Instead of opening the settings to toggle options, there are a lot of toggles you can manage from the command palette. For example: show or hide the tabs bar.
You can open the command palette with ⌘ ⇧ A
on the default Mac keymap.
If you want to lookup the keybinding on your system: the command is called Find Action
.
PHPStorm runs on Java, and there's a file in which you can specify extra options for the JVM to optimise performance. I've written about those options here.
Distraction free mode will hide all panes by default, but you can easily bring them back via the command palette or key bindings.
Besides this "no clutter by default", your code will also align more centered, which can be a much more pleasant reading experience.
The width of this centered code view is configured in Settings > Editor > Code Style > Hard wrap at
.
Do you want to know why a word is highlighted or change the colouring?
There's a command called Jump to Colors and Fonts
which will allow you to edit
the color of your current scheme, for that entry.
I'd love to hear your own tips on how to use PHPStorm. Feel free to let me know via Twitter or e-mail.
]]>The idea behind responsive images is simple: try to serve an image with dimensions as close as possible to the image dimensions on screen. This results in smaller bandwidth usage and faster load times!
For example: if you're displaying an image on a mobile device with a screen of 600px wide, there's no need for downloading that image with a width of 1000px.
The responsive images spec handles not only media queries, but pixel density too. The only thing the server has to do is generate multiple variations of the same image, each with a different size.
If you'd like to know more about how things are done behind the scenes, I'll share some links to interesting resources at the end of this post.
There are different ways to render variations of the same image. The simplest approach could be this: given an image, create 4 variations: 1920px, 1200px, 800px and 400px.
While this approach is easy to implement, it's not the most optimal. The goal of responsive images is to serve faster loading images while maintaining the highest possible quality for the user's screen.
There are two variables in this equation: the width of the user's screen (and therefore the width of the image itself) and the file size of an image.
Say you have two images with the exact same dimensions. Depending on the content in that image and the encoding used, their file sizes could differ a lot.
Another approach could be to manually define the most optimal srcset
for each image.
This is impossible to do for most websites.
A website could have lots of images,
and it's also difficult to manually calculate the dimensions for that optimal srcset
.
Luckily, computers are very good at tedious calculations on a large scale. This approach sounds like a good idea: given an image, generate x-amount of variations of that image, each variation being approximately 10% smaller in file size.
How does that sound? You now have a small margin of possible "overhead"
for variable screen sizes, but at least we're sure that margin won't be more than 10%.
Depending on the size of the image, for example: a thumbnail vs. a hero image;
we could even reduce the margin to 5% instead of 10%.
This will result in a different srcset
for every image,
but that's not our concern: the responsive images spec can handle that for us.
So how can you determine the dimensions of, say 10 variants of the same image, if you only know the dimensions of the original image? This is where high school maths come into play.
We start with these known variables
filesize = 1.000.000
width = 1920
ratio = 9 / 16
height = ratio * width
Next we introduce another one: area
area = width * height
<=> area = width * width * ratio
We say that the pixelprice is filesize / area
pixelprice = filesize / area
Now we can replace variables until we have the desired result
<=> filesize = pixelprice * area
<=> filesize = pixelprice * (width * width * ratio)
<=> width * width * ratio = filesize / pixelprice
<=> width ^ 2 = (filesize / pixelprice) / ratio
<=> width = sqrt((filesize / pixelprice) / ratio)
This proof says that given a constant pixelprice
, we can calculate the width a scaled-down image needs to have a specified filesize. Here's the thing though: pixelprice
is an approximation of what one pixel in this image costs. Because we'll scale down the image as a whole, this approximation is enough to yield accurate results though. Here's the implementation in PHP:
/*
$fileSize file size of the source image
$width width of the source image
$height height of the source image
$area the amount of pixels
`$width * $height` or `$width * $width * $ration`
$pixelPrice the approximate price per pixel:
`$fileSize / $area`
*/
$dimensions = [];
$ratio = $height / $width;
$area = $width * $width * $ratio;
$pixelPrice = $fileSize / $area;
$stepModifier = $fileSize * 0.1;
while ($fileSize > 0) {
$newWidth = floor(
sqrt(
($fileSize / $pixelPrice) / $ratio
)
);
$dimensions[] = new Dimension($newWidth, $newWidth * $ratio);
$fileSize -= $stepModifier;
}
I want to clarify once more that this approach will be able to calculate the dimensions for each variation with a 10% reduction in file size, without having to scale that image beforehand. That means there's no performance overhead or multiple guesses to know how an image should be scaled.
Let's take a look at a picture of a parrot. This image has a fixed srcset
:
This one has a dynamic srcset
:
Feel free to open up your inspector and play around with it in responsive mode. Be sure to disable browser cache and compare which image is loaded on different screen sizes. Also keep in mind that the pixel density of your screen can have an impact.
Can you imagine doing this by hand? Neither can I! One of the first features I proposed when I started working at Spatie, my current job, was to add this behaviour in the Laravel media library, its usage is as simple as this:
$model
->addMedia($yourImageFile)
->withResponsiveImages()
->toMediaCollection();
<img
src="{{ $media->getFullUrl() }}"
srcset="{{ $media->getSrcset() }}"
sizes="[your own logic]"
/>
To finish off, here are the links which I mentioned at the start of this post.
Special thanks to my colleague Sebastian for reviewing and editing this post.
]]>You can read the full message by executing the following query and inspecting the Status
column.
show engine innodb status;
------------------------
LATEST FOREIGN KEY ERROR
------------------------
2018-02-13 11:12:26 0x70000b776000 Error in foreign key constraint of table table/#sql-7fa_247a:
foreign key (`my_foreign_key`) references `table` (`id`)
on delete cascade:
Cannot resolve table name close to:
(`id`)
on delete cascade
]]>mysql -p -u root
> SET GLOBAL general_log = 'ON';
# Turning it off again when finished
> SET GLOBAL general_log = 'OFF';
First, find the mysqld
process ID.
ps auxww | grep mysql
brent 2042 0.0 0.4 2849776 67772 ?? S Fri11AM 0:16.80 /usr/local/opt/mysql/bin/mysqld
Second, use lsof
to find all files used by this process, and filter on log
.
# sudo lsof -p <PID> | grep log
sudo lsof -p 2042 | grep log
mysqld 2042 brent 4u REG 1,4 50331648 780601 /usr/local/var/mysql/ib_logfile0
mysqld 2042 brent 9u REG 1,4 50331648 780602 /usr/local/var/mysql/ib_logfile1
mysqld 2042 brent 26u REG 1,4 35 780672 /usr/local/var/mysql/mysql/general_log.CSM
mysqld 2042 brent 32r REG 1,4 0 780673 /usr/local/var/mysql/mysql/general_log.CSV
mysqld 2042 brent 33w REG 1,4 25504 9719379 /usr/local/var/mysql/HOST.log
/usr/local/var/mysql/HOST.log
is the one you want, HOST
will be the name of your host.
tail -f /usr/local/var/mysql/HOST.log
]]>Dedicating a whole blogpost to curly brackets might seem like overkill but I believe it's worth thinking about them. Not just because of one curly bracket, but because there's a bigger message in all this. Thinking about how we read and write code not only improves the quality of that code, it also increases our own and others ease of mind when working with it. It can improve the fluency of your work and free your mind to think about real important stuff. You know, things like "application logic" for example.
I wrote about visual code improvements a while back in a previous blogpost about cognitive load. Today I want to focus on that one little, yet very important character in our codebase: the curly bracket. More specifically, we're only going to look at the opening curly bracket, because there's little to no discussion about the closing one.
Let's take a look at a code sample.
public function __construct(string $publicDirectory, string $configurationFile, PageParser $pageParser, PageRenderer $pageRenderer) {
// ...
}
A constructor for a render task in Stitcher. It takes two config arguments and two objects. Depending on the width of your screen, this piece of code might be fully visible in your IDE. On this website it surely will not.
So what's wrong with this code? Well first of all, you probably have to scroll to read it. That's a bad thing. Scrolling requires an extra action for the developer to take. You'll have to consciously search for information about the arguments of this method. That time distracts you from focusing on the application code.
Second, if you're a web developer, you probably know people don't read, they rather scan. This is especially true for websites, where the biggest area of attention leans towards the left. And the same goes for reading code. Putting important information to the right makes it more difficult to find, and it also doesn't convey the same importance as things to the left.
In case of an argument list, all arguments are equally important; yet in the above example a lot of useful information is pushed to that right, dark side.
So how do we pull the useful information more to the left?
public function __construct(string $publicDirectory,
string $configurationFile,
PageParser $pageParser,
PageRenderer $pageRenderer) {
// ...
}
This could be the first thing you think about. But it doesn't really scale. As soon as you're refactoring a method name, the alignment breaks. Say we want to make this a static constructor instead of a normal one.
public static function create(string $publicDirectory,
string $configurationFile,
PageParser $pageParser,
PageRenderer $pageRenderer) {
See the alignment breaking? Another issue with this approach is that things are still pushed rather far to the right; let's take a look at another approach.
public function __construct(
string $publicDirectory, string $configurationFile,
PageParser $pageParser, PageRenderer $pageRenderer) {
// ...
}
The advantage here is that the alignment issue on refactoring is solved. However, how will you decide how many arguments should go on one line? Will you make some styling guidelines about this? How will you enforce them? This example has four arguments, but what if it had three or five?
Consistency is key. If there is a consistent rule about this, you won't have to think about it anymore. And like we said before, if you don't have to think about this, there's room in your head for more important things.
So let's continue searching for that consistency.
public function __construct(
string $publicDirectory,
string $configurationFile,
PageParser $pageParser,
PageRenderer $pageRenderer) {
$this->publicDirectory = rtrim($publicDirectory, '/');
$this->configurationFile = $configurationFile;
$this->pageParser = $pageParser;
$this->pageRenderer = $pageRenderer;
}
By giving each argument its own line, we solve the above mentioned problems. But there's still one issue with this example: it's hard to distinguish between the argument list and the method body.
Kevlin Henney visualises this problem in a simple, yet clever way. Let's replace all characters in this code with X's:
XXXXXX XXXXXXXX __XXXXXXXXX(
XXXXXX XXXXXXXXXXXXXXXX,
XXXXXX XXXXXXXXXXXXXXXXXX,
XXXXXXXXXX XXXXXXXXXXX,
XXXXXXXXXXXX XXXXXXXXXXXXX) {
XXXXXXXXXXXXXXXXXXXXXX = XXXXXXXXXXXXXXXXXXXXXXXXXXXX;
XXXXXXXXXXXXXXXXXXXXXXXX = XXXXXXXXXXXXXXXXXX;
XXXXXXXXXXXXXXXXX = XXXXXXXXXXX;
XXXXXXXXXXXXXXXXXXX = XXXXXXXXXXXXX;
}
Can you see how difficult it has become to spot where the argument list ends and the method body starts?
You might say "there's still the curly bracket on the right indicating the end". That's the thing we want to avoid! We want to keep the visual important information to the left. How do we solve it? Kevlin Henney phrased it very well:
Turns out, there is one true place where to put your curly brackets - Kevlin Henney
XXXXXX XXXXXXXX __XXXXXXXXX(
XXXXXX XXXXXXXXXXXXXXXX,
XXXXXX XXXXXXXXXXXXXXXXXX,
XXXXXXXXXX XXXXXXXXXXX,
XXXXXXXXXXXX XXXXXXXXXXXXX
) {
XXXXXXXXXXXXXXXXXXXXXX = XXXXXXXXXXXXXXXXXXXXXXXXXXXX;
XXXXXXXXXXXXXXXXXXXXXXXX = XXXXXXXXXXXXXXXXXX;
XXXXXXXXXXXXXXXXX = XXXXXXXXXXX;
XXXXXXXXXXXXXXXXXXX = XXXXXXXXXXXXX;
}
That is why it makes sense to put that curly bracket on a new line. Here's the final result:
public function __construct(
string $publicDirectory,
string $configurationFile,
PageParser $pageParser,
PageRenderer $pageRenderer
) {
$this->publicDirectory = rtrim($publicDirectory, '/');
$this->configurationFile = $configurationFile;
$this->pageParser = $pageParser;
$this->pageRenderer = $pageRenderer;
}
Now, you might not like this way of structuring your code. You might think it adds unnecessary length to a file. But take a look at the facts:
I like having this rule when coding. There's never a discussion in my head about "should I do it this way or that way"? This consistency helps me write and read my own code, and benefits other developers too, maybe even years later.
Say your function only has one parameter, should it be split on multiple lines? I personally don't think so. And if we're strictly applying the rules above, the curly bracket may be put on the same line.
However, now that we're used to that one almost-empty line between the argument list and the method body, it does seem like a nice idea to use this visual divider also for smaller functions.
XXXXXX XXXXXXXX __XXXXXXXXX(XXXXXX XXXXXXXXXXXXXXXX)
{
XXXXXXXXXXXXXXXXXXXXXX = XXXXXXXXXXXXXXXX;
}
Now we could start arguing about the placement of that closing bracket, but that's a blogpost for another time.
The question about if
, for
, while
and others should of course be addressed too.
In my opinion, the answer is simple, we can apply the same rules to them.
If the operands are pushed too far to the right, and we feel the need to split it, we do it like this:
if (
$firstCondition === $secondCondition
|| $thirdOperand === 1
|| $fourthOperand
) {
// ...
}
Finally, here is a daring thought - and I don't do this myself by the way, because following standards is also a good thing - it might make sense to apply the same rule to short control structures. After all: consistency, right?
foreach ($things as $thing)
{
// ...
}
If you're not convinced by now, I'd love to hear why! You can reach out to me on Twitter or via e-mail. I'm looking forward to discussing this further with you!
If you're looking for more to read on clean code. Feel free to browse this blog a little further. This is the best starting point.
]]>cannot create a JSON value from a string with CHARACTER SET 'binary'
You should find and replace parts of the import file with the following regex:
Find: (X'[^,\)]*')
, and replace by: CONVERT($1 using utf8mb4)
Source: StackOverflow.
]]>Parallel processing in PHP might seem like an edge case for many web developers, but let's take a look at a few use-cases we see at Spatie:
We wanted to create an easy-to-use package, yet one that could solve our use cases.
Some of the examples listed above will not use the new spatie/async
package,
because there's also a queueing system provided with Laravel.
This is how asynchronous code with our package looks like.
use Spatie\Async\Process;
$pool = Pool::create();
foreach (range(1, 5) as $i) {
$pool[] = async(function () use ($i) {
// Something to execute in a child process.
})->then(function (int $output) {
// Handle output returned from the child process.
})->catch(function (Exception $exception) {
// Handle exceptions thrown in the child process.
});
}
await($pool);
If you're into parallel PHP, you've probably heard of Amp and ReactPHP. Our package aims not to compete with those two, as it only solves one tiny aspect of parallelism in PHP; and tries to solve it in a different way.
We did however run some benchmarks to compare our package performance against Amp. Special thanks to Niklas Keller, one of the developers of Amp. He pointed out some mistakes in our previous benchmarks, and helped making them more fair.
The new benchmarks compare a few scenarios.
The first two groups plot the execution time of an empty process,
while the third and fourth groups show the execution time of processes with a different time to finish,
using several sleep
intervals.
Between the two groups, we're also comparing a capped concurrency configuration and a non-capped configuration.
Capped means that there are more processes than the pool can execute at once.
The benchmark code can be found here.
Comparing Amp and spatie/async
I tried to draw a few conclusions from these test.
We've excluded ReactPHP from the benchmarks, because it's not a fair comparison.
ReactPHP doesn't allow to run closures or Tasks
as sub-processes the way Amp and our package do.
With ReactPHP, you're working with plain processes, so there's no way to compare to it.
The biggest difference between our package and Amp is the way of communicating between processes. We're solely relying on process signals to determine when a process is finished. It allows for less overhead, but also excludes Windows as a target platform.
Processes in UNIX systems can send signals to each other.
Depending on what kind of signal is received, a process will act different.
Signals are handled by the kernel, so they are pretty low level.
Before PHP 7.1 though, you had to declare(ticks=1)
to use asynchronous signals in a reliable way.
This means that PHP will check for signals much more often, but it also introduces a lot of overhead:
A tick is an event that occurs for every N low-level tickable statements executed by the parser within the declare block. The value for N is specified using ticks=N within the declare block's directive section.
With PHP 7.1, there's a new way of handling interrupts sent by the kernel.
Zend Engine in PHP 7.1 was extended with ability of safe time-out and interrupt handling. Actually, PHP VM checks for EG(vm_interrupt) flag on each loop iteration, user function entry or internal function exit, and call callback function if necessary.
By using pcntl_async_signals(true)
, PHP will now check for signals in a much more performant way.
A more in-depth explanation can be found in the rfc,
submitted by Dmitry Stogov.
It's thanks to this mechanism that we're able to act on process status changes in a real asynchronous way, without having to rely on sockets or process status polling.
]]>As you might know, normal UUIDs are stored as CHAR(36)
fields in the database.
This has an enormous performance cost, because MySQL is unable to properly index these records.
Take a look at the following graph, plotting the execution time of hundred queries against two datasets: one with 50k rows, one with 500k rows.
That's an average of more than 1.5 seconds when using textual UUIDs!
There's an important edit here: the benchmark above was performed on un-indexed fields. I've since changed the benchmark results to work with indexed textual fields for a more fair comparison. There's still a performance gain to not using textual UUIDs, so keep reading!
Looking around for better alternatives, we found a two-part solution.
Instead of saving UUIDs as CHAR
, it's possible to store their actual binary data in a BINARY
field.
Storing them in this format, MySQL has a lot less trouble indexing this table.
This is the graph plotting a much faster result.
That's an average of 0.00008832061291 seconds per query, in comparison to ~~1.5~~ 0.0001493031979 seconds for the indexed textual UUID.
The binary encoding of UUIDs solved most of the issue. There's one extra step to take though, which allows MySQL to even better index this field for large datasets.
By switching some of the bits in the UUID, more specifically time related data, we're able to save them in a more ordered way. And it seems that MySQL is especially fond of ordered data when creating indices. There's one important thing to note: this time related bits are only available in UUID version 1.
Using this approach, we can see following result.
The optimised approach is actually slower for lookups in a small table,
but it outperforms the normal binary approach on larger datasets.
It even performs better than an AUTO_INCREMENT
integer ID!
But as you can see, we need very large tables before the optimised UUID has a benefit.
I would recommend only using UUIDs when there's a very good use case for them. For example: when you want unique IDs over all tables, and not just one; or if you want to hide exactly how many rows there are in the table.
The MySQL team wrote a blogpost explaining this bit-shifting of UUIDs in further detail. If you'd like to know how it works internally, over there is a good start.
If you're building a Laravel application and would like to use optimised UUIDs in your project, we've made a package especially for you. You'll also find more benchmark details in the README over there.
Finally, if you're looking into implementing this behaviour in a non-Laravel project, you should definitely take a look at Ramsey's UUID package, we're using it too!
]]>PHPStorm is made in Java. If you ever played Minecraft, you know you could allocate extra RAM by adding flags to your startup command. You can also do this in PHPStorm, it's even built into the UI.
Go to help > Edit Custom VM Options
. You can play around with the settings here.
I for one changed the maximum amount of RAM allocated to PHPStorm, and added two graphics options
(at the end of the file).
-Xms500m
-Xmx1500m
-Dawt.useSystemAAFontSettings=lcd
-Dawt.java2d.opengl=true
# Only for people on Mac, it makes Java use an optimised graphics engine.
-Dapple.awt.graphics.UseQuartz=true
PHPStorm also has a file to set custom properties: help > Edit Custom Properties
.
Adding one option here changed the way PHPStorm renders text: it will show text immediately,
instead of analysing it first. The downside is that you can sometimes see a flash of unstyled text.
It feels much smoother though.
editor.zero.latency.typing=true
PHPStorm is a powerful IDE, with lots of functionality built in by default. While I'd highly recommend using these options to their full extent, there are some things that are never used.
Disabling unused plugins can be a start, but disabling inspections has a much bigger impact.
Take a look at the list and decide for yourself which ones you don't need: Settings > Editor > Inspections
.
One plugin in particular has a big performance impact: IntelliLang
. This plugins allows for
languages to be recognised in different file formats. Eg. HTML autocompletion and highlighting in a PHP file.
I would not recommend completely disabling this plugin, but there might be some injections
which you don't need in your projects: Settings > Editor > Language Injections
.
Managing which files PHPStorm must index has to be done on a project level basis. It is worth spending 5 minutes on initial project setup, for projects you'll work hours and days on.
Go to Settings > Directories
to mark directories as excluded. PHPStorm won't index these files.
Directories to exclude would be eg. cache, public and storage directories;
directories which contain generated files from asset building, and last but not least: vendor
and node_modules
.
Excluding directories from indexing means no auto-complete from those directories. So excluding the vendor directory might not be the best idea. There's a neat little trick though, which allows you to whitelist vendor directories you want to use,.
Go to Settings > Languages & Frameworks > PHP
. In here you can set include paths.
By manually specifying which vendor directories should be indexed, you can eliminate a lot of indexing time.
You might eg. always keep dependencies of vendors excluded, because chances are you won't be using those APIs.
If you come across a vendor you need auto-completion for, just add it to the list.
Node modules are "excluded" by default, but they are added as include paths nevertheless.
Because of the size of the node_modules
directory, it can take quite a while to index it.
JavaScript include paths are managed like PHP includes, but in Settings > Languages & Frameworks > JavaScript > Libraries
.
I personally don't write a lot of JavaScript, so I just remove the inclusion of node_modules
completely.
Managing directories requires a bit of time for each project, but it's worth the performance gain in the long run.
There's a confirmed issue in the JRE with certain fonts. While this might seem like a minor detail, certain fonts actually require a lot of processor power to render text, slowing down PHPStorm in its whole.
I've written a separate blog post on this issue, and how you can fix it. You can read it here.
I didn't start this post by writing my own thoughts, because I figured people were looking for some quick tips to speed of their IDE. As a PHP developer, I think that PHPStorm is such a powerful tool, which helps me to write good and maintainable code. I don't want it to stand in my way though, so good performance is an absolute requirement.
With the things listed above, I feel that PHPStorm offers the best balance between performance and intelligence. I've written PHP in Sublime Text for ± 5 years. I did put some time into tweaking PHPStorm to my needs, and now I'm 100% sure I'll never go back to Sublime Text. My IDE is just way too smart and helpful to me. It allows me to focus on real application logic, instead of writing the same boilerplate code over and over again. I'll talk more about the benefits of an IDE over a text editor in another post. For now, I hope that you found these tips helpful.
Happy coding!
Ready for more? I've got a new blog post full of tips for PHPStorm users!
]]>composer require pageon/stitcher-core @beta
Note the a few config parameters are changed. These changes might fall under the category "breaking", but were really needed in order to get a more consistent API, before a real 1.0.0 release comes along.
tags in fenced code blocks.redirect.www
and redirect.https
options. Allowing to automatically redirect non-www to www, and http to https.redirect
option in site config files to make a route redirect to another page.pageon/html-meta
^2.0 from now on. Lots of tweaks to social meta tags were added.async
option which, when ext-pcntl
is installed, will enable asynchronous page rendering.target="_blank"
links by prefixing the URL with *
.sitemap.xml
support. When setting the sitemap.url
variable, a sitemap.xml
will be generated.caches.cdn
becomes cache.cdn
.caches.image
becomes cache.images
.directories.htaccess
is removed.minify
becomes engines.minifier
eninges.async
option.I can't point to some psychological study to back this claim, just my own experience and common sense. Using the mouse as less as possible when coding is a good thing. You're not moving your hands around to grab the mouse, which saves time. Also you don't have to make the mental switch between using a keyboard and a mouse as input device.
I believe these small things have the power to improve our skills as professional programmers significantly. I've experienced a lot of gain by taking the time to learn to use the keyboard as often as I can. While I'm still searching the optimal setup, I can already share some thoughts and techniques. The most important thing to know is that key bindings are a matter of personal taste. So don't take these next points as law, but rather apply them to your own situation.
Key bindings are a personal preference.
A keyboard has a few modifier keys, which allow you to modify the behaviour of other key presses. A long time ago these keys were actually hard wired in the keyboard, to change the electronic bits sent to the computer. In this modern time, it's still good to look at what their original meaning was. It helped me define a formal definition for each modifier key, allowing me to remember what key combination belongs to which action.
Define a personal meaning for each modifier key, and stick to it.
I use this key when "executing" commands. Basically most of what's possible through the menu of an application.
Alt stands for "alternate", changing the behaviour of another key combination. I use this key for a related action of another key binding.
Shift has a double meaning. First it's used for selections, because that's default OS behaviour. Second, it's also often used to reverse the action.
I prefer a maximum of two modifier keys, and if complexer combinations are needed, opt for double key bindings. One exception though: Shift (⇧) may be used in combination with other modifier keys to reverse the action.
Prefer at most two modifier keys, or use double key bindings.
I use the control key for text- and code related manipulations. Actions like moving the cursor, working with selections, working with lines, etc. I find it hard to give a formal definition for the Control key, but its use is clear in most cases.
A note for Windows users: the Control key is used much more in comparison to the Meta (Windows) key. Meaning you probably want to switch the definition of the two, or even ditch the Meta key. Even though this might seem like a good idea, adding the meta key in your workflow can be a good thing, as it adds another modifier key to your availability.
Because the function key is often not accessible on desktop keyboards, I choose not to depend on this key. I only make an exception for some edge cases like page-up or page-down.
Keeping my own definitions in mind, it's easy to start defining key bindings. Though to remember them requires practice. I'd recommend not assigning all key bindings at once, but rather slowly add them when you need them.
Assign new key bindings when you need them.
I choose not to override operating system (OS) key bindings. Things like copy
, paste
, select all
or quit
are
never overridden.
Key binding defaults provided by your IDE or editor, however, may be changed.
If you come from Sublime Text like me, you've probably learned some defaults which you are accustomed with.
When switching to PHPStorm a few years ago, I decided to keep some of those key bindings I knew from Sublime.
There's no need to change OS-level key bindings like
copy
orselect all
.
Even now, I'm still changing key bindings from time to time. Especially when I came up with my definition list. One thing I find useful when learning new key bindings, is to disable the old ones. IDEs like PHPStorm allow you to add multiple combinations for the same action. I prefer to immediately notice when I'm using an old combination. This makes me learn faster.
Remove key bindings you wish to unlearn.
Furthermore, when stuck in a situation, I try not to immediately grab the mouse. I try to think the problem and define what I want to do. Most of the time, I can remember which combination of keys should be pressed, because of the definition list above. When my memory fails me, I'm lucky to be working in an IDE with awesome key binding management, so it's easy to find the correct combination back.
Don't grab the mouse when panicking.
Your keymap is a very personal file, which slowly grows to match your workflow the best. I recommend you storing a backup of your keymap somewhere else, GitHub would be a good place. Here's mine.
Check your keymap into version control.
⌘ p
Search file⌘ ⇧ p
Search recent files⌘ ⌥ p
Search symbols in file⌘ ⌥ space
Show suggestions⌘ ⌥ enter
Go to declaration^ ⌥ →
Move right with camelHops^ ⌥ ←
Move left with camelHops⌥ ↑
Move cursor paragraph up⌥ ↓
Move cursor paragraph down⇧ ⌥ ↑
Extend selection⇧ ⌥ ↓
Shrink selectionI grew in love with key bindings over the years. I still use the mouse for basic navigation, but once I start coding, I try to use it as little as possible. I find that it's easier to work this way. Not only do I gain time by not switching as often to the mouse; I also find it puts less cognitive load on my brain, meaning I'm able to concentrate more on coding.
This might seem like a small thing to do, but as a professional programmer, you're doing those small things many, many times a day. It's worth taking the time to optimise these areas and skills, I find they make me a better programmer.
Do you want to read more about cognitive load? I've written about fonts and visuals in a previous blog post. Do you still have a question or something on your mind? Send me an email!
]]>background-size: cover;
etc., and still have the full benefits of responsive image loading.
<html>
<head>
<style>
img {
width:100%;
}
img.loaded {
display: none;
}
.responsive-image {
width:100%;
height:500px;
background-size: cover;
background-position: center;
}
</style>
</head>
<body>
<div class="responsive-image">
<img src="./small.jpg" srcset="./large.png 3000w, ./medium.jpg 1920w, ./small.jpg 425w" >
</div>
<script>
document.addEventListener('DOMContentLoaded', function() {
const images = document.querySelectorAll('.responsive-image');
[].forEach.call(images, function (imageContainer) {
const image = imageContainer.querySelector('img');
image.addEventListener('load', function () {
if (!image.currentSrc) {
return;
}
imageContainer.style['background-image'] = "url('" + image.currentSrc + "')";
image.classList.add('loaded');
});
})
});
</script>
</body>
</html>
]]>class FileReader implements \Iterator
{
private $handle;
private $current;
public static function read(string $fileName) : FileReader {
return new self($fileName);
}
public function __construct(string $fileName) {
$this->handle = fopen($fileName, 'r');
$this->next();
}
public function __destruct() {
fclose($this->handle);
}
public function current() {
return $this->current;
}
public function next() {
$this->current = fgets($this->handle);
}
public function key() {
return ftell($this->handle);
}
public function valid() {
return !feof($this->handle);
}
public function rewind() {
rewind($this->handle);
}
}
Using the file reader.
$lines = FileReader::read('path_to_large_file.txt');
foreach ($lines as $line) {
echo $line;
}
A comparison to using generators and the yield
keyword, based on the tests I ran:
In comparison to file_get_contents
: reading the same file required of 15MB of memory, whilst
this solution required only 2MB, because it only reads one line in memory at a time.
To round up, this is the generator solution using yield
.
function read($fileName) {
$handle = fopen($fileName, 'r');
while (!feof($handle)) {
yield fgets($handle);
}
fclose($handle);
}
$lines = read('path_to_large_file');
foreach ($lines as $line) {
echo $line;
}
]]>function async(Process $process) : Process {
socket_create_pair(AF_UNIX, SOCK_STREAM, 0, $sockets);
[$parentSocket, $childSocket] = $sockets;
if (($pid = pcntl_fork()) == 0) {
socket_close($childSocket);
socket_write($parentSocket, serialize($process->execute()));
socket_close($parentSocket);
exit;
}
socket_close($parentSocket);
return $process
->setStartTime(time())
->setPid($pid)
->setSocket($childSocket);
}
function wait(array $processes) : array {
$output = [];
while (count($processes)) {
foreach ($processes as $key => $process) {
$processStatus = pcntl_waitpid($process->getPid(), $status, WNOHANG | WUNTRACED);
if ($processStatus == $process->getPid()) {
$output[] = unserialize(socket_read($process->getSocket(), 4096));
socket_close($process->getSocket());
$process->triggerSuccess();
unset($processes[$key]);
} else if ($processStatus == 0) {
if ($process->getStartTime() + $process->getMaxRunTime() < time() || pcntl_wifstopped($status)) {
if (!posix_kill($process->getPid(), SIGKILL)) {
throw new \Exception("Failed to kill {$process->getPid()}: " . posix_strerror(posix_get_last_error()));
}
unset($processes[$key]);
}
} else {
throw new \Exception("Could not reliably manage process {$process->getPid()}");
}
}
if (!count($processes)) {
break;
}
usleep(100000);
}
return $output;
}
The Process
class, used to pass data in a defined way.
abstract class Process
{
protected $pid;
protected $name;
protected $socket;
protected $successCallback;
protected $startTime;
protected $maxRunTime = 300;
public abstract function execute();
public function onSuccess(callable $callback) : Process {
$this->successCallback = $callback;
return $this;
}
public function triggerSuccess() {
if (!$this->successCallback) {
return null;
}
return call_user_func_array($this->successCallback, [$this]);
}
public function setPid($pid) : Process {
$this->pid = $pid;
return $this;
}
public function getPid() {
return $this->pid;
}
public function setSocket($socket) : Process {
$this->socket = $socket;
return $this;
}
public function getSocket() {
return $this->socket;
}
public function setName(string $name) : Process {
$this->name = $name;
return $this;
}
public function getName() : string {
return $this->name;
}
public function setStartTime($startTime) {
$this->startTime = $startTime;
return $this;
}
public function getStartTime() {
return $this->startTime;
}
public function setMaxRunTime(int $maxRunTime) : Process {
$this->maxRunTime = $maxRunTime;
return $this;
}
public function getMaxRunTime() : int {
return $this->maxRunTime;
}
}
A concrete Process implementation.
class MyProcess extends Process
{
public function execute() {
sleep(1);
return true;
}
}
And bringing it all together.
$processA = async(new MyProcess());
$processB = async(new MyProcess());
$output = wait([$processA, $processB]);
print_r($output);
die('Done!');
]]>If you want to reach out, to talk about performance, or with additions to this post, you can always reach me via email.
Without further ado, let's dive into the mystical subject of web performance. We'll start discussing the mindset you should have when building performant websites. Then we'll move on to a lot of practical examples, and links to other learning resources.
If there's one thing you should take away from this post, it's the mindset every web developer should have. The industry builds tools, frameworks and systems to make the life of developers easier. All the while forgetting what web development actually is about. We're not making artisanal pieces of art anymore (maybe we never were?). We're generally aiming for fast development and quick results. We're forgetting about what matters in the end: the website and its visitors.
This post is meant for people with that mindset; people who want to become the best developer they can be. Always pushing yourself to the next level for a better end result. If you're a web developer who relates to this, understanding performance is one of the most important pillars to build upon.
That's it for the philosophical part of this post. Of course I'm completely ignoring the business side of the IT world. I'm not talking about money, time or scope here. I'm talking about improving your own development skills so that you could use that knowledge and experience in spare time projects or for real clients and work.
One of the key components to understand and improve web performance is to know how the browser renders HTML. There's a lot more to it than you might think, and understanding these steps makes you reason completely differently about your own code. Google has the best crash course on the topic: https://developers.google.com/web/fundamentals/performance/, especially the "critical rendering path" section opened my eyes.
Another important concept to understand is static HTML pages. In the end, they are what's served to the user. There's no need to generate pages on the fly, while the user is waiting to see the result. Dynamic websites abuse the user's time for the sake of easy development. Now I'm not saying dynamic websites are bad. What I do say is that every dynamic system should have the technology in place to exclude the dynamic phase from the request/response cycle. More on that topic later. If you're into real static websites, https://staticgen.com is a good place to find the right tool for your needs.
Moving on to responsive images: possibly the number one optimisation when it comes to bandwidth usage. The responsive images spec is designed to address the issue of large images, or render blocking JavaScript workarounds. It's completely backwards compatible (I'm talking to you Edge), and has a good chance of improving your website's loading time: https://responsiveimages.org.
I've already mentioned dynamic websites in the previous section. They are of course a must in the modern web; but you should think about which pages need to render things on the fly, and which could be cacheable. There are many layers of caching possible on the server side. We'll discuss eg. Varnish cache later in this post. Caching your backend code will highly depend on the kind of language and framework you're using. The most important thing to mention about caching is that you shouldn't view your cache as a layer "on top" of your application. It should be an integral part of all the code you write.
As a PHP developer, I'm used to the strict request/response lifecycle every PHP web application goes through. There are also a lot of other languages which provide the same logic for web applications. This approach is very easy to reason about, but it means the application has to be bootstrapped from scratch for every single request. Libraries like ReactPHP or AMP address this issue by enabling the developer to handle multiple requests from a single bootstrapped application. Asynchronous and parallel applications add a lot of complexity at first, and might be very difficult to wrap your head around. But it might very well mean a huge decrease in response time.
Returning to the topic of caching, there's a lot that can be done server side. First of all there are caching headers which you should definitely implement: https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Cache-Control.
Second, you should serve content that's ready to be served. Use a CDN and Varnish in front of your real server. This way you're able to serve images, content pages, etc. immediately, having been already generated before. One of the dangers of using a so called "proxy" like Varnish is that many developers might see it as that "layer on top of your own application". In reality, you'll need to communicate a lot with Varnish from within your own application. You can read more about Varnish here: https://varnish-cache.org.
The benefit of your own server? It's your server. You have control over the resources used and available. Don't put extra load on the client, when you could let your server take care of it. This is of course a very simplified way of thinking about resources. But it's always possible to upgrade your server's hardware, when you have no control over the hardware clients are using.
And lastely, if you haven't implemented HTTP/2 yet: implement HTTP/2! Not sure why? This might give you an idea: https://sitepoint.com/what-is-http2.
Disclaimer: I'm a backend web developer. I have written, and still write lots of CSS and JavaScript code, but I'm not in any way a professional when it comes to frontend web development. So I'll only use common sense and reasoning to share a few concepts of performance improvement.
You should think what resources a page really needs. If that particular page only needs 5 kilobytes out of 100 kilobytes of CSS, then don't load the other 95 kilobytes! The same goes for JavaScript.
Also think about inlining the important resources in your HTML pages, at least while HTTP/2 server push hasn't gone mainstream yet.
A good place to go from here would be Tim Kadlec's blog: https://timkadlec.com.
Lot's of things to think about. This is my personal checklist I try to keep in mind when developing websites, both professionally and in my spare time. Like I said at the beginning of this post, you shouldn't always do everything just because. But you should understand these concepts, and know when it's appropriate to use them. By doing so, you're contributing to the better web.
]]>abstract class Collection implements \ArrayAccess, \Iterator
{
private $position;
private $array = [];
public function __construct() {
$this->position = 0;
}
public function current() {
return $this->array[$this->position];
}
public function offsetGet($offset) {
return isset($this->array[$offset]) ? $this->array[$offset] : null;
}
public function offsetSet($offset, $value) {
if (is_null($offset)) {
$this->array[] = $value;
} else {
$this->array[$offset] = $value;
}
}
public function offsetExists($offset) {
return isset($this->array[$offset]);
}
public function offsetUnset($offset) {
unset($this->array[$offset]);
}
public function next() {
++$this->position;
}
public function key() {
return $this->position;
}
public function valid() {
return isset($this->array[$this->position]);
}
public function rewind() {
$this->position = 0;
}
}
A concrete implementation of the Collection
class.
class TypeCollection extends Collection
{
public function offsetSet($offset, $value) {
if (!$value instanceof Type) {
throw new \InvalidArgumentException("Value must be of type `Type`.");
}
parent::offsetSet($offset, $value);
}
public function offsetGet($offset): ?Type {
return parent::offsetGet($offset);
}
public function current(): Type {
return parent::current();
}
}
Using the TypeCollection
can be done like this.
$collection = new TypeCollection();
$collection[] = new Type();
foreach ($collection as $item) {
var_dump($item);
}
]]>The installation package, pageon/stitcher
, now loads the beta version by default. If you're running an existing project, your should also require the beta version now:
composer require pageon/stitcher-core @beta
FilterAdapter
to prevent undefined index error.init
function isn't required anymore, the constructor can now be used.browse
variable. This variable can be used to browse the detail pages.
It has a next
and prev
key which contains the next and previous entry, if there are any.Brendt\Stitcher\SiteParser
to Brendt\Stitcher\Parser\Site\SiteParser
and refactored its service definition.Brendt\Stitcher\Parser\Site\PageParser
to parse a single page, which is no longer the responsibility of SiteParser
.Imagine you have a collection of blog posts, loaded from a data source.
$posts = $blogModel->find();
Now you want to loop over every post, and do something with its data; let's say, the id
.
foreach ($posts as $post) {
$id = $post->getId();
// Do something
}
This is a scenario that happens often. And it's this scenario we'll explore to discuss why generics are awesome, and why the PHP community desperately needs them.
Let's take a look at the problems of the above approach.
In PHP, an array is a collection of… things.
$posts = [
'foo',
null,
self::BAR,
new Post('Lorem'),
];
Looping over this array of posts would result in a fatal error.
PHP Fatal error: Uncaught Error:
Call to a member function getId() on string
We're calling ->getId()
on the string 'foo'
. Not done. When looping over an array, we want to be sure that
every value is of a certain type. We could do something like this.
foreach ($posts as $post) {
if (! $post instanceof Post) {
continue;
}
$id = $post->getId();
// Do something
}
This would work, but if you've written some production PHP code, you know these checks can grow quickly, and pollute
the codebase. In our example, we could verify the type of each entry in the ->find()
method on $blogModel
.
However, that's just moving the problem from one place to another. It's a bit better though.
There's another problem with data integrity. Say you have a method which requires an array of Post
s.
function handlePosts(array $posts) {
foreach ($posts as $post) {
// ...
}
}
Again, we could add extra checks in this loop, but we could not guarantee that $posts
only holds a collection of Post
objects.
As of PHP 7.0, you could use the ...
operator to work around this issue.
function handlePosts(Post ...$posts) {
foreach ($posts as $post) {
// ...
}
}
But the downside of this approach: you would have to call the function with an unpacked array.
handlePosts(...$posts);
You can imagine it's better to know beforehand whether an array contains only elements of a certain type, rather than manually checking the types within a loop, every, single, time.
We can't do benchmarks on generics, because they don't exist yet, so it's only guessing as to how they would impact performance. It's not far-fetched to assume though, that PHP's optimised behaviour, written in C; is a better way to solve the problem than to write lots of userland code.
I don't know about you, but I use an IDE when writing PHP code. Code completion increases productivity immensely, so I'd also
like to use it here. When looping over posts, we want our IDE to know each $post
is an instance of Post
. Let's take
a look at the plain PHP implementation.
# BlogModel
public function find(): array {
// return ...
}
As of PHP 7.0, return types were added, and in PHP 7.1 they were refined with nullables and void. But there's no way our IDE can know what's inside the array. So we're falling back to PHPDoc.
/**
* @return Post[]
*/
public function find(): array {
// return ...
}
When using a "generic" implementation of e.g. a model class, type hinting the ->find()
method might not be possible.
So we're stuck with type hinting the $posts
variable, in our code.
/** @var Post[] $posts */
$posts = $blogModel->find();
Both the uncertainty of what's exactly in an array, the performance and maintenance impact because of scattered code, and the inconvenience when writing those extra checks, makes me long for a better solution.
That solution, in my opinion is generics. I won't explain in detail what generics do, you can read the RFC to know that. But I will give you an example of how generics could solve these issues, guaranteeing the developer would always have the correct data in a collection.
Big note: generics do not exist in PHP, yet. The RFC targeted PHP 7.1, and has no further information about the future. The following code is based on the the Iterator interface and the ArrayAccess interface, which both exist as of PHP 5.0. At the end, we'll dive into a generics example, which is dummy code.
First we'll create a Collection
class which works in PHP 5.0+. This class implements Iterator
to be able to
loop over its items, and ArrayAccess
to be able to use array-like syntax to add and access items in the
collection.
class Collection implements Iterator, ArrayAccess
{
private int $position;
private array $array = [];
public function __construct() {
$this->position = 0;
}
public function current(): mixed {
return $this->array[$this->position];
}
public function next(): void {
++$this->position;
}
public function key(): int {
return $this->position;
}
public function valid(): bool {
return array_key_exists($this->position, $this->array);
}
public function rewind(): void {
$this->position = 0;
}
public function offsetExists($offset): bool {
return array_key_exists($offset, $this->array);
}
public function offsetGet($offset): mixed {
return $this->array[$offset] ?? null;
}
public function offsetSet($offset, $value): void {
if (is_null($offset)) {
$this->array[] = $value;
} else {
$this->array[$offset] = $value;
}
}
public function offsetUnset($offset): void {
unset($this->array[$offset]);
}
}
Now we can use the class like this.
$collection = new Collection();
$collection[] = new Post(1);
foreach ($collection as $item) {
echo "{$item->getId()}\n";
}
Note that with this simple implementation, there's no guarantee that $collection
only holds Post
object. For example, adding a string would work fine, but would break our loop.
$collection[] = 'abc';
foreach ($collection as $item) {
// This fails
echo "{$item->getId()}\n";
}
With PHP as it is now, we could fix this problem by creating a PostCollection
class.
class PostCollection extends Collection
{
public function current() : ?Post {
return parent::current();
}
public function offsetGet($offset) : ?Post {
return parent::offsetGet($offset);
}
public function offsetSet($offset, $value) {
if (! $value instanceof Post) {
throw new InvalidArgumentException("value must be instance of Post.");
}
parent::offsetSet($offset, $value);
}
}
Now only Post
objects can be added to our collection.
$collection = new PostCollection();
$collection[] = new Post(1);
$collection[] = 'abc';
foreach ($collection as $item) {
echo "{$item->getId()}\n";
}
It works! Even without generics! There's only one issue, you might be able to guess it. This is not scalable. You need a
separate implementation for every type of collection, even though the only difference between those classes would be the type. Also note that IDEs and static analysers will be able to correctly determine the type, based on the return type of offsetGet
in PostCollection
.
You could probably make the subclasses even more convenient to create, by "abusing" late static binding and PHP's reflection API. But you'd still need to create a class, for every type available.
With all that in mind, let's just take a look at the code we would be able to write if generics were implemented in PHP.
This would be one class which could be used for every type. For your convenience, I'll only be writing the changes
compared to the previous Collection
class, so keep that in mind.
class GenericCollection<T> implements Iterator, ArrayAccess
{
public function current() : ?T {
return $this->array[$this->position];
}
public function offsetGet($offset) : ?T {
return $this->array[$offset] ?? null;
}
public function offsetSet($offset, $value) {
if (! $value instanceof T) {
throw new InvalidArgumentException("value must be instance of {T}.");
}
if (is_null($offset)) {
$this->array[] = $value;
} else {
$this->array[$offset] = $value;
}
}
// public function __construct() ...
// public function next() ...
// public function key() ...
// public function valid() ...
// public function rewind() ...
// public function offsetExists($offset) ...
}
$collection = new GenericCollection<Post>();
$collection[] = new Post(1);
// This would throw the InvalidArgumentException.
$collection[] = 'abc';
foreach ($collection as $item) {
echo "{$item->getId()}\n";
}
And that's it! We're using T
as a dynamic type, which can be checked before runtime. And again, the GenericCollection
class would be usable for every type, always.
You can read about the upcoming plugin support in this blogpost. Furthermore, I'm already working on the first plugin to support a REST API. Next step is a web interface to manage your content. For developers, Stitcher 1.0 will of course be completely useable without any plugins.
It's important to note that this update has a breaking change which existing Stitcher projects should take into account.
composer require pageon/stitcher-core 1.0.0-alpha5
A last big refactor has been done to support more extensions in the future. This means both the Console
and the DevController
now live in a different namespace. You'll need an updated version of stitcher
and index.php
. This can be done with the
following commands.
rm ./stitcher
rm ./dev/index.php
cp vendor/pageon/stitcher-core/install/stitcher ./stitcher
cp vendor/pageon/stitcher-core/install/dev/index.php ./dev/index.php
# Remove the cache dir, this might be another directory depending on your configuration.
rm -r .cache/
pageon/html-meta
.src/css
. This is useful when doing includes and IDE auto-completion.In this post, you'll read about Stitcher's plugin system. It might get a bit technical, but is definitely worth the read.
Stitcher plugins are built on top of two powerful components which already exist in many modern projects.
Using these two components, a plugin is no more than a composer package, telling Stitcher it should add its own classes and parameters to the existing services. It's a wonderfully simple concept, and it works like a charm. Like almost everything in Stitcher: the simpler, the better. Let's take a look at an example.
This is what a plugin's folder structure could look like.
MyPlugin/
├── src/
│ ├── My/
│ │ ├── MyPlugin.php
│ │ └── Service.php
├── config.yml
├── services.yml
├── composer.json
└── README.md
The only requirement for a package to be "a plugin" is a class implementing the Brendt\Stitcher\Plugin\Plugin
interface. In this example, that would be My\MyPlugin
. When this class can be autoloaded with composer, your plugin is ready!
The Plugin
interface requires you to only implement three methods. These methods tell Stitcher where the services.yml
and config.yml
files are located and how to initialise the plugin. Any other binding with Stitcher is done via the service container.
namespace My;
use Brendt\Stitcher\Plugin\Plugin;
class MyPlugin implements Plugin
{
public function init() {
return;
}
public function getConfigPath() {
return __DIR__ . '/plugin.config.yml';
}
public function getServicesPath() {
return __DIR__ . '/plugin.services.yml';
}
}
init
methodThe init
method is called after all plugin config is loaded. This method can be used as a hook to add plugin configuration to existing services. An example would be adding a command to the console application.
/**
* @return void
*/
public function init() {
/** @var Console $console */
$console = App::get('app.console');
$console->add(App::get('my.plugin.command.my.cmd'));
}
The name doesn't matter as long as its a yaml file. This file works exactly the same as other config files: key-value pairs can be added and will be available as parameters in the service container. Keys can be nested, but will be flattened when loaded. One thing to note is that plugins cannot override existing parameters.
Your plugin parameters can of course be overridden from within a Stitcher project.
# ./vendor/MyPlugin/plugin.services.yml
my.plugin:
parameter: test
Again, the name doesn't matter, but the root element must be named services
as per Symfony's requirements. You could also add parameters
here.
# ./vendor/MyPlugin/plugin.services.yml
services:
my.plugin.my.service:
class: My\Service
arguments: ['%my.plugin.parameter%', '%directories.src%', '@stitcher']
As you can see, Stitcher services and parameters are available, as well as your own.
Finally, a plugin must be loaded into your project for it to be active. The plugins
parameter in your project's config file is used for doing that.
# ./config.yml
plugins:
- My\MyPlugin
That's it!
This plugin system is so simple, yet it opens the possibility to add all kinds of functionality to a Stitcher project. It's an important step towards some of my own ideas; custom themes and other applications (API and CMS); and we'll discover more of its true strength in the future.
The most important thing for me is its simplicity. When looking at plugin systems in other applications, you'll often find complex setups like a virtual directory structure, a custom plugin loader, dirty file naming conventions, own package managers, etc. I wanted to use existing and proven technologies to build on top on, and keep the system as clean as possible. I believe this approach is a step towards the right direction.
]]>This release brings a lot of optimizations and bugfixes to many parts of Stitcher. The biggest changes are found in brendt/responsive-images
, in which a lot of bugfixes and extra options are added. Furthermore, there's one change reverted, namely the asynchronous support for image rendering. This functionality relied on several amphp
development packages, and broke with almost every update. Async support might be re-added in the future, but for now it's disabled.
One of the biggest new features is the support for custom htaccess headers and with that, HTTP2 server push! This feature has been added and is tested, but not yet used in any real projects. So there's more testing to do before declaring it "stable". You can use it in almost any template function by added the push=true
parameter.
Stitcher also uses papgeon/html-meta
now, and will build on top of this library more and more in the future.
One final new feature is the addition of the cdn
config parameter. This parameter takes an array of files, located in the source directory, and will copy them on-the-fly or during compile-time to the public directory. This way you can expose folders or files directly, without parsing them through Stitcher.
The installation package, pageon/stitcher
, still comes with 1.0.0-alpha3
by default. Feel free to manually update the composer requirement to 1.0.0-alpha4
. The default version will change as soon as HTTP/2 server push is fully tested.
Some people might need to run composer dump-autoload -o
one more time when updating to alpha4.
Before this update, Stitcher was always re-tagged on the fly when new things were added. From now on, tags will only be added after a certain feature set is complete. By doing so, updating Stitcher won't break things as much as it used to do. Keep in mind Stitcher is still in alpha phase, so breaking changes will happen now and then. There's still a small feature set to be added before a first beta release will be available. Slowly but surely, we're getting there.
]]>A short answer might be: there is no technical benefit between both. But the mindset behind the two is completely different.
I think the topic is too interesting to leave it like that. Let's talk about caching.
For many years now, we've been creating systems which help us build websites. Many of those systems are built around the idea of "dynamic websites". Instead of writing HTML and CSS pages; we've designed systems which can load information from a data source (say for example, a MySQL database); parse that data into so called "templates" (these are like blueprints for HTML pages); and finally send the rendered HTML to the client. Of course, there is more than just HTML and CSS to a website, but that's a topic for another day.
Now imagine you've got many visitors on your website, each of them visiting the same page. Rendering that page for every visit would require more server resources than to render the page once, and send that output to everyone asking it. That's a cached page. You could also cache other parts of the application. For example: not always perform the same database query, but rather cache the result of that query and reuse that over and over again.
Evidently, caching is way more than what I just described. My try at a general definition for caching on the web would be something like this.
Once a resource intensive operation is done, remember the outcome. The next time the same operation is requested, you can just give the result instead of doing that operation again.
Caching is a very powerful tool which wraps around your system, enabling it to be much more performant.
Stitcher, and all static site generators, are the opposite. These tools don't wrap around a system. Rather, their core is the HTML output. All other things needed by developers to smoothly build websites, are plugged in into that core. What's the downside? You'll have to re-render parts of your website before they are visible to the visitor. A tedious task. Luckily computers are good at performing the same tedious tasks over and over again. Re-rendering your website isn't really a bother when you have the right tools available.
Another "downside" of static websites? It requires a bit more thought of the developer. But when could that a bad thing?
So static websites do have their downsides. But take a look at the things you're able to "plug in" that HTML rendering core:
Some important things are still missing in Stitcher though.
To be clear: I don't think static site generators are the best solution for all websites. But there are lots of cases which could benesfit from using a static site generator over of a dynamic system and caching. I view many caching systems as like putting a bandaid on top of a wound, but not stitching the wound (pun intended). Don't forget that clearing caches is one of the most difficult parts of software development. But we should also be realistic: the static website approach mainly targets small to medium websites, not complex web applications.
So if you want to give it a go, be sure to check out a static site generator, there are many!
]]>Enabling the optimizer is done by updating Stitcher (1.0.0-alpha2), and adding the following parameter in config.yml
.
engines:
optimizer: true
]]><img src="{$image.src}" srcset="{$image.srcset}" sizes="{$image.sizes}" />
If you would like to read the source code instead of this post, here you go.
Like I wrote earlier, the first version of the scaling down algorithm was based on the width of images. It worked, but it wasn't solving the actual problem: optimizing bandwidth usage. The real solution was in downscaling images based on their filesizes. The problem there: how could you know the dimensions of an image, when you know the desired filesize. This is where high school maths came into play. I was actually surprised how much fun I had figuring out this "formula". I haven't been in school for a few years, and I was rather happy I could use some basic maths skills again!
This is what I did:
filesize = 1.000.000
width = 1920
ratio = 9 / 16
height = ratio * width
area = width * height
<=> area = width * width * ratio
pixelprice = filesize / area
<=> filesize = pixelprice * area
<=> filesize = pixelprice * (width * width * ratio)
<=> width * width * ratio = filesize / pixelprice
<=> width ^ 2 = (filesize / pixelprice) / ratio
<=> width = sqrt((filesize / pixelprice) / ratio)
So given a constant pixelprice
, I can calculate the required width an image needs to have a specified filesize. Here's the thing though: pixelprice
is an approximation of what one pixel in this image costs. That's because not all pixels are worth the same amount of bytes. It heavily depends on which image codecs are used. It is however the best I could do for now, and whilst I might add some more logic in the future, I'd like to try this algorithm out for a while.
So now the Responsive Factory scales down images by filesize instead of width. A much better metric when you're trying to reduce bandwidth usage. This is how the library is used in Stitcher:
use Brendt\Image\Config\DefaultConfigurator;
use Brendt\Image\ResponsiveFactory;
$config = new DefaultConfigurator([
'driver' => Config::get('engines.image'),
'publicPath' => Config::get('directories.public'),
'sourcePath' => Config::get('directories.src'),
'enableCache' => Config::get('caches.image'),
]);
$responsiveFactory = new ResponsiveFactory($config);
All images in Stitcher go through this factory, the factory will generate x-amount of variations of the image, and the browser decides which one it will download. Its pretty cool, and I hope it will help websites to serve more optimized images, while a developer can still focus on the most important parts of his project.
]]>To be clear: the goal of the responsive images spec is to reduce bandwidth used when downloading images. Images nowadays require so much bandwidth. When you think about it, it's insane to load an image which is 2000 pixels wide, when the image on screen is only 500 pixels wide. That's the issue the spec addresses, and that's the issue I wanted to solve in Stitcher.
So I want one image to go in, x-amount of the same image with varying sizes coming out, and let the browser decide which image is the best to load. How could I downscale that source image? That was the most important question I wanted answered. All other problems like accessebility in templates and how to expose the generated image files, were concerns of Stitcher itself.
My first take on downscaling images was the following:
Take the source image and a set of configuration parameters. These parameters would decide the maximum amount of image variations and the minimum width of the image. Eg. I want a maximum of ten images, with the smallest image being 300 pixels wide. Now the algorithm would loop a maximum of 10 times, always creating an image which is 10% smaller in width than the previous one.
You might already see this is not the optimal approach. After all: we're trying to reduce bandwidth used when loading images. There is no guarantee an image which is downscaled 10%, is also reduced in size. Much depends on which image codecs are used, and what's in the image itself. But by using this approach early on, I was able to implement this "image factory" with Stitcher. Next I would be working on optimizing the algorithm, but for the time being I could tackle the Stitcher integration.
Letting Stitcher know about responsive images was both easy and difficult at the same time. The basic framework was already there. So I could easily create an image provider which used the responsive factory, and returned an array representation of the image. The template syntax looks like this:
<img src="{$image.src}" srcset="{$image.srcset}" sizes="{$image.sizes}" />
Unfortunately, there is no way to automate the sizes part, unless you start crawling all CSS and basically implement a browser engine in PHP. My solution for this part is pre-defined sets of sizes. That's still a work in progress though, I'm not sure yet how to make it easy enough to use. For now, I'm just manually specifying sizes when writing template code.
But the tricky part wasn't the sizes, neither the srcset. It was handling paths and URLs. I've noticed this throughout the whole Stitcher framework: creating the right paths and URLs (correct amount of slashes, correct root directory etc.) is actually quite the pain to manage. I'm convinced by now I need some kind of helper which always renders the correct paths and URLs. It's on my todo list.
That's it for this blogpost, next up I'll be writing about optimizing the image algorithm.
]]>All four editors are multi platform, have the command palette and fuzzy finder we’ve grown accustomed to. It’s important to keep in mind that Sublime and Atom are primarily focussed on packages to provide functionality, while Brackets and Visual Studio Code provide a more all-in-one solution from the start. More about packages later, here are the most important differences out of the box.
Visual Studio Code comes with built-in GIT support, a task runner and a linter. You can start to code without having to set up anything. It’s focussed on Node and ASP.NET development, which is reflected in the tools provided. But you can use it for any other language.
Sublime Text provides a lot of themes from the start, has a built in project manager and offers many customisable keybindings and commands to do text manipulation. There are however a lot of packages you’ll want to download immediately.
Atom has a package manager shipped by default. Atom’s file tree sidebar has some very nice features such as GIT support and file manipulation (see below). There’s also a live MarkDown editor which is really neat. But like Sublime, you’ll want to install extra packages from the start.
Brackets has an awesome live preview feature which just blew my mind. Brackets is focussed on front-end web development and provides very good tools to do so. It also comes with a linter, debugger, inline editor and Photoshop integration. There’s an extension manager available too. (That’s the Adobe version of packages, more about those later).
I felt Visual Studio Code and Brackets were really just plug-and-play from the start. Both Sublime and Atom require a lot of tweaking to set everything up for the best coding experience. This isn’t a bad thing, but in this category, Visual Studio Code and Brackets are the best.
Packages (or extensions, thanks Adobe), give you access to a lot of extra features.
Brackets has an extension manager which is rather slow and bulky and has an “Adobe feel” to it. You can easily install packages from a local source, URL or an online repository. The extension manager lacks however good package documentation.
In Sublime, you’ll need Package Control if you want to easily install other packages. There’s a very wide variety of packages available there. Chances are that you’ll be able to do that one thing you like with an existing package. Browsing packages is a bit of a pain from the command palette though. There are many small undocumented packages which makes it often a guess as to what a package really does. The online documentation isn’t user friendly either. It’s mostly a huge pile of text per package.
Atom shines when it comes to packages. It has a built-in package manager which works directly with GitHub. Not only are there a lot of packages available, there’s also a very high standard on documentation. You’ll be able to see screenshots, keybinding references and even animated GIFs explaining how a package works and what it does. All from within Atom. It’s super easy to update packages and Atom will tell you when a package is outdated or uses deprecated code. It shouldn’t surprise you that Atom itself is actually a collection of these same packages.
Visual Studio Code as of VSC V0.10.1 there’s extension support, which looks a lot like Sublime’s Package Control. Because of the recent popularity of Visual Studio Code, there's a big plugin system rising.
Atom is a winner when it comes to packages. The whole system is built upon the package manager, and there’s a big community behind it. That should be no surprise, knowing that GitHub is creating this editor.
You might find it odd I list the file tree as a category. From experience though, I feel the tree is one of the most important features which can really work with or work against you. You might not use the file tree at all, but a lot of people do. So I felt it was right to talk about it here.
Sublime Text is fast and this is also reflected in the tree. It lacks however some important functionality related to file manipulation from the tree.
Brackets has a very bulky and slow tree. Opening folders and files takes a notable time. It also offers only the bare minimal tools like Sublime: new files and folders, renaming, deleting and revealing/searching files.
Visual Studio Code doesn’t have a lot more tools than Brackets or Sublime, but it allows you to move files inside the tree, which is a big help. There are some minor points though. Visual Studio Code doesn’t show tabs, but uses the tree pane to show open files. It makes this pane become cluttered and makes it difficult to find the open file you’re looking for. It’s also not possible to scroll sideways. But you can use the same pane as a search and debugger view, which is space efficient.
Atom has a lot of tree functionality: there are simple tools like copy/paste, but also cut, duplicate, rename etc. You can also move files by dragging them. Atom furthermore integrates GIT project status in the file tree. The tree might feel a bit slower than Sublime or Visual Studio Code though.
Both Atom and Sublime have great file tree features, and both lack some. Sublime can’t be beaten by speed, but Atom offers a lot more functionality. Many people don’t use the tree view in Sublime, but together with Atom’s GIT status you’ll get a good project overview by just looking at the tree.
Performance is one of the most important metrics. All of these editors are performant for sure, but each has its own small differences.
Atom lacks in this category. There are two major issues: startup time and big files. Atom is built upon web technologies (HTML, CSS and JavaScript). It has some major advantages, but takes a while longer to load. It’s however only the startup, and still considerably faster than any IDE. Once everything is loaded, Atom is as fast as Brackets. On the other side, big file loading time is a disaster. Atom will open files once you’ve selected them in the tree view. It’s easy to miss click a minified file, which will make Atom hang for several seconds or even minutes.
Visual Studio Code is a bit faster than Atom and Brackets, it works as you might expect from a Microsoft product: not slow, but also not the fastest.
Brackets is comparable to Atom, but the slow and bulky tree view makes everything feel slower.
Sublime is by far the winner here. It’s lightning fast all the time, and can’t be beaten by any other editor. Atom and Brackets loose this competition, but are still a lot faster than full blown IDEs. Another aspect to keep in mind is the amount of packages you’re using. Atom actually tells you how much milliseconds each package adds to startup time. Sublime is also subject to this: the more packages the slower. But without any doubt: Sublime shines in the field of performance.
Sublime, Brackets and Visual Studio Code offer an easy JSON config file for settings and keybindings. Brackets and Visual Studio Code even open a two column layout when editing settings, one with the defaults and one with your own. A small but convenient feature.
Atom however excels at customisability with its own stylesheet and startup script which can be hacked in any way you want. It has a built-in keybinding debugger, the Chrome developer tools, works with CoffeeScript (JS) and CSS. You don’t need to learn another language to customise Atom, it’s built upon web technologies. Furthermore, each package has its own configuration page with a lot of documentation and sometimes input fields to set parameters.
That was a lot of information! Some of the most important things summarized:
Visual Studio Code is focused on Node and ASP.NET development. It isn’t very customisable but has the Microsoft IDE feel to it. It’s an easy plug and play setup. Files are not shown in tabs, which makes it feel a bit unorganised, but I think that this is a preference and a developer can get used to this method of work.
Sublime Text has a lot of power. It’s fast and reliable. There are a lot of packages to customise your development environment, but they are often not very well documented. Sublime starts out as a text editor, but can be made the perfect, performant IDE with time and effort.
Brackets has some awesome front-end web development features like live previews, linters and PSD integration. The main downside is that it feels a bit slow, especially the file tree.
Atom is built on web technologies and its packages. It’s offers a very nice interface for packages and configuration and is “hackable to the core”. It has some quirks still with performance, but there’s a very active community working on it. Its customisability makes Atom accessible for a wide variety of programmers with their own workflow.
]]>