EntityDecoder::checkForEntityStart()   A
last analyzed

Complexity

Conditions 5
Paths 8

Size

Total Lines 17
Code Lines 9

Duplication

Lines 0
Ratio 0 %

Importance

Changes 5
Bugs 3 Features 0
Metric Value
cc 5
eloc 9
c 5
b 3
f 0
nc 8
nop 1
dl 0
loc 17
rs 9.6111
1
<?php
2
/**
3
 * This class decode style entities from Telegram bot messages (bold, italic, etc.) in text with inline entities that duplicate (when possible) the
4
 * exact style the message has originally when was sended to the bot.
5
 * All this work is necessary because Telegram returns offset and length of the entities in UTF-16 code units that they've been hard to decode correctly in PHP
6
 *
7
 * Inspired By: https://github.com/php-telegram-bot/core/issues/544#issuecomment-564950430
8
 *
9
 * Example usage:
10
 * $entity_decoder = new EntityDecoder('HTML');
11
 * $decoded_text = $entity_decoder->decode($message);
12
 *
13
 * @author LucaDevelop
14
 * @access public
15
 * @see https://github.com/LucaDevelop/telegram-entities-decoder
16
 */
17
18
namespace lucadevelop\TelegramEntitiesDecoder;
19
20
class EntityDecoder
21
{
22
    private $entitiesToParse = ['bold', 'italic', 'code', 'pre', 'text_mention', 'text_link', 'strikethrough', 'underline', 'spoiler', 'blockquote', 'custom_emoji'];
23
    private $entities = [];
24
    private $style;
25
26
      /**
27
       * @param string $style       Either 'HTML', 'Markdown' or 'MarkdownV2'.
28
       *
29
       * @throws InvalidArgumentException if the provided style name in invalid.
30
       */
31
    public function __construct(string $style = 'HTML')
32
    {
33
        if (in_array($style, ["HTML", "MarkdownV2", "Markdown"]))
34
        {
35
            $this->style = $style;
36
        }
37
        else
38
        {
39
            throw new \InvalidArgumentException("Wrong style name");
40
        }
41
    }
42
43
    /**
44
     * Decode entities and return decoded text
45
     *
46
     * @param object $message       message object to reconstruct Entities from (json decoded without assoc).
47
     * @return string
48
     */
49
    public function decode($message): string
50
    {
51
        if (!is_object($message))
52
        {
53
            throw new \Exception('message must be an object');
54
        }
55
        //Get available entities (for text or for attachment like photo, document, etc.)
56
        if (!empty($message->entities))
57
        {
58
            $this->entities = $message->entities;
59
        }
60
        if (!empty($message->caption_entities))
61
        {
62
            $this->entities = $message->caption_entities;
63
        }
64
        //Get internal encoding
65
        $prevencoding = mb_internal_encoding();
66
        //Set encoding to UTF-8
67
        mb_internal_encoding('UTF-8');
68
        //Get available text (text message or caption for attachment)
69
        $textToDecode = (!empty($message->text) ? $message->text : (!empty($message->caption) ? $message->caption : ""));
70
        //if the message has no entities or no text return the original text
71
        if (empty($this->entities) || $textToDecode == "") {
72
            if ($prevencoding)
73
            {
74
                mb_internal_encoding($prevencoding);
0 ignored issues
show
Bug introduced by
It seems like $prevencoding can also be of type true; however, parameter $encoding of mb_internal_encoding() does only seem to accept null|string, 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

74
                mb_internal_encoding(/** @scrutinizer ignore-type */ $prevencoding);
Loading history...
75
            }
76
            return $textToDecode;
77
        }
78
        //split text in char array with UTF-16 code units length
79
        $arrayText = $this->splitCharAndLength($textToDecode);
80
        $finalText = "";
81
82
        $openedEntities = [];
83
        $currenPosition = 0;
84
        //Cycle characters one by one to calculate begins and ends of entities and escape special chars
85
        for ($i = 0, $c = count($arrayText); $i < $c; $i++) {
86
            $offsetAndLength = $currenPosition + $arrayText[$i]['length'];
87
            $entityCheckStart = $this->checkForEntityStart($currenPosition);
88
            $entityCheckStop = $this->checkForEntityStop($offsetAndLength);
89
            if ($entityCheckStart !== false)
90
            {
91
                foreach ($entityCheckStart as $stEntity)
92
                {
93
                    $startChar = $this->getEntityStartString($stEntity);
94
                    $openedEntities[] = $stEntity;
95
                    $finalText .= $startChar;
96
                }
97
                $finalText .= $this->escapeSpecialChars($arrayText[$i]['char'], true, $openedEntities);
98
            }
99
            if ($entityCheckStop !== false)
100
            {
101
                if ($entityCheckStart === false)
102
                {
103
                    $finalText .= $this->escapeSpecialChars($arrayText[$i]['char'], true, $openedEntities);
104
                }
105
                if ($this->style == 'MarkdownV2' && $this->checkMarkdownV2AmbiguousEntities($entityCheckStop))
106
                {
107
                    $stopChar = "_\r__";
108
                    $finalText .= $stopChar;
109
                    array_pop($openedEntities);
110
                    array_pop($openedEntities);
111
                }
112
                foreach ($entityCheckStop as $stEntity)
113
                {
114
                    $stopChar = $this->getEntityStopString($stEntity);
115
                    $finalText .= $stopChar;
116
                    array_pop($openedEntities);
117
                }
118
            }
119
            if ($entityCheckStart === false && $entityCheckStop === false)
120
            {
121
                $isEntityOpen = !empty($openedEntities);
122
                $finalText .= $this->escapeSpecialChars($arrayText[$i]['char'], $isEntityOpen, $openedEntities);
123
            }
124
            $currenPosition = $offsetAndLength;
125
        }
126
        if (!empty($openedEntities))
127
        {
128
            $openedEntities = array_reverse($openedEntities);
129
            foreach ($openedEntities as $oe)
130
            {
131
                $finalText .= $this->getEntityStopString($oe);
132
            }
133
        }
134
        if ($prevencoding)
135
        {
136
            mb_internal_encoding($prevencoding);
137
        }
138
139
        return $finalText;
140
    }
141
142
    /**
143
     * Extract all entities in an array
144
     *
145
     * @param object $message       message object to reconstruct Entities from (json decoded without assoc).
146
     * @return array
147
     */
148
    public function extractAllEntities($message): array
149
    {
150
        $entitiesArray = [];
151
        if (!is_object($message))
152
        {
153
            throw new \Exception('message must be an object');
154
        }
155
        //Get available entities (for text or for attachment like photo, document, etc.)
156
        if (!empty($message->entities))
157
        {
158
            $this->entities = $message->entities;
159
        }
160
        if (!empty($message->caption_entities))
161
        {
162
            $this->entities = $message->caption_entities;
163
        }
164
        //Get internal encoding
165
        $prevencoding = mb_internal_encoding();
166
        //Set encoding to UTF-8
167
        mb_internal_encoding('UTF-8');
168
        //Get available text (text message or caption for attachment)
169
        $textToDecode = (!empty($message->text) ? $message->text : (!empty($message->caption) ? $message->caption : ""));
170
        //if the message has no entities or no text return the empty array
171
        if (empty($this->entities) || $textToDecode == "") {
172
            if ($prevencoding)
173
            {
174
                mb_internal_encoding($prevencoding);
0 ignored issues
show
Bug introduced by
It seems like $prevencoding can also be of type true; however, parameter $encoding of mb_internal_encoding() does only seem to accept null|string, 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

174
                mb_internal_encoding(/** @scrutinizer ignore-type */ $prevencoding);
Loading history...
175
            }
176
            return $entitiesArray;
177
        }
178
        $arrayText = $this->splitCharAndLength($textToDecode);
179
        $entitytext = "";
180
181
        $openedEntities = [];
182
        $currenPosition = 0;
183
        //Cycle characters one by one to calculate begins and ends of entities and escape special chars
184
        for ($i = 0, $c = count($arrayText); $i < $c; $i++) {
185
            $offsetAndLength = $currenPosition + $arrayText[$i]['length'];
186
            $entityCheckStart = $this->checkForEntityStart($currenPosition);
187
            $entityCheckStop = $this->checkForEntityStop($offsetAndLength);
188
            if ($entityCheckStart !== false)
189
            {
190
                foreach ($entityCheckStart as $stEntity)
191
                {
192
                    $startChar = $this->getEntityStartString($stEntity);
193
                    $openedEntities[] = $stEntity;
194
                    $entitytext .= $startChar;
195
                }
196
                $entitytext .= $this->escapeSpecialChars($arrayText[$i]['char'], true, $openedEntities);
197
            }
198
            if ($entityCheckStop !== false)
199
            {
200
                if ($entityCheckStart === false)
201
                {
202
                    $entitytext .= $this->escapeSpecialChars($arrayText[$i]['char'], true, $openedEntities);
203
                }
204
                if ($this->style == 'MarkdownV2' && $this->checkMarkdownV2AmbiguousEntities($entityCheckStop))
205
                {
206
                    $stopChar = "_\r__";
207
                    $entitytext .= $stopChar;
208
                    array_pop($openedEntities);
209
                    array_pop($openedEntities);
210
                    if(empty($openedEntities))
211
                    {
212
                        $entitiesArray[] = $entitytext;
213
                        $entitytext = "";
214
                    }
215
                }
216
                foreach ($entityCheckStop as $stEntity)
217
                {
218
                    $stopChar = $this->getEntityStopString($stEntity);
219
                    $entitytext .= $stopChar;
220
                    array_pop($openedEntities);
221
                    if(empty($openedEntities))
222
                    {
223
                        $entitiesArray[] = $entitytext;
224
                        $entitytext = "";
225
                    }
226
                }
227
            }
228
            if ($entityCheckStart === false && $entityCheckStop === false)
229
            {
230
                $isEntityOpen = !empty($openedEntities);
231
                if($isEntityOpen)
232
                {
233
                    $entitytext .= $this->escapeSpecialChars($arrayText[$i]['char'], $isEntityOpen, $openedEntities);
234
                }
235
            }
236
            $currenPosition = $offsetAndLength;
237
        }
238
        if (!empty($openedEntities))
239
        {
240
            $openedEntities = array_reverse($openedEntities);
241
            foreach ($openedEntities as $oe)
242
            {
243
                $entitytext .= $this->getEntityStopString($oe);
244
                $entitiesArray[] = $entitytext;
245
            }
246
        }
247
        if ($prevencoding)
248
        {
249
            mb_internal_encoding($prevencoding);
250
        }
251
        return $entitiesArray;
252
    }
253
254
    /**
255
     * Split message text in chars array with lengthes
256
     */
257
    protected function splitCharAndLength($string)
258
    {
259
        //Split string in individual unicode points
260
        $str_split_unicode = preg_split('//u', $string, -1, PREG_SPLIT_NO_EMPTY);
261
        $new_string_split = [];
262
        $joiner = false;
263
        for ($i = 0, $c = count($str_split_unicode); $i < $c; $i++)
264
        {
265
            //loop the array
266
            $codepoint = bin2hex(mb_convert_encoding($str_split_unicode[$i], 'UTF-16')); //Get the string rappresentation of the unicode char
0 ignored issues
show
Bug introduced by
It seems like mb_convert_encoding($str..._unicode[$i], 'UTF-16') can also be of type array; however, parameter $string of bin2hex() does only seem to accept string, 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

266
            $codepoint = bin2hex(/** @scrutinizer ignore-type */ mb_convert_encoding($str_split_unicode[$i], 'UTF-16')); //Get the string rappresentation of the unicode char
Loading history...
267
            if ($codepoint == "fe0f" || $codepoint == "1f3fb" || $codepoint == "1f3fc" || $codepoint == "1f3fd" || $codepoint == "1f3fe" || $codepoint == "1f3ff")
268
            {
269
                //Manage the modifiers
270
                $new_string_split[count($new_string_split) - 1] .= $str_split_unicode[$i]; //Apppend the modifier to the previous char
271
            }
272
            else
273
            {
274
                if ($codepoint == "200d")
275
                {
276
                    //Manage the Zero Width Joiner
277
                    $new_string_split[count($new_string_split) - 1] .= $str_split_unicode[$i]; //Apppend the ZWJ to the previous char
278
                    $joiner = true;
279
                }
280
                else
281
                {
282
                    if ($joiner)
283
                    {
284
                        //If previous one was a ZWJ
285
                        $new_string_split[count($new_string_split) - 1] .= $str_split_unicode[$i]; //Apppend to the previous char
286
                        $joiner = false;
287
                    }
288
                    else
289
                    {
290
                        $new_string_split[] = $str_split_unicode[$i]; //New char
291
                    }
292
                }
293
            }
294
        }
295
        $data = [];
296
        foreach ($new_string_split as $s)
297
        {
298
          $data[] = ["char" => $s, "length" => $this->getUTF16CodePointsLength($s)];
299
        }
300
        return $data;
301
    }
302
303
    /**
304
     * Apply Telegram escape rules for the choosen style
305
     */
306
    protected function escapeSpecialChars($char, $isEntityOpen, $entities) {
307
        if ($this->style == 'Markdown')
308
        {
309
            if ($isEntityOpen)
310
            {
311
                $entity = $entities[0];
312
                if ($char == '*' || $char == '_')
313
                {
314
                    if ($char == $this->getEntityStartString($entity))
315
                    {
316
                        return $char."\\".$char.$char;
317
                    }
318
                    else
319
                    {
320
                        return $char;
321
                    }
322
                }
323
                else
324
                {
325
                    return $char;
326
                }
327
            }
328
            else
329
            {
330
                if ($char == '*' || $char == '_' || $char == '[' || $char == '`')
331
                {
332
                    return "\\".$char;
333
                }
334
                else
335
                {
336
                    return $char;
337
                }
338
            }
339
        }
340
        else if ($this->style == 'HTML')
341
        {
342
            return ($char == '<' ? '&lt;' : ($char == '>' ? '&gt;' : ($char == '&' ? '&amp;' : $char)));
343
        }
344
        else if ($this->style == 'MarkdownV2')
345
        {
346
            $isBlockquoteOpen = false;
347
            foreach ($entities as $entity) {
348
                if ($entity->type === 'blockquote') {
349
                    $isBlockquoteOpen = true;
350
                    break;
351
                }
352
            }
353
            if($isBlockquoteOpen && $char == "\n")
354
            {
355
                return $char.'>';
356
            }
357
            else
358
            {
359
                return (in_array($char, ['_', '*', '[', ']', '(', ')', '~', '`', '>', '#', '+', '-', '=', '|', '{', '}', '.', '!', '\\']) ? '\\'.$char : $char);
360
            }
361
        }
362
        else
363
        {
364
            return $char;
365
        }
366
    }
367
368
    /**
369
     * Get the begin string of the entity  for the choosen style
370
     */
371
    protected function getEntityStartString($entity)
372
    {
373
        $startString = '';
374
        if ($this->style == 'Markdown')
375
        {
376
            switch ($entity->type)
377
            {
378
                case 'bold':
379
                {
380
                    $startString = '*';
381
                    break;
382
                }
383
                case 'italic':
384
                {
385
                    $startString = '_';
386
                    break;
387
                }
388
                case 'code':
389
                {
390
                    $startString = '`';
391
                    break;
392
                }
393
                case 'pre':
394
                {
395
                    $startString = '```';
396
                    if (isset($entity->language))
397
                    {
398
                        $startString .= $entity->language;
399
                    }
400
                    $startString .= "\n";
401
                    break;
402
                }
403
                case 'text_mention':
404
                case 'text_link':
405
                {
406
                    $startString = '[';
407
                    break;
408
                }
409
            }
410
        }
411
        else if ($this->style == 'HTML')
412
        {
413
            switch ($entity->type)
414
            {
415
                case 'bold':
416
                {
417
                    $startString = '<b>';
418
                    break;
419
                }
420
                case 'italic':
421
                {
422
                    $startString = '<i>';
423
                    break;
424
                }
425
                case 'underline':
426
                {
427
                    $startString = '<u>';
428
                    break;
429
                }
430
                case 'strikethrough':
431
                {
432
                    $startString = '<s>';
433
                    break;
434
                }
435
                case 'spoiler':
436
                {
437
                    $startString = '<span class="tg-spoiler">';
438
                    break;
439
                }
440
                case 'code':
441
                {
442
                    $startString = '<code>';
443
                    break;
444
                }
445
                case 'pre':
446
                {
447
                    $startString = '<pre>';
448
                    if (isset($entity->language))
449
                    {
450
                        $startString .= '<code class="language-'.$entity->language.'">';
451
                    }
452
                    break;
453
                }
454
                case 'text_mention':
455
                {
456
                    $startString = '<a href="tg://user?id='.$entity->user->id.'">';
457
                    break;
458
                }
459
                case 'text_link':
460
                {
461
                    $startString = '<a href="'.$entity->url.'">';
462
                    break;
463
                }
464
                case 'custom_emoji':
465
                {
466
                    $startString = '<tg-emoji emoji-id="'.$entity->custom_emoji_id.'">';
467
                    break;
468
                }
469
                case 'blockquote':
470
                {
471
                    $startString = '<blockquote>';
472
                    break;
473
                }
474
            }
475
        }
476
        else if ($this->style == 'MarkdownV2')
477
        {
478
            switch ($entity->type)
479
            {
480
                case 'bold':
481
                {
482
                    $startString = '*';
483
                    break;
484
                }
485
                case 'italic':
486
                {
487
                    $startString = '_';
488
                    break;
489
                }
490
                case 'spoiler':
491
                {
492
                    $startString = '||';
493
                    break;
494
                }
495
                case 'code':
496
                {
497
                    $startString = '`';
498
                    break;
499
                }
500
                case 'pre':
501
                {
502
                    $startString = '```';
503
                    if (isset($entity->language))
504
                    {
505
                        $startString .= $entity->language;
506
                    }
507
                    $startString .= "\n";
508
                    break;
509
                }
510
                case 'underline':
511
                {
512
                    $startString .= '__';
513
                    break;
514
                }
515
                case 'strikethrough':
516
                {
517
                    $startString .= '~';
518
                    break;
519
                }
520
                case 'text_mention':
521
                case 'text_link':
522
                {
523
                    $startString = '[';
524
                    break;
525
                }
526
                case 'custom_emoji':
527
                {
528
                    $startString = '![';
529
                    break;
530
                }
531
                case 'blockquote':
532
                {
533
                    $startString = '>';
534
                    break;
535
                }
536
            }
537
        }
538
        return $startString;
539
    }
540
541
    /**
542
     * Check if there are entities that start at the given position and return them
543
     */
544
    protected function checkForEntityStart($pos)
545
    {
546
        $entities = [];
547
        foreach ($this->entities as $entity)
548
        {
549
            if ($entity->offset == $pos)
550
            {
551
                if (in_array($entity->type, $this->entitiesToParse))
552
                {
553
                    $entities[] = $entity;
554
                }
555
            }
556
        }
557
        if (!empty($entities)) {
558
            return $entities;
559
        } else {
560
            return false;
561
        }
562
    }
563
564
    /**
565
     * Get the end string of the entity  for the choosen style
566
     */
567
    protected function getEntityStopString($entity)
568
    {
569
        $stopString = '';
570
        if ($this->style == 'Markdown')
571
        {
572
            switch ($entity->type)
573
            {
574
                case 'bold':
575
                {
576
                    $stopString = '*';
577
                    break;
578
                }
579
                case 'italic':
580
                {
581
                    $stopString = '_';
582
                    break;
583
                }
584
                case 'code':
585
                {
586
                    $stopString = '`';
587
                    break;
588
                }
589
                case 'pre':
590
                {
591
                    $stopString = "\n".'```';
592
                    break;
593
                }
594
                case 'text_mention':
595
                {
596
                    $stopString = '](tg://user?id='.$entity->user->id.')';
597
                    break;
598
                }
599
                case 'text_link':
600
                {
601
                    $stopString = ']('.$entity->url.')';
602
                    break;
603
                }
604
            }
605
        }
606
        else if ($this->style == 'HTML')
607
        {
608
            switch ($entity->type)
609
            {
610
                case 'bold':
611
                {
612
                    $stopString = '</b>';
613
                    break;
614
                }
615
                case 'italic':
616
                {
617
                    $stopString = '</i>';
618
                    break;
619
                }
620
                case 'underline':
621
                {
622
                    $stopString = '</u>';
623
                    break;
624
                }
625
                case 'strikethrough':
626
                {
627
                    $stopString = '</s>';
628
                    break;
629
                }
630
                case 'spoiler':
631
                {
632
                    $stopString = '</span>';
633
                    break;
634
                }
635
                case 'code':
636
                {
637
                    $stopString = '</code>';
638
                    break;
639
                }
640
                case 'pre':
641
                {
642
                    if (isset($entity->language))
643
                    {
644
                        $stopString = '</code>';
645
                    }
646
                    $stopString .= '</pre>';
647
                    break;
648
                }
649
                case 'text_mention':
650
                case 'text_link':
651
                {
652
                    $stopString = '</a>';
653
                    break;
654
                }
655
                case 'custom_emoji':
656
                {
657
                    $stopString = '</tg-emoji>';
658
                    break;
659
                }
660
                case 'blockquote':
661
                {
662
                    $stopString = '</blockquote>';
663
                    break;
664
                }
665
            }
666
        }
667
        else if ($this->style == 'MarkdownV2')
668
        {
669
            switch ($entity->type)
670
            {
671
                case 'bold':
672
                {
673
                    $stopString = '*';
674
                    break;
675
                }
676
                case 'italic':
677
                {
678
                    $stopString = '_';
679
                    break;
680
                }
681
                case 'spoiler':
682
                {
683
                    $stopString = '||';
684
                    break;
685
                }
686
                case 'code':
687
                {
688
                    $stopString = '`';
689
                    break;
690
                }
691
                case 'pre':
692
                {
693
                    $stopString = "\n".'```';
694
                    break;
695
                }
696
                case 'underline':
697
                {
698
                    $stopString = '__';
699
                    break;
700
                }
701
                case 'strikethrough':
702
                {
703
                    $stopString = '~';
704
                    break;
705
                }
706
                case 'text_mention':
707
                {
708
                    $stopString = '](tg://user?id='.$entity->user->id.')';
709
                    break;
710
                }
711
                case 'text_link':
712
                {
713
                    $stopString = ']('.$entity->url.')';
714
                    break;
715
                }
716
                case 'custom_emoji':
717
                {
718
                    $stopString = '](tg://emoji?id='.$entity->custom_emoji_id.')';
719
                    break;
720
                }
721
            }
722
        }
723
        return $stopString;
724
    }
725
726
    /**
727
     * Check if there are entities that end at the given position and return them (reversed because they are nested)
728
     */
729
    protected function checkForEntityStop($pos)
730
    {
731
        $entities = [];
732
        foreach ($this->entities as $entity)
733
        {
734
            if ($entity->offset + $entity->length == $pos)
735
            {
736
                if (in_array($entity->type, $this->entitiesToParse))
737
                {
738
                    $entities[] = $entity;
739
                }
740
            }
741
        }
742
        if (!empty($entities)) {
743
            return array_reverse($entities);
744
        } else {
745
            return false;
746
        }
747
    }
748
749
    /**
750
     * Check for ambiguous entities in MarkdownV2 style (see Telegram docs)
751
     */
752
    protected function checkMarkdownV2AmbiguousEntities(&$entitiesToCheck)
753
    {
754
        $result = false;
755
        $newEntities = [];
756
        $foundIndex = 0;
757
        foreach ($entitiesToCheck as $ec)
758
        {
759
            if ($ec->type == 'italic' || $ec->type == 'underline')
760
            {
761
                $foundIndex++;
762
            }
763
        }
764
        if ($foundIndex == 2)
765
        {
766
            $result = true;
767
            foreach ($entitiesToCheck as $ec)
768
            {
769
                if ($ec->type != 'italic' && $ec->type != 'underline')
770
                {
771
                    $newEntities[] = $ec;
772
                }
773
            }
774
            $entitiesToCheck = $newEntities;
775
        }
776
        return $result;
777
    }
778
779
    /**
780
     * Count UTF-16 code units of the char passed
781
     */
782
    protected function getUTF16CodePointsLength($char) {
783
        $chunks = str_split(bin2hex(mb_convert_encoding($char, 'UTF-16')), 4);
0 ignored issues
show
Bug introduced by
It seems like mb_convert_encoding($char, 'UTF-16') can also be of type array; however, parameter $string of bin2hex() does only seem to accept string, 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

783
        $chunks = str_split(bin2hex(/** @scrutinizer ignore-type */ mb_convert_encoding($char, 'UTF-16')), 4);
Loading history...
784
        return count($chunks);
0 ignored issues
show
Bug introduced by
It seems like $chunks can also be of type true; however, parameter $value of count() does only seem to accept Countable|array, 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

784
        return count(/** @scrutinizer ignore-type */ $chunks);
Loading history...
785
    }
786
}
787