PHP 매직 메서드
PHP 매직 메서드 (Magic Methods)
매직 메서드는 객체에 어떤 동작을 수행할 때 PHP가 기본으로 하는 일을 대신하도록 만든 특별한 메서드예요. 평소에는 직접 부를 일이 거의 없지만, 코드의 특정 지점에서 PHP가 알아서 호출해 주죠.
주의
__로 시작하는 모든 메서드 이름은 PHP가 미리 예약해 둔 이름이에요. PHP의 기본 동작을 바꾸려는 게 아니라면 그런 이름을 쓰지 않는 게 좋아요.
매직 메서드로 취급되는 이름은 이렇게 있어요.
__construct(), __destruct(), __call(), __callStatic(), __get(), __set(), __isset(), __unset(), __serialize(), __unserialize(), __sleep(), __wakeup(), __toString(), __invoke(), __set_state(), __clone(), __debugInfo().
Warning
__construct(),__destruct(),__clone()를 제외한 모든 매직 메서드는public으로 선언해야 해요. 그렇지 않으면E_WARNING이 발생해요. PHP 8.0.0 이전에는__sleep(),__wakeup(),__serialize(),__unserialize(),__set_state()에 대해 별도 진단이 내려지지 않았어요.
Warning 매직 메서드 정의에 타입 선언을 사용한다면, 그 시그니처가 이 문서에서 설명한 것과 정확히 같아야 해요. 다르면 치명적 오류(fatal error)가 발생해요. PHP 8.0.0 이전에는 진단이 없었죠. 다만
__construct()와__destruct()는 반환 타입을 선언하면 안 돼요. 선언하면 치명적 오류가 나요.
__serialize()와 __unserialize()
public function __serialize(): array
public function __unserialize(array $data): void
serialize()는 클래스에 __serialize()라는 매직 메서드가 있는지 확인해요. 있으면 직렬화가 진행되기 전에 그 함수를 먼저 실행해요. 이 함수는 객체의 직렬화된 형태를 나타내는 키/값 쌍의 연관 배열을 만들어 반환해야 해요. 배열을 반환하지 않으면 TypeError가 발생해요.
Note 같은 객체에
__serialize()와__sleep()이 모두 정의돼 있으면__serialize()만 호출되고__sleep()은 무시돼요. 또 객체가Serializable인터페이스를 구현하고 있어도 그 인터페이스의serialize()는 무시되고__serialize()가 대신 쓰여요.
__serialize()의 의도는 객체를 직렬화하기 좋은 형태로 자유롭게 표현하는 데 있어요. 배열의 원소가 객체의 프로퍼티와 대응할 수도 있지만, 꼭 그럴 필요는 없어요.
반대로 unserialize()는 __unserialize()라는 매직 메서드가 있는지 확인해요. 있으면 이 함수에 __serialize()가 반환했던 복원된 배열이 전달돼요. 그러면 그 배열에서 객체의 프로퍼티를 다시 복원할 수 있어요.
Note 같은 객체에
__unserialize()와__wakeup()이 모두 정의돼 있으면__unserialize()만 호출되고__wakeup()은 무시돼요.
Note 이 기능은 PHP 7.4.0부터 사용할 수 있어요.
예제 #1 직렬화와 역직렬화
<?php
class Connection
{
protected $link;
private $dsn, $username, $password;
public function __construct($dsn, $username, $password)
{
$this->dsn = $dsn;
$this->username = $username;
$this->password = $password;
$this->connect();
}
private function connect()
{
$this->link = new PDO($this->dsn, $this->username, $this->password);
}
public function __serialize(): array
{
return [
'dsn' => $this->dsn,
'user' => $this->username,
'pass' => $this->password,
];
}
public function __unserialize(array $data): void
{
$this->dsn = $data['dsn'];
$this->username = $data['user'];
$this->password = $data['pass'];
$this->connect();
}
}?>
__sleep()와 __wakeup()
Warning 이 직렬화 메커니즘은 PHP 8.5.0부터 소프트 디프리케이트(soft-deprecated) 상태예요. 하위 호환성을 위해 유지되고 있지만, 새 코드와 기존 코드 모두
__serialize()와__unserialize()매직 메서드를 쓰도록 옮기는 게 좋아요.
public function __sleep(): array
public function __wakeup(): void
serialize()는 클래스에 __sleep()이라는 매직 메서드가 있는지 확인해요. 있으면 직렬화가 진행되기 전에 그 함수를 실행해요. 이 함수는 객체를 정리할 수 있고, 직렬화해야 할 객체의 모든 변수 이름을 담은 배열을 반환해야 해요. 메서드가 아무것도 반환하지 않으면 null이 직렬화되고 E_NOTICE가 발생해요.
Note
__sleep()은 부모 클래스의 private 프로퍼티 이름을 반환할 수 없어요. 그렇게 하면E_NOTICE수준의 오류가 나요. 그럴 때는__serialize()를 쓰세요.
Note PHP 8.0.0부터
__sleep()이 배열이 아닌 값을 반환하면 경고(warning)가 발생해요. 이전에는 notice였어요.
__sleep()의 의도는 처리되지 않은 데이터를 확정(commit)하거나 비슷한 정리 작업을 하는 데 있어요. 또 아주 큰 객체를 전부 저장할 필요가 없을 때도 유용해요.
반대로 unserialize()는 __wakeup()이라는 매직 메서드가 있는지 확인해요. 있으면 이 함수가 객체가 가질 수 있는 어떤 리소스든 다시 재구성할 수 있어요.
__wakeup()의 의도는 직렬화 도중 끊어졌을 수 있는 데이터베이스 연결을 다시 맺고, 그 밖의 재초기화 작업을 수행하는 데 있어요.
예제 #2 Sleep과 wakeup
<?php
class Connection
{
protected $link;
private $dsn, $username, $password;
public function __construct($dsn, $username, $password)
{
$this->dsn = $dsn;
$this->username = $username;
$this->password = $password;
$this->connect();
}
private function connect()
{
$this->link = new PDO($this->dsn, $this->username, $this->password);
}
public function __sleep()
{
return array('dsn', 'username', 'password');
}
public function __wakeup()
{
$this->connect();
}
}?>
__toString()
public function __toString(): string
__toString() 메서드는 클래스가 문자열처럼 다루어질 때 어떻게 반응할지를 정하게 해줘요. 예를 들어 echo $obj;가 무엇을 출력할지를 결정하는 거예요.
Warning PHP 8.0.0부터 반환 값은 표준 PHP 타입 규칙을 따르는데, 가능하면 문자열로 강제 변환(코얼스)되고 엄격 타입(strict typing)이 꺼져 있을 때 그렇게 돼요.
엄격 타입이 켜져 있으면
Stringable객체는string타입 선언으로 받아들여지지 않아요. 그런 동작을 원한다면 타입 선언을 유니온 타입으로Stringable|string을 받게 해야 해요.PHP 8.0.0부터
__toString()메서드를 가진 클래스는 암묵적으로Stringable인터페이스를 구현하게 되고, 그래서 그 인터페이스에 대한 타입 검사를 통과해요. 그래도 명시적으로 인터페이스를 구현해 두는 걸 권장해요.PHP 7.4에서는 반환 값이 반드시
string이어야 해요. 아니면Error가 발생해요.PHP 7.4.0 이전에는 반환 값이 반드시
string이어야 하고, 아니면 치명적E_RECOVERABLE_ERROR가 발생했어요.
Warning PHP 7.4.0 이전에는
__toString()메서드 안에서 예외를 던질 수 없었어요. 그러면 치명적 오류가 났어요.
예제 #3 간단한 예시
<?php
// Declare a simple class
class TestClass
{
public $foo;
public function __construct($foo)
{
$this->foo = $foo;
}
public function __toString()
{
return $this->foo;
}
}
$class = new TestClass('Hello');
echo $class;
?>
위 예시의 출력은 다음과 같아요.
Hello
__invoke()
function __invoke( ...$values): mixed
__invoke() 메서드는 스크립트가 객체를 함수처럼 호출하려 할 때 호출돼요.
예제 #4 __invoke() 사용하기
<?php
class CallableClass
{
public function __invoke($x)
{
var_dump($x);
}
}
$obj = new CallableClass;
$obj(5);
var_dump(is_callable($obj));
?>
위 예시의 출력은 다음과 같아요.
int(5)
bool(true)
예제 #5 __invoke() 사용하기
<?php
class Sort
{
private $key;
public function __construct(string $key)
{
$this->key = $key;
}
public function __invoke(array $a, array $b): int
{
return $a[$this->key] <=> $b[$this->key];
}
}
$customers = [
['id' => 1, 'first_name' => 'John', 'last_name' => 'Do'],
['id' => 3, 'first_name' => 'Alice', 'last_name' => 'Gustav'],
['id' => 2, 'first_name' => 'Bob', 'last_name' => 'Filipe']
];
// sort customers by first name
usort($customers, new Sort('first_name'));
print_r($customers);
// sort customers by last name
usort($customers, new Sort('last_name'));
print_r($customers);
?>
위 예시의 출력은 다음과 같아요.
Array
(
[0] => Array
(
[id] => 3
[first_name] => Alice
[last_name] => Gustav
)
[1] => Array
(
[id] => 2
[first_name] => Bob
[last_name] => Filipe
)
[2] => Array
(
[id] => 1
[first_name] => John
[last_name] => Do
)
)
Array
(
[0] => Array
(
[id] => 1
[first_name] => John
[last_name] => Do
)
[1] => Array
(
[id] => 2
[first_name] => Bob
[last_name] => Filipe
)
[2] => Array
(
[id] => 3
[first_name] => Alice
[last_name] => Gustav
)
)
__set_state()
static function __set_state(array $properties): object
이 정적 메서드는 var_export()로 내보내진 클래스에 대해 호출돼요.
이 메서드의 유일한 인자는 ['property' => value, ...] 형태로 내보내진 프로퍼티를 담은 배열이에요.
예제 #6 __set_state() 사용하기
<?php
class A
{
public $var1;
public $var2;
public static function __set_state($an_array)
{
$obj = new A;
$obj->var1 = $an_array['var1'];
$obj->var2 = $an_array['var2'];
return $obj;
}
}
$a = new A;
$a->var1 = 5;
$a->var2 = 'foo';
$b = var_export($a, true);
var_dump($b);
eval('$c = ' . $b . ';');
var_dump($c);
?>
위 예시의 출력은 다음과 같아요.
string(60) "A::__set_state(array(
'var1' => 5,
'var2' => 'foo',
))"
object(A)#2 (2) {
["var1"]=>
int(5)
["var2"]=>
string(3) "foo"
}
Note 객체를 내보낼 때
var_export()는 객체의 클래스가__set_state()를 구현했는지 확인하지 않아요. 그래서__set_state()가 구현돼 있지 않으면 객체를 다시 가져올 때Error예외가 발생해요. 특히 일부 내장(internal) 클래스가 그렇죠.클래스가
__set_state()를 구현한 객체만 다시 가져오도록 확인하는 것은 프로그래머의 책임이에요.
__debugInfo()
function __debugInfo(): array
이 메서드는 var_dump()가 객체를 덤프할 때 보여줄 프로퍼티를 얻기 위해 호출돼요. 객체에 이 메서드가 정의되어 있지 않으면 public, protected, private 프로퍼티가 모두 보여요.
PHP 8.5.0부터 __debugInfo()가 null을 반환하는 것은 디프리케이트됐어요. 빈 배열을 반환하는 게 좋아요.
예제 #7 __debugInfo() 사용하기
<?php
class C {
private $prop;
public function __construct($val) {
$this->prop = $val;
}
public function __debugInfo() {
return [
'propSquared' => $this->prop ** 2,
];
}
}
var_dump(new C(42));
?>
위 예시의 출력은 다음과 같아요.
object(C)#1 (1) {
["propSquared"]=>
int(1764)
}