├── .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 | [](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 |