PHP has undergone significant improvements in recent years, especially in the realm of type safety and error detection. However, one feature that is often seen in other programming languages such as Java or TypeScript is generics—a feature that allows you to write code that works with any data type while maintaining strong type safety. As of PHP 8.x, full generics support has not yet been introduced natively, but there are some powerful patterns and third-party libraries that allow you to implement a form of generics.
In this article, we will explore PHP generics, how you can mimic them in modern PHP using type safety features such as union types, and intersection types, and how third-party libraries provide more generic-like functionality. You’ll also learn how to create flexible, reusable code that can handle various types while maintaining the integrity and clarity of your PHP code.
What Are PHP Generics?
Generics allow developers to write reusable code that can operate on different types while still ensuring type safety. For example, in languages that support generics, you can create a single function or class that works with integers, strings, objects, or arrays without sacrificing type checking.
With generics, you could have something like a generic list class that can hold objects of any type, but enforces that all elements are of a single type (e.g., List<int>
, List<string>
). The primary purpose of generics is to enforce type constraints at compile-time, ensuring that the code is type-safe, flexible, and reusable.
While PHP doesn't have native generics support as of PHP 8.x, there are ways to write more flexible and reusable code using advanced type hints and third-party libraries.
How to Use Generics in PHP
Simulating Generics with Union Types
While PHP doesn’t support generics natively, you can create flexible functions or classes by using union types. Union types allow a variable or parameter to be of multiple types, giving you some of the flexibility generics provide.
For example, let’s say you want a function to accept either an int
or a float
and return the same type:
function addNumbers(int|float $a, int|float $b): int|float
{
return $a + $b;
}
echo addNumbers(5, 10); // Output: 15
echo addNumbers(2.5, 7.5); // Output: 10.0
Here, we’ve allowed both int
and float
types for the parameters and return type. While this is not true generics, it simulates the idea by giving more flexibility to the function.
Using Templates for Type Safety
Generics often come into play when you need type consistency across multiple parameters or properties. PHP doesn’t natively support generic templates like T
, but you can mimic this pattern with type constraints using docblocks and static analysis tools such as PHPStan or Psalm.
Here’s an example of how you could define a generic-like class using docblock annotations:
/**
* @template T
*/
class Collection
{
/**
* @var array<T>
*/
private array $items = [];
/**
* Add an item to the collection.
*
* @param T $item
*/
public function addItem(mixed $item): void
{
$this->items[] = $item;
}
/**
* Get all items in the collection.
*
* @return array<T>
*/
public function getItems(): array
{
return $this->items;
}
}
// Example usage:
$intCollection = new Collection();
$intCollection->addItem(1);
$intCollection->addItem(2);
$stringCollection = new Collection();
$stringCollection->addItem("Hello");
$stringCollection->addItem("World");
print_r($intCollection->getItems()); // Output: [1, 2]
print_r($stringCollection->getItems()); // Output: ["Hello", "World"]
In this case, the @template
annotation acts as a type placeholder (like T
in Java). Static analyzers like PHPStan and Psalm will treat this class as if it is truly generic and enforce type consistency when you use it.
Generic Data Structures in PHP
Let’s consider a more practical example of creating a simple generic stack data structure. Although we’ll mimic generics using docblocks and type safety checks, it demonstrates how you can enforce type constraints and maintain flexibility.
/**
* @template T
*/
class Stack
{
/**
* @var array<T>
*/
private array $stack = [];
/**
* Push an item onto the stack.
*
* @param T $item
*/
public function push(mixed $item): void
{
$this->stack[] = $item;
}
/**
* Pop an item from the stack.
*
* @return T|null
*/
public function pop(): mixed
{
return array_pop($this->stack);
}
/**
* Get the number of items on the stack.
*
* @return int
*/
public function size(): int
{
return count($this->stack);
}
}
// Example usage:
$intStack = new Stack();
$intStack->push(1);
$intStack->push(2);
echo $intStack->pop(); // Output: 2
$stringStack = new Stack();
$stringStack->push("First");
$stringStack->push("Second");
echo $stringStack->pop(); // Output: Second
Enforcing Generics with Third-Party Libraries
While PHP doesn't offer native support for generics, some third-party libraries fill the gap by implementing generic-like structures. One of the most popular libraries for this is PHPStan's Generics feature, which allows you to use generics in a similar way to how you would in statically-typed languages.
Here’s an example of how you can use PHPStan to implement type-safe generics:
<?php
use PHPStan\Generics\GenericType;
/**
* @template T
*/
class Queue
{
/**
* @var array<T>
*/
private array $queue = [];
/**
* Add an item to the queue.
*
* @param T $item
*/
public function enqueue(mixed $item): void
{
$this->queue[] = $item;
}
/**
* Remove and return the first item from the queue.
*
* @return T|null
*/
public function dequeue(): mixed
{
return array_shift($this->queue);
}
/**
* Get the current size of the queue.
*
* @return int
*/
public function size(): int
{
return count($this->queue);
}
}
// Example usage:
$intQueue = new Queue();
$intQueue->enqueue(10);
$intQueue->enqueue(20);
echo $intQueue->dequeue(); // Output: 10
$stringQueue = new Queue();
$stringQueue->enqueue("Task 1");
$stringQueue->enqueue("Task 2");
echo $stringQueue->dequeue(); // Output: Task 1
Intersection Types for More Specific Constraints
With PHP 8.1, intersection types were introduced, which allow you to define more specific constraints for type hinting. This can mimic the generic behavior by requiring that a type satisfies multiple interfaces or traits.
<?php
interface Loggable
{
public function log(): void;
}
interface JsonSerializable
{
public function jsonSerialize(): string;
}
function processEntity(Loggable&JsonSerializable $entity): void
{
$entity->log();
echo $entity->jsonSerialize();
}
class User implements Loggable, JsonSerializable
{
public function log(): void
{
echo "User logged\n";
}
public function jsonSerialize(): string
{
return json_encode(["name" => "John Doe"]);
}
}
// Example usage:
$user = new User();
processEntity($user);
In this example, the function processEntity
requires that its argument implements both Loggable
and JsonSerializable
, giving you more control over the types passed to it.
Why Use Generics in PHP?
Generics allow you to write more reusable and flexible code while maintaining type safety. Although PHP doesn’t yet have native generics, you can still apply some of the same principles using techniques such as:
- Union Types: Allowing a function or method to accept multiple types.
- Docblock Annotations: Simulating generics with static analysis tools like PHPStan or Psalm.
- Intersection Types: Adding more specific type constraints by requiring multiple interfaces or traits.
Generics improve code reusability by allowing you to write functions or classes that work with multiple types without compromising on type safety. This prevents common errors like passing the wrong types to a function or method, ensuring that your code is more robust and easier to maintain.
Conclusion
While PHP doesn’t natively support generics as of PHP 8.x, developers can simulate them through advanced typing techniques, annotations, and third-party libraries. By leveraging union types, intersection types, and tools like PHPStan, you can create more flexible, reusable, and type-safe code in your PHP applications.
Generics are an important concept in modern programming that enhances code flexibility while maintaining type safety.