My PHP Issues
For the previous months I’ve been preserving a listing of issues I encountered in PHP
that I’ve discovered to be problematic, or in different phrases issues that annoy
me.
me having issues and being irritated
This isn’t my definitive listing and issues that annoy me in PHP largely rely
on the issues I’m engaged on, and for the previous month I’ve been engaged on:
- Phpactor: PHP language server
- PHP-TUI TUI framework and port of Rust’s Ratatui.
- CLI Parser: me messing about creating a brand new CLI argument parser
- Work Project: giant E-Commerce undertaking primarily based on Spryker – my present day contract.
There are many issues I like in PHP and among the factors that comply with
would annoy me in different languages too!
Constructors ¶
It is a unusual one, but one which bothers me. I clearly see the necessity for
static constructors, however I additionally cringe when utilizing them unnecessarily. Ought to I
use static constructors for every little thing, a particular subset of objects or
introduce them solely when required?
What’s the massive deal you ask? Consistency is the deal. I don’t need to have
to kind new
solely to understand there the category has a non-public constructor or that there are
static constructors which I must be utilizing, however I additionally don’t need to
introduce pointless indirection fo the sake of consistency:
<?php
// instantiation with the brand new key phrase
new Foobar(['foo1', 'foo2']);
// static new
Foobar::new(['foo1', 'foo2']);
// devoted constructor with variadic
Foobar::fromFoos(...$foos);
Langauges equivalent to Rust and Go would not have this downside, primarily as a result of they
don’t have the new
key phrase!
Each languages function “structs” which might be created instantly and not using a
constructor and each have unwritten conventions on utilizing constructor capabilities
(in Rust they’re conventionally connected to the struct – equally to static
constructors in PHP).
The drawback of bypassing (or just not having) a constructor is that you just allow the
“unsupervised” creation of the info construction – you may’t management and implement
the business invariants. Nevertheless that is mitigated in each languages as they
each have bundle stage visibility and a powerful kind system.
Am I suggesting we abolish the new
key phrase and undertake higher varieties and
bundle stage visibility? Sure? No? Perhaps? I don’t know. The reality is it’s
simply one thing that bugs me.
Annotations vs. Attributes ¶
Our static evaluation instruments use annotations:
class Foobar
{
/**
* @var Foobar[]
*/
public array $foobars;
}
That is painful when it’s essential to use this metadata in different contexts:
class FoobarDTO
{
/**
* @var Foobar[]
*/
#[Collection(type: "Foobar")]
public array $foobars;
}
So our tooling can swap to attributes? Let’s have a look at a generic kind outlined
with an annotation:
<?php
class FoobarDTO
{
/**
* @var Foobar<string,Closure(string):int>
*/
public Foobar $foobar;
}
In attribute land this turns into:
<?php
use PhpType{GenericClassType,StringType,IntType};
class FoobarDTO
{
#[GenericClassType('Foobar', [
new StringType(),
new ClosureType(new StringType(), new IntType()),
])]
public Foobar $foobar;
}
Is that higher? In fact not, it’s HORRIBLE. We’re importing varieties for
varieties. We may additionally imagine:
<?php
use PhpType;
class FoobarDTO
{
#[Type('Foobar<string,Closure(string):int>')]
public Foobar $foobar;
}
This may at the very least allow decrease the barrier for sharing this metadata,
though from a utilization standpoint it’s arguably extra cumbersome than an
annotation, it’s nonetheless annoying.
Generics would clear up a lot of this ache, however it’s tricky.
One answer that has been mentioned is extending the PHP parser to simply accept
(but ignore) generic annotations
purely for the sake of static evaluation instruments, for instance:
class Whats up
{
public Foobar<string,Closure> $foobar;
}
This may enable the array<Foobar>
syntax, and perhaps we will even get away with different unique varieties like Closure
:
<?php
use PhpType;
class FoobarDTO
{
public Foobar<string,Closure(string):int> $foobar;
}
I like this! The PHP engine at runtime will solely see
Foobar
however the Reflection API will present entry to the “wealthy” varieties
facilitating static evaluation instruments and serving to to remove lots of the
incidental issues now we have within the ecosystem.
No Nested Attributes ¶
Whereas engaged on a prototype for
PHPBench I used to be experimenting with
permitting customers to compose benchmarking pipelines.
PHPBench must analyse recordsdata which can not even have the identical autoloader as
the principle course of:
<?php
use PhpbenchxInstructionsIterations;
use PhpbenchxInstructionsPhpSampler;
closing class TimeBench
{
#[Iterations(10, new PhpSampler(reps: 10, warmup: 1))]
public perform benchTime(): void
{
foreach ([1, 2, 3, 4] as $b) {
foreach ([1, 2, 3, 4] as $b) {
usleep(1);
}
}
}
Good! However there’s a catch. The Iterations
attribute is only a class identify. We
can replicate the identify utilizing native reflection as a result of, it’s simply a
identify. The new PhpSampler
nonetheless is a worth and can invoke the
autoloader and fail as a result of PHPBench doesn’t essentially exist in that
autoloader.
Nested attributes would look one thing like this I assume:
closing class TimeBench
{
#[Iterations(10, PhpSampler(reps: 10, warmup: 1))]
public perform benchTime(): void
{
// ...
}
}
This may enable PHPBench to learn the attributes even when they didn’t exist in
the opposite course of.
what’s that? you say this method is flawed?, and also you’re in all probability proper, however
it might nonetheless be good if refecting “nested” attributes didn’t require the
autoloader.
See the
RFC for the the reason why
nested had been excluded from the ultimate implementation.
Serialization/deserailization ¶
That is one thing that didn’t actually hassle me till I used Go and Rust.
Deserializing byte streams to things is our each day bread. Whether or not or not it’s HTTP
requests or RPC messages. We have to ingest bytes and map them to information
constructions.
In PHP we begin with:
<?php
$userData = $_POST['user'];
$person = new Person(
$userData['firstName'],
$userData['lastName']
);
If you happen to’re fortunate there could also be even be some if (!array_key_exists
and even
Assert::arrayHasKey
however most of the time we see folks living on a
prayer and simply assuming that
every little thing will kinda work out.
We then have
serialization libraries equivalent to JMS
Serializer and later the Symfony
Serializer. This
is a large enchancment, however each libraries are complicated.
Perhaps I used to be burned by JMS serializer earlier in my profession, and I
nonetheless have nightmares about debugging the Serializer
stack in API Platform.
I don’t instinctively attain for these instruments after I’m writing a software and
as an alternative wrote my very own easy library to deserialize into
objects as a result of I needed to do that:
$config = Invoke::new(Config::class, $config);
My library has no different API. It maps to an object field-for-field through.
the constructor and throws helpful exceptions if values have the improper
varieties, are lacking or if there are additional fields. (it has some severe
limitations too, and I wouldn’t suggest utilizing it in your initiatives).
Much more not too long ago now we have Valinor which
parses kind annotations utilized by PHPStan and Psalm, together with generics. Even
extra not too long ago now we have Serde which has been
created by any individual who clearly feels my ache.
Valinor might be my favorite library because it doesn’t require you to
duplicate your kind definitions with annotations and your DTOs might be
utterly agnostic of the serialization library.
Let’s have a look at deserializing a Strava Activity in Rust:
#[derive(Serialize, Deserialize)]
pub struct Exercise {
pub id: i64,
pub title: String,
pub activity_type: String,
pub description: String,
pub distance: f64,
pub average_speed: Possibility<f64>,
pub moving_time: i64,
pub elapsed_time: i64,
}
let exercise: Exercise = serde_json::from_str("/** json payload */".as_str())?;
With Valinor:
<?php
class Exercise {
public int $id;
public string $title;
public string $activity_type;
public string $description;
public float $distance;
public ?float $average_speed;
public int $moving_time;
public int $elapsed_time;
}
$mapper = (new MapperBuilder())->mapper();
$exercise = $mapper->map(Exercise::class, Supply::json('// json payload'));
Not a nasty comparability! In truth it’s even potential that Valinor, because it now additionally helps
serialization,
SOLVES this concern for me. However till I can show it in any other case,
serialization/deserialization in PHP nonetheless annoys me however hey, at the very least
it’s not Node.
Promoted properties are good, let’s use one!
class Foobar {
public perform __construct(non-public Foobar ...$foobars) {}
}
Oops, can’t use variadics in promoted properties. Why!?! See generics.
Iterator to Array: Protect Keys ¶
I get bitten by this over and once more, yield from
to yield
from
one other generator:
<?php
perform one() {
yield 'bar';
yield from two();
}
perform two() {
yield 'bar';
}
$bars = iterator_to_array(one());
var_dump($bars);
Exhibits:
array(1) {
[0]=>
string(3) "bar"
}
After which passing the second argument as false
:
<?php
// ...
$bars = iterator_to_array(one(), false);
I get reply I used to be anticipating:
array(2) {
[0]=>
string(3) "bar"
[1]=>
string(3) "bar"
}
Why? as a result of false
is preserve_keys
.
Why does this hassle me? As a result of through the years I assume the improper default
conduct, and after realising my error I move true
right here.
Am I saying that is improper? Are my instincts improper? Am I the issue? I don’t know. It simply
irritated me.
Iterators vs. Arrays ¶
Why will we even must name iterator_to_array
! Why can’t array_map
and
pals settle for an iterator?
<?php
$assortment = fetch_a_penguin_collection();
var_dump($assortment::class);
// PenguinCollection
$assortment = array_map(iterator_to_array($foobars, true|false), perform (Penguin $penguin) {
return $penguin;
});
var_dump(get_type($assortment));
// array
Effectively, it might appear that there’s multiple technique to pores and skin an iterator and
implicitly mapping an iterator to an array doesn’t actually make a lot sense.
Ought to or not it’s allowed to move iterators to array capabilities? No, in all probability
not. Has it bugged 🐞 me repeatedly? Sure it has. Am I improper to be bugged? 🤷
Brief closures can not have statements ¶
I like quick closures! However I discover my self changing my beloved quick closures again to lengthy closures
each time:
- I would like so as to add an announcement
- I must debug it[*]
I’d a lot favor to benefit from the quick syntax whereas additionally with the ability to have
a number of statements:
$func = fn($foo) => {
echo 'hey';
echo 'world';
return 'goodbye';
}
This may be higher, and sure it might probably seize variables routinely and no
that’s not complicated.
debugger on a regular basis
There may be one other nice atricle here which broadly argues for multi-line
closures.
Assertion Blocks in Normal ¶
And why cease there?
<?php
$foo = match ($bar) {
'foo' => {
$a = 1;
return $a;
},
};
and even arbitrary scoping like in Rust?
<?php
$foo = 0;
{
$bar = 1;
$foo += $bar;
}
assert(false === isset($bar));
Capabilities that return false ¶
Now we’re attending to the nice previous stuff.
<?php
$worth = json_decode('this isn't legitimate json');
var_dump($worth);
// NULL
$worth = json_decode('null');
var_dump($worth);
// NULL
So … json_decode
returns NULL if there’s an error, but it surely additionally returns
NULL if the worth is err. null
(a legitimate JSON string).
We are able to move flags: JSON_THROW_ON_ERROR
to each, and get a extremely nice an
informative error:
Deadly error: Uncaught JsonException: Syntax error
What about file_get_contents
?
<?php
var_dump(file_get_contents('this/doesnt/exist'));
// false
I like the scent of false
within the morning, however regardless of that I do want that
all PHP’s built-in capabilities threw exceptions. We now have the well-known
safe library which does simply that!
However when you’re like me then you definitely don’t like coupling large quantities of code in
perpetuity to an exterior library.
Is there something we will do about this with out breaking all of the code?
declare(throw_exceptions=true)
perhaps? in all probability not 🤷. If all capabilities
threw exceptions nonetheless I’d be much less irritated.
Inline Lessons ¶
In Go you may effectively declare structs inside structs to create
deep information constructions:
struct Foobar {
Foobar struct {
Foo string
Bar string
}
}
In PHP you may’t and we have to declare them individually:
class FooAndBar {
public string $foo;
public string $bar;
}
class Foobar {
public FooAndBar;
}
Would this be a good suggestion?
class Foobar {
public class {
public string $foo;
public string $bar;
} $foobar;
}
I don’t know. However it might certain make some issues simpler.
Conclusion ¶
It is a subjective submit about issues that annoy me, among the
factors are invalid and for certain folks with way more context and mind energy
than I’ve have thought-about them. It is usually to be anticipated that I take for
granted issues that may annoy different folks.
If I had to decide on one factor to repair in PHP it might be generics assist.
Of the 11 annoyances 3 of them can be solved by generics. Generics assist,
even by kind erasure, would, I feel, take the language to the following stage.
I nonetheless take pleasure in PHP compared to another languages, and it actually has
sensible some benefits over Rust and Go and I’m excited to see it evolve
extra!