Skip to content

Commit bbbca4a

Browse files
committed
feat: ability to record without faking, allow/disallow execution per action class
1 parent da91e4b commit bbbca4a

File tree

7 files changed

+388
-133
lines changed

7 files changed

+388
-133
lines changed

src/Dispatcher/ActionRecording.php

+154
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,154 @@
1+
<?php
2+
3+
namespace BradieTilley\Actions\Dispatcher;
4+
5+
use BradieTilley\Actions\Contracts\Actionable;
6+
use Closure;
7+
use Illuminate\Support\Collection;
8+
use Illuminate\Support\Traits\ReflectsClosures;
9+
use PHPUnit\Framework\Assert as PHPUnit;
10+
11+
trait ActionRecording
12+
{
13+
use ReflectsClosures;
14+
15+
protected bool $recording = false;
16+
17+
/**
18+
* List of actions that have run this session
19+
*
20+
* @var array<class-string<Actionable>,array<Actionable>>
21+
*/
22+
protected array $actions = [];
23+
24+
/**
25+
* Assert if a job was dispatched based on a truth-test callback.
26+
*/
27+
public function assertDispatched(Closure|string $command, Closure|int|null $callback = null): static
28+
{
29+
if ($command instanceof Closure) {
30+
[$command, $callback] = [$this->firstClosureParameterType($command), $command];
31+
/** @var class-string<Actionable> $command */
32+
/** @var Closure $callback */
33+
}
34+
35+
if (is_int($callback)) {
36+
return $this->assertDispatchedTimes($command, $callback);
37+
}
38+
39+
PHPUnit::assertTrue(
40+
$this->dispatched($command, $callback)->count() > 0,
41+
"The expected [{$command}] job was not dispatched.",
42+
);
43+
44+
return $this;
45+
}
46+
47+
/**
48+
* Assert if a job was pushed a number of times.
49+
*/
50+
public function assertDispatchedTimes(Closure|string $command, int $times = 1): static
51+
{
52+
$callback = null;
53+
54+
if ($command instanceof Closure) {
55+
[$command, $callback] = [$this->firstClosureParameterType($command), $command];
56+
/** @var class-string<Actionable> $command */
57+
/** @var Closure $callback */
58+
}
59+
60+
$count = $this->dispatched($command, $callback)->count();
61+
62+
PHPUnit::assertSame(
63+
$times,
64+
$count,
65+
"The expected [{$command}] action was dispatched {$count} times instead of {$times} times.",
66+
);
67+
68+
return $this;
69+
}
70+
71+
/**
72+
* Determine if a job was dispatched based on a truth-test callback.
73+
*/
74+
public function assertNotDispatched(Closure|string $command, ?Closure $callback = null): static
75+
{
76+
if ($command instanceof Closure) {
77+
[$command, $callback] = [$this->firstClosureParameterType($command), $command];
78+
/** @var class-string<Actionable> $command */
79+
/** @var Closure $callback */
80+
}
81+
82+
PHPUnit::assertTrue(
83+
$this->dispatched($command, $callback)->count() === 0,
84+
"The unexpected [{$command}] action was dispatched.",
85+
);
86+
87+
return $this;
88+
}
89+
90+
/**
91+
* Assert that no jobs were dispatched.
92+
*/
93+
public function assertNothingDispatched(): static
94+
{
95+
PHPUnit::assertEmpty($this->actions, 'Actions were dispatched unexpectedly.');
96+
97+
return $this;
98+
}
99+
100+
/**
101+
* Get all of the jobs matching a truth-test callback.
102+
*
103+
* @return Collection<int, Actionable>
104+
*/
105+
public function dispatched(string $command, ?Closure $callback = null): Collection
106+
{
107+
if (! $this->hasDispatched($command)) {
108+
return Collection::make();
109+
}
110+
111+
$callback = $callback ?: fn () => true;
112+
113+
return Collection::make($this->actions[$command])
114+
->filter(fn (Actionable $action) => $callback($action));
115+
}
116+
117+
/**
118+
* Determine if there are any stored commands for a given class.
119+
*/
120+
public function hasDispatched(string $action): bool
121+
{
122+
return isset($this->actions[$action]) && ! empty($this->actions[$action]);
123+
}
124+
125+
public function enableRecording(): static
126+
{
127+
$this->recording = true;
128+
129+
return $this;
130+
}
131+
132+
public function disableRecording(): static
133+
{
134+
$this->recording = false;
135+
136+
return $this;
137+
}
138+
139+
public function resetRecordings(): static
140+
{
141+
$this->actions = [];
142+
143+
return $this;
144+
}
145+
146+
public function recordAction(Actionable $action): void
147+
{
148+
if (! $this->recording) {
149+
return;
150+
}
151+
152+
$this->actions[$action::class][] = $action;
153+
}
154+
}

src/Dispatcher/Dispatcher.php

+4
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,8 @@
1616

1717
class Dispatcher implements DispatcherContract
1818
{
19+
use ActionRecording;
20+
1921
public function __construct(
2022
public readonly Container $container,
2123
public readonly EventsDispatcher $events,
@@ -28,6 +30,8 @@ public function __construct(
2830
*/
2931
public function dispatch(Actionable $action): mixed
3032
{
33+
$this->recordAction($action);
34+
3135
if (empty($middleware = $action->middleware())) {
3236
return $this->doDispatch($action);
3337
}

src/Dispatcher/FakeDispatcher.php

+46-109
Original file line numberDiff line numberDiff line change
@@ -12,12 +12,10 @@
1212
use Illuminate\Support\Arr;
1313
use Illuminate\Support\Collection;
1414
use Illuminate\Support\Testing\Fakes\Fake;
15-
use Illuminate\Support\Traits\ReflectsClosures;
16-
use PHPUnit\Framework\Assert as PHPUnit;
1715

1816
class FakeDispatcher extends ActualDispatcher implements Fake
1917
{
20-
use ReflectsClosures;
18+
use ActionRecording;
2119

2220
/**
2321
* List of actions to fake
@@ -34,11 +32,11 @@ class FakeDispatcher extends ActualDispatcher implements Fake
3432
protected array $actionsToDispatch = [];
3533

3634
/**
37-
* List of actions that have run this session
35+
* List of actions to allow execution even if faked
3836
*
39-
* @var array<class-string<Actionable>,array<Actionable>>
37+
* @var array<class-string<Actionable>, bool>
4038
*/
41-
protected array $actions = [];
39+
protected array $actionsAllowed = [];
4240

4341
/**
4442
* Flag to determine if the actions should still execute
@@ -56,6 +54,7 @@ public function __construct(
5654
) {
5755
parent::__construct($container, $events, $db);
5856
$this->actionsToFake = Arr::wrap($actionsToFake);
57+
$this->enableRecording();
5958
}
6059

6160
/**
@@ -124,18 +123,36 @@ public function disallowExecution(): static
124123
return $this;
125124
}
126125

126+
public function allow(array|string $actions): static
127+
{
128+
foreach (Arr::wrap($actions) as $action) {
129+
$this->actionsAllowed[$action] = true;
130+
}
131+
132+
return $this;
133+
}
134+
135+
public function disallow(array|string $actions): static
136+
{
137+
foreach (Arr::wrap($actions) as $action) {
138+
$this->actionsAllowed[$action] = false;
139+
}
140+
141+
return $this;
142+
}
143+
127144
/**
128145
* Dispatch the given action
129146
*/
130147
public function dispatch(Actionable $action): mixed
131148
{
132-
$this->actions[get_class($action)][] = $action;
149+
$this->recordAction($action);
133150

134151
if (! $this->shouldFakeJob($action)) {
135152
return parent::dispatch($action);
136153
}
137154

138-
if ($this->executeActions) {
155+
if ($this->shouldHandleReal($action)) {
139156
return parent::dispatch($action);
140157
}
141158

@@ -168,6 +185,27 @@ protected function shouldFakeJob(Actionable $action): bool
168185
->isNotEmpty();
169186
}
170187

188+
protected function shouldHandleReal(Actionable $action): bool
189+
{
190+
/**
191+
* If this action is allowed to run
192+
*/
193+
$explicit = $this->actionsAllowed[$action::class] ?? null;
194+
195+
if ($explicit !== null) {
196+
return $explicit;
197+
}
198+
199+
/**
200+
* If all execution is allowed
201+
*/
202+
if ($this->executeActions) {
203+
return true;
204+
}
205+
206+
return false;
207+
}
208+
171209
/**
172210
* Determine if a command should be dispatched or not.
173211
*/
@@ -181,105 +219,4 @@ protected function shouldDispatchCommand(Actionable $action): bool
181219
})
182220
->isNotEmpty();
183221
}
184-
185-
/**
186-
* Assert if a job was dispatched based on a truth-test callback.
187-
*/
188-
public function assertDispatched(Closure|string $command, Closure|int|null $callback = null): static
189-
{
190-
if ($command instanceof Closure) {
191-
[$command, $callback] = [$this->firstClosureParameterType($command), $command];
192-
/** @var class-string<Actionable> $command */
193-
/** @var Closure $callback */
194-
}
195-
196-
if (is_int($callback)) {
197-
return $this->assertDispatchedTimes($command, $callback);
198-
}
199-
200-
PHPUnit::assertTrue(
201-
$this->dispatched($command, $callback)->count() > 0,
202-
"The expected [{$command}] job was not dispatched.",
203-
);
204-
205-
return $this;
206-
}
207-
208-
/**
209-
* Assert if a job was pushed a number of times.
210-
*/
211-
public function assertDispatchedTimes(Closure|string $command, int $times = 1): static
212-
{
213-
$callback = null;
214-
215-
if ($command instanceof Closure) {
216-
[$command, $callback] = [$this->firstClosureParameterType($command), $command];
217-
/** @var class-string<Actionable> $command */
218-
/** @var Closure $callback */
219-
}
220-
221-
$count = $this->dispatched($command, $callback)->count();
222-
223-
PHPUnit::assertSame(
224-
$times,
225-
$count,
226-
"The expected [{$command}] action was dispatched {$count} times instead of {$times} times.",
227-
);
228-
229-
return $this;
230-
}
231-
232-
/**
233-
* Determine if a job was dispatched based on a truth-test callback.
234-
*/
235-
public function assertNotDispatched(Closure|string $command, ?Closure $callback = null): static
236-
{
237-
if ($command instanceof Closure) {
238-
[$command, $callback] = [$this->firstClosureParameterType($command), $command];
239-
/** @var class-string<Actionable> $command */
240-
/** @var Closure $callback */
241-
}
242-
243-
PHPUnit::assertTrue(
244-
$this->dispatched($command, $callback)->count() === 0,
245-
"The unexpected [{$command}] action was dispatched.",
246-
);
247-
248-
return $this;
249-
}
250-
251-
/**
252-
* Assert that no jobs were dispatched.
253-
*/
254-
public function assertNothingDispatched(): static
255-
{
256-
PHPUnit::assertEmpty($this->actions, 'Actions were dispatched unexpectedly.');
257-
258-
return $this;
259-
}
260-
261-
/**
262-
* Get all of the jobs matching a truth-test callback.
263-
*
264-
* @return Collection<int, Actionable>
265-
*/
266-
public function dispatched(string $command, ?Closure $callback = null): Collection
267-
{
268-
if (! $this->hasDispatched($command)) {
269-
return Collection::make();
270-
}
271-
272-
$callback = $callback ?: fn () => true;
273-
274-
return Collection::make($this->actions[$command])
275-
->filter(fn (Actionable $action) => $callback($action));
276-
}
277-
278-
/**
279-
* Determine if there are any stored commands for a given class.
280-
*/
281-
public function hasDispatched(string $action): bool
282-
{
283-
return isset($this->actions[$action]) && ! empty($this->actions[$action]);
284-
}
285222
}

0 commit comments

Comments
 (0)