Skip to content

Commit e3edbd5

Browse files
committed
Updated documentation, adjusted logging message, improved 'UnhandledActionsCommand' and updated tests
1 parent 2e638f4 commit e3edbd5

File tree

8 files changed

+163
-51
lines changed

8 files changed

+163
-51
lines changed

docs/helpers.md

Lines changed: 1 addition & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -4,8 +4,6 @@ title: Helpers
44

55
If you've made it this far, you probably want to know some of the superpowers that _Fraction_ has, right? From here on, things get more interesting, in terms of features. Before diving into the reading, keep in mind that everything you'll see here is directly associated with the Laravel Way, but without resorting to nonsense.
66

7-
First, _"WHY do we need helpers?"_ The answer is simple: **to make your life easier**. _Fraction_ provides a set of helpers that allow you to create and manage actions in a more straightforward way. Let's explore some of these helpers.
8-
97
## Deferred Actions
108

119
As part of Laravel 11, you can trigger deferred actions simply by using the `deferred` method following the action declaration:
@@ -79,7 +77,7 @@ execute('create user', function () {
7977
Behind the scenes, this will write a log to the requested `channel` to help you understand the exact moment the action was performed. The log output will be written as follows:
8078

8179
```txt
82-
[2025-05-31 21:04:10] local.INFO: [<app.name>] Action: [<action name>] executed at 2025-05-31 21:04:10
80+
[2025-05-31 21:04:10] local.INFO: [<app.name>] Action: [<action name>] executed.
8381
```
8482

8583
Keep in mind the log is written right after the process is dispatched, which means the log output does not represent the exact moment the action logic was executed. For situations where you are interacting with `deferred` or `queued` actions, you might see a difference between the log time and the actual execution time of the action logic, due to the way these actions are processed.

docs/hooks.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -61,6 +61,7 @@ Now you _should_ `run` the first action of the pipeline: `send welcome user emai
6161

6262
```php
6363
namespace App\Http\Controllers;
64+
6465
use Illuminate\Http\Request\CreateUserRequest;
6566

6667
class UserController extends Controller

docs/index.md

Lines changed: 10 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@ title: About
66

77
There's no denying that the "Action Pattern" in the Laravel ecosystem is extremely useful and widely used. However, action classes require "too much content" to do basic things. Let's review a basic action class:
88

9-
```php
9+
```php {9}
1010
namespace App\Actions;
1111

1212
use App\Models\User;
@@ -15,16 +15,18 @@ class CreateUser
1515
{
1616
public function handle(array $data): User
1717
{
18-
return User::create($data);
18+
return User::create($data); // [!code focus]
1919
}
2020
}
2121
```
2222

23-
We have a namespace, a class, a method, a return type, a model import, an array as arguments... all of this to create a user. It's overkill for such a simple task, isn't it? For this reason, the _Fraction_ solution is revolutionary in the context of Actions. _Fraction_ allows you to write actions in a simpler and more direct way, without the need for all this structure, **similar to what _PestPHP_ proposes.**
23+
We have a namespace, a class, a method, a return type, a model import, an array as arguments... **all of this to create a user.** It's overkill for such a simple task, isn't it?
24+
25+
For this reason, the _Fraction_ solution is revolutionary in the context of Actions. _Fraction_ allows you to write actions in a simpler and more direct way, without the need for all this structure, **similar to what _PestPHP_ proposes.**
2426

2527
See what the same example would look like with _Fraction_:
2628

27-
```php
29+
```php {3}
2830
// app/Actions/Users.php
2931

3032
execute('create user', function (array $data) {
@@ -34,17 +36,18 @@ execute('create user', function (array $data) {
3436

3537
Then, anywhere in your application, you can `run` this action like this:
3638

37-
```php
39+
```php {9}
3840
namespace App\Http\Controllers;
41+
3942
use Illuminate\Http\Request;
4043

4144
class CreateUserController extends Controller
4245
{
4346
public function store(Request $request)
4447
{
48+
$user = run('create user', $request->all()); // [!code focus]
49+
4550
// ...
46-
47-
$user = run('create user', $request->all());
4851
}
4952
}
5053
```

docs/testing.md

Lines changed: 90 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,9 @@
22
title: Testing
33
---
44

5-
There is nothing "special" about _Fraction_ regarding tests. This means that you can call your actions using `run` inside tests as normal. For example, if you have an action that `creates new user`, you can test it like this:
5+
There is nothing "special" about _Fraction_ regarding tests. This means that you can call your actions using `run` inside tests as normal.
6+
7+
For example, if you have an action that `creates new user`:
68

79
```php
810
// app/Actions/CreateUser.php
@@ -14,11 +16,18 @@ execute('create new user', function (array $data) {
1416
});
1517
```
1618

17-
```php {2}
19+
Then test your action like this:
20+
21+
::: code-group
22+
23+
```php {5} [PestPHP]
1824
test('should be able to create new user', function () {
25+
$name = fake()->name();
26+
$email = fake()->email();
27+
1928
$user = run('create new user', [
20-
'name' => $name = fake()->name(),
21-
'email' => $email = fake()->email(),
29+
'name' => $name,
30+
'email' => $email,
2231
'password' => bcrypt('password'),
2332
]);
2433

@@ -28,22 +37,95 @@ test('should be able to create new user', function () {
2837
});
2938
```
3039

40+
```php {16} [PhpUnit]
41+
namespace Tests\Unit;
42+
43+
use Tests\TestCase;
44+
use App\Models\User;
45+
use Illuminate\Foundation\Testing\RefreshDatabase;
46+
47+
class CreateUserTest extends TestCase
48+
{
49+
use RefreshDatabase;
50+
51+
public function test_it_should_be_able_to_create_new_user(): void
52+
{
53+
$name = fake()->name();
54+
$email = fake()->email();
55+
56+
$user = run('create new user', [
57+
'name' => $name,
58+
'email' => $email,
59+
'password' => bcrypt('password'),
60+
]);
61+
62+
$this->assertInstanceOf(User::class, $user);
63+
64+
$this->assertSame($name, $user->name);
65+
$this->assertSame($email, $user->email);
66+
}
67+
}
68+
```
69+
70+
:::
71+
3172
## Handle Unregistered Actions
3273

33-
Since _Fraction_ is all about fundamentally defining and using actions based on string names, you might accidentally make mistakes that throw exceptions when an action in use has not been registered an exception `\Fraction\Exceptions\ActionNotRegistered` will be thrown.
74+
While you can define actions using `UnitEnum` - [as demonstrated on the using](/using#problem-solution) page, fundamentally _Fraction_ is about defining actions based on string names. For this reason, you can accidentally make typos that throw exceptions, because when an action in use has not been registered, a `\Fraction\Exceptions\ActionNotRegistered` exception will be thrown.
3475

35-
To prevent this, we provide the `actions:unregistered` Artisan command that lists all actions in use in the `app/` namespace and lists the action and the file it was detected in. **This way, you can include this command in a basic test to make sure that everything is ok with defining and using string-based actions**:
76+
To prevent this, we provide the `actions:unregistered` Artisan command that lists all actions in use in the `base_path('app/')` namespace and lists the action and the file it was detected in. This way, you can include this command in a basic test to make sure that everything is ok with defining and using string-based actions:
77+
78+
::: code-group
79+
80+
```php {4} [PestPHP]
81+
use Illuminate\Support\Facades\Artisan;
3682

37-
```php {2}
3883
test('ensure all actions name are correctly applied', function () {
3984
$output = Artisan::call('actions:unregistered');
4085

4186
expect($output)->toBe(0);
4287
});
4388
```
4489

45-
If this test fails, it means that there are actions in your code that are not registered. You can use the command to see which actions are not registered and where they are used in your codebase.
90+
```php {13} [PhpUnit]
91+
namespace Tests\Unit;
92+
93+
use Tests\TestCase;
94+
use Illuminate\Support\Facades\Artisan;
95+
use Illuminate\Foundation\Testing\RefreshDatabase;
96+
97+
class UnregisteredActionsTest extends TestCase
98+
{
99+
use RefreshDatabase;
100+
101+
public function test_ensure_all_actions_names_are_correctly_applied(): void
102+
{
103+
$output = Artisan::call('actions:unregistered');
104+
105+
$this->assertSame(0, $output);
106+
}
107+
}
108+
```
109+
110+
:::
111+
112+
If this test fails - _the `$output` is different from 0_, **it means that there are actions in your code that are not registered.** You can use the command via terminal to see which actions are not registered and where they are used in your codebase.
46113

47114
```bash
48115
php artisan actions:unregistered
49116
```
117+
118+
The output will look like this:
119+
120+
```txt
121+
WARN Unregistered actions found:
122+
123+
┌──────────────────────────────────┬─────────────────────┐
124+
│ File │ Unregistered Action │
125+
├──────────────────────────────────┼─────────────────────┤
126+
│ app/Livewire/Users/Index.php:37 │ create new user │
127+
└──────────────────────────────────┴─────────────────────┘
128+
```
129+
130+
> [!IMPORTANT]
131+
> Behind the scenes, the `actions:unregistered` command will apply a `grep` command using a regular expression that aims to identify the use of the `run` function. For this reason, the command does not differentiate the usage of `run` functions that are commented.

docs/using.md

Lines changed: 2 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -2,8 +2,6 @@
22
title: Using
33
---
44

5-
## Creating and Using
6-
75
While you can create actions manually, there is a `make:action` command that can be used to make it easier to create actions via the terminal. The output of the command is like this:
86

97
```bash
@@ -47,6 +45,7 @@ The execution of the action in this case would be like this:
4745

4846
```php
4947
namespace App\Http\Controllers;
48+
5049
use Illuminate\Http\Request\CreateUserRequest;
5150

5251
class UserController extends Controller
@@ -64,7 +63,7 @@ And yes, you can return anything from an action. The returned value will be rece
6463

6564
## Problem & Solution
6665

67-
One of the problems that was initially noticed when Fraction was created was that we interacted with strings. This is bad because if we forget a single letter, creating or executing the action can become a problem. For this reason you have two easy solutions - _one we will mention now and the other we will mention in the testing section._ You can also use `UnitEnum` to define your actions through cases, which can be useful to avoid writing errors.
66+
One of the problems that was initially noticed when Fraction was created was that we interacted with strings. This is bad because if we forget a single letter, creating or executing the action can become a problem. For this reason you have two easy solutions - [one we will mention now and the other we will mention in the testing section](/testing#handle-unregistered-actions). Your first option is use `UnitEnum` to define your actions through cases, which can be useful to avoid writing errors.
6867

6968
```php
7069
namespace App\Enums;

src/Configurable/LoggedUsing.php

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,7 @@ public function __construct(
1212
public ?string $channel = null,
1313
public ?string $message = null,
1414
) {
15-
$this->message = $message ?? __('[:name] Action: [:action] executed at :time', ['time' => now()->toDateTimeString()]);
15+
$this->message = $message ?? __('[:name] Action: [:action] executed.');
1616
}
1717

1818
/** {@inheritDoc} */

src/Console/UnregisteredActionsCommand.php

Lines changed: 57 additions & 28 deletions
Original file line numberDiff line numberDiff line change
@@ -4,10 +4,10 @@
44

55
namespace Fraction\Console;
66

7-
use Fraction\Facades\Fraction;
7+
use Exception;
88
use Illuminate\Console\Command;
9-
use Illuminate\Support\Facades\File;
10-
use SplFileInfo;
9+
use Symfony\Component\Process\Exception\ProcessFailedException;
10+
use Symfony\Component\Process\Process;
1111

1212
use function Laravel\Prompts\table;
1313

@@ -33,49 +33,78 @@ class UnregisteredActionsCommand extends Command
3333
*/
3434
public function handle(): int
3535
{
36-
$used = [];
37-
$path = base_path('app/');
38-
$files = collect(File::allFiles($path))->filter(fn (SplFileInfo $file) => str_ends_with($file->getFilename(), '.php'));
36+
$find = 'run(';
3937

40-
/** @var SplFileInfo $file */
41-
foreach ($files as $file) {
42-
$content = file_get_contents($file->getRealPath());
38+
$windows = windows_os();
4339

44-
preg_match_all("/run\(\s*['\"]([^'\"]+)['\"]\s*\)/", $content, $matches);
40+
$command = $windows
41+
? ['findstr', '/S', '/N', '/I', $find, base_path('app').'\*.php']
42+
: ['grep', '-rn', $find, base_path('app'), '--include=*.php'];
4543

46-
foreach ($matches[1] as $match) {
47-
$used[$match][] = $file->getRelativePathname(); // @phpstan-ignore-line
48-
}
44+
$process = new Process($command);
45+
46+
try {
47+
$process->mustRun();
48+
49+
return $this->output($process->getOutput());
50+
} catch (ProcessFailedException) {
51+
$this->components->error('No unregistered actions found in the codebase.');
52+
} catch (Exception $exception) {
53+
$this->components->error('Unexpected Error: '.$exception->getMessage());
54+
55+
return self::FAILURE;
4956
}
5057

51-
if (empty($used)) {
52-
$this->components->warn('No actions found in the codebase.');
58+
return self::SUCCESS;
59+
}
5360

61+
/**
62+
* Output the results of the search.
63+
*/
64+
private function output(string $output): int
65+
{
66+
if (blank($output)) {
5467
return self::SUCCESS;
5568
}
5669

57-
$actions = Fraction::all();
70+
$rows = [];
5871

59-
$defined = array_values(array_unique(array_column($actions, 'action')));
60-
$undefined = array_diff(array_keys($used), $defined);
72+
$lines = collect(explode(PHP_EOL, $output))->filter();
6173

62-
if (! empty($undefined)) {
63-
$this->components->warn(count($undefined).' occurrences found');
74+
if ($lines->count() === 0) {
75+
$this->components->info('No unregistered actions found.');
6476

65-
$rows = [];
77+
return self::SUCCESS;
78+
}
79+
80+
$this->components->warn('Unregistered actions found:');
6681

67-
foreach ($undefined as $action) {
68-
$files = implode(', ', array_unique($used[$action]));
69-
$rows[] = [$files, $action];
82+
$lines->lazy()->each(function (string $line) use (&$rows): bool {
83+
preg_match("/^(\/[^\s:]+):\d+:\s*.*?run\(\s*'([^']+)'\s*\)/", $line, $matches);
84+
85+
if (blank($line) || count($matches) < 3) {
86+
return false;
7087
}
7188

72-
table(headers: ['File', 'Unregistered Action'], rows: $rows);
89+
$path = str($matches[0])
90+
->afterLast(base_path())
91+
->beforeLast(':')
92+
->replaceFirst('/', '')
93+
->value();
7394

74-
return self::FAILURE;
95+
$rows[] = [$path, $matches[2]];
96+
97+
return true;
98+
});
99+
100+
if ($rows === []) {
101+
$this->components->info('No unregistered actions found.');
102+
103+
return self::SUCCESS;
75104
}
76105

77-
$this->components->info('No wrong actions found in the codebase.');
106+
table(['File', 'Unregistered Action'], $rows);
78107

79-
return self::SUCCESS;
108+
return self::FAILURE;
80109
}
81110
}

tests/Feature/Execution/AsSyncTest.php

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -151,7 +151,7 @@
151151
expect($test)->toBe(1);
152152

153153
Log::assertLogged(fn (LogEntry $log) => $log->level === 'info'
154-
&& $log->message === '[Laravel] Action: [one] executed at 2025-05-31 21:17:06'
154+
&& $log->message === '[Laravel] Action: [one] executed.'
155155
);
156156
});
157157

0 commit comments

Comments
 (0)