├── .gitignore ├── .travis.yml ├── CHANGELOG.md ├── LICENSE ├── README.md ├── composer.json ├── examples ├── client-tcp.php ├── client-udp.php └── server-udp.php ├── phpunit.xml.dist ├── src ├── Datagram │ ├── Buffer.php │ ├── Factory.php │ └── Socket.php ├── EventLoop │ ├── SelectPoller.php │ └── SocketSelectLoop.php └── Stream │ ├── Connection.php │ ├── Factory.php │ ├── Server.php │ └── StreamBuffer.php └── tests ├── Datagram ├── FactoryTest.php └── SocketTest.php ├── EventLoop ├── AbstractLoopTest.php ├── SelectPollerTest.php └── SocketSelectLoopTest.php └── bootstrap.php /.gitignore: -------------------------------------------------------------------------------- 1 | /vendor 2 | /composer.lock 3 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: php 2 | php: 3 | - 5.4 4 | - 5.3 5 | - hhvm 6 | install: 7 | - composer install --prefer-source --no-interaction 8 | script: 9 | - phpunit=`which phpunit` 10 | - sudo $phpunit --coverage-text 11 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # CHANGELOG 2 | 3 | This file is a manually maintained list of changes for each release. Feel free 4 | to add your changes here when sending pull requests. Also send corrections if 5 | you spot any mistakes. 6 | 7 | ## 0.3.0 (2014-10-25) 8 | 9 | * BC break: Replace clue/datagram with react/datagram 10 | ([#16](https://github.com/clue/php-socket-react/pull/16)) 11 | 12 | ## 0.2.2 (2014-05-10) 13 | 14 | * Support [clue/socket-raw](https://github.com/clue/socket-raw) stable 15 | version `1.*` and update dependencies 16 | ([#10](https://github.com/clue/socket-react/pull/10)) 17 | * Use PSR-4 layout 18 | ([#12](https://github.com/clue/socket-react/pull/12)) 19 | 20 | ## 0.2.1 (2014-03-04) 21 | 22 | * Fix: Make sure `Socket\Factory::createIcmp6()` actually returns an ICMPv6 socket 23 | ([#9](https://github.com/clue/socket-react/pull/9)) 24 | 25 | ## 0.2.0 (2014-03-04) 26 | 27 | * BC break: More SOLID design, reuse existing code, refactor code to fix 28 | ambiguities and ease extending 29 | ([#1](https://github.com/clue/socket-react/pull/1)) 30 | * The event loop handling has been rewritten and now resides in the 31 | `Socket\React\EventLoop` namespace. Whole new API regarding `SelectPoller` 32 | and dedicated `SocketSelectLoop`. 33 | * Rename `Datagram\Datagram` to `Datagram\Socket` 34 | * Merge `Stream\Stream` into `Stream\Connection` 35 | * Remove `bind()`, `connect()` and `setOptionBroadcast()` methods from 36 | `Datagram\Socket` and add respective options to the `Datagram\Factory` class. 37 | This is done in order to keep the API clean and to avoid confusion as to 38 | when it's safe to invoke those methods. 39 | * Require clue/datagram and implement its `Datagram\SocketInterface` 40 | for `Socket\React\Datagram\Socket`. This means that you can now pass an 41 | instance of this class where other libaries expect a datagram socket. 42 | * Fix: Typo in `Socket\React\Stream\Server` that passed null 43 | ([#4](https://github.com/clue/socket-react/pull/4) thanks @cboden!) 44 | * Fix: End connection if reading from stream socket fails 45 | ([#7](https://github.com/clue/socket-react/pull/7)) 46 | * Fix: Compatibility with hhvm 47 | ([#8](https://github.com/clue/socket-react/pull/8)) 48 | 49 | ## 0.1.0 (2013-04-18) 50 | 51 | * First tagged release 52 | 53 | ## 0.0.0 (2013-04-05) 54 | 55 | * Initial concept 56 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2013 Christian Lück 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 furnished 10 | 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 21 | THE SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # clue/socket-react [![Build Status](https://travis-ci.org/clue/php-socket-react.svg?branch=master)](https://travis-ci.org/clue/php-socket-react) 2 | 3 | Binding for raw sockets (ext-sockets) in React PHP. 4 | 5 | ## Quickstart example 6 | 7 | Once [installed](#install), you can use the following example to send UDP broadcast datagrams: 8 | 9 | ```php 10 | $loop = React\EventLoop\Factory::create(); 11 | 12 | $factory = new Socket\React\Datagram\Factory($loop); 13 | 14 | $promise = $factory->createClient('udp://localhost:1337', array('broadcast' => true)); 15 | $promise->then(function (Socket\React\Datagram\Socket $socket) { 16 | $socket->send('test'); 17 | 18 | $socket->on('message', function($data, $peer) { 19 | var_dump('Received', $data, 'from', $peer); 20 | }); 21 | }); 22 | 23 | $loop->run(); 24 | ``` 25 | 26 | See also the [examples](examples). 27 | 28 | ## Install 29 | 30 | The recommended way to install this library is [through composer](http://getcomposer.org). [New to composer?](http://getcomposer.org/doc/00-intro.md) 31 | 32 | ```JSON 33 | { 34 | "require": { 35 | "clue/socket-react": "~0.3.0" 36 | } 37 | } 38 | ``` 39 | 40 | ## Tests 41 | 42 | To run the test suite, you need PHPUnit. Go to the project root and run: 43 | ```` 44 | $ phpunit tests 45 | ```` 46 | 47 | > Note: The test suite contains tests for ICMP sockets which require root access 48 | > on unix/linux systems. Therefor some tests will be skipped unless you run 49 | > `sudo phpunit tests` to execte the full test suite. 50 | 51 | ## License 52 | 53 | MIT 54 | -------------------------------------------------------------------------------- /composer.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "clue/socket-react", 3 | "description": "Binding for raw sockets (ext-sockets) in reactphp", 4 | "keywords": ["sockets", "low-level", "ext-sockets", "react", "async"], 5 | "homepage": "https://github.com/clue/php-socket-react", 6 | "license": "MIT", 7 | "authors": [ 8 | { 9 | "name": "Christian Lück", 10 | "email": "christian@lueck.tv" 11 | } 12 | ], 13 | "require": { 14 | "php": ">=5.3", 15 | "clue/socket-raw": "1.* | 0.1.*", 16 | "evenement/evenement": "1.*", 17 | "react/event-loop": ">=0.2, <0.4", 18 | "react/promise": "1.*", 19 | "react/stream": ">=0.2, <0.4", 20 | "react/socket": ">=0.2, <0.4", 21 | "react/datagram": "~1.0" 22 | }, 23 | "autoload": { 24 | "psr-4": {"Socket\\React\\": "src"} 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /examples/client-tcp.php: -------------------------------------------------------------------------------- 1 | createClient('www.google.com:80')->then(function (Socket\React\Stream\Connection $stream) { 10 | var_dump('Connection established to', $stream->getRemoteAddress()); 11 | $stream->write("GET / HTTP/1.0\r\nHost: www.google.com\r\n\r\n"); 12 | 13 | $stream->on('data', function($data) { 14 | var_dump($data); 15 | }); 16 | $stream->on('close', function() { 17 | var_dump('Connection closed'); 18 | }); 19 | }, function(Exception $e) { 20 | var_dump('Connection failed: ', $e->getMessage()); 21 | echo $e; 22 | }); 23 | 24 | $loop->run(); 25 | -------------------------------------------------------------------------------- /examples/client-udp.php: -------------------------------------------------------------------------------- 1 | createClient($address)->then(function (Socket\React\Datagram\Socket $socket) use ($loop, $address) { 12 | var_dump('Client socket connected to ' . $address . ' created'); 13 | 14 | var_dump('Sending "test"'); 15 | $socket->send('test'); 16 | 17 | $socket->on('message', function($data, $peer) { 18 | var_dump('Received', $data, 'from', $peer); 19 | echo PHP_EOL; 20 | }); 21 | $socket->on('close', function() { 22 | var_dump('Connection closed'); 23 | }); 24 | $socket->on('error', function($error) { 25 | var_dump('Error'); 26 | echo $error; 27 | }); 28 | 29 | var_dump('Reading and forwarding everything from STDIN'); 30 | $loop->addReadStream(STDIN, function() use ($socket) { 31 | $line = trim(fgets(STDIN,8192)); 32 | var_dump('Sending input', $line); 33 | $socket->send($line); 34 | echo PHP_EOL; 35 | }); 36 | 37 | // send tick message every 2 seconds 38 | // $loop->addPeriodicTimer(2.0, function () use ($socket) { 39 | // $socket->send('tick'); 40 | // }); 41 | 42 | }, function(Exception $e) { 43 | var_dump('Creation failed: ', $e->getMessage()); 44 | echo $e; 45 | }); 46 | 47 | $loop->run(); 48 | -------------------------------------------------------------------------------- /examples/server-udp.php: -------------------------------------------------------------------------------- 1 | createServer($address)->then(function (Socket\React\Datagram\Socket $socket) use ($loop, $address) { 12 | var_dump('Server listening on ' . $address .' established'); 13 | 14 | $socket->on('message', function($data, $peer) use ($socket) { 15 | var_dump('Received', $data, 'from', $peer); 16 | 17 | // send back same message to peer 18 | $socket->send($data, $peer); 19 | echo PHP_EOL; 20 | }); 21 | $socket->on('close', function() { 22 | var_dump('Connection closed'); 23 | }); 24 | $socket->on('error', function($error) { 25 | var_dump('Error'); 26 | echo $error; 27 | }); 28 | }, function(Exception $e) { 29 | var_dump('Creation failed: ', $e->getMessage()); 30 | echo $e; 31 | }); 32 | 33 | $loop->run(); 34 | -------------------------------------------------------------------------------- /phpunit.xml.dist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 9 | 10 | 11 | ./tests/ 12 | 13 | 14 | 15 | 16 | ./src/ 17 | 18 | 19 | -------------------------------------------------------------------------------- /src/Datagram/Buffer.php: -------------------------------------------------------------------------------- 1 | socket->send($data, 0); 13 | } else { 14 | $this->socket->sendTo($data, 0, $remoteAddress); 15 | } 16 | } 17 | 18 | protected function handlePause() 19 | { 20 | $this->loop->removeWriteStream($this->socket->getResource()); 21 | } 22 | 23 | protected function handleResume() 24 | { 25 | $this->loop->addWriteStream($this->socket->getResource(), array($this, 'onWritable')); 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /src/Datagram/Factory.php: -------------------------------------------------------------------------------- 1 | loop = $loop; 23 | $this->rawFactory = new RawFactory(); 24 | } 25 | 26 | /** 27 | * Create datagram client socket connect()ed to given remote address 28 | * 29 | * Please note that unlike streaming sockets (TCP/IP), 30 | * datagram sockets usually have no concept of an 31 | * "established connection", i.e. the remote side will NOT be notified 32 | * of any "connection attempt" and no data has to be exchanged. 33 | * 34 | * Usually, there's no /need/ to connect() datagram sockets. If you 35 | * want to send to a specific remote address, see the $remote parameter 36 | * in send() as an alternative. Connect()ing the datagram client to 37 | * the remote side once may be preferrable as it frees you from having 38 | * to pass the remote address along with every send() call and only 39 | * requires a single host name resolution instead of having to perform 40 | * it with every send() call. 41 | * 42 | * @param string $address 43 | * @param array $context (optional) "bindto" or "broadcast" context options 44 | * @return PromiseInterface to return a \Socket\React\Datagram\Datagram 45 | * @uses RawFactory::createFromString() 46 | * @uses RawSocket::setOption() to toggle broadcast option 47 | * @uses RawSocket::bind() to bind to given local address 48 | * @uses RawSocket::setBlocking() to turn on non-blocking mode 49 | * @uses RawSocket::connect() to initiate connection 50 | * @todo consider adding additional socket options 51 | * @todo use async DNS resolver for "bindto" context 52 | */ 53 | public function createClient($address, $context = array()) 54 | { 55 | $that = $this; 56 | $factory = $this->rawFactory; 57 | 58 | return $this->resolve($address)->then(function ($address) use ($factory, $that, $context){ 59 | $scheme = 'udp'; 60 | $socket = $factory->createFromString($address, $scheme); 61 | if ($socket->getType() !== SOCK_DGRAM) { 62 | $socket->close(); 63 | throw new Exception('Not a datagram address scheme'); 64 | } 65 | 66 | if (isset($context['broadcast']) && $context['broadcast']) { 67 | $socket->setOption(SOL_SOCKET, SO_BROADCAST, 1); 68 | } 69 | 70 | if (isset($context['bindto'])) { 71 | $socket->bind($context['bindto']); 72 | } 73 | 74 | $socket->setBlocking(false); 75 | $socket->connect($address); 76 | 77 | return $that->createFromRaw($socket); 78 | }); 79 | } 80 | 81 | /** 82 | * create datagram server socket waiting for incoming messages on the given local address 83 | * 84 | * @param string $address 85 | * @param array $context (optional) "broadcast" context option 86 | * @return PromiseInterface to return a \Socket\React\Datagram\Datagram 87 | * @uses RawFactory::createFromString() 88 | * @uses RawSocket::setBlocking() to turn on non-blocking mode 89 | * @uses RawSocket::bind() to initiate connection 90 | */ 91 | public function createServer($address, $context = array()) 92 | { 93 | $that = $this; 94 | $factory = $this->rawFactory; 95 | 96 | return $this->resolve($address)->then(function ($address) use ($factory, $that, $context){ 97 | $scheme = 'udp'; 98 | $socket = $factory->createFromString($address, $scheme); 99 | if ($socket->getType() !== SOCK_DGRAM) { 100 | $socket->close(); 101 | throw new Exception('Not a datagram address scheme'); 102 | } 103 | 104 | if (isset($context['broadcast']) && $context['broadcast']) { 105 | $socket->setOption(SOL_SOCKET, SO_BROADCAST, 1); 106 | } 107 | 108 | $socket->setBlocking(false); 109 | $socket->bind($address); 110 | 111 | return $that->createFromRaw($socket); 112 | }); 113 | } 114 | 115 | public function createUdp4() 116 | { 117 | return $this->createFromRaw($this->rawFactory->createUdp4()); 118 | } 119 | 120 | public function createUdp6() 121 | { 122 | return $this->createFromRaw($this->rawFactory->createUdp6()); 123 | } 124 | 125 | public function createUdg() 126 | { 127 | return $this->createFromRaw($this->rawFactory->createUdg()); 128 | } 129 | 130 | public function createIcmp4() 131 | { 132 | return $this->createFromRaw($this->rawFactory->createIcmp4()); 133 | } 134 | 135 | public function createIcmp6() 136 | { 137 | return $this->createFromRaw($this->rawFactory->createIcmp6()); 138 | } 139 | 140 | public function createFromRaw(RawSocket $rawSocket) 141 | { 142 | return new Socket($this->getSocketLoop(), $rawSocket); 143 | } 144 | 145 | /** 146 | * return a loop interface that supports adding socket resources 147 | * 148 | * @return LoopInterface 149 | */ 150 | protected function getSocketLoop() 151 | { 152 | if ($this->socketLoop === null) { 153 | if ($this->loop instanceof StreamSelectLoop) { 154 | $this->socketLoop = new SelectPoller($this->loop); 155 | } else { 156 | $this->socketLoop = $this->loop; 157 | } 158 | } 159 | return $this->socketLoop; 160 | } 161 | 162 | /** 163 | * resolve given address via DNS if applicable 164 | * 165 | * Letting host names pass through will not break things, but it 166 | * requires a blocking resolution afterwards. So make sure to try to 167 | * resolve hostnames here. 168 | * 169 | * @param string $address 170 | * @return PromiseInterface 171 | * @todo use Resolver to perform async resolving 172 | */ 173 | private function resolve($address) 174 | { 175 | return When::resolve($address); 176 | } 177 | } 178 | -------------------------------------------------------------------------------- /src/Datagram/Socket.php: -------------------------------------------------------------------------------- 1 | loop->addReadStream($this->socket->getResource(), array($this, 'onReceive')); 25 | } 26 | 27 | public function pause() 28 | { 29 | $this->loop->removeReadStream($this->socket->getResource()); 30 | } 31 | 32 | public function getRemoteAddress() 33 | { 34 | return $this->socket->getPeerName(); 35 | } 36 | 37 | public function getLocalAddress() 38 | { 39 | return $this->socket->getSockName(); 40 | } 41 | 42 | protected function handleReceive(&$remote) 43 | { 44 | return $this->socket->recvFrom($this->bufferSize, 0, $remote); 45 | } 46 | 47 | protected function handleClose() 48 | { 49 | try { 50 | $this->socket->shutdown(); 51 | } 52 | catch (Exception $ignore) { 53 | } 54 | $this->socket->close(); 55 | } 56 | } 57 | -------------------------------------------------------------------------------- /src/EventLoop/SelectPoller.php: -------------------------------------------------------------------------------- 1 | loop = $loop; 23 | } 24 | 25 | public function setPollInterval($pollInterval) 26 | { 27 | $this->pollInterval = $pollInterval; 28 | 29 | // restart with new interval in case it's currently running 30 | if ($this->tid !== null) { 31 | $this->pause(); 32 | $this->resume(); 33 | } 34 | } 35 | 36 | public function getPollInterval() 37 | { 38 | return $this->pollInterval; 39 | } 40 | 41 | public function setPollDuration($pollDuration) 42 | { 43 | $this->pollDurationSec = (int)$pollDuration; 44 | $this->pollDurationUsec = (int)(($pollDuration - (int)$pollDuration) * 1000000); 45 | } 46 | 47 | public function getPollDuration() 48 | { 49 | return ($this->pollDurationSec + $this->pollDurationUsec / 1000000); 50 | } 51 | 52 | 53 | /** 54 | * notify poller to schedule polling ASAP for next tick 55 | * 56 | * this doesn't neccessary have to actually do anything (and in fact 57 | * it does NOT at the moment...), but it's purpose is to notify the 58 | * main loop that something in this poller instance has (likely) changed 59 | * and polling should be performed ASAP. This could re-schedule the 60 | * timer to poll in the next available tick instead of waiting for the 61 | * timer to expire. 62 | * 63 | * @return self $this (chainable) 64 | * @todo actually do something. this is a no-op currently 65 | */ 66 | public function notify() 67 | { 68 | return $this; 69 | } 70 | 71 | private function resume() 72 | { 73 | if ($this->tid === null && $this->hasListeners()) { 74 | $this->tid = $this->loop->addPeriodicTimer($this->pollInterval, array($this, 'poll')); 75 | } 76 | } 77 | 78 | private function pause() 79 | { 80 | if ($this->tid !== null && !$this->hasListeners()) { 81 | $this->loop->cancelTimer($this->tid); 82 | $this->tid = null; 83 | } 84 | } 85 | 86 | public function poll() 87 | { 88 | $this->runStreamSelect(); 89 | } 90 | 91 | protected function getNextEventTimeInMicroSeconds() 92 | { 93 | return $this->getPollDuration() * 1000000; 94 | } 95 | 96 | public function addReadStream($stream, $listener) 97 | { 98 | parent::addReadStream($stream, $listener); 99 | $this->resume(); 100 | } 101 | 102 | public function addWriteStream($stream, $listener) 103 | { 104 | parent::addWriteStream($stream, $listener); 105 | $this->resume(); 106 | } 107 | 108 | public function removeReadStream($stream) 109 | { 110 | parent::removeReadStream($stream); 111 | $this->pause(); 112 | } 113 | 114 | public function removeWriteStream($stream) 115 | { 116 | parent::removeWriteStream($stream); 117 | $this->pause(); 118 | } 119 | 120 | public function removeStream($stream) 121 | { 122 | parent::removeStream($stream); 123 | $this->pause(); 124 | } 125 | 126 | public function tick() 127 | { 128 | return $this->loop->tick(); 129 | } 130 | 131 | public function run() 132 | { 133 | return $this->loop->run(); 134 | } 135 | 136 | public function stop() 137 | { 138 | return $this->loop->stop(); 139 | } 140 | } 141 | -------------------------------------------------------------------------------- /src/EventLoop/SocketSelectLoop.php: -------------------------------------------------------------------------------- 1 | timers = new Timers(); 28 | } 29 | 30 | public function addReadStream($stream, $listener) 31 | { 32 | $this->assertStream($stream); 33 | 34 | $id = (int) $stream; 35 | 36 | if (!isset($this->readStreams[$id])) { 37 | $this->readStreams[$id] = $stream; 38 | $this->readListeners[$id] = $listener; 39 | } 40 | } 41 | 42 | public function addWriteStream($stream, $listener) 43 | { 44 | $this->assertStream($stream); 45 | 46 | $id = (int) $stream; 47 | 48 | if (!isset($this->writeStreams[$id])) { 49 | $this->writeStreams[$id] = $stream; 50 | $this->writeListeners[$id] = $listener; 51 | } 52 | } 53 | 54 | public function removeReadStream($stream) 55 | { 56 | $id = (int) $stream; 57 | 58 | unset( 59 | $this->readStreams[$id], 60 | $this->readListeners[$id] 61 | ); 62 | } 63 | 64 | public function removeWriteStream($stream) 65 | { 66 | $id = (int) $stream; 67 | 68 | unset( 69 | $this->writeStreams[$id], 70 | $this->writeListeners[$id] 71 | ); 72 | } 73 | 74 | public function removeStream($stream) 75 | { 76 | $this->removeReadStream($stream); 77 | $this->removeWriteStream($stream); 78 | } 79 | 80 | public function addTimer($interval, $callback) 81 | { 82 | $timer = new Timer($this, $interval, $callback, false); 83 | $this->timers->add($timer); 84 | 85 | return $timer; 86 | } 87 | 88 | public function addPeriodicTimer($interval, $callback) 89 | { 90 | $timer = new Timer($this, $interval, $callback, true); 91 | $this->timers->add($timer); 92 | 93 | return $timer; 94 | } 95 | 96 | public function cancelTimer(TimerInterface $timer) 97 | { 98 | $this->timers->cancel($timer); 99 | } 100 | 101 | public function isTimerActive(TimerInterface $timer) 102 | { 103 | return $this->timers->contains($timer); 104 | } 105 | 106 | protected function getNextEventTimeInMicroSeconds() 107 | { 108 | $nextEvent = $this->timers->getFirst(); 109 | 110 | if (null === $nextEvent) { 111 | return self::QUANTUM_INTERVAL; 112 | } 113 | 114 | $currentTime = microtime(true); 115 | if ($nextEvent > $currentTime) { 116 | return ($nextEvent - $currentTime) * 1000000; 117 | } 118 | 119 | return 0; 120 | } 121 | 122 | protected function sleepOnPendingTimers() 123 | { 124 | if ($this->timers->isEmpty()) { 125 | $this->running = false; 126 | } else { 127 | // We use usleep() instead of stream_select() to emulate timeouts 128 | // since the latter fails when there are no streams registered for 129 | // read / write events. Blame PHP for us needing this hack. 130 | usleep($this->getNextEventTimeInMicroSeconds()); 131 | } 132 | } 133 | 134 | protected function runStreamSelect() 135 | { 136 | $read = $this->readStreams ?: null; 137 | $write = $this->writeStreams ?: null; 138 | $except = null; 139 | 140 | if (!$read && !$write) { 141 | $this->sleepOnPendingTimers(); 142 | 143 | return; 144 | } 145 | 146 | if (socket_select($read, $write, $except, 0, $this->getNextEventTimeInMicroSeconds()) > 0) { 147 | if ($read) { 148 | foreach ($read as $stream) { 149 | $listener = $this->readListeners[(int) $stream]; 150 | call_user_func($listener, $stream, $this); 151 | } 152 | } 153 | 154 | if ($write) { 155 | foreach ($write as $stream) { 156 | if (!isset($this->writeListeners[(int) $stream])) { 157 | continue; 158 | } 159 | 160 | $listener = $this->writeListeners[(int) $stream]; 161 | call_user_func($listener, $stream, $this); 162 | } 163 | } 164 | } 165 | } 166 | 167 | public function hasListeners() 168 | { 169 | return ($this->readStreams || $this->writeStreams); 170 | } 171 | 172 | public function tick() 173 | { 174 | $this->timers->tick(); 175 | $this->runStreamSelect(); 176 | 177 | return $this->running; 178 | } 179 | 180 | public function run() 181 | { 182 | $this->running = true; 183 | 184 | while ($this->tick()) { 185 | // NOOP 186 | } 187 | } 188 | 189 | public function stop() 190 | { 191 | $this->running = false; 192 | } 193 | 194 | private function assertStream($stream) 195 | { 196 | static $checked = array(); 197 | $type = get_resource_type($stream); 198 | if (is_resource($stream) && !isset($checked[$type])) { 199 | $except = array($stream); 200 | $null = null; 201 | $checked[$type] = (@socket_select($null, $null, $except, 0) !== false); 202 | } 203 | 204 | if (!$checked[$type]) { 205 | throw new InvalidArgumentException('Socket loop only accepts resources of type "Socket", but "' . $type .'" given'); 206 | } 207 | } 208 | } 209 | -------------------------------------------------------------------------------- /src/Stream/Connection.php: -------------------------------------------------------------------------------- 1 | socket = $socket; 32 | $this->loop = $loop; 33 | 34 | $this->buffer = new StreamBuffer($socket, $loop); 35 | 36 | $that = $this; 37 | 38 | $this->buffer->on('error', function ($error) use ($that) { 39 | $that->emit('error', array($error, $that)); 40 | $that->close(); 41 | }); 42 | 43 | $this->buffer->on('drain', function () use ($that) { 44 | $that->emit('drain'); 45 | }); 46 | 47 | $this->resume(); 48 | } 49 | 50 | public function resume() 51 | { 52 | $this->loop->addReadStream($this->socket->getResource(), array($this, 'handleData')); 53 | } 54 | 55 | public function pause() 56 | { 57 | $this->loop->removeReadStream($this->socket->getResource()); 58 | } 59 | 60 | public function isReadable() 61 | { 62 | return true; 63 | } 64 | 65 | public function isWritable() 66 | { 67 | return true; 68 | } 69 | 70 | public function write($data) 71 | { 72 | if (!$this->writable) { 73 | return; 74 | } 75 | 76 | return $this->buffer->write($data); 77 | } 78 | 79 | public function close() 80 | { 81 | if (!$this->writable && !$this->closing) { 82 | return; 83 | } 84 | 85 | $this->closing = false; 86 | 87 | $this->readable = false; 88 | $this->writable = false; 89 | 90 | $this->emit('end', array($this)); 91 | $this->emit('close', array($this)); 92 | 93 | $this->pause(); 94 | $this->buffer->close(); 95 | 96 | $this->socket->shutdown(); 97 | $this->socket->close(); 98 | 99 | $this->removeAllListeners(); 100 | } 101 | 102 | public function end($data = null) 103 | { 104 | if (!$this->writable) { 105 | return; 106 | } 107 | 108 | $that = $this; 109 | $this->buffer->on('close', function() use ($that) { 110 | $that->close(); 111 | }); 112 | $this->buffer->end($data); 113 | } 114 | 115 | public function pipe(WritableStreamInterface $dest, array $options = array()) 116 | { 117 | Util::pipe($this, $dest, $options); 118 | 119 | return $dest; 120 | } 121 | 122 | public function handleData() 123 | { 124 | try { 125 | $data = $this->socket->read($this->bufferSize); 126 | } 127 | catch (\Exception $e) { 128 | return $this->end(); 129 | } 130 | 131 | 132 | if ($data === '') { 133 | $this->end(); 134 | } else { 135 | $this->emit('data', array($data, $this)); 136 | } 137 | } 138 | 139 | public function getRemoteAddress() 140 | { 141 | $name = $this->socket->getPeerName(); 142 | return trim(substr($name, 0, strrpos($name, ':')), '[]'); 143 | } 144 | } 145 | -------------------------------------------------------------------------------- /src/Stream/Factory.php: -------------------------------------------------------------------------------- 1 | loop = $loop; 22 | $this->rawFactory = new RawFactory(); 23 | } 24 | 25 | /** 26 | * create stream client socket connected to given address 27 | * 28 | * @param string $address 29 | * @return PromiseInterface to return a \Sockets\Stream 30 | * @uses RawFactory::createFromString() 31 | * @uses RawSocket::setBlocking() to turn on non-blocking mode 32 | * @uses RawSocket::connect() to initiate async connection 33 | * @uses LoopInterface::addWriteStream() to wait for connection result once 34 | * @uses RawSocket::assertAlive() to check connection result 35 | */ 36 | public function createClient($address) 37 | { 38 | $that = $this; 39 | $factory = $this->rawFactory; 40 | 41 | return $this->resolve($address)->then(function ($address) use ($factory, $that){ 42 | $deferred = new Deferred(); 43 | 44 | $socket = $factory->createFromString($address, $scheme); 45 | if ($socket->getType() !== SOCK_STREAM) { 46 | $socket->close(); 47 | throw new Exception('Not a stream address scheme'); 48 | } 49 | 50 | $socket->setBlocking(false); 51 | 52 | try{ 53 | // socket is nonblocking, so connect should emit EINPROGRESS 54 | $socket->connect($address); 55 | 56 | // socket is already connected immediately? 57 | $deferred->resolve(new Connection($socket, $that->getSocketLoop())); 58 | } 59 | catch(Exception $exception) 60 | { 61 | if ($exception->getCode() === SOCKET_EINPROGRESS) { 62 | // connection in progress => wait for the socket to become writable 63 | $that->getSocketLoop()->addWriteStream($socket->getResource(), function ($resource, $loop) use ($deferred, $socket){ 64 | // only poll for writable event once 65 | $loop->removeWriteStream($resource); 66 | 67 | try { 68 | // assert that socket error is 0 (no TCP RST received) 69 | $socket->assertAlive(); 70 | } 71 | catch (Exception $e) { 72 | // error returned => connected failed 73 | $socket->close(); 74 | 75 | $deferred->reject(new Exception('Error while establishing connection' , $e->getCode(), $e)); 76 | return; 77 | } 78 | 79 | // no error => connection established 80 | $deferred->resolve(new Connection($socket, $loop)); 81 | }); 82 | } else { 83 | // re-throw any other socket error 84 | $socket->close(); 85 | $deferred->reject($exception); 86 | } 87 | } 88 | return $deferred->promise(); 89 | }); 90 | } 91 | 92 | /** 93 | * create stream server socket bound to and listening on the given address for incomming stream client connections 94 | * 95 | * @param string $address 96 | * @return PromiseInterface to return a \Sockets\Server 97 | * @throws Exception on error 98 | * @uses Server::listenAddress() 99 | */ 100 | public function createServer($address) 101 | { 102 | return $this->resolve($address)->then(function ($address) { 103 | $server = new Server($this->getSocketLoop(), $this->rawFactory); 104 | $server->listenAddress($address); 105 | 106 | return $server; 107 | }); 108 | } 109 | 110 | /** 111 | * return a loop interface that supports adding socket resources 112 | * 113 | * @return LoopInterface 114 | */ 115 | public function getSocketLoop() 116 | { 117 | if ($this->socketLoop === null) { 118 | if ($this->loop instanceof StreamSelectLoop) { 119 | $this->socketLoop = new SelectPoller($this->loop); 120 | } else { 121 | $this->socketLoop = $this->loop; 122 | } 123 | } 124 | return $this->socketLoop; 125 | } 126 | 127 | /** 128 | * resolve given address via DNS if applicable 129 | * 130 | * Letting host names pass through will not break things, but it 131 | * requires a blocking resolution afterwards. So make sure to try to 132 | * resolve hostnames here. 133 | * 134 | * @param string $address 135 | * @return PromiseInterface 136 | * @todo use Resolver to perform async resolving 137 | */ 138 | private function resolve($address) 139 | { 140 | return When::resolve($address); 141 | } 142 | } 143 | -------------------------------------------------------------------------------- /src/Stream/Server.php: -------------------------------------------------------------------------------- 1 | loop = $loop; 24 | $this->factory = $factory; 25 | } 26 | 27 | public function listen($port, $host = '127.0.0.1') 28 | { 29 | if (strpos($host, ':') !== false) { 30 | // IPv6 addressing has to use square brackets 31 | $host = '[' . $host . ']'; 32 | } 33 | $address = 'tcp://' . $host . ':' . $port; 34 | 35 | return $this->listenAddress($address); 36 | } 37 | 38 | public function listenAddress($address) 39 | { 40 | if ($this->factory === null) { 41 | $this->factory = new RawFactory(); 42 | } 43 | $this->socket = $this->factory->createServer($address); 44 | if ($this->socket->getType() !== SOCK_STREAM) { 45 | $this->socket->close(); 46 | throw new Exception('Not a stream address scheme'); 47 | } 48 | $this->socket->setBlocking(false); 49 | 50 | $that = $this; 51 | $socket = $this->socket; 52 | $this->loop->addReadStream($this->socket->getResource(), function() use ($socket, $that) { 53 | $clientSocket = $socket->accept(); 54 | 55 | $that->handleConnection($clientSocket); 56 | }); 57 | } 58 | 59 | public function handleConnection(RawSocket $clientSocket) 60 | { 61 | $clientSocket->setBlocking(false); 62 | 63 | $client = $this->createConnection($clientSocket); 64 | 65 | $this->emit('connection', array($client)); 66 | } 67 | 68 | protected function createConnection(RawSocket $clientSocket) 69 | { 70 | return new Connection($clientSocket, $this->loop); 71 | } 72 | 73 | public function getPort() 74 | { 75 | $name = $this->socket->getSockName(); 76 | return (int) substr(strrchr($name, ':'), 1); 77 | } 78 | 79 | public function shutdown() 80 | { 81 | $this->loop->removeReadStream($this->socket->getResource()); 82 | 83 | $this->socket->shutdown(); 84 | $this->socket->close(); 85 | } 86 | } 87 | -------------------------------------------------------------------------------- /src/Stream/StreamBuffer.php: -------------------------------------------------------------------------------- 1 | 0, 23 | 'message' => '', 24 | 'file' => '', 25 | 'line' => 0, 26 | ); 27 | 28 | public function __construct(RawSocket $socket, LoopInterface $loop) 29 | { 30 | $this->socket = $socket; 31 | $this->loop = $loop; 32 | } 33 | 34 | public function isWritable() 35 | { 36 | return $this->writable; 37 | } 38 | 39 | public function write($data) 40 | { 41 | if (!$this->writable) { 42 | return; 43 | } 44 | 45 | $this->data .= $data; 46 | 47 | if (!$this->listening) { 48 | $this->listening = true; 49 | 50 | $this->loop->addWriteStream($this->socket->getResource(), array($this, 'handleWrite')); 51 | } 52 | 53 | $belowSoftLimit = strlen($this->data) < $this->softLimit; 54 | 55 | return $belowSoftLimit; 56 | } 57 | 58 | public function end($data = null) 59 | { 60 | if (null !== $data) { 61 | $this->write($data); 62 | } 63 | 64 | $this->writable = false; 65 | 66 | if ($this->listening) { 67 | $this->on('full-drain', array($this, 'close')); 68 | } else { 69 | $this->close(); 70 | } 71 | } 72 | 73 | public function close() 74 | { 75 | $this->writable = false; 76 | $this->listening = false; 77 | $this->data = ''; 78 | 79 | $this->emit('close'); 80 | } 81 | 82 | public function handleWrite() 83 | { 84 | 85 | // if (!is_resource($this->stream) || feof($this->stream)) { 86 | // $this->emit('error', array(new \RuntimeException('Tried to write to closed or invalid stream.'))); 87 | 88 | // return; 89 | // } 90 | 91 | try { 92 | $sent = $this->socket->write($this->data); 93 | } 94 | catch (Exception $e) { 95 | $this->emit('error', array($e)); 96 | return; 97 | } 98 | 99 | $len = strlen($this->data); 100 | if ($len >= $this->softLimit && $len - $sent < $this->softLimit) { 101 | $this->emit('drain'); 102 | } 103 | 104 | $this->data = (string) substr($this->data, $sent); 105 | 106 | if (0 === strlen($this->data)) { 107 | $this->loop->removeWriteStream($this->socket->getResource()); 108 | $this->listening = false; 109 | 110 | $this->emit('full-drain'); 111 | } 112 | } 113 | } 114 | -------------------------------------------------------------------------------- /tests/Datagram/FactoryTest.php: -------------------------------------------------------------------------------- 1 | loop = React\EventLoop\Factory::create(); 18 | $this->factory = new Factory($this->loop); 19 | } 20 | 21 | public function testSupportsIpv6() 22 | { 23 | // TODO: check this check 24 | if (!defined('AF_INET6')) { 25 | $this->markTestSkipped('This system does not seem to support IPv6 sockets / addressing'); 26 | } 27 | } 28 | 29 | public function testSupportsUnix() 30 | { 31 | // TODO: check this check 32 | if (!defined('AF_UNIX')) { 33 | $this->markTestSkipped('This system does not seem to support UDG (Unix DataGram) sockets'); 34 | } 35 | } 36 | 37 | public function testConstructorWorks() 38 | { 39 | $this->assertInstanceOf('Socket\React\Datagram\Factory', $this->factory); 40 | } 41 | 42 | public function testCreateClientUdp4() 43 | { 44 | $promise = $this->factory->createClient('udp://127.0.0.1:53'); 45 | 46 | $this->assertInstanceOf('React\Promise\PromiseInterface', $promise); 47 | 48 | $promise->then($this->expectCallableOnceParameter('Socket\React\Datagram\Socket'), $this->expectCallableNever()); 49 | 50 | $this->loop->tick(); 51 | } 52 | 53 | public function testCreateClientUdp4Broadcast() 54 | { 55 | $promise = $this->factory->createClient('udp://255.255.255.255:27015', array('broadcast' => true)); 56 | 57 | $this->assertInstanceOf('React\Promise\PromiseInterface', $promise); 58 | 59 | $promise->then($this->expectCallableOnceParameter('Socket\React\Datagram\Socket'), $this->expectCallableNever()); 60 | 61 | $this->loop->tick(); 62 | } 63 | 64 | /** 65 | * explicitly send packets via port 27016 66 | */ 67 | public function testCreateClientUdp4Bind() 68 | { 69 | $promise = $this->factory->createClient('udp://127.0.0.1:53', array('bindto' => '0:27016')); 70 | 71 | $this->assertInstanceOf('React\Promise\PromiseInterface', $promise); 72 | 73 | $promise->then($this->expectCallableOnceParameter('Socket\React\Datagram\Socket'), $this->expectCallableNever()); 74 | 75 | $this->loop->tick(); 76 | } 77 | 78 | public function testCreateClientSchemelessUdp4() 79 | { 80 | $promise = $this->factory->createClient('127.0.0.1:53'); 81 | 82 | $this->assertInstanceOf('React\Promise\PromiseInterface', $promise); 83 | 84 | $promise->then($this->expectCallableOnceParameter('Socket\React\Datagram\Socket'), $this->expectCallableNever()); 85 | 86 | $this->loop->tick(); 87 | } 88 | 89 | /** 90 | * @depends testSupportsIpv6 91 | */ 92 | public function testCreateClientSchemelessUdp6() 93 | { 94 | $promise = $this->factory->createClient('[::1]:53'); 95 | 96 | $this->assertInstanceOf('React\Promise\PromiseInterface', $promise); 97 | 98 | $promise->then($this->expectCallableOnceParameter('Socket\React\Datagram\Socket'), $this->expectCallableNever()); 99 | 100 | $this->loop->tick(); 101 | } 102 | 103 | /** 104 | * creating a TCP socket fails because it is NOT a datagram socket 105 | */ 106 | public function testCreateClientFailTcp() 107 | { 108 | $promise = $this->factory->createClient('tcp://www.google.com:80'); 109 | 110 | $this->assertInstanceOf('React\Promise\PromiseInterface', $promise); 111 | 112 | $promise->then($this->expectCallableNever(), $this->expectCallableOnceParameter('Exception')); 113 | 114 | $this->loop->tick(); 115 | } 116 | 117 | public function testCreateServerUdp4() 118 | { 119 | $promise = $this->factory->createServer('udp://127.0.0.1:0'); 120 | 121 | $this->assertInstanceOf('React\Promise\PromiseInterface', $promise); 122 | 123 | $promise->then($this->expectCallableOnceParameter('Socket\React\Datagram\Socket'), $this->expectCallableNever()); 124 | 125 | $this->loop->tick(); 126 | } 127 | 128 | public function testCreateServerUdp4Broadcast() 129 | { 130 | $promise = $this->factory->createServer('udp://255.255.255.255:27015', array('broadcast' => true)); 131 | 132 | $this->assertInstanceOf('React\Promise\PromiseInterface', $promise); 133 | 134 | $promise->then($this->expectCallableOnceParameter('Socket\React\Datagram\Socket'), $this->expectCallableNever()); 135 | 136 | $this->loop->tick(); 137 | } 138 | 139 | public function testCreateServerSchemelessUdp4() 140 | { 141 | $promise = $this->factory->createServer('127.0.0.1:0'); 142 | 143 | $this->assertInstanceOf('React\Promise\PromiseInterface', $promise); 144 | 145 | $promise->then($this->expectCallableOnceParameter('Socket\React\Datagram\Socket'), $this->expectCallableNever()); 146 | 147 | $this->loop->tick(); 148 | } 149 | 150 | /** 151 | * @depends testSupportsIpv6 152 | */ 153 | public function testCreateServerSchemelessUdp6() 154 | { 155 | $promise = $this->factory->createServer('[::1]:0'); 156 | 157 | $this->assertInstanceOf('React\Promise\PromiseInterface', $promise); 158 | 159 | $promise->then($this->expectCallableOnceParameter('Socket\React\Datagram\Socket'), $this->expectCallableNever()); 160 | 161 | $this->loop->tick(); 162 | } 163 | 164 | /** 165 | * creating a TCP socket fails because it is NOT a datagram socket 166 | */ 167 | public function testCreateServerFailTcp() 168 | { 169 | $promise = $this->factory->createServer('tcp://127.0.0.1:0'); 170 | 171 | $this->assertInstanceOf('React\Promise\PromiseInterface', $promise); 172 | 173 | $promise->then($this->expectCallableNever(), $this->expectCallableOnceParameter('Exception')); 174 | 175 | $this->loop->tick(); 176 | } 177 | 178 | public function testCreateUdp4() 179 | { 180 | $socket = $this->factory->createUdp4(); 181 | 182 | $this->assertInstanceOf('Socket\React\Datagram\Socket', $socket); 183 | } 184 | 185 | /** 186 | * @depends testSupportsIpv6 187 | */ 188 | public function testCreateUdp6() 189 | { 190 | $socket = $this->factory->createUdp6(); 191 | 192 | $this->assertInstanceOf('Socket\React\Datagram\Socket', $socket); 193 | } 194 | 195 | /** 196 | * @depends testSupportsUnix 197 | */ 198 | public function testCreateUdg() 199 | { 200 | $socket = $this->factory->createUdg(); 201 | 202 | $this->assertInstanceOf('Socket\React\Datagram\Socket', $socket); 203 | } 204 | 205 | public function testCreateIcmp4() 206 | { 207 | try { 208 | $socket = $this->factory->createIcmp4(); 209 | } 210 | catch (Exception $e) { 211 | if ($e->getCode() === SOCKET_EPERM) { 212 | // skip if not root 213 | return $this->markTestSkipped('No access to ICMPv4 socket (only root can do so)'); 214 | } 215 | throw $e; 216 | } 217 | 218 | $this->assertInstanceOf('Socket\React\Datagram\Socket', $socket); 219 | } 220 | 221 | /** 222 | * @depends testSupportsIpv6 223 | */ 224 | public function testCreateIcmp6() 225 | { 226 | try { 227 | $socket = $this->factory->createIcmp6(); 228 | } 229 | catch (Exception $e) { 230 | if ($e->getCode() === SOCKET_EPERM) { 231 | // skip if not root 232 | return $this->markTestSkipped('No access to ICMPv4 socket (only root can do so)'); 233 | } 234 | throw $e; 235 | } 236 | 237 | $this->assertInstanceOf('Socket\React\Datagram\Socket', $socket); 238 | } 239 | } 240 | -------------------------------------------------------------------------------- /tests/Datagram/SocketTest.php: -------------------------------------------------------------------------------- 1 | loop = React\EventLoop\Factory::create(); 19 | $this->factory = new Factory($this->loop); 20 | } 21 | 22 | public function testClientServerUdp4() 23 | { 24 | $that = $this; 25 | $loop = $this->loop; 26 | $this->factory->createServer('127.0.0.1:1337')->then(function (Socket $socket) use ($loop, $that) { 27 | $that->assertEquals('127.0.0.1:1337', $socket->getLocalAddress()); 28 | $socket->on('message', function($message, $remote, Socket $socket) use ($loop) { 29 | // for every message we receive, send back the reversed message (ABC -> CBA) 30 | $socket->send(strrev($message), $remote); 31 | $socket->end(); 32 | }); 33 | }); 34 | 35 | $once = $this->expectCallableOnce(); 36 | $this->factory->createClient('127.0.0.1:1337')->then(function (Socket $socket) use ($that, $once) { 37 | $that->assertEquals('127.0.0.1:1337', $socket->getRemoteAddress()); 38 | $socket->send('test'); 39 | 40 | $socket->on('message', $once); 41 | $socket->on('message', function ($message, $remote, $socket) use ($that) { 42 | $that->assertEquals('tset', $message); 43 | $that->assertEquals('127.0.0.1:1337', $remote); 44 | 45 | $socket->close(); 46 | }); 47 | }); 48 | 49 | $this->loop->run(); 50 | } 51 | 52 | /** "connecting" and sending message to an unbound port should not raise any errors (messages will be discarded) */ 53 | public function testClientUdp4Unbound() 54 | { 55 | $this->factory->createClient('127.0.0.1:2')->then(function (Socket $socket) { 56 | $socket->send('test'); 57 | }); 58 | 59 | $this->loop->tick(); 60 | $this->loop->tick(); 61 | } 62 | 63 | /** test to make sure the loop ends once the last socket has been closed */ 64 | public function testClientCloseEndsLoop() 65 | { 66 | $this->factory->createClient('127.0.0.1:2')->then(function (Socket $socket) { 67 | $socket->close(); 68 | }); 69 | 70 | $this->loop->run(); 71 | } 72 | } 73 | -------------------------------------------------------------------------------- /tests/EventLoop/AbstractLoopTest.php: -------------------------------------------------------------------------------- 1 | loop = $this->createLoop(); 21 | 22 | $this->assertInstanceOf('React\EventLoop\LoopInterface', $this->loop); 23 | 24 | $this->factory = new Factory($this->loop); 25 | 26 | $this->rawFactory = new RawFactory(); 27 | } 28 | 29 | abstract function createLoop(); 30 | 31 | public function testClientTcp4() 32 | { 33 | $socket = $this->rawFactory->createClient('www.google.com:80'); 34 | 35 | $loop = $this->loop; 36 | $this->loop->addWriteStream($socket->getResource(), function($resource, $loop) use ($socket) { 37 | $loop->removeWriteStream($resource); 38 | 39 | $socket->write("GET / HTTP/1.1\r\nHost: www.google.com\r\n\r\n"); 40 | }); 41 | 42 | $this->loop->addReadStream($socket->getResource(), function($resource, $loop) use ($socket) { 43 | $loop->removeReadStream($resource); 44 | 45 | }); 46 | 47 | $this->loop->run(); 48 | } 49 | } 50 | -------------------------------------------------------------------------------- /tests/EventLoop/SelectPollerTest.php: -------------------------------------------------------------------------------- 1 | createCallableMock(); 10 | $mock 11 | ->expects($this->once()) 12 | ->method('__invoke'); 13 | 14 | return $mock; 15 | } 16 | 17 | protected function expectCallableNever() 18 | { 19 | $mock = $this->createCallableMock(); 20 | $mock 21 | ->expects($this->never()) 22 | ->method('__invoke'); 23 | 24 | return $mock; 25 | } 26 | 27 | protected function expectCallableOnceParameter($type) 28 | { 29 | $mock = $this->createCallableMock(); 30 | $mock 31 | ->expects($this->once()) 32 | ->method('__invoke') 33 | ->with($this->isInstanceOf($type)); 34 | 35 | return $mock; 36 | } 37 | 38 | /** 39 | * @link https://github.com/reactphp/react/blob/master/tests/React/Tests/Socket/TestCase.php (taken from reactphp/react) 40 | */ 41 | protected function createCallableMock() 42 | { 43 | return $this->getMock('CallableStub'); 44 | } 45 | } 46 | 47 | class CallableStub 48 | { 49 | public function __invoke() 50 | { 51 | } 52 | } 53 | 54 | --------------------------------------------------------------------------------