├── .travis.yml ├── Dockerfile ├── LICENSE ├── README.md ├── bin └── dev-checks ├── composer.json ├── docker-compose.yml ├── infection.json.dist ├── phpbench.json └── src ├── CodePage.php └── EncodingDetector.php /.travis.yml: -------------------------------------------------------------------------------- 1 | language: php 2 | sudo: false 3 | dist: trusty 4 | 5 | cache: 6 | directories: 7 | - $HOME/.cache/composer/files 8 | 9 | php: 10 | - '7.3' 11 | - '7.4' 12 | 13 | #matrix: 14 | # fast_finish: true 15 | # include: 16 | # - php: 7.4snapshot 17 | # env: NO_INFECTION=1 18 | # - php: nightly 19 | # env: NO_INFECTION=1 20 | 21 | #before_install: 22 | # - if [[ $NO_XDEBUG == 1 ]]; then phpenv config-rm xdebug.ini || echo 'No xdebug config.'; fi 23 | # - test "$TRAVIS_PHP_VERSION" != "nightly" || export COMPOSER_FLAGS="$COMPOSER_FLAGS --ignore-platform-reqs" 24 | 25 | before_script: 26 | - phpenv config-rm xdebug.ini 27 | - pecl install pcov 28 | - composer self-update 29 | - COMPOSER_MEMORY_LIMIT=-1 travis_retry composer install --prefer-dist $COMPOSER_FLAGS 30 | 31 | script: 32 | - composer validate --no-check-all 33 | - composer phpcs 34 | - composer phpmd 35 | - composer phpstan 36 | # - if [[ $NO_INFECTION != 1 ]]; then composer infection; fi 37 | - composer coverage 38 | - composer infection 39 | 40 | 41 | notifications: 42 | email: false 43 | -------------------------------------------------------------------------------- /Dockerfile: -------------------------------------------------------------------------------- 1 | FROM php:7.3-cli-alpine 2 | 3 | MAINTAINER onnov@ya.ru 4 | 5 | ARG UID=1001 6 | ARG GID=1001 7 | 8 | RUN apk add --no-cache --update bash git shadow gnu-libiconv g++ autoconf make pcre2-dev\ 9 | && pecl install pcov \ 10 | && docker-php-ext-install iconv \ 11 | && docker-php-ext-enable pcov \ 12 | && apk del --no-cache g++ autoconf make pcre2-dev \ 13 | && rm -rf /tmp/* /var/cache/apk/* \ 14 | && php -r "copy('https://getcomposer.org/installer', 'composer-setup.php');" \ 15 | && php composer-setup.php --install-dir=/bin --filename=composer \ 16 | && php -r "unlink('composer-setup.php');" \ 17 | && usermod -u $UID www-data -s /bin/bash && groupmod -g $GID www-data \ 18 | && rm -rf /tmp/* /var/tmp/* /usr/share/doc/* /var/cache/apk/* /usr/share/php7 \ 19 | && chmod 0777 /var/log -R 20 | 21 | ## iconv hack https://github.com/docker-library/php/issues/240 22 | ENV LD_PRELOAD /usr/lib/preloadable_libiconv.so php 23 | 24 | WORKDIR /var/www/de 25 | USER www-data 26 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2019 onnov 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | [![Build Status](https://travis-ci.org/onnov/detect-encoding.svg?branch=master)](https://travis-ci.org/onnov/detect-encoding) 2 | [![Scrutinizer Code Quality](https://scrutinizer-ci.com/g/onnov/detect-encoding/badges/quality-score.png?b=master)](https://scrutinizer-ci.com/g/onnov/detect-encoding/?branch=master) 3 | [![Code Coverage](https://scrutinizer-ci.com/g/onnov/detect-encoding/badges/coverage.png?b=master)](https://scrutinizer-ci.com/g/onnov/detect-encoding/?branch=master) 4 | [![Latest Stable Version](https://poser.pugx.org/onnov/detect-encoding/v/stable)](https://packagist.org/packages/onnov/detect-encoding) 5 | [![License](https://poser.pugx.org/onnov/detect-encoding/license)](https://packagist.org/packages/onnov/detect-encoding) 6 | 7 | # Detect encoding 8 | 9 | Text encoding definition class based on a range of code page character numbers. 10 | 11 | So far, in PHP v7.* the `mb_detect_encoding` function does not work well. 12 | Therefore, you have to somehow solve this problem. 13 | This class is one solution. 14 | 15 | Built-in encodings and accuracy: 16 | 17 | letters -> | 5 | 15 | 30 | 60 | 120 | 180 | 270 18 | --- | --- | --- | --- |--- |--- |--- |--- 19 | windows-1251 | 99.13 | 98.83 | 98.54 | 99.04 | 99.73 | 99.93 | 100.0 20 | koi8-r | 99.89 | 99.98 | 100.0 | 100.0 | 100.0 | 100.0 | 100.0 21 | iso-8859-5 | 81.79 | 99.27 | 99.98 | 100.0 | 100.0 | 100.0 | 100.0 22 | ibm866 | 99.81 | 99.99 | 100.0 | 100.0 | 100.0 | 100.0 | 100.0 23 | MacCyrillic | 12.79 | 47.49 | 73.48 | 92.15 | 99.30 | 99.94 | 100.0 24 | 25 | Worst accuracy with MacCyrillic, you need at least 60 characters to determine this encoding with an accuracy of 92.15%. Windows-1251 encoding also has very poor accuracy. This is because the numbers of their characters in the tables overlap very much. 26 | 27 | Fortunately, MacCyrillic and ibm866 encodings are not used to encode web pages. By default, they are disabled in the script, but you can enable them if necessary. 28 | 29 | letters -> | 5 | 10 | 15 | 30 | 60 | 30 | --- | --- | --- | --- |--- |--- | 31 | windows-1251 | 99.40 | 99.69 | 99.86 | 99.97 | 100.0 | 32 | koi8-r | 99.89 | 99.98 | 99.98 | 100.0 | 100.0 | 33 | iso-8859-5 | 81.79 | 96.41 | 99.27 | 99.98 | 100.0 | 34 | 35 | The accuracy of the determination is high even in short sentences from 5 to 10 letters. And for phrases from 60 letters, the accuracy of determination reaches 100%. 36 | 37 | Determining the encoding is very fast, for example, text longer than 1,300,000 Cyrillic characters is checked in 0.00096 sec. (on my computer) 38 | 39 | Link to the idea: http://patttern.blogspot.com/2012/07/php-python.html 40 | 41 | --- 42 | ## Installation 43 | [Composer](https://getcomposer.org) (recommended) 44 | Use Composer to install this library from Packagist: onnov/captcha 45 | 46 | Run the following command from your project directory to add the dependency: 47 | ```bash 48 | composer require onnov/detect-encoding 49 | ``` 50 | 51 | Alternatively, add the dependency directly to your composer.json file: 52 | ```json 53 | { 54 | "require": { 55 | "onnov/detect-encoding": "^1.0" 56 | } 57 | } 58 | ``` 59 | 60 | The classes in the project are structured according to the PSR-4 standard, so you can also use your own autoloader or require the needed files directly in your code. 61 | 62 | --- 63 | ## Usage 64 | ```php 65 | use Onnov\DetectEncoding\EncodingDetector; 66 | 67 | $detector = new EncodingDetector(); 68 | ``` 69 | 70 | * Definition of text encoding: 71 | ```php 72 | use Onnov\DetectEncoding\EncodingDetector; 73 | 74 | $detector = new EncodingDetector(); 75 | 76 | $text = 'Проверяемый текст'; 77 | $detector->getEncoding($text); 78 | ``` 79 | 80 | * Method for converting text of an unknown encoding into a given encoding, by default in utf-8 81 | optional parameters: 82 | ```php 83 | use Onnov\DetectEncoding\EncodingDetector; 84 | 85 | $detector = new EncodingDetector(); 86 | 87 | /** 88 | * Method for converting text of an unknown encoding into a given encoding, by default in utf-8 89 | * optional parameters: 90 | * $extra = '//TRANSLIT' (default setting) , other options: '' or '//IGNORE' 91 | * $encoding = 'utf-8' (default setting) , other options: any encoding that is available iconv 92 | * 93 | * @param string $text 94 | * @param string $extra 95 | * @param string $encoding 96 | * 97 | * @return string 98 | * @throws RuntimeException 99 | */ 100 | 101 | $detector->iconvXtoEncoding($text); 102 | ``` 103 | 104 | * Method to enable encoding definition: 105 | ```php 106 | use Onnov\DetectEncoding\EncodingDetector; 107 | 108 | $detector = new EncodingDetector(); 109 | 110 | $detector->enableEncoding([ 111 | EncodingDetector::IBM866, 112 | EncodingDetector::MAC_CYRILLIC, 113 | ]); 114 | ``` 115 | 116 | * Method to disable encoding definition: 117 | ```php 118 | use Onnov\DetectEncoding\EncodingDetector; 119 | 120 | $detector = new EncodingDetector(); 121 | 122 | $detector->disableEncoding([ 123 | EncodingDetector::ISO_8859_5, 124 | ]); 125 | ``` 126 | 127 | * Method for adding custom encoding: 128 | ```php 129 | use Onnov\DetectEncoding\EncodingDetector; 130 | 131 | $detector = new EncodingDetector(); 132 | 133 | $detector->addEncoding([ 134 | 'encodingName' => [ 135 | 'upper' => '1-50,200-250,253', // uppercase character number range 136 | 'lower' => '55-100,120-180,199', // lowercase character number range 137 | ], 138 | ]); 139 | ``` 140 | 141 | * Method to get a custom encoding range: 142 | ```php 143 | use Onnov\DetectEncoding\CodePage; 144 | 145 | // utf-8 encoded alphabet 146 | $cyrillicUppercase = 'АБВГДЕЁЖЗИЙКЛМНОПРСТУФЧЦЧШЩЪЫЬЭЮЯ'; 147 | $cyrillicLowercase = 'абвгдеёжзийклмнопрстуфхцчшщъыьэюя'; 148 | 149 | $codePage = new CodePage(); 150 | $encodingRange = $codePage->getRange($cyrillicUppercase, $cyrillicLowercase, 'koi8-u'); 151 | ``` 152 | 153 | [Tests and examples for the project](https://github.com/onnov/detect-encoding-test) 154 | 155 | --- 156 | 157 | ## Symfony use 158 | Add in services.yaml file: 159 | ```yaml 160 | services: 161 | Onnov\DetectEncoding\EncodingDetector: 162 | autowire: true 163 | ``` 164 | -------------------------------------------------------------------------------- /bin/dev-checks: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | vendor/bin/phpcs --config-set encoding utf-8 4 | vendor/bin/phpcs -p --standard=PSR12 ./src/ ./tests/ 5 | vendor/bin/phpmd src/ text cleancode,codesize,controversial,design,naming,unusedcode 6 | vendor/bin/phpstan analyse src/ -c phpstan.neon --level=7 --no-progress -vvv --memory-limit=-1 7 | vendor/bin/phpbench run benchmarks --report=default 8 | vendor/bin/infection --min-msi=50 --min-covered-msi=70 --log-verbosity=all 9 | vendor/bin/phpunit --coverage-text --coverage-html ./build/coverage/html --coverage-clover ./build/coverage/clover.xml 10 | -------------------------------------------------------------------------------- /composer.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "onnov/detect-encoding", 3 | "description": "Text encoding definition class instead of mb_detect_encoding. Defines: utf-8, windows-1251, koi8-r, iso-8859-5, ibm866, .....", 4 | "type": "library", 5 | "keywords": [ 6 | "mb_detect_encoding", 7 | "encoding", 8 | "utf-8", 9 | "windows-1251", 10 | "koi8-r", 11 | "iso-8859-5", 12 | "ibm866", 13 | "cyrillic", 14 | "iconv" 15 | ], 16 | "homepage": "https://github.com/onnov/detect-encoding", 17 | "license": "MIT", 18 | "authors": [ 19 | { 20 | "name": "onnov", 21 | "email": "oblnn@yandex.ru" 22 | } 23 | ], 24 | "require": { 25 | "php": ">=7.3", 26 | "ext-iconv": "*" 27 | }, 28 | "autoload": { 29 | "psr-4": { 30 | "Onnov\\DetectEncoding\\": "src/" 31 | } 32 | }, 33 | "autoload-dev": { 34 | "psr-4": { 35 | "Onnov\\DetectEncoding\\Tests\\": "tests/" 36 | } 37 | }, 38 | "require-dev": { 39 | "phpunit/phpunit": "*", 40 | "phpbench/phpbench": "*", 41 | "phpmd/phpmd": "*", 42 | "phpstan/phpstan": "*", 43 | "phpstan/phpstan-strict-rules": "*", 44 | "squizlabs/php_codesniffer": "*", 45 | "infection/infection": "*", 46 | "phpcompatibility/php-compatibility": "*", 47 | "roave/backward-compatibility-check": "*" 48 | }, 49 | "scripts": { 50 | "phpcs": "phpcs", 51 | "phpcbf": "phpcbf", 52 | "phpmd": "phpmd src text cleancode,codesize,controversial,design,naming,unusedcode", 53 | "phpstan": "phpstan analyse src/ -c phpstan.neon --level=7 --no-progress -vvv --memory-limit=-1", 54 | "phpunit": "phpunit", 55 | "infection": "infection --log-verbosity=all", 56 | "coveragehtml": "phpunit --coverage-text --coverage-html ./build/coverage/html --coverage-clover ./build/coverage/clover.xml", 57 | "coverage": "phpunit --coverage-text --coverage-clover clover.xml", 58 | "phpbench": "phpbench run benchmarks --report=default", 59 | "bccheck": "roave-backward-compatibility-check" 60 | } 61 | } 62 | -------------------------------------------------------------------------------- /docker-compose.yml: -------------------------------------------------------------------------------- 1 | version: '3.7' 2 | 3 | services: 4 | 5 | de_app: 6 | container_name: de_app 7 | build: 8 | context: . 9 | args: 10 | UID: ${UID:-1001} 11 | GID: ${GID:-1001} 12 | command: tail -f /dev/null 13 | volumes: 14 | - ".:/var/www/de" 15 | - "~/.ssh/:/home/www-data/.ssh/" 16 | -------------------------------------------------------------------------------- /infection.json.dist: -------------------------------------------------------------------------------- 1 | { 2 | "timeout": 5, 3 | "source": { 4 | "directories": [ 5 | "src" 6 | ] 7 | }, 8 | "logs": { 9 | "text": "build/infection/text.log", 10 | "summary": "build/infection/summary.log", 11 | "debug": "build/infection/debug.log", 12 | "perMutator": "build/infection/perMutator.md" 13 | }, 14 | "mutators": { 15 | "@default": true 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /phpbench.json: -------------------------------------------------------------------------------- 1 | { 2 | "bootstrap": "vendor/autoload.php" 3 | } 4 | -------------------------------------------------------------------------------- /src/CodePage.php: -------------------------------------------------------------------------------- 1 | > 28 | */ 29 | public function getRange( 30 | string $uppercaseLetters, 31 | string $lowercaseLetters, 32 | string $encoding 33 | ): array { 34 | return [ 35 | $encoding => [ 36 | 'upper' => $this->getRangeStr($this->getLetterArr( 37 | $uppercaseLetters, 38 | $encoding 39 | )), 40 | 'lower' => $this->getRangeStr($this->getLetterArr( 41 | $lowercaseLetters, 42 | $encoding 43 | )), 44 | ], 45 | ]; 46 | } 47 | 48 | /** 49 | * @param array $array 50 | * 51 | * @return string 52 | */ 53 | private function getRangeStr(array $array): string 54 | { 55 | $ranges = []; 56 | $last = null; 57 | foreach ($array as $current) { 58 | if ($current > $last + 1) { 59 | $lastKey = array_key_last($ranges); 60 | if (null !== $lastKey) { 61 | $ranges[$lastKey][1] = $last; 62 | } 63 | $ranges[] = [$current, null]; 64 | } 65 | $last = (int)$current; 66 | } 67 | $lastKey = array_key_last($ranges); 68 | $ranges[$lastKey][1] = $last; 69 | 70 | $stringIntervals = []; 71 | foreach ($ranges as $interval) { 72 | if (current($interval) < end($interval)) { 73 | $stringIntervals[] = implode('-', $interval); 74 | continue; 75 | } 76 | $stringIntervals[] = array_pop($interval); 77 | } 78 | 79 | return implode(', ', $stringIntervals); 80 | } 81 | 82 | /** 83 | * @param string $strLetters 84 | * @param string $encoding 85 | * 86 | * @return array 87 | */ 88 | private function getLetterArr(string &$strLetters, string $encoding): array 89 | { 90 | $res = []; 91 | $str = iconv('utf-8', $encoding . '//IGNORE', $strLetters); 92 | if (is_string($str)) { 93 | $res = array_keys(count_chars($str, 1)); 94 | } 95 | 96 | return $res; 97 | } 98 | } 99 | -------------------------------------------------------------------------------- /src/EncodingDetector.php: -------------------------------------------------------------------------------- 1 | > */ 33 | protected $rangeModel 34 | = [ 35 | self::WINDOWS_1251 => [ 36 | 'upper' => '168, 192-212, 214-223', 37 | 'lower' => '184, 224-255', 38 | ], 39 | self::KOI8_R => [ 40 | 'upper' => '179, 224-231, 233-255', 41 | 'lower' => '163, 192-223', 42 | ], 43 | self::ISO_8859_5 => [ 44 | 'upper' => '161, 176-196, 198-207', 45 | 'lower' => '208-239, 241', 46 | ], 47 | self::IBM866 => [ 48 | 'upper' => '128-148, 150-159, 240', 49 | 'lower' => '160-175, 224-239, 241', 50 | ], 51 | self::MAC_CYRILLIC => [ 52 | 'upper' => '128-148, 150-159, 221', 53 | 'lower' => '222-254', 54 | ], 55 | ]; 56 | 57 | /** @var mixed[] */ 58 | protected $ranges; 59 | 60 | /** 61 | * EncodingDetector constructor. 62 | */ 63 | public function __construct() 64 | { 65 | // default setting 66 | $this->enableEncoding( 67 | [ 68 | self::WINDOWS_1251, 69 | self::KOI8_R, 70 | self::ISO_8859_5, 71 | ] 72 | ); 73 | } 74 | 75 | /** 76 | * Method to enable encoding definition 77 | * Example: 78 | * $detector->enableEncoding([ 79 | * $detector::IBM866, 80 | * $detector::MAC_CYRILLIC, 81 | * ]); 82 | * 83 | * @param mixed[] $encodingList 84 | */ 85 | public function enableEncoding(array $encodingList): void 86 | { 87 | foreach ($encodingList as $encoding) { 88 | if (isset($this->rangeModel[$encoding])) { 89 | $this->ranges[$encoding] 90 | = $this->getRanges($this->rangeModel[$encoding]); 91 | } 92 | } 93 | } 94 | 95 | /** 96 | * Method to disable encoding definition 97 | * Example: 98 | * $detector->disableEncoding([ 99 | * $detector::ISO_8859_5, 100 | * ]); 101 | * 102 | * @param string[] $encodingList 103 | */ 104 | public function disableEncoding(array $encodingList): void 105 | { 106 | foreach ($encodingList as $encoding) { 107 | unset($this->ranges[$encoding]); 108 | } 109 | } 110 | 111 | /** 112 | * Method for adding custom encoding 113 | * Example: 114 | * $detector->addEncoding([ 115 | * 'encodingName' => [ 116 | * 'upper' => '1-50,200-250,253', // uppercase character number range 117 | * 'lower' => '55-100,120-180,199', // lowercase character number range 118 | * ], 119 | * ]); 120 | * 121 | * @param mixed[] $ranges 122 | */ 123 | public function addEncoding(array $ranges): void 124 | { 125 | foreach ($ranges as $encoding => $config) { 126 | if (isset($config['upper'], $config['lower'])) { 127 | $this->ranges[$encoding] = $this->getRanges($config); 128 | } 129 | } 130 | } 131 | 132 | /** 133 | * Method for converting text of an unknown encoding into a given encoding, by default in utf-8 134 | * optional parameters: 135 | * $extra = '//TRANSLIT' (default setting) , other options: '' or '//IGNORE' 136 | * $encoding = 'utf-8' (default setting) , other options: any encoding that is available iconv 137 | * 138 | * @param string $text 139 | * @param string $extra 140 | * @param string $encoding 141 | * 142 | * @return string 143 | * @throws RuntimeException 144 | */ 145 | public function iconvXtoEncoding( 146 | string &$text, 147 | string $extra = '//TRANSLIT', 148 | string $encoding = EncodingDetector::UTF_8 149 | ): string { 150 | $res = $text; 151 | $xec = $this->getEncoding($text); 152 | if ($xec !== $encoding) { 153 | $msg = 'iconv returned false'; 154 | try { 155 | $res = iconv($xec, $encoding . $extra, $text); 156 | if ($res === false) { 157 | throw new RuntimeException($msg); 158 | } 159 | } catch (Exception $error) { 160 | $msg = $error->getMessage(); 161 | throw new RuntimeException($msg); 162 | } 163 | } 164 | 165 | return $res; 166 | } 167 | 168 | /** 169 | * Definition of text encoding 170 | * 171 | * @param string $text 172 | * 173 | * @return string 174 | */ 175 | public function getEncoding(string &$text): string 176 | { 177 | $result = $this::UTF_8; 178 | if ($this->isUtf($text) === false) { 179 | $res = []; 180 | $chars = count_chars($text, 1); 181 | foreach ($this->ranges as $encoding => $config) { 182 | $upc = array_intersect_key($chars, $config['upper']); 183 | $loc = array_intersect_key($chars, $config['lower']); 184 | $res[$encoding] = (array_sum($upc) + array_sum($loc) * self::LOWER_FACTOR); 185 | } 186 | asort($res); 187 | $result = (string)array_key_last($res); 188 | } 189 | 190 | return $result; 191 | } 192 | 193 | /** 194 | * UTF Encoding Definition Method 195 | * 196 | * @param string $text 197 | * 198 | * @return bool 199 | */ 200 | private function isUtf(string &$text): bool 201 | { 202 | return (bool)preg_match('/./u', $text); 203 | } 204 | 205 | /** 206 | * @param mixed[] $config 207 | * 208 | * @return mixed[] 209 | */ 210 | private function getRanges(array $config): array 211 | { 212 | return [ 213 | 'upper' => $this->getRange($config['upper']), 214 | 'lower' => $this->getRange($config['lower']), 215 | ]; 216 | } 217 | 218 | /** 219 | * Method to convert a range from a string to an array 220 | * 221 | * @param string $str 222 | * 223 | * @return int[] 224 | */ 225 | private function getRange(string &$str): array 226 | { 227 | $ranges = []; 228 | foreach (explode(',', $str) as $item) { 229 | $arr = explode('-', $item); 230 | if (count($arr) > 1) { 231 | $ranges[] = implode(',', range($arr[0], $arr[1])); 232 | } 233 | } 234 | 235 | return array_flip(explode(',', implode(',', $ranges))); 236 | } 237 | 238 | /** 239 | * @return array> 240 | */ 241 | public function getEncodingList(): array 242 | { 243 | return $this->ranges; 244 | } 245 | 246 | /** 247 | * @return string[][] 248 | */ 249 | public function getRangeModel(): array 250 | { 251 | return $this->rangeModel; 252 | } 253 | } 254 | --------------------------------------------------------------------------------