paint-brush
Laravel의 세부 사항 - Facade란 무엇입니까?~에 의해@oussamamater
새로운 역사

Laravel의 세부 사항 - Facade란 무엇입니까?

~에 의해 Oussama Mater14m2024/06/29
Read on Terminal Reader

너무 오래; 읽다

Laravel에는 우리가 자주 사용하는 많은 Facade가 포함되어 있습니다. 우리는 그것이 무엇인지, 어떻게 우리 자신의 Facade를 생성할 수 있는지에 대해 논의하고 실시간 Facade에 대해서도 배울 것입니다.
featured image - Laravel의 세부 사항 - Facade란 무엇입니까?
Oussama Mater HackerNoon profile picture

방금 새로운 Laravel 애플리케이션을 설치하고 부팅했으며 환영 페이지가 표시되었습니다. 다른 모든 사람들과 마찬가지로 여러분도 그것이 어떻게 렌더링되는지 보려고 하다가 web.php 파일에 접속하면 다음 코드를 만나게 됩니다:

 <?php use Illuminate\Support\Facades\Route; Route::get('/', function () { return view('welcome'); });

환영 보기를 어떻게 얻었는지는 분명하지만 Laravel의 라우터가 어떻게 작동하는지 궁금하므로 코드를 살펴보기로 결정했습니다. 초기 가정은 다음과 같습니다. 정적 메서드 get() 을 호출하는 Route 클래스가 있습니다. 그러나 클릭하면 get() 메서드가 없습니다. 그렇다면 어떤 종류의 흑마법이 일어나고 있는 걸까요? 이것을 이해해보자!

일반 외관

단순화를 위해 대부분의 PHPDoc을 제거하고 유형을 인라인 처리했습니다. "..."는 더 많은 코드를 나타냅니다.

혼란을 피하기 위해 IDE를 열고 코드를 따라가는 것이 좋습니다.


예제에 따라 Route 클래스를 살펴보겠습니다.

 <?php namespace Illuminate\Support\Facades; class Route extends Facade { // ... protected static function getFacadeAccessor(): string { return 'router'; } }


여기에는 별다른 내용이 없으며 단지 router 문자열을 반환하는 getFacadeAccessor() 메서드만 있습니다. 이를 염두에 두고 상위 클래스로 이동해 보겠습니다.

 <?php namespace Illuminate\Support\Facades; use RuntimeException; // ... abstract class Facade { // ... public static function __callStatic(string $method, array $args): mixed { $instance = static::getFacadeRoot(); if (! $instance) { throw new RuntimeException('A facade root has not been set.'); } return $instance->$method(...$args); } }

상위 클래스에는 많은 메소드가 있지만 get() 메소드는 없습니다. 하지만 흥미로운 것이 있는데, 바로 __callStatic() 메서드입니다. 이는 우리의 경우 get() 과 같은 정의되지 않은 정적 메서드가 호출될 때마다 호출되는 마법의 메서드입니다. 따라서 __callStatic('get', ['/', Closure()]) 호출은 Route::get() , 경로 / 및 환영 보기를 반환하는 Closure() 를 호출할 때 전달한 내용을 나타냅니다.


__callStatic() 트리거되면 먼저 getFacadeRoot() 호출하여 $instance 변수를 설정하려고 시도합니다. $instance 호출이 전달되어야 하는 실제 클래스를 보유합니다. 자세히 살펴보겠습니다. 조금 있으면 이해가 될 것입니다.

 // Facade.php public static function getFacadeRoot() { return static::resolveFacadeInstance(static::getFacadeAccessor()); }


보세요, 이것은 router 문자열을 반환한 Route 하위 클래스의 getFacadeAccessor() 입니다. 그런 다음 이 router 문자열은 "이 문자열이 나타내는 클래스는 무엇입니까?"라고 말하는 일종의 매핑인 클래스로 해결하려고 시도하는 resolveFacadeInstance() 로 전달됩니다. 어디 보자.

 // Facade.php protected static function resolveFacadeInstance($name) { if (isset(static::$resolvedInstance[$name])) { return static::$resolvedInstance[$name]; } if (static::$app) { if (static::$cached) { return static::$resolvedInstance[$name] = static::$app[$name]; } return static::$app[$name]; } }

먼저 정적 배열인 $resolvedInstance 에 지정된 $name (역시 router )으로 설정된 값이 있는지 확인합니다. 일치하는 항목을 찾으면 해당 값만 반환합니다. 성능을 조금 최적화하기 위한 Laravel 캐싱입니다. 이 캐싱은 단일 요청 내에서 발생합니다. 동일한 요청 내에서 동일한 인수를 사용하여 이 메서드를 여러 번 호출하면 캐시된 값을 사용합니다. 이것이 초기 호출이라고 가정하고 진행해 보겠습니다.


그런 다음 $app 설정되어 있고 $app 애플리케이션 컨테이너의 인스턴스인지 확인합니다.

 // Facade.php protected static \Illuminate\Contracts\Foundation\Application $app;

애플리케이션 컨테이너가 무엇인지 궁금하다면 이를 클래스가 저장되는 상자라고 생각하세요. 해당 수업이 필요할 때 해당 상자에 손을 넣으면 됩니다. 때때로 이 컨테이너는 약간의 마법을 수행합니다. 상자가 비어 있고 수업을 받으려고 손을 뻗는 경우에도 수업을 받을 수 있습니다. 그것은 다른 기사의 주제입니다.


이제 " $app 언제 설정되나요?"라고 궁금해하실 수도 있습니다. 그렇지 않으면 $instance 없어야 하기 때문입니다. 이 애플리케이션 컨테이너는 애플리케이션의 부트스트랩 프로세스 중에 설정됩니다. \Illuminate\Foundation\Http\Kernel 클래스를 간단히 살펴보겠습니다.

 <?php namespace Illuminate\Foundation\Http; use Illuminate\Http\Request; use Illuminate\Http\Response; use Illuminate\Support\Facades\Facade; use Illuminate\Contracts\Http\Kernel as KernelContract; // ... class Kernel implements KernelContract { // ... protected $app; protected $bootstrappers = [ \Illuminate\Foundation\Bootstrap\LoadEnvironmentVariables::class, \Illuminate\Foundation\Bootstrap\LoadConfiguration::class, \Illuminate\Foundation\Bootstrap\HandleExceptions::class, \Illuminate\Foundation\Bootstrap\RegisterFacades::class, // <- this guy \Illuminate\Foundation\Bootstrap\RegisterProviders::class, \Illuminate\Foundation\Bootstrap\BootProviders::class, ]; public function bootstrap(): void { if (! $this->app->hasBeenBootstrapped()) { $this->app->bootstrapWith($this->bootstrappers()); } } }

요청이 들어오면 라우터로 전송됩니다. 그 직전에 bootstrappers 배열을 사용하여 애플리케이션을 준비하는 bootstrap() 메서드가 호출됩니다. \Illuminate\Foundation\Application 클래스에서 bootstrapWith() 메서드를 탐색하면 이러한 부트스트래퍼를 반복하여 bootstrap() 메서드를 호출합니다.


단순화를 위해 bootstrapWith() 에서 호출될 bootstrap() 메서드가 포함되어 있는 \Illuminate\Foundation\Bootstrap\RegisterFacades 에 집중하겠습니다.

 <?php namespace Illuminate\Foundation\Bootstrap; use Illuminate\Contracts\Foundation\Application; use Illuminate\Foundation\AliasLoader; use Illuminate\Foundation\PackageManifest; use Illuminate\Support\Facades\Facade; class RegisterFacades { // ... public function bootstrap(Application $app): void { Facade::clearResolvedInstances(); Facade::setFacadeApplication($app); // Interested here AliasLoader::getInstance(array_merge( $app->make('config')->get('app.aliases', []), $app->make(PackageManifest::class)->aliases() ))->register(); } }


그리고 정적 메소드 setFacadeApplication(). 사용하여 Facade 클래스에 애플리케이션 컨테이너를 설정하고 있습니다.

 // RegisterFacades.php public static function setFacadeApplication($app) { static::$app = $app; }


우리가 resolveFacadeInstance() 내에서 테스트 중인 $app 속성을 할당합니다. 이것은 질문에 대한 답변입니다. 계속하자.

 // Facade.php protected static function resolveFacadeInstance($name) { if (isset(static::$resolvedInstance[$name])) { return static::$resolvedInstance[$name]; } if (static::$app) { if (static::$cached) { return static::$resolvedInstance[$name] = static::$app[$name]; } return static::$app[$name]; } }

애플리케이션 부트스트래핑 중에 $app 설정되었음을 확인했습니다. 다음 단계는 $cached 확인하여 확인된 인스턴스를 캐시해야 하는지 확인하는 것입니다. 기본값은 true입니다. 마지막으로 애플리케이션 컨테이너에서 인스턴스를 검색합니다. 우리의 경우 이는 static::$app['router'] 문자열에 바인딩된 클래스를 제공하도록 요청하는 것과 같습니다 router .


이제 애플리케이션 컨테이너의 인스턴스 임에도 불구하고 배열처럼 $app 액세스하는 이유가 궁금할 것입니다. 글쎄, 당신 말이 맞아요! 그러나 애플리케이션 컨테이너는 ArrayAccess 라는 PHP 인터페이스를 구현하여 배열과 유사한 액세스를 허용합니다. 이 사실을 확인하기 위해 이를 살펴볼 수 있습니다.

 <?php namespace Illuminate\Container; use ArrayAccess; // <- this guy use Illuminate\Contracts\Container\Container as ContainerContract; class Container implements ArrayAccess, ContainerContract { // ... }


따라서, resolveFacadeInstance() 실제로 router 문자열, 특히 \Illuminate\Routing\Router 에 바인딩된 인스턴스를 반환합니다. 내가 어떻게 알았지? Route 파사드를 살펴보세요. 종종 이 Facade가 무엇을 숨기고 있는지, 더 정확하게는 우리 메소드 호출이 어떤 클래스로 프록시될 것인지를 암시하는 PHPDoc @see 발견하게 될 것입니다.


이제 __callStatic 메소드로 돌아갑니다.

 <?php namespace Illuminate\Support\Facades; use RuntimeException; // ... abstract class Facade { // ... public static function __callStatic(string $method, array $args): mixed { $instance = static::getFacadeRoot(); if (! $instance) { throw new RuntimeException('A facade root has not been set.'); } return $instance->$method(...$args); } }


\Illuminate\Routing\Router 클래스의 객체인 $instance 가 있습니다. 우리는 그것이 설정되었는지(우리의 경우 확인됨) 테스트하고 이에 대한 메서드를 직접 호출합니다. 그래서 우리는 끝납니다.

 // Facade.php return $instance->get('/', Closure());


이제 \Illuminate\Routing\Router 클래스 내에 get() 존재하는지 확인할 수 있습니다.

 <?php namespace Illuminate\Routing; use Illuminate\Routing\Route; use Illuminate\Contracts\Routing\BindingRegistrar; use Illuminate\Contracts\Routing\Registrar as RegistrarContract; // ... class Router implements BindingRegistrar, RegistrarContract { // ... public function get(string $uri, array|string|callable|null $action = null): Route { return $this->addRoute(['GET', 'HEAD'], $uri, $action); } }

그게 끝이에요! 결국은 힘들지 않았나요? 요약하자면, Facade는 컨테이너에 바인딩된 문자열을 반환합니다. 예를 들어 hello-world HelloWorld 클래스에 바인딩될 수 있습니다. Facade에서 정의되지 않은 메서드를 정적으로 호출하면 HelloWorldFacade , 예를 들어 __callStatic() 개입합니다.


getFacadeAccessor() 메소드에 등록된 문자열을 컨테이너 내에 바인딩된 항목으로 확인하고 검색된 인스턴스에 대한 호출을 프록시합니다. 따라서 우리는 (new HelloWorld())->method() 로 끝납니다. 그것이 바로 그 본질입니다! 아직도 클릭하지 않으셨나요? 그럼 우리의 외관을 만들어 봅시다!

우리의 외관을 만들자

다음과 같은 수업이 있다고 가정해 보겠습니다.

 <?php namespace App\Http\Controllers; class HelloWorld { public function greet(): string { return "Hello, World!"; } }


목표는 HelloWorld::greet() 호출하는 것입니다. 이를 위해 클래스를 애플리케이션 컨테이너에 바인딩합니다. 먼저 AppServiceProvider 로 이동합니다.

 <?php namespace App\Providers; use App\Http\Controllers; use Illuminate\Support\ServiceProvider; class AppServiceProvider extends ServiceProvider { public function register(): void { $this->app->bind('hello-world', function ($app) { return new HelloWorld; }); } // ... }


이제 애플리케이션 컨테이너(또는 앞서 언급한 상자)에서 hello-world 요청할 때마다 HelloWorld 인스턴스가 반환됩니다. 무엇이 남았나요? hello-world 문자열을 반환하는 Facade를 생성하기만 하면 됩니다.

 <?php namespace App\Http\Facades; use Illuminate\Support\Facades\Facade; class HelloWorldFacade extends Facade { protected static function getFacadeAccessor() { return 'hello-world'; } }


이제 이를 사용할 준비가 되었습니다. web.php.

 <?php use App\Http\Facades; use Illuminate\Support\Facades\Route; Route::get('/', function () { return HelloWorldFacade::greet(); // Hello, World! });


우리는 HelloWorldFacade 파사드에 greet() 존재하지 않는다는 것을 알고 있으며, __callStatic() 트리거됩니다. 애플리케이션 컨테이너에서 문자열(우리의 경우 hello-world )로 표시되는 클래스를 가져옵니다. 그리고 우리는 이미 AppServiceProvider 에서 이 바인딩을 만들었습니다. 우리는 누군가 hello-world 요청할 때마다 HelloWorld 인스턴스를 제공하도록 지시했습니다. 결과적으로 greet() 과 같은 모든 호출은 검색된 HelloWorld 인스턴스에서 작동합니다. 그리고 그게 다야.


축하해요! 당신은 당신만의 외관을 만들었습니다!

Laravel 실시간 파사드

이제 파사드에 대해 잘 이해했으므로 공개해야 할 마술이 하나 더 있습니다. 실시간 Facade를 사용하여 Facade를 생성하지 않고 HelloWorld::greet() 호출할 수 있다고 상상해 보세요.


살펴보자:

 <?php use Facades\App\Http\Controllers; // Notice the prefix use Illuminate\Support\Facades\Route; Route::get('/', function () { return HelloWorld::greet(); // Hello, World! });


컨트롤러의 네임스페이스 앞에 Facades 추가하면 이전과 동일한 결과를 얻을 수 있습니다. 그러나 HelloWorld 컨트롤러에는 greet() 이라는 정적 메서드가 없다는 것은 확실합니다! 그리고 Facades\App\Http\Controllers\HelloWorld 어디에서 왔습니까? 나는 이것이 마법처럼 보일 수도 있다는 것을 이해하지만 일단 이해하면 매우 간단합니다.


앞서 확인한 $app: 설정을 담당하는 클래스 \Illuminate\Foundation\Bootstrap\RegisterFacades 를 자세히 살펴보겠습니다.

 <?php namespace Illuminate\Foundation\Bootstrap; use Illuminate\Contracts\Foundation\Application; use Illuminate\Foundation\AliasLoader; use Illuminate\Foundation\PackageManifest; use Illuminate\Support\Facades\Facade; class RegisterFacades { public function bootstrap(Application $app): void { Facade::clearResolvedInstances(); Facade::setFacadeApplication($app); AliasLoader::getInstance(array_merge( $app->make('config')->get('app.aliases', []), $app->make(PackageManifest::class)->aliases() ))->register(); // Interested here } }


맨 마지막에 register() 메서드가 호출되는 것을 볼 수 있습니다. 내부를 살펴보겠습니다.

 <?php namespace Illuminate\Foundation; class AliasLoader { // ... protected $registered = false; public function register(): void { if (! $this->registered) { $this->prependToLoaderStack(); $this->registered = true; } } }


$registered 변수는 처음에 false 로 설정됩니다. 따라서 if 문을 입력하고 prependToLoaderStack() 메서드를 호출합니다. 이제 구현을 살펴보겠습니다.

 // AliasLoader.php protected function prependToLoaderStack(): void { spl_autoload_register([$this, 'load'], true, true); }


이곳이 바로 마법이 일어나는 곳입니다! Laravel은 정의되지 않은 클래스에 액세스하려고 할 때 트리거되는 내장 PHP 함수 spl_autoload_register() 함수를 호출하고 있습니다. 이러한 상황에서 실행할 논리를 정의합니다. 이 경우 Laravel은 정의되지 않은 호출이 발생할 때 load() 메서드를 호출하도록 선택합니다.


또한 spl_autoload_register() 호출하는 메서드나 함수에 정의되지 않은 클래스의 이름을 자동으로 전달합니다.


load() 메서드를 살펴보겠습니다. 그것은 열쇠임에 틀림없다.

 // AliasLoader.php public function load($alias) { if (static::$facadeNamespace && str_starts_with($alias, static::$facadeNamespace)) { $this->loadFacade($alias); return true; } if (isset($this->aliases[$alias])) { return class_alias($this->aliases[$alias], $alias); } }

$facadeNamespace 설정되어 있는지 확인하고 어떤 클래스가 전달되었는지 확인합니다. 우리의 경우 Facades\App\Http\Controllers\HelloWorld $facadeNamespace 에 설정된 내용으로 시작합니다.


로직은 $facadeNamespace 가 설정되었는지 확인하고 전달된 클래스(이 경우 Facades\App\Http\Controllers\HelloWorld (정의되지 않음))가 $facadeNamespace.

 // AliasLoader.php protected static $facadeNamespace = 'Facades\\';


컨트롤러의 네임스페이스 앞에 Facades 붙여서 조건을 만족시켰으므로 loadFacade() 를 진행합니다.

 // AliasLoader.php protected function loadFacade($alias) { require $this->ensureFacadeExists($alias); }


여기서 메소드에는 ensureFacadeExists() 에서 반환되는 모든 경로가 필요합니다. 따라서 다음 단계는 구현을 탐구하는 것입니다.

 // AliasLoader.php protected function ensureFacadeExists($alias) { if (is_file($path = storage_path('framework/cache/facade-'.sha1($alias).'.php'))) { return $path; } file_put_contents($path, $this->formatFacadeStub( $alias, file_get_contents(__DIR__.'/stubs/facade.stub') )); return $path; }


먼저 framework/cache/facade-'.sha1($alias).'.php' 라는 파일이 존재하는지 확인합니다. 우리의 경우 이 파일이 존재하지 않아 다음 단계인 file_put_contents() 가 시작됩니다. 이 함수는 파일을 생성하고 이를 지정된 $path 에 저장합니다. 파일의 내용은 이름으로 판단하면 스텁에서 외관을 생성하는 formatFacadeStub() 에 의해 생성됩니다. facade.stub 검사하면 다음을 찾을 수 있습니다.

 <?php namespace DummyNamespace; use Illuminate\Support\Facades\Facade; /** * @see \DummyTarget */ class DummyClass extends Facade { /** * Get the registered name of the component. */ protected static function getFacadeAccessor(): string { return 'DummyTarget'; } }


친숙해 보이죠? 이것이 본질적으로 우리가 수동으로 수행한 작업입니다. 이제 formatFacadeStub() Facades\\ 접두사를 제거한 후 더미 콘텐츠를 정의되지 않은 클래스로 대체합니다. 그러면 업데이트된 파사드가 저장됩니다. 결과적으로, loadFacade() 에 파일이 필요할 때 올바르게 수행되고 결국 다음 파일이 필요하게 됩니다.

 <?php namespace Facades\App\Http\Controllers; use Illuminate\Support\Facades\Facade; /** * @see \App\Http\Controllers\HelloWorld */ class HelloWorld extends Facade { /** * Get the registered name of the component. */ protected static function getFacadeAccessor(): string { return 'App\Http\Controllers\HelloWorld'; } }

이제 일반적인 흐름에서는 App\Http\Controllers\HelloWorld 문자열에 바인딩된 모든 인스턴스를 반환하도록 애플리케이션 컨테이너에 요청합니다. 우리는 이 문자열을 아무 것에도 바인딩하지 않았으며 AppServiceProvider 건드리지도 않았습니다. 하지만 맨 처음에 애플리케이션 컨테이너에 대해 언급한 내용을 기억하시나요?


상자가 비어 있어도 인스턴스를 반환 하지만 한 가지 조건으로 클래스에는 생성자가 없어야 합니다. 그렇지 않으면 사용자를 위해 빌드하는 방법을 알 수 없습니다. 우리의 경우 HelloWorld 클래스에는 구성하는 데 인수가 필요하지 않습니다. 따라서 컨테이너는 이를 해결하고 반환하며 모든 호출이 프록시됩니다.


실시간 Facades 요약: 클래스 앞에 Facades 붙였습니다. 애플리케이션 부트스트래핑 중에 Laravel은 정의되지 않은 클래스를 호출할 때 트리거되는 spl_autoload_register() 등록합니다. 결국 load() 메서드로 연결됩니다. load() 내부에서 현재 정의되지 않은 클래스에 Facades 접두사가 붙어 있는지 확인합니다. 일치하므로 Laravel은 이를 로드하려고 시도합니다.


Facade가 존재하지 않기 때문에 스텁에서 생성한 다음 파일이 필요합니다. 그리고 짜잔! 일반적인 외관이 있지만 이것은 즉석에서 만들어졌습니다. 정말 멋지죠?

결론

여기까지 오신 것을 축하드립니다! 조금 부담스러울 수 있다는 점은 이해합니다. 마음에 들지 않았던 섹션을 다시 읽어보세요. IDE에 대한 후속 조치도 도움이 될 수 있습니다. 하지만 더 이상 흑마법이 없어 기분이 좋을 것 같아요. 적어도 처음에는 그렇게 느꼈어요!


그리고 다음에 정적으로 메서드를 호출하면 그렇지 않을 수도 있다는 점을 기억하세요 🪄