[Laravel] 사용자 ServiceProvider 만들기

[Laravel] 사용자 ServiceProvider 만들기 updated_at: 2024-09-13 11:06

사용자 ServiceProvider 만들기

ServiceProvider

기본적인 형태

use Illuminate\Support\ServiceProvider;
..........
class BbsServiceProvider extends ServiceProvider {
  public function register()
  {}

  public function boot()
  {}
}

register vs boot

실행순서는 register()메소드 가 먼저 실행되고 boot()메소드가 실행됩니다.
register 메서드는 컨테이너에 서비스를 등록하는 데만 사용해야 합니다.
boot 메서드 내에서는 이벤트 리스너 등록, 경로 파일 포함, 필터 등록 또는 다른 모든 작업이 가능합니다.

상세설명

use Illuminate\Support\ServiceProvider;

class BbsServiceProvider extends ServiceProvider {
  public function register()
  {
    $this->app->bind('bbs', function($app) {
      return new Bbs;
    });
  }

  public function boot()
  {
    $this->loadRoutesFrom(__DIR__.'/../routes/web.php');

    $this->loadMigrationsFrom(__DIR__.'/migrations/'); // migrations 디렉토리에 있는 모든 파일들을 마이그래이션 한다.
    
    // set assets
    $this->publishes([
      __DIR__.'/Https/public/assets/' => public_path('assets/pondol/bbs'),
    ], 'public');
    ..........
  }
}

상세한 내용은 package development 를 참조 바랍니다.

publishes

현재 package내의 파일들을 각각 지정된 폴더로 이동시킨다. 이 부분은 정의만 되어있고 실제 작동을 하려면

php artisan vendor:publish --provider=MyPackage\MyServiceProvider --force

--provider 대신에 group tag를 사용하여 처리하여도 된다.

--tag

아래처럼 public 이라는 group tag 를 설정하였습니다. 일반적으로는 public, 이나 config를 많이 사용하고 있고 용도에 따라 본인의 group tag를 만들어 사용하여도 된다.

$this->publishes([__DIR__.'/Https/public/assets/' => public_path('assets')], 'public');

group tag를 이용하여 publishing

php artisan vendor:publish --tag=public --force
--force

vendor:publish 명령어를 사용하면 package 내의 모든 assets들이 특정 위치에 카피될 것이다. --force를 사용하면 패키지가 업데이트 될때마다 강제적으로 overwrite될 것이다.

config

아래는 config 파일을 활용하는 두가지 예 (publishes, mergeConfigFrom) 이다.
publishes 는 현재 배포하는 패키지의 config 파일을 laravel application에서 사용하는 config path에다가 copy 하는 것이다.
이렇게 함으로서 배포된 패키지를 사용하는 사용자들은 쉽게 config를 변경할 수 있다.

public function boot(): void
{
  $this->publishes([
    __DIR__.'/../config/courier.php' => config_path('courier.php'),
  ]);
   $this->mergeConfigFrom(
    __DIR__.'/../config/courier.php', 'courier'
  );
}

그러면 mergeConfigFrom 은 왜 사용하는 것일까?
버전업등이나 사용자의 실수(?) 등으로 특정 변수등이 필요한 경우 현재 배포중인 패키지의 config에서 값을 가져오게 하는 것이다.
즉, 기존 config에 있는 변수에 없는 값을 추가로 넣어 안정적인 서비스를 운영할때 필요한 명령이다.

routes

패키지에 경로가 포함된 경우 loadRoutesFrom 메서드를 사용하여 경로를 로드할 수 있습니다. 이 메소드는 애플리케이션의 경로가 캐시되었는지 자동으로 확인하고 경로가 이미 캐시된 경우 경로 파일을 로드하지 않습니다.

public function boot(): void
{
  $this->loadRoutesFrom(__DIR__.'/../routes/web.php');
}

예전에는 아래와 같은 방법으로도 사용했지만 현재는 위의 방법을 추천드립니다.

public function boot()
{
  if (!$this->app->routesAreCached()) {
    require_once __DIR__ . '/Https/routes/web.php';
    require_once __DIR__ . '/Https/routes/api.php';
  }
}

package 내부의 라우트를 사용하는 방법

Route::prefix('what you want prefix')
    ->middleware('what you want middleware')
    ->namespace('what you want namespace')
    ->group(__DIR__ . '/routes/web.php');

MiddleWare

middlewareGroup에 삽입하기

Kernel.php의 $middlewareGroups 에 삽입

app('router')->pushMiddlewareToGroup('web', WhatYourClass::class);
app('router')->pushMiddlewareToGroup('admin', 'role:administrator');

수동으로 처리할 경우 아래와 같다

protected $middlewareGroups = [
..........
  'web' => [
  ..........
    WhatYourClass::class,
  ],
  'admin' => [
    'role:administrator',
  ],
];
routeMiddleware에 삽입하기

Kernel.php의 $middleware 에 삽입

$router->aliasMiddleware('role', \App\Http\Middleware\CheckRole::class);

수동으로 처리할 경우 아래와 같다

protected $routeMiddleware = [
  ..........
  'role' => \App\Http\Middleware\CheckRole::class,
];
middlewae에 삽입하기
$kernel = app(\Illuminate\Contracts\Http\Kernel::class);
$kernel->pushMiddleware(VerifyEmail::class);

Migrations

데이타베이스 마이그레이션이 필요한 경우 사용합니다. 아래 예제중 한가지만 사용하시면 됩니다.

public function boot(): void
{

  // 1. 현재 마이그레이션 파일들을 database > migrations 폴더로 이동하는 경울 
  $this->publishesMigrations([
    __DIR__.'/../database/migrations' => database_path('migrations'),
  ]);
  // 2. 현재 정의된 폴더를 migrations 폴더로 정의하는 경우
  $this->loadMigrationsFrom(__DIR__.'/migrations/'); 
}

migration은 아래 명령을 실행할 때 실제로 db를 생성한다.

\Artisan::call('migrate'); // migration 파일들로 부터 db create or update

언어파일(Language Files)

public function boot(): void
{
  $this->loadTranslationsFrom(__DIR__.'/../lang', 'courier');
}

blade에서 호출시 아래와 같이 하시면 됩니다.

echo trans('courier::messages.welcome');

loadViewsFrom

publishe를 이용하여 resource 폴더에 넣는 방법도 있지만 아래와 같이 loadViewsFrom을 이용하여 package내의 폴더를 지정하는 경우도 있다.
이때는 패키지내임을 지정하고 패키지내임::viewer 파일 형식으로 사용하면 된다.

public function boot(): void
{
    $this->loadViewsFrom(__DIR__.'/../resources/views', 'courier');
}

컨트롤러에서 아래처럼 호출하면 된다.

Route::get('/dashboard', function () {
  return view('courier::dashboard');
});

View Components

use Illuminate\Support\Facades\Blade;
use VendorPackage\View\Components\AlertComponent;
 
/**
 * Bootstrap your package's services.
 */
public function boot(): void
{
  Blade::component('package-alert', AlertComponent::class);
}
  • blade에서는 아래와 같이 사용하시면 됩니다.
<x-package-alert/>

commands

특정 commands 를 artisan에 등록할 경우

$this->commands([InstallCommand::class]);

booted

부팅된 이후에 다양한 기능을 추가하는 경우

$this->app->booted(function () {
});

schedule 입력

$this->app->booted(function () {
  $schedule = app(Schedule::class);
  $schedule->command(UserCommand::class, ['scheduled' => true])->monthlyOn(12, '00:00');
});

Event

Event::subscribe(UserEventSubscriber::class);

EventServiceProvider에 protected $listen = [] 과 깉은 기능을 제공한다.
Multi listen을 원할경우 동일 클래스를 중복해서 사용해 주어야 한다.

Event::listen(
  SocialiteWasCalled::class,
  ['\SocialiteProviders\\Naver\\NaverExtendSocialite', 'handle'],
);

Event::listen(
  SocialiteWasCalled::class,
  ['\SocialiteProviders\\Kakao\\KakaorExtendSocialite', 'handle'],
);

bind

사용하는 곳에 따라 binding이란 의미가 다른데 영어적 해석은 합하여 묶는 것을 의미한다.
php에서의 binding 이라함은 특정 값(변수, 클래스..) 을 다른 특정값에 넣는 것을 의미한다.
라라벨의 바인딩도 비슷한 의미를 가지는데 좀더 세부적으로 설명 드리겠습니다.

단순 binding(Simple Bindings)

대부분의 binding은 service providers에서 등록됩니다. 따라서 service provider를 중심으로 설명드리겠습니다.
Service Provider 내에서는 $this->app(혹은 서비스 프로파이더 밖에서 사용할 경우 App::bind() 퍼사드를 사용하거나 app() helper를 사용하여 접근할 수 있다.) 속성을 활용하여 컨테이너에 접근가능합니다. 따라서 $this->app 으로 접근할때 bind 메소드를 활용하여 클래스나 인터페이스를 등록해 두면 이것을 clouser가 리턴해 줍니다.

  • service provider 내에서 binding
 
$this->app->bind(Transistor::class, function (Application $app) {
  return new Transistor($app->make(PodcastParser::class));
});
  • service provider 밖에서 binding
App::bind(Transistor::class, function (Application $app) {
  // ...
});

bindig 되지 않은 경우만 바인딩할 경우 bindIf 를 사용한다.

$this->app->bindIf(Transistor::class, function (Application $app) {
  return new Transistor($app->make(PodcastParser::class));
});
Binding A Singleton

singleton으로 바인딩을 할경우 같은 클래스나 인터페이스로 다른 곳에서 바인딩하더라도 마지막 바인딩 된 것 하나만 동작한다. 즉 binding을 통한 instance가 여러개 있다고 하더라도 마지막 binding 으로 overwrite 된다.

$this->app->singleton(Transistor::class, function (Application $app) {
  return new Transistor($app->make(PodcastParser::class));
});

여기서도 singletonIf 를 사용하여 singleton으로 정의되었는지 확인할 수 있다.

$this->app->singletonIf(Transistor::class, function (Application $app) {
  return new Transistor($app->make(PodcastParser::class));
});
Binding Interfaces to Implementations

아래코드는 EventPusher를 implement 할때 RedisEventPusher가 inject 된다.

use App\Contracts\EventPusher;
use App\Services\RedisEventPusher;
 
$this->app->bind(EventPusher::class, RedisEventPusher::class); // EventPusher 는  Interface 이고 RedisEventPusher는 일반적인 class 이다.

아래처럼 사용하면

use App\Contracts\EventPusher;
 
/**
 * Create a new class instance.
 */
public function __construct(
    protected EventPusher $pusher,
) {}

runningInConsole

콘솔(CLI) 인지를 확인하는 명령

if ($this->app->runningInConsole()) {
  \DB::connection()->enableQueryLog();
}
평점을 남겨주세요
평점 : 5.0
총 투표수 : 1

질문 및 답글