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)
}