From 499eae1a2ddbe83abf1519b9a70ab7d217e18110 Mon Sep 17 00:00:00 2001 From: Aurimas Niekis Date: Wed, 25 May 2016 15:20:43 +0200 Subject: [PATCH] Initial Commit --- .editorconfig | 11 + .gitattributes | 10 + .gitignore | 4 + .scrutinizer.yml | 15 + .travis.yml | 21 ++ CONDUCT.md | 51 +++ CONTRIBUTING.md | 93 ++++++ LICENSE | 22 ++ README.md | 121 +++++++ composer.json | 34 ++ phpunit.xml.ci | 34 ++ phpunit.xml.dist | 27 ++ .../InvalidPackageReceivedException.php | 35 +++ src/Exception/ProviderNotFoundException.php | 28 ++ src/Package.php | 57 ++++ src/Packet.php | 117 +++++++ src/PacketHandler.php | 289 +++++++++++++++++ src/PacketInterface.php | 26 ++ src/PacketSubscriberInterface.php | 17 + src/StreamHandler.php | 84 +++++ src/Tests/PacketHandlerTest.php | 296 ++++++++++++++++++ src/Tests/PacketTest.php | 49 +++ src/Tests/StreamHandlerTest.php | 145 +++++++++ 23 files changed, 1586 insertions(+) create mode 100644 .editorconfig create mode 100644 .gitattributes create mode 100644 .gitignore create mode 100644 .scrutinizer.yml create mode 100644 .travis.yml create mode 100644 CONDUCT.md create mode 100644 CONTRIBUTING.md create mode 100644 LICENSE create mode 100644 README.md create mode 100644 composer.json create mode 100644 phpunit.xml.ci create mode 100644 phpunit.xml.dist create mode 100644 src/Exception/InvalidPackageReceivedException.php create mode 100644 src/Exception/ProviderNotFoundException.php create mode 100644 src/Package.php create mode 100644 src/Packet.php create mode 100644 src/PacketHandler.php create mode 100644 src/PacketInterface.php create mode 100644 src/PacketSubscriberInterface.php create mode 100644 src/StreamHandler.php create mode 100644 src/Tests/PacketHandlerTest.php create mode 100644 src/Tests/PacketTest.php create mode 100644 src/Tests/StreamHandlerTest.php diff --git a/.editorconfig b/.editorconfig new file mode 100644 index 0000000..d4b9e70 --- /dev/null +++ b/.editorconfig @@ -0,0 +1,11 @@ +; top-most EditorConfig file +root = true + +; Unix-style newlines +[*] +charset = utf-8 +end_of_line = lf +indent_size = 4 +indent_style = space +insert_final_newline = true +trim_trailing_whitespace = true diff --git a/.gitattributes b/.gitattributes new file mode 100644 index 0000000..09c43ea --- /dev/null +++ b/.gitattributes @@ -0,0 +1,10 @@ +src/Tests export-ignore +.editorconfig export-ignore +.gitattributes export-ignore +.gitignore export-ignore +.scrutinizer.yml export-ignore +.travis.yml export-ignore +CONTRIBUTING.md export-ignore +CONDUCT.md export-ignore +phpunit.xml.ci export-ignore +phpunit.xml.dist export-ignore diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..e45d856 --- /dev/null +++ b/.gitignore @@ -0,0 +1,4 @@ +build/ +vendor/ +composer.lock +phpunit.xml diff --git a/.scrutinizer.yml b/.scrutinizer.yml new file mode 100644 index 0000000..6eeb828 --- /dev/null +++ b/.scrutinizer.yml @@ -0,0 +1,15 @@ +filter: + paths: [src/*] + excluded_paths: [src/Tests/*] + +checks: + php: + code_rating: true + duplication: true + +tools: + external_code_coverage: + timeout: 600 + php_code_sniffer: + config: + standard: "PSR2" diff --git a/.travis.yml b/.travis.yml new file mode 100644 index 0000000..5e6b3b0 --- /dev/null +++ b/.travis.yml @@ -0,0 +1,21 @@ +language: php + +cache: + directories: + - $HOME/.composer/cache + +php: + - 7.0 + +before_install: + - travis_retry composer self-update + +install: + - travis_retry composer update --no-interaction + +script: + - composer test-ci + +after_success: + - wget https://scrutinizer-ci.com/ocular.phar + - php ocular.phar code-coverage:upload --format=php-clover build/logs/clover.xml diff --git a/CONDUCT.md b/CONDUCT.md new file mode 100644 index 0000000..cf33a53 --- /dev/null +++ b/CONDUCT.md @@ -0,0 +1,51 @@ +# Contributor Code of Conduct + +As contributors and maintainers of this project, and in the interest of +fostering an open and welcoming community, we pledge to respect all people who +contribute through reporting issues, posting feature requests, updating +documentation, submitting pull requests or patches, and other activities. + +We are committed to making participation in this project a harassment-free +experience for everyone, regardless of level of experience, gender, gender +identity and expression, sexual orientation, disability, personal appearance, +body size, race, ethnicity, age, religion, or nationality. + +Examples of unacceptable behavior by participants include: + +* The use of sexualized language or imagery +* Personal attacks +* Trolling or insulting/derogatory comments +* Public or private harassment +* Publishing other's private information, such as physical or electronic + addresses, without explicit permission +* Other unethical or unprofessional conduct + +Project maintainers have the right and responsibility to remove, edit, or +reject comments, commits, code, wiki edits, issues, and other contributions +that are not aligned to this Code of Conduct, or to ban temporarily or +permanently any contributor for other behaviors that they deem inappropriate, +threatening, offensive, or harmful. + +By adopting this Code of Conduct, project maintainers commit themselves to +fairly and consistently applying these principles to every aspect of managing +this project. Project maintainers who do not follow or enforce the Code of +Conduct may be permanently removed from the project team. + +This code of conduct applies both within project spaces and in public spaces +when an individual is representing the project or its community. + +Instances of abusive, harassing, or otherwise unacceptable behavior may be +reported by contacting a project maintainer at [Aurimas Niekis][email]. All +complaints will be reviewed and investigated and will result in a response that +is deemed necessary and appropriate to the circumstances. Maintainers are +obligated to maintain confidentiality with regard to the reporter of an +incident. + + +This Code of Conduct is adapted from the [Contributor Covenant][homepage], +version 1.3.0, available at +[http://contributor-covenant.org/version/1/3/0/][version] + +[homepage]: http://contributor-covenant.org +[version]: http://contributor-covenant.org/version/1/3/0/ +[email]: mailto:aurimas@niekis.lt diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md new file mode 100644 index 0000000..54e8da4 --- /dev/null +++ b/CONTRIBUTING.md @@ -0,0 +1,93 @@ +# Contributing + +If you're here, you would like to contribute to this repository and you're really welcome! + + +## Bug reports + +If you find a bug or a documentation issue, please report it or even better: fix it :). If you report it, +please be as precise as possible. Here is a little list of required information: + + - Precise description of the bug + - Details of your environment (for example: OS, PHP version, installed extensions) + - Backtrace which might help identifing the bug + + +## Feature requests + +If you think a feature is missing, please report it or even better: implement it :). If you report it, describe the more +precisely what you would like to see implemented and we will discuss what is the best approach for it. If you can do +some research before submitting it and link the resources to your description, you're awesome! It will allow us to more +easily understood/implement it. + + +## Sending a Pull Request + +If you're here, you are going to fix a bug or implement a feature and you're the best! +To do it, first fork the repository, clone it and create a new branch with the following commands: + +``` bash +$ git clone git@github.com:your-name/packet-handler.git +$ git checkout -b feature-or-bug-fix-description +``` + +Then install the dependencies through [Composer](https://getcomposer.org/): + +``` bash +$ composer install +``` + +Write code and tests. When you are ready, run the tests. +(This is usually [PHPUnit](http://phpunit.de/)) + +``` bash +$ composer test +``` + +When you are ready with the code, tested it and documented it, you can commit and push it with the following commands: + +``` bash +$ git commit -m "Feature or bug fix description" +$ git push origin feature-or-bug-fix-description +``` + +**Note:** Please write your commit messages in the imperative and follow the +[guidelines](http://tbaggery.com/2008/04/19/a-note-about-git-commit-messages.html) for clear and concise messages. + +Then [create a pull request](https://help.github.com/articles/creating-a-pull-request/) on GitHub. + +Please make sure that each individual commit in your pull request is meaningful. +If you had to make multiple intermediate commits while developing, +please squash them before submitting with the following commands +(here, we assume you would like to squash 3 commits in a single one): + +``` bash +$ git rebase -i HEAD~3 +``` + +If your branch conflicts with the master branch, you will need to rebase and repush it with the following commands: + +``` bash +$ git remote add upstream git@github.com:ThrusterIO/packet-handler.git +$ git pull --rebase upstream master +$ git push -f origin feature-or-bug-fix-description +``` + + +## Coding standard + +This repository follows the [PSR-2 standard](http://www.php-fig.org/psr/psr-2/) and so, if you want to contribute, +you must follow these rules. + + +## Semver + +We are trying to follow [semver](http://semver.org/). When you are making BC breaking changes, +please let us know why you think it is important. +In this case, your patch can only be included in the next major version. + + +## Code of Conduct + +This project is released with a [Contributor Code of Conduct](CONDUCT.md). +By participating in this project you agree to abide by its terms. diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..a925760 --- /dev/null +++ b/LICENSE @@ -0,0 +1,22 @@ +The MIT License (MIT) + +Copyright (c) 2016 Aurimas Niekis + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. + diff --git a/README.md b/README.md new file mode 100644 index 0000000..497bf85 --- /dev/null +++ b/README.md @@ -0,0 +1,121 @@ +# PacketHandler Component + +[![Latest Version](https://img.shields.io/github/release/ThrusterIO/packet-handler.svg?style=flat-square)] +(https://github.com/ThrusterIO/packet-handler/releases) +[![Software License](https://img.shields.io/badge/license-MIT-brightgreen.svg?style=flat-square)] +(LICENSE) +[![Build Status](https://img.shields.io/travis/ThrusterIO/packet-handler.svg?style=flat-square)] +(https://travis-ci.org/ThrusterIO/packet-handler) +[![Code Coverage](https://img.shields.io/scrutinizer/coverage/g/ThrusterIO/packet-handler.svg?style=flat-square)] +(https://scrutinizer-ci.com/g/ThrusterIO/packet-handler) +[![Quality Score](https://img.shields.io/scrutinizer/g/ThrusterIO/packet-handler.svg?style=flat-square)] +(https://scrutinizer-ci.com/g/ThrusterIO/packet-handler) +[![Total Downloads](https://img.shields.io/packagist/dt/thruster/packet-handler.svg?style=flat-square)] +(https://packagist.org/packets/thruster/packet-handler) + +[![Email](https://img.shields.io/badge/email-team@thruster.io-blue.svg?style=flat-square)] +(mailto:team@thruster.io) + +The Thruster PacketHandler Component. + + +## Install + +Via Composer + +```bash +$ composer require thruster/packet-handler +``` + +## Usage + +```php +use Thruster\Component\EventLoop\EventLoop; +use Thruster\Component\Socket\SocketPair; +use Thruster\Component\PacketHandler\Packet; +use Thruster\Component\PacketHandler\PacketHandler; +use Thruster\Component\PacketHandler\StreamHandler; + +class PingPacket extends Packet +{ + const NAME = 'ping'; + + public function __construct() + { + parent::__construct(self::NAME); + } +} + +class PongPacket extends Packet +{ + const NAME = 'pong'; + + private $pid; + + public function __construct() + { + $this->pid = posix_getpid(); + + parent::__construct(self::NAME); + } + + /** + * @return int + */ + public function getPid() + { + return $this->pid; + } +} + +$loop = new EventLoop(); + +$socketPair = new SocketPair($loop); +$socketPair->create(); + +$packetHandler = new PacketHandler(); +$packetHandler->addHandler(PingPacket::NAME, function (PingPacket $packet) { + $packet->getStreamHandler()->send(new PongPacket()); +}); + +$packetHandler->addHandler(PongPacket::NAME, function (PongPacket $packet) { + echo posix_getpid() . ': Received PONG from ' . $packet->getPid() . PHP_EOL; +}); + +if (pcntl_fork() > 0) { + $connection = $socketPair->useLeft(); + + $packetHandler->addProvider(new StreamHandler($connection)); + + $loop->addPeriodicTimer(2, function () use ($packetHandler) { + $packetHandler->dispatch(new PingPacket()); + }); + + $loop->run(); + +} else { + $loop->afterFork(); + + $connection = $socketPair->useRight(); + + $packetHandler->addProvider(new StreamHandler($connection)); + + $loop->run(); +} +``` + +## Testing + +```bash +$ composer test +``` + + +## Contributing + +Please see [CONTRIBUTING](CONTRIBUTING.md) and [CONDUCT](CONDUCT.md) for details. + + +## License + +Please see [License File](LICENSE) for more information. diff --git a/composer.json b/composer.json new file mode 100644 index 0000000..c51ade5 --- /dev/null +++ b/composer.json @@ -0,0 +1,34 @@ +{ + "name": "thruster/packet-handler", + "type": "library", + "description": "Thruster PacketHandler Component", + "keywords": ["packet-handler", "thruster"], + "homepage": "https://thruster.io", + "license": "MIT", + "authors": [ + { + "name": "Aurimas Niekis", + "email": "aurimas@niekis.lt" + } + ], + "require": { + "php": ">=7.0", + "thruster/stream": "^1.2", + "thruster/event-emitter": "^1.1" + }, + "require-dev": { + "phpunit/phpunit": "~5.1" + }, + "autoload": { + "psr-4": { "Thruster\\Component\\PacketHandler\\": "src" } + }, + "scripts": { + "test": "vendor/bin/phpunit", + "test-ci": "vendor/bin/phpunit -c phpunit.xml.ci" + }, + "extra": { + "branch-alias": { + "dev-master": "1.0-dev" + } + } +} diff --git a/phpunit.xml.ci b/phpunit.xml.ci new file mode 100644 index 0000000..05e3287 --- /dev/null +++ b/phpunit.xml.ci @@ -0,0 +1,34 @@ + + + + + + + + + + ./src/Tests/ + + + + + + ./src + + ./src/Tests + + + + + + + + + + + diff --git a/phpunit.xml.dist b/phpunit.xml.dist new file mode 100644 index 0000000..93c23fc --- /dev/null +++ b/phpunit.xml.dist @@ -0,0 +1,27 @@ + + + + + + + + + + ./src/Tests/ + + + + + + ./src + + ./src/Tests + + + + diff --git a/src/Exception/InvalidPackageReceivedException.php b/src/Exception/InvalidPackageReceivedException.php new file mode 100644 index 0000000..001744e --- /dev/null +++ b/src/Exception/InvalidPackageReceivedException.php @@ -0,0 +1,35 @@ + + */ +class InvalidPackageReceivedException extends \Exception +{ + /** + * @inheritDoc + */ + public function __construct($package) + { + if (is_object($package)) { + $message = sprintf( + 'Received object of class "%s" instead of Package', + get_class($package) + ); + } else { + $message = sprintf( + 'Received "%s" instead of object of Package', + gettype($package) + ); + } + + parent::__construct($message); + } + +} diff --git a/src/Exception/ProviderNotFoundException.php b/src/Exception/ProviderNotFoundException.php new file mode 100644 index 0000000..4df20a5 --- /dev/null +++ b/src/Exception/ProviderNotFoundException.php @@ -0,0 +1,28 @@ + + */ +class ProviderNotFoundException extends Exception +{ + /** + * @inheritDoc + */ + public function __construct($name) + { + $message = sprintf( + 'Provider "%s" not found', + $name + ); + + parent::__construct($message); + } + +} diff --git a/src/Package.php b/src/Package.php new file mode 100644 index 0000000..25d2733 --- /dev/null +++ b/src/Package.php @@ -0,0 +1,57 @@ + + */ +class Package +{ + /** + * @var array + */ + private $data; + + /** + * @var Stream + */ + private $stream; + + public function __construct(array $data) + { + $this->data = $data; + } + + /** + * @return array + */ + public function getData() : array + { + return $this->data; + } + + /** + * @return Stream + */ + public function getStream() : Stream + { + return $this->stream; + } + + /** + * @param Stream $stream + * + * @return $this + */ + public function setStream(Stream $stream) + { + $this->stream = $stream; + + return $this; + } +} diff --git a/src/Packet.php b/src/Packet.php new file mode 100644 index 0000000..8f61c22 --- /dev/null +++ b/src/Packet.php @@ -0,0 +1,117 @@ + + */ +class Packet implements PacketInterface +{ + /** + * @var string + */ + private $name; + + /** + * @var array + */ + protected $data; + + /** + * @var Stream + */ + protected $stream; + + /** + * @var StreamHandler + */ + protected $streamHandler; + + /** + * @var bool + */ + protected $propagationStopped; + + public function __construct(string $name, array $data = []) + { + $this->name = $name; + $this->data = $data; + $this->propagationStopped = false; + } + + /** + * @return string + */ + public function getName() : string + { + return $this->name; + } + + /** + * @return array + */ + public function getData() : array + { + return $this->data; + } + + /** + * @return Stream + */ + public function getStream() : Stream + { + return $this->stream; + } + + /** + * @param Stream $stream + * + * @return $this + */ + public function setStream(Stream $stream) + { + $this->stream = $stream; + + return $this; + } + + /** + * @return StreamHandler + */ + public function getStreamHandler() : StreamHandler + { + return $this->streamHandler; + } + + /** + * @param StreamHandler $streamHandler + * + * @return $this + */ + public function setStreamHandler(StreamHandler $streamHandler) + { + $this->streamHandler = $streamHandler; + + return $this; + } + + public function isPropagationStopped() : bool + { + return $this->propagationStopped; + } + + /** + * @return $this + */ + public function stopPropagation() : self + { + $this->propagationStopped = true; + + return $this; + } +} diff --git a/src/PacketHandler.php b/src/PacketHandler.php new file mode 100644 index 0000000..3631b7e --- /dev/null +++ b/src/PacketHandler.php @@ -0,0 +1,289 @@ + + */ +class PacketHandler +{ + /** + * @var StreamHandler[] + */ + private $providers; + + /** + * @var array + */ + private $handlers; + + /** + * @var array + */ + private $sortedHandlers; + + public function __construct() + { + $this->providers = []; + $this->handlers = []; + $this->sortedHandlers = []; + } + + /** + * @param StreamHandler $streamHandler + * @param string $identifier + * + * @return PacketHandler + */ + public function addProvider(StreamHandler $streamHandler, $identifier = null) : self + { + if (null === $identifier) { + $this->providers[] = $streamHandler; + } else { + $this->providers[$identifier] = $streamHandler; + } + + $streamHandler->on('package', function ($package) use ($streamHandler) { + $this->onPackage($package, $streamHandler); + }); + + return $this; + } + + public function removeProvider(StreamHandler $streamHandler = null, $identifier = null) + { + if (null === $streamHandler && null === $identifier) { + return; + } + + if (null === $identifier) { + if (false !== ($key = array_search($streamHandler, $this->providers, true))) { + unset($this->providers[$key]); + } + } else { + if (false !== isset($this->providers[$identifier])) { + unset($this->providers[$identifier]); + } + } + } + + public function hasProvider($identifier) : bool + { + return isset($this->providers[$identifier]); + } + + public function getProvider($identifier) : StreamHandler + { + if (false === $this->hasProvider($identifier)) { + throw new ProviderNotFoundException($identifier); + } + + return $this->providers[$identifier]; + } + + /** + * @return StreamHandler[] + */ + public function getProviders() : array + { + return $this->providers; + } + + /** + * @param PacketInterface $packet + * + * @return $this + */ + public function dispatch(PacketInterface $packet) + { + foreach ($this->providers as $provider) { + $provider->send($packet); + } + + return $this; + } + + /** + * @param Package $package + */ + public function onPackage(Package $package, StreamHandler $streamHandler) + { + /** @var PacketInterface $packet */ + $data = $package->getData(); + $packet = array_shift($data); + + if (false === ($packet instanceof PacketInterface)) { + return; + } + + $packet->setStream($package->getStream()); + $packet->setStreamHandler($streamHandler); + + $packetType = $packet->getName(); + + if (false === isset($this->handlers[$packetType])) { + return; + } + + $this->doDispatch($this->getHandlers($packetType), $packet); + } + + /** + * @param string $packetType + * @param bool $withPriorities + * + * @return array + */ + public function getHandlers(string $packetType = null, bool $withPriorities = false) : array + { + if (true === $withPriorities) { + return $packetType ? $this->handlers[$packetType] : array_filter($this->handlers); + } + + if (null !== $packetType) { + if (!isset($this->sortedHandlers[$packetType])) { + $this->sortHandlers($packetType); + } + + return $this->sortedHandlers[$packetType]; + } + + foreach ($this->handlers as $packetType => $packetHandlers) { + if (false === isset($this->sortedHandlers[$packetType])) { + $this->sortHandlers($packetType); + } + } + + return array_filter($this->sortedHandlers); + } + + /** + * @param string $packetType + * + * @return bool + */ + public function hasHandlers(string $packetType = null) : bool + { + return (bool) count($this->getHandlers($packetType)); + } + + /** + * @param string $packetType + * @param callable $handler + * @param int $priority + * + * @return $this + */ + public function addHandler(string $packetType, callable $handler, int $priority = 0) : self + { + $this->handlers[$packetType][$priority][] = $handler; + + unset($this->sortedHandlers[$packetType]); + + return $this; + } + + /** + * @param string $packetType + * @param callable $handler + * + * @return $this + */ + public function removeHandler(string $packetType, callable $handler) : self + { + if (false === isset($this->handlers[$packetType])) { + return $this; + } + + foreach ($this->handlers[$packetType] as $priority => $handlers) { + if (false !== ($key = array_search($handler, $handlers, true))) { + unset($this->handlers[$packetType][$priority][$key], $this->sortedHandlers[$packetType]); + } + + if (count($this->handlers[$packetType][$priority]) < 1) { + unset($this->handlers[$packetType][$priority]); + } + } + + if (count($this->handlers[$packetType]) < 1) { + unset($this->handlers[$packetType]); + } + + return $this; + } + + /** + * @param PacketSubscriberInterface $subscriber + * + * @return $this + */ + public function addSubscriber(PacketSubscriberInterface $subscriber) : self + { + foreach ($subscriber->getSubscribedPackets() as $packetType => $params) { + if (is_string($params)) { + $this->addHandler($packetType, [$subscriber, $params]); + } elseif (is_string($params[0])) { + $this->addHandler($packetType, [$subscriber, $params[0]], $params[1] ?? 0); + } else { + foreach ($params as $handler) { + $this->addHandler($packetType, [$subscriber, $handler[0]], $handler[1] ?? 0); + } + } + } + + return $this; + } + + + /** + * @param PacketSubscriberInterface $subscriber + * + * @return $this + */ + public function removeSubscriber(PacketSubscriberInterface $subscriber) + { + foreach ($subscriber->getSubscribedPackets() as $packetType => $params) { + if (is_array($params) && is_array($params[0])) { + foreach ($params as $handler) { + $this->removeHandler($packetType, [$subscriber, $handler[0]]); + } + } else { + $this->removeHandler($packetType, [$subscriber, is_string($params) ? $params : $params[0]]); + } + } + return $this; + } + + /** + * @param array $handlers + * @param PacketInterface $packet + */ + private function doDispatch($handlers, PacketInterface $packet) + { + foreach ($handlers as $handler) { + call_user_func($handler, $packet, $this); + + if ($packet->isPropagationStopped()) { + break; + } + } + } + + /** + * @param string $packetType + */ + private function sortHandlers(string $packetType) + { + $this->sortedHandlers[$packetType] = []; + + if (isset($this->handlers[$packetType])) { + krsort($this->handlers[$packetType]); + $this->sortedHandlers[$packetType] = call_user_func_array('array_merge', $this->handlers[$packetType]); + } + } +} diff --git a/src/PacketInterface.php b/src/PacketInterface.php new file mode 100644 index 0000000..b1288b8 --- /dev/null +++ b/src/PacketInterface.php @@ -0,0 +1,26 @@ + + */ +interface PacketInterface +{ + public function getName() : string; + + public function setStream(Stream $stream); + + public function getStream() : Stream; + + public function setStreamHandler(StreamHandler $stream); + + public function getStreamHandler() : StreamHandler; + + public function isPropagationStopped() : bool; +} diff --git a/src/PacketSubscriberInterface.php b/src/PacketSubscriberInterface.php new file mode 100644 index 0000000..bba4c7e --- /dev/null +++ b/src/PacketSubscriberInterface.php @@ -0,0 +1,17 @@ + + */ +interface PacketSubscriberInterface +{ + /** + * @return array + */ + public static function getSubscribedPackets() : array; +} diff --git a/src/StreamHandler.php b/src/StreamHandler.php new file mode 100644 index 0000000..cd359ae --- /dev/null +++ b/src/StreamHandler.php @@ -0,0 +1,84 @@ + + */ +class StreamHandler implements EventEmitterInterface +{ + use EventEmitterTrait; + + /** + * @var string + */ + private $separator; + + /** + * @var Stream + */ + private $stream; + + /** + * @var string + */ + private $buffer; + + public function __construct(Stream $stream, string $separator = "\r\n\r\n") + { + $this->buffer = ''; + $this->stream = $stream; + $this->separator = $separator; + + $this->stream->on('data', [$this, 'onData']); + } + + public function send($data) + { + $package = new Package(func_get_args()); + + $packageString = base64_encode(serialize($package)) . $this->separator; + + return $this->stream->write($packageString); + } + + public function onData($data) + { + $this->buffer .= $data; + + if (preg_match('/' . $this->separator . '/', $this->buffer)) { + $messages = explode($this->separator, $this->buffer); + + $this->buffer = array_pop($messages); + + foreach ($messages as $message) { + /** @var Package $package */ + $package = unserialize(base64_decode($message)); + + if ($package instanceof Package) { + $package->setStream($this->stream); + + $this->emit('package', [$package]); + } else { + throw new InvalidPackageReceivedException($package); + } + } + } + } + + /** + * @return Stream + */ + public function getStream() + { + return $this->stream; + } +} diff --git a/src/Tests/PacketHandlerTest.php b/src/Tests/PacketHandlerTest.php new file mode 100644 index 0000000..e59a4ad --- /dev/null +++ b/src/Tests/PacketHandlerTest.php @@ -0,0 +1,296 @@ + + */ +class PacketHandlerTest extends \PHPUnit_Framework_TestCase +{ + /** + * @var PacketHandler + */ + protected $packetHandler; + + public function setUp() + { + $this->packetHandler = new PacketHandler(); + } + + + + public function testAddProvider() + { + $mock = $this->getMockBuilder('\Thruster\Component\PacketHandler\StreamHandler') + ->disableOriginalConstructor() + ->getMock(); + + $this->packetHandler->addProvider($mock); + $this->assertEquals($mock, $this->packetHandler->getProvider(0)); + + $this->assertCount(1, $this->packetHandler->getProviders()); + } + + public function testAddProviderIdentifier() + { + $mock = $this->getMockBuilder('\Thruster\Component\PacketHandler\StreamHandler') + ->disableOriginalConstructor() + ->getMock(); + + $this->packetHandler->addProvider($mock, 'a'); + $this->assertEquals($mock, $this->packetHandler->getProvider('a')); + + $this->assertCount(1, $this->packetHandler->getProviders()); + $this->assertArrayHasKey('a', $this->packetHandler->getProviders()); + } + + public function testRemoveProvider() + { + $mock = $this->getMockBuilder('\Thruster\Component\PacketHandler\StreamHandler') + ->disableOriginalConstructor() + ->getMock(); + + $this->packetHandler->addProvider($mock); + + $this->assertCount(1, $this->packetHandler->getProviders()); + + $this->packetHandler->removeProvider($mock); + + $this->assertCount(0, $this->packetHandler->getProviders()); + } + + public function testRemoveProviderWithoutArgs() + { + $mock = $this->getMockBuilder('\Thruster\Component\PacketHandler\StreamHandler') + ->disableOriginalConstructor() + ->getMock(); + + $this->packetHandler->addProvider($mock); + + $this->assertCount(1, $this->packetHandler->getProviders()); + + $this->packetHandler->removeProvider(); + + $this->assertCount(1, $this->packetHandler->getProviders()); + } + + /** + * @expectedException \Thruster\Component\PacketHandler\Exception\ProviderNotFoundException + * @expectedExceptionMessage Provider "foobar" not found + */ + public function testGetProviderNotFound() + { + $this->packetHandler->getProvider('foobar'); + } + + public function testRemoveProviderIdentifier() + { + $mock = $this->getMockBuilder('\Thruster\Component\PacketHandler\StreamHandler') + ->disableOriginalConstructor() + ->getMock(); + + $this->packetHandler->addProvider($mock, 'a'); + $this->assertEquals($mock, $this->packetHandler->getProvider('a')); + + $this->assertCount(1, $this->packetHandler->getProviders()); + $this->assertArrayHasKey('a', $this->packetHandler->getProviders()); + + + $this->packetHandler->removeProvider(null, 'a'); + + $this->assertCount(0, $this->packetHandler->getProviders()); + $this->assertArrayNotHasKey('a', $this->packetHandler->getProviders()); + } + + public function testReceivedPackageNotForPackageHandler() + { + $packet = new Package([]); + + $this->packetHandler->onPackage( + $packet, + new StreamHandler( + $this->getMockBuilder('\Thruster\Component\Stream\Stream') + ->disableOriginalConstructor() + ->getMock() + ) + ); + } + + public function testReceivedPackageWithoutHandler() + { + $streamMock = $this->getMockBuilder('\Thruster\Component\Stream\Stream') + ->disableOriginalConstructor() + ->getMock(); + + $packet = $this->getMockBuilder('\Thruster\Component\PacketHandler\Packet') + ->setConstructorArgs(['foo_bar', ['foo' => 'bar']]) + ->setMethods(['setStream']) + ->getMock(); + + $packet->expects($this->once()) + ->method('setStream') + ->with($streamMock); + + $package = new Package([$packet]); + $package->setStream($streamMock); + + $this->packetHandler->onPackage($package, new StreamHandler($streamMock)); + } + + public function testReceivedPackage() + { + $streamMock = $this->getMockBuilder('\Thruster\Component\Stream\Stream') + ->disableOriginalConstructor() + ->getMock(); + + $packet = $this->getMockBuilder('\Thruster\Component\PacketHandler\Packet') + ->setConstructorArgs(['foo_bar', ['foo' => 'bar']]) + ->setMethods(['setStream']) + ->getMock(); + + $packet->expects($this->once()) + ->method('setStream') + ->with($streamMock); + + $package = new Package([$packet]); + $package->setStream($streamMock); + + $this->packetHandler->addHandler($packet->getName(), $this->expectCallableExactly(1)); + + $this->packetHandler->onPackage($package, new StreamHandler($streamMock)); + } + + public function testAddHasRemoveHandler() + { + $mock = $this->getMockBuilder(__CLASS__) + ->getMock(); + + $callback = $this->getCallableMock(); + + $this->assertFalse($this->packetHandler->hasHandlers()); + $this->packetHandler->addHandler('foo', $callback); + $this->assertTrue($this->packetHandler->hasHandlers()); + + $this->assertEquals( + [ + 'foo' => [ + 0 => [ + $callback, + ], + ], + ], + $this->packetHandler->getHandlers(null, true) + ); + + $this->packetHandler->removeHandler('foo', $callback); + $this->packetHandler->removeHandler('foo', $callback); + $this->assertFalse($this->packetHandler->hasHandlers()); + } + + public function testAddAndRemoveSubscribers() + { + $subscriber = $this->getSubscriber(); + $this->packetHandler->addSubscriber($subscriber); + + $this->assertEquals( + [ + 'foo' => [0 => [[$subscriber, 'foo']]], + 'rab' => [0 => [[$subscriber, 'rab']]], + 'bar' => [ + 0 => [[$subscriber, 'bar']], + 10 => [[$subscriber, 'rab']] + ] + ], + $this->packetHandler->getHandlers(null, true) + ); + + $this->assertTrue($this->packetHandler->hasHandlers()); + $this->packetHandler->removeSubscriber($subscriber); + $this->assertFalse($this->packetHandler->hasHandlers()); + } + + public function testDispatch() + { + $streamHandler = $this->getMockBuilder('\Thruster\Component\PacketHandler\StreamHandler') + ->disableOriginalConstructor() + ->setMethods(['send']) + ->getMock(); + + $packet = new Packet('foo_bar', ['foo' => 'bar']); + + $streamHandler->expects($this->once()) + ->method('send') + ->with($packet); + + $this->packetHandler->addProvider($streamHandler); + + $this->packetHandler->dispatch($packet); + } + + public function getSubscriber() + { + return new class implements PacketSubscriberInterface { + /** + * @inheritDoc + */ + public static function getSubscribedPackets() : array + { + return [ + 'foo' => 'foo', + 'rab' => ['rab'], + 'bar' => [ + ['bar'], + ['rab', 10] + ] + ]; + } + + public function foo() + { + } + + public function bar() + { + } + + public function rab() + { + } + }; + } + + private function expectCallableExactly($amount) + { + $mock = $this->createCallableMock(); + + $mock->expects($this->exactly($amount)) + ->method('someMethod'); + + return [$mock, 'someMethod']; + } + + private function createCallableMock() + { + return $this->getMock(__CLASS__); + } + + private function getCallableMock() + { + $mock = $this->createCallableMock(); + + return [$mock, 'someMethod']; + } + + public function someMethod() + { + } +} diff --git a/src/Tests/PacketTest.php b/src/Tests/PacketTest.php new file mode 100644 index 0000000..7818b08 --- /dev/null +++ b/src/Tests/PacketTest.php @@ -0,0 +1,49 @@ + + */ +class PacketTest extends \PHPUnit_Framework_TestCase +{ + /** + * @var Packet + */ + protected $packet; + + public function setUp() + { + $this->packet = new Packet('foobar', ['foo' => 'bar']); + } + + public function testSimple() + { + $this->assertSame('foobar', $this->packet->getName()); + + $this->assertEquals(['foo' => 'bar'], $this->packet->getData()); + + $stream = $this->getMockBuilder('Thruster\Component\Stream\Stream') + ->disableOriginalConstructor() + ->getMock(); + + $this->packet->setStream($stream); + + $streamHandler = new StreamHandler($stream); + + $this->packet->setStreamHandler($streamHandler); + + $this->assertEquals($streamHandler, $this->packet->getStreamHandler()); + + $this->assertEquals($stream, $this->packet->getStream()); + $this->assertFalse($this->packet->isPropagationStopped()); + $this->packet->stopPropagation(); + $this->assertTrue($this->packet->isPropagationStopped()); + } +} diff --git a/src/Tests/StreamHandlerTest.php b/src/Tests/StreamHandlerTest.php new file mode 100644 index 0000000..0f0e277 --- /dev/null +++ b/src/Tests/StreamHandlerTest.php @@ -0,0 +1,145 @@ + + */ +class StreamHandlerTest extends \PHPUnit_Framework_TestCase +{ + public function testCreation() + { + $streamMock = $this->getMockBuilder('\Thruster\Component\Stream\Stream') + ->disableOriginalConstructor() + ->getMock(); + + $streamHandler = new StreamHandler($streamMock); + + $this->assertEquals($streamMock, $streamHandler->getStream()); + } + + public function testSendMethod() + { + $streamMock = $this->getMockBuilder('\Thruster\Component\Stream\Stream') + ->disableOriginalConstructor() + ->setMethods(['write']) + ->getMock(); + + $streamHandler = new StreamHandler($streamMock, '$$$$$'); + + $data = new \stdClass(); + $data->foo = 'bar'; + + $streamMock->expects($this->once()) + ->method('write') + ->will( + $this->returnCallback( + function ($input) use ($data) { + $expected = base64_encode(serialize(new Package([$data]))) . "$$$$$"; + + $this->assertSame($expected, $input); + + $input = unserialize(base64_decode($input)); + + $this->assertInstanceOf('\Thruster\Component\PacketHandler\Package', $input); + + $this->assertEquals([$data], $input->getData()); + + return true; + } + ) + ); + + $this->assertTrue($streamHandler->send($data)); + } + + public function testReceive() + { + $streamMock = $this->getMockBuilder('\Thruster\Component\Stream\Stream') + ->disableOriginalConstructor() + ->setMethods(null) + ->getMock(); + + $packageHandler = new StreamHandler($streamMock); + $packageHandler->on('package', $this->expectCallableExactly(3)); + + $expectedMessages = ['foo', 'bar', 'kitty']; + + foreach ($expectedMessages as $expectedMessage) { + $package = new Package([$expectedMessage]); + $packageString = base64_encode(serialize($package)) . "\r\n\r\n"; + + for ($i = 0; $i < strlen($packageString);) { + $partSize = mt_rand(1, strlen($packageString) - $i); + $partPackage = substr($packageString, $i, $partSize); + + $i += $partSize; + + $streamMock->emit('data', [$partPackage]); + } + } + } + + /** + * @expectedException \Thruster\Component\PacketHandler\Exception\InvalidPackageReceivedException + * @expectedExceptionMessage Received object of class "stdClass" instead of Package + */ + public function testReceiveInvalid() + { + $streamMock = $this->getMockBuilder('\Thruster\Component\Stream\Stream') + ->disableOriginalConstructor() + ->setMethods(null) + ->getMock(); + + $packageHandler = new StreamHandler($streamMock); + + $package = new \stdClass(); + $packageString = base64_encode(serialize($package)) . "\r\n\r\n"; + + $streamMock->emit('data', [$packageString]); + } + + /** + * @expectedException \Thruster\Component\PacketHandler\Exception\InvalidPackageReceivedException + * @expectedExceptionMessage Received "string" instead of object of Package + */ + public function testReceiveInvalidType() + { + $streamMock = $this->getMockBuilder('\Thruster\Component\Stream\Stream') + ->disableOriginalConstructor() + ->setMethods(null) + ->getMock(); + + $packageHandler = new StreamHandler($streamMock); + + $package = 'asdasdas'; + $packageString = base64_encode(serialize($package)) . "\r\n\r\n"; + + $streamMock->emit('data', [$packageString]); + } + + private function expectCallableExactly($amount) + { + $mock = $this->createCallableMock(); + + $mock->expects($this->exactly($amount)) + ->method('someMethod'); + + return [$mock, 'someMethod']; + } + + private function createCallableMock() + { + return $this->getMock(__CLASS__); + } + + public function someMethod() + { + } +}