1. Property Hooks
예전에는 $user->displayName처럼 읽고 싶어도 결국 getDisplayName() 메서드를 만들었다. PHP 8.4부터는 프로퍼티에 get, set 훅을 직접 붙일 수 있다. 겉으로는 필드처럼 보이지만, 읽거나 쓸 때 정해둔 로직이 돌아간다.
<?php
 
final class UserProfile
{
    public function __construct(
        private string $firstName,
        private string $lastName,
    ) {}
 
    public string $displayName {
        get => "{$this->firstName} {$this->lastName}";
        set (string $value) {
            $parts = preg_split('/\s+/', trim($value), 2);
            if ($parts === false || count($parts) !== 2) {
                throw new InvalidArgumentException('성과 이름을 공백으로 구분해야 합니다.');
            }
            [$this->firstName, $this->lastName] = $parts;
        }
    }
}
 
$profile = new UserProfile('Kim', 'Zero');
echo $profile->displayName;
 
$profile->displayName = 'Park Dev';
echo $profile->displayName;
 
이 문법이 좋은 지점은 “계산된 값”과 “검증이 필요한 값”이다. 그냥 저장만 하는 필드에 전부 hook을 박으면 어디서 값이 바뀌는지 알기 힘들다.
 
2. Asymmetric Visibility
PHP 8.4의 asymmetric visibility는 프로퍼티의 읽기 권한과 쓰기 권한을 따로 나눈다. 외부에서는 값을 읽을 수 있지만, 바꾸는 건 클래스 내부에서만 허용하는 식이다. 이거 하나로 의미 없는 getter가 많이 사라진다.
<?php
 
final class Invoice
{
    public private(set) int $total = 0;
    public protected(set) string $status = 'draft';
 
    public function __construct(
        public readonly string $id,
        int $total,
    ) {
        if ($total < 0) {
            throw new InvalidArgumentException('금액은 음수가 될 수 없습니다.');
        }
        $this->total = $total;
    }
 
    public function pay(): void
    {
        $this->status = 'paid';
    }
}
 
$invoice = new Invoice('INV-1', 49000);
echo $invoice->total;
 
// $invoice->total = 0;
// 클래스 밖에서는 private(set) 프로퍼티를 수정할 수 없다.
 
이 문법은 DTO, 응답 모델, 도메인 객체에서 특히 좋다. 밖에서는 읽기 편하게 열어두고, 값이 망가질 수 있는 쓰기 경로는 클래스 안으로 가둔다. “캡슐화 때문에 무조건 getter 써야 함” 같은 낡은 습관을 꽤나 덜어준다.
 
3. Pipe Operator
PHP 8.5의 |>는 앞의 값을 뒤의 callable로 넘긴다. strtolower(str_replace(... trim($x)))처럼 안쪽부터 읽어야 하던 코드를 위에서 아래로 읽게 만든다.
<?php
 
$title = ' PHP 8.5 Released ';
 
$slug = $title
    |> trim(...)
    |> strtolower(...)
    |> (fn (string $value) => str_replace(' ', '-', $value))
    |> (fn (string $value) => str_replace('.', '', $value));
 
echo $slug;
 
중요한 건 이게 컬렉션 메서드 체이닝이 아니라는 점이다. 앞 단계의 결과값 하나가 다음 callable로 들어간다. 함수의 첫 번째 인자로 바로 넣기 애매하면 fn으로 감싸면 된다.
 
4. Clone With와 NoDiscard
readonly 객체는 안전한 대신에 일부 값만 바꾼 새 객체를 만들 때 코드가 지저분해졌다. PHP 8.5의 clone($object, [...])은 그것을 해결해준다. 여기에 #[\NoDiscard]를 붙이면 “새 객체를 반환했는데 결과를 버리는 실수”까지 잡는다.
<?php
 
readonly class Money
{
    public function __construct(
        public int $amount,
        public string $currency,
    ) {}
 
    #[\NoDiscard]
    public function withCurrency(string $currency): self
    {
        return clone($this, ['currency' => $currency]);
    }
}
 
$price = new Money(12000, 'KRW');
$price->withCurrency('USD');
 
$usdPrice = $price->withCurrency('USD');
 
여기서 첫 번째 호출은 반환값을 버린다. Money는 바뀌지 않는다. 새 객체가 만들어졌는데 그걸 안 받았으니 실수다. #[\NoDiscard]는 이런 종류의 버그를 “코드 리뷰에서 누가 봐주겠지”에 의존하지 않게 한다.
 
작지만 체감 큰 잔기술
PHP 8.4부터는 (new Foo())->bar()처럼 괄호로 감싸던 걸 new Foo()->bar()로 쓸 수 있다. 별거 아닌데 팩토리, 테스트 더블, 짧은 체이닝 코드에서 눈이 편해진다.
PHP 8.5에는 array_first(), array_last()도 들어왔다. 문법은 아니지만 array_key_first()로 키 뽑고 다시 값 꺼내던 잔동작이 사라진다.
Created with dc-code-paste