Skip to content

Commit b42d0ad

Browse files
Merge pull request #25 from kirschbaum-development/feature/events
Paragon Broadcast Events
2 parents 3d82e8b + 4004337 commit b42d0ad

16 files changed

Lines changed: 541 additions & 14 deletions

README.md

Lines changed: 82 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -6,20 +6,36 @@
66

77
A tool for automatically generating typescript/javascript objects and utilities based on their PHP counterparts.
88

9+
## Table of Contents
10+
11+
- [Requirements](#requirements)
12+
- [Installation](#installation)
13+
- [Enums](#enums)
14+
- [Custom Enum Methods](#custom-enum-methods)
15+
- [Ignoring Enums or Methods](#ignoring-enums-or-public-methods)
16+
- [Configuration](#configuration)
17+
- [Recommendations](#recommendations)
18+
- [Automatically Regenerate](#automatically-re-generating-when-modifying-php-enums)
19+
- [Technical Details](#technical-details-)
20+
- [Events](#broadcast-events)
21+
- [Usage](#usage)
22+
23+
924
## Requirements
1025

1126
| Laravel Version | Paragon Version |
1227
|:----------------|:----------------|
13-
| 11.0 | 1.0.x |
14-
| 10.0 | 1.0.x |
28+
| 12.0 | ^1.0 |
29+
| 11.0 | ^1.0 |
30+
| 10.0 | ^1.0 |
1531

1632
## Installation
1733

1834
```bash
1935
composer require kirschbaum-development/paragon
2036
```
2137

22-
### Enums
38+
## Enums
2339

2440
**TL;DR:** Run the following command to generate Typescript (or Javascript) enums from your PHP enums:
2541

@@ -59,7 +75,7 @@ This package also supports generating Javascript enums. To do so, simply pass th
5975
php artisan paragon:enum:generate --javascript
6076
```
6177

62-
### Public Methods
78+
#### Public Methods
6379

6480
A good majority of the time it is useful to use public methods to return a proper human-readable label or some other functionality on an enum. Paragon got this covered too. Assuming the following method exists on the above `Status` enum:
6581

@@ -90,7 +106,7 @@ Status.Active.color() // 'bg-green-100'
90106
Status.Inactive.color() // 'bg-red-100'
91107
```
92108

93-
### Additional Enum Methods
109+
### Custom Enum Methods
94110

95111
While this package ignores static methods on the PHP Enums, we allow you to create additional methods that Paragon will make available for every generated Enum.
96112

@@ -127,12 +143,12 @@ use Kirschbaum\Paragon\Concerns\IgnoreParagon;
127143

128144
enum Status
129145
{
130-
...
146+
// ...
131147

132148
#[IgnoreParagon]
133149
public method ignoreMe()
134150
{
135-
...
151+
// ...
136152
}
137153
}
138154
```
@@ -145,7 +161,7 @@ You can publish the configuration file by running `php artisan vendor:publish` a
145161

146162
It is recommended that the generated path for the enums is added to the `.gitignore` file. Make sure to run this command during deployment if you do this.
147163

148-
## Automatically Re-generating When Modifying PHP Enums
164+
### Automatically Re-generating When Modifying PHP Enums
149165

150166
Install the [`vite-plugin-watch`](https://www.npmjs.com/package/vite-plugin-watch) plugin in your project via `npm`:
151167

@@ -170,7 +186,7 @@ export default defineConfig({
170186
});
171187
```
172188

173-
## Technical Details 🤓
189+
### Technical Details 🤓
174190

175191
Enums are a fantastic addition to the PHP-verse but are really lame in the TypeScript-verse. However, it can be annoying trying to get those enum values on the
176192
front-end of your project. Are you supposed to pass them as a method when returning a view or perhaps via an API? This
@@ -223,4 +239,60 @@ export default Status;
223239

224240
At first glance it appears as though a lot more stuff is happening, but the above generated code allows us to interact
225241
with the enum in a nearly identical way as in PHP. And you may notice the generated TypeScript class extends the `Enum`
226-
class. This gives us some underlying functionality that is available to every enum.
242+
class. This gives us some underlying functionality that is available to every enum.
243+
244+
## Broadcast Events
245+
246+
**TL;DR:** Run the following command to generate a Typescript (or Javascript) broadcast event object based on your broadcastable events:
247+
248+
```bash
249+
php artisan paragon:event:generate
250+
```
251+
252+
Any events within the `App\Events` namespace will be automatically added to the `Events` object. Just make sure your event implements the `ShouldBroadcast` contract!
253+
254+
If javascript is needed, just make sure to add the `--javascript` or `-j` flag.
255+
256+
### Usage
257+
258+
The primary usage of this tool is for Laravel Echo. Listening to for an event class based on a hard-coded string is not exactly preferable. The can easily get out of sync or just get mistyped.
259+
260+
Here is a quick example event:
261+
262+
```php
263+
namespace App\Events;
264+
265+
class ParagonGenerated implements ShouldBroadcast
266+
{
267+
// ...
268+
}
269+
```
270+
271+
Then listen for it with Echo:
272+
273+
```js
274+
import Events from '@/events/Events.ts';
275+
276+
Echo.channel(...)
277+
.listen(Events.ParagonGenerated, /* ... */);
278+
```
279+
280+
If you have defined a `broadcastOn` method that returns a custom name, this will be handled correctly for you as well.
281+
282+
One thing to note is that if an event namespace starts with `App\Events` or `App`, these will be removed from the dot path within the object. If an event is nested, the dot path will reflect this: For example:
283+
284+
```php
285+
namespace App\Support;
286+
287+
class NestedEvent implements ShouldBroadcast
288+
{
289+
// ...
290+
}
291+
```
292+
293+
```js
294+
import Events from '@/events/Events.ts';
295+
296+
Echo.channel(...)
297+
.listen(Events.Support.NestedEvent, /* ... */);
298+
```

config/paragon.php

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -33,10 +33,17 @@
3333
'abstract-class' => 'Enum',
3434

3535
'paths' => [
36-
'php' => '',
36+
'php' => 'Enums',
3737
'ignore' => [],
3838
'generated' => 'js/enums',
3939
'methods' => 'js/vendors/paragon/enums',
4040
],
4141
],
42+
43+
'events' => [
44+
'paths' => [
45+
'php' => 'Events',
46+
'generated' => 'js/events',
47+
],
48+
],
4249
];
Lines changed: 61 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,61 @@
1+
<?php
2+
3+
namespace Kirschbaum\Paragon\Commands;
4+
5+
use Exception;
6+
use Illuminate\Console\Command;
7+
use Illuminate\Support\Arr;
8+
use Kirschbaum\Paragon\Concerns\DiscoverBroadcastEvents;
9+
use Kirschbaum\Paragon\Generators\EventGenerator;
10+
use Symfony\Component\Console\Attribute\AsCommand;
11+
use Symfony\Component\Console\Input\InputOption;
12+
13+
#[AsCommand(name: 'paragon:event:generate', description: 'Generate Typescript/Javascript definitions for Laravel Broadcast Events')]
14+
class GenerateBroadcastEventsCommand extends Command
15+
{
16+
/**
17+
* Execute the console command.
18+
*/
19+
public function handle(): int
20+
{
21+
try {
22+
$config = Arr::wrap(config('paragon.events.paths.php'));
23+
24+
/**
25+
* @var list<string> $paths
26+
*/
27+
$paths = collect($config)
28+
->map(fn (string $path): string => app_path($path))
29+
->toArray();
30+
31+
$events = DiscoverBroadcastEvents::within($paths);
32+
33+
app(EventGenerator::class, ['events' => $events, 'generateJavascript' => $this->option('javascript')])();
34+
} catch (Exception $e) {
35+
$this->components->error($e->getMessage());
36+
37+
return self::FAILURE;
38+
}
39+
40+
$this->components->info("{$events->count()} events have been (re)generated.");
41+
42+
return self::SUCCESS;
43+
}
44+
45+
/**
46+
* Get the console command options.
47+
*
48+
* @return array<int, InputOption>
49+
*/
50+
protected function getOptions(): array
51+
{
52+
return [
53+
new InputOption(
54+
name: 'javascript',
55+
shortcut: 'j',
56+
mode: InputOption::VALUE_NONE,
57+
description: 'Output Javascript files',
58+
),
59+
];
60+
}
61+
}

src/Commands/GenerateEnumsCommand.php

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,7 @@
1818
use Symfony\Component\Console\Attribute\AsCommand;
1919
use Symfony\Component\Console\Input\InputOption;
2020

21-
#[AsCommand(name: 'paragon:enum:generate', description: 'Generate Typescript versions of existing PHP enums')]
21+
#[AsCommand(name: 'paragon:enum:generate', description: 'Generate TypeScript/Javascript versions of existing PHP enums')]
2222
class GenerateEnumsCommand extends Command
2323
{
2424
/**
Lines changed: 112 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,112 @@
1+
<?php
2+
3+
namespace Kirschbaum\Paragon\Concerns;
4+
5+
use Illuminate\Contracts\Broadcasting\ShouldBroadcast;
6+
use Illuminate\Support\Collection;
7+
use ReflectionClass;
8+
use Symfony\Component\Finder\Finder;
9+
use Symfony\Component\Finder\SplFileInfo;
10+
11+
class DiscoverBroadcastEvents
12+
{
13+
/**
14+
* Get all the events by searching the given directory.
15+
*
16+
* @param string|list<string> $paths
17+
*
18+
* @return Collection<string, class-string>
19+
*/
20+
public static function within(array|string $paths): Collection
21+
{
22+
return static::getBroadcastEvents(Finder::create()->files()->in($paths));
23+
}
24+
25+
/**
26+
* Filter the files down to only concrete classes that implement ShouldBroadcast.
27+
*
28+
* @param Finder<string, SplFileInfo> $files
29+
*
30+
* @return Collection<string, class-string>
31+
*/
32+
protected static function getBroadcastEvents(Finder $files): Collection
33+
{
34+
/**
35+
* @var Collection<int, SplFileInfo> $fileCollection
36+
*/
37+
$fileCollection = collect($files);
38+
39+
return $fileCollection
40+
->mapWithKeys(function (SplFileInfo $file) {
41+
$event = static::classFromFile($file);
42+
43+
if (! $event) {
44+
return [];
45+
}
46+
47+
$reflector = new ReflectionClass($event);
48+
49+
if (
50+
$reflector->isInstantiable()
51+
&& $reflector->implementsInterface(ShouldBroadcast::class)
52+
) {
53+
$key = (string) str($event)
54+
->remove(['App\\Events\\', 'App\\'])
55+
->replace('\\', '.');
56+
57+
return [$key => $event];
58+
}
59+
60+
return [];
61+
})
62+
->filter()
63+
->sort();
64+
}
65+
66+
/**
67+
* Extract the class name from the given file path.
68+
*
69+
* @return class-string|false
70+
*/
71+
protected static function classFromFile(SplFileInfo $file): string|false
72+
{
73+
$handle = fopen($file->getRealPath(), 'r');
74+
75+
if (! $handle) {
76+
return false;
77+
}
78+
79+
$namespace = null;
80+
$class = null;
81+
82+
while (($line = fgets($handle)) !== false) {
83+
if (preg_match('/^namespace\s+([^;]+);/', $line, $matches)) {
84+
$namespace = $matches[1];
85+
}
86+
87+
if (preg_match('/^class\s+(\w+)(?:\s*:\s*\w+)?/', $line, $matches)) {
88+
$class = $matches[1];
89+
}
90+
91+
if (
92+
($namespace && $class)
93+
|| preg_match('/\b(enum|trait|interface)\b/', $line)
94+
) {
95+
break;
96+
}
97+
}
98+
99+
fclose($handle);
100+
101+
if ($namespace && $class) {
102+
/**
103+
* @var class-string $className
104+
*/
105+
$className = "{$namespace}\\{$class}";
106+
107+
return $className;
108+
}
109+
110+
return false;
111+
}
112+
}

src/Generators/EnumGenerator.php

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -33,7 +33,7 @@ class EnumGenerator
3333
protected ReflectionEnum $reflector;
3434

3535
/**
36-
* Create new EnumGenerator instance.
36+
* Create a new EnumGenerator instance.
3737
*/
3838
public function __construct(
3939
protected ReflectionEnum $enum,
@@ -99,7 +99,7 @@ protected function contents(): string
9999
}
100100

101101
/**
102-
* Build out the actual enum case object including the name, value if needed, and any public methods.
102+
* Build out the actual enum case object, including the name, value if needed, and any public methods.
103103
*
104104
* @return Collection<string, string>
105105
*/

0 commit comments

Comments
 (0)