Completed
Pull Request — master (#15)
by Massimiliano
05:59
created

Client   A

Complexity

Total Complexity 27

Size/Duplication

Total Lines 280
Duplicated Lines 0 %

Coupling/Cohesion

Components 1
Dependencies 9

Test Coverage

Coverage 100%

Importance

Changes 0
Metric Value
wmc 27
lcom 1
cbo 9
dl 0
loc 280
ccs 103
cts 103
cp 1
rs 10
c 0
b 0
f 0
1
<?php
2
declare(strict_types=1);
3
4
namespace Fazland\SkebbyRestClient\Client;
5
6
use Fazland\SkebbyRestClient\Constant\Charsets;
7
use Fazland\SkebbyRestClient\Constant\EncodingSchemas;
8
use Fazland\SkebbyRestClient\Constant\Endpoints;
9
use Fazland\SkebbyRestClient\Constant\Recipients;
10
use Fazland\SkebbyRestClient\Constant\SendMethods;
11
use Fazland\SkebbyRestClient\Constant\ValidityPeriods;
12
use Fazland\SkebbyRestClient\DataStructure\Response;
13
use Fazland\SkebbyRestClient\DataStructure\Sms;
14
use Fazland\SkebbyRestClient\Event\SmsMessageSent;
15
use Fazland\SkebbyRestClient\Exception\EmptyResponseException;
16
use Fazland\SkebbyRestClient\Exception\NoRecipientsSpecifiedException;
17
use Fazland\SkebbyRestClient\Exception\UnknownErrorResponseException;
18
use Fazland\SkebbyRestClient\Transport\Factory;
19
use Fazland\SkebbyRestClient\Transport\TransportInterface;
20
use libphonenumber\NumberParseException;
21
use libphonenumber\PhoneNumberFormat;
22
use libphonenumber\PhoneNumberUtil;
23
use Psr\EventDispatcher\EventDispatcherInterface;
24
use Symfony\Component\OptionsResolver\OptionsResolver;
25
26
/**
27
 * Skebby REST client.
28
 *
29
 * @author Massimiliano Braglia <[email protected]>
30
 */
31
class Client
32
{
33
    private array $config;
0 ignored issues
show
Bug introduced by
This code did not parse for me. Apparently, there is an error somewhere around this line:

Syntax error, unexpected T_ARRAY, expecting T_FUNCTION or T_CONST
Loading history...
34
35
    private TransportInterface $transport;
36
37
    private ?EventDispatcherInterface $dispatcher;
38
39
    /**
40
     * Client constructor.
41
     *
42
     * @throws \Fazland\SkebbyRestClient\Exception\RuntimeException
43
     */
44
    public function __construct(array $options, ?TransportInterface $transport = null, ?EventDispatcherInterface $dispatcher = null)
45
    {
46
        $resolver = new OptionsResolver();
47
48 9
        $this->configureOptions($resolver);
49
        $this->config = $resolver->resolve($options);
50 9
51
        $this->transport = $transport ?? Factory::createTransport();
52 9
        $this->dispatcher = $dispatcher;
53 9
    }
54
55 9
    /**
56 9
     * Sends an SMS.
57
     *
58
     * @return Response[]
59
     *
60
     * @throws EmptyResponseException
61
     * @throws NoRecipientsSpecifiedException
62
     * @throws UnknownErrorResponseException
63
     */
64
    public function send(Sms $sms): array
65
    {
66
        if (! $sms->hasRecipients()) {
67
            throw new NoRecipientsSpecifiedException();
68
        }
69 9
70
        $messages = [];
71 9
72 1
        $recipients = $sms->getRecipients();
73
        foreach (array_chunk($recipients, Recipients::MAX) as $chunk) {
74
            $message = clone $sms;
75 8
            $message
76
                ->setRecipients($chunk)
77 8
                ->clearRecipientVariables()
78 8
            ;
79 8
80
            foreach ($chunk as $recipient) {
81 8
                if (! isset($sms->getRecipientVariables()[$recipient])) {
82 8
                    continue;
83
                }
84
85 8
                foreach ($sms->getRecipientVariables()[$recipient] as $variable => $value) {
86 8
                    $message->addRecipientVariable($recipient, $variable, $value);
87 6
                }
88
            }
89
90 3
            $messages[] = $message;
91 3
        }
92
93
        $responses = [];
94
        foreach ($messages as $message) {
95 8
            $request = $this->prepareRequest($message);
96
97
            $responses[] = $this->executeRequest($request);
98 8
99 8
            if (null !== $this->dispatcher) {
100 8
                $this->dispatcher->dispatch(new SmsMessageSent($message));
101
            }
102 8
        }
103
104
        return $responses;
105 6
    }
106
107
    /**
108
     * Configure default options for client.
109
     *
110
     * It takes required options username, password, sender and method.
111
     * validity_period MUST be a \DateInterval object if set
112
     * delivery_start MUST be a \DateTime object if set
113
     */
114
    private function configureOptions(OptionsResolver $resolver): void
115
    {
116
        $resolver
117 9
            ->setRequired([
118
                'username',
119
                'password',
120 9
                'sender',
121 9
                'method',
122
            ])
123
            ->setDefaults([
124
                'delivery_start' => null,
125
                'charset' => Charsets::UTF8,
126 9
                'validity_period' => \DateInterval::createFromDateString('2800 minutes'),
127 9
                'encoding_schema' => EncodingSchemas::NORMAL,
128
                'endpoint_uri' => Endpoints::REST_HTTPS,
129 9
            ])
130 9
            ->setAllowedTypes('username', 'string')
131
            ->setAllowedTypes('password', 'string')
132
            ->setAllowedTypes('sender', 'string')
133 9
            ->setAllowedTypes('method', 'string')
134 9
            ->setAllowedTypes('delivery_start', ['null', 'DateTime'])
135 9
            ->setAllowedTypes('validity_period', ['null', 'DateInterval'])
136 9
            ->setAllowedTypes('encoding_schema', 'string')
137 9
            ->setAllowedTypes('charset', 'string')
138 9
            ->setAllowedTypes('endpoint_uri', 'string')
139 9
            ->setAllowedValues('method', [
140 9
                SendMethods::CLASSIC,
141 9
                SendMethods::CLASSIC_PLUS,
142 9
                SendMethods::BASIC,
143 9
                SendMethods::TEST_CLASSIC,
144
                SendMethods::TEST_CLASSIC_PLUS,
145
                SendMethods::TEST_BASIC,
146
            ])
147
            ->setAllowedValues('validity_period', function (\DateInterval $value) {
148
                return $value->i >= ValidityPeriods::MIN && $value->i <= ValidityPeriods::MAX;
149
            })
150 9
            ->setAllowedValues('encoding_schema', [
151 9
                EncodingSchemas::NORMAL,
152 9
                EncodingSchemas::UCS2,
153 9
            ])
154 9
            ->setAllowedValues('charset', [
155 9
                Charsets::ISO_8859_1,
156
                Charsets::UTF8,
157 9
            ])
158 9
        ;
159
    }
160
161
    /**
162 9
     * Converts the {@see Sms} to an array request.
163
     */
164
    private function prepareRequest(Sms $sms): string
165
    {
166
        [$senderString, $senderNumber] = $this->getSenderParams($sms);
167
168
        $deliveryStart = $sms->getDeliveryStart() ?: $this->config['delivery_start'];
169
        $validityPeriod = $sms->getValidityPeriod() ?: $this->config['validity_period'];
170
171 8
        $request = [
172
            'username' => $this->config['username'],
173 8
            'password' => $this->config['password'],
174
            'method' => $this->config['method'],
175 8
            'sender_number' => $senderNumber,
176 8
            'sender_string' => $senderString,
177
            'recipients' => $this->prepareRecipients($sms),
178
            'text' => str_replace(' ', '+', $sms->getText()),
179 8
            'user_reference' => $sms->getUserReference(),
180 8
            'delivery_start' => $deliveryStart ? urlencode($deliveryStart->format(\DateTime::RFC2822)) : null,
181 8
            'validity_period' => $validityPeriod ? $validityPeriod->i : null,
182 8
            'encoding_scheme' => $this->config['encoding_schema'],
183 8
            'charset' => urlencode($this->config['charset']),
184 8
        ];
185 8
186 8
        /*
187 8
         * if sender_string is passed and is empty, it's impossible to use sender_number as sender,
188 8
         * Skebby will use the default sender set in Skebby Administration Panel.
189 8
         */
190 8
        if ('' === trim($request['sender_string'])) {
191
            unset($request['sender_string']);
192
        }
193
194
        $serializedRequest = [];
195
        foreach ($request as $key => $value) {
196
            $serializedRequest[] = "$key=$value";
197 8
        }
198 7
199
        return implode('&', $serializedRequest);
200
    }
201 8
202 8
    /**
203 8
     * Converts the {@see Sms} recipients into an array.
204
     */
205
    private function prepareRecipients(Sms $sms): string
206 8
    {
207
        $recipients = $sms->getRecipients();
208
209
        if (! $sms->hasRecipientVariables()) {
210
            $recipients = array_map([$this, 'normalizePhoneNumber'], $recipients);
211
212
            return json_encode($recipients);
213
        }
214
215
        $recipientVariables = $sms->getRecipientVariables();
216 8
217
        return json_encode(array_map(function ($recipient) use ($recipientVariables) {
218 8
            $targetVariables = [];
219
            if (isset($recipientVariables[$recipient])) {
220 8
                $targetVariables = $recipientVariables[$recipient];
221 5
            }
222
223 5
            return array_merge(['recipient' => $this->normalizePhoneNumber($recipient)], $targetVariables);
224
        }, $recipients), JSON_THROW_ON_ERROR);
225
    }
226 3
227
    /**
228 3
     * Normalizes the phoneNumber.
229 3
     *
230 3
     * @throws NumberParseException
231 3
     */
232
    private function normalizePhoneNumber(string $phoneNumber): string
233
    {
234 3
        $utils = PhoneNumberUtil::getInstance();
235 3
        $parsed = $utils->parse(preg_replace('/^00/', '+', $phoneNumber));
236
237
        $phoneNumber = $utils->format($parsed, PhoneNumberFormat::E164);
238
239
        return substr($phoneNumber, 1);
240
    }
241
242
    /**
243
     * Executes the request.
244
     *
245
     * @throws EmptyResponseException
246
     * @throws UnknownErrorResponseException
247 8
     */
248
    private function executeRequest(string $request): Response
249 8
    {
250 8
        $response = $this->transport->executeRequest($this->config['endpoint_uri'], $request);
251
252 8
        return new Response($response);
253
    }
254 8
255
    /**
256
     * Gets sender parameters (alphanumeric sender or phone number).
257
     *
258
     * @return string[]
259
     */
260
    private function getSenderParams(Sms $sms): array
261
    {
262
        $sender = $sms->getSender() ?: $this->config['sender'];
263
264
        $senderString = '';
265
        $senderNumber = '';
266
267 8
        try {
268
            $senderNumber = $this->normalizePhoneNumber($sender);
269 8
        } catch (NumberParseException $e) {
270
            $senderString = substr($sender, 0, 11);
271 8
        }
272
273
        return [$senderString, $senderNumber];
274
    }
275
}
276