PHP 8.0: Novedades en Tipos de Datos (2/4)

hace 4 años por David Grudl  

La versión 8.0 de PHP acaba de ser liberada. Está llena de nuevas características, como ninguna versión antes. Su introducción merece cuatro artículos separados. En la segunda parte, echaremos un vistazo a los tipos de datos.

Retrocedamos en la historia. La introducción de sugerencias de tipos escalares fue un avance significativo en PHP 7. Casi no sucedió. Andreu Faulds, el autor de la ingeniosa solución, que era totalmente compatible hacia atrás y opcional gracias a declare(strict_types=1), fue duramente rechazado por la comunidad. Afortunadamente, Anthony Ferrara la defendió a ella y a la propuesta en su momento, lanzó una campaña y la RFC pasó muy cerca. Ufff. La mayoría de las novedades de PHP 8 son cortesía del legendario Nikita Popov y todas pasaron la votación como un cuchillo por la mantequilla. El mundo está cambiando para mejor.

PHP 8 trae tipos a la perfección. La gran mayoría de las anotaciones phpDoc como @param, @return y @var serán reemplazadas por notación nativa. Pero lo más importante, los tipos serán comprobados por el motor de PHP. Sólo las descripciones de estructuras como string[] o anotaciones más complejas para PHPStan permanecerán en los comentarios.

Tipos de Unión

Los tipos de unión son una enumeración de dos o más tipos que puede aceptar una variable:

class Button
{
	private string|object $caption;

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

Ciertos tipos de unión han sido introducidos en PHP anteriormente. Tipos anulables, por ejemplo. Como ?string, que es equivalente al tipo de unión string|null. La notación de signo de interrogación puede considerarse una abreviatura. Por supuesto, también funciona en PHP 8, pero no se puede combinar con barras verticales. Así que en lugar de ?string|object hay que escribir string|object|null. Además, iterable siempre fue equivalente a array|Traversable. Puede que le sorprenda que float sea también un tipo de unión, ya que acepta tanto int|float, pero convierte el valor a float.

No se pueden utilizar los pseudotipos void y mixed en uniones porque no tendría 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 otro lado, el autocableado en Nette DI rechaza los tipos unión. Hasta ahora, no hay ningún caso de uso en el que tenga sentido que el constructor acepte uno u otro objeto. Por supuesto, si tal caso de uso 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 el caso del tipo unión (esto es en la versión 3.1; en la versión anterior 3.0, estos métodos sí devuelven para mantener la compatibilidad nula).

mixed

El pseudotipo mixed acepta cualquier valor.

En el caso de parámetros y propiedades, tiene el mismo comportamiento que si no especificáramos ningún tipo. Entonces, ¿cuál es su propósito? Distinguir cuándo un tipo está simplemente ausente y cuándo lo está intencionadamente mixed.

En el caso de los valores de retorno de funciones y métodos, no especificar el tipo difiere de utilizar mixed. Significa lo contrario de void, ya que requiere que la función devuelva algo. Un retorno omitido produce entonces un error fatal.

En la práctica, rara vez se debe utilizar, ya que gracias a los tipos de unión se puede especificar el valor con precisión. Por lo tanto, sólo es adecuado en situaciones excepcionales:

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

false

A diferencia de mixed, puede utilizar el nuevo pseudotipo false exclusivamente en tipos de unión. Surgió de la necesidad de describir el tipo de retorno de las funciones nativas, que históricamente devuelven false en caso de fallo:

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

Por lo tanto, no existe el tipo true. Tampoco se puede utilizar false solo, ni false|null, ni bool|false.

static

El pseudotipo static sólo puede utilizarse como tipo de retorno de un método. Dice que el método devuelve un objeto del mismo tipo que el propio objeto (mientras que self dice que devuelve la clase en la que está definido el método). Es excelente para describir interfaces fluidas:

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 no será introducido en el futuro. Los recursos son una reliquia de la época en que PHP no tenía objetos. Gradualmente, los recursos van a ser reemplazados por objetos. Eventualmente, desaparecerán por completo. Por ejemplo, PHP 8.0 reemplaza el recurso image por el objeto GdImage, y el recurso curl por el objeto CurlHandle.

Stringable

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

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

function print(Stringable|string $s)
{
}

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

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

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

Tipo varianza, contravarianza, covarianza

El Principio de Sustitución de Liskov (LSP) establece que las clases de extensión y las implementaciones de interfaz nunca deben requerir más y proporcionar menos que el padre. Es decir, el método hijo no debe requerir más argumentos o aceptar un rango más estrecho de tipos para los parámetros que el padre, y viceversa, no debe devolver un rango más amplio de tipos. Pero puede devolver menos. ¿Por qué? Porque, de lo contrario, se rompería la herencia. Una función aceptaría un objeto de un tipo concreto, pero no tendría ni idea de qué parámetros puede pasar a sus métodos y qué tipos devolverían. Cualquier hijo podría romperla.

Así que en POO, la clase hija puede:

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

PHP ha sido capaz de hacer esto desde la versión 7.4, y todos los nuevos tipos introducidos en PHP 8.0 también soportan contravarianza y covarianza.

En el caso de mixed, el hijo puede acotar el valor de retorno a cualquier tipo, pero no void, ya que no representa un valor, sino su ausencia. El hijo tampoco puede omitir una declaración de tipo, ya que esto también permite una ausencia de valor.

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

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

Los tipos de unión también pueden ampliarse en los parámetros y reducirse 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 a bool en el parámetro o viceversa bool a false en el valor de retorno.

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

En las próximas partes de esta serie, mostraremos qué son los atributos, qué nuevas funciones y clases han aparecido en PHP e introduciremos el compilador Just in Time.