├── README.md ├── composer.json ├── composer.lock └── src ├── Alphabet ├── AlphabetInterface.php ├── InMemoryAlphabet.php └── Utf8Alphabet.php ├── Config.php ├── DamerauLevenshtein.php ├── DataStore ├── DataStoreInterface.php ├── InMemoryDataStore.php └── NullDataStore.php ├── Levenshtein.php ├── StateSet ├── CostAnnotatedStateSet.php ├── InMemoryStateSet.php └── StateSetInterface.php └── StateSetIndex.php /README.md: -------------------------------------------------------------------------------- 1 | # State Set Index Implementation for PHP 2 | 3 | This implements the algorithm presented in the 2012 research paper "Efficient Similarity Search 4 | in Very Large String Sets" by Dandy Fenz, Dustin Lange, Astrid Rheinländer, Felix Naumann, 5 | and Ulf Leser from the Hasso Plattner Institute, Potsdam, Germany and Humboldt-Universität zu Berlin, Department of 6 | Computer Science, Berlin, Germany. 7 | 8 | The algorithm allows to efficiently search through huge datasets with typos (Levenshtein distance) while keeping the 9 | index size small. [Download the paper and read all the details here][Paper]. 10 | 11 | ## Installation 12 | 13 | Use Composer: 14 | 15 | ``` 16 | composer require toflar/state-set-index 17 | ``` 18 | 19 | ## Usage 20 | 21 | ```php 22 | namespace App; 23 | 24 | use Toflar\StateSetIndex\Alphabet\Utf8Alphabet; 25 | use Toflar\StateSetIndex\DataStore\InMemoryDataStore; 26 | use Toflar\StateSetIndex\StateSet\InMemoryStateSet; 27 | use Toflar\StateSetIndex\StateSetIndex; 28 | 29 | $stateSetIndex = new StateSetIndex( 30 | new Config(6, 4), 31 | new Utf8Alphabet(), 32 | new InMemoryStateSet(), 33 | new InMemoryDataStore() 34 | ); 35 | 36 | $stateSetIndex->index(['Mueller', 'Müller', 'Muentner', 'Muster', 'Mustermann']); 37 | $stateSetIndex->find('Mustre', 2); // Will return ['Muster']; 38 | ``` 39 | 40 | ## Configuration 41 | 42 | You can configure the maximum index length and maximum alphabet size with the `Config` object. Read the 43 | paper for details on what they do. There's no such thing as a recommended size as it very much depends on what 44 | you want to index and or search. 45 | 46 | ## Customization 47 | 48 | This library ships with the algorithm readily prepared for you to use. The main customization areas will be 49 | the alphabet (both the way it maps characters to labels) and the state set storage, if you want to make the index 50 | persistent. Hence, there are two interfaces that allow you to implement your own logic: 51 | 52 | * The `AlphabetInterface` is very straight-forward. It only consists of a `map(string $char, int $alphabetSize)` method 53 | which the library needs to map characters to an internal label. Whether you load/store the alphabet in some 54 | database is up to you. The library ships with an `InMemoryAlphabet` for reference and simple use cases. You don't 55 | even need to store the alphabet as we already have one with the UTF-8 codepoints, that's what `Utf8Alphabet` is 56 | for. In case you don't want to customize the labels, use `Utf8Alphabet`. 57 | * The `StateSetInterface` is responsible to load and store information about the state set of your index. Again, 58 | how you load/store the state set in some database is up to you. The library ships with an `InMemoryStateSet` 59 | for reference and simple use cases and tests. 60 | * The `DataStoreInterface` is responsible for storing the string you index alongside its assigned state. Sometimes 61 | you want to completely customize storage in which case you can use the `NullDataStore` and only use the 62 | assignments you get as a return value from calling `$stateSetIndex->index()`. 63 | 64 | You can not only ask for the final matching results using `$stateSetIndex->findMatchingStates('Mustre', 2)` which is 65 | already filtered using a multibyte implementation of the Levenshtein algorithm, but you can also access intermediary 66 | results which you can use to e.g. search your own database for states etc.: 67 | 68 | * `$stateSetIndex->findMatchingStates('Mustre', 2)` returns the matching states only. 69 | * `$stateSetIndex->findAcceptedStrings('Mustre', 2)` returns the matching states and the respective accepted strings 70 | (unfiltered for false-positives!). 71 | * `$stateSetIndex->find('Mustre', 2)` returns the real matches, filtered for false-positives. 72 | 73 | [Paper]: https://hpi.de/fileadmin/user_upload/fachgebiete/naumann/publications/PDFs/2012_fenz_efficient.pdf -------------------------------------------------------------------------------- /composer.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "toflar/state-set-index", 3 | "description": "Implementation of the State Set Index Algorithm", 4 | "type": "library", 5 | "license": "MIT", 6 | "autoload": { 7 | "psr-4": { 8 | "Toflar\\StateSetIndex\\": "src/" 9 | } 10 | }, 11 | "autoload-dev": { 12 | "psr-4": { 13 | "Toflar\\StateSetIndex\\Test\\": "tests/" 14 | } 15 | }, 16 | "authors": [ 17 | { 18 | "name": "Yanick Witschi", 19 | "email": "yanick.witschi@terminal42.ch" 20 | } 21 | ], 22 | "require": { 23 | "php": "^8.1", 24 | "ext-mbstring": "*" 25 | }, 26 | "require-dev": { 27 | "symplify/easy-coding-standard": "^11.3", 28 | "phpunit/phpunit": "^10.2" 29 | }, 30 | "scripts": { 31 | "cs-fix": "@php vendor/bin/ecs --fix" 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /composer.lock: -------------------------------------------------------------------------------- 1 | { 2 | "_readme": [ 3 | "This file locks the dependencies of your project to a known state", 4 | "Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies", 5 | "This file is @generated automatically" 6 | ], 7 | "content-hash": "e34f8a6ddab0cadd8adc17acdc552e3a", 8 | "packages": [], 9 | "packages-dev": [ 10 | { 11 | "name": "myclabs/deep-copy", 12 | "version": "1.11.1", 13 | "source": { 14 | "type": "git", 15 | "url": "https://github.com/myclabs/DeepCopy.git", 16 | "reference": "7284c22080590fb39f2ffa3e9057f10a4ddd0e0c" 17 | }, 18 | "dist": { 19 | "type": "zip", 20 | "url": "https://api.github.com/repos/myclabs/DeepCopy/zipball/7284c22080590fb39f2ffa3e9057f10a4ddd0e0c", 21 | "reference": "7284c22080590fb39f2ffa3e9057f10a4ddd0e0c", 22 | "shasum": "" 23 | }, 24 | "require": { 25 | "php": "^7.1 || ^8.0" 26 | }, 27 | "conflict": { 28 | "doctrine/collections": "<1.6.8", 29 | "doctrine/common": "<2.13.3 || >=3,<3.2.2" 30 | }, 31 | "require-dev": { 32 | "doctrine/collections": "^1.6.8", 33 | "doctrine/common": "^2.13.3 || ^3.2.2", 34 | "phpunit/phpunit": "^7.5.20 || ^8.5.23 || ^9.5.13" 35 | }, 36 | "type": "library", 37 | "autoload": { 38 | "files": [ 39 | "src/DeepCopy/deep_copy.php" 40 | ], 41 | "psr-4": { 42 | "DeepCopy\\": "src/DeepCopy/" 43 | } 44 | }, 45 | "notification-url": "https://packagist.org/downloads/", 46 | "license": [ 47 | "MIT" 48 | ], 49 | "description": "Create deep copies (clones) of your objects", 50 | "keywords": [ 51 | "clone", 52 | "copy", 53 | "duplicate", 54 | "object", 55 | "object graph" 56 | ], 57 | "support": { 58 | "issues": "https://github.com/myclabs/DeepCopy/issues", 59 | "source": "https://github.com/myclabs/DeepCopy/tree/1.11.1" 60 | }, 61 | "funding": [ 62 | { 63 | "url": "https://tidelift.com/funding/github/packagist/myclabs/deep-copy", 64 | "type": "tidelift" 65 | } 66 | ], 67 | "time": "2023-03-08T13:26:56+00:00" 68 | }, 69 | { 70 | "name": "nikic/php-parser", 71 | "version": "v4.15.5", 72 | "source": { 73 | "type": "git", 74 | "url": "https://github.com/nikic/PHP-Parser.git", 75 | "reference": "11e2663a5bc9db5d714eedb4277ee300403b4a9e" 76 | }, 77 | "dist": { 78 | "type": "zip", 79 | "url": "https://api.github.com/repos/nikic/PHP-Parser/zipball/11e2663a5bc9db5d714eedb4277ee300403b4a9e", 80 | "reference": "11e2663a5bc9db5d714eedb4277ee300403b4a9e", 81 | "shasum": "" 82 | }, 83 | "require": { 84 | "ext-tokenizer": "*", 85 | "php": ">=7.0" 86 | }, 87 | "require-dev": { 88 | "ircmaxell/php-yacc": "^0.0.7", 89 | "phpunit/phpunit": "^6.5 || ^7.0 || ^8.0 || ^9.0" 90 | }, 91 | "bin": [ 92 | "bin/php-parse" 93 | ], 94 | "type": "library", 95 | "extra": { 96 | "branch-alias": { 97 | "dev-master": "4.9-dev" 98 | } 99 | }, 100 | "autoload": { 101 | "psr-4": { 102 | "PhpParser\\": "lib/PhpParser" 103 | } 104 | }, 105 | "notification-url": "https://packagist.org/downloads/", 106 | "license": [ 107 | "BSD-3-Clause" 108 | ], 109 | "authors": [ 110 | { 111 | "name": "Nikita Popov" 112 | } 113 | ], 114 | "description": "A PHP parser written in PHP", 115 | "keywords": [ 116 | "parser", 117 | "php" 118 | ], 119 | "support": { 120 | "issues": "https://github.com/nikic/PHP-Parser/issues", 121 | "source": "https://github.com/nikic/PHP-Parser/tree/v4.15.5" 122 | }, 123 | "time": "2023-05-19T20:20:00+00:00" 124 | }, 125 | { 126 | "name": "phar-io/manifest", 127 | "version": "2.0.3", 128 | "source": { 129 | "type": "git", 130 | "url": "https://github.com/phar-io/manifest.git", 131 | "reference": "97803eca37d319dfa7826cc2437fc020857acb53" 132 | }, 133 | "dist": { 134 | "type": "zip", 135 | "url": "https://api.github.com/repos/phar-io/manifest/zipball/97803eca37d319dfa7826cc2437fc020857acb53", 136 | "reference": "97803eca37d319dfa7826cc2437fc020857acb53", 137 | "shasum": "" 138 | }, 139 | "require": { 140 | "ext-dom": "*", 141 | "ext-phar": "*", 142 | "ext-xmlwriter": "*", 143 | "phar-io/version": "^3.0.1", 144 | "php": "^7.2 || ^8.0" 145 | }, 146 | "type": "library", 147 | "extra": { 148 | "branch-alias": { 149 | "dev-master": "2.0.x-dev" 150 | } 151 | }, 152 | "autoload": { 153 | "classmap": [ 154 | "src/" 155 | ] 156 | }, 157 | "notification-url": "https://packagist.org/downloads/", 158 | "license": [ 159 | "BSD-3-Clause" 160 | ], 161 | "authors": [ 162 | { 163 | "name": "Arne Blankerts", 164 | "email": "arne@blankerts.de", 165 | "role": "Developer" 166 | }, 167 | { 168 | "name": "Sebastian Heuer", 169 | "email": "sebastian@phpeople.de", 170 | "role": "Developer" 171 | }, 172 | { 173 | "name": "Sebastian Bergmann", 174 | "email": "sebastian@phpunit.de", 175 | "role": "Developer" 176 | } 177 | ], 178 | "description": "Component for reading phar.io manifest information from a PHP Archive (PHAR)", 179 | "support": { 180 | "issues": "https://github.com/phar-io/manifest/issues", 181 | "source": "https://github.com/phar-io/manifest/tree/2.0.3" 182 | }, 183 | "time": "2021-07-20T11:28:43+00:00" 184 | }, 185 | { 186 | "name": "phar-io/version", 187 | "version": "3.2.1", 188 | "source": { 189 | "type": "git", 190 | "url": "https://github.com/phar-io/version.git", 191 | "reference": "4f7fd7836c6f332bb2933569e566a0d6c4cbed74" 192 | }, 193 | "dist": { 194 | "type": "zip", 195 | "url": "https://api.github.com/repos/phar-io/version/zipball/4f7fd7836c6f332bb2933569e566a0d6c4cbed74", 196 | "reference": "4f7fd7836c6f332bb2933569e566a0d6c4cbed74", 197 | "shasum": "" 198 | }, 199 | "require": { 200 | "php": "^7.2 || ^8.0" 201 | }, 202 | "type": "library", 203 | "autoload": { 204 | "classmap": [ 205 | "src/" 206 | ] 207 | }, 208 | "notification-url": "https://packagist.org/downloads/", 209 | "license": [ 210 | "BSD-3-Clause" 211 | ], 212 | "authors": [ 213 | { 214 | "name": "Arne Blankerts", 215 | "email": "arne@blankerts.de", 216 | "role": "Developer" 217 | }, 218 | { 219 | "name": "Sebastian Heuer", 220 | "email": "sebastian@phpeople.de", 221 | "role": "Developer" 222 | }, 223 | { 224 | "name": "Sebastian Bergmann", 225 | "email": "sebastian@phpunit.de", 226 | "role": "Developer" 227 | } 228 | ], 229 | "description": "Library for handling version information and constraints", 230 | "support": { 231 | "issues": "https://github.com/phar-io/version/issues", 232 | "source": "https://github.com/phar-io/version/tree/3.2.1" 233 | }, 234 | "time": "2022-02-21T01:04:05+00:00" 235 | }, 236 | { 237 | "name": "phpunit/php-code-coverage", 238 | "version": "10.1.2", 239 | "source": { 240 | "type": "git", 241 | "url": "https://github.com/sebastianbergmann/php-code-coverage.git", 242 | "reference": "db1497ec8dd382e82c962f7abbe0320e4882ee4e" 243 | }, 244 | "dist": { 245 | "type": "zip", 246 | "url": "https://api.github.com/repos/sebastianbergmann/php-code-coverage/zipball/db1497ec8dd382e82c962f7abbe0320e4882ee4e", 247 | "reference": "db1497ec8dd382e82c962f7abbe0320e4882ee4e", 248 | "shasum": "" 249 | }, 250 | "require": { 251 | "ext-dom": "*", 252 | "ext-libxml": "*", 253 | "ext-xmlwriter": "*", 254 | "nikic/php-parser": "^4.15", 255 | "php": ">=8.1", 256 | "phpunit/php-file-iterator": "^4.0", 257 | "phpunit/php-text-template": "^3.0", 258 | "sebastian/code-unit-reverse-lookup": "^3.0", 259 | "sebastian/complexity": "^3.0", 260 | "sebastian/environment": "^6.0", 261 | "sebastian/lines-of-code": "^2.0", 262 | "sebastian/version": "^4.0", 263 | "theseer/tokenizer": "^1.2.0" 264 | }, 265 | "require-dev": { 266 | "phpunit/phpunit": "^10.1" 267 | }, 268 | "suggest": { 269 | "ext-pcov": "PHP extension that provides line coverage", 270 | "ext-xdebug": "PHP extension that provides line coverage as well as branch and path coverage" 271 | }, 272 | "type": "library", 273 | "extra": { 274 | "branch-alias": { 275 | "dev-main": "10.1-dev" 276 | } 277 | }, 278 | "autoload": { 279 | "classmap": [ 280 | "src/" 281 | ] 282 | }, 283 | "notification-url": "https://packagist.org/downloads/", 284 | "license": [ 285 | "BSD-3-Clause" 286 | ], 287 | "authors": [ 288 | { 289 | "name": "Sebastian Bergmann", 290 | "email": "sebastian@phpunit.de", 291 | "role": "lead" 292 | } 293 | ], 294 | "description": "Library that provides collection, processing, and rendering functionality for PHP code coverage information.", 295 | "homepage": "https://github.com/sebastianbergmann/php-code-coverage", 296 | "keywords": [ 297 | "coverage", 298 | "testing", 299 | "xunit" 300 | ], 301 | "support": { 302 | "issues": "https://github.com/sebastianbergmann/php-code-coverage/issues", 303 | "security": "https://github.com/sebastianbergmann/php-code-coverage/security/policy", 304 | "source": "https://github.com/sebastianbergmann/php-code-coverage/tree/10.1.2" 305 | }, 306 | "funding": [ 307 | { 308 | "url": "https://github.com/sebastianbergmann", 309 | "type": "github" 310 | } 311 | ], 312 | "time": "2023-05-22T09:04:27+00:00" 313 | }, 314 | { 315 | "name": "phpunit/php-file-iterator", 316 | "version": "4.0.2", 317 | "source": { 318 | "type": "git", 319 | "url": "https://github.com/sebastianbergmann/php-file-iterator.git", 320 | "reference": "5647d65443818959172645e7ed999217360654b6" 321 | }, 322 | "dist": { 323 | "type": "zip", 324 | "url": "https://api.github.com/repos/sebastianbergmann/php-file-iterator/zipball/5647d65443818959172645e7ed999217360654b6", 325 | "reference": "5647d65443818959172645e7ed999217360654b6", 326 | "shasum": "" 327 | }, 328 | "require": { 329 | "php": ">=8.1" 330 | }, 331 | "require-dev": { 332 | "phpunit/phpunit": "^10.0" 333 | }, 334 | "type": "library", 335 | "extra": { 336 | "branch-alias": { 337 | "dev-main": "4.0-dev" 338 | } 339 | }, 340 | "autoload": { 341 | "classmap": [ 342 | "src/" 343 | ] 344 | }, 345 | "notification-url": "https://packagist.org/downloads/", 346 | "license": [ 347 | "BSD-3-Clause" 348 | ], 349 | "authors": [ 350 | { 351 | "name": "Sebastian Bergmann", 352 | "email": "sebastian@phpunit.de", 353 | "role": "lead" 354 | } 355 | ], 356 | "description": "FilterIterator implementation that filters files based on a list of suffixes.", 357 | "homepage": "https://github.com/sebastianbergmann/php-file-iterator/", 358 | "keywords": [ 359 | "filesystem", 360 | "iterator" 361 | ], 362 | "support": { 363 | "issues": "https://github.com/sebastianbergmann/php-file-iterator/issues", 364 | "security": "https://github.com/sebastianbergmann/php-file-iterator/security/policy", 365 | "source": "https://github.com/sebastianbergmann/php-file-iterator/tree/4.0.2" 366 | }, 367 | "funding": [ 368 | { 369 | "url": "https://github.com/sebastianbergmann", 370 | "type": "github" 371 | } 372 | ], 373 | "time": "2023-05-07T09:13:23+00:00" 374 | }, 375 | { 376 | "name": "phpunit/php-invoker", 377 | "version": "4.0.0", 378 | "source": { 379 | "type": "git", 380 | "url": "https://github.com/sebastianbergmann/php-invoker.git", 381 | "reference": "f5e568ba02fa5ba0ddd0f618391d5a9ea50b06d7" 382 | }, 383 | "dist": { 384 | "type": "zip", 385 | "url": "https://api.github.com/repos/sebastianbergmann/php-invoker/zipball/f5e568ba02fa5ba0ddd0f618391d5a9ea50b06d7", 386 | "reference": "f5e568ba02fa5ba0ddd0f618391d5a9ea50b06d7", 387 | "shasum": "" 388 | }, 389 | "require": { 390 | "php": ">=8.1" 391 | }, 392 | "require-dev": { 393 | "ext-pcntl": "*", 394 | "phpunit/phpunit": "^10.0" 395 | }, 396 | "suggest": { 397 | "ext-pcntl": "*" 398 | }, 399 | "type": "library", 400 | "extra": { 401 | "branch-alias": { 402 | "dev-main": "4.0-dev" 403 | } 404 | }, 405 | "autoload": { 406 | "classmap": [ 407 | "src/" 408 | ] 409 | }, 410 | "notification-url": "https://packagist.org/downloads/", 411 | "license": [ 412 | "BSD-3-Clause" 413 | ], 414 | "authors": [ 415 | { 416 | "name": "Sebastian Bergmann", 417 | "email": "sebastian@phpunit.de", 418 | "role": "lead" 419 | } 420 | ], 421 | "description": "Invoke callables with a timeout", 422 | "homepage": "https://github.com/sebastianbergmann/php-invoker/", 423 | "keywords": [ 424 | "process" 425 | ], 426 | "support": { 427 | "issues": "https://github.com/sebastianbergmann/php-invoker/issues", 428 | "source": "https://github.com/sebastianbergmann/php-invoker/tree/4.0.0" 429 | }, 430 | "funding": [ 431 | { 432 | "url": "https://github.com/sebastianbergmann", 433 | "type": "github" 434 | } 435 | ], 436 | "time": "2023-02-03T06:56:09+00:00" 437 | }, 438 | { 439 | "name": "phpunit/php-text-template", 440 | "version": "3.0.0", 441 | "source": { 442 | "type": "git", 443 | "url": "https://github.com/sebastianbergmann/php-text-template.git", 444 | "reference": "9f3d3709577a527025f55bcf0f7ab8052c8bb37d" 445 | }, 446 | "dist": { 447 | "type": "zip", 448 | "url": "https://api.github.com/repos/sebastianbergmann/php-text-template/zipball/9f3d3709577a527025f55bcf0f7ab8052c8bb37d", 449 | "reference": "9f3d3709577a527025f55bcf0f7ab8052c8bb37d", 450 | "shasum": "" 451 | }, 452 | "require": { 453 | "php": ">=8.1" 454 | }, 455 | "require-dev": { 456 | "phpunit/phpunit": "^10.0" 457 | }, 458 | "type": "library", 459 | "extra": { 460 | "branch-alias": { 461 | "dev-main": "3.0-dev" 462 | } 463 | }, 464 | "autoload": { 465 | "classmap": [ 466 | "src/" 467 | ] 468 | }, 469 | "notification-url": "https://packagist.org/downloads/", 470 | "license": [ 471 | "BSD-3-Clause" 472 | ], 473 | "authors": [ 474 | { 475 | "name": "Sebastian Bergmann", 476 | "email": "sebastian@phpunit.de", 477 | "role": "lead" 478 | } 479 | ], 480 | "description": "Simple template engine.", 481 | "homepage": "https://github.com/sebastianbergmann/php-text-template/", 482 | "keywords": [ 483 | "template" 484 | ], 485 | "support": { 486 | "issues": "https://github.com/sebastianbergmann/php-text-template/issues", 487 | "source": "https://github.com/sebastianbergmann/php-text-template/tree/3.0.0" 488 | }, 489 | "funding": [ 490 | { 491 | "url": "https://github.com/sebastianbergmann", 492 | "type": "github" 493 | } 494 | ], 495 | "time": "2023-02-03T06:56:46+00:00" 496 | }, 497 | { 498 | "name": "phpunit/php-timer", 499 | "version": "6.0.0", 500 | "source": { 501 | "type": "git", 502 | "url": "https://github.com/sebastianbergmann/php-timer.git", 503 | "reference": "e2a2d67966e740530f4a3343fe2e030ffdc1161d" 504 | }, 505 | "dist": { 506 | "type": "zip", 507 | "url": "https://api.github.com/repos/sebastianbergmann/php-timer/zipball/e2a2d67966e740530f4a3343fe2e030ffdc1161d", 508 | "reference": "e2a2d67966e740530f4a3343fe2e030ffdc1161d", 509 | "shasum": "" 510 | }, 511 | "require": { 512 | "php": ">=8.1" 513 | }, 514 | "require-dev": { 515 | "phpunit/phpunit": "^10.0" 516 | }, 517 | "type": "library", 518 | "extra": { 519 | "branch-alias": { 520 | "dev-main": "6.0-dev" 521 | } 522 | }, 523 | "autoload": { 524 | "classmap": [ 525 | "src/" 526 | ] 527 | }, 528 | "notification-url": "https://packagist.org/downloads/", 529 | "license": [ 530 | "BSD-3-Clause" 531 | ], 532 | "authors": [ 533 | { 534 | "name": "Sebastian Bergmann", 535 | "email": "sebastian@phpunit.de", 536 | "role": "lead" 537 | } 538 | ], 539 | "description": "Utility class for timing", 540 | "homepage": "https://github.com/sebastianbergmann/php-timer/", 541 | "keywords": [ 542 | "timer" 543 | ], 544 | "support": { 545 | "issues": "https://github.com/sebastianbergmann/php-timer/issues", 546 | "source": "https://github.com/sebastianbergmann/php-timer/tree/6.0.0" 547 | }, 548 | "funding": [ 549 | { 550 | "url": "https://github.com/sebastianbergmann", 551 | "type": "github" 552 | } 553 | ], 554 | "time": "2023-02-03T06:57:52+00:00" 555 | }, 556 | { 557 | "name": "phpunit/phpunit", 558 | "version": "10.2.2", 559 | "source": { 560 | "type": "git", 561 | "url": "https://github.com/sebastianbergmann/phpunit.git", 562 | "reference": "1ab521b24b88b88310c40c26c0cc4a94ba40ff95" 563 | }, 564 | "dist": { 565 | "type": "zip", 566 | "url": "https://api.github.com/repos/sebastianbergmann/phpunit/zipball/1ab521b24b88b88310c40c26c0cc4a94ba40ff95", 567 | "reference": "1ab521b24b88b88310c40c26c0cc4a94ba40ff95", 568 | "shasum": "" 569 | }, 570 | "require": { 571 | "ext-dom": "*", 572 | "ext-json": "*", 573 | "ext-libxml": "*", 574 | "ext-mbstring": "*", 575 | "ext-xml": "*", 576 | "ext-xmlwriter": "*", 577 | "myclabs/deep-copy": "^1.10.1", 578 | "phar-io/manifest": "^2.0.3", 579 | "phar-io/version": "^3.0.2", 580 | "php": ">=8.1", 581 | "phpunit/php-code-coverage": "^10.1.1", 582 | "phpunit/php-file-iterator": "^4.0", 583 | "phpunit/php-invoker": "^4.0", 584 | "phpunit/php-text-template": "^3.0", 585 | "phpunit/php-timer": "^6.0", 586 | "sebastian/cli-parser": "^2.0", 587 | "sebastian/code-unit": "^2.0", 588 | "sebastian/comparator": "^5.0", 589 | "sebastian/diff": "^5.0", 590 | "sebastian/environment": "^6.0", 591 | "sebastian/exporter": "^5.0", 592 | "sebastian/global-state": "^6.0", 593 | "sebastian/object-enumerator": "^5.0", 594 | "sebastian/recursion-context": "^5.0", 595 | "sebastian/type": "^4.0", 596 | "sebastian/version": "^4.0" 597 | }, 598 | "suggest": { 599 | "ext-soap": "To be able to generate mocks based on WSDL files" 600 | }, 601 | "bin": [ 602 | "phpunit" 603 | ], 604 | "type": "library", 605 | "extra": { 606 | "branch-alias": { 607 | "dev-main": "10.2-dev" 608 | } 609 | }, 610 | "autoload": { 611 | "files": [ 612 | "src/Framework/Assert/Functions.php" 613 | ], 614 | "classmap": [ 615 | "src/" 616 | ] 617 | }, 618 | "notification-url": "https://packagist.org/downloads/", 619 | "license": [ 620 | "BSD-3-Clause" 621 | ], 622 | "authors": [ 623 | { 624 | "name": "Sebastian Bergmann", 625 | "email": "sebastian@phpunit.de", 626 | "role": "lead" 627 | } 628 | ], 629 | "description": "The PHP Unit Testing framework.", 630 | "homepage": "https://phpunit.de/", 631 | "keywords": [ 632 | "phpunit", 633 | "testing", 634 | "xunit" 635 | ], 636 | "support": { 637 | "issues": "https://github.com/sebastianbergmann/phpunit/issues", 638 | "security": "https://github.com/sebastianbergmann/phpunit/security/policy", 639 | "source": "https://github.com/sebastianbergmann/phpunit/tree/10.2.2" 640 | }, 641 | "funding": [ 642 | { 643 | "url": "https://phpunit.de/sponsors.html", 644 | "type": "custom" 645 | }, 646 | { 647 | "url": "https://github.com/sebastianbergmann", 648 | "type": "github" 649 | }, 650 | { 651 | "url": "https://tidelift.com/funding/github/packagist/phpunit/phpunit", 652 | "type": "tidelift" 653 | } 654 | ], 655 | "time": "2023-06-11T06:15:20+00:00" 656 | }, 657 | { 658 | "name": "sebastian/cli-parser", 659 | "version": "2.0.0", 660 | "source": { 661 | "type": "git", 662 | "url": "https://github.com/sebastianbergmann/cli-parser.git", 663 | "reference": "efdc130dbbbb8ef0b545a994fd811725c5282cae" 664 | }, 665 | "dist": { 666 | "type": "zip", 667 | "url": "https://api.github.com/repos/sebastianbergmann/cli-parser/zipball/efdc130dbbbb8ef0b545a994fd811725c5282cae", 668 | "reference": "efdc130dbbbb8ef0b545a994fd811725c5282cae", 669 | "shasum": "" 670 | }, 671 | "require": { 672 | "php": ">=8.1" 673 | }, 674 | "require-dev": { 675 | "phpunit/phpunit": "^10.0" 676 | }, 677 | "type": "library", 678 | "extra": { 679 | "branch-alias": { 680 | "dev-main": "2.0-dev" 681 | } 682 | }, 683 | "autoload": { 684 | "classmap": [ 685 | "src/" 686 | ] 687 | }, 688 | "notification-url": "https://packagist.org/downloads/", 689 | "license": [ 690 | "BSD-3-Clause" 691 | ], 692 | "authors": [ 693 | { 694 | "name": "Sebastian Bergmann", 695 | "email": "sebastian@phpunit.de", 696 | "role": "lead" 697 | } 698 | ], 699 | "description": "Library for parsing CLI options", 700 | "homepage": "https://github.com/sebastianbergmann/cli-parser", 701 | "support": { 702 | "issues": "https://github.com/sebastianbergmann/cli-parser/issues", 703 | "source": "https://github.com/sebastianbergmann/cli-parser/tree/2.0.0" 704 | }, 705 | "funding": [ 706 | { 707 | "url": "https://github.com/sebastianbergmann", 708 | "type": "github" 709 | } 710 | ], 711 | "time": "2023-02-03T06:58:15+00:00" 712 | }, 713 | { 714 | "name": "sebastian/code-unit", 715 | "version": "2.0.0", 716 | "source": { 717 | "type": "git", 718 | "url": "https://github.com/sebastianbergmann/code-unit.git", 719 | "reference": "a81fee9eef0b7a76af11d121767abc44c104e503" 720 | }, 721 | "dist": { 722 | "type": "zip", 723 | "url": "https://api.github.com/repos/sebastianbergmann/code-unit/zipball/a81fee9eef0b7a76af11d121767abc44c104e503", 724 | "reference": "a81fee9eef0b7a76af11d121767abc44c104e503", 725 | "shasum": "" 726 | }, 727 | "require": { 728 | "php": ">=8.1" 729 | }, 730 | "require-dev": { 731 | "phpunit/phpunit": "^10.0" 732 | }, 733 | "type": "library", 734 | "extra": { 735 | "branch-alias": { 736 | "dev-main": "2.0-dev" 737 | } 738 | }, 739 | "autoload": { 740 | "classmap": [ 741 | "src/" 742 | ] 743 | }, 744 | "notification-url": "https://packagist.org/downloads/", 745 | "license": [ 746 | "BSD-3-Clause" 747 | ], 748 | "authors": [ 749 | { 750 | "name": "Sebastian Bergmann", 751 | "email": "sebastian@phpunit.de", 752 | "role": "lead" 753 | } 754 | ], 755 | "description": "Collection of value objects that represent the PHP code units", 756 | "homepage": "https://github.com/sebastianbergmann/code-unit", 757 | "support": { 758 | "issues": "https://github.com/sebastianbergmann/code-unit/issues", 759 | "source": "https://github.com/sebastianbergmann/code-unit/tree/2.0.0" 760 | }, 761 | "funding": [ 762 | { 763 | "url": "https://github.com/sebastianbergmann", 764 | "type": "github" 765 | } 766 | ], 767 | "time": "2023-02-03T06:58:43+00:00" 768 | }, 769 | { 770 | "name": "sebastian/code-unit-reverse-lookup", 771 | "version": "3.0.0", 772 | "source": { 773 | "type": "git", 774 | "url": "https://github.com/sebastianbergmann/code-unit-reverse-lookup.git", 775 | "reference": "5e3a687f7d8ae33fb362c5c0743794bbb2420a1d" 776 | }, 777 | "dist": { 778 | "type": "zip", 779 | "url": "https://api.github.com/repos/sebastianbergmann/code-unit-reverse-lookup/zipball/5e3a687f7d8ae33fb362c5c0743794bbb2420a1d", 780 | "reference": "5e3a687f7d8ae33fb362c5c0743794bbb2420a1d", 781 | "shasum": "" 782 | }, 783 | "require": { 784 | "php": ">=8.1" 785 | }, 786 | "require-dev": { 787 | "phpunit/phpunit": "^10.0" 788 | }, 789 | "type": "library", 790 | "extra": { 791 | "branch-alias": { 792 | "dev-main": "3.0-dev" 793 | } 794 | }, 795 | "autoload": { 796 | "classmap": [ 797 | "src/" 798 | ] 799 | }, 800 | "notification-url": "https://packagist.org/downloads/", 801 | "license": [ 802 | "BSD-3-Clause" 803 | ], 804 | "authors": [ 805 | { 806 | "name": "Sebastian Bergmann", 807 | "email": "sebastian@phpunit.de" 808 | } 809 | ], 810 | "description": "Looks up which function or method a line of code belongs to", 811 | "homepage": "https://github.com/sebastianbergmann/code-unit-reverse-lookup/", 812 | "support": { 813 | "issues": "https://github.com/sebastianbergmann/code-unit-reverse-lookup/issues", 814 | "source": "https://github.com/sebastianbergmann/code-unit-reverse-lookup/tree/3.0.0" 815 | }, 816 | "funding": [ 817 | { 818 | "url": "https://github.com/sebastianbergmann", 819 | "type": "github" 820 | } 821 | ], 822 | "time": "2023-02-03T06:59:15+00:00" 823 | }, 824 | { 825 | "name": "sebastian/comparator", 826 | "version": "5.0.0", 827 | "source": { 828 | "type": "git", 829 | "url": "https://github.com/sebastianbergmann/comparator.git", 830 | "reference": "72f01e6586e0caf6af81297897bd112eb7e9627c" 831 | }, 832 | "dist": { 833 | "type": "zip", 834 | "url": "https://api.github.com/repos/sebastianbergmann/comparator/zipball/72f01e6586e0caf6af81297897bd112eb7e9627c", 835 | "reference": "72f01e6586e0caf6af81297897bd112eb7e9627c", 836 | "shasum": "" 837 | }, 838 | "require": { 839 | "ext-dom": "*", 840 | "ext-mbstring": "*", 841 | "php": ">=8.1", 842 | "sebastian/diff": "^5.0", 843 | "sebastian/exporter": "^5.0" 844 | }, 845 | "require-dev": { 846 | "phpunit/phpunit": "^10.0" 847 | }, 848 | "type": "library", 849 | "extra": { 850 | "branch-alias": { 851 | "dev-main": "5.0-dev" 852 | } 853 | }, 854 | "autoload": { 855 | "classmap": [ 856 | "src/" 857 | ] 858 | }, 859 | "notification-url": "https://packagist.org/downloads/", 860 | "license": [ 861 | "BSD-3-Clause" 862 | ], 863 | "authors": [ 864 | { 865 | "name": "Sebastian Bergmann", 866 | "email": "sebastian@phpunit.de" 867 | }, 868 | { 869 | "name": "Jeff Welch", 870 | "email": "whatthejeff@gmail.com" 871 | }, 872 | { 873 | "name": "Volker Dusch", 874 | "email": "github@wallbash.com" 875 | }, 876 | { 877 | "name": "Bernhard Schussek", 878 | "email": "bschussek@2bepublished.at" 879 | } 880 | ], 881 | "description": "Provides the functionality to compare PHP values for equality", 882 | "homepage": "https://github.com/sebastianbergmann/comparator", 883 | "keywords": [ 884 | "comparator", 885 | "compare", 886 | "equality" 887 | ], 888 | "support": { 889 | "issues": "https://github.com/sebastianbergmann/comparator/issues", 890 | "source": "https://github.com/sebastianbergmann/comparator/tree/5.0.0" 891 | }, 892 | "funding": [ 893 | { 894 | "url": "https://github.com/sebastianbergmann", 895 | "type": "github" 896 | } 897 | ], 898 | "time": "2023-02-03T07:07:16+00:00" 899 | }, 900 | { 901 | "name": "sebastian/complexity", 902 | "version": "3.0.0", 903 | "source": { 904 | "type": "git", 905 | "url": "https://github.com/sebastianbergmann/complexity.git", 906 | "reference": "e67d240970c9dc7ea7b2123a6d520e334dd61dc6" 907 | }, 908 | "dist": { 909 | "type": "zip", 910 | "url": "https://api.github.com/repos/sebastianbergmann/complexity/zipball/e67d240970c9dc7ea7b2123a6d520e334dd61dc6", 911 | "reference": "e67d240970c9dc7ea7b2123a6d520e334dd61dc6", 912 | "shasum": "" 913 | }, 914 | "require": { 915 | "nikic/php-parser": "^4.10", 916 | "php": ">=8.1" 917 | }, 918 | "require-dev": { 919 | "phpunit/phpunit": "^10.0" 920 | }, 921 | "type": "library", 922 | "extra": { 923 | "branch-alias": { 924 | "dev-main": "3.0-dev" 925 | } 926 | }, 927 | "autoload": { 928 | "classmap": [ 929 | "src/" 930 | ] 931 | }, 932 | "notification-url": "https://packagist.org/downloads/", 933 | "license": [ 934 | "BSD-3-Clause" 935 | ], 936 | "authors": [ 937 | { 938 | "name": "Sebastian Bergmann", 939 | "email": "sebastian@phpunit.de", 940 | "role": "lead" 941 | } 942 | ], 943 | "description": "Library for calculating the complexity of PHP code units", 944 | "homepage": "https://github.com/sebastianbergmann/complexity", 945 | "support": { 946 | "issues": "https://github.com/sebastianbergmann/complexity/issues", 947 | "source": "https://github.com/sebastianbergmann/complexity/tree/3.0.0" 948 | }, 949 | "funding": [ 950 | { 951 | "url": "https://github.com/sebastianbergmann", 952 | "type": "github" 953 | } 954 | ], 955 | "time": "2023-02-03T06:59:47+00:00" 956 | }, 957 | { 958 | "name": "sebastian/diff", 959 | "version": "5.0.3", 960 | "source": { 961 | "type": "git", 962 | "url": "https://github.com/sebastianbergmann/diff.git", 963 | "reference": "912dc2fbe3e3c1e7873313cc801b100b6c68c87b" 964 | }, 965 | "dist": { 966 | "type": "zip", 967 | "url": "https://api.github.com/repos/sebastianbergmann/diff/zipball/912dc2fbe3e3c1e7873313cc801b100b6c68c87b", 968 | "reference": "912dc2fbe3e3c1e7873313cc801b100b6c68c87b", 969 | "shasum": "" 970 | }, 971 | "require": { 972 | "php": ">=8.1" 973 | }, 974 | "require-dev": { 975 | "phpunit/phpunit": "^10.0", 976 | "symfony/process": "^4.2 || ^5" 977 | }, 978 | "type": "library", 979 | "extra": { 980 | "branch-alias": { 981 | "dev-main": "5.0-dev" 982 | } 983 | }, 984 | "autoload": { 985 | "classmap": [ 986 | "src/" 987 | ] 988 | }, 989 | "notification-url": "https://packagist.org/downloads/", 990 | "license": [ 991 | "BSD-3-Clause" 992 | ], 993 | "authors": [ 994 | { 995 | "name": "Sebastian Bergmann", 996 | "email": "sebastian@phpunit.de" 997 | }, 998 | { 999 | "name": "Kore Nordmann", 1000 | "email": "mail@kore-nordmann.de" 1001 | } 1002 | ], 1003 | "description": "Diff implementation", 1004 | "homepage": "https://github.com/sebastianbergmann/diff", 1005 | "keywords": [ 1006 | "diff", 1007 | "udiff", 1008 | "unidiff", 1009 | "unified diff" 1010 | ], 1011 | "support": { 1012 | "issues": "https://github.com/sebastianbergmann/diff/issues", 1013 | "security": "https://github.com/sebastianbergmann/diff/security/policy", 1014 | "source": "https://github.com/sebastianbergmann/diff/tree/5.0.3" 1015 | }, 1016 | "funding": [ 1017 | { 1018 | "url": "https://github.com/sebastianbergmann", 1019 | "type": "github" 1020 | } 1021 | ], 1022 | "time": "2023-05-01T07:48:21+00:00" 1023 | }, 1024 | { 1025 | "name": "sebastian/environment", 1026 | "version": "6.0.1", 1027 | "source": { 1028 | "type": "git", 1029 | "url": "https://github.com/sebastianbergmann/environment.git", 1030 | "reference": "43c751b41d74f96cbbd4e07b7aec9675651e2951" 1031 | }, 1032 | "dist": { 1033 | "type": "zip", 1034 | "url": "https://api.github.com/repos/sebastianbergmann/environment/zipball/43c751b41d74f96cbbd4e07b7aec9675651e2951", 1035 | "reference": "43c751b41d74f96cbbd4e07b7aec9675651e2951", 1036 | "shasum": "" 1037 | }, 1038 | "require": { 1039 | "php": ">=8.1" 1040 | }, 1041 | "require-dev": { 1042 | "phpunit/phpunit": "^10.0" 1043 | }, 1044 | "suggest": { 1045 | "ext-posix": "*" 1046 | }, 1047 | "type": "library", 1048 | "extra": { 1049 | "branch-alias": { 1050 | "dev-main": "6.0-dev" 1051 | } 1052 | }, 1053 | "autoload": { 1054 | "classmap": [ 1055 | "src/" 1056 | ] 1057 | }, 1058 | "notification-url": "https://packagist.org/downloads/", 1059 | "license": [ 1060 | "BSD-3-Clause" 1061 | ], 1062 | "authors": [ 1063 | { 1064 | "name": "Sebastian Bergmann", 1065 | "email": "sebastian@phpunit.de" 1066 | } 1067 | ], 1068 | "description": "Provides functionality to handle HHVM/PHP environments", 1069 | "homepage": "https://github.com/sebastianbergmann/environment", 1070 | "keywords": [ 1071 | "Xdebug", 1072 | "environment", 1073 | "hhvm" 1074 | ], 1075 | "support": { 1076 | "issues": "https://github.com/sebastianbergmann/environment/issues", 1077 | "security": "https://github.com/sebastianbergmann/environment/security/policy", 1078 | "source": "https://github.com/sebastianbergmann/environment/tree/6.0.1" 1079 | }, 1080 | "funding": [ 1081 | { 1082 | "url": "https://github.com/sebastianbergmann", 1083 | "type": "github" 1084 | } 1085 | ], 1086 | "time": "2023-04-11T05:39:26+00:00" 1087 | }, 1088 | { 1089 | "name": "sebastian/exporter", 1090 | "version": "5.0.0", 1091 | "source": { 1092 | "type": "git", 1093 | "url": "https://github.com/sebastianbergmann/exporter.git", 1094 | "reference": "f3ec4bf931c0b31e5b413f5b4fc970a7d03338c0" 1095 | }, 1096 | "dist": { 1097 | "type": "zip", 1098 | "url": "https://api.github.com/repos/sebastianbergmann/exporter/zipball/f3ec4bf931c0b31e5b413f5b4fc970a7d03338c0", 1099 | "reference": "f3ec4bf931c0b31e5b413f5b4fc970a7d03338c0", 1100 | "shasum": "" 1101 | }, 1102 | "require": { 1103 | "ext-mbstring": "*", 1104 | "php": ">=8.1", 1105 | "sebastian/recursion-context": "^5.0" 1106 | }, 1107 | "require-dev": { 1108 | "phpunit/phpunit": "^10.0" 1109 | }, 1110 | "type": "library", 1111 | "extra": { 1112 | "branch-alias": { 1113 | "dev-main": "5.0-dev" 1114 | } 1115 | }, 1116 | "autoload": { 1117 | "classmap": [ 1118 | "src/" 1119 | ] 1120 | }, 1121 | "notification-url": "https://packagist.org/downloads/", 1122 | "license": [ 1123 | "BSD-3-Clause" 1124 | ], 1125 | "authors": [ 1126 | { 1127 | "name": "Sebastian Bergmann", 1128 | "email": "sebastian@phpunit.de" 1129 | }, 1130 | { 1131 | "name": "Jeff Welch", 1132 | "email": "whatthejeff@gmail.com" 1133 | }, 1134 | { 1135 | "name": "Volker Dusch", 1136 | "email": "github@wallbash.com" 1137 | }, 1138 | { 1139 | "name": "Adam Harvey", 1140 | "email": "aharvey@php.net" 1141 | }, 1142 | { 1143 | "name": "Bernhard Schussek", 1144 | "email": "bschussek@gmail.com" 1145 | } 1146 | ], 1147 | "description": "Provides the functionality to export PHP variables for visualization", 1148 | "homepage": "https://www.github.com/sebastianbergmann/exporter", 1149 | "keywords": [ 1150 | "export", 1151 | "exporter" 1152 | ], 1153 | "support": { 1154 | "issues": "https://github.com/sebastianbergmann/exporter/issues", 1155 | "source": "https://github.com/sebastianbergmann/exporter/tree/5.0.0" 1156 | }, 1157 | "funding": [ 1158 | { 1159 | "url": "https://github.com/sebastianbergmann", 1160 | "type": "github" 1161 | } 1162 | ], 1163 | "time": "2023-02-03T07:06:49+00:00" 1164 | }, 1165 | { 1166 | "name": "sebastian/global-state", 1167 | "version": "6.0.0", 1168 | "source": { 1169 | "type": "git", 1170 | "url": "https://github.com/sebastianbergmann/global-state.git", 1171 | "reference": "aab257c712de87b90194febd52e4d184551c2d44" 1172 | }, 1173 | "dist": { 1174 | "type": "zip", 1175 | "url": "https://api.github.com/repos/sebastianbergmann/global-state/zipball/aab257c712de87b90194febd52e4d184551c2d44", 1176 | "reference": "aab257c712de87b90194febd52e4d184551c2d44", 1177 | "shasum": "" 1178 | }, 1179 | "require": { 1180 | "php": ">=8.1", 1181 | "sebastian/object-reflector": "^3.0", 1182 | "sebastian/recursion-context": "^5.0" 1183 | }, 1184 | "require-dev": { 1185 | "ext-dom": "*", 1186 | "phpunit/phpunit": "^10.0" 1187 | }, 1188 | "type": "library", 1189 | "extra": { 1190 | "branch-alias": { 1191 | "dev-main": "6.0-dev" 1192 | } 1193 | }, 1194 | "autoload": { 1195 | "classmap": [ 1196 | "src/" 1197 | ] 1198 | }, 1199 | "notification-url": "https://packagist.org/downloads/", 1200 | "license": [ 1201 | "BSD-3-Clause" 1202 | ], 1203 | "authors": [ 1204 | { 1205 | "name": "Sebastian Bergmann", 1206 | "email": "sebastian@phpunit.de" 1207 | } 1208 | ], 1209 | "description": "Snapshotting of global state", 1210 | "homepage": "http://www.github.com/sebastianbergmann/global-state", 1211 | "keywords": [ 1212 | "global state" 1213 | ], 1214 | "support": { 1215 | "issues": "https://github.com/sebastianbergmann/global-state/issues", 1216 | "source": "https://github.com/sebastianbergmann/global-state/tree/6.0.0" 1217 | }, 1218 | "funding": [ 1219 | { 1220 | "url": "https://github.com/sebastianbergmann", 1221 | "type": "github" 1222 | } 1223 | ], 1224 | "time": "2023-02-03T07:07:38+00:00" 1225 | }, 1226 | { 1227 | "name": "sebastian/lines-of-code", 1228 | "version": "2.0.0", 1229 | "source": { 1230 | "type": "git", 1231 | "url": "https://github.com/sebastianbergmann/lines-of-code.git", 1232 | "reference": "17c4d940ecafb3d15d2cf916f4108f664e28b130" 1233 | }, 1234 | "dist": { 1235 | "type": "zip", 1236 | "url": "https://api.github.com/repos/sebastianbergmann/lines-of-code/zipball/17c4d940ecafb3d15d2cf916f4108f664e28b130", 1237 | "reference": "17c4d940ecafb3d15d2cf916f4108f664e28b130", 1238 | "shasum": "" 1239 | }, 1240 | "require": { 1241 | "nikic/php-parser": "^4.10", 1242 | "php": ">=8.1" 1243 | }, 1244 | "require-dev": { 1245 | "phpunit/phpunit": "^10.0" 1246 | }, 1247 | "type": "library", 1248 | "extra": { 1249 | "branch-alias": { 1250 | "dev-main": "2.0-dev" 1251 | } 1252 | }, 1253 | "autoload": { 1254 | "classmap": [ 1255 | "src/" 1256 | ] 1257 | }, 1258 | "notification-url": "https://packagist.org/downloads/", 1259 | "license": [ 1260 | "BSD-3-Clause" 1261 | ], 1262 | "authors": [ 1263 | { 1264 | "name": "Sebastian Bergmann", 1265 | "email": "sebastian@phpunit.de", 1266 | "role": "lead" 1267 | } 1268 | ], 1269 | "description": "Library for counting the lines of code in PHP source code", 1270 | "homepage": "https://github.com/sebastianbergmann/lines-of-code", 1271 | "support": { 1272 | "issues": "https://github.com/sebastianbergmann/lines-of-code/issues", 1273 | "source": "https://github.com/sebastianbergmann/lines-of-code/tree/2.0.0" 1274 | }, 1275 | "funding": [ 1276 | { 1277 | "url": "https://github.com/sebastianbergmann", 1278 | "type": "github" 1279 | } 1280 | ], 1281 | "time": "2023-02-03T07:08:02+00:00" 1282 | }, 1283 | { 1284 | "name": "sebastian/object-enumerator", 1285 | "version": "5.0.0", 1286 | "source": { 1287 | "type": "git", 1288 | "url": "https://github.com/sebastianbergmann/object-enumerator.git", 1289 | "reference": "202d0e344a580d7f7d04b3fafce6933e59dae906" 1290 | }, 1291 | "dist": { 1292 | "type": "zip", 1293 | "url": "https://api.github.com/repos/sebastianbergmann/object-enumerator/zipball/202d0e344a580d7f7d04b3fafce6933e59dae906", 1294 | "reference": "202d0e344a580d7f7d04b3fafce6933e59dae906", 1295 | "shasum": "" 1296 | }, 1297 | "require": { 1298 | "php": ">=8.1", 1299 | "sebastian/object-reflector": "^3.0", 1300 | "sebastian/recursion-context": "^5.0" 1301 | }, 1302 | "require-dev": { 1303 | "phpunit/phpunit": "^10.0" 1304 | }, 1305 | "type": "library", 1306 | "extra": { 1307 | "branch-alias": { 1308 | "dev-main": "5.0-dev" 1309 | } 1310 | }, 1311 | "autoload": { 1312 | "classmap": [ 1313 | "src/" 1314 | ] 1315 | }, 1316 | "notification-url": "https://packagist.org/downloads/", 1317 | "license": [ 1318 | "BSD-3-Clause" 1319 | ], 1320 | "authors": [ 1321 | { 1322 | "name": "Sebastian Bergmann", 1323 | "email": "sebastian@phpunit.de" 1324 | } 1325 | ], 1326 | "description": "Traverses array structures and object graphs to enumerate all referenced objects", 1327 | "homepage": "https://github.com/sebastianbergmann/object-enumerator/", 1328 | "support": { 1329 | "issues": "https://github.com/sebastianbergmann/object-enumerator/issues", 1330 | "source": "https://github.com/sebastianbergmann/object-enumerator/tree/5.0.0" 1331 | }, 1332 | "funding": [ 1333 | { 1334 | "url": "https://github.com/sebastianbergmann", 1335 | "type": "github" 1336 | } 1337 | ], 1338 | "time": "2023-02-03T07:08:32+00:00" 1339 | }, 1340 | { 1341 | "name": "sebastian/object-reflector", 1342 | "version": "3.0.0", 1343 | "source": { 1344 | "type": "git", 1345 | "url": "https://github.com/sebastianbergmann/object-reflector.git", 1346 | "reference": "24ed13d98130f0e7122df55d06c5c4942a577957" 1347 | }, 1348 | "dist": { 1349 | "type": "zip", 1350 | "url": "https://api.github.com/repos/sebastianbergmann/object-reflector/zipball/24ed13d98130f0e7122df55d06c5c4942a577957", 1351 | "reference": "24ed13d98130f0e7122df55d06c5c4942a577957", 1352 | "shasum": "" 1353 | }, 1354 | "require": { 1355 | "php": ">=8.1" 1356 | }, 1357 | "require-dev": { 1358 | "phpunit/phpunit": "^10.0" 1359 | }, 1360 | "type": "library", 1361 | "extra": { 1362 | "branch-alias": { 1363 | "dev-main": "3.0-dev" 1364 | } 1365 | }, 1366 | "autoload": { 1367 | "classmap": [ 1368 | "src/" 1369 | ] 1370 | }, 1371 | "notification-url": "https://packagist.org/downloads/", 1372 | "license": [ 1373 | "BSD-3-Clause" 1374 | ], 1375 | "authors": [ 1376 | { 1377 | "name": "Sebastian Bergmann", 1378 | "email": "sebastian@phpunit.de" 1379 | } 1380 | ], 1381 | "description": "Allows reflection of object attributes, including inherited and non-public ones", 1382 | "homepage": "https://github.com/sebastianbergmann/object-reflector/", 1383 | "support": { 1384 | "issues": "https://github.com/sebastianbergmann/object-reflector/issues", 1385 | "source": "https://github.com/sebastianbergmann/object-reflector/tree/3.0.0" 1386 | }, 1387 | "funding": [ 1388 | { 1389 | "url": "https://github.com/sebastianbergmann", 1390 | "type": "github" 1391 | } 1392 | ], 1393 | "time": "2023-02-03T07:06:18+00:00" 1394 | }, 1395 | { 1396 | "name": "sebastian/recursion-context", 1397 | "version": "5.0.0", 1398 | "source": { 1399 | "type": "git", 1400 | "url": "https://github.com/sebastianbergmann/recursion-context.git", 1401 | "reference": "05909fb5bc7df4c52992396d0116aed689f93712" 1402 | }, 1403 | "dist": { 1404 | "type": "zip", 1405 | "url": "https://api.github.com/repos/sebastianbergmann/recursion-context/zipball/05909fb5bc7df4c52992396d0116aed689f93712", 1406 | "reference": "05909fb5bc7df4c52992396d0116aed689f93712", 1407 | "shasum": "" 1408 | }, 1409 | "require": { 1410 | "php": ">=8.1" 1411 | }, 1412 | "require-dev": { 1413 | "phpunit/phpunit": "^10.0" 1414 | }, 1415 | "type": "library", 1416 | "extra": { 1417 | "branch-alias": { 1418 | "dev-main": "5.0-dev" 1419 | } 1420 | }, 1421 | "autoload": { 1422 | "classmap": [ 1423 | "src/" 1424 | ] 1425 | }, 1426 | "notification-url": "https://packagist.org/downloads/", 1427 | "license": [ 1428 | "BSD-3-Clause" 1429 | ], 1430 | "authors": [ 1431 | { 1432 | "name": "Sebastian Bergmann", 1433 | "email": "sebastian@phpunit.de" 1434 | }, 1435 | { 1436 | "name": "Jeff Welch", 1437 | "email": "whatthejeff@gmail.com" 1438 | }, 1439 | { 1440 | "name": "Adam Harvey", 1441 | "email": "aharvey@php.net" 1442 | } 1443 | ], 1444 | "description": "Provides functionality to recursively process PHP variables", 1445 | "homepage": "https://github.com/sebastianbergmann/recursion-context", 1446 | "support": { 1447 | "issues": "https://github.com/sebastianbergmann/recursion-context/issues", 1448 | "source": "https://github.com/sebastianbergmann/recursion-context/tree/5.0.0" 1449 | }, 1450 | "funding": [ 1451 | { 1452 | "url": "https://github.com/sebastianbergmann", 1453 | "type": "github" 1454 | } 1455 | ], 1456 | "time": "2023-02-03T07:05:40+00:00" 1457 | }, 1458 | { 1459 | "name": "sebastian/type", 1460 | "version": "4.0.0", 1461 | "source": { 1462 | "type": "git", 1463 | "url": "https://github.com/sebastianbergmann/type.git", 1464 | "reference": "462699a16464c3944eefc02ebdd77882bd3925bf" 1465 | }, 1466 | "dist": { 1467 | "type": "zip", 1468 | "url": "https://api.github.com/repos/sebastianbergmann/type/zipball/462699a16464c3944eefc02ebdd77882bd3925bf", 1469 | "reference": "462699a16464c3944eefc02ebdd77882bd3925bf", 1470 | "shasum": "" 1471 | }, 1472 | "require": { 1473 | "php": ">=8.1" 1474 | }, 1475 | "require-dev": { 1476 | "phpunit/phpunit": "^10.0" 1477 | }, 1478 | "type": "library", 1479 | "extra": { 1480 | "branch-alias": { 1481 | "dev-main": "4.0-dev" 1482 | } 1483 | }, 1484 | "autoload": { 1485 | "classmap": [ 1486 | "src/" 1487 | ] 1488 | }, 1489 | "notification-url": "https://packagist.org/downloads/", 1490 | "license": [ 1491 | "BSD-3-Clause" 1492 | ], 1493 | "authors": [ 1494 | { 1495 | "name": "Sebastian Bergmann", 1496 | "email": "sebastian@phpunit.de", 1497 | "role": "lead" 1498 | } 1499 | ], 1500 | "description": "Collection of value objects that represent the types of the PHP type system", 1501 | "homepage": "https://github.com/sebastianbergmann/type", 1502 | "support": { 1503 | "issues": "https://github.com/sebastianbergmann/type/issues", 1504 | "source": "https://github.com/sebastianbergmann/type/tree/4.0.0" 1505 | }, 1506 | "funding": [ 1507 | { 1508 | "url": "https://github.com/sebastianbergmann", 1509 | "type": "github" 1510 | } 1511 | ], 1512 | "time": "2023-02-03T07:10:45+00:00" 1513 | }, 1514 | { 1515 | "name": "sebastian/version", 1516 | "version": "4.0.1", 1517 | "source": { 1518 | "type": "git", 1519 | "url": "https://github.com/sebastianbergmann/version.git", 1520 | "reference": "c51fa83a5d8f43f1402e3f32a005e6262244ef17" 1521 | }, 1522 | "dist": { 1523 | "type": "zip", 1524 | "url": "https://api.github.com/repos/sebastianbergmann/version/zipball/c51fa83a5d8f43f1402e3f32a005e6262244ef17", 1525 | "reference": "c51fa83a5d8f43f1402e3f32a005e6262244ef17", 1526 | "shasum": "" 1527 | }, 1528 | "require": { 1529 | "php": ">=8.1" 1530 | }, 1531 | "type": "library", 1532 | "extra": { 1533 | "branch-alias": { 1534 | "dev-main": "4.0-dev" 1535 | } 1536 | }, 1537 | "autoload": { 1538 | "classmap": [ 1539 | "src/" 1540 | ] 1541 | }, 1542 | "notification-url": "https://packagist.org/downloads/", 1543 | "license": [ 1544 | "BSD-3-Clause" 1545 | ], 1546 | "authors": [ 1547 | { 1548 | "name": "Sebastian Bergmann", 1549 | "email": "sebastian@phpunit.de", 1550 | "role": "lead" 1551 | } 1552 | ], 1553 | "description": "Library that helps with managing the version number of Git-hosted PHP projects", 1554 | "homepage": "https://github.com/sebastianbergmann/version", 1555 | "support": { 1556 | "issues": "https://github.com/sebastianbergmann/version/issues", 1557 | "source": "https://github.com/sebastianbergmann/version/tree/4.0.1" 1558 | }, 1559 | "funding": [ 1560 | { 1561 | "url": "https://github.com/sebastianbergmann", 1562 | "type": "github" 1563 | } 1564 | ], 1565 | "time": "2023-02-07T11:34:05+00:00" 1566 | }, 1567 | { 1568 | "name": "symplify/easy-coding-standard", 1569 | "version": "11.5.0", 1570 | "source": { 1571 | "type": "git", 1572 | "url": "https://github.com/easy-coding-standard/easy-coding-standard.git", 1573 | "reference": "1d2400f7bfe92e3754ce71f0782f2c0521bade3d" 1574 | }, 1575 | "dist": { 1576 | "type": "zip", 1577 | "url": "https://api.github.com/repos/easy-coding-standard/easy-coding-standard/zipball/1d2400f7bfe92e3754ce71f0782f2c0521bade3d", 1578 | "reference": "1d2400f7bfe92e3754ce71f0782f2c0521bade3d", 1579 | "shasum": "" 1580 | }, 1581 | "require": { 1582 | "php": ">=7.2" 1583 | }, 1584 | "conflict": { 1585 | "friendsofphp/php-cs-fixer": "<3.0", 1586 | "squizlabs/php_codesniffer": "<3.6", 1587 | "symplify/coding-standard": "<11.3" 1588 | }, 1589 | "bin": [ 1590 | "bin/ecs" 1591 | ], 1592 | "type": "library", 1593 | "autoload": { 1594 | "files": [ 1595 | "bootstrap.php" 1596 | ] 1597 | }, 1598 | "notification-url": "https://packagist.org/downloads/", 1599 | "license": [ 1600 | "MIT" 1601 | ], 1602 | "description": "Use Coding Standard with 0-knowledge of PHP-CS-Fixer and PHP_CodeSniffer", 1603 | "keywords": [ 1604 | "Code style", 1605 | "automation", 1606 | "fixer", 1607 | "static analysis" 1608 | ], 1609 | "support": { 1610 | "issues": "https://github.com/easy-coding-standard/easy-coding-standard/issues", 1611 | "source": "https://github.com/easy-coding-standard/easy-coding-standard/tree/11.5.0" 1612 | }, 1613 | "funding": [ 1614 | { 1615 | "url": "https://www.paypal.me/rectorphp", 1616 | "type": "custom" 1617 | }, 1618 | { 1619 | "url": "https://github.com/tomasvotruba", 1620 | "type": "github" 1621 | } 1622 | ], 1623 | "time": "2023-06-21T06:26:15+00:00" 1624 | }, 1625 | { 1626 | "name": "theseer/tokenizer", 1627 | "version": "1.2.1", 1628 | "source": { 1629 | "type": "git", 1630 | "url": "https://github.com/theseer/tokenizer.git", 1631 | "reference": "34a41e998c2183e22995f158c581e7b5e755ab9e" 1632 | }, 1633 | "dist": { 1634 | "type": "zip", 1635 | "url": "https://api.github.com/repos/theseer/tokenizer/zipball/34a41e998c2183e22995f158c581e7b5e755ab9e", 1636 | "reference": "34a41e998c2183e22995f158c581e7b5e755ab9e", 1637 | "shasum": "" 1638 | }, 1639 | "require": { 1640 | "ext-dom": "*", 1641 | "ext-tokenizer": "*", 1642 | "ext-xmlwriter": "*", 1643 | "php": "^7.2 || ^8.0" 1644 | }, 1645 | "type": "library", 1646 | "autoload": { 1647 | "classmap": [ 1648 | "src/" 1649 | ] 1650 | }, 1651 | "notification-url": "https://packagist.org/downloads/", 1652 | "license": [ 1653 | "BSD-3-Clause" 1654 | ], 1655 | "authors": [ 1656 | { 1657 | "name": "Arne Blankerts", 1658 | "email": "arne@blankerts.de", 1659 | "role": "Developer" 1660 | } 1661 | ], 1662 | "description": "A small library for converting tokenized PHP source code into XML and potentially other formats", 1663 | "support": { 1664 | "issues": "https://github.com/theseer/tokenizer/issues", 1665 | "source": "https://github.com/theseer/tokenizer/tree/1.2.1" 1666 | }, 1667 | "funding": [ 1668 | { 1669 | "url": "https://github.com/theseer", 1670 | "type": "github" 1671 | } 1672 | ], 1673 | "time": "2021-07-28T10:34:58+00:00" 1674 | } 1675 | ], 1676 | "aliases": [], 1677 | "minimum-stability": "stable", 1678 | "stability-flags": [], 1679 | "prefer-stable": false, 1680 | "prefer-lowest": false, 1681 | "platform": { 1682 | "php": "^8.1", 1683 | "ext-mbstring": "*" 1684 | }, 1685 | "platform-dev": [], 1686 | "plugin-api-version": "2.3.0" 1687 | } 1688 | -------------------------------------------------------------------------------- /src/Alphabet/AlphabetInterface.php: -------------------------------------------------------------------------------- 1 | $alphabet 11 | */ 12 | public function __construct( 13 | private array $alphabet = [] 14 | ) { 15 | } 16 | 17 | public function add(string $char, int $label): self 18 | { 19 | $this->alphabet[$char] = $label; 20 | 21 | return $this; 22 | } 23 | 24 | public function all(): array 25 | { 26 | return $this->alphabet; 27 | } 28 | 29 | public function count(): int 30 | { 31 | return \count($this->alphabet); 32 | } 33 | 34 | public function get(string $char): ?int 35 | { 36 | return $this->alphabet[$char] ?? null; 37 | } 38 | 39 | public function has(string $char): bool 40 | { 41 | return isset($this->alphabet[$char]); 42 | } 43 | 44 | public function map(string $char, int $alphabetSize): int 45 | { 46 | if ($this->has($char)) { 47 | return $this->get($char); 48 | } 49 | 50 | $newLabel = $this->count() % $alphabetSize + 1; 51 | $this->add($char, $newLabel); 52 | 53 | return $newLabel; 54 | } 55 | } 56 | -------------------------------------------------------------------------------- /src/Alphabet/Utf8Alphabet.php: -------------------------------------------------------------------------------- 1 | > 9 | */ 10 | private array $cache = []; 11 | 12 | public function map(string $char, int $alphabetSize): int 13 | { 14 | if (!isset($this->cache[$alphabetSize][$char])) { 15 | // +1 in order to never assign 0 16 | $this->cache[$alphabetSize][$char] = (mb_ord($char, 'UTF-8') % $alphabetSize) + 1; 17 | } 18 | 19 | return $this->cache[$alphabetSize][$char]; 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /src/Config.php: -------------------------------------------------------------------------------- 1 | alphabetSize; 16 | } 17 | 18 | public function getIndexLength(): int 19 | { 20 | return $this->indexLength; 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /src/DamerauLevenshtein.php: -------------------------------------------------------------------------------- 1 | $commonSuffixLength) { 47 | $suffix = mb_substr($suffix, 1); 48 | } 49 | $string1 = substr($string1, 0, -\strlen($suffix) ?: null); 50 | $string2 = substr($string2, 0, -\strlen($suffix) ?: null); 51 | } 52 | 53 | $chars1 = mb_str_split($string1); 54 | $chars2 = mb_str_split($string2); 55 | $string1Length = \count($chars1); 56 | $string2Length = \count($chars2); 57 | 58 | if ($string1Length === 0) { 59 | return min($maxDistance, $string2Length * $insertionCost); 60 | } 61 | 62 | if ($string2Length === 0) { 63 | return min($maxDistance, $string1Length * $deletionCost); 64 | } 65 | 66 | // Distance can never be higher than deleting string1 and inserting string2 67 | $maxDistance = min($maxDistance, $string1Length * $deletionCost + $string2Length * $insertionCost); 68 | 69 | $requiredDeletions = max(0, $string1Length - $string2Length); 70 | $requiredInsertions = max(0, $string2Length - $string1Length); 71 | 72 | // Distance required to bring both strings to the same length 73 | $lengthDistance = $requiredInsertions * $insertionCost + $requiredDeletions * $deletionCost; 74 | 75 | // Length difference is too big 76 | if ($maxDistance <= $lengthDistance) { 77 | return $maxDistance; 78 | } 79 | 80 | // After length correction, how many deletion/insertion pairs are maximally possible 81 | $maxDeletionInsertionPairs = max(0, floor(($maxDistance - $lengthDistance) / ($deletionCost + $insertionCost))); 82 | 83 | $maxDeletions = $requiredDeletions + $maxDeletionInsertionPairs; 84 | $maxInsertions = $requiredInsertions + $maxDeletionInsertionPairs; 85 | $matrixSize = 1 + $maxDeletions + $maxInsertions; 86 | 87 | // We only store the latest two rows and flip the access between them. 88 | $matrix = [ 89 | array_fill(0, $matrixSize, $maxDistance), 90 | array_fill(0, $matrixSize, $maxDistance), 91 | ]; 92 | 93 | // Fill the row before the first one starting with 0 94 | for ($i = $maxDeletions; $i < $matrixSize; ++$i) { 95 | $matrix[0][$i] = ($i - $maxDeletions) * $insertionCost; 96 | } 97 | 98 | // Iterate through string1 (rows) 99 | for ($i = 0; $i < $string1Length; ++$i) { 100 | $currentRow = ($i + 1) % 2; 101 | $lastRow = $i % 2; 102 | 103 | // Iterate through string2 (columns) 104 | for ($j = 0; $j < $matrixSize; ++$j) { 105 | $col = $j - $maxDeletions + $i; 106 | 107 | // Fill the column before the first one starting with 0 108 | if ($col < 0) { 109 | $matrix[$currentRow][$j] = ($i - $col) * $deletionCost; 110 | continue; 111 | } 112 | 113 | if ($col >= $string2Length) { 114 | continue; 115 | } 116 | 117 | if ($i && ($chars1[$i] ?? '') === ($chars2[$col - 1] ?? '') && ($chars1[$i - 1] ?? '') === ($chars2[$col] ?? '')) { 118 | // In this case $matrix[$currentRow][$j] refers to the value 119 | // two rows above and two columns to the left in the matrix. 120 | $transpositioned = $matrix[$currentRow][$j] + $transpositionCost; 121 | } else { 122 | $transpositioned = $maxDistance; 123 | } 124 | 125 | $matrix[$currentRow][$j] = min( 126 | $transpositioned, 127 | ($matrix[$lastRow][$j + 1] ?? $maxDistance) + $deletionCost, 128 | ($matrix[$currentRow][$j - 1] ?? $maxDistance) + $insertionCost, 129 | ($matrix[$lastRow][$j] ?? $maxDistance) + ((($chars1[$i] ?? '') === ($chars2[$col] ?? '')) ? 0 : $replacementCost), 130 | ); 131 | } 132 | 133 | if (min($matrix[$currentRow]) >= $maxDistance && min($matrix[$lastRow]) + $transpositionCost >= $maxDistance) { 134 | return $maxDistance; 135 | } 136 | } 137 | 138 | // Return the distance value found in the last row in the last column 139 | return min($maxDistance, $matrix[$currentRow ?? 0][$maxDeletions - ($string1Length - $string2Length)]); 140 | } 141 | } 142 | -------------------------------------------------------------------------------- /src/DataStore/DataStoreInterface.php: -------------------------------------------------------------------------------- 1 | > 14 | */ 15 | public function getForStates(array $states = []): array; 16 | 17 | public function remove(int $state, string $string): void; 18 | } 19 | -------------------------------------------------------------------------------- /src/DataStore/InMemoryDataStore.php: -------------------------------------------------------------------------------- 1 | > 9 | */ 10 | private array $data = []; 11 | 12 | public function add(int $state, string $string): void 13 | { 14 | $this->data[$state][] = $string; 15 | } 16 | 17 | public function all(): array 18 | { 19 | return $this->data; 20 | } 21 | 22 | public function getForStates(array $states = []): array 23 | { 24 | return array_intersect_key($this->data, array_flip($states)); 25 | } 26 | 27 | public function remove(int $state, string $string): void 28 | { 29 | $updated = array_values(array_diff($this->data[$state] ?? [], [$string])); 30 | 31 | if ($updated) { 32 | $this->data[$state] = $updated; 33 | } else { 34 | unset($this->data[$state]); 35 | } 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /src/DataStore/NullDataStore.php: -------------------------------------------------------------------------------- 1 | = 128) { 28 | throw new \InvalidArgumentException('Strings with more than 128 individual unicode characters are not supported.'); 29 | } 30 | $map[$mbc] = \chr(128 + \count($map)); 31 | } 32 | } 33 | 34 | // finally remap non-ascii characters 35 | return strtr($str, $map); 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /src/StateSet/CostAnnotatedStateSet.php: -------------------------------------------------------------------------------- 1 | 11 | */ 12 | private array $set = []; 13 | 14 | /** 15 | * Adds a state with a cost to this set. 16 | * If this sets already contains the given state with a higher cost, replaces it. 17 | */ 18 | public function add(int $state, int $cost): void 19 | { 20 | if (!isset($this->set[$state])) { 21 | $this->set[$state] = $cost; 22 | return; 23 | } 24 | 25 | // Lowest cost always wins 26 | if ($cost < $this->set[$state]) { 27 | $this->set[$state] = $cost; 28 | } 29 | } 30 | 31 | /** 32 | * Key: State 33 | * Value: Cost 34 | * @return array 35 | */ 36 | public function all(): array 37 | { 38 | return $this->set; 39 | } 40 | 41 | public function mergeWith(CostAnnotatedStateSet $stateSet): self 42 | { 43 | $clone = clone $this; 44 | 45 | foreach ($stateSet->all() as $state => $cost) { 46 | $clone->add($state, $cost); 47 | } 48 | 49 | return $clone; 50 | } 51 | 52 | public function states(): array 53 | { 54 | $states = array_values(array_keys($this->set)); 55 | sort($states); 56 | return $states; 57 | } 58 | } 59 | -------------------------------------------------------------------------------- /src/StateSet/InMemoryStateSet.php: -------------------------------------------------------------------------------- 1 | 9 | */ 10 | public function __construct( 11 | private array $states = [] 12 | ) { 13 | } 14 | 15 | public function add(int $state): void 16 | { 17 | $this->states[$state] = true; 18 | } 19 | 20 | public function all(): array 21 | { 22 | return array_keys($this->states); 23 | } 24 | 25 | public function has(int $state): bool 26 | { 27 | return isset($this->states[$state]); 28 | } 29 | 30 | public function remove(int $state): void 31 | { 32 | unset($this->states[$state]); 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /src/StateSet/StateSetInterface.php: -------------------------------------------------------------------------------- 1 | 11 | */ 12 | public function all(): array; 13 | 14 | public function has(int $state): bool; 15 | 16 | public function remove(int $state): void; 17 | } 18 | -------------------------------------------------------------------------------- /src/StateSetIndex.php: -------------------------------------------------------------------------------- 1 | 14 | */ 15 | private array $indexCache = []; 16 | 17 | /** 18 | * @var array 19 | */ 20 | private array $matchingStatesCache = []; 21 | 22 | public function __construct( 23 | private Config $config, 24 | private AlphabetInterface $alphabet, 25 | public StateSetInterface $stateSet, 26 | private DataStoreInterface $dataStore, 27 | ) { 28 | } 29 | 30 | /** 31 | * Returns the matching strings. 32 | * 33 | * @return array 34 | */ 35 | public function find(string $string, int $editDistance, int $transpositionCost = 1): array 36 | { 37 | $acceptedStringsPerState = $this->findAcceptedStrings($string, $editDistance, $transpositionCost); 38 | $stringLength = mb_strlen($string); 39 | $filtered = []; 40 | 41 | foreach ($acceptedStringsPerState as $acceptedStrings) { 42 | foreach ($acceptedStrings as $acceptedString) { 43 | // Early aborts (cheaper) for cases we know are absolutely never going to match 44 | if (abs($stringLength - mb_strlen($acceptedString)) > $editDistance) { 45 | continue; 46 | } 47 | 48 | if (DamerauLevenshtein::distance($string, $acceptedString, $editDistance + 1, 1, 1, 1, $transpositionCost) <= $editDistance) { 49 | $filtered[] = $acceptedString; 50 | } 51 | } 52 | } 53 | 54 | return array_unique($filtered); 55 | } 56 | 57 | /** 58 | * Returns the matching strings per state. Key is the state and the value is an array of matching strings 59 | * for that state. 60 | * 61 | * @return array> 62 | */ 63 | public function findAcceptedStrings(string $string, int $editDistance, int $transpositionCost): array 64 | { 65 | return $this->dataStore->getForStates($this->findMatchingStates($string, $editDistance, $transpositionCost)); 66 | } 67 | 68 | /** 69 | * Returns the matching states. 70 | * 71 | * @return array 72 | */ 73 | public function findMatchingStates(string $string, int $editDistance, int $transpositionCost): array 74 | { 75 | $cacheKey = $string . ';' . $editDistance . ';' . $transpositionCost; 76 | 77 | // Seen this already, skip 78 | if (isset($this->matchingStatesCache[$cacheKey])) { 79 | return $this->matchingStatesCache[$cacheKey]; 80 | } 81 | 82 | // Initial states 83 | $states = $this->getReachableStates(0, $editDistance); 84 | $lastSubstitutions = []; 85 | $lastMappedChar = null; 86 | 87 | $this->loopOverEveryCharacter($string, function (int $mappedChar) use (&$states, &$lastSubstitutions, &$lastMappedChar, $editDistance, $transpositionCost) { 88 | $statesStar = new CostAnnotatedStateSet(); // This is S∗ in the paper 89 | $substitutionStates = []; 90 | 91 | foreach ($states->all() as $state => $cost) { 92 | $statesStarC = new CostAnnotatedStateSet(); // This is S∗c in the paper 93 | 94 | // Deletion 95 | if ($cost + 1 <= $editDistance) { 96 | $statesStarC->add($state, $cost + 1); 97 | } 98 | 99 | // Match & Substitution 100 | for ($i = 1; $i <= $this->config->getAlphabetSize(); $i++) { 101 | $newState = (int) ($state * $this->config->getAlphabetSize() + $i); 102 | 103 | if ($this->stateSet->has($newState)) { 104 | if ($i === $mappedChar) { 105 | // Match 106 | $statesStarC->add($newState, $cost); 107 | } elseif ($cost + 1 <= $editDistance) { 108 | // Substitution 109 | $statesStarC->add($newState, $cost + 1); 110 | $substitutionStates[$i] ??= new CostAnnotatedStateSet(); 111 | $substitutionStates[$i]->add($newState, $cost + 1); 112 | } 113 | } 114 | } 115 | 116 | // Insertion 117 | foreach ($statesStarC->all() as $newState => $newCost) { 118 | $statesStar = $statesStar->mergeWith($this->getReachableStates( 119 | $newState, 120 | $editDistance, 121 | $newCost 122 | )); 123 | } 124 | } 125 | 126 | // Transposition 127 | // Takes all substitution states from the previous step that matched 128 | // the current char and adds a followup substitution state using the 129 | // previous char and assigns a combined cost of $transpositionCost. 130 | foreach (($lastSubstitutions[$mappedChar] ?? null)?->all() ?? [] as $state => $cost) { 131 | $newState = (int) ($state * $this->config->getAlphabetSize() + $lastMappedChar); 132 | $statesStar = $statesStar->mergeWith($this->getReachableStates( 133 | $newState, 134 | $editDistance, 135 | $cost - 1 + $transpositionCost, 136 | )); 137 | } 138 | 139 | $states = $statesStar; 140 | $lastMappedChar = $mappedChar; 141 | $lastSubstitutions = $substitutionStates; 142 | }); 143 | 144 | return $this->matchingStatesCache[$cacheKey] = $states->states(); 145 | } 146 | 147 | public function getAlphabet(): AlphabetInterface 148 | { 149 | return $this->alphabet; 150 | } 151 | 152 | public function getConfig(): Config 153 | { 154 | return $this->config; 155 | } 156 | 157 | public function getStateSet(): StateSetInterface 158 | { 159 | return $this->stateSet; 160 | } 161 | 162 | /** 163 | * Indexes an array of strings and returns an array where all strings have their state assigned. 164 | * 165 | * @return array 166 | */ 167 | public function index(array $strings): array 168 | { 169 | $assigned = []; 170 | 171 | foreach ($strings as $string) { 172 | // Seen this already, skip 173 | if (isset($this->indexCache[$string])) { 174 | $assigned[$string] = $this->indexCache[$string]; 175 | continue; 176 | } 177 | 178 | $state = 0; 179 | $this->loopOverEveryCharacter($string, function (int $mappedChar) use (&$state) { 180 | $newState = (int) ($state * $this->config->getAlphabetSize() + $mappedChar); 181 | 182 | $this->stateSet->add($newState); 183 | $state = $newState; 184 | }); 185 | 186 | $assigned[$string] = $this->indexCache[$string] = $state; 187 | $this->dataStore->add($state, $string); 188 | } 189 | 190 | return $assigned; 191 | } 192 | 193 | /** 194 | * Removes an array of strings from the index. 195 | */ 196 | public function removeFromIndex(array $strings): void 197 | { 198 | foreach ($strings as $string) { 199 | unset($this->indexCache[$string]); 200 | 201 | $states = []; 202 | $state = 0; 203 | $this->loopOverEveryCharacter($string, function (int $mappedChar) use (&$state, &$states) { 204 | $states[] = $state = (int) ($state * $this->config->getAlphabetSize() + $mappedChar); 205 | }); 206 | 207 | $this->dataStore->remove($state, $string); 208 | 209 | foreach (array_reverse($states) as $state) { 210 | // If a state is shared with another string or a state exists that follows the current one we must stop 211 | // the removal process as all previous states and the current one must be kept. 212 | if (isset($this->dataStore->getForStates([$state])[$state]) || $this->hasNextState($state)) { 213 | continue 2; 214 | } 215 | 216 | $this->stateSet->remove($state); 217 | } 218 | } 219 | } 220 | 221 | private function getReachableStates(int $startState, int $editDistance, int $currentDistance = 0): CostAnnotatedStateSet 222 | { 223 | $reachable = new CostAnnotatedStateSet(); 224 | 225 | if ($currentDistance > $editDistance) { 226 | return $reachable; 227 | } 228 | 229 | // A state is always able to reach itself 230 | $reachable->add($startState, $currentDistance); 231 | 232 | if ($currentDistance >= $editDistance) { 233 | return $reachable; 234 | } 235 | 236 | for ($c = 1; $c <= $this->config->getAlphabetSize(); $c++) { 237 | $state = $startState * $this->config->getAlphabetSize() + $c; 238 | if ($this->stateSet->has($state)) { 239 | $reachable = $reachable->mergeWith($this->getReachableStates($state, $editDistance, $currentDistance + 1)); 240 | } 241 | } 242 | 243 | return $reachable; 244 | } 245 | 246 | /** 247 | * Returns true if a state exists that follows the given state 248 | */ 249 | private function hasNextState(int $state): bool 250 | { 251 | for ($c = 1; $c <= $this->config->getAlphabetSize(); ++$c) { 252 | if ($this->stateSet->has($state * $this->config->getAlphabetSize() + $c)) { 253 | return true; 254 | } 255 | } 256 | 257 | return false; 258 | } 259 | 260 | /** 261 | * @param \Closure(int) $closure 262 | */ 263 | private function loopOverEveryCharacter(string $string, \Closure $closure): void 264 | { 265 | $indexedSubstringLength = min($this->config->getIndexLength(), mb_strlen($string)); 266 | $indexedSubstring = mb_substr($string, 0, $indexedSubstringLength); 267 | 268 | foreach (mb_str_split($indexedSubstring) as $char) { 269 | $mappedChar = $this->alphabet->map($char, $this->config->getAlphabetSize()); 270 | $closure($mappedChar); 271 | } 272 | } 273 | } 274 | --------------------------------------------------------------------------------