From 4ffc0834563ca73dcf0edc3e47439e9296b81a1b Mon Sep 17 00:00:00 2001 From: Tony Lea Date: Fri, 21 Mar 2025 11:44:23 -0400 Subject: [PATCH 01/33] Adding initial trial for the framework specific echo lib --- .../Console/BroadcastingInstallCommand.php | 63 ++++++--- .../Foundation/Console/stubs/use-echo-ts.stub | 127 ++++++++++++++++++ 2 files changed, 171 insertions(+), 19 deletions(-) create mode 100644 src/Illuminate/Foundation/Console/stubs/use-echo-ts.stub diff --git a/src/Illuminate/Foundation/Console/BroadcastingInstallCommand.php b/src/Illuminate/Foundation/Console/BroadcastingInstallCommand.php index b23689671d18..1eb7dfb1a7a9 100644 --- a/src/Illuminate/Foundation/Console/BroadcastingInstallCommand.php +++ b/src/Illuminate/Foundation/Console/BroadcastingInstallCommand.php @@ -26,7 +26,8 @@ class BroadcastingInstallCommand extends Command {--composer=global : Absolute path to the Composer binary which should be used to install packages} {--force : Overwrite any existing broadcasting routes file} {--without-reverb : Do not prompt to install Laravel Reverb} - {--without-node : Do not prompt to install Node dependencies}'; + {--without-node : Do not prompt to install Node dependencies} + {--react : Use React TypeScript Echo implementation instead of JavaScript}'; /** * The console command description. @@ -55,24 +56,45 @@ public function handle() $this->enableBroadcastServiceProvider(); // Install bootstrapping... - if (! file_exists($echoScriptPath = $this->laravel->resourcePath('js/echo.js'))) { - if (! is_dir($directory = $this->laravel->resourcePath('js'))) { - mkdir($directory, 0755, true); + if ($this->option('react')) { + // For React, use the TypeScript implementation + $hooksDirectory = $this->laravel->resourcePath('js/hooks'); + $echoScriptPath = $hooksDirectory.'/use-echo.ts'; + + if (! file_exists($echoScriptPath)) { + // Create the hooks directory if it doesn't exist + if (! is_dir($hooksDirectory)) { + if (! is_dir($this->laravel->resourcePath('js'))) { + mkdir($this->laravel->resourcePath('js'), 0755, true); + } + mkdir($hooksDirectory, 0755, true); + } + + copy(__DIR__.'/stubs/use-echo-ts.stub', $echoScriptPath); + $this->components->info("Created React TypeScript Echo implementation at [resources/js/hooks/use-echo.ts]."); } + } else { + // Standard JavaScript implementation + if (! file_exists($echoScriptPath = $this->laravel->resourcePath('js/echo.js'))) { + if (! is_dir($directory = $this->laravel->resourcePath('js'))) { + mkdir($directory, 0755, true); + } - copy(__DIR__.'/stubs/echo-js.stub', $echoScriptPath); - } - - if (file_exists($bootstrapScriptPath = $this->laravel->resourcePath('js/bootstrap.js'))) { - $bootstrapScript = file_get_contents( - $bootstrapScriptPath - ); + copy(__DIR__.'/stubs/echo-js.stub', $echoScriptPath); + } - if (! str_contains($bootstrapScript, './echo')) { - file_put_contents( - $bootstrapScriptPath, - trim($bootstrapScript.PHP_EOL.file_get_contents(__DIR__.'/stubs/echo-bootstrap-js.stub')).PHP_EOL, + // Only add the bootstrap import for the standard JS implementation + if (file_exists($bootstrapScriptPath = $this->laravel->resourcePath('js/bootstrap.js'))) { + $bootstrapScript = file_get_contents( + $bootstrapScriptPath ); + + if (! str_contains($bootstrapScript, './echo')) { + file_put_contents( + $bootstrapScriptPath, + trim($bootstrapScript.PHP_EOL.file_get_contents(__DIR__.'/stubs/echo-bootstrap-js.stub')).PHP_EOL, + ); + } } } @@ -177,24 +199,27 @@ protected function installNodeDependencies() $this->components->info('Installing and building Node dependencies.'); + // Node dependencies are the same regardless of whether --react flag is set + $packages = 'laravel-echo pusher-js'; + if (file_exists(base_path('pnpm-lock.yaml'))) { $commands = [ - 'pnpm add --save-dev laravel-echo pusher-js', + "pnpm add --save-dev {$packages}", 'pnpm run build', ]; } elseif (file_exists(base_path('yarn.lock'))) { $commands = [ - 'yarn add --dev laravel-echo pusher-js', + "yarn add --dev {$packages}", 'yarn run build', ]; } elseif (file_exists(base_path('bun.lock')) || file_exists(base_path('bun.lockb'))) { $commands = [ - 'bun add --dev laravel-echo pusher-js', + "bun add --dev {$packages}", 'bun run build', ]; } else { $commands = [ - 'npm install --save-dev laravel-echo pusher-js', + "npm install --save-dev {$packages}", 'npm run build', ]; } diff --git a/src/Illuminate/Foundation/Console/stubs/use-echo-ts.stub b/src/Illuminate/Foundation/Console/stubs/use-echo-ts.stub new file mode 100644 index 000000000000..96f53989d76c --- /dev/null +++ b/src/Illuminate/Foundation/Console/stubs/use-echo-ts.stub @@ -0,0 +1,127 @@ +import { useEffect, useRef } from 'react'; +import Echo from 'laravel-echo'; +import Pusher from 'pusher-js'; + +// Define types for Echo channels +interface Channel { + listen(event: string, callback: (payload: any) => void): Channel; + stopListening(event: string, callback?: (payload: any) => void): Channel; +} + +interface EchoInstance extends Echo { + channel(channel: string): Channel; + private(channel: string): Channel; + leaveChannel(channel: string): void; +} + +interface ChannelData { + count: number; + channel: Channel; +} + +interface Channels { + [channelName: string]: ChannelData; +} + +// Create a singleton Echo instance +let echoInstance: EchoInstance | null = null; + +// Initialize Echo only once +const getEchoInstance = (): EchoInstance => { + if (!echoInstance) { + // Temporarily add Pusher to window object for Echo initialization + // This is a compromise - we're still avoiding permanent global namespace pollution + // by only adding it temporarily during initialization + const originalPusher = (window as any).Pusher; + (window as any).Pusher = Pusher; + + // Configure Echo with Reverb + echoInstance = new Echo({ + broadcaster: 'reverb', + key: import.meta.env.VITE_REVERB_APP_KEY, + wsHost: import.meta.env.VITE_REVERB_HOST, + wsPort: import.meta.env.VITE_REVERB_PORT ?? 80, + wssPort: import.meta.env.VITE_REVERB_PORT ?? 443, + forceTLS: (import.meta.env.VITE_REVERB_SCHEME ?? 'https') === 'https', + enabledTransports: ['ws', 'wss'], + }) as EchoInstance; + + // Restore the original Pusher value to avoid side effects + if (originalPusher) { + (window as any).Pusher = originalPusher; + } else { + delete (window as any).Pusher; + } + } + return echoInstance; +}; + +// Keep track of all active channels +const channels: Channels = {}; + +// Export Echo instance for direct access if needed +export const echo = getEchoInstance(); + +// Helper functions to interact with Echo +export const subscribeToChannel = (channelName: string, isPrivate = false): Channel => { + return isPrivate ? echo.private(channelName) : echo.channel(channelName); +}; + +export const leaveChannel = (channelName: string): void => { + echo.leaveChannel(channelName); +}; + +// The main hook for using Echo in React components +export default function useEcho( + channel: string, + event: string | string[], + callback: (payload: any) => void, + dependencies = [], + visibility: 'private' | 'public' = 'private' +) { + const eventRef = useRef(callback); + + useEffect(() => { + // Always use the latest callback + eventRef.current = callback; + + const channelName = visibility === 'public' ? channel : `${visibility}-${channel}`; + const isPrivate = visibility === 'private'; + + // Reuse existing channel subscription or create a new one + if (!channels[channelName]) { + channels[channelName] = { + count: 1, + channel: subscribeToChannel(channel, isPrivate), + }; + } else { + channels[channelName].count += 1; + } + + const subscription = channels[channelName].channel; + + const listener = (payload: any) => { + eventRef.current(payload); + }; + + const events = Array.isArray(event) ? event : [event]; + + // Subscribe to all events + events.forEach((e) => { + subscription.listen(e, listener); + }); + + // Cleanup function + return () => { + events.forEach((e) => { + subscription.stopListening(e, listener); + }); + + channels[channelName].count -= 1; + if (channels[channelName].count === 0) { + leaveChannel(channelName); + delete channels[channelName]; + } + }; + }, [...dependencies]); // eslint-disable-line +} \ No newline at end of file From f1da2c559f9b1e0c27de22eaed6c3656ee35064b Mon Sep 17 00:00:00 2001 From: Tony Lea Date: Fri, 21 Mar 2025 11:46:45 -0400 Subject: [PATCH 02/33] few more updates --- .../Foundation/Console/BroadcastingInstallCommand.php | 11 ++++------- 1 file changed, 4 insertions(+), 7 deletions(-) diff --git a/src/Illuminate/Foundation/Console/BroadcastingInstallCommand.php b/src/Illuminate/Foundation/Console/BroadcastingInstallCommand.php index 1eb7dfb1a7a9..559dd36e608e 100644 --- a/src/Illuminate/Foundation/Console/BroadcastingInstallCommand.php +++ b/src/Illuminate/Foundation/Console/BroadcastingInstallCommand.php @@ -199,27 +199,24 @@ protected function installNodeDependencies() $this->components->info('Installing and building Node dependencies.'); - // Node dependencies are the same regardless of whether --react flag is set - $packages = 'laravel-echo pusher-js'; - if (file_exists(base_path('pnpm-lock.yaml'))) { $commands = [ - "pnpm add --save-dev {$packages}", + 'pnpm add --save-dev laravel-echo pusher-js', 'pnpm run build', ]; } elseif (file_exists(base_path('yarn.lock'))) { $commands = [ - "yarn add --dev {$packages}", + 'yarn add --dev laravel-echo pusher-js', 'yarn run build', ]; } elseif (file_exists(base_path('bun.lock')) || file_exists(base_path('bun.lockb'))) { $commands = [ - "bun add --dev {$packages}", + 'bun add --dev laravel-echo pusher-js', 'bun run build', ]; } else { $commands = [ - "npm install --save-dev {$packages}", + 'npm install --save-dev laravel-echo pusher-js', 'npm run build', ]; } From b828985179537b5d165bafe2ffaba0d3ceb49493 Mon Sep 17 00:00:00 2001 From: Tony Lea Date: Tue, 25 Mar 2025 16:46:22 -0400 Subject: [PATCH 03/33] Adding functionality for vue composable --- .../Console/BroadcastingInstallCommand.php | 97 +++++++-- .../Foundation/Console/stubs/useEcho-ts.stub | 185 ++++++++++++++++++ 2 files changed, 262 insertions(+), 20 deletions(-) create mode 100644 src/Illuminate/Foundation/Console/stubs/useEcho-ts.stub diff --git a/src/Illuminate/Foundation/Console/BroadcastingInstallCommand.php b/src/Illuminate/Foundation/Console/BroadcastingInstallCommand.php index 559dd36e608e..dfab93fbd3b2 100644 --- a/src/Illuminate/Foundation/Console/BroadcastingInstallCommand.php +++ b/src/Illuminate/Foundation/Console/BroadcastingInstallCommand.php @@ -26,8 +26,7 @@ class BroadcastingInstallCommand extends Command {--composer=global : Absolute path to the Composer binary which should be used to install packages} {--force : Overwrite any existing broadcasting routes file} {--without-reverb : Do not prompt to install Laravel Reverb} - {--without-node : Do not prompt to install Node dependencies} - {--react : Use React TypeScript Echo implementation instead of JavaScript}'; + {--without-node : Do not prompt to install Node dependencies}'; /** * The console command description. @@ -48,30 +47,19 @@ public function handle() // Install channel routes file... if (! file_exists($broadcastingRoutesPath = $this->laravel->basePath('routes/channels.php')) || $this->option('force')) { $this->components->info("Published 'channels' route file."); - copy(__DIR__.'/stubs/broadcasting-routes.stub', $broadcastingRoutesPath); } $this->uncommentChannelsRoutesFile(); $this->enableBroadcastServiceProvider(); - // Install bootstrapping... - if ($this->option('react')) { - // For React, use the TypeScript implementation - $hooksDirectory = $this->laravel->resourcePath('js/hooks'); - $echoScriptPath = $hooksDirectory.'/use-echo.ts'; - - if (! file_exists($echoScriptPath)) { - // Create the hooks directory if it doesn't exist - if (! is_dir($hooksDirectory)) { - if (! is_dir($this->laravel->resourcePath('js'))) { - mkdir($this->laravel->resourcePath('js'), 0755, true); - } - mkdir($hooksDirectory, 0755, true); - } - - copy(__DIR__.'/stubs/use-echo-ts.stub', $echoScriptPath); - $this->components->info("Created React TypeScript Echo implementation at [resources/js/hooks/use-echo.ts]."); + // We have a specific echo version for React and Vue with Typescript, + // so check if this app contains React or Vue with Typescript + if ($reactOrVue = $this->appContainsReactOrVueWithTypescript()) { + if($reactOrVue === 'react') { + $this->installReactTypescriptEcho(); + } elseif($reactOrVue === 'vue') { + $this->installVueTypescriptEcho(); } } else { // Standard JavaScript implementation @@ -103,6 +91,27 @@ public function handle() $this->installNodeDependencies(); } + /** + * Detect if the user is using React or Vue with Typescript and then install the corresponding Echo implementation + * + * @return null | 'react' | 'vue' + */ + protected function appContainsReactOrVueWithTypescript() + { + $packageJsonPath = $this->laravel->basePath('package.json'); + if (!file_exists($packageJsonPath)) { + return null; + } + $packageJson = json_decode(file_get_contents($packageJsonPath), true); + if (isset($packageJson['dependencies']['react']) || isset($packageJson['dependencies']['vue'])) { + // Check if dependencies also contains typescript + if (isset($packageJson['dependencies']['typescript'])) { + return isset($packageJson['dependencies']['react']) ? 'react' : 'vue'; + } + } + return null; + } + /** * Uncomment the "channels" routes file in the application bootstrap file. * @@ -156,6 +165,54 @@ protected function enableBroadcastServiceProvider() } } + /** + * Install the React TypeScript Echo implementation. + * + * @return void + */ + protected function installReactTypescriptEcho() + { + $hooksDirectory = $this->laravel->resourcePath('js/hooks'); + $echoScriptPath = $hooksDirectory.'/use-echo.ts'; + + if (! file_exists($echoScriptPath)) { + // Create the hooks directory if it doesn't exist + if (! is_dir($hooksDirectory)) { + if (! is_dir($this->laravel->resourcePath('js'))) { + mkdir($this->laravel->resourcePath('js'), 0755, true); + } + mkdir($hooksDirectory, 0755, true); + } + + copy(__DIR__.'/stubs/use-echo-ts.stub', $echoScriptPath); + $this->components->info("Created React TypeScript Echo implementation at [resources/js/hooks/use-echo.ts]."); + } + } + + /** + * Install the Vue TypeScript Echo implementation. + * + * @return void + */ + protected function installVueTypescriptEcho() + { + $echoScriptPath = $this->laravel->resourcePath('js/composables/useEcho.ts'); + + if (! file_exists($echoScriptPath)) { + $composablesDirectory = $this->laravel->resourcePath('js/composables'); + + if (! is_dir($composablesDirectory)) { + if (! is_dir($this->laravel->resourcePath('js'))) { + mkdir($this->laravel->resourcePath('js'), 0755, true); + } + mkdir($composablesDirectory, 0755, true); + } + + copy(__DIR__.'/stubs/useEcho-ts.stub', $echoScriptPath); + $this->components->info("Created Vue TypeScript Echo implementation at [resources/js/composables/useEcho.ts]."); + } + } + /** * Install Laravel Reverb into the application if desired. * diff --git a/src/Illuminate/Foundation/Console/stubs/useEcho-ts.stub b/src/Illuminate/Foundation/Console/stubs/useEcho-ts.stub new file mode 100644 index 000000000000..3d2eb40f99f9 --- /dev/null +++ b/src/Illuminate/Foundation/Console/stubs/useEcho-ts.stub @@ -0,0 +1,185 @@ +import { ref, onMounted, onUnmounted, watch } from 'vue'; +import Echo, { EchoOptions } from 'laravel-echo'; +import Pusher from 'pusher-js'; + +// Define types for Echo channels +interface Channel { + listen(event: string, callback: (payload: any) => void): Channel; + stopListening(event: string, callback?: (payload: any) => void): Channel; +} + +interface EchoInstance extends Echo { + channel(channel: string): Channel; + private(channel: string): Channel; + leaveChannel(channel: string): void; +} + +interface ChannelData { + count: number; + channel: Channel; +} + +interface Channels { + [channelName: string]: ChannelData; +} + +// Create a singleton Echo instance +let echoInstance: EchoInstance | null = null; +let echoConfig: EchoOptions | null = null; + +// Configure Echo with custom options +export const configureEcho = (config: EchoOptions): void => { + echoConfig = config; + // Reset the instance if it was already created + if (echoInstance) { + echoInstance = null; + } +}; + +// Initialize Echo only once +const getEchoInstance = (): EchoInstance | null => { + if (!echoInstance) { + if (!echoConfig) { + console.error('Echo has not been configured. Please call configureEcho() with your configuration options before using Echo.'); + return null; + } + + // Temporarily add Pusher to window object for Echo initialization + // This is a compromise - we're still avoiding permanent global namespace pollution + // by only adding it temporarily during initialization + const originalPusher = (window as any).Pusher; + (window as any).Pusher = Pusher; + + // Configure Echo with provided config + echoInstance = new Echo(echoConfig) as EchoInstance; + + // Restore the original Pusher value to avoid side effects + if (originalPusher) { + (window as any).Pusher = originalPusher; + } else { + delete (window as any).Pusher; + } + } + return echoInstance; +}; + +// Keep track of all active channels +const channels: Channels = {}; + +// Export Echo instance for direct access if needed +export const echo = (): EchoInstance | null => getEchoInstance(); + +// Helper functions to interact with Echo +export const subscribeToChannel = (channelName: string, isPrivate = false): Channel | null => { + const instance = getEchoInstance(); + if (!instance) return null; + return isPrivate ? instance.private(channelName) : instance.channel(channelName); +}; + +export const leaveChannel = (channelName: string): void => { + const instance = getEchoInstance(); + if (!instance) return; + instance.leaveChannel(channelName); +}; + +// The main composable for using Echo in Vue components +export const useEcho = ( + channelName: string, + event: string | string[], + callback: (payload: any) => void, + dependencies: any[] = [], + visibility: 'private' | 'public' = 'private' +) => { + // Use ref to store the current callback + const eventCallback = ref(callback); + + // Track subscription for cleanup + let subscription: Channel | null = null; + let events: string[] = []; + let fullChannelName = ''; + + // Setup function to handle subscription + const setupSubscription = () => { + // Update callback ref + eventCallback.value = callback; + + // Format channel name based on visibility + fullChannelName = visibility === 'public' ? channelName : `${visibility}-${channelName}`; + const isPrivate = visibility === 'private'; + + // Reuse existing channel subscription or create a new one + if (!channels[fullChannelName]) { + const channel = subscribeToChannel(channelName, isPrivate); + if (!channel) return; + channels[fullChannelName] = { + count: 1, + channel, + }; + } else { + channels[fullChannelName].count += 1; + } + + subscription = channels[fullChannelName].channel; + + // Create listener function + const listener = (payload: any) => { + eventCallback.value(payload); + }; + + // Convert event to array if it's a single string + events = Array.isArray(event) ? event : [event]; + + // Subscribe to all events + events.forEach((e) => { + subscription?.listen(e, listener); + }); + }; + + // Cleanup function + const cleanup = () => { + if (subscription && events.length > 0) { + events.forEach((e) => { + subscription?.stopListening(e); + }); + + if (fullChannelName && channels[fullChannelName]) { + channels[fullChannelName].count -= 1; + if (channels[fullChannelName].count === 0) { + leaveChannel(fullChannelName); + delete channels[fullChannelName]; + } + } + } + }; + + // Setup subscription when component is mounted + onMounted(() => { + setupSubscription(); + }); + + // Clean up subscription when component is unmounted + onUnmounted(() => { + cleanup(); + }); + + // Watch dependencies and re-subscribe when they change + if (dependencies.length > 0) { + // Create a watch effect for each dependency + dependencies.forEach((dep, index) => { + watch(() => dependencies[index], () => { + // Clean up old subscription + cleanup(); + // Setup new subscription + setupSubscription(); + }, { deep: true }); + }); + } + + // Return the Echo instance for additional control if needed + return { + echo: getEchoInstance(), + leaveChannel: () => { + cleanup(); + } + }; +} \ No newline at end of file From f9ac68a39ed7b0b531f0c58d7f702632e6e452d2 Mon Sep 17 00:00:00 2001 From: Tony Lea Date: Tue, 25 Mar 2025 17:22:56 -0400 Subject: [PATCH 04/33] updating the react hook with updated config options --- .../Foundation/Console/stubs/use-echo-ts.stub | 79 +++++++++++++------ 1 file changed, 55 insertions(+), 24 deletions(-) diff --git a/src/Illuminate/Foundation/Console/stubs/use-echo-ts.stub b/src/Illuminate/Foundation/Console/stubs/use-echo-ts.stub index 96f53989d76c..310d97fa5616 100644 --- a/src/Illuminate/Foundation/Console/stubs/use-echo-ts.stub +++ b/src/Illuminate/Foundation/Console/stubs/use-echo-ts.stub @@ -1,5 +1,5 @@ import { useEffect, useRef } from 'react'; -import Echo from 'laravel-echo'; +import Echo, { EchoOptions } from 'laravel-echo'; import Pusher from 'pusher-js'; // Define types for Echo channels @@ -25,26 +25,33 @@ interface Channels { // Create a singleton Echo instance let echoInstance: EchoInstance | null = null; +let echoConfig: EchoOptions | null = null; + +// Configure Echo with custom options +export const configureEcho = (config: EchoOptions): void => { + echoConfig = config; + // Reset the instance if it was already created + if (echoInstance) { + echoInstance = null; + } +}; // Initialize Echo only once -const getEchoInstance = (): EchoInstance => { +const getEchoInstance = (): EchoInstance | null => { if (!echoInstance) { + if (!echoConfig) { + console.error('Echo has not been configured. Please call configureEcho() with your configuration options before using Echo.'); + return null; + } + // Temporarily add Pusher to window object for Echo initialization // This is a compromise - we're still avoiding permanent global namespace pollution // by only adding it temporarily during initialization const originalPusher = (window as any).Pusher; (window as any).Pusher = Pusher; - // Configure Echo with Reverb - echoInstance = new Echo({ - broadcaster: 'reverb', - key: import.meta.env.VITE_REVERB_APP_KEY, - wsHost: import.meta.env.VITE_REVERB_HOST, - wsPort: import.meta.env.VITE_REVERB_PORT ?? 80, - wssPort: import.meta.env.VITE_REVERB_PORT ?? 443, - forceTLS: (import.meta.env.VITE_REVERB_SCHEME ?? 'https') === 'https', - enabledTransports: ['ws', 'wss'], - }) as EchoInstance; + // Configure Echo with provided config + echoInstance = new Echo(echoConfig) as EchoInstance; // Restore the original Pusher value to avoid side effects if (originalPusher) { @@ -60,25 +67,29 @@ const getEchoInstance = (): EchoInstance => { const channels: Channels = {}; // Export Echo instance for direct access if needed -export const echo = getEchoInstance(); +export const echo = (): EchoInstance | null => getEchoInstance(); // Helper functions to interact with Echo -export const subscribeToChannel = (channelName: string, isPrivate = false): Channel => { - return isPrivate ? echo.private(channelName) : echo.channel(channelName); +export const subscribeToChannel = (channelName: string, isPrivate = false): Channel | null => { + const instance = getEchoInstance(); + if (!instance) return null; + return isPrivate ? instance.private(channelName) : instance.channel(channelName); }; export const leaveChannel = (channelName: string): void => { - echo.leaveChannel(channelName); + const instance = getEchoInstance(); + if (!instance) return; + instance.leaveChannel(channelName); }; // The main hook for using Echo in React components -export default function useEcho( +export const useEcho = ( channel: string, event: string | string[], callback: (payload: any) => void, dependencies = [], visibility: 'private' | 'public' = 'private' -) { +) => { const eventRef = useRef(callback); useEffect(() => { @@ -90,9 +101,12 @@ export default function useEcho( // Reuse existing channel subscription or create a new one if (!channels[channelName]) { + const channelSubscription = subscribeToChannel(channel, isPrivate); + if (!channelSubscription) return; + channels[channelName] = { count: 1, - channel: subscribeToChannel(channel, isPrivate), + channel: channelSubscription, }; } else { channels[channelName].count += 1; @@ -117,11 +131,28 @@ export default function useEcho( subscription.stopListening(e, listener); }); - channels[channelName].count -= 1; - if (channels[channelName].count === 0) { - leaveChannel(channelName); - delete channels[channelName]; + if (channels[channelName]) { + channels[channelName].count -= 1; + if (channels[channelName].count === 0) { + leaveChannel(channelName); + delete channels[channelName]; + } } }; }, [...dependencies]); // eslint-disable-line -} \ No newline at end of file + + // Return the Echo instance for additional control if needed + return { + echo: getEchoInstance(), + leaveChannel: () => { + const channelName = visibility === 'public' ? channel : `${visibility}-${channel}`; + if (channels[channelName]) { + channels[channelName].count -= 1; + if (channels[channelName].count === 0) { + leaveChannel(channelName); + delete channels[channelName]; + } + } + } + }; +}; \ No newline at end of file From 1b0fe3fb6883d5d4315cc453a8cafefd03e722b7 Mon Sep 17 00:00:00 2001 From: Tony Lea Date: Wed, 26 Mar 2025 08:58:12 -0400 Subject: [PATCH 05/33] Adding the configure code injection step --- .../Console/BroadcastingInstallCommand.php | 71 +++++++++++++++++++ 1 file changed, 71 insertions(+) diff --git a/src/Illuminate/Foundation/Console/BroadcastingInstallCommand.php b/src/Illuminate/Foundation/Console/BroadcastingInstallCommand.php index dfab93fbd3b2..dbe6953a7f18 100644 --- a/src/Illuminate/Foundation/Console/BroadcastingInstallCommand.php +++ b/src/Illuminate/Foundation/Console/BroadcastingInstallCommand.php @@ -61,6 +61,9 @@ public function handle() } elseif($reactOrVue === 'vue') { $this->installVueTypescriptEcho(); } + + // Inject Echo configuration for both React and Vue applications + $this->injectEchoConfigurationInApp($reactOrVue); } else { // Standard JavaScript implementation if (! file_exists($echoScriptPath = $this->laravel->resourcePath('js/echo.js'))) { @@ -213,6 +216,74 @@ protected function installVueTypescriptEcho() } } + /** + * Inject Echo configuration into the application's main file. + * + * @param string|null $appType The application type ('react', 'vue', or null) + * @return void + */ + protected function injectEchoConfigurationInApp(?string $appType = null) + { + // If app type is not provided, detect it + if ($appType === null) { + $appType = $this->appContainsReactOrVueWithTypescript(); + } + + // Determine file path and import path based on app type + if ($appType === 'vue') { + $filePath = resource_path('js/app.ts'); + $importPath = './composables/useEcho'; + $fileExtension = 'ts'; + } else { // Default to React + $filePath = resource_path('js/app.tsx'); + $importPath = './hooks/use-echo'; + $fileExtension = 'tsx'; + } + + // Check if file exists + if (!file_exists($filePath)) { + $this->components->warn("Could not find {$filePath}. Echo configuration not added."); + return; + } + + $contents = file_get_contents($filePath); + + // Prepare Echo configuration code + $echoCode = <<components->info("Echo configuration added to app.{$fileExtension} after imports."); + } + } else { + // Add the Echo configuration to the top of the file if no import statements are found + $newContents = $echoCode . "\n" . $contents; + file_put_contents($filePath, $newContents); + $this->components->info("Echo configuration added to the top of app.{$fileExtension}."); + } + } + + /** * Install Laravel Reverb into the application if desired. * From 14f498396e88951280fff05176832112ed7ee644 Mon Sep 17 00:00:00 2001 From: Tony Lea Date: Wed, 26 Mar 2025 09:34:59 -0400 Subject: [PATCH 06/33] Getting styleCI to pass --- .../Foundation/Console/BroadcastingInstallCommand.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Illuminate/Foundation/Console/BroadcastingInstallCommand.php b/src/Illuminate/Foundation/Console/BroadcastingInstallCommand.php index dbe6953a7f18..89ad0ee69739 100644 --- a/src/Illuminate/Foundation/Console/BroadcastingInstallCommand.php +++ b/src/Illuminate/Foundation/Console/BroadcastingInstallCommand.php @@ -222,7 +222,7 @@ protected function installVueTypescriptEcho() * @param string|null $appType The application type ('react', 'vue', or null) * @return void */ - protected function injectEchoConfigurationInApp(?string $appType = null) + protected function injectEchoConfigurationInApp(string $appType = null) { // If app type is not provided, detect it if ($appType === null) { From 582b10bae72b9de54b6fdc3df13aafa81d729dff Mon Sep 17 00:00:00 2001 From: Tony Lea Date: Fri, 4 Apr 2025 13:03:24 -0400 Subject: [PATCH 07/33] removing the useEcho stubs, instead will be added to laravel-echo npm package --- .../Console/BroadcastingInstallCommand.php | 56 +----- .../Foundation/Console/stubs/use-echo-ts.stub | 158 --------------- .../Foundation/Console/stubs/useEcho-ts.stub | 185 ------------------ 3 files changed, 1 insertion(+), 398 deletions(-) delete mode 100644 src/Illuminate/Foundation/Console/stubs/use-echo-ts.stub delete mode 100644 src/Illuminate/Foundation/Console/stubs/useEcho-ts.stub diff --git a/src/Illuminate/Foundation/Console/BroadcastingInstallCommand.php b/src/Illuminate/Foundation/Console/BroadcastingInstallCommand.php index 89ad0ee69739..6f2df676ffa9 100644 --- a/src/Illuminate/Foundation/Console/BroadcastingInstallCommand.php +++ b/src/Illuminate/Foundation/Console/BroadcastingInstallCommand.php @@ -56,13 +56,7 @@ public function handle() // We have a specific echo version for React and Vue with Typescript, // so check if this app contains React or Vue with Typescript if ($reactOrVue = $this->appContainsReactOrVueWithTypescript()) { - if($reactOrVue === 'react') { - $this->installReactTypescriptEcho(); - } elseif($reactOrVue === 'vue') { - $this->installVueTypescriptEcho(); - } - - // Inject Echo configuration for both React and Vue applications + // Inject Echo configuration for both React or Vue typescript applications $this->injectEchoConfigurationInApp($reactOrVue); } else { // Standard JavaScript implementation @@ -168,54 +162,6 @@ protected function enableBroadcastServiceProvider() } } - /** - * Install the React TypeScript Echo implementation. - * - * @return void - */ - protected function installReactTypescriptEcho() - { - $hooksDirectory = $this->laravel->resourcePath('js/hooks'); - $echoScriptPath = $hooksDirectory.'/use-echo.ts'; - - if (! file_exists($echoScriptPath)) { - // Create the hooks directory if it doesn't exist - if (! is_dir($hooksDirectory)) { - if (! is_dir($this->laravel->resourcePath('js'))) { - mkdir($this->laravel->resourcePath('js'), 0755, true); - } - mkdir($hooksDirectory, 0755, true); - } - - copy(__DIR__.'/stubs/use-echo-ts.stub', $echoScriptPath); - $this->components->info("Created React TypeScript Echo implementation at [resources/js/hooks/use-echo.ts]."); - } - } - - /** - * Install the Vue TypeScript Echo implementation. - * - * @return void - */ - protected function installVueTypescriptEcho() - { - $echoScriptPath = $this->laravel->resourcePath('js/composables/useEcho.ts'); - - if (! file_exists($echoScriptPath)) { - $composablesDirectory = $this->laravel->resourcePath('js/composables'); - - if (! is_dir($composablesDirectory)) { - if (! is_dir($this->laravel->resourcePath('js'))) { - mkdir($this->laravel->resourcePath('js'), 0755, true); - } - mkdir($composablesDirectory, 0755, true); - } - - copy(__DIR__.'/stubs/useEcho-ts.stub', $echoScriptPath); - $this->components->info("Created Vue TypeScript Echo implementation at [resources/js/composables/useEcho.ts]."); - } - } - /** * Inject Echo configuration into the application's main file. * diff --git a/src/Illuminate/Foundation/Console/stubs/use-echo-ts.stub b/src/Illuminate/Foundation/Console/stubs/use-echo-ts.stub deleted file mode 100644 index 310d97fa5616..000000000000 --- a/src/Illuminate/Foundation/Console/stubs/use-echo-ts.stub +++ /dev/null @@ -1,158 +0,0 @@ -import { useEffect, useRef } from 'react'; -import Echo, { EchoOptions } from 'laravel-echo'; -import Pusher from 'pusher-js'; - -// Define types for Echo channels -interface Channel { - listen(event: string, callback: (payload: any) => void): Channel; - stopListening(event: string, callback?: (payload: any) => void): Channel; -} - -interface EchoInstance extends Echo { - channel(channel: string): Channel; - private(channel: string): Channel; - leaveChannel(channel: string): void; -} - -interface ChannelData { - count: number; - channel: Channel; -} - -interface Channels { - [channelName: string]: ChannelData; -} - -// Create a singleton Echo instance -let echoInstance: EchoInstance | null = null; -let echoConfig: EchoOptions | null = null; - -// Configure Echo with custom options -export const configureEcho = (config: EchoOptions): void => { - echoConfig = config; - // Reset the instance if it was already created - if (echoInstance) { - echoInstance = null; - } -}; - -// Initialize Echo only once -const getEchoInstance = (): EchoInstance | null => { - if (!echoInstance) { - if (!echoConfig) { - console.error('Echo has not been configured. Please call configureEcho() with your configuration options before using Echo.'); - return null; - } - - // Temporarily add Pusher to window object for Echo initialization - // This is a compromise - we're still avoiding permanent global namespace pollution - // by only adding it temporarily during initialization - const originalPusher = (window as any).Pusher; - (window as any).Pusher = Pusher; - - // Configure Echo with provided config - echoInstance = new Echo(echoConfig) as EchoInstance; - - // Restore the original Pusher value to avoid side effects - if (originalPusher) { - (window as any).Pusher = originalPusher; - } else { - delete (window as any).Pusher; - } - } - return echoInstance; -}; - -// Keep track of all active channels -const channels: Channels = {}; - -// Export Echo instance for direct access if needed -export const echo = (): EchoInstance | null => getEchoInstance(); - -// Helper functions to interact with Echo -export const subscribeToChannel = (channelName: string, isPrivate = false): Channel | null => { - const instance = getEchoInstance(); - if (!instance) return null; - return isPrivate ? instance.private(channelName) : instance.channel(channelName); -}; - -export const leaveChannel = (channelName: string): void => { - const instance = getEchoInstance(); - if (!instance) return; - instance.leaveChannel(channelName); -}; - -// The main hook for using Echo in React components -export const useEcho = ( - channel: string, - event: string | string[], - callback: (payload: any) => void, - dependencies = [], - visibility: 'private' | 'public' = 'private' -) => { - const eventRef = useRef(callback); - - useEffect(() => { - // Always use the latest callback - eventRef.current = callback; - - const channelName = visibility === 'public' ? channel : `${visibility}-${channel}`; - const isPrivate = visibility === 'private'; - - // Reuse existing channel subscription or create a new one - if (!channels[channelName]) { - const channelSubscription = subscribeToChannel(channel, isPrivate); - if (!channelSubscription) return; - - channels[channelName] = { - count: 1, - channel: channelSubscription, - }; - } else { - channels[channelName].count += 1; - } - - const subscription = channels[channelName].channel; - - const listener = (payload: any) => { - eventRef.current(payload); - }; - - const events = Array.isArray(event) ? event : [event]; - - // Subscribe to all events - events.forEach((e) => { - subscription.listen(e, listener); - }); - - // Cleanup function - return () => { - events.forEach((e) => { - subscription.stopListening(e, listener); - }); - - if (channels[channelName]) { - channels[channelName].count -= 1; - if (channels[channelName].count === 0) { - leaveChannel(channelName); - delete channels[channelName]; - } - } - }; - }, [...dependencies]); // eslint-disable-line - - // Return the Echo instance for additional control if needed - return { - echo: getEchoInstance(), - leaveChannel: () => { - const channelName = visibility === 'public' ? channel : `${visibility}-${channel}`; - if (channels[channelName]) { - channels[channelName].count -= 1; - if (channels[channelName].count === 0) { - leaveChannel(channelName); - delete channels[channelName]; - } - } - } - }; -}; \ No newline at end of file diff --git a/src/Illuminate/Foundation/Console/stubs/useEcho-ts.stub b/src/Illuminate/Foundation/Console/stubs/useEcho-ts.stub deleted file mode 100644 index 3d2eb40f99f9..000000000000 --- a/src/Illuminate/Foundation/Console/stubs/useEcho-ts.stub +++ /dev/null @@ -1,185 +0,0 @@ -import { ref, onMounted, onUnmounted, watch } from 'vue'; -import Echo, { EchoOptions } from 'laravel-echo'; -import Pusher from 'pusher-js'; - -// Define types for Echo channels -interface Channel { - listen(event: string, callback: (payload: any) => void): Channel; - stopListening(event: string, callback?: (payload: any) => void): Channel; -} - -interface EchoInstance extends Echo { - channel(channel: string): Channel; - private(channel: string): Channel; - leaveChannel(channel: string): void; -} - -interface ChannelData { - count: number; - channel: Channel; -} - -interface Channels { - [channelName: string]: ChannelData; -} - -// Create a singleton Echo instance -let echoInstance: EchoInstance | null = null; -let echoConfig: EchoOptions | null = null; - -// Configure Echo with custom options -export const configureEcho = (config: EchoOptions): void => { - echoConfig = config; - // Reset the instance if it was already created - if (echoInstance) { - echoInstance = null; - } -}; - -// Initialize Echo only once -const getEchoInstance = (): EchoInstance | null => { - if (!echoInstance) { - if (!echoConfig) { - console.error('Echo has not been configured. Please call configureEcho() with your configuration options before using Echo.'); - return null; - } - - // Temporarily add Pusher to window object for Echo initialization - // This is a compromise - we're still avoiding permanent global namespace pollution - // by only adding it temporarily during initialization - const originalPusher = (window as any).Pusher; - (window as any).Pusher = Pusher; - - // Configure Echo with provided config - echoInstance = new Echo(echoConfig) as EchoInstance; - - // Restore the original Pusher value to avoid side effects - if (originalPusher) { - (window as any).Pusher = originalPusher; - } else { - delete (window as any).Pusher; - } - } - return echoInstance; -}; - -// Keep track of all active channels -const channels: Channels = {}; - -// Export Echo instance for direct access if needed -export const echo = (): EchoInstance | null => getEchoInstance(); - -// Helper functions to interact with Echo -export const subscribeToChannel = (channelName: string, isPrivate = false): Channel | null => { - const instance = getEchoInstance(); - if (!instance) return null; - return isPrivate ? instance.private(channelName) : instance.channel(channelName); -}; - -export const leaveChannel = (channelName: string): void => { - const instance = getEchoInstance(); - if (!instance) return; - instance.leaveChannel(channelName); -}; - -// The main composable for using Echo in Vue components -export const useEcho = ( - channelName: string, - event: string | string[], - callback: (payload: any) => void, - dependencies: any[] = [], - visibility: 'private' | 'public' = 'private' -) => { - // Use ref to store the current callback - const eventCallback = ref(callback); - - // Track subscription for cleanup - let subscription: Channel | null = null; - let events: string[] = []; - let fullChannelName = ''; - - // Setup function to handle subscription - const setupSubscription = () => { - // Update callback ref - eventCallback.value = callback; - - // Format channel name based on visibility - fullChannelName = visibility === 'public' ? channelName : `${visibility}-${channelName}`; - const isPrivate = visibility === 'private'; - - // Reuse existing channel subscription or create a new one - if (!channels[fullChannelName]) { - const channel = subscribeToChannel(channelName, isPrivate); - if (!channel) return; - channels[fullChannelName] = { - count: 1, - channel, - }; - } else { - channels[fullChannelName].count += 1; - } - - subscription = channels[fullChannelName].channel; - - // Create listener function - const listener = (payload: any) => { - eventCallback.value(payload); - }; - - // Convert event to array if it's a single string - events = Array.isArray(event) ? event : [event]; - - // Subscribe to all events - events.forEach((e) => { - subscription?.listen(e, listener); - }); - }; - - // Cleanup function - const cleanup = () => { - if (subscription && events.length > 0) { - events.forEach((e) => { - subscription?.stopListening(e); - }); - - if (fullChannelName && channels[fullChannelName]) { - channels[fullChannelName].count -= 1; - if (channels[fullChannelName].count === 0) { - leaveChannel(fullChannelName); - delete channels[fullChannelName]; - } - } - } - }; - - // Setup subscription when component is mounted - onMounted(() => { - setupSubscription(); - }); - - // Clean up subscription when component is unmounted - onUnmounted(() => { - cleanup(); - }); - - // Watch dependencies and re-subscribe when they change - if (dependencies.length > 0) { - // Create a watch effect for each dependency - dependencies.forEach((dep, index) => { - watch(() => dependencies[index], () => { - // Clean up old subscription - cleanup(); - // Setup new subscription - setupSubscription(); - }, { deep: true }); - }); - } - - // Return the Echo instance for additional control if needed - return { - echo: getEchoInstance(), - leaveChannel: () => { - cleanup(); - } - }; -} \ No newline at end of file From fdd1a2611f778793059ebd67079d6feb72814491 Mon Sep 17 00:00:00 2001 From: Tony Lea Date: Fri, 4 Apr 2025 13:04:07 -0400 Subject: [PATCH 08/33] fix spacing --- src/Illuminate/Foundation/Console/BroadcastingInstallCommand.php | 1 + 1 file changed, 1 insertion(+) diff --git a/src/Illuminate/Foundation/Console/BroadcastingInstallCommand.php b/src/Illuminate/Foundation/Console/BroadcastingInstallCommand.php index 6f2df676ffa9..00075882dcd0 100644 --- a/src/Illuminate/Foundation/Console/BroadcastingInstallCommand.php +++ b/src/Illuminate/Foundation/Console/BroadcastingInstallCommand.php @@ -47,6 +47,7 @@ public function handle() // Install channel routes file... if (! file_exists($broadcastingRoutesPath = $this->laravel->basePath('routes/channels.php')) || $this->option('force')) { $this->components->info("Published 'channels' route file."); + copy(__DIR__.'/stubs/broadcasting-routes.stub', $broadcastingRoutesPath); } From 44da9f81f746bf1e46b3c7082f61cc479d318d2b Mon Sep 17 00:00:00 2001 From: Tony Lea Date: Fri, 4 Apr 2025 13:04:32 -0400 Subject: [PATCH 09/33] fix spacing --- .../Foundation/Console/BroadcastingInstallCommand.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Illuminate/Foundation/Console/BroadcastingInstallCommand.php b/src/Illuminate/Foundation/Console/BroadcastingInstallCommand.php index 00075882dcd0..be41b7e378c1 100644 --- a/src/Illuminate/Foundation/Console/BroadcastingInstallCommand.php +++ b/src/Illuminate/Foundation/Console/BroadcastingInstallCommand.php @@ -47,7 +47,7 @@ public function handle() // Install channel routes file... if (! file_exists($broadcastingRoutesPath = $this->laravel->basePath('routes/channels.php')) || $this->option('force')) { $this->components->info("Published 'channels' route file."); - + copy(__DIR__.'/stubs/broadcasting-routes.stub', $broadcastingRoutesPath); } From b96f130020a9cbb1115befe611bcf56ed0daf249 Mon Sep 17 00:00:00 2001 From: Tony Lea Date: Fri, 4 Apr 2025 13:11:23 -0400 Subject: [PATCH 10/33] fix spacing --- .../Console/BroadcastingInstallCommand.php | 19 +++++++------------ 1 file changed, 7 insertions(+), 12 deletions(-) diff --git a/src/Illuminate/Foundation/Console/BroadcastingInstallCommand.php b/src/Illuminate/Foundation/Console/BroadcastingInstallCommand.php index be41b7e378c1..f69bea87a569 100644 --- a/src/Illuminate/Foundation/Console/BroadcastingInstallCommand.php +++ b/src/Illuminate/Foundation/Console/BroadcastingInstallCommand.php @@ -98,7 +98,7 @@ protected function appContainsReactOrVueWithTypescript() { $packageJsonPath = $this->laravel->basePath('package.json'); if (!file_exists($packageJsonPath)) { - return null; + return; } $packageJson = json_decode(file_get_contents($packageJsonPath), true); if (isset($packageJson['dependencies']['react']) || isset($packageJson['dependencies']['vue'])) { @@ -107,7 +107,7 @@ protected function appContainsReactOrVueWithTypescript() return isset($packageJson['dependencies']['react']) ? 'react' : 'vue'; } } - return null; + return; } /** @@ -166,24 +166,19 @@ protected function enableBroadcastServiceProvider() /** * Inject Echo configuration into the application's main file. * - * @param string|null $appType The application type ('react', 'vue', or null) + * @param string $appType The application type ('react' or 'vue') * @return void */ - protected function injectEchoConfigurationInApp(string $appType = null) - { - // If app type is not provided, detect it - if ($appType === null) { - $appType = $this->appContainsReactOrVueWithTypescript(); - } - + protected function injectEchoConfigurationInApp(string $appType = 'react') + { // Determine file path and import path based on app type if ($appType === 'vue') { $filePath = resource_path('js/app.ts'); - $importPath = './composables/useEcho'; + $importPath = 'laravel-echo/vue'; $fileExtension = 'ts'; } else { // Default to React $filePath = resource_path('js/app.tsx'); - $importPath = './hooks/use-echo'; + $importPath = 'laravel-echo/react'; $fileExtension = 'tsx'; } From c1eac4db6557f68ee3ed2170d6af7c0763fb0f01 Mon Sep 17 00:00:00 2001 From: Tony Lea Date: Fri, 4 Apr 2025 13:26:18 -0400 Subject: [PATCH 11/33] making methods more efficient --- .../Console/BroadcastingInstallCommand.php | 51 +++++++++++-------- 1 file changed, 31 insertions(+), 20 deletions(-) diff --git a/src/Illuminate/Foundation/Console/BroadcastingInstallCommand.php b/src/Illuminate/Foundation/Console/BroadcastingInstallCommand.php index f69bea87a569..34a183b29f7e 100644 --- a/src/Illuminate/Foundation/Console/BroadcastingInstallCommand.php +++ b/src/Illuminate/Foundation/Console/BroadcastingInstallCommand.php @@ -56,9 +56,9 @@ public function handle() // We have a specific echo version for React and Vue with Typescript, // so check if this app contains React or Vue with Typescript - if ($reactOrVue = $this->appContainsReactOrVueWithTypescript()) { - // Inject Echo configuration for both React or Vue typescript applications - $this->injectEchoConfigurationInApp($reactOrVue); + if ($this->appContainsReactWithTypescript() || $this->appContainsVueWithTypescript()) { + // If this is a React/Vue app with typescript, inject the Echo configuration in the app.tsx or app.ts file + $this->injectEchoConfigurationInApp(); } else { // Standard JavaScript implementation if (! file_exists($echoScriptPath = $this->laravel->resourcePath('js/echo.js'))) { @@ -90,24 +90,35 @@ public function handle() } /** - * Detect if the user is using React or Vue with Typescript and then install the corresponding Echo implementation + * Detect if the user is using React with TypeScript * - * @return null | 'react' | 'vue' + * @return bool */ - protected function appContainsReactOrVueWithTypescript() + protected function appContainsReactWithTypescript(): bool { $packageJsonPath = $this->laravel->basePath('package.json'); if (!file_exists($packageJsonPath)) { - return; + return false; } $packageJson = json_decode(file_get_contents($packageJsonPath), true); - if (isset($packageJson['dependencies']['react']) || isset($packageJson['dependencies']['vue'])) { - // Check if dependencies also contains typescript - if (isset($packageJson['dependencies']['typescript'])) { - return isset($packageJson['dependencies']['react']) ? 'react' : 'vue'; - } + return isset($packageJson['dependencies']['react']) && + isset($packageJson['dependencies']['typescript']); + } + + /** + * Detect if the user is using Vue with TypeScript + * + * @return bool + */ + protected function appContainsVueWithTypescript(): bool + { + $packageJsonPath = $this->laravel->basePath('package.json'); + if (!file_exists($packageJsonPath)) { + return false; } - return; + $packageJson = json_decode(file_get_contents($packageJsonPath), true); + return isset($packageJson['dependencies']['vue']) && + isset($packageJson['dependencies']['typescript']); } /** @@ -166,17 +177,17 @@ protected function enableBroadcastServiceProvider() /** * Inject Echo configuration into the application's main file. * - * @param string $appType The application type ('react' or 'vue') * @return void */ - protected function injectEchoConfigurationInApp(string $appType = 'react') - { - // Determine file path and import path based on app type - if ($appType === 'vue') { + protected function injectEchoConfigurationInApp() + { + // Detect which stack we are using and set appropriate configuration + if ($this->appContainsVueWithTypescript()) { $filePath = resource_path('js/app.ts'); $importPath = 'laravel-echo/vue'; $fileExtension = 'ts'; - } else { // Default to React + } else { + // Default to React $filePath = resource_path('js/app.tsx'); $importPath = 'laravel-echo/react'; $fileExtension = 'tsx'; @@ -215,7 +226,7 @@ protected function injectEchoConfigurationInApp(string $appType = 'react') $insertPos = $pos + strlen($lastImport); $newContents = substr($contents, 0, $insertPos) . "\n" . $echoCode . substr($contents, $insertPos); file_put_contents($filePath, $newContents); - $this->components->info("Echo configuration added to app.{$fileExtension} after imports."); + $this->components->info("Echo configuration added to app.{$fileExtension}."); } } else { // Add the Echo configuration to the top of the file if no import statements are found From ca7b4ac8c49c55615c76894add451572fcb0557a Mon Sep 17 00:00:00 2001 From: Tony Lea Date: Fri, 4 Apr 2025 13:28:54 -0400 Subject: [PATCH 12/33] making methods more efficient --- .../Foundation/Console/BroadcastingInstallCommand.php | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/Illuminate/Foundation/Console/BroadcastingInstallCommand.php b/src/Illuminate/Foundation/Console/BroadcastingInstallCommand.php index 34a183b29f7e..e5b6e6fecddf 100644 --- a/src/Illuminate/Foundation/Console/BroadcastingInstallCommand.php +++ b/src/Illuminate/Foundation/Console/BroadcastingInstallCommand.php @@ -216,9 +216,10 @@ protected function injectEchoConfigurationInApp() }); JS; - // Match all imports + // Find all imports preg_match_all('/^import .+;$/m', $contents, $matches); + // Add Echo configuration after the last import if (!empty($matches[0])) { $lastImport = end($matches[0]); $pos = strrpos($contents, $lastImport); From 08c1d83a7a046279eade04ada9f72c9cf7d6364f Mon Sep 17 00:00:00 2001 From: Joe Tannenbaum Date: Wed, 7 May 2025 15:54:41 -0400 Subject: [PATCH 13/33] updates to utilize the new packages --- .../Console/BroadcastingInstallCommand.php | 151 +++++++++++------- 1 file changed, 90 insertions(+), 61 deletions(-) diff --git a/src/Illuminate/Foundation/Console/BroadcastingInstallCommand.php b/src/Illuminate/Foundation/Console/BroadcastingInstallCommand.php index e5b6e6fecddf..36afa892a505 100644 --- a/src/Illuminate/Foundation/Console/BroadcastingInstallCommand.php +++ b/src/Illuminate/Foundation/Console/BroadcastingInstallCommand.php @@ -35,6 +35,11 @@ class BroadcastingInstallCommand extends Command */ protected $description = 'Create a broadcasting channel routes file'; + protected $frameworkPackages = [ + 'vue' => '@laravel/echo-vue', + 'react' => '@laravel/echo-react', + ]; + /** * Execute the console command. * @@ -48,17 +53,15 @@ public function handle() if (! file_exists($broadcastingRoutesPath = $this->laravel->basePath('routes/channels.php')) || $this->option('force')) { $this->components->info("Published 'channels' route file."); - copy(__DIR__.'/stubs/broadcasting-routes.stub', $broadcastingRoutesPath); + copy(__DIR__ . '/stubs/broadcasting-routes.stub', $broadcastingRoutesPath); } $this->uncommentChannelsRoutesFile(); $this->enableBroadcastServiceProvider(); - // We have a specific echo version for React and Vue with Typescript, - // so check if this app contains React or Vue with Typescript - if ($this->appContainsReactWithTypescript() || $this->appContainsVueWithTypescript()) { - // If this is a React/Vue app with typescript, inject the Echo configuration in the app.tsx or app.ts file - $this->injectEchoConfigurationInApp(); + if ($this->isUsingSupportedFramework()) { + // If this is a supported framework, we will use the framework-specific Echo helpers + $this->injectFrameworkSpecificConfiguration(); } else { // Standard JavaScript implementation if (! file_exists($echoScriptPath = $this->laravel->resourcePath('js/echo.js'))) { @@ -66,7 +69,7 @@ public function handle() mkdir($directory, 0755, true); } - copy(__DIR__.'/stubs/echo-js.stub', $echoScriptPath); + copy(__DIR__ . '/stubs/echo-js.stub', $echoScriptPath); } // Only add the bootstrap import for the standard JS implementation @@ -78,7 +81,7 @@ public function handle() if (! str_contains($bootstrapScript, './echo')) { file_put_contents( $bootstrapScriptPath, - trim($bootstrapScript.PHP_EOL.file_get_contents(__DIR__.'/stubs/echo-bootstrap-js.stub')).PHP_EOL, + trim($bootstrapScript . PHP_EOL . file_get_contents(__DIR__ . '/stubs/echo-bootstrap-js.stub')) . PHP_EOL, ); } } @@ -90,35 +93,52 @@ public function handle() } /** - * Detect if the user is using React with TypeScript + * Detect if the user is using a supported framework (React or Vue) * * @return bool */ - protected function appContainsReactWithTypescript(): bool + protected function isUsingSupportedFramework(): bool { - $packageJsonPath = $this->laravel->basePath('package.json'); - if (!file_exists($packageJsonPath)) { - return false; - } - $packageJson = json_decode(file_get_contents($packageJsonPath), true); - return isset($packageJson['dependencies']['react']) && - isset($packageJson['dependencies']['typescript']); + return $this->appUsesReact() || $this->appUsesVue(); + } + + /** + * Detect if the user is using React + * + * @return bool + */ + protected function appUsesReact(): bool + { + return $this->packageDependenciesInclude('react'); + } + + /** + * Detect if the user is using Vue + * + * @return bool + */ + protected function appUsesVue(): bool + { + return $this->packageDependenciesInclude('vue'); } /** - * Detect if the user is using Vue with TypeScript + * Detect if the package is installed * * @return bool */ - protected function appContainsVueWithTypescript(): bool + protected function packageDependenciesInclude(string $package): bool { $packageJsonPath = $this->laravel->basePath('package.json'); + if (!file_exists($packageJsonPath)) { return false; } + $packageJson = json_decode(file_get_contents($packageJsonPath), true); - return isset($packageJson['dependencies']['vue']) && - isset($packageJson['dependencies']['typescript']); + + return isset($packageJson['dependencies'][$package]) || + isset($packageJson['devDependencies'][$package]); } /** @@ -143,7 +163,7 @@ protected function uncommentChannelsRoutesFile() } elseif (str_contains($content, 'commands: __DIR__.\'/../routes/console.php\',')) { (new Filesystem)->replaceInFile( 'commands: __DIR__.\'/../routes/console.php\',', - 'commands: __DIR__.\'/../routes/console.php\','.PHP_EOL.' channels: __DIR__.\'/../routes/channels.php\',', + 'commands: __DIR__.\'/../routes/console.php\',' . PHP_EOL . ' channels: __DIR__.\'/../routes/channels.php\',', $appBootstrapPath, ); } @@ -158,8 +178,10 @@ protected function enableBroadcastServiceProvider() { $filesystem = new Filesystem; - if (! $filesystem->exists(app()->configPath('app.php')) || - ! $filesystem->exists('app/Providers/BroadcastServiceProvider.php')) { + if ( + ! $filesystem->exists(app()->configPath('app.php')) || + ! $filesystem->exists('app/Providers/BroadcastServiceProvider.php') + ) { return; } @@ -179,64 +201,63 @@ protected function enableBroadcastServiceProvider() * * @return void */ - protected function injectEchoConfigurationInApp() + protected function injectFrameworkSpecificConfiguration() { - // Detect which stack we are using and set appropriate configuration - if ($this->appContainsVueWithTypescript()) { - $filePath = resource_path('js/app.ts'); - $importPath = 'laravel-echo/vue'; - $fileExtension = 'ts'; + if ($this->appUsesVue()) { + $importPath = $this->frameworkPackages['vue']; + $filePaths = [ + $this->laravel->resourcePath('js/app.ts'), + $this->laravel->resourcePath('js/app.js'), + ]; } else { - // Default to React - $filePath = resource_path('js/app.tsx'); - $importPath = 'laravel-echo/react'; - $fileExtension = 'tsx'; + $importPath = $this->frameworkPackages['react']; + $filePaths = [ + $this->laravel->resourcePath('js/app.tsx'), + $this->laravel->resourcePath('js/app.jsx'), + ]; } - + + $filePath = array_filter($filePaths, function ($path) { + return file_exists($path); + })[0] ?? null; + // Check if file exists - if (!file_exists($filePath)) { - $this->components->warn("Could not find {$filePath}. Echo configuration not added."); + if (!$filePath) { + $this->components->warn("Could not find {$filePaths[0]}. Echo configuration not added."); return; } - + $contents = file_get_contents($filePath); - // Prepare Echo configuration code $echoCode = <<components->info("Echo configuration added to app.{$fileExtension}."); } - } else { - // Add the Echo configuration to the top of the file if no import statements are found - $newContents = $echoCode . "\n" . $contents; - file_put_contents($filePath, $newContents); - $this->components->info("Echo configuration added to the top of app.{$fileExtension}."); } + + $this->components->info('Echo configuration added to ' . basename($filePath) . '.'); } - + /** * Install Laravel Reverb into the application if desired. @@ -281,24 +302,32 @@ protected function installNodeDependencies() $this->components->info('Installing and building Node dependencies.'); + $additionalPackage = ''; + + if ($this->appUsesVue()) { + $additionalPackage = $this->frameworkPackages['vue']; + } elseif ($this->appUsesReact()) { + $additionalPackage = $this->frameworkPackages['react']; + } + if (file_exists(base_path('pnpm-lock.yaml'))) { $commands = [ - 'pnpm add --save-dev laravel-echo pusher-js', + trim('pnpm add --save-dev laravel-echo pusher-js ' . $additionalPackage), 'pnpm run build', ]; } elseif (file_exists(base_path('yarn.lock'))) { $commands = [ - 'yarn add --dev laravel-echo pusher-js', + trim('yarn add --dev laravel-echo pusher-js ' . $additionalPackage), 'yarn run build', ]; } elseif (file_exists(base_path('bun.lock')) || file_exists(base_path('bun.lockb'))) { $commands = [ - 'bun add --dev laravel-echo pusher-js', + trim('bun add --dev laravel-echo pusher-js ' . $additionalPackage), 'bun run build', ]; } else { $commands = [ - 'npm install --save-dev laravel-echo pusher-js', + trim('npm install --save-dev laravel-echo pusher-js ' . $additionalPackage), 'npm run build', ]; } @@ -311,7 +340,7 @@ protected function installNodeDependencies() } if ($command->run()->failed()) { - $this->components->warn("Node dependency installation failed. Please run the following commands manually: \n\n".implode(' && ', $commands)); + $this->components->warn("Node dependency installation failed. Please run the following commands manually: \n\n" . implode(' && ', $commands)); } else { $this->components->info('Node dependencies installed successfully.'); } From dcc38b20c0463183dbda4236eebc67051e874b92 Mon Sep 17 00:00:00 2001 From: Joe Tannenbaum Date: Thu, 8 May 2025 13:42:03 -0400 Subject: [PATCH 14/33] Update BroadcastingInstallCommand.php --- .../Console/BroadcastingInstallCommand.php | 187 ++++++++++++++++-- 1 file changed, 174 insertions(+), 13 deletions(-) diff --git a/src/Illuminate/Foundation/Console/BroadcastingInstallCommand.php b/src/Illuminate/Foundation/Console/BroadcastingInstallCommand.php index 36afa892a505..f614c8b5d0ff 100644 --- a/src/Illuminate/Foundation/Console/BroadcastingInstallCommand.php +++ b/src/Illuminate/Foundation/Console/BroadcastingInstallCommand.php @@ -11,6 +11,9 @@ use function Illuminate\Support\artisan_binary; use function Illuminate\Support\php_binary; use function Laravel\Prompts\confirm; +use function Laravel\Prompts\password; +use function Laravel\Prompts\select; +use function Laravel\Prompts\text; #[AsCommand(name: 'install:broadcasting')] class BroadcastingInstallCommand extends Command @@ -26,6 +29,9 @@ class BroadcastingInstallCommand extends Command {--composer=global : Absolute path to the Composer binary which should be used to install packages} {--force : Overwrite any existing broadcasting routes file} {--without-reverb : Do not prompt to install Laravel Reverb} + {--reverb : Install Laravel Reverb as the default broadcaster} + {--pusher : Install Pusher as the default broadcaster} + {--ably : Install Ably as the default broadcaster} {--without-node : Do not prompt to install Node dependencies}'; /** @@ -35,6 +41,13 @@ class BroadcastingInstallCommand extends Command */ protected $description = 'Create a broadcasting channel routes file'; + /** + * The broadcasting driver to use. + * + * @var string|null + */ + protected $driver = null; + protected $frameworkPackages = [ 'vue' => '@laravel/echo-vue', 'react' => '@laravel/echo-react', @@ -59,6 +72,10 @@ public function handle() $this->uncommentChannelsRoutesFile(); $this->enableBroadcastServiceProvider(); + $this->driver = $this->resolveDriver(); + + $this->collectDriverConfig(); + if ($this->isUsingSupportedFramework()) { // If this is a supported framework, we will use the framework-specific Echo helpers $this->injectFrameworkSpecificConfiguration(); @@ -92,6 +109,151 @@ public function handle() $this->installNodeDependencies(); } + /** + * Resolve the provider to use based on the user's choice. + * + * @return string + */ + protected function resolveDriver(): string + { + if ($this->option('reverb')) { + return 'reverb'; + } + + if ($this->option('pusher')) { + return 'pusher'; + } + + if ($this->option('ably')) { + return 'ably'; + } + + return select('Which broadcasting driver would you like to use?', [ + 'reverb' => 'Laravel Reverb', + 'pusher' => 'Pusher', + 'ably' => 'Ably', + ]); + } + + /** + * Collect the driver configuration. + * + * @return void + */ + protected function collectDriverConfig() + { + $envPath = $this->laravel->basePath('.env'); + + if (! file_exists($envPath)) { + return; + } + + match ($this->driver) { + 'reverb' => $this->collectReverbConfig(), + 'pusher' => $this->collectPusherConfig(), + 'ably' => $this->collectAblyConfig(), + }; + } + + /** + * Collect the Reverb configuration. + * + * @return void + */ + protected function collectReverbConfig() + { + $appKey = password('Reverb App Key', 'Enter your Reverb app key'); + $appSecret = text('Reverb Host', 'Enter your Reverb host'); + + $this->addToEnv([ + 'REVERB_APP_KEY' => $appKey, + 'REVERB_APP_SECRET' => $appSecret, + ]); + } + + /** + * Collect the Pusher configuration. + * + * @return void + */ + protected function collectPusherConfig() + { + $key = password('Pusher App Key', 'Enter your Pusher app key'); + $cluster = select('Pusher App Cluster', [ + 'mt1', + 'us2', + 'us3', + 'eu', + 'ap1', + 'ap2', + 'ap3', + 'ap4', + 'sa1', + ]); + + $this->addToEnv([ + 'PUSHER_APP_KEY' => $key, + 'PUSHER_APP_CLUSTER' => $cluster, + ]); + } + + /** + * Collect the Ably configuration. + * + * @return void + */ + protected function collectAblyConfig() + { + $key = password('Ably Public Key', 'Enter your Ably public key'); + + $this->addToEnv([ + 'ABLY_PUBLIC_KEY' => $key, + ]); + } + + protected function addToEnv(array $values, $addViteEnvs = true) + { + $envPath = $this->laravel->basePath('.env'); + + if (! file_exists($envPath)) { + return; + } + + $filesystem = new Filesystem(); + + $envContent = $filesystem->get($envPath); + + if ($addViteEnvs) { + foreach ($values as $key => $value) { + $values["VITE_{$key}"] = '"${' . $key . '}"'; + } + } + + $currentPrefix = null; + + foreach ($values as $key => $value) { + $prefix = explode('_', $key)[0]; + + if ($currentPrefix !== $prefix) { + $filesystem->append($envPath, PHP_EOL); + $currentPrefix = $prefix; + } + + if (str_contains($envContent, "{$key}=" . PHP_EOL)) { + $filesystem->replaceInFile( + "{$key}=" . PHP_EOL, + "{$key}={$value}" . PHP_EOL, + $envPath, + ); + } elseif (str_contains($envContent, "{$key}=")) { + $this->components->warn("The {$key} environment variable already exists in your .env file. Please update it manually."); + $this->components->warn("{$key}={$value}"); + } else { + $filesystem->append($envPath, "{$key}={$value}\n"); + } + } + } + /** * Detect if the user is using a supported framework (React or Vue) * @@ -233,7 +395,7 @@ protected function injectFrameworkSpecificConfiguration() import { configureEcho } from '{$importPath}'; configureEcho({ - broadcaster: 'reverb', + broadcaster: '{$this->driver}', }); JS; @@ -266,7 +428,7 @@ protected function injectFrameworkSpecificConfiguration() */ protected function installReverb() { - if ($this->option('without-reverb') || InstalledVersions::isInstalled('laravel/reverb')) { + if ($this->driver !== 'reverb' || $this->option('without-reverb') || InstalledVersions::isInstalled('laravel/reverb')) { return; } @@ -302,36 +464,35 @@ protected function installNodeDependencies() $this->components->info('Installing and building Node dependencies.'); - $additionalPackage = ''; - - if ($this->appUsesVue()) { - $additionalPackage = $this->frameworkPackages['vue']; - } elseif ($this->appUsesReact()) { - $additionalPackage = $this->frameworkPackages['react']; - } if (file_exists(base_path('pnpm-lock.yaml'))) { $commands = [ - trim('pnpm add --save-dev laravel-echo pusher-js ' . $additionalPackage), + 'pnpm add --save-dev laravel-echo pusher-js', 'pnpm run build', ]; } elseif (file_exists(base_path('yarn.lock'))) { $commands = [ - trim('yarn add --dev laravel-echo pusher-js ' . $additionalPackage), + 'yarn add --dev laravel-echo pusher-js', 'yarn run build', ]; } elseif (file_exists(base_path('bun.lock')) || file_exists(base_path('bun.lockb'))) { $commands = [ - trim('bun add --dev laravel-echo pusher-js ' . $additionalPackage), + 'bun add --dev laravel-echo pusher-js', 'bun run build', ]; } else { $commands = [ - trim('npm install --save-dev laravel-echo pusher-js ' . $additionalPackage), + 'npm install --save-dev laravel-echo pusher-js', 'npm run build', ]; } + if ($this->appUsesVue()) { + $commands[0] .= ' ' . $this->frameworkPackages['vue']; + } elseif ($this->appUsesReact()) { + $commands[0] .= ' ' . $this->frameworkPackages['react']; + } + $command = Process::command(implode(' && ', $commands)) ->path(base_path()); From ff2ba56f739312cc3ddcfdd1d58c35f5267909a1 Mon Sep 17 00:00:00 2001 From: Joe Tannenbaum Date: Thu, 8 May 2025 14:11:35 -0400 Subject: [PATCH 15/33] better value detection for .env --- .../Console/BroadcastingInstallCommand.php | 36 +++++++++++++------ 1 file changed, 26 insertions(+), 10 deletions(-) diff --git a/src/Illuminate/Foundation/Console/BroadcastingInstallCommand.php b/src/Illuminate/Foundation/Console/BroadcastingInstallCommand.php index f614c8b5d0ff..405e2cb350ff 100644 --- a/src/Illuminate/Foundation/Console/BroadcastingInstallCommand.php +++ b/src/Illuminate/Foundation/Console/BroadcastingInstallCommand.php @@ -219,9 +219,6 @@ protected function addToEnv(array $values, $addViteEnvs = true) return; } - $filesystem = new Filesystem(); - - $envContent = $filesystem->get($envPath); if ($addViteEnvs) { foreach ($values as $key => $value) { @@ -229,6 +226,8 @@ protected function addToEnv(array $values, $addViteEnvs = true) } } + $filesystem = new Filesystem; + $currentPrefix = null; foreach ($values as $key => $value) { @@ -239,17 +238,34 @@ protected function addToEnv(array $values, $addViteEnvs = true) $currentPrefix = $prefix; } - if (str_contains($envContent, "{$key}=" . PHP_EOL)) { - $filesystem->replaceInFile( - "{$key}=" . PHP_EOL, - "{$key}={$value}" . PHP_EOL, + $envContent = $filesystem->get($envPath); + + $newLine = $key . '=' . $value; + + preg_match('/^' . preg_quote($newLine, '/') . '$/m', $envContent, $existingLine); + + if (count($existingLine)) { + continue; + } + + preg_match('/^' . $key . '=$/m', $envContent, $emptyKeyMatches, PREG_OFFSET_CAPTURE); + preg_match('/^' . $key . '=/m', $envContent, $keyMatches, PREG_OFFSET_CAPTURE); + + if (count($emptyKeyMatches) > 0) { + $filesystem->put( $envPath, + substr_replace( + $envContent, + $newLine, + $emptyKeyMatches[0][1], + strlen($emptyKeyMatches[0][0]) + ), ); - } elseif (str_contains($envContent, "{$key}=")) { + } elseif (count($keyMatches) > 0) { $this->components->warn("The {$key} environment variable already exists in your .env file. Please update it manually."); - $this->components->warn("{$key}={$value}"); + $this->components->warn($newLine); } else { - $filesystem->append($envPath, "{$key}={$value}\n"); + $filesystem->append($envPath, "{$newLine}\n"); } } } From da5ef4a6ff1d1feffa9d736ed3adb2243a4aa108 Mon Sep 17 00:00:00 2001 From: Joe Tannenbaum Date: Thu, 8 May 2025 14:17:09 -0400 Subject: [PATCH 16/33] Update BroadcastingInstallCommand.php --- .../Foundation/Console/BroadcastingInstallCommand.php | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/src/Illuminate/Foundation/Console/BroadcastingInstallCommand.php b/src/Illuminate/Foundation/Console/BroadcastingInstallCommand.php index 405e2cb350ff..bce936f9bcba 100644 --- a/src/Illuminate/Foundation/Console/BroadcastingInstallCommand.php +++ b/src/Illuminate/Foundation/Console/BroadcastingInstallCommand.php @@ -4,6 +4,7 @@ use Composer\InstalledVersions; use Illuminate\Console\Command; +use Illuminate\Contracts\Filesystem\FileNotFoundException; use Illuminate\Filesystem\Filesystem; use Illuminate\Support\Facades\Process; use Symfony\Component\Console\Attribute\AsCommand; @@ -211,6 +212,14 @@ protected function collectAblyConfig() ]); } + /** + * Add key-value pairs to the .env file. + * + * @param array $values Key-value pairs to add to the .env file + * @param bool $addViteEnvs Also add the corresponding Vite environment variables + * + * @return void + */ protected function addToEnv(array $values, $addViteEnvs = true) { $envPath = $this->laravel->basePath('.env'); From 5694157cec03f77a34f535090159cba775fd46aa Mon Sep 17 00:00:00 2001 From: Joe Tannenbaum Date: Thu, 8 May 2025 14:40:44 -0400 Subject: [PATCH 17/33] Update BroadcastingInstallCommand.php --- src/Illuminate/Foundation/Console/BroadcastingInstallCommand.php | 1 - 1 file changed, 1 deletion(-) diff --git a/src/Illuminate/Foundation/Console/BroadcastingInstallCommand.php b/src/Illuminate/Foundation/Console/BroadcastingInstallCommand.php index bce936f9bcba..4d99850852a2 100644 --- a/src/Illuminate/Foundation/Console/BroadcastingInstallCommand.php +++ b/src/Illuminate/Foundation/Console/BroadcastingInstallCommand.php @@ -4,7 +4,6 @@ use Composer\InstalledVersions; use Illuminate\Console\Command; -use Illuminate\Contracts\Filesystem\FileNotFoundException; use Illuminate\Filesystem\Filesystem; use Illuminate\Support\Facades\Process; use Symfony\Component\Console\Attribute\AsCommand; From 6803598a7884330362e3a377ddf6877f0c7078b5 Mon Sep 17 00:00:00 2001 From: Joe Tannenbaum Date: Thu, 8 May 2025 14:41:10 -0400 Subject: [PATCH 18/33] Update BroadcastingInstallCommand.php --- .../Foundation/Console/BroadcastingInstallCommand.php | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/src/Illuminate/Foundation/Console/BroadcastingInstallCommand.php b/src/Illuminate/Foundation/Console/BroadcastingInstallCommand.php index 4d99850852a2..ba7f99e76283 100644 --- a/src/Illuminate/Foundation/Console/BroadcastingInstallCommand.php +++ b/src/Illuminate/Foundation/Console/BroadcastingInstallCommand.php @@ -48,6 +48,11 @@ class BroadcastingInstallCommand extends Command */ protected $driver = null; + /** + * The framework packages to install. + * + * @var array + */ protected $frameworkPackages = [ 'vue' => '@laravel/echo-vue', 'react' => '@laravel/echo-react', From f1a333382e001b11c2764b445e58dbe8b36be717 Mon Sep 17 00:00:00 2001 From: Joe Tannenbaum Date: Thu, 8 May 2025 14:43:17 -0400 Subject: [PATCH 19/33] Update BroadcastingInstallCommand.php --- .../Console/BroadcastingInstallCommand.php | 51 +++++++++---------- 1 file changed, 24 insertions(+), 27 deletions(-) diff --git a/src/Illuminate/Foundation/Console/BroadcastingInstallCommand.php b/src/Illuminate/Foundation/Console/BroadcastingInstallCommand.php index ba7f99e76283..c60127945fb8 100644 --- a/src/Illuminate/Foundation/Console/BroadcastingInstallCommand.php +++ b/src/Illuminate/Foundation/Console/BroadcastingInstallCommand.php @@ -71,7 +71,7 @@ public function handle() if (! file_exists($broadcastingRoutesPath = $this->laravel->basePath('routes/channels.php')) || $this->option('force')) { $this->components->info("Published 'channels' route file."); - copy(__DIR__ . '/stubs/broadcasting-routes.stub', $broadcastingRoutesPath); + copy(__DIR__.'/stubs/broadcasting-routes.stub', $broadcastingRoutesPath); } $this->uncommentChannelsRoutesFile(); @@ -91,7 +91,7 @@ public function handle() mkdir($directory, 0755, true); } - copy(__DIR__ . '/stubs/echo-js.stub', $echoScriptPath); + copy(__DIR__.'/stubs/echo-js.stub', $echoScriptPath); } // Only add the bootstrap import for the standard JS implementation @@ -103,7 +103,7 @@ public function handle() if (! str_contains($bootstrapScript, './echo')) { file_put_contents( $bootstrapScriptPath, - trim($bootstrapScript . PHP_EOL . file_get_contents(__DIR__ . '/stubs/echo-bootstrap-js.stub')) . PHP_EOL, + trim($bootstrapScript.PHP_EOL.file_get_contents(__DIR__.'/stubs/echo-bootstrap-js.stub')).PHP_EOL, ); } } @@ -219,9 +219,8 @@ protected function collectAblyConfig() /** * Add key-value pairs to the .env file. * - * @param array $values Key-value pairs to add to the .env file - * @param bool $addViteEnvs Also add the corresponding Vite environment variables - * + * @param array $values Key-value pairs to add to the .env file + * @param bool $addViteEnvs Also add the corresponding Vite environment variables * @return void */ protected function addToEnv(array $values, $addViteEnvs = true) @@ -232,10 +231,9 @@ protected function addToEnv(array $values, $addViteEnvs = true) return; } - if ($addViteEnvs) { foreach ($values as $key => $value) { - $values["VITE_{$key}"] = '"${' . $key . '}"'; + $values["VITE_{$key}"] = '"${'.$key.'}"'; } } @@ -253,16 +251,16 @@ protected function addToEnv(array $values, $addViteEnvs = true) $envContent = $filesystem->get($envPath); - $newLine = $key . '=' . $value; + $newLine = $key.'='.$value; - preg_match('/^' . preg_quote($newLine, '/') . '$/m', $envContent, $existingLine); + preg_match('/^'.preg_quote($newLine, '/').'$/m', $envContent, $existingLine); if (count($existingLine)) { continue; } - preg_match('/^' . $key . '=$/m', $envContent, $emptyKeyMatches, PREG_OFFSET_CAPTURE); - preg_match('/^' . $key . '=/m', $envContent, $keyMatches, PREG_OFFSET_CAPTURE); + preg_match('/^'.$key.'=$/m', $envContent, $emptyKeyMatches, PREG_OFFSET_CAPTURE); + preg_match('/^'.$key.'=/m', $envContent, $keyMatches, PREG_OFFSET_CAPTURE); if (count($emptyKeyMatches) > 0) { $filesystem->put( @@ -284,7 +282,7 @@ protected function addToEnv(array $values, $addViteEnvs = true) } /** - * Detect if the user is using a supported framework (React or Vue) + * Detect if the user is using a supported framework (React or Vue). * * @return bool */ @@ -294,7 +292,7 @@ protected function isUsingSupportedFramework(): bool } /** - * Detect if the user is using React + * Detect if the user is using React. * * @return bool */ @@ -304,7 +302,7 @@ protected function appUsesReact(): bool } /** - * Detect if the user is using Vue + * Detect if the user is using Vue. * * @return bool */ @@ -314,7 +312,7 @@ protected function appUsesVue(): bool } /** - * Detect if the package is installed + * Detect if the package is installed. * * @return bool */ @@ -322,7 +320,7 @@ protected function packageDependenciesInclude(string $package): bool { $packageJsonPath = $this->laravel->basePath('package.json'); - if (!file_exists($packageJsonPath)) { + if (! file_exists($packageJsonPath)) { return false; } @@ -354,7 +352,7 @@ protected function uncommentChannelsRoutesFile() } elseif (str_contains($content, 'commands: __DIR__.\'/../routes/console.php\',')) { (new Filesystem)->replaceInFile( 'commands: __DIR__.\'/../routes/console.php\',', - 'commands: __DIR__.\'/../routes/console.php\',' . PHP_EOL . ' channels: __DIR__.\'/../routes/channels.php\',', + 'commands: __DIR__.\'/../routes/console.php\','.PHP_EOL.' channels: __DIR__.\'/../routes/channels.php\',', $appBootstrapPath, ); } @@ -413,8 +411,9 @@ protected function injectFrameworkSpecificConfiguration() })[0] ?? null; // Check if file exists - if (!$filePath) { + if (! $filePath) { $this->components->warn("Could not find {$filePaths[0]}. Echo configuration not added."); + return; } @@ -432,7 +431,7 @@ protected function injectFrameworkSpecificConfiguration() if (empty($matches[0])) { // Add the Echo configuration to the top of the file if no import statements are found - $newContents = $echoCode . PHP_EOL . $contents; + $newContents = $echoCode.PHP_EOL.$contents; file_put_contents($filePath, $newContents); } else { // Add Echo configuration after the last import @@ -441,15 +440,14 @@ protected function injectFrameworkSpecificConfiguration() if ($pos !== false) { $insertPos = $pos + strlen($lastImport); - $newContents = substr($contents, 0, $insertPos) . PHP_EOL . $echoCode . substr($contents, $insertPos); + $newContents = substr($contents, 0, $insertPos).PHP_EOL.$echoCode.substr($contents, $insertPos); file_put_contents($filePath, $newContents); } } - $this->components->info('Echo configuration added to ' . basename($filePath) . '.'); + $this->components->info('Echo configuration added to '.basename($filePath).'.'); } - /** * Install Laravel Reverb into the application if desired. * @@ -493,7 +491,6 @@ protected function installNodeDependencies() $this->components->info('Installing and building Node dependencies.'); - if (file_exists(base_path('pnpm-lock.yaml'))) { $commands = [ 'pnpm add --save-dev laravel-echo pusher-js', @@ -517,9 +514,9 @@ protected function installNodeDependencies() } if ($this->appUsesVue()) { - $commands[0] .= ' ' . $this->frameworkPackages['vue']; + $commands[0] .= ' '.$this->frameworkPackages['vue']; } elseif ($this->appUsesReact()) { - $commands[0] .= ' ' . $this->frameworkPackages['react']; + $commands[0] .= ' '.$this->frameworkPackages['react']; } $command = Process::command(implode(' && ', $commands)) @@ -530,7 +527,7 @@ protected function installNodeDependencies() } if ($command->run()->failed()) { - $this->components->warn("Node dependency installation failed. Please run the following commands manually: \n\n" . implode(' && ', $commands)); + $this->components->warn("Node dependency installation failed. Please run the following commands manually: \n\n".implode(' && ', $commands)); } else { $this->components->info('Node dependencies installed successfully.'); } From 01129ac72b0302ebeb650d4c801500a038e60075 Mon Sep 17 00:00:00 2001 From: Joe Tannenbaum Date: Thu, 8 May 2025 14:44:54 -0400 Subject: [PATCH 20/33] Update BroadcastingInstallCommand.php --- src/Illuminate/Foundation/Console/BroadcastingInstallCommand.php | 1 - 1 file changed, 1 deletion(-) diff --git a/src/Illuminate/Foundation/Console/BroadcastingInstallCommand.php b/src/Illuminate/Foundation/Console/BroadcastingInstallCommand.php index c60127945fb8..210d07c13ad9 100644 --- a/src/Illuminate/Foundation/Console/BroadcastingInstallCommand.php +++ b/src/Illuminate/Foundation/Console/BroadcastingInstallCommand.php @@ -410,7 +410,6 @@ protected function injectFrameworkSpecificConfiguration() return file_exists($path); })[0] ?? null; - // Check if file exists if (! $filePath) { $this->components->warn("Could not find {$filePaths[0]}. Echo configuration not added."); From afe3a4a60934563e6e57ec976604e11ef966277a Mon Sep 17 00:00:00 2001 From: Taylor Otwell Date: Sat, 10 May 2025 13:40:04 -0500 Subject: [PATCH 21/33] formatting --- .../Console/BroadcastingInstallCommand.php | 265 +++++++++--------- 1 file changed, 133 insertions(+), 132 deletions(-) diff --git a/src/Illuminate/Foundation/Console/BroadcastingInstallCommand.php b/src/Illuminate/Foundation/Console/BroadcastingInstallCommand.php index 210d07c13ad9..712fcc6eb118 100644 --- a/src/Illuminate/Foundation/Console/BroadcastingInstallCommand.php +++ b/src/Illuminate/Foundation/Console/BroadcastingInstallCommand.php @@ -54,8 +54,8 @@ class BroadcastingInstallCommand extends Command * @var array */ protected $frameworkPackages = [ - 'vue' => '@laravel/echo-vue', 'react' => '@laravel/echo-react', + 'vue' => '@laravel/echo-vue', ]; /** @@ -82,10 +82,10 @@ public function handle() $this->collectDriverConfig(); if ($this->isUsingSupportedFramework()) { - // If this is a supported framework, we will use the framework-specific Echo helpers + // If this is a supported framework, we will use the framework-specific Echo helpers... $this->injectFrameworkSpecificConfiguration(); } else { - // Standard JavaScript implementation + // Standard JavaScript implementation... if (! file_exists($echoScriptPath = $this->laravel->resourcePath('js/echo.js'))) { if (! is_dir($directory = $this->laravel->resourcePath('js'))) { mkdir($directory, 0755, true); @@ -94,7 +94,7 @@ public function handle() copy(__DIR__.'/stubs/echo-js.stub', $echoScriptPath); } - // Only add the bootstrap import for the standard JS implementation + // Only add the bootstrap import for the standard JS implementation... if (file_exists($bootstrapScriptPath = $this->laravel->resourcePath('js/bootstrap.js'))) { $bootstrapScript = file_get_contents( $bootstrapScriptPath @@ -115,29 +115,58 @@ public function handle() } /** - * Resolve the provider to use based on the user's choice. + * Uncomment the "channels" routes file in the application bootstrap file. * - * @return string + * @return void */ - protected function resolveDriver(): string + protected function uncommentChannelsRoutesFile() { - if ($this->option('reverb')) { - return 'reverb'; - } + $appBootstrapPath = $this->laravel->bootstrapPath('app.php'); - if ($this->option('pusher')) { - return 'pusher'; + $content = file_get_contents($appBootstrapPath); + + if (str_contains($content, '// channels: ')) { + (new Filesystem)->replaceInFile( + '// channels: ', + 'channels: ', + $appBootstrapPath, + ); + } elseif (str_contains($content, 'channels: ')) { + return; + } elseif (str_contains($content, 'commands: __DIR__.\'/../routes/console.php\',')) { + (new Filesystem)->replaceInFile( + 'commands: __DIR__.\'/../routes/console.php\',', + 'commands: __DIR__.\'/../routes/console.php\','.PHP_EOL.' channels: __DIR__.\'/../routes/channels.php\',', + $appBootstrapPath, + ); } + } - if ($this->option('ably')) { - return 'ably'; + /** + * Uncomment the "BroadcastServiceProvider" in the application configuration. + * + * @return void + */ + protected function enableBroadcastServiceProvider() + { + $filesystem = new Filesystem; + + if ( + ! $filesystem->exists(app()->configPath('app.php')) || + ! $filesystem->exists('app/Providers/BroadcastServiceProvider.php') + ) { + return; } - return select('Which broadcasting driver would you like to use?', [ - 'reverb' => 'Laravel Reverb', - 'pusher' => 'Pusher', - 'ably' => 'Ably', - ]); + $config = $filesystem->get(app()->configPath('app.php')); + + if (str_contains($config, '// App\Providers\BroadcastServiceProvider::class')) { + $filesystem->replaceInFile( + '// App\Providers\BroadcastServiceProvider::class', + 'App\Providers\BroadcastServiceProvider::class', + app()->configPath('app.php'), + ); + } } /** @@ -167,13 +196,13 @@ protected function collectDriverConfig() */ protected function collectReverbConfig() { - $appKey = password('Reverb App Key', 'Enter your Reverb app key'); - $appSecret = text('Reverb Host', 'Enter your Reverb host'); + // $appKey = text('Reverb App Key', 'Enter your Reverb app key', default: 'my-app-key'); + // $appSecret = text('Reverb Secret', 'Enter your Reverb secret', default: 'my-app-secret'); - $this->addToEnv([ - 'REVERB_APP_KEY' => $appKey, - 'REVERB_APP_SECRET' => $appSecret, - ]); + // $this->addToEnv([ + // 'REVERB_APP_KEY' => $appKey, + // 'REVERB_APP_SECRET' => $appSecret, + // ]); } /** @@ -184,6 +213,7 @@ protected function collectReverbConfig() protected function collectPusherConfig() { $key = password('Pusher App Key', 'Enter your Pusher app key'); + $cluster = select('Pusher App Cluster', [ 'mt1', 'us2', @@ -219,8 +249,8 @@ protected function collectAblyConfig() /** * Add key-value pairs to the .env file. * - * @param array $values Key-value pairs to add to the .env file - * @param bool $addViteEnvs Also add the corresponding Vite environment variables + * @param array $values + * @param bool $addViteEnvs * @return void */ protected function addToEnv(array $values, $addViteEnvs = true) @@ -273,7 +303,7 @@ protected function addToEnv(array $values, $addViteEnvs = true) ), ); } elseif (count($keyMatches) > 0) { - $this->components->warn("The {$key} environment variable already exists in your .env file. Please update it manually."); + $this->components->warn("The [{$key}] environment variable already exists in your .env file. Please update this value manually."); $this->components->warn($newLine); } else { $filesystem->append($envPath, "{$newLine}\n"); @@ -281,110 +311,6 @@ protected function addToEnv(array $values, $addViteEnvs = true) } } - /** - * Detect if the user is using a supported framework (React or Vue). - * - * @return bool - */ - protected function isUsingSupportedFramework(): bool - { - return $this->appUsesReact() || $this->appUsesVue(); - } - - /** - * Detect if the user is using React. - * - * @return bool - */ - protected function appUsesReact(): bool - { - return $this->packageDependenciesInclude('react'); - } - - /** - * Detect if the user is using Vue. - * - * @return bool - */ - protected function appUsesVue(): bool - { - return $this->packageDependenciesInclude('vue'); - } - - /** - * Detect if the package is installed. - * - * @return bool - */ - protected function packageDependenciesInclude(string $package): bool - { - $packageJsonPath = $this->laravel->basePath('package.json'); - - if (! file_exists($packageJsonPath)) { - return false; - } - - $packageJson = json_decode(file_get_contents($packageJsonPath), true); - - return isset($packageJson['dependencies'][$package]) || - isset($packageJson['devDependencies'][$package]); - } - - /** - * Uncomment the "channels" routes file in the application bootstrap file. - * - * @return void - */ - protected function uncommentChannelsRoutesFile() - { - $appBootstrapPath = $this->laravel->bootstrapPath('app.php'); - - $content = file_get_contents($appBootstrapPath); - - if (str_contains($content, '// channels: ')) { - (new Filesystem)->replaceInFile( - '// channels: ', - 'channels: ', - $appBootstrapPath, - ); - } elseif (str_contains($content, 'channels: ')) { - return; - } elseif (str_contains($content, 'commands: __DIR__.\'/../routes/console.php\',')) { - (new Filesystem)->replaceInFile( - 'commands: __DIR__.\'/../routes/console.php\',', - 'commands: __DIR__.\'/../routes/console.php\','.PHP_EOL.' channels: __DIR__.\'/../routes/channels.php\',', - $appBootstrapPath, - ); - } - } - - /** - * Uncomment the "BroadcastServiceProvider" in the application configuration. - * - * @return void - */ - protected function enableBroadcastServiceProvider() - { - $filesystem = new Filesystem; - - if ( - ! $filesystem->exists(app()->configPath('app.php')) || - ! $filesystem->exists('app/Providers/BroadcastServiceProvider.php') - ) { - return; - } - - $config = $filesystem->get(app()->configPath('app.php')); - - if (str_contains($config, '// App\Providers\BroadcastServiceProvider::class')) { - $filesystem->replaceInFile( - '// App\Providers\BroadcastServiceProvider::class', - 'App\Providers\BroadcastServiceProvider::class', - app()->configPath('app.php'), - ); - } - } - /** * Inject Echo configuration into the application's main file. * @@ -531,4 +457,79 @@ protected function installNodeDependencies() $this->components->info('Node dependencies installed successfully.'); } } + + /** + * Resolve the provider to use based on the user's choice. + * + * @return string + */ + protected function resolveDriver(): string + { + if ($this->option('reverb')) { + return 'reverb'; + } + + if ($this->option('pusher')) { + return 'pusher'; + } + + if ($this->option('ably')) { + return 'ably'; + } + + return select('Which broadcasting driver would you like to use?', [ + 'reverb' => 'Laravel Reverb', + 'pusher' => 'Pusher', + 'ably' => 'Ably', + ]); + } + + /** + * Detect if the user is using a supported framework (React or Vue). + * + * @return bool + */ + protected function isUsingSupportedFramework(): bool + { + return $this->appUsesReact() || $this->appUsesVue(); + } + + /** + * Detect if the user is using React. + * + * @return bool + */ + protected function appUsesReact(): bool + { + return $this->packageDependenciesInclude('react'); + } + + /** + * Detect if the user is using Vue. + * + * @return bool + */ + protected function appUsesVue(): bool + { + return $this->packageDependenciesInclude('vue'); + } + + /** + * Detect if the package is installed. + * + * @return bool + */ + protected function packageDependenciesInclude(string $package): bool + { + $packageJsonPath = $this->laravel->basePath('package.json'); + + if (! file_exists($packageJsonPath)) { + return false; + } + + $packageJson = json_decode(file_get_contents($packageJsonPath), true); + + return isset($packageJson['dependencies'][$package]) || + isset($packageJson['devDependencies'][$package]); + } } From eed2e370757584e704ea593605e8587db82446a5 Mon Sep 17 00:00:00 2001 From: Joe Tannenbaum Date: Sat, 10 May 2025 17:12:03 -0400 Subject: [PATCH 22/33] Update BroadcastingInstallCommand.php --- .../Console/BroadcastingInstallCommand.php | 17 ++++++++++++++--- 1 file changed, 14 insertions(+), 3 deletions(-) diff --git a/src/Illuminate/Foundation/Console/BroadcastingInstallCommand.php b/src/Illuminate/Foundation/Console/BroadcastingInstallCommand.php index 712fcc6eb118..32a816dffea0 100644 --- a/src/Illuminate/Foundation/Console/BroadcastingInstallCommand.php +++ b/src/Illuminate/Foundation/Console/BroadcastingInstallCommand.php @@ -212,7 +212,9 @@ protected function collectReverbConfig() */ protected function collectPusherConfig() { + $appId = text('Pusher App ID', 'Enter your Pusher app ID'); $key = password('Pusher App Key', 'Enter your Pusher app key'); + $secret = password('Pusher App Secret', 'Enter your Pusher app secret'); $cluster = select('Pusher App Cluster', [ 'mt1', @@ -227,9 +229,18 @@ protected function collectPusherConfig() ]); $this->addToEnv([ + 'PUSHER_APP_ID' => $appId, 'PUSHER_APP_KEY' => $key, + 'PUSHER_APP_SECRET' => $secret, 'PUSHER_APP_CLUSTER' => $cluster, - ]); + 'PUSHER_PORT' => 443, + 'PUSHER_SCHEME' => 'https', + 'VITE_PUSHER_APP_KEY' => '${PUSHER_APP_KEY}', + 'VITE_PUSHER_APP_CLUSTER' => '${PUSHER_APP_CLUSTER}', + 'VITE_PUSHER_HOST' => '${PUSHER_HOST}', + 'VITE_PUSHER_PORT' => '${PUSHER_PORT}', + 'VITE_PUSHER_SCHEME' => '${PUSHER_SCHEME}', + ], false); } /** @@ -263,7 +274,7 @@ protected function addToEnv(array $values, $addViteEnvs = true) if ($addViteEnvs) { foreach ($values as $key => $value) { - $values["VITE_{$key}"] = '"${'.$key.'}"'; + $values["VITE_{$key}"] = '${'.$key.'}'; } } @@ -281,7 +292,7 @@ protected function addToEnv(array $values, $addViteEnvs = true) $envContent = $filesystem->get($envPath); - $newLine = $key.'='.$value; + $newLine = $key.'='.(is_string($value) ? '"'.$value.'"' : $value); preg_match('/^'.preg_quote($newLine, '/').'$/m', $envContent, $existingLine); From d35e413e483e8148677348f698a2b955947aad93 Mon Sep 17 00:00:00 2001 From: Taylor Otwell Date: Mon, 12 May 2025 13:43:37 -0500 Subject: [PATCH 23/33] formatting --- .../Console/BroadcastingInstallCommand.php | 38 +++++++------------ 1 file changed, 13 insertions(+), 25 deletions(-) diff --git a/src/Illuminate/Foundation/Console/BroadcastingInstallCommand.php b/src/Illuminate/Foundation/Console/BroadcastingInstallCommand.php index 32a816dffea0..57d7735beda7 100644 --- a/src/Illuminate/Foundation/Console/BroadcastingInstallCommand.php +++ b/src/Illuminate/Foundation/Console/BroadcastingInstallCommand.php @@ -183,28 +183,11 @@ protected function collectDriverConfig() } match ($this->driver) { - 'reverb' => $this->collectReverbConfig(), 'pusher' => $this->collectPusherConfig(), 'ably' => $this->collectAblyConfig(), }; } - /** - * Collect the Reverb configuration. - * - * @return void - */ - protected function collectReverbConfig() - { - // $appKey = text('Reverb App Key', 'Enter your Reverb app key', default: 'my-app-key'); - // $appSecret = text('Reverb Secret', 'Enter your Reverb secret', default: 'my-app-secret'); - - // $this->addToEnv([ - // 'REVERB_APP_KEY' => $appKey, - // 'REVERB_APP_SECRET' => $appSecret, - // ]); - } - /** * Collect the Pusher configuration. * @@ -331,12 +314,14 @@ protected function injectFrameworkSpecificConfiguration() { if ($this->appUsesVue()) { $importPath = $this->frameworkPackages['vue']; + $filePaths = [ $this->laravel->resourcePath('js/app.ts'), $this->laravel->resourcePath('js/app.js'), ]; } else { $importPath = $this->frameworkPackages['react']; + $filePaths = [ $this->laravel->resourcePath('js/app.tsx'), $this->laravel->resourcePath('js/app.jsx'), @@ -348,7 +333,7 @@ protected function injectFrameworkSpecificConfiguration() })[0] ?? null; if (! $filePath) { - $this->components->warn("Could not find {$filePaths[0]}. Echo configuration not added."); + $this->components->warn("Could not find file [{$filePaths[0]}]. Skipping automatic Echo configuration."); return; } @@ -366,22 +351,25 @@ protected function injectFrameworkSpecificConfiguration() preg_match_all('/^import .+;$/m', $contents, $matches); if (empty($matches[0])) { - // Add the Echo configuration to the top of the file if no import statements are found + // Add the Echo configuration to the top of the file if no import statements are found... $newContents = $echoCode.PHP_EOL.$contents; + file_put_contents($filePath, $newContents); } else { - // Add Echo configuration after the last import + // Add Echo configuration after the last import... $lastImport = end($matches[0]); - $pos = strrpos($contents, $lastImport); - if ($pos !== false) { - $insertPos = $pos + strlen($lastImport); - $newContents = substr($contents, 0, $insertPos).PHP_EOL.$echoCode.substr($contents, $insertPos); + $positionOfLastImport = strrpos($contents, $lastImport); + + if ($positionOfLastImport !== false) { + $insertPosition = $positionOfLastImport + strlen($lastImport); + $newContents = substr($contents, 0, $insertPosition).PHP_EOL.$echoCode.substr($contents, $insertPosition); + file_put_contents($filePath, $newContents); } } - $this->components->info('Echo configuration added to '.basename($filePath).'.'); + $this->components->info('Echo configuration added to ['.basename($filePath).'].'); } /** From eeb4199f67f6a580a75664c69bf8e015f0ab8c0e Mon Sep 17 00:00:00 2001 From: Joe Tannenbaum Date: Mon, 12 May 2025 16:18:41 -0400 Subject: [PATCH 24/33] writeVariable(s) env helpers --- src/Illuminate/Support/Env.php | 125 ++++++++++++++ tests/Support/SupportHelpersTest.php | 250 ++++++++++++++++++++++++++- 2 files changed, 370 insertions(+), 5 deletions(-) diff --git a/src/Illuminate/Support/Env.php b/src/Illuminate/Support/Env.php index 702f61d44f4c..1586d86ad130 100644 --- a/src/Illuminate/Support/Env.php +++ b/src/Illuminate/Support/Env.php @@ -5,6 +5,8 @@ use Closure; use Dotenv\Repository\Adapter\PutenvAdapter; use Dotenv\Repository\RepositoryBuilder; +use Illuminate\Contracts\Filesystem\FileNotFoundException; +use Illuminate\Filesystem\Filesystem; use PhpOption\Option; use RuntimeException; @@ -114,6 +116,129 @@ public static function getOrFail($key) return self::getOption($key)->getOrThrow(new RuntimeException("Environment variable [$key] has no value.")); } + /** + * Write an array of key-value pairs to the environment file. + * + * @param array $variables + * @param string $pathToFile + * @param bool $overwrite + * @return void + * + * @throws RuntimeException + * @throws FileNotFoundException + */ + public static function writeVariables(array $variables, string $pathToFile, bool $overwrite = false): void + { + $filesystem = new Filesystem; + + if ($filesystem->missing($pathToFile)) { + throw new RuntimeException("The file [{$pathToFile}] does not exist."); + } + + $lines = explode(PHP_EOL, $filesystem->get($pathToFile)); + + foreach ($variables as $key => $value) { + $lines = self::addVariableToEnvContents($key, $value, $lines, $overwrite); + } + + $filesystem->put($pathToFile, implode(PHP_EOL, $lines)); + } + + /** + * Write a single key-value pair to the environment file. + * + * @param string $key + * @param mixed $value + * @param string $pathToFile + * @param bool $overwrite + * @return void + * + * @throws RuntimeException + * @throws FileNotFoundException + */ + public static function writeVariable(string $key, mixed $value, string $pathToFile, bool $overwrite = false): void + { + $filesystem = new Filesystem; + + if ($filesystem->missing($pathToFile)) { + throw new RuntimeException("The file [{$pathToFile}] does not exist."); + } + + $envContent = $filesystem->get($pathToFile); + + $lines = explode(PHP_EOL, $envContent); + $lines = self::addVariableToEnvContents($key, $value, $lines, $overwrite); + + $filesystem->put($pathToFile, implode(PHP_EOL, $lines)); + } + + /** + * Add a variable to the environment file contents. + * + * @param string $key + * @param mixed $value + * @param array $envLines + * @param bool $overwrite + * @return array + */ + protected static function addVariableToEnvContents(string $key, mixed $value, array $envLines, bool $overwrite): array + { + $prefix = explode('_', $key)[0].'_'; + $lastPrefixIndex = -1; + + $shouldQuote = preg_match('/^[a-zA-z0-9]+$/', $value) === 0; + + $lineToAddVariations = [ + $key.'='.(is_string($value) ? '"'.addslashes($value).'"' : $value), + $key.'='.(is_string($value) ? "'".addslashes($value)."'" : $value), + $key.'='.$value, + ]; + + $lineToAdd = $shouldQuote ? $lineToAddVariations[0] : $lineToAddVariations[2]; + + foreach ($envLines as $index => $line) { + if (str_starts_with($line, $prefix)) { + $lastPrefixIndex = $index; + } + + if (in_array($line, $lineToAddVariations)) { + // This exact line already exists, so we don't need to add it again. + return $envLines; + } + + if ($line === $key.'=') { + // If the value is empty, we can replace it with the new value. + $envLines[$index] = $lineToAdd; + + return $envLines; + } + + if (str_starts_with($line, $key.'=')) { + if (! $overwrite) { + return $envLines; + } + + $envLines[$index] = $lineToAdd; + + return $envLines; + } + } + + if ($lastPrefixIndex === -1) { + if (count($envLines) && $envLines[count($envLines) - 1] !== '') { + $envLines[] = ''; + } + + return array_merge($envLines, [$lineToAdd]); + } + + return array_merge( + array_slice($envLines, 0, $lastPrefixIndex + 1), + [$lineToAdd], + array_slice($envLines, $lastPrefixIndex + 1) + ); + } + /** * Get the possible option for this environment variable. * diff --git a/tests/Support/SupportHelpersTest.php b/tests/Support/SupportHelpersTest.php index ee67f818a3cd..e9a52219c68a 100644 --- a/tests/Support/SupportHelpersTest.php +++ b/tests/Support/SupportHelpersTest.php @@ -8,6 +8,7 @@ use Error; use Illuminate\Contracts\Support\Htmlable; use Illuminate\Database\Eloquent\Model; +use Illuminate\Filesystem\Filesystem; use Illuminate\Support\Env; use Illuminate\Support\Optional; use Illuminate\Support\Sleep; @@ -26,10 +27,21 @@ class SupportHelpersTest extends TestCase { + protected function setUp(): void + { + mkdir(__DIR__.'/tmp'); + + parent::setUp(); + } + protected function tearDown(): void { m::close(); + if (is_dir(__DIR__.'/tmp')) { + (new Filesystem)->deleteDirectory(__DIR__.'/tmp'); + } + parent::tearDown(); } @@ -747,11 +759,13 @@ class_uses_recursive(SupportTestClassThree::class) public function testTraitUsesRecursive() { - $this->assertSame([ - 'Illuminate\Tests\Support\SupportTestTraitTwo' => 'Illuminate\Tests\Support\SupportTestTraitTwo', - 'Illuminate\Tests\Support\SupportTestTraitOne' => 'Illuminate\Tests\Support\SupportTestTraitOne', - ], - trait_uses_recursive(SupportTestClassOne::class)); + $this->assertSame( + [ + 'Illuminate\Tests\Support\SupportTestTraitTwo' => 'Illuminate\Tests\Support\SupportTestTraitTwo', + 'Illuminate\Tests\Support\SupportTestTraitOne' => 'Illuminate\Tests\Support\SupportTestTraitOne', + ], + trait_uses_recursive(SupportTestClassOne::class) + ); $this->assertSame([], trait_uses_recursive(SupportTestClassTwo::class)); } @@ -1212,6 +1226,232 @@ public function testEnvEscapedString() $this->assertSame('x"null"x', env('foo')); } + public function testWriteArrayOfEnvVariablesToFile() + { + $filesystem = new Filesystem; + $path = __DIR__.'/tmp/env-test-file'; + $filesystem->put($path, <<<'ENV' + APP_NAME=Laravel + APP_ENV=local + APP_KEY=base64:randomkey + APP_DEBUG=true + APP_URL=http://localhost + + DB_CONNECTION=mysql + DB_HOST= + ENV); + + Env::writeVariables([ + 'APP_VIBE' => 'chill', + 'DB_HOST' => '127:0:0:1', + 'DB_PORT' => 3306, + 'BRAND_NEW_PREFIX' => 'fresh value', + ], $path); + + $this->assertSame( + <<<'ENV' + APP_NAME=Laravel + APP_ENV=local + APP_KEY=base64:randomkey + APP_DEBUG=true + APP_URL=http://localhost + APP_VIBE=chill + + DB_CONNECTION=mysql + DB_HOST="127:0:0:1" + DB_PORT=3306 + + BRAND_NEW_PREFIX="fresh value" + ENV, + $filesystem->get($path) + ); + } + + public function testWriteArrayOfEnvVariablesToFileAndOverwrite() + { + $filesystem = new Filesystem; + $path = __DIR__.'/tmp/env-test-file'; + $filesystem->put($path, <<<'ENV' + APP_NAME=Laravel + APP_ENV=local + APP_KEY=base64:randomkey + APP_DEBUG=true + APP_URL=http://localhost + + DB_CONNECTION=mysql + DB_HOST= + ENV); + + Env::writeVariables([ + 'APP_VIBE' => 'chill', + 'DB_HOST' => '127:0:0:1', + 'DB_CONNECTION' => 'sqlite', + ], $path, true); + + $this->assertSame( + <<<'ENV' + APP_NAME=Laravel + APP_ENV=local + APP_KEY=base64:randomkey + APP_DEBUG=true + APP_URL=http://localhost + APP_VIBE=chill + + DB_CONNECTION=sqlite + DB_HOST="127:0:0:1" + ENV, + $filesystem->get($path) + ); + } + + public function testWillNotOverwriteArrayOfVariables() + { + $filesystem = new Filesystem; + $path = __DIR__.'/tmp/env-test-file'; + $filesystem->put($path, <<<'ENV' + APP_NAME=Laravel + APP_ENV=local + APP_KEY=base64:randomkey + APP_DEBUG=true + APP_URL=http://localhost + APP_VIBE=odd + + DB_CONNECTION=mysql + DB_HOST= + ENV); + + Env::writeVariables([ + 'APP_VIBE' => 'chill', + 'DB_HOST' => '127:0:0:1', + ], $path); + + $this->assertSame( + <<<'ENV' + APP_NAME=Laravel + APP_ENV=local + APP_KEY=base64:randomkey + APP_DEBUG=true + APP_URL=http://localhost + APP_VIBE=odd + + DB_CONNECTION=mysql + DB_HOST="127:0:0:1" + ENV, + $filesystem->get($path) + ); + } + + public function testWriteVariableToFile() + { + $filesystem = new Filesystem; + $path = __DIR__.'/tmp/env-test-file'; + $filesystem->put($path, <<<'ENV' + APP_NAME=Laravel + APP_ENV=local + APP_KEY=base64:randomkey + APP_DEBUG=true + APP_URL=http://localhost + + DB_CONNECTION=mysql + DB_HOST= + ENV); + + Env::writeVariable('APP_VIBE', 'chill', $path); + + $this->assertSame( + <<<'ENV' + APP_NAME=Laravel + APP_ENV=local + APP_KEY=base64:randomkey + APP_DEBUG=true + APP_URL=http://localhost + APP_VIBE=chill + + DB_CONNECTION=mysql + DB_HOST= + ENV, + $filesystem->get($path) + ); + } + + public function testWillNotOverwriteVariable() + { + $filesystem = new Filesystem; + $path = __DIR__.'/tmp/env-test-file'; + $filesystem->put($path, <<<'ENV' + APP_NAME=Laravel + APP_ENV=local + APP_KEY=base64:randomkey + APP_DEBUG=true + APP_URL=http://localhost + APP_VIBE=odd + + DB_CONNECTION=mysql + DB_HOST= + ENV); + + Env::writeVariable('APP_VIBE', 'chill', $path); + + $this->assertSame( + <<<'ENV' + APP_NAME=Laravel + APP_ENV=local + APP_KEY=base64:randomkey + APP_DEBUG=true + APP_URL=http://localhost + APP_VIBE=odd + + DB_CONNECTION=mysql + DB_HOST= + ENV, + $filesystem->get($path) + ); + } + + public function testWriteVariableToFileAndOverwrite() + { + $filesystem = new Filesystem; + $path = __DIR__.'/tmp/env-test-file'; + $filesystem->put($path, <<<'ENV' + APP_NAME=Laravel + APP_ENV=local + APP_KEY=base64:randomkey + APP_DEBUG=true + APP_URL=http://localhost + APP_VIBE=odd + + DB_CONNECTION=mysql + DB_HOST= + ENV); + + Env::writeVariable('APP_VIBE', 'chill', $path, true); + + $this->assertSame( + <<<'ENV' + APP_NAME=Laravel + APP_ENV=local + APP_KEY=base64:randomkey + APP_DEBUG=true + APP_URL=http://localhost + APP_VIBE=chill + + DB_CONNECTION=mysql + DB_HOST= + ENV, + $filesystem->get($path) + ); + } + + public function testWillThrowAnExceptionIfFileIsMissingWhenTryingToWriteVariables(): void + { + $this->expectExceptionObject(new RuntimeException('The file [missing-file] does not exist.')); + + Env::writeVariables([ + 'APP_VIBE' => 'chill', + 'DB_HOST' => '127:0:0:1', + ], 'missing-file'); + } + public function testGetFromSERVERFirst() { $_ENV['foo'] = 'From $_ENV'; From 0aab9b58a5e025dd7d5b5b004821bf56aada7266 Mon Sep 17 00:00:00 2001 From: Joe Tannenbaum Date: Mon, 12 May 2025 16:44:15 -0400 Subject: [PATCH 25/33] handle blank values cleanly --- src/Illuminate/Support/Env.php | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/src/Illuminate/Support/Env.php b/src/Illuminate/Support/Env.php index 1586d86ad130..a52c10bd4f2a 100644 --- a/src/Illuminate/Support/Env.php +++ b/src/Illuminate/Support/Env.php @@ -196,6 +196,10 @@ protected static function addVariableToEnvContents(string $key, mixed $value, ar $lineToAdd = $shouldQuote ? $lineToAddVariations[0] : $lineToAddVariations[2]; + if ($value === '') { + $lineToAdd = $key.'='; + } + foreach ($envLines as $index => $line) { if (str_starts_with($line, $prefix)) { $lastPrefixIndex = $index; From 4c20ed00b7cfe6c3fd9875ad62d06190c6df3cda Mon Sep 17 00:00:00 2001 From: Joe Tannenbaum Date: Mon, 12 May 2025 16:44:32 -0400 Subject: [PATCH 26/33] use the env variable writer --- .../Console/BroadcastingInstallCommand.php | 104 ++++++------------ 1 file changed, 34 insertions(+), 70 deletions(-) diff --git a/src/Illuminate/Foundation/Console/BroadcastingInstallCommand.php b/src/Illuminate/Foundation/Console/BroadcastingInstallCommand.php index 57d7735beda7..2bf3ac9d3b9c 100644 --- a/src/Illuminate/Foundation/Console/BroadcastingInstallCommand.php +++ b/src/Illuminate/Foundation/Console/BroadcastingInstallCommand.php @@ -5,6 +5,7 @@ use Composer\InstalledVersions; use Illuminate\Console\Command; use Illuminate\Filesystem\Filesystem; +use Illuminate\Support\Env; use Illuminate\Support\Facades\Process; use Symfony\Component\Console\Attribute\AsCommand; @@ -79,7 +80,10 @@ public function handle() $this->driver = $this->resolveDriver(); + Env::writeVariable('BROADCAST_CONNECTION', $this->driver, $this->laravel->basePath('.env'), true); + $this->collectDriverConfig(); + $this->installDriverPackages(); if ($this->isUsingSupportedFramework()) { // If this is a supported framework, we will use the framework-specific Echo helpers... @@ -188,6 +192,26 @@ protected function collectDriverConfig() }; } + /** + * Install the driver packages. + * + * @return void + */ + protected function installDriverPackages() + { + $package = match ($this->driver) { + 'pusher' => 'pusher/pusher-php-server', + 'ably' => 'ably/ably-php', + default => null, + }; + + if (! $package || InstalledVersions::isInstalled($package)) { + return; + } + + $this->requireComposerPackages($this->option('composer'), [$package]); + } + /** * Collect the Pusher configuration. * @@ -211,7 +235,7 @@ protected function collectPusherConfig() 'sa1', ]); - $this->addToEnv([ + Env::writeVariables([ 'PUSHER_APP_ID' => $appId, 'PUSHER_APP_KEY' => $key, 'PUSHER_APP_SECRET' => $secret, @@ -223,7 +247,7 @@ protected function collectPusherConfig() 'VITE_PUSHER_HOST' => '${PUSHER_HOST}', 'VITE_PUSHER_PORT' => '${PUSHER_PORT}', 'VITE_PUSHER_SCHEME' => '${PUSHER_SCHEME}', - ], false); + ], $this->laravel->basePath('.env')); } /** @@ -233,76 +257,16 @@ protected function collectPusherConfig() */ protected function collectAblyConfig() { - $key = password('Ably Public Key', 'Enter your Ably public key'); - - $this->addToEnv([ - 'ABLY_PUBLIC_KEY' => $key, - ]); - } + $key = password('Ably Key', 'Enter your Ably key'); + $publicKey = password('Ably Public Key', 'Enter your Ably public key'); - /** - * Add key-value pairs to the .env file. - * - * @param array $values - * @param bool $addViteEnvs - * @return void - */ - protected function addToEnv(array $values, $addViteEnvs = true) - { - $envPath = $this->laravel->basePath('.env'); + $publicKey = explode(':', $publicKey)[0] ?? $publicKey; - if (! file_exists($envPath)) { - return; - } - - if ($addViteEnvs) { - foreach ($values as $key => $value) { - $values["VITE_{$key}"] = '${'.$key.'}'; - } - } - - $filesystem = new Filesystem; - - $currentPrefix = null; - - foreach ($values as $key => $value) { - $prefix = explode('_', $key)[0]; - - if ($currentPrefix !== $prefix) { - $filesystem->append($envPath, PHP_EOL); - $currentPrefix = $prefix; - } - - $envContent = $filesystem->get($envPath); - - $newLine = $key.'='.(is_string($value) ? '"'.$value.'"' : $value); - - preg_match('/^'.preg_quote($newLine, '/').'$/m', $envContent, $existingLine); - - if (count($existingLine)) { - continue; - } - - preg_match('/^'.$key.'=$/m', $envContent, $emptyKeyMatches, PREG_OFFSET_CAPTURE); - preg_match('/^'.$key.'=/m', $envContent, $keyMatches, PREG_OFFSET_CAPTURE); - - if (count($emptyKeyMatches) > 0) { - $filesystem->put( - $envPath, - substr_replace( - $envContent, - $newLine, - $emptyKeyMatches[0][1], - strlen($emptyKeyMatches[0][0]) - ), - ); - } elseif (count($keyMatches) > 0) { - $this->components->warn("The [{$key}] environment variable already exists in your .env file. Please update this value manually."); - $this->components->warn($newLine); - } else { - $filesystem->append($envPath, "{$newLine}\n"); - } - } + Env::writeVariables([ + 'ABLY_KEY' => $key, + 'ABLY_PUBLIC_KEY' => $publicKey, + 'VITE_ABLY_PUBLIC_KEY' => '${ABLY_PUBLIC_KEY}', + ], $this->laravel->basePath('.env')); } /** From 9e26989430a6fc1642eac537c350edf8bae46040 Mon Sep 17 00:00:00 2001 From: Joe Tannenbaum Date: Mon, 12 May 2025 16:58:15 -0400 Subject: [PATCH 27/33] unhandle match case --- src/Illuminate/Foundation/Console/BroadcastingInstallCommand.php | 1 + 1 file changed, 1 insertion(+) diff --git a/src/Illuminate/Foundation/Console/BroadcastingInstallCommand.php b/src/Illuminate/Foundation/Console/BroadcastingInstallCommand.php index 2bf3ac9d3b9c..6f2845ae71b7 100644 --- a/src/Illuminate/Foundation/Console/BroadcastingInstallCommand.php +++ b/src/Illuminate/Foundation/Console/BroadcastingInstallCommand.php @@ -189,6 +189,7 @@ protected function collectDriverConfig() match ($this->driver) { 'pusher' => $this->collectPusherConfig(), 'ably' => $this->collectAblyConfig(), + default => null, }; } From 8d51a5084b5ef7707dbbfa0a0463f3870c2ce196 Mon Sep 17 00:00:00 2001 From: Joe Tannenbaum Date: Mon, 12 May 2025 17:09:01 -0400 Subject: [PATCH 28/33] no need to ask for public key --- .../Foundation/Console/BroadcastingInstallCommand.php | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/src/Illuminate/Foundation/Console/BroadcastingInstallCommand.php b/src/Illuminate/Foundation/Console/BroadcastingInstallCommand.php index 6f2845ae71b7..6f38777d7b8c 100644 --- a/src/Illuminate/Foundation/Console/BroadcastingInstallCommand.php +++ b/src/Illuminate/Foundation/Console/BroadcastingInstallCommand.php @@ -259,9 +259,8 @@ protected function collectPusherConfig() protected function collectAblyConfig() { $key = password('Ably Key', 'Enter your Ably key'); - $publicKey = password('Ably Public Key', 'Enter your Ably public key'); - - $publicKey = explode(':', $publicKey)[0] ?? $publicKey; + + $publicKey = explode(':', $key)[0] ?? $key; Env::writeVariables([ 'ABLY_KEY' => $key, From e2725cd35b1ac52e68e0f0a5d9f6b2ad929d877c Mon Sep 17 00:00:00 2001 From: Joe Tannenbaum Date: Mon, 12 May 2025 22:11:33 -0400 Subject: [PATCH 29/33] warn about pusher protocol support --- .../Foundation/Console/BroadcastingInstallCommand.php | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/src/Illuminate/Foundation/Console/BroadcastingInstallCommand.php b/src/Illuminate/Foundation/Console/BroadcastingInstallCommand.php index 6f38777d7b8c..05bc9d5cf20d 100644 --- a/src/Illuminate/Foundation/Console/BroadcastingInstallCommand.php +++ b/src/Illuminate/Foundation/Console/BroadcastingInstallCommand.php @@ -259,7 +259,7 @@ protected function collectPusherConfig() protected function collectAblyConfig() { $key = password('Ably Key', 'Enter your Ably key'); - + $publicKey = explode(':', $key)[0] ?? $key; Env::writeVariables([ @@ -267,6 +267,8 @@ protected function collectAblyConfig() 'ABLY_PUBLIC_KEY' => $publicKey, 'VITE_ABLY_PUBLIC_KEY' => '${ABLY_PUBLIC_KEY}', ], $this->laravel->basePath('.env')); + + $this->components->warn('Make sure to check off "Pusher protocol support" in your Ably app settings.'); } /** From 3329fe5e05bd4e7e05ab58f18ac1b8a0fa55f335 Mon Sep 17 00:00:00 2001 From: Joe Tannenbaum Date: Tue, 13 May 2025 09:24:42 -0400 Subject: [PATCH 30/33] move the ably warning up so that it's visible longer --- .../Foundation/Console/BroadcastingInstallCommand.php | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/Illuminate/Foundation/Console/BroadcastingInstallCommand.php b/src/Illuminate/Foundation/Console/BroadcastingInstallCommand.php index 05bc9d5cf20d..914c1a80eb15 100644 --- a/src/Illuminate/Foundation/Console/BroadcastingInstallCommand.php +++ b/src/Illuminate/Foundation/Console/BroadcastingInstallCommand.php @@ -258,6 +258,8 @@ protected function collectPusherConfig() */ protected function collectAblyConfig() { + $this->components->warn('Make sure to check off "Pusher protocol support" in your Ably app settings.'); + $key = password('Ably Key', 'Enter your Ably key'); $publicKey = explode(':', $key)[0] ?? $key; @@ -267,8 +269,6 @@ protected function collectAblyConfig() 'ABLY_PUBLIC_KEY' => $publicKey, 'VITE_ABLY_PUBLIC_KEY' => '${ABLY_PUBLIC_KEY}', ], $this->laravel->basePath('.env')); - - $this->components->warn('Make sure to check off "Pusher protocol support" in your Ably app settings.'); } /** From e52205e69d116cf68c7958fe0d73cda37e532223 Mon Sep 17 00:00:00 2001 From: Taylor Otwell Date: Tue, 13 May 2025 12:27:17 -0500 Subject: [PATCH 31/33] enable --- .../Foundation/Console/BroadcastingInstallCommand.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Illuminate/Foundation/Console/BroadcastingInstallCommand.php b/src/Illuminate/Foundation/Console/BroadcastingInstallCommand.php index 914c1a80eb15..811eee6421a7 100644 --- a/src/Illuminate/Foundation/Console/BroadcastingInstallCommand.php +++ b/src/Illuminate/Foundation/Console/BroadcastingInstallCommand.php @@ -258,7 +258,7 @@ protected function collectPusherConfig() */ protected function collectAblyConfig() { - $this->components->warn('Make sure to check off "Pusher protocol support" in your Ably app settings.'); + $this->components->warn('Make sure to enable "Pusher protocol support" in your Ably app settings.'); $key = password('Ably Key', 'Enter your Ably key'); From 61c25cfe22ef61554aeade6f9b9d7ec61d4276b9 Mon Sep 17 00:00:00 2001 From: Joe Tannenbaum Date: Tue, 13 May 2025 13:32:42 -0400 Subject: [PATCH 32/33] driver specific stubs --- .../Console/BroadcastingInstallCommand.php | 8 +++++++- .../Foundation/Console/stubs/echo-js-ably.stub | 13 +++++++++++++ .../Foundation/Console/stubs/echo-js-pusher.stub | 15 +++++++++++++++ .../stubs/{echo-js.stub => echo-js-reverb.stub} | 0 4 files changed, 35 insertions(+), 1 deletion(-) create mode 100644 src/Illuminate/Foundation/Console/stubs/echo-js-ably.stub create mode 100644 src/Illuminate/Foundation/Console/stubs/echo-js-pusher.stub rename src/Illuminate/Foundation/Console/stubs/{echo-js.stub => echo-js-reverb.stub} (100%) diff --git a/src/Illuminate/Foundation/Console/BroadcastingInstallCommand.php b/src/Illuminate/Foundation/Console/BroadcastingInstallCommand.php index 914c1a80eb15..0cfc9e077e7e 100644 --- a/src/Illuminate/Foundation/Console/BroadcastingInstallCommand.php +++ b/src/Illuminate/Foundation/Console/BroadcastingInstallCommand.php @@ -95,7 +95,13 @@ public function handle() mkdir($directory, 0755, true); } - copy(__DIR__.'/stubs/echo-js.stub', $echoScriptPath); + $stubPath = __DIR__.'/stubs/echo-js-'.$this->driver.'.stub'; + + if (! file_exists($stubPath)) { + $stubPath = __DIR__.'/stubs/echo-js-reverb.stub'; + } + + copy($stubPath, $echoScriptPath); } // Only add the bootstrap import for the standard JS implementation... diff --git a/src/Illuminate/Foundation/Console/stubs/echo-js-ably.stub b/src/Illuminate/Foundation/Console/stubs/echo-js-ably.stub new file mode 100644 index 000000000000..ec518d214668 --- /dev/null +++ b/src/Illuminate/Foundation/Console/stubs/echo-js-ably.stub @@ -0,0 +1,13 @@ +import Echo from 'laravel-echo'; + +import Pusher from 'pusher-js'; +window.Pusher = Pusher; + +window.Echo = new Echo({ + broadcaster: "pusher", + key: import.meta.env.VITE_ABLY_PUBLIC_KEY, + wsHost: "realtime-pusher.ably.io", + wsPort: 443, + disableStats: true, + encrypted: true, +}); diff --git a/src/Illuminate/Foundation/Console/stubs/echo-js-pusher.stub b/src/Illuminate/Foundation/Console/stubs/echo-js-pusher.stub new file mode 100644 index 000000000000..5a8a7f7e31ef --- /dev/null +++ b/src/Illuminate/Foundation/Console/stubs/echo-js-pusher.stub @@ -0,0 +1,15 @@ +import Echo from 'laravel-echo'; + +import Pusher from 'pusher-js'; +window.Pusher = Pusher; + +window.Echo = new Echo({ + broadcaster: "pusher", + key: import.meta.env.VITE_PUSHER_APP_KEY, + cluster: import.meta.env.VITE_PUSHER_APP_CLUSTER, + forceTLS: true, + wsHost: import.meta.env.VITE_PUSHER_HOST, + wsPort: import.meta.env.VITE_PUSHER_PORT, + wssPort: import.meta.env.VITE_PUSHER_PORT, + enabledTransports: ["ws", "wss"], +}); diff --git a/src/Illuminate/Foundation/Console/stubs/echo-js.stub b/src/Illuminate/Foundation/Console/stubs/echo-js-reverb.stub similarity index 100% rename from src/Illuminate/Foundation/Console/stubs/echo-js.stub rename to src/Illuminate/Foundation/Console/stubs/echo-js-reverb.stub From f89336ced5c611a99485b59c0db951c0b182d142 Mon Sep 17 00:00:00 2001 From: Joe Tannenbaum Date: Tue, 13 May 2025 13:45:13 -0400 Subject: [PATCH 33/33] hopefully fix line endings --- tests/Support/SupportHelpersTest.php | 264 +++++++++++++-------------- 1 file changed, 132 insertions(+), 132 deletions(-) diff --git a/tests/Support/SupportHelpersTest.php b/tests/Support/SupportHelpersTest.php index e9a52219c68a..1074e97dd6d9 100644 --- a/tests/Support/SupportHelpersTest.php +++ b/tests/Support/SupportHelpersTest.php @@ -1230,16 +1230,16 @@ public function testWriteArrayOfEnvVariablesToFile() { $filesystem = new Filesystem; $path = __DIR__.'/tmp/env-test-file'; - $filesystem->put($path, <<<'ENV' - APP_NAME=Laravel - APP_ENV=local - APP_KEY=base64:randomkey - APP_DEBUG=true - APP_URL=http://localhost - - DB_CONNECTION=mysql - DB_HOST= - ENV); + $filesystem->put($path, implode(PHP_EOL, [ + 'APP_NAME=Laravel', + 'APP_ENV=local', + 'APP_KEY=base64:randomkey', + 'APP_DEBUG=true', + 'APP_URL=http://localhost', + '', + 'DB_CONNECTION=mysql', + 'DB_HOST=', + ])); Env::writeVariables([ 'APP_VIBE' => 'chill', @@ -1249,20 +1249,20 @@ public function testWriteArrayOfEnvVariablesToFile() ], $path); $this->assertSame( - <<<'ENV' - APP_NAME=Laravel - APP_ENV=local - APP_KEY=base64:randomkey - APP_DEBUG=true - APP_URL=http://localhost - APP_VIBE=chill - - DB_CONNECTION=mysql - DB_HOST="127:0:0:1" - DB_PORT=3306 - - BRAND_NEW_PREFIX="fresh value" - ENV, + implode(PHP_EOL, [ + 'APP_NAME=Laravel', + 'APP_ENV=local', + 'APP_KEY=base64:randomkey', + 'APP_DEBUG=true', + 'APP_URL=http://localhost', + 'APP_VIBE=chill', + '', + 'DB_CONNECTION=mysql', + 'DB_HOST="127:0:0:1"', + 'DB_PORT=3306', + '', + 'BRAND_NEW_PREFIX="fresh value"', + ]), $filesystem->get($path) ); } @@ -1271,16 +1271,16 @@ public function testWriteArrayOfEnvVariablesToFileAndOverwrite() { $filesystem = new Filesystem; $path = __DIR__.'/tmp/env-test-file'; - $filesystem->put($path, <<<'ENV' - APP_NAME=Laravel - APP_ENV=local - APP_KEY=base64:randomkey - APP_DEBUG=true - APP_URL=http://localhost - - DB_CONNECTION=mysql - DB_HOST= - ENV); + $filesystem->put($path, implode(PHP_EOL, [ + 'APP_NAME=Laravel', + 'APP_ENV=local', + 'APP_KEY=base64:randomkey', + 'APP_DEBUG=true', + 'APP_URL=http://localhost', + '', + 'DB_CONNECTION=mysql', + 'DB_HOST=', + ])); Env::writeVariables([ 'APP_VIBE' => 'chill', @@ -1289,17 +1289,17 @@ public function testWriteArrayOfEnvVariablesToFileAndOverwrite() ], $path, true); $this->assertSame( - <<<'ENV' - APP_NAME=Laravel - APP_ENV=local - APP_KEY=base64:randomkey - APP_DEBUG=true - APP_URL=http://localhost - APP_VIBE=chill - - DB_CONNECTION=sqlite - DB_HOST="127:0:0:1" - ENV, + implode(PHP_EOL, [ + 'APP_NAME=Laravel', + 'APP_ENV=local', + 'APP_KEY=base64:randomkey', + 'APP_DEBUG=true', + 'APP_URL=http://localhost', + 'APP_VIBE=chill', + '', + 'DB_CONNECTION=sqlite', + 'DB_HOST="127:0:0:1"', + ]), $filesystem->get($path) ); } @@ -1308,17 +1308,17 @@ public function testWillNotOverwriteArrayOfVariables() { $filesystem = new Filesystem; $path = __DIR__.'/tmp/env-test-file'; - $filesystem->put($path, <<<'ENV' - APP_NAME=Laravel - APP_ENV=local - APP_KEY=base64:randomkey - APP_DEBUG=true - APP_URL=http://localhost - APP_VIBE=odd - - DB_CONNECTION=mysql - DB_HOST= - ENV); + $filesystem->put($path, implode(PHP_EOL, [ + 'APP_NAME=Laravel', + 'APP_ENV=local', + 'APP_KEY=base64:randomkey', + 'APP_DEBUG=true', + 'APP_URL=http://localhost', + 'APP_VIBE=odd', + '', + 'DB_CONNECTION=mysql', + 'DB_HOST=', + ])); Env::writeVariables([ 'APP_VIBE' => 'chill', @@ -1326,17 +1326,17 @@ public function testWillNotOverwriteArrayOfVariables() ], $path); $this->assertSame( - <<<'ENV' - APP_NAME=Laravel - APP_ENV=local - APP_KEY=base64:randomkey - APP_DEBUG=true - APP_URL=http://localhost - APP_VIBE=odd - - DB_CONNECTION=mysql - DB_HOST="127:0:0:1" - ENV, + implode(PHP_EOL, [ + 'APP_NAME=Laravel', + 'APP_ENV=local', + 'APP_KEY=base64:randomkey', + 'APP_DEBUG=true', + 'APP_URL=http://localhost', + 'APP_VIBE=odd', + '', + 'DB_CONNECTION=mysql', + 'DB_HOST="127:0:0:1"', + ]), $filesystem->get($path) ); } @@ -1345,31 +1345,31 @@ public function testWriteVariableToFile() { $filesystem = new Filesystem; $path = __DIR__.'/tmp/env-test-file'; - $filesystem->put($path, <<<'ENV' - APP_NAME=Laravel - APP_ENV=local - APP_KEY=base64:randomkey - APP_DEBUG=true - APP_URL=http://localhost - - DB_CONNECTION=mysql - DB_HOST= - ENV); + $filesystem->put($path, implode(PHP_EOL, [ + 'APP_NAME=Laravel', + 'APP_ENV=local', + 'APP_KEY=base64:randomkey', + 'APP_DEBUG=true', + 'APP_URL=http://localhost', + '', + 'DB_CONNECTION=mysql', + 'DB_HOST=', + ])); Env::writeVariable('APP_VIBE', 'chill', $path); $this->assertSame( - <<<'ENV' - APP_NAME=Laravel - APP_ENV=local - APP_KEY=base64:randomkey - APP_DEBUG=true - APP_URL=http://localhost - APP_VIBE=chill - - DB_CONNECTION=mysql - DB_HOST= - ENV, + implode(PHP_EOL, [ + 'APP_NAME=Laravel', + 'APP_ENV=local', + 'APP_KEY=base64:randomkey', + 'APP_DEBUG=true', + 'APP_URL=http://localhost', + 'APP_VIBE=chill', + '', + 'DB_CONNECTION=mysql', + 'DB_HOST=', + ]), $filesystem->get($path) ); } @@ -1378,32 +1378,32 @@ public function testWillNotOverwriteVariable() { $filesystem = new Filesystem; $path = __DIR__.'/tmp/env-test-file'; - $filesystem->put($path, <<<'ENV' - APP_NAME=Laravel - APP_ENV=local - APP_KEY=base64:randomkey - APP_DEBUG=true - APP_URL=http://localhost - APP_VIBE=odd - - DB_CONNECTION=mysql - DB_HOST= - ENV); + $filesystem->put($path, implode(PHP_EOL, [ + 'APP_NAME=Laravel', + 'APP_ENV=local', + 'APP_KEY=base64:randomkey', + 'APP_DEBUG=true', + 'APP_URL=http://localhost', + 'APP_VIBE=odd', + '', + 'DB_CONNECTION=mysql', + 'DB_HOST=', + ])); Env::writeVariable('APP_VIBE', 'chill', $path); $this->assertSame( - <<<'ENV' - APP_NAME=Laravel - APP_ENV=local - APP_KEY=base64:randomkey - APP_DEBUG=true - APP_URL=http://localhost - APP_VIBE=odd - - DB_CONNECTION=mysql - DB_HOST= - ENV, + implode(PHP_EOL, [ + 'APP_NAME=Laravel', + 'APP_ENV=local', + 'APP_KEY=base64:randomkey', + 'APP_DEBUG=true', + 'APP_URL=http://localhost', + 'APP_VIBE=odd', + '', + 'DB_CONNECTION=mysql', + 'DB_HOST=', + ]), $filesystem->get($path) ); } @@ -1412,32 +1412,32 @@ public function testWriteVariableToFileAndOverwrite() { $filesystem = new Filesystem; $path = __DIR__.'/tmp/env-test-file'; - $filesystem->put($path, <<<'ENV' - APP_NAME=Laravel - APP_ENV=local - APP_KEY=base64:randomkey - APP_DEBUG=true - APP_URL=http://localhost - APP_VIBE=odd - - DB_CONNECTION=mysql - DB_HOST= - ENV); + $filesystem->put($path, implode(PHP_EOL, [ + 'APP_NAME=Laravel', + 'APP_ENV=local', + 'APP_KEY=base64:randomkey', + 'APP_DEBUG=true', + 'APP_URL=http://localhost', + 'APP_VIBE=odd', + '', + 'DB_CONNECTION=mysql', + 'DB_HOST=', + ])); Env::writeVariable('APP_VIBE', 'chill', $path, true); $this->assertSame( - <<<'ENV' - APP_NAME=Laravel - APP_ENV=local - APP_KEY=base64:randomkey - APP_DEBUG=true - APP_URL=http://localhost - APP_VIBE=chill - - DB_CONNECTION=mysql - DB_HOST= - ENV, + implode(PHP_EOL, [ + 'APP_NAME=Laravel', + 'APP_ENV=local', + 'APP_KEY=base64:randomkey', + 'APP_DEBUG=true', + 'APP_URL=http://localhost', + 'APP_VIBE=chill', + '', + 'DB_CONNECTION=mysql', + 'DB_HOST=', + ]), $filesystem->get($path) ); }