有时我们需要将应用程序逻辑放在控制器或模型之外的某个地方,通常称为服务。但是,有几种方法可以使用它们-作为静态“helpers”,作为对象或依赖注入。让我们看看每一个什么时候用是合适的。

我在本主题中看到的最大问题–关于如何使用依赖注入和服务的文章很多,但是几乎没有解释为什么应该使用它以及何时用是真正有用的。因此,让我们通过一些理论深入研究示例。

在这篇文章中,我们将介绍一个报告示例,使用不同的技术将代码从控制器移动到服务:

  1. 第一种方法:从控制器到静态服务“Helper助手”
  2. 第二种方法:使用非静态方法创建服务对象
  3. 第三种方式:带有参数的服务对象
  4. 第四种方式:依赖注入–简单案例
  5. 第五种方式:通过接口进行依赖注入–高级用法

初始示例:报告控制器

假设我们要建立一个月度报告,如下所示:

如果将它们全部放入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'));
        // ...
    }
}

 

这里到底发生了什么?

  1. 我们正在创建Controller的私有属性$reportService
  2. 我们正在将ReportService类型的参数传递给__construct()方法。
  3. 在构造函数内部,我们正在将该参数分配给该私有属性。
  4. 然后,在我们所有的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对象。

如果我们可以控制该变量值怎么办?例如,如果我们可以在测试阶段传递某些服务,而在实际使用中传递另一服务怎么办?

为此,我们将创建一个接口和将实现该接口的该服务的两个类。就像合同一样-接口将定义在实现该接口的所有类中应该存在的属性和方法。让我们创建一个例子。

还记得上面的示例ReportServiceYearlyReportService中的这两个服务吗?让他们实现相同的接口。

我们创建一个新文件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

Laravel:何时使用依赖注入,服务和静态方法
标签: