From ab9ca9d1e8e007d8d3ec6123675170e6ae7ba084 Mon Sep 17 00:00:00 2001 From: Larry Garfield Date: Mon, 12 Dec 2016 17:01:27 -0600 Subject: [PATCH 1/2] A simple websocket chat server based on a SitePoint article. --- .platform.app.yaml | 9 + .platform/routes.yaml | 7 + README.md | 6 +- composer.json | 8 +- composer.lock | 501 +++++++++++++++++++++++++++++++++++++++++- run.php | 26 ++- src/Chat.php | 43 ++++ web/css/style.css | 60 +++++ web/index.html | 47 ++++ web/js/main.js | 78 +++++++ 10 files changed, 768 insertions(+), 17 deletions(-) create mode 100644 src/Chat.php create mode 100644 web/css/style.css create mode 100644 web/index.html create mode 100644 web/js/main.js diff --git a/.platform.app.yaml b/.platform.app.yaml index 4704f6a..3f82322 100644 --- a/.platform.app.yaml +++ b/.platform.app.yaml @@ -19,6 +19,15 @@ web: upstream: socket_family: tcp protocol: http + locations: + /: + root: web + scripts: false + allow: true + index: + - index.html + /ws: + passthru: true # The size of the persistent disk of the application (in MB). disk: 512 diff --git a/.platform/routes.yaml b/.platform/routes.yaml index cc2220b..5d6e1b9 100644 --- a/.platform/routes.yaml +++ b/.platform/routes.yaml @@ -2,3 +2,10 @@ type: upstream # the first part should be your project name upstream: "app:http" + +"http://{default}/ws": + type: upstream + # the first part should be your project name + upstream: "app:http" + cache: + enabled: false diff --git a/README.md b/README.md index 6a38abd..1db12bd 100644 --- a/README.md +++ b/README.md @@ -1,6 +1,6 @@ -# React PHP template for Platform.sh +# React PHP Websocket example for Platform.sh -This project provides a starter kit for using React PHP on Platform.sh. It is only a basic Hello-World level example, but can be used as the basis for an actual project. +This branch shows how to setup a simple chat server using React PHP and the Ratchet websocket library on Platform.sh. It is very closely based on [this tutorial](https://www.sitepoint.com/how-to-quickly-build-a-chat-app-with-ratchet/) from SitePoint.com. ## Starting a new project @@ -12,7 +12,7 @@ To start a new project based on this template, follow these 3 simple steps: 3. Run the provided Git commands to add a Platform.sh remote and push the code to the Platform.sh repository. -That's it! You now have a working "hello world" level project you can build on. +That's it! Now visit your site in 2 separate browser windows and enter a chat name. You'll be able to enter a message on one window and have the message immediately appear in the other. ## Using as a reference diff --git a/composer.json b/composer.json index ff081c6..2c55b5b 100644 --- a/composer.json +++ b/composer.json @@ -1,9 +1,13 @@ { "name": "platformsh/reactphp-example", - "description": "A simple Hello World using React PHP", + "description": "A basic websocket chat server using React PHP", "license": "MIT", "require": { "react/react": "^0.4.2", - "react/http": "v0.4.2" + "react/http": "v0.4.2", + "cboden/ratchet": "^0.3.5" + }, + "autoload": { + "psr-4": {"ChatApp\\": "src/" } } } diff --git a/composer.lock b/composer.lock index 33082e7..625f3d4 100644 --- a/composer.lock +++ b/composer.lock @@ -4,9 +4,57 @@ "Read more about it at https://getcomposer.org/doc/01-basic-usage.md#composer-lock-the-lock-file", "This file is @generated automatically" ], - "hash": "bd8ba91e7ff47a0f9b85b9b81eb8fa76", - "content-hash": "d0eaf4ea511578fd8caeced7f5c597b8", + "hash": "74eff3c40181c8838648159452500fbe", + "content-hash": "4e65e922bd36f10b4a55296ac45cb1d3", "packages": [ + { + "name": "cboden/ratchet", + "version": "v0.3.5", + "source": { + "type": "git", + "url": "https://github.com/ratchetphp/Ratchet.git", + "reference": "b5ccecad9390db85d2c8df7cbeb047292fbbf4b8" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/ratchetphp/Ratchet/zipball/b5ccecad9390db85d2c8df7cbeb047292fbbf4b8", + "reference": "b5ccecad9390db85d2c8df7cbeb047292fbbf4b8", + "shasum": "" + }, + "require": { + "guzzle/http": "^3.6", + "php": ">=5.3.9", + "react/socket": "^0.3 || ^0.4", + "symfony/http-foundation": "^2.2|^3.0", + "symfony/routing": "^2.2|^3.0" + }, + "type": "library", + "autoload": { + "psr-4": { + "Ratchet\\": "src/Ratchet" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Chris Boden", + "email": "cboden@gmail.com", + "role": "Developer" + } + ], + "description": "PHP WebSocket library", + "homepage": "http://socketo.me", + "keywords": [ + "Ratchet", + "WebSockets", + "server", + "sockets" + ], + "time": "2016-05-25 12:55:03" + }, { "name": "evenement/evenement", "version": "v2.0.0", @@ -53,6 +101,208 @@ ], "time": "2012-11-02 14:49:47" }, + { + "name": "guzzle/common", + "version": "v3.9.2", + "target-dir": "Guzzle/Common", + "source": { + "type": "git", + "url": "https://github.com/Guzzle3/common.git", + "reference": "2e36af7cf2ce3ea1f2d7c2831843b883a8e7b7dc" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/Guzzle3/common/zipball/2e36af7cf2ce3ea1f2d7c2831843b883a8e7b7dc", + "reference": "2e36af7cf2ce3ea1f2d7c2831843b883a8e7b7dc", + "shasum": "" + }, + "require": { + "php": ">=5.3.2", + "symfony/event-dispatcher": ">=2.1" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "3.7-dev" + } + }, + "autoload": { + "psr-0": { + "Guzzle\\Common": "" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "description": "Common libraries used by Guzzle", + "homepage": "http://guzzlephp.org/", + "keywords": [ + "collection", + "common", + "event", + "exception" + ], + "abandoned": "guzzle/guzzle", + "time": "2014-08-11 04:32:36" + }, + { + "name": "guzzle/http", + "version": "v3.9.2", + "target-dir": "Guzzle/Http", + "source": { + "type": "git", + "url": "https://github.com/Guzzle3/http.git", + "reference": "1e8dd1e2ba9dc42332396f39fbfab950b2301dc5" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/Guzzle3/http/zipball/1e8dd1e2ba9dc42332396f39fbfab950b2301dc5", + "reference": "1e8dd1e2ba9dc42332396f39fbfab950b2301dc5", + "shasum": "" + }, + "require": { + "guzzle/common": "self.version", + "guzzle/parser": "self.version", + "guzzle/stream": "self.version", + "php": ">=5.3.2" + }, + "suggest": { + "ext-curl": "*" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "3.7-dev" + } + }, + "autoload": { + "psr-0": { + "Guzzle\\Http": "" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Michael Dowling", + "email": "mtdowling@gmail.com", + "homepage": "https://github.com/mtdowling" + } + ], + "description": "HTTP libraries used by Guzzle", + "homepage": "http://guzzlephp.org/", + "keywords": [ + "Guzzle", + "client", + "curl", + "http", + "http client" + ], + "abandoned": "guzzle/guzzle", + "time": "2014-08-11 04:32:36" + }, + { + "name": "guzzle/parser", + "version": "v3.9.2", + "target-dir": "Guzzle/Parser", + "source": { + "type": "git", + "url": "https://github.com/Guzzle3/parser.git", + "reference": "6874d171318a8e93eb6d224cf85e4678490b625c" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/Guzzle3/parser/zipball/6874d171318a8e93eb6d224cf85e4678490b625c", + "reference": "6874d171318a8e93eb6d224cf85e4678490b625c", + "shasum": "" + }, + "require": { + "php": ">=5.3.2" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "3.7-dev" + } + }, + "autoload": { + "psr-0": { + "Guzzle\\Parser": "" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "description": "Interchangeable parsers used by Guzzle", + "homepage": "http://guzzlephp.org/", + "keywords": [ + "URI Template", + "cookie", + "http", + "message", + "url" + ], + "abandoned": "guzzle/guzzle", + "time": "2014-02-05 18:29:46" + }, + { + "name": "guzzle/stream", + "version": "v3.9.2", + "target-dir": "Guzzle/Stream", + "source": { + "type": "git", + "url": "https://github.com/Guzzle3/stream.git", + "reference": "60c7fed02e98d2c518dae8f97874c8f4622100f0" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/Guzzle3/stream/zipball/60c7fed02e98d2c518dae8f97874c8f4622100f0", + "reference": "60c7fed02e98d2c518dae8f97874c8f4622100f0", + "shasum": "" + }, + "require": { + "guzzle/common": "self.version", + "php": ">=5.3.2" + }, + "suggest": { + "guzzle/http": "To convert Guzzle request objects to PHP streams" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "3.7-dev" + } + }, + "autoload": { + "psr-0": { + "Guzzle\\Stream": "" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Michael Dowling", + "email": "mtdowling@gmail.com", + "homepage": "https://github.com/mtdowling" + } + ], + "description": "Guzzle stream wrapper component", + "homepage": "http://guzzlephp.org/", + "keywords": [ + "Guzzle", + "component", + "stream" + ], + "abandoned": "guzzle/guzzle", + "time": "2014-05-01 21:36:02" + }, { "name": "guzzlehttp/psr7", "version": "1.3.1", @@ -615,6 +865,253 @@ "stream" ], "time": "2016-11-13 17:06:02" + }, + { + "name": "symfony/event-dispatcher", + "version": "v3.2.0", + "source": { + "type": "git", + "url": "https://github.com/symfony/event-dispatcher.git", + "reference": "e8f47a327c2f0fd5aa04fa60af2b693006ed7283" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/event-dispatcher/zipball/e8f47a327c2f0fd5aa04fa60af2b693006ed7283", + "reference": "e8f47a327c2f0fd5aa04fa60af2b693006ed7283", + "shasum": "" + }, + "require": { + "php": ">=5.5.9" + }, + "require-dev": { + "psr/log": "~1.0", + "symfony/config": "~2.8|~3.0", + "symfony/dependency-injection": "~2.8|~3.0", + "symfony/expression-language": "~2.8|~3.0", + "symfony/stopwatch": "~2.8|~3.0" + }, + "suggest": { + "symfony/dependency-injection": "", + "symfony/http-kernel": "" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "3.2-dev" + } + }, + "autoload": { + "psr-4": { + "Symfony\\Component\\EventDispatcher\\": "" + }, + "exclude-from-classmap": [ + "/Tests/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Fabien Potencier", + "email": "fabien@symfony.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Symfony EventDispatcher Component", + "homepage": "https://symfony.com", + "time": "2016-10-13 06:29:04" + }, + { + "name": "symfony/http-foundation", + "version": "v3.2.0", + "source": { + "type": "git", + "url": "https://github.com/symfony/http-foundation.git", + "reference": "9963bc29d7f4398b137dd8efc480efe54fdbe5f1" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/http-foundation/zipball/9963bc29d7f4398b137dd8efc480efe54fdbe5f1", + "reference": "9963bc29d7f4398b137dd8efc480efe54fdbe5f1", + "shasum": "" + }, + "require": { + "php": ">=5.5.9", + "symfony/polyfill-mbstring": "~1.1" + }, + "require-dev": { + "symfony/expression-language": "~2.8|~3.0" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "3.2-dev" + } + }, + "autoload": { + "psr-4": { + "Symfony\\Component\\HttpFoundation\\": "" + }, + "exclude-from-classmap": [ + "/Tests/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Fabien Potencier", + "email": "fabien@symfony.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Symfony HttpFoundation Component", + "homepage": "https://symfony.com", + "time": "2016-11-27 04:21:38" + }, + { + "name": "symfony/polyfill-mbstring", + "version": "v1.3.0", + "source": { + "type": "git", + "url": "https://github.com/symfony/polyfill-mbstring.git", + "reference": "e79d363049d1c2128f133a2667e4f4190904f7f4" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/polyfill-mbstring/zipball/e79d363049d1c2128f133a2667e4f4190904f7f4", + "reference": "e79d363049d1c2128f133a2667e4f4190904f7f4", + "shasum": "" + }, + "require": { + "php": ">=5.3.3" + }, + "suggest": { + "ext-mbstring": "For best performance" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "1.3-dev" + } + }, + "autoload": { + "psr-4": { + "Symfony\\Polyfill\\Mbstring\\": "" + }, + "files": [ + "bootstrap.php" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Nicolas Grekas", + "email": "p@tchwork.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Symfony polyfill for the Mbstring extension", + "homepage": "https://symfony.com", + "keywords": [ + "compatibility", + "mbstring", + "polyfill", + "portable", + "shim" + ], + "time": "2016-11-14 01:06:16" + }, + { + "name": "symfony/routing", + "version": "v3.2.0", + "source": { + "type": "git", + "url": "https://github.com/symfony/routing.git", + "reference": "3f239c0e049d8920928674cd55e21061182b0106" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/routing/zipball/3f239c0e049d8920928674cd55e21061182b0106", + "reference": "3f239c0e049d8920928674cd55e21061182b0106", + "shasum": "" + }, + "require": { + "php": ">=5.5.9" + }, + "conflict": { + "symfony/config": "<2.8" + }, + "require-dev": { + "doctrine/annotations": "~1.0", + "doctrine/common": "~2.2", + "psr/log": "~1.0", + "symfony/config": "~2.8|~3.0", + "symfony/expression-language": "~2.8|~3.0", + "symfony/http-foundation": "~2.8|~3.0", + "symfony/yaml": "~2.8|~3.0" + }, + "suggest": { + "doctrine/annotations": "For using the annotation loader", + "symfony/config": "For using the all-in-one router or any loader", + "symfony/dependency-injection": "For loading routes from a service", + "symfony/expression-language": "For using expression matching", + "symfony/http-foundation": "For using a Symfony Request object", + "symfony/yaml": "For using the YAML loader" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "3.2-dev" + } + }, + "autoload": { + "psr-4": { + "Symfony\\Component\\Routing\\": "" + }, + "exclude-from-classmap": [ + "/Tests/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Fabien Potencier", + "email": "fabien@symfony.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Symfony Routing Component", + "homepage": "https://symfony.com", + "keywords": [ + "router", + "routing", + "uri", + "url" + ], + "time": "2016-11-25 12:32:42" } ], "packages-dev": [], diff --git a/run.php b/run.php index cac13f5..afd4eee 100644 --- a/run.php +++ b/run.php @@ -2,17 +2,23 @@ require 'vendor/autoload.php'; -$app = function ($request, $response) { - $response->writeHead(200, array('Content-Type' => 'text/plain')); - $response->end("Hello World\n"); -}; -$loop = React\EventLoop\Factory::create(); -$socket = new React\Socket\Server($loop); -$http = new React\Http\Server($socket, $loop); +use Ratchet\Server\IoServer; +use Ratchet\Http\HttpServer; +use Ratchet\WebSocket\WsServer; +use ChatApp\Chat; -$http->on('request', $app); +$port = getenv('PORT') ?: 8080; -$socket->listen(getenv('PORT')); +$server = IoServer::factory( + new HttpServer( + new WsServer( + new Chat() + ) + ), + $port +); -$loop->run(); +print "Starting server...\n"; + +$server->run(); diff --git a/src/Chat.php b/src/Chat.php new file mode 100644 index 0000000..42e3106 --- /dev/null +++ b/src/Chat.php @@ -0,0 +1,43 @@ +clients = new \SplObjectStorage; + } + + public function onOpen(ConnectionInterface $conn) { + //store the new connection + $this->clients->attach($conn); + + echo "someone connected\n"; + } + + public function onMessage(ConnectionInterface $from, $msg) { + //send the message to all the other clients except the one who sent. + foreach ($this->clients as $client) { + if ($from !== $client) { + $client->send($msg); + } + } + } + + public function onClose(ConnectionInterface $conn) { + $this->clients->detach($conn); + echo "someone has disconnected\n"; + } + + public function onError(ConnectionInterface $conn, \Exception $e) { + echo "An error has occurred: {$e->getMessage()}\n"; + $conn->close(); + } +} diff --git a/web/css/style.css b/web/css/style.css new file mode 100644 index 0000000..7ce8dba --- /dev/null +++ b/web/css/style.css @@ -0,0 +1,60 @@ +.hidden { + display: none; +} + +#wrapper { + width: 800px; + margin: 0 auto; +} + +#leave-room { + margin-bottom: 10px; + float: right; +} + +#user-container { + width: 500px; + margin: 0 auto; + text-align: center; +} + +#main-container { + width: 500px; + margin: 0 auto; +} + +#messages { + height: 300px; + width: 500px; + border: 1px solid #ccc; + padding: 20px; + text-align: left; + overflow-y: scroll; +} + +#msg-container { + padding: 20px; +} + +#msg { + width: 400px; +} + +.user { + font-weight: bold; +} + +.msg { + margin-bottom: 10px; + overflow: hidden; +} + +.time { + float: right; + color: #939393; + font-size: 13px; +} + +.details { + margin-top: 20px; +} diff --git a/web/index.html b/web/index.html new file mode 100644 index 0000000..7c1e6f6 --- /dev/null +++ b/web/index.html @@ -0,0 +1,47 @@ + + + + + chatapp + + + + + + + +
+
+ + + +
+ + + +
+ + + + + + diff --git a/web/js/main.js b/web/js/main.js new file mode 100644 index 0000000..23a2645 --- /dev/null +++ b/web/js/main.js @@ -0,0 +1,78 @@ +(function(){ + + var user; + var messages = []; + + var messages_template = Handlebars.compile($('#messages-template').html()); + + function updateMessages(msg){ + messages.push(msg); + var messages_html = messages_template({'messages': messages}); + $('#messages').html(messages_html); + $("#messages").animate({ scrollTop: $('#messages')[0].scrollHeight}, 1000); + } + + //var conn = new WebSocket('ws://' + window.location.hostname + ':8080'); + var conn = new WebSocket('ws://' + window.location.hostname + '/ws'); + conn.onopen = function(e) { + console.log("Connection established!"); + }; + + conn.onmessage = function(e) { + var msg = JSON.parse(e.data); + updateMessages(msg); + }; + + + $('#join-chat').click(function(){ + user = $('#user').val(); + $('#user-container').addClass('hidden'); + $('#main-container').removeClass('hidden'); + + var msg = { + 'user': user, + 'text': user + ' entered the room', + 'time': moment().format('hh:mm a') + }; + + updateMessages(msg); + conn.send(JSON.stringify(msg)); + + $('#user').val(''); + }); + + + $('#send-msg').click(function(){ + var text = $('#msg').val(); + var msg = { + 'user': user, + 'text': text, + 'time': moment().format('hh:mm a') + }; + updateMessages(msg); + conn.send(JSON.stringify(msg)); + + $('#msg').val(''); + }); + + + $('#leave-room').click(function(){ + + var msg = { + 'user': user, + 'text': user + ' has left the room', + 'time': moment().format('hh:mm a') + }; + updateMessages(msg); + conn.send(JSON.stringify(msg)); + + $('#messages').html(''); + messages = []; + + $('#main-container').addClass('hidden'); + $('#user-container').removeClass('hidden'); + + conn.close(); + }); + +})(); From f7116cf8cebecd3cb0c28475fbdd7a743c043de2 Mon Sep 17 00:00:00 2001 From: Larry Garfield Date: Wed, 28 Dec 2016 12:08:35 -0600 Subject: [PATCH 2/2] Enable ext/event and use PHP 7.1 --- .platform.app.yaml | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/.platform.app.yaml b/.platform.app.yaml index 3f82322..46e9e7b 100644 --- a/.platform.app.yaml +++ b/.platform.app.yaml @@ -5,7 +5,7 @@ name: app # The type of the application to build. -type: php:7.0 +type: php:7.1 build: flavor: composer @@ -31,3 +31,8 @@ web: # The size of the persistent disk of the application (in MB). disk: 512 + +# Enable the event PHP extension, which provides a faster core event loop. +runtime: + extensions: + - event