리플렉션 없이 PHP에서 개인 속성에 액세스

13046 단어 webdevphpprogramming
이 게시물은 원래 Dan's Musings에 게시되었습니다.

나는 최근에 코드 리뷰 중에 이 기법에 대해 논의하고 있었는데, 이 멋진 기법이 잘 알려지지 않았을 수도 있다는 것을 깨달았습니다. 나는 몇 년 전에 우연히 그것을 발견했습니다.

엄격하게 테스트 목적으로 전용 또는 보호된 속성이나 메서드에 액세스해야 하는 경우가 있습니다. 우리의 일반적인 성향은 이를 위해 Reflection을 사용하는 것입니다. 리플렉션은 설정하기 위한 추가 상용구가 많기 때문에 약간 번거롭습니다.

그러나 클로저는 실제로 훨씬 더 쉽게 할 수 있는 멋진 방법을 제공합니다. JavaScript bind 메서드를 사용한 적이 있다면 PHP에서도 동일한 작업을 수행할 수 있습니다.

샘플 클래스부터 시작하겠습니다.

class Person
{
    public string $name = 'Dan';
    protected int $age  = 40;
    private bool $cool  = true;

    protected static string $species = 'homo sapien';

    protected static function evolve(): void
    {
        static::$species = 'homo superior';
    }

    protected function birthday(): void
    {
        $this->age++;
    }

    public function howOldAmI(): int
    {
        return $this->age;
    }
}

$me = new Person();


비정적 속성



우리는 읽고 바꿀 줄 안다$me->name . 퍼블릭이라 문제가 없습니다.

이제 테스트 어딘가에서 정확한 연령을 설정해야 한다면 어떻게 해야 할까요? 그것은 보호된 속성이므로 직접 변경할 수는 없지만 클로저를 사용하면 이를 우회할 수 있습니다.

(fn (int $newAge) => $this->age = $newAge)->call($me, 30); // Changes $me->age to 30


그래서 우리는 여기서 무엇을 했습니까? 조금 분해합시다.

$changeAge = (fn (int $newAge) => $this->age = $newAge);


먼저, 전달된 내용으로 변경되는$this->age 클로저를 생성합니다. $changeAge(30) 를 하려고 하면 클래스 외부에서 이것을 정의했기 때문에 오류가 발생합니다. $this는 실제로 여기에 존재하지 않거나 이 코드를 단위 테스트에 넣는 경우 $this는 단위 테스트 클래스 자체를 참조합니다.

그것이 call가 들어오는 곳입니다. Closure::call 은 클로저를 새 객체에 바인딩하고 전달한 인수로 호출합니다. 즉, call에 전달된 첫 번째 인수는 클로저 내에서 $this가 됩니다. 다른 모든 인수는 함수 자체에 전달됩니다.

$changeAge->call($me, 30);

$age$cool 속성을 변경하지 않고 간단히 액세스할 수도 있습니다. 따라서 다음과 같이 할 수 있습니다.

$amICool = (fn () => $this->cool)->call($me); // true
$myAge   = (fn () => $this->age)->call($me);


정적 속성



이는 비정적 속성 또는 메서드에 적합합니다. 정적 인 것은 어떻습니까?

$mySpecies = (fn () => static::$species)->bindTo(null, Person::class)(); // homo sapien
(fn () => static::evolve())->bindTo(null, Person::class)(); // Changes to homo superior
(fn (string $newSpecies) => static::$species = $newSpecies)->bindTo(null, Person::class)('homo erectus'); // devolve to homo erectus


그래서 이것은 조금 다릅니다. 먼저 지정된 개체 또는 클래스에 다시 바인딩된 새 클로저를 반환하는 Closure::bindTo 을 사용하고 있습니다. bindTo 를 사용하여 객체(예: call )를 전달하거나 해당 null을 그대로 두고 정적 바인딩을 변경하는 클래스를 대신 전달할 수 있습니다. 그것이 우리가 여기서 한 일입니다. 그리고 그것은 새로운 클로저를 반환하기 때문에 당신은 그것을 호출해야 합니다. 이것이 우리가 뒤에 추가()가 있는 이유입니다.

따라서 이것도 단계별로 분해해 보겠습니다.

$getSpecies = (fn () => static::$species);


현재 범위의 $species를 반환하는 클로저.

$boundGetSpecies = $getSpecies->bindTo(null, Person::class);


범위를 Person 로 변경하는 새 클로저는 static 에 대한 모든 참조가 Person 를 참조함을 의미합니다.

$mySpecies = $boundGetSpecies();


또는 세 번째 항목에서와 같이 종을 임의의 값으로 변경하려는 경우:

// Create species changing closure, initially bound to the current scope
$changeSpecies = (fn (string $newSpecies) => static::$species = $newSpecies);
// Create a new Closure, bound to the Person scope
$changePersonSpecies = $changeSpecies->bindTo(null, Person::class);
// Change it to whatever
$changePersonSpecies('homo erectus');


객체에 bindTo 사용


bindTo 와 유사한 방식으로 call 를 사용할 수 있습니다.

(fn () => $this->birthday())->bindTo($me)();


그것을 분해합시다.

$haveABirthday = (fn () => $this->birthday());


현재 범위 내의 개체에서 birthday()를 호출하는 클로저입니다.

$haveMyBirthday = $haveABirthday->bindTo($me);


범위로 $me를 사용하여 새 클로저를 만듭니다. 이렇게 하면 $this에 대한 모든 참조는 $me를 참조합니다.

$haveMyBirthday();

$haveMyBirthday는 클로저이므로 실제로 호출해야 합니다.

결론



물론 ReflectionClass ReflectionObject 을 사용하여 이 모든 작업을 수행할 수 있지만 이 기술은 단일 개인 메서드를 호출하는 것이 한 줄의 코드이기 때문에 작업을 크게 단순화합니다.

나는 이것을 훨씬 더 쉽게 만드는 package I recently wrote에서 실제로 이것을 사용합니다.

다시 한 번 말하지만 프로덕션 코드에서는 이 작업을 수행하지 않는 것이 좋습니다. 가시성이 존재하는 이유가 있습니다. 서버의 코드에서 이와 같이 우회해서는 안 됩니다. 그러나 테스트를 위해 몇 가지 개체를 만지작거려야 하는 경우 이 기술을 사용하면 이를 단순화할 수 있습니다.

좋은 웹페이지 즐겨찾기