├── .gitignore ├── .travis.yml ├── LICENSE ├── README.md ├── composer.json ├── phpunit.xml ├── pure ├── src ├── Client.php ├── Command │ ├── CommandInterface.php │ ├── DeleteCommand.php │ ├── PingCommand.php │ └── StorageCommand.php ├── Console │ ├── ClientCommand.php │ └── StartCommand.php ├── Helper │ ├── All.php │ └── Filter.php ├── Proxy.php ├── Proxy │ └── Generator.php ├── Server.php └── Storage │ ├── ArrayStorage.php │ ├── PriorityQueueStorage.php │ ├── QueueStorage.php │ ├── StackStorage.php │ └── StorageInterface.php └── test ├── ClientServerTest.php ├── Storage ├── QueueStorageTest.php └── StackStorageTest.php └── bootstrap.php /.gitignore: -------------------------------------------------------------------------------- 1 | vendor/ 2 | .idea/ 3 | composer.lock 4 | pure.phar 5 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: php 2 | 3 | php: 4 | - 5.4 5 | - 5.5 6 | - 5.6 7 | - hhvm 8 | - hhvm-nightly 9 | 10 | matrix: 11 | allow_failures: 12 | - php: hhvm 13 | - php: hhvm-nightly 14 | 15 | install: 16 | - composer --dev --prefer-source --no-interaction install 17 | 18 | script: 19 | - phpunit --coverage-text 20 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2014 Medvedev Anton 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in 13 | all 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. -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # PurePHP Key-Value Storage 2 | [![Build Status](https://travis-ci.org/antonmedv/purephp.svg?branch=master)](https://travis-ci.org/antonmedv/purephp) 3 | 4 | This is simple key-value storage written on PHP. It does not use files, or other database, just pure PHP. 5 | 6 | ## Installation 7 | Via Composer: 8 | 9 | ``` 10 | composer require elfet/pure 11 | ``` 12 | 13 | Now you can run pure like this: `php vendor/bin/pure` 14 | 15 | Or you can install PurePHP globally to run pure by `pure` command. 16 | 17 | ## Quick Guide 18 | Start PurePHP by this command: 19 | 20 | ``` 21 | pure start & 22 | ``` 23 | 24 | Now PurePHP server is running. Run this command: 25 | 26 | ``` 27 | pure client 28 | ``` 29 | 30 | Now you can test PurePHP by simple commands like this: 31 | 32 | ``` 33 | > pure.queue.collection.push('hello') 34 | > pure.queue.collection.push('world') 35 | > pure.queue.collection.pop() ~ ' ' ~ pure.queue.collection.pop() 36 | string(11) "hello world" 37 | ``` 38 | 39 | In pure console you can write commands on [Expression Language](https://github.com/symfony/expression-language). To exit from console type `exit` command. 40 | 41 | ## Documentation 42 | 43 | ### Connection to PurePHP server 44 | ```php 45 | $port = 1337; // Default port value 46 | $host = '127.0.0.1'; // Default host value 47 | //... 48 | $pure = new Pure\Client($port, $host); 49 | ``` 50 | 51 | ### Storages 52 | 53 | PurePHP provide different types on storages. All supported storages are in [src/Storage](https://github.com/elfet/purephp/tree/master/src/Storage). You can access them by next methods and work with them like you work with them directly. 54 | 55 | You do not need to manually create any collection. They will be automatically create at first access. 56 | 57 | ```php 58 | $pure->map('collection')->... 59 | $pure->stack('collection')->... 60 | $pure->queue('collection')->... 61 | $pure->priority('collection')->... 62 | ``` 63 | 64 | Or you can access them by magic methods. 65 | 66 | ```php 67 | $pure->map->collection->... 68 | $pure->stack->collection->... 69 | $pure->queue->collection->... 70 | //... 71 | ``` 72 | 73 | ### Array Storage `->map` 74 | 75 | This is simple storage what uses php array to store your data. 76 | 77 | To store date in collection use `push` method: 78 | ```php 79 | $pure->map('collection')->push(['hello' => 'world']); 80 | ``` 81 | 82 | To get value by key from collection use `get` method: 83 | ```php 84 | $value = $pure->map('collection')->get('hello'); // will return 'world'. 85 | ``` 86 | 87 | To receive all elements use `all` method: 88 | ```php 89 | $all = $pure->map('collection')->all(); 90 | ``` 91 | 92 | You can check if key exist by `has` method, and delete element by `delete` method. 93 | 94 | ### Stack Storage `->stack` 95 | 96 | This storage use `SplStack` to store your data. 97 | 98 | You can use all `SplStack` methods and also `all` method. 99 | 100 | ### Queue Storage `->queue` 101 | 102 | This storage use `SplQueue` to store your data. 103 | 104 | You can use `SplQueue` methods and also `all` method. 105 | 106 | `SplQueue` uses `enqueue` and `deenqueue` to push and pop from queue. In QueueStorage your can use `push` and `pop` methods to do this. 107 | 108 | ### Priority Queue Storage `->priority` 109 | 110 | This storage use `SplPriorityQueue` to store your data. 111 | 112 | You can use all `SplPriorityQueue` methods and also `all` method. 113 | 114 | ### Filtering 115 | 116 | Every storage support function `filter`. 117 | 118 | Example: 119 | 120 | ```php 121 | // Get all elements that more than a 100. 122 | $result = $pure->queue('collection')->filter('value > 100'); 123 | 124 | // Limit to 10. 125 | $result = $pure->priority('collection')->filter('value > 100', 10); 126 | 127 | // Complex expression. 128 | $result = $pure->of('collection')->filter('value["year"] > 2000 and value["name"] matches "/term/"'); 129 | ``` 130 | 131 | Filter rules uses [Expression Language](https://github.com/symfony/expression-language). 132 | In expression available two variables: `key` and `value`. 133 | 134 | ### Deleting 135 | 136 | You can delete storages by `delete` method: 137 | 138 | ``` 139 | $pure->delete('collection'); 140 | ``` 141 | 142 | ## TODO 143 | 144 | * Dump to file 145 | * Load from file 146 | * Replication 147 | * Sharding 148 | -------------------------------------------------------------------------------- /composer.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "elfet/pure", 3 | "description": "Pure PHP key-value storage", 4 | "authors": [ 5 | { 6 | "name": "Anton Medvedev", 7 | "email": "anton@elfet.ru" 8 | } 9 | ], 10 | "autoload": { 11 | "psr-4": { 12 | "Pure\\": "src/" 13 | } 14 | }, 15 | "require": { 16 | "react/react": "~0.4", 17 | "symfony/console": "~2.6 || ~3.0 || ~4.0", 18 | "symfony/expression-language": "~2.6 || ~3.0 || ~4.0", 19 | "symfony/debug": "~2.6 || ~3.0 || ~4.0" 20 | }, 21 | "require-dev": { 22 | "symfony/finder": "~2.6 || ~3.0 || ~4.0", 23 | "phpunit/phpunit": "~4.4", 24 | "symfony/process": "~2.6 || ~3.0 || ~4.0" 25 | }, 26 | "bin": ["pure"], 27 | "license": "MIT", 28 | "minimum-stability": "dev" 29 | } 30 | -------------------------------------------------------------------------------- /phpunit.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | test/ 5 | 6 | 7 | 8 | 9 | ./vendor 10 | 11 | 12 | 13 | -------------------------------------------------------------------------------- /pure: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env php 2 | add(new Pure\Console\StartCommand()); 22 | $console->add(new Pure\Console\ClientCommand()); 23 | $console->run(); 24 | -------------------------------------------------------------------------------- /src/Client.php: -------------------------------------------------------------------------------- 1 | 3 | * 4 | * For the full copyright and license information, please view the LICENSE 5 | * file that was distributed with this source code. 6 | */ 7 | 8 | namespace Pure; 9 | 10 | class Client 11 | { 12 | const END_OF_COMMAND = 'END_OF_COMMAND'; 13 | 14 | const READ_SIZE = 4096; 15 | 16 | /** 17 | * @var int 18 | */ 19 | private $port; 20 | 21 | /** 22 | * @var string 23 | */ 24 | private $host; 25 | 26 | /** 27 | * @var resource 28 | */ 29 | private $socket; 30 | 31 | /** 32 | * @param int $port 33 | * @param string $host 34 | * @throws \RuntimeException 35 | */ 36 | public function __construct($port, $host = '127.0.0.1') 37 | { 38 | $this->host = $host; 39 | $this->port = $port; 40 | 41 | $this->socket = socket_create(AF_INET, SOCK_STREAM, SOL_TCP); 42 | 43 | if ($this->socket === false) { 44 | throw new \RuntimeException(socket_strerror(socket_last_error())); 45 | } 46 | 47 | $result = socket_connect($this->socket, $this->host, $this->port); 48 | if ($result === false) { 49 | throw new \RuntimeException(socket_strerror(socket_last_error($this->socket))); 50 | } 51 | } 52 | 53 | /** 54 | * @param string $name 55 | * @return \Pure\Storage\ArrayStorage 56 | */ 57 | public function map($name) 58 | { 59 | return new Proxy($this, 'Pure\Storage\ArrayStorage', $name); 60 | } 61 | 62 | /** 63 | * @param string $name 64 | * @return \Pure\Storage\PriorityQueueStorage 65 | */ 66 | public function priority($name) 67 | { 68 | return new Proxy($this, 'Pure\Storage\PriorityQueueStorage', $name); 69 | } 70 | 71 | /** 72 | * @param string $name 73 | * @return \Pure\Storage\QueueStorage 74 | */ 75 | public function queue($name) 76 | { 77 | return new Proxy($this, 'Pure\Storage\QueueStorage', $name); 78 | } 79 | 80 | /** 81 | * @param string $name 82 | * @return \Pure\Storage\StackStorage 83 | */ 84 | public function stack($name) 85 | { 86 | return new Proxy($this, 'Pure\Storage\StackStorage', $name); 87 | } 88 | 89 | /** 90 | * @param string $alias 91 | * @return Proxy\Generator 92 | * @throws \RuntimeException 93 | */ 94 | public function __get($alias) 95 | { 96 | if (in_array($alias, ['map', 'priority', 'queue', 'stack'], true)) { 97 | return new Proxy\Generator($this, $alias); 98 | } else { 99 | throw new \RuntimeException("There are no method `$alias` in client class."); 100 | } 101 | } 102 | 103 | /** 104 | * @param array $command 105 | * @return mixed 106 | * @throws \RuntimeException 107 | */ 108 | public function command($command) 109 | { 110 | $body = json_encode($command) . self::END_OF_COMMAND; 111 | @socket_write($this->socket, $body, strlen($body)); 112 | 113 | $command = null; 114 | 115 | $buffer = ''; 116 | while ($read = @socket_read($this->socket, self::READ_SIZE)) { 117 | $buffer .= $read; 118 | 119 | if (strpos($buffer, Server::END_OF_RESULT)) { 120 | $chunks = explode(Server::END_OF_RESULT, $buffer, 2); 121 | $command = json_decode($chunks[0], true); 122 | break; 123 | } 124 | } 125 | 126 | if (Server::RESULT === $command[0]) { 127 | return $command[1]; 128 | } elseif (Server::EXCEPTION === $command[0]) { 129 | $class = $command[1]; 130 | throw new $class($command[2]); 131 | } else { 132 | throw new \RuntimeException('Unknown command from server.'); 133 | } 134 | } 135 | 136 | /** 137 | * Close socket. 138 | */ 139 | public function close() 140 | { 141 | socket_close($this->socket); 142 | } 143 | 144 | /** 145 | * Delete storage on server. 146 | * 147 | * @param string $name 148 | * @return bool 149 | */ 150 | public function delete($name) 151 | { 152 | return $this->command(['Pure\Command\DeleteCommand', $name]); 153 | } 154 | 155 | /** 156 | * Checks if server is alive. 157 | * 158 | * @return bool 159 | */ 160 | public function ping() 161 | { 162 | try { 163 | $pong = $this->command(['Pure\Command\PingCommand', 'ping']); 164 | } catch (\RuntimeException $e) { 165 | return false; 166 | } 167 | 168 | return $pong === 'pong'; 169 | } 170 | } 171 | -------------------------------------------------------------------------------- /src/Command/CommandInterface.php: -------------------------------------------------------------------------------- 1 | 3 | * 4 | * For the full copyright and license information, please view the LICENSE 5 | * file that was distributed with this source code. 6 | */ 7 | 8 | namespace Pure\Command; 9 | 10 | use Pure\Server; 11 | use React\Socket\ConnectionInterface; 12 | 13 | interface CommandInterface 14 | { 15 | /** 16 | * @param Server $server 17 | */ 18 | public function __construct(Server $server); 19 | 20 | /** 21 | * @param $arguments 22 | * @param ConnectionInterface $connection 23 | * @return mixed 24 | */ 25 | public function run($arguments, ConnectionInterface $connection); 26 | } 27 | -------------------------------------------------------------------------------- /src/Command/DeleteCommand.php: -------------------------------------------------------------------------------- 1 | 3 | * 4 | * For the full copyright and license information, please view the LICENSE 5 | * file that was distributed with this source code. 6 | */ 7 | 8 | namespace Pure\Command; 9 | 10 | use Pure\Server; 11 | use React\Socket\ConnectionInterface; 12 | 13 | class DeleteCommand implements CommandInterface 14 | { 15 | /** 16 | * @var Server 17 | */ 18 | private $server; 19 | 20 | /** 21 | * @param Server $server 22 | */ 23 | public function __construct(Server $server) 24 | { 25 | $this->server = $server; 26 | } 27 | 28 | /** 29 | * Run delete storage command. 30 | * 31 | * @param array $arguments 32 | * @param ConnectionInterface $connection 33 | * @return bool 34 | */ 35 | public function run($arguments, ConnectionInterface $connection) 36 | { 37 | list($name) = $arguments; 38 | 39 | if (isset($this->server[$name])) { 40 | unset($this->server[$name]); 41 | return true; 42 | } else { 43 | return false; 44 | } 45 | } 46 | } 47 | -------------------------------------------------------------------------------- /src/Command/PingCommand.php: -------------------------------------------------------------------------------- 1 | 3 | * 4 | * For the full copyright and license information, please view the LICENSE 5 | * file that was distributed with this source code. 6 | */ 7 | 8 | namespace Pure\Command; 9 | 10 | use Pure\Server; 11 | use React\Socket\ConnectionInterface; 12 | 13 | class PingCommand implements CommandInterface 14 | { 15 | /** 16 | * @var Server 17 | */ 18 | private $server; 19 | 20 | /** 21 | * @param Server $server 22 | */ 23 | public function __construct(Server $server) 24 | { 25 | $this->server = $server; 26 | } 27 | 28 | /** 29 | * Ping-Pong 30 | * 31 | * @param array $arguments 32 | * @param ConnectionInterface $connection 33 | * @return string 34 | */ 35 | public function run($arguments, ConnectionInterface $connection) 36 | { 37 | list($ping) = $arguments; 38 | 39 | if ($ping === 'ping') { 40 | return 'pong'; 41 | } else { 42 | throw new \RuntimeException("Ping command must receive `ping` instead of `$ping`."); 43 | } 44 | } 45 | } 46 | -------------------------------------------------------------------------------- /src/Command/StorageCommand.php: -------------------------------------------------------------------------------- 1 | 3 | * 4 | * For the full copyright and license information, please view the LICENSE 5 | * file that was distributed with this source code. 6 | */ 7 | 8 | namespace Pure\Command; 9 | 10 | use Pure\Server; 11 | use React\Socket\ConnectionInterface; 12 | 13 | class StorageCommand implements CommandInterface 14 | { 15 | /** 16 | * @var Server 17 | */ 18 | private $server; 19 | 20 | /** 21 | * @param Server $server 22 | */ 23 | public function __construct(Server $server) 24 | { 25 | $this->server = $server; 26 | } 27 | 28 | /** 29 | * Runs storage command. 30 | * Command represented as array of next structure [class, name, method, args] where 31 | * class - storage full class name, 32 | * name - name of storage to store (every storage has to have unique name), 33 | * method - method of storage to call, 34 | * args - arguments for that method. 35 | * 36 | * @param array $arguments 37 | * @param ConnectionInterface $connection 38 | * @return array 39 | * @throws \RuntimeException 40 | */ 41 | public function run($arguments, ConnectionInterface $connection) 42 | { 43 | list($class, $name, $method, $args) = $arguments; 44 | 45 | if (isset($this->server[$name])) { 46 | if (!$this->server[$name] instanceof $class) { 47 | throw new \RuntimeException("Storage `$name` has type `" . get_class($this->server[$name]) . "` (you request `$class`)"); 48 | } 49 | } else { 50 | $this->server[$name] = new $class(); 51 | } 52 | 53 | $call = [$this->server[$name], $method]; 54 | $result = call_user_func_array($call, $args); 55 | 56 | return $result; 57 | } 58 | } 59 | -------------------------------------------------------------------------------- /src/Console/ClientCommand.php: -------------------------------------------------------------------------------- 1 | 3 | * 4 | * For the full copyright and license information, please view the LICENSE 5 | * file that was distributed with this source code. 6 | */ 7 | 8 | namespace Pure\Console; 9 | 10 | use Pure\Client; 11 | use Symfony\Component\Console\Command\Command; 12 | use Symfony\Component\Console\Input\InputInterface; 13 | use Symfony\Component\Console\Input\InputOption; 14 | use Symfony\Component\Console\Output\OutputInterface; 15 | use Symfony\Component\Console\Question\Question; 16 | use Symfony\Component\ExpressionLanguage\ExpressionLanguage; 17 | 18 | class ClientCommand extends Command 19 | { 20 | /** 21 | * {@inheritdoc} 22 | */ 23 | protected function configure() 24 | { 25 | $this 26 | ->setName('client') 27 | ->setDescription('Console client for PurePHP server') 28 | ->addOption('port', 'p', InputOption::VALUE_OPTIONAL, 'Port number', 1337) 29 | ->addOption('host', null, InputOption::VALUE_OPTIONAL, 'Host address', '127.0.0.1'); 30 | } 31 | 32 | /** 33 | * {@inheritdoc} 34 | */ 35 | protected function execute(InputInterface $input, OutputInterface $output) 36 | { 37 | $output->writeln('Welcome to Console Client for PurePHP server.'); 38 | 39 | $pure = new Client($input->getOption('port'), $input->getOption('host')); 40 | 41 | $language = new ExpressionLanguage(); 42 | 43 | $auto = [ 44 | 'pure', 45 | 'pure.map', 46 | 'pure.queue', 47 | 'pure.stack', 48 | 'pure.priority', 49 | 'pure.delete', 50 | 'pure.ping', 51 | 'exit', 52 | ]; 53 | 54 | $helper = $this->getHelper('question'); 55 | $question = new Question('> ', ''); 56 | 57 | do { 58 | $question->setAutocompleterValues($auto); 59 | $command = $helper->ask($input, $output, $question); 60 | $auto[] = $command; 61 | 62 | if ('exit' === $command) { 63 | break; 64 | } 65 | 66 | try { 67 | $result = $language->evaluate($command, [ 68 | 'pure' => $pure, 69 | ]); 70 | 71 | var_dump($result); 72 | 73 | } catch (\Exception $e) { 74 | $output->writeln('' . get_class($e) . ": \n" . $e->getMessage() . ''); 75 | } 76 | 77 | } while (true); 78 | } 79 | } 80 | -------------------------------------------------------------------------------- /src/Console/StartCommand.php: -------------------------------------------------------------------------------- 1 | 3 | * 4 | * For the full copyright and license information, please view the LICENSE 5 | * file that was distributed with this source code. 6 | */ 7 | 8 | namespace Pure\Console; 9 | 10 | use Pure\Server; 11 | use Symfony\Component\Console\Command\Command; 12 | use Symfony\Component\Console\Input\InputInterface; 13 | use Symfony\Component\Console\Input\InputOption; 14 | use Symfony\Component\Console\Output\OutputInterface; 15 | 16 | class StartCommand extends Command 17 | { 18 | /** 19 | * {@inheritdoc} 20 | */ 21 | protected function configure() 22 | { 23 | $this 24 | ->setName('start') 25 | ->setDescription('Start PurePHP server') 26 | ->addOption('port', 'p', InputOption::VALUE_OPTIONAL, 'Port number', 1337) 27 | ->addOption('host', null, InputOption::VALUE_OPTIONAL, 'Host address', '127.0.0.1'); 28 | } 29 | 30 | /** 31 | * {@inheritdoc} 32 | */ 33 | protected function execute(InputInterface $input, OutputInterface $output) 34 | { 35 | if (OutputInterface::VERBOSITY_VERBOSE <= $output->getVerbosity()) { 36 | $output->writeln('PurePHP server started.'); 37 | } 38 | 39 | $server = new Server($input->getOption('port'), $input->getOption('host')); 40 | 41 | if (OutputInterface::VERBOSITY_DEBUG <= $output->getVerbosity()) { 42 | $server->setLogger(function ($log) use ($output) { 43 | $output->writeln($log); 44 | }); 45 | } 46 | 47 | $server->run(); 48 | } 49 | } 50 | -------------------------------------------------------------------------------- /src/Helper/All.php: -------------------------------------------------------------------------------- 1 | 3 | * 4 | * For the full copyright and license information, please view the LICENSE 5 | * file that was distributed with this source code. 6 | */ 7 | 8 | namespace Pure\Helper; 9 | 10 | trait All 11 | { 12 | /** 13 | * Return all data. 14 | * 15 | * @return array 16 | */ 17 | public function all() 18 | { 19 | return iterator_to_array($this); 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /src/Helper/Filter.php: -------------------------------------------------------------------------------- 1 | 3 | * 4 | * For the full copyright and license information, please view the LICENSE 5 | * file that was distributed with this source code. 6 | */ 7 | 8 | namespace Pure\Helper; 9 | 10 | use Symfony\Component\ExpressionLanguage\ExpressionLanguage; 11 | 12 | trait Filter 13 | { 14 | /** 15 | * Write filter string in ExpressionLanguage. 16 | * In expression you can use next variables: 17 | * `key` - current element key. 18 | * `value` - current element value. 19 | * 20 | * Examples: 21 | * value in ['good_customers', 'collaborator'] 22 | * key > 100 and value not in ["misc"] 23 | * 24 | * @link http://symfony.com/doc/current/components/expression_language/introduction.html 25 | * @param string $filter 26 | * @param null|int $limit How many elements to search. 27 | * @return array|null 28 | */ 29 | public function filter($filter, $limit = null) 30 | { 31 | static $language = null; 32 | 33 | if (null === $language) { 34 | $language = new ExpressionLanguage(); 35 | } 36 | 37 | $count = 0; 38 | $result = []; 39 | foreach ($this as $key => $value) { 40 | $pass = $language->evaluate($filter, [ 41 | 'key' => $key, 42 | 'value' => $value, 43 | ]); 44 | 45 | if ($pass) { 46 | $result[$key] = $value; 47 | $count++; 48 | 49 | if (null !== $limit && $count >= $limit) { 50 | break; 51 | } 52 | } 53 | } 54 | 55 | return empty($result) ? null : $result; 56 | } 57 | } 58 | -------------------------------------------------------------------------------- /src/Proxy.php: -------------------------------------------------------------------------------- 1 | 3 | * 4 | * For the full copyright and license information, please view the LICENSE 5 | * file that was distributed with this source code. 6 | */ 7 | 8 | namespace Pure; 9 | 10 | class Proxy 11 | { 12 | /** 13 | * @var Client 14 | */ 15 | private $client; 16 | 17 | /** 18 | * @var string 19 | */ 20 | private $class; 21 | 22 | /** 23 | * @var \ReflectionClass 24 | */ 25 | private $reflectionClass; 26 | 27 | /** 28 | * @var string 29 | */ 30 | private $name; 31 | 32 | /** 33 | * @param Client $client 34 | * @param string $class 35 | * @param string $name 36 | */ 37 | public function __construct(Client $client, $class, $name) 38 | { 39 | $this->client = $client; 40 | $this->class = $class; 41 | $this->reflectionClass = new \ReflectionClass($class); 42 | $this->name = $name; 43 | } 44 | 45 | /** 46 | * @param string $method 47 | * @param array $arguments 48 | * @return mixed 49 | * @throws \RuntimeException 50 | */ 51 | public function __call($method, $arguments) 52 | { 53 | if ($this->reflectionClass->hasMethod($method)) { 54 | return $this->client->command(['Pure\Command\StorageCommand', $this->class, $this->name, $method, $arguments]); 55 | } else { 56 | throw new \RuntimeException("Class `{$this->class}` does not have method `{$method}`."); 57 | } 58 | } 59 | } 60 | -------------------------------------------------------------------------------- /src/Proxy/Generator.php: -------------------------------------------------------------------------------- 1 | 3 | * 4 | * For the full copyright and license information, please view the LICENSE 5 | * file that was distributed with this source code. 6 | */ 7 | 8 | namespace Pure\Proxy; 9 | 10 | use Pure\Client; 11 | use Pure\Proxy; 12 | 13 | /** 14 | * Class help to generate proxy object when accessing pure storage like what `$pure->stack->name->pop()`. 15 | */ 16 | class Generator 17 | { 18 | /** 19 | * @var Client 20 | */ 21 | private $client; 22 | 23 | /** 24 | * @var string 25 | */ 26 | private $alias; 27 | 28 | /** 29 | * @param Client $client 30 | * @param $alias 31 | */ 32 | public function __construct(Client $client, $alias) 33 | { 34 | $this->client = $client; 35 | $this->alias = $alias; 36 | } 37 | 38 | /** 39 | * @param $name 40 | * @return \Pure\Storage\StorageInterface 41 | */ 42 | public function __get($name) 43 | { 44 | return $this->client->{$this->alias}($name); 45 | } 46 | } 47 | -------------------------------------------------------------------------------- /src/Server.php: -------------------------------------------------------------------------------- 1 | 3 | * 4 | * For the full copyright and license information, please view the LICENSE 5 | * file that was distributed with this source code. 6 | */ 7 | 8 | namespace Pure; 9 | 10 | use React\EventLoop\Factory as LoopFactory; 11 | use React\Socket\Server as SocketServer; 12 | use React\Socket\ConnectionInterface; 13 | 14 | class Server implements \ArrayAccess 15 | { 16 | const RESULT = 0; 17 | 18 | const EXCEPTION = 1; 19 | 20 | const END_OF_RESULT = 'END_OF_RESULT'; 21 | 22 | /** 23 | * @var int 24 | */ 25 | private $port; 26 | 27 | /** 28 | * @var string 29 | */ 30 | private $host; 31 | 32 | /** 33 | * @var \Closure 34 | */ 35 | private $logger; 36 | 37 | /** 38 | * @var \React\EventLoop\LoopInterface 39 | */ 40 | private $loop; 41 | 42 | /** 43 | * @var SocketServer 44 | */ 45 | private $socket; 46 | 47 | /** 48 | * @var Storage\StorageInterface[] 49 | */ 50 | private $storages = []; 51 | 52 | /** 53 | * @var Command\CommandInterface[] 54 | */ 55 | private $commands = []; 56 | 57 | /** 58 | * @param int $port 59 | * @param string $host 60 | */ 61 | public function __construct($port, $host = '127.0.0.1') 62 | { 63 | $this->host = $host; 64 | $this->port = $port; 65 | 66 | $this->loop = LoopFactory::create(); 67 | 68 | $this->socket = new SocketServer($this->loop); 69 | $this->socket->on('connection', array($this, 'onConnection')); 70 | } 71 | 72 | /** 73 | * Start event loop. 74 | */ 75 | public function run() 76 | { 77 | $this->log("Server listening on {$this->host}:{$this->port}"); 78 | $this->socket->listen($this->port, $this->host); 79 | $this->loop->run(); 80 | } 81 | 82 | /** 83 | * On every new connection add event handler to receive commands from clients. 84 | * 85 | * @param ConnectionInterface $connection 86 | */ 87 | public function onConnection(ConnectionInterface $connection) 88 | { 89 | $this->log('New connection from ' . $connection->getRemoteAddress()); 90 | 91 | $buffer = ''; 92 | $connection->on('data', function ($data) use (&$buffer, &$connection) { 93 | $buffer .= $data; 94 | 95 | if (strpos($buffer, Client::END_OF_COMMAND)) { 96 | $chunks = explode(Client::END_OF_COMMAND, $buffer); 97 | $count = count($chunks); 98 | $buffer = $chunks[$count - 1]; 99 | 100 | for ($i = 0; $i < $count - 1; $i++) { 101 | $command = json_decode($chunks[$i], true); 102 | $this->runCommand($command, $connection); 103 | } 104 | } 105 | }); 106 | } 107 | 108 | /** 109 | * Detect and run command received from client. 110 | * @param array $arguments 111 | * @param ConnectionInterface $connection 112 | */ 113 | private function runCommand($arguments, ConnectionInterface $connection) 114 | { 115 | try { 116 | 117 | $commandClass = array_shift($arguments); 118 | 119 | if (null !== $this->getLogger()) { 120 | $this->log( 121 | 'Command from ' . $connection->getRemoteAddress() . 122 | ": [$commandClass] " . 123 | join(', ', array_map('json_encode', $arguments)) 124 | ); 125 | } 126 | 127 | 128 | if (isset($this->commands[$commandClass])) { 129 | $command = $this->commands[$commandClass]; 130 | } else { 131 | 132 | if (!class_exists($commandClass)) { 133 | throw new \RuntimeException("Command class `$commandClass` does not found."); 134 | } 135 | 136 | $command = new $commandClass($this); 137 | 138 | if (!$command instanceof Command\CommandInterface) { 139 | throw new \RuntimeException("Every command must implement Command\\CommandInterface."); 140 | } 141 | 142 | $this->commands[$commandClass] = $command; 143 | } 144 | 145 | $result = $command->run($arguments, $connection); 146 | $result = [self::RESULT, $result]; 147 | 148 | } catch (\Exception $e) { 149 | 150 | $result = [self::EXCEPTION, get_class($e), $e->getMessage()]; 151 | $this->log('Exception: ' . $e->getMessage()); 152 | 153 | } 154 | 155 | $connection->write(json_encode($result) . self::END_OF_RESULT); 156 | } 157 | 158 | /** 159 | * Before logging massage set logger with `setLogger` method. 160 | * 161 | * @param string $message 162 | */ 163 | public function log($message) 164 | { 165 | if (is_callable($this->logger)) { 166 | $this->logger->__invoke($message); 167 | } 168 | } 169 | 170 | /** 171 | * @param callable $callback 172 | */ 173 | public function setLogger(\Closure $callback) 174 | { 175 | $this->logger = $callback; 176 | } 177 | 178 | /** 179 | * @return callable 180 | */ 181 | public function getLogger() 182 | { 183 | return $this->logger; 184 | } 185 | 186 | /** 187 | * @param string $name 188 | * @param mixed|Storage\StorageInterface $storage 189 | */ 190 | public function setStorage($name, $storage) 191 | { 192 | $this->storages[$name] = $storage; 193 | } 194 | 195 | /** 196 | * @param string $name 197 | * @return Storage\StorageInterface 198 | */ 199 | public function getStorage($name) 200 | { 201 | return $this->storages[$name]; 202 | } 203 | 204 | /** 205 | * @return \React\EventLoop\LoopInterface 206 | */ 207 | public function getLoop() 208 | { 209 | return $this->loop; 210 | } 211 | 212 | /** 213 | * {@inheritdoc} 214 | */ 215 | public function offsetExists($offset) 216 | { 217 | return isset($this->storages[$offset]); 218 | } 219 | 220 | /** 221 | * {@inheritdoc} 222 | */ 223 | public function offsetGet($offset) 224 | { 225 | return $this->storages[$offset]; 226 | } 227 | 228 | /** 229 | * {@inheritdoc} 230 | */ 231 | public function offsetSet($offset, $value) 232 | { 233 | $this->storages[$offset] = $value; 234 | } 235 | 236 | /** 237 | * {@inheritdoc} 238 | */ 239 | public function offsetUnset($offset) 240 | { 241 | unset($this->storages[$offset]); 242 | } 243 | } 244 | -------------------------------------------------------------------------------- /src/Storage/ArrayStorage.php: -------------------------------------------------------------------------------- 1 | 3 | * 4 | * For the full copyright and license information, please view the LICENSE 5 | * file that was distributed with this source code. 6 | */ 7 | 8 | namespace Pure\Storage; 9 | 10 | use Pure\Helper; 11 | 12 | class ArrayStorage extends \ArrayIterator implements StorageInterface 13 | { 14 | use Helper\All; 15 | use Helper\Filter; 16 | 17 | /** 18 | * @param array $array 19 | */ 20 | public function push($array) 21 | { 22 | foreach ((array)$array as $key => $value) { 23 | $this[$key] = $value; 24 | } 25 | } 26 | 27 | /** 28 | * @param mixed $key 29 | * @return null 30 | */ 31 | public function get($key) 32 | { 33 | if ($this->has($key)) { 34 | return $this[$key]; 35 | } else { 36 | return null; 37 | } 38 | } 39 | 40 | /** 41 | * @param mixed $key 42 | * @return bool 43 | */ 44 | public function has($key) 45 | { 46 | return isset($this[$key]); 47 | } 48 | 49 | /** 50 | * Delete element by key. 51 | * 52 | * @param mixed $key 53 | */ 54 | public function delete($key) 55 | { 56 | unset($this[$key]); 57 | } 58 | } 59 | -------------------------------------------------------------------------------- /src/Storage/PriorityQueueStorage.php: -------------------------------------------------------------------------------- 1 | 3 | * 4 | * For the full copyright and license information, please view the LICENSE 5 | * file that was distributed with this source code. 6 | */ 7 | 8 | namespace Pure\Storage; 9 | 10 | use Pure\Helper; 11 | 12 | class PriorityQueueStorage extends \SplPriorityQueue implements StorageInterface 13 | { 14 | use Helper\All; 15 | use Helper\Filter; 16 | } 17 | -------------------------------------------------------------------------------- /src/Storage/QueueStorage.php: -------------------------------------------------------------------------------- 1 | 3 | * 4 | * For the full copyright and license information, please view the LICENSE 5 | * file that was distributed with this source code. 6 | */ 7 | 8 | namespace Pure\Storage; 9 | 10 | use Pure\Helper; 11 | 12 | class QueueStorage extends \SplQueue implements StorageInterface 13 | { 14 | use Helper\All; 15 | use Helper\Filter; 16 | 17 | /** 18 | * Dequeue queue. 19 | * 20 | * @return mixed 21 | */ 22 | public function pop() 23 | { 24 | return $this->dequeue(); 25 | } 26 | 27 | /** 28 | * Enqueue queue. 29 | * 30 | * @param mixed $value 31 | */ 32 | public function push($value) 33 | { 34 | $this->enqueue($value); 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /src/Storage/StackStorage.php: -------------------------------------------------------------------------------- 1 | 3 | * 4 | * For the full copyright and license information, please view the LICENSE 5 | * file that was distributed with this source code. 6 | */ 7 | 8 | namespace Pure\Storage; 9 | 10 | use Pure\Helper; 11 | 12 | class StackStorage extends \SplStack implements StorageInterface 13 | { 14 | use Helper\All; 15 | use Helper\Filter; 16 | } 17 | -------------------------------------------------------------------------------- /src/Storage/StorageInterface.php: -------------------------------------------------------------------------------- 1 | 3 | * 4 | * For the full copyright and license information, please view the LICENSE 5 | * file that was distributed with this source code. 6 | */ 7 | 8 | namespace Pure\Storage; 9 | 10 | interface StorageInterface extends \Iterator, \Countable 11 | { 12 | } 13 | -------------------------------------------------------------------------------- /test/ClientServerTest.php: -------------------------------------------------------------------------------- 1 | 3 | * 4 | * For the full copyright and license information, please view the LICENSE 5 | * file that was distributed with this source code. 6 | */ 7 | 8 | namespace Pure; 9 | 10 | use Symfony\Component\Process\Process; 11 | 12 | class ClientServerTest extends \PHPUnit_Framework_TestCase 13 | { 14 | const PORT = 1337; 15 | 16 | static $process; 17 | 18 | public static function setUpBeforeClass() 19 | { 20 | self::$process = new Process('php ' . __DIR__ . '/../pure start --port=' . self::PORT); 21 | self::$process->start(); 22 | 23 | sleep(1); 24 | } 25 | 26 | public static function tearDownAfterClass() 27 | { 28 | self::$process->stop(3, SIGKILL); 29 | } 30 | 31 | public function testPing() 32 | { 33 | $client = new Client(self::PORT); 34 | 35 | $this->assertTrue($client->ping()); 36 | } 37 | 38 | public function testQueue() 39 | { 40 | $client = new Client(self::PORT); 41 | 42 | $client->queue('test')->push(1); 43 | $client->queue('test')->push(2); 44 | $client->queue('test')->push(3); 45 | 46 | $this->assertEquals(1, $client->queue('test')->pop()); 47 | $this->assertEquals(2, $client->queue('test')->pop()); 48 | $this->assertEquals(3, $client->queue('test')->pop()); 49 | } 50 | } 51 | -------------------------------------------------------------------------------- /test/Storage/QueueStorageTest.php: -------------------------------------------------------------------------------- 1 | 3 | * 4 | * For the full copyright and license information, please view the LICENSE 5 | * file that was distributed with this source code. 6 | */ 7 | 8 | namespace Pure\Storage; 9 | 10 | class QueueStorageTest extends \PHPUnit_Framework_TestCase 11 | { 12 | public function testPushAndPop() 13 | { 14 | $queue = new QueueStorage(); 15 | $queue->push(1); 16 | $queue->push(2); 17 | $queue->push(3); 18 | 19 | $this->assertEquals(1, $queue->pop()); 20 | $this->assertEquals(2, $queue->pop()); 21 | $this->assertEquals(3, $queue->pop()); 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /test/Storage/StackStorageTest.php: -------------------------------------------------------------------------------- 1 | 3 | * 4 | * For the full copyright and license information, please view the LICENSE 5 | * file that was distributed with this source code. 6 | */ 7 | 8 | namespace Pure\Storage; 9 | 10 | class StackTest extends \PHPUnit_Framework_TestCase 11 | { 12 | public function testPushAndPop() 13 | { 14 | $queue = new StackStorage(); 15 | $queue->push(1); 16 | $queue->push(2); 17 | $queue->push(3); 18 | 19 | $this->assertEquals(3, $queue->pop()); 20 | $this->assertEquals(2, $queue->pop()); 21 | $this->assertEquals(1, $queue->pop()); 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /test/bootstrap.php: -------------------------------------------------------------------------------- 1 |