Completed
Push — master ( 10de85...76baec )
by Kacper
03:21
created

XmppClient::setParser()   A

Complexity

Conditions 2
Paths 2

Size

Total Lines 9
Code Lines 5

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 0
CRAP Score 6

Importance

Changes 0
Metric Value
cc 2
eloc 5
nc 2
nop 1
dl 0
loc 9
ccs 0
cts 6
cp 0
crap 6
rs 9.6666
c 0
b 0
f 0
1
<?php
2
/**
3
 * Nucleus - XMPP Library for PHP
4
 *
5
 * Copyright (C) 2016, Some rights reserved.
6
 *
7
 * @author Kacper "Kadet" Donat <[email protected]>
8
 *
9
 * Contact with author:
10
 * Xmpp: [email protected]
11
 * E-mail: [email protected]
12
 *
13
 * From Kadet with love.
14
 */
15
16
namespace Kadet\Xmpp;
17
18
use DI\Container;
19
use DI\ContainerBuilder;
20
use Interop\Container\ContainerInterface;
21
use Kadet\Xmpp\Exception\InvalidArgumentException;
22
use Kadet\Xmpp\Exception\WriteOnlyException;
23
use Kadet\Xmpp\Module\Authenticator;
24
use Kadet\Xmpp\Module\Binding;
25
use Kadet\Xmpp\Module\ClientModule;
26
use Kadet\Xmpp\Module\ClientModuleInterface;
27
use Kadet\Xmpp\Module\SaslAuthenticator;
28
use Kadet\Xmpp\Module\TlsEnabler;
29
use Kadet\Xmpp\Network\Connector;
30
use Kadet\Xmpp\Stream\Features;
31
use Kadet\Xmpp\Utils\Accessors;
32
use Kadet\Xmpp\Utils\filter as with;
33
use Kadet\Xmpp\Utils\ObservableCollection;
34
use Kadet\Xmpp\Utils\ServiceManager;
35
use Kadet\Xmpp\Xml\XmlElementFactory;
36
use Kadet\Xmpp\Xml\XmlParser;
37
use Kadet\Xmpp\Xml\XmlStream;
38
use React\EventLoop\LoopInterface;
39
40
/**
41
 * Class XmppClient
42
 * @package Kadet\Xmpp
43
 *
44
 * @property Features           $features  Features provided by that stream
45
 * @property XmlParser          $parser    XmlParser instance used to process data from stream, can be exchanged only
46
 *                                         when client is not connected to server.
47
 * @property string             $resource  Client's jid resource
48
 * @property Jid                $jid       Client's jid (Jabber Identifier) address.
49
 * @property ContainerInterface $container Dependency container used for module management.
50
 * @property string             $language  Stream language (reflects xml:language attribute)
51
 * @property string             $state     Current client state
52
 *                                         `disconnected`   - not connected to any server,
53
 *                                         `connected`      - connected to server, but nothing happened yet,
54
 *                                         `secured`        - [optional] TLS negotiation succeeded, after stream restart
55
 *                                         `authenticated`  - authentication succeeded,
56
 *                                         `bound`          - resource binding succeeded,
57
 *                                         `ready`          - client is ready to operate
58
 *
59
 *                                         However modules can add custom states.
60
 *
61
 * @property Connector    $connector Connector used for obtaining stream
62
 * @property-write string       $password  Password used for client authentication
63
 */
64
class XmppClient extends XmlStream implements ContainerInterface
65
{
66
    use ServiceManager, Accessors;
67
68
    /**
69
     * Connector used to instantiate stream connection to server.
70
     *
71
     * @var Connector
72
     */
73
    private $_connector;
74
75
    /**
76
     * Client's jid (Jabber Identifier) address.
77
     *
78
     * @var Jid
79
     */
80
    private $_jid;
81
82
    /**
83
     * Dependency container used as service manager.
84
     *
85
     * @var Container
86
     */
87
    private $_container;
88
89
    /**
90
     * Features provided by that stream
91
     *
92
     * @var Features
93
     */
94
    private $_features;
95
96
    /**
97
     * Current client state.
98
     *
99
     * @var string
100
     */
101
    private $_state = 'disconnected';
102
    private $_attributes = [];
103
    private $_lang;
104
105
    /**
106
     * XmppClient constructor.
107
     * @param Jid              $jid
108
     * @param array            $options {
109
     *     @var XmlParser          $parser          Parser used for interpreting streams.
110
     *     @var ClientModule[]     $modules         Additional modules registered when creating client.
111
     *     @var string             $language        Stream language (reflects xml:language attribute)
112
     *     @var ContainerInterface $container       Dependency container used for module management.
113
     *     @var bool               $default-modules Load default modules or not
114
     * }
115
     */
116
    public function __construct(Jid $jid, array $options = [])
117
    {
118
        $options = array_replace([
119
            'parser'    => new XmlParser(new XmlElementFactory()),
120
            'language'  => 'en',
121
            'container' => ContainerBuilder::buildDevContainer(),
122
            'connector' => $options['connector'] ?? new Connector\TcpXmppConnector($jid->domain, $options['loop']),
123
            'jid'       => $jid,
124
125
            'modules'         => [],
126
            'default-modules' => true,
127
        ], $options);
128
129
        parent::__construct($options['parser'], null);
130
        unset($options['parser']);
131
132
        $this->applyOptions($options);
133
134
        $this->on('element', function (Features $element) {
135
            $this->_features = $element;
136
            $this->emit('features', [$element]);
137
        }, Features::class);
138
139
        $this->on('close', function (Features $element) {
0 ignored issues
show
Unused Code introduced by
The parameter $element is not used and could be removed.

This check looks from parameters that have been defined for a function or method, but which are not used in the method body.

Loading history...
140
            $this->state = 'disconnected';
141
        }, Features::class);
142
    }
143
144
    public function applyOptions(array $options)
145
    {
146
        $options = \Kadet\Xmpp\Utils\helper\rearrange($options, [
147
            'container' => 6,
148
            'jid'       => 5,
149
            'connector' => 4,
150
            'modules'   => 3,
151
            'password'  => -1
152
        ]);
153
154
        if ($options['default-modules']) {
155
            $options['modules'] = array_merge([
156
                TlsEnabler::class    => new TlsEnabler(),
157
                Binding::class       => new Binding(),
158
                Authenticator::class => new SaslAuthenticator()
159
            ], $options['modules']);
160
        }
161
162
        foreach ($options as $name => $value) {
163
            $this->$name = $value;
164
        }
165
    }
166
167
    public function restart()
168
    {
169
        $this->getLogger()->debug('Restarting stream', $this->_attributes);
170
        $this->start($this->_attributes);
171
    }
172
173
    public function start(array $attributes = [])
174
    {
175
        $this->_attributes = $attributes;
176
177
        parent::start(array_merge([
178
            'xmlns'    => 'jabber:client',
179
            'version'  => '1.0',
180
            'xml:lang' => $this->_lang
181
        ], $attributes));
182
    }
183
184
    public function connect()
185
    {
186
        $this->getLogger()->debug("Connecting to {$this->_jid->domain}");
187
188
        $this->_connector->connect();
189
    }
190
191
    public function bind($jid)
192
    {
193
        $this->jid = new Jid($jid);
194
        $this->emit('bind', [$jid]);
195
196
        $this->state = 'bound';
197
198
        $queue = new ObservableCollection();
199
        $queue->on('empty', function () {
200
            $this->state = 'ready';
201
        });
202
203
        $this->emit('init', [$queue]);
204
    }
205
206
    public function register(ClientModuleInterface $module, $alias = true)
207
    {
208
        $module->setClient($this);
209
        if ($alias === true) {
210
            $this->_container->set(get_class($module), $module);
211
            $aliases = array_merge(class_implements($module), array_slice(class_parents($module), 1));
212
            foreach ($aliases as $alias) {
213
                if (!$this->has($alias)) {
214
                    $this->_container->set($alias, $module);
215
                }
216
            }
217
        } else {
218
            $this->_container->set($alias === true ? get_class($module) : $alias, $module);
0 ignored issues
show
Bug introduced by
It seems like $alias === true ? get_class($module) : $alias can also be of type boolean; however, DI\Container::set() does only seem to accept string, maybe add an additional type check?

If a method or function can return multiple different values and unless you are sure that you only can receive a single value in this context, we recommend to add an additional type check:

/**
 * @return array|string
 */
function returnsDifferentValues($x) {
    if ($x) {
        return 'foo';
    }

    return array();
}

$x = returnsDifferentValues($y);
if (is_array($x)) {
    // $x is an array.
}

If this a common case that PHP Analyzer should handle natively, please let us know by opening an issue.

Loading history...
219
        }
220
    }
221
222
    private function handleConnect($stream)
223
    {
224
        $this->exchangeStream($stream);
225
226
        $this->getLogger()->info("Connected to {$this->_jid->domain}");
227
        $this->start([
228
            'from' => (string)$this->_jid,
229
            'to'   => $this->_jid->domain
230
        ]);
231
232
        $this->state = 'connected';
233
234
        return $this->emit('connect');
235
    }
236
237
    //region Features
238
    public function getFeatures()
239
    {
240
        return $this->_features;
241
    }
242
    //endregion
243
244
    //region Parser
245
    public function setParser(XmlParser $parser)
246
    {
247
        if($this->state !== "disconnected") {
248
            throw new \BadMethodCallException('Parser can be changed only when client is disconnected.');
249
        }
250
251
        parent::setParser($parser);
252
        $this->_parser->factory->load(require __DIR__ . '/XmlElementLookup.php');
253
    }
254
255
    public function getParser()
256
    {
257
        return $this->_parser;
258
    }
259
    //endregion
260
261
    //region Connector
262
    protected function setConnector($connector)
263
    {
264
        if ($connector instanceof LoopInterface) {
265
            $this->_connector = new Connector\TcpXmppConnector($this->_jid->domain, $connector);
266
        } elseif ($connector instanceof Connector) {
267
            $this->_connector = $connector;
268
        } else {
269
            throw new InvalidArgumentException(sprintf(
270
                '$connector must be either %s or %s instance, %s given.',
271
                LoopInterface::class, Connector::class, \Kadet\Xmpp\Utils\helper\typeof($connector)
272
            ));
273
        }
274
275
        $this->_connector->on('connect', function ($stream) {
276
            return $this->handleConnect($stream);
277
        });
278
    }
279
280
    public function getConnector()
281
    {
282
        return $this->_connector;
283
    }
284
    //endregion
285
286
    //region Resource
287
    public function setResource(string $resource)
288
    {
289
        $this->_jid = new Jid($this->_jid->domain, $this->_jid->local, $resource);
290
    }
291
292
    public function getResource()
293
    {
294
        return $this->_jid->resource;
295
    }
296
    //endregion
297
298
    //region Password
299
    public function setPassword(string $password)
300
    {
301
        $this->get(Authenticator::class)->setPassword($password);
302
    }
303
304
    public function getPassword()
305
    {
306
        throw new WriteOnlyException("Password can't be obtained.");
307
    }
308
    //endregion
309
310
    //region Modules
311
    public function setModules(array $modules)
312
    {
313
        foreach ($modules as $name => $module) {
314
            $this->register($module, is_string($name) ? $name : true);
0 ignored issues
show
Bug introduced by
It seems like is_string($name) ? $name : true can also be of type string; however, Kadet\Xmpp\XmppClient::register() does only seem to accept boolean, maybe add an additional type check?

If a method or function can return multiple different values and unless you are sure that you only can receive a single value in this context, we recommend to add an additional type check:

/**
 * @return array|string
 */
function returnsDifferentValues($x) {
    if ($x) {
        return 'foo';
    }

    return array();
}

$x = returnsDifferentValues($y);
if (is_array($x)) {
    // $x is an array.
}

If this a common case that PHP Analyzer should handle natively, please let us know by opening an issue.

Loading history...
315
        }
316
    }
317
    //endregion
318
319
    //region State
320
    public function setState($state)
321
    {
322
        $this->_state = $state;
323
        $this->emit('state', [$state]);
324
    }
325
326
    public function getState()
327
    {
328
        return $this->_state;
329
    }
330
    //endregion
331
332
    //region Container
333
    protected function getContainer() : ContainerInterface
334
    {
335
        return $this->_container;
336
    }
337
338
    protected function setContainer(Container $container)
339
    {
340
        $this->_container = $container;
341
    }
342
    //endregion
343
344
    //region Language
345
    public function getLanguage(): string
346
    {
347
        return $this->_lang;
348
    }
349
350
    public function setLanguage(string $language)
351
    {
352
        $this->_lang = $language;
353
    }
354
    //endregion
355
356
    //region JID
357
    public function getJid()
358
    {
359
        return $this->_jid;
360
    }
361
362
    protected function setJid(Jid $jid)
363
    {
364
        $this->_jid = $jid;
365
    }
366
    //endregion
367
}
368