Skip to content
This repository has been archived by the owner on Jul 2, 2019. It is now read-only.

Add RFC for Type Declarations #3

Closed
wants to merge 11 commits into from
219 changes: 219 additions & 0 deletions text/0000-type-declarations.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,219 @@
- Start Date: 2018-02-21
- RFC PR:
- WordPress Coding Standards Issue:

# Summary

[Type Declarations](http://php.net/manual/en/functions.arguments.php#functions.arguments.type-declaration) make code more readable. They also protect code, because Type Declarations are a way of enforcing specific data types in parameters passed to a function.

# Basic Example

You can force callers to pass an `array`, or a specific type of object, such as a `WP_REST_Request`. If a caller doesn't pass the data type expected, PHP 5 triggers a recoverable fatal error and PHP 7 throws a [TypeError](http://php.net/manual/en/class.typeerror.php) exception. This behavior catches problems right away.

```php
protected function foo( Bar $bar, array $items ) {
// $bar must be an instance of the Bar class.
// $items must be passed as an array.
}
```

# Motivation

PHP is a [weakly typed language](https://en.wikipedia.org/wiki/Strong_and_weak_typing). It doesn't require you to declare data types. However, variables still have data types associated with them (e.g., `string`, `int`). In a weakly typed language you can do things like adding a `string` to an `int` with no error. While this can be wonderful in some cases, it can also lead to unanticipated behavior and bugs.

For example, PHP functions and class methods accept parameters, such as `function foo( $bar, $items )`. If Type Declarations aren't being used, the only way a function knows what it's being passed is to run `is_*()` checks, `instanceof`, or use type casting. Otherwise, `$bar` could be anything, and there's a chance that invalid data types would go unnoticed and produce an unexpected or incorrect return value. Imagine type casting an array to an integer. That's forcing a wrong into a right, instead of correcting the underlying issue. A caller should pass the right data type to begin with.

Copy link

@schlessera schlessera Mar 8, 2018

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'd add an additional paragraph here along the lines of:

Moreover, as the wrong value does not throw an immediate error, it might get passed down to
another function, and yet another, and so on. Most often, you'll end up with an error that is
thrown in a completely unrelated part of the code, or in the worst case, no error being
thrown at all, while the frontend happily renders bogus data.

Basically emphasizing why throwing an immediate error is so important.

So Type Declarations are advantageous, because ultimately, they produce improved error messages that catch problems right away. In the same way we _discourage_ use of the `@` error control operator and _encourage_ strict comparison `===`, Type Declarations shine a light a bugs. They're a way of being more explicit about the expected data type. An added benefit is more readable code.

# Detailed Design

## Valid Type Declarations in PHP 5.1+

- Class or interface name; e.g., `WP_REST_Request`
- `self`, which references own class or interface name
- `parent`, which references parent class or interface name
- `array`, to require an array

```php
protected function foo( Bar $bar, array $items ) {
// $bar must be an instance of the Bar class.
// $items must be passed as an array.
}
```

## Modern Versions of PHP

- The [`callable` Type Declaration](http://php.net/manual/en/functions.arguments.php#functions.arguments.type-declaration) became available in PHP 5.4.
- [Scalar Type Declarations](http://php.net/manual/en/migration70.new-features.php#migration70.new-features.scalar-type-declarations): `string`, `int`, `float`, and `bool` became available in PHP 7.0, along with support for [Return Type Declarations](http://php.net/manual/en/migration70.new-features.php#migration70.new-features.return-type-declarations) and [Strict Typing](http://php.net/manual/en/functions.arguments.php#functions.arguments.type-declaration.strict).
- The [`iterable` Type Declaration](http://php.net/manual/en/migration71.new-features.php#migration71.new-features.iterable-pseudo-type), [Nullable Types](http://php.net/manual/en/migration71.new-features.php#migration71.new-features.nullable-types), and [`void` Return Type Declaration](http://php.net/manual/en/migration71.new-features.php#migration71.new-features.void-functions) became available in PHP 7.1.
- The [`object` Type Declaration](http://php.net/manual/en/migration72.new-features.php#migration72.new-features.object-type) became available in PHP 7.2.

## Formatting

_**Note:** All of these formatting rules follow [PSR-12](https://github.com/php-fig/fig-standards/blob/master/proposed/extended-coding-style-guide.md#45-method-and-function-arguments), with one additional clarification: There MUST be one space before and after a Type Declaration; e.g., `( array $items`, not `(array$items`._

### Whitespace

**The additional PSR-12 clarification:**
There MUST be one space before and after a Type Declaration.

```php
protected function foo( Bar $bar, array $items ) {
// ...
}
```

**Nullable Types:**
There MUST NOT be a space between the question mark and the Type Declaration.

```php
protected function foo( ?Bar $bar, ?array $items ) {
// ...
}
```

#### Return Type Declarations

The colon and Type Declaration MUST be on the same line as a function's closing parentheses. There MUST NOT be spaces between the function's closing parentheses and colon. There MUST be one space after the colon, followed by the Type Declaration. There MUST be one space after the Type Declaration, before the function's opening curly bracket.

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The "no space between closing parentheses and colon" matches PSR-12. However, having a space in there might be closer to the "use extensive spacing" approach of WordPress.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

WP coding standards is ambivalent about colons:

  • for alternative control structures, a space is required before, (space or) new line after for () :
  • ternaries require spacing on both sides of the colon
  • for case $a: and default, the colon should not have a space before it

All together, the "no space between the parentheses and colon" for return types, seems to balance things out ;-)
(quite apart from making the divide between WP and the wider PHP a little smaller)


```php
protected function foo(): array {
// ...
}
```

**Nullable Types:**
There MUST NOT be a space between the question mark and the Type Declaration.

```php
protected function foo(): ?array {
// ...
}
```

### Required CaSe

The caSe of Type Declarations MUST be lowercase (e.g., `array`), except for class and interface names, which MUST follow the name as declared by the class or interface; e.g., `WP_REST_Request`

```php
protected function foo( array $data, WP_REST_Request $request ) {
// ...
}
```

### Required Form

_**Note:** Use `int`, not `integer`. Use `bool`, not `boolean`._

The following are valid Type Declarations:

- Class or interface name; e.g., `WP_REST_Request`
- `self`, which references own class or interface name
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I've ran some additional tests as parent is largely undocumented. Turns out to have been available and properly working since PHP 5.2 (5.0).

Note: while officially self and parent were available since PHP 5.0, they are only recognized properly since PHP 5.2. Before that, they would be seen as class names.

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Nice! Thanks for researching this. I confirmed here: https://3v4l.org/TN3O6 and added parent as a valid type declaration.

- `parent`, which references parent class or interface name
- `array`
- `callable`
- `string`
- `int`
- `float`
- `bool`
- `iterable`
- `object`
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm missing void in the list.

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed. Thanks!

- `void`, as a Return Type Declaration

## WordPress Core Compatibility

At this time, the additional Type Declarations: `callable`, `string`, `int`, `float`, `bool`, `iterable`, and `object` must be avoided in WordPress Core. The same is true for [Return Type Declarations](http://php.net/manual/en/migration70.new-features.php#migration70.new-features.return-type-declarations), [Strict Typing](http://php.net/manual/en/functions.arguments.php#functions.arguments.type-declaration.strict), and [Nullable Types](http://php.net/manual/en/migration71.new-features.php#migration71.new-features.nullable-types). Please see [WordPress requirements](https://wordpress.org/about/requirements/) (PHP 5.2.4+) and [this table](http://php.net/manual/en/functions.arguments.php#functions.arguments.type-declaration) for further details.

The only Type Declarations supported in WordPress Core at this time, are:

- Class or interface name; e.g., `WP_REST_Request`
- `self`, which references own class or interface name
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

parent

- `parent`, which references parent class or interface name
- `array`, to require an array

_These can only be used in function parameters, not as Return Type Declarations._

## When to Use Type Declarations

When you're writing a function or a class method that expects to receive an array or a specific object type, and there is no reason to accept anything other than that specific type.

```php
protected function foo( Bar $bar, array $items ) {
// $bar must be an instance of the Bar class.
// $items must be passed as an array.
}
```

## When Not to Use Type Declarations

If the function you're writing is part of a public API and enforcing a specific data type would make the function less flexible in the eyes of a caller. For example, the [`get_post()`](https://developer.wordpress.org/reference/functions/get_post/) function in WordPress core accepts `int|WP_Post|null` as the first parameter. This maximizes flexibility for callers, making the function more convenient in a variety of circumstances.

Likewise, if you're writing a public function for an API and it needs a `WP_Post` instance, it's better not to enforce `WP_Post` with a type declaration. Instead, use the `get_post()` function to resolve the `$post` reference, making it possible for a caller to pass `int|WP_Post|null` to your function as well.

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This paragraph seems only partially related to type declarations. Although I agree that WP_Post should not be used, I think the most important reason is because it is a final class without an interface. Using that type declaration seals off any future extensibility. Using some other form of type checking is what throughout all of the WordPress code base anyway.


```php
function foo( $post ) {
$post = get_post( $post );
...
}
```

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Given the current PHP requirements, I think that type declarations should only be used right now with types that are:

  • interfaces or classes
  • considered to be part of the public interface
  • extensible

All other type declarations should be discouraged, as they cause more problems than they solve:

  • Using array immediately renders almost any future improvements using smart objects impossible. It makes it impossible for objects to stand in as collections of elements. Often times, when the doc-block states that a function needs an array, it actually means it needs a Traversable, because it wants to loop over it, but cannot do that as PHP 5.2 is missing that concept, or it needs an ArrayAccess because it wants to use indexed access. Using array should be avoided at all costs, unless there is a specific reason why it needs to be the actual internal type of array.
  • Using classes that are not extensible means that future changes need to be done by changing an element of the public interface, instead of _ extending_ it. This is again an unneeded hindrance to flexibility. This is why segregated interfaces (i.e. Renderable or Taggable instead of WP_Post) should always be preferred, however WordPress is not really big on using interfaces.

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Due to the above, I'd also recommend changing the examples in this document to not so prominently show the array type declaration.

# Drawbacks

## Runtime Errors

Using Type Declarations can lead to recoverable fatal errors in PHP 5, and [TypeError](http://php.net/manual/en/class.typeerror.php) exceptions in PHP 7. This is both a blessing and a curse. Runtime errors are mostly advantageous for reasons already stated elsewhere in this RFC; i.e., they help catch bugs right away.

However, unlike [`_doing_it_wrong()`](https://developer.wordpress.org/reference/functions/_doing_it_wrong/), errors associated with invalid data types are more difficult to suppress. Imagine a plugin author writing code that calls upon a core function. If the core function uses Type Declarations to enforce specific data types, and the plugin passes an invalid type, an HTTP error response or WSOD could occur.

# Adoption Strategy

Retrofitting existing functions with Type Declarations should be avoided 💯. Doing so would suddenly enforce a rule that did not exist prior, causing runtime errors. For example, imagine themes and plugins calling core functions that previously accepted multiple data types, but now require a specific type. Not good! This scenario must be avoided.

However, new functions (particularly protected and private methods of a class) are encouraged to use Type Declarations supported by the minimum version of PHP they are targeting. If targeting a modern version of PHP (7.0+), the use of [Return Type Declarations](http://php.net/manual/en/functions.returning-values.php#functions.returning-values.type-declaration) is encouraged also.

# Teaching Strategy

Type Declarations were once known as Type Hints in PHP 5. That name is no longer appropriate, because later versions of PHP added support for [Return Type Declarations](http://php.net/manual/en/migration70.new-features.php#migration70.new-features.return-type-declarations), [Strict Typing](http://php.net/manual/en/functions.arguments.php#functions.arguments.type-declaration.strict), and [Nullable Types](http://php.net/manual/en/migration71.new-features.php#migration71.new-features.nullable-types). For this reason, please use the up-to-date and official terminology:

- [Type Declarations](http://php.net/manual/en/functions.arguments.php#functions.arguments.type-declaration)
- [Scalar Type Declarations](http://php.net/manual/en/migration70.new-features.php#migration70.new-features.scalar-type-declarations)
- [Return Type Declarations](http://php.net/manual/en/migration70.new-features.php#migration70.new-features.return-type-declarations)
- [Strict Typing](http://php.net/manual/en/functions.arguments.php#functions.arguments.type-declaration.strict) (aka: Strict Mode)
- [Nullable Types](http://php.net/manual/en/migration71.new-features.php#migration71.new-features.nullable-types)

## Default Behavior & Strict Mode

[Strict Mode](http://php.net/manual/en/functions.arguments.php#functions.arguments.type-declaration.strict) is possible in PHP 7.0+. Strict Mode only impacts [Scalar Type Declarations](http://php.net/manual/en/migration70.new-features.php#migration70.new-features.scalar-type-declarations): `string`, `int`, `float`, and `bool`. This can be somewhat confusing, so it must be explained in some detail and the [official documentation](http://php.net/manual/en/functions.arguments.php#functions.arguments.type-declaration.strict) should be reviewed carefully. It's important to understand both the default behavior and also the impact Strict Mode has.

### Default Behavior

By default, PHP will, if possible, coerce scalar values of the wrong type into the expected type. For example, a function that is given an `int` for a parameter that expects a `string` will get a variable of type `string`. The `int` is magically converted into the `string` expected by a Scalar Type Declaration.

This is also true in scalar Return Type Declarations. By default, scalar return values will be coerced to the correct type if they are not already of that type.

_**Implication:** Because Strict Mode only impacts Scalar Type Declarations, when an invalid type is passed to a function using a non-scalar Type Declaration, it always produces a recoverable fatal error in PHP 5, or a [TypeError](http://php.net/manual/en/class.typeerror.php) in PHP 7. This is the default behavior and it cannot be altered, because Strict Mode has no impact on non-scalar Type Declarations._

### Enabling Strict Mode

Strict Mode can be enabled on a per-file basis using the `declare` directive:

```php
<?php
declare( strict_types=1 );
```

In Strict Mode, when both the function call and the function declaration are in a strict-typed file, only a variable of the exact type of the Scalar Type Declaration will be accepted. Otherwise, a [TypeError](http://php.net/manual/en/class.typeerror.php) will be thrown. The only exception to this rule is that an `int` may be given to a function expecting a `float`.

This is also true for Return Type Declarations. Whenever a function is defined in a strict-typed file, a scalar return value must be of the correct type, otherwise a [TypeError](http://php.net/manual/en/class.typeerror.php) will be thrown.

For further details, see: [Strict Typing](http://php.net/manual/en/functions.arguments.php#functions.arguments.type-declaration.strict)

# Unresolved Questions

- If sniffs are added for Type Declarations:
- Are there any circumstances in which Type Declarations **MUST** be used?
- Are there any circumstances in which Type Declarations **MUST NOT** be used?

- Should steps be taken to suppress runtime errors caused by invalid data types when running in a production environment? If so, what options are available for consideration?

- In PHP 7.0+ it is possible to catch [TypeError](http://php.net/manual/en/class.typeerror.php) exceptions.