Skip to content

Easily implement broadcasting in a React/Vue Typescript app (Starter Kits) #55170

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 35 commits into from
May 13, 2025
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
35 commits
Select commit Hold shift + click to select a range
4ffc083
Adding initial trial for the framework specific echo lib
tnylea Mar 21, 2025
f1da2c5
few more updates
tnylea Mar 21, 2025
b828985
Adding functionality for vue composable
tnylea Mar 25, 2025
f9ac68a
updating the react hook with updated config options
tnylea Mar 25, 2025
1b0fe3f
Adding the configure code injection step
tnylea Mar 26, 2025
14f4983
Getting styleCI to pass
tnylea Mar 26, 2025
582b10b
removing the useEcho stubs, instead will be added to laravel-echo npm…
tnylea Apr 4, 2025
fdd1a26
fix spacing
tnylea Apr 4, 2025
44da9f8
fix spacing
tnylea Apr 4, 2025
b96f130
fix spacing
tnylea Apr 4, 2025
c1eac4d
making methods more efficient
tnylea Apr 4, 2025
ca7b4ac
making methods more efficient
tnylea Apr 4, 2025
08c1d83
updates to utilize the new packages
joetannenbaum May 7, 2025
dcc38b2
Update BroadcastingInstallCommand.php
joetannenbaum May 8, 2025
ff2ba56
better value detection for .env
joetannenbaum May 8, 2025
da5ef4a
Update BroadcastingInstallCommand.php
joetannenbaum May 8, 2025
5694157
Update BroadcastingInstallCommand.php
joetannenbaum May 8, 2025
6803598
Update BroadcastingInstallCommand.php
joetannenbaum May 8, 2025
f1a3333
Update BroadcastingInstallCommand.php
joetannenbaum May 8, 2025
01129ac
Update BroadcastingInstallCommand.php
joetannenbaum May 8, 2025
afe3a4a
formatting
taylorotwell May 10, 2025
eed2e37
Update BroadcastingInstallCommand.php
joetannenbaum May 10, 2025
d35e413
formatting
taylorotwell May 12, 2025
eeb4199
writeVariable(s) env helpers
joetannenbaum May 12, 2025
0533a2d
Merge branch 'broadcastStarterKits' of github.com:tnylea/laravel-fram…
joetannenbaum May 12, 2025
0aab9b5
handle blank values cleanly
joetannenbaum May 12, 2025
4c20ed0
use the env variable writer
joetannenbaum May 12, 2025
9e26989
unhandle match case
joetannenbaum May 12, 2025
8d51a50
no need to ask for public key
joetannenbaum May 12, 2025
e2725cd
warn about pusher protocol support
joetannenbaum May 13, 2025
3329fe5
move the ably warning up so that it's visible longer
joetannenbaum May 13, 2025
e52205e
enable
taylorotwell May 13, 2025
61c25cf
driver specific stubs
joetannenbaum May 13, 2025
4880d30
Merge branch 'broadcastStarterKits' of github.com:tnylea/laravel-fram…
joetannenbaum May 13, 2025
f89336c
hopefully fix line endings
joetannenbaum May 13, 2025
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
327 changes: 309 additions & 18 deletions src/Illuminate/Foundation/Console/BroadcastingInstallCommand.php
Original file line number Diff line number Diff line change
Expand Up @@ -5,12 +5,16 @@
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;

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
Expand All @@ -26,6 +30,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}';

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

/**
* The broadcasting driver to use.
*
* @var string|null
*/
protected $driver = null;

/**
* The framework packages to install.
*
* @var array
*/
protected $frameworkPackages = [
'react' => '@laravel/echo-react',
'vue' => '@laravel/echo-vue',
];

/**
* Execute the console command.
*
Expand All @@ -54,25 +78,44 @@ public function handle()
$this->uncommentChannelsRoutesFile();
$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);
}
$this->driver = $this->resolveDriver();

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

if (file_exists($bootstrapScriptPath = $this->laravel->resourcePath('js/bootstrap.js'))) {
$bootstrapScript = file_get_contents(
$bootstrapScriptPath
);
$this->collectDriverConfig();
$this->installDriverPackages();

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'))) {
if (! is_dir($directory = $this->laravel->resourcePath('js'))) {
mkdir($directory, 0755, true);
}

$stubPath = __DIR__.'/stubs/echo-js-'.$this->driver.'.stub';

if (! file_exists($stubPath)) {
$stubPath = __DIR__.'/stubs/echo-js-reverb.stub';
}

copy($stubPath, $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,
);
}
}
}

Expand Down Expand Up @@ -118,8 +161,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;
}

Expand All @@ -134,14 +179,179 @@ protected function enableBroadcastServiceProvider()
}
}

/**
* Collect the driver configuration.
*
* @return void
*/
protected function collectDriverConfig()
{
$envPath = $this->laravel->basePath('.env');

if (! file_exists($envPath)) {
return;
}

match ($this->driver) {
'pusher' => $this->collectPusherConfig(),
'ably' => $this->collectAblyConfig(),
default => null,
};
}

/**
* 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.
*
* @return void
*/
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',
'us2',
'us3',
'eu',
'ap1',
'ap2',
'ap3',
'ap4',
'sa1',
]);

Env::writeVariables([
'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}',
], $this->laravel->basePath('.env'));
}

/**
* Collect the Ably configuration.
*
* @return void
*/
protected function collectAblyConfig()
{
$this->components->warn('Make sure to enable "Pusher protocol support" in your Ably app settings.');

$key = password('Ably Key', 'Enter your Ably key');

$publicKey = explode(':', $key)[0] ?? $key;

Env::writeVariables([
'ABLY_KEY' => $key,
'ABLY_PUBLIC_KEY' => $publicKey,
'VITE_ABLY_PUBLIC_KEY' => '${ABLY_PUBLIC_KEY}',
], $this->laravel->basePath('.env'));
}

/**
* Inject Echo configuration into the application's main file.
*
* @return void
*/
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'),
];
}

$filePath = array_filter($filePaths, function ($path) {
return file_exists($path);
})[0] ?? null;

if (! $filePath) {
$this->components->warn("Could not find file [{$filePaths[0]}]. Skipping automatic Echo configuration.");

return;
}

$contents = file_get_contents($filePath);

$echoCode = <<<JS
import { configureEcho } from '{$importPath}';

configureEcho({
broadcaster: '{$this->driver}',
});
JS;

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...
$newContents = $echoCode.PHP_EOL.$contents;

file_put_contents($filePath, $newContents);
} else {
// Add Echo configuration after the last import...
$lastImport = end($matches[0]);

$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).'].');
}

/**
* Install Laravel Reverb into the application if desired.
*
* @return void
*/
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;
}

Expand Down Expand Up @@ -199,6 +409,12 @@ protected function installNodeDependencies()
];
}

if ($this->appUsesVue()) {
$commands[0] .= ' '.$this->frameworkPackages['vue'];
} elseif ($this->appUsesReact()) {
$commands[0] .= ' '.$this->frameworkPackages['react'];
}

$command = Process::command(implode(' && ', $commands))
->path(base_path());

Expand All @@ -212,4 +428,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]);
}
}
Loading