Completed
Push — master ( 939cef...4aee11 )
by Kacper
03:59
created

XmppClient::getContainer()   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
c 0
b 0
f 0
dl 0
loc 4
cc 1
eloc 2
nc 1
nop 0
ccs 0
cts 2
cp 0
crap 2
rs 10
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\Component\Authenticator;
24
use Kadet\Xmpp\Component\Binding;
25
use Kadet\Xmpp\Component\Component;
26
use Kadet\Xmpp\Component\ComponentInterface;
27
use Kadet\Xmpp\Component\SaslAuthenticator;
28
use Kadet\Xmpp\Component\TlsEnabler;
29
use Kadet\Xmpp\Network\Connector;
30
use Kadet\Xmpp\Stanza\Stanza;
31
use Kadet\Xmpp\Stream\Features;
32
use Kadet\Xmpp\Utils\Accessors;
33
use Kadet\Xmpp\Utils\filter as with;
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
use React\Promise\Deferred;
40
use React\Promise\ExtendedPromiseInterface;
41
42
/**
43
 * Class XmppClient
44
 * @package Kadet\Xmpp
45
 *
46
 * @property Features           $features  Features provided by that stream
47
 * @property XmlParser          $parser    XmlParser instance used to process data from stream, can be exchanged only
48
 *                                         when client is not connected to server.
49
 * @property string             $resource  Client's jid resource
50
 * @property Jid                $jid       Client's jid (Jabber Identifier) address.
51
 * @property ContainerInterface $container Dependency container used for module management.
52
 * @property string             $language  Stream language (reflects xml:language attribute)
53
 * @property string             $state     Current client state
54
 *                                         `disconnected`   - not connected to any server,
55
 *                                         `connected`      - connected to server, but nothing happened yet,
56
 *                                         `secured`        - [optional] TLS negotiation succeeded, after stream restart
57
 *                                         `authenticated`  - authentication succeeded,
58
 *                                         `bound`          - resource binding succeeded,
59
 *                                         `ready`          - client is ready to operate
60
 *
61
 *                                         However modules can add custom states.
62
 *
63
 * @property Connector    $connector Connector used for obtaining stream
64
 * @property-write string       $password  Password used for client authentication
65
 */
66
class XmppClient extends XmlStream implements ContainerInterface
67
{
68
    use ServiceManager, Accessors;
69
70
    /**
71
     * Connector used to instantiate stream connection to server.
72
     *
73
     * @var Connector
74
     */
75
    private $_connector;
76
77
    /**
78
     * Client's jid (Jabber Identifier) address.
79
     *
80
     * @var Jid
81
     */
82
    private $_jid;
83
84
    /**
85
     * Dependency container used as service manager.
86
     *
87
     * @var Container
88
     */
89
    private $_container;
90
91
    /**
92
     * Features provided by that stream
93
     *
94
     * @var Features
95
     */
96
    private $_features;
97
98
    /**
99
     * Current client state.
100
     *
101
     * @var string
102
     */
103
    private $_state = 'disconnected';
104
    private $_lang;
105
106
    /**
107
     * XmppClient constructor.
108
     * @param Jid                  $jid
109
     * @param array                $options   {
110
     *     @var XmlParser          $parser    Parser used for interpreting streams.
111
     *     @var Component[]        $modules   Additional modules registered when creating client.
112
     *     @var string             $language  Stream language (reflects xml:language attribute)
113
     *     @var ContainerInterface $container Dependency container used for module management.
114
     *     @var bool               $default   -modules Load default modules or not
115
     * }
116
     */
117
    public function __construct(Jid $jid, array $options = [])
118
    {
119
        $options = array_replace([
120
            'parser'    => new XmlParser(new XmlElementFactory()),
121
            'language'  => 'en',
122
            'container' => ContainerBuilder::buildDevContainer(),
123
            'connector' => $options['connector'] ?? new Connector\TcpXmppConnector($jid->domain, $options['loop']),
124
            'jid'       => $jid,
125
126
            'modules'         => [],
127
            'default-modules' => true,
128
        ], $options);
129
130
        parent::__construct($options['parser'], null);
131
        unset($options['parser']);
132
133
        $this->applyOptions($options);
134
135
        $this->on('element', function (Features $element) {
136
            $this->_features = $element;
137
            $this->emit('features', [$element]);
138
        }, Features::class);
139
140
        $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...
141
            $this->state = 'disconnected';
142
        }, Features::class);
143
    }
144
145
    public function applyOptions(array $options)
146
    {
147
        $options = \Kadet\Xmpp\Utils\helper\rearrange($options, [
148
            'container' => 6,
149
            'jid'       => 5,
150
            'connector' => 4,
151
            'modules'   => 3,
152
            'password'  => -1
153
        ]);
154
155
        if ($options['default-modules']) {
156
            $options['modules'] = array_merge([
157
                TlsEnabler::class    => new TlsEnabler(),
158
                Binding::class       => new Binding(),
159
                Authenticator::class => new SaslAuthenticator()
160
            ], $options['modules']);
161
        }
162
163
        foreach ($options as $name => $value) {
164
            $this->$name = $value;
165
        }
166
    }
167
168
    public function start(array $attributes = [])
169
    {
170
        parent::start(array_merge([
171
            'xmlns'    => 'jabber:client',
172
            'version'  => '1.0',
173
            'language' => $this->_lang
174
        ], $attributes));
175
    }
176
177
    public function connect()
178
    {
179
        $this->getLogger()->debug("Connecting to {$this->_jid->domain}");
180
181
        $this->_connector->connect();
182
    }
183
184
    public function bind($jid)
185
    {
186
        $this->jid = new Jid($jid);
187
        $this->emit('bind', [$jid]);
188
189
        $this->state = 'bound';
190
191
        $queue = new \SplQueue();
192
        $this->emit('init', [ $queue ]);
193
194
        \React\Promise\all(iterator_to_array($queue))->then(function() {
195
            $this->state = 'ready';
196
        });
197
    }
198
199
    /**
200
     * Registers module in client's dependency container.
201
     *
202
     * @param ComponentInterface $module    Module to be registered
203
     * @param bool|string|array  $alias     Module alias, class name by default.
204
     *                                      `true` for aliasing interfaces and parents too,
205
     *                                      `false` for aliasing as class name only
206
     *                                      array for multiple aliases,
207
     *                                      and any string for alias name
208
     */
209
    public function register(ComponentInterface $module, $alias = true)
210
    {
211
        $module->setClient($this);
212
        if ($alias === true) {
213
            $this->_container->set(get_class($module), $module);
214
215
            $this->_addToContainer($module, array_merge(class_implements($module), array_slice(class_parents($module), 1)));
216
        } elseif(is_array($alias)) {
217
            $this->_addToContainer($module, $alias);
218
        } else {
219
            $this->_addToContainer($module, [ $alias === false ? get_class($module) : $alias ]);
220
        }
221
    }
222
223
    private function _addToContainer(ComponentInterface $module, array $aliases) {
224
        foreach ($aliases as $name) {
225
            if (!$this->has($name)) {
226
                $this->_container->set($name, $module);
227
            }
228
        }
229
    }
230
231
    /**
232
     * Sends stanza to server and returns promise with server response.
233
     *
234
     * @param Stanza $stanza
235
     * @return ExtendedPromiseInterface
236
     */
237
    public function send(Stanza $stanza) : ExtendedPromiseInterface
238
    {
239
        $deferred = new Deferred();
240
241
        $this->once('element', function(Stanza $stanza) use ($deferred) {
242
            if($stanza->type === "error") {
243
                $deferred->reject($stanza);
244
            } else {
245
                $deferred->resolve($stanza);
246
            }
247
        }, with\stanza\id($stanza->id));
248
        $this->write($stanza);
249
250
        return $deferred->promise();
251
    }
252
253
    private function handleConnect($stream)
254
    {
255
        $this->exchangeStream($stream);
256
257
        $this->getLogger()->info("Connected to {$this->_jid->domain}");
258
        $this->start([
259
            'from' => (string)$this->_jid,
260
            'to'   => $this->_jid->domain
261
        ]);
262
263
        $this->state = 'connected';
264
265
        return $this->emit('connect');
266
    }
267
268
    //region Features
269
    public function getFeatures()
270
    {
271
        return $this->_features;
272
    }
273
    //endregion
274
275
    //region Parser
276
    public function setParser(XmlParser $parser)
277
    {
278
        if($this->state !== "disconnected") {
279
            throw new \BadMethodCallException('Parser can be changed only when client is disconnected.');
280
        }
281
282
        parent::setParser($parser);
283
        $this->_parser->factory->load(require __DIR__ . '/XmlElementLookup.php');
284
    }
285
286
    public function getParser()
287
    {
288
        return $this->_parser;
289
    }
290
    //endregion
291
292
    //region Connector
293
    protected function setConnector($connector)
294
    {
295
        if ($connector instanceof LoopInterface) {
296
            $this->_connector = new Connector\TcpXmppConnector($this->_jid->domain, $connector);
297
        } elseif ($connector instanceof Connector) {
298
            $this->_connector = $connector;
299
        } else {
300
            throw new InvalidArgumentException(sprintf(
301
                '$connector must be either %s or %s instance, %s given.',
302
                LoopInterface::class, Connector::class, \Kadet\Xmpp\Utils\helper\typeof($connector)
303
            ));
304
        }
305
306
        $this->_connector->on('connect', function ($stream) {
307
            return $this->handleConnect($stream);
308
        });
309
    }
310
311
    public function getConnector()
312
    {
313
        return $this->_connector;
314
    }
315
    //endregion
316
317
    //region Resource
318
    public function setResource(string $resource)
319
    {
320
        $this->_jid = new Jid($this->_jid->domain, $this->_jid->local, $resource);
321
    }
322
323
    public function getResource()
324
    {
325
        return $this->_jid->resource;
326
    }
327
    //endregion
328
329
    //region Password
330
    public function setPassword(string $password)
331
    {
332
        $this->get(Authenticator::class)->setPassword($password);
333
    }
334
335
    public function getPassword()
336
    {
337
        throw new WriteOnlyException("Password can't be obtained.");
338
    }
339
    //endregion
340
341
    //region Modules
342
    public function setModules(array $modules)
343
    {
344
        foreach ($modules as $name => $module) {
345
            $this->register($module, is_string($name) ? $name : true);
346
        }
347
    }
348
    //endregion
349
350
    //region State
351
    public function setState($state)
352
    {
353
        $this->_state = $state;
354
        $this->emit('state', [$state]);
355
    }
356
357
    public function getState()
358
    {
359
        return $this->_state;
360
    }
361
    //endregion
362
363
    //region Container
364
    protected function getContainer() : ContainerInterface
365
    {
366
        return $this->_container;
367
    }
368
369
    protected function setContainer(Container $container)
370
    {
371
        $this->_container = $container;
372
    }
373
    //endregion
374
375
    //region Language
376
    public function getLanguage(): string
377
    {
378
        return $this->_lang;
379
    }
380
381
    public function setLanguage(string $language)
382
    {
383
        $this->_lang = $language;
384
    }
385
    //endregion
386
387
    //region JID
388
    public function getJid()
389
    {
390
        return $this->_jid;
391
    }
392
393
    protected function setJid(Jid $jid)
394
    {
395
        $this->_jid = $jid;
396
    }
397
    //endregion
398
}
399