lucid-arch/laravel

此包已被弃用且不再维护。没有建议的替代包。

Laravel 框架。

资助包维护!
Patreon

安装次数: 3,275

依赖者: 0

建议者: 0

安全: 0

星标: 376

观察者: 31

分支: 69

类型:项目

v8.0.0 2020-10-26 11:51 UTC

README

Lucid 架构是一种软件架构,它将代码库维护作为应用扩展时的核心,从难以管理到消除未来将成为遗留代码的腐朽代码,并将日常用语如功能和服务转化为实际、具体的代码。

更多关于 Lucid 架构概念 的信息。

如果你喜欢视频,可以观看 2016 年 LaraconEU 上 Lucid 架构的公告。

构建可扩展应用程序的 Lucid 架构 - Laracon EU 2016

Abed Halawi - The Lucid Architecture for Building Scalable Applications

加入 Slack 社区

Slack Status

索引

安装

8.x

要立即使用 Lucid 开始您的项目,请运行以下命令

composer create-project lucid-arch/laravel my-project

这将为您提供一个带有 Lucid 的 Laravel 8 安装。如果您想下载 Laravel 的其他版本,也可以指定。

7.x

composer create-project lucid-arch/laravel=7.x my-project-7.x
6.x
composer create-project lucid-arch/laravel=6.x my-project-6.x
5.5
composer create-project lucid-arch/laravel=5.5.x my-project-5.5

简介

目录结构

src
├── Data
├── Domains
    └── * domain name *
            ├── Jobs
├── Foundation
└── Services
    └── * service name *
        ├── Console
        ├── Features
        ├── Http
        ├── Providers
        ├── Tests
        ├── database
        └── resources

组件

组件 路径 描述
服务 src/Service/[service] 放置 服务 的位置
功能 src/Services/[service]/Features/[feature] 放置 服务功能
任务 src/Domains/[domain]/Jobs/[job] 放置暴露域功能的 任务
数据 src/Data 放置模型、仓库、值对象以及任何数据相关的内容
基础 src/Foundation 放置用于应用程序中跨基础(抽象)元素的位置

服务

应用程序的每个部分都是一个服务(例如 Api、Web、Backend)。通常,这些服务将有它们自己的处理和响应请求的方式,实现我们应用程序的不同功能,因此,每个服务都将有它们自己的路由、控制器、功能和操作。服务将看起来像是 Laravel 应用程序的子安装,尽管它只是逻辑上的分组。

为了更好地理解服务背后的概念,可以将术语理解为:“我们的应用程序通过API服务公开数据”,“您可以通过后端服务来操纵和管理数据”。

使用Lucid的另一个好处是,当应用程序需要时,它使过渡到微服务架构的过程更加简单。请参阅微服务

服务目录结构

想象一下,我们生成了一个名为 Api 的服务,可以使用 lucid 命令行工具来生成,具体做法如下:

您可能需要设置才能使用 lucid 命令。

lucid make:service api

我们将得到以下目录结构

src
└── Services
    └── Api
        ├── Console
        ├── Features
        ├── Http
        │   ├── Controllers
        │   ├── Middleware
        │   ├── Requests
        │   └── routes.php
        ├── Providers
        │   ├── ApiServiceProvider.php
        │   └── RouteServiceProvider.php
        ├── Tests
        │   └── Features
        ├── database
        │   ├── migrations
        │   └── seeds
        └── resources
            ├── lang
            └── views
                └── welcome.blade.php

特性

特性在我们的应用程序中就像一个类,正如其名(例如登录特性、酒店搜索特性等)。特性是服务控制器将提供的内容,因此我们的控制器方法最终只有一行,因此是史上最薄的控制器!以下是一个生成特性和通过控制器提供该特性的示例

您可能需要设置才能使用 lucid 命令。

重要!您至少需要一个服务来托管您的特性。在此示例中,我们使用之前生成的Api服务,在命令中称为api

lucid make:feature SearchUsers api

我们将有 src/Services/Api/Features/SearchUsersFeature.php 和其测试 src/Services/Api/Tests/Features/SearchUsersFeatureTest.php

在特性类内部,有一个 handle 方法,这是我们调用该特性时会调用的方法,它支持依赖注入,这是定义依赖关系的完美地方。

现在我们需要一个控制器来提供该特性

lucid make:controller user api

我们的 UserControllersrc/Services/Api/Http/Controllers/UserController.php,为了提供该特性,在控制器方法中需要调用其 serve 方法,如下所示:

namespace App\Services\Api\Http\Controllers;

use App\Services\Api\Features\SearchUsersFeature;

class UserController extends Controller
{
    public function index()
    {
        return $this->serve(SearchUsersFeature::class);
    }
}

视图

要访问服务视图文件,请在文件名前加上服务名称,后面跟着两个冒号 ::

例如,在blade中扩展视图文件

@extends('servicename::index')

与作业的使用类似

new RespondWithViewJob('servicename::user.login')

RespondWithJsonJob 接受以下参数

RespondWithViewJob($template, $data = [], $status = 200, array $headers = []);

使用数据模板的示例

$this->run(new RespondWithViewJob('servicename::user.list', ['users' => $users]));

或者

$this->run(RespondWithViewJob::class, [
    'template' => 'servicename::user.list',
    'data' => [
        'users' => $users
    ],
]);

作业

作业负责应用程序执行的某个元素,并在特性的完成中扮演一个步骤的角色。它们是我们代码中可重用性的守护者。

作业生活在域中,这要求它们必须是抽象的、隔离的和独立的,无论是同一域中的其他作业还是其他域的作业——无论情况如何,没有作业应该调度另一个作业。

它们可以由任何服务的任何特性运行,并且是服务之间以及域之间通信的唯一方式。

示例:我们的 SearchUsersFeature 必须执行以下步骤

  • 验证用户搜索查询
  • 将查询记录到我们可以查看的位置
  • 如果找到结果
    • 记录结果以供以后参考(异步)
    • 增加找到元素上的搜索次数(异步)
    • 返回结果
  • 如果没有找到结果
    • 将查询记录在“高优先级”日志中,以便可以给予更多关注

每个这些步骤都将有一个同名作业,只实现该步骤。它们必须生成在实现其功能的相应域中,即我们的 ValidateUserSearchQueryJob 与用户输入有关,因此它应该在 User 域中。而日志与用户无关,可能在多个其他地方使用,因此它获得自己的域,我们在该 Log 域中生成 LogSearchResultsJob

要生成一个工作项,请使用 make:job <job> <domain> 命令

lucid make:job SearchUsersByName user

与功能类似,工作项也实现了 handle 方法来解析其依赖项,但有时我们可能需要传递与依赖项无关的输入,那些是工作项构造函数的参数,在 src/Domains/User/Jobs/SearchUsersByNameJob.php 中指定

namespace App\Domains\User\Jobs;

use Lucid\Foundation\Job;

class SearchUsersByNameJob extends Job
{
    private $query;
    private $limit;

    public function __construct($query, $limit = 25)
    {
        $this->query = $query;
        $this->limit = $limit;
    }

    public function handle(User $user)
    {
        return $user->where('name', $this->query)->take($this->limit)->get();
    }
}

现在我们需要运行这个步骤以及我们在 SearchUsersFeature::handle 中提到的其余步骤

public function handle(Request $request)
{
    // validate input - if not valid the validator should
    // throw an exception of InvalidArgumentException
    $this->run(new ValidateUserSearchQueryJob($request->input()));

    $results = $this->run(SearchUsersJob::class, [
        'query' => $request->input('query'),
    ]);

    if (empty($results)) {
        $this->run(LogEmptySearchResultsJob::class, [
            'date' => new DateTime(),
            'query' => $request->query(),
        ]);

        $response = $this->run(new RespondWithJsonErrorJob('No users found'));
    } else {
        // this job is queueable so it will automatically get queued
        // and dispatched later.
        $this->run(LogUserSearchJob::class, [
            'date' => new DateTime(),
            'query' => $request->input(),
            'results' => $results->lists('id'), // only the ids of the results are required
        ]);

        $response = $this->run(new RespondWithJsonJob($results));
    }

    return $response;
}

正如您所看到的,步骤的顺序在遵循每个 $this->run 调用时可以尽可能清晰地读取,每个工作项的签名也很容易理解

关于上述实现的一些注意事项

  • 通过实例化它来运行工作项(如 $this->run(new SomeJob))和传递其类名(如 $this->run(SomeJob::class))之间没有区别,这只是一种个人偏好,为了可读性和编写更少的代码,当工作项只接受一个参数时,就会进行实例化
  • 我们调用带有其类名的工作项时使用的参数顺序与其在工作项构造函数签名中的顺序无关。例如
$this->run(LogUserSearchJob::class, [
    'date' => new DateTime(),
    'query' => $request->input(),
    'resultIds' => $results->lists('id'), // only the ids of the results are required
]);
class LogUserSearchJob
{
    public function __construct($query, array $resultIds, DateTime $date)
    {
        // ...
    }
}

这将完美地工作,只要键名('resultIds' => ...)与构造函数中变量的名称($resultIds)相同

  • 当然,我们需要使用正确的命名空间创建和导入(use)我们的工作项类,但在这里我们不会这样做,因为这个文档只是为了展示,并不打算运行,有关示例,请参阅 入门

数据

数据并不是一个组件,更像是一个目录,包含所有与数据相关的类,如模型、存储库、值对象以及任何与数据有关的内容(算法等)。

基础

这是一个放置基础元素的地方,这些元素作为不属于任何组件的最抽象的类,目前包含 ServiceProvider,它是服务和框架(Laravel)之间的链接。您可能永远不需要或使用此目录中的任何其他内容,但如果您遇到需要跨所有组件共享的类,则可以自由使用此目录。

每个服务都必须在创建后在基础服务提供者内部进行注册,以便Laravel了解它,只需将 $this->app->register([service name]ServiceProvider::class); 添加到基础 ServiceProviderregister 方法中即可。例如,使用 Api 服务

// ...
use App\Services\Api\Providers\ApiServiceProvider;
// ...
public function register()
{
    $this->app->register(ApiServiceProvider::class);
}

入门

此项目包含 Lucid Console,它提供了一个交互式用户界面和命令行界面,这些界面对于脚手架和探索服务、功能和作业非常有用。

设置

lucid 可执行文件将在 vendor/bin 中。如果您没有将 ./vendor/bin/ 作为 PATH 的一部分,您需要使用 ./vendor/bin/lucid 来执行它,否则可以使用以下命令将其添加,以便能够简单地调用 lucid

export PATH="./vendor/bin:$PATH"

要查看所有可用的命令,请运行 lucid 或查看 CLI 参考

启动交互式控制台(UI)

  1. 一种运行应用程序的方法是使用内置的服务器,通过运行以下命令来实现
php artisan serve

任何其他方法也都适用(Apache、Nginx 等...)

  1. 运行 php artisan vendor:publish --provider="Lucid\Console\LucidServiceProvider"
  2. 访问您的应用程序 /lucid/dashboard

1. 创建服务

命令行界面
lucid make:service Api
用户界面

使用上述方法之一,在 src/Services 下的 Api 文件夹中必须创建一个新的服务文件夹。

Api 目录最初将包含以下目录

src/Services/Api
├── Console         # Everything that has to do with the Console (i.e. Commands)
├── Features        # Contains the Api's Features classes
├── Http            # Routes, controllers and middlewares
├── Providers       # Service providers and binding
├── database        # Database migrations and seeders
└── resources       # Assets, Lang and Views

为了让 Laravel 识别我们刚刚创建的服务,还需要进行一个额外的步骤。

注册服务

  • 打开 src/Foundation/Providers/ServiceProvider
  • 添加 use App\Services\Api\Providers\ApiServiceProvider
  • register 方法中添加 $this->app->register(ApiServiceProvider::class)

2. 创建功能

命令行界面
lucid make:feature ListUsers api
用户界面

使用上述方法之一,新的功能可以在 src/Services/Api/Features/ListUsersFeature.php 中找到。现在您可以在它的 handle 方法中填充大量作业。

3. 创建作业

该项目包含一些作业,可以在其对应的 src/Domains 目录下找到

命令行界面
lucid make:job GetUsers user
用户界面

使用上述方法之一,新的作业可以在 src/Domains/User/Jobs/GetUsers 中找到,现在您可以在它的 handle 方法中填充功能。在这个例子中,我们将仅添加一个静态的 return 语句

public function handle()
{
    return [
        ['name' => 'John Doe'],
        ['name' => 'Jane Doe'],
        ['name' => 'Tommy Atkins'],
    ];
}

4. 一体化

回到我们之前生成的功能,添加 $this->run(GetUsersJob)(请记住使用正确的命名空间 App\Domains\User\Jobs\GetUsersJob 导入作业)。

运行作业

ListUsersFeature::handle(Request $request)

public function handle(Request $request)
{
    $users = $this->run(GetUsersJob::class);

    return $this->run(new RespondWithJsonJob($users));
}

RespondWithJsonJob 是该项目附带的一些作业之一,它位于 Http 域,用于以结构化的 JSON 格式响应请求。

提供功能

为了提供该功能,我们需要创建一个路由和一个执行此操作的控制器。

使用以下命令生成一个普通的控制器

lucid make:controller user api --plain

向其中添加 get 方法

class UserController extends Controller
{
    public function get()
    {
        return $this->serve(ListUsersFeature::class);
    }
}

我们只需要创建一个将请求委派给我们的 get 方法的路由

src/Services/Api/Http/routes.php 中,您将找到路由组 Route::group(['prefix' => 'api'], function() {... 在该组中添加 /users 路由。

Route::get('/users', 'UserController@get');

现在,如果您访问 /api/users,您应该看到 JSON 结构。

微服务

最近您是否经常听到微服务这个词,并想知道它是如何工作的,或者您是否想基于微服务来规划您的下一个项目,或者为微服务转型做好充分的准备,Lucid将是您最佳的选择。Lucid的设计以可扩展性为核心,考虑到了微服务转型,因此将应用的不同部分称为“服务”并非巧合。然而,[建议](https://martinfowler.com.cn/bliki/MonolithFirst.html "nofollow noindex noopener external ugc")只有当您的单体应用程序变得非常大,使用微服务对于项目的进展和维护至关重要时,才进行转型;因为一旦您使用Lucid构建了应用程序,向微服务架构的过渡将更容易规划,实施起来也更加直接。您可以在[这里](https://github.com/lucid-architecture/laravel-microservice "nofollow noindex noopener external ugc")查看Lucid的微服务对应版本。

以下是关于从单体到微服务转型的更多方法。

事件钩子

Lucid提供了事件钩子,允许您监听每个分发的功能、操作或任务。这对于跟踪特别有用。

use Illuminate\Support\Facades\Event;
use Lucid\Foundation\Events\FeatureStarted;
use Lucid\Foundation\Events\OperationStarted;
use Lucid\Foundation\Events\JobStarted;

Event::listen(FeatureStarted::class, function (FeatureStarted $event) {
    // $event->name
    // $event->arguments
});

Event::listen(OperationStarted::class, function (OperationStarted $event) {
    // $event->name
    // $event->arguments
});

Event::listen(JobStarted::class, function (JobStarted $event) {
    // $event->name
    // $event->arguments
});