Passed
Push — master ( fc052b...13a97b )
by Camilo
02:52
created

Client::isItPingTime()   A

Complexity

Conditions 3
Paths 3

Size

Total Lines 9
Code Lines 6

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 0
CRAP Score 12

Importance

Changes 0
Metric Value
dl 0
loc 9
ccs 0
cts 6
cp 0
rs 9.6666
c 0
b 0
f 0
cc 3
eloc 6
nc 3
nop 0
crap 12
1
<?php
2
3
declare(strict_types=1);
4
5
namespace unreal4u\MQTT;
6
7
use Psr\Log\LoggerInterface;
8
use unreal4u\MQTT\Exceptions\NotConnected;
9
use unreal4u\MQTT\Exceptions\ServerClosedConnection;
10
use unreal4u\MQTT\Internals\ClientInterface;
11
use unreal4u\MQTT\Internals\ReadableContentInterface;
12
use unreal4u\MQTT\Internals\WritableContentInterface;
13
use unreal4u\MQTT\Protocol\Connect;
14
use unreal4u\MQTT\Protocol\Disconnect;
15
16
/**
17
 * Class Client
18
 * @package unreal4u\MQTT
19
 */
20
final class Client implements ClientInterface
21
{
22
    /**
23
     * Where all the magic happens
24
     * @var Resource
25
     */
26
    private $socket;
27
28
    /**
29
     * Logs all activity
30
     * @var LoggerInterface
31
     */
32
    private $logger;
33
34
    /**
35
     * Fast way to know whether we are connected or not
36
     * @var bool
37
     */
38
    private $isConnected = false;
39
40
    /**
41
     * Annotates the last time there was known to be communication with the MQTT server
42
     * @var \DateTimeImmutable
43
     */
44
    private $lastCommunication;
45
46
    /**
47
     * Internal holder of connection parameters
48
     * @var Connect\Parameters
49
     */
50
    private $connectionParameters;
51
52
    /**
53
     * @inheritdoc
54
     */
55
    public function __construct(LoggerInterface $logger = null)
56
    {
57
        if ($logger === null) {
58
            $logger = new DummyLogger();
59
        }
60
61
        $this->logger = $logger;
62
    }
63
64
    /**
65
     * @inheritdoc
66
     * @throws \unreal4u\MQTT\Exceptions\NotConnected
67
     * @throws \unreal4u\MQTT\Exceptions\Connect\NoConnectionParametersDefined
68
     * @throws \unreal4u\MQTT\Exceptions\ServerClosedConnection
69
     */
70
    public function __destruct()
71
    {
72
        if ($this->socket !== null) {
73
            $this->logger->info('Currently connected to broker, disconnecting from it');
74
75
            $this->sendData(new Disconnect($this->logger));
76
        }
77
    }
78
79
    /**
80
     * @inheritdoc
81
     */
82
    public function getSocket()
83
    {
84
        return $this->socket;
85
    }
86
87
    /**
88
     * @inheritdoc
89
     */
90
    public function readSocketData(int $bytes): string
91
    {
92
        $this->logger->debug('Reading bytes from socket', ['numberOfBytes' => $bytes]);
93
        return fread($this->socket, $bytes);
0 ignored issues
show
Bug Best Practice introduced by
The expression return fread($this->socket, $bytes) could return the type false which is incompatible with the type-hinted return string. Consider adding an additional type-check to rule them out.
Loading history...
94
    }
95
96
    /**
97
     * @inheritdoc
98
     */
99
    public function readSocketHeader(): string
100
    {
101
        $this->logger->debug('Reading header from response');
102
        return $this->readSocketData(4);
103
    }
104
105
    /**
106
     * @inheritdoc
107
     * @throws \unreal4u\MQTT\Exceptions\ServerClosedConnection
108
     * @throws \unreal4u\MQTT\Exceptions\NotConnected
109
     */
110
    public function sendSocketData(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
        if ($writtenBytes !== $sizeOfString) {
121
            $this->logger->error('Written bytes do NOT correspond with size of string!', [
122
                'writtenBytes' => $writtenBytes,
123
                'sizeOfString' => $sizeOfString,
124
            ]);
125
126
            throw new ServerClosedConnection('The server may have disconnected the current client');
127
        }
128
        $this->logger->debug('Sent data to socket', ['writtenBytes' => $writtenBytes, 'sizeOfString' => $sizeOfString]);
129
130
        if ($object->shouldExpectAnswer() === true) {
131
            return $this->readSocketHeader();
132
        }
133
134
        return '';
135
    }
136
137
    /**
138
     * Special handling of the connect part: create the socket
139
     *
140
     * @param Connect $connection
141
     * @return bool
142
     * @throws \unreal4u\MQTT\Exceptions\Connect\NoConnectionParametersDefined
143
     */
144
    private function generateSocketConnection(Connect $connection): bool
145
    {
146
        $this->logger->debug('Creating socket connection');
147
        $this->connectionParameters = $connection->getConnectionParameters();
148
        $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...
149
            $this->connectionParameters->getConnectionUrl(),
150
            $errorNumber,
151
            $errorString,
152
            60,
153
            STREAM_CLIENT_CONNECT
154
        );
155
156
        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

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

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

169
        stream_set_blocking($this->socket, /** @scrutinizer ignore-type */ $newStatus);
Loading history...
170
        return $this;
171
    }
172
173
    /**
174
     * @inheritdoc
175
     * @throws \unreal4u\MQTT\Exceptions\ServerClosedConnection
176
     * @throws \unreal4u\MQTT\Exceptions\Connect\NoConnectionParametersDefined
177
     * @throws \unreal4u\MQTT\Exceptions\NotConnected
178
     */
179
    public function sendData(WritableContentInterface $object): ReadableContentInterface
180
    {
181
        $currentObject = \get_class($object);
182
        $this->logger->debug('Validating object', ['object' => $currentObject]);
183
184
        if ($object instanceof Connect) {
185
            $this->generateSocketConnection($object);
186
        }
187
188
        $this->logger->info('About to send data', ['object' => $currentObject]);
189
        $readableContent = $object->expectAnswer($this->sendSocketData($object), $this);
190
        /*
191
         * Some objects must perform certain actions on the connection, for example:
192
         * - ConnAck must set the connected bit
193
         * - PingResp must reset the internal last-communication datetime
194
         */
195
        $this->logger->debug('Executing special actions for this object', [
196
            'originObject' => $currentObject,
197
            'responseObject' => \get_class($readableContent),
198
        ]);
199
        $readableContent->performSpecialActions($this, $object);
200
201
        return $readableContent;
202
    }
203
204
    /**
205
     * @inheritdoc
206
     */
207
    public function isItPingTime(): bool
208
    {
209
        $secondsDifference = (new \DateTime('now'))->getTimestamp() - $this->lastCommunication->getTimestamp();
210
        $this->logger->debug('Checking time difference', ['secondsDifference' => $secondsDifference]);
211
212
        return
213
            $this->isConnected() &&
214
            $this->connectionParameters->getKeepAlivePeriod() > 0 &&
215
            $secondsDifference >= $this->connectionParameters->getKeepAlivePeriod();
216
    }
217
218
    /**
219
     * @inheritdoc
220
     */
221
    public function updateLastCommunication(): ClientInterface
222
    {
223
        $lastCommunication = null;
224
        if ($this->lastCommunication !== null) {
225
            $lastCommunication = $this->lastCommunication->format('Y-m-d H:i:s.u');
226
        }
227
        // "now" does not support microseconds, so create the timestamp with a format that does
228
        $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...
Bug introduced by
The call to DateTimeImmutable::createFromFormat() has too few arguments starting with timezone. ( Ignorable by Annotation )

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

228
        /** @scrutinizer ignore-call */ 
229
        $this->lastCommunication = \DateTimeImmutable::createFromFormat('U.u', sprintf('%.6F', microtime(true)));

This check compares calls to functions or methods with their respective definitions. If the call has less arguments than are defined, it raises an issue.

If a function is defined several times with a different number of parameters, the check may pick up the wrong definition and report false positives. One codebase where this has been known to happen is Wordpress. Please note the @ignore annotation hint above.

Loading history...
229
        $this->logger->debug('Updating internal last communication timestamp', [
230
            'previousValue' => $lastCommunication,
231
            'currentValue' => $this->lastCommunication->format('Y-m-d H:i:s.u'),
232
        ]);
233
        return $this;
234
    }
235
236
    /**
237
     * @inheritdoc
238
     */
239
    public function setConnected(bool $isConnected): ClientInterface
240
    {
241
        $this->logger->debug('Setting internal connected property', ['connected' => $isConnected]);
242
        $this->isConnected = $isConnected;
243
        if ($this->isConnected() === false) {
244
            $this->socket = null;
245
        }
246
247
        return $this;
248
    }
249
250
    /**
251
     * @inheritdoc
252
     */
253
    public function isConnected(): bool
254
    {
255
        return $this->isConnected;
256
    }
257
}
258