├── .editorconfig
├── .gitignore
├── .travis.yml
├── LICENSE
├── README.md
├── composer.json
├── config
├── bootstrap.php
├── routes.php
└── websocket.default.php
├── phpunit.xml.dist
├── src
├── Controller
│ └── ExampleController.php
├── Lib
│ ├── Websocket.php
│ ├── WebsocketInterface.php
│ └── WebsocketWorker.php
├── Shell
│ └── WebsocketServerShell.php
└── Template
│ ├── Element
│ ├── user_data_panel.ctp
│ └── user_form_panel.ctp
│ └── Example
│ └── index.ctp
├── webroot
└── js
│ ├── app
│ └── controllers
│ │ └── example
│ │ ├── index_controller.js
│ │ ├── user_data_panel_controller.js
│ │ └── user_form_panel_controller.js
│ └── lib
│ └── websocket.js
└── websocket.png
/.editorconfig:
--------------------------------------------------------------------------------
1 | ; This file is for unifying the coding style for different editors and IDEs.
2 | ; More information at http://editorconfig.org
3 |
4 | root = true
5 |
6 | [*]
7 | indent_style = space
8 | indent_size = 4
9 | end_of_line = lf
10 | insert_final_newline = true
11 | trim_trailing_whitespace = true
12 |
13 | [*.bat]
14 | end_of_line = crlf
15 |
16 | [*.yml]
17 | indent_style = space
18 | indent_size = 2
19 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | .DS_Store
2 | .tags
3 | composer.lock
4 | /vendor/*
5 |
--------------------------------------------------------------------------------
/.travis.yml:
--------------------------------------------------------------------------------
1 | language: php
2 |
3 | php:
4 | - 7.1
5 |
6 | before_script:
7 | - composer self-update
8 | - composer install --prefer-dist
9 |
10 | script:
11 | - vendor/bin/phpcs -pn --extensions=php --standard=vendor/cakephp/cakephp-codesniffer/CakePHP ./src
12 |
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | MIT License
2 |
3 | Copyright (c) 2017 scherer software (http://scherer-software.de)
4 |
5 | Permission is hereby granted, free of charge, to any person obtaining a copy
6 | of this software and associated documentation files (the "Software"), to deal
7 | in the Software without restriction, including without limitation the rights
8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9 | copies of the Software, and to permit persons to whom the Software is
10 | furnished to do so, subject to the following conditions:
11 |
12 | The above copyright notice and this permission notice shall be included in all
13 | copies or substantial portions of the Software.
14 |
15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21 | SOFTWARE.
22 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | 
2 |
3 | [](https://travis-ci.org/scherersoftware/cake-websocket)
4 | [](https://packagist.org/packages/scherersoftware/cake-websocket)
5 | [](https://packagist.org/packages/scherersoftware/cake-websocket)
6 | [](https://packagist.org/packages/scherersoftware/cake-websocket)
7 | [](https://packagist.org/packages/scherersoftware/cake-websocket)
8 |
9 | ## Introduction
10 |
11 | This CakePHP 3 plugin gives you an easy way to add websocket capability to your web application.
12 |
13 | #### Main Packages
14 |
15 | - [Cake Frontend Bridge](https://github.com/scherersoftware/cake-frontend-bridge)
16 | - [Ratchet](https://github.com/ratchetphp/Ratchet)
17 | - [CakePHP Queuesadilla](https://github.com/josegonzalez/cakephp-queuesadilla)
18 |
19 | #### Requirements
20 |
21 | - CakePHP 3.3 or higher
22 | - PHP 7.1
23 |
24 | ---
25 |
26 | ## Usage in 4 easy steps
27 |
28 | **Note:** You can checkout our [CakePHP App Template](https://github.com/scherersoftware/cake-app-template) for testing it on a clean app setup with preinstalled dependencies.
29 |
30 | #### 1. Define a new event
31 |
32 | **Example for `websocket_events.php`**
33 |
34 | ```
35 | ...
36 | 'userDataUpdated' => [
37 | 'audience' => [
38 | 'includeAllNotAuthenticated' => false,
39 | 'includeAllAuthenticated' => true
40 | ]
41 | ]
42 | ...
43 | ```
44 |
45 | #### 2. Publish the event in server context (e.g. Shell, Controller, Table...)
46 |
47 | **Example for `UsersController.php`**
48 |
49 | ```
50 | ...
51 | use Websocket\Lib\Websocket;
52 | ...
53 | if ($this->Users->save($exampleUser)) {
54 | Websocket::publishEvent('userDataUpdated', ['editedUserId' => $exampleUser->id]);
55 | }
56 | ...
57 | ```
58 |
59 | #### 3. Let the client receive the event and define a callback
60 |
61 | **Example for `../users/index_controller.js`**
62 |
63 | ```
64 | ...
65 | App.Websocket.onEvent('userDataUpdated', function(payload) {
66 | if (payload.editedUserId === this.exampleUser.id) {
67 | alert('Someone changed the data of this user!');
68 | }
69 | }.bind(this));
70 | ...
71 | ```
72 |
73 | #### 4. Run the websocket server shell and start testing!
74 |
75 | ```
76 | $ bin/cake websocket_server
77 | ```
78 |
79 | ---
80 |
81 | ## Installation
82 |
83 | #### 1. Require the plugin
84 |
85 | You can install this plugin into your CakePHP application using [composer](http://getcomposer.org).
86 |
87 | The recommended way to install composer packages is:
88 |
89 | ```
90 | composer require scherersoftware/cake-websocket
91 | ```
92 |
93 | #### 2. Load the plugin
94 |
95 | The next step is to load the plugin properly inside your bootstrap.php:
96 |
97 | ```
98 | Plugin::load('Websocket', ['bootstrap' => true, 'routes' => true]);
99 | ```
100 |
101 | #### 3. Configure app config
102 |
103 | - **File:** `/config/app.php`
104 |
105 | ```
106 | [
109 | 'ssl' => false,
110 | 'host' => '127.0.0.1',
111 | 'externalHost' => 'cws.dev',
112 | 'port' => 8889,
113 | 'frontendPath' => [
114 | 'ssl' => [
115 | 'path' => '/wss/',
116 | 'usePort' => false
117 | ],
118 | 'normal' => [
119 | 'path' => '/',
120 | 'usePort' => true
121 | ]
122 | ],
123 | 'sessionCookieName' => 'cws',
124 | 'Queue' => [
125 | 'name' => 'websocket',
126 | 'loopInterval' => 0.1,
127 | ]
128 | ]
129 | ...
130 | ```
131 |
132 | #### 4. Create and configure websocket events
133 |
134 | - **File:** `/config/websocket_events.php`
135 |
136 | ```
137 | [
140 | 'audience' => [
141 | 'includeAllNotAuthenticated' => false,
142 | 'includeAllAuthenticated' => true
143 | ]
144 | ]
145 | ];
146 | ```
147 |
148 | #### 5. Configure `AppController.php`
149 |
150 | In your `src/Controller/AppController.php`, insert the following pieces of code
151 |
152 |
153 | **Usage:**
154 |
155 | ```
156 | use Websocket\Lib\Websocket;
157 | ```
158 |
159 | **beforeFilter():**
160 |
161 | ```
162 | ...
163 | $this->FrontendBridge->setJson('websocketFrontendConfig', Websocket::getFrontendConfig());
164 | ...
165 | ```
166 |
167 |
168 | #### 6. Make the JS websocket lib globally accessible under `App.Websocket`
169 |
170 | - Load the file /webroot/lib/websocket.js after loading the Frontend Bridge assets
171 |
172 | #### 7. Setup sessions properly if not alread done
173 |
174 | Please follow the [Cake Sessions Documentation](https://book.cakephp.org/3.0/en/development/sessions.html)
175 |
176 | #### 8. Setup Apache SSL ProxyPass if necessary
177 |
178 | Make sure these modules are activated:
179 | - mod_proxy.so
180 | - mod_proxy_wstunnel.so
181 |
182 | Edit your vhosts configuration and add this to the ssl section:
183 |
184 | ```
185 | ProxyPass /wss/ ws://localhost:8889/
186 | ```
187 |
188 | ---
189 |
190 | ## Roadmap
191 |
192 | #### 1.1.0
193 | - Unit Tests
194 | - Add a websocket_connections table which stores all active connections
195 |
--------------------------------------------------------------------------------
/composer.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "scherersoftware/cake-websocket",
3 | "description": "Websocket plugin for CakePHP",
4 | "license": "MIT",
5 | "type": "cakephp-plugin",
6 | "homepage": "https://github.com/scherersoftware/cake-websocket",
7 | "require": {
8 | "php": ">=7.1.0",
9 | "cakephp/cakephp": ">=3.3.2 < 4.0.0",
10 | "react/event-loop": "0.4.*|0.3.*",
11 | "react/Socket": "^0.3 || ^0.4",
12 | "cboden/ratchet": "^0.3.6",
13 | "josegonzalez/cakephp-queuesadilla": ">=0.3.5",
14 | "codekanzlei/cake-frontend-bridge": ">=1.0.0"
15 | },
16 | "require-dev": {
17 | "phpunit/phpunit": "*",
18 | "cakephp/cakephp-codesniffer": "dev-master"
19 | },
20 | "autoload": {
21 | "psr-4": {
22 | "Websocket\\": "src"
23 | }
24 | },
25 | "autoload-dev": {
26 | "psr-4": {
27 | "Websocket\\Test\\": "tests",
28 | "Cake\\Test\\": "./vendor/cakephp/cakephp/tests"
29 | }
30 | },
31 | "scripts": {
32 | "cs-check": "vendor/bin/phpcs --colors -p --standard=vendor/cakephp/cakephp-codesniffer/CakePHP ./src"
33 | }
34 | }
35 |
--------------------------------------------------------------------------------
/config/bootstrap.php:
--------------------------------------------------------------------------------
1 | '/websocket'],
9 | function (RouteBuilder $routes) {
10 | $routes->fallbacks(DashedRoute::class);
11 | }
12 | );
13 |
--------------------------------------------------------------------------------
/config/websocket.default.php:
--------------------------------------------------------------------------------
1 | [
4 | 'ssl' => false,
5 | 'host' => 'cws.dev',
6 | 'externalHost' => 'cws.dev',
7 | 'port' => 8889,
8 | 'frontendPath' => [
9 | 'ssl' => [
10 | 'path' => '/wss/',
11 | 'usePort' => false
12 | ],
13 | 'normal' => [
14 | 'path' => '/',
15 | 'usePort' => true
16 | ]
17 | ],
18 | 'sessionCookieName' => 'cws',
19 | 'Queue' => [
20 | 'name' => 'websocket',
21 | 'loopInterval' => 0.1,
22 | ]
23 | ]
24 | ];
25 |
--------------------------------------------------------------------------------
/phpunit.xml.dist:
--------------------------------------------------------------------------------
1 |
2 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 | ./tests/TestCase
18 |
19 |
20 |
21 |
22 |
23 |
26 |
27 |
28 |
29 |
30 |
31 |
32 |
33 |
34 |
35 | ./vendor/
36 | ./vendor/
37 |
38 | ./tests/
39 | ./tests/
40 |
41 |
42 |
43 |
44 |
--------------------------------------------------------------------------------
/src/Controller/ExampleController.php:
--------------------------------------------------------------------------------
1 | loadModel('Users');
19 |
20 | $exampleUser = $this->Users->find()
21 | ->order([
22 | 'created' => 'ASC'
23 | ])
24 | ->first();
25 |
26 | $this->set('exampleUser', $exampleUser);
27 | }
28 |
29 | /**
30 | * action to load data panel of user
31 | *
32 | * @return \Cake\Network\Response
33 | */
34 | public function userDataPanel(): Response
35 | {
36 | $this->loadModel('Users');
37 |
38 | $exampleUser = $this->Users->find()
39 | ->order([
40 | 'created' => 'ASC'
41 | ])
42 | ->first();
43 |
44 | $this->FrontendBridge->setBoth('exampleUser', $exampleUser);
45 |
46 | return $this->render('/Element/user_data_panel');
47 | }
48 |
49 | /**
50 | * action to load form panel of user
51 | *
52 | * @return \Cake\Network\Response
53 | */
54 | public function userFormPanel(): Response
55 | {
56 | $this->loadModel('Users');
57 |
58 | $exampleUser = $this->Users->find()
59 | ->order([
60 | 'created' => 'ASC'
61 | ])
62 | ->first();
63 |
64 | if ($this->request->is(['post', 'patch', 'put'])) {
65 | $exampleUser->accessible('*', false);
66 | $exampleUser->accessible(['firstname', 'lastname'], true);
67 | $this->Users->patchEntity($exampleUser, $this->request->data, [
68 | 'validate' => false
69 | ]);
70 |
71 | if ($this->Users->save($exampleUser)) {
72 | Websocket::publishEvent('userDataUpdated', ['editedUserId' => $exampleUser->id], []);
73 | }
74 | }
75 |
76 | $this->FrontendBridge->setBoth('exampleUser', $exampleUser);
77 |
78 | return $this->render('/Element/user_form_panel');
79 | }
80 | }
81 |
--------------------------------------------------------------------------------
/src/Lib/Websocket.php:
--------------------------------------------------------------------------------
1 | false,
23 | * // whether all authenticated clients should receive the event (overwrites event default)
24 | * 'includeAllAuthenticated' => true,
25 | * // authenticated clients to send the event to (works independent of the settings above)
26 | * 'userIds' => []
27 | * ]
28 | * @return bool
29 | * @throws \Exception if config of given event name is invalid
30 | */
31 | public static function publishEvent(string $eventName, array $payload = [], array $audience = []): bool
32 | {
33 | if (defined('PHPUNIT_TESTSUITE')) {
34 | return false;
35 | }
36 |
37 | if (!self::validateEventConfig($eventName)) {
38 | throw new \Exception('Invalid Websocket event config.');
39 | }
40 |
41 | $audience = Hash::merge(Configure::read('Websocket.events.' . $eventName . '.audience'), $audience);
42 |
43 | return Queue::push($eventName, [
44 | 'payload' => $payload,
45 | 'audience' => $audience
46 | ], [
47 | 'queue' => Configure::read('Websocket.Queue.name')
48 | ]);
49 | }
50 |
51 | /**
52 | * Validates config of one given or all configured events
53 | *
54 | * @param string|null $eventName name of event
55 | * @return bool
56 | */
57 | public static function validateEventConfig(string $eventName = null): bool
58 | {
59 | $eventConfigs = Configure::read('Websocket.events');
60 |
61 | if (!is_null($eventName) && !array_key_exists($eventName, $eventConfigs)) {
62 | return false;
63 | }
64 |
65 | foreach ($eventConfigs as $eventName => $eventConfig) {
66 | if (empty($eventConfig['audience'])) {
67 | return false;
68 | }
69 |
70 | if (!isset($eventConfig['audience']['includeAllNotAuthenticated'])
71 | || !is_bool($eventConfig['audience']['includeAllNotAuthenticated'])
72 | ) {
73 | return false;
74 | }
75 |
76 | if (!isset($eventConfig['audience']['includeAllAuthenticated'])
77 | || !is_bool($eventConfig['audience']['includeAllAuthenticated'])
78 | ) {
79 | return false;
80 | }
81 |
82 | if (isset($eventConfig['audience']['userIds'])) {
83 | if (!is_array($eventConfig['audience']['userIds'])) {
84 | return false;
85 | }
86 | } else {
87 | Configure::write('Websocket.events.' . $eventName . '.audience.userIds', []);
88 | }
89 | }
90 |
91 | return true;
92 | }
93 |
94 | /**
95 | * get reduced websocket config for the frontend
96 | *
97 | * @return array
98 | */
99 | public static function getFrontendConfig(): array
100 | {
101 | $paths = Configure::read('Websocket.frontendPath');
102 | $pathUsed = Configure::read('Websocket.ssl') ? $paths['ssl'] : $paths['normal'];
103 | $host = Configure::read('Websocket.ssl') ? 'wss://' : 'ws://';
104 | $host .= Configure::read('Websocket.externalHost');
105 |
106 | return [
107 | 'host' => $host,
108 | 'port' => Configure::read('Websocket.port'),
109 | 'path' => $pathUsed['path'],
110 | 'usePort' => $pathUsed['usePort']
111 | ];
112 | }
113 | }
114 |
--------------------------------------------------------------------------------
/src/Lib/WebsocketInterface.php:
--------------------------------------------------------------------------------
1 | __session = Session::create(Configure::read('Session'));
37 | }
38 |
39 | /**
40 | * Publish an event base on given queue entry
41 | *
42 | * @param array $queueEntry database entry array
43 | * @return true
44 | */
45 | public function publishEvent(array $queueEntry): bool
46 | {
47 | $eventName = $queueEntry['class'];
48 | $payload = $queueEntry['args'][0]['payload'];
49 | $audience = $queueEntry['args'][0]['audience'];
50 |
51 | foreach ($this->__connections as $connection) {
52 | if ($this->_userIsInAudience($connection['userId'], $audience)) {
53 | $connection['connection']->send(json_encode(compact('eventName', 'payload')));
54 | }
55 | }
56 |
57 | return true;
58 | }
59 |
60 | /**
61 | * {@inheritDoc}
62 | */
63 | public function onOpen(ConnectionInterface $connection): void
64 | {
65 | $symphonySessionId = $connection->wrappedConn->WAMP->sessionId;
66 | $this->__connections[$symphonySessionId]['connection'] = $connection;
67 | $this->__connections[$symphonySessionId]['userId'] = $this->__getUserIdFromSession($connection);
68 | }
69 |
70 | /**
71 | * {@inheritDoc}
72 | */
73 | public function onClose(ConnectionInterface $connection): void
74 | {
75 | unset($this->__connections[$connection->wrappedConn->WAMP->sessionId]);
76 | }
77 |
78 | /**
79 | * {@inheritDoc}
80 | */
81 | public function onError(ConnectionInterface $connection, \Exception $e): void
82 | {
83 | unset($this->__connections[$connection->wrappedConn->WAMP->sessionId]);
84 | }
85 |
86 | /**
87 | * {@inheritDoc}
88 | */
89 | public function onCall(ConnectionInterface $connection, $id, $topic, array $params): void
90 | {
91 | $connection->close();
92 | }
93 |
94 | /**
95 | * {@inheritDoc}
96 | */
97 | public function onPublish(ConnectionInterface $connection, $topic, $event, array $exclude, array $eligible): void
98 | {
99 | $connection->close();
100 | }
101 |
102 | /**
103 | * {@inheritDoc}
104 | */
105 | public function onUnSubscribe(ConnectionInterface $connection, $topic): void
106 | {
107 | $connection->close();
108 | }
109 |
110 | /**
111 | * {@inheritDoc}
112 | */
113 | public function onSubscribe(ConnectionInterface $connection, $topic): void
114 | {
115 | $connection->close();
116 | }
117 |
118 | /**
119 | * Check if user is in given target audience
120 | *
121 | * @param null|string $userId user identifier or null if user is not logged in
122 | * @param array $audience ruleset of audience for event
123 | * @return bool
124 | */
125 | protected function _userIsInAudience(?string $userId, array $audience): bool
126 | {
127 | if (in_array($userId, $audience['userIds'])) {
128 | return true;
129 | }
130 | if (empty($userId)) {
131 | return $audience['includeAllNotAuthenticated'];
132 | } else {
133 | return $audience['includeAllAuthenticated'];
134 | }
135 | }
136 |
137 | /**
138 | * Get the user id from the session
139 | *
140 | * @param ConnectionInterface $connection websocket client connection
141 | * @return null|string
142 | */
143 | private function __getUserIdFromSession(ConnectionInterface $connection): ?string
144 | {
145 | $userId = null;
146 | $sessionId = null;
147 | $sessionCookieName = Configure::read('Websocket.sessionCookieName');
148 | if (!empty($connection->WebSocket->request->getCookies()[$sessionCookieName])) {
149 | $sessionId = $connection->WebSocket->request->getCookies()[$sessionCookieName];
150 | }
151 | if (!empty($sessionId)) {
152 | session_abort();
153 | $this->__session->id($sessionId);
154 | session_start();
155 | $userId = $this->__session->read('Auth.User.id');
156 | }
157 |
158 | return $userId;
159 | }
160 | }
161 |
--------------------------------------------------------------------------------
/src/Lib/WebsocketWorker.php:
--------------------------------------------------------------------------------
1 | __loop = $loop;
52 | $this->__websocketInterface = $websocketInterface;
53 | $this->__logger = $logger;
54 |
55 | $this->__loop->addPeriodicTimer(Configure::read('Websocket.Queue.loopInterval'), function () {
56 | $this->work();
57 | });
58 |
59 | $this->__initialSetup();
60 | }
61 |
62 | /**
63 | * {@inheritDoc}
64 | * @SuppressWarnings(PHPMD.CyclomaticComplexity)
65 | */
66 | public function work()
67 | {
68 | if (!$this->connect()) {
69 | $this->logger()->alert(sprintf('Worker unable to connect, exiting'));
70 | $this->dispatchEvent('Worker.job.connectionFailed');
71 |
72 | return false;
73 | }
74 |
75 | $jobClass = $this->engine->getJobClass();
76 |
77 | $this->iterations++;
78 | $item = $this->engine->pop($this->queue);
79 | $this->dispatchEvent('Worker.job.seen', ['item' => $item]);
80 | if (empty($item)) {
81 | $this->logger()->debug('No job!');
82 | $this->dispatchEvent('Worker.job.empty');
83 |
84 | return;
85 | }
86 |
87 | $success = false;
88 | $job = new $jobClass($item, $this->engine);
89 |
90 | try {
91 | $success = $this->perform($item);
92 | } catch (Exception $e) {
93 | $this->logger()->alert(sprintf('Exception: "%s"', $e->getMessage()));
94 | $this->dispatchEvent('Worker.job.exception', [
95 | 'job' => $job,
96 | 'exception' => $e,
97 | ]);
98 | }
99 |
100 | if ($success) {
101 | $this->logger()->debug('Success. Acknowledging job on queue.');
102 | $job->acknowledge();
103 | $this->dispatchEvent('Worker.job.success', ['job' => $job]);
104 |
105 | return;
106 | }
107 |
108 | $this->logger()->info('Failed. Releasing job to queue');
109 | $job->release();
110 | $this->dispatchEvent('Worker.job.failure', ['job' => $job]);
111 |
112 | return true;
113 | }
114 |
115 | /**
116 | * Connect to the data storage using the configured engine
117 | *
118 | * @return bool
119 | */
120 | public function connect(): bool
121 | {
122 | $maxIterations = $this->maxIterations ? sprintf(', max iterations %s', $this->maxIterations) : '';
123 | $this->logger()->info(sprintf('Starting worker%s', $maxIterations));
124 |
125 | return (bool)$this->engine->connection();
126 | }
127 |
128 | /**
129 | * Publishes event based on database entry
130 | *
131 | * @param array $queueEntry database entry
132 | * @return bool
133 | */
134 | public function perform(array $queueEntry): bool
135 | {
136 | return $this->__websocketInterface->publishEvent($queueEntry);
137 | }
138 |
139 | /**
140 | * helper method to do all the dirty construction work
141 | *
142 | * @return void
143 | */
144 | private function __initialSetup(): void
145 | {
146 | // FIXME clean this up maybe
147 | $logger = Log::engine('error');
148 | if (!empty($this->__logger)) {
149 | $logger = $this->__logger;
150 | }
151 | $engine = Queue::engine('default');
152 | $engine->setLogger($logger);
153 | $engine->config('queue', Configure::read('Websocket.Queue.name'));
154 | $this->engine = $engine;
155 | $this->queue = Configure::read('Websocket.Queue.name');
156 | $this->maxIterations = null;
157 | $this->iterations = 0;
158 | $this->maxRuntime = null;
159 | $this->runtime = 0;
160 | $this->name = get_class($this->engine) . ' Worker';
161 | $this->setLogger($logger);
162 | $this->StatsListener = new StatsListener;
163 | $this->attachListener($this->StatsListener);
164 | register_shutdown_function([&$this, 'shutdownHandler']);
165 | }
166 |
167 | /**
168 | * {@inheritDoc}
169 | */
170 | protected function disconnect()
171 | {
172 | }
173 | }
174 |
--------------------------------------------------------------------------------
/src/Shell/WebsocketServerShell.php:
--------------------------------------------------------------------------------
1 | addOption('logger', [
36 | 'help' => 'Name of a configured logger',
37 | 'short' => 'l',
38 | ]);
39 |
40 | return $parser;
41 | }
42 |
43 | /**
44 | * main function
45 | *
46 | * @return void
47 | */
48 | public function main(): void
49 | {
50 | $loop = Factory::create();
51 | $websocketInterface = new WebsocketInterface;
52 |
53 | $logger = null;
54 | if (isset($this->params['logger']) && Log::engine($this->params['logger']) !== false) {
55 | $logger = Log::engine($this->params['logger']);
56 | }
57 |
58 | $websocketWorker = new WebsocketWorker($loop, $websocketInterface, Queue::engine('default'), $logger);
59 |
60 | $websocketWorker->attachListener('Worker.job.exception', function ($event) {
61 | $exception = $event->data['exception'];
62 | $exception->job = $event->data['job'];
63 | $sentryHandler = new SentryHandler();
64 | $sentryHandler->handle($exception);
65 | });
66 |
67 | $websocketWorker->attachListener('Worker.job.start', function ($event) {
68 | ConnectionManager::get('default')->disconnect();
69 | });
70 |
71 | $websocketWorker->attachListener('Worker.job.success', function ($event) {
72 | ConnectionManager::get('default')->disconnect();
73 | });
74 |
75 | $websocketWorker->attachListener('Worker.job.failure', function ($event) {
76 | $failedJob = $event->data['job'];
77 | $failedItem = $failedJob->item();
78 | $options = [
79 | 'queue' => 'failed',
80 | 'failedJob' => $failedJob
81 | ];
82 | Queue::push($failedItem['class'], $failedJob->data(), $options);
83 | ConnectionManager::get('default')->disconnect();
84 | });
85 |
86 | $serverSocket = new Server($loop);
87 | $serverSocket->listen(Configure::read('Websocket.port'), Configure::read('Websocket.host'));
88 |
89 | $webServer = new IoServer(
90 | new HttpServer(
91 | new WsServer(
92 | new WampServer(
93 | $websocketInterface
94 | )
95 | )
96 | ),
97 | $serverSocket
98 | );
99 |
100 | $loop->run();
101 | }
102 | }
103 |
--------------------------------------------------------------------------------
/src/Template/Element/user_data_panel.ctp:
--------------------------------------------------------------------------------
1 |
2 |
7 |
8 |
9 | - Firstname
10 | - = $exampleUser->firstname ?>
11 |
12 | - Lastname
13 | - = $exampleUser->lastname ?>
14 |
15 |
16 |
17 |
--------------------------------------------------------------------------------
/src/Template/Element/user_form_panel.ctp:
--------------------------------------------------------------------------------
1 |
19 |
--------------------------------------------------------------------------------
/src/Template/Example/index.ctp:
--------------------------------------------------------------------------------
1 | Websocket Example
2 |
3 |
11 |
--------------------------------------------------------------------------------
/webroot/js/app/controllers/example/index_controller.js:
--------------------------------------------------------------------------------
1 | App.Controllers.ExampleIndexController = Frontend.AppController.extend({
2 | startup: function() {
3 | App.Main.loadJsonAction({
4 | 'plugin': 'websocket',
5 | 'controller': 'example',
6 | 'action': 'userDataPanel'
7 | }, {
8 | target: this.$('.user-data-panel-target'),
9 | replaceTarget: true
10 | });
11 |
12 | App.Main.loadJsonAction({
13 | 'plugin': 'websocket',
14 | 'controller': 'example',
15 | 'action': 'userFormPanel'
16 | }, {
17 | target: this.$('.user-form-panel-target'),
18 | replaceTarget: true
19 | });
20 | }
21 | });
22 |
--------------------------------------------------------------------------------
/webroot/js/app/controllers/example/user_data_panel_controller.js:
--------------------------------------------------------------------------------
1 | App.Controllers.ExampleUserDataPanelController = Frontend.AppController.extend({
2 | exampleUser: null,
3 | startup: function() {
4 | this.exampleUser = this.getVar('exampleUser');
5 | App.Websocket.onEvent('userDataUpdated', function(payload) {
6 | if (payload.editedUserId === this.exampleUser.id) {
7 | this.reloadPanel();
8 | }
9 |
10 | }.bind(this));
11 | },
12 | reloadPanel: function() {
13 | var url = {
14 | 'plugin': 'websocket',
15 | 'controller': 'example',
16 | 'action': 'userDataPanel'
17 | };
18 | App.Main.loadJsonAction(url, {
19 | target: $(this._dom),
20 | replaceTarget: true
21 | });
22 | }
23 | });
24 |
--------------------------------------------------------------------------------
/webroot/js/app/controllers/example/user_form_panel_controller.js:
--------------------------------------------------------------------------------
1 | App.Controllers.ExampleUserFormPanelController = Frontend.AppController.extend({
2 | startup: function() {
3 | var $form = this.$('form');
4 | $form.on('submit', function(e){
5 | e.preventDefault();
6 | var url = {
7 | 'plugin': 'websocket',
8 | 'controller': 'example',
9 | 'action': 'userFormPanel'
10 | };
11 | App.Main.loadJsonAction(url, {
12 | data: $form.serialize(),
13 | onComplete: function() {
14 | this.$('input[type="text"]').val('');
15 | }.bind(this)
16 | });
17 | }.bind(this));
18 | }
19 | });
20 |
--------------------------------------------------------------------------------
/webroot/js/lib/websocket.js:
--------------------------------------------------------------------------------
1 | Frontend.App.Websocket = Class.extend({
2 | _socket: null,
3 | _eventCallbacks: {},
4 | _isConnected: false,
5 | _host: null,
6 | _port: null,
7 | _reconnectCounter: 0,
8 | init: function(config) {
9 | this._host = config.host;
10 | this._port = config.port;
11 | this._path = config.path;
12 | this._usePort = config.usePort;
13 | },
14 | setup: function() {
15 | if (this._isConnected) {
16 | return;
17 | }
18 |
19 | try {
20 | this._socket = new WebSocket(this._buildUrl());
21 | this._socket.onopen = function (e) {
22 | this._isConnected = true;
23 | this.onOpened(e)
24 | return;
25 | }.bind(this);
26 | this._socket.onclose = function (e) {
27 | this._isConnected = false;
28 | this.onClosed(e)
29 | this._triggerReconnect()
30 | return;
31 | }.bind(this);
32 | } catch (e) {
33 | this._isConnected = false;
34 | this._triggerReconnect()
35 | }
36 | },
37 | onOpened: function(e) {
38 | this._socket.onmessage = function(event) {
39 | var data = JSON.parse(event.data);
40 | if (this._eventCallbacks[data.eventName] !== undefined) {
41 | $.each(this._eventCallbacks[data.eventName], function(callbackId, callback) {
42 | if (typeof callback === "function") {
43 | callback(data.payload);
44 | }
45 | }.bind(this));
46 | }
47 | }.bind(this);
48 | },
49 | onClosed: function(e) {
50 | },
51 | onEvent: function(action, callback) {
52 | if (!(action in this._eventCallbacks)) {
53 | this._eventCallbacks[action] = [];
54 | }
55 | this._eventCallbacks[action].push(callback);
56 |
57 | return this._eventCallbacks[action].length - 1;
58 | },
59 | removeEventCallback: function(action, callbackId) {
60 | if (action in this._eventCallbacks && callbackId in this._eventCallbacks[action]) {
61 | delete this._eventCallbacks[action][callbackId];
62 | return true;
63 | }
64 |
65 | return false;
66 | },
67 | _triggerReconnect: function() {
68 | if (this._isConnected || this._reconnectCounter > 10) {
69 | return;
70 | }
71 | this._reconnectCounter++;
72 | setTimeout(function() {
73 | this.setup();
74 | }.bind(this), 2000);
75 | },
76 | _buildUrl: function() {
77 | var url = this._host;
78 | if (this._usePort) {
79 | url += ':' + this._port;
80 | }
81 | url += this._path;
82 | return url;
83 | }
84 | });
85 | window.App.Websocket = new Frontend.App.Websocket(App.Main.appData.jsonData.websocketFrontendConfig);
86 | $(document).ready(function() {
87 | window.App.Websocket.setup();
88 | });
89 |
--------------------------------------------------------------------------------
/websocket.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/scherersoftware/cake-websocket/c4f84f70adb0524997d07785dfbfab14eaf20a3e/websocket.png
--------------------------------------------------------------------------------