Attributes
Attributes (속성)
PHP 8부터 등장한 기능이에요. 속성(attribute)은 클래스, 메서드, 함수, 파라미터, 프로퍼티, 상수에 구조화된, 기계가 읽을 수 있는 메타데이터를 붙이는 방법이에요. 이 글에서는 속성이 무엇인지, 문법은 어떻게 되는지, 리플렉션으로 어떻게 읽는지, 그리고 속성 클래스는 어떻게 선언하는지 순서대로 봐볼게요.
개요 (Overview)
PHP 속성은 클래스, 메서드, 함수, 파라미터, 프로퍼티, 상수에 구조화된 기계가 읽을 수 있는 메타데이터를 붙여줘요. 이 메타데이터는 Reflection API를 통해 런타임에 조회할 수 있어서, 코드를 수정하지 않고도 동적인 동작을 만들 수 있죠. 속성은 메타데이터로 코드를 꾸미는 선언적인 방법을 제공해요.
속성이 유용한 가장 큰 이유는 기능의 구현과 사용을 분리할 수 있다는 점이에요. 인터페이스가 메서드를 강제해서 구조를 정의한다면, 속성은 메서드·함수·프로퍼티·상수처럼 여러 요소에 메타데이터를 얹어줘요. 인터페이스가 '이 메서드를 반드시 구현해라'라고 강제하는 자리에서, 속성은 코드 구조를 건드리지 않은 채 코드에 정보를 붙이는 거예요.
속성은 선택적인 인터페이스 메서드를 보완하거나 대체할 수도 있어요. 구조를 강제하는 대신 메타데이터를 제공하는 식이죠. 예를 들어 애플리케이션에서 어떤 작업을 나타내는 ActionHandler 인터페이스를 생각해볼게요. 일부 구현체는 준비(setup) 단계가 필요하고, 어떤 건 필요 없을 수 있어요. 모든 구현 클래스에 setUp() 메서드를 강제하는 대신, 속성을 써서 '이 작업은 준비가 필요하다'는 걸 표시할 수 있어요. 이렇게 하면 유연성이 커지고, 필요할 때는 같은 속성을 여러 번 적용할 수도 있죠.
<?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);
속성 문법 (Attribute syntax)
속성 문법은 몇 가지 핵심 요소로 이루어져요. 속성 선언은 #[로 시작해서 ]로 끝나요. 그 안에는 쉼표로 구분해 속성을 하나 이상 나열할 수 있어요. 속성 이름은 using-namespaces-basics에서 설명한 것처럼 비정규(unqualified), 정규(qualified), 완전 정규(fully-qualified) 중 어떤 형태든 쓸 수 있어요.
속성에 넘기는 인자는 선택사항이고 괄호 ()로 감싸요. 인자로는 리터럴 값이나 상수 표현식만 사용할 수 있어요. 위치 인자(positional)와 이름 인자(named) 문법을 모두 지원해요.
속성 이름과 그 인자는 하나의 클래스로 해석되고, 리플렉션 API를 통해 해당 속성의 인스턴스가 요청될 때 인자는 그 클래스의 생성자로 전달돼요. 그래서 속성 하나당 클래스 하나를 만드는 걸 권장해요.
<?php
// a.php
namespace MyExample;
use Attribute;
#[Attribute]
class MyAttribute
{
const VALUE = 'value';
private $value;
public function __construct($value = null)
{
$this->value = $value;
}
}
// b.php
namespace Another;
use MyExample\MyAttribute;
#[MyAttribute]
#[\MyExample\MyAttribute]
#[MyAttribute(1234)]
#[MyAttribute(value: 1234)]
#[MyAttribute(MyAttribute::VALUE)]
#[MyAttribute(array("key" => "value"))]
#[MyAttribute(100 + 200)]
class Thing
{
}
#[MyAttribute(1234), MyAttribute(5678)]
class AnotherThing
{
}
리플렉션 API로 속성 읽기
클래스, 메서드, 함수, 파라미터, 프로퍼티, 클래스 상수에 붙은 속성을 읽으려면 Reflection API가 제공하는 getAttributes() 메서드를 사용해요. 이 메서드는 ReflectionAttribute 인스턴스 배열을 돌려줘요. 각 인스턴스에서 속성의 이름과 인자를 조회할 수 있고, 그 속성이 나타내는 실제 인스턴스를 만들 수도 있어요.
반영된 속성 파악(representation)과 실제 인스턴스를 분리해 두면 오류 처리를 더 잘 제어할 수 있어요. 예를 들어 속성 클래스가 없는 경우, 인자 타입이 잘못된 경우, 값이 빠진 경우 같은 문제들을 말이죠. 속성 클래스의 객체는 ReflectionAttribute::newInstance()를 호출한 뒤에야 만들어져요. 그래서 인자 검증이 그 시점에 일어나게 돼요.
<?php
#[Attribute]
class MyAttribute
{
public $value;
public function __construct($value)
{
$this->value = $value;
}
}
#[MyAttribute(value: 1234)]
class Thing
{
}
function dumpAttributeData($reflection) {
$attributes = $reflection->getAttributes();
foreach ($attributes as $attribute) {
var_dump($attribute->getName());
var_dump($attribute->getArguments());
var_dump($attribute->newInstance());
}
}
dumpAttributeData(new ReflectionClass(Thing::class));
위 예제의 출력은 다음과 같아요.
string(11) "MyAttribute"
array(1) {
["value"]=>
int(1234)
}
object(MyAttribute)#3 (1) {
["value"]=>
int(1234)
}
반영 인스턴스의 모든 속성을 돌면서 살펴보는 대신, 특정 속성 클래스만 골라낼 수도 있어요. getAttributes()에 속성 클래스 이름을 인자로 넘기면 그 클래스의 속성만 돌려받아요.
<?php
#[Attribute]
class MyAttribute
{
public $value;
public function __construct($value)
{
$this->value = $value;
}
}
#[MyAttribute(value: 1234)]
class Thing
{
}
function dumpMyAttributeData($reflection) {
$attributes = $reflection->getAttributes(MyAttribute::class);
foreach ($attributes as $attribute) {
var_dump($attribute->getName());
var_dump($attribute->getArguments());
var_dump($attribute->newInstance());
}
}
dumpMyAttributeData(new ReflectionClass(Thing::class));
속성 클래스 선언하기
속성은 클래스 하나당 하나씩 따로 정의하는 걸 권장해요. 가장 단순한 경우에는 #[Attribute] 선언이 붙은 빈 클래스면 충분해요. 이 속성 클래스는 use 문으로 전역 네임스페이스에서 가져올 수 있어요.
<?php
namespace Example;
use Attribute;
#[Attribute]
class MyAttribute
{
}
속성을 적용할 수 있는 선언의 종류를 제한하고 싶다면, #[Attribute] 선언의 첫 번째 인자로 비트마스크(bitmask)를 넘기면 돼요.
<?php
namespace Example;
use Attribute;
#[Attribute(Attribute::TARGET_METHOD | Attribute::TARGET_FUNCTION)]
class MyAttribute
{
}
이렇게 하면 MyAttribute 를 다른 종류의 대상에 선언했을 때, ReflectionAttribute::newInstance()를 호출하는 시점에 예외가 발생해요.
지정할 수 있는 대상은 다음과 같아요.
Attribute::TARGET_CLASS— 클래스Attribute::TARGET_FUNCTION— 함수Attribute::TARGET_METHOD— 메서드Attribute::TARGET_PROPERTY— 프로퍼티Attribute::TARGET_CLASS_CONSTANT— 클래스 상수Attribute::TARGET_PARAMETER— 파라미터Attribute::TARGET_ALL— 모든 대상
기본적으로 속성은 선언 하나당 한 번만 사용할 수 있어요. 같은 속성을 한 선언 안에서 여러 번 사용하게 하고 싶다면, #[Attribute] 선언의 비트마스크에 Attribute::IS_REPEATABLE 플래그를 넣어주면 돼요.
<?php
namespace Example;
use Attribute;
#[Attribute(Attribute::TARGET_METHOD | Attribute::TARGET_FUNCTION | Attribute::IS_REPEATABLE)]
class MyAttribute
{
}