Completed
Pull Request — master (#16)
by Alessandro
06:12
created

Client::prepareRequest()   B

Complexity

Conditions 7
Paths 64

Size

Total Lines 37

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 16
CRAP Score 7

Importance

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