Skip to content

Commit 423ce9c

Browse files
committed
Pretty big rework
1 parent 9ee0691 commit 423ce9c

33 files changed

+356
-143
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: 39 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -3,68 +3,86 @@
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
1522
*
1623
* @throws Throwable
1724
*
1825
* @return mixed|void
1926
*/
20-
public function act(Actionable $action)
27+
public function act(string $action)
2128
{
29+
$this->arguments = array_slice(func_get_args(), 1);
30+
2231
return $this->handle($action);
2332
}
2433

2534
/**
2635
* Initiate the given action if the given condition is true.
2736
*
2837
* @param $condition
29-
* @param Actionable $action
38+
* @param string $action
3039
*
3140
* @throws Throwable
3241
*
3342
* @return mixed|void
3443
*/
35-
public function actWhen($condition, Actionable $action)
44+
public function actWhen($condition, string $action)
3645
{
3746
if ($condition) {
38-
return $this->act($action);
47+
$this->arguments = array_slice(func_get_args(), 2);
48+
49+
return $this->handle($action);
3950
}
4051
}
4152

4253
/**
4354
* Initiate the action if the given condition is false.
4455
*
4556
* @param $condition
46-
* @param Actionable $action
57+
* @param string $action
4758
*
4859
* @throws Throwable
4960
*
5061
* @return mixed|void
5162
*/
52-
public function actUnless($condition, Actionable $action)
63+
public function actUnless($condition, string $action)
5364
{
54-
return $this->actWhen(! $condition, $action);
65+
if (! $condition) {
66+
$this->arguments = array_slice(func_get_args(), 2);
67+
68+
return $this->handle($action);
69+
}
5570
}
5671

5772
/**
5873
* Handle the given action.
5974
*
60-
* @param Actionable $action
75+
* @param string $action
6176
*
6277
* @throws Throwable
6378
*
6479
* @return mixed|void
6580
*/
66-
protected function handle(Actionable $action)
81+
protected function handle(string $action)
6782
{
83+
$action = new $action(...$this->arguments);
84+
85+
$this->checkActionForInterface($action);
6886
$this->raiseBeforeActionEvent($action);
6987

7088
try {
@@ -90,20 +108,27 @@ protected function actionHasFailedMethod(Actionable $action): bool
90108
return method_exists($action, 'failed');
91109
}
92110

111+
protected function checkActionForInterface($action): void
112+
{
113+
throw_unless(
114+
$action instanceof Actionable,
115+
ActionableInterfaceNotFoundException::class
116+
);
117+
}
118+
93119
/**
94120
* Dispatch appropriate action event.
95121
*
96122
* @param string $event
97123
* @param Actionable $action
98-
* @param Throwable|null $exception
99124
*
100125
* @return void
101126
*/
102-
protected function dispatchEvent(string $event, Actionable $action, ?Throwable $exception = null): void
127+
protected function dispatchEvent(string $event, Actionable $action): void
103128
{
104129
if ($this->eventExists($action, $event)) {
105130
// Gather method arguments except for the `$event` argument.
106-
$arguments = Arr::except(func_get_args(), 0);
131+
$arguments = array_slice(func_get_args(), 1);
107132

108133
event(new $action->{$event}(...$arguments));
109134
}

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)