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 example
<?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 example
<?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 example
<?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 example
<?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 example
<?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 example
<?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 example
<?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 example
<?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.