郝 玉华

郝 玉华

1658380740

如何在 Gmai 中批量删除电子邮件

我听到我的一些同事谈论要达到收件箱零。所以我开始想办法清除我超过 4000 条未读消息。经过几天的搜索,我找到了一种方法。

我什至继续从垃圾箱中删除了 20,000 封电子邮件,并在此过程中节省了超过 1 GB 的磁盘空间。
收件箱0

今天,我想向您展示如何在 Gmail 应用程序中批量删除电子邮件——无论您有成千上万的邮件。

如何删除 Gmail 中的所有未读邮件

第 1 步:登录到您的 Gmail 帐户

第 2 步:在搜索栏中,键入in:unread并点击ENTER。这将显示 50 条未读消息。
ss4-2

第 3 步:选中右上角的复选框以选择 50 封未读电子邮件。
SS5-2

第 4 步:单击“选择与此搜索匹配的所有对话”的消息。这将选择所有未读邮件。
ss6-2

第 5 步:单击顶部的删除图标。
ss7-3

第 6 步:将出现一个弹出窗口,询问您是否要批量删除消息。单击“确定”。
SS8-2

这就是您可以在 Gmail 中批量删除邮件的方式。
SS9-2

如何清除 Gmail 垃圾箱

第 1 步:要清除垃圾箱中的邮件,请单击左侧的“更多”并选择垃圾箱。
SS10-2

第 2 步:单击右上角的复选框以选择垃圾箱中的邮件。
SS11-2

第 3 步:选择垃圾箱中的所有邮件,然后单击“永久删除”。
SS12-2

第 4 步:单击确定以确认您要删除所有消息。
SS13-1

您应该会收到一条消息,指出 x 封电子邮件已被永久删除。如果即使一切都清楚,您也没有收到消息,请刷新页面。
ss14-1

您还可以删除促销或社交选项卡中的消息。

如何从促销列表中删除电子邮件?

要删除促销选项卡中的电子邮件,请按照以下步骤操作。

第 1 步:点击右侧的更多,然后选择类别。
ss15

第 2 步:点击促销。
ss16

第 2 步:单击右上角的复选框以选择促销选项卡中的所有 50 条消息。
ss17

第 3 步:单击促销选项卡中的选择所有对话。
SS18

第 4 步:单击顶部的删除图标。
ss19

第 5 步:确认您要删除促销选项卡中的所有消息。
SS20

您应该会收到一条消息,表明对话已被移至垃圾箱。
SS21

如果您想删除其他选项卡(例如社交或论坛)中的消息,请重复您执行的过程以删除促销选项卡中的所有消息。

结论

我希望这篇文章可以帮助您删除 Gmail 应用中不需要的邮件,这样您也可以将收件箱归零。

您还可以通过其他方式使用搜索运算符来查询 Gmail 应用程序并显示多年来收到的邮件,以便您可以随心所欲地使用它们。您可以在 Google 支持中找到这些搜索运算符。

谢谢阅读。

来源:https ://www.freecodecamp.org/news/how-to-batch-delete-emails-in-gmail-delete-multiple-email-messages/

#email  #google 

What is GEEK

Buddha Community

如何在 Gmai 中批量删除电子邮件
郝 玉华

郝 玉华

1658380740

如何在 Gmai 中批量删除电子邮件

我听到我的一些同事谈论要达到收件箱零。所以我开始想办法清除我超过 4000 条未读消息。经过几天的搜索,我找到了一种方法。

我什至继续从垃圾箱中删除了 20,000 封电子邮件,并在此过程中节省了超过 1 GB 的磁盘空间。
收件箱0

今天,我想向您展示如何在 Gmail 应用程序中批量删除电子邮件——无论您有成千上万的邮件。

如何删除 Gmail 中的所有未读邮件

第 1 步:登录到您的 Gmail 帐户

第 2 步:在搜索栏中,键入in:unread并点击ENTER。这将显示 50 条未读消息。
ss4-2

第 3 步:选中右上角的复选框以选择 50 封未读电子邮件。
SS5-2

第 4 步:单击“选择与此搜索匹配的所有对话”的消息。这将选择所有未读邮件。
ss6-2

第 5 步:单击顶部的删除图标。
ss7-3

第 6 步:将出现一个弹出窗口,询问您是否要批量删除消息。单击“确定”。
SS8-2

这就是您可以在 Gmail 中批量删除邮件的方式。
SS9-2

如何清除 Gmail 垃圾箱

第 1 步:要清除垃圾箱中的邮件,请单击左侧的“更多”并选择垃圾箱。
SS10-2

第 2 步:单击右上角的复选框以选择垃圾箱中的邮件。
SS11-2

第 3 步:选择垃圾箱中的所有邮件,然后单击“永久删除”。
SS12-2

第 4 步:单击确定以确认您要删除所有消息。
SS13-1

您应该会收到一条消息,指出 x 封电子邮件已被永久删除。如果即使一切都清楚,您也没有收到消息,请刷新页面。
ss14-1

您还可以删除促销或社交选项卡中的消息。

如何从促销列表中删除电子邮件?

要删除促销选项卡中的电子邮件,请按照以下步骤操作。

第 1 步:点击右侧的更多,然后选择类别。
ss15

第 2 步:点击促销。
ss16

第 2 步:单击右上角的复选框以选择促销选项卡中的所有 50 条消息。
ss17

第 3 步:单击促销选项卡中的选择所有对话。
SS18

第 4 步:单击顶部的删除图标。
ss19

第 5 步:确认您要删除促销选项卡中的所有消息。
SS20

您应该会收到一条消息,表明对话已被移至垃圾箱。
SS21

如果您想删除其他选项卡(例如社交或论坛)中的消息,请重复您执行的过程以删除促销选项卡中的所有消息。

结论

我希望这篇文章可以帮助您删除 Gmail 应用中不需要的邮件,这样您也可以将收件箱归零。

您还可以通过其他方式使用搜索运算符来查询 Gmail 应用程序并显示多年来收到的邮件,以便您可以随心所欲地使用它们。您可以在 Google 支持中找到这些搜索运算符。

谢谢阅读。

来源:https ://www.freecodecamp.org/news/how-to-batch-delete-emails-in-gmail-delete-multiple-email-messages/

#email  #google 

许 志强

许 志强

1656567600

如何在 Svelte 中创建弹出框

我在没有任何第三方设计系统库的情况下实现了我上一个项目Papyrs的 UI 组件——也就是说,我从头开始创建了所有组件。我这样做是为了完全控制和灵活地控制我自以为是的布局中的杂项砖块。

在这篇博文中,我分享了如何在Svelte中开发弹出框组件。

 

骨骼

弹出框是一个浮动容器,它呈现在锚点(通常是一个按钮)旁边的内容上,它启动了它的显示。为了提高叠加层的视觉焦点,通常使用背景来部分模糊其后面的视图。

我们可以通过在一个名为​​的组件中复制上面的骨架来开始实现,该组件Popover.svelte包含一个button​​和div​。

<button>Open</button>

<div
    role="dialog"
    aria-labelledby="Title"
    aria-describedby="Description"
    aria-orientation="vertical"
>
    <div>Backdrop</div>
    <div>Content</div>
</div>

为了提高可访问性,我们可以设置dialog角色并提供一些aria信息(有关更多详细信息,请参阅MDN 文档)。

 

动画

我们创建一个boolean状态—— visible​​——来显示或关闭弹出框。单击时button,状态设置为true并呈现叠加层。相反,当点击背景时,它会变成false​并关闭。

此外,我们在弹出框上添加了一个点击监听器,它除了停止事件传播之外什么都不做。这对于避免在用户与其内容交互时关闭覆盖非常有用。

借助过渡指令,我们还可以使叠加层优雅地出现和消失——也称为“Svelte 的黑魔法”😁。

<script lang="ts">
  import { fade, scale } from 'svelte/transition';
  import { quintOut } from 'svelte/easing';

  let visible = false;
</script>

<button on:click={() => (visible = true)}>Open</button>

{#if visible}
  <div
    role="dialog"
    aria-labelledby="Title"
    aria-describedby="Description"
    aria-orientation="vertical"
    transition:fade
    on:click|stopPropagation
  >
    <div
      on:click|stopPropagation={() => (visible = false)}
      transition:scale={{ delay: 25, duration: 150, easing: quintOut }}
    >
      Backdrop
    </div>
    <div>Content</div>
  </div>
{/if}

 

​定位于内容

​无论页面是否滚动,都应该在所有内容上呈现弹出框。因此我们可以使用一个fixed位置作为起点。它的内容和背景都设置了一个absolute定位。背景也应该覆盖屏幕,但它是覆盖层的子元素——因此是“绝对的”——并且内容应该位于锚点旁边。

我们添加到解决方案中的其余 CSS 代码是宽度、高度或颜色的最小样式设置。

<script lang="ts">
  import { fade, scale } from 'svelte/transition';
  import { quintOut } from 'svelte/easing';

  let visible = false;
</script>

<button on:click={() => (visible = true)}>Open</button>

{#if visible}
  <div
    role="dialog"
    aria-labelledby="Title"
    aria-describedby="Description"
    aria-orientation="vertical"
    transition:fade
    class="popover"
    on:click|stopPropagation
  >
    <div
      on:click|stopPropagation={() => (visible = false)}
      transition:scale={{ delay: 25, duration: 150, easing: quintOut }}
      class="backdrop"
    />
    <div class="wrapper">Content</div>
  </div>
{/if}

<style>
  .popover {
    position: fixed;
    inset: 0;

    z-index: 997;
  }

  .backdrop {
    position: absolute;
    inset: 0;

    background: rgba(0, 0, 0, 0.3);
  }

  .wrapper {
    position: absolute;

    min-width: 200px;
    max-width: 200px;

    min-height: 100px;

    width: fit-content;
    height: auto;

    overflow: hidden;

    display: flex;
    flex-direction: column;
    align-items: flex-start;

    background: white;
    color: black;
  }
</style>

 

​定位在锚点旁边

​要设置按钮旁边的叠加层,我们必须获取该元素的引用以找到它在视口中的位置。为此,我们可以bind锚定。

当引用准备好或调整窗口大小时(如果用户调整浏览器大小,位置可能会改变),我们使用getBoundingClientRect()方法来查询有关位置的信息。

我们最终将这些 JavaScript 信息转换为 CSS 变量,以在我们想要设置的确切位置呈现弹出框的内容。

<script lang="ts">
  // ...
  
  let anchor: HTMLButtonElement | undefined = undefined;

  let bottom: number;
  let left: number;

  const initPosition = () =>
    ({ bottom, left } = anchor?.getBoundingClientRect() ?? { bottom: 0, left: 0 });

  $: anchor, initPosition();
</script>

<svelte:window on:resize={initPosition} />

<button on:click={() => (visible = true)} bind:this={anchor}>Open</button>

{#if visible}
  <div
    role="dialog"
    aria-labelledby="Title"
    aria-describedby="Description"
    aria-orientation="vertical"
    transition:fade
    class="popover"
    on:click|stopPropagation
    style="--popover-top: {`${bottom}px`}; --popover-left: {`${left}px`}"
  >
    <!-- ... -->
  </div>
{/if}

<style>
  /** ... */

  .wrapper {
    position: absolute;

    top: calc(var(--popover-top) + 10px);
    left: var(--popover-left);

    /** ... */
  }
</style>

​上面的代码片段被修剪以仅展示与本章相关的内容。总而言之,组件的代码如下:

<script lang="ts">
  import { fade, scale } from 'svelte/transition';
  import { quintOut } from 'svelte/easing';

  let visible = false;
  let anchor: HTMLButtonElement | undefined = undefined;

  let bottom: number;
  let left: number;

  const initPosition = () =>
    ({ bottom, left } = anchor?.getBoundingClientRect() ?? { bottom: 0, left: 0 });

  $: anchor, initPosition();
</script>

<svelte:window on:resize={initPosition} />

<button on:click={() => (visible = true)} bind:this={anchor}>Open</button>

{#if visible}
  <div
    role="dialog"
    aria-labelledby="Title"
    aria-describedby="Description"
    aria-orientation="vertical"
    transition:fade
    class="popover"
    on:click|stopPropagation
    style="--popover-top: {`${bottom}px`}; --popover-left: {`${left}px`}"
  >
    <div
      on:click|stopPropagation={() => (visible = false)}
      transition:scale={{ delay: 25, duration: 150, easing: quintOut }}
      class="backdrop"
    />
    <div class="wrapper">Content</div>
  </div>
{/if}

<style>
  .popover {
    position: fixed;
    inset: 0;

    z-index: 997;
  }

  .backdrop {
    position: absolute;
    inset: 0;

    background: rgba(0, 0, 0, 0.3);
  }

  .wrapper {
    position: absolute;

    top: calc(var(--popover-top) + 10px);
    left: var(--popover-left);

    min-width: 200px;
    max-width: 200px;

    min-height: 100px;

    width: fit-content;
    height: auto;

    overflow: hidden;

    display: flex;
    flex-direction: column;
    align-items: flex-start;

    background: white;
    color: black;
  }
</style>

 

而且……就是这样!我们已经实现了一个最低限度的自定义弹出框,可以在任何 Svelte 应用程序中使用,而无需任何依赖。

结论

我们没有美化解决方案,因此结果在边缘看起来仍然很粗糙,但是弹出框按预期工作。

要从那里进行迭代,实现更多选项或使其闪亮,例如,您可以查看GitHub 上Papyrs的开源代码🤗

​走向无限,超越
大卫

 来源:https ://betterprogramming.pub/create-a-popover-in-svelte-fe7dd2eeebb1

#svelte 

高橋  陽子

高橋 陽子

1657524855

如何在 PHP 中創建與框架無關的應用程序

無論您使用何種工具集,都可以創建與框架無關的應用程序。最近,PHP 開發人員談論了很多關於領域驅動設計(DDD)——六邊形架構和類似模式。他們的主要目標是將業務邏輯與框架、存儲和第三方相關代碼分開。我想進一步討論這個主題,並向您展示如何將域內容與所有必要的樣板文件分開,這些樣板文件是讓一切正常運行所需的。我將使用兩個非常流行的 PHP 框架——LaravelSymfony。

什麼是與框架無關的應用程序?

與框架無關意味著包含應用程序業務規則的代碼與框架文件小心分離。因此,您的域對用於處理 HTTP 相關通信的框架一無所知。但這不僅僅是關於Laravel / Symfony(或任何其他你想到的框架)。我也希望它與 ORM/第三方庫無關。此外,我不希望我的業務邏輯綁定到 Doctrine 的註釋或 Eloquent 模型。它需要清潔並完全獨立工作。

為什麼要將應用程序的業務邏輯與其他元素分開?

你可能會想:“這太荒謬了,我永遠不會改變現有項目中的框架或存儲。” 真的嗎?你100%確定嗎?我一直在從事一個正在從Zend Framework 2 遷移到 Symfony 4的項目。如果應用程序域與實現部分分離,那會容易得多。我們只需要為一組合約和目標框架相關類切換實現。因此,它本可以為我們節省大量時間和麻煩。

如果你不碰任何東西,你不可能打破它。分散在與框架相關的類中的域代碼在遷移過程中很容易被破壞。測試對你沒有幫助,因為你必須重寫它們(你也可以破壞它們)。

所有潛在問題都與新工具有關,您不會意外更改應用程序的行為。

即使您無法想像在項目生命週期中更改存儲,您也可能希望更改 ORM 甚至擺脫它。如果業務邏輯模型不與存儲庫分離,這通常是一項非常耗時的任務。更不用說,人們在設計領域類時經常考慮到特定的存儲,而不是關注它們的自然關係。將所有內容分開甚至可以讓您使用兩種不同的存儲實現來檢查哪一種更適合您的需求。

如何實現與框架無關的方法?

這並不像聽起來那麼困難。我們將使用幾種技術使事情變得更容易。

  •  
    • 如果Run已經開始,則無法註冊Runner
    • 如果跑步者已經報名參加跑步,則不能再次參加
  • Run中保存Runner 結果
    • 如果Runner沒有參加過Run ,則無法保存其Result
    • 如果結果已過期,則無法保存(在我們的示例中,結果最多可在運行五天后添加)
    • 如果Runner達到Run的時間限制,則無法保存其Result
    • 如果Runner的Result已保存,則無法再次保存

這些可能不是在這種情況下應該使用的所有規則,但對於我們的示例,這絕對足夠了。

有了一些業務知識,我們就可以開始對我們的領域進行編程了。讓我們從幾個模型開始:

<?php

declare(strict_types = 1);

namespace Domain\Model;

use Common\Id;

final class Run
{
    private $id;

    private $name;

    /**
     * Time limit in seconds
     */
    private $timeLimit;

    private $startAt;

    private $type;

    /**
     * Length in meters
     */
    private $length;

    public function __construct(
        Id $id,
        string $name,
        int $timeLimit,
        \DateTime $startAt,
        RunType $type,
        int $length
    ) {
        $this->id = $id;
        $this->name = $name;
        $this->timeLimit = $timeLimit;
        $this->startAt = $startAt;
        $this->type = $type;
        $this->length = $length;
    }

    public function getId(): Id
    {
        return $this->id;
    }

    public function getName(): string
    {
        return $this->name;
    }

    public function getTimeLimit(): int
    {
        return $this->timeLimit;
    }

    public function getStartAt(): \DateTime
    {
        return $this->startAt;
    }

    public function getType(): RunType
    {
        return $this->type;
    }

    public function getLength(): int
    {
        return $this->length;
    }
}

這是我們的Run模型。如您所見,它非常簡單,只是帶有構造函數和 getter 的幾個屬性。你可能會問,Id類是什麼?它是存儲 UUID 的可自我驗證的類 - 看看吧。還有一個RunType類存儲可能的運行類型。但是,這對於我們的示例並不重要。

下一個是Runner模型:

<?php

declare(strict_types = 1);

namespace Domain\Model;

use Common\Id;
use Domain\Exception\RunAlreadyParticipated;
use Domain\Exception\RunAlreadyStarted;
use Domain\Exception\RunNotParticipated;
use Domain\Exception\RunResultAlreadySaved;
use Domain\Exception\RunResultExpired;
use Domain\Exception\TimeLimitReached;

final class Runner extends User
{
    const RUN_RESULT_EXPIRY_DAYS = 5;

    /**
     * @var RunParticipation[]
     */
    private $participations;

    /**
     * @var RunResult[]
     */
    private $results;

    public function __construct(
        Id $id,
        string $email,
        string $password,
        array $participations = [],
        array $results = []
    ) {
        $this->participations = $participations;
        $this->results = $results;

        parent::__construct(
            $id,
            $email,
            $password
        );
    }

    /**
     * @return RunParticipation[]
     */
    public function getParticipations(): array
    {
        return $this->participations;
    }

    /**
     * @return RunResult[]
     */
    public function getResults(): array
    {
        return $this->results;
    }

    /**
     * @return RunParticipation
     * @throws RunAlreadyParticipated
     * @throws RunAlreadyStarted
     */
    public function participate(Run $run): RunParticipation
    {
        if (isset($this->participations[(string)$run->getId()])) {
            throw RunAlreadyParticipated::forRun($run, $this);
        }

        if ($run->getStartAt() < new \DateTime()) {
            throw RunAlreadyStarted::forRun($run);
        }

        $runParticipation = new RunParticipation($run, $this->getId());
        $this->participations[] = $runParticipation;

        return $runParticipation;
    }

    /**
     * @throws RunNotParticipated
     * @throws RunResultAlreadySaved
     * @throws RunResultExpired
     * @throws TimeLimitReached
     */
    public function result(Run $run, int $time): RunResult
    {
        if (!isset($this->participations[(string)$run->getId()])) {
            throw RunNotParticipated::forRun($run, $this);
        }

        if ($run->getStartAt()->diff(new \DateTime())->d > self::RUN_RESULT_EXPIRY_DAYS) {
            throw RunResultExpired::forRun($run, $this);
        }

        if ($time > $run->getTimeLimit()) {
            throw TimeLimitReached::forRun($run, $this);
        }

        if (isset($this->results[(string)$run->getId()])) {
            throw RunResultAlreadySaved::forRun($run, $this);
        }

        $runResult = new RunResult($run, $this->getId(), $time);
        $this->results[(string)$run->getId()] = $runResult;

        return $runResult;
    }
}

在這裡,我們有一些有趣的事情要解釋:

  1. Runner模型使用包含簡單屬性(如電子郵件或密碼)的User 。
  2. 我們在這裡有RunParticipationRunResult類,它們將我們的跑步者與跑步及其結果聯繫起來。稍後我將向您展示其中一門課程。
  3. 我決定在第一個中保留RunnerRun之間的關係(你可以自由地做其他方式,這只是我的設計決定)。
  4. 我們這裡有兩種有趣的商業方法:參與結果。它們是我們系統的核心,負責本示例中的幾乎所有業務邏輯。如您所見,它們包含一組簡單的條件(由我們的業務規則描述),如果不滿足則拋出異常。除此之外,他們創建特定關係類的新實例,將其添加到集合中並返回以便應用程序的其他部分更輕鬆地處理。

我想向您展示一個業務異常的示例:

<?php

declare(strict_types = 1);

namespace Domain\Exception;

use Domain\Model\Run;
use Domain\Model\Runner;

final class RunNotParticipated extends DomainException
{
    public static function forRun(Run $run, Runner $runner): self
    {
        return new self(
            sprintf("Runner %s didn't participate in run %s", $runner->getId(), $run->getId())
        );
    }
}

如您所見,它是一個非常簡單的類,其中包含特定的消息。請注意,它直接擴展了 DomainException而不是 PHP 的\Exception。這給了我們更多的控制權,並有助於以一種特殊的方式處理域異常。

現在讓我們看一下其中一個關係類RunResult

<?php

declare(strict_types = 1);

namespace Domain\Model;

use Common\Id;

final class RunResult
{
    private $run;

    private $runnerId;

    /**
     * Time in seconds
     */
    private $time;

    public function __construct(Run $run, Id $runnerId, int $time)
    {
        $this->run = $run;
        $this->runnerId = $runnerId;
        $this->time = $time;
    }

    public function getRun(): Run
    {
        return $this->run;
    }

    public function getRunnerId(): Id
    {
        return $this->runnerId;
    }

    public function getTime(): int
    {
        return $this->time;
    }
}

我們將從Runner模型的角度使用它,因此它包含Run。Runner id 僅用於更輕鬆地與應用程序的其他層集成。它還保存著跑步者在比賽中取得的時間成績。

RunParticipation模型非常相似。它不包含其他屬性。

請記住,RunParticipationRunResult仍然是域的一部分,它們與如何在基礎設施層和實際存儲實現中實現無關。

在域中,我們不僅有模型,還需要一種從存儲中獲取模型的方法。為此,我們將使用存儲庫模式。下面介紹了仍然是我們域的一部分的示例存儲庫合同。

<?php

namespace Domain\Repository;

use Common\Id;
use Domain\Exception\RunnerNotFound;
use Domain\Model\Runner;

interface RunnerRepository
{
    /**
     * @throws RunnerNotFound
     */
    public function getById(Id $runnerId): Runner;
}
<?php

namespace Domain\Repository;

use Domain\Model\RunParticipation;

interface RunParticipationRepository
{
    public function save(RunParticipation $runParticipation): void;
}

使用第一個,只能從存儲中檢索跑步者,第二個只是保存他的參與。我們只關注重要元素!

現在我們已經解釋了我們的領域層,是時候看看應用程序了。在這一層,我們將使用命令總線,因此對於每個應用程序操作,都有一個類負責它。在這種模式中,我們有一個非常簡單的 Command 類,其中包含由系統參與者引入的數據。然後,它由 Handler 處理,該 Handler 在域層的幫助下執行特定的邏輯。我們看一下runner的報名例子:

<?php

declare(strict_types = 1);

namespace Application\Command;

use Common\Id;

final class EnrollRunnerToRun
{
    private $runnerId;

    private $runId;

    public function __construct(Id $runnerId, Id $runId)
    {
        $this->runnerId = $runnerId;
        $this->runId = $runId;
    }

    public function getRunnerId(): Id
    {
        return $this->runnerId;
    }

    public function getRunId(): Id
    {
        return $this->runId;
    }
}
<?php

declare(strict_types = 1);

namespace Application\Handler;

use Application\Command\EnrollRunnerToRun;
use Domain\Repository\RunnerRepository;
use Domain\Repository\RunParticipationRepository;
use Domain\Repository\RunRepository;

final class EnrollRunnerToRunHandler
{
    private $runnerRepository;

    private $runRepository;

    private $runParticipationRepository;

    public function __construct(
        RunnerRepository $runnerRepository,
        RunRepository $runRepository,
        RunParticipationRepository $runParticipationRepository
    ) {
        $this->runnerRepository = $runnerRepository;
        $this->runRepository = $runRepository;
        $this->runParticipationRepository = $runParticipationRepository;
    }

    /**
     * @throws \Domain\Exception\RunAlreadyParticipated
     * @throws \Domain\Exception\RunAlreadyStarted
     * @throws \Domain\Exception\RunNotFound
     * @throws \Domain\Exception\RunnerNotFound
     */
    public function handle(EnrollRunnerToRun $command): void
    {
        $run = $this->runRepository->getById($command->getRunId());
        $runner = $this->runnerRepository->getById($command->getRunnerId());

        $runParticipation = $runner->participate($run);

        $this->runParticipationRepository->save($runParticipation);
    }
}

該命令是不言自明的。這只是我們應用程序的一個用例。Handler 使用域存儲庫來獲取所需的模型,執行指定的業務邏輯任務並將其結果保存回存儲。有什麼值得注意的?層之間的關係:域對應用程序一無所知,但應用程序使用域來執行其任務。此外,該應用程序仍然與第三方工具無關。我們不必選擇框架或存儲!

框架呢?

而且……就是這樣!我們有一個完整的應用程序。只有幾個小障礙:它不能處理 HTTP/cli 請求,沒有存儲空間,所以我們沒有真正的數據可以使用。它還缺少一些框架和第三方庫必不可少的其他東西,以使我們的生活更輕鬆。所以,是時候介紹框架了

框架和存儲

將與框架無關的應用程序與存儲連接

我決定在這種特殊情況下我將使用MySQL 數據庫。這個決定背後沒有具體的原因——它只是非常流行,大多數程序員都非常了解它並且經常使用它。

首先,我們需要為我們的領域模型準備 Eloquent 表示。我們再來看看領域 Runner 模型:

<?php

declare(strict_types = 1);

namespace Domain\Model;

use Common\Id;
use Domain\Exception\RunAlreadyParticipated;
use Domain\Exception\RunAlreadyStarted;
use Domain\Exception\RunNotParticipated;
use Domain\Exception\RunResultAlreadySaved;
use Domain\Exception\RunResultExpired;
use Domain\Exception\TimeLimitReached;

final class Runner extends User
{
    const RUN_RESULT_EXPIRY_DAYS = 5;

    /**
     * @var RunParticipation[]
     */
    private $participations;

    /**
     * @var RunResult[]
     */
    private $results;

    public function __construct(
        Id $id,
        string $email,
        string $password,
        array $participations = [],
        array $results = []
    ) {
        $this->participations = $participations;
        $this->results = $results;

        parent::__construct(
            $id,
            $email,
            $password
        );
    }

    /**
     * @return RunParticipation[]
     */
    public function getParticipations(): array
    {
        return $this->participations;
    }

    /**
     * @return RunResult[]
     */
    public function getResults(): array
    {
        return $this->results;
    }

    /**
     * @return RunParticipation
     * @throws RunAlreadyParticipated
     * @throws RunAlreadyStarted
     */
    public function participate(Run $run): RunParticipation
    {
        if (isset($this->participations[(string)$run->getId()])) {
            throw RunAlreadyParticipated::forRun($run, $this);
        }

        if ($run->getStartAt() < new \DateTime()) {
            throw RunAlreadyStarted::forRun($run);
        }

        $runParticipation = new RunParticipation($run, $this->getId());
        $this->participations[] = $runParticipation;

        return $runParticipation;
    }

    /**
     * @throws RunNotParticipated
     * @throws RunResultAlreadySaved
     * @throws RunResultExpired
     * @throws TimeLimitReached
     */
    public function result(Run $run, int $time): RunResult
    {
        if (!isset($this->participations[(string)$run->getId()])) {
            throw RunNotParticipated::forRun($run, $this);
        }

        if ($run->getStartAt()->diff(new \DateTime())->d > self::RUN_RESULT_EXPIRY_DAYS) {
            throw RunResultExpired::forRun($run, $this);
        }

        if ($time > $run->getTimeLimit()) {
            throw TimeLimitReached::forRun($run, $this);
        }

        if (isset($this->results[(string)$run->getId()])) {
            throw RunResultAlreadySaved::forRun($run, $this);
        }

        $runResult = new RunResult($run, $this->getId(), $time);
        $this->results[(string)$run->getId()] = $runResult;

        return $runResult;
    }
}

現在是 Eloquent 等價物:

<?php

namespace Infrastructure\Eloquent\Model;

use Illuminate\Database\Eloquent\Model;
use Infrastucture\Eloquent\Model\RunResult;

class Runner extends Model
{
    /**
     * The attributes that are mass assignable.
     *
     * @var array
     */
    protected $fillable = [
        'id',
        'email',
        'password',
    ];

    /**
     * The attributes that should be hidden for arrays.
     *
     * @var array
     */
    protected $hidden = [
        'password'
    ];

    /**
     * @var string
     */
    protected $table = 'runners';

    /**
     * @var string
     */
    protected $keyType = 'string';

    /**
     * @var bool
     */
    public $timestamps = false;

    /**
     * @var bool
     */
    public $incrementing = false;

    public function participations()
    {
        return $this->hasMany(RunParticipation::class);
    }

    public function results()
    {
        return $this->hasMany(RunResult::class);
    }
}

請注意,Eloquent 模型包含與其他存儲模型的關係。我們稍後會回复他們。

現在有一個關鍵問題:如何連接這兩個完全不同的類?由於它們根本不相似,因此擴展可能不是一個好主意,特別是因為存儲模型已經擴展了 ORM 的 Model 類。好吧,我們還有另一個選擇。讓我們編寫一個能夠在這兩個模型之間進行雙向轉換的類。我們稱它為變壓器。這裡是:

<?php

declare(strict_types = 1);

namespace Infrastructure\Eloquent\Transformer;

use Infrastructure\Eloquent\Model\Runner as Entity;
use Domain\Model\Runner as Domain;

class RunnerTransformer
{
    private $runParticipationTransformer;

    private $runResultTransformer;

    public function __construct(
        RunParticipationTransformer $runParticipationTransformer,
        RunResultTransformer $runResultTransformer
    ) {
        $this->runParticipationTransformer = $runParticipationTransformer;
        $this->runResultTransformer = $runResultTransformer;
    }

    /**
     * @throws \Common\Exception\InvalidIdException
     * @throws \Domain\Exception\InvalidRunType
     */
    public function entityToDomain(Entity $entity): Domain
    {
        $dbParticipations = $entity->participations()->get();
        $dbResults = $entity->results()->get();
        $runnerId = \Common\Id::create($entity->id);
        $participations = $this->runParticipationTransformer->entityToDomainMany($dbParticipations);
        $results = $this->runResultTransformer->entityToDomainMany($dbResults);

        return new Domain($runnerId, $entity->email, $entity->password, $participations, $results);
    }

    public function domainToEntity(Domain $domain): Entity
    {
        $entity = new Entity();
        $entity->id = (string)$domain->getId();
        $entity->email = $domain->getEmail();
        $entity->password = $domain->getPassword();

        return $entity;
    }
}

現在,讓我們省略為處理跑步者所需的其他模型而注入的額外轉換器。我們希望專注於方法entityToDomaindomainToEntity。正如你所看到的,多虧了轉換器類,我們可以輕鬆地將我們的域模型與 Eloquent 連接起來,我們的存儲也可以正常工作。

下面是另一個基於 RunParticipation 的 Eloquent 模型示例。

<?php

namespace Infrastructure\Eloquent\Model;

use Illuminate\Database\Eloquent\Model;

class RunParticipation extends Model
{
    /**
     * The attributes that are mass assignable.
     *
     * @var array
     */
    protected $fillable = [
        'runner_id',
        'run_id',
    ];

    /**
     * @var string
     */
    protected $table = 'run_participations';

    /**
     * @var string
     */
    protected $keyType = 'string';

    /**
     * @var bool
     */
    public $timestamps = false;

    /**
     * @var bool
     */
    public $incrementing = false;

    public function runner()
    {
        return $this->belongsTo(Runner::class);
    }

    public function run()
    {
        return $this->belongsTo(Run::class);
    }

現在對應的轉換類:

<?php

declare(strict_types = 1);

namespace Infrastructure\Eloquent\Transformer;

use Illuminate\Database\Eloquent\Collection;
use Infrastructure\Eloquent\Model\RunParticipation as Entity;
use Domain\Model\RunParticipation as Domain;

class RunParticipationTransformer
{
    private $runTransformer;

    public function __construct(RunTransformer $runTransformer)
    {
        $this->runTransformer = $runTransformer;
    }

    /**
     * @throws \Common\Exception\InvalidIdException
     * @throws \Domain\Exception\InvalidRunType
     */
    public function entityToDomain(Entity $entity): Domain
    {
        $dbRun = $entity->run()->get()->pop();
        $runnerId = \Common\Id::create($entity->runner_id);
        $run = $this->runTransformer->entityToDomain($dbRun);

        return new Domain($run, $runnerId);
    }

    /**
     * @throws \Common\Exception\InvalidIdException
     * @throws \Domain\Exception\InvalidRunType
     */
    public function entityToDomainMany(Collection $entities): array
    {
        $domains = [];

        foreach ($entities as $entity) {
            $domains[$entity->run_id] = $this->entityToDomain($entity);
        }

        return $domains;
    }

    public function domainToEntity(Domain $domain): Entity
    {
        $run = $domain->getRun();

        $entity = new Entity();
        $entity->run_id = (string)$run->getId();
        $entity->runner_id = (string)$domain->getRunnerId();

        return $entity;
    }
}

如您所見,我們有額外的 entityToDomainMany方法。它用於為特定跑步者翻譯整個 RunParticipation 集合。

現在我們有可能翻譯模型,所以我們最終可以履行存儲庫合同。我們可以使用facees而不必擔心耦合問題,因為一切都是分開的。讓我們看一下 RunnerRepository 的實現:

<?php

declare(strict_types = 1);

namespace Infrastructure\Eloquent\Repository;

use Common\Id;
use Domain\Exception\RunnerNotFound;
use Domain\Model\Runner;
use Infrastructure\Eloquent\Transformer\RunnerTransformer;

class RunnerRepository implements \Domain\Repository\RunnerRepository
{
    private $runnerTransformer;

    public function __construct(RunnerTransformer $runnerTransformer)
    {
        $this->runnerTransformer = $runnerTransformer;
    }

    /**
     * {@inheritdoc}
     */
    public function getById(Id $runnerId): Runner
    {
        $runner = \Infrastructure\Eloquent\Model\Runner::find((string)$runnerId);

        if (null === $runner) {
            throw RunnerNotFound::forId($runnerId);
        }

        return $this->runnerTransformer->entityToDomain($runner);
    }
}

它在 MySQL 中查找數據並使用轉換器將其轉換為域的 Runner 模型。

我們還可以檢查用於保存數據而不是檢索數據的存儲庫:

<?php

declare(strict_types = 1);

namespace Infrastructure\Eloquent\Repository;

use Domain\Model\RunParticipation;
use Infrastructure\Eloquent\Transformer\RunParticipationTransformer;

class RunParticipationRepository implements \Domain\Repository\RunParticipationRepository
{
    private $runParticipationTransformer;

    public function __construct(RunParticipationTransformer $runParticipationTransformer)
    {
        $this->runParticipationTransformer = $runParticipationTransformer;
    }

    /**
     * {@inheritdoc}
     */
    public function save(RunParticipation $runParticipation): void
    {
        $dbRunParticipation = $this->runParticipationTransformer->domainToEntity($runParticipation);

        $dbRunParticipation->save();
    }
}

它非常簡單,只有兩行​​有意義的代碼。由於“單一職責原則”,這些類中的大多數都非常小且易於閱讀。

這就是存儲的全部內容!我們已經將我們的業務模型連接到 ORM,並且可以從外部源讀取/寫入真實數據。

與框架無關的應用程序的框架

現在讓我們將所有東西都連接到Laravel。我們的連接點是應用層。我決定,我不想與 HTTP 糾纏不清——它是如此常見和無聊,想要嘗試不同的東西,所以我們將通過 CLI 發送請求。幸運的是,這比存儲準備要容易得多。我們將盡可能使用依賴注入。      

我們需要使用命令總線實現。我選擇了戰術家。Laravel 有現成的包,我用的是 joselfonseca/laravel-tactician

這是我們的參與控制台命令:

<?php

namespace App\Console\Commands;

use Application\Command\EnrollRunnerToRun;
use Application\Handler\EnrollRunnerToRunHandler;
use Common\Id;
use Domain\Exception\DomainException;
use Illuminate\Console\Command;
use Joselfonseca\LaravelTactician\CommandBusInterface;

class RunnerParticipate extends Command
{
    /**
     * The name and signature of the console command.
     *
     * @var string
     */
    protected $signature = 'runner:enroll {runnerId} {runId}';

    /**
     * The console command description.
     *
     * @var string
     */
    protected $description = 'Enroll runner to run';

    /**
     * Create a new command instance.
     *
     * @return void
     */
    public function __construct()
    {
        parent::__construct();
    }

    /**
     * Execute the console command.
     *
     * @return mixed
     */
    public function handle(CommandBusInterface $commandBus)
    {
        $commandBus->addHandler(EnrollRunnerToRun::class, EnrollRunnerToRunHandler::class);

        $runnerId = Id::create($this->argument('runnerId'));
        $runId = Id::create($this->argument('runId'));

        $command = new EnrollRunnerToRun($runnerId, $runId);

        try {
            $commandBus->dispatch($command);

            $this->info('Runner enrolled');
        } catch (DomainException $e) {
            $this->error($e->getMessage());
        }
    }
}

我們接受兩個參數—— runnerIdrunId處理。該方法本身非常簡單:

  • 第 45 行- 通知命令總線哪個處理程序應該處理指定的命令,
  • 第 47-48 行- 創建Id對象,因此它們將由應用程序命令處理
  • 第 50 行– 實例化命令,
  • 第 52-58 行- 通過命令總線發送命令並通知成功。如果不滿足任何業務規則,我們會捕獲DomainException並通知用戶問題所在。

你可能會問,為什麼我們不早點使用框架驗證器來驗證數據。事實是,我們應該這樣做,但我們已經跳過它,因為這個應用程序被簡化了。您應該始終使用框架驗證您的數據!域異常是最後的立場,我們通常不應該達到這個立場。

我們不能忘記依賴注入,所以這裡是我使用的提供者:

<?php

namespace App\Providers;

use Domain\Repository\RunnerRepository;
use Domain\Repository\RunParticipationRepository;
use Domain\Repository\RunRepository;
use Domain\Repository\RunResultRepository;
use Illuminate\Support\ServiceProvider;

class RunnerProvider extends ServiceProvider
{
    public $bindings = [
        RunnerRepository::class => \Infrastructure\Eloquent\Repository\RunnerRepository::class,
        RunRepository::class => \Infrastructure\Eloquent\Repository\RunRepository::class,
        RunParticipationRepository::class => \Infrastructure\Eloquent\Repository\RunParticipationRepository::class,
        RunResultRepository::class => \Infrastructure\Eloquent\Repository\RunResultRepository::class,
    ];

    /**
     * Register services.
     *
     * @return void
     */
    public function register()
    {
        //
    }

    /**
     * Bootstrap services.
     *
     * @return void
     */
    public function boot()
    {
        //
    }
}

我們只需要為我們的合約(接口)綁定,其餘的由 Laravel 自動完成。在這一步並編寫了一些數據庫遷移之後,我們的應用程序就可以正常工作了。甚至沒有一條邏輯依賴於框架或庫。

與框架無關意味著獨立

如您所見,與框架無關並不是很困難,我們只需要有意識地使用模式並添加一些開箱即用的思維即可。為了證明我們的領域和應用層是真正獨立的,我將重用它們,這次是使用 Symfony 和 Doctrine。我將在本文末尾描述該過程。同時,請查看帶有工作 Laravel 應用程序的存儲庫。

切換框架和數據庫工具

今天在我們的技術堆棧菜單中,我們有: 

  • Symfony 4.2
  • 教義(MySQL 5.7)
  • 戰術家(命令總線實現)

正如你所看到的,它與 Laravel 的並沒有太大的不同。我們只切換了框架和數據庫工具。我決定使用 Doctrine,因為這個相當成熟的 ORM 在開發人員中非常流行。就那麼簡單。那麼,讓我們開始吧。

首先,將與框架無關的應用程序與存儲連接……

乍一看,您可能會注意到 Eloquent 和 Doctrine 之間的巨大差異。再看一下我們領域的 Runner 模型:

<?php

declare(strict_types = 1);

namespace Domain\Model;

use Common\Id;
use Domain\Exception\RunAlreadyParticipated;
use Domain\Exception\RunAlreadyStarted;
use Domain\Exception\RunNotParticipated;
use Domain\Exception\RunResultAlreadySaved;
use Domain\Exception\RunResultExpired;
use Domain\Exception\TimeLimitReached;

final class Runner extends User
{
    const RUN_RESULT_EXPIRY_DAYS = 5;

    /**
     * @var RunParticipation[]
     */
    private $participations;

    /**
     * @var RunResult[]
     */
    private $results;

    public function __construct(
        Id $id,
        string $email,
        string $password,
        array $participations = [],
        array $results = []
    ) {
        $this->participations = $participations;
        $this->results = $results;

        parent::__construct(
            $id,
            $email,
            $password
        );
    }

    /**
     * @return RunParticipation[]
     */
    public function getParticipations(): array
    {
        return $this->participations;
    }

    /**
     * @return RunResult[]
     */
    public function getResults(): array
    {
        return $this->results;
    }

    /**
     * @return RunParticipation
     * @throws RunAlreadyParticipated
     * @throws RunAlreadyStarted
     */
    public function participate(Run $run): RunParticipation
    {
        if (isset($this->participations[(string)$run->getId()])) {
            throw RunAlreadyParticipated::forRun($run, $this);
        }

        if ($run->getStartAt() < new \DateTime()) {
            throw RunAlreadyStarted::forRun($run);
        }

        $runParticipation = new RunParticipation($run, $this->getId());
        $this->participations[] = $runParticipation;

        return $runParticipation;
    }

    /**
     * @throws RunNotParticipated
     * @throws RunResultAlreadySaved
     * @throws RunResultExpired
     * @throws TimeLimitReached
     */
    public function result(Run $run, int $time): RunResult
    {
        if (!isset($this->participations[(string)$run->getId()])) {
            throw RunNotParticipated::forRun($run, $this);
        }

        if ($run->getStartAt()->diff(new \DateTime())->d > self::RUN_RESULT_EXPIRY_DAYS) {
            throw RunResultExpired::forRun($run, $this);
        }

        if ($time > $run->getTimeLimit()) {
            throw TimeLimitReached::forRun($run, $this);
        }

        if (isset($this->results[(string)$run->getId()])) {
            throw RunResultAlreadySaved::forRun($run, $this);
        }

        $runResult = new RunResult($run, $this->getId(), $time);
        $this->results[(string)$run->getId()] = $runResult;

        return $runResult;
    }
}

……及其等效的教義,

<?php

declare(strict_types = 1);

namespace Infrastructure\Framework\Entity;

use Doctrine\Common\Collections\ArrayCollection;
use Doctrine\ORM\Mapping as ORM;

/**
 * @ORM\Entity
 * @ORM\Table(name="runners")
 */
final class Runner extends User
{
    /**
     * @var RunParticipation[]
     * @ORM\OneToMany(targetEntity="RunParticipation", mappedBy="runner")
     */
    private $participations;

    /**
     * @var RunResult[]
     * @ORM\OneToMany(targetEntity="RunResult", mappedBy="runner")
     */
    private $results;

    public function __construct(
        string $id,
        string $email,
        string $password,
        array $participations = [],
        array $results = []
    ) {
        $this->participations = new ArrayCollection($participations);
        $this->results = new ArrayCollection($results);

        parent::__construct(
            $id,
            $email,
            $password
        );
    }

    /**
     * @return RunParticipation[]
     */
    public function getParticipations(): array
    {
        return $this->participations->toArray();
    }

    /**
     * @return RunResult[]
     */
    public function getResults(): array
    {
        return $this->results->toArray();
    }
}

與相應的變壓器。

<?php

declare(strict_types = 1);

namespace Infrastructure\Doctrine\Transformer;

use Infrastructure\Framework\Entity\Runner as Entity;
use Domain\Model\Runner as Domain;

class RunnerTransformer
{
    private $runParticipationTransformer;

    private $runResultTransformer;

    public function __construct(
        RunParticipationTransformer $runParticipationTransformer,
        RunResultTransformer $runResultTransformer
    ) {
        $this->runParticipationTransformer = $runParticipationTransformer;
        $this->runResultTransformer = $runResultTransformer;
    }

    /**
     * @throws \Common\Exception\InvalidIdException
     * @throws \Domain\Exception\InvalidRunType
     */
    public function entityToDomain(Entity $entity): Domain
    {
        $dbParticipations = $entity->getParticipations();
        $dbResults = $entity->getResults();
        $runnerId = \Common\Id::create($entity->getId());

        $participations = $this->runParticipationTransformer->entityToDomainMany($dbParticipations);
        $results = $this->runResultTransformer->entityToDomainMany($dbResults);

        return new Domain($runnerId, $entity->getEmail(), $entity->getPassword(), $participations, $results);
    }

    public function domainToEntity(Domain $domain): Entity
    {
        $run = $domain->getRun();
        $entity = new Entity();
        $entity->run_id = (string)$run->getId();
        $entity->runnerId = (string)$domain->getRunnerId();
        $entity->time = $domain->getTime();

        return $entity;
    }
}

不幸的是,Doctrine 的實體根本不是獨立的。為了保持類之間的關係,您必須使用ORM 工具中的ArrayCollection類。我敢打賭,您想對簡單的數組進行操作,並且絕對不想讓您的域被外部的東西弄得亂七八糟。因此,為什麼您需要單獨的實體和轉換器進行雙向翻譯。此外,Doctrine 可能會嘗試強制您執行特定的操作(例如存儲整個相關對象而不是其 id),並且轉換器可能會幫助您控制它。

在 RunParticipation 實體的示例中可以很容易地註意到這些問題:

<?php

declare(strict_types = 1);

namespace Infrastructure\Framework\Entity;

use Common\Id;
use Doctrine\ORM\Mapping as ORM;

/**
 * @ORM\Entity
 * @ORM\Table(name="run_participations")
 */
final class RunParticipation
{
    /**
     * @ORM\Id
     * @ORM\ManyToOne(targetEntity="Run", cascade={"persist"})
     */
    private $run;

    /**
     * @ORM\Id
     * @ORM\ManyToOne(targetEntity="Runner", inversedBy="participations", cascade={"persist"})
     */
    private $runner;

    public function __construct(Run $run, Runner $runner)
    {
        $this->run = $run;
        $this->runner = $runner;
    }

    public function getRun(): Run
    {
        return $this->run;
    }

    public function getRunnerId(): string
    {
        return $this->runner->getId();
    }
}

由於 ORM 的工作方式,我必須將整個 Runner 實體保留在內部。域模型只需要 runner id,而不需要整個對象。另一個問題是實體管理器需要跟踪這些實體並保留額外的信息,這樣它的“魔法”才能發揮作用。在從域轉換為實體時,這會導致很多問題。您必須從 Doctrine 的存儲庫中獲取所有實體,以便它可以跟踪它們。幸運的是,有辦法處理它。請注意,我在實體的兩個字段上都使用了@Id註釋,所以我們在這裡有一個複合主鍵。多虧了這一點,我們才免受兩列重複對的影響。

我們來看看變壓器:

<?php

declare(strict_types = 1);

namespace Infrastructure\Doctrine\Transformer;

use Doctrine\ORM\EntityManagerInterface;
use Infrastructure\Framework\Entity\Run;
use Infrastructure\Framework\Entity\Runner;
use Infrastructure\Framework\Entity\RunParticipation as Entity;
use Domain\Model\RunParticipation as Domain;

class RunParticipationTransformer
{
    private $runTransformer;

    private $entityManager;

    public function __construct(RunTransformer $runTransformer, EntityManagerInterface $entityManager)
    {
        $this->runTransformer = $runTransformer;
        $this->entityManager = $entityManager;
    }

    /**
     * @throws \Common\Exception\InvalidIdException
     * @throws \Domain\Exception\InvalidRunType
     */
    public function entityToDomain(Entity $entity): Domain
    {
        $dbRun = $entity->getRun();
        $runnerId = \Common\Id::create($entity->getRunnerId());
        $run = $this->runTransformer->entityToDomain($dbRun);

        return new Domain($run, $runnerId);
    }

    /**
     * @throws \Common\Exception\InvalidIdException
     * @throws \Domain\Exception\InvalidRunType
     */
    public function entityToDomainMany(array $entities): array
    {
        $domains = [];

        foreach ($entities as $entity) {
            $domains[$entity->getRun()->getId()] = $this->entityToDomain($entity);
        }

        return $domains;
    }

    public function domainToEntity(Domain $domain): Entity
    {
        $run = $this->entityManager->getReference(Run::class, (string)$domain->getRun()->getId());
        $runner = $this->entityManager->getReference(Runner::class, (string)$domain->getRunnerId());

        return new Entity(
            $run,
            $runner
        );
    }
}

我們的興趣中心是domainToEntity方法。如您所見,我使用來自實體管理器的引用而不是真實實體。我必須從 Doctrine 的存儲庫中獲取它們,以便 ORM 可以跟踪它們。除了將數據保存到數據庫之外,我們不會將它們用於任何其他用途,因此它們就是我們所需要的。除了轉換器與 Laravel 非常相似之外,它們只是處理表和列的不同對象表示。

從存儲的角度來看,最後要看到的是存儲庫。下面的示例顯示了讀/寫存儲庫。

跑步者存儲庫:

<?php

declare(strict_types = 1);

namespace Infrastructure\Framework\Repository;

use Common\Id;
use Doctrine\ORM\EntityManagerInterface;
use Domain\Exception\RunnerNotFound;
use Domain\Model\Runner;
use Infrastructure\Doctrine\Transformer\RunnerTransformer;

class RunnerRepository implements \Domain\Repository\RunnerRepository
{
    private $entityManager;

    private $entityRepository;

    private $runnerTransformer;

    public function __construct(EntityManagerInterface $entityManager, RunnerTransformer $runnerTransformer)
    {
        $this->entityManager = $entityManager;
        $this->entityRepository = $entityManager->getRepository(\Infrastructure\Framework\Entity\Runner::class);
        $this->runnerTransformer = $runnerTransformer;
    }

    /**
     * {@inheritdoc}
     */
    public function getById(Id $runnerId): Runner
    {
        $runner = $this->entityRepository->find((string)$runnerId);

        if (null === $runner) {
            throw RunnerNotFound::forId($runnerId);
        }

        return $this->runnerTransformer->entityToDomain($runner);
    }
}

運行參與存儲庫:

<?php

declare(strict_types = 1);

namespace Infrastructure\Framework\Repository;

use Doctrine\ORM\EntityManagerInterface;
use Domain\Model\RunParticipation;
use Infrastructure\Doctrine\Transformer\RunParticipationTransformer;
use Infrastructure\Framework\Entity\RunParticipation as RunParticipationEntity;

class RunParticipationRepository implements \Domain\Repository\RunParticipationRepository
{
    private $entityManager;

    private $entityRepository;

    private $runParticipationTransformer;

    public function __construct(EntityManagerInterface $entityManager, RunParticipationTransformer $runParticipationTransformer)
    {
        $this->entityManager = $entityManager;
        $this->entityRepository = $entityManager->getRepository(RunParticipationEntity::class);
        $this->runParticipationTransformer = $runParticipationTransformer;
    }

    /**
     * {@inheritdoc}
     */
    public function save(RunParticipation $runParticipation): void
    {
        $dbRunParticipation = $this->runParticipationTransformer->domainToEntity($runParticipation);

        $this->entityManager->persist($dbRunParticipation);
        $this->entityManager->flush();
    }
}

我如何從這個角度比較 Doctrine 和 Eloquent?一般來說,我更喜歡 Doctrine 的 ORM,因為在我看來,它具有更清晰的模型並且更易於使用。但是,在這種特定情況下,它的限制和規則比 Eloquent 更難遵循。

現在,與框架無關的應用程序需要一個框架

好吧,這部分與 Laravel 沒有太大區別。看看這個——我把整個 Symfony 代碼移到了基礎設施部分。我們再次在 CLI 中執行此操作以進行適當的比較。對於 Symfony ,phpleague/tactician-bundle包是可用的。

現在是時候檢查控制台命令類了:

<?php

declare(strict_types = 1);

namespace Infrastructure\Framework\Command;

use Application\Command\EnrollRunnerToRun;
use Application\Handler\EnrollRunnerToRunHandler;
use Common\Id;
use Domain\Exception\DomainException;
use League\Tactician\CommandBus;
use Symfony\Component\Console\Command\Command;
use Symfony\Component\Console\Input\InputArgument;
use Symfony\Component\Console\Input\InputInterface;
use Symfony\Component\Console\Output\OutputInterface;

class RunnerParticipateCommand extends Command
{
    private $commandBus;

    protected static $defaultName = 'runner:enroll';

    /**
     * @param $commandBus
     */
    public function __construct(CommandBus $commandBus)
    {
        $this->commandBus = $commandBus;

        parent::__construct();
    }

    protected function configure()
    {
        $this->setDescription('Enroll runner to run');

        $this->addArgument('runnerId', InputArgument::REQUIRED, 'The id of runner');
        $this->addArgument('runId', InputArgument::REQUIRED, 'The id of run');
    }

    protected function execute(InputInterface $input, OutputInterface $output)
    {
        $runnerId = Id::create($input->getArgument('runnerId'));
        $runId = Id::create($input->getArgument('runId'));
        $command = new EnrollRunnerToRun($runnerId, $runId);
        try {
            $this->commandBus->handle($command);
            $output->writeln('Runner enrolled');
        } catch (DomainException $e) {
            $output->writeln($e->getMessage());
        }
    }
}

如您所見,主要邏輯與 Laravel 中的完全相同——您獲取控制台參數,將其傳遞給命令並讓命令總線處理它。您可能注意到我不再將命令與此類中的總線連接起來。由於其類型提示功能,它由 Tactician 自動完成(命令根據處理程序的參數解析)。

您可以在下面找到允許我實現它的配置部分:

Application\Handler\:
    resource: '../src/Application/Handler/*'
    tags:
        - { name: tactician.handler, typehints: true }

最後的結論

在整篇文章中,我們根本沒有觸及我們的領域,只是使用了不同的工具。多虧了這一點,毫無疑問,主要邏輯可以正常工作(儘管一些測試不會受到傷害——以防萬一😉)。我知道我提供給您的代碼並不完美,還有改進的餘地(例如,Eloquent/Doctrine 的數據庫模式不同,儘管轉換不應該導致任何數據丟失),但我實現了我的目標:框架- 用完全不同的工具包裝的不可知應用程序

鏈接:https ://tsh.io/blog/how-create-framework-agnostic-application-in-php/

#php #框架

郝 玉华

郝 玉华

1657359780

如何在 Angular 中使用模板引用变量

在这篇文章中,您将学习如何在 Angular 中使用模板引用变量,通常称为“模板引用”。

在 Angular 中,组件有一个template属性,它保存元素和其他组件。模板引用变量是一种允许我们访问模板的一部分的功能。

这可以是一个元素、组件,也可以是一个指令。模板引用变量被巧妙地实现,可以多种方式使用。

第一个可能是简单地导出对元素的引用。在这里,我们可以将 a 附加#到 an<input>并提供一个变量名(因此模板引用“变量”):

<input type="text" #coffee>

您可以将此语法视为“导出”。我们正在导出对元素的引用。

这意味着我们现在可以访问该引用变量上的属性,就好像它是通过纯 JavaScript 返回给我们的一样(想想你会使用什么回来document.querySelector('input'),这就是我们在这里所拥有的):

<input type="text" #coffee>

<p>{{ coffee.value }}</p>

这将注销一个空字符串,coffee.value因为我们没有value. 我们的coffee变量直接给了我们一个HTMLInputElement.

为了让我们在键入时看到值,我们需要引入ngModel指令:

<input type="text" ngModel #coffee>

<p>{{ coffee.value }}</p>

试一试,然后在<input>:

https://stackblitz.com/edit/angular-ivy-t3y6jt?file=src%2Fapp%2Fapp.component.ts

所以这是模板引用的下一个重要功能。

让我们导出对我们的引用ngModel并更改#coffee返回我们的内容的上下文。

通过指定#coffee,我们隐含地让 Angular 决定要导出什么,因为除了绑定到元素之外,我们没有指定任何其他内容。

我们正在绑定ngModel,现在是我们的“一部分” <input>。让我们导出它:

<input type="text" ngModel #coffee="ngModel">

<p>Value: {{ coffee.value }}</p>
<p>Pristine: {{ coffee.pristine }}</p>
<p>Touched: {{ coffee.touched }}</p>

通过传递,#coffee="ngModel"我们显式地绑定了对跟踪ngModel指令的引用。

我们不再有HTMLInputElement. 我们有一个参考NgControl

你可以在这里查看NgControl的源代码,它扩展了NgControlAbstractControlDirective类。

我们为什么要看这个?因为它向您展示了您可以使用的所有属性,这正是我们不仅引用value而且pristine还引用的原因touched

在下面试一试,我们的模板引用变量正在镜像ngModel

我们可以进一步这样做并访问组件内部的模板引用,因此我们可以从内部访问属性和方法,class而不仅仅是template.

https://stackblitz.com/edit/angular-ivy-jwci6u?file=src%2Fapp%2Fapp.component.ts

这是通过使用装饰器TemplateRefElementRef@ViewChild装饰器一起使用来实现的。阅读上面关于如何做和更深入的工作的文章,但基本上它看起来像这样:

@Component({...})
export class AppComponent {
  @ViewChild('username') input: ElementRef<HTMLInputElement>;
}

这是对模板引用的一个很好的介绍,我希望它能让您更深入地了解如何使用它们、何时何地使用它们。不仅如此,当你声明一个模板引用以及如何导出对诸如指令之类的东西的引用时会发生什么。

快乐复习! 

来源:https ://ultimatecourses.com/blog/angular-template-reference-variables

#angular 

田辺  亮介

田辺 亮介

1661748026

如何在 Java 中使用長變量優化循環

OpenJDK中的即時 (JIT) 編譯器通過許多優化來提高Java性能,尤其是在循環中。直到最近,許多優化僅在循環索引是變量時才有效。本文展示瞭如何升級HotSpot 虛擬機以添加相同的變量優化。本文特別介紹了越界檢查(也稱為範圍檢查)。intlong

為什麼為長變量添加優化

Java 以及許多其他現代語言的重要承諾之一是捕獲越界錯誤,例如當您錯誤地結束循環時 atarray.length而不是 at array.length-1。HotSpot 虛擬機盡可能消除範圍檢查以優化性能。正如在前一篇文章中所討論的,當編譯器不確定索引是否會保持在數組的範圍內時,JIT 編譯器會啟用範圍檢查。

JIT 編譯器執行範圍檢查如下:

for (int i = start; i < stop; i += stride)) {
  if (scale * i + offset >=u array.length) { // range check
    deoptimize();
  }
  // access to element scale * i + offset of array
}

在前面的代碼中,>=u是一個無符號比較,該deoptimize()函數導致線程在拋出越界異常的解釋器中繼續執行。

在最近的 OpenJDK 版本之前,範圍檢查的優化只有在循環變量iint. 對於 a long,將始終執行範圍檢查。許多其他優化也無法使用。

將循環優化限制在int索引的原因是它們被認為是唯一常見的值得特殊處理的。一個原因是循環通常會遍歷數組。因為 Java 數組的大小是 32 位整數,所以int很自然地選擇了循環變量。

不過,用法正在演變。巴拿馬項目 為開發人員提供了一種更好的方式來訪問堆外內存區域。內存中的偏移量是 64 位,因此使用該 API 遍歷內存的循環傾向於使用long循環變量。

long最初對計數循環缺乏適當的優化是巴拿馬項目的一個痛點,它在庫代碼中實施了變通方法來檢測偏移量適合 32 位整數的情況。在這種情況下,項目包含 JIT 可以更好地優化的特殊代碼。但是,一旦本文中介紹的改進推出,這些變通方法就變得不必要並且可以刪除。最終結果是簡化的庫代碼具有更好的整體性能。

需要優化的迴路形狀如下:

for (long i = start; i < stop; i += stride)) {
  if (scale * i + offset >=u length) { // range check
    deoptimize();
  }
  // access to memory at offset scale * i + offset
}

本文中的討論假設一個向上的循環(即增加的索引),以及一個正的scale. 該項目已經概括了優化以適應遞減循環和負循環scale,但我們不會包括這些情況,因為它們會使討論過於復雜。

識別和優化長計數循環

教編譯器使用long循環變量而不是循環變量來識別新的循環形狀int是相當簡單的,但不足以啟用現有的優化,例如範圍檢查消除、循環展開和向量化。這些優化過程也必須適應在long計數循環上運行。這樣做並不是包含long變量的簡單問題,因為某些優化必須處理整數溢出。int它們通過將一些 32 位整數值提升為 64 位整數來防止變量溢出。在 64 位整數上支持這些相同的優化需要升級到編譯器缺乏支持的下一個更大的整數類型(可能是 128 位)。

有沒有辦法升級現有的優化long而不需要重寫這些優化並招致引入錯誤的高風險?

我們採用的解決方案首先將long計數循環轉換為具有int計數內循環的嵌套循環。前面的例子大致轉換為:

for (long i = start; i < stop;) {
  int j;
  for (j = 0; j < min(stop - i, max_int); j += (int)stride) {
    if (scale * (j + i) + offset >=u length) { // range check
      deoptimize();
    }
    // access to memory at offset scale * (j+i) + offset
  }
  i += j;
}

這裡,max_int表示最大的有符號 32 位整數。內部循環的循環變量也是一個 32 位整數。該內部循環具有計數循環的形狀,因此它受制於現有的計數循環優化,例如展開。添加一個額外的循環有一些開銷,但如果原始循環執行大量迭代,大部分時間應該花在內部循環中,並且設置它的成本應該可以忽略不計。

請注意,此轉換僅對stride適合 32 位整數的值有效,並且如果stride是相對較小的 32 位整數,則主要是有效的。(否則,內部循環只執行少量迭代,嵌套循環的開銷更大。)

這種額外的轉換具有對long循環計數器啟用多個現有循環優化的好處。但是轉換後的循環仍然沒有觸發一項重要的優化:消除範圍檢查。實際上,前一個循環嵌套中的範圍檢查仍然用 64 位整數表示:j+ilength. 下一節討論我們如何確保範圍檢查具有正確的形狀,以便編譯器識別它並啟用範圍檢查優化。

用於範圍檢查的新 API 點

消除範圍檢查的先決條件是確保循環具有編譯器可以正確優化的形狀。特別是,循環中的範圍檢查必須遵循如下規範,如果範圍檢查失敗,則進行無符號比較和去優化:

if (scale * i + offset >=u length) {
  deoptimize();
}

Java 中的數組訪問包括範圍檢查,因此編譯器可以自由生成最適合優化的模式。在巴拿馬項目的內存訪問 API 中,範圍檢查沒有內置到語言中。因此,必須由 API 以類似於以下的代碼顯式執行檢查:

long o = scale * i + offset;
if (o >= length || o < 0) {
  throw new SomeException();
}

然後,JIT 編譯器必須將此代碼模式識別為範圍檢查,這將很複雜,難以可靠地執行。畢竟,編寫此邏輯的方法不止一種。

我們有一個更強大的解決方案,它涉及使用新的 API 點擴展核心 Java 庫以進行範圍檢查:

long o = java.util.Objects.checkIndex(scale * i + offset, length);

參數的checkIndex()調用已經存在int

我們已經讓 JIT 編譯器知道新的 API 函數。編譯器可以使用它自己的實現,checkIndex()只要它的行為在外部調用者看來與 Java 實現相同。

用編譯器提供的函數替換函數的標準實現的技術稱為編譯器內在函數。可以精心設計該實現以進行優化。

從JDK 16 開始,新checkIndex()函數和相應的底層內在函數可用。請注意,新功能不限於內存訪問 API。它提供了一種執行範圍檢查的可靠方法,可以由虛擬機很好地優化,使該功能成為所有開發人員對核心庫的寶貴補充。

優化遠程檢查

到目前為止,我們已經討論了循環的轉換,以使其適合現有的優化。本著同樣的精神,本節討論如何轉換範圍檢查以觸發現有的long索引範圍檢查優化。我們需要將嵌套循環重塑為:

for (long i = start; i < stop;) {
  int j;
  int scale' = ..;
  int offset' = ..;
  int length' = ..;
  for (j = 0; j < min(stop - i, max_int); j += (int)stride) {
    if (scale' * j + offset' >=u length') {
      deoptimize();
    }
    // access to memory at offset scale * (j+i) + offset
  }
  i += j;
}

, scale',offset'length'變量以撇號 ( ') 結尾,表示從另一個變量派生或與另一個變量相關的變量。這些變量是 32 位整數,在內循環中是不變的。因為範圍檢查表示為對內部循環的循環變量進行操作的 32 位比較,內部循環本身就是一個具有 32 位索引的循環,因此會觸發現有的優化。

例如,假設循環預測優化了這個循環嵌套,結果大概是:

for (long i = start; i < stop;) {
  int j;
  int scale' = ..;
  int offset' = ..;
  int length' = ..;
  if (scale' * 0 + offset' >=u length') {
    deoptimize();
  }
  if (scale' * jmax + offset' >=u length') {
    deoptimize();
  }
  for (j = 0; j < min(stop - i, max_int); j += (int)stride) {
    // access to memory at offset scale * (j+i) + offset
  }
  i += j;
}

jmaxj是特定迭代的內部循環中的最大值i

假設,正如我們之前所做的那樣,內部循環運行大量迭代,範圍檢查基本上是免費的。

scale'offset'length'必須是初始範圍檢查的變量scaleoffsetlength和的導數。i它們都是 64 位整數。讓我們看看如何計算它們。

scale'可以設置為scale,但前提是它適合 32 位整數。否則,無法轉換範圍檢查。這裡的另一個棘手問題是scale' * j可能會超出int範圍。解決該問題的一種簡單方法是調整內部循環的邊界,以便永遠不會發生溢出,例如:

for (long i = start; i < stop;) {
  int j;
  int scale' = scale;
  int offset' = ..;
  int length' = ..;
  for (j = 0; j < min(stop - i, max_int / scale); j += (int)stride) {
    if (scale' * j + offset' >=u length') {
      deoptimize();
    }
    // access to memory at offset scale * (j+i) + offset
  }
  i += j;
}

如果scale結果是一個相對較大的 32 位整數,則內部循環的迭代次數很少,這種轉換不太可能得到回報。巴拿馬項目預計規模較小scale。通常,這個變量是一些基本數據類型的大小。

讓我們將外部循環j的某些迭代的值範圍記為。然後在範圍內(請記住,此討論假定為正)。我們稱之為區間。然後可以用and來表達and 。在最簡單的情況下(沒有溢出),我們設置:i[0, jmax]scale * (i + j) + offset[scale * i + offset, scale * (i + jmax) + offset]scale[range_min, range_max]offset'length'range_minrange_maxrange_min >= 0

  • offset' = 0
  • length' = max(range_min, min(length, range_max+1)) - range_min

讓我們看看為什麼它按預期工作——也就是說,為什麼j當初始範圍檢查(用 表示)成功時轉換的範圍檢查(用 表示i)成功,而在初始檢查失敗時失敗。

我們知道j屬於[0, jmax]. 然後range有值scale * (i + j) + offsetrange'有值scale * j + offset'。使用我們之前定義的變量range是 in[range_min, range_max]range = range' + range_min。的各種值會發生什麼length

當長度大於 range_max 時

在這種情況下,範圍檢查總是成功的。length'range_max+1 - range_min。範圍檢查變為:

range' <u range_max+1 - range_min

此表達式始終為真,因為rangeis within[range_min, range_max]和 insiderange'也是如此[0, range_max - range_min]

當長度小於 range_min 時

在這種情況下,範圍檢查總是失敗。length'0。範圍檢查變為range'<u 0,始終為假(無符號值不能為負)。

當長度在 [range_min, range_max]

如果length在 中[range_min, range_max],則範圍檢查有時會成功,有時會失敗。length'length - range_min。範圍檢查變為:

range' <u length - range_min

這相當於:

0 <= range' < length - range_min

或者

range_min <= range' + range_min < length

或者

range_min <= range < length

複製片段

因為我們假設range_min為正數,所以這與range <u length轉換前的範圍檢查相同。

length'必須是 32 位整數,但根據 64 位值計算。但是請注意,這range_max - range_min適合 32 位整數,因為循環邊界需要 scale * j 適合 32 位整數。這反過來又保證了length'可以安全地存儲為 32 位值。

廣泛的優化可容納更多的 Java 循環

本文介紹了 OpenJDK HotSpot 虛擬機中最近的優化,這些優化支持帶有long循環變量的循環,特別關注對其進行的範圍檢查。我概述了開發人員可以期望得到適當優化的代碼模式。本文還展示了新的 API 如何需要特殊的虛擬機支持,以及整個 Java 平台如何發展以滿足不斷變化的使用需求。最後,我展示了使用long索引重塑循環如何實現一系列優化。

鏈接:https ://developers.redhat.com/articles/2022/08/25/optimize-loops-long-variables-java#extensive_optimizations_accommodate_more_java_loops

#java