我如何使用 Laravel 来控制混乱

Laravel

背景前言

在撰写本文时,我是一名全栈的Web开发人员@TBFiles, 这是一家葡萄牙公司,在安哥拉和莫桑比克也有业务TBFiles。我们的核心业务是帮助金融证券实现工作流程设计和文档与数字流程的优化。我们帮助客户传输离线信息(一般是文档、表单、流程)。通过使用数字流程而不是物理文档,我们的客户节省了时间、空间(因为他们不必处理物理归档),并以友好、可搜索和可扩展的方式保持所有信息可通过单个应用程序访问。

这种服务要求我们使用不同的技术(如FTP、电子邮件和第三方Web服务)支持数据收集和提取,每个客户都有自己的组织信息的方式。

混乱

到目前为止,每当新客户订阅我们的“数字档案”产品时,我们的团队就必须创建一个特定的PHP脚本,该脚本将连接到该客户使用的任何系统,导入所需的数据以建立索引,并处理以下特定逻辑提取并保存在我们的基础架构中,然后在我们的应用程序中显示该信息。这项工作可能需要一些时间,具体取决于客户的规格,我们需要处理的系统的数量和类型,以及我们的团队是否需要与第三方技术团队进行协调,仅举几例……

这是可以管理的,但与其他所有事情一样,当我们遇到大量使用此服务的客户时,这种类型的工作流开始适得其反。

有人可能会问:“但是,该团队中的任何人都没有在开发服务时考虑过扩展吗?”。 我回答:*“当然! 但是这里有一个叫做“管理”的东西,它是朋友的交货日期,而他们的律师叫压力***。 除了玩笑,第一步是快速创建它,因此可以对其进行测试并证明是可行的业务。

事实证明,这是一个很好的产品,我们的团队聚在一起讨论了如何应对开始出现的混乱。 而且我们需要快速行动,因为管理部门的律师(The Pressure)从未离开过大楼,并且我们发现“交付日期”有一个双胞胎……

派对开始!

经过团队的快速集思广益,我们得出的结论是,我们必须开发一个两步过程:一个 Collector (用于获取信息)和一个 Extractor (用于处理数据提取并将其发送到我们的主程序)。 应用程序的持久层,可供我们的客户使用。

我负责开发 Collector 部分,另一个同事负责开发 Extractor 。 我很兴奋! 我是团队的最新成员,已经被分配了这种职责。 我立即开始工作。

收集器的要求是:

-它必须能够连接到公司当前支持的所有数据源(电子邮件收件箱,FTP 文件存储库和共享目录),并保存所有找到的信息,以规范化的方式对其进行组织并存储以供后代使用。

-它必须具有一个 Web UI,以允许非技术用户访问和配置数据存储桶(一种数据类型的存储库,例如:电子邮件存储桶),并设置规则以使系统确切知道要导入的内容以及随后执行一些导入后操作以进行清理工作。

-它必须具有 API 层,以允许其他内部服务访问数据以进行特定处理和可能需要的其他业务逻辑。

这三个要点是我这部分系统的核心。 交货日期设置为首次启动后的两周(可以理解成开发任务进度)。

为工作选择工具

两个星期是一个挑战……我需要完成自己的任务,然后与同事一起测试结果。为了能够交付良好,健壮的应用程序,我立即想到了 Laravel 框架。我甚至没有三思而后行:在此项目之前,我已经在另一个项目中成功使用了它,体验很好,这归功于它流利的语法,文档完善的界面和出色的社区。基准测试也令人印象深刻。

好,我可以做到!

Laravel 新的收集器

我开始考虑实现的方案。

在开始编写代码之前,我花时间去思考如何处理电子邮件。我知道对于 FTP 和共享文件夹(我们还有其他服务可以扫描物理文档并使它们在共享文件夹中可用),我可以简单地使用 Flysystem,它可以和 Laravel 一起开箱即用。但是,电子邮件访问和处理并非像人们想象的那样简单。我想的是:必须有一个扩展包!。因此,我在 GitHub 搜索了一些可用的项目,可以帮助我处理 IMAP 连接的繁重工作。

我很快通过 David de Boer 找到了 IMAP。正是我需要的:一个很好的,经过测试的库,通过非常简单的 API 处理 IMAP 连接,消息搜索,检索以及附件下载。这些例子清楚地告诉我,我可以在不需要太多工作的情况下使用它!

在 Composer 执行此操作时,我在考虑如何以一种简单的方式使所有内容都可配置。我需要添加一个 UI 层,以便经过身份验证的用户可以配置不同的连接,数据验证规则以及清理已处理数据的一些操作。

受 AWS S3 Buckets 的影响,我想到了 Buckets。实际上,这是一个非常简单的术语,用来描述包含内容的资源。 Bucket 仅具有一种类型的数据,即包含那么过滤掉不符合某些业务规则数据的特定规则,并且还将包含过滤后的数据导入我们的系统后要执行的操作。

UI层

我不是设计师。即使我的生活取决于它,我也绝对不能画出任何有用的东西...幸运的是,存在 Bootstrap 之类的项目,可以帮助我。另外,有一个名为 Tabler 的 UI 项目,可以用来处理我的 「原始设计」。

因此,我创建了普通索引,编辑并创建了用于管理 Buckets 的面板,并将它们与我们现有的客户表关联。在接下来的两天内,我创建了必要的面板来管理 Buckets 规则和操作。另外,我为要导入的电子邮件做了必要的清单和显示面板,并为它们可能关联的每个附件提供了下载链接。

我还添加了一个用于启动和停止存储桶处理作业的按钮。

有趣的东西

在工作的第一周结束时,我将准备好所有控制器,模型和视图以接收来自Buckets 中定义的源数据。

确保对所有 Bucket 配置的加密,因为它将保存客户的访问信息。以纯文本形式保存这是一个非常明智的信息,因此我使用了 Laravel 可以使用的 encrypt()decrypt() 方法。在将数据保存到数据库之前,我会先对其进行加密,然后仅在访问它们时对其进行解密。

之后,我对系统的工作原理进行了更多的思考。我知道我需要使用队列类,通过将要处理的实际工作发送到后台来使该应用程序尽可能灵活。但是如何最好地解决呢?我的系统应如何处理?为了帮助我思考,我提出了以下工作流程:

-计划命令应查询符合处理条件的所有活动 Buckets

-对于找到的每个活动 Bucket,应调度一个新的 Process Job 以连接到定义的源。

-对于每个数据(如果是消息,文件或其他受支持的媒体),相应的处理任务(这里的任务就是队列的意思)应获取 Bucket 规则,并且仅处理通过每个规则的数据。

-如果其中一个规则未正确验证,则处理任务将忽略此数据。

-最后,处理任务将导入获取数据的规范化版本,检查附件(如果是电子邮件),然后将其下载到磁盘以进行存档。如果有任何PDF文件,我们将需要提取文本并将其保存到持久层以在我们的应用程序上显示。

-最后,处理任务将执行 Bucket 配置中定义的所有操作。

整理好工作流程后,我创建了一个新的 Laravel 命令,该命令每分钟都会查询一次,假设 Buckets 被处理并为每个任务分派一个任务,如下所示:

<?php
namespace App\Console\Commands;
use App\Bucket;
use App\Jobs\ProcessEmailBuckets;
use App\Jobs\ProcessFtpBuckets;
use App\Jobs\ProcessNfsBuckets;
use Illuminate\Console\Command;
class Collector extends Command
{
    /**
     * The name and signature of the console command.
     *
     * @var string
     */
    protected $signature = 'collector';
    /**
     * The console command description.
     *
     * @var string
     */
    protected $description = 'Initializes the processing of buckets';
    /**
     * Execute the console command.
     *
     * @param Bucket $buckets
     * @return mixed
     */
    public function handle(Bucket $buckets)
    {
        $buckets->freeForProcess()->get()->each(function ($bucket) {
            if ($bucket->type === 'email') {
                ProcessEmailBuckets::dispatch($bucket)->onQueue('collector');
            } elseif ($bucket->type === 'ftp') {
                ProcessFtpBuckets::dispatch($bucket)->onQueue('collector');
            } elseif ($bucket->type === 'nfs') {
                ProcessNfsBuckets::dispatch($bucket)->onQueue('collector');
            }
        });
    }
}

为了避免任何重叠,我使用 withoutOverlapping() 方法链接了此 Collector 命令,如下所示:

/**
 * 定义应用程序的命令时间表.
 *
 * @param  \Illuminate\Console\Scheduling\Schedule  $schedule
 * @return void
 */
protected function schedule(Schedule $schedule)
{
    $schedule->command('collector')->everyMinute()->withoutOverlapping();
}

得益于强大的 Flysystem 支持,我很快就获得了 ProcessFtpBucketsProcessNfsBuckets 的 Job 类。

但是,当我编写 ProcessEmailBuckets 功能时,我开始考虑:它不应该了解 IMAP 库的具体实现。为了避免陷入一个常见的陷阱(即 硬编码,应该面向接口,而不是面向实现),我创建了存储库类 EmailServiceRepository 用作 IMAP 库的包装器,其中包括一些简单的方法将使任务类能够处理邮箱连接。具体如下

<?php
namespace App\Repositories\Contracts;
interface EmailServiceInterface
{
  /**
   * 连接到服务器并进行身份验证.
   *
   * @param string $server
   * @param string $username
   * @param string $password
   * @return self $this
   */
  public function connect(string $server, string $username, string $password);

  /**
   * 所有可用的邮箱
   *
   * @return array
   */
  public function listMailboxes();

  /**
   * 所有消息
   *
   * @param string $mailbox
   * @param array $filters
   * @return mixed
   */
  public function getEmails(string $mailbox, array $filters = []);

  /**
   * 创建规范化的消息元数据
   *
   * @param Message $message
   * @return array
   */
  public function normalize(Message $message);

  /**
   * 下载附件到磁盘并返回格式化的元数据
   *
   * @param Attachment $attachment
   * @param string $baseFolder
   * @return array
   */
  public function downloadAttachment(Attachment $attachment, string $baseFolder = 'attachments');

  /**
   * 将给定的消息标记为已读
   *
   * @param string $mailbox
   * @param string $messagesNumbers
   * @return mixed
   */
  public function markAsRead(string $mailbox, string $messagesNumbers);

  /**
   * 将给定的消息标记为未读
   *
   * @param string $mailbox
   * @param string $messagesNumbers
   * @return mixed
   */
  public function markAsUnRead(string $mailbox, string $messagesNumbers);

  /**
   * 将消息移动到给定的邮箱路径
   *
   * @param Message $message
   * @param string $mailbox
   * @return void
   */
  public function move(Message $message, string $mailbox);

  /**
   * 删除给定消息
   *
   * @param int $messageNumber
   * @param string $mailbox
   */
  public function delete(int $messageNumber, string $mailbox = 'INBOX');
}

现在,如果需要,我可以在将来的任何时候交换具体的实现,只要我尊重该接口,就无需更改 Process Job 类。

规则与动作

并非所有通讯都将被导入,因为它可能是 SPAM,与 Bucket 范围无关。为了处理这些情况,我创建了一个基于规则的简单验证系统和导入后操作(用于以后的清理)。

通过 UI 定义的规则基本上是说 Bucket 仅关心特定数据。例如,可以将Bucket 配置为仅存档从特定电子邮件地址发送的通信。该规则配置将设置ProcessEmailBuckets 类以忽略该电子邮件未发送的任何消息。

规则可以配置为检查发件人地址,收件人的地址,主题或正文上的关键字,是否有附件和其他附件……它们甚至可以堆叠在一起,这意味着消息仅在通过时才被导入为 Bucket 定义的所有规则,看起来比实际复杂得多。为验证起见,这是验证器方法:

/**
 * 根据导入规则验证给定的消息
 *
 * @param array $message
 * @return mixed
 */
protected function validate(array $message)
{
    return $this->bucket->rules()->get()->every(function ($rule) use ($message) {
        switch ($rule->validator) {
            case 'all':
                return true;
                break;

            case 'sender':
                return str_contains($message['from'], $rule->param);
                break;

            case 'receiver':
                return str_is($message['to'], $rule->param);
                break;

            case 'subject':
                return str_contains($message['subject'], $rule->param);
                break;

            case 'body':
                return str_contains($message['body'], $rule->param);
                break;

            case 'attachments':
                return !empty($message['attachments']);
                break;

            default:
                return false;
        }
    });
}

遵循相同的逻辑,操作(也是通过 UI 定义的)在成功导入消息后,配置要由ProcessEmailsBuckets 类完成的工作。可以采取以下措施之一:将邮件标记为已读/已导入,将邮件移至另一个邮箱,然后从源中删除该邮件。这将确保源保持有条理,并为下一次运行进行优化。这是我的实现方式

/**
   * 在定义流程动作后执行
   *
   * @param array $message
   */
  protected function executeActions(array $message)
  {
      $this->bucket->actions()->get()->each(function ($action) use ($message) {
          if ($action->type === 'flag_as_seen') {
              $this->repository->markAsRead($this->bucket->connection['mailbox'], $message['number']);
          } elseif ($action->type === 'move') {
              $message = $this->repository->getEmails($this->bucket->connection['mailbox'], ['number' => $message['number']]);
              $this->repository->move($message, $action->param);
          } elseif ($action->type === 'delete') {
              $this->repository->delete($this->bucket->connection['mailbox'], $message['number']);
          }
      });
  }

处理附件

我需要处理的另一个细微差别是从 PDF 提取文本。因此,如果 ProcessEmailBuckets Job 类检测到 PDF 文件/附件,不仅会下载并存档它,而且还应该分派另一个作业类 ScanPdfAttachments

此类将运行安装在服务器上的名为 PdfToText 的第三方软件,该软件将 PDF文件路径作为输入,并输出其文本。然后,ScanPdfAttachments 类将获取该输出并将其保存到数据库中与 PDF 文件关联的列中。这样,提取器就可以使用此文本来收集相关的业务数据。

我需要指出的是,只需从一家著名的公司获取一个名为 pdf-to-text 的包,并调用它们提供的简单静态方法:Pdf::getText()。顺便提一句,该公司名为 PpatieSpatie (他们开发了很多很棒的包)

另外,这使我想起我需要给他们寄一张明信片,以便使用他们的第三方包!

最后保存!

现在我已经准备好我的 收集器 了!它成功地允许创建存储桶,定义其规则和操作,并访问通过 Web UI 导入的数据,我在后台运行流程作业,并且拥有一个API 层,因此我的同事可以从我的设备中获取规范化的数据。 收集并处理特定客户的业务提取规则和数据传递。

现在,我们不需要为每个订阅“数字档案”服务的客户创建新的脚本。我们只需创建必要的 Buckets,设置数据导入和规范化的规则,然后继续我们的生活,因为我们知道收集器将积极监听要提取的数据,并将其提供给其他内部系统,如提取器

由于Laravel framework,所有这些工作都在大约两周内完成了,这使我能够抽象出常见的需求并专注于我的主要问题。包括测试!


作者的注释

非常感谢您阅读本文!

你可能会注意到,我没有透露许多技术细节,也没有透露系统的其他几个部分。我与目前的雇主有一份非常严格的合同,不允许我展示该项目的所有细节。如果您对此感到失望,我真的很抱歉...

但是,我展示的是我的思考过程,选择的工具以及做出决策的原因。每个人都可以在其他语言和/或框架上做出相同甚至更好的解决方案。重要的是思考的过程,培养把大问题转换成小问题并且还不偏离核心业务的能力。

本文中的所有译文仅用于学习和交流目的,转载请务必注明文章译者、出处、和本文链接
我们的翻译工作遵照 CC 协议,如果我们的工作有侵犯到您的权益,请及时联系我们。

原文地址:https://medium.com/@josepostiga/how-i-ma...

译文地址:https://learnku.com/laravel/t/39027

本文为协同翻译文章,如您发现瑕疵请点击「改进」按钮提交优化建议
《L01 基础入门》
我们将带你从零开发一个项目并部署到线上,本课程教授 Web 开发中专业、实用的技能,如 Git 工作流、Laravel Mix 前端工作流等。
《G01 Go 实战入门》
从零开始带你一步步开发一个 Go 博客项目,让你在最短的时间内学会使用 Go 进行编码。项目结构很大程度上参考了 Laravel。
讨论数量: 0
(= ̄ω ̄=)··· 暂无内容!

讨论应以学习和精进为目的。请勿发布不友善或者负能量的内容,与人为善,比聪明更重要!