Merge 2.x changes (#1043)
* Resolve conflicts * Apply fixes from StyleCI (#1042)
This commit is contained in:
parent
c53e78d5a0
commit
fb958fb851
|
|
@ -20,41 +20,16 @@ jobs:
|
|||
fail-fast: false
|
||||
matrix:
|
||||
php:
|
||||
- '7.3'
|
||||
- '7.4'
|
||||
- '8.0'
|
||||
- '8.1'
|
||||
laravel:
|
||||
- 6.*
|
||||
- 7.*
|
||||
- 8.*
|
||||
- 9.*
|
||||
prefer:
|
||||
- 'prefer-lowest'
|
||||
- 'prefer-stable'
|
||||
include:
|
||||
- laravel: '6.*'
|
||||
testbench: '4.*'
|
||||
phpunit: '^8.5.8|^9.3.3'
|
||||
- laravel: '7.*'
|
||||
testbench: '5.*'
|
||||
phpunit: '^8.5.8|^9.3.3'
|
||||
- laravel: '8.*'
|
||||
testbench: '6.*'
|
||||
phpunit: '^9.3.3'
|
||||
exclude:
|
||||
- php: '8.0'
|
||||
laravel: 6.*
|
||||
prefer: 'prefer-lowest'
|
||||
- php: '8.0'
|
||||
laravel: 7.*
|
||||
prefer: 'prefer-lowest'
|
||||
- php: '8.1'
|
||||
laravel: 6.*
|
||||
- php: '8.1'
|
||||
laravel: 7.*
|
||||
- php: '8.1'
|
||||
laravel: 8.*
|
||||
prefer: 'prefer-lowest'
|
||||
- laravel: '9.*'
|
||||
testbench: '7.*'
|
||||
|
||||
name: PHP ${{ matrix.php }} - Laravel ${{ matrix.laravel }} --${{ matrix.prefer }}
|
||||
|
||||
|
|
@ -68,6 +43,13 @@ jobs:
|
|||
extensions: dom, curl, libxml, mbstring, zip, pcntl, pdo, sqlite, pdo_sqlite, bcmath, soap, intl, gd, exif, iconv
|
||||
coverage: pcov
|
||||
|
||||
- name: Setup MySQL
|
||||
uses: haltuf/mysql-action@master
|
||||
with:
|
||||
mysql version: '8.0'
|
||||
mysql database: 'websockets_test'
|
||||
mysql root password: 'password'
|
||||
|
||||
- name: Setup Redis
|
||||
uses: supercharge/redis-github-action@1.1.0
|
||||
with:
|
||||
|
|
@ -81,7 +63,7 @@ jobs:
|
|||
|
||||
- name: Install dependencies
|
||||
run: |
|
||||
composer require "laravel/framework:${{ matrix.laravel }}" "phpunit/phpunit:${{ matrix.phpunit }}" "orchestra/testbench-browser-kit:${{ matrix.testbench }}" "orchestra/database:${{ matrix.testbench }}" --no-interaction --no-update
|
||||
composer require "laravel/framework:${{ matrix.laravel }}" "orchestra/testbench-browser-kit:${{ matrix.testbench }}" "orchestra/database:${{ matrix.testbench }}" --no-interaction --no-update
|
||||
composer update --${{ matrix.prefer }} --prefer-dist --no-interaction --no-suggest
|
||||
|
||||
- name: Run tests for Local
|
||||
|
|
|
|||
|
|
@ -29,27 +29,33 @@
|
|||
}
|
||||
],
|
||||
"require": {
|
||||
"cboden/ratchet": "^0.4.1",
|
||||
"clue/redis-react": "^2.5",
|
||||
"php": "^8.0|^8.1",
|
||||
"cboden/ratchet": "^0.4.4",
|
||||
"clue/block-react": "^1.5",
|
||||
"clue/reactphp-sqlite": "^1.0",
|
||||
"clue/redis-react": "^2.6",
|
||||
"doctrine/dbal": "^2.9",
|
||||
"evenement/evenement": "^2.0|^3.0",
|
||||
"facade/ignition-contracts": "^1.0",
|
||||
"guzzlehttp/psr7": "^1.7|^2.0",
|
||||
"illuminate/broadcasting": "^6.3|^7.0|^8.0|^9.0",
|
||||
"illuminate/console": "^6.3|^7.0|^8.0|^9.0",
|
||||
"illuminate/http": "^6.3|^7.0|^8.0|^9.0",
|
||||
"illuminate/queue": "^6.3|^7.0|^8.0|^9.0",
|
||||
"illuminate/routing": "^6.3|^7.0|^8.0|^9.0",
|
||||
"illuminate/support": "^6.3|^7.0|^8.0|^9.0",
|
||||
"pusher/pusher-php-server": "^3.0|^4.0|^5.0|^6.0|^7.0",
|
||||
"guzzlehttp/psr7": "^1.5",
|
||||
"illuminate/broadcasting": "^9.0",
|
||||
"illuminate/console": "^9.0",
|
||||
"illuminate/http": "^9.0",
|
||||
"illuminate/queue": "^9.0",
|
||||
"illuminate/routing": "^9.0",
|
||||
"illuminate/support": "^9.0",
|
||||
"pusher/pusher-php-server": "^6.0|^7.0",
|
||||
"react/mysql": "^0.5",
|
||||
"react/promise": "^2.8",
|
||||
"symfony/http-kernel": "^4.4|^5.4|^6.0",
|
||||
"symfony/http-kernel": "^5.0|^6.0",
|
||||
"symfony/psr-http-message-bridge": "^1.1|^2.0"
|
||||
},
|
||||
"require-dev": {
|
||||
"clue/block-react": "^1.4",
|
||||
"clue/buzz-react": "^2.9",
|
||||
"laravel/legacy-factories": "^1.1",
|
||||
"orchestra/testbench-browser-kit": "^4.0|^5.0|^6.0",
|
||||
"phpunit/phpunit": "^8.5.8|^9.3.3"
|
||||
"orchestra/testbench-browser-kit": "^7.0",
|
||||
"phpunit/phpunit": "^9.0",
|
||||
"ratchet/pawl": "^0.3.5"
|
||||
},
|
||||
"suggest": {
|
||||
"ext-pcntl": "Running the server needs pcntl to listen to command signals and soft-shutdown.",
|
||||
|
|
@ -71,7 +77,8 @@
|
|||
"config": {
|
||||
"sort-packages": true
|
||||
},
|
||||
"minimum-stability": "stable",
|
||||
"minimum-stability": "dev",
|
||||
"prefer-stable": true,
|
||||
"extra": {
|
||||
"laravel": {
|
||||
"providers": [
|
||||
|
|
|
|||
|
|
@ -37,13 +37,40 @@ return [
|
|||
| the use of the TCP protocol based on, for example, a list of allowed
|
||||
| applications.
|
||||
| By default, it uses the defined array in the config file, but you can
|
||||
| anytime implement the same interface as the class and add your own
|
||||
| custom method to retrieve the apps.
|
||||
| choose to use SQLite or MySQL application managers, or define a
|
||||
| custom application manager.
|
||||
|
|
||||
*/
|
||||
|
||||
'app' => \BeyondCode\LaravelWebSockets\Apps\ConfigAppManager::class,
|
||||
|
||||
/*
|
||||
|--------------------------------------------------------------------------
|
||||
| SQLite application manager
|
||||
|--------------------------------------------------------------------------
|
||||
|
|
||||
| The SQLite database to use when using the SQLite application manager.
|
||||
|
|
||||
*/
|
||||
|
||||
'sqlite' => [
|
||||
'database' => storage_path('laravel-websockets.sqlite'),
|
||||
],
|
||||
|
||||
/*
|
||||
|--------------------------------------------------------------------------
|
||||
| MySql application manager
|
||||
|--------------------------------------------------------------------------
|
||||
|
|
||||
| The MySQL database connection to use.
|
||||
|
|
||||
*/
|
||||
|
||||
'mysql' => [
|
||||
'connection' => env('DB_CONNECTION', 'mysql'),
|
||||
|
||||
'table' => 'websockets_apps',
|
||||
],
|
||||
],
|
||||
|
||||
/*
|
||||
|
|
|
|||
|
|
@ -0,0 +1,40 @@
|
|||
<?php
|
||||
|
||||
use Illuminate\Database\Migrations\Migration;
|
||||
use Illuminate\Database\Schema\Blueprint;
|
||||
use Illuminate\Support\Facades\Schema;
|
||||
|
||||
class CreateWebSocketsAppsTable extends Migration
|
||||
{
|
||||
/**
|
||||
* Run the migrations.
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function up()
|
||||
{
|
||||
Schema::create('websockets_apps', function (Blueprint $table) {
|
||||
$table->string('id')->index();
|
||||
$table->string('key');
|
||||
$table->string('secret');
|
||||
$table->string('name');
|
||||
$table->string('host')->nullable();
|
||||
$table->string('path')->nullable();
|
||||
$table->boolean('enable_client_messages')->default(false);
|
||||
$table->boolean('enable_statistics')->default(true);
|
||||
$table->unsignedInteger('capacity')->nullable();
|
||||
$table->string('allowed_origins');
|
||||
$table->nullableTimestamps();
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Reverse the migrations.
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function down()
|
||||
{
|
||||
Schema::dropIfExists('websockets_apps');
|
||||
}
|
||||
}
|
||||
|
|
@ -16,9 +16,9 @@ class CreateWebSocketsStatisticsEntriesTable extends Migration
|
|||
Schema::create('websockets_statistics_entries', function (Blueprint $table) {
|
||||
$table->increments('id');
|
||||
$table->string('app_id');
|
||||
$table->integer('peak_connection_count');
|
||||
$table->integer('websocket_message_count');
|
||||
$table->integer('api_message_count');
|
||||
$table->integer('peak_connections_count');
|
||||
$table->integer('websocket_messages_count');
|
||||
$table->integer('api_messages_count');
|
||||
$table->nullableTimestamps();
|
||||
});
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,44 +0,0 @@
|
|||
<?php
|
||||
|
||||
use Illuminate\Database\Migrations\Migration;
|
||||
use Illuminate\Database\Schema\Blueprint;
|
||||
use Illuminate\Support\Facades\Schema;
|
||||
|
||||
class RenameStatisticsCounters extends Migration
|
||||
{
|
||||
/**
|
||||
* Run the migrations.
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function up()
|
||||
{
|
||||
Schema::table('websockets_statistics_entries', function (Blueprint $table) {
|
||||
$table->renameColumn('peak_connection_count', 'peak_connections_count');
|
||||
});
|
||||
Schema::table('websockets_statistics_entries', function (Blueprint $table) {
|
||||
$table->renameColumn('websocket_message_count', 'websocket_messages_count');
|
||||
});
|
||||
Schema::table('websockets_statistics_entries', function (Blueprint $table) {
|
||||
$table->renameColumn('api_message_count', 'api_messages_count');
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Reverse the migrations.
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function down()
|
||||
{
|
||||
Schema::table('websockets_statistics_entries', function (Blueprint $table) {
|
||||
$table->renameColumn('peak_connections_count', 'peak_connection_count');
|
||||
});
|
||||
Schema::table('websockets_statistics_entries', function (Blueprint $table) {
|
||||
$table->renameColumn('websocket_messages_count', 'websocket_message_count');
|
||||
});
|
||||
Schema::table('websockets_statistics_entries', function (Blueprint $table) {
|
||||
$table->renameColumn('api_messages_count', 'api_message_count');
|
||||
});
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,12 @@
|
|||
CREATE TABLE IF NOT EXISTS apps (
|
||||
id STRING NOT NULL,
|
||||
key STRING NOT NULL,
|
||||
secret STRING NOT NULL,
|
||||
name STRING NOT NULL,
|
||||
host STRING NULLABLE,
|
||||
path STRING NULLABLE,
|
||||
enable_client_messages BOOLEAN DEFAULT 0,
|
||||
enable_statistics BOOLEAN DEFAULT 1,
|
||||
capacity INTEGER NULLABLE,
|
||||
allowed_origins STRING NULLABLE
|
||||
)
|
||||
|
|
@ -0,0 +1,183 @@
|
|||
@extends('websockets::layout')
|
||||
|
||||
@section('title')
|
||||
Apps
|
||||
@endsection
|
||||
|
||||
@section('content')
|
||||
<div class="flex flex-col py-8" id="app">
|
||||
<form action="{{ route('laravel-websockets.apps.store') }}" method="POST">
|
||||
@csrf
|
||||
<div>
|
||||
<div>
|
||||
<div>
|
||||
<h3 class="text-lg leading-6 font-medium text-gray-900">
|
||||
Add new app
|
||||
</h3>
|
||||
</div>
|
||||
|
||||
@if($errors->isNotEmpty())
|
||||
<div class="bg-red-500 text-white my-4 p-4 rounded">
|
||||
@foreach($errors->all() as $error)
|
||||
{{ $error }}<br>
|
||||
@endforeach
|
||||
</div>
|
||||
@endif
|
||||
|
||||
<div class="mt-6 sm:mt-5">
|
||||
<div class="sm:grid sm:grid-cols-3 sm:gap-4 sm:items-start sm:border-t sm:border-gray-200 sm:pt-5">
|
||||
<label for="name"
|
||||
class="block text-sm font-medium leading-5 text-gray-700 sm:mt-px sm:pt-2">
|
||||
Name
|
||||
</label>
|
||||
<div class="mt-1 sm:mt-0 sm:col-span-2">
|
||||
<div class="max-w-lg flex rounded-md shadow-sm">
|
||||
<input id="name" name="name"
|
||||
class="flex-1 form-input block w-full rounded-md transition duration-150 ease-in-out sm:text-sm sm:leading-5"/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="sm:grid sm:grid-cols-3 sm:gap-4 sm:items-start sm:pt-5">
|
||||
<label for="allowed_origins"
|
||||
class="block text-sm font-medium leading-5 text-gray-700 sm:mt-px sm:pt-2">
|
||||
Allowed origins (comma separated)
|
||||
</label>
|
||||
<div class="mt-1 sm:mt-0 sm:col-span-2">
|
||||
<div class="max-w-lg flex rounded-md shadow-sm">
|
||||
<input id="allowed_origins" name="allowed_origins"
|
||||
class="flex-1 form-input block w-full rounded-md transition duration-150 ease-in-out sm:text-sm sm:leading-5"/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="sm:grid sm:grid-cols-3 sm:gap-4 sm:items-start sm:pt-5">
|
||||
<label for="enable_statistics"
|
||||
class="block text-sm font-medium leading-5 text-gray-700 sm:mt-px sm:pt-2">
|
||||
Enable Statistics
|
||||
</label>
|
||||
<div class="mt-1 sm:mt-0 sm:col-span-2">
|
||||
<div class="mt-2 flex items-center justify-between">
|
||||
<div class="flex items-center">
|
||||
<input id="enable_statistics"
|
||||
name="enable_statistics"
|
||||
value="1" type="checkbox" class="form-checkbox h-4 w-4 text-indigo-600 transition duration-150 ease-in-out" />
|
||||
<label for="enable_statistics" class="ml-2 block text-sm leading-5 text-gray-900">
|
||||
Yes
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="sm:grid sm:grid-cols-3 sm:gap-4 sm:items-start sm:pt-5">
|
||||
<label for="enable_client_messages"
|
||||
class="block text-sm font-medium leading-5 text-gray-700 sm:mt-px sm:pt-2">
|
||||
Enable Client Messages
|
||||
</label>
|
||||
<div class="mt-1 sm:mt-0 sm:col-span-2">
|
||||
<div class="mt-2 flex items-center justify-between">
|
||||
<div class="flex items-center">
|
||||
<input id="enable_client_messages"
|
||||
name="enable_client_messages"
|
||||
value="1" type="checkbox" class="form-checkbox h-4 w-4 text-indigo-600 transition duration-150 ease-in-out" />
|
||||
<label for="enable_client_messages" class="ml-2 block text-sm leading-5 text-gray-900">
|
||||
Yes
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="mt-8 border-t border-gray-200 pt-5">
|
||||
<div class="flex justify-end">
|
||||
<span class="ml-3 inline-flex rounded-md shadow-sm">
|
||||
<button type="submit"
|
||||
@click.prevent="saveUser"
|
||||
class="inline-flex justify-center py-2 px-4 border border-transparent text-sm leading-5 font-medium rounded-md text-white bg-indigo-600 hover:bg-indigo-500 focus:outline-none focus:border-indigo-700 focus:shadow-outline-indigo active:bg-indigo-700 transition duration-150 ease-in-out">
|
||||
Save
|
||||
</button>
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</form>
|
||||
<div class="-my-2 py-2 overflow-x-auto sm:-mx-6 sm:px-6 lg:-mx-8 lg:px-8">
|
||||
<div
|
||||
class="align-middle inline-block min-w-full shadow overflow-hidden sm:rounded-lg border-b border-gray-200">
|
||||
<table class="min-w-full" v-if="apps.length > 0">
|
||||
<thead>
|
||||
<tr>
|
||||
<th class="px-6 py-3 border-b border-gray-200 bg-gray-50 text-left text-xs leading-4 font-medium text-gray-500 uppercase tracking-wider">
|
||||
Name
|
||||
</th>
|
||||
<th class="px-6 py-3 border-b border-gray-200 bg-gray-50 text-left text-xs leading-4 font-medium text-gray-500 uppercase tracking-wider">
|
||||
Allowed origins
|
||||
</th>
|
||||
<th class="px-6 py-3 border-b border-gray-200 bg-gray-50 text-left text-xs leading-4 font-medium text-gray-500 uppercase tracking-wider">
|
||||
Statistics
|
||||
</th>
|
||||
<th class="px-6 py-3 border-b border-gray-200 bg-gray-50 text-left text-xs leading-4 font-medium text-gray-500 uppercase tracking-wider">
|
||||
Client Messages
|
||||
</th>
|
||||
<th class="px-6 py-3 border-b border-gray-200 bg-gray-50"></th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody class="bg-white">
|
||||
<tr v-for="app in apps">
|
||||
<td class="px-6 py-4 whitespace-no-wrap border-b border-gray-200 text-sm leading-5 font-medium text-gray-900">
|
||||
@{{ app.name }}
|
||||
</td>
|
||||
<td class="px-6 py-4 whitespace-no-wrap border-b border-gray-200 text-sm leading-5 text-gray-500">
|
||||
@{{ app.allowed_origins || '*' }}
|
||||
</td>
|
||||
<td class="px-6 py-4 whitespace-no-wrap border-b border-gray-200 text-sm leading-5 text-gray-500">
|
||||
<span v-if="app.enable_statistics">Yes</span>
|
||||
<span v-else>No</span>
|
||||
</td>
|
||||
<td class="px-6 py-4 whitespace-no-wrap border-b border-gray-200 text-sm leading-5 text-gray-500">
|
||||
<span v-if="app.enable_client_messages">Yes</span>
|
||||
<span v-else>No</span>
|
||||
</td>
|
||||
<td class="px-6 py-4 whitespace-no-wrap text-right border-b border-gray-200 text-sm leading-5 font-medium">
|
||||
<a href="#" @click.prevent="showInstructions(app)"
|
||||
class="pl-4 text-gray-600 hover:text-gray-900">Installation instructions</a>
|
||||
<a href="#" @click.prevent="deleteUser(user)"
|
||||
class="pl-4 text-red-600 hover:text-red-900">Delete</a>
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="py-4" v-if="app">
|
||||
<p class="pb-1">Modify your <code>.env</code> file:</p>
|
||||
<pre class="bg-gray-100 p-4 rounded">PUSHER_APP_HOST=@{{ app.host === null ? window.location.hostname : app.host }}
|
||||
PUSHER_APP_PORT={{ $port }}
|
||||
PUSHER_APP_KEY=@{{ app.key }}
|
||||
PUSHER_APP_ID=@{{ app.id }}
|
||||
PUSHER_APP_SECRET=@{{ app.secret }}
|
||||
PUSHER_APP_SCHEME=https
|
||||
MIX_PUSHER_APP_HOST="${PUSHER_APP_HOST}"
|
||||
MIX_PUSHER_APP_PORT="${PUSHER_APP_PORT}"
|
||||
MIX_PUSHER_APP_KEY="${PUSHER_APP_KEY}"</pre>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
@endsection
|
||||
|
||||
@section('scripts')
|
||||
<script>
|
||||
new Vue({
|
||||
el: '#app',
|
||||
data: {
|
||||
app: null,
|
||||
apps: @json($apps),
|
||||
},
|
||||
methods: {
|
||||
showInstructions(app) {
|
||||
this.app = app;
|
||||
}
|
||||
}
|
||||
});
|
||||
</script>
|
||||
@endsection
|
||||
|
|
@ -1,32 +1,13 @@
|
|||
<html>
|
||||
<head>
|
||||
<meta http-equiv="Content-Type" content="text/html; charset=ISO-8859-1">
|
||||
@extends('websockets::layout')
|
||||
|
||||
<title>WebSockets Dashboard</title>
|
||||
@section('title')
|
||||
Dashboard
|
||||
@endsection
|
||||
|
||||
<link href="https://unpkg.com/tailwindcss@^1.0/dist/tailwind.min.css" rel="stylesheet">
|
||||
<link href="https://cdn.jsdelivr.net/npm/vue-json-editor@1.4.2/assets/jsoneditor.min.css" rel="stylesheet">
|
||||
|
||||
<script src="https://js.pusher.com/7.0/pusher.min.js"></script>
|
||||
<script src="https://cdn.jsdelivr.net/npm/vue@2.5.17/dist/vue.min.js"></script>
|
||||
<script src="https://cdn.plot.ly/plotly-latest.min.js"></script>
|
||||
<script src="https://cdn.jsdelivr.net/npm/axios/dist/axios.min.js"></script>
|
||||
<script src="https://cdn.jsdelivr.net/npm/v-jsoneditor@1.4.1/dist/v-jsoneditor.min.js"></script>
|
||||
|
||||
<script>
|
||||
window.baseURL = '{{ url(request()->path()) }}';
|
||||
axios.defaults.baseURL = baseURL;
|
||||
</script>
|
||||
</head>
|
||||
|
||||
<body class="px-6">
|
||||
@section('content')
|
||||
<div
|
||||
id="app"
|
||||
class="mx-auto"
|
||||
:class="{
|
||||
'max-w-xl': ! connected,
|
||||
'max-w-6xl': connected,
|
||||
}"
|
||||
>
|
||||
<div class="w-full my-6 rounded-lg bg-gray-100 p-6">
|
||||
<div class="font-semibold uppercase text-gray-700 mb-6">
|
||||
|
|
@ -235,7 +216,10 @@
|
|||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<script>
|
||||
@endsection
|
||||
|
||||
@section('scripts')
|
||||
<script>
|
||||
new Vue({
|
||||
el: '#app',
|
||||
data: {
|
||||
|
|
@ -447,6 +431,5 @@
|
|||
},
|
||||
},
|
||||
});
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
</script>
|
||||
@endsection
|
||||
|
|
|
|||
|
|
@ -0,0 +1,93 @@
|
|||
<html>
|
||||
<head>
|
||||
<meta http-equiv="Content-Type" content="text/html; charset=ISO-8859-1">
|
||||
|
||||
<title>Laravel WebSockets</title>
|
||||
|
||||
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/@tailwindcss/ui@latest/dist/tailwind-ui.min.css">
|
||||
<link href="https://cdn.jsdelivr.net/npm/vue-json-editor@1.4.2/assets/jsoneditor.min.css" rel="stylesheet">
|
||||
|
||||
<script src="https://js.pusher.com/7.0/pusher.min.js"></script>
|
||||
<script src="https://cdn.jsdelivr.net/npm/vue@2.5.17/dist/vue.min.js"></script>
|
||||
<script src="https://cdn.plot.ly/plotly-latest.min.js"></script>
|
||||
<script src="https://cdn.jsdelivr.net/npm/axios/dist/axios.min.js"></script>
|
||||
<script src="https://cdn.jsdelivr.net/npm/v-jsoneditor@1.4.1/dist/v-jsoneditor.min.js"></script>
|
||||
|
||||
<script>
|
||||
window.baseURL = '{{ url(request()->path()) }}';
|
||||
axios.defaults.baseURL = baseURL;
|
||||
</script>
|
||||
</head>
|
||||
|
||||
<body>
|
||||
<div class="min-h-screen bg-white">
|
||||
<nav class="bg-white border-b border-gray-200">
|
||||
<div class="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8">
|
||||
<div class="flex justify-between h-16">
|
||||
<div class="flex">
|
||||
<div class="flex-shrink-0 flex items-center font-bold">
|
||||
Laravel WebSockets
|
||||
</div>
|
||||
<div class="hidden sm:-my-px sm:ml-6 sm:flex">
|
||||
<a href="{{ route('laravel-websockets.dashboard') }}"
|
||||
class="
|
||||
@if(Route::is('laravel-websockets.dashboard'))
|
||||
border-indigo-500 focus:border-indigo-700 text-gray-900
|
||||
@else
|
||||
border-transparent text-gray-500 hover:text-gray-700 hover:border-gray-300 focus:text-gray-700 focus:border-gray-300
|
||||
@endif
|
||||
inline-flex items-center px-1 pt-1 border-b-2 text-sm font-medium leading-5 focus:outline-none transition duration-150 ease-in-out">
|
||||
Dashboard
|
||||
</a>
|
||||
<a href="{{ route('laravel-websockets.apps') }}"
|
||||
class="
|
||||
@if(Route::is('laravel-websockets.apps'))
|
||||
border-indigo-500 focus:border-indigo-700 text-gray-900
|
||||
@else
|
||||
border-transparent text-gray-500 hover:text-gray-700 hover:border-gray-300 focus:text-gray-700 focus:border-gray-300
|
||||
@endif
|
||||
ml-8 inline-flex items-center px-1 pt-1 border-b-2 text-sm font-medium leading-5 focus:outline-none transition duration-150 ease-in-out">
|
||||
Apps
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
<div class="-mr-2 flex items-center sm:hidden">
|
||||
<!-- Mobile menu button -->
|
||||
<button
|
||||
class="inline-flex items-center justify-center p-2 rounded-md text-gray-400 hover:text-gray-500 hover:bg-gray-100 focus:outline-none focus:bg-gray-100 focus:text-gray-500 transition duration-150 ease-in-out">
|
||||
<!-- Menu open: "hidden", Menu closed: "block" -->
|
||||
<svg class="block h-6 w-6" stroke="currentColor" fill="none" viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2"
|
||||
d="M4 6h16M4 12h16M4 18h16"/>
|
||||
</svg>
|
||||
<!-- Menu open: "block", Menu closed: "hidden" -->
|
||||
<svg class="hidden h-6 w-6" stroke="currentColor" fill="none" viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2"
|
||||
d="M6 18L18 6M6 6l12 12"/>
|
||||
</svg>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</nav>
|
||||
|
||||
|
||||
<div class="py-10">
|
||||
<header>
|
||||
<div class="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8">
|
||||
<h1 class="text-3xl font-bold leading-tight text-gray-900">
|
||||
@yield('title')
|
||||
</h1>
|
||||
</div>
|
||||
</header>
|
||||
<main>
|
||||
<div class="max-w-7xl mx-auto sm:px-6 lg:px-8">
|
||||
@yield('content')
|
||||
</div>
|
||||
</main>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@yield('scripts')
|
||||
</body>
|
||||
</html>
|
||||
|
|
@ -17,6 +17,7 @@ use Psr\Http\Message\RequestInterface;
|
|||
use Pusher\Pusher;
|
||||
use Ratchet\ConnectionInterface;
|
||||
use Ratchet\Http\HttpServerInterface;
|
||||
use React\Promise\Deferred;
|
||||
use React\Promise\PromiseInterface;
|
||||
use Symfony\Bridge\PsrHttpMessage\Factory\HttpFoundationFactory;
|
||||
use Symfony\Component\HttpKernel\Exception\HttpException;
|
||||
|
|
@ -52,13 +53,6 @@ abstract class Controller implements HttpServerInterface
|
|||
*/
|
||||
protected $channelManager;
|
||||
|
||||
/**
|
||||
* The app attached with this request.
|
||||
*
|
||||
* @var \BeyondCode\LaravelWebSockets\Apps\App|null
|
||||
*/
|
||||
protected $app;
|
||||
|
||||
/**
|
||||
* Initialize the request.
|
||||
*
|
||||
|
|
@ -184,11 +178,25 @@ abstract class Controller implements HttpServerInterface
|
|||
|
||||
$laravelRequest = Request::createFromBase((new HttpFoundationFactory)->createRequest($serverRequest));
|
||||
|
||||
$this->ensureValidAppId($laravelRequest->get('appId'))
|
||||
->ensureValidSignature($laravelRequest);
|
||||
$this
|
||||
->ensureValidAppId($laravelRequest->appId)
|
||||
->then(function ($app) use ($laravelRequest, $connection) {
|
||||
try {
|
||||
$this->ensureValidSignature($app, $laravelRequest);
|
||||
} catch (HttpException $exception) {
|
||||
$this->onError($connection, $exception);
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
// Invoke the controller action
|
||||
try {
|
||||
$response = $this($laravelRequest);
|
||||
} catch (HttpException $exception) {
|
||||
$this->onError($connection, $exception);
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
// Allow for async IO in the controller action
|
||||
if ($response instanceof PromiseInterface) {
|
||||
|
|
@ -200,10 +208,13 @@ abstract class Controller implements HttpServerInterface
|
|||
}
|
||||
|
||||
if ($response instanceof HttpException) {
|
||||
throw $response;
|
||||
$this->onError($connection, $response);
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
$this->sendAndClose($connection, $response);
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
@ -222,29 +233,34 @@ abstract class Controller implements HttpServerInterface
|
|||
* Ensure app existence.
|
||||
*
|
||||
* @param mixed $appId
|
||||
* @return $this
|
||||
* @return PromiseInterface
|
||||
*
|
||||
* @throws \Symfony\Component\HttpKernel\Exception\HttpException
|
||||
*/
|
||||
public function ensureValidAppId($appId)
|
||||
{
|
||||
if (! $appId || ! $this->app = App::findById($appId)) {
|
||||
$deferred = new Deferred();
|
||||
|
||||
App::findById($appId)
|
||||
->then(function ($app) use ($appId, $deferred) {
|
||||
if (! $app) {
|
||||
throw new HttpException(401, "Unknown app id `{$appId}` provided.");
|
||||
}
|
||||
$deferred->resolve($app);
|
||||
});
|
||||
|
||||
return $this;
|
||||
return $deferred->promise();
|
||||
}
|
||||
|
||||
/**
|
||||
* Ensure signature integrity coming from an
|
||||
* authorized application.
|
||||
*
|
||||
* @param \GuzzleHttp\Psr7\ServerRequest $request
|
||||
* @param App $app
|
||||
* @param Request $request
|
||||
* @return $this
|
||||
*
|
||||
* @throws \Symfony\Component\HttpKernel\Exception\HttpException
|
||||
*/
|
||||
protected function ensureValidSignature(Request $request)
|
||||
protected function ensureValidSignature(App $app, Request $request)
|
||||
{
|
||||
// The `auth_signature` & `body_md5` parameters are not included when calculating the `auth_signature` value.
|
||||
// The `appId`, `appKey` & `channelName` parameters are actually route parameters and are never supplied by the client.
|
||||
|
|
@ -261,7 +277,7 @@ abstract class Controller implements HttpServerInterface
|
|||
|
||||
$signature = "{$request->getMethod()}\n/{$request->path()}\n".Pusher::array_implode('=', '&', $params);
|
||||
|
||||
$authSignature = hash_hmac('sha256', $signature, $this->app->secret);
|
||||
$authSignature = hash_hmac('sha256', $signature, $app->secret);
|
||||
|
||||
if ($authSignature !== $request->get('auth_signature')) {
|
||||
throw new HttpException(401, 'Invalid auth signature provided.');
|
||||
|
|
|
|||
|
|
@ -5,6 +5,7 @@ namespace BeyondCode\LaravelWebSockets\API;
|
|||
use BeyondCode\LaravelWebSockets\DashboardLogger;
|
||||
use BeyondCode\LaravelWebSockets\Facades\StatisticsCollector;
|
||||
use Illuminate\Http\Request;
|
||||
use React\Promise\Deferred;
|
||||
|
||||
class TriggerEvent extends Controller
|
||||
{
|
||||
|
|
@ -16,11 +17,15 @@ class TriggerEvent extends Controller
|
|||
*/
|
||||
public function __invoke(Request $request)
|
||||
{
|
||||
if ($request->has('channel')) {
|
||||
$channels = [$request->get('channel')];
|
||||
} else {
|
||||
$channels = $request->channels ?: [];
|
||||
|
||||
if (is_string($channels)) {
|
||||
$channels = [$channels];
|
||||
}
|
||||
}
|
||||
|
||||
foreach ($channels as $channelName) {
|
||||
// Here you can use the ->find(), even if the channel
|
||||
|
|
@ -49,7 +54,11 @@ class TriggerEvent extends Controller
|
|||
$request->appId, $request->socket_id, $channelName, (object) $payload
|
||||
);
|
||||
|
||||
if ($this->app->statisticsEnabled) {
|
||||
$deferred = new Deferred();
|
||||
|
||||
$this->ensureValidAppId($request->appId)
|
||||
->then(function ($app) use ($request, $channelName, $deferred) {
|
||||
if ($app->statisticsEnabled) {
|
||||
StatisticsCollector::apiMessage($request->appId);
|
||||
}
|
||||
|
||||
|
|
@ -58,8 +67,11 @@ class TriggerEvent extends Controller
|
|||
'channel' => $channelName,
|
||||
'payload' => $request->data,
|
||||
]);
|
||||
|
||||
$deferred->resolve((object) []);
|
||||
});
|
||||
}
|
||||
|
||||
return (object) [];
|
||||
return $deferred->promise();
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -3,6 +3,7 @@
|
|||
namespace BeyondCode\LaravelWebSockets\Apps;
|
||||
|
||||
use BeyondCode\LaravelWebSockets\Contracts\AppManager;
|
||||
use React\Promise\PromiseInterface;
|
||||
|
||||
class App
|
||||
{
|
||||
|
|
@ -40,7 +41,7 @@ class App
|
|||
* Find the app by id.
|
||||
*
|
||||
* @param string|int $appId
|
||||
* @return \BeyondCode\LaravelWebSockets\Apps\App|null
|
||||
* @return PromiseInterface
|
||||
*/
|
||||
public static function findById($appId)
|
||||
{
|
||||
|
|
@ -51,9 +52,9 @@ class App
|
|||
* Find the app by app key.
|
||||
*
|
||||
* @param string $appKey
|
||||
* @return \BeyondCode\LaravelWebSockets\Apps\App|null
|
||||
* @return PromiseInterface
|
||||
*/
|
||||
public static function findByKey($appKey): ?self
|
||||
public static function findByKey($appKey): PromiseInterface
|
||||
{
|
||||
return app(AppManager::class)->findByKey($appKey);
|
||||
}
|
||||
|
|
@ -62,9 +63,9 @@ class App
|
|||
* Find the app by app secret.
|
||||
*
|
||||
* @param string $appSecret
|
||||
* @return \BeyondCode\LaravelWebSockets\Apps\App|null
|
||||
* @return PromiseInterface
|
||||
*/
|
||||
public static function findBySecret($appSecret): ?self
|
||||
public static function findBySecret($appSecret): PromiseInterface
|
||||
{
|
||||
return app(AppManager::class)->findBySecret($appSecret);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -3,6 +3,8 @@
|
|||
namespace BeyondCode\LaravelWebSockets\Apps;
|
||||
|
||||
use BeyondCode\LaravelWebSockets\Contracts\AppManager;
|
||||
use React\Promise\PromiseInterface;
|
||||
use function React\Promise\resolve as resolvePromise;
|
||||
|
||||
class ConfigAppManager implements AppManager
|
||||
{
|
||||
|
|
@ -26,54 +28,64 @@ class ConfigAppManager implements AppManager
|
|||
/**
|
||||
* Get all apps.
|
||||
*
|
||||
* @return array[\BeyondCode\LaravelWebSockets\Apps\App]
|
||||
* @return PromiseInterface
|
||||
*/
|
||||
public function all(): array
|
||||
public function all(): PromiseInterface
|
||||
{
|
||||
return $this->apps
|
||||
return resolvePromise($this->apps
|
||||
->map(function (array $appAttributes) {
|
||||
return $this->convertIntoApp($appAttributes);
|
||||
})
|
||||
->toArray();
|
||||
->toArray());
|
||||
}
|
||||
|
||||
/**
|
||||
* Get app by id.
|
||||
*
|
||||
* @param string|int $appId
|
||||
* @return \BeyondCode\LaravelWebSockets\Apps\App|null
|
||||
* @return PromiseInterface
|
||||
*/
|
||||
public function findById($appId): ?App
|
||||
public function findById($appId): PromiseInterface
|
||||
{
|
||||
return $this->convertIntoApp(
|
||||
return resolvePromise($this->convertIntoApp(
|
||||
$this->apps->firstWhere('id', $appId)
|
||||
);
|
||||
));
|
||||
}
|
||||
|
||||
/**
|
||||
* Get app by app key.
|
||||
*
|
||||
* @param string $appKey
|
||||
* @return \BeyondCode\LaravelWebSockets\Apps\App|null
|
||||
* @return PromiseInterface
|
||||
*/
|
||||
public function findByKey($appKey): ?App
|
||||
public function findByKey($appKey): PromiseInterface
|
||||
{
|
||||
return $this->convertIntoApp(
|
||||
return resolvePromise($this->convertIntoApp(
|
||||
$this->apps->firstWhere('key', $appKey)
|
||||
);
|
||||
));
|
||||
}
|
||||
|
||||
/**
|
||||
* Get app by secret.
|
||||
*
|
||||
* @param string $appSecret
|
||||
* @return \BeyondCode\LaravelWebSockets\Apps\App|null
|
||||
* @return PromiseInterface
|
||||
*/
|
||||
public function findBySecret($appSecret): ?App
|
||||
public function findBySecret($appSecret): PromiseInterface
|
||||
{
|
||||
return $this->convertIntoApp(
|
||||
return resolvePromise($this->convertIntoApp(
|
||||
$this->apps->firstWhere('secret', $appSecret)
|
||||
);
|
||||
));
|
||||
}
|
||||
|
||||
/**
|
||||
* @inheritDoc
|
||||
*/
|
||||
public function createApp($appData): PromiseInterface
|
||||
{
|
||||
$this->apps->push($appData);
|
||||
|
||||
return resolvePromise();
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
@ -107,8 +119,8 @@ class ConfigAppManager implements AppManager
|
|||
}
|
||||
|
||||
$app
|
||||
->enableClientMessages($appAttributes['enable_client_messages'])
|
||||
->enableStatistics($appAttributes['enable_statistics'])
|
||||
->enableClientMessages((bool) $appAttributes['enable_client_messages'])
|
||||
->enableStatistics((bool) $appAttributes['enable_statistics'])
|
||||
->setCapacity($appAttributes['capacity'] ?? null)
|
||||
->setAllowedOrigins($appAttributes['allowed_origins'] ?? []);
|
||||
|
||||
|
|
|
|||
|
|
@ -0,0 +1,171 @@
|
|||
<?php
|
||||
|
||||
namespace BeyondCode\LaravelWebSockets\Apps;
|
||||
|
||||
use BeyondCode\LaravelWebSockets\Contracts\AppManager;
|
||||
use React\MySQL\ConnectionInterface;
|
||||
use React\MySQL\QueryResult;
|
||||
use React\Promise\Deferred;
|
||||
use React\Promise\PromiseInterface;
|
||||
|
||||
class MysqlAppManager implements AppManager
|
||||
{
|
||||
/**
|
||||
* The database connection.
|
||||
*
|
||||
* @var ConnectionInterface
|
||||
*/
|
||||
protected $database;
|
||||
|
||||
/**
|
||||
* Initialize the class.
|
||||
*
|
||||
* @param ConnectionInterface $database
|
||||
*/
|
||||
public function __construct(ConnectionInterface $database)
|
||||
{
|
||||
$this->database = $database;
|
||||
}
|
||||
|
||||
protected function getTableName(): string
|
||||
{
|
||||
return config('websockets.managers.mysql.table');
|
||||
}
|
||||
|
||||
/**
|
||||
* Get all apps.
|
||||
*
|
||||
* @return PromiseInterface
|
||||
*/
|
||||
public function all(): PromiseInterface
|
||||
{
|
||||
$deferred = new Deferred();
|
||||
|
||||
$this->database->query('SELECT * FROM `'.$this->getTableName().'`')
|
||||
->then(function (QueryResult $result) use ($deferred) {
|
||||
$deferred->resolve($result->resultRows);
|
||||
}, function ($error) use ($deferred) {
|
||||
$deferred->reject($error);
|
||||
});
|
||||
|
||||
return $deferred->promise();
|
||||
}
|
||||
|
||||
/**
|
||||
* Get app by id.
|
||||
*
|
||||
* @param string|int $appId
|
||||
* @return PromiseInterface
|
||||
*/
|
||||
public function findById($appId): PromiseInterface
|
||||
{
|
||||
$deferred = new Deferred();
|
||||
|
||||
$this->database->query('SELECT * from `'.$this->getTableName().'` WHERE `id` = ?', [$appId])
|
||||
->then(function (QueryResult $result) use ($deferred) {
|
||||
$deferred->resolve($this->convertIntoApp($result->resultRows[0]));
|
||||
}, function ($error) use ($deferred) {
|
||||
$deferred->reject($error);
|
||||
});
|
||||
|
||||
return $deferred->promise();
|
||||
}
|
||||
|
||||
/**
|
||||
* Get app by app key.
|
||||
*
|
||||
* @param string $appKey
|
||||
* @return PromiseInterface
|
||||
*/
|
||||
public function findByKey($appKey): PromiseInterface
|
||||
{
|
||||
$deferred = new Deferred();
|
||||
|
||||
$this->database->query('SELECT * from `'.$this->getTableName().'` WHERE `key` = ?', [$appKey])
|
||||
->then(function (QueryResult $result) use ($deferred) {
|
||||
$deferred->resolve($this->convertIntoApp($result->resultRows[0]));
|
||||
}, function ($error) use ($deferred) {
|
||||
$deferred->reject($error);
|
||||
});
|
||||
|
||||
return $deferred->promise();
|
||||
}
|
||||
|
||||
/**
|
||||
* Get app by secret.
|
||||
*
|
||||
* @param string $appSecret
|
||||
* @return PromiseInterface
|
||||
*/
|
||||
public function findBySecret($appSecret): PromiseInterface
|
||||
{
|
||||
$deferred = new Deferred();
|
||||
|
||||
$this->database->query('SELECT * from `'.$this->getTableName().'` WHERE `secret` = ?', [$appSecret])
|
||||
->then(function (QueryResult $result) use ($deferred) {
|
||||
$deferred->resolve($this->convertIntoApp($result->resultRows[0]));
|
||||
}, function ($error) use ($deferred) {
|
||||
$deferred->reject($error);
|
||||
});
|
||||
|
||||
return $deferred->promise();
|
||||
}
|
||||
|
||||
/**
|
||||
* Map the app into an App instance.
|
||||
*
|
||||
* @param array|null $app
|
||||
* @return \BeyondCode\LaravelWebSockets\Apps\App|null
|
||||
*/
|
||||
protected function convertIntoApp(?array $appAttributes): ?App
|
||||
{
|
||||
if (! $appAttributes) {
|
||||
return null;
|
||||
}
|
||||
|
||||
$app = new App(
|
||||
$appAttributes['id'],
|
||||
$appAttributes['key'],
|
||||
$appAttributes['secret']
|
||||
);
|
||||
|
||||
if (isset($appAttributes['name'])) {
|
||||
$app->setName($appAttributes['name']);
|
||||
}
|
||||
|
||||
if (isset($appAttributes['host'])) {
|
||||
$app->setHost($appAttributes['host']);
|
||||
}
|
||||
|
||||
if (isset($appAttributes['path'])) {
|
||||
$app->setPath($appAttributes['path']);
|
||||
}
|
||||
|
||||
$app
|
||||
->enableClientMessages((bool) $appAttributes['enable_client_messages'])
|
||||
->enableStatistics((bool) $appAttributes['enable_statistics'])
|
||||
->setCapacity($appAttributes['capacity'] ?? null)
|
||||
->setAllowedOrigins(array_filter(explode(',', $appAttributes['allowed_origins'])));
|
||||
|
||||
return $app;
|
||||
}
|
||||
|
||||
/**
|
||||
* @inheritDoc
|
||||
*/
|
||||
public function createApp($appData): PromiseInterface
|
||||
{
|
||||
$deferred = new Deferred();
|
||||
|
||||
$this->database->query(
|
||||
'INSERT INTO `'.$this->getTableName().'` (`id`, `key`, `secret`, `name`, `enable_client_messages`, `enable_statistics`, `allowed_origins`, `capacity`) VALUES (?, ?, ?, ?, ?, ?, ?, ?)',
|
||||
[$appData['id'], $appData['key'], $appData['secret'], $appData['name'], $appData['enable_client_messages'], $appData['enable_statistics'], $appData['allowed_origins'] ?? '', $appData['capacity'] ?? null])
|
||||
->then(function () use ($deferred) {
|
||||
$deferred->resolve();
|
||||
}, function ($error) use ($deferred) {
|
||||
$deferred->reject($error);
|
||||
});
|
||||
|
||||
return $deferred->promise();
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,167 @@
|
|||
<?php
|
||||
|
||||
namespace BeyondCode\LaravelWebSockets\Apps;
|
||||
|
||||
use BeyondCode\LaravelWebSockets\Contracts\AppManager;
|
||||
use Clue\React\SQLite\DatabaseInterface;
|
||||
use Clue\React\SQLite\Result;
|
||||
use React\Promise\Deferred;
|
||||
use React\Promise\PromiseInterface;
|
||||
|
||||
class SQLiteAppManager implements AppManager
|
||||
{
|
||||
/**
|
||||
* The database connection.
|
||||
*
|
||||
* @var DatabaseInterface
|
||||
*/
|
||||
protected $database;
|
||||
|
||||
/**
|
||||
* Initialize the class.
|
||||
*
|
||||
* @param DatabaseInterface $database
|
||||
*/
|
||||
public function __construct(DatabaseInterface $database)
|
||||
{
|
||||
$this->database = $database;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get all apps.
|
||||
*
|
||||
* @return PromiseInterface
|
||||
*/
|
||||
public function all(): PromiseInterface
|
||||
{
|
||||
$deferred = new Deferred();
|
||||
|
||||
$this->database->query('SELECT * FROM `apps`')
|
||||
->then(function (Result $result) use ($deferred) {
|
||||
$deferred->resolve($result->rows);
|
||||
}, function ($error) use ($deferred) {
|
||||
$deferred->reject($error);
|
||||
});
|
||||
|
||||
return $deferred->promise();
|
||||
}
|
||||
|
||||
/**
|
||||
* Get app by id.
|
||||
*
|
||||
* @param string|int $appId
|
||||
* @return PromiseInterface
|
||||
*/
|
||||
public function findById($appId): PromiseInterface
|
||||
{
|
||||
$deferred = new Deferred();
|
||||
|
||||
$this->database->query('SELECT * from apps WHERE `id` = :id', ['id' => $appId])
|
||||
->then(function (Result $result) use ($deferred) {
|
||||
$deferred->resolve($this->convertIntoApp($result->rows[0]));
|
||||
}, function ($error) use ($deferred) {
|
||||
$deferred->reject($error);
|
||||
});
|
||||
|
||||
return $deferred->promise();
|
||||
}
|
||||
|
||||
/**
|
||||
* Get app by app key.
|
||||
*
|
||||
* @param string $appKey
|
||||
* @return PromiseInterface
|
||||
*/
|
||||
public function findByKey($appKey): PromiseInterface
|
||||
{
|
||||
$deferred = new Deferred();
|
||||
|
||||
$this->database->query('SELECT * from apps WHERE `key` = :key', ['key' => $appKey])
|
||||
->then(function (Result $result) use ($deferred) {
|
||||
$deferred->resolve($this->convertIntoApp($result->rows[0]));
|
||||
}, function ($error) use ($deferred) {
|
||||
$deferred->reject($error);
|
||||
});
|
||||
|
||||
return $deferred->promise();
|
||||
}
|
||||
|
||||
/**
|
||||
* Get app by secret.
|
||||
*
|
||||
* @param string $appSecret
|
||||
* @return PromiseInterface
|
||||
*/
|
||||
public function findBySecret($appSecret): PromiseInterface
|
||||
{
|
||||
$deferred = new Deferred();
|
||||
|
||||
$this->database->query('SELECT * from apps WHERE `secret` = :secret', ['secret' => $appSecret])
|
||||
->then(function (Result $result) use ($deferred) {
|
||||
$deferred->resolve($this->convertIntoApp($result->rows[0]));
|
||||
}, function ($error) use ($deferred) {
|
||||
$deferred->reject($error);
|
||||
});
|
||||
|
||||
return $deferred->promise();
|
||||
}
|
||||
|
||||
/**
|
||||
* Map the app into an App instance.
|
||||
*
|
||||
* @param array|null $app
|
||||
* @return \BeyondCode\LaravelWebSockets\Apps\App|null
|
||||
*/
|
||||
protected function convertIntoApp(?array $appAttributes): ?App
|
||||
{
|
||||
if (! $appAttributes) {
|
||||
return null;
|
||||
}
|
||||
|
||||
$app = new App(
|
||||
$appAttributes['id'],
|
||||
$appAttributes['key'],
|
||||
$appAttributes['secret']
|
||||
);
|
||||
|
||||
if (isset($appAttributes['name'])) {
|
||||
$app->setName($appAttributes['name']);
|
||||
}
|
||||
|
||||
if (isset($appAttributes['host'])) {
|
||||
$app->setHost($appAttributes['host']);
|
||||
}
|
||||
|
||||
if (isset($appAttributes['path'])) {
|
||||
$app->setPath($appAttributes['path']);
|
||||
}
|
||||
|
||||
$app
|
||||
->enableClientMessages((bool) $appAttributes['enable_client_messages'])
|
||||
->enableStatistics((bool) $appAttributes['enable_statistics'])
|
||||
->setCapacity($appAttributes['capacity'] ?? null)
|
||||
->setAllowedOrigins(array_filter(explode(',', $appAttributes['allowed_origins'])));
|
||||
|
||||
return $app;
|
||||
}
|
||||
|
||||
/**
|
||||
* @inheritDoc
|
||||
*/
|
||||
public function createApp($appData): PromiseInterface
|
||||
{
|
||||
$deferred = new Deferred();
|
||||
|
||||
$this->database->query('
|
||||
INSERT INTO apps (id, key, secret, name, host, path, enable_client_messages, enable_statistics, capacity, allowed_origins)
|
||||
VALUES (:id, :key, :secret, :name, :host, :path, :enable_client_messages, :enable_statistics, :capacity, :allowed_origins)
|
||||
', $appData)
|
||||
->then(function (Result $result) use ($deferred) {
|
||||
$deferred->resolve();
|
||||
}, function ($error) use ($deferred) {
|
||||
$deferred->reject($error);
|
||||
});
|
||||
|
||||
return $deferred->promise();
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,55 @@
|
|||
<?php
|
||||
|
||||
namespace BeyondCode\LaravelWebSockets\Cache;
|
||||
|
||||
use BeyondCode\LaravelWebSockets\Helpers;
|
||||
use Illuminate\Cache\ArrayLock as LaravelLock;
|
||||
use React\Promise\PromiseInterface;
|
||||
|
||||
class ArrayLock extends Lock
|
||||
{
|
||||
/**
|
||||
* The parent array cache store.
|
||||
*
|
||||
* @var \Illuminate\Cache\ArrayStore
|
||||
*/
|
||||
protected $store;
|
||||
|
||||
/**
|
||||
* Internal Laravel Array Lock.
|
||||
*
|
||||
* @var \Illuminate\Cache\ArrayLock
|
||||
*/
|
||||
protected $lock;
|
||||
|
||||
/**
|
||||
* Create a new lock instance.
|
||||
*
|
||||
* @param \Illuminate\Cache\ArrayStore $store
|
||||
* @param string $name
|
||||
* @param int $seconds
|
||||
* @param string|null $owner
|
||||
* @return void
|
||||
*/
|
||||
public function __construct($store, $name, $seconds, $owner = null)
|
||||
{
|
||||
parent::__construct($name, $seconds, $owner);
|
||||
|
||||
$this->lock = new LaravelLock($store, $name, $seconds, $owner);
|
||||
}
|
||||
|
||||
public function acquire(): PromiseInterface
|
||||
{
|
||||
return Helpers::createFulfilledPromise($this->lock->acquire());
|
||||
}
|
||||
|
||||
public function get($callback = null): PromiseInterface
|
||||
{
|
||||
return $this->lock->get($callback);
|
||||
}
|
||||
|
||||
public function release(): PromiseInterface
|
||||
{
|
||||
return Helpers::createFulfilledPromise($this->lock->release());
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,46 @@
|
|||
<?php
|
||||
|
||||
namespace BeyondCode\LaravelWebSockets\Cache;
|
||||
|
||||
use Illuminate\Support\Str;
|
||||
use React\Promise\PromiseInterface;
|
||||
|
||||
abstract class Lock
|
||||
{
|
||||
/**
|
||||
* The name of the lock.
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
protected $name;
|
||||
|
||||
/**
|
||||
* The number of seconds the lock should be maintained.
|
||||
*
|
||||
* @var int
|
||||
*/
|
||||
protected $seconds;
|
||||
|
||||
/**
|
||||
* The scope identifier of this lock.
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
protected $owner;
|
||||
|
||||
public function __construct($name, $seconds, $owner = null)
|
||||
{
|
||||
if (is_null($owner)) {
|
||||
$owner = Str::random();
|
||||
}
|
||||
$this->name = $name;
|
||||
$this->seconds = $seconds;
|
||||
$this->owner = $owner;
|
||||
}
|
||||
|
||||
abstract public function acquire(): PromiseInterface;
|
||||
|
||||
abstract public function get($callback = null): PromiseInterface;
|
||||
|
||||
abstract public function release(): PromiseInterface;
|
||||
}
|
||||
|
|
@ -0,0 +1,69 @@
|
|||
<?php
|
||||
|
||||
namespace BeyondCode\LaravelWebSockets\Cache;
|
||||
|
||||
use Clue\React\Redis\Client;
|
||||
use Illuminate\Cache\LuaScripts;
|
||||
use React\Promise\Deferred;
|
||||
use React\Promise\PromiseInterface;
|
||||
|
||||
class RedisLock extends Lock
|
||||
{
|
||||
/**
|
||||
* The asynchronous redis client.
|
||||
*
|
||||
* @var Client
|
||||
*/
|
||||
protected $redis;
|
||||
|
||||
public function __construct(Client $redis, $name, $seconds, $owner = null)
|
||||
{
|
||||
parent::__construct($name, $seconds, $owner);
|
||||
|
||||
$this->redis = $redis;
|
||||
}
|
||||
|
||||
public function acquire(): PromiseInterface
|
||||
{
|
||||
$promise = new Deferred();
|
||||
|
||||
if ($this->seconds > 0) {
|
||||
$this->redis
|
||||
->set($this->name, $this->owner, 'EX', $this->seconds, 'NX')
|
||||
->then(function ($result) use ($promise) {
|
||||
$promise->resolve($result === true);
|
||||
});
|
||||
} else {
|
||||
$this->redis
|
||||
->setnx($this->name, $this->owner)
|
||||
->then(function ($result) use ($promise) {
|
||||
$promise->resolve($result === 1);
|
||||
});
|
||||
}
|
||||
|
||||
return $promise->promise();
|
||||
}
|
||||
|
||||
public function get($callback = null): PromiseInterface
|
||||
{
|
||||
$promise = new Deferred();
|
||||
|
||||
$this->acquire()
|
||||
->then(function ($result) use ($callback, $promise) {
|
||||
if ($result) {
|
||||
try {
|
||||
$callback();
|
||||
} finally {
|
||||
$promise->resolve($this->release());
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
return $promise->promise();
|
||||
}
|
||||
|
||||
public function release(): PromiseInterface
|
||||
{
|
||||
return $this->redis->eval(LuaScripts::releaseLock(), 1, $this->name, $this->owner);
|
||||
}
|
||||
}
|
||||
|
|
@ -2,17 +2,18 @@
|
|||
|
||||
namespace BeyondCode\LaravelWebSockets\ChannelManagers;
|
||||
|
||||
use BeyondCode\LaravelWebSockets\Cache\ArrayLock;
|
||||
use BeyondCode\LaravelWebSockets\Channels\Channel;
|
||||
use BeyondCode\LaravelWebSockets\Channels\PresenceChannel;
|
||||
use BeyondCode\LaravelWebSockets\Channels\PrivateChannel;
|
||||
use BeyondCode\LaravelWebSockets\Contracts\ChannelManager;
|
||||
use BeyondCode\LaravelWebSockets\Helpers;
|
||||
use Carbon\Carbon;
|
||||
use Illuminate\Cache\ArrayLock;
|
||||
use Illuminate\Cache\ArrayStore;
|
||||
use Illuminate\Support\Str;
|
||||
use Ratchet\ConnectionInterface;
|
||||
use React\EventLoop\LoopInterface;
|
||||
use function React\Promise\all;
|
||||
use React\Promise\PromiseInterface;
|
||||
use stdClass;
|
||||
|
||||
|
|
@ -226,9 +227,7 @@ class LocalChannelManager implements ChannelManager
|
|||
{
|
||||
$channel = $this->findOrCreate($connection->app->id, $channelName);
|
||||
|
||||
return Helpers::createFulfilledPromise(
|
||||
$channel->unsubscribe($connection, $payload)
|
||||
);
|
||||
return $channel->unsubscribe($connection, $payload);
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
@ -439,26 +438,24 @@ class LocalChannelManager implements ChannelManager
|
|||
*/
|
||||
public function removeObsoleteConnections(): PromiseInterface
|
||||
{
|
||||
$lock = $this->lock();
|
||||
try {
|
||||
if (! $lock->acquire()) {
|
||||
return Helpers::createFulfilledPromise(false);
|
||||
}
|
||||
return $this->lock()->get(function () {
|
||||
return $this->getLocalConnections()
|
||||
->then(function ($connections) {
|
||||
$promises = [];
|
||||
|
||||
$this->getLocalConnections()->then(function ($connections) {
|
||||
foreach ($connections as $connection) {
|
||||
$differenceInSeconds = $connection->lastPongedAt->diffInSeconds(Carbon::now());
|
||||
|
||||
if ($differenceInSeconds > 120) {
|
||||
$this->unsubscribeFromAllChannels($connection);
|
||||
$promises[] = $this->unsubscribeFromAllChannels($connection);
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
return Helpers::createFulfilledPromise(true);
|
||||
} finally {
|
||||
optional($lock)->forceRelease();
|
||||
}
|
||||
return all($promises);
|
||||
})->then(function () {
|
||||
$this->lock()->release();
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
@ -557,7 +554,7 @@ class LocalChannelManager implements ChannelManager
|
|||
/**
|
||||
* Get a new ArrayLock instance to avoid race conditions.
|
||||
*
|
||||
* @return \Illuminate\Cache\CacheLock
|
||||
* @return ArrayLock
|
||||
*/
|
||||
protected function lock()
|
||||
{
|
||||
|
|
|
|||
|
|
@ -2,17 +2,19 @@
|
|||
|
||||
namespace BeyondCode\LaravelWebSockets\ChannelManagers;
|
||||
|
||||
use BeyondCode\LaravelWebSockets\Cache\RedisLock;
|
||||
use BeyondCode\LaravelWebSockets\Channels\Channel;
|
||||
use BeyondCode\LaravelWebSockets\DashboardLogger;
|
||||
use BeyondCode\LaravelWebSockets\Helpers;
|
||||
use BeyondCode\LaravelWebSockets\Server\MockableConnection;
|
||||
use Carbon\Carbon;
|
||||
use Clue\React\Redis\Client;
|
||||
use Clue\React\Redis\Factory;
|
||||
use Illuminate\Cache\RedisLock;
|
||||
use Illuminate\Support\Facades\Redis;
|
||||
use Illuminate\Support\Str;
|
||||
use Ratchet\ConnectionInterface;
|
||||
use React\EventLoop\LoopInterface;
|
||||
use function React\Promise\all;
|
||||
use React\Promise\PromiseInterface;
|
||||
use stdClass;
|
||||
|
||||
|
|
@ -100,9 +102,12 @@ class RedisChannelManager extends LocalChannelManager
|
|||
{
|
||||
return $this->getGlobalChannels($connection->app->id)
|
||||
->then(function ($channels) use ($connection) {
|
||||
$promises = [];
|
||||
foreach ($channels as $channel) {
|
||||
$this->unsubscribeFromChannel($connection, $channel, new stdClass);
|
||||
$promises[] = $this->unsubscribeFromChannel($connection, $channel, new stdClass);
|
||||
}
|
||||
|
||||
return all($promises);
|
||||
})
|
||||
->then(function () use ($connection) {
|
||||
return parent::unsubscribeFromAllChannels($connection);
|
||||
|
|
@ -144,19 +149,37 @@ class RedisChannelManager extends LocalChannelManager
|
|||
*/
|
||||
public function unsubscribeFromChannel(ConnectionInterface $connection, string $channelName, stdClass $payload): PromiseInterface
|
||||
{
|
||||
return parent::unsubscribeFromChannel($connection, $channelName, $payload)
|
||||
->then(function () use ($connection, $channelName) {
|
||||
return $this->decrementSubscriptionsCount($connection->app->id, $channelName);
|
||||
})
|
||||
return $this->getGlobalConnectionsCount($connection->app->id, $channelName)
|
||||
->then(function ($count) use ($connection, $channelName) {
|
||||
if ($count === 0) {
|
||||
// Make sure to not stay subscribed to the PubSub topic
|
||||
// if there are no connections.
|
||||
return $this->unsubscribeFromTopic($connection->app->id, $channelName);
|
||||
}
|
||||
|
||||
return Helpers::createFulfilledPromise(null);
|
||||
})
|
||||
->then(function () use ($connection, $channelName) {
|
||||
return $this->decrementSubscriptionsCount($connection->app->id, $channelName)
|
||||
->then(function ($count) use ($connection, $channelName) {
|
||||
$this->removeConnectionFromSet($connection);
|
||||
// If the total connections count gets to 0 after unsubscribe,
|
||||
// try again to check & unsubscribe from the PubSub topic if needed.
|
||||
if ($count < 1) {
|
||||
$this->removeChannelFromSet($connection->app->id, $channelName);
|
||||
$this->unsubscribeFromTopic($connection->app->id, $channelName);
|
||||
$promises = [];
|
||||
|
||||
$promises[] = $this->unsubscribeFromTopic($connection->app->id, $channelName);
|
||||
$promises[] = $this->removeChannelFromSet($connection->app->id, $channelName);
|
||||
|
||||
return all($promises);
|
||||
}
|
||||
});
|
||||
})
|
||||
->then(function () use ($connection) {
|
||||
return $this->removeConnectionFromSet($connection);
|
||||
})
|
||||
->then(function () use ($connection, $channelName, $payload) {
|
||||
return parent::unsubscribeFromChannel($connection, $channelName, $payload);
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
@ -371,23 +394,21 @@ class RedisChannelManager extends LocalChannelManager
|
|||
*/
|
||||
public function removeObsoleteConnections(): PromiseInterface
|
||||
{
|
||||
$lock = $this->lock();
|
||||
try {
|
||||
$lock->get(function () {
|
||||
$this->getConnectionsFromSet(0, now()->subMinutes(2)->format('U'))
|
||||
return $this->lock()->get(function () {
|
||||
return $this->getConnectionsFromSet(0, now()->subMinutes(2)->format('U'))
|
||||
->then(function ($connections) {
|
||||
$promises = [];
|
||||
foreach ($connections as $socketId => $appId) {
|
||||
$connection = $this->fakeConnectionForApp($appId, $socketId);
|
||||
|
||||
$this->unsubscribeFromAllChannels($connection);
|
||||
$promises[] = $this->unsubscribeFromAllChannels($connection);
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
return all($promises);
|
||||
});
|
||||
})->then(function () {
|
||||
return parent::removeObsoleteConnections();
|
||||
} finally {
|
||||
optional($lock)->forceRelease();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
@ -846,11 +867,11 @@ class RedisChannelManager extends LocalChannelManager
|
|||
/**
|
||||
* Get a new RedisLock instance to avoid race conditions.
|
||||
*
|
||||
* @return \Illuminate\Cache\CacheLock
|
||||
* @return RedisLock
|
||||
*/
|
||||
protected function lock()
|
||||
{
|
||||
return new RedisLock($this->redis, static::$lockName, 0);
|
||||
return new RedisLock($this->publishClient, static::$lockName, 0);
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
|
|||
|
|
@ -6,9 +6,11 @@ use BeyondCode\LaravelWebSockets\Contracts\ChannelManager;
|
|||
use BeyondCode\LaravelWebSockets\DashboardLogger;
|
||||
use BeyondCode\LaravelWebSockets\Events\SubscribedToChannel;
|
||||
use BeyondCode\LaravelWebSockets\Events\UnsubscribedFromChannel;
|
||||
use BeyondCode\LaravelWebSockets\Helpers;
|
||||
use BeyondCode\LaravelWebSockets\Server\Exceptions\InvalidSignature;
|
||||
use Illuminate\Support\Str;
|
||||
use Ratchet\ConnectionInterface;
|
||||
use React\Promise\PromiseInterface;
|
||||
use stdClass;
|
||||
|
||||
class Channel
|
||||
|
|
@ -116,12 +118,12 @@ class Channel
|
|||
* Unsubscribe connection from the channel.
|
||||
*
|
||||
* @param \Ratchet\ConnectionInterface $connection
|
||||
* @return bool
|
||||
* @return PromiseInterface
|
||||
*/
|
||||
public function unsubscribe(ConnectionInterface $connection): bool
|
||||
public function unsubscribe(ConnectionInterface $connection): PromiseInterface
|
||||
{
|
||||
if (! $this->hasConnection($connection)) {
|
||||
return false;
|
||||
return Helpers::createFulfilledPromise(false);
|
||||
}
|
||||
|
||||
unset($this->connections[$connection->socketId]);
|
||||
|
|
@ -132,7 +134,7 @@ class Channel
|
|||
$this->getName()
|
||||
);
|
||||
|
||||
return true;
|
||||
return Helpers::createFulfilledPromise(true);
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
|
|||
|
|
@ -5,8 +5,10 @@ namespace BeyondCode\LaravelWebSockets\Channels;
|
|||
use BeyondCode\LaravelWebSockets\DashboardLogger;
|
||||
use BeyondCode\LaravelWebSockets\Events\SubscribedToChannel;
|
||||
use BeyondCode\LaravelWebSockets\Events\UnsubscribedFromChannel;
|
||||
use BeyondCode\LaravelWebSockets\Helpers;
|
||||
use BeyondCode\LaravelWebSockets\Server\Exceptions\InvalidSignature;
|
||||
use Ratchet\ConnectionInterface;
|
||||
use React\Promise\PromiseInterface;
|
||||
use stdClass;
|
||||
|
||||
class PresenceChannel extends PrivateChannel
|
||||
|
|
@ -100,30 +102,30 @@ class PresenceChannel extends PrivateChannel
|
|||
* Unsubscribe connection from the channel.
|
||||
*
|
||||
* @param \Ratchet\ConnectionInterface $connection
|
||||
* @return bool
|
||||
* @return PromiseInterface
|
||||
*/
|
||||
public function unsubscribe(ConnectionInterface $connection): bool
|
||||
public function unsubscribe(ConnectionInterface $connection): PromiseInterface
|
||||
{
|
||||
$truth = parent::unsubscribe($connection);
|
||||
|
||||
$this->channelManager
|
||||
return $this->channelManager
|
||||
->getChannelMember($connection, $this->getName())
|
||||
->then(function ($user) {
|
||||
return @json_decode($user);
|
||||
})
|
||||
->then(function ($user) use ($connection) {
|
||||
if (! $user) {
|
||||
return;
|
||||
return Helpers::createFulfilledPromise(true);
|
||||
}
|
||||
|
||||
$this->channelManager
|
||||
return $this->channelManager
|
||||
->userLeftPresenceChannel($connection, $user, $this->getName())
|
||||
->then(function () use ($connection, $user) {
|
||||
// The `pusher_internal:member_removed` is triggered when a user leaves a channel.
|
||||
// It's quite possible that a user can have multiple connections to the same channel
|
||||
// (for example by having multiple browser tabs open)
|
||||
// and in this case the events will only be triggered when the last one is closed.
|
||||
$this->channelManager
|
||||
return $this->channelManager
|
||||
->getMemberSockets($user->user_id, $connection->app->id, $this->getName())
|
||||
->then(function ($sockets) use ($connection, $user) {
|
||||
if (count($sockets) === 0) {
|
||||
|
|
@ -149,8 +151,9 @@ class PresenceChannel extends PrivateChannel
|
|||
}
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
})
|
||||
->then(function () use ($truth) {
|
||||
return $truth;
|
||||
});
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -12,6 +12,8 @@ use BeyondCode\LaravelWebSockets\ServerFactory;
|
|||
use Illuminate\Console\Command;
|
||||
use Illuminate\Support\Facades\Cache;
|
||||
use React\EventLoop\Factory as LoopFactory;
|
||||
use React\EventLoop\LoopInterface;
|
||||
use function React\Promise\all;
|
||||
|
||||
class StartServer extends Command
|
||||
{
|
||||
|
|
@ -69,6 +71,10 @@ class StartServer extends Command
|
|||
*/
|
||||
public function handle()
|
||||
{
|
||||
$this->laravel->singleton(LoopInterface::class, function () {
|
||||
return $this->loop;
|
||||
});
|
||||
|
||||
$this->configureLoggers();
|
||||
|
||||
$this->configureManagers();
|
||||
|
|
@ -311,9 +317,13 @@ class StartServer extends Command
|
|||
// be automatically be unsubscribed from all channels.
|
||||
$channelManager->getLocalConnections()
|
||||
->then(function ($connections) {
|
||||
foreach ($connections as $connection) {
|
||||
return all(collect($connections)->map(function ($connection) {
|
||||
return app('websockets.handler')
|
||||
->onClose($connection)
|
||||
->then(function () use ($connection) {
|
||||
$connection->close();
|
||||
}
|
||||
});
|
||||
})->toArray());
|
||||
})
|
||||
->then(function () {
|
||||
$this->loop->stop();
|
||||
|
|
|
|||
|
|
@ -3,37 +3,46 @@
|
|||
namespace BeyondCode\LaravelWebSockets\Contracts;
|
||||
|
||||
use BeyondCode\LaravelWebSockets\Apps\App;
|
||||
use React\Promise\PromiseInterface;
|
||||
|
||||
interface AppManager
|
||||
{
|
||||
/**
|
||||
* Get all apps.
|
||||
*
|
||||
* @return array[\BeyondCode\LaravelWebSockets\Apps\App]
|
||||
* @return PromiseInterface
|
||||
*/
|
||||
public function all(): array;
|
||||
public function all(): PromiseInterface;
|
||||
|
||||
/**
|
||||
* Get app by id.
|
||||
*
|
||||
* @param string|int $appId
|
||||
* @return \BeyondCode\LaravelWebSockets\Apps\App|null
|
||||
* @return PromiseInterface
|
||||
*/
|
||||
public function findById($appId): ?App;
|
||||
public function findById($appId): PromiseInterface;
|
||||
|
||||
/**
|
||||
* Get app by app key.
|
||||
*
|
||||
* @param string $appKey
|
||||
* @return \BeyondCode\LaravelWebSockets\Apps\App|null
|
||||
* @return PromiseInterface
|
||||
*/
|
||||
public function findByKey($appKey): ?App;
|
||||
public function findByKey($appKey): PromiseInterface;
|
||||
|
||||
/**
|
||||
* Get app by secret.
|
||||
*
|
||||
* @param string $appSecret
|
||||
* @return \BeyondCode\LaravelWebSockets\Apps\App|null
|
||||
* @return PromiseInterface
|
||||
*/
|
||||
public function findBySecret($appSecret): ?App;
|
||||
public function findBySecret($appSecret): PromiseInterface;
|
||||
|
||||
/**
|
||||
* Create a new app.
|
||||
*
|
||||
* @param $appData
|
||||
* @return PromiseInterface
|
||||
*/
|
||||
public function createApp($appData): PromiseInterface;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -4,8 +4,10 @@ namespace BeyondCode\LaravelWebSockets\Dashboard\Http\Controllers;
|
|||
|
||||
use BeyondCode\LaravelWebSockets\Apps\App;
|
||||
use BeyondCode\LaravelWebSockets\Concerns\PushesToPusher;
|
||||
use function Clue\React\Block\await;
|
||||
use Illuminate\Broadcasting\Broadcasters\PusherBroadcaster;
|
||||
use Illuminate\Http\Request;
|
||||
use React\EventLoop\LoopInterface;
|
||||
|
||||
class AuthenticateDashboard
|
||||
{
|
||||
|
|
@ -21,7 +23,7 @@ class AuthenticateDashboard
|
|||
*/
|
||||
public function __invoke(Request $request)
|
||||
{
|
||||
$app = App::findById($request->header('X-App-Id'));
|
||||
$app = await(App::findById($request->header('X-App-Id')), app(LoopInterface::class));
|
||||
|
||||
$broadcaster = $this->getPusherBroadcaster([
|
||||
'key' => $app->key,
|
||||
|
|
|
|||
|
|
@ -0,0 +1,26 @@
|
|||
<?php
|
||||
|
||||
namespace BeyondCode\LaravelWebSockets\Dashboard\Http\Controllers;
|
||||
|
||||
use BeyondCode\LaravelWebSockets\Contracts\AppManager;
|
||||
use function Clue\React\Block\await;
|
||||
use Illuminate\Http\Request;
|
||||
use React\EventLoop\LoopInterface;
|
||||
|
||||
class ShowApps
|
||||
{
|
||||
/**
|
||||
* Show the configured apps.
|
||||
*
|
||||
* @param \Illuminate\Http\Request $request
|
||||
* @param \BeyondCode\LaravelWebSockets\Contracts\AppManager $apps
|
||||
* @return void
|
||||
*/
|
||||
public function __invoke(Request $request, AppManager $apps)
|
||||
{
|
||||
return view('websockets::apps', [
|
||||
'apps' => await($apps->all(), app(LoopInterface::class), 2.0),
|
||||
'port' => config('websockets.dashboard.port', 6001),
|
||||
]);
|
||||
}
|
||||
}
|
||||
|
|
@ -4,7 +4,9 @@ namespace BeyondCode\LaravelWebSockets\Dashboard\Http\Controllers;
|
|||
|
||||
use BeyondCode\LaravelWebSockets\Contracts\AppManager;
|
||||
use BeyondCode\LaravelWebSockets\DashboardLogger;
|
||||
use function Clue\React\Block\await;
|
||||
use Illuminate\Http\Request;
|
||||
use React\EventLoop\LoopInterface;
|
||||
|
||||
class ShowDashboard
|
||||
{
|
||||
|
|
@ -18,7 +20,7 @@ class ShowDashboard
|
|||
public function __invoke(Request $request, AppManager $apps)
|
||||
{
|
||||
return view('websockets::dashboard', [
|
||||
'apps' => $apps->all(),
|
||||
'apps' => await($apps->all(), app(LoopInterface::class), 2.0),
|
||||
'port' => config('websockets.dashboard.port', 6001),
|
||||
'channels' => DashboardLogger::$channels,
|
||||
'logPrefix' => DashboardLogger::LOG_CHANNEL_PREFIX,
|
||||
|
|
|
|||
|
|
@ -0,0 +1,36 @@
|
|||
<?php
|
||||
|
||||
namespace BeyondCode\LaravelWebSockets\Dashboard\Http\Controllers;
|
||||
|
||||
use BeyondCode\LaravelWebSockets\Contracts\AppManager;
|
||||
use BeyondCode\LaravelWebSockets\Dashboard\Http\Requests\StoreAppRequest;
|
||||
use function Clue\React\Block\await;
|
||||
use Illuminate\Support\Str;
|
||||
use React\EventLoop\LoopInterface;
|
||||
|
||||
class StoreApp
|
||||
{
|
||||
/**
|
||||
* Show the configured apps.
|
||||
*
|
||||
* @param StoreAppRequest $request
|
||||
* @param \BeyondCode\LaravelWebSockets\Contracts\AppManager $apps
|
||||
* @return void
|
||||
*/
|
||||
public function __invoke(StoreAppRequest $request, AppManager $apps)
|
||||
{
|
||||
$appData = [
|
||||
'id' => (string) Str::uuid(),
|
||||
'key' => (string) Str::uuid(),
|
||||
'secret' => (string) Str::uuid(),
|
||||
'name' => $request->get('name'),
|
||||
'enable_client_messages' => $request->has('enable_client_messages'),
|
||||
'enable_statistics' => $request->has('enable_statistics'),
|
||||
'allowed_origins' => $request->get('allowed_origins'),
|
||||
];
|
||||
|
||||
await($apps->createApp($appData), app(LoopInterface::class));
|
||||
|
||||
return redirect()->route('laravel-websockets.apps');
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,20 @@
|
|||
<?php
|
||||
|
||||
namespace BeyondCode\LaravelWebSockets\Dashboard\Http\Requests;
|
||||
|
||||
use Illuminate\Foundation\Http\FormRequest;
|
||||
|
||||
class StoreAppRequest extends FormRequest
|
||||
{
|
||||
public function authenticate()
|
||||
{
|
||||
return true;
|
||||
}
|
||||
|
||||
public function rules()
|
||||
{
|
||||
return [
|
||||
'name' => 'required',
|
||||
];
|
||||
}
|
||||
}
|
||||
|
|
@ -3,7 +3,9 @@
|
|||
namespace BeyondCode\LaravelWebSockets\Rules;
|
||||
|
||||
use BeyondCode\LaravelWebSockets\Contracts\AppManager;
|
||||
use function Clue\React\Block\await;
|
||||
use Illuminate\Contracts\Validation\Rule;
|
||||
use React\EventLoop\Factory;
|
||||
|
||||
class AppId implements Rule
|
||||
{
|
||||
|
|
@ -18,7 +20,7 @@ class AppId implements Rule
|
|||
{
|
||||
$manager = app(AppManager::class);
|
||||
|
||||
return $manager->findById($value) ? true : false;
|
||||
return await($manager->findById($value), Factory::create()) ? true : false;
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
|
|||
|
|
@ -70,7 +70,7 @@ class Router
|
|||
*/
|
||||
public function registerRoutes()
|
||||
{
|
||||
$this->get('/app/{appKey}', config('websockets.handlers.websocket'));
|
||||
$this->get('/app/{appKey}', 'websockets.handler');
|
||||
$this->post('/apps/{appId}/events', config('websockets.handlers.trigger_event'));
|
||||
$this->get('/apps/{appId}/channels', config('websockets.handlers.fetch_channels'));
|
||||
$this->get('/apps/{appId}/channels/{channelName}', config('websockets.handlers.fetch_channel'));
|
||||
|
|
@ -191,9 +191,10 @@ class Router
|
|||
*/
|
||||
protected function getRoute(string $method, string $uri, $action): Route
|
||||
{
|
||||
$action = app($action);
|
||||
$action = is_subclass_of($action, MessageComponentInterface::class)
|
||||
? $this->createWebSocketsServer($action)
|
||||
: app($action);
|
||||
: $action;
|
||||
|
||||
return new Route($uri, ['_controller' => $action], [], [], null, [], [$method]);
|
||||
}
|
||||
|
|
@ -201,13 +202,11 @@ class Router
|
|||
/**
|
||||
* Create a new websockets server to handle the action.
|
||||
*
|
||||
* @param string $action
|
||||
* @param MessageComponentInterface $app
|
||||
* @return \Ratchet\WebSocket\WsServer
|
||||
*/
|
||||
protected function createWebSocketsServer(string $action): WsServer
|
||||
protected function createWebSocketsServer($app): WsServer
|
||||
{
|
||||
$app = app($action);
|
||||
|
||||
if (WebsocketsLogger::isEnabled()) {
|
||||
$app = WebsocketsLogger::decorate($app);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -9,10 +9,14 @@ use BeyondCode\LaravelWebSockets\Events\ConnectionClosed;
|
|||
use BeyondCode\LaravelWebSockets\Events\NewConnection;
|
||||
use BeyondCode\LaravelWebSockets\Events\WebSocketMessageReceived;
|
||||
use BeyondCode\LaravelWebSockets\Facades\StatisticsCollector;
|
||||
use BeyondCode\LaravelWebSockets\Helpers;
|
||||
use BeyondCode\LaravelWebSockets\Server\Exceptions\WebSocketException;
|
||||
use Exception;
|
||||
use Ratchet\ConnectionInterface;
|
||||
use Ratchet\RFC6455\Messaging\MessageInterface;
|
||||
use Ratchet\WebSocket\MessageComponentInterface;
|
||||
use React\Promise\Deferred;
|
||||
use React\Promise\PromiseInterface;
|
||||
|
||||
class WebSocketHandler implements MessageComponentInterface
|
||||
{
|
||||
|
|
@ -47,7 +51,9 @@ class WebSocketHandler implements MessageComponentInterface
|
|||
}
|
||||
|
||||
$this->verifyAppKey($connection)
|
||||
->verifyOrigin($connection)
|
||||
->then(function () use ($connection) {
|
||||
try {
|
||||
$this->verifyOrigin($connection)
|
||||
->limitConcurrentConnections($connection)
|
||||
->generateSocketId($connection)
|
||||
->establishConnection($connection);
|
||||
|
|
@ -71,6 +77,12 @@ class WebSocketHandler implements MessageComponentInterface
|
|||
|
||||
NewConnection::dispatch($connection->app->id, $connection->socketId);
|
||||
}
|
||||
} catch (WebSocketException $exception) {
|
||||
$this->onError($connection, $exception);
|
||||
}
|
||||
}, function ($exception) use ($connection) {
|
||||
$this->onError($connection, $exception);
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
@ -105,11 +117,11 @@ class WebSocketHandler implements MessageComponentInterface
|
|||
* Handle the websocket close.
|
||||
*
|
||||
* @param \Ratchet\ConnectionInterface $connection
|
||||
* @return void
|
||||
* @return PromiseInterface
|
||||
*/
|
||||
public function onClose(ConnectionInterface $connection)
|
||||
{
|
||||
$this->channelManager
|
||||
return $this->channelManager
|
||||
->unsubscribeFromAllChannels($connection)
|
||||
->then(function (bool $unsubscribed) use ($connection) {
|
||||
if (isset($connection->app)) {
|
||||
|
|
@ -117,8 +129,13 @@ class WebSocketHandler implements MessageComponentInterface
|
|||
StatisticsCollector::disconnection($connection->app->id);
|
||||
}
|
||||
|
||||
$this->channelManager->unsubscribeFromApp($connection->app->id);
|
||||
return $this->channelManager->unsubscribeFromApp($connection->app->id);
|
||||
}
|
||||
|
||||
return Helpers::createFulfilledPromise(true);
|
||||
})
|
||||
->then(function () use ($connection) {
|
||||
if (isset($connection->app)) {
|
||||
DashboardLogger::log($connection->app->id, DashboardLogger::TYPE_DISCONNECTED, [
|
||||
'socketId' => $connection->socketId,
|
||||
]);
|
||||
|
|
@ -160,21 +177,28 @@ class WebSocketHandler implements MessageComponentInterface
|
|||
* Verify the app key validity.
|
||||
*
|
||||
* @param \Ratchet\ConnectionInterface $connection
|
||||
* @return $this
|
||||
* @return PromiseInterface
|
||||
*/
|
||||
protected function verifyAppKey(ConnectionInterface $connection)
|
||||
protected function verifyAppKey(ConnectionInterface $connection): PromiseInterface
|
||||
{
|
||||
$deferred = new Deferred();
|
||||
|
||||
$query = QueryParameters::create($connection->httpRequest);
|
||||
|
||||
$appKey = $query->get('appKey');
|
||||
|
||||
if (! $app = App::findByKey($appKey)) {
|
||||
throw new Exceptions\UnknownAppKey($appKey);
|
||||
App::findByKey($appKey)
|
||||
->then(function ($app) use ($appKey, $connection, $deferred) {
|
||||
if (! $app) {
|
||||
$deferred->reject(new Exceptions\UnknownAppKey($appKey));
|
||||
}
|
||||
|
||||
$connection->app = $app;
|
||||
|
||||
return $this;
|
||||
$deferred->resolve();
|
||||
});
|
||||
|
||||
return $deferred->promise();
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
|
|||
|
|
@ -92,14 +92,15 @@ class MemoryCollector implements StatisticsCollector
|
|||
{
|
||||
$this->getStatistics()->then(function ($statistics) {
|
||||
foreach ($statistics as $appId => $statistic) {
|
||||
if (! $statistic->isEnabled()) {
|
||||
continue;
|
||||
$statistic->isEnabled()->then(function ($isEnabled) use ($appId, $statistic) {
|
||||
if (! $isEnabled) {
|
||||
return;
|
||||
}
|
||||
|
||||
if ($statistic->shouldHaveTracesRemoved()) {
|
||||
$this->resetAppTraces($appId);
|
||||
|
||||
continue;
|
||||
return;
|
||||
}
|
||||
|
||||
$this->createRecord($statistic, $appId);
|
||||
|
|
@ -111,6 +112,7 @@ class MemoryCollector implements StatisticsCollector
|
|||
is_null($connections) ? 0 : $connections
|
||||
);
|
||||
});
|
||||
});
|
||||
}
|
||||
});
|
||||
}
|
||||
|
|
|
|||
|
|
@ -3,6 +3,8 @@
|
|||
namespace BeyondCode\LaravelWebSockets\Statistics;
|
||||
|
||||
use BeyondCode\LaravelWebSockets\Apps\App;
|
||||
use React\Promise\Deferred;
|
||||
use React\Promise\PromiseInterface;
|
||||
|
||||
class Statistic
|
||||
{
|
||||
|
|
@ -118,11 +120,17 @@ class Statistic
|
|||
/**
|
||||
* Check if the app has statistics enabled.
|
||||
*
|
||||
* @return bool
|
||||
* @return PromiseInterface
|
||||
*/
|
||||
public function isEnabled(): bool
|
||||
public function isEnabled(): PromiseInterface
|
||||
{
|
||||
return App::findById($this->appId)->statisticsEnabled;
|
||||
$deferred = new Deferred();
|
||||
|
||||
App::findById($this->appId)->then(function ($app) use ($deferred) {
|
||||
$deferred->resolve($app->statisticsEnabled);
|
||||
});
|
||||
|
||||
return $deferred->promise();
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
|
|||
|
|
@ -6,15 +6,25 @@ use BeyondCode\LaravelWebSockets\Contracts\StatisticsCollector;
|
|||
use BeyondCode\LaravelWebSockets\Contracts\StatisticsStore;
|
||||
use BeyondCode\LaravelWebSockets\Dashboard\Http\Controllers\AuthenticateDashboard;
|
||||
use BeyondCode\LaravelWebSockets\Dashboard\Http\Controllers\SendMessage;
|
||||
use BeyondCode\LaravelWebSockets\Dashboard\Http\Controllers\ShowApps;
|
||||
use BeyondCode\LaravelWebSockets\Dashboard\Http\Controllers\ShowDashboard;
|
||||
use BeyondCode\LaravelWebSockets\Dashboard\Http\Controllers\ShowStatistics;
|
||||
use BeyondCode\LaravelWebSockets\Dashboard\Http\Controllers\StoreApp;
|
||||
use BeyondCode\LaravelWebSockets\Dashboard\Http\Middleware\Authorize as AuthorizeDashboard;
|
||||
use BeyondCode\LaravelWebSockets\Queue\AsyncRedisConnector;
|
||||
use BeyondCode\LaravelWebSockets\Server\Router;
|
||||
use Clue\React\SQLite\DatabaseInterface;
|
||||
use Clue\React\SQLite\Factory as SQLiteFactory;
|
||||
use Illuminate\Support\Facades\Gate;
|
||||
use Illuminate\Support\Facades\Queue;
|
||||
use Illuminate\Support\Facades\Route;
|
||||
use Illuminate\Support\ServiceProvider;
|
||||
use React\EventLoop\Factory;
|
||||
use React\EventLoop\LoopInterface;
|
||||
use React\MySQL\ConnectionInterface;
|
||||
use React\MySQL\Factory as MySQLFactory;
|
||||
use SplFileInfo;
|
||||
use Symfony\Component\Finder\Finder;
|
||||
|
||||
class WebSocketsServiceProvider extends ServiceProvider
|
||||
{
|
||||
|
|
@ -38,8 +48,16 @@ class WebSocketsServiceProvider extends ServiceProvider
|
|||
__DIR__.'/../database/migrations/0000_00_00_000000_rename_statistics_counters.php' => database_path('migrations/0000_00_00_000000_rename_statistics_counters.php'),
|
||||
], 'migrations');
|
||||
|
||||
$this->registerEventLoop();
|
||||
|
||||
$this->registerSQLiteDatabase();
|
||||
|
||||
$this->registerMySqlDatabase();
|
||||
|
||||
$this->registerAsyncRedisQueueDriver();
|
||||
|
||||
$this->registerWebSocketHandler();
|
||||
|
||||
$this->registerRouter();
|
||||
|
||||
$this->registerManagers();
|
||||
|
|
@ -61,6 +79,13 @@ class WebSocketsServiceProvider extends ServiceProvider
|
|||
//
|
||||
}
|
||||
|
||||
protected function registerEventLoop()
|
||||
{
|
||||
$this->app->singleton(LoopInterface::class, function () {
|
||||
return Factory::create();
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Register the async, non-blocking Redis queue driver.
|
||||
*
|
||||
|
|
@ -73,6 +98,47 @@ class WebSocketsServiceProvider extends ServiceProvider
|
|||
});
|
||||
}
|
||||
|
||||
protected function registerSQLiteDatabase()
|
||||
{
|
||||
$this->app->singleton(DatabaseInterface::class, function () {
|
||||
$factory = new SQLiteFactory($this->app->make(LoopInterface::class));
|
||||
|
||||
$database = $factory->openLazy(
|
||||
config('websockets.managers.sqlite.database', ':memory:')
|
||||
);
|
||||
|
||||
$migrations = (new Finder())
|
||||
->files()
|
||||
->ignoreDotFiles(true)
|
||||
->in(__DIR__.'/../database/migrations/sqlite')
|
||||
->name('*.sql');
|
||||
|
||||
/** @var SplFileInfo $migration */
|
||||
foreach ($migrations as $migration) {
|
||||
$database->exec($migration->getContents());
|
||||
}
|
||||
|
||||
return $database;
|
||||
});
|
||||
}
|
||||
|
||||
protected function registerMySqlDatabase()
|
||||
{
|
||||
$this->app->singleton(ConnectionInterface::class, function () {
|
||||
$factory = new MySQLFactory($this->app->make(LoopInterface::class));
|
||||
|
||||
$connectionKey = 'database.connections.'.config('websockets.managers.mysql.connection');
|
||||
|
||||
$auth = trim(config($connectionKey.'.username').':'.config($connectionKey.'.password'), ':');
|
||||
$connection = trim(config($connectionKey.'.host').':'.config($connectionKey.'.port'), ':');
|
||||
$database = config($connectionKey.'.database');
|
||||
|
||||
$database = $factory->createLazyConnection(trim("{$auth}@{$connection}/{$database}", '@'));
|
||||
|
||||
return $database;
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Register the statistics-related contracts.
|
||||
*
|
||||
|
|
@ -98,7 +164,7 @@ class WebSocketsServiceProvider extends ServiceProvider
|
|||
}
|
||||
|
||||
/**
|
||||
* Regsiter the dashboard components.
|
||||
* Register the dashboard components.
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
|
|
@ -165,6 +231,8 @@ class WebSocketsServiceProvider extends ServiceProvider
|
|||
'middleware' => config('websockets.dashboard.middleware', [AuthorizeDashboard::class]),
|
||||
], function () {
|
||||
Route::get('/', ShowDashboard::class)->name('dashboard');
|
||||
Route::get('/apps', ShowApps::class)->name('apps');
|
||||
Route::post('/apps', StoreApp::class)->name('apps.store');
|
||||
Route::get('/api/{appId}/statistics', ShowStatistics::class)->name('statistics');
|
||||
Route::post('/auth', AuthenticateDashboard::class)->name('auth');
|
||||
Route::post('/event', SendMessage::class)->name('event');
|
||||
|
|
@ -182,4 +250,11 @@ class WebSocketsServiceProvider extends ServiceProvider
|
|||
return $this->app->environment('local');
|
||||
});
|
||||
}
|
||||
|
||||
protected function registerWebSocketHandler()
|
||||
{
|
||||
$this->app->singleton('websockets.handler', function () {
|
||||
return app(config('websockets.handlers.websocket'));
|
||||
});
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -0,0 +1,98 @@
|
|||
<?php
|
||||
|
||||
namespace BeyondCode\LaravelWebSockets\Test\Apps;
|
||||
|
||||
use BeyondCode\LaravelWebSockets\Apps\App;
|
||||
use BeyondCode\LaravelWebSockets\Apps\ConfigAppManager;
|
||||
use BeyondCode\LaravelWebSockets\Contracts\AppManager;
|
||||
use BeyondCode\LaravelWebSockets\Test\TestCase;
|
||||
|
||||
class ConfigAppManagerTest extends TestCase
|
||||
{
|
||||
/** @var AppManager */
|
||||
protected $apps;
|
||||
|
||||
public function getEnvironmentSetUp($app)
|
||||
{
|
||||
parent::getEnvironmentSetUp($app);
|
||||
|
||||
$app['config']->set('websockets.managers.app', ConfigAppManager::class);
|
||||
$app['config']->set('websockets.apps', []);
|
||||
}
|
||||
|
||||
public function setUp(): void
|
||||
{
|
||||
parent::setUp();
|
||||
|
||||
$this->apps = app()->make(AppManager::class);
|
||||
}
|
||||
|
||||
public function test_can_return_all_apps()
|
||||
{
|
||||
$apps = $this->await($this->apps->all());
|
||||
$this->assertCount(0, $apps);
|
||||
|
||||
$this->await($this->apps->createApp([
|
||||
'id' => 1,
|
||||
'key' => 'test',
|
||||
'secret' => 'secret',
|
||||
'name' => 'Test',
|
||||
'enable_client_messages' => true,
|
||||
'enable_statistics' => false,
|
||||
]));
|
||||
|
||||
$apps = $this->await($this->apps->all());
|
||||
$this->assertCount(1, $apps);
|
||||
}
|
||||
|
||||
public function test_can_find_apps_by_id()
|
||||
{
|
||||
$this->await($this->apps->createApp([
|
||||
'id' => 1,
|
||||
'key' => 'test',
|
||||
'secret' => 'secret',
|
||||
'name' => 'Test',
|
||||
'enable_client_messages' => true,
|
||||
'enable_statistics' => false,
|
||||
]));
|
||||
|
||||
$app = $this->await($this->apps->findById(1));
|
||||
|
||||
$this->assertInstanceOf(App::class, $app);
|
||||
$this->assertSame('test', $app->key);
|
||||
}
|
||||
|
||||
public function test_can_find_apps_by_key()
|
||||
{
|
||||
$this->await($this->apps->createApp([
|
||||
'id' => 1,
|
||||
'key' => 'key',
|
||||
'secret' => 'secret',
|
||||
'name' => 'Test',
|
||||
'enable_client_messages' => true,
|
||||
'enable_statistics' => false,
|
||||
]));
|
||||
|
||||
$app = $this->await($this->apps->findByKey('key'));
|
||||
|
||||
$this->assertInstanceOf(App::class, $app);
|
||||
$this->assertSame('key', $app->key);
|
||||
}
|
||||
|
||||
public function test_can_find_apps_by_secret()
|
||||
{
|
||||
$this->await($this->apps->createApp([
|
||||
'id' => 1,
|
||||
'key' => 'key',
|
||||
'secret' => 'secret',
|
||||
'name' => 'Test',
|
||||
'enable_client_messages' => true,
|
||||
'enable_statistics' => false,
|
||||
]));
|
||||
|
||||
$app = $this->await($this->apps->findBySecret('secret'));
|
||||
|
||||
$this->assertInstanceOf(App::class, $app);
|
||||
$this->assertSame('key', $app->key);
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,110 @@
|
|||
<?php
|
||||
|
||||
namespace BeyondCode\LaravelWebSockets\Test\Apps;
|
||||
|
||||
use BeyondCode\LaravelWebSockets\Apps\App;
|
||||
use BeyondCode\LaravelWebSockets\Apps\MysqlAppManager;
|
||||
use BeyondCode\LaravelWebSockets\Contracts\AppManager;
|
||||
use BeyondCode\LaravelWebSockets\Test\TestCase;
|
||||
|
||||
class MysqlAppManagerTest extends TestCase
|
||||
{
|
||||
/** @var AppManager */
|
||||
protected $apps;
|
||||
|
||||
public function getEnvironmentSetUp($app)
|
||||
{
|
||||
parent::getEnvironmentSetUp($app);
|
||||
|
||||
$app['config']->set('websockets.managers.app', MysqlAppManager::class);
|
||||
$app['config']->set('database.connections.mysql.database', 'websockets_test');
|
||||
$app['config']->set('database.connections.mysql.username', 'root');
|
||||
$app['config']->set('database.connections.mysql.password', 'password');
|
||||
|
||||
$app['config']->set('websockets.managers.mysql.table', 'websockets_apps');
|
||||
$app['config']->set('websockets.managers.mysql.connection', 'mysql');
|
||||
$app['config']->set('database.connections.default', 'mysql');
|
||||
}
|
||||
|
||||
public function setUp(): void
|
||||
{
|
||||
parent::setUp();
|
||||
|
||||
$this->artisan('migrate:fresh', [
|
||||
'--database' => 'mysql',
|
||||
'--realpath' => true,
|
||||
'--path' => __DIR__.'/../../database/migrations/',
|
||||
]);
|
||||
|
||||
$this->apps = app()->make(AppManager::class);
|
||||
}
|
||||
|
||||
public function test_can_return_all_apps()
|
||||
{
|
||||
$apps = $this->await($this->apps->all());
|
||||
$this->assertCount(0, $apps);
|
||||
|
||||
$this->await($this->apps->createApp([
|
||||
'id' => 1,
|
||||
'key' => 'test',
|
||||
'secret' => 'secret',
|
||||
'name' => 'Test',
|
||||
'enable_client_messages' => true,
|
||||
'enable_statistics' => false,
|
||||
]));
|
||||
|
||||
$apps = $this->await($this->apps->all());
|
||||
$this->assertCount(1, $apps);
|
||||
}
|
||||
|
||||
public function test_can_find_apps_by_id()
|
||||
{
|
||||
$this->await($this->apps->createApp([
|
||||
'id' => 1,
|
||||
'key' => 'test',
|
||||
'secret' => 'secret',
|
||||
'name' => 'Test',
|
||||
'enable_client_messages' => true,
|
||||
'enable_statistics' => false,
|
||||
]));
|
||||
|
||||
$app = $this->await($this->apps->findById(1));
|
||||
|
||||
$this->assertInstanceOf(App::class, $app);
|
||||
$this->assertSame('test', $app->key);
|
||||
}
|
||||
|
||||
public function test_can_find_apps_by_key()
|
||||
{
|
||||
$this->await($this->apps->createApp([
|
||||
'id' => 1,
|
||||
'key' => 'key',
|
||||
'secret' => 'secret',
|
||||
'name' => 'Test',
|
||||
'enable_client_messages' => true,
|
||||
'enable_statistics' => false,
|
||||
]));
|
||||
|
||||
$app = $this->await($this->apps->findByKey('key'));
|
||||
|
||||
$this->assertInstanceOf(App::class, $app);
|
||||
$this->assertSame('key', $app->key);
|
||||
}
|
||||
|
||||
public function test_can_find_apps_by_secret()
|
||||
{
|
||||
$this->await($this->apps->createApp([
|
||||
'id' => 1,
|
||||
'key' => 'key',
|
||||
'secret' => 'secret',
|
||||
'name' => 'Test',
|
||||
'enable_client_messages' => true,
|
||||
'enable_statistics' => false,
|
||||
]));
|
||||
|
||||
$app = $this->await($this->apps->findBySecret('secret'));
|
||||
|
||||
$this->assertInstanceOf(App::class, $app);
|
||||
$this->assertSame('key', $app->key);
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,97 @@
|
|||
<?php
|
||||
|
||||
namespace BeyondCode\LaravelWebSockets\Test\Apps;
|
||||
|
||||
use BeyondCode\LaravelWebSockets\Apps\App;
|
||||
use BeyondCode\LaravelWebSockets\Apps\SQLiteAppManager;
|
||||
use BeyondCode\LaravelWebSockets\Contracts\AppManager;
|
||||
use BeyondCode\LaravelWebSockets\Test\TestCase;
|
||||
|
||||
class SqliteAppManagerTest extends TestCase
|
||||
{
|
||||
/** @var AppManager */
|
||||
protected $apps;
|
||||
|
||||
public function getEnvironmentSetUp($app)
|
||||
{
|
||||
parent::getEnvironmentSetUp($app);
|
||||
|
||||
$app['config']->set('websockets.managers.app', SQLiteAppManager::class);
|
||||
}
|
||||
|
||||
public function setUp(): void
|
||||
{
|
||||
parent::setUp();
|
||||
|
||||
$this->apps = app()->make(AppManager::class);
|
||||
}
|
||||
|
||||
public function test_can_return_all_apps()
|
||||
{
|
||||
$apps = $this->await($this->apps->all());
|
||||
$this->assertCount(0, $apps);
|
||||
|
||||
$this->await($this->apps->createApp([
|
||||
'id' => 1,
|
||||
'key' => 'test',
|
||||
'secret' => 'secret',
|
||||
'name' => 'Test',
|
||||
'enable_client_messages' => true,
|
||||
'enable_statistics' => false,
|
||||
]));
|
||||
|
||||
$apps = $this->await($this->apps->all());
|
||||
$this->assertCount(1, $apps);
|
||||
}
|
||||
|
||||
public function test_can_find_apps_by_id()
|
||||
{
|
||||
$this->await($this->apps->createApp([
|
||||
'id' => 1,
|
||||
'key' => 'test',
|
||||
'secret' => 'secret',
|
||||
'name' => 'Test',
|
||||
'enable_client_messages' => true,
|
||||
'enable_statistics' => false,
|
||||
]));
|
||||
|
||||
$app = $this->await($this->apps->findById(1));
|
||||
|
||||
$this->assertInstanceOf(App::class, $app);
|
||||
$this->assertSame('test', $app->key);
|
||||
}
|
||||
|
||||
public function test_can_find_apps_by_key()
|
||||
{
|
||||
$this->await($this->apps->createApp([
|
||||
'id' => 1,
|
||||
'key' => 'key',
|
||||
'secret' => 'secret',
|
||||
'name' => 'Test',
|
||||
'enable_client_messages' => true,
|
||||
'enable_statistics' => false,
|
||||
]));
|
||||
|
||||
$app = $this->await($this->apps->findByKey('key'));
|
||||
|
||||
$this->assertInstanceOf(App::class, $app);
|
||||
$this->assertSame('key', $app->key);
|
||||
}
|
||||
|
||||
public function test_can_find_apps_by_secret()
|
||||
{
|
||||
$this->await($this->apps->createApp([
|
||||
'id' => 1,
|
||||
'key' => 'key',
|
||||
'secret' => 'secret',
|
||||
'name' => 'Test',
|
||||
'enable_client_messages' => true,
|
||||
'enable_statistics' => false,
|
||||
]));
|
||||
|
||||
$app = $this->await($this->apps->findBySecret('secret'));
|
||||
|
||||
$this->assertInstanceOf(App::class, $app);
|
||||
$this->assertSame('key', $app->key);
|
||||
}
|
||||
}
|
||||
|
|
@ -2,43 +2,43 @@
|
|||
|
||||
namespace BeyondCode\LaravelWebSockets\Test;
|
||||
|
||||
use BeyondCode\LaravelWebSockets\Server\Exceptions\OriginNotAllowed;
|
||||
use BeyondCode\LaravelWebSockets\Server\Exceptions\UnknownAppKey;
|
||||
|
||||
class ConnectionTest extends TestCase
|
||||
{
|
||||
public function test_cannot_connect_with_a_wrong_app_key()
|
||||
{
|
||||
$this->expectException(UnknownAppKey::class);
|
||||
$this->startServer();
|
||||
|
||||
$this->newActiveConnection(['public-channel'], 'NonWorkingKey');
|
||||
$response = $this->await($this->joinWebSocketServer(['public-channel'], 'NonWorkingKey'));
|
||||
$this->assertSame('{"event":"pusher:error","data":{"message":"Could not find app key `NonWorkingKey`.","code":4001}}', (string) $response);
|
||||
}
|
||||
|
||||
public function test_unconnected_app_cannot_store_statistics()
|
||||
{
|
||||
$this->expectException(UnknownAppKey::class);
|
||||
$this->startServer();
|
||||
|
||||
$this->newActiveConnection(['public-channel'], 'NonWorkingKey');
|
||||
$response = $this->await($this->joinWebSocketServer(['public-channel'], 'NonWorkingKey'));
|
||||
$this->assertSame('{"event":"pusher:error","data":{"message":"Could not find app key `NonWorkingKey`.","code":4001}}', (string) $response);
|
||||
|
||||
$this->assertCount(0, $this->statisticsCollector->getStatistics());
|
||||
$count = $this->await($this->statisticsCollector->getStatistics());
|
||||
$this->assertCount(0, $count);
|
||||
}
|
||||
|
||||
public function test_origin_validation_should_fail_for_no_origin()
|
||||
{
|
||||
$this->expectException(OriginNotAllowed::class);
|
||||
$this->startServer();
|
||||
|
||||
$connection = $this->newConnection('TestOrigin');
|
||||
|
||||
$this->pusherServer->onOpen($connection);
|
||||
$response = $this->await($this->joinWebSocketServer(['public-channel'], 'TestOrigin'));
|
||||
$this->assertSame('{"event":"pusher:error","data":{"message":"The origin is not allowed for `TestOrigin`.","code":4009}}', (string) $response);
|
||||
}
|
||||
|
||||
public function test_origin_validation_should_fail_for_wrong_origin()
|
||||
{
|
||||
$this->expectException(OriginNotAllowed::class);
|
||||
$this->startServer();
|
||||
|
||||
$connection = $this->newConnection('TestOrigin', ['Origin' => 'https://google.ro']);
|
||||
|
||||
$this->pusherServer->onOpen($connection);
|
||||
$response = $this->await($this->joinWebSocketServer(['public-channel'], 'TestOrigin', ['Origin' => 'https://google.ro']));
|
||||
$this->assertSame('{"event":"pusher:error","data":{"message":"The origin is not allowed for `TestOrigin`.","code":4009}}', (string) $response);
|
||||
}
|
||||
|
||||
public function test_origin_validation_should_pass_for_the_right_origin()
|
||||
|
|
|
|||
|
|
@ -0,0 +1,38 @@
|
|||
<?php
|
||||
|
||||
namespace BeyondCode\LaravelWebSockets\Test\Dashboard;
|
||||
|
||||
use BeyondCode\LaravelWebSockets\Apps\SQLiteAppManager;
|
||||
use BeyondCode\LaravelWebSockets\Test\Models\User;
|
||||
use BeyondCode\LaravelWebSockets\Test\TestCase;
|
||||
|
||||
class AppsTest extends TestCase
|
||||
{
|
||||
public function getEnvironmentSetUp($app)
|
||||
{
|
||||
parent::getEnvironmentSetUp($app);
|
||||
|
||||
$app['config']->set('websockets.managers.app', SQLiteAppManager::class);
|
||||
}
|
||||
|
||||
public function test_can_list_all_apps()
|
||||
{
|
||||
$this->actingAs(factory(User::class)->create())
|
||||
->get(route('laravel-websockets.apps'))
|
||||
->assertViewHas('apps', []);
|
||||
}
|
||||
|
||||
public function test_can_create_app()
|
||||
{
|
||||
$this->actingAs(factory(User::class)->create())
|
||||
->post(route('laravel-websockets.apps.store', [
|
||||
'name' => 'New App',
|
||||
]));
|
||||
|
||||
$this->actingAs(factory(User::class)->create())
|
||||
->get(route('laravel-websockets.apps'))
|
||||
->assertViewHas('apps', function ($apps) {
|
||||
return count($apps) === 1 && $apps[0]['name'] === 'New App';
|
||||
});
|
||||
}
|
||||
}
|
||||
|
|
@ -17,7 +17,6 @@ class DashboardTest extends TestCase
|
|||
{
|
||||
$this->actingAs(factory(User::class)->create())
|
||||
->get(route('laravel-websockets.dashboard'))
|
||||
->assertResponseOk()
|
||||
->see('WebSockets Dashboard');
|
||||
->assertResponseOk();
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -5,33 +5,26 @@ namespace BeyondCode\LaravelWebSockets\Test;
|
|||
use BeyondCode\LaravelWebSockets\API\FetchChannel;
|
||||
use GuzzleHttp\Psr7\Request;
|
||||
use Illuminate\Http\JsonResponse;
|
||||
use Symfony\Component\HttpKernel\Exception\HttpException;
|
||||
use Pusher\Pusher;
|
||||
|
||||
class FetchChannelTest extends TestCase
|
||||
{
|
||||
public function test_invalid_signatures_can_not_access_the_api()
|
||||
{
|
||||
$this->expectException(HttpException::class);
|
||||
$this->expectExceptionMessage('Invalid auth signature provided.');
|
||||
$this->startServer();
|
||||
|
||||
$connection = new Mocks\Connection;
|
||||
$requestPath = '/apps/1234/channels/my-channel';
|
||||
|
||||
$requestPath = '/apps/1234/channel/my-channel';
|
||||
|
||||
$routeParams = [
|
||||
'appId' => '1234',
|
||||
'channelName' => 'my-channel',
|
||||
];
|
||||
|
||||
$queryString = self::build_auth_query_string(
|
||||
$queryString = http_build_query(Pusher::build_auth_query_params(
|
||||
'TestKey', 'InvalidSecret', 'GET', $requestPath
|
||||
);
|
||||
));
|
||||
|
||||
$request = new Request('GET', "{$requestPath}?{$queryString}&".http_build_query($routeParams));
|
||||
$request = new Request('GET', "{$requestPath}?{$queryString}");
|
||||
|
||||
$controller = app(FetchChannel::class);
|
||||
$response = $this->await($this->browser->get('http://localhost:4000'."{$requestPath}?{$queryString}"));
|
||||
|
||||
$controller->onOpen($connection, $request);
|
||||
$this->assertSame(401, $response->getStatusCode());
|
||||
$this->assertSame('{"error":"Invalid auth signature provided."}', $response->getBody()->getContents());
|
||||
}
|
||||
|
||||
public function test_it_returns_the_channel_information()
|
||||
|
|
@ -47,7 +40,7 @@ class FetchChannelTest extends TestCase
|
|||
'channelName' => 'my-channel',
|
||||
];
|
||||
|
||||
$queryString = self::build_auth_query_string('TestKey', 'TestSecret', 'GET', $requestPath);
|
||||
$queryString = http_build_query(Pusher::build_auth_query_params('TestKey', 'TestSecret', 'GET', $requestPath));
|
||||
|
||||
$request = new Request('GET', "{$requestPath}?{$queryString}&".http_build_query($routeParams));
|
||||
|
||||
|
|
@ -78,7 +71,7 @@ class FetchChannelTest extends TestCase
|
|||
'channelName' => 'presence-channel',
|
||||
];
|
||||
|
||||
$queryString = self::build_auth_query_string('TestKey', 'TestSecret', 'GET', $requestPath);
|
||||
$queryString = http_build_query(Pusher::build_auth_query_params('TestKey', 'TestSecret', 'GET', $requestPath));
|
||||
|
||||
$request = new Request('GET', "{$requestPath}?{$queryString}&".http_build_query($routeParams));
|
||||
|
||||
|
|
@ -100,26 +93,17 @@ class FetchChannelTest extends TestCase
|
|||
{
|
||||
$this->skipOnRedisReplication();
|
||||
|
||||
$this->expectException(HttpException::class);
|
||||
$this->expectExceptionMessage('Unknown channel');
|
||||
$this->startServer();
|
||||
|
||||
$this->newActiveConnection(['my-channel']);
|
||||
|
||||
$connection = new Mocks\Connection;
|
||||
$requestPath = '/apps/1234/channels/invalid-channel';
|
||||
|
||||
$requestPath = '/apps/1234/channel/invalid-channel';
|
||||
$queryString = http_build_query(Pusher::build_auth_query_params('TestKey', 'TestSecret', 'GET', $requestPath));
|
||||
|
||||
$routeParams = [
|
||||
'appId' => '1234',
|
||||
'channelName' => 'invalid-channel',
|
||||
];
|
||||
$response = $this->await($this->browser->get('http://localhost:4000'."{$requestPath}?{$queryString}"));
|
||||
|
||||
$queryString = self::build_auth_query_string('TestKey', 'TestSecret', 'GET', $requestPath);
|
||||
|
||||
$request = new Request('GET', "{$requestPath}?{$queryString}&".http_build_query($routeParams));
|
||||
|
||||
$controller = app(FetchChannel::class);
|
||||
|
||||
$controller->onOpen($connection, $request);
|
||||
$this->assertSame(404, $response->getStatusCode());
|
||||
$this->assertSame('{"error":"Unknown channel `invalid-channel`."}', $response->getBody()->getContents());
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -5,32 +5,26 @@ namespace BeyondCode\LaravelWebSockets\Test;
|
|||
use BeyondCode\LaravelWebSockets\API\FetchChannels;
|
||||
use GuzzleHttp\Psr7\Request;
|
||||
use Illuminate\Http\JsonResponse;
|
||||
use Symfony\Component\HttpKernel\Exception\HttpException;
|
||||
use Pusher\Pusher;
|
||||
|
||||
class FetchChannelsTest extends TestCase
|
||||
{
|
||||
public function test_invalid_signatures_can_not_access_the_api()
|
||||
{
|
||||
$this->expectException(HttpException::class);
|
||||
$this->expectExceptionMessage('Invalid auth signature provided.');
|
||||
|
||||
$connection = new Mocks\Connection;
|
||||
$this->startServer();
|
||||
|
||||
$requestPath = '/apps/1234/channels';
|
||||
|
||||
$routeParams = [
|
||||
'appId' => '1234',
|
||||
];
|
||||
|
||||
$queryString = self::build_auth_query_string(
|
||||
$queryString = http_build_query(Pusher::build_auth_query_params(
|
||||
'TestKey', 'InvalidSecret', 'GET', $requestPath
|
||||
);
|
||||
));
|
||||
|
||||
$request = new Request('GET', "{$requestPath}?{$queryString}&".http_build_query($routeParams));
|
||||
$request = new Request('GET', "{$requestPath}?{$queryString}");
|
||||
|
||||
$controller = app(FetchChannels::class);
|
||||
$response = $this->await($this->browser->get('http://localhost:4000'."{$requestPath}?{$queryString}"));
|
||||
|
||||
$controller->onOpen($connection, $request);
|
||||
$this->assertSame(401, $response->getStatusCode());
|
||||
$this->assertSame('{"error":"Invalid auth signature provided."}', $response->getBody()->getContents());
|
||||
}
|
||||
|
||||
public function test_it_returns_the_channel_information()
|
||||
|
|
@ -45,9 +39,9 @@ class FetchChannelsTest extends TestCase
|
|||
'appId' => '1234',
|
||||
];
|
||||
|
||||
$queryString = self::build_auth_query_string(
|
||||
$queryString = http_build_query(Pusher::build_auth_query_params(
|
||||
'TestKey', 'TestSecret', 'GET', $requestPath
|
||||
);
|
||||
));
|
||||
|
||||
$request = new Request('GET', "{$requestPath}?{$queryString}&".http_build_query($routeParams));
|
||||
|
||||
|
|
@ -80,9 +74,9 @@ class FetchChannelsTest extends TestCase
|
|||
'appId' => '1234',
|
||||
];
|
||||
|
||||
$queryString = self::build_auth_query_string('TestKey', 'TestSecret', 'GET', $requestPath, [
|
||||
$queryString = http_build_query(Pusher::build_auth_query_params('TestKey', 'TestSecret', 'GET', $requestPath, [
|
||||
'filter_by_prefix' => 'presence-global',
|
||||
]);
|
||||
]));
|
||||
|
||||
$request = new Request('GET', "{$requestPath}?{$queryString}&".http_build_query($routeParams));
|
||||
|
||||
|
|
@ -116,10 +110,10 @@ class FetchChannelsTest extends TestCase
|
|||
'appId' => '1234',
|
||||
];
|
||||
|
||||
$queryString = self::build_auth_query_string('TestKey', 'TestSecret', 'GET', $requestPath, [
|
||||
$queryString = http_build_query(Pusher::build_auth_query_params('TestKey', 'TestSecret', 'GET', $requestPath, [
|
||||
'filter_by_prefix' => 'presence-global',
|
||||
'info' => 'user_count',
|
||||
]);
|
||||
]));
|
||||
|
||||
$request = new Request('GET', "{$requestPath}?{$queryString}&".http_build_query($routeParams));
|
||||
|
||||
|
|
@ -144,29 +138,18 @@ class FetchChannelsTest extends TestCase
|
|||
|
||||
public function test_can_not_get_non_presence_channel_user_count()
|
||||
{
|
||||
$this->expectException(HttpException::class);
|
||||
$this->expectExceptionMessage('Request must be limited to presence channels in order to fetch user_count');
|
||||
|
||||
$connection = new Mocks\Connection;
|
||||
$this->startServer();
|
||||
|
||||
$requestPath = '/apps/1234/channels';
|
||||
|
||||
$routeParams = [
|
||||
'appId' => '1234',
|
||||
];
|
||||
|
||||
$queryString = self::build_auth_query_string('TestKey', 'TestSecret', 'GET', $requestPath, [
|
||||
$queryString = http_build_query(Pusher::build_auth_query_params('TestKey', 'TestSecret', 'GET', $requestPath, [
|
||||
'info' => 'user_count',
|
||||
]);
|
||||
]));
|
||||
|
||||
$request = new Request('GET', "{$requestPath}?{$queryString}&".http_build_query($routeParams));
|
||||
$response = $this->await($this->browser->get('http://localhost:4000'."{$requestPath}?{$queryString}"));
|
||||
|
||||
$controller = app(FetchChannels::class);
|
||||
|
||||
$controller->onOpen($connection, $request);
|
||||
|
||||
/** @var JsonResponse $response */
|
||||
$response = array_pop($connection->sentRawData);
|
||||
$this->assertSame(400, $response->getStatusCode());
|
||||
$this->assertSame('{"error":"Request must be limited to presence channels in order to fetch user_count"}', $response->getBody()->getContents());
|
||||
}
|
||||
|
||||
public function test_it_returns_empty_object_for_no_channels_found()
|
||||
|
|
@ -179,7 +162,7 @@ class FetchChannelsTest extends TestCase
|
|||
'appId' => '1234',
|
||||
];
|
||||
|
||||
$queryString = self::build_auth_query_string('TestKey', 'TestSecret', 'GET', $requestPath);
|
||||
$queryString = http_build_query(Pusher::build_auth_query_params('TestKey', 'TestSecret', 'GET', $requestPath));
|
||||
|
||||
$request = new Request('GET', "{$requestPath}?{$queryString}&".http_build_query($routeParams));
|
||||
|
||||
|
|
|
|||
|
|
@ -4,87 +4,56 @@ namespace BeyondCode\LaravelWebSockets\Test;
|
|||
|
||||
use BeyondCode\LaravelWebSockets\API\FetchUsers;
|
||||
use GuzzleHttp\Psr7\Request;
|
||||
use Symfony\Component\HttpKernel\Exception\HttpException;
|
||||
use Pusher\Pusher;
|
||||
|
||||
class FetchUsersTest extends TestCase
|
||||
{
|
||||
public function test_invalid_signatures_can_not_access_the_api()
|
||||
{
|
||||
$this->expectException(HttpException::class);
|
||||
$this->expectExceptionMessage('Invalid auth signature provided.');
|
||||
$this->startServer();
|
||||
|
||||
$connection = new Mocks\Connection;
|
||||
$requestPath = '/apps/1234/channels/my-channel/users';
|
||||
|
||||
$requestPath = '/apps/1234/channel/my-channel';
|
||||
|
||||
$routeParams = [
|
||||
'appId' => '1234',
|
||||
'channelName' => 'my-channel',
|
||||
];
|
||||
|
||||
$queryString = self::build_auth_query_string(
|
||||
$queryString = http_build_query(Pusher::build_auth_query_params(
|
||||
'TestKey', 'InvalidSecret', 'GET', $requestPath
|
||||
);
|
||||
));
|
||||
|
||||
$request = new Request('GET', "{$requestPath}?{$queryString}&".http_build_query($routeParams));
|
||||
$response = $this->await($this->browser->get('http://localhost:4000'."{$requestPath}?{$queryString}"));
|
||||
|
||||
$controller = app(FetchUsers::class);
|
||||
|
||||
$controller->onOpen($connection, $request);
|
||||
$this->assertSame(401, $response->getStatusCode());
|
||||
$this->assertSame('{"error":"Invalid auth signature provided."}', $response->getBody()->getContents());
|
||||
}
|
||||
|
||||
public function test_it_only_returns_data_for_presence_channels()
|
||||
{
|
||||
$this->expectException(HttpException::class);
|
||||
$this->expectExceptionMessage('Invalid presence channel');
|
||||
$this->startServer();
|
||||
|
||||
$this->newActiveConnection(['my-channel']);
|
||||
$requestPath = '/apps/1234/channels/my-channel/users';
|
||||
|
||||
$connection = new Mocks\Connection;
|
||||
|
||||
$requestPath = '/apps/1234/channel/my-channel/users';
|
||||
|
||||
$routeParams = [
|
||||
'appId' => '1234',
|
||||
'channelName' => 'my-channel',
|
||||
];
|
||||
|
||||
$queryString = self::build_auth_query_string(
|
||||
$queryString = http_build_query(Pusher::build_auth_query_params(
|
||||
'TestKey', 'TestSecret', 'GET', $requestPath
|
||||
);
|
||||
));
|
||||
|
||||
$request = new Request('GET', "{$requestPath}?{$queryString}&".http_build_query($routeParams));
|
||||
$response = $this->await($this->browser->get('http://localhost:4000'."{$requestPath}?{$queryString}"));
|
||||
|
||||
$controller = app(FetchUsers::class);
|
||||
|
||||
$controller->onOpen($connection, $request);
|
||||
$this->assertSame(400, $response->getStatusCode());
|
||||
$this->assertSame('{"error":"Invalid presence channel `my-channel`"}', $response->getBody()->getContents());
|
||||
}
|
||||
|
||||
public function test_it_returns_404_for_invalid_channels()
|
||||
public function test_it_returns_400_for_invalid_channels()
|
||||
{
|
||||
$this->expectException(HttpException::class);
|
||||
$this->expectExceptionMessage('Invalid presence channel');
|
||||
$this->startServer();
|
||||
|
||||
$this->newActiveConnection(['my-channel']);
|
||||
$requestPath = '/apps/1234/channels/invalid-channel/users';
|
||||
|
||||
$connection = new Mocks\Connection;
|
||||
|
||||
$requestPath = '/apps/1234/channel/invalid-channel/users';
|
||||
|
||||
$routeParams = [
|
||||
'appId' => '1234',
|
||||
'channelName' => 'invalid-channel',
|
||||
];
|
||||
|
||||
$queryString = self::build_auth_query_string(
|
||||
$queryString = http_build_query(Pusher::build_auth_query_params(
|
||||
'TestKey', 'TestSecret', 'GET', $requestPath
|
||||
);
|
||||
));
|
||||
|
||||
$request = new Request('GET', "{$requestPath}?{$queryString}&".http_build_query($routeParams));
|
||||
$response = $this->await($this->browser->get('http://localhost:4000'."{$requestPath}?{$queryString}"));
|
||||
|
||||
$controller = app(FetchUsers::class);
|
||||
|
||||
$controller->onOpen($connection, $request);
|
||||
$this->assertSame(400, $response->getStatusCode());
|
||||
$this->assertSame('{"error":"Invalid presence channel `invalid-channel`"}', $response->getBody()->getContents());
|
||||
}
|
||||
|
||||
public function test_it_returns_connected_user_information()
|
||||
|
|
@ -100,7 +69,7 @@ class FetchUsersTest extends TestCase
|
|||
'channelName' => 'presence-channel',
|
||||
];
|
||||
|
||||
$queryString = self::build_auth_query_string('TestKey', 'TestSecret', 'GET', $requestPath);
|
||||
$queryString = http_build_query(Pusher::build_auth_query_params('TestKey', 'TestSecret', 'GET', $requestPath));
|
||||
|
||||
$request = new Request('GET', "{$requestPath}?{$queryString}&".http_build_query($routeParams));
|
||||
|
||||
|
|
@ -130,7 +99,7 @@ class FetchUsersTest extends TestCase
|
|||
'channelName' => 'presence-channel',
|
||||
];
|
||||
|
||||
$queryString = self::build_auth_query_string('TestKey', 'TestSecret', 'GET', $requestPath);
|
||||
$queryString = http_build_query(Pusher::build_auth_query_params('TestKey', 'TestSecret', 'GET', $requestPath));
|
||||
|
||||
$request = new Request('GET', "{$requestPath}?{$queryString}&".http_build_query($routeParams));
|
||||
|
||||
|
|
|
|||
|
|
@ -6,6 +6,7 @@ use BeyondCode\LaravelWebSockets\API\TriggerEvent;
|
|||
use BeyondCode\LaravelWebSockets\Server\Exceptions\InvalidSignature;
|
||||
use GuzzleHttp\Psr7\Request;
|
||||
use Illuminate\Http\JsonResponse;
|
||||
use Pusher\Pusher;
|
||||
use Ratchet\ConnectionInterface;
|
||||
|
||||
class PresenceChannelTest extends TestCase
|
||||
|
|
@ -418,13 +419,13 @@ class PresenceChannelTest extends TestCase
|
|||
'appId' => '1234',
|
||||
];
|
||||
|
||||
$queryString = self::build_auth_query_string(
|
||||
$queryString = http_build_query(Pusher::build_auth_query_params(
|
||||
'TestKey', 'TestSecret', 'POST', $requestPath, [
|
||||
'name' => 'some-event',
|
||||
'channels' => ['presence-channel'],
|
||||
'data' => json_encode(['some-data' => 'yes']),
|
||||
],
|
||||
);
|
||||
));
|
||||
|
||||
$request = new Request('POST', "{$requestPath}?{$queryString}&".http_build_query($routeParams));
|
||||
|
||||
|
|
@ -459,13 +460,13 @@ class PresenceChannelTest extends TestCase
|
|||
'appId' => '1234',
|
||||
];
|
||||
|
||||
$queryString = self::build_auth_query_string(
|
||||
$queryString = http_build_query(Pusher::build_auth_query_params(
|
||||
'TestKey', 'TestSecret', 'POST', $requestPath, [
|
||||
'name' => 'some-event',
|
||||
'channels' => ['presence-channel'],
|
||||
'data' => json_encode(['some-data' => 'yes']),
|
||||
],
|
||||
);
|
||||
));
|
||||
|
||||
$request = new Request('POST', "{$requestPath}?{$queryString}&".http_build_query($routeParams));
|
||||
|
||||
|
|
@ -507,13 +508,13 @@ class PresenceChannelTest extends TestCase
|
|||
'appId' => '1234',
|
||||
];
|
||||
|
||||
$queryString = self::build_auth_query_string(
|
||||
$queryString = http_build_query(Pusher::build_auth_query_params(
|
||||
'TestKey', 'TestSecret', 'POST', $requestPath, [
|
||||
'name' => 'some-event',
|
||||
'channels' => ['presence-channel'],
|
||||
'data' => json_encode(['some-data' => 'yes']),
|
||||
],
|
||||
);
|
||||
));
|
||||
|
||||
$request = new Request('POST', "{$requestPath}?{$queryString}&".http_build_query($routeParams));
|
||||
|
||||
|
|
|
|||
|
|
@ -6,6 +6,7 @@ use BeyondCode\LaravelWebSockets\API\TriggerEvent;
|
|||
use BeyondCode\LaravelWebSockets\Server\Exceptions\InvalidSignature;
|
||||
use GuzzleHttp\Psr7\Request;
|
||||
use Illuminate\Http\JsonResponse;
|
||||
use Pusher\Pusher;
|
||||
use Ratchet\ConnectionInterface;
|
||||
|
||||
class PrivateChannelTest extends TestCase
|
||||
|
|
@ -238,13 +239,13 @@ class PrivateChannelTest extends TestCase
|
|||
'appId' => '1234',
|
||||
];
|
||||
|
||||
$queryString = self::build_auth_query_string(
|
||||
$queryString = http_build_query(Pusher::build_auth_query_params(
|
||||
'TestKey', 'TestSecret', 'POST', $requestPath, [
|
||||
'name' => 'some-event',
|
||||
'channels' => ['private-channel'],
|
||||
'data' => json_encode(['some-data' => 'yes']),
|
||||
],
|
||||
);
|
||||
));
|
||||
|
||||
$request = new Request('POST', "{$requestPath}?{$queryString}&".http_build_query($routeParams));
|
||||
|
||||
|
|
@ -279,13 +280,13 @@ class PrivateChannelTest extends TestCase
|
|||
'appId' => '1234',
|
||||
];
|
||||
|
||||
$queryString = self::build_auth_query_string(
|
||||
$queryString = http_build_query(Pusher::build_auth_query_params(
|
||||
'TestKey', 'TestSecret', 'POST', $requestPath, [
|
||||
'name' => 'some-event',
|
||||
'channels' => ['private-channel'],
|
||||
'data' => json_encode(['some-data' => 'yes']),
|
||||
],
|
||||
);
|
||||
));
|
||||
|
||||
$request = new Request('POST', "{$requestPath}?{$queryString}&".http_build_query($routeParams));
|
||||
|
||||
|
|
@ -327,13 +328,13 @@ class PrivateChannelTest extends TestCase
|
|||
'appId' => '1234',
|
||||
];
|
||||
|
||||
$queryString = self::build_auth_query_string(
|
||||
$queryString = http_build_query(Pusher::build_auth_query_params(
|
||||
'TestKey', 'TestSecret', 'POST', $requestPath, [
|
||||
'name' => 'some-event',
|
||||
'channels' => ['private-channel'],
|
||||
'data' => json_encode(['some-data' => 'yes']),
|
||||
],
|
||||
);
|
||||
));
|
||||
|
||||
$request = new Request('POST', "{$requestPath}?{$queryString}&".http_build_query($routeParams));
|
||||
|
||||
|
|
|
|||
|
|
@ -5,6 +5,7 @@ namespace BeyondCode\LaravelWebSockets\Test;
|
|||
use BeyondCode\LaravelWebSockets\API\TriggerEvent;
|
||||
use GuzzleHttp\Psr7\Request;
|
||||
use Illuminate\Http\JsonResponse;
|
||||
use Pusher\Pusher;
|
||||
use Ratchet\ConnectionInterface;
|
||||
|
||||
class PublicChannelTest extends TestCase
|
||||
|
|
@ -209,41 +210,28 @@ class PublicChannelTest extends TestCase
|
|||
|
||||
public function test_it_fires_the_event_to_public_channel()
|
||||
{
|
||||
$this->newActiveConnection(['public-channel']);
|
||||
|
||||
$connection = new Mocks\Connection;
|
||||
$this->startServer();
|
||||
|
||||
$requestPath = '/apps/1234/events';
|
||||
|
||||
$routeParams = [
|
||||
'appId' => '1234',
|
||||
];
|
||||
|
||||
$queryString = self::build_auth_query_string(
|
||||
$queryString = http_build_query(Pusher::build_auth_query_params(
|
||||
'TestKey', 'TestSecret', 'POST', $requestPath, [
|
||||
'name' => 'some-event',
|
||||
'channels' => ['public-channel'],
|
||||
'data' => json_encode(['some-data' => 'yes']),
|
||||
],
|
||||
);
|
||||
));
|
||||
|
||||
$request = new Request('POST', "{$requestPath}?{$queryString}&".http_build_query($routeParams));
|
||||
$response = $this->await($this->browser->post('http://localhost:4000'."{$requestPath}?{$queryString}"));
|
||||
|
||||
$controller = app(TriggerEvent::class);
|
||||
|
||||
$controller->onOpen($connection, $request);
|
||||
|
||||
/** @var JsonResponse $response */
|
||||
$response = array_pop($connection->sentRawData);
|
||||
|
||||
$this->assertSame([], json_decode($response->getContent(), true));
|
||||
$this->assertSame([], json_decode((string) $response->getBody(), true));
|
||||
|
||||
$this->statisticsCollector
|
||||
->getAppStatistics('1234')
|
||||
->then(function ($statistic) {
|
||||
$this->assertEquals([
|
||||
'peak_connections_count' => 1,
|
||||
'websocket_messages_count' => 1,
|
||||
'peak_connections_count' => 0,
|
||||
'websocket_messages_count' => 0,
|
||||
'api_messages_count' => 1,
|
||||
'app_id' => '1234',
|
||||
], $statistic->toArray());
|
||||
|
|
@ -260,13 +248,13 @@ class PublicChannelTest extends TestCase
|
|||
'appId' => '1234',
|
||||
];
|
||||
|
||||
$queryString = self::build_auth_query_string(
|
||||
$queryString = http_build_query(Pusher::build_auth_query_params(
|
||||
'TestKey', 'TestSecret', 'POST', $requestPath, [
|
||||
'name' => 'some-event',
|
||||
'channels' => ['public-channel'],
|
||||
'data' => json_encode(['some-data' => 'yes']),
|
||||
],
|
||||
);
|
||||
));
|
||||
|
||||
$request = new Request('POST', "{$requestPath}?{$queryString}&".http_build_query($routeParams));
|
||||
|
||||
|
|
@ -308,13 +296,13 @@ class PublicChannelTest extends TestCase
|
|||
'appId' => '1234',
|
||||
];
|
||||
|
||||
$queryString = self::build_auth_query_string(
|
||||
$queryString = http_build_query(Pusher::build_auth_query_params(
|
||||
'TestKey', 'TestSecret', 'POST', $requestPath, [
|
||||
'name' => 'some-event',
|
||||
'channels' => ['public-channel'],
|
||||
'data' => json_encode(['some-data' => 'yes']),
|
||||
],
|
||||
);
|
||||
));
|
||||
|
||||
$request = new Request('POST', "{$requestPath}?{$queryString}&".http_build_query($routeParams));
|
||||
|
||||
|
|
|
|||
|
|
@ -7,14 +7,39 @@ use BeyondCode\LaravelWebSockets\Contracts\StatisticsCollector;
|
|||
use BeyondCode\LaravelWebSockets\Contracts\StatisticsStore;
|
||||
use BeyondCode\LaravelWebSockets\Facades\WebSocketRouter;
|
||||
use BeyondCode\LaravelWebSockets\Helpers;
|
||||
use BeyondCode\LaravelWebSockets\Server\Loggers\HttpLogger;
|
||||
use BeyondCode\LaravelWebSockets\Server\Loggers\WebSocketsLogger;
|
||||
use BeyondCode\LaravelWebSockets\ServerFactory;
|
||||
use function Clue\React\Block\await;
|
||||
use Clue\React\Buzz\Browser;
|
||||
use GuzzleHttp\Psr7\Request;
|
||||
use Illuminate\Support\Facades\Redis;
|
||||
use Orchestra\Testbench\BrowserKit\TestCase as Orchestra;
|
||||
use Pusher\Pusher;
|
||||
use Ratchet\Server\IoServer;
|
||||
use React\EventLoop\Factory as LoopFactory;
|
||||
use React\EventLoop\LoopInterface;
|
||||
use React\Promise\Deferred;
|
||||
use React\Promise\PromiseInterface;
|
||||
use Symfony\Component\Console\Output\BufferedOutput;
|
||||
|
||||
abstract class TestCase extends Orchestra
|
||||
{
|
||||
const AWAIT_TIMEOUT = 5.0;
|
||||
|
||||
/**
|
||||
* The test Browser.
|
||||
*
|
||||
* @var \Clue\React\Buzz\Browser
|
||||
*/
|
||||
protected $browser;
|
||||
|
||||
/**
|
||||
* The test WebSocket server.
|
||||
*
|
||||
* @var IoServer
|
||||
*/
|
||||
protected $server;
|
||||
|
||||
/**
|
||||
* A test Pusher server.
|
||||
*
|
||||
|
|
@ -73,11 +98,31 @@ abstract class TestCase extends Orchestra
|
|||
|
||||
$this->loop = LoopFactory::create();
|
||||
|
||||
$this->app->singleton(LoopInterface::class, function () {
|
||||
return $this->loop;
|
||||
});
|
||||
|
||||
$this->browser = (new Browser($this->loop))
|
||||
->withFollowRedirects(false)
|
||||
->withRejectErrorResponse(false);
|
||||
|
||||
$this->app->singleton(HttpLogger::class, function () {
|
||||
return (new HttpLogger(new BufferedOutput()))
|
||||
->enable(false)
|
||||
->verbose(false);
|
||||
});
|
||||
|
||||
$this->app->singleton(WebSocketsLogger::class, function () {
|
||||
return (new WebSocketsLogger(new BufferedOutput()))
|
||||
->enable(false)
|
||||
->verbose(false);
|
||||
});
|
||||
|
||||
$this->replicationMode = getenv('REPLICATION_MODE') ?: 'local';
|
||||
|
||||
$this->resetDatabase();
|
||||
$this->loadLaravelMigrations(['--database' => 'sqlite']);
|
||||
$this->loadMigrationsFrom(__DIR__.'/database/migrations');
|
||||
$this->loadMigrationsFrom(__DIR__.'/../database/migrations');
|
||||
$this->withFactories(__DIR__.'/database/factories');
|
||||
|
||||
$this->registerCustomPath();
|
||||
|
|
@ -102,6 +147,15 @@ abstract class TestCase extends Orchestra
|
|||
}
|
||||
}
|
||||
|
||||
protected function tearDown(): void
|
||||
{
|
||||
parent::tearDown();
|
||||
|
||||
if ($this->server) {
|
||||
$this->server->socket->close();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
|
|
@ -270,6 +324,11 @@ abstract class TestCase extends Orchestra
|
|||
$this->channelManager = $this->app->make(ChannelManager::class);
|
||||
}
|
||||
|
||||
protected function await(PromiseInterface $promise, LoopInterface $loop = null, $timeout = null)
|
||||
{
|
||||
return await($promise, $loop ?? $this->loop, $timeout ?? static::AWAIT_TIMEOUT);
|
||||
}
|
||||
|
||||
/**
|
||||
* Unregister the managers for testing purposes.
|
||||
*
|
||||
|
|
@ -338,6 +397,19 @@ abstract class TestCase extends Orchestra
|
|||
return $connection;
|
||||
}
|
||||
|
||||
protected function joinWebSocketServer(array $channelsToJoin = [], string $appKey = 'TestKey', array $headers = [])
|
||||
{
|
||||
$promise = new Deferred();
|
||||
|
||||
\Ratchet\Client\connect("ws://localhost:4000/app/{$appKey}", [], [], $this->loop)->then(function ($conn) use ($promise) {
|
||||
$conn->on('message', function ($msg) use ($promise) {
|
||||
$promise->resolve($msg);
|
||||
});
|
||||
});
|
||||
|
||||
return $promise->promise();
|
||||
}
|
||||
|
||||
/**
|
||||
* Get a connected websocket connection.
|
||||
*
|
||||
|
|
@ -485,27 +557,16 @@ abstract class TestCase extends Orchestra
|
|||
}
|
||||
}
|
||||
|
||||
protected static function build_auth_query_string(
|
||||
$auth_key,
|
||||
$auth_secret,
|
||||
$request_method,
|
||||
$request_path,
|
||||
$query_params = [],
|
||||
$auth_version = '1.0',
|
||||
$auth_timestamp = null
|
||||
) {
|
||||
$method = method_exists(Pusher::class, 'build_auth_query_params') ? 'build_auth_query_params' : 'build_auth_query_string';
|
||||
protected function startServer()
|
||||
{
|
||||
$server = new ServerFactory('0.0.0.0', 4000);
|
||||
|
||||
$params = Pusher::$method(
|
||||
$auth_key, $auth_secret, $request_method, $request_path, $query_params, $auth_version, $auth_timestamp
|
||||
);
|
||||
WebSocketRouter::registerRoutes();
|
||||
|
||||
if ($method == 'build_auth_query_string') {
|
||||
return $params;
|
||||
}
|
||||
|
||||
ksort($params);
|
||||
|
||||
return http_build_query($params);
|
||||
$this->server = $server
|
||||
->setLoop($this->loop)
|
||||
->withRoutes(WebSocketRouter::getRoutes())
|
||||
->setConsoleOutput(new BufferedOutput())
|
||||
->createServer();
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -2,33 +2,25 @@
|
|||
|
||||
namespace BeyondCode\LaravelWebSockets\Test;
|
||||
|
||||
use BeyondCode\LaravelWebSockets\API\TriggerEvent;
|
||||
use GuzzleHttp\Psr7\Request;
|
||||
use Symfony\Component\HttpKernel\Exception\HttpException;
|
||||
use Pusher\Pusher;
|
||||
|
||||
class TriggerEventTest extends TestCase
|
||||
{
|
||||
public function test_invalid_signatures_can_not_fire_the_event()
|
||||
{
|
||||
$this->expectException(HttpException::class);
|
||||
$this->expectExceptionMessage('Invalid auth signature provided.');
|
||||
$this->startServer();
|
||||
|
||||
$connection = new Mocks\Connection;
|
||||
|
||||
$requestPath = '/apps/1234/events';
|
||||
|
||||
$routeParams = [
|
||||
'appId' => '1234',
|
||||
];
|
||||
|
||||
$queryString = self::build_auth_query_string(
|
||||
$queryString = http_build_query(Pusher::build_auth_query_params(
|
||||
'TestKey', 'InvalidSecret', 'GET', $requestPath
|
||||
);
|
||||
));
|
||||
|
||||
$request = new Request('GET', "{$requestPath}?{$queryString}&".http_build_query($routeParams));
|
||||
$response = $this->await($this->browser->get('http://localhost:4000'."{$requestPath}?{$queryString}"));
|
||||
|
||||
$controller = app(TriggerEvent::class);
|
||||
|
||||
$controller->onOpen($connection, $request);
|
||||
$this->assertSame(405, $response->getStatusCode());
|
||||
$this->assertSame('', $response->getBody()->getContents());
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,35 +0,0 @@
|
|||
<?php
|
||||
|
||||
use Illuminate\Database\Migrations\Migration;
|
||||
use Illuminate\Database\Schema\Blueprint;
|
||||
use Illuminate\Support\Facades\Schema;
|
||||
|
||||
class CreateWebSocketsStatisticsEntriesTable extends Migration
|
||||
{
|
||||
/**
|
||||
* Run the migrations.
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function up()
|
||||
{
|
||||
Schema::create('websockets_statistics_entries', function (Blueprint $table) {
|
||||
$table->increments('id');
|
||||
$table->string('app_id');
|
||||
$table->integer('peak_connections_count');
|
||||
$table->integer('websocket_messages_count');
|
||||
$table->integer('api_messages_count');
|
||||
$table->nullableTimestamps();
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Reverse the migrations.
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function down()
|
||||
{
|
||||
Schema::dropIfExists('websockets_statistics_entries');
|
||||
}
|
||||
}
|
||||
Loading…
Reference in New Issue