objects namespaces and application architecture
Covariance and Contravariance
Covariance and contravariance describe which type changes are allowed when a child class overrides a parent method or implements an interface.
The short version for PHP is:
- return types may become more specific in the child method
- parameter types may become less specific in the child method
These rules preserve substitutability. If code expects the parent type, it must still be safe to use the child type in its place.
Covariant Return Types
Covariance lets a child method return a more specific type than the parent method promised.
<?php
declare(strict_types=1);
class Response
{
public function body(): string
{
return 'response';
}
}
final class JsonResponse extends Response
{
public function body(): string
{
return '{"ok":true}';
}
}
class Controller
{
public function show(): Response
{
return new Response();
}
}
final class ApiController extends Controller
{
public function show(): JsonResponse
{
return new JsonResponse();
}
}
$controller = new ApiController();
echo $controller->show()->body() . PHP_EOL;
// Prints:
// {"ok":true}
Controller::show() promises a Response. ApiController::show() returns a JsonResponse, which is still a Response, so callers relying on the parent contract are safe.
Contravariant Parameter Types
Contravariance lets a child method accept a broader parameter type than the parent method required.
<?php
declare(strict_types=1);
class User
{
public function __construct(
public string $email,
) {
}
}
final class AdminUser extends User
{
}
class AdminNotifier
{
public function notify(AdminUser $user): string
{
return 'Admin notice for ' . $user->email;
}
}
final class AnyUserNotifier extends AdminNotifier
{
public function notify(User $user): string
{
return 'User notice for ' . $user->email;
}
}
$notifier = new AnyUserNotifier();
echo $notifier->notify(new AdminUser('admin@example.com')) . PHP_EOL;
// Prints:
// User notice for admin@example.com
The parent requires an AdminUser. The child accepts any User, which includes admin users. That is safe because code that calls the method with an AdminUser still works.
Why Narrower Parameters Are Unsafe
If a child method required a more specific parameter than the parent, code using the parent contract could break.
Imagine a parent method accepts any User, but the child only accepts AdminUser. Code that correctly passes a normal User to the parent contract would fail with the child. PHP rejects that kind of override.
The rule protects callers.
Interfaces Follow The Same Rules
The same rules apply when implementing interfaces.
<?php
declare(strict_types=1);
interface ReportFactory
{
public function create(): Report;
}
class Report
{
}
final class SalesReport extends Report
{
}
final class SalesReportFactory implements ReportFactory
{
public function create(): SalesReport
{
return new SalesReport();
}
}
$factory = new SalesReportFactory();
echo $factory->create()::class . PHP_EOL;
// Prints:
// SalesReport
The interface promises a Report. Returning a SalesReport is allowed because it is more specific.
Practical Review Rule
When checking an override, ask:
- Does the child return something the parent caller can still use?
- Does the child accept at least everything the parent promised to accept?
Return values go narrower. Parameters go wider.
What You Should Be Able To Do
After this lesson, you should be able to explain that covariant returns are more specific, contravariant parameters are less specific, and both rules exist to keep inheritance safe.
For junior work, the practical skill is understanding why a method signature override is accepted or rejected by PHP, especially when working with interfaces, framework base classes, and static analysis errors.
Practice
Practice: Override Method Types Safely
Create a small PHP example that demonstrates one covariant return and one contravariant parameter.
Task
Build:
- a base
Responseclass and aJsonResponsesubclass - a base controller method that returns
Response - a child controller method that returns
JsonResponse - a base notifier method that accepts a specific user subtype
- a child notifier method that accepts a broader user type
Use strict types. Keep the expected output in the PHP code block as printed lines or comments.
Check Your Work
Confirm:
- the child controller return type is more specific
- the child notifier parameter type is broader
- both examples still satisfy the parent contract
Afterward, explain why narrowing a parameter type in the child method would be unsafe.
Show solution
This solution uses a more specific return type in the child controller and a broader parameter type in the child notifier.
<?php
declare(strict_types=1);
class Response
{
public function body(): string
{
return 'response';
}
}
final class JsonResponse extends Response
{
public function body(): string
{
return '{"ok":true}';
}
}
class Controller
{
public function show(): Response
{
return new Response();
}
}
final class ApiController extends Controller
{
public function show(): JsonResponse
{
return new JsonResponse();
}
}
class User
{
public function __construct(
public string $email,
) {
}
}
final class AdminUser extends User
{
}
class AdminNotifier
{
public function notify(AdminUser $user): string
{
return 'Admin notice for ' . $user->email;
}
}
final class AnyUserNotifier extends AdminNotifier
{
public function notify(User $user): string
{
return 'User notice for ' . $user->email;
}
}
$controller = new ApiController();
$notifier = new AnyUserNotifier();
echo $controller->show()->body() . PHP_EOL;
echo $notifier->notify(new AdminUser('admin@example.com')) . PHP_EOL;
// Prints:
// {"ok":true}
// User notice for admin@example.com
Narrowing a child parameter would be unsafe because code using the parent contract might pass a value the parent accepts, only for the child implementation to reject it.