读《PHP的那些奇葩设计》:5年老PHPer踩过的两个最坑的语言特性

读《PHP的那些奇葩设计》:5年老PHPer踩过的两个最坑的语言特性

导读

This article is written by a developer with 5 years of PHP experience, sharing two of the most unintuitive design flaws in PHP that have caused countless production bugs, while also arguing that the widespread negative reputation of PHP is mostly outdated. Modern PHP is already a mature general-purpose language, and we can avoid most pitfalls as long as we understand its unique characteristics.

这篇文章来自一位有5年PHP开发经验的程序员,他分享了PHP里两个最反直觉、曾经给他带来过无数线上Bug的设计缺陷,同时也认为现在行业里对PHP的负面评价大多是过时的刻板印象——现代PHP已经是很成熟的通用语言,只要摸透它的特性完全可以避开绝大多数坑。

I've been coding in PHP at work for the last 5 years. My org's entire backend is written in PHP—a decision made in 2007 when the company first started. It's not a language I ever imagined myself using prior to working there, but life takes you in all sorts of directions you don't expect.

我在工作中已经写了5年PHP,我之前所在公司的整个后端都是用PHP写的,这个技术选型是2007年公司刚成立的时候定的。入职之前我从来没想过自己会用PHP写代码,但人生总就是会往你意料之外的方向走。

Article illustration

PHP gets a bad rep in the industry, despite being a mature and capable language. But it's mostly based on out-of-date understanding of what PHP can do. Recent versions have caught up with most other languages in terms of features; by this point it's a pretty versatile general-purpose language. Certainly not *just* for serving HTML, as it was originally designed.

现在行业里对PHP的评价很差,但它其实已经是个很成熟好用的语言了。这种坏名声大多是基于大家对PHP能力的过时认知,新版本的PHP在特性上已经追上了绝大多数其他语言,现在它已经是个用途非常广泛的通用语言,早就不是当初那个只用来生成HTML的工具了。

I'm no longer working at the aforementioned company, so I'm reflecting on my experience with PHP after all these years and there's some things I've always found odd about it.

我现在已经从之前那家公司离职了,所以刚好可以复盘一下这些年用PHP的经验,里面有不少设计我一直觉得特别奇怪。

And more than just odd, some of it's language features are really unintuitive and have been prone to cause bugs. This comes from personal experience and many previous headaches at work. I'll explain two of the biggest offenders in this post—in short:

不止是奇怪,有些语言特性甚至非常反直觉,特别容易引发Bug,这都是我之前在工作中踩过无数坑得出来的经验。这篇文章我就来讲讲里面最坑的两个点:

1. Arrays are weird and overloaded

1. 数组设计得过于重载,非常怪异

2. The type system is clunky

2. 类型系统设计得很笨拙

数组根本不是真的“数组”

PHP's standard library basically only has one data structure: the `array`. This was intentional; it was designed to be a general-purpose, flexible data structure that can cover a variety of use cases. It's technically an *ordered key-value dictionary*, not an array in the traditional computer science sense.

PHP标准库基本上只有一种数据结构:`array`。这是故意设计成这样的,它的定位就是通用、灵活的数据结构,能覆盖各种使用场景。严格来说它其实是“有序键值字典”,根本不是传统计算机科学里定义的数组。

Article illustration

Unfortunately, with flexibility comes complexity. If you want to create a collection of fixed-size objects in an allocated memory block, you can't really do that. PHP pretends to support them, but the illusion breaks down in unexpected ways.

但问题也出在这种灵活性上:如果你想创建一个内存连续的固定大小对象集合,PHP根本做不到。它表面上看起来支持普通数组,但你用的时候总会在意想不到的地方打破这种假象。

Let's say I have a bunch of fruits. PHP let's me define a fruits "array" and I can do normal array things with it.

举个例子,比如我有一堆水果,我可以在PHP里定义一个水果“数组”,也能用普通数组的操作来处理它,看起来一切正常。但只要你对这个“简单数组”做修改,它键值存储的本质就会暴露出来。

When you use one of PHP's built-in functions for standard array operations like sorting or filtering, it will operate on the keys AND values of your array. If it mutates the array in-place or by a return value, the key order will likely become inconsistent.

当你用PHP内置的排序、过滤这类标准数组操作的时候,函数会同时操作数组的键和值。不管是原地修改还是返回新数组,最终的键顺序大概率都会变得混乱。

The only way to put these arrays back into a naturally indexed state is to use the `array_values()` function. You just have to know that, or else you end up with subtle bugs.

想要把数组变回正常的自然索引状态,唯一的方法是调用`array_values()`函数。你必须记住这个冷知识,不然就会踩很多难以排查的隐性Bug。

**It's just strange to me that PHP doesn't support simple collections of objects. It's annoying to have to manage these arbitrary numeric keys when all you really want is ordinal indexing like 99% of the time. It feels like a leaky abstraction.**

我实在想不通为什么PHP不支持简单的对象集合,99%的情况下我们只需要普通的顺序索引,却还要额外去管理这些混乱的数字键,这个抽象设计的漏缝实在太明显了。

类属性类型特别让人困惑

In PHP5, a native type system was added to the language. It was expanded over time and by PHP7 you could define the types for your class's properties. Although PHP is a scripting language, type declarations will help catch bugs during testing, or even during development with the use of static analysis tools like *PHPStan*.

PHP5的时候才加入了原生类型系统,后续不断扩展,到PHP7的时候已经可以给类属性定义类型了。虽然PHP是脚本语言,但类型声明可以帮我们在测试阶段发现Bug,配合PHPStan这类静态分析工具甚至在开发阶段就能提前排错。

But PHP's type system has some quirks since it was built on an existing dynamically typed language. The rules had to be designed *after* the behaviour was already there. For class properties, there's a hidden uninitialized state that can pop up if you're not careful.

但PHP的类型系统有不少奇怪的地方,毕竟它是在已有的动态类型语言基础上补出来的,规则都是在现有语言行为之后才设计的。比如类属性就有一个隐藏的“未初始化”状态,稍不注意就会踩坑。

Let's define a `Book`类有三个`string`属性:我这里演示了声明字符串属性类型的三种方式:1. 不声明类型;2. 声明为string类型;3. 声明为可空string类型。

PHP7之前所有类属性都是第一种:无类型。因为类型系统是可选的,它必须和旧的行为兼容,这就带来了很奇怪的后果。比如你觉得实例化一个Book对象之后,这三个属性的值会是什么?

Trick question! Only the untyped `$title` property will have a value, and that value is `NULL`. That seems fine and is roughly in line with how I'd expect a language to use a `NULL` value. But the other two properties will NOT have a value because they don't exist, or rather they could exist but haven't been initialized yet.

这是个陷阱题!只有没声明类型的`$title`属性会有值,值是`NULL`,这看起来还比较符合我们对NULL的预期。但另外两个属性根本没有值——它们甚至可以说不存在,或者说还没有被初始化。

**This example exposes the "uninitialized" state that a property can be in, which is NOT the same as `NULL`. This distinction frustratingly comes up when you try to do a null check on these properties:**

这个例子暴露了类属性的“未初始化”状态,它和`NULL`根本不是一回事。最让人头疼的是,当你想对这些属性做空值检查的时候,这种区别就会带来大麻烦:如果你尝试访问未初始化的属性,不会只是警告,而是直接报致命错误。这种情况在你把数据反序列化为PHP对象的时候特别常见,如果某个字段的数据不存在,对应的属性可能根本不会被初始化。

This lax behaviour for property definitions makes writing code around them harder. Especially when you take into account that any object can have properties dynamically added to them:

这种宽松的属性定义行为让相关代码的编写难度大了很多,更何况PHP的对象还可以动态添加属性。

**So I feel like the class property type system does little to help you understand what a given object is composed of, and in some respects has made it *less* clear because it's introduced this new uninitialized state.** As a developer, it's hard to write defensive code because you're never sure which checks to do for all these situations: `is_null()`, `isset`(), `property_exists()`, `empty()`... it's not obvious which functions cover which states.

所以我觉得类属性类型系统根本没帮我们搞清楚对象的结构,某种程度上反而更混乱了,因为它引入了新的未初始化状态。作为开发者,我们很难写出防御性代码,因为你永远不知道该用什么方法做检查:`is_null()`、`isset()`、`property_exists()`、`empty()`……根本搞不清哪个函数对应哪种状态。

I'd argue that uninitialized did not need to be a state at all. For nullable typed properties, just default them to `null` the way untyped properties are. And for non-nullable types, require them to be be defined in the constructor OR require a default value at declaration. Similar requirements already exist for the `readonly` attribute, so it's certainly feasible for the PHP execution engine to enforce it.

我觉得根本不需要有“未初始化”这个状态。可空的类型属性完全可以像无类型属性一样默认设为`null`,而非空类型可以要求必须在构造函数里赋值,或者声明的时候给默认值。现在`readonly`属性已经有类似的强制要求了,所以PHP引擎完全有能力实现这种约束。

总结

Despite all the critiquing I've done in this article, I still think the amount of hate PHP gets is undeserved. Like any language, it has it's quirks and tradeoffs, but you can still accomplish any task using PHP that you could in another language. The more you know about a language, the better you can structure things to work "with the grain" and write more idiomatic code.

虽然我这篇文章吐槽了PHP这么多,但我还是觉得它收到的恶意太多了,有点冤。和所有语言一样,它有自己的怪癖和取舍,但你用PHP能完成其他语言能做的任何任务。你对一门语言了解得越深,就越能顺着它的特性做设计,写出更地道的代码。

Some things I *do* enjoy about PHP:

我也很喜欢PHP的这些优点:

1. It's a scripting language, so development friction is low. Make a file change and it instantly takes effect.

1. 它是脚本语言,开发成本很低,改完文件立刻就能生效。

2. Laravel is a solid web framework with tons of extensible functionality. It's opinionated and definitely leans into the "auto-magical" framework style, but it was设计得很好,用起来很舒服。

3. All the \$ signs help remind you what you're doing it all for at the end of the day 🤑

3. 代码里所有的$符号时刻提醒你,你写代码是为了赚钱嘛,这不香吗?🤑


来源:https://flowtwo.io/post/php's-oddities