前言
Laravel 服務容器是管理類別依賴與執行依賴注入的工具。依賴注入指的是:類別的依賴透過建構子「注入」,或在某些情況下透過「setter」方法注入。
由於被注入的類別可以更容易地抽換成其他的實作,所以我們可以很容易地「mock」,或者建立一個假的類別實作來測試我們的應用程式。
簡易綁定
首先新增一個路由在 routes/web.php 檔。
1
   | Route::get('pay', 'OrderController@store');
  | 
 
新增一個 OrderController 控制器。
1
   | php artisan make:controller OrderController
   | 
 
新增一個 app/Billing/PaymentGateway.php 檔,做為一個付款的閘道器。
1 2 3 4 5 6 7 8 9 10 11 12 13 14
   | namespace App\Billing;
  use Illuminate\Support\Str;
  class PaymentGateway {     public function charge(int $amount)     {         return [             'amount' => $amount,             'confirmation_number' => Str::random(),         ];     } }
   | 
 
修改 OrderController 控制器:
1 2 3 4 5 6 7 8 9 10 11 12 13
   | namespace App\Http\Controllers;
  use App\Billing\PaymentGateway;
  class OrderController extends Controller {     public function store()     {         $paymentGateway = new PaymentGateway();
          dd($paymentGateway->charge(2500));     } }
   | 
 
結果:
1 2 3 4
   | array:2 [▼   "amount" => 2500   "confirmation_number" => "XvS8O4BNLrhz6gUK" ]
   | 
 
修改 OrderController 控制器,將 PaymentGateway 閘道器改為依賴注入的方式:
1 2 3 4 5 6 7 8 9 10 11
   | namespace App\Http\Controllers;
  use App\Billing\PaymentGateway;
  class OrderController extends Controller {     public function store(PaymentGateway $paymentGateway)     {         dd($paymentGateway->charge(2500));     } }
   | 
 
一樣可以運作:
1 2 3 4
   | array:2 [▼   "amount" => 2500   "confirmation_number" => "iB3HK755ZMqo7ob0" ]
   | 
 
但是如果有參數要放進 PaymentGateway 閘道器的建構子,像是:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22
   | namespace App\Billing;
  use Illuminate\Support\Str;
  class PaymentGateway {     private $currency;
      public function __construct(string $currency)     {         $this->currency = $currency;     }
      public function charge(int $amount)     {         return [             'amount' => $amount,             'confirmation_number' => Str::random(),             'currency' => $this->currency,         ];     } }
   | 
 
就會出錯,因為我們沒有提供參數給它:
1 2
   | Illuminate\Contracts\Container\BindingResolutionException Unresolvable dependency resolving [Parameter 
   | 
 
因此我們需要在 app/Providers/AppServiceProvider.php 服務提供者中註冊一個容器綁定。
首先透過 $this->app 物件屬性來取得整個應用程式容器,並使用 bind() 方法註冊一個綁定,傳遞一組希望綁定的類別或介面名稱作為第一個參數,接著第二個參數放入用來回傳類別實例的閉包。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29
   | namespace App\Providers;
  use App\Billing\PaymentGateway; use Illuminate\Support\ServiceProvider;
  class AppServiceProvider extends ServiceProvider {     
 
 
 
      public function register()     {         $this->app->bind(PaymentGateway::class, function ($app) {             return new PaymentGateway('NTD');         });     }
      
 
 
 
      public function boot()     {              } }
   | 
 
結果:
1 2 3 4 5
   | array:3 [▼   "amount" => 2500   "confirmation_number" => "hZL4jafa5sxY942S"   "currency" => "NTD" ]
   | 
 
如果有很多個控制器都需要使用這個閘道器,只需要從服務提供者修改參數即可,而不需要到每個控制器去修改。
單例綁定
接下來,在 PaymentGateway 閘道器建立一個 setDiscount() 方法,用來設置折扣金額:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30
   | namespace App\Billing;
  use Illuminate\Support\Str;
  class PaymentGateway {     private $currency;
      private $discount = 0;
      public function __construct(string $currency)     {         $this->currency = $currency;     }
      public function setDiscount(int $amount)     {         $this->discount = $amount;     }
      public function charge(int $amount)     {         return [             'amount' => $amount - $this->discount,             'confirmation_number' => Str::random(),             'currency' => $this->currency,             'discount' => $this->discount,         ];     } }
   | 
 
新增一個 app/Order/OrderDetails.php 檔,用來取得訂單資訊:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24
   |  namespace App\Order;
  use App\Billing\PaymentGateway;
  class OrderDetails {     private $paymentGateway;
      public function __construct(PaymentGateway $paymentGateway)     {         $this->paymentGateway = $paymentGateway;     }
      public function all()     {         $this->paymentGateway->setDiscount(500);
          return [             'name' => 'Memo Chou',             'Address' => 'Taiwan',         ];     } }
 
  | 
 
修改 OrderController 控制器,使用 all() 方法取得訂單:
1 2 3 4 5 6 7 8 9 10 11 12 13 14
   | namespace App\Http\Controllers;
  use App\Billing\PaymentGateway; use App\Order\OrderDetails;
  class OrderController extends Controller {     public function store(OrderDetails $orderDetails, PaymentGateway $paymentGateway)     {         $order = $orderDetails->all();
          dd($paymentGateway->charge(2500));     } }
   | 
 
結果:
1 2 3 4 5 6
   | array:4 [▼   "amount" => 2500   "confirmation_number" => "YuVBGKPIqJMrjIij"   "currency" => "NTD"   "discount" => 0 ]
   | 
 
由於被注入到 OrderDetails 類別和 OrderController 類別的 PaymentGateway 閘道器是兩個不同的實例,所以 discount 會是 0。
我們需要使用 singleton() 方法,將服務提供者所註冊的容器綁定改為單例。被綁定至容器中的類別或介面只會被解析一次,之後的呼叫都會從容器中回傳相同的實例。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29
   | namespace App\Providers;
  use App\Billing\PaymentGateway; use Illuminate\Support\ServiceProvider;
  class AppServiceProvider extends ServiceProvider {     
 
 
 
      public function register()     {         $this->app->singleton(PaymentGateway::class, function ($app) {             return new PaymentGateway('NTD');         });     }
      
 
 
 
      public function boot()     {              } }
   | 
 
結果:
1 2 3 4 5 6
   | array:4 [▼   "amount" => 2000   "confirmation_number" => "F2MFaYLIKFfKl7sD"   "currency" => "NTD"   "discount" => 500 ]
   | 
 
綁定介面至實作
由於付款方式可能不只一種,因此新增一個 app/Billing/PaymentGatewayContract.php 介面,讓所有的付款方式都去實作:
1 2 3 4 5 6 7 8
   | namespace App\Billing;
  interface PaymentGatewayContract {     public function setDiscount(int $amount);
      public function charge(int $amount); }
   | 
 
將 PaymentGateway 閘道器重新命名為 BankPaymentGateway ,並實作 PaymentGatewayContract 介面。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30
   | namespace App\Billing;
  use Illuminate\Support\Str;
  class BankPaymentGateway implements PaymentGatewayContract {     private $currency;
      private $discount = 0;
      public function __construct(string $currency)     {         $this->currency = $currency;     }
      public function setDiscount(int $amount)     {         $this->discount = $amount;     }
      public function charge(int $amount)     {         return [             'amount' => $amount - $this->discount,             'confirmation_number' => Str::random(),             'currency' => $this->currency,             'discount' => $this->discount,         ];     } }
   | 
 
將 OrderController 控制器中注入的 PaymentGateway 閘道器改為 PaymentGatewayContract 介面:
1 2 3 4 5 6 7 8 9 10 11 12 13 14
   | namespace App\Http\Controllers;
  use App\Billing\PaymentGatewayContract; use App\Order\OrderDetails;
  class OrderController extends Controller {     public function store(OrderDetails $orderDetails, PaymentGatewayContract $paymentGateway)     {         $order = $orderDetails->all();
          dd($paymentGateway->charge(2500));     } }
   | 
 
將 OrderDetails 類別中注入的 PaymentGateway 閘道器也改為 PaymentGatewayContract 介面:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23
   | namespace App\Order;
  use App\Billing\PaymentGatewayContract;
  class OrderDetails {     private $paymentGateway;
      public function __construct(PaymentGatewayContract $paymentGateway)     {         $this->paymentGateway = $paymentGateway;     }
      public function all()     {         $this->paymentGateway->setDiscount(500);
          return [             'name' => 'Memo Chou',             'Address' => 'Taiwan',         ];     } }
   | 
 
修改 AppServiceProvider 服務提供者,將原先綁定的 PaymentGateway 類別改為 PaymentGatewayContract 介面:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30
   | namespace App\Providers;
  use App\Billing\BankPaymentGateway; use App\Billing\PaymentGatewayContract; use Illuminate\Support\ServiceProvider;
  class AppServiceProvider extends ServiceProvider {     
 
 
 
      public function register()     {         $this->app->singleton(PaymentGatewayContract::class, function ($app) {             return new BankPaymentGateway('NTD');         });     }
      
 
 
 
      public function boot()     {              } }
   | 
 
結果:
1 2 3 4 5 6
   | array:4 [▼   "amount" => 2000   "confirmation_number" => "quDGIkn79T3dHWD1"   "currency" => "NTD"   "discount" => 500 ]
   | 
 
接下來,新增一個 CreditPaymentGateway 閘道器,一樣也是實作 PaymentGatewayContract 介面:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33
   | namespace App\Billing;
  use Illuminate\Support\Str;
  class CreditPaymentGateway implements PaymentGatewayContract {     private $currency;
      private $discount = 0;
      public function __construct(string $currency)     {         $this->currency = $currency;     }
      public function setDiscount(int $amount)     {         $this->discount = $amount;     }
      public function charge(int $amount)     {         $fees = $amount * 0.03;
          return [             'amount' => $amount - $this->discount + $fees,             'confirmation_number' => Str::random(),             'currency' => $this->currency,             'discount' => $this->discount,             'fees' => $fees,         ];     } }
   | 
 
如果要變更付款方式,只要在 AppServiceProvider 服務提供者將要綁定的實例改為 CreditPaymentGateway 閘道器即可。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30
   | namespace App\Providers;
  use App\Billing\CreditPaymentGateway; use App\Billing\PaymentGatewayContract; use Illuminate\Support\ServiceProvider;
  class AppServiceProvider extends ServiceProvider {     
 
 
 
      public function register()     {         $this->app->singleton(PaymentGatewayContract::class, function ($app) {             return new CreditPaymentGateway('NTD');         });     }
      
 
 
 
      public function boot()     {              } }
   | 
 
結果:
1 2 3 4 5 6 7
   | array:5 [▼   "amount" => 2075.0   "confirmation_number" => "jOM7mLCGYgIPR1B8"   "currency" => "NTD"   "discount" => 500   "fees" => 75.0 ]
   | 
 
如果要動態切換付款方式,只要將服務提供者修改如下即可:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37
   | namespace App\Providers;
  use App\Billing\BankPaymentGateway; use App\Billing\CreditPaymentGateway; use App\Billing\PaymentGatewayContract; use Illuminate\Support\ServiceProvider;
  class AppServiceProvider extends ServiceProvider {     
 
 
 
      public function register()     {         $this->app->singleton(PaymentGatewayContract::class, function ($app) {             switch (request()->payMethod) {                 case 'bank':                     return new BankPaymentGateway('NTD');
                  default:                     return new CreditPaymentGateway('NTD');             }         });     }
      
 
 
 
      public function boot()     {              } }
   | 
 
前往路由 pay?payMethod=bank,結果:
1 2 3 4 5 6
   | array:4 [▼   "amount" => 2000   "confirmation_number" => "joHVe6Ah19wVZWx5"   "currency" => "NTD"   "discount" => 500 ]
   | 
 
前往路由 pay?payMethod=credit,結果:
1 2 3 4 5 6 7
   | array:5 [▼   "amount" => 2075.0   "confirmation_number" => "s2w2dkTw98XWbkhs"   "currency" => "NTD"   "discount" => 500   "fees" => 75.0 ]
   | 
 
建立服務提供者
新增一個獨立的服務提供者來處理付款。
1
   | php artisan make:provider PaymentServiceProvider
   | 
 
將原來寫在 AppServiceProvider 中的容器綁定移至 PaymentServiceProvider 中。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37
   | namespace App\Providers;
  use App\Billing\BankPaymentGateway; use App\Billing\CreditPaymentGateway; use App\Billing\PaymentGatewayContract; use Illuminate\Support\ServiceProvider;
  class PaymentServiceProvider extends ServiceProvider {     
 
 
 
      public function register()     {         $this->app->singleton(PaymentGatewayContract::class, function ($app) {             switch (request()->payMethod) {                 case 'bank':                     return new BankPaymentGateway('NTD');
                  default:                     return new CreditPaymentGateway('NTD');             }         });     }
      
 
 
 
      public function boot()     {              } }
   | 
 
在 config/app.php 設定檔中註冊一個服務提供者:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17
   | 'providers' => [
      
      
 
      App\Providers\AppServiceProvider::class,     App\Providers\AuthServiceProvider::class,          App\Providers\EventServiceProvider::class,     App\Providers\RouteServiceProvider::class,     App\Providers\PaymentServiceProvider::class, 
      
  ],
   | 
 
前往路由 pay?payMethod=bank,結果:
1 2 3 4 5 6
   | array:4 [▼   "amount" => 2000   "confirmation_number" => "joHVe6Ah19wVZWx5"   "currency" => "NTD"   "discount" => 500 ]
   | 
 
前往路由 pay?payMethod=credit,結果:
1 2 3 4 5 6 7
   | array:5 [▼   "amount" => 2075.0   "confirmation_number" => "s2w2dkTw98XWbkhs"   "currency" => "NTD"   "discount" => 500   "fees" => 75.0 ]
   | 
 
參考資料