├── .gitignore
├── .editorconfig
├── examples
├── console
├── publish.php
└── auth.go
├── .travis.yml
├── composer.json
├── src
├── config
│ └── phpnsq.php
└── phpnsq
│ ├── Cmd
│ ├── Base.php
│ └── Subscribe.php
│ ├── Stream
│ ├── Socket.php
│ ├── IntPacker.php
│ ├── Writer.php
│ ├── Message.php
│ └── Reader.php
│ ├── Utils
│ ├── Logging.php
│ └── LogFormatter.php
│ ├── Conn
│ ├── Lookupd.php
│ ├── Config.php
│ ├── Pool.php
│ └── Nsqd.php
│ └── PhpNsq.php
├── LICENSE
├── phpunit.xml
└── README.md
/.gitignore:
--------------------------------------------------------------------------------
1 | /vendor/
2 | /tmp
3 | /composer.lock
4 | .idea
5 | .sonarlint
6 | phpnsq.iml
7 | .ac-php-conf.json
8 | GPATH
9 | GRTAGS
10 | GTAGS
11 |
--------------------------------------------------------------------------------
/.editorconfig:
--------------------------------------------------------------------------------
1 | #editorconfig.org
2 | root = true
3 |
4 | [*.php]
5 | indent_style = space
6 | indent_size = 4
7 | end_of_line = lf
8 | charset = utf-8
9 | trim_trailing_whitespace = true
10 | insert_trailing_newline = true
11 |
12 | [*.md]
13 | trim_trailing_whitespace = false
14 |
--------------------------------------------------------------------------------
/examples/console:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env php
2 | add(new Subscribe($config, null));
14 |
15 | $application->run();
16 |
--------------------------------------------------------------------------------
/.travis.yml:
--------------------------------------------------------------------------------
1 | language: php
2 |
3 | php:
4 | - 7.1
5 | - nightly
6 |
7 | install:
8 | - go get -u -v github.com/campoy/embedmd
9 | - embedmd -d *.md
10 | - docker pull nsqio/nsq
11 | - docker run -d --name lookupd -p 4160:4160 -p 4161:4161 nsqio/nsq /nsqlookupd
12 | - docker run -d --name nsqd -p 4150:4150 -p 4151:4151 nsqio/nsq /nsqd
13 | - composer install
14 | - composer dump-autoload --optimize
15 |
16 | script:
17 | - phpunit -v --coverage-clover=coverage.xml
18 |
19 | after_success:
20 | - bash <(curl -s https://codecov.io/bash)
21 |
--------------------------------------------------------------------------------
/examples/publish.php:
--------------------------------------------------------------------------------
1 | setTopic("sample_topic")->publish("Hello nsq.");
11 | var_dump($msg);
12 |
13 | //defered publish
14 | $msg = $phpnsq->setTopic("sample_topic")->publishDefer("this is a defered message.", 10);
15 | var_dump($msg);
16 |
17 | //multiple publish
18 | $messages = [
19 | "Hello, I am nsq client",
20 | "There are so many libraries developed by PHP",
21 | "Oh, no, PHP is not so good and slowly",
22 | ];
23 | $msg = $phpnsq->setTopic("sample_topic")->publishMulti(...$messages);
24 | var_dump($msg);
25 |
--------------------------------------------------------------------------------
/examples/auth.go:
--------------------------------------------------------------------------------
1 | package main
2 |
3 | import (
4 | "io"
5 | "log"
6 | "net/http"
7 | )
8 |
9 | func main() {
10 | // Hello world, the web server
11 | helloHandler := func(w http.ResponseWriter, req *http.Request) {
12 | if req.ParseForm() == nil && req.Form.Get("secret") == "secret" {
13 | io.WriteString(w, `{
14 | "ttl": 3600,
15 | "identity": "username",
16 | "identity_url": "https://....",
17 | "authorizations": [
18 | {
19 | "permissions": [
20 | "subscribe",
21 | "publish"
22 | ],
23 | "topic": ".*",
24 | "channels": [
25 | ".*"
26 | ]
27 | }
28 | ]
29 | }`)
30 | }
31 | }
32 |
33 | http.HandleFunc("/auth", helloHandler)
34 | log.Fatal(http.ListenAndServe(":8000", nil))
35 | }
--------------------------------------------------------------------------------
/composer.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "okstuff/phpnsq",
3 | "description": "PHP Client for nsq",
4 | "type": "library",
5 | "keywords": ["nsq", "queue"],
6 | "homepage": "http://github.com/wk30/phpnsq",
7 | "require": {
8 | "monolog/monolog": "^1.25.0|^2.0",
9 | "symfony/console": "^v5.0",
10 | "react/event-loop": "^v0.4|^v0.5|^v1.1",
11 | "bramus/monolog-colored-line-formatter": "^3.0"
12 | },
13 | "autoload": {
14 | "psr-4": {
15 | "OkStuff\\PhpNsq\\": "src/phpnsq/"
16 | }
17 | },
18 | "autoload-dev": {
19 | "psr-4": {
20 | "OkStuff\\PhpNsq\\Tests\\": "tests/"
21 | }
22 | },
23 | "authors": [
24 | {
25 | "name": "wk30",
26 | "email": "foo_stacker@yeah.net"
27 | }
28 | ],
29 | "license": "MIT"
30 | }
31 |
--------------------------------------------------------------------------------
/src/config/phpnsq.php:
--------------------------------------------------------------------------------
1 | [
5 | "nsqd_addrs" => [//this is needed
6 | "127.0.0.1:4150",
7 | ],
8 | "lookupd_addrs" => [
9 | "127.0.0.1:4161",//only support http protocol
10 | ],
11 | "lookupd_switch" => true,//recommend to use lookupd
12 | "logdir" => "/tmp",
13 | "auth_secret" => "secret",
14 | "auth_switch" => false,
15 | //FIXME:
16 | // "tls_config" => [
17 | // "local_cert" => "/home/vagrant/docker/nsqio/certs/client.pem",
18 | // "local_pk" => "/home/vagrant/docker/nsqio/certs/client.key",
19 | // "cafile" => "/home/vagrant/docker/nsqio/certs/ca.pem",
20 | // "passphrase" => "test", //if your cert has a passphrase
21 | // "cn_match" => "test",
22 | // "peer_fingerprint" => "sha256",
23 | // ],
24 | ],
25 | ];
26 |
--------------------------------------------------------------------------------
/src/phpnsq/Cmd/Base.php:
--------------------------------------------------------------------------------
1 | run();
26 | }
27 |
28 | public function addReadStream($socket, Closure $closure)
29 | {
30 | self::$loop->addReadStream($socket, $closure);
31 |
32 | return $this;
33 | }
34 |
35 | public function addPeriodicTimer($interval, Closure $closure)
36 | {
37 | self::$loop->addPeriodicTimer($interval, $closure);
38 |
39 | return $this;
40 | }
41 | }
42 |
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | MIT License
2 |
3 | Copyright (c) 2018 okstuff
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 all
13 | copies or substantial portions of the Software.
14 |
15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21 | SOFTWARE.
22 |
--------------------------------------------------------------------------------
/src/phpnsq/Cmd/Subscribe.php:
--------------------------------------------------------------------------------
1 | setName(self::COMMAND_NAME)
17 | ->addArgument("topic", InputArgument::REQUIRED, "The topic you want to subscribe")
18 | ->addArgument("channel", InputArgument::REQUIRED, "The channel you want to subscribe")
19 | ->setDescription('subscribe new notification.')
20 | ->setHelp("This command allows you to subscribe notifications...");
21 | }
22 |
23 | public function execute(InputInterface $input, OutputInterface $output)
24 | {
25 | $phpnsq = self::$phpnsq;
26 | $phpnsq->setTopic($input->getArgument("topic"))
27 | ->setChannel($input->getArgument("channel"))
28 | ->subscribe($this, function (Message $message) use ($phpnsq, $output) {
29 | // $output->writeln($message->toJson());
30 | $phpnsq->getLogger()->info("READ", $message->toArray());
31 | });
32 | //excuted every five seconds.
33 | $this->addPeriodicTimer(5, function () use ($output) {
34 | $memory = memory_get_usage() / 1024;
35 | $formatted = number_format($memory, 3) . 'K';
36 | $output->writeln(date("Y-m-d H:i:s") . " ############ Current memory usage: {$formatted} ############");
37 | });
38 | $this->runLoop();
39 | }
40 | }
41 |
--------------------------------------------------------------------------------
/src/phpnsq/Stream/Socket.php:
--------------------------------------------------------------------------------
1 | = $written) {
23 | throw new Exception("Could not write " . strlen($buffer) . " bytes to {$socket}");
24 | }
25 |
26 | return $written;
27 | }
28 |
29 | public static function recvFrom($socket, $length)
30 | {
31 | $buffer = @stream_socket_recvfrom($socket, $length);
32 | if (empty($buffer)) {
33 | throw new Exception("Read 0 bytes from {$socket}");
34 | }
35 |
36 | return $buffer;
37 | }
38 |
39 | public static function select(array &$read, array &$write, $timeout)
40 | {
41 | $streamPool = [
42 | "read" => $read,
43 | "write" => $write,
44 | ];
45 | if ($read || $write) {
46 | $except = null;
47 |
48 | $available = @stream_select($read, $write, $except, $timeout);
49 | if ($available > 0) {
50 | return $available;
51 | } else if ($available === 0) {
52 | var_dump(date("Y-m-d H:i:s"));
53 | throw new Exception("stream_select() timeout after {$timeout} seconds");
54 | } else {
55 | throw new Exception("stream_select() failed");
56 | }
57 | }
58 |
59 | $timeout && usleep($timeout);
60 |
61 | return 0;
62 | }
63 | }
64 |
--------------------------------------------------------------------------------
/src/phpnsq/Utils/Logging.php:
--------------------------------------------------------------------------------
1 | handler = new Logger($name);
18 | $this->dirname = $dirname;
19 |
20 | $this->handler->pushHandler((new StreamHandler($this->getLogFile()))->setFormatter(new LineFormatter()));
21 | $this->handler->pushHandler((new StreamHandler("php://stdout"))->setFormatter(new LogFormatter(true)));
22 | }
23 |
24 | public function getHandler()
25 | {
26 | return $this->handler;
27 | }
28 |
29 | public function debug($msg, ...$context)
30 | {
31 | $this->handler->debug($msg, $context);
32 | }
33 |
34 | public function info($msg, ...$context)
35 | {
36 | $this->handler->info($msg, $context);
37 | }
38 |
39 | public function warn($msg, ...$context)
40 | {
41 | $this->handler->warning($msg, $context);
42 | }
43 |
44 | public function error($msg, ...$context)
45 | {
46 | $this->handler->error($msg, $context);
47 | }
48 |
49 | public function notice($msg, ...$context)
50 | {
51 | $this->handler->notice($msg, $context);
52 | }
53 |
54 | private function getLogFile()
55 | {
56 | $filename = $this->dirname . DIRECTORY_SEPARATOR . "phpnsq-" . date("Ymd") . ".log";
57 | try {
58 | if (!file_exists($this->dirname)) {
59 | mkdir($this->dirname, 0755);
60 | }
61 |
62 | if (!file_exists($filename)) {
63 | touch($filename);
64 | }
65 | } catch (Exception $e) {
66 | throw new Exception("Create `$filename` failed.");
67 | }
68 |
69 | return $filename;
70 | }
71 | }
72 |
--------------------------------------------------------------------------------
/phpunit.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
18 |
19 |
20 |
21 |
22 |
23 |
24 |
25 |
26 |
27 |
28 |
29 | ./src/phpnsq/
30 |
31 | ./src/config
32 | ./src/config/phpnsq.yml
33 |
34 |
35 |
36 |
37 |
38 |
39 | ./tests
40 |
41 |
42 |
43 |
44 |
45 |
46 |
47 |
48 |
49 |
--------------------------------------------------------------------------------
/src/phpnsq/Conn/Lookupd.php:
--------------------------------------------------------------------------------
1 | config = $config;
18 | }
19 |
20 | public function getConfig()
21 | {
22 | return $this->config;
23 | }
24 |
25 | public function getProducers(string $topic)
26 | {
27 | $nsqdConns = [];
28 |
29 | if ($this->nsqdConnected) {
30 | return $nsqdConns;
31 | }
32 |
33 | $defaults = array(
34 | CURLOPT_URL => sprintf(self::lookupTopicUri, $this->config->host, $this->config->port, $topic),
35 | CURLOPT_HEADER => 0,
36 | CURLOPT_RETURNTRANSFER => TRUE,
37 | CURLOPT_TIMEOUT => 4
38 | );
39 |
40 | $ch = curl_init();
41 | curl_setopt_array($ch, $defaults);
42 | if( ! $result = curl_exec($ch)) {
43 | trigger_error(curl_error($ch));
44 | }
45 | curl_close($ch);
46 |
47 | $d = json_decode($result, true);
48 | if (isset($d["message"]) && $d["message"] == "TOPIC_NOT_FOUND") {
49 | return $nsqdConns;
50 | }
51 |
52 | foreach ($d["producers"] as $producer) {
53 | array_push($nsqdConns, $this->connectProducer($producer));
54 | }
55 |
56 | $this->nsqdConnected = true;
57 |
58 | return $nsqdConns;
59 | }
60 |
61 | private function connectProducer($producer)
62 | {
63 | $config = new Config($producer["broadcast_address"], $producer["tcp_port"]);
64 | $config->set("authSwitch", $this->config->get("authSwitch"))
65 | ->set("authSecret", $this->config->get("authSecret"))
66 | ->set("logdir", $this->config->get("logdir"));
67 | if (!empty($this->config->get("tlsConfig"))) {
68 | $config->set("tlsConfig", $this->config->get("tlsConfig"));
69 | }
70 | return new Nsqd($config);
71 | }
72 | }
73 |
--------------------------------------------------------------------------------
/src/phpnsq/Stream/IntPacker.php:
--------------------------------------------------------------------------------
1 | 60,
16 | 'min' => 0.1,
17 | 'max' => 5 * 60,
18 | ];
19 | private $writeTimeout = [
20 | 'default' => 1,
21 | 'min' => 0.1,
22 | 'max' => 5 * 60,
23 | ];
24 |
25 | //TODO: need to be fixed
26 | private $backoffStrategy;
27 | private $maxBackoffDuration = [
28 | 'default' => 2 * 60,
29 | 'min' => 0,
30 | 'max' => 60 * 60,
31 | ];
32 | private $backoffMultiplier = [
33 | 'default' => 1,
34 | 'min' => 0,
35 | 'max' => 60 * 60,
36 | ];
37 |
38 | private $maxAttempts = [
39 | 'default' => 5,
40 | 'min' => 0,
41 | 'max' => 65535,
42 | ];
43 |
44 | private $heartbeatInterval = 30;
45 |
46 | private $tlsConfig;
47 |
48 | private $blocking = true;
49 |
50 | private $authSwitch = false;
51 |
52 | private $authSecret = "";
53 |
54 | private $logdir = "";
55 |
56 | public function __construct($host = "", $port = 0)
57 | {
58 | $this->host = $host;
59 | $this->port = $port;
60 | }
61 |
62 | public function set($key, $val)
63 | {
64 | if (is_array($this->$key)) {
65 | $this->$key['default'] = $val;
66 | } else {
67 | $this->$key = $val;
68 | }
69 |
70 | return $this;
71 | }
72 |
73 | public function get($key)
74 | {
75 | return $this->$key;
76 | }
77 |
78 | //check if all the value is between min and max value.
79 | public function validate()
80 | {
81 | foreach ($this as $key => $val) {
82 | if (is_array($val) && count($val) == 3) {
83 | if (!isset($val['default']) || !isset($val['min']) || !isset($val['max'])) {
84 | throw new Exception(sprintf("invalid %s value", $key));
85 | }
86 |
87 | if ($val['default'] < $val['min']) {
88 | throw new Exception(sprintf("invalid %s ! %v(default) < %v(min)", $key, $val['default'], $val['min']));
89 | }
90 |
91 | if ($val['default'] > $val['max']) {
92 | throw new Exception(sprintf("invalid %s ! %v(default) > %v(max)", $key, $val['default'], $val['max']));
93 | }
94 | }
95 | }
96 |
97 | return true;
98 | }
99 | }
100 |
--------------------------------------------------------------------------------
/src/phpnsq/Conn/Pool.php:
--------------------------------------------------------------------------------
1 | nsqCfg = $nsq["nsq"];
14 |
15 | if ($lookupd) {
16 | foreach ($nsq["nsq"]["lookupd_addrs"] as $value) {
17 | $addr = explode(":", $value);
18 | $config = new Config($addr[0], $addr[1]);
19 | $config->set("authSwitch", $nsq["nsq"]["auth_switch"])
20 | ->set("authSecret", $nsq["nsq"]["auth_secret"])
21 | ->set("logdir", $nsq["nsq"]["logdir"]);
22 | if (!empty($nsq["nsq"]["tls_config"])) {
23 | $config->set("tlsConfig", $nsq["nsq"]["tls_config"]);
24 | }
25 | $this->addLookupd(new Lookupd($config));
26 | }
27 | } else {
28 | $this->addNsqd();
29 | }
30 | }
31 |
32 | private function addNsqd()
33 | {
34 | foreach ($this->nsqCfg["nsqd_addrs"] as $value) {
35 | $addr = explode(":", $value);
36 | $config = new Config($addr[0], $addr[1]);
37 | $config->set("authSwitch", $this->nsqCfg["auth_switch"])
38 | ->set("authSecret", $this->nsqCfg["auth_secret"])
39 | ->set("logdir", $this->nsqCfg["logdir"]);
40 | if (!empty($this->nsqCfg["tls_config"])) {
41 | $config->set("tlsConfig", $this->nsqCfg["tls_config"]);
42 | }
43 | $this->addConn(new Nsqd($config));
44 | }
45 | }
46 |
47 | public function addConn(Nsqd ...$conns)
48 | {
49 | foreach ($conns as $conn) {
50 | array_push($this->pool, $conn);
51 | }
52 |
53 | return $this;
54 | }
55 |
56 | public function getConn()
57 | {
58 | return $this->pool[array_rand($this->pool)];
59 | }
60 |
61 | public function addNsqdByLookupd(Lookupd $conn, string $topic)
62 | {
63 | $nsqdConns = $conn->getProducers($topic);
64 | if (count($nsqdConns) <= 0) {
65 | $this->addNsqd();
66 | } else {
67 | $this->addConn(...$nsqdConns);
68 | }
69 |
70 | return $this;
71 | }
72 |
73 | public function addLookupd(Lookupd $conn)
74 | {
75 | array_push($this->lookupdPool, $conn);
76 |
77 | return $this;
78 | }
79 |
80 | public function getLookupd()
81 | {
82 | return $this->lookupdPool[array_rand($this->lookupdPool)];
83 | }
84 |
85 | public function getLookupdCount()
86 | {
87 | return count($this->lookupdPool);
88 | }
89 | }
90 |
--------------------------------------------------------------------------------
/src/phpnsq/Stream/Writer.php:
--------------------------------------------------------------------------------
1 | = 1) {
99 | $str .= sprintf(" %s", implode(" ", $params));
100 | }
101 | $str .= "\n";
102 |
103 | return $str;
104 | }
105 | }
106 |
--------------------------------------------------------------------------------
/src/phpnsq/Utils/LogFormatter.php:
--------------------------------------------------------------------------------
1 | ignoreEmptyContextAndExtra = $ignoreEmptyContextAndExtra;
14 | parent::__construct(null, null, null, false, $ignoreEmptyContextAndExtra);
15 | }
16 |
17 | /**
18 | * {@inheritdoc}
19 | */
20 | public function format(array $record) : string
21 | {
22 | $vars = parent::normalize($record);
23 |
24 | $output = $this->format;
25 |
26 | foreach ($vars['extra'] as $var => $val) {
27 | if (false !== strpos($output, '%extra.' . $var . '%')) {
28 | $output = str_replace('%extra.' . $var . '%', $this->stringify($val), $output);
29 | unset($vars['extra'][$var]);
30 | }
31 | }
32 |
33 |
34 | foreach ($vars['context'] as $var => $val) {
35 | if (false !== strpos($output, '%context.' . $var . '%')) {
36 | $output = str_replace('%context.' . $var . '%', $this->stringify($val), $output);
37 | unset($vars['context'][$var]);
38 | }
39 | }
40 |
41 | $output = $this->coloredOutput($vars);
42 |
43 | // remove leftover %extra.xxx% and %context.xxx% if any
44 | if (false !== strpos($output, '%')) {
45 | $output = preg_replace('/%(?:extra|context)\..+?%/', '', $output);
46 | }
47 |
48 | // Let the parent class to the formatting, yet wrap it in the color linked to the level
49 | return $output;
50 | }
51 |
52 | protected function coloredOutput(&$vars)
53 | {
54 | $coloredOutput = "[%datetime%] %channel%.%level_name%: ";
55 | foreach ($vars as $var => $val) {
56 | if (false !== strpos($coloredOutput, '%' . $var . '%')) {
57 | $coloredOutput = str_replace('%' . $var . '%', $this->stringify($val), $coloredOutput);
58 | }
59 | }
60 | //Get the Color Scheme
61 | $colorScheme = $this->getColorScheme();
62 | $coloredOutput = $colorScheme->getColorizeString($vars['level']) . $coloredOutput . $colorScheme->getResetString();
63 |
64 | $normalOutput = "%message% %context% %extra%\n";
65 | if ($this->ignoreEmptyContextAndExtra) {
66 | if (empty($vars['context'])) {
67 | unset($vars['context']);
68 | $normalOutput = str_replace('%context%', '', $normalOutput);
69 | }
70 |
71 | if (empty($vars['extra'])) {
72 | unset($vars['extra']);
73 | $normalOutput = str_replace('%extra%', '', $normalOutput);
74 | }
75 | }
76 |
77 | foreach ($vars as $var => $val) {
78 | if (false !== strpos($normalOutput, '%' . $var . '%')) {
79 | $normalOutput = str_replace('%' . $var . '%', $this->stringify($val), $normalOutput);
80 | }
81 | }
82 |
83 | return $coloredOutput . $normalOutput;
84 | }
85 | }
86 |
--------------------------------------------------------------------------------
/src/phpnsq/Stream/Message.php:
--------------------------------------------------------------------------------
1 | timestamp = microtime(true);
18 | }
19 |
20 | /**
21 | * @return bool
22 | */
23 | public function isDecoded()
24 | {
25 | return $this->decoded;
26 | }
27 |
28 | public function setDecoded()
29 | {
30 | $this->decoded = true;
31 |
32 | return $this;
33 | }
34 |
35 | /**
36 | * @return mixed
37 | */
38 | public function getId()
39 | {
40 | return $this->id;
41 | }
42 |
43 | /**
44 | * @param mixed $id
45 | */
46 | public function setId($id)
47 | {
48 | $this->id = $id;
49 |
50 | return $this;
51 | }
52 |
53 | /**
54 | * @return mixed
55 | */
56 | public function getBody()
57 | {
58 | return $this->body;
59 | }
60 |
61 | /**
62 | * @param mixed $body
63 | */
64 | public function setBody($body)
65 | {
66 | $this->body = $body;
67 |
68 | return $this;
69 | }
70 |
71 | /**
72 | * @return mixed
73 | */
74 | public function getTimestamp()
75 | {
76 | return $this->timestamp;
77 | }
78 |
79 | /**
80 | * @param mixed $timestamp
81 | */
82 | public function setTimestamp($timestamp = null)
83 | {
84 | if (null === $timestamp) {
85 | $this->timestamp = microtime(true);
86 | }
87 | $this->timestamp = $timestamp;
88 |
89 | return $this;
90 | }
91 |
92 | /**
93 | * @return mixed
94 | */
95 | public function getAttempts()
96 | {
97 | return $this->attempts;
98 | }
99 |
100 | /**
101 | * @param mixed $attempts
102 | */
103 | public function setAttempts($attempts)
104 | {
105 | $this->attempts = $attempts;
106 |
107 | return $this;
108 | }
109 |
110 | /**
111 | * @return mixed
112 | */
113 | public function getNsqdAddr()
114 | {
115 | return $this->nsqdAddr;
116 | }
117 |
118 | /**
119 | * @param mixed $nsqdAddr
120 | */
121 | public function setNsqdAddr($nsqdAddr)
122 | {
123 | $this->nsqdAddr = $nsqdAddr;
124 |
125 | return $this;
126 | }
127 |
128 | /**
129 | * @return mixed
130 | */
131 | public function getDelegate()
132 | {
133 | return $this->delegate;
134 | }
135 |
136 | /**
137 | * @param mixed $delegate
138 | */
139 | public function setDelegate($delegate)
140 | {
141 | $this->delegate = $delegate;
142 |
143 | return $this;
144 | }
145 |
146 | public function toArray()
147 | {
148 | return [
149 | "id" => $this->getId(),
150 | "body" => $this->getBody(),
151 | "timestamp" => $this->getTimestamp(),
152 | "decoded" => $this->isDecoded(),
153 | "attempts" => $this->getAttempts(),
154 | "nsqdAddr" => $this->getNsqdAddr(),
155 | "delegate" => $this->getDelegate(),
156 | ];
157 | }
158 |
159 | public function toJson()
160 | {
161 | return json_encode($this->toArray(), JSON_FORCE_OBJECT);
162 | }
163 | }
164 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # phpnsq
2 |
3 | [](https://travis-ci.org/okstuff/phpnsq)
4 |
5 | ## Install
6 | ```shell
7 | composer require okstuff/phpnsq
8 | ```
9 |
10 | ## Subscribe one topic and channel
11 | ```shell
12 | php examples/console phpnsq:sub
13 | ```
14 |
15 | ## Notice
16 | Before try this, you should install [nsq](http://nsq.io) by yourself.
17 |
18 | ## Examples
19 |
20 | 1. Publish
21 |
22 | [embedmd]:# (examples/publish.php php)
23 | ```php
24 | setTopic("sample_topic")->publish("Hello nsq.");
34 | var_dump($msg);
35 |
36 | //defered publish
37 | $msg = $phpnsq->setTopic("sample_topic")->publishDefer("this is a defered message.", 10);
38 | var_dump($msg);
39 |
40 | //multiple publish
41 | $messages = [
42 | "Hello, I am nsq client",
43 | "There are so many libraries developed by PHP",
44 | "Oh, no, PHP is not so good and slowly",
45 | ];
46 | $msg = $phpnsq->setTopic("sample_topic")->publishMulti(...$messages);
47 | var_dump($msg);
48 | ```
49 |
50 | 2. Subscribe
51 |
52 | [embedmd]:# (src/phpnsq/Cmd/Subscribe.php php)
53 | ```php
54 | setName(self::COMMAND_NAME)
70 | ->addArgument("topic", InputArgument::REQUIRED, "The topic you want to subscribe")
71 | ->addArgument("channel", InputArgument::REQUIRED, "The channel you want to subscribe")
72 | ->setDescription('subscribe new notification.')
73 | ->setHelp("This command allows you to subscribe notifications...");
74 | }
75 |
76 | public function execute(InputInterface $input, OutputInterface $output)
77 | {
78 | $phpnsq = self::$phpnsq;
79 | $phpnsq->setTopic($input->getArgument("topic"))
80 | ->setChannel($input->getArgument("channel"))
81 | ->subscribe($this, function (Message $message) use ($phpnsq, $output) {
82 | // $output->writeln($message->toJson());
83 | $phpnsq->getLogger()->info("READ", $message->toArray());
84 | });
85 | //excuted every five seconds.
86 | $this->addPeriodicTimer(5, function () use ($output) {
87 | $memory = memory_get_usage() / 1024;
88 | $formatted = number_format($memory, 3) . 'K';
89 | $output->writeln(date("Y-m-d H:i:s") . " ############ Current memory usage: {$formatted} ############");
90 | });
91 | $this->runLoop();
92 | }
93 | }
94 | ```
95 |
96 | 3. Console
97 |
98 | [embedmd]:# (examples/console php)
99 | ```php
100 | #!/usr/bin/env php
101 | add(new Subscribe($config, null));
113 |
114 | $application->run();
115 | ```
116 |
--------------------------------------------------------------------------------
/src/phpnsq/Stream/Reader.php:
--------------------------------------------------------------------------------
1 | conn = $conn;
23 | }
24 |
25 | public function bindConn(Nsqd $conn)
26 | {
27 | $this->conn = $conn;
28 |
29 | return $this;
30 | }
31 |
32 | public function bindFrame()
33 | {
34 | $size = 0;
35 | $type = 0;
36 | try {
37 | $size = $this->readInt(4);
38 | $type = $this->readInt(4);
39 | } catch (Exception $e) {
40 | throw new Exception("Error reading message frame [$size, $type] ({$e->getMessage()})");
41 | }
42 |
43 | $frame = [
44 | "size" => $size,
45 | "type" => $type,
46 | ];
47 |
48 | try {
49 | if (self::TYPE_RESPONSE == $type) {
50 | $frame["response"] = $this->readString($size - 4);
51 | } elseif (self::TYPE_ERROR == $type) {
52 | $frame["error"] = $this->readString($size - 4);
53 | }
54 | } catch (Exception $e) {
55 | throw new Exception("Error reading frame details [$size, $type] ({$e->getMessage()})");
56 | }
57 |
58 | $this->frame = $frame;
59 |
60 | return $this;
61 | }
62 |
63 | // DecodeMessage deserializes data (as []byte) and creates a new Message
64 | // message format:
65 | // [x][x][x][x][x][x][x][x][x][x][x][x][x][x][x][x][x][x][x][x][x][x][x][x][x][x][x][x][x][x]...
66 | // | (int64) || || (hex string encoded in ASCII) || (binary)
67 | // | 8-byte || || 16-byte || N-byte
68 | // ------------------------------------------------------------------------------------------...
69 | // nanosecond timestamp ^^ message ID message body
70 | // (uint16)
71 | // 2-byte
72 | // attempts
73 | public function getMessage()
74 | {
75 | $msg = null;
76 | if (null !== $this->frame) {
77 | switch ($this->frame["type"]) {
78 | case self::TYPE_MESSAGE:
79 | $msg = (new Message())->setTimestamp($this->readInt64(8))
80 | ->setAttempts($this->readUInt16(2))
81 | ->setId($this->readString(16))
82 | ->setBody($this->readString($this->frame["size"] - 30))
83 | ->setDecoded();
84 | break;
85 | case self::TYPE_RESPONSE:
86 | $msg = $this->frame["response"];
87 | break;
88 | case self::TYPE_ERROR:
89 | $msg = $this->frame["error"];
90 | break;
91 | }
92 |
93 | }
94 |
95 | return $msg;
96 | }
97 |
98 | public function isMessage()
99 | {
100 | return self::TYPE_MESSAGE == $this->frame["type"];
101 | }
102 |
103 | public function isHeartbeat()
104 | {
105 | return $this->isResponse(self::HEARTBEAT);
106 | }
107 |
108 | public function isOk()
109 | {
110 | return $this->isResponse(self::OK);
111 | }
112 |
113 | public function isResponse($response = null)
114 | {
115 | return isset($this->frame["response"])
116 | && self::TYPE_RESPONSE == $this->frame["type"]
117 | && (null === $response || $response === $this->frame["response"]);
118 | }
119 |
120 | private function readInt($size)
121 | {
122 | list(, $tmp) = unpack("N", $this->conn->read($size));
123 |
124 | return sprintf("%u", $tmp);
125 | }
126 |
127 | private function readInt64($size)
128 | {
129 | return IntPacker::int64($this->conn->read($size));
130 | }
131 |
132 | private function readUInt16($size)
133 | {
134 | return IntPacker::uInt16($this->conn->read($size));
135 | }
136 |
137 | private function readString($size)
138 | {
139 | $bytes = unpack("c{$size}chars", $this->conn->read($size));
140 |
141 | return implode(array_map("chr", $bytes));
142 | }
143 | }
144 |
--------------------------------------------------------------------------------
/src/phpnsq/PhpNsq.php:
--------------------------------------------------------------------------------
1 | reader = new reader();
25 | $this->logger = new Logging("PHPNSQ", $nsq["nsq"]["logdir"]);
26 | $this->pool = new Pool($nsq, $nsq["nsq"]["lookupd_switch"]);
27 | }
28 |
29 | public function getLogger()
30 | {
31 | return $this->logger;
32 | }
33 |
34 | public function setChannel(string $channel)
35 | {
36 | $this->channel = $channel;
37 |
38 | return $this;
39 | }
40 |
41 | public function setTopic(string $topic)
42 | {
43 | $this->topic = $topic;
44 |
45 | if ($this->pool->getLookupdCount() > 0) {
46 | $lookupd = $this->pool->getLookupd();
47 | $this->pool->addNsqdByLookupd($lookupd, $topic);
48 | }
49 |
50 | return $this;
51 | }
52 |
53 | public function publish(string $message)
54 | {
55 | $msg = null;
56 | try {
57 | $conn = $this->pool->getConn();
58 | $conn->write(Writer::pub($this->topic, $message));
59 |
60 | $msg = $this->reader->bindConn($conn)->bindFrame()->getMessage();
61 | } catch (Exception $e) {
62 | $this->logger->error("publish error", $e);
63 | $msg = $e->getMessage();
64 | }
65 |
66 | return $msg;
67 | }
68 |
69 | public function publishMulti(string ...$messages)
70 | {
71 | $msg = null;
72 | try {
73 | $conn = $this->pool->getConn();
74 | $conn->write(Writer::mpub($this->topic, $messages));
75 |
76 | $msg = $this->reader->bindConn($conn)->bindFrame()->getMessage();
77 | } catch (Exception $e) {
78 | $this->logger->error("publish error", $e);
79 | $msg = $e->getMessage();
80 | }
81 |
82 | return $msg;
83 | }
84 |
85 | public function publishDefer(string $message, int $deferTime)
86 | {
87 | $msg = null;
88 | try {
89 | $conn = $this->pool->getConn();
90 | $conn->write(Writer::dpub($this->topic, $deferTime, $message));
91 |
92 | $msg = $this->reader->bindConn($conn)->bindFrame()->getMessage();
93 | } catch (Exception $e) {
94 | $this->logger->error("publish error", $e);
95 | $msg = $e->getMessage();
96 | }
97 |
98 | return $msg;
99 | }
100 |
101 | public function subscribe(SubscribeCommand $cmd, Closure $callback)
102 | {
103 | try {
104 | $conn = $this->pool->getConn();
105 | $sock = $conn->getSock();
106 |
107 | $cmd->addReadStream($sock, function ($sock) use ($conn, $callback) {
108 | $this->handleMessage($conn, $callback);
109 | });
110 |
111 | $conn->write(Writer::sub($this->topic, $this->channel))
112 | ->write(Writer::rdy(1));
113 | } catch (Exception $e) {
114 | $this->logger->error("subscribe error", $e);
115 | }
116 | }
117 |
118 | protected function handleMessage(Nsqd $conn, Closure $callback)
119 | {
120 | $reader = $this->reader->bindConn($conn)->bindFrame();
121 |
122 | if ($reader->isHeartbeat()) {
123 | $conn->write(Writer::nop());
124 | } elseif ($reader->isMessage()) {
125 |
126 | $msg = $reader->getMessage();
127 | try {
128 | call_user_func($callback, $msg);
129 | } catch (Exception $e) {
130 | $this->logger->error("Will be requeued: ", $e->getMessage());
131 |
132 | $conn->write(Writer::touch($msg->getId()))
133 | ->write(Writer::req(
134 | $msg->getId(),
135 | $conn->getConfig()->get("defaultRequeueDelay")["default"]
136 | ));
137 | }
138 |
139 | $conn->write(Writer::fin($msg->getId()))
140 | ->write(Writer::rdy(1));
141 | } elseif ($reader->isOk()) {
142 | $this->logger->info('Ignoring "OK" frame in SUB loop');
143 | } else {
144 | $this->logger->error("Error/unexpected frame received: ", $reader);
145 | }
146 | }
147 | }
148 |
--------------------------------------------------------------------------------
/src/phpnsq/Conn/Nsqd.php:
--------------------------------------------------------------------------------
1 | config = $config;
22 | }
23 |
24 | public function getConfig()
25 | {
26 | return $this->config;
27 | }
28 |
29 | public function read($len = 0)
30 | {
31 | $data = '';
32 | $timeout = $this->config->get("readTimeout")["default"];
33 | $this->reader = [$sock = $this->getSock()];
34 | while (strlen($data) < $len) {
35 | $readable = Socket::select($this->reader, $this->writer, $timeout);
36 | if ($readable > 0) {
37 | $buffer = Socket::recvFrom($sock, $len);
38 | $data .= $buffer;
39 | $len -= strlen($buffer);
40 | }
41 | }
42 |
43 | return $data;
44 | }
45 |
46 | public function write($buffer)
47 | {
48 | $timeout = $this->config->get("writeTimeout")["default"];
49 | $this->writer = [$sock = $this->getSock()];
50 | while (strlen($buffer) > 0) {
51 | $writable = Socket::select($this->reader, $this->writer, $timeout);
52 | if ($writable > 0) {
53 | $buffer = substr($buffer, Socket::sendTo($sock, $buffer));
54 | }
55 | }
56 |
57 | return $this;
58 | }
59 |
60 | public function __destruct()
61 | {
62 | fclose($this->getSock());
63 | }
64 |
65 | public function getSock()
66 | {
67 | if (null === $this->sock) {
68 | $this->sock = Socket::client($this->config->host, $this->config->port);
69 |
70 | if (false === $this->config->get("blocking")) {
71 | stream_set_blocking($this->sock, 0);
72 | }
73 |
74 | $this->write(Writer::MAGIC_V2);
75 | $this->auth();
76 |
77 | //FIXME: Really shit php code.
78 | $tlsConfig=$this->config->get("tlsConfig");
79 | $context = $this->sock;
80 | if (null !== $tlsConfig) {
81 | $this->write(Writer::identify(["tls_v1" => true]));
82 |
83 | if ($tlsConfig["local_cert"]) {
84 | if (!file_exists($tlsConfig["local_cert"])) {
85 | throw new Exception("Local cert file not exists");
86 | }
87 | if (!stream_context_set_option($context, 'tcp', 'local_cert', $tlsConfig["local_cert"])) {
88 | throw new Exception("Could not set cert");
89 | }
90 | }
91 | if ($tlsConfig["local_pk"]) {
92 | if (!file_exists($tlsConfig["local_pk"])) {
93 | throw new Exception("Local pk file not exists");
94 | }
95 | if (!stream_context_set_option($context, 'tcp', 'local_pk', $tlsConfig["local_pk"])) {
96 | throw new Exception("Could not set local_pk");
97 | }
98 | }
99 | if ($tlsConfig["passphrase"] && !stream_context_set_option($context, 'tcp', 'passphrase', $tlsConfig["passphrase"])) {
100 | throw New Exception("Could not set passphrase for your ssl cert");
101 | }
102 | if ($tlsConfig["cn_match"] && !stream_context_set_option($context, 'tcp', 'CN_match', $tlsConfig["cn_match"])) {
103 | throw new Exception("Could not set CN_match");
104 | }
105 | if ($tlsConfig["peer_fingerprint"] && !stream_context_set_option($context, 'tcp', 'peer_fingerprint', $tlsConfig["peer_fingerprint"])) {
106 | throw new Exception("Could not set peer_fingerprint");
107 | }
108 | stream_context_set_option($context, 'tcp', 'allow_self_signed', true);
109 | stream_context_set_option($context, 'tcp', 'verify_peer', true);
110 | stream_context_set_option($context, 'tcp', 'cafile', $tlsConfig["cafile"]);
111 | }
112 |
113 | $this->sock = $context;
114 | }
115 |
116 | return $this->sock;
117 | }
118 |
119 | private function auth()
120 | {
121 | if ($this->config->get("authSwitch")) {
122 | $this->write(Writer::auth($this->config->get("authSecret")));
123 | $msg = (new Reader())->bindConn($this)->bindFrame()->getMessage();
124 | (new Logging("PHPNSQ", $this->config->get("logdir")))->info($msg);
125 | }
126 | }
127 | }
128 |
--------------------------------------------------------------------------------