my-com/laravel-conditional-actions

v1.0.1 2020-02-13 15:33 UTC

README

Build Status

此包允许通过API配置业务逻辑,而无需更改您的代码。当您不知道特定条件,因为它们由您的经理/用户等动态定义时,这非常有用。

如何使用

代码库提供预定义的条件、操作、目标以及API,以便将它们组合到业务逻辑中以供最终用户使用。对象

  • Target - 为条件和操作提供所有必要的数据;
  • State - 键值对。操作应在应用时更新状态;
  • Condition - 条件具有 check 方法,它返回成功或不成功(布尔值);
  • Action - 操作具有 apply 方法,它更改 State 或执行任何其他操作,并返回更改后的 State

生命周期

  • Target 创建一个 State 对象;
  • Target 获取所有相关活动的 Condition,按优先级排序并运行每个条件的检查;
  • 对于成功的 ConditionCondition 获取所有相关的操作并将它们应用到 State 上;
  • Action 返回更改后的 State,它用于下一个条件或操作;
  • 检查所有 Condition 后,Target 将新的 State 传递给 applyState 方法。您可以根据需要使用其状态。

入门

例如,您有一个玩具店。您的营销部门针对某些玩具进行促销。如果用户在过去购买过玩具或今天是他的生日,"芭比娃娃" 应该有10%的折扣。促销从2019/05/01 00:00开始,到2019/05/01 23:59结束。

您应该创建

条件

  • 用户过去购买过玩具(《HasPaidToysCondition》)
  • 今天是他的生日(《TodayIsBirthdayCondition》)

操作

  • "芭比娃娃" 应该有10%的折扣(《DiscountAction》)

对于时间限制(促销从2019/05/01 00:00开始,到2019/05/01 23:59结束),您可以使用字段 starts_atends_at

两个条件都应该成功。您可以使用包中的 AllOfCondition 条件。

营销部门可以用来进行促销,而无需更改您的代码。

促销的最终方案

■ AllOfCondition (condition)
│ # fields: ['id' => 1, 'starts_at' => '2019-05-01 00:00:00', 'ends_at' => '2019-05-01 23:59:59']
│    ║
│    ╚═» ░ DiscountAction (action)
│          # fields: ['parameters' => ['discount' => 10]]
│
├─── ■ TodayIsBirthdayCondition (condition)
│      # fields: ['parent_id' => 1]
│
└─── ■ HasPaidToysCondition (condition)
       # fields: ['parent_id' => 1, 'parameters' => ['toy_id' => 5]]

让我们进入实施!

安装包

composer require my-com/laravel-conditional-actions

Laravel

对于版本 < 5.5

将包服务提供者添加到 config/app.php

return [
    // ...
    'providers' => [
        // ...
        ConditionalActions\ConditionalActionsServiceProvider::class,
    ],
    // ...

对于 laravel >= 5.5

Laravel 5.5 使用包自动发现,因此无需手动添加 ServiceProvider

Lumen

app.php 中注册服务提供者和配置

$app->configure('conditional-actions');
$app->register(ConditionalActions\ConditionalActionsServiceProvider::class);

添加迁移

php artisan ca:tables
php artisan migrate
php artisan vendor:publish --provider="ConditionalActions\ConditionalActionsServiceProvider"

命令选项

Description:
  Create a migration for the conditional actions database tables

Usage:
  ca:tables [options]

Options:
      --migrations-path[=MIGRATIONS-PATH]  Path to migrations directory (relative to framework base path) [default: "database/migrations"]

实现 Target

Target 是一个对象,它为条件和操作提供所有必要的数据。它也可以是一个 eloquent 模型。

由于 Toy 是条件操作的实体,它应该使用 EloquentTarget 特性(特性具有关系和一些获取模型条件的方法)

class ToysPriceTarget implements TargetContract
{
    use RunsConditionalActions;

    /** @var Toy */
    public $toy;

    /** @var User */
    public $user;

    public $finalPrice;

    public function __construct(Toy $toy, User $user)
    {
        $this->toy = $toy;
        $this->user = $user;
    }

    /**
     * Gets state from target.
     *
     * @return StateContract
     */
    public function getInitialState(): StateContract
    {
        return $this->newState([
            'price' => $this->toy->price,
        ]);
    }

    /**
     * Sets the state to the target.
     *
     * @param StateContract $state
     */
    public function applyState(StateContract $state): void
    {
        $this->finalPrice = $state->getAttribute('price');
    }

    /**
     * Gets root target conditions.
     *
     * @return iterable|ConditionContract[]
     */
    public function getRootConditions(): iterable
    {
        return $this->toy->getRootConditions();
    }

    /**
     * Gets children target conditions.
     *
     * @param int $parentId
     *
     * @return iterable|ConditionContract[]
     */
    public function getChildrenConditions(int $parentId): iterable
    {
        return $this->toy->getChildrenConditions($parentId);
    }
}

实现条件

每个条件都应该实现 ConditionalActions\Contracts\ConditionContract 接口。该包有一个基类 ConditionalActions\Entities\Conditions\BaseCondition,其中包含所有接口方法,除了 check 方法。

class HasPaidToysCondition extends BaseCondition
{
    /** @var ToysService */
    private $toysService;

    // You can use dependency injection in constructor
    public function __construct(ToysService $toysService)
    {
        $this->toysService = $toysService;
    }

    /**
     * Runs condition check.
     *
     * @param TargetContract $target
     * @param StateContract $state
     *
     * @return bool
     */
    public function check(TargetContract $target, StateContract $state): bool
    {
        $toyId = $this->parameters['toy_id'] ?? null;

        if (!($target instanceof ToysPriceTarget) || is_null($toyId)) {
            return false;
        }

        return $this->toysService->hasPaidToy($target->user, $toyId);
    }
}
class TodayIsBirthdayCondition extends BaseCondition
{
    /** @var ToysService */
    private $toysService;

    // You can use dependency injection in constructor
    public function __construct(ToysService $toysService)
    {
        $this->toysService = $toysService;
    }

    /**
     * Runs condition check.
     *
     * @param TargetContract $target
     * @param StateContract $state
     *
     * @return bool
     */
    public function check(TargetContract $target, StateContract $state): bool
    {
        if (!($target instanceof ToysPriceTarget)) {
            return false;
        }

        return Carbon::now()->isSameDay($target->user->birthday);
    }
}

实现操作

每个条件都应该实现 ConditionalActions\Contracts\ActionContract 接口。该包有一个基类 ConditionalActions\Entities\Actions\BaseAction,其中包含所有接口方法,除了 apply 方法。

class DiscountAction extends BaseAction
{
    /**
     * Applies action to the state and returns a new state.
     *
     * @param StateContract $state
     *
     * @return StateContract
     */
    public function apply(StateContract $state): StateContract
    {
        $discount = $this->parameters['discount'] ?? 0;
        $currentPrice = $state->getAttribute('price');
        $state->setAttribute('price', $currentPrice - $currentPrice / 100 * $discount);

        return $state;
    }
}

将条件添加到配置 config/conditional-actions.php

return [
    'conditions' => [
        'AllOfCondition' => ConditionalActions\Entities\Conditions\AllOfCondition::class,
        'OneOfCondition' => ConditionalActions\Entities\Conditions\OneOfCondition::class,
        'TrueCondition' => ConditionalActions\Entities\Conditions\TrueCondition::class,

        'CurrentTimeCondition' => App\ConditionalActions\Conditions\CurrentTimeCondition::class,
        'HasPaidToysCondition' => App\ConditionalActions\Conditions\HasPaidToysCondition::class,
        'TodayIsBirthdayCondition' => App\ConditionalActions\Conditions\TodayIsBirthdayCondition::class,
    ],
    'actions' => [
        'UpdateStateAttributeAction' => ConditionalActions\Entities\Actions\UpdateStateAttributeAction::class,

        'DiscountAction' => App\ConditionalActions\Actions\DiscountAction::class,
    ],
    'use_logger' => env('APP_DEBUG', false),
];

Toy 模型实现 API 以添加条件和操作

您可以使用 eloquent 模型或任何其他对象将业务逻辑放入外部存储。

该包为条件和操作提供基本的 CRUD。您可以选择启用它

use ConditionalActions\ConditionalActions;
use Illuminate\Support\ServiceProvider;

class RouteServiceProvider extends ServiceProvider
{
    // ...
    public function register()
    {
        ConditionalActions::routes();
    }
}

或者您可以实现自己的API。示例:

# This example is not an API. You can create API as you needed.

/** @var Toy $toy */
$toy = Toy::find(10);

/** @var Condition $allOf */
$allOf = $toy->conditions()->create([
    'name' => 'AllOfCondition',
    'starts_at' => '2019-05-01 00:00:00',
    'ends_at' => '2019-05-01 23:59:59',
]);

$allOf->actions()->create([
    'name' => 'DiscountAction',
    'parameters' => ['discount' => 10],
]);

$todayIsBirthday = $allOf->childrenConditions()->make([
    'name' => 'TodayIsBirthdayCondition',
]);

$hasPaidToy = $allOf->childrenConditions()->make([
    'name' => 'HasPaidToysCondition',
    'parameters' => ['toy_id' => 5],
]);

$toy->conditions()->saveMany([$allOf, $hasPaidToy, $todayIsBirthday]);

运行条件操作

$toy = Toy::find(10);

// Create a target instance
$target = new ToysPriceTarget(Auth::user(), $toy);
/*
 * Run conditional actions.
 * This method will iterate over all its conditions stored in database and apply actions related to succeed conditions
 */
$newState = $target->runConditionalActions();
dump($newState->getAttribute('price'));

备注:

该包包括条件和操作

  • 条件 AllOfCondition - 当所有子条件都成功时成功。所有子操作都将包含在父条件 AllOfCondition 中;
  • 条件 OneOfCondition - 当任何子条件成功时成功。第一个成功条件的所有子操作将包含在父条件 OneOfCondition 中;
  • 条件 TrueCondition - 总是成功;
  • 操作 UpdateStateAttributeAction - 更新状态中的属性值。

条件和操作都有字段

  • priority - 执行优先级;
  • 可空 starts_atends_at - 在特定时间段启用条件或操作;
  • parameters - 条件或操作的参数;
  • is_inverted - 决定条件结果是否应该被反转。