有时我们需要将应用程序逻辑放在控制器或模型之外的某个地方,通常称为服务。但是,有几种方法可以使用它们-作为静态“helpers”,作为对象或依赖注入。让我们看看每一个什么时候用是合适的。
我在本主题中看到的最大问题–关于如何使用依赖注入和服务的文章很多,但是几乎没有解释为什么应该使用它以及何时用是真正有用的。因此,让我们通过一些理论深入研究示例。
在这篇文章中,我们将介绍一个报告示例,使用不同的技术将代码从控制器移动到服务:
- 第一种方法:从控制器到静态服务“Helper助手”
- 第二种方法:使用非静态方法创建服务对象
- 第三种方式:带有参数的服务对象
- 第四种方式:依赖注入–简单案例
- 第五种方式:通过接口进行依赖注入–高级用法
初始示例:报告控制器
假设我们要建立一个月度报告,如下所示:
如果将它们全部放入Controller,它将看起来像这样:
// ... use statements class ClientReportController extends Controller { public function index(Request $request) { $q = Transaction::with('project') ->with('transaction_type') ->with('income_source') ->with('currency') ->orderBy('transaction_date', 'desc'); if ($request->has('project')) { $q->where('project_id', $request->project); } $transactions = $q->get(); $entries = []; foreach ($transactions as $row) { // ... Another 50 lines of code to fill in $entries by month } return view('report', compact('entries')); } }
现在,您看到数据库查询以及隐藏的50行代码–对于Controller来说可能太多了,所以我们需要将它们存储在某个地方,对吗?
第一种方法:从控制器到静态服务“Helper助手”
从Controller分离逻辑的最流行方法是创建一个单独的类,通常称为Service。换句话说,它可以被称为“helper”或简称为“函数”。
注意:服务类不是Laravel本身的一部分,没有make:service Artisan命令。它只是用于计算的简单PHP类,“服务”只是其典型名称。
因此,我们创建了一个文件app/Services/ReportService.php:
namespace App\Services; use App\Transaction; use Carbon\Carbon; class ReportService { public static function getTransactionReport(int $projectId = NULL) { $q = Transaction::with('project') ->with('transaction_type') ->with('income_source') ->with('currency') ->orderBy('transaction_date', 'desc'); if (!is_null($projectId)) { $q->where('project_id', $projectId); } $transactions = $q->get(); $entries = []; foreach ($transactions as $row) { // ... Same 50 lines of code copied from Controller to here } return $entries; } }
现在,我们可以从Controller调用该函数,如下所示:
// ... other use statements use App\Services\ReportService; class ClientReportController extends Controller { public function index(Request $request) { $entries = ReportService::getTransactionReport($request->input('project')); return view('report', compact('entries')); } }
就是这样,Controller现在要干净得多,对吗?
如您所见,我们使用了静态方法,并使用::语法对其进行了调用,因此实际上并未为该Service类创建对象。
什么时候使用?
通常,当您可以轻松地用简单的函数替换它,而无需使用类。它就像一个全局帮助器,但是位于ReportService类的内部只是为了保持面向对象的代码,并保持命名空间和文件夹的顺序。
另外,请记住,静态方法和类是无状态的。这意味着该方法仅被调用一次,并且不会在该类本身内保存任何数据。
但是,如果您确实想在该服务中保留一些数据……
第二种方法:使用非静态方法创建服务对象
初始化该类的另一种方法是使该方法成为非静态方法,并创建一个对象:
app/Services/ReportService.php:
class ReportService { // Just "public", but no "static" public function getTransactionReport(int $projectId = NULL) { // ... Absolutely the same code as in static version return $entries; } }
ClientReportController:
// ... other use statements use App\Services\ReportService; class ClientReportController extends Controller { public function index(Request $request) { $entries = (new ReportService())->getTransactionReport($request->input('project')); return view('report', compact('entries'); } }
或者,如果您不喜欢较长的单行代码:
$reportService = new ReportService(); $entries = $reportService->getTransactionReport($request->input('project'));
与静态方法没有太大区别,对吗?
这是因为,对于这种简单的情况,实际上没有任何区别。
但是,如果服务中有几个方法并且想要“链接”它们,立即又一个又一个地调用,这将很有用,因此每个方法都将返回相同的服务实例。您可以观看我8分钟的视频,或者作为一个简单的示例,在这里查看:
class ReportService { private $year; public function setYear($year) { $this->year = $year; return $this; } public function getTransactionReport(int $projectId = NULL) { $q = Transaction::with('project') ->with('transaction_type') ->with('income_source') ->with('currency') ->whereYear('transaction_date', $this->year) ->orderBy('transaction_date', 'desc'); // ... Other code
然后在Controller中,执行以下操作:
public function index(Request $request) { $entries = (new ReportService()) ->setYear(2020) ->getTransactionReport($request->input('project')); // ... Other code
什么时候使用?
老实说,在极少数情况下,主要用于链接方法,如上面的示例所示。。
如果您的Service在创建其对象new ReportService()时不接受任何参数,则只需使用静态方法即可。您根本不需要创建对象。
第三种方式:带有参数的服务对象
但是,如果要使用参数创建该服务怎么办?例如,您希望有一个Yearly年度报告,并通过实际的$year来初始化该类,该类将应用于该服务内部的所有方法。
app/Services/YearlyReportService.php:
class YearlyReportService { private $year; public function __construct(int $year) { $this->year = $year; } public function getTransactionReport(int $projectId = NULL) { // Notice the ->whereYear('transaction_date', $this->year) $q = Transaction::with('project') ->with('transaction_type') ->with('income_source') ->with('currency') ->whereYear('transaction_date', $this->year) ->orderBy('transaction_date', 'desc'); $entries = []; foreach ($transactions as $row) { // ... Same 50 line of code } return $entries; } // Another report that uses the same $this->year public function getIncomeReport(int $projectId = NULL) { // Notice the ->whereYear('transaction_date', $this->year) $q = Transaction::with('project') ->with('transaction_type') ->with('income_source') ->with('currency') ->whereYear('transaction_date', $this->year) ->where('transaction_type', 'income') ->orderBy('transaction_date', 'desc'); $entries = []; // ... Some more logic return $entries; } }
看起来更复杂吧?
但现在,由于此,下面是我们可以在控制器中操作的。
// ... other use statements use App\Services\YearlyReportService; class ClientReportController extends Controller { public function index(Request $request) { $year = $request->input('year', date('Y')); // default to current year $reportService = new YearlyReportService($year); $fullReport = $reportService->getTransactionReport($request->input('project')); $incomeReport = $reportService->getIncomeReport($request->input('project')); } }
在此示例中,服务的两种方法都将使用我们在创建对象时传递的相同Year参数。
现在,实际创建该对象而不是使用静态方法就很有意义。因为现在我们的服务确实具有实际状态,并且取决于一年。
什么时候使用?
当您的服务具有参数并且希望创建其对象时,将传递一些参数值,这些参数值将在调用该服务对象的所有 Service 方法时重新使用。
第四种方式:依赖注入–简单案例
如果Controller中有几种方法,并且想在所有方法中重用同一服务,则还可以将其作为类型提示的参数注入Controller的构造函数中,如下所示:
class ClientReportController extends Controller { private $reportService; public function __construct(ReportService $service) { $this->reportService = $service; } public function index(Request $request) { $entries = $this->reportService->getTransactionReport($request->input('project')); // ... } public function income(Request $request) { $entries = $this->reportService->getIncomeReport($request->input('project')); // ... } }
这里到底发生了什么?
- 我们正在创建Controller的私有属性$reportService;
- 我们正在将ReportService类型的参数传递给__construct()方法。
- 在构造函数内部,我们正在将该参数分配给该私有属性。
- 然后,在我们所有的Controller中,我们可以使用$this->reportService及其所有方法。
这由Laravel本身提供支持,因此您不必担心实际创建该类对象,只需将正确的参数类型传递给构造函数即可。
什么时候使用?
当Controller中有多个方法想要使用同一Service且Service不需要任何参数时(例如上例中的$year)。这种方法只是节省时间,因此您不必在每个Controller方法中都执行new ReportService() 。
但是,等等,类型提示注入还有更多–您可以通过任何方法(不仅是Controller)执行此操作。这称为方法注入。
像这样:
class ClientReportController extends Controller { public function index(Request $request, ReportService $reportService) { $entries = $reportService->getTransactionReport($request->input('project')); // ... }
如您所见,不需要构造函数或私有属性,只需注入一个类型提示变量,并在方法内使用它。Laravel创造了这个对象"通过魔法"。
但是,老实说,对于我们的确切示例而言,它没什么用,这些注入所带来的不仅仅是编写服务,而是编写更多的代码,对吗?那么使用依赖注入的实际好处是什么?
第五种方式:通过接口进行依赖注入–高级用法
在前面的示例中,我们将一个参数传递给控制器,Laravel“神奇地”解析了该参数以在后台创建一个Service对象。
如果我们可以控制该变量值怎么办?例如,如果我们可以在测试阶段传递某些服务,而在实际使用中传递另一服务怎么办?
为此,我们将创建一个接口和将实现该接口的该服务的两个类。就像合同一样-接口将定义在实现该接口的所有类中应该存在的属性和方法。让我们创建一个例子。
还记得上面的示例ReportService和YearlyReportService中的这两个服务吗?让他们实现相同的接口。
我们创建一个新文件app/Interfaces/ReportServiceInterface.php:
namespace App\Interfaces; interface ReportServiceInterface { public function getTransactionReport(int $projectId = NULL); }
就是这样,我们在这里不需要执行任何操作–接口只是一组规则,而没有任何“主体”方法。因此,在这里,我们定义实现该接口的每个类都必须具有该getTransactionReport()方法。
现在,app/Services/ReportService.php:
use App\Interfaces\ReportServiceInterface; class ReportService implements ReportServiceInterface { public function getTransactionReport(int $projectId = NULL) { // ... same old code
另外,app/Services/YearlyReportService.php:
use App\Interfaces\ReportServiceInterface; class YearlyReportService implements ReportServiceInterface { private $year; public function __construct(int $year = NULL) { $this->year = $year; } public function getTransactionReport(int $projectId = NULL) { // Again, same old code with $year as a parameter
现在,主要部分–我们将哪种类型的提示输入到Controller中?ReportService还是YearlyReportService?
实际上,我们不再键入类的提示,而是键入一个接口的类型提示。
use App\Interfaces\ReportServiceInterface; class ClientReportController extends Controller { private $reportService; public function __construct(ReportServiceInterface $reportService) { $this->reportService = $reportService; } public function index(Request $request) { $entries = $this->reportService->getTransactionReport($request->input('project')); // ... Same old code
这里的主要部分是__construct(ReportServiceInterface $reportService)。现在,我们可以附加和交换实现该接口的任何类。
但是,默认情况下,由于框架不知道要使用哪个类,因此我们会丢失Laravel的“魔术注入”。因此,如果您将其保留为这样,则会出现错误:
Illuminate\Contracts\Container\BindingResolutionException
Target [App\Interfaces\ReportServiceInterface] is not instantiable while building [App\Http\Controllers\Admin\ClientReportController].
没错,我们没有说要实例化哪个类。
我们需要通过在app/Providers/AppServiceProvider.php中的register()方法中执行此操作。
为了使此示例完全清楚,让我们添加一个 if 语句,该逻辑是,如果我们有一个本地环境,我们将使用 ReportService,否则我们需要"YearlyReportService"。
use App\Interfaces\ReportServiceInterface; use App\Services\ReportService; use App\Services\YearlyReportService; class AppServiceProvider extends ServiceProvider { /** * Register any application services. * * @return void */ public function register() { if (app()->environment('local')) { $this->app->bind(ReportServiceInterface::class, function () { return new ReportService(); }); } else { $this->app->bind(ReportServiceInterface::class, function () { return new YearlyReportService(); }); } } }
看看这里发生了什么?
我们将根据当前工作的位置(本地计算机或实时服务器)来选择要使用的服务。
什么时候使用?
上面的示例可能是带接口的依赖项注入的最常用用法–当您需要根据某些条件交换服务时,可以在服务提供者中轻松地做到这一点。
更多示例如,当您交换电子邮件提供商或付款提供商时等。但是,当然,确保两个服务实现相同的接口很重要(而且不容易)。
长文章, 对吧?是的,因为这个话题非常复杂,我想用非常真实的例子来解释它,这样您不仅会了解如何使用依赖注入和服务,还了解为什么使用它们以及何时使用每个案例。
via https://quickadminpanel.com/blog/laravel-when-to-use-dependency-injection-services-and-static-methods/
原创翻译,转载请注明来自lenix的博客,地址https://blog.p2hp.com/archives/7523