PHP 8.0: Tipos de datos (2/4)

hace 4 años por David Grudl  

Ha salido la versión 8.0 de PHP. Está tan repleta de novedades como ninguna versión anterior. Su presentación ha requerido cuatro artículos separados. En este segundo, veremos los tipos de datos.

Volvamos a la historia. Un avance fundamental de PHP 7 fue la introducción de los type hints escalares. Casi no sucedió. La autora de la increíble solución, Andreu Faulds, que gracias a declare(strict_types=1) era totalmente retrocompatible y opcional, fue rechazada duramente por la comunidad. Afortunadamente, en aquel entonces, Anthony Ferrara la defendió a ella y a su propuesta, lanzó una campaña y el RFC pasó por muy poco. Ufff. La mayoría de las novedades en PHP 8 son obra del legendario Nikita Popov y en la votación pasaron como la seda. El mundo está cambiando a mejor.

PHP 8 lleva los tipos a la perfección. Desaparecerá la gran mayoría de las anotaciones phpDoc @param, @return y @var en el código y serán reemplazadas por la notación nativa y, sobre todo, por el control del motor de PHP. En los comentarios solo quedarán descripciones de estructuras como string[] o anotaciones más complejas para PHPStan.

Tipos de Unión (Union Types)

Los tipos de unión son una enumeración de dos o más tipos que un valor puede tomar:

class Button
{
	private string|object $caption;

	public function setCaption(string|object $caption)
	{
		$this->caption = $caption;
	}
}

PHP ya conocía algunos tipos de unión especiales anteriormente. Por ejemplo, los tipos anulables como ?string, que es el equivalente al tipo de unión string|null y la notación con signo de interrogación se puede considerar solo como una abreviatura. Por supuesto, también funciona en PHP 8, pero no se puede combinar con barras verticales, por lo que en lugar de ?string|object es necesario escribir el string|object|null completo. Además, iterable siempre fue el equivalente a array|Traversable. Quizás le sorprenda que el tipo de unión sea en realidad también float, que en realidad acepta int|float, pero lo convierte a float.

En las uniones no se pueden usar los pseudotipos void y mixed, porque no tendría ningún sentido.

Nette está preparado para tipos de unión. En Schema, Expect::from() los acepta, y los presentadores también los aceptan. Puedes usarlos en métodos render y action, por ejemplo:

public function renderDetail(int|array $id)
{
	...
}

Por el contrario, el autowiring en Nette DI rechaza los tipos de unión. Falta todavía un caso de uso donde tendría sentido que, por ejemplo, un constructor aceptara uno u otro objeto. Por supuesto, si aparece, será posible ajustar el comportamiento del contenedor en consecuencia.

Los métodos getParameterType(), getReturnType() y getPropertyType() en Nette\Utils\Reflection lanzan una excepción en caso de un tipo de unión (en la versión 3.1, en la versión anterior 3.0 devuelven null por compatibilidad).

mixed

El pseudotipo mixed indica que el valor puede ser absolutamente cualquier cosa.

En el caso de parámetros y propiedades, en realidad es el mismo comportamiento que si no indicáramos ningún tipo. ¿Para qué sirve entonces? Para poder distinguir cuándo simplemente falta el tipo y cuándo es realmente mixed.

En el caso del valor de retorno de una función o método, no indicar el tipo difiere de indicar el tipo mixed. En realidad, es lo opuesto a void, ya que indica que la función debe devolver algo. Un return ausente es entonces un error fatal.

En la práctica, debería usarlo raramente, porque gracias a los tipos de unión puede especificar el valor con mayor precisión. Por lo tanto, es útil en situaciones excepcionales:

function dump(mixed $var): mixed
{
	// imprimir variable
	return $var;
}

false

El nuevo pseudotipo false, por el contrario, solo se puede usar en tipos de unión. Surgió de la necesidad de describir nativamente el tipo del valor de retorno en funciones nativas que históricamente devuelven false en caso de fracaso:

function strpos(string $haystack, string $needle): int|false
{
}

Por esta razón, no existe el tipo true, tampoco se puede usar false solo, ni false|null o bool|false.

static

El pseudotipo static solo se puede usar como tipo de retorno de un método. Indica que el método devuelve un objeto del mismo tipo que el objeto mismo (mientras que self indica que devuelve la clase en la que se define el método). Lo cual es excelente para describir interfaces fluidas (fluent interfaces):

class Item
{
	public function setValue($val): static
	{
		$this->value = $val;
		return $this;
	}
}

class ItemChild extends Item
{
	public function childMethod()
	{
	}
}

$child = new ItemChild;
$child->setValue(10)
	->childMethod();

resource

Este tipo no existe en PHP 8 y tampoco se introducirá en el futuro. Los resources son una reliquia histórica de los tiempos en que PHP aún no tenía objetos. Gradualmente, los resources serán reemplazados por objetos y con el tiempo este tipo desaparecerá por completo. Por ejemplo, PHP 8.0 reemplaza el resource que representa una imagen por el objeto GdImage y el resource de conexión curl por el objeto CurlHandle.

Stringable

Es una interfaz que implementa automáticamente cada objeto con el método mágico __toString().

class Email
{
	public function __toString(): string
	{
		return $this->value;
	}
}

function print(Stringable|string $s)
{
}

print('abc');
print(new Email);

En la definición de la clase, es posible indicar explícitamente class Email implements Stringable, pero no es necesario.

Este estilo de nomenclatura también se refleja en Nette\Utils\Html, que implementa la interfaz Nette\HtmlStringable en lugar de la anterior IHtmlString. Los objetos de este tipo, por ejemplo, no son escapados por Latte.

Varianza de tipos, contravarianza, covarianza

El principio de sustitución de Liskov (Liskov Substitution Principle – LSP) dice que los descendientes de una clase y las implementaciones de una interfaz nunca deben requerir más ni proporcionar menos que el padre. Es decir, que un método del descendiente no debe requerir más argumentos o aceptar en los parámetros un rango de tipos más estrecho que el ancestro y, por el contrario, no debe devolver un rango de tipos más amplio. Pero puede devolver uno más estrecho. ¿Por qué? Porque de lo contrario, la herencia no funcionaría en absoluto. Una función aceptaría un objeto de un tipo determinado, pero no sabría qué parámetros se pueden pasar a los métodos ni qué devolverán realmente, porque cualquier descendiente podría romperlo.

Por lo tanto, en POO se aplica que un descendiente puede:

  • en los parámetros aceptar un rango de tipos más amplio (esto se llama contravarianza)
  • devolver un rango de tipos más estrecho (covarianza)
  • y las propiedades no pueden cambiar de tipo (son invariantes)

PHP sabe hacer esto desde la versión 7.4 y todos los tipos recién introducidos en PHP 8.0 también soportan contravarianza y covarianza.

En el caso de mixed, el descendiente puede estrechar el valor de retorno a cualquier tipo, pero no a void, porque no es un tipo de valor, sino su ausencia. Tampoco el descendiente puede omitir el tipo, porque eso también permite la ausencia.

class A
{
    public function foo(mixed $foo): mixed
    {}
}

class B extends A
{
    public function foo($foo): string
    {}
}

También los tipos de unión se pueden ampliar en los parámetros y estrechar en los valores de retorno:

class A
{
    public function foo(string|int $foo): string|int
    {}
}

class B extends A
{
    public function foo(string|int|float $foo): string
    {}
}

Además, false puede ampliarse en el parámetro a bool o, por el contrario, bool en el valor de retorno puede estrecharse a false.

Todas las infracciones contra la covarianza/contravarianza conducen a un error fatal en PHP 8.0.

En las próximas entregas mostraremos qué son los atributos, qué nuevas funciones y clases han aparecido en PHP y presentaremos el Just in Time Compiler.