StreamSocketClient::write()   A
last analyzed

Complexity

Conditions 6
Paths 5

Size

Total Lines 34
Code Lines 17

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 6
CRAP Score 7.3329

Importance

Changes 4
Bugs 0 Features 0
Metric Value
cc 6
eloc 17
c 4
b 0
f 0
nc 5
nop 1
dl 0
loc 34
rs 9.0777
ccs 6
cts 9
cp 0.6667
crap 7.3329
1
<?php
2
declare(strict_types=1);
3
4
/*
5
 * This file is part of the php-gelf package.
6
 *
7
 * (c) Benjamin Zikarsky <http://benjamin-zikarsky.de>
8
 *
9
 * For the full copyright and license information, please view the LICENSE
10
 * file that was distributed with this source code.
11
 */
12
13
namespace Gelf\Transport;
14
15
use RuntimeException;
16
17
/**
18
 * StreamSocketClient is a very simple OO-Wrapper around the PHP
19
 * stream_socket-library and some specific stream-functions like
20
 * fwrite, etc.
21
 *
22
 * @author Benjamin Zikarsky <[email protected]>
23
 */
24
class StreamSocketClient
25
{
26
    private const DEFAULT_SOCKET_TIMEOUT = 30;
27
28
    private mixed $socket = null;
29
    private int $connectTimeout = self::DEFAULT_SOCKET_TIMEOUT;
30
31
    public function __construct(
32
        private string $scheme,
33
        private string $host,
34
        private int $port,
35
        private array $context = []
36
    ) {
37
    }
38
39
    /**
40
     * Destructor, closes socket if possible
41
     */
42
    public function __destruct()
43
    {
44
        $this->close();
45
    }
46
47
48
    /**
49
     * Internal function mimicking the behaviour of static::initSocket
50
     * which will get new functionality instead of the deprecated
51
     * "factory"
52
     *
53
     * @return resource
54
     *
55
     * @throws RuntimeException on connection-failure
56
     */
57
    private function buildSocket(): mixed
58
    {
59
        $socketDescriptor = sprintf(
60
            "%s://%s:%d",
61
            $this->scheme,
62
            $this->host,
63
            $this->port
64
        );
65
66
        $socket = @stream_socket_client(
67 45
            $socketDescriptor,
68
            $errNo,
69 45
            $errStr,
70 45
            $this->connectTimeout,
71 45
            \STREAM_CLIENT_CONNECT,
72 45
            stream_context_create($this->context)
73 45
        );
74
75
        if ($socket === false) {
76
            throw new RuntimeException(
77
                sprintf(
78 33
                    "Failed to create socket-client for %s: %s (%s)",
79
                    $socketDescriptor,
80 33
                    $errStr,
81 33
                    $errNo
82
                )
83
            );
84
        }
85
86
        // set non-blocking for UDP
87
        if (strcasecmp("udp", $this->scheme) == 0) {
88
            stream_set_blocking($socket, true);
89
        }
90
91
        return $socket;
92
    }
93
94
    /**
95
     * Returns raw-socket-resource
96
     *
97
     * @return resource
98
     */
99
    public function getSocket(): mixed
100
    {
101
        // lazy initializing of socket-descriptor
102
        if (!$this->socket) {
103
            $this->socket = $this->buildSocket();
104
        }
105
106
        return $this->socket;
107
    }
108
109
    /**
110
     * Writes a given string to the socket and returns the
111
     * number of written bytes
112
     */
113
    public function write(string $buffer): int
114
    {
115
        $bufLen = strlen($buffer);
116
117
        $socket = $this->getSocket();
118
        $written = 0;
119
120
        while ($written < $bufLen) {
121
            // PHP's fwrite does not behave nice in regard to errors, so we wrap
122
            // it with a temporary error handler and treat every warning/notice as
123
            // an error
124
            $failed = false;
125
            $errorMessage = "Failed to write to socket";
126
            /** @psalm-suppress InvalidArgument */
127
            set_error_handler(function ($errno, $errstr) use (&$failed, &$errorMessage) {
128
                $failed = true;
129
                $errorMessage .= ": $errstr ($errno)";
130
            });
131
            $byteCount = fwrite($socket, substr($buffer, $written));
132
            restore_error_handler();
133
134
            if ($byteCount === 0 && defined('HHVM_VERSION')) {
135
                $failed = true;
136
            }
137
138 15
            if ($failed || $byteCount === false) {
139
                throw new RuntimeException($errorMessage);
140 15
            }
141 15
142 15
            $written += $byteCount;
143 15
        }
144 15
145
146
        return $written;
147 15
    }
148 15
149
    /**
150
     * Reads a given number of bytes from the socket
151 15
     */
152 15
    public function read(int $byteCount): string
153 15
    {
154
        return fread($this->getSocket(), $byteCount);
155
    }
156 15
157 1
    /**
158
     * Closes underlying socket explicitly
159 1
     */
160
    public function close(): void
161
    {
162
        if (!is_resource($this->socket)) {
163
            return;
164
        }
165
166
        fclose($this->socket);
167
        $this->socket = null;
168 14
    }
169 2
170
    /**
171
     * Checks if the socket is closed
172 14
     */
173
    public function isClosed(): bool
174
    {
175
        return $this->socket === null;
176
    }
177
178
    /**
179
     * Returns the current connect-timeout
180 15
     */
181
    public function getConnectTimeout(): int
182
    {
183 15
        return $this->connectTimeout;
184 15
    }
185
186
    /**
187 14
     * Sets the connect-timeout
188
     */
189
    public function setConnectTimeout(int $timeout): void
190
    {
191
        if (!$this->isClosed()) {
192
            throw new \LogicException("Cannot change socket properties with an open connection");
193
        }
194
195
        $this->connectTimeout = $timeout;
196
    }
197
198
    /**
199
     * Returns the stream context
200 8
     */
201
    public function getContext(): array
202 8
    {
203 8
        return $this->context;
204
    }
205 8
206 8
    /**
207
     * Sets the stream context
208 8
     */
209
    public function setContext(array $context): void
210
    {
211
        if (!$this->isClosed()) {
212 8
            throw new \LogicException("Cannot change socket properties with an open connection");
213 8
        }
214
215 1
        $this->context = $context;
216 1
    }
217
}
218