Passed
Push — master ( bc3840...800ced )
by Camilo
01: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 unreal4u\MQTT\Exceptions\NotConnected;
8
use unreal4u\MQTT\Exceptions\ServerClosedConnection;
9
use unreal4u\MQTT\Internals\ClientInterface;
10
use unreal4u\MQTT\Internals\ProtocolBase;
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 extends ProtocolBase implements ClientInterface
21
{
22
    /**
23
     * Where all the magic happens
24
     * @var Resource
25
     */
26
    private $socket;
27
28
    /**
29
     * Fast way to know whether we are connected or not
30
     * @var bool
31
     */
32
    private $isConnected = false;
33
34
    /**
35
     * Annotates the last time there was known to be communication with the MQTT server
36
     * @var \DateTimeImmutable
37
     */
38
    private $lastCommunication;
39
40
    /**
41
     * Internal holder of connection parameters
42
     * @var Connect\Parameters
43
     */
44
    private $connectionParameters;
45
46
    /**
47
     * @inheritdoc
48
     * @throws \unreal4u\MQTT\Exceptions\NotConnected
49
     * @throws \unreal4u\MQTT\Exceptions\Connect\NoConnectionParametersDefined
50
     * @throws \unreal4u\MQTT\Exceptions\ServerClosedConnection
51
     */
52
    public function __destruct()
53
    {
54
        if ($this->socket !== null) {
55
            $this->logger->info('Currently connected to broker, disconnecting from it');
56
57
            $this->sendData(new Disconnect($this->logger));
58
        }
59
    }
60
61
    /**
62
     * @inheritdoc
63
     */
64
    public function getSocket()
65
    {
66
        return $this->socket;
67
    }
68
69
    /**
70
     * @inheritdoc
71
     */
72
    public function readSocketData(int $bytes): string
73
    {
74
        $this->logger->debug('Reading bytes from socket', ['numberOfBytes' => $bytes]);
75
        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...
76
    }
77
78
    /**
79
     * @inheritdoc
80
     */
81
    public function readSocketHeader(): string
82
    {
83
        $this->logger->debug('Reading header from response');
84
        return $this->readSocketData(4);
85
    }
86
87
    /**
88
     * @inheritdoc
89
     * @throws \unreal4u\MQTT\Exceptions\ServerClosedConnection
90
     * @throws \unreal4u\MQTT\Exceptions\NotConnected
91
     */
92
    public function sendSocketData(WritableContentInterface $object): string
93
    {
94
        if ($this->socket === null) {
95
            $this->logger->alert('Not connected before sending data');
96
            throw new NotConnected('Please connect before performing any other request');
97
        }
98
99
        $writableString = $object->createSendableMessage();
100
        $sizeOfString = \strlen($writableString);
101
        $writtenBytes = fwrite($this->socket, $writableString, $sizeOfString);
102
        if ($writtenBytes !== $sizeOfString) {
103
            $this->logger->error('Written bytes do NOT correspond with size of string!', [
104
                'writtenBytes' => $writtenBytes,
105
                'sizeOfString' => $sizeOfString,
106
            ]);
107
108
            throw new ServerClosedConnection('The server may have disconnected the current client');
109
        }
110
        $this->logger->debug('Sent data to socket', ['writtenBytes' => $writtenBytes, 'sizeOfString' => $sizeOfString]);
111
112
        if ($object->shouldExpectAnswer() === true) {
113
            return $this->readSocketHeader();
114
        }
115
116
        return '';
117
    }
118
119
    /**
120
     * Special handling of the connect part: create the socket
121
     *
122
     * @param Connect $connection
123
     * @return bool
124
     * @throws \unreal4u\MQTT\Exceptions\Connect\NoConnectionParametersDefined
125
     */
126
    private function generateSocketConnection(Connect $connection): bool
127
    {
128
        $this->logger->debug('Creating socket connection');
129
        $this->connectionParameters = $connection->getConnectionParameters();
130
        $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...
131
            $this->connectionParameters->getConnectionUrl(),
132
            $errorNumber,
133
            $errorString,
134
            60,
135
            STREAM_CLIENT_CONNECT
136
        );
137
138
        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

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

141
        $this->logger->debug('Created socket connection successfully, continuing', stream_get_meta_data(/** @scrutinizer ignore-type */ $this->socket));
Loading history...
142
        return true;
143
    }
144
145
    /**
146
     * @inheritdoc
147
     */
148
    public function setBlocking(bool $newStatus): ClientInterface
149
    {
150
        $this->logger->debug('Setting new blocking status', ['newStatus' => $newStatus]);
151
        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

151
        stream_set_blocking($this->socket, /** @scrutinizer ignore-type */ $newStatus);
Loading history...
152
        return $this;
153
    }
154
155
    /**
156
     * @inheritdoc
157
     * @throws \unreal4u\MQTT\Exceptions\ServerClosedConnection
158
     * @throws \unreal4u\MQTT\Exceptions\Connect\NoConnectionParametersDefined
159
     * @throws \unreal4u\MQTT\Exceptions\NotConnected
160
     */
161
    public function sendData(WritableContentInterface $object): ReadableContentInterface
162
    {
163
        $currentObject = \get_class($object);
164
        $this->logger->debug('Validating object', ['object' => $currentObject]);
165
166
        if ($object instanceof Connect) {
167
            $this->generateSocketConnection($object);
168
        }
169
170
        $this->logger->info('About to send data', ['object' => $currentObject]);
171
        $readableContent = $object->expectAnswer($this->sendSocketData($object), $this);
172
        /*
173
         * Some objects must perform certain actions on the connection, for example:
174
         * - ConnAck must set the connected bit
175
         * - PingResp must reset the internal last-communication datetime
176
         */
177
        $this->logger->debug('Executing special actions for this object', [
178
            'originObject' => $currentObject,
179
            'responseObject' => \get_class($readableContent),
180
        ]);
181
        $readableContent->performSpecialActions($this, $object);
182
183
        return $readableContent;
184
    }
185
186
    /**
187
     * @inheritdoc
188
     */
189
    public function isItPingTime(): bool
190
    {
191
        $secondsDifference = (new \DateTime('now'))->getTimestamp() - $this->lastCommunication->getTimestamp();
192
        $this->logger->debug('Checking time difference', ['secondsDifference' => $secondsDifference]);
193
194
        return
195
            $this->isConnected() &&
196
            $this->connectionParameters->getKeepAlivePeriod() > 0 &&
197
            $secondsDifference >= $this->connectionParameters->getKeepAlivePeriod();
198
    }
199
200
    /**
201
     * @inheritdoc
202
     */
203
    public function updateLastCommunication(): ClientInterface
204
    {
205
        $lastCommunication = null;
206
        if ($this->lastCommunication !== null) {
207
            $lastCommunication = $this->lastCommunication->format('Y-m-d H:i:s.u');
208
        }
209
        // "now" does not support microseconds, so create the timestamp with a format that does
210
        $this->lastCommunication = \DateTimeImmutable::createFromFormat('U.u', sprintf('%.6F', microtime(true)));
0 ignored issues
show
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

210
        /** @scrutinizer ignore-call */ 
211
        $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...
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...
211
        $this->logger->debug('Updating internal last communication timestamp', [
212
            'previousValue' => $lastCommunication,
213
            'currentValue' => $this->lastCommunication->format('Y-m-d H:i:s.u'),
214
        ]);
215
        return $this;
216
    }
217
218
    /**
219
     * @inheritdoc
220
     */
221
    public function setConnected(bool $isConnected): ClientInterface
222
    {
223
        $this->logger->debug('Setting internal connected property', ['connected' => $isConnected]);
224
        $this->isConnected = $isConnected;
225
        if ($this->isConnected() === false) {
226
            $this->socket = null;
227
        }
228
229
        return $this;
230
    }
231
232
    /**
233
     * @inheritdoc
234
     */
235
    public function isConnected(): bool
236
    {
237
        return $this->isConnected;
238
    }
239
}
240