The type system of PHP is no longer fit for modern web programming

PHP, like Javascript, started as a scripting language for the web, with PHP for server side and Javascript for client side. They were initially designed to add simple, dynamic elements into static web pages in an era when processing power was just a fraction of today, so they were designed to be easy-to-use by people with minimal programming knowledge, for example, they allowed the use of undeclared variables without specifying types, and the operators would try to intepret the operands in their expected type even if a different type was given. For example, in PHP, '3'+'4' results in 7, which makes it convenient to deal with HTML form inputs, which are always strings.

Such “features” designed for ease of use in simple functionalities are clearly not fit for purpose when full-featured applications with user interactions (Web 2.0), not just content-delivering websites (Web 1.0), are built and run wholly on the web. Therefore, over time, both PHP and Javascript gain features for “proper” programming. For example, in PHP, closures, namespaces, constant expressions, null coalescing operator, type declarations, union types, intersection types are gradually added over time, with the severity of bad programming practices gradually upgraded from Notices to Warnings to Errors; while in Javascript, strict mode, modules, async / await, null coalescing operator, and more built-in higher-order functions are added as well.

Despite that they are copying features from each other, and from other programming languages, they have used a completely different approach to enhance their type safety.

Type safety in PHP

In PHP, all values are of one of the following types: the primitive types null, int, bool, float, string; or the composite types array, object; or the special resource type which is specific to a certain library and can’t be defined natively. Values of user-defined classes are objects, with stdClass being the class of objects created without specifying a specific class, such as a type cast to an object.

Type declarations

PHP, like Javascript, did not historically have type declarations. Starting from PHP 5, with the improved class system, it started to become possible to specify types in the parameter list. However, at that time, only classes could be specified. It was not possible to specify that a function parameter should be an int.

PHPDoc, a library used to generate documentations for the language, can be used to add “type hints” which are checked by an external type checker, such as PHPStan or an IDE, but as comments they are not enforced by the runtime.

PHP 7.0 added the ability to specify primitive types in function parameters and return values – they are called “type declarations”. There are two possible operating modes: “strict mode”, where an error is produced if the value does not match the declaration, and the default mode, where a value is implicitly converted to the declared type. PHP 7.4 extended the type declarations to class members. However, the range of supported types in type declarations was extended gradually, and it still isn’t possible to specify all possible types in type declarations:

  • Support for the object type, without a particular class, was only added in PHP 7.2.
  • Support for the null type, as a standalone type, was only added in PHP 8.2.
  • It is still not possible to declare a function to accept a particular resource type, although the plan is to deprecate the resource type altogether by class types defined in the library.

There are also some other pseudo-types usable in type declarations, which do not correspond to a real type:

  • true, false – the pseudo-type false was added in PHP 8 to indicate that a function can accept or return false in addition to the other specified type, however, they, and the pseudo-type true, can only be used standalone since PHP 8.2. It is not possible to specify any other specific values apart from true, false or null (a unary type).
  • void – a pseudo-type which specifies that a function does not return a useful value. Under the hood, null is actually returned, but it is a syntax error to specify a value in the return statement in a void function, and IDEs can produce a warning when a return value of void is used.
  • never – a pseudo-type which contains no values, called the “bottom type” in type theory. It indicates that a function does not return normally.
  • self, parent, static – used to specify relative classes in the class hierarchy.
  • callable – a pseudo-type which represents anything which can be called. Internally it can be either a
    • string, representing the name of a function – before closures were added in PHP 5.3, we had to define a named function to pass it into higher order functions such as array_map
    • array with the object at index 0, and the name as a string of the method to be called at index 1, representing a method to be called on a particular object
    • object containing a method called __invoke, including Closure objects resulted from anonymous functions.
  • mixed – a pseudo-type which encompasses everything, the top type in the type theory. PHPStan by default suppresses type checking on mixed types (analogous to TypeScript’s any, or Dart’s dynamic), but at level 9, the only operation allowed is to assigned it into another mixed variable (analogous to TypeScript’s unknown, or Dart’s Object?).
  • iterable – a pseudo-type for values usable in a foreach loop, i.e. arrays and Traversables.

It is not possible to specify type aliases in PHP.

PHP 8.0 added union types, and PHP 8.2 added intersection types. However, it is still not possible to specify the parameters and return types of higher-order function types or generic types in the language, which makes it pretty useless for declarative programming techniques involving heavy use of higher-order functions such as array_map, array_reduce or array_filter.

Because of the above limitations, PHPDoc, along with its cumbersome syntax, is still needed for the majority of cases involving complicated types, generic programming and higher-order functions, to achieve full type safety by a static type checker.

Type safety in Javascript

In Javascript, all values are of one of the following types: the primitive types null, undefined, number, bigint, string, symbol, or the object type. Javascript uses prototype-based inheritance which a class-based inheritance can be implemented on, and is done in ECMAScript 6. In Javascript, the class of an object is represented by its constructor function, while the class itself a function which can be called to create an instance of the class. I actually made a class where an instance of the class is a subclass of it.

Type declarations

The current version of ECMAScript does not support type declarations, however, a proposal is made to add syntactic support for type declarations, but not runtime type checking, like in the case of Python, which allows external type checkers to be used. Currently, JSDoc is used to add type hints as comments for external type checkers including IDEs, like PHPDoc in PHP, therefore, they share the same problems including cumbersome syntax, and non-integration to the language syntax. It is called “Typed Javascript” for some people.

TypeScript

The most popular way to add type declaration into JavaScript is called TypeScript. It is a near-superset, but not a strict superset, of JavaScript, which adds type declarations into the language. TypeScript code needs to be compiled into JavaScript before it can be used by a JavaScript runtime, while most JavaScript code can be interpreted as-is by a TypeScript compiler.

When compiling TypeScript into JavaScript, under most circumstances, the type declarations are simply removed, which forms the basis of the type declarations proposal above, although there are some TypeScript-specific features, e.g. enums, which generates Javascript code.

The type system of TypeScript supports the full set of operations you would expect to be able to do, for example, not only can it represents primitive and class types, it can also be used to specify the internal structure of an array type with differing elements, or higher-order functions with generic types, required for type safety when doing functional programming. In fact, it is possible to do arithmetic using the type system, and taking the matter to the extreme, the type system in TypeScript is Turing-complete, meaning that it can compute everything which is computable.

For example, in TypeScript, Array<T>.find has signatures of

find<S extends T>(predicate: (value: T, index: number, obj: T[]) => value is S, thisArg?: any): S | undefined
find(predicate: (value: T, index: number, obj: T[]) => unknown, thisArg?: any): T | undefined;

In plain terms, the first line means that find accepts a predicate function, which accepts a value of type T, an index of type number, and an obj of type T[], and that function acts a type guard to confirm that the value is of type S, which must be a subtype of T. In addition, find also accepts an optional thisArg parameter which can be anything. It will return type S or undefined. The second line is the corresponding version where predicate isn’t a type predicate, in such case, it simply return type T or undefined.

The only drawback of using TypeScript is that, a build step is needed to compile TypeScript into JavaScript, but for any non-trivial JavaScript projects, a build step normally already exists to transpile / minify JavaScript for production use, making the integration of TypeScript trivial.

Server-side JavaScript

Although JavaScript was initially created to run code in the web browser, it is possible to run it on the server, just like any other programming languages. Node.js is a standalone runtime for JavaScript which can be installed on a server, while Express.js is a framework to run server-side applications with Node.js. As such, it is possible to use TypeScript to write server-side applications which compile to JavaScript.

Conclusion

Although PHP has been enhanced a lot since PHP 7, it still hasn’t caught up other programming languages in terms of type safety, making it less suitable for complicated modern web applications. Unless it improves drastically in PHP 9, the decline is inevitable.

Leave a Reply

Your email address will not be published. Required fields are marked *