Passed
Push — master ( 1a5854...5fb2f0 )
by Camilo
02:34
created

Client::checkForConnectionErrors()   A

Complexity

Conditions 2
Paths 2

Size

Total Lines 12
Code Lines 6

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 0
CRAP Score 6

Importance

Changes 0
Metric Value
dl 0
loc 12
ccs 0
cts 7
cp 0
rs 9.4285
c 0
b 0
f 0
cc 2
eloc 6
nc 2
nop 2
crap 6
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
 * Class Client
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\UnmatchingPacketIdentifiers
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
        $this->logger->debug('Sent data to socket', ['writtenBytes' => $writtenBytes, 'sizeOfString' => $sizeOfString]);
130
131
        $returnValue = '';
132
        if ($object->shouldExpectAnswer() === true) {
133
            $this->enableSynchronousTransfer(true);
134
            $returnValue = $this->readBrokerHeader();
135
            $this->enableSynchronousTransfer(false);
136
        }
137
138
        return $returnValue;
139
    }
140
141
    /**
142
     * Checks for socket error connections, will throw an exception if any is found
143
     *
144
     * @param int $errorCode
145
     * @param string $errorDescription
146
     * @return Client
147
     * @throws \unreal4u\MQTT\Exceptions\NotConnected
148
     */
149
    private function checkForConnectionErrors(int $errorCode, string $errorDescription): self
150
    {
151
        if ($errorCode !== 0) {
152
            $this->logger->critical('Could not connect to broker', [
153
                'errorCode' => $errorCode,
154
                'errorDescription' => $errorDescription,
155
            ]);
156
157
            throw new NotConnected('Could not connect to broker: ' . $errorDescription, $errorCode);
158
        }
159
160
        return $this;
161
    }
162
163
    /**
164
     * Special handling of the connect part: create the socket
165
     *
166
     * @param Connect $connection
167
     * @return bool
168
     * @throws \unreal4u\MQTT\Exceptions\NotConnected
169
     * @throws \unreal4u\MQTT\Exceptions\Connect\NoConnectionParametersDefined
170
     */
171
    private function generateSocketConnection(Connect $connection): bool
172
    {
173
        $this->logger->debug('Creating socket connection');
174
        $this->connectionParameters = $connection->getConnectionParameters();
175
        $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...
176
            $this->connectionParameters->getConnectionUrl(),
177
            $errorCode,
178
            $errorDescription,
179
            60,
180
            STREAM_CLIENT_CONNECT
181
        );
182
183
        $this->checkForConnectionErrors($errorCode, $errorDescription);
184
185
        stream_set_timeout($this->socket, (int)floor($this->connectionParameters->getKeepAlivePeriod() * 1.5));
0 ignored issues
show
Bug introduced by
It seems like $this->socket can also be of type false; however, parameter $stream of stream_set_timeout() 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

185
        stream_set_timeout(/** @scrutinizer ignore-type */ $this->socket, (int)floor($this->connectionParameters->getKeepAlivePeriod() * 1.5));
Loading history...
186
187
        $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

187
        $this->logger->debug('Created socket connection successfully, continuing', stream_get_meta_data(/** @scrutinizer ignore-type */ $this->socket));
Loading history...
188
        return true;
189
    }
190
191
    /**
192
     * @inheritdoc
193
     */
194
    public function enableSynchronousTransfer(bool $newStatus): ClientInterface
195
    {
196
        $this->logger->debug('Setting new blocking status', ['newStatus' => $newStatus]);
197
        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

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