Skip to content

Commit f3f927a

Browse files
Merge pull request #4 from kirschbaum-development/rework
Pretty big rework
2 parents 9ee0691 + f165ff1 commit f3f927a

33 files changed

+365
-146
lines changed

README.md

Lines changed: 43 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -21,7 +21,7 @@ The special sauce here is that you get to tell the Action which events you want
2121

2222
## Requirements
2323

24-
This package requires Laravel 6.0 or higher.
24+
This package requires Laravel 6.0 or higher and PHP 7.2.5 or higher.
2525

2626
## Installation
2727

@@ -106,9 +106,9 @@ The package also has a facade. Here's the syntax:
106106
```php
107107
use Kirschbaum\Actions\Facades\Action;
108108

109-
Action::act(new ChuckNorris($data));
110-
Action::actWhen($isChuckNorrisMighty, new ChuckNorris($data));
111-
Action::actUnless($isChuckNorrisPuny, new ChuckNorris($data));
109+
Action::act(ChuckNorris::class, $data);
110+
Action::actWhen($isChuckNorrisMighty, ChuckNorris::class, $data);
111+
Action::actUnless($isChuckNorrisPuny, ChuckNorris::class, $data);
112112
```
113113

114114
The usage is nearly identical to calling the methods directly on the Action as mentioned in the section above. The benefit here is that you can easily test actions using `Action::shouldReceive('act')`, `Action::shouldReceive('actWhen')` or `Action::shouldReceive('actUnless')`.
@@ -118,23 +118,23 @@ The usage is nearly identical to calling the methods directly on the Action as m
118118
The package also has a few handy helpers to get Chuck in action. Here's the syntax:
119119

120120
```php
121-
act(new ChuckNorris($data));
122-
act_when($isChuckNorrisMighty, new ChuckNorris($data));
123-
act_unless($isChuckNorrisPuny, new ChuckNorris($data));
121+
act(ChuckNorris::class, $data);
122+
act_when($isChuckNorrisMighty, ChuckNorris::class, $data);
123+
act_unless($isChuckNorrisPuny, ChuckNorris::class, $data);
124124
```
125125

126126
### Dependency Injection
127127

128128
You can even inject Actions as a dependencies inside your application!
129129

130130
```php
131-
use Kirschbaum\Actions\Contracts\Actionable;
131+
use Kirschbaum\Actions\Action;
132132

133-
public function index (Actionable $action)
133+
public function index (Action $action)
134134
{
135-
$action->act(new ChuckNorris($data));
136-
$action->actWhen($isChuckNorrisMighty, new ChuckNorris($data));
137-
$action->actUnless($isChuckNorrisPuny, new ChuckNorris($data));
135+
$action->act(ChuckNorris::class, $data);
136+
$action->actWhen($isChuckNorrisMighty, ChuckNorris::class, $data);
137+
$action->actUnless($isChuckNorrisPuny, ChuckNorris::class, $data);
138138
}
139139
```
140140

@@ -171,6 +171,22 @@ Another option for handling failures is to tell the Action to throw its own exce
171171
public $exception = SeagalFailedException::class;
172172
```
173173

174+
## Auto-Discovery and Configuration
175+
176+
Out of the box, actions are automatically discovered and bound to Laravel's container, which allows for easier testing of your actions. If you need to add a custom path if you are placing your actions somewhere other than `app/Actions`, make sure to publish the configs.
177+
178+
```bash
179+
php artisan vendor:publish --tag laravel-actions
180+
```
181+
182+
If you want to disable auto-discovery, publish the config and return an empty array from the `paths` key.
183+
184+
```php
185+
return [
186+
'paths' => [],
187+
];
188+
```
189+
174190
## Testing
175191

176192
Have no fear. Testing all of this is very straightforward. There are two approaches to testing built in.
@@ -189,12 +205,24 @@ Action::shouldReceive('act')
189205

190206
### Mocking
191207

192-
If you are using helpers, the `CanAct` trait, or dependency injection, you can easily mock the `Actionable` interface with Laravel's mocking tools.
208+
If you are using helpers, the `CanAct` trait, or dependency injection, you can easily mock the `Action` class with Laravel's mocking tools.
209+
210+
```php
211+
use Kirschbaum\Actions\Action;
212+
213+
$this->mock(Action::class, function ($mock) {
214+
$mock->shouldReceive('act')
215+
->once()
216+
->andReturnTrue();
217+
});
218+
```
219+
220+
Because actions are bound into Laravel's container by default, you can test specific actions as well.
193221

194222
```php
195-
use Kirschbaum\Actions\Contracts\Actionable;
223+
use App\Actions\ChuckNorris;
196224

197-
$this->mock(Actionable::class, function ($mock) {
225+
$this->mock(ChuckNorris::class, function ($mock) {
198226
$mock->shouldReceive('act')
199227
->once()
200228
->andReturnTrue();

config/laravel-actions.php

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
<?php
2+
3+
return [
4+
/*
5+
|--------------------------------------------------------------------------
6+
| Auto-discovery Action Paths
7+
|--------------------------------------------------------------------------
8+
|
9+
| The paths for auto-discovering actions classes. Each path must be a full path to
10+
| a directory within your Laravel application. If you would rather disable this
11+
| cool feature and/or bind them yourself, just return an empty array instead.
12+
*/
13+
'paths' => [
14+
app_path('Actions'),
15+
],
16+
];

src/Action.php

Lines changed: 43 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -3,68 +3,89 @@
33
namespace Kirschbaum\Actions;
44

55
use Throwable;
6-
use Illuminate\Support\Arr;
76
use Kirschbaum\Actions\Contracts\Actionable;
7+
use Kirschbaum\Actions\Exceptions\ActionableInterfaceNotFoundException;
88

99
class Action
1010
{
11+
/**
12+
* Arguments to pass into the action's constructor.
13+
*
14+
* @var array
15+
*/
16+
protected $arguments;
17+
1118
/**
1219
* Initiate the given action.
1320
*
14-
* @param Actionable $action
21+
* @param string $action
22+
* @param mixed ...$arguments
1523
*
1624
* @throws Throwable
1725
*
18-
* @return mixed|void
26+
* @return mixed
1927
*/
20-
public function act(Actionable $action)
28+
public function act(string $action, ...$arguments)
2129
{
30+
$this->arguments = $arguments;
31+
2232
return $this->handle($action);
2333
}
2434

2535
/**
2636
* Initiate the given action if the given condition is true.
2737
*
2838
* @param $condition
29-
* @param Actionable $action
39+
* @param string $action
40+
* @param mixed ...$arguments
3041
*
3142
* @throws Throwable
3243
*
3344
* @return mixed|void
3445
*/
35-
public function actWhen($condition, Actionable $action)
46+
public function actWhen($condition, string $action, ...$arguments)
3647
{
3748
if ($condition) {
38-
return $this->act($action);
49+
$this->arguments = $arguments;
50+
51+
return $this->handle($action);
3952
}
4053
}
4154

4255
/**
4356
* Initiate the action if the given condition is false.
4457
*
4558
* @param $condition
46-
* @param Actionable $action
59+
* @param string $action
60+
* @param mixed ...$arguments
4761
*
4862
* @throws Throwable
4963
*
5064
* @return mixed|void
5165
*/
52-
public function actUnless($condition, Actionable $action)
66+
public function actUnless($condition, string $action, ...$arguments)
5367
{
54-
return $this->actWhen(! $condition, $action);
68+
if (! $condition) {
69+
$this->arguments = array_slice(func_get_args(), 2);
70+
71+
return $this->handle($action);
72+
}
5573
}
5674

5775
/**
5876
* Handle the given action.
5977
*
60-
* @param Actionable $action
78+
* @param string $action
6179
*
6280
* @throws Throwable
6381
*
6482
* @return mixed|void
6583
*/
66-
protected function handle(Actionable $action)
84+
protected function handle(string $action)
6785
{
86+
$action = new $action(...$this->arguments);
87+
88+
$this->checkActionForInterface($action);
6889
$this->raiseBeforeActionEvent($action);
6990

7091
try {
@@ -90,20 +111,27 @@ protected function actionHasFailedMethod(Actionable $action): bool
90111
return method_exists($action, 'failed');
91112
}
92113

114+
protected function checkActionForInterface($action): void
115+
{
116+
throw_unless(
117+
$action instanceof Actionable,
118+
ActionableInterfaceNotFoundException::class
119+
);
120+
}
121+
93122
/**
94123
* Dispatch appropriate action event.
95124
*
96125
* @param string $event
97126
* @param Actionable $action
98-
* @param Throwable|null $exception
99127
*
100128
* @return void
101129
*/
102-
protected function dispatchEvent(string $event, Actionable $action, ?Throwable $exception = null): void
130+
protected function dispatchEvent(string $event, Actionable $action): void
103131
{
104132
if ($this->eventExists($action, $event)) {
105133
// Gather method arguments except for the `$event` argument.
106-
$arguments = Arr::except(func_get_args(), 0);
134+
$arguments = array_slice(func_get_args(), 1);
107135

108136
event(new $action->{$event}(...$arguments));
109137
}

src/ActionsServiceProvider.php

Lines changed: 98 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,10 @@
22

33
namespace Kirschbaum\Actions;
44

5+
use ReflectionClass;
6+
use ReflectionException;
7+
use Illuminate\Support\Str;
8+
use Symfony\Component\Finder\Finder;
59
use Illuminate\Support\ServiceProvider;
610
use Kirschbaum\Actions\Contracts\Actionable;
711
use Kirschbaum\Actions\Commands\MakeActionCommand;
@@ -15,15 +19,88 @@ class ActionsServiceProvider extends ServiceProvider
1519
*/
1620
public $bindings = [
1721
'actions' => Action::class,
18-
Actionable::class => Action::class,
22+
Action::class => Action::class,
1923
];
2024

25+
/**
26+
* Register any application services.
27+
*
28+
* @return void
29+
*/
30+
public function register(): void
31+
{
32+
$this->registerMergeConfig();
33+
}
34+
2135
/**
2236
* Bootstrap any package services.
2337
*
38+
* @throws ReflectionException
39+
*
2440
* @return void
2541
*/
26-
public function boot()
42+
public function boot(): void
43+
{
44+
$this->bootConsoleCommands();
45+
46+
$this->bootPublishConfig();
47+
48+
$this->bootAutoDiscoverActions();
49+
}
50+
51+
/**
52+
* Get the services provided by the provider.
53+
*
54+
* @return array
55+
*/
56+
public function provides(): array
57+
{
58+
return [Action::class];
59+
}
60+
61+
/**
62+
* Auto-discover actions classes.
63+
*
64+
* @throws ReflectionException
65+
*
66+
* @return void
67+
*/
68+
protected function bootAutoDiscoverActions(): void
69+
{
70+
$paths = collect(config('laravel-actions.paths'))
71+
->unique()
72+
->filter(function ($path) {
73+
return is_dir($path);
74+
});
75+
76+
if ($paths->isEmpty()) {
77+
return;
78+
}
79+
80+
$namespace = $this->app->getNamespace();
81+
82+
foreach ((new Finder())->in($paths->toArray())->files() as $action) {
83+
$action = $namespace . str_replace(
84+
['/', '.php'],
85+
['\\', ''],
86+
Str::after($action->getRealPath(), realpath(app_path()) . DIRECTORY_SEPARATOR)
87+
);
88+
89+
if (
90+
is_subclass_of($action, Actionable::class)
91+
&& ! (new ReflectionClass($action))->isAbstract()
92+
) {
93+
$this->app->bind($action, Action::class);
94+
}
95+
}
96+
}
97+
98+
/**
99+
* Load console commands for actions.
100+
*
101+
* @return void
102+
*/
103+
protected function bootConsoleCommands(): void
27104
{
28105
if ($this->app->runningInConsole()) {
29106
$this->commands([
@@ -33,12 +110,27 @@ public function boot()
33110
}
34111

35112
/**
36-
* Get the services provided by the provider.
113+
* Publish action configuration file.
37114
*
38-
* @return array
115+
* @return void
39116
*/
40-
public function provides(): array
117+
protected function bootPublishConfig(): void
41118
{
42-
return [Action::class];
119+
$this->publishes([
120+
__DIR__ . '/../config/laravel-actions.php' => config_path('laravel-actions.php'),
121+
], 'laravel-actions');
122+
}
123+
124+
/**
125+
* Register merging of configuration file.
126+
*
127+
* @return void
128+
*/
129+
protected function registerMergeConfig(): void
130+
{
131+
$this->mergeConfigFrom(
132+
__DIR__ . '/../config/laravel-actions.php',
133+
'laravel-actions'
134+
);
43135
}
44136
}
Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
<?php
2+
3+
namespace Kirschbaum\Actions\Exceptions;
4+
5+
use Exception;
6+
7+
class ActionableInterfaceNotFoundException extends Exception
8+
{
9+
}

0 commit comments

Comments
 (0)