PHP 객체지향: 클래스·인터페이스·enum과 8.5 신기능 실측

readonly 클래스, trait 충돌, static과 self, backed enum, 프로퍼티 훅, 8.5 파이프 연산자를 PHP 8.5.6에서 실행한 진짜 에러와 객체·배열 메모리 실측으로 정리.

시리즈: PHP 강좌 3/6 (중급). 함수와 폼 처리는 할 줄 알고, 이제 코드를 클래스로 정리하고 싶은 분을 위한 글입니다. 다 읽고 나면 class·interface·abstract class·trait·enum 중 무엇을 골라야 하는지, 잘못 골랐을 때 PHP 8.5가 어떤 에러를 내는지 알게 됩니다.

회사에서 물려받은 PHP 코드를 열어 보면 생성자에서 인자 다섯 개를 프로퍼티 다섯 개에 하나씩 복사하고, 상태값은 'Y'/'N' 같은 문자열 상수로 비교하고, 필드마다 getX()가 붙어 있는 경우가 많습니다. 이 코드는 지금도 돌아가지만, 요즘 PHP라면 첫 줄에서 막아 줄 버그를 그대로 통과시킵니다. 이 글은 그런 코드를 유지보수하거나 새로 쓰는 분을 위해, 같은 일을 PHP 8.x 문법으로 다시 짜고 PHP 8.5.6 CLI(Apple Silicon Mac, M1 Pro, 따로 적지 않으면 OPcache 끔)에서 전부 실행해 실수마다 나오는 에러를 그대로 보여 줍니다.

Java나 C#을 먼저 배웠다면 개념 대부분은 그대로 통합니다. 비교는 Java 클래스·레코드·인터페이스 강좌를 참고하세요. PHP만의 함정은 trait, static, 그리고 JSON으로 안 나가는 enum입니다.

클래스, 생성자 승격, readonly

생성자 프로퍼티 승격(constructor property promotion, PHP 8.0)은 생성자 매개변수 자리에서 프로퍼티 선언과 대입을 한 번에 합니다. readonly(8.1)는 클래스 안에서 딱 한 번만 대입할 수 있게 합니다.

<?php
declare(strict_types=1);

final class User
{
    public function __construct(
        public readonly int $id,
        public readonly string $email,
        private array $roles = [],
    ) {}

    public function withEmail(string $email): static
    {
        return new static($this->id, $email, $this->roles);
    }
}

$u = new User(1, 'a@example.com');
$u2 = $u->withEmail('b@example.com');
echo $u2->email, ' / original: ', $u->email, PHP_EOL;

try {
    $u->email = 'hack@example.com';
} catch (Error $e) {
    echo get_class($e), ': ', $e->getMessage(), PHP_EOL;
}
b@example.com / original: a@example.com
Error: Cannot modify readonly property User::$email

예외 클래스를 보세요. Exception이 아니라 Error입니다. catch (Exception $e)로는 잡히지 않아서 스크립트가 그대로 죽습니다. 신입분들이 가장 많이 놓치는 부분입니다.

readonly class(8.2)는 모든 프로퍼티를 readonly로 만들고 동적 프로퍼티도 막습니다.

readonly class Point
{
    public function __construct(public int $x, public int $y) {}
}
$p = new Point(1, 2);
$p->x = 10;   // Error: Cannot modify readonly property Point::$x
$p->z = 3;    // Error: Cannot create dynamic property Point::$z

실제로 실행해서 확인한 readonly 규칙 세 가지입니다.

class A { public readonly $x; }        -> Fatal error: Readonly property A::$x must have type
class A { public readonly int $x = 5; } -> Fatal error: Readonly property A::$x cannot have default value
$b->items[] = 3;  (readonly 배열)       -> Error: Cannot indirectly modify readonly property Bag::$items

다만 readonly는 얕습니다. readonly 프로퍼티에 ArrayObject를 넣어 두면 그 안에 원소를 추가할 수 있었고, 개수가 1에서 2로 늘었습니다. readonly가 지키는 것은 참조이지 그 뒤의 객체가 아닙니다.

PHP 8.5 clone-with, 그리고 실제로 걸린 함정

PHP 8.5부터 clone에 바꿀 프로퍼티 배열을 넘길 수 있어서 withX() 같은 메서드가 짧아집니다. 처음에는 클래스 바깥에서 불렀습니다.

$p2 = clone($p, ['x' => 99]);
Fatal error: Uncaught Error: Cannot modify protected(set) readonly property Point::$x from global scope in ~/demo/a1_readonly.php:54

메시지가 규칙을 말해 줍니다. readonly 프로퍼티는 암묵적으로 protected(set)이라서 clone-with를 거쳐도 클래스 안(또는 자식 클래스)에서만 쓸 수 있습니다. 메서드 안으로 옮기면 동작합니다.

public function withX(int $x): static
{
    return clone($this, ['x' => $x]);
}
// 1,2 -> 99,2

인터페이스, 추상 클래스, trait

인터페이스는 계약입니다. 메서드 모양만 있고 상태는 없습니다. 추상 클래스는 상태와 완성된 메서드를 가질 수 있지만 직접 new 할 수 없습니다. trait는 컴파일러가 도와주는 복사·붙여넣기로, 메서드가 사용하는 클래스 안에 그대로 들어갑니다.

interface Notifier
{
    public function send(string $to, string $msg): bool;
}

abstract class BaseNotifier implements Notifier
{
    private array $log = [];
    public function send(string $to, string $msg): bool
    {
        $ok = $this->deliver($to, trim($msg));
        $this->log[] = sprintf('%s %s %s', static::class, $to, $ok ? 'OK' : 'FAIL');
        return $ok;
    }
    abstract protected function deliver(string $to, string $msg): bool;
    public function log(): array { return $this->log; }
}

trait Retries
{
    public function hello(): string { return 'Retries::hello'; }
    protected function attempt(callable $fn, int $times = 3): bool
    {
        for ($i = 1; $i <= $times; $i++) {
            if ($fn($i)) return true;
        }
        return false;
    }
}
trait Greets
{
    public function hello(): string { return 'Greets::hello'; }
}

final class MailNotifier extends BaseNotifier
{
    use Retries, Greets {
        Greets::hello insteadof Retries;
        Retries::hello as protected retriesHello;
    }
    protected function deliver(string $to, string $msg): bool
    {
        return $this->attempt(fn(int $i) => $i >= 3);   // 3번째 시도에서 성공
    }
    public function both(): string { return $this->hello() . ' + ' . $this->retriesHello(); }
}

$m = new MailNotifier();
var_dump($m->send('user@example.com', '  hi  '));
echo $m->log()[0], PHP_EOL, $m->both(), PHP_EOL;
bool(true)
MailNotifier user@example.com OK
Greets::hello + Retries::hello

두 trait에 같은 이름의 메서드가 있으면 insteadof로 이길 쪽을 정하고, as로 진 쪽을 다른 이름으로 남깁니다(여기서는 가시성도 protected로 바꿨습니다). 이 블록이 없으면 클래스가 컴파일되지 않습니다.

Fatal error: Trait method B::hello has not been applied as C::hello, because of collision with A::hello in ~/demo/a3b.php on line 4

첫 주에 꼭 만나는 에러 두 개도 같이 봅시다.

Fatal error: Class Sq contains 1 abstract method and must therefore be declared abstract or implement the remaining method (Shape::area) in ~/demo/a3c.php on line 3
Fatal error: Uncaught Error: Cannot instantiate abstract class Base in ~/demo/a3d.php:3

첫 번째는 컴파일 에러(파일이 아예 실행되지 않음), 두 번째는 catch로 잡을 수 있는 런타임 Error입니다. PHP 8.5에서는 컴파일 에러에도 Stack trace: 블록이 붙습니다. 8.5의 Fatal error 백트레이스 기능 때문이지 다른 문제가 생긴 게 아닙니다.

판단 그림. 먼저 값이 정해진 유한한 집합 중 하나인가. 예라면 enum. 아니라면 여러 클래스가 같은 메서드 모양을 갖고 내용만 다른가. 예라면 interface. 아니라면 상태와 코드도 공유하는가. 예라면 abstract class. 아니라면 서로 관계없는 클래스에서 재사용하는 작은 메서드 묶음인가. 예라면 trait, 그 밖에는 평범한 final class(데이터를 담으면 readonly).
어떤 문법을 쓸지 고르는 순서. 왼쪽 위 질문부터 화살표를 따라가다 처음 '예'가 나오는 곳에서 멈춘다.

static 과 self: 늦은 정적 바인딩

self는 "이 줄이 적힌 클래스", static은 "메서드가 호출된 클래스"입니다. 차이는 상속에서만 드러나는데, ORM이나 모델 베이스 클래스가 바로 그 자리입니다.

class Model
{
    public static function createSelf(): self   { return new self(); }
    public static function createStatic(): static { return new static(); }
    public static function table(): string     { return strtolower(static::class) . 's'; }
    public static function tableSelf(): string { return strtolower(self::class) . 's'; }
}
class Post extends Model {}

echo get_class(Post::createSelf()), PHP_EOL;
echo get_class(Post::createStatic()), PHP_EOL;
echo Post::table(), PHP_EOL;
echo Post::tableSelf(), PHP_EOL;
Model
Post
posts
models

static 프로퍼티에도 함정이 하나 있습니다. 자식 클래스가 다시 선언하지 않으면 부모의 static 프로퍼티를 공유합니다.

class Counter
{
    protected static int $count = 0;
    public static function inc(): int { return ++static::$count; }
}
class A extends Counter {}
class B extends Counter { protected static int $count = 100; }
A::inc(); A::inc();
echo 'A: ', A::inc(), ' Counter: ', Counter::inc(), ' B: ', B::inc(), PHP_EOL;
A: 3 Counter: 4 B: 101

A와 Counter는 같은 변수를 세고 있었습니다. 자식마다 카운터를 따로 두려면 자식마다 다시 선언해야 하고, 그보다는 인스턴스 프로퍼티를 쓰는 편이 낫습니다.

enum: backed 값, from 과 tryFrom

enum(8.1)은 타입 검사기가 이해하는 닫힌 값의 집합입니다. backed enum은 DB나 JSON용으로 string 또는 int 값을 함께 가집니다.

enum Status: string
{
    case Draft = 'draft';
    case Published = 'published';
    case Archived = 'archived';

    public function label(): string
    {
        return match ($this) {
            self::Draft => '임시저장',
            self::Published => '공개',
            self::Archived => '보관',
        };
    }
    public static function fromInput(?string $s): self
    {
        return self::tryFrom((string) $s) ?? self::Draft;
    }
}

$s = Status::from('published');
echo $s->name, ' ', $s->value, ' ', $s->label(), PHP_EOL;
var_dump(Status::tryFrom('deleted'));
echo Status::fromInput(null)->name, PHP_EOL;
echo json_encode(['status' => Status::Archived]), PHP_EOL;
Status::from('deleted');
Published published 공개
NULL
Draft
{"status":"archived"}
ValueError: "deleted" is not a valid backing value for enum Status

from()은 믿을 수 있는 데이터(우리 DB에서 읽은 값: 잘못된 값이면 버그이니 예외가 맞음)에, tryFrom()은 사용자 입력(null이면 무효, 대체값은 직접 정함)에 씁니다.

declare(strict_types=1) 아래에서는 int backed enum이 $_GET에서 온 문자열 '3'을 받지 않습니다. TypeError: Priority::from(): Argument #1 ($value) must be of type int, string given. strict types를 끄면 같은 호출이 Priority::High를 돌려줬습니다. 경계에서 (int)로 바꾸세요.

흔한 실수 → 실제로 일어나는 일 → 해결

실수 1: enum을 문자열과 비교. Suit::Hearts == 'Hearts'는 bool(false)입니다. 에러도 경고도 없이 if 안이 영원히 실행되지 않습니다. 해결: case끼리 비교(=== Suit::Hearts)하거나, backed enum이면 ->value를 비교합니다.

실수 2: 순수(unbacked) enum을 json_encode. false가 돌아오고 echo는 아무것도 출력하지 않았습니다. json_last_error_msg()는 Non-backed enums have no default serialization이었고, JSON_THROW_ON_ERROR를 주면 같은 메시지의 JsonException이 납니다. 해결: 프로세스 밖으로 나갈 enum이면 backed로 만듭니다.

실수 3: case를 추가하고 match를 빠뜨림. case Live를 추가한 뒤 Draft만 처리하는 match를 호출했습니다.

Fatal error: Uncaught UnhandledMatchError: Unhandled match case Status::Live in ~/demo/a5d.php:4

PHP는 컴파일 시점에 빠진 case를 검사하지 않습니다(Swift나 Dart와 다름). 해결: match를 enum 안의 메서드로 두어 case를 추가하는 곳과 라벨을 고치는 곳을 같게 하고, PHPStan 같은 정적 분석기로 빈틈을 잡습니다.

실수 4: 일반 클래스에서 프로퍼티 이름 오타. $u->nmae = 'x'는 Deprecated: Creation of dynamic property U::$nmae is deprecated를 찍고 계속 실행됐습니다. 해결: 프로퍼티를 선언하고, 데이터 클래스는 readonly로(그러면 Error가 됨), 개발 환경에서는 Deprecated도 실패로 취급합니다.

일급 callable, 프로퍼티 훅, 8.5 파이프 연산자

일급 callable 문법(first-class callable, 8.1)은 이름(...)으로 함수나 메서드를 Closure로 만듭니다. 예전의 [$obj, 'method'] 배열도 되지만, 오타가 실행될 때에야 드러났습니다.

$make = $slug->make(...);     // $slug 에 묶인 Closure
print_r(array_map($make, ['Hello World!', '  Enums & Traits  ']));
array_map([$slug, 'mkae'], $titles);
Array
(
    [0] => hello-world
    [1] => enums-traits
)
TypeError: array_map(): Argument #1 ($callback) must be a valid callback or null, class Slug does not have a method "mkae"

$slug->mkae(...)는 적힌 바로 그 줄에서 Error: Call to undefined method Slug::mkae()로 실패해 추적하기 쉽습니다.

프로퍼티 훅과 비대칭 가시성(8.4)은 getter·setter 대부분을 대신합니다.

final class Account
{
    public string $email {
        set(string $value) {
            $value = strtolower(trim($value));
            if (!filter_var($value, FILTER_VALIDATE_EMAIL)) {
                throw new InvalidArgumentException("invalid email: $value");
            }
            $this->email = $value;
        }
    }
    public string $domain {                       // 가상 프로퍼티: 저장 공간 없음
        get => substr($this->email, strpos($this->email, '@') + 1);
    }
    public private(set) int $loginCount = 0;      // 읽기는 어디서나, 쓰기는 안에서만

    public function __construct(string $email) { $this->email = $email; }
    public function login(): void { $this->loginCount++; }
}
$a = new Account('  Alice@Example.COM ');
echo $a->email, ' | ', $a->domain, PHP_EOL;     // alice@example.com | example.com

바깥에서 쓰려고 하면 세 가지 다른 결과가 나왔습니다.

$a->loginCount = 99        -> Error: Cannot modify private(set) property Account::$loginCount from global scope
$a->domain = 'x.com'       -> Error: Property Account::$domain is read-only
$a->email = 'not-an-email' -> InvalidArgumentException: invalid email: not-an-email

가상 프로퍼티 $domain은 저장 공간이 없어서 var_dump($a)에 나오지 않습니다.

PHP 8.5의 파이프 연산자 |>는 왼쪽 값을 오른쪽 callable의 유일한 인자로 넘깁니다.

$slug = '  Hello PHP 8.5 World  '
    |> trim(...)
    |> strtolower(...)
    |> (fn(string $s) => preg_replace('/[^a-z0-9]+/', '-', $s))
    |> (fn(string $s) => trim($s, '-'));
echo $slug, PHP_EOL;   // hello-php-8-5-world

8.5.6에서 확인한 것:

코드 8.5.6 결과
괄호 없는 |> fn($s) => $s . '!' Fatal error: Arrow functions on the right hand side of |> must be parenthesized
|> str_replace('a', 'x', ...) (부분 적용) Parse error: syntax error, unexpected token ")" — 8.5에는 없음
|> intdiv(...) (인자 2개 필요) ArgumentCountError: intdiv() expects exactly 2 arguments, 1 given
참조 매개변수 함수 Error: addOne(): Argument #1 ($a) could not be passed by reference
array_first($xs), array_last($xs) (8.5 신규) 2, 3; array_first([])는 NULL
#[\NoDiscard] 반환값 무시 Warning: The return value of function withTax() should either be used or intentionally ignored by casting it as (void), …

Composer 없이 오토로딩

Composer 프로젝트도 밑에서는 spl_autoload_register를 씁니다. PSR-4 로더를 직접 열 줄 정도로 써 보면 동작이 보입니다.

// main.php — App\  =>  src/
spl_autoload_register(function (string $class): void {
    $prefix = 'App\\';
    if (!str_starts_with($class, $prefix)) {
        return;                       // 우리 것이 아니면 다른 로더에게 넘김
    }
    $file = __DIR__ . '/src/' . str_replace('\\', '/', substr($class, strlen($prefix))) . '.php';
    echo "[autoload] $class\n";
    if (is_file($file)) {
        require $file;
    }
});

$repo = new App\Repo\UserRepo();      // src/Repo/UserRepo.php, 안에서 App\Model\User 사용
foreach ($repo->all() as $u) echo $u->id, ' ', $u->name, PHP_EOL;
var_dump(class_exists('App\Model\Missing'));
new App\Model\Missing();
[autoload] App\Repo\UserRepo
[autoload] App\Model\User
1 alice
2 bob
[autoload] App\Model\Missing
bool(false)
[autoload] App\Model\Missing

Fatal error: Uncaught Error: Class "App\Model\Missing" not found in ~/demo/autoload/main.php:25

두 가지를 알 수 있습니다. use App\Model\User;만으로는 아무것도 로드되지 않습니다. 파일은 클래스를 처음 사용할 때 로드됩니다. 그리고 실패한 조회는 캐시되지 않아 같은 클래스에 대해 로더가 다시 돌았습니다. 로더가 디렉터리를 훑는 등 비싼 일을 하면 그 비용이 반복됩니다. 또 대소문자를 구분하는 리눅스 서버에서는 App\model\User가 Model/User.php를 찾지 못합니다. 기본 설정의 macOS 디스크에서는 찾아지기 때문에, 로컬에서 되던 코드가 서버에서 깨지는 단골 원인입니다.

실측: 10만 건, 객체 vs 배열

"배열이 객체보다 가볍다"는 말을 직접 재 봤습니다. 한 행은 {id, name, age, active}, 10만 건을 만들기 전과 후의 memory_get_usage() 차이, PHP 8.5.6 CLI, Apple Silicon Mac(M1 Pro), 3회 실행입니다.

$rows[] = match ($mode) {
    'assoc'    => ['id' => $i, 'name' => $name, 'age' => 20 + $i % 50, 'active' => $i % 2 === 0],
    'list'     => [$i, $name, 20 + $i % 50, $i % 2 === 0],
    'object'   => new UserObj($i, $name, 20 + $i % 50, $i % 2 === 0),   // 타입 있는 승격 프로퍼티 4개
    'stdclass' => (object) ['id' => $i, 'name' => $name, /* … */],
};
형태 메모리 행당 바이트 생성 시간 (3회 중앙값)
타입 있는 클래스 17.5 MB 183 15.6 ms
리스트 배열 26.4 MB 277 9.3 ms
연관 배열 41.7 MB 437 12.2 ms
stdClass 46.5 MB 487 13.2 ms
막대그래프. 10만 건 기준으로 타입이 있는 프로퍼티 4개짜리 클래스는 17.5MB, 숫자 키 리스트 배열은 26.4MB, 연관 배열은 41.7MB, stdClass 는 46.5MB.
{id, name, age, active} 10만 건이 쓰는 메모리. PHP 8.5.6 CLI, Apple Silicon Mac(M1 Pro)에서 memory_get_usage()로 측정. 3회 모두 같은 값.

메모리는 세 번 모두 같았습니다. 프로퍼티를 선언한 객체는 값 4개를 고정된 슬롯에 담고, 연관 배열은 행마다 키가 들어간 해시 테이블을 따로 가집니다. 행당 약 61바이트는 name 문자열이라 모든 형태에 공통입니다. 객체는 행마다 생성자를 호출하므로 만드는 시간은 조금 더 걸렸고, OPcache를 켜도 메모리 수치는 같았습니다. stdClass는 배열 같은 동적 해시 테이블에 객체 오버헤드까지 더해 가장 컸습니다. 가져오기 작업이나 리포트처럼 많은 레코드를 메모리에 들고 있다면 작은 readonly 클래스가 더 안전하고 더 작습니다.

판단표

필요한 것 쓸 것 이유
정해진 값의 집합(상태, 권한) backed enum 잘못된 값은 ValueError, JSON 출력 가능
계약 하나, 구현 여러 개 interface 테스트 대역, 드라이버 교체
공유 상태와 고정된 흐름 abstract class 템플릿 메서드, 자식은 한 단계만 채움
관계없는 클래스에 공통 헬퍼 trait 공통 부모 불필요, 충돌은 명시적으로 해결
값 객체 / DTO final readonly class 불변, 타입 있음, 이번 측정에서 연관 배열의 약 1/2.4
대입 시 검증 프로퍼티 훅 set (8.4+) getter·setter 보일러플레이트 제거
밖에서는 읽기만, 안에서는 쓰기 public private(set) (8.4+) 카운터, ID
베이스 클래스의 팩토리 new static() 자식 클래스를 반환

한계와 버전 메모

  • 모든 예제는 PHP 8.5.6 CLI에서만 실행했습니다. 버전 정보는 이전 버전을 돌려 본 것이 아니라 PHP 변경 이력 기준입니다. 승격 8.0, enum·readonly 프로퍼티·일급 callable 8.1, readonly 클래스·동적 프로퍼티 Deprecated 8.2, 프로퍼티 훅·비대칭 가시성 8.4, 파이프 연산자·clone-with·array_first/last·#[\NoDiscard]·Fatal error 백트레이스 8.5. 서버가 8.3이면 훅과 |>는 파싱 에러이니 쓰기 전에 서버에서 php -v를 확인하세요.
  • 메모리 수치는 이 레코드 모양과 64비트 PHP 8.5 기준입니다. 필드가 많거나 문자열이 길면 비율이 바뀝니다. 생성 시간은 한 자릿수 ms라 흔들림이 크므로 "객체 생성이 조금 느렸다" 이상으로 읽지 마세요.
  • 자주 호출되는 경로에서 프로퍼티 훅의 오버헤드는 재지 않았습니다.

연습 문제

int backed enum Role(Admin = 1, Editor = 2, Viewer = 3)과, $_GET['role']을 역할로 바꾸는 정적 메서드 fromQuery(?string $raw): self를 작성하세요. 값이 없거나 숫자가 아니거나 모르는 값이면 Viewer로 대체하고, strict_types=1에서 동작해야 합니다.

정답

<?php
declare(strict_types=1);

enum Role: int
{
    case Admin = 1;
    case Editor = 2;
    case Viewer = 3;

    public static function fromQuery(?string $raw): self
    {
        if ($raw === null || !ctype_digit($raw)) {
            return self::Viewer;
        }
        return self::tryFrom((int) $raw) ?? self::Viewer;
    }
}

foreach (['1', '2', '9', 'abc', null, '-1'] as $q) {
    echo var_export($q, true), ' => ', Role::fromQuery($q)->name, PHP_EOL;
}
'1' => Admin
'2' => Editor
'9' => Viewer
'abc' => Viewer
NULL => Viewer
'-1' => Viewer

(int) 변환을 빼면 strict types에서 tryFrom('1')이 TypeError: Role::tryFrom(): Argument #1 ($value) must be of type int, string given을 던집니다. tryFrom이 너그러운 것은 값이지 타입이 아닙니다.


이전: PHP 강좌 2 — 함수, 폼, 내장 서버 · 다음: PHP 강좌 4 — PDO, SQLite, SQL 인젝션

자주 묻는 질문

PHP 객체가 배열보다 메모리를 더 쓰나요?

프로퍼티를 선언한 클래스라면 오히려 적었습니다. PHP 8.5.6에서 필드 4개짜리 10만 건이 타입 있는 클래스 17.5MB, 리스트 배열 26.4MB, 연관 배열 41.7MB, stdClass 46.5MB였습니다. 생성 시간은 객체 쪽이 조금 느렸습니다(중앙값 약 15.6ms 대 연관 배열 12.2ms).

self 와 static 은 무엇이 다른가요?

self 는 코드가 적힌 클래스, static 은 실제로 호출된 클래스입니다(늦은 정적 바인딩). Post::createSelf() 는 Model 을, Post::createStatic() 은 Post 를 돌려줬습니다.

파이프 연산자 |> 와 프로퍼티 훅은 지금 써도 되나요?

프로퍼티 훅과 비대칭 가시성은 PHP 8.4, 파이프 연산자는 8.5부터입니다. 모두 8.5.6에서 동작했습니다. |> 오른쪽의 화살표 함수는 괄호로 감싸야 하며, 감싸지 않으면 컴파일 단계에서 Fatal error 가 납니다.

  • #php
  • #중급
  • #객체지향
  • #enum
  • #readonly
  • #trait
  • #php85

t.laboratory

직접 만들고 운영하는 앱·서버·서비스에서 겪은 문제와, 그 해결에 쓴 코드를 기록하는 기술 블로그입니다.

운영자 소개 · 앱

↑