├── .gitignore
├── tests
├── Helpers
│ └── Dummy
│ │ ├── CanBeFired.php
│ │ ├── DummyEloquentModel.php
│ │ ├── DummyFailedCondition.php
│ │ ├── DummyValidationDataTarget.php
│ │ ├── DummySucceedCondition.php
│ │ ├── DummyAction.php
│ │ ├── DummyCondition.php
│ │ ├── DummyTestHelper.php
│ │ ├── DummyEloquentTarget.php
│ │ └── DummyTarget.php
├── database
│ ├── factories
│ │ ├── DummyEloquentTargetFactory.php
│ │ ├── ConditionFactory.php
│ │ └── ConditionActionFactory.php
│ └── migrations
│ │ ├── 2019_02_20_123456_create_dummy_target_table.php
│ │ ├── 2019_02_19_110944_create_condition_actions_table.php
│ │ └── 2019_02_19_110943_create_conditions_table.php
├── EloquentConditionalActionsTestCase.php
├── helpers.php
├── Unit
│ ├── Entities
│ │ ├── Actions
│ │ │ └── UpdateStateAttributeActionTest.php
│ │ └── Conditions
│ │ │ ├── HasChildrenConditionsTestCase.php
│ │ │ ├── ValidationConditionTest.php
│ │ │ ├── OneOfConditionTest.php
│ │ │ └── AllOfConditionTest.php
│ └── ConditionActionManagerTest.php
├── ConditionalActionsTestCase.php
└── Feature
│ ├── ConditionalActionManagerTest.php
│ ├── Entities
│ └── Eloquent
│ │ ├── ConditionActionTest.php
│ │ └── ConditionTest.php
│ ├── Repositories
│ ├── EloquentConditionRepositoryTest.php
│ └── EloquentActionRepositoryTest.php
│ ├── Traits
│ └── EloquentTargetTest.php
│ └── Http
│ └── Conditions
│ ├── ConditionsControllerTest.php
│ └── ActionsControllerTest.php
├── src
├── Traits
│ ├── UsesValidation.php
│ ├── RunsConditionalActions.php
│ └── EloquentTarget.php
├── ConditionalActionException.php
├── Contracts
│ ├── TargetProviders
│ │ └── ProvidesValidationData.php
│ ├── StateContract.php
│ ├── Repositories
│ │ ├── ActionRepository.php
│ │ └── ConditionRepository.php
│ ├── TargetContract.php
│ ├── ActionContract.php
│ └── ConditionContract.php
├── Exceptions
│ ├── ActionNotFoundException.php
│ ├── ConditionNotFoundException.php
│ └── BaseException.php
├── Http
│ ├── Presenters
│ │ ├── Presenter.php
│ │ ├── ResponseWrapper.php
│ │ ├── ActionPresenter.php
│ │ ├── ConditionPresenter.php
│ │ └── IterablePresenter.php
│ └── Controllers
│ │ ├── ActionsController.php
│ │ └── ConditionsController.php
├── Entities
│ ├── Actions
│ │ ├── UpdateStateAttributeAction.php
│ │ └── BaseAction.php
│ ├── Conditions
│ │ ├── TrueCondition.php
│ │ ├── OneOfCondition.php
│ │ ├── AllOfCondition.php
│ │ ├── ValidationCondition.php
│ │ └── BaseCondition.php
│ ├── Eloquent
│ │ ├── ValidatesModel.php
│ │ ├── Action.php
│ │ └── Condition.php
│ └── State.php
├── RouteRegistrar.php
├── Console
│ ├── stubs
│ │ ├── condition_actions.stub
│ │ └── conditions.stub
│ └── ConditionalActionsTable.php
├── ConditionalActionsServiceProvider.php
├── ConditionalActionManager.php
├── ConditionalActions.php
└── Repositories
│ ├── EloquentActionRepository.php
│ └── EloquentConditionRepository.php
├── .travis.yml
├── ci
└── install-composer.sh
├── config
└── conditional-actions.php
├── phpunit.xml
├── LICENSE
├── grumphp.yml
├── composer.json
├── docs
└── openapi.yaml
├── .php_cs.dist.php
└── readme.md
/.gitignore:
--------------------------------------------------------------------------------
1 | .idea
2 | vendor
3 | composer.lock
4 | .php_cs.cache
5 |
6 |
--------------------------------------------------------------------------------
/tests/Helpers/Dummy/CanBeFired.php:
--------------------------------------------------------------------------------
1 | define(DummyEloquentModel::class, function (Generator $faker) {
7 | return [
8 | 'name' => $faker->word,
9 | ];
10 | });
11 |
--------------------------------------------------------------------------------
/src/Http/Presenters/Presenter.php:
--------------------------------------------------------------------------------
1 | responseRoot, $data);
16 | }
17 | }
18 |
--------------------------------------------------------------------------------
/src/Contracts/StateContract.php:
--------------------------------------------------------------------------------
1 | parameters as $key => $value) {
12 | $state->setAttribute($key, $value);
13 | }
14 |
15 | return $state;
16 | }
17 | }
18 |
--------------------------------------------------------------------------------
/tests/database/factories/ConditionFactory.php:
--------------------------------------------------------------------------------
1 | define(Condition::class, function (Generator $faker) {
8 | return [
9 | 'name' => 'TrueCondition',
10 | ];
11 | });
12 |
13 | $factory->state(Condition::class, 'inactive', function (Generator $faker) {
14 | return [
15 | 'ends_at' => Carbon::minValue()->toDateTimeString(),
16 | ];
17 | });
18 |
--------------------------------------------------------------------------------
/tests/EloquentConditionalActionsTestCase.php:
--------------------------------------------------------------------------------
1 | withFactories(realpath(\dirname(__DIR__) . '/tests/database/factories'));
11 | $this->loadMigrationsFrom([
12 | '--database' => 'testing',
13 | '--path' => realpath(\dirname(__DIR__) . '/tests/database/migrations'),
14 | ]);
15 | }
16 | }
17 |
--------------------------------------------------------------------------------
/ci/install-composer.sh:
--------------------------------------------------------------------------------
1 | #!/bin/sh
2 |
3 | EXPECTED_SIGNATURE="$(wget -q -O - https://composer.github.io/installer.sig)"
4 | php -r "copy('https://getcomposer.org/installer', 'composer-setup.php');"
5 | ACTUAL_SIGNATURE="$(php -r "echo hash_file('sha384', 'composer-setup.php');")"
6 |
7 | if [ "$EXPECTED_SIGNATURE" != "$ACTUAL_SIGNATURE" ]
8 | then
9 | >&2 echo 'ERROR: Invalid installer signature'
10 | rm composer-setup.php
11 | exit 1
12 | fi
13 |
14 | php composer-setup.php --quiet
15 | RESULT=$?
16 | rm composer-setup.php
17 | exit ${RESULT}
18 |
--------------------------------------------------------------------------------
/src/Contracts/Repositories/ActionRepository.php:
--------------------------------------------------------------------------------
1 | define(Action::class, function (Generator $faker) {
9 | return [
10 | 'condition_id' => factory(Condition::class),
11 | 'name' => 'UpdateStateAttributeAction',
12 | ];
13 | });
14 |
15 | $factory->state(Condition::class, 'inactive', function (Generator $faker) {
16 | return [
17 | 'ends_at' => Carbon::minValue()->toDateTimeString(),
18 | ];
19 | });
20 |
--------------------------------------------------------------------------------
/tests/Helpers/Dummy/DummyFailedCondition.php:
--------------------------------------------------------------------------------
1 | isFired = true;
21 |
22 | return false;
23 | }
24 | }
25 |
--------------------------------------------------------------------------------
/tests/Helpers/Dummy/DummyValidationDataTarget.php:
--------------------------------------------------------------------------------
1 | validationData = $validationData;
16 | }
17 |
18 | public function getValidationData(): array
19 | {
20 | return $this->validationData;
21 | }
22 | }
23 |
--------------------------------------------------------------------------------
/tests/Helpers/Dummy/DummySucceedCondition.php:
--------------------------------------------------------------------------------
1 | isFired = true;
21 |
22 | return true;
23 | }
24 | }
25 |
--------------------------------------------------------------------------------
/config/conditional-actions.php:
--------------------------------------------------------------------------------
1 | [
5 | 'AllOfCondition' => ConditionalActions\Entities\Conditions\AllOfCondition::class,
6 | 'OneOfCondition' => ConditionalActions\Entities\Conditions\OneOfCondition::class,
7 | 'TrueCondition' => ConditionalActions\Entities\Conditions\TrueCondition::class,
8 | 'ValidationCondition' => ConditionalActions\Entities\Conditions\ValidationCondition::class,
9 | ],
10 | 'actions' => [
11 | 'UpdateStateAttributeAction' => ConditionalActions\Entities\Actions\UpdateStateAttributeAction::class,
12 | ],
13 | 'use_logger' => env('APP_DEBUG', false),
14 | ];
15 |
--------------------------------------------------------------------------------
/tests/helpers.php:
--------------------------------------------------------------------------------
1 | states($states)->create($attributes);
6 | }
7 | }
8 |
9 | if (!function_exists('createMany')) {
10 | function createMany($model, $keys = [], $attributesTable = [], $baseRow = [], string ...$states) {
11 | $collection = new \Illuminate\Database\Eloquent\Collection();
12 | foreach ($attributesTable as $attributes) {
13 | $collection->push(create($model, array_merge($baseRow, array_combine($keys, $attributes)), ...$states));
14 | }
15 |
16 | return $collection;
17 | }
18 | }
19 |
--------------------------------------------------------------------------------
/tests/Helpers/Dummy/DummyAction.php:
--------------------------------------------------------------------------------
1 | isFired = true;
22 |
23 | return $state;
24 | }
25 |
26 | public function isFired(): bool
27 | {
28 | return $this->isFired;
29 | }
30 | }
31 |
--------------------------------------------------------------------------------
/tests/database/migrations/2019_02_20_123456_create_dummy_target_table.php:
--------------------------------------------------------------------------------
1 | increments('id');
18 | $table->string('name');
19 | $table->timestamps();
20 | });
21 | }
22 |
23 | /**
24 | * Reverse the migrations.
25 | *
26 | * @return void
27 | */
28 | public function down()
29 | {
30 | Schema::dropIfExists('dummy_eloquent_models');
31 | }
32 | }
33 |
--------------------------------------------------------------------------------
/src/Entities/Eloquent/ValidatesModel.php:
--------------------------------------------------------------------------------
1 | attributesToArray(),
21 | static::validatingRules()
22 | );
23 |
24 | if ($validator->fails()) {
25 | throw new ValidationException($validator);
26 | }
27 | }
28 | });
29 | }
30 | }
31 |
--------------------------------------------------------------------------------
/src/Contracts/TargetContract.php:
--------------------------------------------------------------------------------
1 |
2 |
10 |
11 |
12 | ./tests/Unit
13 |
14 |
15 | ./tests/Feature
16 |
17 |
18 |
19 |
20 | ./src
21 |
22 |
23 |
24 |
25 |
26 |
27 |
--------------------------------------------------------------------------------
/src/Http/Presenters/ActionPresenter.php:
--------------------------------------------------------------------------------
1 | conditionalActions = $conditionalActions;
16 | }
17 |
18 | public function attributes(?ActionContract $action): array
19 | {
20 | return $action ? [
21 | 'id' => $action->getId(),
22 | 'name' => $this->conditionalActions->getActionName(\get_class($action)),
23 | 'parameters' => $action->getParameters(),
24 | 'priority' => $action->getPriority(),
25 | 'starts_at' => $action->getStartsAt(),
26 | 'ends_at' => $action->getEndsAt(),
27 | ] : null;
28 | }
29 | }
30 |
--------------------------------------------------------------------------------
/src/Exceptions/BaseException.php:
--------------------------------------------------------------------------------
1 | getCode()])
22 | ? $this->getCode()
23 | : Response::HTTP_INTERNAL_SERVER_ERROR;
24 |
25 | return response([
26 | 'error' => [
27 | 'message' => $this->getMessage(),
28 | ],
29 | ], $exceptionCode);
30 | }
31 | }
32 |
--------------------------------------------------------------------------------
/src/Entities/Conditions/OneOfCondition.php:
--------------------------------------------------------------------------------
1 | getChildrenConditions($this->id) as $condition) {
21 | $conditionResult = $condition->check($target, $state) !== $condition->isInverted();
22 |
23 | if ($conditionResult === $this->expectedResult()) {
24 | $this->prependActions(...$condition->getActions());
25 |
26 | return true;
27 | }
28 | }
29 |
30 | return false;
31 | }
32 | }
33 |
--------------------------------------------------------------------------------
/src/RouteRegistrar.php:
--------------------------------------------------------------------------------
1 | router = $router;
24 | }
25 |
26 | /**
27 | * Register routes for transient tokens, clients, and personal access tokens.
28 | */
29 | public function all()
30 | {
31 | $this->router->resource('conditions', 'ConditionsController')
32 | ->only('store', 'show', 'update', 'destroy')
33 | ->names('conditions');
34 |
35 | $this->router->resource('actions', 'ActionsController')
36 | ->only('store', 'show', 'update', 'destroy')
37 | ->names('actions');
38 | }
39 | }
40 |
--------------------------------------------------------------------------------
/src/Http/Presenters/ConditionPresenter.php:
--------------------------------------------------------------------------------
1 | conditionalActions = $conditionalActions;
16 | }
17 |
18 | public function attributes(?ConditionContract $condition): array
19 | {
20 | return $condition ? [
21 | 'id' => $condition->getId(),
22 | 'name' => $this->conditionalActions->getConditionName(\get_class($condition)),
23 | 'is_inverted' => $condition->isInverted(),
24 | 'parameters' => $condition->getParameters(),
25 | 'priority' => $condition->getPriority(),
26 | 'starts_at' => $condition->getStartsAt(),
27 | 'ends_at' => $condition->getEndsAt(),
28 | ] : null;
29 | }
30 | }
31 |
--------------------------------------------------------------------------------
/tests/Unit/Entities/Actions/UpdateStateAttributeActionTest.php:
--------------------------------------------------------------------------------
1 | 'first',
15 | 'two' => 'second',
16 | ]);
17 | /** @var UpdateStateAttributeAction $action */
18 | $action = \app(UpdateStateAttributeAction::class);
19 | $action->setParameters([
20 | 'one' => 'updated',
21 | 'four' => 'new attribute',
22 | ]);
23 |
24 | $newState = $action->apply($state);
25 |
26 | $this->assertEquals('updated', $newState->getAttribute('one'));
27 | $this->assertEquals('second', $newState->getAttribute('two'));
28 | $this->assertEquals('new attribute', $newState->getAttribute('four'));
29 | }
30 | }
31 |
--------------------------------------------------------------------------------
/src/Entities/Conditions/AllOfCondition.php:
--------------------------------------------------------------------------------
1 | getChildrenConditions($this->id) as $condition) {
23 | $conditionResult = $condition->check($target, $state) !== $condition->isInverted();
24 |
25 | if ($conditionResult !== $this->expectedResult()) {
26 | return false;
27 | }
28 | $actions = \array_merge($actions, \collect($condition->getActions())->toArray());
29 | }
30 |
31 | $this->prependActions(...$actions);
32 |
33 | return true;
34 | }
35 | }
36 |
--------------------------------------------------------------------------------
/src/Traits/RunsConditionalActions.php:
--------------------------------------------------------------------------------
1 | useLogger = \config('conditional-actions.use_logger');
20 |
21 | return new State($attributes);
22 | }
23 |
24 | public function useLogger(): self
25 | {
26 | $this->useLogger = true;
27 |
28 | return $this;
29 | }
30 |
31 | public function runConditionalActions(): StateContract
32 | {
33 | /** @var ConditionalActionManager $manager */
34 | $manager = \app(ConditionalActionManager::class);
35 | $manager->useLogger = $this->useLogger;
36 |
37 | return $manager->run($this, $this->getInitialState());
38 | }
39 | }
40 |
--------------------------------------------------------------------------------
/src/Console/stubs/condition_actions.stub:
--------------------------------------------------------------------------------
1 | increments('id');
18 | $table->unsignedInteger('condition_id');
19 | $table->string('name');
20 | $table->json('parameters')->nullable();
21 | $table->integer('priority')->default(0);
22 | $table->timestamp('starts_at')->nullable();
23 | $table->timestamp('ends_at')->nullable();
24 | $table->timestamps();
25 | $table->softDeletes();
26 | });
27 | }
28 |
29 | /**
30 | * Reverse the migrations.
31 | *
32 | * @return void
33 | */
34 | public function down()
35 | {
36 | Schema::dropIfExists('{{table}}');
37 | }
38 | }
39 |
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | MIT License
2 |
3 | Copyright (c) 2019 My.com B.V.
4 |
5 | Permission is hereby granted, free of charge, to any person obtaining a copy
6 | of this software and associated documentation files (the "Software"), to deal
7 | in the Software without restriction, including without limitation the rights
8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9 | copies of the Software, and to permit persons to whom the Software is
10 | furnished to do so, subject to the following conditions:
11 |
12 | The above copyright notice and this permission notice shall be included in all
13 | copies or substantial portions of the Software.
14 |
15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21 | SOFTWARE.
22 |
--------------------------------------------------------------------------------
/src/Entities/State.php:
--------------------------------------------------------------------------------
1 | setAttributes($attributes);
15 | }
16 |
17 | public function setAttributes(iterable $attributes): void
18 | {
19 | foreach ($attributes as $attribute => $value) {
20 | $this->attributes[$attribute] = $value;
21 | }
22 | }
23 |
24 | public function setAttribute(string $path, $value): void
25 | {
26 | Arr::set($this->attributes, $path, $value);
27 | }
28 |
29 | public function getAttribute(string $path, $default = null)
30 | {
31 | return Arr::get($this->attributes, $path, $default);
32 | }
33 |
34 | public function toArray(): array
35 | {
36 | return $this->attributes;
37 | }
38 |
39 | public function fromArray(array $array)
40 | {
41 | $this->setAttributes($array);
42 | }
43 | }
44 |
--------------------------------------------------------------------------------
/grumphp.yml:
--------------------------------------------------------------------------------
1 | parameters:
2 | git_dir: .
3 | bin_dir: vendor/bin
4 | tasks:
5 | composer:
6 | file: ./composer.json
7 | no_check_publish: true
8 |
9 | git_blacklist:
10 | keywords:
11 | - "die("
12 | - "var_dump("
13 | - " dd("
14 | - "dump("
15 | - "exit;"
16 | - "exit("
17 |
18 | phpcpd:
19 | directory: './src'
20 | fuzzy: true
21 | min_lines: 5
22 | min_tokens: 70
23 | triggered_by: ['php']
24 |
25 | securitychecker:
26 | lockfile: composer.lock
27 | format: ~
28 | end_point: ~
29 | timeout: ~
30 | run_always: false
31 |
32 | phpcsfixer2:
33 | allow_risky: true
34 | cache_file: '.php_cs.cache'
35 | config: '.php_cs.dist.php'
36 | rules: []
37 | using_cache: true
38 | config_contains_finder: true
39 | verbose: true
40 | diff: true
41 | triggered_by: ['php']
42 |
43 | phpmd:
44 | exclude: ['docs', 'tests', 'vendor']
45 | ruleset: ['codesize', 'controversial', 'naming']
46 | triggered_by: ['php']
47 |
48 | phpversion:
49 | project: '7.2'
50 |
--------------------------------------------------------------------------------
/tests/database/migrations/2019_02_19_110944_create_condition_actions_table.php:
--------------------------------------------------------------------------------
1 | increments('id');
18 | $table->unsignedInteger('condition_id');
19 | $table->string('name');
20 | $table->json('parameters')->nullable();
21 | $table->integer('priority')->default(0);
22 | $table->timestamp('starts_at')->nullable();
23 | $table->timestamp('ends_at')->nullable();
24 | $table->timestamps();
25 | $table->softDeletes();
26 | });
27 | }
28 |
29 | /**
30 | * Reverse the migrations.
31 | *
32 | * @return void
33 | */
34 | public function down()
35 | {
36 | Schema::dropIfExists('condition_actions');
37 | }
38 | }
39 |
--------------------------------------------------------------------------------
/src/Http/Presenters/IterablePresenter.php:
--------------------------------------------------------------------------------
1 | presenter = $presenter;
15 | }
16 |
17 | public function __call($name, $arguments)
18 | {
19 | if (!\method_exists($this->presenter, $name)) {
20 | throw new RuntimeException(
21 | \sprintf('Method %s not found in presenter %s', $name, \get_class($this->presenter))
22 | );
23 | }
24 |
25 | if (!\count($arguments)) {
26 | throw new RuntimeException('No arguments');
27 | }
28 |
29 | $items = \array_shift($arguments);
30 |
31 | if (!\is_iterable($items)) {
32 | throw new RuntimeException('Received items is not an iterable');
33 | }
34 |
35 | $response = [];
36 |
37 | foreach ($items as $i => $item) {
38 | $response[$i] = $this->presenter->{$name}($item);
39 | }
40 |
41 | return $response;
42 | }
43 | }
44 |
--------------------------------------------------------------------------------
/composer.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "my-com/laravel-conditional-actions",
3 | "authors": [
4 | {
5 | "name": "Aleksandr Paramonov",
6 | "email": "a.paramonov@corp.my.com"
7 | }
8 | ],
9 | "license": "MIT",
10 | "autoload": {
11 | "psr-4": {
12 | "ConditionalActions\\": "./src"
13 | }
14 | },
15 | "autoload-dev": {
16 | "psr-4": {
17 | "Tests\\": "./tests"
18 | },
19 | "files": ["./tests/helpers.php"]
20 | },
21 | "require": {
22 | "illuminate/support": "^6.0",
23 | "illuminate/database": "^6.0",
24 | "illuminate/config": "^6.0",
25 | "illuminate/http": "^6.0",
26 | "ext-json": "*"
27 | },
28 | "extra": {
29 | "laravel": {
30 | "providers": "ConditionalActions\\ConditionalActionsServiceProvider"
31 | }
32 | },
33 | "require-dev": {
34 | "orchestra/testbench": "^4.0",
35 | "mockery/mockery": "^1.2",
36 | "phpro/grumphp": "^0.15.0",
37 | "phpmd/phpmd": "^2.6",
38 | "friendsofphp/php-cs-fixer": "^2.14",
39 | "sebastian/phpcpd": "^4.1"
40 | }
41 | }
42 |
--------------------------------------------------------------------------------
/src/Entities/Conditions/ValidationCondition.php:
--------------------------------------------------------------------------------
1 | getValidationData(), $this->parameters ?? []);
33 |
34 | return !$validator->fails();
35 | }
36 | }
37 |
--------------------------------------------------------------------------------
/src/Console/stubs/conditions.stub:
--------------------------------------------------------------------------------
1 | increments('id');
18 | $table->nullableMorphs('target');
19 | $table->string('name');
20 | $table->json('parameters')->nullable();
21 | $table->boolean('is_inverted')->default(false);
22 | $table->integer('priority')->default(0);
23 | $table->unsignedInteger('parent_id')->nullable();
24 | $table->timestamp('starts_at')->nullable();
25 | $table->timestamp('ends_at')->nullable();
26 | $table->timestamps();
27 | $table->softDeletes();
28 | });
29 | }
30 |
31 | /**
32 | * Reverse the migrations.
33 | *
34 | * @return void
35 | */
36 | public function down()
37 | {
38 | Schema::dropIfExists('{{table}}');
39 | }
40 | }
41 |
--------------------------------------------------------------------------------
/tests/ConditionalActionsTestCase.php:
--------------------------------------------------------------------------------
1 | true,
23 | 'conditional-actions.conditions.DummySucceedCondition' => DummySucceedCondition::class,
24 | 'conditional-actions.conditions.DummyFailedCondition' => DummyFailedCondition::class,
25 | 'conditional-actions.actions.DummyAction' => DummyAction::class,
26 | ]);
27 |
28 | Carbon::serializeUsing(function (Carbon $carbon) {
29 | return $carbon->toIso8601String();
30 | });
31 | }
32 |
33 | protected function getPackageProviders($app)
34 | {
35 | return [ConditionalActionsServiceProvider::class];
36 | }
37 | }
38 |
--------------------------------------------------------------------------------
/tests/database/migrations/2019_02_19_110943_create_conditions_table.php:
--------------------------------------------------------------------------------
1 | increments('id');
18 | $table->nullableMorphs('target');
19 | $table->string('name');
20 | $table->json('parameters')->nullable();
21 | $table->boolean('is_inverted')->default(false);
22 | $table->integer('priority')->default(0);
23 | $table->unsignedInteger('parent_id')->nullable();
24 | $table->timestamp('starts_at')->nullable();
25 | $table->timestamp('ends_at')->nullable();
26 | $table->timestamps();
27 | $table->softDeletes();
28 | });
29 | }
30 |
31 | /**
32 | * Reverse the migrations.
33 | *
34 | * @return void
35 | */
36 | public function down()
37 | {
38 | Schema::dropIfExists('conditions');
39 | }
40 | }
41 |
--------------------------------------------------------------------------------
/tests/Helpers/Dummy/DummyCondition.php:
--------------------------------------------------------------------------------
1 | id = $id;
19 | $this->parentId = $parentId;
20 | }
21 |
22 | public static function withActions(int $id, ?int $parentId, ActionContract ...$actions): self
23 | {
24 | return \tap(new static($id, $parentId), function (self $condition) use ($actions) {
25 | $condition->setActions($actions);
26 | });
27 | }
28 |
29 | /**
30 | * @return int
31 | */
32 | public function getId(): int
33 | {
34 | return $this->id;
35 | }
36 |
37 | /**
38 | * @return int
39 | */
40 | public function getParentId(): ?int
41 | {
42 | return $this->parentId;
43 | }
44 |
45 | /**
46 | * @return bool
47 | */
48 | public function isFired(): bool
49 | {
50 | return $this->isFired;
51 | }
52 | }
53 |
--------------------------------------------------------------------------------
/src/ConditionalActionsServiceProvider.php:
--------------------------------------------------------------------------------
1 | mergeConfigFrom(
17 | __DIR__ . '/../config/conditional-actions.php',
18 | 'conditional-actions'
19 | );
20 |
21 | $this->publishes([
22 | __DIR__ . '/../config/conditional-actions.php' => \config_path('conditional-actions.php'),
23 | ]);
24 |
25 | $this->commands([
26 | ConditionalActionsTable::class,
27 | ]);
28 | }
29 |
30 | public function register()
31 | {
32 | $this->app->singleton(ConditionalActions::class);
33 | $this->app->bind(ActionRepository::class, EloquentActionRepository::class);
34 | $this->app->bind(ConditionRepository::class, EloquentConditionRepository::class);
35 | }
36 | }
37 |
--------------------------------------------------------------------------------
/tests/Helpers/Dummy/DummyTestHelper.php:
--------------------------------------------------------------------------------
1 | id, $parentId, ...$actions);
31 | }
32 |
33 | protected function failedCondition(?int $parentId = null, ActionContract ...$actions): DummyCondition
34 | {
35 | return DummyFailedCondition::withActions(++$this->id, $parentId, ...$actions);
36 | }
37 |
38 | protected function assertFired(CanBeFired ...$items): void
39 | {
40 | foreach ($items as $item) {
41 | Assert::assertTrue($item->isFired());
42 | }
43 | }
44 |
45 | protected function assertNotFired(CanBeFired ...$items): void
46 | {
47 | foreach ($items as $item) {
48 | Assert::assertFalse($item->isFired());
49 | }
50 | }
51 | }
52 |
--------------------------------------------------------------------------------
/tests/Unit/Entities/Conditions/HasChildrenConditionsTestCase.php:
--------------------------------------------------------------------------------
1 | target = new DummyTarget();
26 | $this->action = new DummyAction();
27 |
28 | $this->testCondition = $condition;
29 | $this->testCondition->setId(++$this->id);
30 | $this->testCondition->setActions([$this->action]);
31 |
32 | $this->target->addConditions($this->testCondition);
33 | }
34 |
35 | protected function succeedChildrenCondition(ActionContract ...$actions): DummyCondition
36 | {
37 | return $this->succeedCondition($this->testCondition->getId(), ...$actions);
38 | }
39 |
40 | protected function failedChildrenCondition(ActionContract ...$actions): DummyCondition
41 | {
42 | return $this->failedCondition($this->testCondition->getId(), ...$actions);
43 | }
44 | }
45 |
--------------------------------------------------------------------------------
/src/Traits/EloquentTarget.php:
--------------------------------------------------------------------------------
1 | morphMany(Condition::class, 'target');
21 | }
22 |
23 | /**
24 | * Gets target conditions.
25 | *
26 | * @return iterable|ConditionContract[]
27 | */
28 | public function getRootConditions(): iterable
29 | {
30 | return $this->getChildrenConditions(null);
31 | }
32 |
33 | /**
34 | * Gets children target conditions.
35 | *
36 | * @param int $parentId
37 | *
38 | * @return iterable|ConditionContract[]
39 | */
40 | public function getChildrenConditions(?int $parentId): iterable
41 | {
42 | return $this->conditions
43 | ->filter(function (Condition $condition) use ($parentId) {
44 | return $condition->parent_id === $parentId && $condition->isActive();
45 | })
46 | ->sortBy('priority')
47 | ->map(function (Condition $condition) {
48 | return $condition->toCondition();
49 | })
50 | ->values();
51 | }
52 | }
53 |
--------------------------------------------------------------------------------
/tests/Feature/ConditionalActionManagerTest.php:
--------------------------------------------------------------------------------
1 | 'AllOfCondition']);
19 | /** @var Condition $child */
20 | $child = \create(Condition::class, ['name' => 'TrueCondition', 'parent_id' => $allOf->id]);
21 |
22 | \create(Action::class, [
23 | 'name' => 'UpdateStateAttributeAction',
24 | 'condition_id' => $allOf->id,
25 | 'parameters' => ['value' => 10],
26 | ]);
27 |
28 | \create(Action::class, [
29 | 'name' => 'UpdateStateAttributeAction',
30 | 'condition_id' => $allOf->id,
31 | 'parameters' => ['value' => 20],
32 | ]);
33 |
34 | $model->conditions()->saveMany([$allOf, $child]);
35 |
36 | $target = new DummyEloquentTarget($model);
37 |
38 | $state = $target->runConditionalActions();
39 |
40 | $this->assertEquals(20, $state->getAttribute('value'));
41 | $this->assertEquals(['value' => 20], $target->state);
42 | }
43 | }
44 |
--------------------------------------------------------------------------------
/tests/Helpers/Dummy/DummyEloquentTarget.php:
--------------------------------------------------------------------------------
1 | model = $model;
22 | }
23 |
24 | /**
25 | * Gets state from target.
26 | *
27 | * @return StateContract
28 | */
29 | public function getInitialState(): StateContract
30 | {
31 | return $this->newState(['value' => 1]);
32 | }
33 |
34 | /**
35 | * Sets the state to the target.
36 | *
37 | * @param StateContract $state
38 | */
39 | public function applyState(StateContract $state): void
40 | {
41 | $this->state = $state->toArray();
42 | }
43 |
44 | /**
45 | * Gets root target conditions.
46 | *
47 | * @return iterable|ConditionContract[]
48 | */
49 | public function getRootConditions(): iterable
50 | {
51 | return $this->model->getRootConditions();
52 | }
53 |
54 | /**
55 | * Gets children target conditions.
56 | *
57 | * @param int $parentId
58 | *
59 | * @return iterable|ConditionContract[]
60 | */
61 | public function getChildrenConditions(int $parentId): iterable
62 | {
63 | return $this->model->getChildrenConditions($parentId);
64 | }
65 | }
66 |
--------------------------------------------------------------------------------
/src/ConditionalActionManager.php:
--------------------------------------------------------------------------------
1 | logger = $logger;
19 | }
20 |
21 | public function run(TargetContract $target, StateContract $state): StateContract
22 | {
23 | $this->log(\sprintf('Initial state: %s', \json_encode($state->toArray())));
24 |
25 | foreach ($target->getRootConditions() as $condition) {
26 | $conditionName = \sprintf(
27 | '%s "%s"',
28 | $condition->isInverted() ? 'Inverted condition' : 'Condition',
29 | \class_basename($condition)
30 | );
31 |
32 | if ($condition->check($target, $state) !== $condition->isInverted()) {
33 | $this->log("[OK]\t" . $conditionName);
34 |
35 | foreach ($condition->getActions() as $action) {
36 | $state = $action->apply($state);
37 |
38 | $this->log(\sprintf(' Apply action "%s"', \class_basename($action)));
39 | $this->log(\sprintf(' => New state: %s', \json_encode($state->toArray())));
40 | }
41 | } else {
42 | $this->log("[SKIP]\t" . $conditionName);
43 | }
44 | }
45 |
46 | $target->applyState($state);
47 | $this->log(\sprintf('Finish state: %s', \json_encode($state->toArray())));
48 |
49 | return $state;
50 | }
51 |
52 | protected function log(string $message)
53 | {
54 | if (!$this->useLogger) {
55 | return;
56 | }
57 |
58 | $this->logger->debug($message);
59 | }
60 | }
61 |
--------------------------------------------------------------------------------
/src/ConditionalActions.php:
--------------------------------------------------------------------------------
1 | conditionNames)) {
19 | return;
20 | }
21 |
22 | $this->conditionNames = \array_flip(\config('conditional-actions.conditions', []));
23 | $this->actionNames = \array_flip(\config('conditional-actions.actions', []));
24 | }
25 |
26 | public function getActionNames(): array
27 | {
28 | $this->loadConfig();
29 |
30 | return $this->actionNames;
31 | }
32 |
33 | public function getActionName(string $className): ?string
34 | {
35 | $this->loadConfig();
36 |
37 | return Arr::get($this->actionNames, $className);
38 | }
39 |
40 | public function getConditionNames(): array
41 | {
42 | $this->loadConfig();
43 |
44 | return $this->conditionNames;
45 | }
46 |
47 | public function getConditionName(string $className): ?string
48 | {
49 | $this->loadConfig();
50 |
51 | return Arr::get($this->conditionNames, $className);
52 | }
53 |
54 | public static function routes($callback = null, array $options = [])
55 | {
56 | $callback = $callback ?: function ($router) {
57 | $router->all();
58 | };
59 | $defaultOptions = [
60 | 'prefix' => 'api/v1/conditional-actions',
61 | 'namespace' => 'ConditionalActions\Http\Controllers',
62 | ];
63 |
64 | $options = \array_merge($defaultOptions, $options);
65 | Route::group($options, function ($router) use ($callback) {
66 | $callback(new RouteRegistrar($router));
67 | });
68 | }
69 | }
70 |
--------------------------------------------------------------------------------
/tests/Unit/Entities/Conditions/ValidationConditionTest.php:
--------------------------------------------------------------------------------
1 | condition = new ValidationCondition();
20 | }
21 |
22 | /**
23 | * @param array $validationData
24 | * @param array $conditionParams
25 | * @param bool $result
26 | *
27 | * @dataProvider provider_test_validation
28 | * @throws ConditionalActionException
29 | */
30 | public function test_validation(array $validationData, array $conditionParams, bool $result)
31 | {
32 | $this->condition->setParameters($conditionParams);
33 | $target = new DummyValidationDataTarget($validationData);
34 |
35 | $this->assertEquals($result, $this->condition->check($target, $target->getInitialState()));
36 | }
37 |
38 | public function provider_test_validation(): array
39 | {
40 | return [
41 | 'succeeded' => [
42 | ['foo' => ['bar' => 10]],
43 | ['foo.bar' => 'required|int|max:9'],
44 | false,
45 | ],
46 | 'failed' => [
47 | ['foo' => ['bar' => 10]],
48 | ['foo.bar' => 'required|int|max:11'],
49 | true,
50 | ],
51 | ];
52 | }
53 |
54 | /**
55 | * @throws ConditionalActionException
56 | */
57 | public function test_exception_when_provides_validation_data_contract_not_implemented()
58 | {
59 | $target = new DummyTarget();
60 |
61 | $this->expectException(ConditionalActionException::class);
62 | $this->expectExceptionCode(400);
63 |
64 | $this->condition->check($target, $target->getInitialState());
65 | }
66 | }
67 |
--------------------------------------------------------------------------------
/src/Repositories/EloquentActionRepository.php:
--------------------------------------------------------------------------------
1 | model = $model;
21 | }
22 |
23 | public function find(int $id): ActionContract
24 | {
25 | return \optional($this->model->newQuery()->find($id))->toAction();
26 | }
27 |
28 | public function store(array $attributes): ActionContract
29 | {
30 | return \optional($this->model->newQuery()->create($attributes))->toAction();
31 | }
32 |
33 | /**
34 | * @param int $id
35 | * @param array $attributes
36 | *
37 | * @throws ActionNotFoundException
38 | * @throws \Throwable
39 | *
40 | * @return ActionContract
41 | */
42 | public function update(int $id, array $attributes): ActionContract
43 | {
44 | /** @var Action $condition */
45 | $condition = $this->model->newQuery()->find($id);
46 |
47 | if (!$condition) {
48 | throw new ActionNotFoundException(\sprintf('Action %s not found', $id));
49 | }
50 |
51 | $condition->update($attributes);
52 |
53 | return $condition->toAction();
54 | }
55 |
56 | /**
57 | * @param int $id
58 | *
59 | * @throws ActionNotFoundException
60 | * @throws \Throwable
61 | *
62 | * @return ActionContract
63 | */
64 | public function destroy(int $id): ActionContract
65 | {
66 | /** @var Action $condition */
67 | $condition = $this->model->newQuery()->find($id);
68 |
69 | if (!$condition) {
70 | throw new ActionNotFoundException(\sprintf('Action %s not found', $id));
71 | }
72 |
73 | $condition->delete();
74 |
75 | return $condition->toAction();
76 | }
77 | }
78 |
--------------------------------------------------------------------------------
/src/Repositories/EloquentConditionRepository.php:
--------------------------------------------------------------------------------
1 | model = $model;
21 | }
22 |
23 | public function find(int $id): ConditionContract
24 | {
25 | return \optional($this->model->newQuery()->find($id))->toCondition();
26 | }
27 |
28 | public function store(array $attributes): ConditionContract
29 | {
30 | return \optional($this->model->newQuery()->create($attributes))->toCondition();
31 | }
32 |
33 | /**
34 | * @param int $id
35 | * @param array $attributes
36 | *
37 | * @throws ConditionNotFoundException
38 | * @throws \Throwable
39 | *
40 | * @return ConditionContract
41 | */
42 | public function update(int $id, array $attributes): ConditionContract
43 | {
44 | /** @var Condition $condition */
45 | $condition = $this->model->newQuery()->find($id);
46 |
47 | if (!$condition) {
48 | throw new ConditionNotFoundException(\sprintf('Condition %s not found', $id));
49 | }
50 |
51 | $condition->update($attributes);
52 |
53 | return $condition->toCondition();
54 | }
55 |
56 | /**
57 | * @param int $id
58 | *
59 | * @throws ConditionNotFoundException
60 | * @throws \Throwable
61 | *
62 | * @return ConditionContract
63 | */
64 | public function destroy(int $id): ConditionContract
65 | {
66 | /** @var Condition $condition */
67 | $condition = $this->model->newQuery()->find($id);
68 |
69 | if (!$condition) {
70 | throw new ConditionNotFoundException(\sprintf('Condition %s not found', $id));
71 | }
72 |
73 | $condition->delete();
74 |
75 | return $condition->toCondition();
76 | }
77 | }
78 |
--------------------------------------------------------------------------------
/tests/Helpers/Dummy/DummyTarget.php:
--------------------------------------------------------------------------------
1 | conditions = \collect();
26 | }
27 |
28 | /**
29 | * Gets state from target.
30 | *
31 | * @return StateContract
32 | */
33 | public function getInitialState(): StateContract
34 | {
35 | return $this->newState([]);
36 | }
37 |
38 | /**
39 | * Sets the state to the target.
40 | *
41 | * @param StateContract $state
42 | */
43 | public function applyState(StateContract $state): void
44 | {
45 | $this->isFired = true;
46 | $this->state = $state;
47 | }
48 |
49 | /**
50 | * Gets root target conditions.
51 | *
52 | * @return iterable|ConditionContract[]
53 | */
54 | public function getRootConditions(): iterable
55 | {
56 | return $this->getChildrenConditions(null);
57 | }
58 |
59 | /**
60 | * Gets children target conditions.
61 | *
62 | * @param int|null $parentId
63 | *
64 | * @return iterable|ConditionContract[]
65 | */
66 | public function getChildrenConditions(?int $parentId): iterable
67 | {
68 | return $this->conditions->filter(function (ConditionContract $condition) use ($parentId) {
69 | return $condition->getParentId() === $parentId;
70 | });
71 | }
72 |
73 | public function addConditions(ConditionContract ...$conditions): self
74 | {
75 | foreach ($conditions as $condition) {
76 | $this->conditions->push($condition);
77 | }
78 |
79 | return $this;
80 | }
81 |
82 | public function isFired(): bool
83 | {
84 | return $this->isFired;
85 | }
86 | }
87 |
--------------------------------------------------------------------------------
/src/Contracts/ActionContract.php:
--------------------------------------------------------------------------------
1 | manager = \app(ConditionalActionManager::class);
22 | $this->target = new DummyTarget();
23 | }
24 |
25 | public function test_run_condition_actions_when_condition_succeed()
26 | {
27 | $actions = $this->makeActions(2);
28 | $condition = $this->succeedCondition(null, ...$actions);
29 | $this->target->addConditions($condition);
30 |
31 | $this->target->runConditionalActions();
32 |
33 | $this->assertFired($condition);
34 | $this->assertFired(...$actions);
35 | $this->assertFired($this->target);
36 | }
37 |
38 | public function test_not_run_condition_actions_when_condition_failed()
39 | {
40 | $actions = $this->makeActions(2);
41 | $condition = $this->failedCondition(null, ...$actions);
42 | $this->target->addConditions($condition);
43 |
44 | $this->target->runConditionalActions();
45 |
46 | $this->assertFired($condition);
47 | $this->assertNotFired(...$actions);
48 | }
49 |
50 | public function test_run_condition_check_even_if_previous_condition_is_failed()
51 | {
52 | $actions = $this->makeActions(2);
53 | $conditions = [
54 | $this->failedCondition(null, $actions[0]),
55 | $this->succeedCondition(null, $actions[1]),
56 | ];
57 | $this->target->addConditions(...$conditions);
58 |
59 | $this->target->runConditionalActions();
60 |
61 | $this->assertFired(...$conditions);
62 | $this->assertNotFired($actions[0]);
63 | $this->assertFired($actions[1]);
64 | }
65 |
66 | public function test_inverted_failed_condition_is_succeed_condition()
67 | {
68 | $actions = $this->makeActions(2);
69 | $condition = $this->failedCondition(null, ...$actions);
70 | $condition->setIsInverted(true);
71 | $this->target->addConditions($condition);
72 |
73 | $this->target->runConditionalActions();
74 |
75 | $this->assertFired($condition);
76 | $this->assertFired(...$actions);
77 | }
78 | }
79 |
--------------------------------------------------------------------------------
/src/Entities/Eloquent/Action.php:
--------------------------------------------------------------------------------
1 | 'datetime',
42 | 'ends_at' => 'datetime',
43 | 'parameters' => 'array',
44 | 'priority' => 'int',
45 | 'condition_id' => 'int',
46 | ];
47 |
48 | public static function validatingRules(): array
49 | {
50 | return ['name' => 'required'];
51 | }
52 |
53 | public function isActive(): bool
54 | {
55 | return Carbon::now()->between(
56 | $this->starts_at ?? Carbon::minValue(),
57 | $this->ends_at ?? Carbon::maxValue()
58 | );
59 | }
60 |
61 | /**
62 | * @throws \Throwable
63 | *
64 | * @return ActionContract
65 | */
66 | public function toAction(): ActionContract
67 | {
68 | $className = \config("conditional-actions.actions.{$this->name}");
69 |
70 | \throw_unless(
71 | $className,
72 | ActionNotFoundException::class,
73 | \sprintf('Action %s not found', $this->name),
74 | Response::HTTP_NOT_FOUND
75 | );
76 |
77 | /** @var ActionContract $action */
78 | $action = \app($className);
79 | $action
80 | ->setId($this->id)
81 | ->setParameters($this->parameters ?? [])
82 | ->setPriority($this->priority ?? 0)
83 | ->setStartsAt($this->starts_at)
84 | ->setEndsAt($this->ends_at);
85 |
86 | return $action->setParameters($this->parameters);
87 | }
88 | }
89 |
--------------------------------------------------------------------------------
/src/Http/Controllers/ActionsController.php:
--------------------------------------------------------------------------------
1 | actionRepository = $actionRepository;
28 | $this->actionPresenter = $actionPresenter;
29 | }
30 |
31 | public function show(int $actionId)
32 | {
33 | $action = $this->actionRepository->find($actionId);
34 |
35 | return $this->jsonResponse($this->actionPresenter->attributes($action));
36 | }
37 |
38 | public function store(Request $request, ConditionalActions $conditionalActions)
39 | {
40 | $data = $this->validate($request, [
41 | 'name' => ['required', new In($conditionalActions->getActionNames())],
42 | 'condition_id' => 'required|numeric',
43 | 'parameters' => 'array',
44 | 'priority' => 'integer',
45 | 'starts_at' => 'date',
46 | 'ends_at' => 'date',
47 | ]);
48 |
49 | $action = $this->actionRepository->store($data);
50 |
51 | return $this->jsonResponse($this->actionPresenter->attributes($action));
52 | }
53 |
54 | public function update(Request $request, ConditionalActions $conditionalActions, int $actionId)
55 | {
56 | $data = $this->validate($request, [
57 | 'name' => ['required', new In($conditionalActions->getActionNames())],
58 | 'condition_id' => 'required|numeric',
59 | 'parameters' => 'array',
60 | 'priority' => 'integer',
61 | 'starts_at' => 'date',
62 | 'ends_at' => 'date',
63 | ]);
64 |
65 | $action = $this->actionRepository->update($actionId, $data);
66 |
67 | return $this->jsonResponse($this->actionPresenter->attributes($action));
68 | }
69 |
70 | public function destroy(int $actionId)
71 | {
72 | $condition = $this->actionRepository->destroy($actionId);
73 |
74 | return $this->jsonResponse($this->actionPresenter->attributes($condition));
75 | }
76 | }
77 |
--------------------------------------------------------------------------------
/src/Http/Controllers/ConditionsController.php:
--------------------------------------------------------------------------------
1 | conditionsRepository = $conditionRepository;
28 | $this->conditionPresenter = $conditionPresenter;
29 | }
30 |
31 | public function show(int $conditionId)
32 | {
33 | $condition = $this->conditionsRepository->find($conditionId);
34 |
35 | return $this->jsonResponse($this->conditionPresenter->attributes($condition));
36 | }
37 |
38 | public function store(Request $request, ConditionalActions $conditionalActions)
39 | {
40 | $data = $this->validate($request, [
41 | 'name' => ['required', new In($conditionalActions->getConditionNames())],
42 | 'is_inverted' => 'boolean',
43 | 'parameters' => 'array',
44 | 'priority' => 'integer',
45 | 'starts_at' => 'date',
46 | 'ends_at' => 'date',
47 | ]);
48 |
49 | $condition = $this->conditionsRepository->store($data);
50 |
51 | return $this->jsonResponse($this->conditionPresenter->attributes($condition));
52 | }
53 |
54 | public function update(Request $request, ConditionalActions $conditionalActions, int $conditionId)
55 | {
56 | $data = $this->validate($request, [
57 | 'name' => ['required', new In($conditionalActions->getConditionNames())],
58 | 'is_inverted' => 'boolean',
59 | 'parameters' => 'array',
60 | 'priority' => 'integer',
61 | 'starts_at' => 'date',
62 | 'ends_at' => 'date',
63 | ]);
64 |
65 | $condition = $this->conditionsRepository->update($conditionId, $data);
66 |
67 | return $this->jsonResponse($this->conditionPresenter->attributes($condition));
68 | }
69 |
70 | public function destroy(int $conditionId)
71 | {
72 | $condition = $this->conditionsRepository->destroy($conditionId);
73 |
74 | return $this->jsonResponse($this->conditionPresenter->attributes($condition));
75 | }
76 | }
77 |
--------------------------------------------------------------------------------
/src/Entities/Actions/BaseAction.php:
--------------------------------------------------------------------------------
1 | parameters = $parameters ?? [];
38 |
39 | return $this;
40 | }
41 |
42 | /**
43 | * Gets action parameters.
44 | *
45 | * @return iterable
46 | */
47 | public function getParameters(): iterable
48 | {
49 | return $this->parameters;
50 | }
51 |
52 | /**
53 | * Sets the action identifier.
54 | *
55 | * @param int|null $id
56 | *
57 | * @return BaseAction
58 | */
59 | public function setId(?int $id): self
60 | {
61 | $this->id = $id;
62 |
63 | return $this;
64 | }
65 |
66 | /**
67 | * Gets action identifier.
68 | *
69 | * @return int|null
70 | */
71 | public function getId(): ?int
72 | {
73 | return $this->id;
74 | }
75 |
76 | /**
77 | * Sets the action priority.
78 | *
79 | * @param int $priority
80 | *
81 | * @return BaseAction
82 | */
83 | public function setPriority(int $priority): self
84 | {
85 | $this->priority = $priority;
86 |
87 | return $this;
88 | }
89 |
90 | /**
91 | * Gets action priority.
92 | *
93 | * @return int
94 | */
95 | public function getPriority(): int
96 | {
97 | return $this->priority;
98 | }
99 |
100 | /**
101 | * Sets the start time.
102 | *
103 | * @param Carbon|null $startsAt
104 | *
105 | * @return BaseAction
106 | */
107 | public function setStartsAt(?Carbon $startsAt): self
108 | {
109 | $this->startsAt = $startsAt;
110 |
111 | return $this;
112 | }
113 |
114 | /**
115 | * Gets start time.
116 | *
117 | * @return Carbon|null
118 | */
119 | public function getStartsAt(): ?Carbon
120 | {
121 | return $this->startsAt;
122 | }
123 |
124 | /**
125 | * Sets the finish time.
126 | *
127 | * @param Carbon|null $endsAt
128 | *
129 | * @return BaseAction
130 | */
131 | public function setEndsAt(?Carbon $endsAt): self
132 | {
133 | $this->endsAt = $endsAt;
134 |
135 | return $this;
136 | }
137 |
138 | /**
139 | * Gets finish time.
140 | *
141 | * @return Carbon|null
142 | */
143 | public function getEndsAt(): ?Carbon
144 | {
145 | return $this->endsAt;
146 | }
147 | }
148 |
--------------------------------------------------------------------------------
/src/Console/ConditionalActionsTable.php:
--------------------------------------------------------------------------------
1 | files = $files;
42 | $this->composer = $composer;
43 | }
44 |
45 | /**
46 | * @throws FileNotFoundException
47 | */
48 | public function handle()
49 | {
50 | $path = $this->laravel->basePath($this->option('migrations-path'));
51 | $tables = ['conditions', 'condition_actions'];
52 |
53 | foreach ($tables as $i => $table) {
54 | // Sleep needs to correct migrations order
55 | if ($i > 0) {
56 | \sleep(1);
57 | }
58 | $filename = $this->replaceMigration(
59 | $this->createBaseMigration($table, $path),
60 | $table,
61 | Str::studly($table)
62 | );
63 |
64 | $fileName = \trim(Str::after($filename, $path), '/');
65 | $this->line(\sprintf('Migration created: %s', $fileName));
66 | }
67 |
68 | $this->composer->dumpAutoloads();
69 | }
70 |
71 | /**
72 | * Create a base migration file for the table.
73 | *
74 | * @param string $table
75 | * @param string $path
76 | *
77 | * @return string
78 | */
79 | protected function createBaseMigration(string $table, string $path)
80 | {
81 | return $this->laravel['migration.creator']->create(
82 | 'create_' . $table . '_table',
83 | $path
84 | );
85 | }
86 |
87 | /**
88 | * Replace the generated migration with the table stub.
89 | *
90 | * @param string $path
91 | * @param string $table
92 | * @param string $tableClassName
93 | *
94 | * @throws FileNotFoundException
95 | *
96 | * @return string
97 | */
98 | protected function replaceMigration($path, $table, $tableClassName)
99 | {
100 | $stub = \str_replace(
101 | ['{{table}}', '{{tableClassName}}'],
102 | [$table, $tableClassName],
103 | $this->files->get(__DIR__ . "/stubs/{$table}.stub")
104 | );
105 |
106 | $this->files->put($path, $stub);
107 |
108 | return $path;
109 | }
110 | }
111 |
--------------------------------------------------------------------------------
/tests/Feature/Entities/Eloquent/ConditionActionTest.php:
--------------------------------------------------------------------------------
1 | now));
22 | }
23 |
24 | /**
25 | * @dataProvider provider_is_active
26 | *
27 | * @param bool $isActive
28 | * @param Carbon|null $startsAt
29 | * @param Carbon|null $endsAt
30 | */
31 | public function test_is_active(bool $isActive, $startsAt, $endsAt)
32 | {
33 | /** @var Action $action */
34 | $action = \create(Action::class, [
35 | 'starts_at' => $startsAt,
36 | 'ends_at' => $endsAt,
37 | ]);
38 |
39 | $this->assertEquals($isActive, $action->isActive());
40 | }
41 |
42 | public function provider_is_active(): array
43 | {
44 | Carbon::setTestNow(Carbon::parse($this->now));
45 |
46 | return [
47 | 'starts_at at now and ends_at in future' => [true, Carbon::now(), Carbon::tomorrow()],
48 | 'starts_at in past and ends_at at now' => [true, Carbon::yesterday(), Carbon::now()],
49 | 'starts_at in past and ends_at in future' => [true, Carbon::yesterday(), Carbon::tomorrow()],
50 | 'starts_at is null and ends_at at now' => [true, null, Carbon::now()],
51 | 'starts_at at now and ends_at is null' => [true, Carbon::now(), null],
52 | 'starts_at is null and ends_at is null' => [true, null, null],
53 | 'starts_at in past and ends_at in past' => [false, Carbon::yesterday()->subDay(), Carbon::yesterday()],
54 | 'starts_at is null and ends_at in past' => [false, null, Carbon::yesterday()],
55 | 'starts_at in future and ends_at in future' => [false, Carbon::tomorrow(), Carbon::tomorrow()->addDay()],
56 | 'starts_at in future and ends_at is null' => [false, Carbon::tomorrow(), null],
57 | ];
58 | }
59 |
60 | public function test_to_action_exception_when_action_not_exists()
61 | {
62 | /** @var Action $action */
63 | $action = \create(Action::class, ['name' => 'NotExists']);
64 |
65 | $this->expectException(ActionNotFoundException::class);
66 | $this->expectExceptionCode(Response::HTTP_NOT_FOUND);
67 | $this->expectExceptionMessage('Action NotExists not found');
68 |
69 | $action->toAction();
70 | }
71 |
72 | public function test_to_action_make_correct_action()
73 | {
74 | /** @var Action $action */
75 | $action = \create(Action::class, [
76 | 'name' => 'UpdateStateAttributeAction',
77 | 'parameters' => ['one' => 'first'],
78 | ]);
79 |
80 | $actualAction = $action->toAction();
81 |
82 | $this->assertInstanceOf(UpdateStateAttributeAction::class, $actualAction);
83 | $this->assertEquals($action->parameters, $actualAction->getParameters());
84 | }
85 |
86 | public function test_validate_name()
87 | {
88 | $this->expectException(ValidationException::class);
89 | \create(Action::class, ['name' => '']);
90 | }
91 | }
92 |
--------------------------------------------------------------------------------
/src/Contracts/ConditionContract.php:
--------------------------------------------------------------------------------
1 | 'datetime',
49 | 'ends_at' => 'datetime',
50 | 'parameters' => 'array',
51 | 'is_inverted' => 'boolean',
52 | 'priority' => 'int',
53 | 'parent_id' => 'int',
54 | 'target_id' => 'int',
55 | ];
56 |
57 | public static function validatingRules(): array
58 | {
59 | return ['name' => 'required'];
60 | }
61 |
62 | public function actions(): HasMany
63 | {
64 | return $this->hasMany(Action::class);
65 | }
66 |
67 | public function childrenConditions(): HasMany
68 | {
69 | return $this->hasMany(self::class, 'parent_id', 'id');
70 | }
71 |
72 | public function isActive(): bool
73 | {
74 | return Carbon::now()->between(
75 | $this->starts_at ?? Carbon::minValue(),
76 | $this->ends_at ?? Carbon::maxValue()
77 | );
78 | }
79 |
80 | /**
81 | * @throws \Throwable
82 | *
83 | * @return ConditionContract
84 | */
85 | public function toCondition(): ConditionContract
86 | {
87 | $className = \config("conditional-actions.conditions.{$this->name}");
88 |
89 | \throw_unless(
90 | $className,
91 | ConditionNotFoundException::class,
92 | \sprintf('Condition %s not found', $this->name),
93 | Response::HTTP_NOT_FOUND
94 | );
95 |
96 | /** @var ConditionContract $condition */
97 | $condition = \app($className);
98 |
99 | return $condition->setId($this->id)
100 | ->setActions($this->getActiveActions()->map(function (Action $action) {
101 | return $action->toAction();
102 | }))
103 | ->setIsInverted($this->is_inverted)
104 | ->setPriority($this->priority ?? 0)
105 | ->setStartsAt($this->starts_at)
106 | ->setEndsAt($this->ends_at)
107 | ->setParameters($this->parameters);
108 | }
109 |
110 | /**
111 | * @return Action[]|Collection
112 | */
113 | public function getActiveActions()
114 | {
115 | return $this->actions
116 | ->filter(function (Action $action) {
117 | return $action->isActive();
118 | })
119 | ->sortBy('priority')
120 | ->values();
121 | }
122 | }
123 |
--------------------------------------------------------------------------------
/tests/Unit/Entities/Conditions/OneOfConditionTest.php:
--------------------------------------------------------------------------------
1 | makeTestCondition(\app(OneOfCondition::class));
14 | }
15 |
16 | public function test_true_when_has_succeed_conditions()
17 | {
18 | $conditions = [
19 | $this->failedChildrenCondition(new DummyAction()),
20 | $this->succeedChildrenCondition(new DummyAction()),
21 | $this->succeedChildrenCondition(new DummyAction()),
22 | ];
23 | $this->target->addConditions(...$conditions);
24 |
25 | $result = $this->testCondition->check($this->target, $this->target->getInitialState());
26 |
27 | $this->assertTrue($result);
28 | $this->assertFired($conditions[0]);
29 | $this->assertFired($conditions[1]);
30 | $this->assertNotFired($conditions[2]);
31 | }
32 |
33 | public function test_false_when_no_succeeds()
34 | {
35 | $conditions = [
36 | $this->failedChildrenCondition(new DummyAction()),
37 | $this->failedChildrenCondition(new DummyAction()),
38 | ];
39 | $this->target->addConditions(...$conditions);
40 |
41 | $result = $this->testCondition->check($this->target, $this->target->getInitialState());
42 |
43 | $this->assertFalse($result);
44 | $this->assertFired(...$conditions);
45 | }
46 |
47 | public function test_collect_actions_of_first_succeed_condition()
48 | {
49 | $actions = $this->makeActions(4);
50 | $conditions = [
51 | $this->failedChildrenCondition($actions[0]),
52 | $this->succeedChildrenCondition($actions[1]),
53 | $this->succeedChildrenCondition($actions[2]),
54 | ];
55 | $this->target->addConditions(...$conditions);
56 | $this->testCondition->setActions([$this->action, $actions[3]]);
57 |
58 | $this->testCondition->check($this->target, $this->target->getInitialState());
59 |
60 | $this->assertSame(
61 | [$actions[1], $this->action, $actions[3]],
62 | $this->testCondition->getActions()
63 | );
64 | }
65 |
66 | public function test_dont_collect_actions_when_no_succeed_conditions()
67 | {
68 | $conditions = [
69 | $this->failedChildrenCondition(new DummyAction()),
70 | $this->failedChildrenCondition(new DummyAction()),
71 | ];
72 | $this->target->addConditions(...$conditions);
73 |
74 | $this->testCondition->check($this->target, $this->target->getInitialState());
75 |
76 | $this->assertSame(
77 | [$this->action],
78 | $this->testCondition->getActions()
79 | );
80 | }
81 |
82 | public function test_children_inverted_failed_condition_succeed()
83 | {
84 | $condition = $this->failedChildrenCondition();
85 | $condition->setIsInverted(true);
86 | $this->target->addConditions($condition);
87 |
88 | $result = $this->testCondition->check($this->target, $this->target->getInitialState());
89 |
90 | $this->assertTrue($result);
91 | $this->assertFired($condition);
92 | }
93 |
94 | public function test_one_of_inverted_failed_condition_succeed()
95 | {
96 | $condition = $this->failedChildrenCondition();
97 | $this->testCondition->setIsInverted(true);
98 | $this->target->addConditions($condition);
99 |
100 | $result = $this->testCondition->check($this->target, $this->target->getInitialState());
101 |
102 | $this->assertTrue($result);
103 | $this->assertFired($condition);
104 | }
105 | }
106 |
--------------------------------------------------------------------------------
/tests/Unit/Entities/Conditions/AllOfConditionTest.php:
--------------------------------------------------------------------------------
1 | makeTestCondition(\app(AllOfCondition::class));
15 | }
16 |
17 | public function test_true_when_all_conditions_is_succeed()
18 | {
19 | /** @var DummyCondition[] $conditions */
20 | $conditions = [
21 | $this->succeedChildrenCondition(),
22 | $this->succeedChildrenCondition(),
23 | $this->succeedChildrenCondition(),
24 | ];
25 | $this->target->addConditions(...$conditions);
26 |
27 | $result = $this->testCondition->check($this->target, $this->target->getInitialState());
28 |
29 | $this->assertTrue($result);
30 | $this->assertFired(...$conditions);
31 | }
32 |
33 | public function test_false_when_has_failed_condition()
34 | {
35 | /** @var DummyCondition[] $conditions */
36 | $conditions = [
37 | $this->succeedChildrenCondition(),
38 | $this->failedChildrenCondition(),
39 | $this->succeedChildrenCondition(),
40 | ];
41 | $this->target->addConditions(...$conditions);
42 |
43 | $result = $this->testCondition->check($this->target, $this->target->getInitialState());
44 |
45 | $this->assertFalse($result);
46 |
47 | $this->assertFired($conditions[0]);
48 | $this->assertFired($conditions[1]);
49 | $this->assertNotFired($conditions[2]);
50 | }
51 |
52 | public function test_child_actions_collected()
53 | {
54 | $action1 = new DummyAction();
55 | $action2 = new DummyAction();
56 | /** @var DummyCondition[] $conditions */
57 | $conditions = [
58 | $this->succeedChildrenCondition(),
59 | $this->succeedChildrenCondition($action1),
60 | ];
61 | $this->target->addConditions(...$conditions);
62 | $this->testCondition->setActions([$this->action, $action2]);
63 |
64 | $result = $this->testCondition->check($this->target, $this->target->getInitialState());
65 |
66 | $this->assertTrue($result);
67 | $this->assertSame(
68 | [$action1, $this->action, $action2],
69 | $this->testCondition->getActions()
70 | );
71 | }
72 |
73 | public function test_child_actions_not_collected_when_failed()
74 | {
75 | /** @var DummyCondition[] $conditions */
76 | $conditions = [
77 | $this->succeedChildrenCondition(),
78 | $this->succeedChildrenCondition(new DummyAction()),
79 | $this->failedChildrenCondition(new DummyAction()),
80 | ];
81 | $this->target->addConditions(...$conditions);
82 |
83 | $result = $this->testCondition->check($this->target, $this->target->getInitialState());
84 |
85 | $this->assertFalse($result);
86 | $this->assertSame([$this->action], $this->testCondition->getActions());
87 | }
88 |
89 | public function test_children_inverted_failed_condition_succeed()
90 | {
91 | $condition = $this->failedChildrenCondition();
92 | $condition->setIsInverted(true);
93 | $this->target->addConditions($condition);
94 |
95 | $result = $this->testCondition->check($this->target, $this->target->getInitialState());
96 |
97 | $this->assertTrue($result);
98 | $this->assertFired($condition);
99 | }
100 |
101 | public function test_all_of_inverted_failed_condition_succeed()
102 | {
103 | $this->testCondition->setIsInverted(true);
104 | $condition = $this->failedChildrenCondition();
105 | $this->target->addConditions($condition);
106 |
107 | $result = $this->testCondition->check($this->target, $this->target->getInitialState());
108 |
109 | $this->assertTrue($result);
110 | $this->assertFired($condition);
111 | }
112 | }
113 |
--------------------------------------------------------------------------------
/tests/Feature/Repositories/EloquentConditionRepositoryTest.php:
--------------------------------------------------------------------------------
1 | 'TrueCondition',
18 | 'priority' => 5,
19 | 'is_inverted' => true,
20 | 'starts_at' => '2019-01-01 10:00:00',
21 | 'ends_at' => '2019-01-02 20:00:00',
22 | ];
23 |
24 | protected function setUp(): void
25 | {
26 | parent::setUp();
27 | $this->conditionsRepository = \app(EloquentConditionRepository::class);
28 | }
29 |
30 | public function test_store()
31 | {
32 | $condition = $this->conditionsRepository->store($this->validAttributes);
33 |
34 | $this->assertIsNumeric($condition->getId());
35 | $this->assertConditionAttributes($condition);
36 | }
37 |
38 | public function test_update()
39 | {
40 | /** @var Condition $initial */
41 | $initial = \create(Condition::class, [
42 | 'name' => 'AllOfCondition',
43 | 'priority' => 10,
44 | 'is_inverted' => false,
45 | 'starts_at' => '2018-10-10 00:00:00',
46 | 'ends_at' => '2018-11-10 00:00:00',
47 | ]);
48 |
49 | $condition = $this->conditionsRepository->update($initial->id, $this->validAttributes);
50 |
51 | $this->assertEquals($initial->id, $condition->getId());
52 | $this->assertConditionAttributes($condition);
53 | }
54 |
55 | public function test_update_non_existent()
56 | {
57 | $count = Condition::query()->count();
58 |
59 | $this->expectException(ConditionNotFoundException::class);
60 | $this->expectExceptionMessage('Condition 5 not found');
61 |
62 | $this->conditionsRepository->update(5, $this->validAttributes);
63 |
64 | $this->assertEquals($count, Condition::query()->count());
65 | }
66 |
67 | public function test_find()
68 | {
69 | /** @var Condition $initial */
70 | $initial = \create(Condition::class, $this->validAttributes);
71 |
72 | $condition = $this->conditionsRepository->find($initial->id);
73 |
74 | $this->assertEquals($initial->id, $condition->getId());
75 | $this->assertConditionAttributes($condition);
76 | }
77 |
78 | public function test_destroy()
79 | {
80 | /** @var Condition $initial */
81 | $initial = \create(Condition::class, $this->validAttributes);
82 |
83 | $condition = $this->conditionsRepository->destroy($initial->id);
84 |
85 | /** @var Condition $deleted */
86 | $deleted = Condition::query()->onlyTrashed()->find($initial->id);
87 | $this->assertNotNull($deleted);
88 | $this->assertTrue($deleted->trashed());
89 | $this->assertEquals($deleted->id, $condition->getId());
90 | $this->assertConditionAttributes($condition);
91 | }
92 |
93 | public function test_destroy_non_existent()
94 | {
95 | $count = Condition::query()->count();
96 |
97 | $this->expectException(ConditionNotFoundException::class);
98 | $this->expectExceptionMessage('Condition 5 not found');
99 |
100 | $this->conditionsRepository->destroy(5);
101 |
102 | $this->assertEquals($count, Condition::query()->count());
103 | }
104 |
105 | private function assertConditionAttributes(\ConditionalActions\Contracts\ConditionContract $condition): void
106 | {
107 | $this->assertInstanceOf(TrueCondition::class, $condition);
108 | $this->assertEquals($this->validAttributes['priority'], $condition->getPriority());
109 | $this->assertEquals($this->validAttributes['is_inverted'], $condition->isInverted());
110 | $this->assertEquals($this->validAttributes['starts_at'], $condition->getStartsAt()->toDateTimeString());
111 | $this->assertEquals($this->validAttributes['ends_at'], $condition->getEndsAt()->toDateTimeString());
112 | }
113 | }
114 |
--------------------------------------------------------------------------------
/tests/Feature/Traits/EloquentTargetTest.php:
--------------------------------------------------------------------------------
1 | create();
19 | $children = \create(Condition::class, ['parent_id' => $roots[1]->id]);
20 | \create(Condition::class, ['parent_id' => $roots[1]->id]);
21 | $target->conditions()->saveMany([$roots[1], $children]);
22 |
23 | $actualRoots = $target->getRootConditions();
24 | $actualChildren = $target->getChildrenConditions($roots[1]->id);
25 |
26 | $this->assertEquals(
27 | [$roots[1]->id],
28 | \iterator_to_array(\collect($actualRoots)->map->getId())
29 | );
30 | $this->assertEquals(
31 | [$children->id],
32 | \iterator_to_array(\collect($actualChildren)->map->getId())
33 | );
34 | }
35 |
36 | public function test_get_root_and_children_conditions_filtered_by_active()
37 | {
38 | /** @var DummyEloquentModel $target */
39 | $target = \create(DummyEloquentModel::class);
40 | /** @var Condition $activeRoot */
41 | $activeRoot = \create(Condition::class);
42 | $inactiveRoot = \create(Condition::class, [], 'inactive');
43 | $inactiveChildren = $activeRoot->childrenConditions()->save(\create(Condition::class, [], 'inactive'));
44 | /** @var Condition $activeChildren */
45 | $activeChildren = $activeRoot->childrenConditions()->save(\create(Condition::class));
46 | $target->conditions()->saveMany([$activeRoot, $inactiveRoot, $activeChildren, $inactiveChildren]);
47 |
48 | $actualRoots = $target->getRootConditions();
49 | $actualChildren = $target->getChildrenConditions($activeRoot->id);
50 |
51 | $this->assertEquals(
52 | [$activeRoot->id],
53 | \iterator_to_array(\collect($actualRoots)->map->getId())
54 | );
55 | $this->assertEquals(
56 | [$activeChildren->id],
57 | \iterator_to_array(\collect($actualChildren)->map->getId())
58 | );
59 | }
60 |
61 | public function test_get_root_and_children_conditions_sorted_by_priority()
62 | {
63 | /** @var DummyEloquentModel $target */
64 | $target = \create(DummyEloquentModel::class);
65 | /** @var Condition $root10 */
66 | [$root10, $root20, $root15] = \createMany(Condition::class, ['priority'], [[10], [20], [15]]);
67 | [$children10, $children20, $children15] = \createMany(Condition::class, ['priority'], [[10], [20], [15]]);
68 | $root10->childrenConditions()->saveMany([$children10, $children20, $children15]);
69 | $target->conditions()->saveMany([$root10, $root20, $root15, $children10, $children20, $children15]);
70 |
71 | $actualRoots = $target->getRootConditions();
72 | $actualChildren = $target->getChildrenConditions($root10->id);
73 |
74 | $this->assertEquals(
75 | [$root10->id, $root15->id, $root20->id],
76 | \iterator_to_array(\collect($actualRoots)->map->getId())
77 | );
78 | $this->assertEquals(
79 | [$children10->id, $children15->id, $children20->id],
80 | \iterator_to_array(\collect($actualChildren)->map->getId())
81 | );
82 | }
83 |
84 | public function test_get_root_and_children_conditions_returns_condition_contracts()
85 | {
86 | /** @var DummyEloquentModel $target */
87 | $target = \create(DummyEloquentModel::class);
88 | /** @var Condition $root */
89 | $root = \create(Condition::class);
90 | $children = $root->childrenConditions()->save(\create(Condition::class, ['priority' => 10]));
91 | $target->conditions()->saveMany([$root, $children]);
92 |
93 | $actualRoots = $target->getRootConditions();
94 | $actualChildren = $target->getChildrenConditions($root->id);
95 |
96 | $this->assertInstanceOf(ConditionContract::class, $actualRoots[0]);
97 | $this->assertInstanceOf(ConditionContract::class, $actualChildren[0]);
98 | }
99 | }
100 |
--------------------------------------------------------------------------------
/tests/Feature/Repositories/EloquentActionRepositoryTest.php:
--------------------------------------------------------------------------------
1 | 'UpdateStateAttributeAction',
20 | 'priority' => 5,
21 | 'parameters' => ['param' => 'value'],
22 | 'starts_at' => '2019-01-01 10:00:00',
23 | 'ends_at' => '2019-01-02 20:00:00',
24 | ];
25 |
26 | protected function setUp(): void
27 | {
28 | parent::setUp();
29 |
30 | $this->actionsRepository = \app(EloquentActionRepository::class);
31 | /** @var Condition $condition */
32 | $condition = Condition::query()->create(['name' => 'TrueCondition']);
33 | $this->validAttributes['condition_id'] = $condition->id;
34 | }
35 |
36 | public function test_store()
37 | {
38 | $condition = $this->actionsRepository->store($this->validAttributes);
39 |
40 | $this->assertIsNumeric($condition->getId());
41 | $this->assertConditionAttributes($condition);
42 | }
43 |
44 | public function test_update()
45 | {
46 | /** @var Condition $initial */
47 | $initial = \create(Action::class, [
48 | 'name' => 'UpdateStateAttributeAction',
49 | 'priority' => 10,
50 | 'condition_id' => $this->validAttributes['condition_id'],
51 | 'starts_at' => '2018-10-10 00:00:00',
52 | 'ends_at' => '2018-11-10 00:00:00',
53 | ]);
54 |
55 | $condition = $this->actionsRepository->update($initial->id, $this->validAttributes);
56 |
57 | $this->assertEquals($initial->id, $condition->getId());
58 | $this->assertConditionAttributes($condition);
59 | }
60 |
61 | public function test_update_non_existent()
62 | {
63 | $count = Action::query()->count();
64 |
65 | $this->expectException(ActionNotFoundException::class);
66 | $this->expectExceptionMessage('Action 5 not found');
67 |
68 | $this->actionsRepository->update(5, $this->validAttributes);
69 |
70 | $this->assertEquals($count, Action::query()->count());
71 | }
72 |
73 | public function test_find()
74 | {
75 | /** @var Action $initial */
76 | $initial = \create(Action::class, $this->validAttributes);
77 |
78 | $action = $this->actionsRepository->find($initial->id);
79 |
80 | $this->assertEquals($initial->id, $action->getId());
81 | $this->assertConditionAttributes($action);
82 | }
83 |
84 | public function test_destroy()
85 | {
86 | /** @var Condition $initial */
87 | $initial = \create(Action::class, $this->validAttributes);
88 |
89 | $action = $this->actionsRepository->destroy($initial->id);
90 |
91 | /** @var Condition $deleted */
92 | $deleted = Action::query()->onlyTrashed()->find($initial->id);
93 | $this->assertNotNull($deleted);
94 | $this->assertTrue($deleted->trashed());
95 | $this->assertEquals($deleted->id, $action->getId());
96 | $this->assertConditionAttributes($action);
97 | }
98 |
99 | public function test_destroy_non_existent()
100 | {
101 | $count = Action::query()->count();
102 |
103 | $this->expectException(ActionNotFoundException::class);
104 | $this->expectExceptionMessage('Action 5 not found');
105 |
106 | $this->actionsRepository->destroy(5);
107 |
108 | $this->assertEquals($count, Action::query()->count());
109 | }
110 |
111 | private function assertConditionAttributes(ActionContract $condition): void
112 | {
113 | $this->assertInstanceOf(UpdateStateAttributeAction::class, $condition);
114 | $this->assertEquals($this->validAttributes['priority'], $condition->getPriority());
115 | $this->assertEquals($this->validAttributes['parameters'], $condition->getParameters());
116 | $this->assertEquals($this->validAttributes['starts_at'], $condition->getStartsAt()->toDateTimeString());
117 | $this->assertEquals($this->validAttributes['ends_at'], $condition->getEndsAt()->toDateTimeString());
118 | }
119 | }
120 |
--------------------------------------------------------------------------------
/tests/Feature/Entities/Eloquent/ConditionTest.php:
--------------------------------------------------------------------------------
1 | now));
23 | }
24 |
25 | /**
26 | * @dataProvider provider_is_active
27 | *
28 | * @param bool $isActive
29 | * @param Carbon|null $startsAt
30 | * @param Carbon|null $endsAt
31 | */
32 | public function test_is_active(bool $isActive, $startsAt, $endsAt)
33 | {
34 | /** @var Condition $condition */
35 | $condition = \create(Condition::class, [
36 | 'starts_at' => $startsAt,
37 | 'ends_at' => $endsAt,
38 | ]);
39 |
40 | $this->assertEquals($isActive, $condition->isActive());
41 | }
42 |
43 | public function provider_is_active(): array
44 | {
45 | Carbon::setTestNow(Carbon::parse($this->now));
46 |
47 | return [
48 | 'starts_at at now and ends_at in future' => [true, Carbon::now(), Carbon::tomorrow()],
49 | 'starts_at in past and ends_at at now' => [true, Carbon::yesterday(), Carbon::now()],
50 | 'starts_at in past and ends_at in future' => [true, Carbon::yesterday(), Carbon::tomorrow()],
51 | 'starts_at is null and ends_at at now' => [true, null, Carbon::now()],
52 | 'starts_at at now and ends_at is null' => [true, Carbon::now(), null],
53 | 'starts_at is null and ends_at is null' => [true, null, null],
54 | 'starts_at in past and ends_at in past' => [false, Carbon::yesterday()->subDay(), Carbon::yesterday()],
55 | 'starts_at is null and ends_at in past' => [false, null, Carbon::yesterday()],
56 | 'starts_at in future and ends_at in future' => [false, Carbon::tomorrow(), Carbon::tomorrow()->addDay()],
57 | 'starts_at in future and ends_at is null' => [false, Carbon::tomorrow(), null],
58 | ];
59 | }
60 |
61 | public function test_to_condition_exception_when_condition_not_exists()
62 | {
63 | /** @var Condition $condition */
64 | $condition = \create(Condition::class, ['name' => 'NotExists']);
65 |
66 | $this->expectException(ConditionNotFoundException::class);
67 | $this->expectExceptionCode(Response::HTTP_NOT_FOUND);
68 | $this->expectExceptionMessage('Condition NotExists not found');
69 |
70 | $condition->toCondition();
71 | }
72 |
73 | public function test_to_condition_make_correct_condition()
74 | {
75 | /** @var Condition $condition */
76 | $condition = \create(Condition::class, [
77 | 'name' => 'TrueCondition',
78 | 'is_inverted' => true,
79 | 'parameters' => ['one' => 'first'],
80 | ]);
81 | /** @var Action $action */
82 | $action = \create(Action::class);
83 | $condition->actions()->save($action);
84 |
85 | $actualCondition = $condition->toCondition();
86 |
87 | $this->assertInstanceOf(TrueCondition::class, $actualCondition);
88 | $this->assertEquals($condition->id, $actualCondition->getId());
89 | $this->assertEquals($condition->is_inverted, $actualCondition->isInverted());
90 | $this->assertEquals($condition->parameters, $actualCondition->getParameters());
91 | $this->assertEquals([$action->toAction()], $actualCondition->getActions());
92 | }
93 |
94 | public function test_get_active_actions_filtered_not_active()
95 | {
96 | /** @var Condition $condition */
97 | $condition = \create(Condition::class);
98 | /** @var Action $action */
99 | $activeAction = \create(Action::class);
100 | $inactiveAction = \create(Action::class, ['ends_at' => Carbon::yesterday()]);
101 | $condition->actions()->saveMany([$activeAction, $inactiveAction]);
102 |
103 | $actions = $condition->getActiveActions();
104 |
105 | $this->assertEquals([$activeAction->id], $actions->pluck('id')->toArray());
106 | }
107 |
108 | public function test_get_active_actions_sorted_by_priority()
109 | {
110 | /** @var Condition $condition */
111 | $condition = \create(Condition::class);
112 | [$action10, $action5, $action20] = \createMany(Action::class, ['priority'], [[10], [5], [20]]);
113 | $condition->actions()->saveMany([$action10, $action5, $action20]);
114 |
115 | $actions = $condition->getActiveActions();
116 |
117 | $this->assertEquals(
118 | [$action5->id, $action10->id, $action20->id],
119 | $actions->pluck('id')->toArray()
120 | );
121 | }
122 |
123 | public function test_validate_name()
124 | {
125 | $this->expectException(ValidationException::class);
126 | \create(Condition::class, ['name' => '']);
127 | }
128 | }
129 |
--------------------------------------------------------------------------------
/src/Entities/Conditions/BaseCondition.php:
--------------------------------------------------------------------------------
1 | id = $id;
60 |
61 | return $this;
62 | }
63 |
64 | /**
65 | * Gets condition identifier.
66 | *
67 | * @return int
68 | */
69 | public function getId(): int
70 | {
71 | return $this->id;
72 | }
73 |
74 | /**
75 | * Sets parent identifier.
76 | *
77 | * @param int|null $parentId
78 | *
79 | * @return ConditionContract
80 | */
81 | public function setParentId(?int $parentId): ConditionContract
82 | {
83 | $this->parentId = $parentId;
84 |
85 | return $this;
86 | }
87 |
88 | /**
89 | * Gets parent identifier.
90 | *
91 | * @return int|null
92 | */
93 | public function getParentId(): ?int
94 | {
95 | return $this->parentId;
96 | }
97 |
98 | /**
99 | * Sets inverted flag.
100 | *
101 | * @param bool|null $isInverted
102 | *
103 | * @return ConditionContract
104 | */
105 | public function setIsInverted(?bool $isInverted): ConditionContract
106 | {
107 | $this->isInverted = $isInverted ?? false;
108 |
109 | return $this;
110 | }
111 |
112 | /**
113 | * Sets the priority.
114 | *
115 | * @param int $priority
116 | *
117 | * @return BaseCondition
118 | */
119 | public function setPriority(int $priority): self
120 | {
121 | $this->priority = $priority;
122 |
123 | return $this;
124 | }
125 |
126 | /**
127 | * Gets priority.
128 | *
129 | * @return int
130 | */
131 | public function getPriority(): int
132 | {
133 | return $this->priority;
134 | }
135 |
136 | /**
137 | * Sets the starts time.
138 | *
139 | * @param Carbon|null $startsAt
140 | *
141 | * @return BaseCondition
142 | */
143 | public function setStartsAt(?Carbon $startsAt): self
144 | {
145 | $this->startsAt = $startsAt;
146 |
147 | return $this;
148 | }
149 |
150 | /**
151 | * Gets starts time.
152 | *
153 | * @return Carbon|null
154 | */
155 | public function getStartsAt(): ?Carbon
156 | {
157 | return $this->startsAt;
158 | }
159 |
160 | /**
161 | * Sets the finishes time.
162 | *
163 | * @param Carbon|null $endsAt
164 | *
165 | * @return BaseCondition
166 | */
167 | public function setEndsAt(?Carbon $endsAt): self
168 | {
169 | $this->endsAt = $endsAt;
170 |
171 | return $this;
172 | }
173 |
174 | /**
175 | * Gets finishes time.
176 | *
177 | * @return Carbon|null
178 | */
179 | public function getEndsAt(): ?Carbon
180 | {
181 | return $this->endsAt;
182 | }
183 |
184 | /**
185 | * Determines whether this condition result should be inverted.
186 | *
187 | * @return bool
188 | */
189 | public function isInverted(): bool
190 | {
191 | return $this->isInverted;
192 | }
193 |
194 | /**
195 | * Sets actions.
196 | *
197 | * @param iterable|null $actions
198 | *
199 | * @return ConditionContract
200 | */
201 | public function setActions(?iterable $actions): ConditionContract
202 | {
203 | $this->actions = \collect($actions)->toArray();
204 |
205 | return $this;
206 | }
207 |
208 | /**
209 | * Gets the condition actions.
210 | *
211 | * @return iterable|ActionContract[]
212 | */
213 | public function getActions(): iterable
214 | {
215 | return $this->actions;
216 | }
217 |
218 | /**
219 | * Adds actions to actions queue.
220 | *
221 | * @param ActionContract ...$actions
222 | */
223 | protected function addActions(ActionContract ...$actions)
224 | {
225 | $this->actions = \array_merge(
226 | (array) $this->actions,
227 | $actions
228 | );
229 | }
230 |
231 | /**
232 | * Adds actions to actions queue.
233 | *
234 | * @param ActionContract ...$actions
235 | */
236 | protected function prependActions(ActionContract ...$actions)
237 | {
238 | $this->actions = \array_merge(
239 | $actions,
240 | (array) $this->actions
241 | );
242 | }
243 |
244 | /**
245 | * Sets the condition parameters.
246 | *
247 | * @param array $parameters
248 | *
249 | * @return ConditionContract
250 | */
251 | public function setParameters(?array $parameters): ConditionContract
252 | {
253 | $this->parameters = $parameters ?? [];
254 |
255 | return $this;
256 | }
257 |
258 | /**
259 | * Gets condition parameters.
260 | *
261 | * @return iterable
262 | */
263 | public function getParameters(): iterable
264 | {
265 | return $this->parameters;
266 | }
267 |
268 | public function toArray()
269 | {
270 | return [
271 | 'id' => $this,
272 | ];
273 | }
274 |
275 | protected function expectedResult(): bool
276 | {
277 | return !$this->isInverted();
278 | }
279 | }
280 |
--------------------------------------------------------------------------------
/docs/openapi.yaml:
--------------------------------------------------------------------------------
1 | openapi: 3.0.1
2 |
3 | info:
4 | description: "Put your business logic to external storage."
5 | version: "1.0.0-alpha.1"
6 | title: "Conditional Actions API"
7 | contact:
8 | email: "a.paramonov@corp.my.com"
9 | license:
10 | name: "MIT"
11 | servers:
12 | - url: "/api/v1/conditional-actions/"
13 |
14 | paths:
15 | /conditions:
16 | post:
17 | tags:
18 | - "Conditions"
19 | summary: "Add a new condition to the store"
20 | operationId: "postCondition"
21 | requestBody:
22 | $ref: "#/components/requestBodies/ConditionRequest"
23 | responses:
24 | 200:
25 | $ref: "#/components/responses/ConditionResponse"
26 | 422:
27 | description: "Validation exception"
28 |
29 | /conditions/{conditionId}:
30 | parameters:
31 | - $ref: "#/components/parameters/conditionId"
32 |
33 | put:
34 | tags:
35 | - "Conditions"
36 | summary: "Update condition"
37 | operationId: "putCondition"
38 | requestBody:
39 | $ref: "#/components/requestBodies/ConditionRequest"
40 | responses:
41 | 200:
42 | $ref: "#/components/responses/ConditionResponse"
43 | 404:
44 | description: "Condition not found"
45 | 422:
46 | description: "Validation exception"
47 |
48 | get:
49 | tags:
50 | - "Conditions"
51 | summary: "Get condition"
52 | operationId: "getCondition"
53 | responses:
54 | 200:
55 | $ref: "#/components/responses/ConditionResponse"
56 | 404:
57 | description: "Condition not found"
58 |
59 | delete:
60 | tags:
61 | - "Conditions"
62 | summary: "Delete condition"
63 | operationId: "deleteCondition"
64 | responses:
65 | 200:
66 | $ref: "#/components/responses/ConditionResponse"
67 | 404:
68 | description: "Condition not found"
69 |
70 | /actions:
71 | post:
72 | tags:
73 | - "Actions"
74 | summary: "Add a new action to the condition"
75 | operationId: "postAction"
76 | requestBody:
77 | $ref: "#/components/requestBodies/ActionRequest"
78 | responses:
79 | 200:
80 | $ref: "#/components/responses/ActionResponse"
81 | 422:
82 | description: "Validation exception"
83 |
84 | /actions/{actionId}:
85 | parameters:
86 | - $ref: "#/components/parameters/actionId"
87 |
88 | put:
89 | tags:
90 | - "Actions"
91 | summary: "Update an action"
92 | operationId: "putAction"
93 | requestBody:
94 | $ref: "#/components/requestBodies/ActionRequest"
95 | responses:
96 | 200:
97 | $ref: "#/components/responses/ActionResponse"
98 | 404:
99 | description: "Action not found"
100 | 422:
101 | description: "Validation exception"
102 |
103 | get:
104 | tags:
105 | - "Actions"
106 | summary: "Get action"
107 | operationId: "getAction"
108 | responses:
109 | 200:
110 | $ref: "#/components/responses/ActionResponse"
111 | 404:
112 | description: "Action not found"
113 |
114 | delete:
115 | tags:
116 | - "Actions"
117 | summary: "Delete action"
118 | operationId: "deleteAction"
119 | responses:
120 | 200:
121 | $ref: "#/components/responses/ActionResponse"
122 | 404:
123 | description: "Action not found"
124 |
125 | components:
126 | requestBodies:
127 | ConditionRequest:
128 | content:
129 | application/json:
130 | schema:
131 | $ref: "#/components/schemas/Condition"
132 |
133 | ActionRequest:
134 | content:
135 | application/json:
136 | schema:
137 | $ref: "#/components/schemas/Action"
138 |
139 | schemas:
140 |
141 | # Models
142 | Condition:
143 | allOf:
144 | - type: "object"
145 | required:
146 | - "name"
147 | properties:
148 | name:
149 | type: "string"
150 | example: "AllOfCondition"
151 | is_inverted:
152 | type: "boolean"
153 | default: false
154 | example: false
155 | parameters:
156 | type: "object"
157 | - $ref: "#/components/schemas/priority"
158 | - $ref: "#/components/schemas/startsAt"
159 | - $ref: "#/components/schemas/endsAt"
160 |
161 | ConditionFull:
162 | allOf:
163 | - $ref: "#/components/schemas/Condition"
164 | - type: "object"
165 | properties:
166 | id:
167 | type: "integer"
168 | description: "Condition identifier"
169 | example: 1
170 | target_type:
171 | type: "string"
172 | description: "Type of target"
173 | example: "App\\Models\\Toy"
174 | target_id:
175 | type: "integer"
176 |
177 | Action:
178 | allOf:
179 | - type: "object"
180 | required:
181 | - "name"
182 | properties:
183 | name:
184 | type: "string"
185 | example: "DiscountAction"
186 | parameters:
187 | description: "Action parameters"
188 | type: "object"
189 | example:
190 | discount: 10
191 | - $ref: "#/components/schemas/priority"
192 | - $ref: "#/components/schemas/startsAt"
193 | - $ref: "#/components/schemas/endsAt"
194 |
195 | ActionFull:
196 | allOf:
197 | - $ref: "#/components/schemas/Action"
198 | - type: "object"
199 | properties:
200 | id:
201 | type: "integer"
202 | description: "Condition identifier"
203 | example: 1
204 |
205 | # Properties
206 | priority:
207 | type: "object"
208 | properties:
209 | priority:
210 | type: "integer"
211 | default: "0"
212 | example: 1
213 |
214 | startsAt:
215 | type: "object"
216 | properties:
217 | starts_at:
218 | description: "Start time"
219 | type: "string"
220 | format: "date-time"
221 | nullable: true
222 | default: ""
223 | example: "2019-05-01 10:00:00"
224 |
225 | endsAt:
226 | type: "object"
227 | properties:
228 | ends_at:
229 | description: "Finish time"
230 | type: "string"
231 | format: "date-time"
232 | default: ""
233 | nullable: true
234 | example: "2019-05-10 10:00:00"
235 |
236 |
237 | parameters:
238 | conditionId:
239 | name: "conditionId"
240 | in: "path"
241 | required: true
242 | description: "The condition identifier"
243 | schema:
244 | type: "integer"
245 |
246 | actionId:
247 | name: "actionId"
248 | in: "path"
249 | required: true
250 | description: "The action identifier"
251 | schema:
252 | type: "integer"
253 |
254 | responses:
255 | ConditionResponse:
256 | description: "Condition object"
257 | content:
258 | application/json:
259 | schema:
260 | $ref: "#/components/schemas/ConditionFull"
261 |
262 | ActionResponse:
263 | description: "Action object"
264 | content:
265 | application/json:
266 | schema:
267 | $ref: "#/components/schemas/ActionFull"
268 |
--------------------------------------------------------------------------------
/tests/Feature/Http/Conditions/ConditionsControllerTest.php:
--------------------------------------------------------------------------------
1 | conditionRepositoryMock = Mockery::mock(ConditionRepository::class);
26 | $this->app->instance(ConditionRepository::class, $this->conditionRepositoryMock);
27 |
28 | ConditionalActions::routes();
29 | }
30 |
31 | public function test_store_success()
32 | {
33 | $params = ['name' => 'TrueCondition'];
34 | $condition = $this->makeCondition();
35 | $this->conditionRepositoryMock
36 | ->shouldReceive('store')
37 | ->once()
38 | ->with($params)
39 | ->andReturn($condition);
40 |
41 | $response = $this->postJson(\route('conditions.store'), $params);
42 |
43 | $this->assertResponse($response, $condition);
44 | }
45 |
46 | /**
47 | * @dataProvider provider_store_validation
48 | *
49 | * @param array $params
50 | * @param string ...$errorKeys
51 | */
52 | public function test_store_validation(array $params = [], string ...$errorKeys)
53 | {
54 | $request = $this->postJson(\route('conditions.store', $params));
55 | $request->assertStatus(Response::HTTP_UNPROCESSABLE_ENTITY);
56 | $this->assertValidationErrors($errorKeys, $request);
57 | }
58 |
59 | public function provider_store_validation(): array
60 | {
61 | return [
62 | 'name omitted' => [[], 'name'],
63 | 'unknown name' => [['name' => 'UnknownCondition'], 'name'],
64 | 'is_inverted is not boolean' => [['name' => 'TrueCondition', 'is_inverted' => 'string'], 'is_inverted'],
65 | 'parameters is not array' => [['name' => 'TrueCondition', 'parameters' => 'string'], 'parameters'],
66 | 'priority is not array' => [['name' => 'TrueCondition', 'priority' => 'string'], 'priority'],
67 | 'starts_at is not a date' => [['name' => 'TrueCondition', 'starts_at' => 'string'], 'starts_at'],
68 | 'ends_at is not a date' => [['name' => 'TrueCondition', 'ends_at' => 'string'], 'ends_at'],
69 | ];
70 | }
71 |
72 | public function test_update_success()
73 | {
74 | $params = ['name' => 'TrueCondition'];
75 | $condition = $this->makeCondition();
76 | $this->conditionRepositoryMock
77 | ->shouldReceive('update')
78 | ->once()
79 | ->with($condition->getId(), $params)
80 | ->andReturn($condition);
81 |
82 | $response = $this->putJson(\route('conditions.update', $condition->getId()), $params);
83 |
84 | $this->assertResponse($response, $condition);
85 | }
86 |
87 | public function test_update_non_existent()
88 | {
89 | $params = ['name' => 'TrueCondition'];
90 | $exception = new ConditionNotFoundException('Condition not found');
91 | $this->conditionRepositoryMock
92 | ->shouldReceive('update')
93 | ->once()
94 | ->with(5, $params)
95 | ->andThrow($exception);
96 |
97 | $response = $this->putJson(\route('conditions.update', 5), $params);
98 |
99 | $response->assertStatus(Response::HTTP_NOT_FOUND);
100 | $this->assertEquals($exception->getMessage(), $response->json('error.message'));
101 | }
102 |
103 | public function test_show_success()
104 | {
105 | $params = ['name' => 'TrueCondition'];
106 | $condition = $this->makeCondition();
107 | $this->conditionRepositoryMock
108 | ->shouldReceive('find')
109 | ->once()
110 | ->with($condition->getId())
111 | ->andReturn($condition);
112 |
113 | $response = $this->getJson(\route('conditions.show', $condition->getId()), $params);
114 |
115 | $this->assertResponse($response, $condition);
116 | }
117 |
118 | public function test_destroy_success()
119 | {
120 | $condition = $this->makeCondition();
121 | $this->conditionRepositoryMock
122 | ->shouldReceive('destroy')
123 | ->once()
124 | ->with($condition->getId())
125 | ->andReturn($condition);
126 |
127 | $response = $this->deleteJson(\route('conditions.destroy', $condition->getId()));
128 |
129 | $this->assertResponse($response, $condition);
130 | }
131 |
132 | public function test_destroy_non_existent()
133 | {
134 | $exception = new ConditionNotFoundException('Condition not found');
135 | $this->conditionRepositoryMock
136 | ->shouldReceive('destroy')
137 | ->once()
138 | ->with(5)
139 | ->andThrow($exception);
140 |
141 | $response = $this->deleteJson(\route('conditions.destroy', 5));
142 |
143 | $response->assertStatus(Response::HTTP_NOT_FOUND);
144 | $this->assertEquals($exception->getMessage(), $response->json('error.message'));
145 | }
146 |
147 | /**
148 | * @dataProvider provider_store_validation
149 | *
150 | * @param array $params
151 | * @param string ...$errorKeys
152 | */
153 | public function test_update_validation(array $params = [], string ...$errorKeys)
154 | {
155 | $conditionId = 2;
156 | $request = $this->putJson(\route('conditions.update', $conditionId), $params);
157 | $request->assertStatus(Response::HTTP_UNPROCESSABLE_ENTITY);
158 | $this->assertValidationErrors($errorKeys, $request);
159 | }
160 |
161 | /**
162 | * @param array $errorKeys
163 | * @param TestResponse $request
164 | */
165 | public function assertValidationErrors(array $errorKeys, TestResponse $request): void
166 | {
167 | $this->assertEquals(
168 | \collect($request->json('errors'))->keys()->sort()->values()->toArray(),
169 | \collect($errorKeys)->sort()->values()->toArray()
170 | );
171 | }
172 |
173 | private function makeCondition(): BaseCondition
174 | {
175 | $condition = (new TrueCondition())
176 | ->setId(3)
177 | ->setPriority(5)
178 | ->setIsInverted(true)
179 | ->setStartsAt(Carbon::yesterday())
180 | ->setEndsAt(Carbon::tomorrow());
181 |
182 | return $condition;
183 | }
184 |
185 | private function assertResponse(TestResponse $response, BaseCondition $condition): void
186 | {
187 | $response->assertOk();
188 | $this->assertEquals('TrueCondition', $response->json('data.name'));
189 | $this->assertEquals($condition->getId(), $response->json('data.id'));
190 | $this->assertEquals($condition->isInverted(), $response->json('data.is_inverted'));
191 | $this->assertEquals($condition->getParameters(), $response->json('data.parameters'));
192 | $this->assertEquals($condition->getPriority(), $response->json('data.priority'));
193 | $this->assertEquals($condition->getStartsAt()->toIso8601String(), $response->json('data.starts_at'));
194 | $this->assertEquals($condition->getEndsAt()->toIso8601String(), $response->json('data.ends_at'));
195 | }
196 | }
197 |
--------------------------------------------------------------------------------
/tests/Feature/Http/Conditions/ActionsControllerTest.php:
--------------------------------------------------------------------------------
1 | 'UpdateStateAttributeAction', 'condition_id' => 10];
22 |
23 | protected function setUp(): void
24 | {
25 | parent::setUp();
26 |
27 | $this->actionRepositoryMock = Mockery::mock(ActionRepository::class);
28 | $this->app->instance(ActionRepository::class, $this->actionRepositoryMock);
29 |
30 | ConditionalActions::routes();
31 | }
32 |
33 | public function test_store_success()
34 | {
35 | $action = $this->makeAction();
36 | $this->actionRepositoryMock
37 | ->shouldReceive('store')
38 | ->once()
39 | ->with($this->validParams)
40 | ->andReturn($action);
41 |
42 | $response = $this->postJson(\route('actions.store'), $this->validParams);
43 |
44 | $this->assertResponse($response, $action);
45 | }
46 |
47 | /**
48 | * @dataProvider provider_store_validation
49 | *
50 | * @param array $params
51 | * @param string ...$errorKeys
52 | */
53 | public function test_store_validation(array $params = [], string ...$errorKeys)
54 | {
55 | $request = $this->postJson(\route('actions.store', $params));
56 | $request->assertStatus(Response::HTTP_UNPROCESSABLE_ENTITY);
57 | $this->assertValidationErrors($errorKeys, $request);
58 | }
59 |
60 | public function provider_store_validation(): array
61 | {
62 | return [
63 | 'name omitted' => [
64 | ['condition_id' => 10],
65 | 'name',
66 | ],
67 | 'unknown name' => [
68 | ['condition_id' => 10, 'name' => 'UnknownAction'],
69 | 'name',
70 | ],
71 | 'condition_id omitted' => [
72 | ['name' => 'UpdateStateAttributeAction'],
73 | 'condition_id',
74 | ],
75 | 'condition_id is not numeric' => [
76 | ['condition_id' => 'string', 'name' => 'UpdateStateAttributeAction'],
77 | 'condition_id',
78 | ],
79 | 'parameters is not array' => [
80 | ['condition_id' => 10, 'name' => 'UpdateStateAttributeAction', 'parameters' => 'string'],
81 | 'parameters',
82 | ],
83 | 'priority is not array' => [
84 | ['condition_id' => 10, 'name' => 'UpdateStateAttributeAction', 'priority' => 'string'],
85 | 'priority',
86 | ],
87 | 'starts_at is not a date' => [
88 | ['condition_id' => 10, 'name' => 'UpdateStateAttributeAction', 'starts_at' => 'string'],
89 | 'starts_at',
90 | ],
91 | 'ends_at is not a date' => [
92 | ['condition_id' => 10, 'name' => 'UpdateStateAttributeAction', 'ends_at' => 'string'],
93 | 'ends_at',
94 | ],
95 | ];
96 | }
97 |
98 | public function test_update_success()
99 | {
100 | $action = $this->makeAction();
101 | $this->actionRepositoryMock
102 | ->shouldReceive('update')
103 | ->once()
104 | ->with($action->getId(), $this->validParams)
105 | ->andReturn($action);
106 |
107 | $response = $this->putJson(\route('actions.update', $action->getId()), $this->validParams);
108 |
109 | $this->assertResponse($response, $action);
110 | }
111 |
112 | public function test_update_non_existent()
113 | {
114 | $exception = new ActionNotFoundException('Action not found');
115 | $this->actionRepositoryMock
116 | ->shouldReceive('update')
117 | ->once()
118 | ->with(5, $this->validParams)
119 | ->andThrow($exception);
120 |
121 | $response = $this->putJson(\route('actions.update', 5), $this->validParams);
122 |
123 | $response->assertStatus(Response::HTTP_NOT_FOUND);
124 | $this->assertEquals($exception->getMessage(), $response->json('error.message'));
125 | }
126 |
127 | public function test_show_success()
128 | {
129 | $action = $this->makeAction();
130 | $this->actionRepositoryMock
131 | ->shouldReceive('find')
132 | ->once()
133 | ->with($action->getId())
134 | ->andReturn($action);
135 |
136 | $response = $this->getJson(\route('actions.show', $action->getId()), $this->validParams);
137 |
138 | $this->assertResponse($response, $action);
139 | }
140 |
141 | public function test_destroy_success()
142 | {
143 | $action = $this->makeAction();
144 | $this->actionRepositoryMock
145 | ->shouldReceive('destroy')
146 | ->once()
147 | ->with($action->getId())
148 | ->andReturn($action);
149 |
150 | $response = $this->deleteJson(\route('actions.destroy', $action->getId()));
151 |
152 | $this->assertResponse($response, $action);
153 | }
154 |
155 | public function test_destroy_non_existent()
156 | {
157 | $exception = new ActionNotFoundException('Action not found');
158 | $this->actionRepositoryMock
159 | ->shouldReceive('destroy')
160 | ->once()
161 | ->with(5)
162 | ->andThrow($exception);
163 |
164 | $response = $this->deleteJson(\route('actions.destroy', 5));
165 |
166 | $response->assertStatus(Response::HTTP_NOT_FOUND);
167 | $this->assertEquals($exception->getMessage(), $response->json('error.message'));
168 | }
169 |
170 | /**
171 | * @dataProvider provider_store_validation
172 | *
173 | * @param array $params
174 | * @param string ...$errorKeys
175 | */
176 | public function test_update_validation(array $params = [], string ...$errorKeys)
177 | {
178 | $actionId = 2;
179 | $request = $this->putJson(\route('actions.update', $actionId), $params);
180 | $request->assertStatus(Response::HTTP_UNPROCESSABLE_ENTITY);
181 | $this->assertValidationErrors($errorKeys, $request);
182 | }
183 |
184 | /**
185 | * @param array $errorKeys
186 | * @param TestResponse $request
187 | */
188 | public function assertValidationErrors(array $errorKeys, TestResponse $request): void
189 | {
190 | $this->assertEquals(
191 | \collect($request->json('errors'))->keys()->sort()->values()->toArray(),
192 | \collect($errorKeys)->sort()->values()->toArray()
193 | );
194 | }
195 |
196 | private function makeAction(): BaseAction
197 | {
198 | $action = (new UpdateStateAttributeAction())
199 | ->setId(3)
200 | ->setPriority(5)
201 | ->setParameters(['param1' => 'value'])
202 | ->setStartsAt(Carbon::yesterday())
203 | ->setEndsAt(Carbon::tomorrow());
204 |
205 | return $action;
206 | }
207 |
208 | private function assertResponse(TestResponse $response, BaseAction $action): void
209 | {
210 | $response->assertOk();
211 | $this->assertEquals('UpdateStateAttributeAction', $response->json('data.name'));
212 | $this->assertEquals($action->getId(), $response->json('data.id'));
213 | $this->assertEquals($action->getParameters(), $response->json('data.parameters'));
214 | $this->assertEquals($action->getPriority(), $response->json('data.priority'));
215 | $this->assertEquals($action->getStartsAt()->toIso8601String(), $response->json('data.starts_at'));
216 | $this->assertEquals($action->getEndsAt()->toIso8601String(), $response->json('data.ends_at'));
217 | }
218 | }
219 |
--------------------------------------------------------------------------------
/.php_cs.dist.php:
--------------------------------------------------------------------------------
1 | exclude(['bootstrap', 'database', 'storage', 'tests', 'vendor', 'app/Api'])
11 | ->notName('_ide_helper*.php')
12 | ->notName('server.php')
13 | ->in(__DIR__)
14 | ;
15 |
16 | return PhpCsFixer\Config::create()
17 | ->setRules([
18 | '@PSR2' => true,
19 | 'align_multiline_comment' => true,
20 | 'array_indentation' => true,
21 | 'array_syntax' => ['syntax' => 'short'],
22 | 'blank_line_after_opening_tag' => true,
23 | 'blank_line_before_statement' => ['statements' => ['break', 'continue', 'declare', 'die', 'do', 'exit', 'for', 'foreach', 'goto', 'if', 'include', 'include_once', 'require', 'require_once', 'return', 'switch', 'throw', 'try', 'while', 'yield']],
24 | 'cast_spaces' => true,
25 | 'class_attributes_separation' => true,
26 | 'combine_consecutive_issets' => true,
27 | 'combine_consecutive_unsets' => true,
28 | 'compact_nullable_typehint' => true,
29 | 'dir_constant' => true,
30 | 'ereg_to_preg' => true,
31 | 'explicit_indirect_variable' => true,
32 | 'explicit_string_variable' => true,
33 | 'function_to_constant' => true,
34 | 'function_typehint_space' => true,
35 | 'include' => true,
36 | 'linebreak_after_opening_tag' => true,
37 | 'magic_constant_casing' => true,
38 | 'mb_str_functions' => true,
39 | 'method_chaining_indentation' => true,
40 | 'modernize_types_casting' => true,
41 | 'multiline_whitespace_before_semicolons' => true,
42 | 'native_function_casing' => true,
43 | 'native_function_invocation' => true,
44 | 'new_with_braces' => true,
45 | 'no_alias_functions' => true,
46 | 'no_alternative_syntax' => true,
47 | 'no_blank_lines_after_class_opening' => true,
48 | 'no_blank_lines_after_phpdoc' => true,
49 | 'no_empty_comment' => true,
50 | 'no_empty_phpdoc' => true,
51 | 'no_empty_statement' => true,
52 | 'no_extra_blank_lines' => true,
53 | 'no_homoglyph_names' => true,
54 | 'no_leading_import_slash' => true,
55 | 'no_leading_namespace_whitespace' => true,
56 | 'no_mixed_echo_print' => true,
57 | 'no_multiline_whitespace_around_double_arrow' => true,
58 | 'no_null_property_initialization' => true,
59 | 'no_php4_constructor' => true,
60 | 'no_short_bool_cast' => true,
61 | 'no_short_echo_tag' => true,
62 | 'no_singleline_whitespace_before_semicolons' => true,
63 | 'no_spaces_around_offset' => true,
64 | 'no_superfluous_elseif' => true,
65 | 'no_trailing_comma_in_list_call' => true,
66 | 'no_trailing_comma_in_singleline_array' => true,
67 | 'no_unneeded_control_parentheses' => true,
68 | 'no_unneeded_curly_braces' => true,
69 | 'no_unneeded_final_method' => true,
70 | 'no_unset_on_property' => true,
71 | 'no_unused_imports' => true,
72 | 'no_useless_else' => true,
73 | 'no_useless_return' => true,
74 | 'no_whitespace_before_comma_in_array' => true,
75 | 'no_whitespace_in_blank_line' => true,
76 | 'non_printable_character' => ['use_escape_sequences_in_strings' => true],
77 | 'normalize_index_brace' => true,
78 | 'object_operator_without_whitespace' => true,
79 | 'phpdoc_add_missing_param_annotation' => true,
80 | 'phpdoc_annotation_without_dot' => true,
81 | 'phpdoc_indent' => true,
82 | 'phpdoc_inline_tag' => true,
83 | 'phpdoc_no_access' => true,
84 | 'phpdoc_no_alias_tag' => true,
85 | 'phpdoc_no_empty_return' => true,
86 | 'phpdoc_no_package' => true,
87 | 'phpdoc_no_useless_inheritdoc' => true,
88 | 'phpdoc_order' => true,
89 | 'phpdoc_return_self_reference' => true,
90 | 'phpdoc_scalar' => true,
91 | 'phpdoc_separation' => true,
92 | 'phpdoc_single_line_var_spacing' => true,
93 | 'phpdoc_trim' => true,
94 | 'phpdoc_trim_consecutive_blank_line_separation' => true,
95 | 'phpdoc_types' => true,
96 | 'phpdoc_var_without_name' => true,
97 | 'psr4' => true,
98 | 'random_api_migration' => true,
99 | 'return_assignment' => true,
100 | 'return_type_declaration' => true,
101 | 'self_accessor' => true,
102 | 'semicolon_after_instruction' => true,
103 | 'set_type_to_cast' => true,
104 | 'short_scalar_cast' => true,
105 | 'simplified_null_return' => true,
106 | 'single_blank_line_before_namespace' => true,
107 | 'single_line_comment_style' => true,
108 | 'single_quote' => true,
109 | 'space_after_semicolon' => ['remove_in_empty_for_expressions' => true],
110 | 'standardize_increment' => true,
111 | 'standardize_not_equals' => true,
112 | 'strict_param' => true,
113 | 'ternary_operator_spaces' => true,
114 | 'ternary_to_null_coalescing' => true,
115 | 'trailing_comma_in_multiline_array' => true,
116 | 'trim_array_spaces' => true,
117 | 'unary_operator_spaces' => true,
118 | 'whitespace_after_comma_in_array' => true,
119 | 'yoda_style' => true,
120 | ])
121 | ->setFinder($finder);
122 |
123 | /*
124 | This document has been generated with
125 | https://mlocati.github.io/php-cs-fixer-configurator/
126 | you can change this configuration by importing this YAML code:
127 |
128 | fixerSets:
129 | - '@PSR2'
130 | fixers:
131 | align_multiline_comment: true
132 | array_indentation: true
133 | array_syntax:
134 | syntax: short
135 | blank_line_after_opening_tag: true
136 | blank_line_before_statement:
137 | statements:
138 | - break
139 | - continue
140 | - declare
141 | - die
142 | - do
143 | - exit
144 | - for
145 | - foreach
146 | - goto
147 | - if
148 | - include
149 | - include_once
150 | - require
151 | - require_once
152 | - return
153 | - switch
154 | - throw
155 | - try
156 | - while
157 | - yield
158 | cast_spaces: true
159 | class_attributes_separation: true
160 | combine_consecutive_issets: true
161 | combine_consecutive_unsets: true
162 | compact_nullable_typehint: true
163 | dir_constant: true
164 | ereg_to_preg: true
165 | explicit_indirect_variable: true
166 | explicit_string_variable: true
167 | function_to_constant: true
168 | function_typehint_space: true
169 | include: true
170 | linebreak_after_opening_tag: true
171 | magic_constant_casing: true
172 | mb_str_functions: true
173 | method_chaining_indentation: true
174 | modernize_types_casting: true
175 | multiline_whitespace_before_semicolons: true
176 | native_function_casing: true
177 | native_function_invocation: true
178 | new_with_braces: true
179 | no_alias_functions: true
180 | no_alternative_syntax: true
181 | no_blank_lines_after_class_opening: true
182 | no_blank_lines_after_phpdoc: true
183 | no_empty_comment: true
184 | no_empty_phpdoc: true
185 | no_empty_statement: true
186 | no_extra_blank_lines: true
187 | no_homoglyph_names: true
188 | no_leading_import_slash: true
189 | no_leading_namespace_whitespace: true
190 | no_mixed_echo_print: true
191 | no_multiline_whitespace_around_double_arrow: true
192 | no_null_property_initialization: true
193 | no_php4_constructor: true
194 | no_short_bool_cast: true
195 | no_short_echo_tag: true
196 | no_singleline_whitespace_before_semicolons: true
197 | no_spaces_around_offset: true
198 | no_superfluous_elseif: true
199 | no_trailing_comma_in_list_call: true
200 | no_trailing_comma_in_singleline_array: true
201 | no_unneeded_control_parentheses: true
202 | no_unneeded_curly_braces: true
203 | no_unneeded_final_method: true
204 | no_unset_on_property: true
205 | no_unused_imports: true
206 | no_useless_else: true
207 | no_useless_return: true
208 | no_whitespace_before_comma_in_array: true
209 | no_whitespace_in_blank_line: true
210 | non_printable_character:
211 | use_escape_sequences_in_strings: true
212 | normalize_index_brace: true
213 | object_operator_without_whitespace: true
214 | phpdoc_add_missing_param_annotation: true
215 | phpdoc_annotation_without_dot: true
216 | phpdoc_indent: true
217 | phpdoc_inline_tag: true
218 | phpdoc_no_access: true
219 | phpdoc_no_alias_tag: true
220 | phpdoc_no_empty_return: true
221 | phpdoc_no_package: true
222 | phpdoc_no_useless_inheritdoc: true
223 | phpdoc_order: true
224 | phpdoc_return_self_reference: true
225 | phpdoc_scalar: true
226 | phpdoc_separation: true
227 | phpdoc_single_line_var_spacing: true
228 | phpdoc_trim: true
229 | phpdoc_trim_consecutive_blank_line_separation: true
230 | phpdoc_types: true
231 | phpdoc_var_without_name: true
232 | psr4: true
233 | random_api_migration: true
234 | return_assignment: true
235 | return_type_declaration: true
236 | self_accessor: true
237 | semicolon_after_instruction: true
238 | set_type_to_cast: true
239 | short_scalar_cast: true
240 | simplified_null_return: true
241 | single_blank_line_before_namespace: true
242 | single_line_comment_style: true
243 | single_quote: true
244 | space_after_semicolon:
245 | remove_in_empty_for_expressions: true
246 | standardize_increment: true
247 | standardize_not_equals: true
248 | strict_param: true
249 | ternary_operator_spaces: true
250 | ternary_to_null_coalescing: true
251 | trailing_comma_in_multiline_array: true
252 | trim_array_spaces: true
253 | unary_operator_spaces: true
254 | whitespace_after_comma_in_array: true
255 | yoda_style: true
256 | risky: true
257 |
258 | */
259 |
--------------------------------------------------------------------------------
/readme.md:
--------------------------------------------------------------------------------
1 | # Conditional Actions
2 |
3 | 
4 |
5 | This package allows configuring business logic by API without changing your code.
6 | This is helpful when you don`t know specific conditions because they defined dynamically by your managers/users/etc.
7 |
8 | ## How it works
9 |
10 | Codebase provides predefined conditions, actions, targets and API for mix them into business logic to end users.
11 | Objects:
12 | * `Target` - provides all necessary data for conditions and actions;
13 | * `State` - key-value pairs. Actions should update state when applying;
14 | * `Condition` - condition has method `check`, it returns succeed it or not (bool);
15 | * `Action` - action has method `apply`, it change `State` or make any other actions and returns changed `State`;
16 |
17 | Lifecycle:
18 | * `Target` creates a `State` object;
19 | * `Target` gets all related active `Condition` sorted by priority and run the check on each condition;
20 | * For succeeded `Condition`, `Condition` gets all related actions and apply them to the `State`;
21 | * `Action` returns changed `State` which used in next conditions or actions;
22 | * After checking all `Condition`, `Target` gets new `State` to `applyState` method. You can use its state as you needed.
23 |
24 | ## Get started
25 |
26 | For example, you have a shop that sells toys. Your marketing runs some promotions for specific toys.
27 | If a user buys chests in the past or today is his birthday, "Barbie doll" should have a 10% discount.
28 | Promotion starts at 2019/05/01 at 00:00 and finishes at 2019/05/01 at 23:59.
29 |
30 | You should create:
31 |
32 | Conditions:
33 | * User bought toys in the past (`HasPaidToysCondition`)
34 | * Today is his birthday (`TodayIsBirthdayCondition`)
35 |
36 | Action:
37 | * "Barbie doll" should have a 10% discount (`DiscountAction`)
38 |
39 | For time restrictions (Promotion starts at 2019/05/01 00:00 and finishes at 2019/05/01 23:59) you can use fields `starts_at` and `ends_at`.
40 |
41 | Both conditions should be succeeded. You can use `AllOfCondition` condition from the package.
42 |
43 | Marketing can use it for promotions without changing your code.
44 |
45 | The final scheme for promotion:
46 |
47 | ```
48 | ■ AllOfCondition (condition)
49 | │ # fields: ['id' => 1, 'starts_at' => '2019-05-01 00:00:00', 'ends_at' => '2019-05-01 23:59:59']
50 | │ ║
51 | │ ╚═» ░ DiscountAction (action)
52 | │ # fields: ['parameters' => ['discount' => 10]]
53 | │
54 | ├─── ■ TodayIsBirthdayCondition (condition)
55 | │ # fields: ['parent_id' => 1]
56 | │
57 | └─── ■ HasPaidToysCondition (condition)
58 | # fields: ['parent_id' => 1, 'parameters' => ['toy_id' => 5]]
59 | ```
60 |
61 | Let`s go to implementation!
62 |
63 | ### Install package
64 | ```bash
65 | composer require my-com/laravel-conditional-actions
66 | ```
67 |
68 | ### Laravel
69 | #### For versions < 5.5:
70 | Add package service provider to `config/app.php`:
71 | ```php
72 | return [
73 | // ...
74 | 'providers' => [
75 | // ...
76 | ConditionalActions\ConditionalActionsServiceProvider::class,
77 | ],
78 | // ...
79 | ```
80 | #### For laravel >= 5.5
81 | Laravel 5.5 uses Package Auto-Discovery, so doesn't require you to manually add the ServiceProvider
82 |
83 | ### Lumen
84 | Register service provider and config in `app.php`:
85 | ```php
86 | $app->configure('conditional-actions');
87 | $app->register(ConditionalActions\ConditionalActionsServiceProvider::class);
88 | ```
89 |
90 | ### Add migrations
91 | ```bash
92 | php artisan ca:tables
93 | php artisan migrate
94 | php artisan vendor:publish --provider="ConditionalActions\ConditionalActionsServiceProvider"
95 | ```
96 |
97 | > Command options:
98 | > ```bash
99 | > Description:
100 | > Create a migration for the conditional actions database tables
101 | >
102 | > Usage:
103 | > ca:tables [options]
104 | >
105 | > Options:
106 | > --migrations-path[=MIGRATIONS-PATH] Path to migrations directory (relative to framework base path) [default: "database/migrations"]
107 | > ```
108 |
109 | ### Implement Target
110 |
111 | Target is an object that provides all necessary data for conditions and actions. It can be also an eloquent model.
112 |
113 | Since `Toy` - object for conditional actions, it should use `EloquentTarget` trait (trait has relationships and some method to get conditions for model)
114 |
115 | ```php
116 | class ToysPriceTarget implements TargetContract
117 | {
118 | use RunsConditionalActions;
119 |
120 | /** @var Toy */
121 | public $toy;
122 |
123 | /** @var User */
124 | public $user;
125 |
126 | public $finalPrice;
127 |
128 | public function __construct(Toy $toy, User $user)
129 | {
130 | $this->toy = $toy;
131 | $this->user = $user;
132 | }
133 |
134 | /**
135 | * Gets state from target.
136 | *
137 | * @return StateContract
138 | */
139 | public function getInitialState(): StateContract
140 | {
141 | return $this->newState([
142 | 'price' => $this->toy->price,
143 | ]);
144 | }
145 |
146 | /**
147 | * Sets the state to the target.
148 | *
149 | * @param StateContract $state
150 | */
151 | public function applyState(StateContract $state): void
152 | {
153 | $this->finalPrice = $state->getAttribute('price');
154 | }
155 |
156 | /**
157 | * Gets root target conditions.
158 | *
159 | * @return iterable|ConditionContract[]
160 | */
161 | public function getRootConditions(): iterable
162 | {
163 | return $this->toy->getRootConditions();
164 | }
165 |
166 | /**
167 | * Gets children target conditions.
168 | *
169 | * @param int $parentId
170 | *
171 | * @return iterable|ConditionContract[]
172 | */
173 | public function getChildrenConditions(int $parentId): iterable
174 | {
175 | return $this->toy->getChildrenConditions($parentId);
176 | }
177 | }
178 | ```
179 |
180 | ### Implement conditions
181 |
182 | Each condition should implement `ConditionalActions\Contracts\ConditionContract` contract.
183 | The package has a base abstract class `ConditionalActions\Entities\Conditions\BaseCondition` with all contract methods except the `check` method.
184 |
185 | ```php
186 | class HasPaidToysCondition extends BaseCondition
187 | {
188 | /** @var ToysService */
189 | private $toysService;
190 |
191 | // You can use dependency injection in constructor
192 | public function __construct(ToysService $toysService)
193 | {
194 | $this->toysService = $toysService;
195 | }
196 |
197 | /**
198 | * Runs condition check.
199 | *
200 | * @param TargetContract $target
201 | * @param StateContract $state
202 | *
203 | * @return bool
204 | */
205 | public function check(TargetContract $target, StateContract $state): bool
206 | {
207 | $toyId = $this->parameters['toy_id'] ?? null;
208 |
209 | if (!($target instanceof ToysPriceTarget) || is_null($toyId)) {
210 | return false;
211 | }
212 |
213 | return $this->toysService->hasPaidToy($target->user, $toyId);
214 | }
215 | }
216 | ```
217 |
218 | ```php
219 | class TodayIsBirthdayCondition extends BaseCondition
220 | {
221 | /** @var ToysService */
222 | private $toysService;
223 |
224 | // You can use dependency injection in constructor
225 | public function __construct(ToysService $toysService)
226 | {
227 | $this->toysService = $toysService;
228 | }
229 |
230 | /**
231 | * Runs condition check.
232 | *
233 | * @param TargetContract $target
234 | * @param StateContract $state
235 | *
236 | * @return bool
237 | */
238 | public function check(TargetContract $target, StateContract $state): bool
239 | {
240 | if (!($target instanceof ToysPriceTarget)) {
241 | return false;
242 | }
243 |
244 | return Carbon::now()->isSameDay($target->user->birthday);
245 | }
246 | }
247 | ```
248 |
249 | ### Implement action
250 |
251 | Each condition should implement `ConditionalActions\Contracts\ActionContract` contract.
252 | The package has a base abstract class `ConditionalActions\Entities\Actions\BaseAction` with all contract methods except the `apply` method.
253 |
254 | ```php
255 | class DiscountAction extends BaseAction
256 | {
257 | /**
258 | * Applies action to the state and returns a new state.
259 | *
260 | * @param StateContract $state
261 | *
262 | * @return StateContract
263 | */
264 | public function apply(StateContract $state): StateContract
265 | {
266 | $discount = $this->parameters['discount'] ?? 0;
267 | $currentPrice = $state->getAttribute('price');
268 | $state->setAttribute('price', $currentPrice - $currentPrice / 100 * $discount);
269 |
270 | return $state;
271 | }
272 | }
273 | ```
274 |
275 | ### Add conditions to config `config/conditional-actions.php`
276 |
277 | ```php
278 |
279 | return [
280 | 'conditions' => [
281 | 'AllOfCondition' => ConditionalActions\Entities\Conditions\AllOfCondition::class,
282 | 'OneOfCondition' => ConditionalActions\Entities\Conditions\OneOfCondition::class,
283 | 'TrueCondition' => ConditionalActions\Entities\Conditions\TrueCondition::class,
284 |
285 | 'CurrentTimeCondition' => App\ConditionalActions\Conditions\CurrentTimeCondition::class,
286 | 'HasPaidToysCondition' => App\ConditionalActions\Conditions\HasPaidToysCondition::class,
287 | 'TodayIsBirthdayCondition' => App\ConditionalActions\Conditions\TodayIsBirthdayCondition::class,
288 | ],
289 | 'actions' => [
290 | 'UpdateStateAttributeAction' => ConditionalActions\Entities\Actions\UpdateStateAttributeAction::class,
291 |
292 | 'DiscountAction' => App\ConditionalActions\Actions\DiscountAction::class,
293 | ],
294 | 'use_logger' => env('APP_DEBUG', false),
295 | ];
296 | ```
297 |
298 | ### Implement API for adds conditions and actions for `Toy` model
299 |
300 | You can use eloquent models or any other objects to put business logic into external storage.
301 |
302 | The package has basic CRUD for conditions and actions. You can enable it:
303 | ```php
304 | use ConditionalActions\ConditionalActions;
305 | use Illuminate\Support\ServiceProvider;
306 |
307 | class RouteServiceProvider extends ServiceProvider
308 | {
309 | // ...
310 | public function register()
311 | {
312 | ConditionalActions::routes();
313 | }
314 | }
315 | ```
316 |
317 | Or you can implement your own API. Sample example:
318 | ```php
319 | # This example is not an API. You can create API as you needed.
320 |
321 | /** @var Toy $toy */
322 | $toy = Toy::find(10);
323 |
324 | /** @var Condition $allOf */
325 | $allOf = $toy->conditions()->create([
326 | 'name' => 'AllOfCondition',
327 | 'starts_at' => '2019-05-01 00:00:00',
328 | 'ends_at' => '2019-05-01 23:59:59',
329 | ]);
330 |
331 | $allOf->actions()->create([
332 | 'name' => 'DiscountAction',
333 | 'parameters' => ['discount' => 10],
334 | ]);
335 |
336 | $todayIsBirthday = $allOf->childrenConditions()->make([
337 | 'name' => 'TodayIsBirthdayCondition',
338 | ]);
339 |
340 | $hasPaidToy = $allOf->childrenConditions()->make([
341 | 'name' => 'HasPaidToysCondition',
342 | 'parameters' => ['toy_id' => 5],
343 | ]);
344 |
345 | $toy->conditions()->saveMany([$allOf, $hasPaidToy, $todayIsBirthday]);
346 | ```
347 |
348 | ### Run conditional actions
349 |
350 | ```php
351 | $toy = Toy::find(10);
352 |
353 | // Create a target instance
354 | $target = new ToysPriceTarget(Auth::user(), $toy);
355 | /*
356 | * Run conditional actions.
357 | * This method will iterate over all its conditions stored in database and apply actions related to succeed conditions
358 | */
359 | $newState = $target->runConditionalActions();
360 | dump($newState->getAttribute('price'));
361 | ```
362 |
363 | ## P.S.
364 |
365 | The package includes conditions and actions:
366 |
367 | * Condition `AllOfCondition` - succeeded when **all** children conditions are succeeded. All children actions will be included to parent `AllOfCondition` condition;
368 | * Condition `OneOfCondition` - succeeded when **any of** children conditions are succeeded. All children actions for **first** succeeded condition will be included to parent `OneOfCondition` condition;
369 | * Condition `TrueCondition` - always succeeded;
370 | * Action `UpdateStateAttributeAction` - Updates an attribute value in the state.
371 |
372 | Both conditions and actions have fields:
373 | * `priority` - execution priority;
374 | * nullable `starts_at` and `ends_at` - enables condition or action at specific time period;
375 | * `parameters` - parameters of conditions or actions;
376 | * `is_inverted` - determines whether the condition result should be inverted.
377 |
--------------------------------------------------------------------------------