advanced php language
Predefined Attributes
PHP has attributes that are understood by the engine itself. They are different from framework attributes because PHP gives them built-in behaviour.
You do not need to memorise every detail on day one, but you should recognise them in code reviews and know why they were added.
Attribute
#[Attribute] marks a class as an attribute class. Without it, the class is just a normal class and should not be used as metadata.
<?php
declare(strict_types=1);
#[Attribute(Attribute::TARGET_METHOD)]
final class Route
{
public function __construct(
public string $path,
) {
}
}
final class ProductController
{
#[Route('/products')]
public function index(): string
{
return 'products';
}
}
$method = new ReflectionMethod(ProductController::class, 'index');
$attribute = $method->getAttributes(Route::class)[0]->newInstance();
echo $attribute->path . PHP_EOL;
// Prints:
// /products
Frameworks use this pattern for routes, validation rules, ORM mapping, event listeners, tests, and dependency injection.
Override
#[\Override] tells PHP that a method is intended to override a parent method or implement an interface method. PHP reports an error if it does not.
<?php
declare(strict_types=1);
interface SendsMail
{
public function send(string $to): void;
}
final class LogMailer implements SendsMail
{
#[\Override]
public function send(string $to): void
{
echo 'sent to ' . $to . PHP_EOL;
}
}
(new LogMailer())->send('ada@example.com');
// Prints:
// sent to ada@example.com
This protects against typo bugs during refactors. If the interface method changes and this method no longer overrides it, PHP can tell you.
Deprecated
#[\Deprecated] marks code that still exists but should no longer be used. It is useful during migrations where old callers need time to move.
<?php
declare(strict_types=1);
#[\Deprecated('Use normaliseEmail() instead.')]
function cleanEmail(string $email): string
{
return normaliseEmail($email);
}
function normaliseEmail(string $email): string
{
return strtolower(trim($email));
}
echo normaliseEmail(' ADA@EXAMPLE.COM ') . PHP_EOL;
// Prints:
// ada@example.com
Deprecation is not deletion. It is a warning path that lets a team change callers before removing the old API.
NoDiscard
#[\NoDiscard] marks a function or method where ignoring the return value is probably a mistake.
<?php
declare(strict_types=1);
#[\NoDiscard('Store the returned value; strings are immutable.')]
function normaliseEmail(string $email): string
{
return strtolower(trim($email));
}
$email = normaliseEmail(' ADA@EXAMPLE.COM ');
echo $email . PHP_EOL;
// Prints:
// ada@example.com
This is useful for immutable updates, result objects, parser results, and functions where the returned value carries the important outcome.
SensitiveParameter
#[\SensitiveParameter] marks a parameter as sensitive so stack traces avoid exposing the value.
<?php
declare(strict_types=1);
function authenticate(string $email, #[\SensitiveParameter] string $password): void
{
if ($password === '') {
throw new InvalidArgumentException('Password is required.');
}
echo 'checked ' . $email . PHP_EOL;
}
authenticate('ada@example.com', 'secret');
// Prints:
// checked ada@example.com
Use it for passwords, tokens, API keys, session secrets, and anything that should not appear in logs or traces.
AllowDynamicProperties
#[\AllowDynamicProperties] permits old-style dynamic properties on a class.
<?php
declare(strict_types=1);
#[\AllowDynamicProperties]
final class LegacyRecord
{
}
$record = new LegacyRecord();
$record->externalId = 'A100';
echo $record->externalId . PHP_EOL;
// Prints:
// A100
Treat this as a migration tool, not a design target. Modern PHP code should usually declare properties explicitly.
ReturnTypeWillChange
#[\ReturnTypeWillChange] suppresses compatibility notices when older code implements an internal interface without the modern return type.
<?php
declare(strict_types=1);
final class LegacyCollection implements IteratorAggregate
{
/**
* @return Traversable<int, string>
*/
#[\ReturnTypeWillChange]
public function getIterator()
{
return new ArrayIterator(['one', 'two']);
}
}
foreach (new LegacyCollection() as $value) {
echo $value . PHP_EOL;
}
// Prints:
// one
// two
Use this only when maintaining legacy compatibility. New code should normally add the correct return type instead.
What You Should Be Able To Do
After this lesson, you should be able to recognise PHP's predefined attributes, explain the runtime behaviour they provide, and avoid using migration attributes as a shortcut in new code.
For junior PHP work, this matters because attributes appear in modern framework code, library code, and PHP upgrades. Recognising the built-in ones helps you understand which metadata PHP itself enforces.
Practice
Practice: Use Engine Attributes Deliberately
Create a small example that uses predefined attributes where they solve real problems.
Task
Build:
- an interface with one method
- an implementation that marks the method with
#[\Override] - a function with
#[\NoDiscard] - a function that marks a password parameter with
#[\SensitiveParameter]
Use strict types. Show the implementation working and keep the expected output inside the PHP code block as printed lines or comments.
Afterward, add a short note explaining why these attributes are useful to PHP itself, not only to a framework.
Show solution
This solution uses predefined attributes for override checking, return-value protection, and sensitive parameter handling.
<?php
declare(strict_types=1);
interface PasswordHasher
{
public function hash(string $password): string;
}
final class Sha256PasswordHasher implements PasswordHasher
{
#[\Override]
public function hash(#[\SensitiveParameter] string $password): string
{
return hash('sha256', $password);
}
}
#[\NoDiscard('Use the returned normalised email value.')]
function normaliseEmail(string $email): string
{
return strtolower(trim($email));
}
$hasher = new Sha256PasswordHasher();
$email = normaliseEmail(' ADA@EXAMPLE.COM ');
echo $email . PHP_EOL;
echo strlen($hasher->hash('secret')) . PHP_EOL;
// Prints:
// ada@example.com
// 64
These attributes are useful to PHP itself. #[\Override] lets PHP verify the method really implements the interface, #[\NoDiscard] warns when an important return value is ignored, and #[\SensitiveParameter] keeps secrets out of stack traces.