(PHP 8)
PHP attributes provide structured, machine-readable metadata for classes, methods, functions, parameters, properties, and constants. They can be inspected at runtime via the Reflection API, enabling dynamic behavior without modifying code. Attributes provide a declarative way to annotate code with metadata.
Attributes enable the decoupling of a feature's implementation from its usage. While interfaces define structure by enforcing methods, attributes provide metadata across multiple elements, including methods, functions, properties, and constants. Unlike interfaces, which enforce method implementations, attributes annotate code without altering its structure.
Attributes can complement or replace optional interface methods by providing metadata instead of
enforced structure. Consider an ActionHandler
interface that represents an
operation in an application. Some implementations may require a setup step while others do not.
Instead of forcing all classes implementing ActionHandler
to define a
setUp()
method, an attribute can indicate setup requirements. This approach
increases flexibility, allowing attributes to be applied multiple times when necessary.
Example #1 Implementing optional methods of an interface with Attributes
<?php
interface ActionHandler
{
public function execute();
}
#[Attribute]
class SetUp {}
class CopyFile implements ActionHandler
{
public string $fileName;
public string $targetDirectory;
#[SetUp]
public function fileExists()
{
if (!file_exists($this->fileName)) {
throw new RuntimeException("File does not exist");
}
}
#[SetUp]
public function targetDirectoryExists()
{
if (!file_exists($this->targetDirectory)) {
mkdir($this->targetDirectory);
} elseif (!is_dir($this->targetDirectory)) {
throw new RuntimeException("Target directory $this->targetDirectory is not a directory");
}
}
public function execute()
{
copy($this->fileName, $this->targetDirectory . '/' . basename($this->fileName));
}
}
function executeAction(ActionHandler $actionHandler)
{
$reflection = new ReflectionObject($actionHandler);
foreach ($reflection->getMethods() as $method) {
$attributes = $method->getAttributes(SetUp::class);
if (count($attributes) > 0) {
$methodName = $method->getName();
$actionHandler->$methodName();
}
}
$actionHandler->execute();
}
$copyAction = new CopyFile();
$copyAction->fileName = "/tmp/foo.jpg";
$copyAction->targetDirectory = "/home/user";
executeAction($copyAction);
While the example displays us what we can accomplish with attributes, it should be kept in mind that the main idea behind attributes is to attach static metadata to code (methods, properties, etc.).
This metadata often includes concepts such as "markers" and "configuration". For example, you can write a serializer using reflection that only serializes marked properties (with optional configuration, such as field name in serialized file). This is reminiscent of serializers written for C# applications.
That said, full reflection and attributes go hand in hand. If your use case is satisfied by inheritance or interfaces, prefer that. The most common use case for attributes is when you have no prior information about the provided object/class.
<?php
interface JsonSerializable
{
public function toJson() : array;
}
?>
versus, using attributes,
<?php
#[Attribute]
class JsonSerialize
{
public function __constructor(public ?string $fieldName = null) {}
}
class VersionedObject
{
#[JsonSerialize]
public const version = '0.0.1';
}
public class UserLandClass extends VersionedObject
{
#[JsonSerialize('call it Jackson')]
public string $myValue;
}
?>
The example above is a little extra convoluted with the existence of the VersionedObject class as I wished to display that with attribute mark ups, you do not need to care how the base class manages its attributes (no call to parent in overriden method).
I've tried Harshdeeps example and it didn't run out of the box and I think it is not complete, so I wrote a complete and working naive example regarding attribute based serialization.
<?php
declare(strict_types=1);
#[Attribute(Attribute::TARGET_CLASS_CONSTANT|Attribute::TARGET_PROPERTY)]
class JsonSerialize
{
public function __construct(public ?string $fieldName = null) {}
}
class VersionedObject
{
#[JsonSerialize]
public const version = '0.0.1';
}
class UserLandClass extends VersionedObject
{
protected string $notSerialized = 'nope';
#[JsonSerialize('foobar')]
public string $myValue = '';
#[JsonSerialize('companyName')]
public string $company = '';
#[JsonSerialize('userLandClass')]
protected ?UserLandClass $test;
public function __construct(?UserLandClass $userLandClass = null)
{
$this->test = $userLandClass;
}
}
class AttributeBasedJsonSerializer {
protected const ATTRIBUTE_NAME = 'JsonSerialize';
public function serialize($object)
{
$data = $this->extract($object);
return json_encode($data, JSON_THROW_ON_ERROR);
}
protected function reflectProperties(array $data, ReflectionClass $reflectionClass, object $object)
{
$reflectionProperties = $reflectionClass->getProperties();
foreach ($reflectionProperties as $reflectionProperty) {
$attributes = $reflectionProperty->getAttributes(static::ATTRIBUTE_NAME);
foreach ($attributes as $attribute) {
$instance = $attribute->newInstance();
$name = $instance->fieldName ?? $reflectionProperty->getName();
$value = $reflectionProperty->getValue($object);
if (is_object($value)) {
$value = $this->extract($value);
}
$data[$name] = $value;
}
}
return $data;
}
protected function reflectConstants(array $data, ReflectionClass $reflectionClass)
{
$reflectionConstants = $reflectionClass->getReflectionConstants();
foreach ($reflectionConstants as $reflectionConstant) {
$attributes = $reflectionConstant->getAttributes(static::ATTRIBUTE_NAME);
foreach ($attributes as $attribute) {
$instance = $attribute->newInstance();
$name = $instance->fieldName ?? $reflectionConstant->getName();
$value = $reflectionConstant->getValue();
if (is_object($value)) {
$value = $this->extract($value);
}
$data[$name] = $value;
}
}
return $data;
}
protected function extract(object $object)
{
$data = [];
$reflectionClass = new ReflectionClass($object);
$data = $this->reflectProperties($data, $reflectionClass, $object);
$data = $this->reflectConstants($data, $reflectionClass);
return $data;
}
}
$userLandClass = new UserLandClass();
$userLandClass->company = 'some company name';
$userLandClass->myValue = 'my value';
$userLandClass2 = new UserLandClass($userLandClass);
$userLandClass2->company = 'second';
$userLandClass2->myValue = 'my second value';
$serializer = new AttributeBasedJsonSerializer();
$json = $serializer->serialize($userLandClass2);
var_dump(json_decode($json, true));