├── .gitignore ├── .travis.yml ├── CHANGELOG.md ├── LICENSE ├── README.md ├── composer.json ├── examples ├── bidirectional │ ├── client.php │ └── server.php ├── phpcr │ ├── RemoteSessionClient.php │ ├── RemoteSessionServer.php │ ├── client.php │ ├── server.php │ └── simple │ │ ├── client.js │ │ ├── client.php │ │ └── server.php ├── simple │ ├── client.php │ └── server.php └── tests │ └── memory │ ├── client.php │ └── server.php ├── phpunit.xml.dist ├── src └── DNode │ ├── DNode.php │ ├── InputStream.php │ ├── LogStream.php │ ├── OutputStream.php │ ├── Protocol.php │ ├── RemoteProxy.php │ ├── Session.php │ └── Stream.php └── tests ├── DNode ├── CallableStub.php ├── Dog.php ├── FunctionalTest.php ├── ProtocolTest.php ├── RemoteProxyTest.php ├── ServerStub.php ├── SessionTest.php ├── TestCase.php └── Transformer.php └── bootstrap.php /.gitignore: -------------------------------------------------------------------------------- 1 | composer.lock 2 | composer.phar 3 | vendor/ 4 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: php 2 | 3 | php: 4 | - 5.3 5 | - 5.4 6 | - 5.5 7 | 8 | before_script: 9 | - composer install --dev --prefer-source 10 | 11 | script: phpunit --coverage-text 12 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | CHANGELOG 2 | ========= 3 | 4 | * 0.2.0 (2014-01-31) 5 | 6 | * Feature: Bump react dependency to 0.3.* 7 | * Bugfix: Do not scrub constructor or destructor (@astephens25) 8 | 9 | * 0.1.3 (2013-02-05) 10 | 11 | * Bugfix: Free session object properly in the server context 12 | * Improve client-side error handling 13 | 14 | * 0.1.2 (2012-11-29) 15 | 16 | * Use react streams internally 17 | * New maintainer igorw 18 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright (c) 2011 Henri Bergius 2 | 3 | Permission is hereby granted, free of charge, to any person 4 | obtaining a copy of this software and associated documentation 5 | files (the "Software"), to deal in the Software without 6 | restriction, including without limitation the rights to use, 7 | copy, modify, merge, publish, distribute, sublicense, and/or sell 8 | copies of the Software, and to permit persons to whom the 9 | Software is furnished to do so, subject to the following 10 | conditions: 11 | 12 | The above copyright notice and this permission notice shall be 13 | included in all copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, 16 | EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES 17 | OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND 18 | NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT 19 | HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, 20 | WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING 21 | FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR 22 | OTHER DEALINGS IN THE SOFTWARE. 23 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | DNode protocol for PHP 2 | ====================== 3 | 4 | This project implements the [DNode](http://substack.net/dnode) remote procedure call protocol for PHP. The intent is to enable PHP scripts to act as part of a distributed Node.js cloud, allowing Node to call PHP code, and PHP to call Node code. 5 | 6 | You can read more about DNode and PHP in the [introductory blog post](http://bergie.iki.fi/blog/dnode-make_php_and_node-js_talk_to_each_other/). 7 | 8 | [![Build Status](https://secure.travis-ci.org/bergie/dnode-php.png?branch=master)](http://travis-ci.org/bergie/dnode-php) 9 | 10 | ## Installing 11 | 12 | dnode-php can be installed using the [Composer](http://packagist.org/) tool. You can either add `dnode/dnode` to your package dependencies, or if you want to install dnode-php as standalone, go to the main directory of this package and run: 13 | 14 | $ wget http://getcomposer.org/composer.phar 15 | $ php composer.phar install 16 | 17 | You can then use the composer-generated autoloader to access the DNode classes: 18 | 19 | require 'vendor/autoload.php'; 20 | 21 | ## Running the examples 22 | 23 | After installing, you can run the DNode examples located in the examples directory. Each example contains both a client and a server. 24 | 25 | For example: 26 | 27 | $ php examples/simple/server.php 28 | $ php examples/simple/client.php 29 | n = 3300 30 | 31 | The examples have been written to be compatible with the [DNode examples](https://github.com/substack/dnode/tree/master/example), meaning that you can use any combination of PHP-to-PHP, Node-to-Node, PHP-to-Node, or Node-to-PHP as you wish. 32 | 33 | $ node simple/client.js 34 | n = 3300 35 | 36 | ## Current limitations 37 | 38 | * Only regular, non-encrypted TCP sockets are supported 39 | 40 | ## Development 41 | 42 | dnode-php is under heavy development. If you want to participate, please send pull requests. 43 | -------------------------------------------------------------------------------- /composer.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "dnode/dnode", 3 | "type": "library", 4 | "description": "DNode RPC protocol for PHP 5.3", 5 | "keywords": ["dnode", "nodejs", "rpc"], 6 | "homepage": "https://github.com/bergie/dnode-php", 7 | "license": "MIT", 8 | "authors": [ 9 | { 10 | "name": "Henri Bergius", 11 | "email": "henri.bergius@iki.fi", 12 | "homepage": "http://bergie.iki.fi/" 13 | }, 14 | { 15 | "name": "Igor Wiedler", 16 | "email": "igor@wiedler.ch" 17 | } 18 | ], 19 | "require": { 20 | "php": ">=5.3.0", 21 | "evenement/evenement": "~1.0", 22 | "react/socket": "0.3.*" 23 | }, 24 | "autoload": { 25 | "psr-0": { 26 | "DNode": "src" 27 | } 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /examples/bidirectional/client.php: -------------------------------------------------------------------------------- 1 | connect(6060, function($remote, $connection) { 21 | // Ask server for temperature in Fahrenheit 22 | $remote->clientTempF(function($degF) use ($connection) { 23 | echo "{$degF}° F\n"; 24 | // Close the connection 25 | $connection->end(); 26 | }); 27 | }); 28 | 29 | $loop->run(); 30 | -------------------------------------------------------------------------------- /examples/bidirectional/server.php: -------------------------------------------------------------------------------- 1 | remote->temperature(function($degC) use ($cb) { 13 | $degF = round($degC * 9 / 5 + 32); 14 | $cb($degF); 15 | }); 16 | } 17 | } 18 | 19 | $loop = new React\EventLoop\StreamSelectLoop(); 20 | 21 | // Create a DNode server 22 | $server = new DNode\DNode($loop, new Converter()); 23 | $server->listen(6060); 24 | 25 | $loop->run(); 26 | -------------------------------------------------------------------------------- /examples/phpcr/RemoteSessionClient.php: -------------------------------------------------------------------------------- 1 | loop = new React\EventLoop\StreamSelectLoop(); 17 | 18 | $this->port = $port; 19 | $this->dnode = new DNode\DNode($this->loop, $this); 20 | } 21 | 22 | public function getPropertyValue($path) 23 | { 24 | $this->dnode->connect($this->port, function($remote, $connection) use ($path) { 25 | /* Get property value from the server */ 26 | $remote->getPropertyValue($path, function() use ($connection) { 27 | /* Close the connection */ 28 | $connection->end(); 29 | }); 30 | }); 31 | $this->loop->run(); 32 | 33 | if ($this->exception != null) { 34 | $exception = $this->exception; 35 | $msg = $this->error; 36 | $this->exception = null; 37 | $this->error = null; 38 | throw new $exception($msg); 39 | } 40 | 41 | return $this->value; 42 | } 43 | 44 | public function setException($exception, $msg) { 45 | $this->exception = $exception; 46 | $this->error = $msg; 47 | } 48 | 49 | /* Set value */ 50 | public function setValue($a, $cb) 51 | { 52 | $this->value = $a; 53 | $cb(); 54 | } 55 | 56 | public function getValue() 57 | { 58 | return $this->value; 59 | } 60 | } 61 | 62 | /* 63 | $crSession = new RemoteSession(6060); 64 | $value = $crSession->getPropertyValue("/jcr:primaryType"); 65 | var_dump ($crSession->getException()); 66 | var_dump ($crSession->getError()); 67 | var_dump ($value);*/ 68 | -------------------------------------------------------------------------------- /examples/phpcr/RemoteSessionServer.php: -------------------------------------------------------------------------------- 1 | loop = new React\EventLoop\StreamSelectLoop(); 15 | 16 | $this->crSession = $repository->login($credentials, $workspace); 17 | $this->dnode = new DNode\DNode($this->loop, $this); 18 | } 19 | 20 | /* Get value of the property at defined path */ 21 | public function getPropertyValue($path, $cb) 22 | { 23 | $value = null; 24 | try { 25 | $value = $this->crSession->getProperty($path)->getValue(); 26 | } catch (\Exception $e) { 27 | $this->remote->setException(get_class($e), $e->getMessage()); 28 | } 29 | $this->remote->setValue($value, function() use ($cb) { 30 | $cb(); 31 | }); 32 | } 33 | 34 | public function listen($port) 35 | { 36 | $this->dnode->listen($port); 37 | $this->loop->run(); 38 | } 39 | } 40 | 41 | /* 42 | $credentials = new \PHPCR\SimpleCredentials("admin", "password"); 43 | $params = array ( 44 | 'midgard2.configuration.file' => getenv('MIDGARD_ENV_GLOBAL_SHAREDIR') . "/midgard2.conf" 45 | ); 46 | 47 | $repository = Midgard\PHPCR\RepositoryFactory::getRepository($params); 48 | 49 | $server = new RemoteSessionServer($repository, $credentials); 50 | $server->listen(6060); 51 | 52 | exit; 53 | 54 | $loop = new React\EventLoop\StreamSelectLoop(); 55 | 56 | // Create a DNode server 57 | $server = new DNode\DNode($loop, new Converter()); 58 | $server->listen(6060); 59 | 60 | $loop->run(); 61 | */ 62 | -------------------------------------------------------------------------------- /examples/phpcr/client.php: -------------------------------------------------------------------------------- 1 | getPropertyValue("/jcr:primaryType"); 7 | var_dump ($value); 8 | 9 | ?> 10 | -------------------------------------------------------------------------------- /examples/phpcr/server.php: -------------------------------------------------------------------------------- 1 | getenv('MIDGARD_ENV_GLOBAL_SHAREDIR') . "/midgard2.conf" 8 | ); 9 | $repository = Midgard\PHPCR\RepositoryFactory::getRepository($params); 10 | 11 | $server = new RemoteSessionServer($repository, $credentials); 12 | $server->listen(6060); 13 | 14 | 15 | -------------------------------------------------------------------------------- /examples/phpcr/simple/client.js: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env node 2 | 3 | require.paths.push('/usr/local/lib/node_modules'); 4 | 5 | var dnode = require('dnode'); 6 | 7 | dnode.connect(7070, function (remote, conn) { 8 | remote.getNodes('default', '/', function (value, exception, error) { 9 | console.log(value); 10 | console.log('Exception: ' + exception + ' thrown with message: ' + error); 11 | conn.end(); 12 | }); 13 | }); 14 | 15 | dnode.connect(7070, function (remote, conn) { 16 | remote.getPropertyValue('default', '/jcr:primaryType', function (value, exception, error) { 17 | console.log(value); 18 | console.log('Exception: ' + exception + ' thrown with message: ' + error); 19 | conn.end(); 20 | }); 21 | }); 22 | 23 | dnode.connect(7070, function (remote, conn) { 24 | remote.itemExists('default', '/Sir/Lancelot/Likes/Blue', function (value, exception, error) { 25 | console.log('Item at specified path doesn\'t exist.'); 26 | conn.end(); 27 | }); 28 | }); 29 | 30 | -------------------------------------------------------------------------------- /examples/phpcr/simple/client.php: -------------------------------------------------------------------------------- 1 | connect(7070, function($remote, $connection) { 10 | $remote->getPropertyValue("default", "/jcr:primaryType", function($val, $exc, $error) use ($connection) { 11 | echo $val; 12 | if ($exc != null) 13 | echo "Exception {$exc} thrown with message {$error} \n"; 14 | $connection->end(); 15 | }); 16 | }); 17 | 18 | $loop->run(); 19 | -------------------------------------------------------------------------------- /examples/phpcr/simple/server.php: -------------------------------------------------------------------------------- 1 | loop = new React\EventLoop\StreamSelectLoop(); 23 | 24 | $this->repository = $repository; 25 | $this->dnode = new DNode\DNode($this->loop, $this); 26 | } 27 | 28 | private function validateSessionName($name, $cb) 29 | { 30 | if (!array_key_exists($name, $this->sessions)) { 31 | $cb(null, 'RepositoryException', 'Named session $sessionName not found'); 32 | return false; 33 | } 34 | return true; 35 | } 36 | 37 | /** 38 | * Get the names of children nodes 39 | * 40 | * @param $sessionName - name of the session 41 | * @param $path - absolute path of the parent node 42 | * @param $cb - callback function 43 | * 44 | * @return void 45 | */ 46 | public function getNodes($sessionName, $path, $cb) 47 | { 48 | if (!$this->validateSessionName($sessionName, $cb)) 49 | return false; 50 | 51 | $exception = null; 52 | $msg = null; 53 | $names = array (); 54 | 55 | try { 56 | $parent = $this->sessions[$sessionName]->getNode($path); 57 | $nodes = $parent->getNodes(); 58 | $names = array_keys ($nodes->getArrayCopy()); 59 | } catch (\Exception $e) { 60 | $exception = get_class($e); 61 | $msg = $e->getMessage(); 62 | } 63 | 64 | $cb($names, $exception, $msg); 65 | } 66 | 67 | /** 68 | * Get the names of all properties 69 | * 70 | * @param $sessionName - name of the session 71 | * @param $path - absolute path of the node 72 | * @param $cb - callback function 73 | * 74 | * @return void 75 | */ 76 | public function getProperties($sessionName, $path, $cb) 77 | { 78 | if (!$this->validateSessionName($sessionName, $cb)) 79 | return false; 80 | 81 | $exception = null; 82 | $msg = null; 83 | $names = array (); 84 | 85 | try { 86 | $parent = $this->sessions[$sessionName]->getNode($path); 87 | $properties = $parent->getProperties (); 88 | $names = array_keys ($properties); 89 | } catch (\Exception $e) { 90 | $exception = get_class($e); 91 | $msg = $e->getMessage(); 92 | } 93 | 94 | $cb($names, $exception, $msg); 95 | } 96 | 97 | /* Get value of the property at specified path */ 98 | public function getPropertyValue($sessionName, $path, $cb) 99 | { 100 | if (!array_key_exists($sessionName, $this->sessions)) { 101 | $cb(null, 'RepositoryException', 'Named session $sessionName not found'); 102 | } 103 | 104 | $exception = null; 105 | $msg = null; 106 | $val = null; 107 | 108 | try { 109 | $val = $this->sessions[$sessionName]->getProperty($path)->getValue(); 110 | } catch (\Exception $e){ 111 | $exception = get_class($e); 112 | $msg = $e->getMessage(); 113 | } 114 | 115 | $cb($val, $exception, $msg); 116 | } 117 | 118 | /* Check if specified item exists at path specified path */ 119 | public function itemExists($sessionName, $path, $cb) 120 | { 121 | if (!$this->validateSessionName($sessionName, $cb)) 122 | return false; 123 | 124 | $exists = $this->sessions[$sessionName]->itemExists ($path); 125 | $cb($exists, null, null); 126 | } 127 | 128 | public function addNode($sessionName, $path, $name, $type, $cb) 129 | { 130 | if (!$this->validateSessionName($sessionName, $cb)) 131 | return false; 132 | 133 | $exception = null; 134 | $msg = null; 135 | 136 | try { 137 | $parent = $this->sessions[$sessionName]->getNode($path); 138 | $parent->addNode($name, $type); 139 | } catch (\Exception $e) { 140 | $exception = get_class($e); 141 | $msg = $e->getMessage(); 142 | } 143 | 144 | $cb($exception, $msg); 145 | } 146 | 147 | public function setProperty($sessionName, $path, $name, $value, $type, $cb) 148 | { 149 | if (!$this->validateSessionName($sessionName, $cb)) 150 | return false; 151 | 152 | $exception = null; 153 | $msg = null; 154 | 155 | try { 156 | $parent = $this->sessions[$sessionName]->getNode($path); 157 | $parent->setProperty($name, $value, $type); 158 | } catch (\Exception $e) { 159 | $exception = get_class($e); 160 | $msg = $e->getMessage(); 161 | } 162 | 163 | $cb($exception, $msg); 164 | } 165 | 166 | /* Create named session */ 167 | public function createSession($sessionName, $name, $password) 168 | { 169 | $credentials = new \PHPCR\SimpleCredentials($name, $password); 170 | $this->sessions[$sessionName] = $this->repository->login($credentials); 171 | } 172 | 173 | public function listen($port) 174 | { 175 | $this->dnode->listen($port); 176 | $this->loop->run(); 177 | } 178 | } 179 | 180 | /* Initialize PHPCR Repository */ 181 | $params = array ( 182 | 'midgard2.configuration.file' => getenv('MIDGARD_ENV_GLOBAL_SHAREDIR') . "/midgard2.conf" 183 | ); 184 | $repository = Midgard\PHPCR\RepositoryFactory::getRepository($params); 185 | 186 | /* Initialize server for given repository */ 187 | $server = new SimpleRemoteRepository($repository); 188 | $server->createSession("default", "admin", "password"); 189 | $server->listen(7070); 190 | -------------------------------------------------------------------------------- /examples/simple/client.php: -------------------------------------------------------------------------------- 1 | connect(7070, function($remote, $connection) { 10 | $remote->zing(33, function($n) use ($connection) { 11 | echo "n = {$n}\n"; 12 | $connection->end(); 13 | }); 14 | }); 15 | 16 | $loop->run(); 17 | -------------------------------------------------------------------------------- /examples/simple/server.php: -------------------------------------------------------------------------------- 1 | listen(7070); 21 | 22 | $loop->run(); 23 | -------------------------------------------------------------------------------- /examples/tests/memory/client.php: -------------------------------------------------------------------------------- 1 | addPeriodicTimer(0.001, function () use ($loop, &$i) { 12 | $i++; 13 | 14 | $client = new DNode\DNode($loop); 15 | $client->connect(7070, function ($remote, $conn) { 16 | $remote->zing(33, function ($n) use ($conn) { 17 | $conn->end(); 18 | }); 19 | }); 20 | }); 21 | 22 | $loop->addPeriodicTimer(2, function () use (&$i) { 23 | $kmem = memory_get_usage(true) / 1024; 24 | echo "Run: $i\n"; 25 | echo "Memory: $kmem KiB\n"; 26 | }); 27 | 28 | $loop->run(); 29 | -------------------------------------------------------------------------------- /examples/tests/memory/server.php: -------------------------------------------------------------------------------- 1 | i++; 16 | $callback($n * 100); 17 | } 18 | } 19 | 20 | $zinger = new Zinger(); 21 | 22 | $server = new DNode\DNode($loop, $zinger); 23 | $server->listen(7070); 24 | 25 | $loop->addPeriodicTimer(2, function () use ($zinger) { 26 | $kmem = memory_get_usage(true) / 1024; 27 | echo "Run: {$zinger->i}\n"; 28 | echo "Memory: $kmem KiB\n"; 29 | }); 30 | 31 | $loop->run(); 32 | -------------------------------------------------------------------------------- /phpunit.xml.dist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 7 | 8 | 9 | ./tests/DNode/ 10 | 11 | 12 | 13 | 14 | 15 | ./src/ 16 | 17 | 18 | 19 | -------------------------------------------------------------------------------- /src/DNode/DNode.php: -------------------------------------------------------------------------------- 1 | loop = $loop; 19 | 20 | $wrapper = $wrapper ?: new \StdClass(); 21 | $this->protocol = new Protocol($wrapper); 22 | } 23 | 24 | public function using($middleware) 25 | { 26 | $this->stack[] = $middleware; 27 | return $this; 28 | } 29 | 30 | public function connect() 31 | { 32 | $params = $this->protocol->parseArgs(func_get_args()); 33 | if (!isset($params['host'])) { 34 | $params['host'] = '127.0.0.1'; 35 | } 36 | 37 | if (!isset($params['port'])) { 38 | throw new \Exception("For now we only support TCP connections to a defined port"); 39 | } 40 | 41 | $client = @stream_socket_client("tcp://{$params['host']}:{$params['port']}"); 42 | if (!$client) { 43 | $e = new \RuntimeException("No connection to DNode server in tcp://{$params['host']}:{$params['port']}"); 44 | $this->emit('error', array($e)); 45 | 46 | if (!count($this->listeners('error'))) { 47 | trigger_error((string) $e, E_USER_ERROR); 48 | } 49 | 50 | return; 51 | } 52 | 53 | $conn = new Connection($client, $this->loop); 54 | $this->handleConnection($conn, $params); 55 | } 56 | 57 | public function listen() 58 | { 59 | $params = $this->protocol->parseArgs(func_get_args()); 60 | if (!isset($params['host'])) { 61 | $params['host'] = '127.0.0.1'; 62 | } 63 | 64 | if (!isset($params['port'])) { 65 | throw new \Exception("For now we only support TCP connections to a defined port"); 66 | } 67 | 68 | $that = $this; 69 | 70 | $server = new Server($this->loop); 71 | $server->on('connection', function ($conn) use ($that, $params) { 72 | $that->handleConnection($conn, $params); 73 | }); 74 | $server->listen($params['port'], $params['host']); 75 | 76 | return $server; 77 | } 78 | 79 | public function handleConnection(ConnectionInterface $conn, $params) 80 | { 81 | $client = $this->protocol->create(); 82 | 83 | $onReady = isset($params['block']) ? $params['block'] : null; 84 | $stream = new Stream($this, $client, $onReady); 85 | 86 | $conn->pipe($stream)->pipe($conn); 87 | 88 | $client->start(); 89 | } 90 | 91 | public function end() 92 | { 93 | $this->protocol->end(); 94 | $this->emit('end'); 95 | } 96 | 97 | public function close() 98 | { 99 | // FIXME: $this->server does not exist 100 | $this->server->close(); 101 | } 102 | } 103 | -------------------------------------------------------------------------------- /src/DNode/InputStream.php: -------------------------------------------------------------------------------- 1 | client = $client; 13 | 14 | $that = $this; 15 | 16 | $client->on('end', function () use ($that) { 17 | $that->end(); 18 | }); 19 | } 20 | 21 | public function write($data) 22 | { 23 | $this->buffer .= $data; 24 | if (false !== strpos($this->buffer, "\n")) { 25 | $commands = explode("\n", $this->buffer); 26 | $tail = array_pop($commands); 27 | 28 | foreach ($commands as $command) { 29 | $this->client->parse($command); 30 | } 31 | 32 | $this->buffer = $tail; 33 | } 34 | } 35 | 36 | public function close() 37 | { 38 | if ($this->closed) { 39 | return; 40 | } 41 | 42 | parent::close(); 43 | 44 | $this->client->end(); 45 | } 46 | } 47 | -------------------------------------------------------------------------------- /src/DNode/LogStream.php: -------------------------------------------------------------------------------- 1 | prefix = $prefix; 14 | } 15 | 16 | public function filter($data) 17 | { 18 | echo $this->prefix.': '.$data; 19 | 20 | return $data; 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /src/DNode/OutputStream.php: -------------------------------------------------------------------------------- 1 | on('request', function (array $request) use ($that) { 12 | $that->emit('data', array(json_encode($request)."\n")); 13 | }); 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /src/DNode/Protocol.php: -------------------------------------------------------------------------------- 1 | wrapper = $wrapper; 13 | } 14 | 15 | public function create() 16 | { 17 | // FIXME: Random ID generation, should be unique 18 | $id = microtime(); 19 | $session = new Session($id, $this->wrapper); 20 | 21 | $that = $this; 22 | $session->on('end', function () use ($that, $id) { 23 | return $that->destroy($id); 24 | }); 25 | 26 | $this->sessions[$id] = $session; 27 | 28 | return $session; 29 | } 30 | 31 | public function destroy($id) 32 | { 33 | unset($this->sessions[$id]); 34 | } 35 | 36 | public function end() 37 | { 38 | foreach ($this->sessions as $id => $session) { 39 | $this->sessions[$id]->end(); 40 | } 41 | } 42 | 43 | public function parseArgs($args) { 44 | $params = array(); 45 | 46 | foreach ($args as $arg) { 47 | if (is_string($arg)) { 48 | if (preg_match('/^\d+$/', $arg)) { 49 | $params['port'] = $arg; 50 | continue; 51 | } 52 | if (preg_match('/^\\//', $arg)) { 53 | $params['path'] = $arg; 54 | continue; 55 | } 56 | $params['host'] = $arg; 57 | continue; 58 | } 59 | 60 | if (is_numeric($arg)) { 61 | $params['port'] = $arg; 62 | continue; 63 | } 64 | 65 | if (is_object($arg)) { 66 | if ($arg instanceof \Closure) { 67 | $params['block'] = $arg; 68 | continue; 69 | } 70 | 71 | if ($arg instanceof ServerInterface) { 72 | $params['server'] = $arg; 73 | continue; 74 | } 75 | 76 | foreach ($arg as $key => $value) { 77 | $params[$key] = $value; 78 | } 79 | continue; 80 | } 81 | 82 | throw new \InvalidArgumentException("Not sure what to do about " . gettype($arg) . " arguments"); 83 | } 84 | 85 | return $params; 86 | } 87 | } 88 | -------------------------------------------------------------------------------- /src/DNode/RemoteProxy.php: -------------------------------------------------------------------------------- 1 | methods; 11 | } 12 | 13 | public function setMethod($method, $closure) 14 | { 15 | $this->methods[$method] = $closure; 16 | } 17 | 18 | public function __call($method, $args) 19 | { 20 | if (!isset($this->methods[$method])) { 21 | throw new \BadMethodCallException("Method {$method} not available"); 22 | } 23 | 24 | call_user_func_array($this->methods[$method], $args); 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /src/DNode/Session.php: -------------------------------------------------------------------------------- 1 | id = $id; 31 | $this->wrapper = $wrapper; 32 | $this->remote = new RemoteProxy(); 33 | $this->wrapper->remote =& $this->remote; 34 | } 35 | 36 | public function start() 37 | { 38 | // Send our methods to the other party 39 | $this->request('methods', array($this->wrapper)); 40 | } 41 | 42 | public function end() 43 | { 44 | $this->emit('end'); 45 | $this->removeAllListeners(); 46 | 47 | $this->callbacks = array(); 48 | $this->wrapped = array(); 49 | $this->remote = null; 50 | $this->wrapper = null; 51 | } 52 | 53 | public function request($method, $args) 54 | { 55 | // Wrap callbacks in arguments 56 | $scrub = $this->scrub($args); 57 | 58 | $request = array( 59 | 'method' => $method, 60 | 'arguments' => $scrub['arguments'], 61 | 'callbacks' => $scrub['callbacks'], 62 | 'links' => $scrub['links'] 63 | ); 64 | 65 | $this->emit('request', array($request)); 66 | } 67 | 68 | public function parse($line) 69 | { 70 | // TODO: Error handling for JSON parsing 71 | $msg = json_decode($line); 72 | // TODO: Try/catch handle 73 | $this->handle($msg); 74 | } 75 | 76 | public function handle($req) 77 | { 78 | $session = $this; 79 | 80 | // Register callbacks from request 81 | $args = $this->unscrub($req); 82 | 83 | if ($req->method === 'methods') { 84 | // Got a methods list from the remote 85 | return $this->handleMethods($args[0]); 86 | } 87 | if ($req->method === 'error') { 88 | // Got an error from the remote 89 | return $this->emit('remoteError', array($args[0])); 90 | } 91 | if (is_string($req->method)) { 92 | if (is_callable(array($this, $req->method))) { 93 | return call_user_func_array(array($this, $req->method), $args); 94 | } 95 | return $this->emit('error', array("Request for non-enumerable method: {$req->method}")); 96 | } 97 | if (is_numeric($req->method)) { 98 | call_user_func_array($this->callbacks[$req->method], $args); 99 | } 100 | } 101 | 102 | private function handleMethods($methods) 103 | { 104 | if (!is_object($methods)) { 105 | $methods = new \StdClass(); 106 | } 107 | 108 | foreach ($methods as $key => $value) { 109 | $this->remote->setMethod($key, $value); 110 | } 111 | 112 | $this->emit('remote', array($this->remote)); 113 | $this->ready = true; 114 | $this->emit('ready'); 115 | } 116 | 117 | private function scrub($obj) 118 | { 119 | $paths = array(); 120 | $links = array(); 121 | 122 | // TODO: Deep traversal 123 | foreach ($obj as $id => $node) { 124 | if (is_object($node)) { 125 | if ($node instanceof \Closure) { 126 | $this->callbacks[$this->cbId] = $node; 127 | $paths[$this->cbId] = array($id); 128 | $this->cbId++; 129 | $obj[$id] = '[Function]'; 130 | continue; 131 | } 132 | 133 | $reflector = new \ReflectionClass($node); 134 | $methods = $reflector->getMethods(); 135 | foreach ($methods as $method) { 136 | if (!$method->isPublic() || $method->isConstructor() || $method->isDestructor()) { 137 | continue; 138 | } 139 | 140 | $methodName = $method->getName(); 141 | 142 | $this->callbacks[$this->cbId] = function() use ($methodName, $node) { 143 | call_user_func_array(array($node, $methodName), func_get_args()); 144 | }; 145 | $paths[$this->cbId] = array($id, $methodName); 146 | $this->cbId++; 147 | $node->$methodName = '[Function]'; 148 | } 149 | } 150 | } 151 | 152 | return array( 153 | 'arguments' => $obj, 154 | 'callbacks' => $paths, 155 | 'links' => $links 156 | ); 157 | } 158 | 159 | /** 160 | * Replace callbacks. The supplied function should take a callback 161 | * id and return a callback of its own. 162 | */ 163 | private function unscrub($msg) { 164 | $args = $msg->arguments; 165 | $session = $this; 166 | foreach ($msg->callbacks as $id => $path) { 167 | if (!isset($this->wrapped[$id])) { 168 | $this->wrapped[$id] = function() use ($session, $id) { 169 | $session->request((int) $id, func_get_args()); 170 | }; 171 | } 172 | $location =& $args; 173 | foreach ($path as $part) { 174 | if (is_array($location)) { 175 | $location =& $location[$part]; 176 | continue; 177 | } 178 | $location =& $location->$part; 179 | } 180 | $location = $this->wrapped[$id]; 181 | } 182 | return $args; 183 | } 184 | } 185 | -------------------------------------------------------------------------------- /src/DNode/Stream.php: -------------------------------------------------------------------------------- 1 | dnode = $dnode; 12 | 13 | foreach ($this->dnode->stack as $middleware) { 14 | call_user_func($middleware, array($client->instance, $client->remote, $client)); 15 | } 16 | 17 | if ($onReady) { 18 | $client->on('ready', function () use ($client, $onReady) { 19 | call_user_func($onReady, $client->remote, $client); 20 | }); 21 | } 22 | 23 | $input = new InputStream($client); 24 | $output = new OutputStream($client); 25 | 26 | parent::__construct($output, $input); 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /tests/DNode/CallableStub.php: -------------------------------------------------------------------------------- 1 | listen(5004); 21 | 22 | $client = new DNode($loop); 23 | $client->connect(5004, function ($remote, $conn) use (&$captured, $socket) { 24 | $remote->transform('fou', function ($transformed) use ($conn, &$captured, $socket) { 25 | $captured = $transformed; 26 | $conn->end(); 27 | $socket->shutdown(); 28 | }); 29 | }); 30 | 31 | $loop->run(); 32 | 33 | $this->assertSame('FOO', $captured); 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /tests/DNode/ProtocolTest.php: -------------------------------------------------------------------------------- 1 | protocol = new Protocol(new Dog()); 9 | } 10 | 11 | /** 12 | * @test 13 | * @covers DNode\Protocol::__construct 14 | * @covers DNode\Protocol::create 15 | */ 16 | public function createShouldReturnSession() 17 | { 18 | $session = $this->protocol->create(); 19 | $this->assertInstanceOf('DNode\Session', $session); 20 | } 21 | 22 | /** 23 | * @test 24 | * @covers DNode\Protocol::destroy 25 | */ 26 | public function destroyShouldUnsetSession() 27 | { 28 | $session = $this->protocol->create(); 29 | $this->protocol->destroy($session->id); 30 | } 31 | 32 | /** 33 | * @test 34 | * @covers DNode\Protocol::end 35 | */ 36 | public function endShouldCallEndOnAllSessions() 37 | { 38 | $sessions = array( 39 | $this->protocol->create(), 40 | $this->protocol->create(), 41 | ); 42 | 43 | foreach ($sessions as $session) { 44 | $session->on('end', $this->expectCallableOnce()); 45 | } 46 | 47 | $this->protocol->end(); 48 | } 49 | 50 | /** 51 | * @test 52 | * @covers DNode\Protocol::parseArgs 53 | * @dataProvider provideParseArgs 54 | */ 55 | public function parseArgsShouldParseArgsCorrectly($expected, $args) 56 | { 57 | $this->assertSame($expected, $this->protocol->parseArgs($args)); 58 | } 59 | 60 | public function provideParseArgs() 61 | { 62 | $closure = function () {}; 63 | $server = new ServerStub(); 64 | 65 | $obj = new \stdClass(); 66 | $obj->foo = 'bar'; 67 | $obj->baz = 'qux'; 68 | 69 | return array( 70 | 'string number becomes port' => array( 71 | array('port' => '8080'), 72 | array('8080'), 73 | ), 74 | 'leading / becomes path' => array( 75 | array('path' => '/foo'), 76 | array('/foo'), 77 | ), 78 | 'string becomes host' => array( 79 | array('host' => 'foo'), 80 | array('foo'), 81 | ), 82 | 'integer becomes port' => array( 83 | array('port' => 8080), 84 | array(8080), 85 | ), 86 | 'Closure becomes block' => array( 87 | array('block' => $closure), 88 | array($closure), 89 | ), 90 | 'ServerInterface becomes server' => array( 91 | array('server' => $server), 92 | array($server), 93 | ), 94 | 'random object becomes key => val' => array( 95 | array('foo' => 'bar', 'baz' => 'qux'), 96 | array($obj), 97 | ), 98 | ); 99 | } 100 | 101 | /** 102 | * @test 103 | * @covers DNode\Protocol::parseArgs 104 | * @expectedException InvalidArgumentException 105 | * @expectedExceptionMessage Not sure what to do about array arguments 106 | */ 107 | public function parseArgsShouldRejectInvalidArgs() 108 | { 109 | $args = array(array('wat')); 110 | $this->protocol->parseArgs($args); 111 | } 112 | } 113 | -------------------------------------------------------------------------------- /tests/DNode/RemoteProxyTest.php: -------------------------------------------------------------------------------- 1 | proxy = new RemoteProxy(); 9 | } 10 | 11 | /** @test */ 12 | public function getMethodsShouldDefaultToEmptyArray() 13 | { 14 | $this->assertSame(array(), $this->proxy->getMethods()); 15 | } 16 | 17 | /** @test */ 18 | public function setMethodShouldAddMethodToList() 19 | { 20 | $foo = function () {}; 21 | $this->proxy->setMethod('foo', $foo); 22 | 23 | $this->assertSame(array('foo' => $foo), $this->proxy->getMethods()); 24 | } 25 | 26 | /** @test */ 27 | public function setMethodShouldAcceptMultipleCalls() 28 | { 29 | $foo = function () {}; 30 | $bar = function () {}; 31 | 32 | $this->proxy->setMethod('foo', $foo); 33 | $this->proxy->setMethod('bar', $bar); 34 | 35 | $this->assertSame(array('foo' => $foo, 'bar' => $bar), $this->proxy->getMethods()); 36 | } 37 | 38 | /** @test */ 39 | public function setMethodShouldOverrideExistingMethod() 40 | { 41 | $foo = function () {}; 42 | $bar = function () {}; 43 | 44 | $this->proxy->setMethod('foo', $foo); 45 | $this->proxy->setMethod('foo', $bar); 46 | 47 | $this->assertSame(array('foo' => $bar), $this->proxy->getMethods()); 48 | } 49 | 50 | /** @test */ 51 | public function proxyShouldDelegateMissingMethodsWithMagic() 52 | { 53 | $foo = $this->expectCallableOnceWithArg('a'); 54 | $bar = $this->expectCallableOnceWithArg('b'); 55 | 56 | $this->proxy->setMethod('foo', $foo); 57 | $this->proxy->setMethod('bar', $bar); 58 | 59 | $this->proxy->foo('a'); 60 | $this->proxy->bar('b'); 61 | } 62 | 63 | /** 64 | * @test 65 | * @expectedException BadMethodCallException 66 | * @expectedExceptionMessage Method baz not available 67 | */ 68 | public function proxyShouldThrowExceptionOnNonExistentMethod() 69 | { 70 | $this->proxy->baz(); 71 | } 72 | } 73 | -------------------------------------------------------------------------------- /tests/DNode/ServerStub.php: -------------------------------------------------------------------------------- 1 | 'methods', 14 | 'arguments' => array($dog), 15 | 'callbacks' => array( 16 | array(0, 'bark'), 17 | array(0, 'meow'), 18 | ), 19 | 'links' => array(), 20 | ); 21 | $session->on('request', $this->expectCallableOnceWithArg($expected)); 22 | $session->start(); 23 | } 24 | 25 | /** @test */ 26 | public function endShouldEmitEndEvent() 27 | { 28 | $dog = new Dog(); 29 | $session = new Session(0, $dog); 30 | 31 | $session->on('end', $this->expectCallableOnce()); 32 | $session->end(); 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /tests/DNode/TestCase.php: -------------------------------------------------------------------------------- 1 | getMock('DNode\CallableStub'); 9 | $callable 10 | ->expects($this->once()) 11 | ->method('__invoke'); 12 | 13 | return $callable; 14 | } 15 | 16 | protected function expectCallableOnceWithArg($arg) 17 | { 18 | $callable = $this->getMock('DNode\CallableStub'); 19 | $callable 20 | ->expects($this->once()) 21 | ->method('__invoke') 22 | ->with($arg); 23 | 24 | return $callable; 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /tests/DNode/Transformer.php: -------------------------------------------------------------------------------- 1 | add('DNode', __DIR__); 5 | --------------------------------------------------------------------------------