Skip to content

Commit e134fe9

Browse files
tnyleajoetannenbaumtaylorotwell
authored
Easily implement broadcasting in a React/Vue Typescript app (Starter Kits) (#55170)
* Adding initial trial for the framework specific echo lib * few more updates * Adding functionality for vue composable * updating the react hook with updated config options * Adding the configure code injection step * Getting styleCI to pass * removing the useEcho stubs, instead will be added to laravel-echo npm package * fix spacing * fix spacing * fix spacing * making methods more efficient * making methods more efficient * updates to utilize the new packages * Update BroadcastingInstallCommand.php * better value detection for .env * Update BroadcastingInstallCommand.php * Update BroadcastingInstallCommand.php * Update BroadcastingInstallCommand.php * Update BroadcastingInstallCommand.php * Update BroadcastingInstallCommand.php * formatting * Update BroadcastingInstallCommand.php * formatting * writeVariable(s) env helpers * handle blank values cleanly * use the env variable writer * unhandle match case * no need to ask for public key * warn about pusher protocol support * move the ably warning up so that it's visible longer * enable * driver specific stubs * hopefully fix line endings --------- Co-authored-by: Joe Tannenbaum <joe.tannenbaum@laravel.com> Co-authored-by: Taylor Otwell <taylor@laravel.com>
1 parent f979990 commit e134fe9

File tree

6 files changed

+711
-23
lines changed

6 files changed

+711
-23
lines changed

src/Illuminate/Foundation/Console/BroadcastingInstallCommand.php

Lines changed: 309 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -5,12 +5,16 @@
55
use Composer\InstalledVersions;
66
use Illuminate\Console\Command;
77
use Illuminate\Filesystem\Filesystem;
8+
use Illuminate\Support\Env;
89
use Illuminate\Support\Facades\Process;
910
use Symfony\Component\Console\Attribute\AsCommand;
1011

1112
use function Illuminate\Support\artisan_binary;
1213
use function Illuminate\Support\php_binary;
1314
use function Laravel\Prompts\confirm;
15+
use function Laravel\Prompts\password;
16+
use function Laravel\Prompts\select;
17+
use function Laravel\Prompts\text;
1418

1519
#[AsCommand(name: 'install:broadcasting')]
1620
class BroadcastingInstallCommand extends Command
@@ -26,6 +30,9 @@ class BroadcastingInstallCommand extends Command
2630
{--composer=global : Absolute path to the Composer binary which should be used to install packages}
2731
{--force : Overwrite any existing broadcasting routes file}
2832
{--without-reverb : Do not prompt to install Laravel Reverb}
33+
{--reverb : Install Laravel Reverb as the default broadcaster}
34+
{--pusher : Install Pusher as the default broadcaster}
35+
{--ably : Install Ably as the default broadcaster}
2936
{--without-node : Do not prompt to install Node dependencies}';
3037

3138
/**
@@ -35,6 +42,23 @@ class BroadcastingInstallCommand extends Command
3542
*/
3643
protected $description = 'Create a broadcasting channel routes file';
3744

45+
/**
46+
* The broadcasting driver to use.
47+
*
48+
* @var string|null
49+
*/
50+
protected $driver = null;
51+
52+
/**
53+
* The framework packages to install.
54+
*
55+
* @var array
56+
*/
57+
protected $frameworkPackages = [
58+
'react' => '@laravel/echo-react',
59+
'vue' => '@laravel/echo-vue',
60+
];
61+
3862
/**
3963
* Execute the console command.
4064
*
@@ -54,25 +78,44 @@ public function handle()
5478
$this->uncommentChannelsRoutesFile();
5579
$this->enableBroadcastServiceProvider();
5680

57-
// Install bootstrapping...
58-
if (! file_exists($echoScriptPath = $this->laravel->resourcePath('js/echo.js'))) {
59-
if (! is_dir($directory = $this->laravel->resourcePath('js'))) {
60-
mkdir($directory, 0755, true);
61-
}
81+
$this->driver = $this->resolveDriver();
6282

63-
copy(__DIR__.'/stubs/echo-js.stub', $echoScriptPath);
64-
}
83+
Env::writeVariable('BROADCAST_CONNECTION', $this->driver, $this->laravel->basePath('.env'), true);
6584

66-
if (file_exists($bootstrapScriptPath = $this->laravel->resourcePath('js/bootstrap.js'))) {
67-
$bootstrapScript = file_get_contents(
68-
$bootstrapScriptPath
69-
);
85+
$this->collectDriverConfig();
86+
$this->installDriverPackages();
87+
88+
if ($this->isUsingSupportedFramework()) {
89+
// If this is a supported framework, we will use the framework-specific Echo helpers...
90+
$this->injectFrameworkSpecificConfiguration();
91+
} else {
92+
// Standard JavaScript implementation...
93+
if (! file_exists($echoScriptPath = $this->laravel->resourcePath('js/echo.js'))) {
94+
if (! is_dir($directory = $this->laravel->resourcePath('js'))) {
95+
mkdir($directory, 0755, true);
96+
}
97+
98+
$stubPath = __DIR__.'/stubs/echo-js-'.$this->driver.'.stub';
99+
100+
if (! file_exists($stubPath)) {
101+
$stubPath = __DIR__.'/stubs/echo-js-reverb.stub';
102+
}
103+
104+
copy($stubPath, $echoScriptPath);
105+
}
70106

71-
if (! str_contains($bootstrapScript, './echo')) {
72-
file_put_contents(
73-
$bootstrapScriptPath,
74-
trim($bootstrapScript.PHP_EOL.file_get_contents(__DIR__.'/stubs/echo-bootstrap-js.stub')).PHP_EOL,
107+
// Only add the bootstrap import for the standard JS implementation...
108+
if (file_exists($bootstrapScriptPath = $this->laravel->resourcePath('js/bootstrap.js'))) {
109+
$bootstrapScript = file_get_contents(
110+
$bootstrapScriptPath
75111
);
112+
113+
if (! str_contains($bootstrapScript, './echo')) {
114+
file_put_contents(
115+
$bootstrapScriptPath,
116+
trim($bootstrapScript.PHP_EOL.file_get_contents(__DIR__.'/stubs/echo-bootstrap-js.stub')).PHP_EOL,
117+
);
118+
}
76119
}
77120
}
78121

@@ -118,8 +161,10 @@ protected function enableBroadcastServiceProvider()
118161
{
119162
$filesystem = new Filesystem;
120163

121-
if (! $filesystem->exists(app()->configPath('app.php')) ||
122-
! $filesystem->exists('app/Providers/BroadcastServiceProvider.php')) {
164+
if (
165+
! $filesystem->exists(app()->configPath('app.php')) ||
166+
! $filesystem->exists('app/Providers/BroadcastServiceProvider.php')
167+
) {
123168
return;
124169
}
125170

@@ -134,14 +179,179 @@ protected function enableBroadcastServiceProvider()
134179
}
135180
}
136181

182+
/**
183+
* Collect the driver configuration.
184+
*
185+
* @return void
186+
*/
187+
protected function collectDriverConfig()
188+
{
189+
$envPath = $this->laravel->basePath('.env');
190+
191+
if (! file_exists($envPath)) {
192+
return;
193+
}
194+
195+
match ($this->driver) {
196+
'pusher' => $this->collectPusherConfig(),
197+
'ably' => $this->collectAblyConfig(),
198+
default => null,
199+
};
200+
}
201+
202+
/**
203+
* Install the driver packages.
204+
*
205+
* @return void
206+
*/
207+
protected function installDriverPackages()
208+
{
209+
$package = match ($this->driver) {
210+
'pusher' => 'pusher/pusher-php-server',
211+
'ably' => 'ably/ably-php',
212+
default => null,
213+
};
214+
215+
if (! $package || InstalledVersions::isInstalled($package)) {
216+
return;
217+
}
218+
219+
$this->requireComposerPackages($this->option('composer'), [$package]);
220+
}
221+
222+
/**
223+
* Collect the Pusher configuration.
224+
*
225+
* @return void
226+
*/
227+
protected function collectPusherConfig()
228+
{
229+
$appId = text('Pusher App ID', 'Enter your Pusher app ID');
230+
$key = password('Pusher App Key', 'Enter your Pusher app key');
231+
$secret = password('Pusher App Secret', 'Enter your Pusher app secret');
232+
233+
$cluster = select('Pusher App Cluster', [
234+
'mt1',
235+
'us2',
236+
'us3',
237+
'eu',
238+
'ap1',
239+
'ap2',
240+
'ap3',
241+
'ap4',
242+
'sa1',
243+
]);
244+
245+
Env::writeVariables([
246+
'PUSHER_APP_ID' => $appId,
247+
'PUSHER_APP_KEY' => $key,
248+
'PUSHER_APP_SECRET' => $secret,
249+
'PUSHER_APP_CLUSTER' => $cluster,
250+
'PUSHER_PORT' => 443,
251+
'PUSHER_SCHEME' => 'https',
252+
'VITE_PUSHER_APP_KEY' => '${PUSHER_APP_KEY}',
253+
'VITE_PUSHER_APP_CLUSTER' => '${PUSHER_APP_CLUSTER}',
254+
'VITE_PUSHER_HOST' => '${PUSHER_HOST}',
255+
'VITE_PUSHER_PORT' => '${PUSHER_PORT}',
256+
'VITE_PUSHER_SCHEME' => '${PUSHER_SCHEME}',
257+
], $this->laravel->basePath('.env'));
258+
}
259+
260+
/**
261+
* Collect the Ably configuration.
262+
*
263+
* @return void
264+
*/
265+
protected function collectAblyConfig()
266+
{
267+
$this->components->warn('Make sure to enable "Pusher protocol support" in your Ably app settings.');
268+
269+
$key = password('Ably Key', 'Enter your Ably key');
270+
271+
$publicKey = explode(':', $key)[0] ?? $key;
272+
273+
Env::writeVariables([
274+
'ABLY_KEY' => $key,
275+
'ABLY_PUBLIC_KEY' => $publicKey,
276+
'VITE_ABLY_PUBLIC_KEY' => '${ABLY_PUBLIC_KEY}',
277+
], $this->laravel->basePath('.env'));
278+
}
279+
280+
/**
281+
* Inject Echo configuration into the application's main file.
282+
*
283+
* @return void
284+
*/
285+
protected function injectFrameworkSpecificConfiguration()
286+
{
287+
if ($this->appUsesVue()) {
288+
$importPath = $this->frameworkPackages['vue'];
289+
290+
$filePaths = [
291+
$this->laravel->resourcePath('js/app.ts'),
292+
$this->laravel->resourcePath('js/app.js'),
293+
];
294+
} else {
295+
$importPath = $this->frameworkPackages['react'];
296+
297+
$filePaths = [
298+
$this->laravel->resourcePath('js/app.tsx'),
299+
$this->laravel->resourcePath('js/app.jsx'),
300+
];
301+
}
302+
303+
$filePath = array_filter($filePaths, function ($path) {
304+
return file_exists($path);
305+
})[0] ?? null;
306+
307+
if (! $filePath) {
308+
$this->components->warn("Could not find file [{$filePaths[0]}]. Skipping automatic Echo configuration.");
309+
310+
return;
311+
}
312+
313+
$contents = file_get_contents($filePath);
314+
315+
$echoCode = <<<JS
316+
import { configureEcho } from '{$importPath}';
317+
318+
configureEcho({
319+
broadcaster: '{$this->driver}',
320+
});
321+
JS;
322+
323+
preg_match_all('/^import .+;$/m', $contents, $matches);
324+
325+
if (empty($matches[0])) {
326+
// Add the Echo configuration to the top of the file if no import statements are found...
327+
$newContents = $echoCode.PHP_EOL.$contents;
328+
329+
file_put_contents($filePath, $newContents);
330+
} else {
331+
// Add Echo configuration after the last import...
332+
$lastImport = end($matches[0]);
333+
334+
$positionOfLastImport = strrpos($contents, $lastImport);
335+
336+
if ($positionOfLastImport !== false) {
337+
$insertPosition = $positionOfLastImport + strlen($lastImport);
338+
$newContents = substr($contents, 0, $insertPosition).PHP_EOL.$echoCode.substr($contents, $insertPosition);
339+
340+
file_put_contents($filePath, $newContents);
341+
}
342+
}
343+
344+
$this->components->info('Echo configuration added to ['.basename($filePath).'].');
345+
}
346+
137347
/**
138348
* Install Laravel Reverb into the application if desired.
139349
*
140350
* @return void
141351
*/
142352
protected function installReverb()
143353
{
144-
if ($this->option('without-reverb') || InstalledVersions::isInstalled('laravel/reverb')) {
354+
if ($this->driver !== 'reverb' || $this->option('without-reverb') || InstalledVersions::isInstalled('laravel/reverb')) {
145355
return;
146356
}
147357

@@ -199,6 +409,12 @@ protected function installNodeDependencies()
199409
];
200410
}
201411

412+
if ($this->appUsesVue()) {
413+
$commands[0] .= ' '.$this->frameworkPackages['vue'];
414+
} elseif ($this->appUsesReact()) {
415+
$commands[0] .= ' '.$this->frameworkPackages['react'];
416+
}
417+
202418
$command = Process::command(implode(' && ', $commands))
203419
->path(base_path());
204420

@@ -212,4 +428,79 @@ protected function installNodeDependencies()
212428
$this->components->info('Node dependencies installed successfully.');
213429
}
214430
}
431+
432+
/**
433+
* Resolve the provider to use based on the user's choice.
434+
*
435+
* @return string
436+
*/
437+
protected function resolveDriver(): string
438+
{
439+
if ($this->option('reverb')) {
440+
return 'reverb';
441+
}
442+
443+
if ($this->option('pusher')) {
444+
return 'pusher';
445+
}
446+
447+
if ($this->option('ably')) {
448+
return 'ably';
449+
}
450+
451+
return select('Which broadcasting driver would you like to use?', [
452+
'reverb' => 'Laravel Reverb',
453+
'pusher' => 'Pusher',
454+
'ably' => 'Ably',
455+
]);
456+
}
457+
458+
/**
459+
* Detect if the user is using a supported framework (React or Vue).
460+
*
461+
* @return bool
462+
*/
463+
protected function isUsingSupportedFramework(): bool
464+
{
465+
return $this->appUsesReact() || $this->appUsesVue();
466+
}
467+
468+
/**
469+
* Detect if the user is using React.
470+
*
471+
* @return bool
472+
*/
473+
protected function appUsesReact(): bool
474+
{
475+
return $this->packageDependenciesInclude('react');
476+
}
477+
478+
/**
479+
* Detect if the user is using Vue.
480+
*
481+
* @return bool
482+
*/
483+
protected function appUsesVue(): bool
484+
{
485+
return $this->packageDependenciesInclude('vue');
486+
}
487+
488+
/**
489+
* Detect if the package is installed.
490+
*
491+
* @return bool
492+
*/
493+
protected function packageDependenciesInclude(string $package): bool
494+
{
495+
$packageJsonPath = $this->laravel->basePath('package.json');
496+
497+
if (! file_exists($packageJsonPath)) {
498+
return false;
499+
}
500+
501+
$packageJson = json_decode(file_get_contents($packageJsonPath), true);
502+
503+
return isset($packageJson['dependencies'][$package]) ||
504+
isset($packageJson['devDependencies'][$package]);
505+
}
215506
}

0 commit comments

Comments
 (0)