Challenge - Reflection app
Build an automatic serializer that works with reflection and attributes.
Serialization with reflection and attributes
In this lesson you write the ReflectionSerializer class. ReflectionClass reads the structure of a class; getProperties() lists its properties. The getAttributes() call on each property returns the attached attributes as a ReflectionAttribute array.
Attributes are classes marked with #[Attribute]. An attribute like #[Field("id")] tells the serializer which key to use. newInstance() builds the attribute instance so you can read its value. You declare which properties serialize with attributes instead of hard-coding the field list.
Reading attributes
The typical attribute reading flow is: filter with $prop->getAttributes(Field::class), then build the instance with $attrs[0]->newInstance(). You read fields like name off the instance.
This pattern is declarative: serialization behavior lives next to the property, not in a separate map. Adding a new field means writing #[Field("new")] above it; the serializer stays untouched. This is a textbook application of the open-closed principle.
Exercise Write the ReflectionSerializer class
The starting code already has the ETL pipeline. Now define the Field attribute class marked with #[Attribute]; it should expose public string $name = "" as a promoted property. Then write the Product class: id and name properties tagged with #[Field("id")] and #[Field("name")].
Finally write ReflectionSerializer::serialize(object $obj): array. Read the class with new ReflectionClass($obj), iterate getProperties(), call getAttributes(Field::class) on each and skip when empty. Build the Field instance with newInstance(); if name is empty use the property name, otherwise use the name value. The demo prints Product(7, "kale") via json_encode.
Show example solution
:serialize use new ReflectionClass($obj), iterate getProperties(), call getAttributes(Field::class) on each, use newInstance() to build the Field instance and use its name as the key. Write the demo Product class too, then print the result with json_encode.
Example solution:
<?php
// ETL pipeline + Reflection: write ReflectionSerializer
function readRows(array $rows): Generator {
foreach ($rows as $row) {
yield $row;
}
}
function transform(iterable $rows, callable $fn): Generator {
foreach ($rows as $row) {
yield $fn($row);
}
}
function pipeline(array $rows, callable $fn): Generator {
yield from transform(readRows($rows), $fn);
}
function loadAll(iterable $rows): array {
$out = [];
foreach ($rows as $row) {
$out[] = $row;
}
return $out;
}
#[Attribute]
class Field {
public function __construct(public string $name = "") {}
}
class Product {
public function __construct(
#[Field("id")] public int $id,
#[Field("name")] public string $name,
) {}
}
class ReflectionSerializer {
public function serialize(object $obj): array {
$rc = new ReflectionClass($obj);
$out = [];
foreach ($rc->getProperties() as $prop) {
$attrs = $prop->getAttributes(Field::class);
if (empty($attrs)) continue;
$field = $attrs[0]->newInstance();
$key = $field->name !== "" ? $field->name : $prop->getName();
$out[$key] = $prop->getValue($obj);
}
return $out;
}
}
// Demo:
$p = new Product(7, "kale");
$s = new ReflectionSerializer();
echo json_encode($s->serialize($p), JSON_UNESCAPED_UNICODE) . "\n";
Test yourself
-
Match reflection concepts
- ReflectionClass
- Reads class structure
- getAttributes
- Returns attribute list
- newInstance
- Creates attribute instance
- getValue
- Returns property value
-
What does ReflectionProperty::getAttributes return?
- Only the property name as a string
- The property value directly
- An array of ReflectionAttribute (may be empty)
- A new ReflectionClass instance
Show answer
An array of ReflectionAttribute (may be empty) — Correct. getAttributes returns an array of ReflectionAttribute; the array is empty when no attribute is present.
-
Complete the ReflectionClass call
$rc = ____($obj);Show answer
$rc = new ReflectionClass($obj);