Completed
Push — master ( e13f3c...396657 )
by Camilo
04:51
created

Client::setSocketTimeout()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 7
Code Lines 4

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
dl 0
loc 7
rs 9.4285
c 0
b 0
f 0
cc 1
eloc 4
nc 1
nop 0
1
<?php
2
3
declare(strict_types=1);
4
5
namespace unreal4u\MQTT;
6
7
use unreal4u\MQTT\Application\EmptyWritableResponse;
8
use unreal4u\MQTT\Exceptions\NotConnected;
9
use unreal4u\MQTT\Exceptions\ServerClosedConnection;
10
use unreal4u\MQTT\Internals\ClientInterface;
11
use unreal4u\MQTT\Internals\ProtocolBase;
12
use unreal4u\MQTT\Internals\ReadableContentInterface;
13
use unreal4u\MQTT\Internals\WritableContentInterface;
14
use unreal4u\MQTT\Protocol\Connect;
15
use unreal4u\MQTT\Protocol\Disconnect;
16
17
/**
18
 * Example working client that implements all methods from the mandatory ClientInterface
19
 * @package unreal4u\MQTT
20
 */
21
final class Client extends ProtocolBase implements ClientInterface
22
{
23
    /**
24
     * Where all the magic happens
25
     * @var Resource
26
     */
27
    private $socket;
28
29
    /**
30
     * Fast way to know whether we are connected or not
31
     * @var bool
32
     */
33
    private $isConnected = false;
34
35
    /**
36
     * Fast way to know whether we are currently in locked mode or not
37
     * @var bool
38
     */
39
    private $isCurrentlyLocked = false;
40
41
    /**
42
     * Annotates the last time there was known to be communication with the MQTT server
43
     * @var \DateTimeImmutable
44
     */
45
    private $lastCommunication;
46
47
    /**
48
     * Internal holder of connection parameters
49
     * @var Connect\Parameters
50
     */
51
    private $connectionParameters;
52
53
    /**
54
     * Temporary holder for async requests so that they can be handled synchronously
55
     * @var WritableContentInterface[]
56
     */
57
    private $objectStack = [];
58
59
    /**
60
     * @inheritdoc
61
     * @throws \LogicException
62
     * @throws \unreal4u\MQTT\Exceptions\NonMatchingPacketIdentifiers
63
     * @throws \unreal4u\MQTT\Exceptions\NotConnected
64
     * @throws \unreal4u\MQTT\Exceptions\Connect\NoConnectionParametersDefined
65
     * @throws \unreal4u\MQTT\Exceptions\ServerClosedConnection
66
     */
67
    public function __destruct()
68
    {
69
        if ($this->isConnected() === true) {
70
            $this->logger->info('Currently connected to broker, disconnecting from it');
71
72
            $this->processObject(new Disconnect($this->logger));
73
        }
74
    }
75
76
    /**
77
     * @inheritdoc
78
     */
79
    public function shutdownConnection(): bool
80
    {
81
        return stream_socket_shutdown($this->socket, STREAM_SHUT_RDWR);
82
    }
83
84
    /**
85
     * @inheritdoc
86
     */
87
    public function readBrokerData(int $bytes): string
88
    {
89
        $this->logger->debug('Reading bytes from socket', [
90
            'numberOfBytes' => $bytes,
91
            'isLocked' => $this->isCurrentlyLocked,
92
        ]);
93
        return fread($this->socket, $bytes);
94
    }
95
96
    /**
97
     * @inheritdoc
98
     */
99
    public function readBrokerHeader(): string
100
    {
101
        $this->logger->debug('Reading header from response');
102
        return $this->readBrokerData(4);
103
    }
104
105
    /**
106
     * @inheritdoc
107
     * @throws \unreal4u\MQTT\Exceptions\ServerClosedConnection
108
     * @throws \unreal4u\MQTT\Exceptions\NotConnected
109
     */
110
    public function sendBrokerData(WritableContentInterface $object): string
111
    {
112
        if ($this->socket === null) {
113
            $this->logger->alert('Not connected before sending data');
114
            throw new NotConnected('Please connect before performing any other request');
115
        }
116
117
        $writableString = $object->createSendableMessage();
118
        $sizeOfString = \strlen($writableString);
119
        $writtenBytes = fwrite($this->socket, $writableString, $sizeOfString);
120
        // $this->logger->debug('Sent string', ['binaryString' => str2bin($writableString)]); // Handy for debugging
121
        if ($writtenBytes !== $sizeOfString) {
122
            $this->logger->error('Written bytes do NOT correspond with size of string!', [
123
                'writtenBytes' => $writtenBytes,
124
                'sizeOfString' => $sizeOfString,
125
            ]);
126
127
            throw new ServerClosedConnection('The server may have disconnected the current client');
128
        }
129
130
        $this->logger->debug('Sent data to socket', ['writtenBytes' => $writtenBytes, 'sizeOfString' => $sizeOfString]);
131
        return $this->checkAndReturnAnswer($object);
132
    }
133
134
    /**
135
     * Checks on the writable object whether we should wait for an answer and either wait or return an empty string
136
     *
137
     * @param WritableContentInterface $object
138
     * @return string
139
     */
140
    private function checkAndReturnAnswer(WritableContentInterface $object): string
141
    {
142
        $returnValue = '';
143
        if ($object->shouldExpectAnswer() === true) {
144
            $this->enableSynchronousTransfer(true);
145
            $returnValue = $this->readBrokerHeader();
146
            $this->enableSynchronousTransfer(false);
147
        }
148
149
        return $returnValue;
150
    }
151
152
    /**
153
     * Checks for socket error connections, will throw an exception if any is found
154
     *
155
     * @param int $errorCode
156
     * @param string $errorDescription
157
     * @return Client
158
     * @throws \unreal4u\MQTT\Exceptions\NotConnected
159
     */
160
    private function checkForConnectionErrors(int $errorCode, string $errorDescription): self
161
    {
162
        if ($errorCode !== 0 || $this->socket === false) {
163
            $this->logger->critical('Could not connect to broker', [
164
                'errorCode' => $errorCode,
165
                'errorDescription' => $errorDescription,
166
            ]);
167
168
            throw new NotConnected('Could not connect to broker: ' . $errorDescription, $errorCode);
169
        }
170
171
        return $this;
172
    }
173
174
    /**
175
     * Special handling of the connect part: create the socket
176
     *
177
     * @param Connect $connection
178
     * @return bool
179
     * @throws \unreal4u\MQTT\Exceptions\NotConnected
180
     * @throws \unreal4u\MQTT\Exceptions\Connect\NoConnectionParametersDefined
181
     */
182
    private function generateSocketConnection(Connect $connection): bool
183
    {
184
        $this->logger->debug('Creating socket connection');
185
        $this->connectionParameters = $connection->getConnectionParameters();
186
        $this->socket = stream_socket_client(
0 ignored issues
show
Documentation Bug introduced by
It seems like stream_socket_client($th...\STREAM_CLIENT_CONNECT) can also be of type false. However, the property $socket is declared as type resource. Maybe add an additional type check?

Our type inference engine has found a suspicous assignment of a value to a property. This check raises an issue when a value that can be of a mixed type is assigned to a property that is type hinted more strictly.

For example, imagine you have a variable $accountId that can either hold an Id object or false (if there is no account id yet). Your code now assigns that value to the id property of an instance of the Account class. This class holds a proper account, so the id value must no longer be false.

Either this assignment is in error or a type check should be added for that assignment.

class Id
{
    public $id;

    public function __construct($id)
    {
        $this->id = $id;
    }

}

class Account
{
    /** @var  Id $id */
    public $id;
}

$account_id = false;

if (starsAreRight()) {
    $account_id = new Id(42);
}

$account = new Account();
if ($account instanceof Id)
{
    $account->id = $account_id;
}
Loading history...
187
            $this->connectionParameters->getConnectionUrl(),
188
            $errorCode,
189
            $errorDescription,
190
            60,
191
            STREAM_CLIENT_CONNECT
192
        );
193
194
        $this
195
            ->checkForConnectionErrors($errorCode, $errorDescription)
196
            ->setSocketTimeout();
197
198
        $this->logger->debug('Created socket connection successfully, continuing', stream_get_meta_data($this->socket));
0 ignored issues
show
Bug introduced by
It seems like $this->socket can also be of type false; however, parameter $stream of stream_get_meta_data() does only seem to accept resource, maybe add an additional type check? ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-type  annotation

198
        $this->logger->debug('Created socket connection successfully, continuing', stream_get_meta_data(/** @scrutinizer ignore-type */ $this->socket));
Loading history...
199
        return true;
200
    }
201
202
    /**
203
     * Calculates and sets the timeout on the socket connection according to the MQTT standard
204
     *
205
     * 1.5 times the keep alive period is the maximum amount of time the connection may remain idle before the
206
     * broker decides to close it.
207
     * Odd numbers will also produce a 0.5 second extra time, take this into account as well
208
     *
209
     * @return Client
210
     */
211
    private function setSocketTimeout(): self
212
    {
213
        $timeCalculation = $this->connectionParameters->getKeepAlivePeriod() * 1.5;
214
        $seconds = (int)floor($timeCalculation);
215
        stream_set_timeout($this->socket, $seconds, (int)($timeCalculation - $seconds) * 1000);
216
217
        return $this;
218
    }
219
220
    /**
221
     * @inheritdoc
222
     */
223
    public function enableSynchronousTransfer(bool $newStatus): ClientInterface
224
    {
225
        $this->logger->debug('Setting new blocking status', ['newStatus' => $newStatus]);
226
        stream_set_blocking($this->socket, $newStatus);
0 ignored issues
show
Bug introduced by
$newStatus of type boolean is incompatible with the type integer expected by parameter $mode of stream_set_blocking(). ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-type  annotation

226
        stream_set_blocking($this->socket, /** @scrutinizer ignore-type */ $newStatus);
Loading history...
227
        $this->isCurrentlyLocked = $newStatus;
228
        return $this;
229
    }
230
231
    /**
232
     * Stuff that has to happen before we actually begin sending data through our socket
233
     *
234
     * @param WritableContentInterface $object
235
     * @return Client
236
     * @throws \unreal4u\MQTT\Exceptions\NotConnected
237
     * @throws \unreal4u\MQTT\Exceptions\Connect\NoConnectionParametersDefined
238
     */
239
    private function preSocketCommunication(WritableContentInterface $object): self
240
    {
241
        $this->objectStack[$object::getControlPacketValue()] = $object;
242
243
        if ($object instanceof Connect) {
244
            $this->generateSocketConnection($object);
245
        }
246
247
        return $this;
248
    }
249
250
    /**
251
     * Checks in the object stack whether there is some method that might issue the current ReadableContent
252
     *
253
     * @param ReadableContentInterface $readableContent
254
     * @return WritableContentInterface
255
     * @throws \LogicException
256
     */
257
    private function postSocketCommunication(ReadableContentInterface $readableContent): WritableContentInterface
258
    {
259
        $originPacket = null;
260
261
        $originPacketIdentifier = $readableContent->getOriginControlPacket();
262
        if (array_key_exists($originPacketIdentifier, $this->objectStack)) {
263
            $this->logger->debug('Origin packet found, returning it', ['originKey' => $originPacketIdentifier]);
264
            $originPacket = $this->objectStack[$originPacketIdentifier];
265
            unset($this->objectStack[$originPacketIdentifier]);
266
        } elseif ($originPacketIdentifier === 0) {
267
            $originPacket = new EmptyWritableResponse($this->logger);
268
        } else {
269
            $this->logger->warning('No origin packet found!', [
270
                'originKey' => $originPacketIdentifier,
271
                'stack' => array_keys($this->objectStack),
272
            ]);
273
            #throw new \LogicException('No origin instance could be found in the stack, please check');
274
            $originPacket = new EmptyWritableResponse($this->logger);
275
        }
276
277
        return $originPacket;
278
    }
279
280
    /**
281
     * @inheritdoc
282
     * @throws \LogicException
283
     * @throws \unreal4u\MQTT\Exceptions\NonMatchingPacketIdentifiers
284
     * @throws \unreal4u\MQTT\Exceptions\ServerClosedConnection
285
     * @throws \unreal4u\MQTT\Exceptions\Connect\NoConnectionParametersDefined
286
     * @throws \unreal4u\MQTT\Exceptions\NotConnected
287
     */
288
    public function processObject(WritableContentInterface $object): ReadableContentInterface
289
    {
290
        $currentObject = \get_class($object);
291
        $this->logger->debug('Validating object', ['object' => $currentObject]);
292
293
        $this->preSocketCommunication($object);
294
295
        $this->logger->info('About to send data', ['object' => $currentObject]);
296
        $readableContent = $object->expectAnswer($this->sendBrokerData($object), $this);
297
        /*
298
         * Some objects must perform certain actions on the connection, for example:
299
         * - ConnAck must set the connected bit
300
         * - PingResp must reset the internal last-communication datetime
301
         */
302
        $this->logger->debug('Checking stack and performing special operations', [
303
            'originObject' => $currentObject,
304
            'responseObject' => \get_class($readableContent),
305
        ]);
306
307
        $readableContent->performSpecialActions($this, $this->postSocketCommunication($readableContent));
308
309
        return $readableContent;
310
    }
311
312
    /**
313
     * @inheritdoc
314
     */
315
    public function isItPingTime(): bool
316
    {
317
        $secondsDifference = (new \DateTime('now'))->getTimestamp() - $this->lastCommunication->getTimestamp();
318
        $this->logger->debug('Checking time difference', [
319
            'secondsDifference' => $secondsDifference,
320
            'keepAlivePeriod' => $this->connectionParameters->getKeepAlivePeriod(),
321
        ]);
322
323
        return
324
            $this->isConnected() &&
325
            $this->connectionParameters->getKeepAlivePeriod() > 0 &&
326
            $secondsDifference >= $this->connectionParameters->getKeepAlivePeriod()# &&
327
            #!array_key_exists(PingReq::CONTROL_PACKET_VALUE, $this->objectStack)
328
            ;
329
    }
330
331
    /**
332
     * @inheritdoc
333
     */
334
    public function updateLastCommunication(): ClientInterface
335
    {
336
        $lastCommunication = null;
337
        if ($this->lastCommunication !== null) {
338
            $lastCommunication = $this->lastCommunication->format('Y-m-d H:i:s.u');
339
        }
340
        // "now" does not support microseconds, so create the timestamp with a format that does
341
        $this->lastCommunication = \DateTimeImmutable::createFromFormat('U.u', sprintf('%.6F', microtime(true)));
0 ignored issues
show
Documentation Bug introduced by
It seems like DateTimeImmutable::creat....6F', microtime(true))) can also be of type false. However, the property $lastCommunication is declared as type DateTimeImmutable. Maybe add an additional type check?

Our type inference engine has found a suspicous assignment of a value to a property. This check raises an issue when a value that can be of a mixed type is assigned to a property that is type hinted more strictly.

For example, imagine you have a variable $accountId that can either hold an Id object or false (if there is no account id yet). Your code now assigns that value to the id property of an instance of the Account class. This class holds a proper account, so the id value must no longer be false.

Either this assignment is in error or a type check should be added for that assignment.

class Id
{
    public $id;

    public function __construct($id)
    {
        $this->id = $id;
    }

}

class Account
{
    /** @var  Id $id */
    public $id;
}

$account_id = false;

if (starsAreRight()) {
    $account_id = new Id(42);
}

$account = new Account();
if ($account instanceof Id)
{
    $account->id = $account_id;
}
Loading history...
342
        $this->logger->debug('Updating internal last communication timestamp', [
343
            'previousValue' => $lastCommunication,
344
            'currentValue' => $this->lastCommunication->format('Y-m-d H:i:s.u'),
345
        ]);
346
        return $this;
347
    }
348
349
    /**
350
     * @inheritdoc
351
     */
352
    public function setConnected(bool $isConnected): ClientInterface
353
    {
354
        $this->logger->debug('Setting internal connected property', ['connected' => $isConnected]);
355
        $this->isConnected = $isConnected;
356
        if ($this->isConnected() === false) {
357
            $this->socket = null;
358
        }
359
360
        return $this;
361
    }
362
363
    /**
364
     * @inheritdoc
365
     */
366
    public function isConnected(): bool
367
    {
368
        return $this->isConnected;
369
    }
370
}
371