55use Composer \InstalledVersions ;
66use Illuminate \Console \Command ;
77use Illuminate \Filesystem \Filesystem ;
8+ use Illuminate \Support \Env ;
89use Illuminate \Support \Facades \Process ;
910use Symfony \Component \Console \Attribute \AsCommand ;
1011
1112use function Illuminate \Support \artisan_binary ;
1213use function Illuminate \Support \php_binary ;
1314use 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 ' )]
1620class 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