├── .codecov.yml ├── .github └── workflows │ └── ci.yml ├── .styleci.yml ├── CONTRIBUTING.md ├── LICENSE ├── README.md ├── composer.json ├── config └── websockets.php ├── database └── migrations │ ├── 0000_00_00_000000_create_websockets_apps_table.php │ ├── 0000_00_00_000000_create_websockets_statistics_entries_table.php │ └── sqlite │ └── 0000_00_00_000000_create_apps_table.sql ├── docs ├── _index.md ├── advanced-usage │ ├── _index.md │ ├── app-providers.md │ ├── custom-websocket-handlers.md │ ├── dispatched-events.md │ ├── non-blocking-queue-driver.md │ └── webhooks.md ├── basic-usage │ ├── _index.md │ ├── pusher.md │ ├── restarting.md │ ├── sail.md │ ├── ssl.md │ └── starting.md ├── debugging │ ├── _index.md │ ├── console.md │ └── dashboard.md ├── faq │ ├── _index.md │ ├── cloudflare.md │ ├── deploying.md │ └── scaling.md ├── getting-started │ ├── _index.md │ ├── installation.md │ ├── introduction.md │ └── questions-issues.md └── horizontal-scaling │ ├── _index.md │ ├── getting-started.md │ └── redis.md ├── phpunit.xml ├── resources └── views │ ├── apps.blade.php │ ├── dashboard.blade.php │ └── layout.blade.php └── src ├── API ├── Controller.php ├── FetchChannel.php ├── FetchChannels.php ├── FetchUsers.php └── TriggerEvent.php ├── Apps ├── App.php ├── ConfigAppManager.php ├── MysqlAppManager.php └── SQLiteAppManager.php ├── Cache ├── ArrayLock.php ├── Lock.php └── RedisLock.php ├── ChannelManagers ├── LocalChannelManager.php └── RedisChannelManager.php ├── Channels ├── Channel.php ├── PresenceChannel.php └── PrivateChannel.php ├── Concerns └── PushesToPusher.php ├── Console └── Commands │ ├── CleanStatistics.php │ ├── FlushCollectedStatistics.php │ ├── RestartServer.php │ └── StartServer.php ├── Contracts ├── AppManager.php ├── ChannelManager.php ├── PusherMessage.php ├── StatisticsCollector.php └── StatisticsStore.php ├── Dashboard └── Http │ ├── Controllers │ ├── AuthenticateDashboard.php │ ├── SendMessage.php │ ├── ShowApps.php │ ├── ShowDashboard.php │ ├── ShowStatistics.php │ └── StoreApp.php │ ├── Middleware │ └── Authorize.php │ └── Requests │ └── StoreAppRequest.php ├── DashboardLogger.php ├── Events ├── ConnectionClosed.php ├── ConnectionPonged.php ├── NewConnection.php ├── SubscribedToChannel.php ├── UnsubscribedFromChannel.php └── WebSocketMessageReceived.php ├── Facades ├── StatisticsCollector.php ├── StatisticsStore.php └── WebSocketRouter.php ├── Helpers.php ├── Models └── WebSocketsStatisticsEntry.php ├── Queue ├── AsyncRedisConnector.php └── AsyncRedisQueue.php ├── Rules └── AppId.php ├── Server ├── Exceptions │ ├── ConnectionsOverCapacity.php │ ├── InvalidSignature.php │ ├── OriginNotAllowed.php │ ├── UnknownAppKey.php │ └── WebSocketException.php ├── HealthHandler.php ├── HttpServer.php ├── Loggers │ ├── ConnectionLogger.php │ ├── HttpLogger.php │ ├── Logger.php │ └── WebSocketsLogger.php ├── Messages │ ├── PusherChannelProtocolMessage.php │ ├── PusherClientMessage.php │ └── PusherMessageFactory.php ├── MockableConnection.php ├── QueryParameters.php ├── Router.php └── WebSocketHandler.php ├── ServerFactory.php ├── Statistics ├── Collectors │ ├── MemoryCollector.php │ └── RedisCollector.php ├── Statistic.php └── Stores │ └── DatabaseStore.php └── WebSocketsServiceProvider.php /.codecov.yml: -------------------------------------------------------------------------------- 1 | codecov: 2 | notify: 3 | require_ci_to_pass: yes 4 | 5 | coverage: 6 | precision: 2 7 | round: down 8 | range: "70...100" 9 | 10 | status: 11 | project: yes 12 | patch: yes 13 | changes: no 14 | 15 | comment: 16 | layout: "reach, diff, flags, files, footer" 17 | behavior: default 18 | require_changes: no 19 | -------------------------------------------------------------------------------- /.github/workflows/ci.yml: -------------------------------------------------------------------------------- 1 | name: CI 2 | 3 | on: 4 | push: 5 | branches: 6 | - '*' 7 | tags: 8 | - '*' 9 | pull_request: 10 | branches: 11 | - '*' 12 | 13 | jobs: 14 | build: 15 | if: "!contains(github.event.head_commit.message, 'skip ci')" 16 | 17 | runs-on: ubuntu-latest 18 | 19 | strategy: 20 | fail-fast: false 21 | matrix: 22 | php: 23 | - '8.0' 24 | - '8.1' 25 | laravel: 26 | - 9.* 27 | prefer: 28 | - 'prefer-lowest' 29 | - 'prefer-stable' 30 | include: 31 | - laravel: '9.*' 32 | testbench: '7.*' 33 | 34 | name: PHP ${{ matrix.php }} - Laravel ${{ matrix.laravel }} --${{ matrix.prefer }} 35 | 36 | steps: 37 | - uses: actions/checkout@v1 38 | 39 | - name: Setup PHP 40 | uses: shivammathur/setup-php@v2 41 | with: 42 | php-version: ${{ matrix.php }} 43 | extensions: dom, curl, libxml, mbstring, zip, pcntl, pdo, sqlite, pdo_sqlite, bcmath, soap, intl, gd, exif, iconv 44 | coverage: pcov 45 | 46 | - name: Setup MySQL 47 | uses: haltuf/mysql-action@master 48 | with: 49 | mysql version: '8.0' 50 | mysql database: 'websockets_test' 51 | mysql root password: 'password' 52 | 53 | - name: Setup Redis 54 | uses: supercharge/redis-github-action@1.1.0 55 | with: 56 | redis-version: 6 57 | 58 | - uses: actions/cache@v1 59 | name: Cache dependencies 60 | with: 61 | path: ~/.composer/cache/files 62 | key: composer-php-${{ matrix.php }}-${{ matrix.laravel }}-${{ matrix.prefer }}-${{ hashFiles('composer.json') }} 63 | 64 | - name: Install dependencies 65 | run: | 66 | composer require "laravel/framework:${{ matrix.laravel }}" "orchestra/testbench-browser-kit:${{ matrix.testbench }}" "orchestra/database:${{ matrix.testbench }}" --no-interaction --no-update 67 | composer update --${{ matrix.prefer }} --prefer-dist --no-interaction --no-suggest 68 | 69 | - name: Run tests for Local 70 | run: | 71 | REPLICATION_MODE=local vendor/bin/phpunit --coverage-text --coverage-clover=coverage_local.xml 72 | 73 | - name: Run tests for Redis 74 | run: | 75 | REPLICATION_MODE=redis vendor/bin/phpunit --coverage-text --coverage-clover=coverage_redis.xml 76 | 77 | - uses: codecov/codecov-action@v1 78 | with: 79 | fail_ci_if_error: false 80 | file: '*.xml' 81 | token: ${{ secrets.CODECOV_TOKEN }} 82 | -------------------------------------------------------------------------------- /.styleci.yml: -------------------------------------------------------------------------------- 1 | preset: laravel -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | # Contributing 2 | 3 | Contributions are **welcome** and will be fully **credited**. 4 | 5 | Please read and understand the contribution guide before creating an issue or pull request. 6 | 7 | ## Etiquette 8 | 9 | This project is open source, and as such, the maintainers give their free time to build and maintain the source code 10 | held within. They make the code freely available in the hope that it will be of use to other developers. It would be 11 | extremely unfair for them to suffer abuse or anger for their hard work. 12 | 13 | Please be considerate towards maintainers when raising issues or presenting pull requests. Let's show the 14 | world that developers are civilized and selfless people. 15 | 16 | It's the duty of the maintainer to ensure that all submissions to the project are of sufficient 17 | quality to benefit the project. Many developers have different skillsets, strengths, and weaknesses. Respect the maintainer's decision, and do not be upset or abusive if your submission is not used. 18 | 19 | ## Viability 20 | 21 | When requesting or submitting new features, first consider whether it might be useful to others. Open 22 | source projects are used by many developers, who may have entirely different needs to your own. Think about 23 | whether or not your feature is likely to be used by other users of the project. 24 | 25 | ## Procedure 26 | 27 | Before filing an issue: 28 | 29 | - Attempt to replicate the problem, to ensure that it wasn't a coincidental incident. 30 | - Check to make sure your feature suggestion isn't already present within the project. 31 | - Check the pull requests tab to ensure that the bug doesn't have a fix in progress. 32 | - Check the pull requests tab to ensure that the feature isn't already in progress. 33 | 34 | Before submitting a pull request: 35 | 36 | - Check the codebase to ensure that your feature doesn't already exist. 37 | - Check the pull requests to ensure that another person hasn't already submitted the feature or fix. 38 | 39 | ## Requirements 40 | 41 | If the project maintainer has any additional requirements, you will find them listed here. 42 | 43 | - **[PSR-2 Coding Standard](https://github.com/php-fig/fig-standards/blob/master/accepted/PSR-2-coding-style-guide.md)** - The easiest way to apply the conventions is to install [PHP Code Sniffer](https://pear.php.net/package/PHP_CodeSniffer). 44 | 45 | - **Add tests!** - Your patch won't be accepted if it doesn't have tests. 46 | 47 | - **Document any change in behaviour** - Make sure the `README.md` and any other relevant documentation are kept up-to-date. 48 | 49 | - **Consider our release cycle** - We try to follow [SemVer v2.0.0](https://semver.org/). Randomly breaking public APIs is not an option. 50 | 51 | - **One pull request per feature** - If you want to do more than one thing, send multiple pull requests. 52 | 53 | - **Send coherent history** - Make sure each individual commit in your pull request is meaningful. If you had to make multiple intermediate commits while developing, please [squash them](https://www.git-scm.com/book/en/v2/Git-Tools-Rewriting-History#Changing-Multiple-Commit-Messages) before submitting. 54 | 55 | **Happy coding**! 56 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) Beyond Code GmbH 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in 13 | all copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 21 | THE SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Laravel WebSockets 🛰 2 | 3 | > [!NOTE] 4 | > Laravel WebSockets is no longer maintained. If you are looking for a PHP-based WebSocket solution, check out [Laravel Reverb](https://reverb.laravel.com) which is also built on top of ReactPHP and allows you to horizontally scale the WebSocket server. 5 | 6 | [![Latest Version on Packagist](https://img.shields.io/packagist/v/beyondcode/laravel-websockets.svg?style=flat-square)](https://packagist.org/packages/beyondcode/laravel-websockets) 7 | ![GitHub Workflow Status](https://img.shields.io/github/workflow/status/beyondcode/laravel-websockets/run-tests?label=tests) 8 | [![Quality Score](https://img.shields.io/scrutinizer/g/beyondcode/laravel-websockets.svg?style=flat-square)](https://scrutinizer-ci.com/g/beyondcode/laravel-websockets) 9 | [![Total Downloads](https://img.shields.io/packagist/dt/beyondcode/laravel-websockets.svg?style=flat-square)](https://packagist.org/packages/beyondcode/laravel-websockets) 10 | 11 | Bring the power of WebSockets to your Laravel application. Drop-in Pusher replacement, SSL support, Laravel Echo support and a debug dashboard are just some of its features. 12 | 13 | [![https://tinkerwell.app/?ref=github](https://tinkerwell.app/images/card-v3.png)](https://tinkerwell.app/?ref=github) 14 | 15 | ## Documentation 16 | 17 | For installation instructions, in-depth usage and deployment details, please take a look at the [official documentation](https://beyondco.de/docs/laravel-websockets/getting-started/introduction/). 18 | 19 | ### Changelog 20 | 21 | Please see [CHANGELOG](CHANGELOG.md) for more information on what has changed recently. 22 | 23 | ## Contributing 24 | 25 | Please see [CONTRIBUTING](CONTRIBUTING.md) for details. 26 | 27 | ### Security 28 | 29 | If you discover any security related issues, please email marcel@beyondco.de instead of using the issue tracker. 30 | 31 | ## Credits 32 | 33 | - [Marcel Pociot](https://github.com/mpociot) 34 | - [Freek Van der Herten](https://github.com/freekmurze) 35 | - [All Contributors](../../contributors) 36 | 37 | ## License 38 | 39 | The MIT License (MIT). Please see [License File](LICENSE.md) for more information. 40 | -------------------------------------------------------------------------------- /composer.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "beyondcode/laravel-websockets", 3 | "description": "An easy to launch a Pusher-compatible WebSockets server for Laravel.", 4 | "keywords": [ 5 | "beyondcode", 6 | "laravel-websockets", 7 | "laravel", 8 | "php" 9 | ], 10 | "license": "MIT", 11 | "homepage": "https://github.com/beyondcode/laravel-websockets", 12 | "authors": [ 13 | { 14 | "name": "Marcel Pociot", 15 | "email": "marcel@beyondco.de", 16 | "homepage": "https://beyondcode.de", 17 | "role": "Developer" 18 | }, 19 | { 20 | "name": "Freek Van der Herten", 21 | "email": "freek@spatie.be", 22 | "homepage": "https://spatie.be", 23 | "role": "Developer" 24 | }, 25 | { 26 | "name": "Alex Renoki", 27 | "homepage": "https://github.com/rennokki", 28 | "role": "Developer" 29 | } 30 | ], 31 | "require": { 32 | "php": "^8.0|^8.1", 33 | "cboden/ratchet": "^0.4.4", 34 | "clue/block-react": "^1.5", 35 | "clue/reactphp-sqlite": "^1.0", 36 | "clue/redis-react": "^2.6", 37 | "doctrine/dbal": "^2.9", 38 | "evenement/evenement": "^2.0|^3.0", 39 | "facade/ignition-contracts": "^1.0", 40 | "guzzlehttp/psr7": "^1.5", 41 | "illuminate/broadcasting": "^9.0", 42 | "illuminate/console": "^9.0", 43 | "illuminate/http": "^9.0", 44 | "illuminate/queue": "^9.0", 45 | "illuminate/routing": "^9.0", 46 | "illuminate/support": "^9.0", 47 | "pusher/pusher-php-server": "^6.0|^7.0", 48 | "react/mysql": "^0.5", 49 | "react/promise": "^2.8", 50 | "symfony/http-kernel": "^5.0|^6.0", 51 | "symfony/psr-http-message-bridge": "^1.1|^2.0" 52 | }, 53 | "require-dev": { 54 | "clue/buzz-react": "^2.9", 55 | "laravel/legacy-factories": "^1.1", 56 | "orchestra/testbench-browser-kit": "^7.0", 57 | "phpunit/phpunit": "^9.0", 58 | "ratchet/pawl": "^0.3.5" 59 | }, 60 | "suggest": { 61 | "ext-pcntl": "Running the server needs pcntl to listen to command signals and soft-shutdown.", 62 | "doctrine/dbal": "Required to run database migrations (^2.9|^3.0)." 63 | }, 64 | "autoload": { 65 | "psr-4": { 66 | "BeyondCode\\LaravelWebSockets\\": "src/" 67 | } 68 | }, 69 | "autoload-dev": { 70 | "psr-4": { 71 | "BeyondCode\\LaravelWebSockets\\Test\\": "tests" 72 | } 73 | }, 74 | "scripts": { 75 | "test": "vendor/bin/phpunit" 76 | }, 77 | "config": { 78 | "sort-packages": true 79 | }, 80 | "minimum-stability": "dev", 81 | "prefer-stable": true, 82 | "extra": { 83 | "laravel": { 84 | "providers": [ 85 | "BeyondCode\\LaravelWebSockets\\WebSocketsServiceProvider" 86 | ], 87 | "aliases": { 88 | "WebSocketRouter": "BeyondCode\\LaravelWebSockets\\Facades\\WebSocketRouter" 89 | } 90 | } 91 | } 92 | } 93 | -------------------------------------------------------------------------------- /database/migrations/0000_00_00_000000_create_websockets_apps_table.php: -------------------------------------------------------------------------------- 1 | string('id')->index(); 18 | $table->string('key'); 19 | $table->string('secret'); 20 | $table->string('name'); 21 | $table->string('host')->nullable(); 22 | $table->string('path')->nullable(); 23 | $table->boolean('enable_client_messages')->default(false); 24 | $table->boolean('enable_statistics')->default(true); 25 | $table->unsignedInteger('capacity')->nullable(); 26 | $table->string('allowed_origins'); 27 | $table->nullableTimestamps(); 28 | }); 29 | } 30 | 31 | /** 32 | * Reverse the migrations. 33 | * 34 | * @return void 35 | */ 36 | public function down() 37 | { 38 | Schema::dropIfExists('websockets_apps'); 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /database/migrations/0000_00_00_000000_create_websockets_statistics_entries_table.php: -------------------------------------------------------------------------------- 1 | increments('id'); 18 | $table->string('app_id'); 19 | $table->integer('peak_connections_count'); 20 | $table->integer('websocket_messages_count'); 21 | $table->integer('api_messages_count'); 22 | $table->nullableTimestamps(); 23 | }); 24 | } 25 | 26 | /** 27 | * Reverse the migrations. 28 | * 29 | * @return void 30 | */ 31 | public function down() 32 | { 33 | Schema::dropIfExists('websockets_statistics_entries'); 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /database/migrations/sqlite/0000_00_00_000000_create_apps_table.sql: -------------------------------------------------------------------------------- 1 | CREATE TABLE IF NOT EXISTS apps ( 2 | id STRING NOT NULL, 3 | key STRING NOT NULL, 4 | secret STRING NOT NULL, 5 | name STRING NOT NULL, 6 | host STRING NULLABLE, 7 | path STRING NULLABLE, 8 | enable_client_messages BOOLEAN DEFAULT 0, 9 | enable_statistics BOOLEAN DEFAULT 1, 10 | capacity INTEGER NULLABLE, 11 | allowed_origins STRING NULLABLE 12 | ) 13 | -------------------------------------------------------------------------------- /docs/_index.md: -------------------------------------------------------------------------------- 1 | --- 2 | packageName: Laravel Websockets 3 | githubUrl: https://github.com/beyondcode/laravel-websockets 4 | --- 5 | -------------------------------------------------------------------------------- /docs/advanced-usage/_index.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: Advanced Usage 3 | order: 4 4 | --- 5 | -------------------------------------------------------------------------------- /docs/advanced-usage/app-providers.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: Custom App Managers 3 | order: 1 4 | --- 5 | 6 | # Custom App Managers 7 | 8 | With the multi-tenancy support of Laravel WebSockets, the default way of storing and retrieving the apps is by using the `websockets.php` config file. 9 | 10 | Depending on your setup, you might have your app configuration stored elsewhere and having to keep the configuration in sync with your app storage can be tedious. To simplify this, you can create your own `AppManager` class that will take care of retrieving the WebSocket credentials for a specific WebSocket application. 11 | 12 | > Make sure that you do **not** perform any IO blocking tasks in your `AppManager`, as they will interfere with the asynchronous WebSocket execution. 13 | 14 | In order to create your custom `AppManager`, create a class that implements the `BeyondCode\LaravelWebSockets\Contracts\AppManager` interface. 15 | 16 | This is what it looks like: 17 | 18 | ```php 19 | interface AppManager 20 | { 21 | /** @return array[BeyondCode\LaravelWebSockets\Apps\App] */ 22 | public function all(): array; 23 | 24 | /** @return BeyondCode\LaravelWebSockets\Apps\App */ 25 | public function findById($appId): ?App; 26 | 27 | /** @return BeyondCode\LaravelWebSockets\Apps\App */ 28 | public function findByKey($appKey): ?App; 29 | 30 | /** @return BeyondCode\LaravelWebSockets\Apps\App */ 31 | public function findBySecret($appSecret): ?App; 32 | } 33 | ``` 34 | 35 | The following is an example AppManager that utilizes an Eloquent model: 36 | ```php 37 | namespace App\Managers; 38 | 39 | use App\Application; 40 | use BeyondCode\LaravelWebSockets\Apps\App; 41 | use BeyondCode\LaravelWebSockets\Contracts\AppManager; 42 | 43 | class MyCustomAppManager implements AppManager 44 | { 45 | public function all() : array 46 | { 47 | return Application::all() 48 | ->map(function($app) { 49 | return $this->normalize($app->toArray()); 50 | }) 51 | ->toArray(); 52 | } 53 | 54 | public function findById($appId) : ?App 55 | { 56 | return $this->normalize(Application::findById($appId)->toArray()); 57 | } 58 | 59 | public function findByKey($appKey) : ?App 60 | { 61 | return $this->normalize(Application::findByKey($appKey)->toArray()); 62 | } 63 | 64 | public function findBySecret($appSecret) : ?App 65 | { 66 | return $this->normalize(Application::findBySecret($appSecret)->toArray()); 67 | } 68 | 69 | protected function normalize(?array $appAttributes) : ?App 70 | { 71 | if (! $appAttributes) { 72 | return null; 73 | } 74 | 75 | $app = new App( 76 | $appAttributes['id'], 77 | $appAttributes['key'], 78 | $appAttributes['secret'] 79 | ); 80 | 81 | if (isset($appAttributes['name'])) { 82 | $app->setName($appAttributes['name']); 83 | } 84 | 85 | if (isset($appAttributes['host'])) { 86 | $app->setHost($appAttributes['host']); 87 | } 88 | 89 | $app 90 | ->enableClientMessages($appAttributes['enable_client_messages']) 91 | ->enableStatistics($appAttributes['enable_statistics']); 92 | 93 | return $app; 94 | } 95 | } 96 | ``` 97 | 98 | Once you have implemented your own AppManager, you need to set it in the `websockets.php` configuration file: 99 | 100 | ```php 101 | 'managers' => [ 102 | 103 | /* 104 | |-------------------------------------------------------------------------- 105 | | Application Manager 106 | |-------------------------------------------------------------------------- 107 | | 108 | | An Application manager determines how your websocket server allows 109 | | the use of the TCP protocol based on, for example, a list of allowed 110 | | applications. 111 | | By default, it uses the defined array in the config file, but you can 112 | | anytime implement the same interface as the class and add your own 113 | | custom method to retrieve the apps. 114 | | 115 | */ 116 | 117 | 'app' => \App\Managers\MyCustomAppManager::class, 118 | 119 | ], 120 | ``` 121 | -------------------------------------------------------------------------------- /docs/advanced-usage/custom-websocket-handlers.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: Custom WebSocket Handlers 3 | order: 2 4 | --- 5 | 6 | # Custom WebSocket Handlers 7 | 8 | While this package's main purpose is to make the usage of either the Pusher JavaScript client or Laravel Echo as easy as possible, you are not limited to the Pusher protocol at all. 9 | There might be situations where all you need is a simple, bare-bone, WebSocket server where you want to have full control over the incoming payload and what you want to do with it - without having "channels" in the way. 10 | 11 | You can easily create your own custom WebSocketHandler class. All you need to do is implement Ratchets `Ratchet\WebSocket\MessageComponentInterface`. 12 | 13 | Once implemented, you will have a class that looks something like this: 14 | 15 | ```php 16 | namespace App; 17 | 18 | use Exception; 19 | use Ratchet\ConnectionInterface; 20 | use Ratchet\RFC6455\Messaging\MessageInterface; 21 | use Ratchet\WebSocket\MessageComponentInterface; 22 | 23 | class MyCustomWebSocketHandler implements MessageComponentInterface 24 | { 25 | public function onOpen(ConnectionInterface $connection) 26 | { 27 | // TODO: Implement onOpen() method. 28 | } 29 | 30 | public function onClose(ConnectionInterface $connection) 31 | { 32 | // TODO: Implement onClose() method. 33 | } 34 | 35 | public function onError(ConnectionInterface $connection, Exception $e) 36 | { 37 | // TODO: Implement onError() method. 38 | } 39 | 40 | public function onMessage(ConnectionInterface $connection, MessageInterface $msg) 41 | { 42 | // TODO: Implement onMessage() method. 43 | } 44 | } 45 | ``` 46 | 47 | In the class itself you have full control over all the lifecycle events of your WebSocket connections and can intercept the incoming messages and react to them. 48 | 49 | The only part missing is, that you will need to tell our WebSocket server to load this handler at a specific route endpoint. This can be achieved using the `WebSocketsRouter` facade. 50 | 51 | This class takes care of registering the routes with the actual webSocket server. You can use the `get` method to define a custom WebSocket endpoint. The method needs two arguments: the path where the WebSocket handled should be available and the fully qualified classname of the WebSocket handler class. 52 | 53 | This could, for example, be done inside your `routes/web.php` file. 54 | 55 | ```php 56 | WebSocketsRouter::addCustomRoute('GET', '/my-websocket', \App\MyCustomWebSocketHandler::class); 57 | ``` 58 | 59 | Once you've added the custom WebSocket route, be sure to restart our WebSocket server for the changes to take place. 60 | -------------------------------------------------------------------------------- /docs/advanced-usage/dispatched-events.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: Dispatched Events 3 | order: 5 4 | --- 5 | 6 | # Dispatched Events 7 | 8 | Laravel WebSockets takes advantage of Laravel's Event dispatching observer, in a way that you can handle in-server events outside of it. 9 | 10 | For example, you can listen for events like when a new connection establishes or when an user joins a presence channel. 11 | 12 | ## Events 13 | 14 | Below you will find a list of dispatched events: 15 | 16 | - `BeyondCode\LaravelWebSockets\Events\NewConnection` - when a connection successfully establishes on the server 17 | - `BeyondCode\LaravelWebSockets\Events\ConnectionClosed` - when a connection leaves the server 18 | - `BeyondCode\LaravelWebSockets\Events\SubscribedToChannel` - when a connection subscribes to a specific channel 19 | - `BeyondCode\LaravelWebSockets\Events\UnsubscribedFromChannel` - when a connection unsubscribes from a specific channel 20 | - `BeyondCode\LaravelWebSockets\Events\WebSocketMessageReceived` - when the server receives a message 21 | - `BeyondCode\LaravelWebSockets\EventsConnectionPonged` - when a connection pings to the server that it is still alive 22 | 23 | ## Queued Listeners 24 | 25 | Because the default Redis connection (either PhpRedis or Predis) is a blocking I/O method and can cause problems with the server speed and availability, you might want to check the [Non-Blocking Queue Driver](non-blocking-queue-driver.md) documentation that helps you create the Async Redis queue driver that is going to fix the Blocking I/O issue. 26 | 27 | If set up, you can use the `async-redis` queue driver in your listeners: 28 | 29 | ```php 30 | [ 79 | App\Listeners\HandleNewConnections::class, 80 | ], 81 | ]; 82 | ``` 83 | -------------------------------------------------------------------------------- /docs/advanced-usage/non-blocking-queue-driver.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: Non-Blocking Queue Driver 3 | order: 4 4 | --- 5 | 6 | # Non-Blocking Queue Driver 7 | 8 | In Laravel, he default Redis connection also interacts with the queues. Since you might want to dispatch jobs on Redis from the server, you can encounter an anti-pattern of using a blocking I/O connection (like PhpRedis or PRedis) within the WebSockets server. 9 | 10 | To solve this issue, you can configure the built-in queue driver that uses the Async Redis connection when it's possible, like within the WebSockets server. It's highly recommended to switch your queue to it if you are going to use the queues within the server controllers, for example. 11 | 12 | Add the `async-redis` queue driver to your list of connections. The configuration parameters are compatible with the default `redis` driver: 13 | 14 | ```php 15 | 'connections' => [ 16 | 'async-redis' => [ 17 | 'driver' => 'async-redis', 18 | 'connection' => env('WEBSOCKETS_REDIS_REPLICATION_CONNECTION', 'default'), 19 | 'queue' => env('REDIS_QUEUE', 'default'), 20 | 'retry_after' => 90, 21 | 'block_for' => null, 22 | ], 23 | ] 24 | ``` 25 | 26 | Also, make sure that the default queue driver is set to `async-redis`: 27 | 28 | ``` 29 | QUEUE_CONNECTION=async-redis 30 | ``` 31 | -------------------------------------------------------------------------------- /docs/advanced-usage/webhooks.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: Webhooks 3 | order: 3 4 | --- 5 | 6 | # Webhooks 7 | 8 | While you can create any custom websocket handlers, you might still want to intercept and run your own custom business logic on each websocket connection. 9 | 10 | In Pusher, there are [Pusher Webhooks](https://pusher.com/docs/channels/server_api/webhooks) that do this job. However, since the implementation is a pure controller, 11 | you might want to extend it and update the config file to reflect the changes: 12 | 13 | For example, running your own business logic on connection open and close: 14 | 15 | ```php 16 | namespace App\Controllers\WebSockets; 17 | 18 | use BeyondCode\LaravelWebSockets\WebSockets\WebSocketHandler as BaseWebSocketHandler; 19 | use Ratchet\ConnectionInterface; 20 | 21 | class WebSocketHandler extends BaseWebSocketHandler 22 | { 23 | public function onOpen(ConnectionInterface $connection) 24 | { 25 | parent::onOpen($connection); 26 | 27 | // Run code on open 28 | // $connection->app contains the app details 29 | // $this->channelManager is accessible 30 | } 31 | 32 | public function onClose(ConnectionInterface $connection) 33 | { 34 | parent::onClose($connection); 35 | 36 | // Run code on close. 37 | // $connection->app contains the app details 38 | // $this->channelManager is accessible 39 | } 40 | } 41 | ``` 42 | 43 | Once you implemented it, replace the `handlers.websocket` class name in config: 44 | 45 | ```php 46 | 'handlers' => [ 47 | 48 | 'websocket' => App\Controllers\WebSockets\WebSocketHandler::class, 49 | 50 | ], 51 | ``` 52 | 53 | A server restart is required afterwards. 54 | -------------------------------------------------------------------------------- /docs/basic-usage/_index.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: Basic Usage 3 | order: 2 4 | --- 5 | -------------------------------------------------------------------------------- /docs/basic-usage/pusher.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: Pusher Replacement 3 | order: 1 4 | --- 5 | 6 | # Pusher Replacement 7 | 8 | The easiest way to get started with Laravel WebSockets is by using it as a [Pusher](https://pusher.com) replacement. The integrated WebSocket and HTTP Server has complete feature parity with the Pusher WebSocket and HTTP API. In addition to that, this package also ships with an easy to use debugging dashboard to see all incoming and outgoing WebSocket requests. 9 | 10 | To make it clear, the package does not restrict connections numbers or depend on the Pusher's service. It does comply with the Pusher protocol to make it easy to use the Pusher SDK with it. 11 | 12 | ## Requirements 13 | 14 | To make use of the Laravel WebSockets package in combination with Pusher, you first need to install the official Pusher PHP SDK. 15 | 16 | If you are not yet familiar with the concept of Broadcasting in Laravel, please take a look at the [Laravel documentation](https://laravel.com/docs/8.0/broadcasting). 17 | 18 | ```bash 19 | composer require pusher/pusher-php-server "~4.0" 20 | ``` 21 | 22 | Next, you should make sure to use Pusher as your broadcasting driver. This can be achieved by setting the `BROADCAST_DRIVER` environment variable in your `.env` file: 23 | 24 | ``` 25 | BROADCAST_DRIVER=pusher 26 | ``` 27 | 28 | ## Pusher Configuration 29 | 30 | When broadcasting events from your Laravel application to your WebSocket server, the default behavior is to send the event information to the official Pusher server. But since the Laravel WebSockets package comes with its own Pusher API implementation, we need to tell Laravel to send the events to our own server. 31 | 32 | To do this, you should add the `host` and `port` configuration key to your `config/broadcasting.php` and add it to the `pusher` section. The default port of the Laravel WebSocket server is 6001. 33 | 34 | ```php 35 | 'pusher' => [ 36 | 'driver' => 'pusher', 37 | 'key' => env('PUSHER_APP_KEY'), 38 | 'secret' => env('PUSHER_APP_SECRET'), 39 | 'app_id' => env('PUSHER_APP_ID'), 40 | 'options' => [ 41 | 'cluster' => env('PUSHER_APP_CLUSTER'), 42 | 'encrypted' => true, 43 | 'host' => env('PUSHER_APP_HOST', '127.0.0.1'), 44 | 'port' => env('PUSHER_APP_PORT', 6001), 45 | 'scheme' => env('PUSHER_APP_SCHEME', 'http'), 46 | 'curl_options' => [ 47 | CURLOPT_SSL_VERIFYHOST => 0, 48 | CURLOPT_SSL_VERIFYPEER => 0, 49 | ], 50 | ], 51 | ], 52 | ``` 53 | 54 | ## Configuring WebSocket Apps 55 | 56 | The Laravel WebSocket Pusher replacement server comes with multi-tenancy support out of the box. This means that you could host it independently from your current Laravel application and serve multiple WebSocket applications with one server. 57 | 58 | To make the move from an existing Pusher setup to this package as easy as possible, the default app simply uses your existing Pusher configuration. 59 | 60 | ::: warning 61 | Make sure to use the same app id, key and secret as in your broadcasting configuration section. Otherwise broadcasting events from Laravel will not work. 62 | ::: 63 | 64 | ::: tip 65 | When using Laravel WebSockets as a Pusher replacement without having used Pusher before, it does not matter what you set as your `PUSHER_` variables. Just make sure they are unique for each project. 66 | ::: 67 | 68 | You may add additional apps in your `config/websockets.php` file. 69 | 70 | ```php 71 | 'apps' => [ 72 | [ 73 | 'id' => env('PUSHER_APP_ID'), 74 | 'name' => env('APP_NAME'), 75 | 'key' => env('PUSHER_APP_KEY'), 76 | 'secret' => env('PUSHER_APP_SECRET'), 77 | 'path' => env('PUSHER_APP_PATH'), 78 | 'capacity' => null, 79 | 'enable_client_messages' => false, 80 | 'enable_statistics' => true, 81 | 'allowed_origins' => [], 82 | ], 83 | ], 84 | ``` 85 | 86 | ### Client Messages 87 | 88 | For each app in your configuration file, you can define if this specific app should support a client-to-client messages. Usually all WebSocket messages go through your Laravel application before they will be broadcasted to other users. But sometimes you may want to enable a direct client-to-client communication instead of sending the events over the server. For example, a "typing" event in a chat application. 89 | 90 | It is important that you apply additional care when using client messages, since these originate from other users, and could be subject to tampering by a malicious user of your site. 91 | 92 | To enable or disable client messages, you can modify the `enable_client_messages` setting. The default value is `false`. 93 | 94 | ### Statistics 95 | 96 | The Laravel WebSockets package comes with an out-of-the-box statistic solution that will give you key insights into the current status of your WebSocket server. 97 | 98 | To enable or disable the statistics for one of your apps, you can modify the `enable_statistics` setting. The default value is `true`. 99 | 100 | ## Usage with Laravel Echo 101 | 102 | The Laravel WebSockets package integrates nicely into [Laravel Echo](https://laravel.com/docs/8.0/broadcasting#receiving-broadcasts) to integrate into your frontend application and receive broadcasted events. 103 | If you are new to Laravel Echo, be sure to take a look at the [official documentation](https://laravel.com/docs/8.0/broadcasting#receiving-broadcasts). 104 | 105 | To make Laravel Echo work with Laravel WebSockets, you need to make some minor configuration changes when working with Laravel Echo. Add the `wsHost` and `wsPort` parameters and point them to your Laravel WebSocket server host and port. 106 | 107 | By default, the Pusher JavaScript client tries to send statistic information - you should disable this using the `disableStats` option. 108 | 109 | ::: tip 110 | When using Laravel WebSockets in combination with a custom SSL certificate, be sure to use the `encrypted` option and set it to `true`. 111 | ::: 112 | 113 | ```js 114 | import Echo from 'laravel-echo'; 115 | 116 | window.Pusher = require('pusher-js'); 117 | 118 | window.Echo = new Echo({ 119 | broadcaster: 'pusher', 120 | key: 'your-pusher-key', 121 | wsHost: window.location.hostname, 122 | wsPort: 6001, 123 | forceTLS: false, 124 | disableStats: true, 125 | enabledTransports: ['ws', 'wss'], 126 | }); 127 | ``` 128 | 129 | Now you can use all Laravel Echo features in combination with Laravel WebSockets, such as [Presence Channels](https://laravel.com/docs/8.x/broadcasting#presence-channels), [Notifications](https://laravel.com/docs/8.x/broadcasting#notifications) and [Client Events](https://laravel.com/docs/8.x/broadcasting#client-events). 130 | -------------------------------------------------------------------------------- /docs/basic-usage/restarting.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: Restarting Server 3 | order: 4 4 | --- 5 | 6 | # Restarting Server 7 | 8 | If you use Supervisor to keep your server alive, you might want to restart it just like `queue:restart` does. 9 | 10 | To do so, consider using the `websockets:restart`. In a maximum of 10 seconds since issuing the command, the server will be restarted. 11 | 12 | ```bash 13 | php artisan websockets:restart 14 | ``` 15 | -------------------------------------------------------------------------------- /docs/basic-usage/sail.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: Laravel Sail 3 | order: 5 4 | --- 5 | 6 | # Run in Laravel Sail 7 | 8 | To be able to use Laravel Websockets in Sail, you should just forward the port: 9 | 10 | ```yaml 11 | # For more information: https://laravel.com/docs/sail 12 | version: '3' 13 | services: 14 | laravel.test: 15 | build: 16 | context: ./vendor/laravel/sail/runtimes/8.0 17 | dockerfile: Dockerfile 18 | args: 19 | WWWGROUP: '${WWWGROUP}' 20 | image: sail-8.0/app 21 | ports: 22 | - '${APP_PORT:-80}:80' 23 | - '${LARAVEL_WEBSOCKETS_PORT:-6001}:${LARAVEL_WEBSOCKETS_PORT:-6001}' 24 | ``` 25 | -------------------------------------------------------------------------------- /docs/basic-usage/starting.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: Starting the WebSocket server 3 | order: 2 4 | --- 5 | 6 | # Starting the WebSocket server 7 | 8 | Once you have configured your WebSocket apps and Pusher settings, you can start the Laravel WebSocket server by issuing the artisan command: 9 | 10 | ```bash 11 | php artisan websockets:serve 12 | ``` 13 | 14 | ## Using a different port 15 | 16 | The default port of the Laravel WebSocket server is `6001`. You may pass a different port to the command using the `--port` option. 17 | 18 | ```bash 19 | php artisan websockets:serve --port=3030 20 | ``` 21 | 22 | This will start listening on port `3030`. 23 | 24 | ## Restricting the listening host 25 | 26 | By default, the Laravel WebSocket server will listen on `0.0.0.0` and will allow incoming connections from all networks. If you want to restrict this, you can start the server with a `--host` option, followed by an IP. 27 | 28 | For example, by using `127.0.0.1`, you will only allow WebSocket connections from localhost. 29 | 30 | ```bash 31 | php artisan websockets:serve --host=127.0.0.1 32 | ``` 33 | -------------------------------------------------------------------------------- /docs/debugging/_index.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: Debugging 3 | order: 3 4 | --- 5 | -------------------------------------------------------------------------------- /docs/debugging/console.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: Console Logging 3 | order: 1 4 | --- 5 | 6 | # Console Logging 7 | 8 | When you start the Laravel WebSocket server and your application is in debug mode, you will automatically see all incoming and outgoing WebSocket events in your terminal. 9 | 10 | On production environments, you shall use the `--debug` flag to display the events in the terminal. 11 | 12 | ![Console Logging](/img/console.png) 13 | -------------------------------------------------------------------------------- /docs/debugging/dashboard.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: Debug Dashboard 3 | order: 2 4 | --- 5 | 6 | # Debug Dashboard 7 | 8 | In addition to logging the events to the console, you can also use a real-time dashboard that shows you all incoming connections, events and disconnects the moment they happen on your WebSocket server. 9 | 10 | ![Debug Dashboard](/img/dashboard.jpg) 11 | 12 | ## Accessing the Dashboard 13 | 14 | The default location of the WebSocket dashboard is at `/laravel-websockets`. The routes get automatically registered. 15 | If you want to change the URL of the dashboard, you can configure it with the `path` setting in your `config/websockets.php` file. 16 | 17 | To access the debug dashboard, you can visit the dashboard URL of your Laravel project in the browser 18 | Since your WebSocket server has support for multiple apps, you can select which app you want to connect to and inspect. 19 | 20 | By pressing the "Connect" button, you can establish the WebSocket connection and see all events taking place on your WebSocket server from there on in real-time. 21 | 22 | **Note:** Be sure to set the ``APP_URL`` env variable to match the current URL where your project is running to be sure the stats graph works properly. 23 | 24 | ## Protecting the Dashboard 25 | 26 | By default, access to the WebSocket dashboard is only allowed while your application environment is set to `local`. 27 | 28 | However, you can change this behavior by overriding the Laravel Gate being used. A good place for this is the `AuthServiceProvider` that ships with Laravel. 29 | 30 | ```php 31 | public function boot() 32 | { 33 | $this->registerPolicies(); 34 | 35 | Gate::define('viewWebSocketsDashboard', function ($user = null) { 36 | return in_array($user->email, [ 37 | // 38 | ]); 39 | }); 40 | } 41 | ``` 42 | 43 | ## Statistics 44 | 45 | This package allows you to record key metrics of your WebSocket server. The WebSocket server will store a snapshot of the current number of peak connections, the amount of received WebSocket messages and the amount of received API messages defined in a fixed interval. The default setting is to store a snapshot every 60 seconds. 46 | 47 | In addition to simply storing the statistic information in your database, you can also see the statistics as they happen in real-time on the debug dashboard. 48 | 49 | ![Real-Time Statistics](/img/statistics.gif) 50 | 51 | You can modify this interval by changing the `interval_in_seconds` setting in your config file. 52 | 53 | ## Cleanup old Statistics 54 | 55 | After using the WebSocket server for a while you will have recorded a lot of statistical data that you might no longer need. This package provides an artisan command `websockets:clean` to clean these statistic log entries. 56 | 57 | Running this command will result in the deletion of all recorded statistics that are older than the number of days specified in the `delete_statistics_older_than_days` setting of the config file. 58 | 59 | You can leverage Laravel's scheduler to run the clean up command now and then. 60 | 61 | ```php 62 | //app/Console/Kernel.php 63 | 64 | protected function schedule(Schedule $schedule) 65 | { 66 | $schedule->command('websockets:clean')->daily(); 67 | } 68 | ``` 69 | 70 | ## Disable Statistics 71 | 72 | Each app contains an `enable_statistics` that defines wether that app generates statistics or not. The statistics are being stored for the `interval_in_seconds` seconds and then they are inserted in the database. 73 | 74 | However, to disable it entirely and void any incoming statistic, you can call `--disable-statistics` when running the server command: 75 | 76 | ```bash 77 | php artisan websockets:serve --disable-statistics 78 | ``` 79 | 80 | ## Event Creator 81 | 82 | The dashboard also comes with an easy-to-use event creator, that lets you manually send events to your channels. 83 | 84 | Simply enter the channel, the event name and provide a valid JSON payload to send it to all connected clients in the given channel. 85 | -------------------------------------------------------------------------------- /docs/faq/_index.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: FAQ 3 | order: 6 4 | --- 5 | -------------------------------------------------------------------------------- /docs/faq/cloudflare.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: Cloudflare 3 | order: 3 4 | --- 5 | 6 | # Cloudflare 7 | 8 | In some cases, you might use Cloudflare and notice that your production server does not seem to respond to your `:6001` port. 9 | 10 | This is because Cloudflare does not seem to open ports, [excepting a few of them](https://blog.cloudflare.com/cloudflare-now-supporting-more-ports/). 11 | 12 | To mitigate this issue, for example, you can run your server on port `2096`: 13 | 14 | ```bash 15 | php artisan websockets:serve --port=2096 16 | ``` 17 | 18 | You will notice that the new `:2096` websockets server will work properly. 19 | -------------------------------------------------------------------------------- /docs/faq/deploying.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: Deploying 3 | order: 1 4 | --- 5 | 6 | # Deploying 7 | 8 | When your application is ready to get deployed, here are some tips to improve your WebSocket server. 9 | 10 | ### Open Connection Limit 11 | 12 | On Unix systems, every user that connects to your WebSocket server is represented as a file somewhere on the system. 13 | As a security measurement of every Unix based OS, the number of "file descriptors" an application may have open at a time is limited - most of the time to a default value of 1024 - which would result in a maximum number of 1024 concurrent users on your WebSocket server. 14 | 15 | In addition to the OS restrictions, this package makes use of an event loop called "stream_select", which has a hard limit of 1024. 16 | 17 | #### Increasing the maximum number of file descriptors 18 | 19 | The operating system limit of open "file descriptors" can be increased using the `ulimit` command. The `-n` option modifies the number of open file descriptors. 20 | 21 | ```bash 22 | ulimit -n 10000 23 | ``` 24 | 25 | The `ulimit` command only **temporarily** increases the maximum number of open file descriptors. To permanently modify this value, you can edit it in your operating system `limits.conf` file. 26 | 27 | You are best to do so by creating a file in the `limits.d` directory. This will work for both Red Hat & Ubuntu derivatives. 28 | 29 | ```bash 30 | $ cat /etc/security/limits.d/laravel-echo.conf 31 | laravel-echo soft nofile 10000 32 | ``` 33 | 34 | The above example assumes you will run your echo server as the `laravel-echo` user, you are free to change that to your liking. 35 | 36 | #### Changing the event loop 37 | 38 | To make use of a different event loop, that does not have a hard limit of 1024 concurrent connections, you can either install the `ev` or `event` PECL extension using: 39 | 40 | ```bash 41 | sudo pecl install ev 42 | # or 43 | sudo pecl install event 44 | ``` 45 | 46 | #### Deploying on Laravel Forge 47 | 48 | If your are using [Laravel Forge](https://forge.laravel.com/) for the deployment [this article by Alex Bouma](https://alex.bouma.dev/installing-laravel-websockets-on-forge) might help you out. 49 | 50 | #### Deploying on Laravel Vapor 51 | 52 | Since [Laravel Vapor](https://vapor.laravel.com) runs on a serverless architecture, you will need to spin up an actual EC2 Instance that runs in the same VPC as the Lambda function to be able to make use of the WebSocket connection. 53 | 54 | The Lambda function will make sure your HTTP request gets fulfilled, then the EC2 Instance will be continuously polled through the WebSocket protocol. 55 | 56 | ## Keeping the socket server running with supervisord 57 | 58 | The `websockets:serve` daemon needs to always be running in order to accept connections. This is a prime use case for `supervisor`, a task runner on Linux. 59 | 60 | First, make sure `supervisor` is installed. 61 | 62 | ```bash 63 | # On Debian / Ubuntu 64 | apt install supervisor 65 | 66 | # On Red Hat / CentOS 67 | yum install supervisor 68 | systemctl enable supervisord 69 | ``` 70 | 71 | Once installed, add a new process that `supervisor` needs to keep running. You place your configurations in the `/etc/supervisor/conf.d` (Debian/Ubuntu) or `/etc/supervisord.d` (Red Hat/CentOS) directory. 72 | 73 | Within that directory, create a new file called `websockets.conf`. 74 | 75 | ```bash 76 | [program:websockets] 77 | command=/usr/bin/php /home/laravel-echo/laravel-websockets/artisan websockets:serve 78 | numprocs=1 79 | autostart=true 80 | autorestart=true 81 | user=laravel-echo 82 | ``` 83 | 84 | Once created, instruct `supervisor` to reload its configuration files (without impacting the already running `supervisor` jobs). 85 | 86 | ```bash 87 | supervisorctl update 88 | supervisorctl start websockets 89 | ``` 90 | 91 | Your echo server should now be running (you can verify this with `supervisorctl status`). If it were to crash, `supervisor` will automatically restart it. 92 | 93 | Please note that, by default, just like file descriptiors, `supervisor` will force a maximum number of open files onto all the processes that it manages. This is configured by the `minfds` parameter in `supervisord.conf`. 94 | 95 | If you want to increase the maximum number of open files, you may do so in `/etc/supervisor/supervisord.conf` (Debian/Ubuntu) or `/etc/supervisord.conf` (Red Hat/CentOS): 96 | 97 | ``` 98 | [supervisord] 99 | minfds=10240; (min. avail startup file descriptors;default 1024) 100 | ``` 101 | 102 | After changing this setting, you'll need to restart the supervisor process (which in turn will restart all your processes that it manages). 103 | 104 | ## Debugging supervisor 105 | 106 | If you run into issues with Supervisor, like not supporting a lot of connections, consider checking the [Ratched docs on deploying with Supervisor](http://socketo.me/docs/deploy#supervisor). 107 | -------------------------------------------------------------------------------- /docs/faq/scaling.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: Benchmarks 3 | order: 2 4 | --- 5 | 6 | # Benchmarks 7 | 8 | Of course, this is not a question with an easy answer as your mileage may vary. But with the appropriate server-side configuration your WebSocket server can easily hold a **lot** of concurrent connections. 9 | 10 | This is an example benchmark that was done on the smallest Digital Ocean droplet, that also had a couple of other Laravel projects running. On this specific server, the maximum number of **concurrent** connections ended up being ~15,000. 11 | 12 | ![Benchmark](/img/simultaneous_users.png) 13 | 14 | Here is another benchmark that was run on a 2GB Digital Ocean droplet with 2 CPUs. The maximum number of **concurrent** connections on this server setup is nearly 60,000. 15 | 16 | ![Benchmark](/img/simultaneous_users_2gb.png) 17 | 18 | Make sure to take a look at the [Deployment Tips](/docs/laravel-websockets/faq/deploying) to find out how to improve your specific setup. 19 | 20 | # Horizontal Scaling 21 | 22 | When deploying to multi-node environments, you will notice that the server won't behave correctly. Check [Horizontal Scaling](../horizontal-scaling/getting-started.md) section. 23 | -------------------------------------------------------------------------------- /docs/getting-started/_index.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: Getting Started 3 | order: 1 4 | --- 5 | -------------------------------------------------------------------------------- /docs/getting-started/installation.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: Installation 3 | order: 2 4 | --- 5 | 6 | # Installation 7 | 8 | Laravel WebSockets can be installed via composer: 9 | 10 | ```bash 11 | composer require beyondcode/laravel-websockets 12 | ``` 13 | 14 | The package will automatically register a service provider. 15 | 16 | You need to publish the WebSocket configuration file: 17 | 18 | ```bash 19 | php artisan vendor:publish --provider="BeyondCode\LaravelWebSockets\WebSocketsServiceProvider" --tag="config" 20 | ``` 21 | 22 | # Statistics 23 | 24 | This package comes with migrations to store statistic information while running your WebSocket server. For more info, check the [Debug Dashboard](../debugging/dashboard.md) section. 25 | 26 | You can publish the migration file using: 27 | 28 | ```bash 29 | php artisan vendor:publish --provider="BeyondCode\LaravelWebSockets\WebSocketsServiceProvider" --tag="migrations" 30 | ``` 31 | 32 | Run the migrations with: 33 | 34 | ```bash 35 | php artisan migrate 36 | ``` 37 | -------------------------------------------------------------------------------- /docs/getting-started/introduction.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: Introduction 3 | order: 1 4 | --- 5 | 6 | # Laravel WebSockets 🛰 7 | 8 | WebSockets for Laravel. Done right. 9 | 10 | Laravel WebSockets is a package for Laravel that will get your application started with WebSockets in no-time! It has a drop-in Pusher API replacement, has a debug dashboard, realtime statistics and even allows you to create custom WebSocket controllers. 11 | 12 | Once installed, you can start it with one simple command: 13 | 14 | ```php 15 | php artisan websockets:serve 16 | ``` 17 | 18 | --- 19 | 20 | If you want to know how all of it works under the hood, we wrote an in-depth [blogpost](https://murze.be/introducing-laravel-websockets-an-easy-to-use-websocket-server-implemented-in-php) about it. 21 | 22 | To help you get started, you can also take a look at the [demo repository](https://github.com/beyondcode/laravel-websockets-demo), that implements a basic Chat built with this package. 23 | -------------------------------------------------------------------------------- /docs/getting-started/questions-issues.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: Questions and issues 3 | order: 3 4 | --- 5 | 6 | # Questions and issues 7 | 8 | Find yourself stuck using the package? Found a bug? Do you have general questions or suggestions for improving laravel-websockets? Feel free to create an issue on [GitHub](https://github.com/beyondcode/laravel-websockets/issues), we'll try to address it as soon as possible. 9 | 10 | If you've found a bug regarding security please mail [marcel@beyondco.de](mailto:marcel@beyondco.de) instead of using the issue tracker. -------------------------------------------------------------------------------- /docs/horizontal-scaling/_index.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: Horizontal Scaling 3 | order: 5 4 | --- 5 | -------------------------------------------------------------------------------- /docs/horizontal-scaling/getting-started.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: Getting Started 3 | order: 1 4 | --- 5 | 6 | When running Laravel WebSockets without additional configuration, you won't be able to scale your servers out. 7 | 8 | For example, even with Sticky Load Balancer settings, you won't be able to keep track of your users' connections to notify them properly when messages occur if you got multiple nodes that run the same `websockets:serve` command. 9 | 10 | The reason why this happen is because the default channel manager runs on arrays, which is not a database other instances can access. 11 | 12 | To do so, we need a database and a way of notifying other instances when connections occur. 13 | 14 | For example, Redis does a great job by encapsulating the both the way of notifying (Pub/Sub module) and the storage (key-value datastore). 15 | 16 | ## Configure the replication 17 | 18 | To enable the replication, simply change the `replication.mode` name in the `websockets.php` file: 19 | 20 | ```php 21 | 'replication' => [ 22 | 23 | 'mode' => 'redis', 24 | 25 | ... 26 | 27 | ], 28 | ``` 29 | 30 | Now, when your app broadcasts the message, it will make sure the connection reaches other servers which are under the same load balancer. 31 | 32 | The available drivers for replication are: 33 | 34 | - [Redis](redis) 35 | -------------------------------------------------------------------------------- /docs/horizontal-scaling/redis.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: Redis Mode 3 | order: 2 4 | --- 5 | 6 | # Redis Mode 7 | 8 | Redis has the powerful ability to act both as a key-value store and as a PubSub service. This way, the connected servers will communicate between them whenever a message hits the server, so you can scale out to any amount of servers while preserving the WebSockets functionalities. 9 | 10 | ## Configure Redis mode 11 | 12 | To enable the replication, simply change the `replication.mode` name in the `websockets.php` file to `redis`: 13 | 14 | ```php 15 | 'replication' => [ 16 | 17 | 'mode' => 'redis', 18 | 19 | ... 20 | 21 | ], 22 | ``` 23 | 24 | You can set the connection name to the Redis database under `redis`: 25 | 26 | ```php 27 | 'replication' => [ 28 | 29 | 'modes' => 30 | 31 | 'redis' => [ 32 | 33 | 'connection' => 'default', 34 | 35 | ], 36 | 37 | ], 38 | 39 | ], 40 | ``` 41 | 42 | The connections can be found in your `config/database.php` file, under the `redis` key. 43 | -------------------------------------------------------------------------------- /phpunit.xml: -------------------------------------------------------------------------------- 1 | 2 | 12 | 13 | 14 | tests 15 | 16 | 17 | 18 | 19 | src/ 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | -------------------------------------------------------------------------------- /resources/views/apps.blade.php: -------------------------------------------------------------------------------- 1 | @extends('websockets::layout') 2 | 3 | @section('title') 4 | Apps 5 | @endsection 6 | 7 | @section('content') 8 |
9 |
10 | @csrf 11 |
12 |
13 |
14 |

15 | Add new app 16 |

17 |
18 | 19 | @if($errors->isNotEmpty()) 20 |
21 | @foreach($errors->all() as $error) 22 | {{ $error }}
23 | @endforeach 24 |
25 | @endif 26 | 27 |
28 |
29 | 33 |
34 |
35 | 37 |
38 |
39 |
40 |
41 | 45 |
46 |
47 | 49 |
50 |
51 |
52 |
53 | 57 |
58 |
59 |
60 | 63 | 66 |
67 |
68 |
69 |
70 |
71 | 75 |
76 |
77 |
78 | 81 | 84 |
85 |
86 |
87 |
88 |
89 |
90 |
91 | 92 | 97 | 98 |
99 |
100 |
101 |
102 |
103 |
104 |
106 | 107 | 108 | 109 | 112 | 115 | 118 | 121 | 122 | 123 | 124 | 125 | 126 | 129 | 132 | 136 | 140 | 146 | 147 | 148 |
110 | Name 111 | 113 | Allowed origins 114 | 116 | Statistics 117 | 119 | Client Messages 120 |
127 | @{{ app.name }} 128 | 130 | @{{ app.allowed_origins || '*' }} 131 | 133 | Yes 134 | No 135 | 137 | Yes 138 | No 139 | 141 | Installation instructions 143 | Delete 145 |
149 |
150 |
151 | 152 |
153 |

Modify your .env file:

154 |
PUSHER_APP_HOST=@{{ app.host === null ? window.location.hostname : app.host }}
155 | PUSHER_APP_PORT={{ $port }}
156 | PUSHER_APP_KEY=@{{ app.key }}
157 | PUSHER_APP_ID=@{{ app.id }}
158 | PUSHER_APP_SECRET=@{{ app.secret }}
159 | PUSHER_APP_SCHEME=https
160 | MIX_PUSHER_APP_HOST="${PUSHER_APP_HOST}"
161 | MIX_PUSHER_APP_PORT="${PUSHER_APP_PORT}"
162 | MIX_PUSHER_APP_KEY="${PUSHER_APP_KEY}"
163 |
164 | 165 |
166 | @endsection 167 | 168 | @section('scripts') 169 | 183 | @endsection 184 | -------------------------------------------------------------------------------- /resources/views/layout.blade.php: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | Laravel WebSockets 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 20 | 21 | 22 | 23 |
24 | 73 | 74 | 75 |
76 |
77 |
78 |

79 | @yield('title') 80 |

81 |
82 |
83 |
84 |
85 | @yield('content') 86 |
87 |
88 |
89 |
90 | 91 | @yield('scripts') 92 | 93 | 94 | -------------------------------------------------------------------------------- /src/API/FetchChannel.php: -------------------------------------------------------------------------------- 1 | channelManager->find( 20 | $request->appId, $request->channelName 21 | ); 22 | 23 | if (is_null($channel)) { 24 | return new HttpException(404, "Unknown channel `{$request->channelName}`."); 25 | } 26 | 27 | return $this->channelManager 28 | ->getGlobalConnectionsCount($request->appId, $request->channelName) 29 | ->then(function ($connectionsCount) use ($request) { 30 | // For the presence channels, we need a slightly different response 31 | // that need an additional call. 32 | if (Str::startsWith($request->channelName, 'presence-')) { 33 | return $this->channelManager 34 | ->getChannelsMembersCount($request->appId, [$request->channelName]) 35 | ->then(function ($channelMembers) use ($connectionsCount, $request) { 36 | return [ 37 | 'occupied' => $connectionsCount > 0, 38 | 'subscription_count' => $connectionsCount, 39 | 'user_count' => $channelMembers[$request->channelName] ?? 0, 40 | ]; 41 | }); 42 | } 43 | 44 | // For the rest of the channels, we might as well 45 | // send the basic response with the subscriptions count. 46 | return [ 47 | 'occupied' => $connectionsCount > 0, 48 | 'subscription_count' => $connectionsCount, 49 | ]; 50 | }); 51 | } 52 | } 53 | -------------------------------------------------------------------------------- /src/API/FetchChannels.php: -------------------------------------------------------------------------------- 1 | has('info')) { 24 | $attributes = explode(',', trim($request->info)); 25 | 26 | if (in_array('user_count', $attributes) && ! Str::startsWith($request->filter_by_prefix, 'presence-')) { 27 | throw new HttpException(400, 'Request must be limited to presence channels in order to fetch user_count'); 28 | } 29 | } 30 | 31 | return $this->channelManager 32 | ->getGlobalChannels($request->appId) 33 | ->then(function ($channels) use ($request, $attributes) { 34 | $channels = collect($channels)->keyBy(function ($channel) { 35 | return $channel instanceof Channel 36 | ? $channel->getName() 37 | : $channel; 38 | }); 39 | 40 | if ($request->has('filter_by_prefix')) { 41 | $channels = $channels->filter(function ($channel, $channelName) use ($request) { 42 | return Str::startsWith($channelName, $request->filter_by_prefix); 43 | }); 44 | } 45 | 46 | $channelNames = $channels->map(function ($channel) { 47 | return $channel instanceof Channel 48 | ? $channel->getName() 49 | : $channel; 50 | })->toArray(); 51 | 52 | return $this->channelManager 53 | ->getChannelsMembersCount($request->appId, $channelNames) 54 | ->then(function ($counts) use ($channels, $attributes) { 55 | $channels = $channels->map(function ($channel) use ($counts, $attributes) { 56 | $info = new stdClass; 57 | 58 | $channelName = $channel instanceof Channel 59 | ? $channel->getName() 60 | : $channel; 61 | 62 | if (in_array('user_count', $attributes)) { 63 | $info->user_count = $counts[$channelName]; 64 | } 65 | 66 | return $info; 67 | })->sortBy(function ($content, $name) { 68 | return $name; 69 | })->all(); 70 | 71 | return [ 72 | 'channels' => $channels ?: new stdClass, 73 | ]; 74 | }); 75 | }); 76 | } 77 | } 78 | -------------------------------------------------------------------------------- /src/API/FetchUsers.php: -------------------------------------------------------------------------------- 1 | channelName, 'presence-')) { 20 | return new HttpException(400, "Invalid presence channel `{$request->channelName}`"); 21 | } 22 | 23 | return $this->channelManager 24 | ->getChannelMembers($request->appId, $request->channelName) 25 | ->then(function ($members) { 26 | $users = collect($members)->map(function ($user) { 27 | return ['id' => $user->user_id]; 28 | })->values()->toArray(); 29 | 30 | return [ 31 | 'users' => $users, 32 | ]; 33 | }); 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /src/API/TriggerEvent.php: -------------------------------------------------------------------------------- 1 | has('channel')) { 22 | $channels = [$request->get('channel')]; 23 | } else { 24 | $channels = $request->channels ?: []; 25 | 26 | if (is_string($channels)) { 27 | $channels = [$channels]; 28 | } 29 | } 30 | 31 | foreach ($channels as $channelName) { 32 | // Here you can use the ->find(), even if the channel 33 | // does not exist on the server. If it does not exist, 34 | // then the message simply will get broadcasted 35 | // across the other servers. 36 | $channel = $this->channelManager->find( 37 | $request->appId, $channelName 38 | ); 39 | 40 | $payload = [ 41 | 'event' => $request->name, 42 | 'channel' => $channelName, 43 | 'data' => $request->data, 44 | ]; 45 | 46 | if ($channel) { 47 | $channel->broadcastLocallyToEveryoneExcept( 48 | (object) $payload, 49 | $request->socket_id, 50 | $request->appId 51 | ); 52 | } 53 | 54 | $this->channelManager->broadcastAcrossServers( 55 | $request->appId, $request->socket_id, $channelName, (object) $payload 56 | ); 57 | 58 | $deferred = new Deferred(); 59 | 60 | $this->ensureValidAppId($request->appId) 61 | ->then(function ($app) use ($request, $channelName, $deferred) { 62 | if ($app->statisticsEnabled) { 63 | StatisticsCollector::apiMessage($request->appId); 64 | } 65 | 66 | DashboardLogger::log($request->appId, DashboardLogger::TYPE_API_MESSAGE, [ 67 | 'event' => $request->name, 68 | 'channel' => $channelName, 69 | 'payload' => $request->data, 70 | ]); 71 | 72 | $deferred->resolve((object) []); 73 | }); 74 | } 75 | 76 | return $deferred->promise(); 77 | } 78 | } 79 | -------------------------------------------------------------------------------- /src/Apps/App.php: -------------------------------------------------------------------------------- 1 | findById($appId); 49 | } 50 | 51 | /** 52 | * Find the app by app key. 53 | * 54 | * @param string $appKey 55 | * @return PromiseInterface 56 | */ 57 | public static function findByKey($appKey): PromiseInterface 58 | { 59 | return app(AppManager::class)->findByKey($appKey); 60 | } 61 | 62 | /** 63 | * Find the app by app secret. 64 | * 65 | * @param string $appSecret 66 | * @return PromiseInterface 67 | */ 68 | public static function findBySecret($appSecret): PromiseInterface 69 | { 70 | return app(AppManager::class)->findBySecret($appSecret); 71 | } 72 | 73 | /** 74 | * Initialize the Web Socket app instance. 75 | * 76 | * @param string|int $appId 77 | * @param string $key 78 | * @param string $secret 79 | * @return void 80 | */ 81 | public function __construct($appId, $appKey, $appSecret) 82 | { 83 | $this->id = $appId; 84 | $this->key = $appKey; 85 | $this->secret = $appSecret; 86 | } 87 | 88 | /** 89 | * Set the name of the app. 90 | * 91 | * @param string $appName 92 | * @return $this 93 | */ 94 | public function setName(string $appName) 95 | { 96 | $this->name = $appName; 97 | 98 | return $this; 99 | } 100 | 101 | /** 102 | * Set the app host. 103 | * 104 | * @param string $host 105 | * @return $this 106 | */ 107 | public function setHost(string $host) 108 | { 109 | $this->host = $host; 110 | 111 | return $this; 112 | } 113 | 114 | /** 115 | * Set path for the app. 116 | * 117 | * @param string $path 118 | * @return $this 119 | */ 120 | public function setPath(string $path) 121 | { 122 | $this->path = $path; 123 | 124 | return $this; 125 | } 126 | 127 | /** 128 | * Enable client messages. 129 | * 130 | * @param bool $enabled 131 | * @return $this 132 | */ 133 | public function enableClientMessages(bool $enabled = true) 134 | { 135 | $this->clientMessagesEnabled = $enabled; 136 | 137 | return $this; 138 | } 139 | 140 | /** 141 | * Set the maximum capacity for the app. 142 | * 143 | * @param int|null $capacity 144 | * @return $this 145 | */ 146 | public function setCapacity(?int $capacity) 147 | { 148 | $this->capacity = $capacity; 149 | 150 | return $this; 151 | } 152 | 153 | /** 154 | * Enable statistics for the app. 155 | * 156 | * @param bool $enabled 157 | * @return $this 158 | */ 159 | public function enableStatistics(bool $enabled = true) 160 | { 161 | $this->statisticsEnabled = $enabled; 162 | 163 | return $this; 164 | } 165 | 166 | /** 167 | * Add whitelisted origins. 168 | * 169 | * @param array $allowedOrigins 170 | * @return $this 171 | */ 172 | public function setAllowedOrigins(array $allowedOrigins) 173 | { 174 | $this->allowedOrigins = $allowedOrigins; 175 | 176 | return $this; 177 | } 178 | } 179 | -------------------------------------------------------------------------------- /src/Apps/ConfigAppManager.php: -------------------------------------------------------------------------------- 1 | apps = collect(config('websockets.apps')); 27 | } 28 | 29 | /** 30 | * Get all apps. 31 | * 32 | * @return PromiseInterface 33 | */ 34 | public function all(): PromiseInterface 35 | { 36 | return resolvePromise($this->apps 37 | ->map(function (array $appAttributes) { 38 | return $this->convertIntoApp($appAttributes); 39 | }) 40 | ->toArray()); 41 | } 42 | 43 | /** 44 | * Get app by id. 45 | * 46 | * @param string|int $appId 47 | * @return PromiseInterface 48 | */ 49 | public function findById($appId): PromiseInterface 50 | { 51 | return resolvePromise($this->convertIntoApp( 52 | $this->apps->firstWhere('id', $appId) 53 | )); 54 | } 55 | 56 | /** 57 | * Get app by app key. 58 | * 59 | * @param string $appKey 60 | * @return PromiseInterface 61 | */ 62 | public function findByKey($appKey): PromiseInterface 63 | { 64 | return resolvePromise($this->convertIntoApp( 65 | $this->apps->firstWhere('key', $appKey) 66 | )); 67 | } 68 | 69 | /** 70 | * Get app by secret. 71 | * 72 | * @param string $appSecret 73 | * @return PromiseInterface 74 | */ 75 | public function findBySecret($appSecret): PromiseInterface 76 | { 77 | return resolvePromise($this->convertIntoApp( 78 | $this->apps->firstWhere('secret', $appSecret) 79 | )); 80 | } 81 | 82 | /** 83 | * @inheritDoc 84 | */ 85 | public function createApp($appData): PromiseInterface 86 | { 87 | $this->apps->push($appData); 88 | 89 | return resolvePromise(); 90 | } 91 | 92 | /** 93 | * Map the app into an App instance. 94 | * 95 | * @param array|null $app 96 | * @return \BeyondCode\LaravelWebSockets\Apps\App|null 97 | */ 98 | protected function convertIntoApp(?array $appAttributes): ?App 99 | { 100 | if (! $appAttributes) { 101 | return null; 102 | } 103 | 104 | $app = new App( 105 | $appAttributes['id'], 106 | $appAttributes['key'], 107 | $appAttributes['secret'] 108 | ); 109 | 110 | if (isset($appAttributes['name'])) { 111 | $app->setName($appAttributes['name']); 112 | } 113 | 114 | if (isset($appAttributes['host'])) { 115 | $app->setHost($appAttributes['host']); 116 | } 117 | 118 | if (isset($appAttributes['path'])) { 119 | $app->setPath($appAttributes['path']); 120 | } 121 | 122 | $app 123 | ->enableClientMessages((bool) $appAttributes['enable_client_messages']) 124 | ->enableStatistics((bool) $appAttributes['enable_statistics']) 125 | ->setCapacity($appAttributes['capacity'] ?? null) 126 | ->setAllowedOrigins($appAttributes['allowed_origins'] ?? []); 127 | 128 | return $app; 129 | } 130 | } 131 | -------------------------------------------------------------------------------- /src/Apps/MysqlAppManager.php: -------------------------------------------------------------------------------- 1 | database = $database; 28 | } 29 | 30 | protected function getTableName(): string 31 | { 32 | return config('websockets.managers.mysql.table'); 33 | } 34 | 35 | /** 36 | * Get all apps. 37 | * 38 | * @return PromiseInterface 39 | */ 40 | public function all(): PromiseInterface 41 | { 42 | $deferred = new Deferred(); 43 | 44 | $this->database->query('SELECT * FROM `'.$this->getTableName().'`') 45 | ->then(function (QueryResult $result) use ($deferred) { 46 | $deferred->resolve($result->resultRows); 47 | }, function ($error) use ($deferred) { 48 | $deferred->reject($error); 49 | }); 50 | 51 | return $deferred->promise(); 52 | } 53 | 54 | /** 55 | * Get app by id. 56 | * 57 | * @param string|int $appId 58 | * @return PromiseInterface 59 | */ 60 | public function findById($appId): PromiseInterface 61 | { 62 | $deferred = new Deferred(); 63 | 64 | $this->database->query('SELECT * from `'.$this->getTableName().'` WHERE `id` = ?', [$appId]) 65 | ->then(function (QueryResult $result) use ($deferred) { 66 | $deferred->resolve($this->convertIntoApp($result->resultRows[0])); 67 | }, function ($error) use ($deferred) { 68 | $deferred->reject($error); 69 | }); 70 | 71 | return $deferred->promise(); 72 | } 73 | 74 | /** 75 | * Get app by app key. 76 | * 77 | * @param string $appKey 78 | * @return PromiseInterface 79 | */ 80 | public function findByKey($appKey): PromiseInterface 81 | { 82 | $deferred = new Deferred(); 83 | 84 | $this->database->query('SELECT * from `'.$this->getTableName().'` WHERE `key` = ?', [$appKey]) 85 | ->then(function (QueryResult $result) use ($deferred) { 86 | $deferred->resolve($this->convertIntoApp($result->resultRows[0])); 87 | }, function ($error) use ($deferred) { 88 | $deferred->reject($error); 89 | }); 90 | 91 | return $deferred->promise(); 92 | } 93 | 94 | /** 95 | * Get app by secret. 96 | * 97 | * @param string $appSecret 98 | * @return PromiseInterface 99 | */ 100 | public function findBySecret($appSecret): PromiseInterface 101 | { 102 | $deferred = new Deferred(); 103 | 104 | $this->database->query('SELECT * from `'.$this->getTableName().'` WHERE `secret` = ?', [$appSecret]) 105 | ->then(function (QueryResult $result) use ($deferred) { 106 | $deferred->resolve($this->convertIntoApp($result->resultRows[0])); 107 | }, function ($error) use ($deferred) { 108 | $deferred->reject($error); 109 | }); 110 | 111 | return $deferred->promise(); 112 | } 113 | 114 | /** 115 | * Map the app into an App instance. 116 | * 117 | * @param array|null $app 118 | * @return \BeyondCode\LaravelWebSockets\Apps\App|null 119 | */ 120 | protected function convertIntoApp(?array $appAttributes): ?App 121 | { 122 | if (! $appAttributes) { 123 | return null; 124 | } 125 | 126 | $app = new App( 127 | $appAttributes['id'], 128 | $appAttributes['key'], 129 | $appAttributes['secret'] 130 | ); 131 | 132 | if (isset($appAttributes['name'])) { 133 | $app->setName($appAttributes['name']); 134 | } 135 | 136 | if (isset($appAttributes['host'])) { 137 | $app->setHost($appAttributes['host']); 138 | } 139 | 140 | if (isset($appAttributes['path'])) { 141 | $app->setPath($appAttributes['path']); 142 | } 143 | 144 | $app 145 | ->enableClientMessages((bool) $appAttributes['enable_client_messages']) 146 | ->enableStatistics((bool) $appAttributes['enable_statistics']) 147 | ->setCapacity($appAttributes['capacity'] ?? null) 148 | ->setAllowedOrigins(array_filter(explode(',', $appAttributes['allowed_origins']))); 149 | 150 | return $app; 151 | } 152 | 153 | /** 154 | * @inheritDoc 155 | */ 156 | public function createApp($appData): PromiseInterface 157 | { 158 | $deferred = new Deferred(); 159 | 160 | $this->database->query( 161 | 'INSERT INTO `'.$this->getTableName().'` (`id`, `key`, `secret`, `name`, `enable_client_messages`, `enable_statistics`, `allowed_origins`, `capacity`) VALUES (?, ?, ?, ?, ?, ?, ?, ?)', 162 | [$appData['id'], $appData['key'], $appData['secret'], $appData['name'], $appData['enable_client_messages'], $appData['enable_statistics'], $appData['allowed_origins'] ?? '', $appData['capacity'] ?? null]) 163 | ->then(function () use ($deferred) { 164 | $deferred->resolve(); 165 | }, function ($error) use ($deferred) { 166 | $deferred->reject($error); 167 | }); 168 | 169 | return $deferred->promise(); 170 | } 171 | } 172 | -------------------------------------------------------------------------------- /src/Apps/SQLiteAppManager.php: -------------------------------------------------------------------------------- 1 | database = $database; 28 | } 29 | 30 | /** 31 | * Get all apps. 32 | * 33 | * @return PromiseInterface 34 | */ 35 | public function all(): PromiseInterface 36 | { 37 | $deferred = new Deferred(); 38 | 39 | $this->database->query('SELECT * FROM `apps`') 40 | ->then(function (Result $result) use ($deferred) { 41 | $deferred->resolve($result->rows); 42 | }, function ($error) use ($deferred) { 43 | $deferred->reject($error); 44 | }); 45 | 46 | return $deferred->promise(); 47 | } 48 | 49 | /** 50 | * Get app by id. 51 | * 52 | * @param string|int $appId 53 | * @return PromiseInterface 54 | */ 55 | public function findById($appId): PromiseInterface 56 | { 57 | $deferred = new Deferred(); 58 | 59 | $this->database->query('SELECT * from apps WHERE `id` = :id', ['id' => $appId]) 60 | ->then(function (Result $result) use ($deferred) { 61 | $deferred->resolve($this->convertIntoApp($result->rows[0])); 62 | }, function ($error) use ($deferred) { 63 | $deferred->reject($error); 64 | }); 65 | 66 | return $deferred->promise(); 67 | } 68 | 69 | /** 70 | * Get app by app key. 71 | * 72 | * @param string $appKey 73 | * @return PromiseInterface 74 | */ 75 | public function findByKey($appKey): PromiseInterface 76 | { 77 | $deferred = new Deferred(); 78 | 79 | $this->database->query('SELECT * from apps WHERE `key` = :key', ['key' => $appKey]) 80 | ->then(function (Result $result) use ($deferred) { 81 | $deferred->resolve($this->convertIntoApp($result->rows[0])); 82 | }, function ($error) use ($deferred) { 83 | $deferred->reject($error); 84 | }); 85 | 86 | return $deferred->promise(); 87 | } 88 | 89 | /** 90 | * Get app by secret. 91 | * 92 | * @param string $appSecret 93 | * @return PromiseInterface 94 | */ 95 | public function findBySecret($appSecret): PromiseInterface 96 | { 97 | $deferred = new Deferred(); 98 | 99 | $this->database->query('SELECT * from apps WHERE `secret` = :secret', ['secret' => $appSecret]) 100 | ->then(function (Result $result) use ($deferred) { 101 | $deferred->resolve($this->convertIntoApp($result->rows[0])); 102 | }, function ($error) use ($deferred) { 103 | $deferred->reject($error); 104 | }); 105 | 106 | return $deferred->promise(); 107 | } 108 | 109 | /** 110 | * Map the app into an App instance. 111 | * 112 | * @param array|null $app 113 | * @return \BeyondCode\LaravelWebSockets\Apps\App|null 114 | */ 115 | protected function convertIntoApp(?array $appAttributes): ?App 116 | { 117 | if (! $appAttributes) { 118 | return null; 119 | } 120 | 121 | $app = new App( 122 | $appAttributes['id'], 123 | $appAttributes['key'], 124 | $appAttributes['secret'] 125 | ); 126 | 127 | if (isset($appAttributes['name'])) { 128 | $app->setName($appAttributes['name']); 129 | } 130 | 131 | if (isset($appAttributes['host'])) { 132 | $app->setHost($appAttributes['host']); 133 | } 134 | 135 | if (isset($appAttributes['path'])) { 136 | $app->setPath($appAttributes['path']); 137 | } 138 | 139 | $app 140 | ->enableClientMessages((bool) $appAttributes['enable_client_messages']) 141 | ->enableStatistics((bool) $appAttributes['enable_statistics']) 142 | ->setCapacity($appAttributes['capacity'] ?? null) 143 | ->setAllowedOrigins(array_filter(explode(',', $appAttributes['allowed_origins']))); 144 | 145 | return $app; 146 | } 147 | 148 | /** 149 | * @inheritDoc 150 | */ 151 | public function createApp($appData): PromiseInterface 152 | { 153 | $deferred = new Deferred(); 154 | 155 | $this->database->query(' 156 | INSERT INTO apps (id, key, secret, name, host, path, enable_client_messages, enable_statistics, capacity, allowed_origins) 157 | VALUES (:id, :key, :secret, :name, :host, :path, :enable_client_messages, :enable_statistics, :capacity, :allowed_origins) 158 | ', $appData) 159 | ->then(function (Result $result) use ($deferred) { 160 | $deferred->resolve(); 161 | }, function ($error) use ($deferred) { 162 | $deferred->reject($error); 163 | }); 164 | 165 | return $deferred->promise(); 166 | } 167 | } 168 | -------------------------------------------------------------------------------- /src/Cache/ArrayLock.php: -------------------------------------------------------------------------------- 1 | lock = new LaravelLock($store, $name, $seconds, $owner); 39 | } 40 | 41 | public function acquire(): PromiseInterface 42 | { 43 | return Helpers::createFulfilledPromise($this->lock->acquire()); 44 | } 45 | 46 | public function get($callback = null): PromiseInterface 47 | { 48 | return $this->lock->get($callback); 49 | } 50 | 51 | public function release(): PromiseInterface 52 | { 53 | return Helpers::createFulfilledPromise($this->lock->release()); 54 | } 55 | } 56 | -------------------------------------------------------------------------------- /src/Cache/Lock.php: -------------------------------------------------------------------------------- 1 | name = $name; 37 | $this->seconds = $seconds; 38 | $this->owner = $owner; 39 | } 40 | 41 | abstract public function acquire(): PromiseInterface; 42 | 43 | abstract public function get($callback = null): PromiseInterface; 44 | 45 | abstract public function release(): PromiseInterface; 46 | } 47 | -------------------------------------------------------------------------------- /src/Cache/RedisLock.php: -------------------------------------------------------------------------------- 1 | redis = $redis; 24 | } 25 | 26 | public function acquire(): PromiseInterface 27 | { 28 | $promise = new Deferred(); 29 | 30 | if ($this->seconds > 0) { 31 | $this->redis 32 | ->set($this->name, $this->owner, 'EX', $this->seconds, 'NX') 33 | ->then(function ($result) use ($promise) { 34 | $promise->resolve($result === true); 35 | }); 36 | } else { 37 | $this->redis 38 | ->setnx($this->name, $this->owner) 39 | ->then(function ($result) use ($promise) { 40 | $promise->resolve($result === 1); 41 | }); 42 | } 43 | 44 | return $promise->promise(); 45 | } 46 | 47 | public function get($callback = null): PromiseInterface 48 | { 49 | $promise = new Deferred(); 50 | 51 | $this->acquire() 52 | ->then(function ($result) use ($callback, $promise) { 53 | if ($result) { 54 | try { 55 | $callback(); 56 | } finally { 57 | $promise->resolve($this->release()); 58 | } 59 | } 60 | }); 61 | 62 | return $promise->promise(); 63 | } 64 | 65 | public function release(): PromiseInterface 66 | { 67 | return $this->redis->eval(LuaScripts::releaseLock(), 1, $this->name, $this->owner); 68 | } 69 | } 70 | -------------------------------------------------------------------------------- /src/Channels/Channel.php: -------------------------------------------------------------------------------- 1 | name = $name; 41 | $this->channelManager = app(ChannelManager::class); 42 | } 43 | 44 | /** 45 | * Get channel name. 46 | * 47 | * @return string 48 | */ 49 | public function getName() 50 | { 51 | return $this->name; 52 | } 53 | 54 | /** 55 | * Get the list of subscribed connections. 56 | * 57 | * @return array 58 | */ 59 | public function getConnections() 60 | { 61 | return $this->connections; 62 | } 63 | 64 | /** 65 | * Get connection by socketId. 66 | * 67 | * @param string socketId 68 | * @return ?ConnectionInterface 69 | */ 70 | public function getConnection(string $socketId): ?ConnectionInterface 71 | { 72 | return $this->connections[$socketId] ?? null; 73 | } 74 | 75 | /** 76 | * Check if the channel has connections. 77 | * 78 | * @return bool 79 | */ 80 | public function hasConnections(): bool 81 | { 82 | return count($this->getConnections()) > 0; 83 | } 84 | 85 | /** 86 | * Add a new connection to the channel. 87 | * 88 | * @see https://pusher.com/docs/pusher_protocol#presence-channel-events 89 | * 90 | * @param \Ratchet\ConnectionInterface $connection 91 | * @param \stdClass $payload 92 | * @return bool 93 | */ 94 | public function subscribe(ConnectionInterface $connection, stdClass $payload): bool 95 | { 96 | $this->saveConnection($connection); 97 | 98 | $connection->send(json_encode([ 99 | 'event' => 'pusher_internal:subscription_succeeded', 100 | 'channel' => $this->getName(), 101 | ])); 102 | 103 | DashboardLogger::log($connection->app->id, DashboardLogger::TYPE_SUBSCRIBED, [ 104 | 'socketId' => $connection->socketId, 105 | 'channel' => $this->getName(), 106 | ]); 107 | 108 | SubscribedToChannel::dispatch( 109 | $connection->app->id, 110 | $connection->socketId, 111 | $this->getName(), 112 | ); 113 | 114 | return true; 115 | } 116 | 117 | /** 118 | * Unsubscribe connection from the channel. 119 | * 120 | * @param \Ratchet\ConnectionInterface $connection 121 | * @return PromiseInterface 122 | */ 123 | public function unsubscribe(ConnectionInterface $connection): PromiseInterface 124 | { 125 | if (! $this->hasConnection($connection)) { 126 | return Helpers::createFulfilledPromise(false); 127 | } 128 | 129 | unset($this->connections[$connection->socketId]); 130 | 131 | UnsubscribedFromChannel::dispatch( 132 | $connection->app->id, 133 | $connection->socketId, 134 | $this->getName() 135 | ); 136 | 137 | return Helpers::createFulfilledPromise(true); 138 | } 139 | 140 | /** 141 | * Check if the given connection exists. 142 | * 143 | * @param \Ratchet\ConnectionInterface $connection 144 | * @return bool 145 | */ 146 | public function hasConnection(ConnectionInterface $connection): bool 147 | { 148 | return isset($this->connections[$connection->socketId]); 149 | } 150 | 151 | /** 152 | * Store the connection to the subscribers list. 153 | * 154 | * @param \Ratchet\ConnectionInterface $connection 155 | * @return void 156 | */ 157 | public function saveConnection(ConnectionInterface $connection) 158 | { 159 | $this->connections[$connection->socketId] = $connection; 160 | } 161 | 162 | /** 163 | * Broadcast a payload to the subscribed connections. 164 | * 165 | * @param string|int $appId 166 | * @param \stdClass $payload 167 | * @param bool $replicate 168 | * @return bool 169 | */ 170 | public function broadcast($appId, stdClass $payload, bool $replicate = true): bool 171 | { 172 | collect($this->getConnections()) 173 | ->each(function ($connection) use ($payload) { 174 | $connection->send(json_encode($payload)); 175 | $this->channelManager->connectionPonged($connection); 176 | }); 177 | 178 | if ($replicate) { 179 | $this->channelManager->broadcastAcrossServers($appId, null, $this->getName(), $payload); 180 | } 181 | 182 | return true; 183 | } 184 | 185 | /** 186 | * Broadcast a payload to the locally-subscribed connections. 187 | * 188 | * @param string|int $appId 189 | * @param \stdClass $payload 190 | * @return bool 191 | */ 192 | public function broadcastLocally($appId, stdClass $payload): bool 193 | { 194 | return $this->broadcast($appId, $payload, false); 195 | } 196 | 197 | /** 198 | * Broadcast the payload, but exclude a specific socket id. 199 | * 200 | * @param \stdClass $payload 201 | * @param string|null $socketId 202 | * @param string|int $appId 203 | * @param bool $replicate 204 | * @return bool 205 | */ 206 | public function broadcastToEveryoneExcept(stdClass $payload, ?string $socketId, $appId, bool $replicate = true) 207 | { 208 | if ($replicate) { 209 | $this->channelManager->broadcastAcrossServers($appId, $socketId, $this->getName(), $payload); 210 | } 211 | 212 | if (is_null($socketId)) { 213 | return $this->broadcast($appId, $payload, false); 214 | } 215 | 216 | collect($this->getConnections())->each(function (ConnectionInterface $connection) use ($socketId, $payload) { 217 | if ($connection->socketId !== $socketId) { 218 | $connection->send(json_encode($payload)); 219 | $this->channelManager->connectionPonged($connection); 220 | } 221 | }); 222 | 223 | return true; 224 | } 225 | 226 | /** 227 | * Broadcast the payload, but exclude a specific socket id. 228 | * 229 | * @param \stdClass $payload 230 | * @param string|null $socketId 231 | * @param string|int $appId 232 | * @return bool 233 | */ 234 | public function broadcastLocallyToEveryoneExcept(stdClass $payload, ?string $socketId, $appId) 235 | { 236 | return $this->broadcastToEveryoneExcept( 237 | $payload, $socketId, $appId, false 238 | ); 239 | } 240 | 241 | /** 242 | * Check if the signature for the payload is valid. 243 | * 244 | * @param \Ratchet\ConnectionInterface $connection 245 | * @param \stdClass $payload 246 | * @return void 247 | * 248 | * @throws InvalidSignature 249 | */ 250 | protected function verifySignature(ConnectionInterface $connection, stdClass $payload) 251 | { 252 | $signature = "{$connection->socketId}:{$this->getName()}"; 253 | 254 | if (isset($payload->channel_data)) { 255 | $signature .= ":{$payload->channel_data}"; 256 | } 257 | 258 | if (! hash_equals( 259 | hash_hmac('sha256', $signature, $connection->app->secret), 260 | Str::after($payload->auth, ':')) 261 | ) { 262 | throw new InvalidSignature; 263 | } 264 | } 265 | } 266 | -------------------------------------------------------------------------------- /src/Channels/PresenceChannel.php: -------------------------------------------------------------------------------- 1 | verifySignature($connection, $payload); 30 | 31 | $this->saveConnection($connection); 32 | 33 | $user = json_decode($payload->channel_data); 34 | 35 | $this->channelManager 36 | ->userJoinedPresenceChannel($connection, $user, $this->getName(), $payload) 37 | ->then(function () use ($connection) { 38 | $this->channelManager 39 | ->getChannelMembers($connection->app->id, $this->getName()) 40 | ->then(function ($users) use ($connection) { 41 | $hash = []; 42 | 43 | foreach ($users as $socketId => $user) { 44 | $hash[$user->user_id] = $user->user_info ?? []; 45 | } 46 | 47 | $connection->send(json_encode([ 48 | 'event' => 'pusher_internal:subscription_succeeded', 49 | 'channel' => $this->getName(), 50 | 'data' => json_encode([ 51 | 'presence' => [ 52 | 'ids' => collect($users)->map(function ($user) { 53 | return (string) $user->user_id; 54 | })->values(), 55 | 'hash' => $hash, 56 | 'count' => count($users), 57 | ], 58 | ]), 59 | ])); 60 | }); 61 | }) 62 | ->then(function () use ($connection, $user, $payload) { 63 | // The `pusher_internal:member_added` event is triggered when a user joins a channel. 64 | // It's quite possible that a user can have multiple connections to the same channel 65 | // (for example by having multiple browser tabs open) 66 | // and in this case the events will only be triggered when the first tab is opened. 67 | $this->channelManager 68 | ->getMemberSockets($user->user_id, $connection->app->id, $this->getName()) 69 | ->then(function ($sockets) use ($payload, $connection, $user) { 70 | if (count($sockets) === 1) { 71 | $memberAddedPayload = [ 72 | 'event' => 'pusher_internal:member_added', 73 | 'channel' => $this->getName(), 74 | 'data' => $payload->channel_data, 75 | ]; 76 | 77 | $this->broadcastToEveryoneExcept( 78 | (object) $memberAddedPayload, $connection->socketId, 79 | $connection->app->id 80 | ); 81 | 82 | SubscribedToChannel::dispatch( 83 | $connection->app->id, 84 | $connection->socketId, 85 | $this->getName(), 86 | $user 87 | ); 88 | } 89 | 90 | DashboardLogger::log($connection->app->id, DashboardLogger::TYPE_SUBSCRIBED, [ 91 | 'socketId' => $connection->socketId, 92 | 'channel' => $this->getName(), 93 | 'duplicate-connection' => count($sockets) > 1, 94 | ]); 95 | }); 96 | }); 97 | 98 | return true; 99 | } 100 | 101 | /** 102 | * Unsubscribe connection from the channel. 103 | * 104 | * @param \Ratchet\ConnectionInterface $connection 105 | * @return PromiseInterface 106 | */ 107 | public function unsubscribe(ConnectionInterface $connection): PromiseInterface 108 | { 109 | $truth = parent::unsubscribe($connection); 110 | 111 | return $this->channelManager 112 | ->getChannelMember($connection, $this->getName()) 113 | ->then(function ($user) { 114 | return @json_decode($user); 115 | }) 116 | ->then(function ($user) use ($connection) { 117 | if (! $user) { 118 | return Helpers::createFulfilledPromise(true); 119 | } 120 | 121 | return $this->channelManager 122 | ->userLeftPresenceChannel($connection, $user, $this->getName()) 123 | ->then(function () use ($connection, $user) { 124 | // The `pusher_internal:member_removed` is triggered when a user leaves a channel. 125 | // It's quite possible that a user can have multiple connections to the same channel 126 | // (for example by having multiple browser tabs open) 127 | // and in this case the events will only be triggered when the last one is closed. 128 | return $this->channelManager 129 | ->getMemberSockets($user->user_id, $connection->app->id, $this->getName()) 130 | ->then(function ($sockets) use ($connection, $user) { 131 | if (count($sockets) === 0) { 132 | $memberRemovedPayload = [ 133 | 'event' => 'pusher_internal:member_removed', 134 | 'channel' => $this->getName(), 135 | 'data' => json_encode([ 136 | 'user_id' => $user->user_id, 137 | ]), 138 | ]; 139 | 140 | $this->broadcastToEveryoneExcept( 141 | (object) $memberRemovedPayload, $connection->socketId, 142 | $connection->app->id 143 | ); 144 | 145 | UnsubscribedFromChannel::dispatch( 146 | $connection->app->id, 147 | $connection->socketId, 148 | $this->getName(), 149 | $user 150 | ); 151 | } 152 | }); 153 | }); 154 | }) 155 | ->then(function () use ($truth) { 156 | return $truth; 157 | }); 158 | } 159 | } 160 | -------------------------------------------------------------------------------- /src/Channels/PrivateChannel.php: -------------------------------------------------------------------------------- 1 | verifySignature($connection, $payload); 25 | 26 | return parent::subscribe($connection, $payload); 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /src/Concerns/PushesToPusher.php: -------------------------------------------------------------------------------- 1 | comment('Cleaning WebSocket Statistics...'); 35 | 36 | $days = $this->option('days') ?: config('statistics.delete_statistics_older_than_days'); 37 | 38 | $amountDeleted = StatisticsStore::delete( 39 | now()->subDays($days), $this->argument('appId') 40 | ); 41 | 42 | $this->info("Deleted {$amountDeleted} record(s) from the WebSocket statistics storage."); 43 | } 44 | } 45 | -------------------------------------------------------------------------------- /src/Console/Commands/FlushCollectedStatistics.php: -------------------------------------------------------------------------------- 1 | comment('Flushing the collected WebSocket Statistics...'); 32 | 33 | StatisticsCollector::flush(); 34 | 35 | $this->line('Flush complete!'); 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /src/Console/Commands/RestartServer.php: -------------------------------------------------------------------------------- 1 | currentTime() 37 | ); 38 | 39 | $this->info( 40 | 'Broadcasted the restart signal to the WebSocket server!' 41 | ); 42 | } 43 | } 44 | -------------------------------------------------------------------------------- /src/Contracts/AppManager.php: -------------------------------------------------------------------------------- 1 | header('X-App-Id')), app(LoopInterface::class)); 28 | 29 | $broadcaster = $this->getPusherBroadcaster([ 30 | 'key' => $app->key, 31 | 'secret' => $app->secret, 32 | 'id' => $app->id, 33 | ]); 34 | 35 | /* 36 | * Since the dashboard itself is already secured by the 37 | * Authorize middleware, we can trust all channel 38 | * authentication requests in here. 39 | */ 40 | return $broadcaster->validAuthenticationResponse($request, []); 41 | } 42 | } 43 | -------------------------------------------------------------------------------- /src/Dashboard/Http/Controllers/SendMessage.php: -------------------------------------------------------------------------------- 1 | validate([ 23 | 'appId' => ['required', new AppId], 24 | 'key' => 'required|string', 25 | 'secret' => 'required|string', 26 | 'event' => 'required|string', 27 | 'channel' => 'required|string', 28 | 'data' => 'required|json', 29 | ]); 30 | 31 | $broadcaster = $this->getPusherBroadcaster([ 32 | 'key' => $request->key, 33 | 'secret' => $request->secret, 34 | 'id' => $request->appId, 35 | ]); 36 | 37 | try { 38 | $decodedData = json_decode($request->data, true); 39 | 40 | $broadcaster->broadcast( 41 | [$request->channel], 42 | $request->event, 43 | $decodedData ?: [] 44 | ); 45 | } catch (Throwable $e) { 46 | return response()->json([ 47 | 'ok' => false, 48 | 'exception' => $e->getMessage(), 49 | ]); 50 | } 51 | 52 | return response()->json([ 53 | 'ok' => true, 54 | ]); 55 | } 56 | } 57 | -------------------------------------------------------------------------------- /src/Dashboard/Http/Controllers/ShowApps.php: -------------------------------------------------------------------------------- 1 | await($apps->all(), app(LoopInterface::class), 2.0), 24 | 'port' => config('websockets.dashboard.port', 6001), 25 | ]); 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /src/Dashboard/Http/Controllers/ShowDashboard.php: -------------------------------------------------------------------------------- 1 | await($apps->all(), app(LoopInterface::class), 2.0), 25 | 'port' => config('websockets.dashboard.port', 6001), 26 | 'channels' => DashboardLogger::$channels, 27 | 'logPrefix' => DashboardLogger::LOG_CHANNEL_PREFIX, 28 | 'refreshInterval' => config('websockets.statistics.interval_in_seconds'), 29 | ]); 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /src/Dashboard/Http/Controllers/ShowStatistics.php: -------------------------------------------------------------------------------- 1 | whereAppId($appId) 21 | ->latest() 22 | ->limit(120); 23 | }; 24 | 25 | $processCollection = function ($collection) { 26 | return $collection->reverse(); 27 | }; 28 | 29 | return StatisticsStore::getForGraph( 30 | $processQuery, $processCollection 31 | ); 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /src/Dashboard/Http/Controllers/StoreApp.php: -------------------------------------------------------------------------------- 1 | (string) Str::uuid(), 25 | 'key' => (string) Str::uuid(), 26 | 'secret' => (string) Str::uuid(), 27 | 'name' => $request->get('name'), 28 | 'enable_client_messages' => $request->has('enable_client_messages'), 29 | 'enable_statistics' => $request->has('enable_statistics'), 30 | 'allowed_origins' => $request->get('allowed_origins'), 31 | ]; 32 | 33 | await($apps->createApp($appData), app(LoopInterface::class)); 34 | 35 | return redirect()->route('laravel-websockets.apps'); 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /src/Dashboard/Http/Middleware/Authorize.php: -------------------------------------------------------------------------------- 1 | user()]) 19 | ? $next($request) 20 | : abort(403); 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /src/Dashboard/Http/Requests/StoreAppRequest.php: -------------------------------------------------------------------------------- 1 | 'required', 18 | ]; 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /src/DashboardLogger.php: -------------------------------------------------------------------------------- 1 | 'log-message', 59 | 'channel' => $channelName, 60 | 'data' => [ 61 | 'type' => $type, 62 | 'time' => strftime('%H:%M:%S'), 63 | 'details' => $details, 64 | ], 65 | ]; 66 | 67 | // Here you can use the ->find(), even if the channel 68 | // does not exist on the server. If it does not exist, 69 | // then the message simply will get broadcasted 70 | // across the other servers. 71 | $channel = $channelManager->find($appId, $channelName); 72 | 73 | if ($channel) { 74 | $channel->broadcastLocally( 75 | $appId, (object) $payload 76 | ); 77 | } 78 | 79 | $channelManager->broadcastAcrossServers( 80 | $appId, null, $channelName, (object) $payload 81 | ); 82 | } 83 | } 84 | -------------------------------------------------------------------------------- /src/Events/ConnectionClosed.php: -------------------------------------------------------------------------------- 1 | appId = $appId; 36 | $this->socketId = $socketId; 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /src/Events/ConnectionPonged.php: -------------------------------------------------------------------------------- 1 | appId = $appId; 36 | $this->socketId = $socketId; 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /src/Events/NewConnection.php: -------------------------------------------------------------------------------- 1 | appId = $appId; 36 | $this->socketId = $socketId; 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /src/Events/SubscribedToChannel.php: -------------------------------------------------------------------------------- 1 | appId = $appId; 53 | $this->socketId = $socketId; 54 | $this->channelName = $channelName; 55 | $this->user = $user; 56 | } 57 | } 58 | -------------------------------------------------------------------------------- /src/Events/UnsubscribedFromChannel.php: -------------------------------------------------------------------------------- 1 | appId = $appId; 53 | $this->socketId = $socketId; 54 | $this->channelName = $channelName; 55 | $this->user = $user; 56 | } 57 | } 58 | -------------------------------------------------------------------------------- /src/Events/WebSocketMessageReceived.php: -------------------------------------------------------------------------------- 1 | appId = $appId; 52 | $this->socketId = $socketId; 53 | $this->message = $message; 54 | $this->decodedMessage = json_decode($message->getPayload(), true); 55 | } 56 | } 57 | -------------------------------------------------------------------------------- /src/Facades/StatisticsCollector.php: -------------------------------------------------------------------------------- 1 | value array. 29 | [$keys, $values] = collect($list)->partition(function ($value, $key) { 30 | return $key % 2 === 0; 31 | }); 32 | 33 | return array_combine($keys->all(), $values->all()); 34 | } 35 | 36 | /** 37 | * Create a new fulfilled promise with a value. 38 | * 39 | * @param mixed $value 40 | * @return \React\Promise\PromiseInterface 41 | */ 42 | public static function createFulfilledPromise($value): PromiseInterface 43 | { 44 | $resolver = config( 45 | 'websockets.promise_resolver', \React\Promise\FulfilledPromise::class 46 | ); 47 | 48 | return new $resolver($value, static::$loop); 49 | } 50 | } 51 | -------------------------------------------------------------------------------- /src/Models/WebSocketsStatisticsEntry.php: -------------------------------------------------------------------------------- 1 | redis, $config['queue'], 19 | $config['connection'] ?? $this->connection, 20 | $config['retry_after'] ?? 60, 21 | $config['block_for'] ?? null 22 | ); 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /src/Queue/AsyncRedisQueue.php: -------------------------------------------------------------------------------- 1 | container->bound(ChannelManager::class) 18 | ? $this->container->make(ChannelManager::class) 19 | : null; 20 | 21 | return $channelManager && method_exists($channelManager, 'getRedisClient') 22 | ? $channelManager->getRedisClient() 23 | : parent::getConnection(); 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /src/Rules/AppId.php: -------------------------------------------------------------------------------- 1 | findById($value), Factory::create()) ? true : false; 25 | } 26 | 27 | /** 28 | * The validation message. 29 | * 30 | * @return string 31 | */ 32 | public function message() 33 | { 34 | return 'There is no app registered with the given id. Make sure the websockets config file contains an app for this id or that your custom AppManager returns an app for this id.'; 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /src/Server/Exceptions/ConnectionsOverCapacity.php: -------------------------------------------------------------------------------- 1 | trigger('Over capacity', 4100); 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /src/Server/Exceptions/InvalidSignature.php: -------------------------------------------------------------------------------- 1 | trigger('Invalid Signature', 4009); 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /src/Server/Exceptions/OriginNotAllowed.php: -------------------------------------------------------------------------------- 1 | trigger("The origin is not allowed for `{$appKey}`.", 4009); 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /src/Server/Exceptions/UnknownAppKey.php: -------------------------------------------------------------------------------- 1 | trigger("Could not find app key `{$appKey}`.", 4001); 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /src/Server/Exceptions/WebSocketException.php: -------------------------------------------------------------------------------- 1 | 'pusher:error', 18 | 'data' => [ 19 | 'message' => $this->getMessage(), 20 | 'code' => $this->getCode(), 21 | ], 22 | ]; 23 | } 24 | 25 | /** 26 | * Trigger the exception message. 27 | * 28 | * @param string $message 29 | * @param int $code 30 | * @return void 31 | */ 32 | public function trigger(string $message, int $code = 4001) 33 | { 34 | $this->message = $message; 35 | $this->code = $code; 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /src/Server/HealthHandler.php: -------------------------------------------------------------------------------- 1 | 'application/json'], 26 | json_encode(['ok' => true]) 27 | ); 28 | 29 | tap($connection)->send(Message::toString($response))->close(); 30 | } 31 | 32 | /** 33 | * Handle the incoming message. 34 | * 35 | * @param \Ratchet\ConnectionInterface $connection 36 | * @param string $message 37 | * @return void 38 | */ 39 | public function onMessage(ConnectionInterface $connection, $message) 40 | { 41 | // 42 | } 43 | 44 | /** 45 | * Handle the websocket close. 46 | * 47 | * @param \Ratchet\ConnectionInterface $connection 48 | * @return void 49 | */ 50 | public function onClose(ConnectionInterface $connection) 51 | { 52 | // 53 | } 54 | 55 | /** 56 | * Handle the websocket errors. 57 | * 58 | * @param \Ratchet\ConnectionInterface $connection 59 | * @param WebSocketException $exception 60 | * @return void 61 | */ 62 | public function onError(ConnectionInterface $connection, Exception $exception) 63 | { 64 | // 65 | } 66 | } 67 | -------------------------------------------------------------------------------- /src/Server/HttpServer.php: -------------------------------------------------------------------------------- 1 | _reqParser->maxSize = $maxRequestSize; 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /src/Server/Loggers/ConnectionLogger.php: -------------------------------------------------------------------------------- 1 | setConnection($app); 27 | } 28 | 29 | /** 30 | * Set a new connection to watch. 31 | * 32 | * @param \Ratchet\ConnectionInterface $connection 33 | * @return $this 34 | */ 35 | public function setConnection(ConnectionInterface $connection) 36 | { 37 | $this->connection = $connection; 38 | 39 | return $this; 40 | } 41 | 42 | /** 43 | * Send data through the connection. 44 | * 45 | * @param string $data 46 | * @return void 47 | */ 48 | public function send($data) 49 | { 50 | $socketId = $this->connection->socketId ?? null; 51 | $appId = $this->connection->app->id ?? null; 52 | 53 | $this->info("[{$appId}][{$socketId}] Sending message ".($this->verbose ? $data : '')); 54 | 55 | $this->connection->send($data); 56 | } 57 | 58 | /** 59 | * Close the connection. 60 | * 61 | * @return void 62 | */ 63 | public function close() 64 | { 65 | $socketId = $this->connection->socketId ?? null; 66 | $appId = $this->connection->app->id ?? null; 67 | 68 | $this->warn("[{$appId}][{$socketId}] Closing connection"); 69 | 70 | $this->connection->close(); 71 | } 72 | 73 | /** 74 | * {@inheritdoc} 75 | */ 76 | public function __set($name, $value) 77 | { 78 | return $this->connection->$name = $value; 79 | } 80 | 81 | /** 82 | * {@inheritdoc} 83 | */ 84 | public function __get($name) 85 | { 86 | return $this->connection->$name; 87 | } 88 | 89 | /** 90 | * {@inheritdoc} 91 | */ 92 | public function __isset($name) 93 | { 94 | return isset($this->connection->$name); 95 | } 96 | 97 | /** 98 | * {@inheritdoc} 99 | */ 100 | public function __unset($name) 101 | { 102 | unset($this->connection->$name); 103 | } 104 | } 105 | -------------------------------------------------------------------------------- /src/Server/Loggers/HttpLogger.php: -------------------------------------------------------------------------------- 1 | setApp($app); 29 | } 30 | 31 | /** 32 | * Set a new app to watch. 33 | * 34 | * @param \Ratchet\MessageComponentInterface $app 35 | * @return $this 36 | */ 37 | public function setApp(MessageComponentInterface $app) 38 | { 39 | $this->app = $app; 40 | 41 | return $this; 42 | } 43 | 44 | /** 45 | * Handle the HTTP open request. 46 | * 47 | * @param \Ratchet\ConnectionInterface $connection 48 | * @return void 49 | */ 50 | public function onOpen(ConnectionInterface $connection) 51 | { 52 | $this->app->onOpen($connection); 53 | } 54 | 55 | /** 56 | * Handle the HTTP message request. 57 | * 58 | * @param \Ratchet\ConnectionInterface $connection 59 | * @param mixed $message 60 | * @return void 61 | */ 62 | public function onMessage(ConnectionInterface $connection, $message) 63 | { 64 | $this->app->onMessage($connection, $message); 65 | } 66 | 67 | /** 68 | * Handle the HTTP close request. 69 | * 70 | * @param \Ratchet\ConnectionInterface $connection 71 | * @return void 72 | */ 73 | public function onClose(ConnectionInterface $connection) 74 | { 75 | $this->app->onClose($connection); 76 | } 77 | 78 | /** 79 | * Handle HTTP errors. 80 | * 81 | * @param \Ratchet\ConnectionInterface $connection 82 | * @param Exception $exception 83 | * @return void 84 | */ 85 | public function onError(ConnectionInterface $connection, Exception $exception) 86 | { 87 | $exceptionClass = get_class($exception); 88 | 89 | $message = "Exception `{$exceptionClass}` thrown: `{$exception->getMessage()}`"; 90 | 91 | if ($this->verbose) { 92 | $message .= $exception->getTraceAsString(); 93 | } 94 | 95 | $this->error($message); 96 | 97 | $this->app->onError($connection, $exception); 98 | } 99 | } 100 | -------------------------------------------------------------------------------- /src/Server/Loggers/Logger.php: -------------------------------------------------------------------------------- 1 | enabled; 41 | } 42 | 43 | /** 44 | * Create a new Logger instance. 45 | * 46 | * @param \Symfony\Component\Console\Output\OutputInterface $consoleOutput 47 | * @return void 48 | */ 49 | public function __construct(OutputInterface $consoleOutput) 50 | { 51 | $this->consoleOutput = $consoleOutput; 52 | } 53 | 54 | /** 55 | * Enable the logger. 56 | * 57 | * @param bool $enabled 58 | * @return $this 59 | */ 60 | public function enable($enabled = true) 61 | { 62 | $this->enabled = $enabled; 63 | 64 | return $this; 65 | } 66 | 67 | /** 68 | * Enable the verbose mode. 69 | * 70 | * @param bool $verbose 71 | * @return $this 72 | */ 73 | public function verbose($verbose = false) 74 | { 75 | $this->verbose = $verbose; 76 | 77 | return $this; 78 | } 79 | 80 | /** 81 | * Trigger an Info message. 82 | * 83 | * @param string $message 84 | * @return void 85 | */ 86 | protected function info(string $message) 87 | { 88 | $this->line($message, 'info'); 89 | } 90 | 91 | /** 92 | * Trigger a Warning message. 93 | * 94 | * @param string $message 95 | * @return void 96 | */ 97 | protected function warn(string $message) 98 | { 99 | if (! $this->consoleOutput->getFormatter()->hasStyle('warning')) { 100 | $style = new OutputFormatterStyle('yellow'); 101 | 102 | $this->consoleOutput->getFormatter()->setStyle('warning', $style); 103 | } 104 | 105 | $this->line($message, 'warning'); 106 | } 107 | 108 | /** 109 | * Trigger an Error message. 110 | * 111 | * @param string $message 112 | * @return void 113 | */ 114 | protected function error(string $message) 115 | { 116 | $this->line($message, 'error'); 117 | } 118 | 119 | protected function line(string $message, string $style) 120 | { 121 | $this->consoleOutput->writeln( 122 | $style ? "<{$style}>{$message}" : $message 123 | ); 124 | } 125 | } 126 | -------------------------------------------------------------------------------- /src/Server/Loggers/WebSocketsLogger.php: -------------------------------------------------------------------------------- 1 | setApp($app); 31 | } 32 | 33 | /** 34 | * Set a new app to watch. 35 | * 36 | * @param \Ratchet\MessageComponentInterface $app 37 | * @return $this 38 | */ 39 | public function setApp(MessageComponentInterface $app) 40 | { 41 | $this->app = $app; 42 | 43 | return $this; 44 | } 45 | 46 | /** 47 | * Handle the HTTP open request. 48 | * 49 | * @param \Ratchet\ConnectionInterface $connection 50 | * @return void 51 | */ 52 | public function onOpen(ConnectionInterface $connection) 53 | { 54 | $appKey = QueryParameters::create($connection->httpRequest)->get('appKey'); 55 | 56 | $this->info("[$appKey] New connection opened."); 57 | 58 | $this->app->onOpen(ConnectionLogger::decorate($connection)); 59 | } 60 | 61 | /** 62 | * Handle the HTTP message request. 63 | * 64 | * @param \Ratchet\ConnectionInterface $connection 65 | * @param \Ratchet\RFC6455\Messaging\MessageInterface $message 66 | * @return void 67 | */ 68 | public function onMessage(ConnectionInterface $connection, MessageInterface $message) 69 | { 70 | $this->info("[{$connection->app->id}][{$connection->socketId}] Received message ".($this->verbose ? $message->getPayload() : '')); 71 | 72 | $this->app->onMessage(ConnectionLogger::decorate($connection), $message); 73 | } 74 | 75 | /** 76 | * Handle the HTTP close request. 77 | * 78 | * @param \Ratchet\ConnectionInterface $connection 79 | * @return void 80 | */ 81 | public function onClose(ConnectionInterface $connection) 82 | { 83 | $socketId = $connection->socketId ?? null; 84 | $appId = $connection->app->id ?? null; 85 | 86 | $this->warn("[{$appId}][{$socketId}] Connection closed"); 87 | 88 | $this->app->onClose(ConnectionLogger::decorate($connection)); 89 | } 90 | 91 | /** 92 | * Handle HTTP errors. 93 | * 94 | * @param \Ratchet\ConnectionInterface $connection 95 | * @param Exception $exception 96 | * @return void 97 | */ 98 | public function onError(ConnectionInterface $connection, Exception $exception) 99 | { 100 | $exceptionClass = get_class($exception); 101 | 102 | $appId = $connection->app->id ?? 'Unknown app id'; 103 | 104 | $message = "[{$appId}] Exception `{$exceptionClass}` thrown: `{$exception->getMessage()}`"; 105 | 106 | if ($this->verbose) { 107 | $message .= $exception->getTraceAsString(); 108 | } 109 | 110 | $this->error($message); 111 | 112 | $this->app->onError(ConnectionLogger::decorate($connection), $exception); 113 | } 114 | } 115 | -------------------------------------------------------------------------------- /src/Server/Messages/PusherChannelProtocolMessage.php: -------------------------------------------------------------------------------- 1 | payload->event, ':')); 20 | 21 | if (method_exists($this, $eventName) && $eventName !== 'respond') { 22 | call_user_func([$this, $eventName], $this->connection, $this->payload->data ?? new stdClass()); 23 | } 24 | } 25 | 26 | /** 27 | * Ping the connection. 28 | * 29 | * @see https://pusher.com/docs/pusher_protocol#ping-pong 30 | * 31 | * @param \Ratchet\ConnectionInterface $connection 32 | * @return void 33 | */ 34 | protected function ping(ConnectionInterface $connection) 35 | { 36 | $this->channelManager 37 | ->connectionPonged($connection) 38 | ->then(function () use ($connection) { 39 | $connection->send(json_encode(['event' => 'pusher:pong'])); 40 | 41 | ConnectionPonged::dispatch($connection->app->id, $connection->socketId); 42 | }); 43 | } 44 | 45 | /** 46 | * Subscribe to channel. 47 | * 48 | * @see https://pusher.com/docs/pusher_protocol#pusher-subscribe 49 | * 50 | * @param \Ratchet\ConnectionInterface $connection 51 | * @param \stdClass $payload 52 | * @return void 53 | */ 54 | protected function subscribe(ConnectionInterface $connection, stdClass $payload) 55 | { 56 | $this->channelManager->subscribeToChannel($connection, $payload->channel, $payload); 57 | } 58 | 59 | /** 60 | * Unsubscribe from the channel. 61 | * 62 | * @param \Ratchet\ConnectionInterface $connection 63 | * @param \stdClass $payload 64 | * @return void 65 | */ 66 | public function unsubscribe(ConnectionInterface $connection, stdClass $payload) 67 | { 68 | $this->channelManager->unsubscribeFromChannel($connection, $payload->channel, $payload); 69 | } 70 | } 71 | -------------------------------------------------------------------------------- /src/Server/Messages/PusherClientMessage.php: -------------------------------------------------------------------------------- 1 | payload = $payload; 45 | $this->connection = $connection; 46 | $this->channelManager = $channelManager; 47 | } 48 | 49 | /** 50 | * Respond to the message construction. 51 | * 52 | * @return void 53 | */ 54 | public function respond() 55 | { 56 | if (! Str::startsWith($this->payload->event, 'client-')) { 57 | return; 58 | } 59 | 60 | if (! $this->connection->app->clientMessagesEnabled) { 61 | return; 62 | } 63 | 64 | $channel = $this->channelManager->find( 65 | $this->connection->app->id, $this->payload->channel 66 | ); 67 | 68 | optional($channel)->broadcastToEveryoneExcept( 69 | $this->payload, $this->connection->socketId, $this->connection->app->id 70 | ); 71 | 72 | DashboardLogger::log($this->connection->app->id, DashboardLogger::TYPE_WS_MESSAGE, [ 73 | 'socketId' => $this->connection->socketId, 74 | 'event' => $this->payload->event, 75 | 'channel' => $this->payload->channel, 76 | 'data' => $this->payload, 77 | ]); 78 | } 79 | } 80 | -------------------------------------------------------------------------------- /src/Server/Messages/PusherMessageFactory.php: -------------------------------------------------------------------------------- 1 | getPayload()); 27 | 28 | return Str::startsWith($payload->event, 'pusher:') 29 | ? new PusherChannelProtocolMessage($payload, $connection, $channelManager) 30 | : new PusherClientMessage($payload, $connection, $channelManager); 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /src/Server/MockableConnection.php: -------------------------------------------------------------------------------- 1 | app = new stdClass; 20 | 21 | $this->app->id = $appId; 22 | $this->socketId = $socketId; 23 | } 24 | 25 | /** 26 | * Send data to the connection. 27 | * 28 | * @param string $data 29 | * @return \Ratchet\ConnectionInterface 30 | */ 31 | public function send($data) 32 | { 33 | // 34 | } 35 | 36 | /** 37 | * Close the connection. 38 | * 39 | * @return void 40 | */ 41 | public function close() 42 | { 43 | // 44 | } 45 | } 46 | -------------------------------------------------------------------------------- /src/Server/QueryParameters.php: -------------------------------------------------------------------------------- 1 | request = $request; 30 | } 31 | 32 | /** 33 | * Get all query parameters. 34 | * 35 | * @return array 36 | */ 37 | public function all(): array 38 | { 39 | $queryParameters = []; 40 | 41 | parse_str($this->request->getUri()->getQuery(), $queryParameters); 42 | 43 | return $queryParameters; 44 | } 45 | 46 | /** 47 | * Get a specific query parameter. 48 | * 49 | * @param string $name 50 | * @return string 51 | */ 52 | public function get(string $name): string 53 | { 54 | return $this->all()[$name] ?? ''; 55 | } 56 | } 57 | -------------------------------------------------------------------------------- /src/Server/Router.php: -------------------------------------------------------------------------------- 1 | routes = new RouteCollection; 36 | 37 | $this->customRoutes = [ 38 | 'get' => new Collection, 39 | 'post' => new Collection, 40 | 'put' => new Collection, 41 | 'patch' => new Collection, 42 | 'delete' => new Collection, 43 | ]; 44 | } 45 | 46 | /** 47 | * Get the routes. 48 | * 49 | * @return \Symfony\Component\Routing\RouteCollection 50 | */ 51 | public function getRoutes(): RouteCollection 52 | { 53 | return $this->routes; 54 | } 55 | 56 | /** 57 | * Get the list of routes that still need to be registered. 58 | * 59 | * @return array[Collection] 60 | */ 61 | public function getCustomRoutes(): array 62 | { 63 | return $this->customRoutes; 64 | } 65 | 66 | /** 67 | * Register the default routes. 68 | * 69 | * @return void 70 | */ 71 | public function registerRoutes() 72 | { 73 | $this->get('/app/{appKey}', 'websockets.handler'); 74 | $this->post('/apps/{appId}/events', config('websockets.handlers.trigger_event')); 75 | $this->get('/apps/{appId}/channels', config('websockets.handlers.fetch_channels')); 76 | $this->get('/apps/{appId}/channels/{channelName}', config('websockets.handlers.fetch_channel')); 77 | $this->get('/apps/{appId}/channels/{channelName}/users', config('websockets.handlers.fetch_users')); 78 | $this->get('/health', config('websockets.handlers.health')); 79 | 80 | $this->registerCustomRoutes(); 81 | } 82 | 83 | /** 84 | * Add a GET route. 85 | * 86 | * @param string $uri 87 | * @param string $action 88 | * @return void 89 | */ 90 | public function get(string $uri, $action) 91 | { 92 | $this->addRoute('GET', $uri, $action); 93 | } 94 | 95 | /** 96 | * Add a POST route. 97 | * 98 | * @param string $uri 99 | * @param string $action 100 | * @return void 101 | */ 102 | public function post(string $uri, $action) 103 | { 104 | $this->addRoute('POST', $uri, $action); 105 | } 106 | 107 | /** 108 | * Add a PUT route. 109 | * 110 | * @param string $uri 111 | * @param string $action 112 | * @return void 113 | */ 114 | public function put(string $uri, $action) 115 | { 116 | $this->addRoute('PUT', $uri, $action); 117 | } 118 | 119 | /** 120 | * Add a PATCH route. 121 | * 122 | * @param string $uri 123 | * @param string $action 124 | * @return void 125 | */ 126 | public function patch(string $uri, $action) 127 | { 128 | $this->addRoute('PATCH', $uri, $action); 129 | } 130 | 131 | /** 132 | * Add a DELETE route. 133 | * 134 | * @param string $uri 135 | * @param string $action 136 | * @return void 137 | */ 138 | public function delete(string $uri, $action) 139 | { 140 | $this->addRoute('DELETE', $uri, $action); 141 | } 142 | 143 | /** 144 | * Add a new route to the list. 145 | * 146 | * @param string $method 147 | * @param string $uri 148 | * @param string $action 149 | * @return void 150 | */ 151 | public function addRoute(string $method, string $uri, $action) 152 | { 153 | $this->routes->add($uri, $this->getRoute($method, $uri, $action)); 154 | } 155 | 156 | /** 157 | * Add a new custom route. Registered routes 158 | * will be resolved at server spin-up. 159 | * 160 | * @param string $method 161 | * @param string $uri 162 | * @param string $action 163 | * @return void 164 | */ 165 | public function addCustomRoute(string $method, $uri, $action) 166 | { 167 | $this->customRoutes[strtolower($method)]->put($uri, $action); 168 | } 169 | 170 | /** 171 | * Register the custom routes into the main RouteCollection. 172 | * 173 | * @return void 174 | */ 175 | public function registerCustomRoutes() 176 | { 177 | foreach ($this->customRoutes as $method => $actions) { 178 | $actions->each(function ($action, $uri) use ($method) { 179 | $this->{$method}($uri, $action); 180 | }); 181 | } 182 | } 183 | 184 | /** 185 | * Get the route of a specified method, uri and action. 186 | * 187 | * @param string $method 188 | * @param string $uri 189 | * @param string $action 190 | * @return \Symfony\Component\Routing\Route 191 | */ 192 | protected function getRoute(string $method, string $uri, $action): Route 193 | { 194 | $action = app($action); 195 | $action = is_subclass_of($action, MessageComponentInterface::class) 196 | ? $this->createWebSocketsServer($action) 197 | : $action; 198 | 199 | return new Route($uri, ['_controller' => $action], [], [], null, [], [$method]); 200 | } 201 | 202 | /** 203 | * Create a new websockets server to handle the action. 204 | * 205 | * @param MessageComponentInterface $app 206 | * @return \Ratchet\WebSocket\WsServer 207 | */ 208 | protected function createWebSocketsServer($app): WsServer 209 | { 210 | if (WebsocketsLogger::isEnabled()) { 211 | $app = WebsocketsLogger::decorate($app); 212 | } 213 | 214 | return new WsServer($app); 215 | } 216 | } 217 | -------------------------------------------------------------------------------- /src/ServerFactory.php: -------------------------------------------------------------------------------- 1 | host = $host; 65 | $this->port = $port; 66 | 67 | $this->loop = LoopFactory::create(); 68 | } 69 | 70 | /** 71 | * Add the routes. 72 | * 73 | * @param \Symfony\Component\Routing\RouteCollection $routes 74 | * @return $this 75 | */ 76 | public function withRoutes(RouteCollection $routes) 77 | { 78 | $this->routes = $routes; 79 | 80 | return $this; 81 | } 82 | 83 | /** 84 | * Set the loop instance. 85 | * 86 | * @param \React\EventLoop\LoopInterface $loop 87 | * @return $this 88 | */ 89 | public function setLoop(LoopInterface $loop) 90 | { 91 | $this->loop = $loop; 92 | 93 | return $this; 94 | } 95 | 96 | /** 97 | * Set the console output. 98 | * 99 | * @param \Symfony\Component\Console\Output\OutputInterface $consoleOutput 100 | * @return $this 101 | */ 102 | public function setConsoleOutput(OutputInterface $consoleOutput) 103 | { 104 | $this->consoleOutput = $consoleOutput; 105 | 106 | return $this; 107 | } 108 | 109 | /** 110 | * Set up the server. 111 | * 112 | * @return \Ratchet\Server\IoServer 113 | */ 114 | public function createServer(): IoServer 115 | { 116 | $socket = new Server("{$this->host}:{$this->port}", $this->loop); 117 | 118 | if (config('websockets.ssl.local_cert')) { 119 | $socket = new SecureServer($socket, $this->loop, config('websockets.ssl')); 120 | } 121 | 122 | $app = new Router( 123 | new UrlMatcher($this->routes, new RequestContext) 124 | ); 125 | 126 | $httpServer = new HttpServer($app, config('websockets.max_request_size_in_kb') * 1024); 127 | 128 | if (HttpLogger::isEnabled()) { 129 | $httpServer = HttpLogger::decorate($httpServer); 130 | } 131 | 132 | return new IoServer($httpServer, $socket, $this->loop); 133 | } 134 | } 135 | -------------------------------------------------------------------------------- /src/Statistics/Collectors/MemoryCollector.php: -------------------------------------------------------------------------------- 1 | channelManager = app(ChannelManager::class); 36 | } 37 | 38 | /** 39 | * Handle the incoming websocket message. 40 | * 41 | * @param string|int $appId 42 | * @return void 43 | */ 44 | public function webSocketMessage($appId) 45 | { 46 | $this->findOrMake($appId) 47 | ->webSocketMessage(); 48 | } 49 | 50 | /** 51 | * Handle the incoming API message. 52 | * 53 | * @param string|int $appId 54 | * @return void 55 | */ 56 | public function apiMessage($appId) 57 | { 58 | $this->findOrMake($appId) 59 | ->apiMessage(); 60 | } 61 | 62 | /** 63 | * Handle the new conection. 64 | * 65 | * @param string|int $appId 66 | * @return void 67 | */ 68 | public function connection($appId) 69 | { 70 | $this->findOrMake($appId) 71 | ->connection(); 72 | } 73 | 74 | /** 75 | * Handle disconnections. 76 | * 77 | * @param string|int $appId 78 | * @return void 79 | */ 80 | public function disconnection($appId) 81 | { 82 | $this->findOrMake($appId) 83 | ->disconnection(); 84 | } 85 | 86 | /** 87 | * Save all the stored statistics. 88 | * 89 | * @return void 90 | */ 91 | public function save() 92 | { 93 | $this->getStatistics()->then(function ($statistics) { 94 | foreach ($statistics as $appId => $statistic) { 95 | $statistic->isEnabled()->then(function ($isEnabled) use ($appId, $statistic) { 96 | if (! $isEnabled) { 97 | return; 98 | } 99 | 100 | if ($statistic->shouldHaveTracesRemoved()) { 101 | $this->resetAppTraces($appId); 102 | 103 | return; 104 | } 105 | 106 | $this->createRecord($statistic, $appId); 107 | 108 | $this->channelManager 109 | ->getGlobalConnectionsCount($appId) 110 | ->then(function ($connections) use ($statistic) { 111 | $statistic->reset( 112 | is_null($connections) ? 0 : $connections 113 | ); 114 | }); 115 | }); 116 | } 117 | }); 118 | } 119 | 120 | /** 121 | * Flush the stored statistics. 122 | * 123 | * @return void 124 | */ 125 | public function flush() 126 | { 127 | $this->statistics = []; 128 | } 129 | 130 | /** 131 | * Get the saved statistics. 132 | * 133 | * @return PromiseInterface[array] 134 | */ 135 | public function getStatistics(): PromiseInterface 136 | { 137 | return Helpers::createFulfilledPromise($this->statistics); 138 | } 139 | 140 | /** 141 | * Get the saved statistics for an app. 142 | * 143 | * @param string|int $appId 144 | * @return PromiseInterface[\BeyondCode\LaravelWebSockets\Statistics\Statistic|null] 145 | */ 146 | public function getAppStatistics($appId): PromiseInterface 147 | { 148 | return Helpers::createFulfilledPromise( 149 | $this->statistics[$appId] ?? null 150 | ); 151 | } 152 | 153 | /** 154 | * Remove all app traces from the database if no connections have been set 155 | * in the meanwhile since last save. 156 | * 157 | * @param string|int $appId 158 | * @return void 159 | */ 160 | public function resetAppTraces($appId) 161 | { 162 | unset($this->statistics[$appId]); 163 | } 164 | 165 | /** 166 | * Find or create a defined statistic for an app. 167 | * 168 | * @param string|int $appId 169 | * @return \BeyondCode\LaravelWebSockets\Statistics\Statistic 170 | */ 171 | protected function findOrMake($appId): Statistic 172 | { 173 | if (! isset($this->statistics[$appId])) { 174 | $this->statistics[$appId] = Statistic::new($appId); 175 | } 176 | 177 | return $this->statistics[$appId]; 178 | } 179 | 180 | /** 181 | * Create a new record using the Statistic Store. 182 | * 183 | * @param \BeyondCode\LaravelWebSockets\Statistics\Statistic $statistic 184 | * @param mixed $appId 185 | * @return void 186 | */ 187 | public function createRecord(Statistic $statistic, $appId) 188 | { 189 | StatisticsStore::store($statistic->toArray()); 190 | } 191 | } 192 | -------------------------------------------------------------------------------- /src/Statistics/Statistic.php: -------------------------------------------------------------------------------- 1 | appId = $appId; 55 | } 56 | 57 | /** 58 | * Create a new statistic instance. 59 | * 60 | * @param string|int $appId 61 | * @return \BeyondCode\LaravelWebSockets\Statistics\Statistic 62 | */ 63 | public static function new($appId) 64 | { 65 | return new static($appId); 66 | } 67 | 68 | /** 69 | * Set the current connections count. 70 | * 71 | * @param int $currentConnectionsCount 72 | * @return $this 73 | */ 74 | public function setCurrentConnectionsCount(int $currentConnectionsCount) 75 | { 76 | $this->currentConnectionsCount = $currentConnectionsCount; 77 | 78 | return $this; 79 | } 80 | 81 | /** 82 | * Set the peak connections count. 83 | * 84 | * @param int $peakConnectionsCount 85 | * @return $this 86 | */ 87 | public function setPeakConnectionsCount(int $peakConnectionsCount) 88 | { 89 | $this->peakConnectionsCount = $peakConnectionsCount; 90 | 91 | return $this; 92 | } 93 | 94 | /** 95 | * Set the peak connections count. 96 | * 97 | * @param int $webSocketMessagesCount 98 | * @return $this 99 | */ 100 | public function setWebSocketMessagesCount(int $webSocketMessagesCount) 101 | { 102 | $this->webSocketMessagesCount = $webSocketMessagesCount; 103 | 104 | return $this; 105 | } 106 | 107 | /** 108 | * Set the peak connections count. 109 | * 110 | * @param int $apiMessagesCount 111 | * @return $this 112 | */ 113 | public function setApiMessagesCount(int $apiMessagesCount) 114 | { 115 | $this->apiMessagesCount = $apiMessagesCount; 116 | 117 | return $this; 118 | } 119 | 120 | /** 121 | * Check if the app has statistics enabled. 122 | * 123 | * @return PromiseInterface 124 | */ 125 | public function isEnabled(): PromiseInterface 126 | { 127 | $deferred = new Deferred(); 128 | 129 | App::findById($this->appId)->then(function ($app) use ($deferred) { 130 | $deferred->resolve($app->statisticsEnabled); 131 | }); 132 | 133 | return $deferred->promise(); 134 | } 135 | 136 | /** 137 | * Handle a new connection increment. 138 | * 139 | * @return void 140 | */ 141 | public function connection() 142 | { 143 | $this->currentConnectionsCount++; 144 | 145 | $this->peakConnectionsCount = max($this->currentConnectionsCount, $this->peakConnectionsCount); 146 | } 147 | 148 | /** 149 | * Handle a disconnection decrement. 150 | * 151 | * @return void 152 | */ 153 | public function disconnection() 154 | { 155 | $this->currentConnectionsCount--; 156 | 157 | $this->peakConnectionsCount = max($this->currentConnectionsCount, $this->peakConnectionsCount); 158 | } 159 | 160 | /** 161 | * Handle a new websocket message. 162 | * 163 | * @return void 164 | */ 165 | public function webSocketMessage() 166 | { 167 | $this->webSocketMessagesCount++; 168 | } 169 | 170 | /** 171 | * Handle a new api message. 172 | * 173 | * @return void 174 | */ 175 | public function apiMessage() 176 | { 177 | $this->apiMessagesCount++; 178 | } 179 | 180 | /** 181 | * Reset all the connections to a specific count. 182 | * 183 | * @param int $currentConnectionsCount 184 | * @return void 185 | */ 186 | public function reset(int $currentConnectionsCount) 187 | { 188 | $this->currentConnectionsCount = $currentConnectionsCount; 189 | $this->peakConnectionsCount = max(0, $currentConnectionsCount); 190 | $this->webSocketMessagesCount = 0; 191 | $this->apiMessagesCount = 0; 192 | } 193 | 194 | /** 195 | * Check if the current statistic entry is empty. This means 196 | * that the statistic entry can be easily deleted if no activity 197 | * occured for a while. 198 | * 199 | * @return bool 200 | */ 201 | public function shouldHaveTracesRemoved(): bool 202 | { 203 | return $this->currentConnectionsCount === 0 && $this->peakConnectionsCount === 0; 204 | } 205 | 206 | /** 207 | * Transform the statistic to array. 208 | * 209 | * @return array 210 | */ 211 | public function toArray() 212 | { 213 | return [ 214 | 'app_id' => $this->appId, 215 | 'peak_connections_count' => $this->peakConnectionsCount, 216 | 'websocket_messages_count' => $this->webSocketMessagesCount, 217 | 'api_messages_count' => $this->apiMessagesCount, 218 | ]; 219 | } 220 | } 221 | -------------------------------------------------------------------------------- /src/Statistics/Stores/DatabaseStore.php: -------------------------------------------------------------------------------- 1 | toDateTimeString()) 43 | ->when(! is_null($appId), function ($query) use ($appId) { 44 | return $query->whereAppId($appId); 45 | }) 46 | ->delete(); 47 | } 48 | 49 | /** 50 | * Get the query result as eloquent collection. 51 | * 52 | * @param callable $processQuery 53 | * @return \Illuminate\Support\Collection 54 | */ 55 | public function getRawRecords(callable $processQuery = null) 56 | { 57 | return static::$model::query() 58 | ->when(! is_null($processQuery), function ($query) use ($processQuery) { 59 | return call_user_func($processQuery, $query); 60 | }, function ($query) { 61 | return $query->latest()->limit(120); 62 | })->get(); 63 | } 64 | 65 | /** 66 | * Get the results for a specific query. 67 | * 68 | * @param callable $processQuery 69 | * @param callable $processCollection 70 | * @return array 71 | */ 72 | public function getRecords(callable $processQuery = null, callable $processCollection = null): array 73 | { 74 | return $this->getRawRecords($processQuery) 75 | ->when(! is_null($processCollection), function ($collection) use ($processCollection) { 76 | return call_user_func($processCollection, $collection); 77 | }) 78 | ->map(function (Model $statistic) { 79 | return $this->statisticToArray($statistic); 80 | }) 81 | ->toArray(); 82 | } 83 | 84 | /** 85 | * Get the results for a specific query into a 86 | * format that is easily to read for graphs. 87 | * 88 | * @param callable $processQuery 89 | * @param callable $processCollection 90 | * @return array 91 | */ 92 | public function getForGraph(callable $processQuery = null, callable $processCollection = null): array 93 | { 94 | $statistics = collect( 95 | $this->getRecords($processQuery, $processCollection) 96 | ); 97 | 98 | return $this->statisticsToGraph($statistics); 99 | } 100 | 101 | /** 102 | * Turn the statistic model to an array. 103 | * 104 | * @param \Illuminate\Database\Eloquent\Model $statistic 105 | * @return array 106 | */ 107 | protected function statisticToArray(Model $statistic): array 108 | { 109 | return [ 110 | 'timestamp' => (string) $statistic->created_at, 111 | 'peak_connections_count' => $statistic->peak_connections_count, 112 | 'websocket_messages_count' => $statistic->websocket_messages_count, 113 | 'api_messages_count' => $statistic->api_messages_count, 114 | ]; 115 | } 116 | 117 | /** 118 | * Turn the statistics collection to an array used for graph. 119 | * 120 | * @param \Illuminate\Support\Collection $statistics 121 | * @return array 122 | */ 123 | protected function statisticsToGraph(Collection $statistics): array 124 | { 125 | return [ 126 | 'peak_connections' => [ 127 | 'x' => $statistics->pluck('timestamp')->toArray(), 128 | 'y' => $statistics->pluck('peak_connections_count')->toArray(), 129 | ], 130 | 'websocket_messages_count' => [ 131 | 'x' => $statistics->pluck('timestamp')->toArray(), 132 | 'y' => $statistics->pluck('websocket_messages_count')->toArray(), 133 | ], 134 | 'api_messages_count' => [ 135 | 'x' => $statistics->pluck('timestamp')->toArray(), 136 | 'y' => $statistics->pluck('api_messages_count')->toArray(), 137 | ], 138 | ]; 139 | } 140 | } 141 | -------------------------------------------------------------------------------- /src/WebSocketsServiceProvider.php: -------------------------------------------------------------------------------- 1 | publishes([ 39 | __DIR__.'/../config/websockets.php' => config_path('websockets.php'), 40 | ], 'config'); 41 | 42 | $this->mergeConfigFrom( 43 | __DIR__.'/../config/websockets.php', 'websockets' 44 | ); 45 | 46 | $this->publishes([ 47 | __DIR__.'/../database/migrations/0000_00_00_000000_create_websockets_statistics_entries_table.php' => database_path('migrations/0000_00_00_000000_create_websockets_statistics_entries_table.php'), 48 | __DIR__.'/../database/migrations/0000_00_00_000000_rename_statistics_counters.php' => database_path('migrations/0000_00_00_000000_rename_statistics_counters.php'), 49 | ], 'migrations'); 50 | 51 | $this->registerEventLoop(); 52 | 53 | $this->registerSQLiteDatabase(); 54 | 55 | $this->registerMySqlDatabase(); 56 | 57 | $this->registerAsyncRedisQueueDriver(); 58 | 59 | $this->registerWebSocketHandler(); 60 | 61 | $this->registerRouter(); 62 | 63 | $this->registerManagers(); 64 | 65 | $this->registerStatistics(); 66 | 67 | $this->registerDashboard(); 68 | 69 | $this->registerCommands(); 70 | } 71 | 72 | /** 73 | * Register the service provider. 74 | * 75 | * @return void 76 | */ 77 | public function register() 78 | { 79 | // 80 | } 81 | 82 | protected function registerEventLoop() 83 | { 84 | $this->app->singleton(LoopInterface::class, function () { 85 | return Factory::create(); 86 | }); 87 | } 88 | 89 | /** 90 | * Register the async, non-blocking Redis queue driver. 91 | * 92 | * @return void 93 | */ 94 | protected function registerAsyncRedisQueueDriver() 95 | { 96 | Queue::extend('async-redis', function () { 97 | return new AsyncRedisConnector($this->app['redis']); 98 | }); 99 | } 100 | 101 | protected function registerSQLiteDatabase() 102 | { 103 | $this->app->singleton(DatabaseInterface::class, function () { 104 | $factory = new SQLiteFactory($this->app->make(LoopInterface::class)); 105 | 106 | $database = $factory->openLazy( 107 | config('websockets.managers.sqlite.database', ':memory:') 108 | ); 109 | 110 | $migrations = (new Finder()) 111 | ->files() 112 | ->ignoreDotFiles(true) 113 | ->in(__DIR__.'/../database/migrations/sqlite') 114 | ->name('*.sql'); 115 | 116 | /** @var SplFileInfo $migration */ 117 | foreach ($migrations as $migration) { 118 | $database->exec($migration->getContents()); 119 | } 120 | 121 | return $database; 122 | }); 123 | } 124 | 125 | protected function registerMySqlDatabase() 126 | { 127 | $this->app->singleton(ConnectionInterface::class, function () { 128 | $factory = new MySQLFactory($this->app->make(LoopInterface::class)); 129 | 130 | $connectionKey = 'database.connections.'.config('websockets.managers.mysql.connection'); 131 | 132 | $auth = trim(config($connectionKey.'.username').':'.config($connectionKey.'.password'), ':'); 133 | $connection = trim(config($connectionKey.'.host').':'.config($connectionKey.'.port'), ':'); 134 | $database = config($connectionKey.'.database'); 135 | 136 | $database = $factory->createLazyConnection(trim("{$auth}@{$connection}/{$database}", '@')); 137 | 138 | return $database; 139 | }); 140 | } 141 | 142 | /** 143 | * Register the statistics-related contracts. 144 | * 145 | * @return void 146 | */ 147 | protected function registerStatistics() 148 | { 149 | $this->app->singleton(StatisticsStore::class, function ($app) { 150 | $config = $app['config']['websockets']; 151 | $class = $config['statistics']['store']; 152 | 153 | return new $class; 154 | }); 155 | 156 | $this->app->singleton(StatisticsCollector::class, function ($app) { 157 | $config = $app['config']['websockets']; 158 | $replicationMode = $config['replication']['mode'] ?? 'local'; 159 | 160 | $class = $config['replication']['modes'][$replicationMode]['collector']; 161 | 162 | return new $class; 163 | }); 164 | } 165 | 166 | /** 167 | * Register the dashboard components. 168 | * 169 | * @return void 170 | */ 171 | protected function registerDashboard() 172 | { 173 | $this->loadViewsFrom(__DIR__.'/../resources/views/', 'websockets'); 174 | 175 | $this->registerDashboardRoutes(); 176 | $this->registerDashboardGate(); 177 | } 178 | 179 | /** 180 | * Register the package commands. 181 | * 182 | * @return void 183 | */ 184 | protected function registerCommands() 185 | { 186 | $this->commands([ 187 | Console\Commands\StartServer::class, 188 | Console\Commands\RestartServer::class, 189 | Console\Commands\CleanStatistics::class, 190 | Console\Commands\FlushCollectedStatistics::class, 191 | ]); 192 | } 193 | 194 | /** 195 | * Register the routing. 196 | * 197 | * @return void 198 | */ 199 | protected function registerRouter() 200 | { 201 | $this->app->singleton('websockets.router', function () { 202 | return new Router; 203 | }); 204 | } 205 | 206 | /** 207 | * Register the managers for the app. 208 | * 209 | * @return void 210 | */ 211 | protected function registerManagers() 212 | { 213 | $this->app->singleton(Contracts\AppManager::class, function ($app) { 214 | $config = $app['config']['websockets']; 215 | 216 | return $this->app->make($config['managers']['app']); 217 | }); 218 | } 219 | 220 | /** 221 | * Register the dashboard routes. 222 | * 223 | * @return void 224 | */ 225 | protected function registerDashboardRoutes() 226 | { 227 | Route::group([ 228 | 'domain' => config('websockets.dashboard.domain'), 229 | 'prefix' => config('websockets.dashboard.path'), 230 | 'as' => 'laravel-websockets.', 231 | 'middleware' => config('websockets.dashboard.middleware', [AuthorizeDashboard::class]), 232 | ], function () { 233 | Route::get('/', ShowDashboard::class)->name('dashboard'); 234 | Route::get('/apps', ShowApps::class)->name('apps'); 235 | Route::post('/apps', StoreApp::class)->name('apps.store'); 236 | Route::get('/api/{appId}/statistics', ShowStatistics::class)->name('statistics'); 237 | Route::post('/auth', AuthenticateDashboard::class)->name('auth'); 238 | Route::post('/event', SendMessage::class)->name('event'); 239 | }); 240 | } 241 | 242 | /** 243 | * Register the dashboard gate. 244 | * 245 | * @return void 246 | */ 247 | protected function registerDashboardGate() 248 | { 249 | Gate::define('viewWebSocketsDashboard', function ($user = null) { 250 | return $this->app->environment('local'); 251 | }); 252 | } 253 | 254 | protected function registerWebSocketHandler() 255 | { 256 | $this->app->singleton('websockets.handler', function () { 257 | return app(config('websockets.handlers.websocket')); 258 | }); 259 | } 260 | } 261 | --------------------------------------------------------------------------------