Completed
Push — master ( 086e81...68ff47 )
by Camilo
02:31
created

Client::readBrokerHeader()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 4
Code Lines 2

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 0
CRAP Score 2

Importance

Changes 0
Metric Value
cc 1
eloc 2
nc 1
nop 0
dl 0
loc 4
ccs 0
cts 3
cp 0
crap 2
rs 10
c 0
b 0
f 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
 * 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 \unreal4u\MQTT\Exceptions\NotConnected
62
     * @throws \unreal4u\MQTT\Exceptions\Connect\NoConnectionParametersDefined
63
     * @throws \unreal4u\MQTT\Exceptions\ServerClosedConnection
64
     */
65
    public function __destruct()
66
    {
67
        if ($this->socket !== null) {
68
            $this->logger->info('Currently connected to broker, disconnecting from it');
69
70
            $this->processObject(new Disconnect($this->logger));
71
        }
72
    }
73
74
    /**
75
     * @inheritdoc
76
     */
77
    public function shutdownConnection(): bool
78
    {
79
        return stream_socket_shutdown($this->socket, STREAM_SHUT_RDWR);
80
    }
81
82
    /**
83
     * @inheritdoc
84
     */
85
    public function readBrokerData(int $bytes): string
86
    {
87
        $this->logger->debug('Reading bytes from socket', [
88
            'numberOfBytes' => $bytes,
89
            'isLocked' => $this->isCurrentlyLocked,
90
        ]);
91
        return fread($this->socket, $bytes);
92
    }
93
94
    /**
95
     * @inheritdoc
96
     */
97
    public function readBrokerHeader(): string
98
    {
99
        $this->logger->debug('Reading header from response');
100
        return $this->readBrokerData(4);
101
    }
102
103
    /**
104
     * @inheritdoc
105
     * @throws \unreal4u\MQTT\Exceptions\ServerClosedConnection
106
     * @throws \unreal4u\MQTT\Exceptions\NotConnected
107
     */
108
    public function sendBrokerData(WritableContentInterface $object): string
109
    {
110
        if ($this->socket === null) {
111
            $this->logger->alert('Not connected before sending data');
112
            throw new NotConnected('Please connect before performing any other request');
113
        }
114
115
        $writableString = $object->createSendableMessage();
116
        $sizeOfString = \strlen($writableString);
117
        $writtenBytes = fwrite($this->socket, $writableString, $sizeOfString);
118
        // $this->logger->debug('Sent string', ['binaryString' => str2bin($writableString)]); // Handy for debugging
119
        if ($writtenBytes !== $sizeOfString) {
120
            $this->logger->error('Written bytes do NOT correspond with size of string!', [
121
                'writtenBytes' => $writtenBytes,
122
                'sizeOfString' => $sizeOfString,
123
            ]);
124
125
            throw new ServerClosedConnection('The server may have disconnected the current client');
126
        }
127
        $this->logger->debug('Sent data to socket', ['writtenBytes' => $writtenBytes, 'sizeOfString' => $sizeOfString]);
128
129
        $returnValue = '';
130
        if ($object->shouldExpectAnswer() === true) {
131
            $this->enableSynchronousTransfer(true);
132
            $returnValue = $this->readBrokerHeader();
133
            $this->enableSynchronousTransfer(false);
134
        }
135
136
        return $returnValue;
137
    }
138
139
    /**
140
     * Special handling of the connect part: create the socket
141
     *
142
     * @param Connect $connection
143
     * @return bool
144
     * @throws \unreal4u\MQTT\Exceptions\Connect\NoConnectionParametersDefined
145
     */
146
    private function generateSocketConnection(Connect $connection): bool
147
    {
148
        $this->logger->debug('Creating socket connection');
149
        $this->connectionParameters = $connection->getConnectionParameters();
150
        $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...
151
            $this->connectionParameters->getConnectionUrl(),
152
            $errorNumber,
153
            $errorString,
154
            60,
155
            STREAM_CLIENT_CONNECT
156
        );
157
158
        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

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

160
        $this->logger->debug('Created socket connection successfully, continuing', stream_get_meta_data(/** @scrutinizer ignore-type */ $this->socket));
Loading history...
161
        return true;
162
    }
163
164
    /**
165
     * @inheritdoc
166
     */
167
    public function enableSynchronousTransfer(bool $newStatus): ClientInterface
168
    {
169
        $this->logger->debug('Setting new blocking status', ['newStatus' => $newStatus]);
170
        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

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