PHP has changed a lot since the PHP 5 days.
Modern PHP is strongly typed, object-oriented, expressive, and has many features that help us write code that is easier to understand and harder to break.
If you are preparing for a Senior PHP Developer role, you don’t need to memorize every PHP feature.
But you should understand the important ones, know why they exist, and recognize when they make your code better.
This article explains some of the most important modern PHP features in very simple English.
Examples below assume modern PHP 8.x. Some features require PHP 8.1, 8.2, 8.3, 8.4, or 8.5.
1. Named Arguments
Introduced in PHP 8.0
Normally, when calling a function, you pass arguments based on their position.
For example:
function createUser(
string $name,
int $age,
bool $active
) {
// ...
}
createUser('John', 30, true);
Here:
'John' → $name
30 → $age
true → $active
The problem is that when a function has many parameters, it can become difficult to understand what each value means.
Named arguments solve this
You can specify the parameter name:
createUser(
name: 'John',
age: 30,
active: true
);
Now the code explains itself.
You can also skip optional parameters:
function connect(
string $host,
int $port = 3306,
bool $ssl = false
) {
// ...
}
connect(
host: 'localhost',
ssl: true
);
You don’t need to write:
connect('localhost', 3306, true);
Why is this useful for a senior developer?
Because senior developers care about readability.
Compare:
sendEmail(
$user,
true,
false,
5
);
with:
sendEmail(
user: $user,
sendCopy: true,
trackEmail: false,
retryCount: 5
);
The second version is much easier to understand.
Important warning
Named arguments depend on parameter names.
This:
function test(string $username)
is different from:
function test(string $userName)
Changing parameter names can therefore affect callers using named arguments.
Senior developer takeaway: use named arguments when they make an API call clearer, especially when several parameters have the same basic type or many optional parameters exist.
PHP introduced named arguments in PHP 8.0.
2. Union Types
Introduced in PHP 8.0
A union type means:
“This value can be one of several types.”
For example:
function findUser(int|string $id): User
{
// ...
}
This means $id can be:
findUser(10);
or:
findUser('abc-123');
Both are valid.
But this is not:
findUser([]);
because an array is neither int nor string.
Before PHP 8
Developers often used PHPDoc:
/**
* @param int|string $id
*/
function findUser($id)
{
}
PHP itself didn’t enforce that documentation.
Now PHP can understand and enforce it:
function findUser(int|string $id)
{
}
Another example
function formatValue(string|int|float $value): string
{
return (string) $value;
}
The value can be:
formatValue('hello');
formatValue(100);
formatValue(10.5);
Why does this matter?
It makes your function’s contract very clear.
When I see:
function process(Order|Invoice $document)
I immediately know:
This function accepts either an Order or an Invoice.
That’s much better than:
function process($document)
Senior developer takeaway: use native types whenever possible instead of relying on PHPDoc for information PHP itself can enforce.
Union types were introduced in PHP 8.0.
3. Attributes #[...]
Introduced in PHP 8.0
This is one of the most important modern PHP concepts.
You may have seen code like:
#[Route('/users')]
public function users()
{
}
What is #[...]?
It is an attribute.
An attribute is basically:
Extra information attached to a class, method, property, parameter, etc.
Before PHP 8
Frameworks commonly used PHPDoc annotations:
/**
* @Route("/users")
*/
public function users()
{
}
Modern PHP can use:
#[Route('/users')]
public function users()
{
}
Attributes are structured language metadata rather than specially formatted comments.
Why are attributes useful?
Frameworks can inspect them using Reflection.
For example, imagine:
#[Cache(ttl: 3600)]
public function getProducts()
{
}
A framework could inspect the method and say:
“This method has the Cache attribute, so cache its result for 3600 seconds.”
Real-world examples
You will see attributes in frameworks and libraries for things such as:
#[Route('/users')]
#[ORM\Entity]
#[ORM\Column(type: 'string')]
#[IsGranted('ROLE_ADMIN')]
#[SensitiveParameter]
The exact attributes depend on the framework/library.
Important point
Attributes do not mean PHPDoc is dead.
PHPDoc is still useful for things such as:
/**
* @var list<User>
*/
private array $users;
or generic/static-analysis information that PHP’s native type system cannot express.
So don’t think:
“Attributes completely replace PHPDoc.”
Think:
“Attributes are the native PHP mechanism for structured metadata.”
Senior developer takeaway: you should be comfortable reading and creating custom attributes and understanding that frameworks can inspect them using Reflection.
4. Constructor Property Promotion
Introduced in PHP 8.0
This feature removes a lot of boring code.
Suppose you have:
class User
{
private string $name;
private string $email;
private int $age;
public function __construct(
string $name,
string $email,
int $age
) {
$this->name = $name;
$this->email = $email;
$this->age = $age;
}
}
There is a lot of repetition.
PHP lets us write:
class User
{
public function __construct(
private string $name,
private string $email,
private int $age
) {
}
}
That’s it.
PHP understands:
private string $name;
private string $email;
private int $age;
and automatically assigns the constructor arguments to those properties.
You can also combine it with readonly
class User
{
public function __construct(
public readonly int $id,
public readonly string $name
) {
}
}
This is extremely common in DTOs and value objects.
Why senior developers should know it
Because modern PHP codebases use constructor promotion heavily.
Instead of:
private LoggerInterface $logger;
private UserRepository $repository;
private MailerInterface $mailer;
public function __construct(
LoggerInterface $logger,
UserRepository $repository,
MailerInterface $mailer
) {
$this->logger = $logger;
$this->repository = $repository;
$this->mailer = $mailer;
}
you can write:
public function __construct(
private LoggerInterface $logger,
private UserRepository $repository,
private MailerInterface $mailer
) {
}
Much cleaner.
5. Enums
Introduced in PHP 8.1
Enums are extremely important for modern PHP.
Imagine a user’s status:
pending
active
blocked
A beginner might write:
class User
{
public const STATUS_PENDING = 'pending';
public const STATUS_ACTIVE = 'active';
public const STATUS_BLOCKED = 'blocked';
}
Then:
function updateStatus(string $status)
{
}
The problem?
This is also allowed:
updateStatus('banana');
PHP doesn’t know that banana is not a valid status.
Enum solves this
enum UserStatus
{
case Pending;
case Active;
case Blocked;
}
Now:
function updateStatus(UserStatus $status): void
{
}
You call:
updateStatus(UserStatus::Active);
But:
updateStatus('banana');
is invalid.
That’s a huge advantage.
PHP’s own documentation describes enums as a way to get validation through the type system instead of simply using a set of constants.
Backed enums
Often your database stores:
pending
active
blocked
You can create a backed enum:
enum UserStatus: string
{
case Pending = 'pending';
case Active = 'active';
case Blocked = 'blocked';
}
Now:
$status = UserStatus::Active;
echo $status->value;
Output:
active
You can also convert a database value into an enum:
$status = UserStatus::from('active');
Or safely:
$status = UserStatus::tryFrom('something');
from() throws when there is no matching case, while tryFrom() returns null.
Enums can contain methods
For example:
enum UserStatus: string
{
case Pending = 'pending';
case Active = 'active';
case Blocked = 'blocked';
public function label(): string
{
return match ($this) {
self::Pending => 'Waiting',
self::Active => 'Active User',
self::Blocked => 'Blocked User',
};
}
}
Then:
echo UserStatus::Active->label();
Enums can contain methods and implement interfaces.
Senior developer takeaway
Whenever you have a fixed set of valid states, ask yourself:
“Should this be an enum?”
Examples:
OrderStatus
PaymentStatus
UserRole
Environment
Currency
NotificationType
SubscriptionPlan
Enums are one of the features I would consider must-know for senior PHP development.
6. Readonly Properties
Introduced in PHP 8.1
Readonly means:
Once this property gets a value, you cannot change it.
Example:
class User
{
public readonly int $id;
public function __construct(int $id)
{
$this->id = $id;
}
}
This works:
$user = new User(10);
echo $user->id;
But this doesn’t:
$user->id = 20;
PHP throws an error.
Readonly properties can only be initialized once.
Why is this useful?
Imagine a Money object:
class Money
{
public function __construct(
public readonly int $amount,
public readonly string $currency
) {
}
}
Once created:
$money = new Money(100, 'USD');
you don’t want:
$money->amount = 999999;
That could create unexpected behavior.
Readonly makes the object’s state stable.
Important detail
Readonly doesn’t mean the object inside the property is automatically immutable.
For example:
class User
{
public function __construct(
public readonly Address $address
) {
}
}
You cannot replace:
$user->address = new Address(...);
But the Address object itself may still be mutable.
This distinction is important for senior developers.
7. Readonly Classes
Introduced in PHP 8.2
Instead of doing this:
class User
{
public readonly int $id;
public readonly string $name;
public readonly string $email;
}
you can make the whole class readonly:
readonly class User
{
public int $id;
public string $name;
public string $email;
}
All instance properties become readonly automatically. A readonly class also prevents dynamic properties.
Very useful for DTOs
For example:
readonly class UserData
{
public function __construct(
public int $id,
public string $name,
public string $email
) {
}
}
Now this object represents data that should not change after creation.
Think of it like this
Normal class:
Object
├── name → can change
├── email → can change
└── age → can change
Readonly class:
Object
├── name → cannot change
├── email → cannot change
└── age → cannot change
This is particularly useful for:
- DTOs
- Value Objects
- configuration objects
- immutable data structures
8. true, false, and null as Standalone Types
Introduced in PHP 8.2
Normally we write:
function isEnabled(): bool
But sometimes you know that a function always returns false.
Now you can write:
function featureAvailable(): false
{
return false;
}
Similarly:
function getNothing(): null
{
return null;
}
And:
function alwaysWorks(): true
{
return true;
}
PHP 8.2 added null, false, and true as standalone types.
Why would you use this?
Mostly in advanced APIs and precise return types.
For example:
function authenticationFailed(): false
{
return false;
}
It tells static analysis and other developers exactly what the function does.
You may also see combinations such as:
function findSomething(): User|null
{
// ...
}
or:
function parse(): User|false
{
// ...
}
Senior developer takeaway
You don’t need to use true/false/null everywhere.
But you should understand them when reading modern PHP code.
9. Typed Class Constants
Introduced in PHP 8.3
Before PHP 8.3:
class Config
{
public const VERSION = '1.0';
}
There was no type declaration.
PHP 8.3 lets you write:
class Config
{
public const string VERSION = '1.0';
public const int MAX_USERS = 100;
}
Now PHP knows what type the constants must have.
For example:
class Config
{
public const string VERSION = '1.0';
}
This is invalid:
class Config
{
public const string VERSION = [];
}
Why is this useful?
It makes class contracts stronger.
Consider:
interface ApiConfig
{
public const string VERSION = 'v1';
}
Implementing classes cannot suddenly change that constant into an array or integer.
This becomes especially useful in large codebases and inheritance hierarchies.
10. #[Override]
Introduced in PHP 8.3
This is a small feature but very useful.
Suppose you have:
class Animal
{
public function speak(): string
{
return '...';
}
}
And:
class Dog extends Animal
{
public function speak(): string
{
return 'Woof';
}
}
The second speak() is intended to override the parent method.
You can now write:
class Dog extends Animal
{
#[Override]
public function speak(): string
{
return 'Woof';
}
}
PHP checks that the parent class or implemented interface actually has a matching method. If it doesn’t, PHP reports an error.
Why is this surprisingly useful?
Imagine this:
class UserService
{
protected function validateUser(): void
{
}
}
You create:
class AdminService extends UserService
{
protected function validateUsr(): void
{
}
}
Oops.
You typed:
validateUsr
instead of:
validateUser
Without #[Override], you might accidentally create a completely new method.
With:
#[Override]
protected function validateUsr(): void
{
}
PHP tells you:
There is no parent method with this name.
This is exactly the kind of feature that helps prevent subtle bugs during refactoring.
PHP 8.5 update
In PHP 8.5, #[Override] can also be used on properties.
11. Property Hooks
Introduced in PHP 8.4
This is one of the more advanced modern PHP features.
Before property hooks, developers often used getters and setters:
class User
{
private string $name;
public function getName(): string
{
return $this->name;
}
public function setName(string $name): void
{
$this->name = strtolower($name);
}
}
Then:
$user->setName('JOHN');
echo $user->getName();
PHP 8.4 introduced property hooks, allowing behavior directly on the property.
For example:
class User
{
public string $name {
set(string $value) {
$this->name = strtolower($value);
}
}
}
Now:
$user->name = 'JOHN';
echo $user->name;
The setter logic runs automatically.
The result is:
john
Getter example
class Product
{
public function __construct(
private float $price
) {
}
public string $formattedPrice {
get => '$' . number_format($this->price, 2);
}
}
Now:
$product->formattedPrice;
might return:
$19.99
There isn’t necessarily a real stored $formattedPrice value.
It is a computed property.
Another example
class User
{
public string $email {
set(string $value) {
$this->email = strtolower(trim($value));
}
}
}
Then:
$user->email = ' JOHN@EXAMPLE.COM ';
automatically becomes:
john@example.com
Is this just replacing __get() and __set()?
Not exactly.
Property hooks are attached to declared properties and give normal property syntax with custom get/set behavior.
Magic methods such as __get() and __set() are used for inaccessible/undefined properties and have different semantics.
So don’t think:
“Property hooks are simply a new name for
__get()and__set().”
They solve related problems in a much more explicit and type-aware way.
Also important: property hooks are not compatible with readonly properties. If you need controlled read/write access, asymmetric visibility may be a better fit.
Senior developer takeaway: understand property hooks well, but don’t use them merely because they’re new. Use them when a property genuinely has behavior such as normalization, validation, or calculated values.
12. Asymmetric Property Visibility
Introduced in PHP 8.4
This feature solves a very common problem:
“Everyone should be able to read this property, but only this class should be able to change it.”
Before PHP 8.4, you might write:
class User
{
private string $name;
public function getName(): string
{
return $this->name;
}
public function setName(string $name): void
{
$this->name = $name;
}
}
PHP 8.4 lets you express the intention directly:
class User
{
public private(set) string $name;
public function __construct(string $name)
{
$this->name = $name;
}
}
Now:
echo $user->name;
works.
But:
$user->name = 'John';
from outside the class does not work.
Only the class itself can set it.
PHP calls this asymmetric visibility because reading and writing have different visibility rules.
Very useful example
class BankAccount
{
public private(set) float $balance;
public function __construct(float $balance)
{
$this->balance = $balance;
}
public function deposit(float $amount): void
{
$this->balance += $amount;
}
}
Outside:
echo $account->balance;
Allowed.
But:
$account->balance = 999999;
Not allowed.
Inside:
public function deposit(float $amount): void
{
$this->balance += $amount;
}
Allowed.
readonly vs asymmetric visibility
This distinction is important.
Readonly
public readonly string $name;
Means:
Set once. Never change again.
Asymmetric visibility
public private(set) string $name;
Means:
Everyone can read it, but only the class can change it.
So:
readonly
↓
write once
private(set)
↓
write only from allowed scope
Asymmetric visibility is therefore useful when an object has controlled mutability.
13. Stringable Objects
Introduced in PHP 8.0
Suppose you have:
class User
{
public function __toString(): string
{
return 'John';
}
}
You can do:
$user = new User();
echo $user;
PHP automatically treats the object as something that can be converted to a string.
PHP 8 introduced the Stringable interface.
The interesting part is:
If a class has
__toString(), PHP automatically considers itStringable.
You don’t have to explicitly write:
implements Stringable
although explicitly declaring it can make the intention clearer.
This is especially useful with union types:
function printValue(string|Stringable $value): void
{
echo $value;
}
Now this works:
printValue('hello');
and:
printValue(new User());
Senior developer takeaway
Know what Stringable means.
It is particularly useful when designing APIs that accept either:
string
or:
object that can become a string
14. throw as an Expression
Introduced in PHP 8.0
Before PHP 8, throw was treated as a statement.
Modern PHP allows throw where an expression is expected.
This makes code like this possible:
$name = $user->name
?? throw new RuntimeException('Name is required');
Think of it as:
If $user->name exists:
use it
Otherwise:
throw an exception
Another example:
$id = $request->get('id')
?? throw new InvalidArgumentException('ID is required');
You can also use it with short arrow functions:
$findUser = fn (int $id) =>
$id > 0
? findUser($id)
: throw new InvalidArgumentException('Invalid ID');
PHP 8 changed throw from a statement to an expression, allowing it in places where an expression is required.
Don’t overuse it
This:
$name = $user->name
?? throw new RuntimeException('Name missing');
is readable.
But don’t turn every piece of code into clever one-liners.
Senior development is not about using the maximum number of language features.
It is about choosing the clearest solution.
15. #[SensitiveParameter]
Introduced in PHP 8.2
This is a very practical security feature.
Imagine:
function login(string $username, string $password)
{
throw new Exception('Login failed');
}
When PHP creates a stack trace, sensitive function arguments can potentially appear in it.
You don’t want a password appearing in logs.
You can mark it:
function login(
string $username,
#[SensitiveParameter] string $password
) {
throw new Exception('Login failed');
}
PHP will redact the sensitive parameter from the stack trace.
Where should you use it?
Think about:
password
API key
access token
secret key
private credential
credit-card-related sensitive data
For example:
function connectToApi(
string $url,
#[SensitiveParameter] string $apiKey
) {
// ...
}
This is a small feature, but it demonstrates something important:
Senior developers think about security even in debugging and error handling.
16. json_validate()
Introduced in PHP 8.3
Before PHP 8.3, developers sometimes did something like:
json_decode($json);
if (json_last_error() !== JSON_ERROR_NONE) {
// Invalid JSON
}
PHP 8.3 introduced:
json_validate($json);
Example:
$json = '{"name":"John"}';
if (json_validate($json)) {
echo 'Valid JSON';
}
Returns:
true
Invalid JSON:
$json = '{"name":}';
if (!json_validate($json)) {
echo 'Invalid JSON';
}
It is useful when you only need to know:
“Is this valid JSON?”
and don’t actually need the decoded data.
The important performance detail is that json_validate() avoids building the decoded array/object structure, so it can use less memory when validation is all you need. But don’t do this unnecessarily:
json_validate($json);
json_decode($json);
because the JSON gets parsed twice.
Senior developer takeaway: use it when validation is the only goal. If you immediately need the decoded value, just decode it.
17. never Return Type
Introduced in PHP 8.1
This one sounds strange at first.
Normally:
function getName(): string
{
return 'John';
}
The function returns a string.
With:
function stopApplication(): never
{
exit;
}
the function never returns.
It either:
- throws an exception
- calls
exit() - calls
die() - otherwise terminates execution
PHP introduced never specifically to describe functions that do not return.
Example
function redirect(string $url): never
{
header("Location: $url");
exit;
}
Then:
function checkAuthentication(): never
{
redirect('/login');
}
Why is this useful?
It gives static-analysis tools more information.
Consider:
function fail(string $message): never
{
throw new RuntimeException($message);
}
Then:
function getUserName(?User $user): string
{
if ($user === null) {
fail('User not found');
}
return $user->name;
}
A static analyzer can understand:
After
fail(), execution doesn’t continue.
That’s very useful in large applications.
18. #[Deprecated]
Introduced in PHP 8.4
When you have an old method that you don’t want people to use anymore, you can mark it as deprecated.
Example:
#[\Deprecated(
message: 'Use getUserById() instead',
since: '2.0'
)]
function getUser($id)
{
// old implementation
}
Calling it produces a deprecation notice.
Why is this useful?
Imagine you have a large application.
You have:
getUser()
but you’ve created:
getUserById()
You don’t want to immediately delete the old function because hundreds of places may still use it.
You can mark it:
#[\Deprecated(
message: 'Use getUserById() instead',
since: '2.0'
)]
Now developers get a warning when they use it.
This makes gradual migrations easier.
Senior developer use case
This is particularly useful when maintaining:
function redirect(string $url): never
{
header("Location: $url");
exit;
}
Then:
function checkAuthentication(): never
{
redirect('/login');
}
Why is this useful?
It gives static-analysis tools more information.
function fail(string $message): never
{
throw new RuntimeException($message);
}
- large applications
- shared libraries
- packages
- public APIs
- framework components
It’s basically PHP saying:
“This still works, but please stop using it.”
As of PHP 8.5, #[Deprecated] can also target traits and compile-time constants outside classes.
19. Fibers
Introduced in PHP 8.1
This is probably the most advanced topic in your list.
Don’t worry if it doesn’t immediately make sense.
The simple explanation is:
A Fiber lets PHP pause a running function and continue it later.
Imagine:
Function starts
↓
Do some work
↓
PAUSE
↓
Other work can happen
↓
RESUME
↓
Continue where we stopped
Example:
$fiber = new Fiber(function (): void {
echo "Start\n";
$value = Fiber::suspend('paused');
echo "Received: $value\n";
});
$result = $fiber->start();
echo "Fiber returned: $result\n";
$fiber->resume('Hello');
Conceptually, the output is:
Start
Fiber returned: paused
Received: Hello
The important thing is that:
Fiber::suspend();
pauses the Fiber.
Later:
$fiber->resume();
continues it.
PHP describes Fibers as full-stack interruptible functions. Unlike generators, a Fiber has its own call stack and can be suspended from deep inside nested function calls.
Why were Fibers added?
They are useful for building things such as:
- asynchronous systems
- event loops
- non-blocking I/O abstractions
- concurrency libraries
- async frameworks
For example, libraries such as async/event-loop systems can use Fibers so application code can look more synchronous while the underlying system handles concurrency.
Are Fibers mandatory for every senior PHP developer?
You should understand the concept.
But you probably won’t write raw Fibers every day in a normal Laravel/Symfony CRUD application.
If you work on:
Laravel application
Symfony API
WordPress plugin
normal SaaS backend
REST API
you are much more likely to use:
Enums
Readonly
Attributes
Dependency Injection
Union Types
Property Hooks
Asymmetric Visibility
than manually creating Fibers.
But if you work on:
high-performance systems
async PHP
event loops
network servers
concurrent applications
Fibers become much more important.
Putting Everything Together
The really interesting part is that these features work very well together.
Consider a modern API DTO.
Instead of old-style PHP:
class User
{
private int $id;
private string $name;
private string $email;
public function __construct(
int $id,
string $name,
string $email
) {
$this->id = $id;
$this->name = $name;
$this->email = $email;
}
public function getName(): string
{
return $this->name;
}
}
Modern PHP could look like:
readonly class User
{
public function __construct(
public int $id,
public string $name,
public string $email,
public UserStatus $status,
) {
}
}
And:
enum UserStatus: string
{
case Active = 'active';
case Blocked = 'blocked';
}
Now we have:
readonly class→ object cannot be modified after initialization- constructor property promotion → less boilerplate
- typed properties → clear contracts
- enum → valid statuses are enforced
- modern PHP syntax → less code, but more meaning
That’s the direction modern PHP has taken.
A Realistic Senior-Level Example
Don’t worry if it doesn’t immediately make sense.
A Fiber lets PHP pause a running function and continue it later.
Function starts
↓
Do some work
↓
PAUSE
↓
Other work can happen
↓
RESUME
↓
Continue where we stopped
Example:
$fiber = new Fiber(function (): void {
echo "Start\n";
$value = Fiber::suspend('paused');
echo "Received: $value\n";
});
$result = $fiber->start();
echo "Fiber returned: $result\n";
$fiber->resume('Hello');
Conceptually, the output is:
Start
Fiber returned: paused
Received: Hello
The important thing is that:
Fiber::suspend();
pauses the Fiber.
Later:
$fiber->resume();
continues it.
PHP describes Fibers as full-stack interruptible functions. Unlike generators, a Fiber has its own call stack and can be suspended from deep inside nested function calls.
Why were Fibers added?
They are useful for building things such as:
- asynchronous systems
- event loops
- non-blocking I/O abstractions
- concurrency libraries
- async frameworks
For example, libraries such as async/event-loop systems can use Fibers so application code can look more synchronous while the underlying system handles concurrency.
Are Fibers mandatory for every senior PHP developer?
If you work on:
Let’s combine several features:
enum OrderStatus: string
{
case Pending = 'pending';
case Paid = 'paid';
case Cancelled = 'cancelled';
}
Then:
readonly class Order
{
public function __construct(
public int $id,
public float $amount,
public OrderStatus $status,
) {
}
}
Service:
class OrderService
{
public function findOrder(int|string $id): Order
{
// ...
}
}
And:
class PaymentService
{
public function __construct(
private OrderService $orders
) {
}
public function pay(int|string $orderId): void
{
$order = $this->orders->findOrder(
id: $orderId
);
if ($order->status !== OrderStatus::Pending) {
throw new RuntimeException(
'Order cannot be paid.'
);
}
// ...
}
}
Look at how much information is visible directly in the code.
Order
├── id → int
├── amount → float
└── status → OrderStatus
findOrder()
└── accepts int OR string
PaymentService
└── receives OrderService through constructor
OrderStatus
└── only Pending / Paid / Cancelled
This is what modern PHP’s type system is trying to give you:
Make invalid states difficult to represent.
What Should a Senior PHP Developer Actually Remember?
You don’t need to memorize every syntax detail.
Instead, remember the problem each feature solves.
| Feature | Remember it as |
|---|---|
| Named arguments | “I can call parameters by name.” |
| Union types | “This value can be A OR B.” |
| Attributes | “Metadata directly attached to PHP code.” |
| Constructor promotion | “Declare + initialize properties in the constructor.” |
| Enums | “A fixed set of valid values.” |
| Readonly property | “Set once, then never replace.” |
| Readonly class | “The whole object is readonly.” |
true / false / null types | “Extremely precise return/value types.” |
| Typed constants | “Constants can have a type.” |
#[Override] | “PHP checks that I really override something.” |
| Property hooks | “Run logic when a property is read/written.” |
| Asymmetric visibility | “Public read, restricted write.” |
Stringable | “An object that can behave like a string.” |
throw expression | “I can throw inside an expression.” |
#[SensitiveParameter] | “Hide secrets from stack traces.” |
json_validate() | “Check JSON without decoding it.” |
never | “This function does not come back.” |
#[Deprecated] | “This API still works, but don’t use it.” |
| Fibers | “Pause and resume execution.” |
My Recommended Learning Priority
If your goal is a Senior PHP Developer interview, I would study them in this order:
Level 1 — Absolutely know these
1. Union types
int|string
2. Named arguments
sendEmail(
to: $email,
subject: $subject
);
3. Constructor property promotion
public function __construct(
private UserRepository $repository
) {}
4. Attributes
#[Route('/users')]
5. Enums
enum Status: string
{
case Active = 'active';
}
6. Readonly
public readonly string $id;
and:
readonly class User {}
These are fundamental modern PHP concepts. PHP 8.0 introduced named arguments, union types, attributes, and constructor property promotion; PHP 8.1 added enums and readonly properties; PHP 8.2 added readonly classes.
Level 2 — Very Important for Modern PHP
Learn these well:
#[Override]
public private(set) string $name;
public string $name {
set(string $value) {
$this->name = trim($value);
}
}
These features are particularly relevant if your target codebase is running PHP 8.4+, because property hooks and asymmetric visibility were introduced in PHP 8.4.
Level 3 — Know and Recognize
Understand these:
#[SensitiveParameter]
json_validate($json);
function fail(): never
#[Deprecated]
public const string VERSION = '1.0';
These aren’t necessarily things you’ll use every day, but a senior developer should be comfortable reading and explaining them.
Typed class constants and json_validate() arrived in PHP 8.3, while #[Deprecated] arrived in PHP 8.4.
Level 4 — Understand the Concept
Finally:
Fiber
You should understand:
What is a Fiber?
Why would an async library use it?
How is it different from a Generator?
When would I need it?
You don’t necessarily need to memorize the entire Fiber API unless you’re working on asynchronous/concurrent PHP systems.
Fibers have been available since PHP 8.1.
The Bigger Senior Developer Lesson
The most important thing here isn’t the syntax.
A senior developer should look at these features and understand the design philosophy behind them.
Modern PHP is increasingly encouraging developers to make their intentions explicit.
Instead of:
function process($data)
you can write:
function process(Order|Invoice $data): void
Instead of:
class User
{
private string $name;
}
with lots of getter/setter boilerplate, you can use:
readonly class User
{
public function __construct(
public string $name
) {}
}
Instead of:
$status = 'active';
you can have:
$status = UserStatus::Active;
Instead of relying on comments to say:
"This method overrides a parent method"
you can tell PHP:
#[Override]
Instead of:
"Everybody can read this, but please don't modify it."
you can express the rule:
public private(set) string $name;
That is the real evolution of modern PHP:
Put more of your application’s rules into the language and type system, instead of leaving them as comments, conventions, or developer assumptions.
And that is exactly the mindset I would expect from a senior PHP developer.
Final Cheat Sheet
If you remember nothing else, remember this:
PHP 8.0
├── Named Arguments
├── Union Types
├── Attributes
└── Constructor Property Promotion
PHP 8.1
├── Enums
├── Readonly Properties
├── never
└── Fibers
PHP 8.2
├── Readonly Classes
└── true / false / null standalone types
PHP 8.3
├── Typed Class Constants
├── #[Override]
└── json_validate()
PHP 8.4
├── Property Hooks
├── Asymmetric Visibility
└── #[Deprecated]
PHP 8.5
└── More improvements to existing features
(including #[Override] support for properties)
The version progression is confirmed by the official PHP release documentation.
