From ae667f198b2a62cbfb9e8b6d7e9b7ac8079b3fa0 Mon Sep 17 00:00:00 2001 From: Rick Kukiela Date: Thu, 2 Aug 2018 01:02:39 -0500 Subject: [PATCH 01/14] Update README.MD --- README.MD | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/README.MD b/README.MD index 0d5f6d5..5209e30 100644 --- a/README.MD +++ b/README.MD @@ -1,3 +1,5 @@ +### WARNING - THIS IS A HACKY UPDATE WITH DICTIONARY SUPPORT AND MODIFICATIONS TO THE COMMAND LINE - USE AT YUOR OWN RISK! + ##Hunspell PHP wrapper This is a hunspell php wrapper. @@ -7,4 +9,4 @@ Example $hunspell = new \HunspellPHP\Hunspell(); var_dump($hunspell->find('otwórz')); -``` \ No newline at end of file +``` From f6e2aa6a23e7462c4c2647e853e5b8dad099ac65 Mon Sep 17 00:00:00 2001 From: Rick Date: Thu, 2 Aug 2018 22:04:57 -0500 Subject: [PATCH 02/14] See readme for changes list. Changed shell command to work with windows and standard bash shell. Changed a split character in parsing method. --- README.MD | 9 +++++++-- src/HunspellPHP/Hunspell.php | 25 ++++++++++++++++++++----- 2 files changed, 27 insertions(+), 7 deletions(-) diff --git a/README.MD b/README.MD index 5209e30..dab5c13 100644 --- a/README.MD +++ b/README.MD @@ -1,8 +1,13 @@ -### WARNING - THIS IS A HACKY UPDATE WITH DICTIONARY SUPPORT AND MODIFICATIONS TO THE COMMAND LINE - USE AT YUOR OWN RISK! - ##Hunspell PHP wrapper This is a hunspell php wrapper. +=================== +### This fork changes the shell commands with windows and bash style shells. This will no longer work on the system the original author wrote it on. I'm not sure what type of shell they were using but the manner in which they were setting the environment variable does not work on windows or centos/bash. This fork addresses that problem by using the windows "set" command via powershell which supports piping. The command for non windows machines was changed to use "export" for setting the environment. + +An additional change was made to the parsing of the return value as the `PHP_EOL` value used in the original source was not working on my machine. This was changed to "\n" and it works great. +=================== + + Example =================== ```php diff --git a/src/HunspellPHP/Hunspell.php b/src/HunspellPHP/Hunspell.php index 3352c99..9fa7e10 100644 --- a/src/HunspellPHP/Hunspell.php +++ b/src/HunspellPHP/Hunspell.php @@ -125,7 +125,13 @@ protected function clear($input) */ protected function findCommand($input) { - return shell_exec(sprintf("LANG=%s; echo '%s' | hunspell -d %s", $this->encoding, $input, $this->language)); + + if (strtoupper(substr(PHP_OS, 0, 3)) === 'WIN') { + return shell_exec(sprintf("powershell \"set LANG='%s'; echo '%s' | hunspell -d %s\"", $this->encoding, $input, $this->language)); + } else { + return shell_exec(sprintf("export LANG='%s'; echo '%s' | hunspell -d %s", $this->encoding, $input, $this->language)); + } + } /** @@ -134,7 +140,13 @@ protected function findCommand($input) */ protected function stemCommand($input) { - return shell_exec(sprintf("LANG=%s; echo '%s' | hunspell -d %s -s", $this->encoding, $input, $this->language)); + + if (strtoupper(substr(PHP_OS, 0, 3)) === 'WIN') { + return shell_exec(sprintf("powershell \"set LANG='%s'; echo '%s' | hunspell -d %s -s\"", $this->encoding, $input, $this->language)); + } else { + return shell_exec(sprintf("export LANG='%s'; echo '%s' | hunspell -d %s -s", $this->encoding, $input, $this->language)); + } + } /** @@ -144,10 +156,13 @@ protected function stemCommand($input) */ protected function preParse($input, $words) { - $result = explode(PHP_EOL, trim($input)); - unset($result[0]); - $words = array_map('trim', explode(" ", $words)); + $result = explode("\n", trim($input)); + array_shift($result); + $words = array_map('trim', preg_split('/\W/', $words)); + if(sizeof($result) != sizeof($words)) { + return []; + } return array_combine($words, $result); } From 97b4d1d0851efe3c8f68cc7b21a0f40a6e35a8f1 Mon Sep 17 00:00:00 2001 From: Rick Date: Thu, 2 Aug 2018 22:07:27 -0500 Subject: [PATCH 03/14] fixed readme --- README.MD | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/README.MD b/README.MD index dab5c13..46be110 100644 --- a/README.MD +++ b/README.MD @@ -1,11 +1,9 @@ ##Hunspell PHP wrapper This is a hunspell php wrapper. -=================== -### This fork changes the shell commands with windows and bash style shells. This will no longer work on the system the original author wrote it on. I'm not sure what type of shell they were using but the manner in which they were setting the environment variable does not work on windows or centos/bash. This fork addresses that problem by using the windows "set" command via powershell which supports piping. The command for non windows machines was changed to use "export" for setting the environment. +This fork changes the shell commands with windows and bash style shells. This will no longer work on the system the original author wrote it on. I'm not sure what type of shell they were using but the manner in which they were setting the environment variable does not work on windows or centos/bash. This fork addresses that problem by using the windows "set" command via powershell which supports piping. The command for non windows machines was changed to use "export" for setting the environment. An additional change was made to the parsing of the return value as the `PHP_EOL` value used in the original source was not working on my machine. This was changed to "\n" and it works great. -=================== Example From 6a64128be98cb7f61e3cd9f4dbaecce70ec09998 Mon Sep 17 00:00:00 2001 From: Rick Date: Thu, 2 Aug 2018 23:44:08 -0500 Subject: [PATCH 04/14] changed package name in composer.json --- composer.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/composer.json b/composer.json index 3f2c637..60298ac 100644 --- a/composer.json +++ b/composer.json @@ -1,5 +1,5 @@ { - "name": "hunspell-php/hunspell-php", + "name": "belniakmedia/hunspell-php", "description": "Hunspell PHP wrapper", "minimum-stability": "dev", "license": "MIT", From b77685be69178f5017539b011b197de3a9e564f9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Janusz=20=C5=BBukowicz?= Date: Wed, 8 Aug 2018 14:27:08 +0200 Subject: [PATCH 05/14] Update composer.json --- composer.json | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/composer.json b/composer.json index 60298ac..07dd31f 100644 --- a/composer.json +++ b/composer.json @@ -1,5 +1,5 @@ { - "name": "belniakmedia/hunspell-php", + "name": "hunspell-php/hunspell-php", "description": "Hunspell PHP wrapper", "minimum-stability": "dev", "license": "MIT", @@ -17,4 +17,4 @@ "HunspellPHP\\": "src/HunspellPHP" } } -} \ No newline at end of file +} From fbbc7ff64e885305aea50f313654c4877cda9812 Mon Sep 17 00:00:00 2001 From: Rick Kukiela Date: Wed, 8 Aug 2018 11:20:00 -0500 Subject: [PATCH 06/14] Please do not edit this fork. Merge it and then edit your version. --- composer.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/composer.json b/composer.json index 07dd31f..478aabc 100644 --- a/composer.json +++ b/composer.json @@ -1,5 +1,5 @@ { - "name": "hunspell-php/hunspell-php", + "name": "belniakmedia/hunspell-php", "description": "Hunspell PHP wrapper", "minimum-stability": "dev", "license": "MIT", From ab78e1d7c686da1c09ba03576bdd7531e79bfa69 Mon Sep 17 00:00:00 2001 From: Rick Date: Mon, 30 Jan 2023 10:27:16 -0600 Subject: [PATCH 07/14] Fixed a few php8 type issues where stemcommand and findcommand could return null which would cuase warnings when used. --- composer.json | 1 + src/HunspellPHP/Hunspell.php | 19 ++++++++----------- 2 files changed, 9 insertions(+), 11 deletions(-) diff --git a/composer.json b/composer.json index 60298ac..34e788c 100644 --- a/composer.json +++ b/composer.json @@ -2,6 +2,7 @@ "name": "belniakmedia/hunspell-php", "description": "Hunspell PHP wrapper", "minimum-stability": "dev", + "version": "1.2", "license": "MIT", "authors": [ { diff --git a/src/HunspellPHP/Hunspell.php b/src/HunspellPHP/Hunspell.php index 9fa7e10..d9e49b0 100644 --- a/src/HunspellPHP/Hunspell.php +++ b/src/HunspellPHP/Hunspell.php @@ -1,4 +1,5 @@ -preParse($this->findCommand($words), $words); + $results = $this->preParse((string)$this->findCommand($words), $words); $response = []; foreach ($results as $word => $result) { $matches = []; - $match = preg_match($this->matcher, $result, $matches); - + preg_match($this->matcher, $result, $matches); $matches['input'] = $word; $response[] = $this->parse($matches); } @@ -99,15 +98,13 @@ public function find($words) * @param string $word word to find * @return HunspellStemResponse * @throws InvalidMatchTypeException - * @throws InvalidResultException * @throws WordNotFoundException */ public function stem($word) { - $result = explode(PHP_EOL, $this->stemCommand($word)); + $result = explode(PHP_EOL, (string)$this->stemCommand($word)); $result['input'] = $word; - $result = $this->stemParse($result); - return $result; + return $this->stemParse($result); } /** @@ -120,7 +117,7 @@ protected function clear($input) } /** - * @return string + * @return null|string * @param string $input */ protected function findCommand($input) @@ -135,7 +132,7 @@ protected function findCommand($input) } /** - * @return string + * @return null|string * @param string $input */ protected function stemCommand($input) From ac58979a986cec3e9cc09d4246a02fc6e5005150 Mon Sep 17 00:00:00 2001 From: Rick Date: Wed, 15 Feb 2023 18:19:56 -0600 Subject: [PATCH 08/14] Version 2.0.0 Added - Added PHP8.0 typed class, - Added constructor to main `HunspellPHP` class where the `$dictionary`, `$encoding` and `$dictionary_path` cal be set/overridden during initialization. - Added `$dictionary_path` as a new argument were the dictionary files path may be specified (system default search locations are used otherwise). Additional `get()` and `set()`methods added. - Added functionality to `findCommand` method via new `(bool)$stem_mode` argument. Removed - Removed `findStemCommand` method. - Removed unused exception classes. - Removed `HunspellPHP\Exceptions` namespace. - Removed composer.lock from repo. Fixed - Renamed `$language` more appropriately `$dictionary` since that is what that property is referencing. - Moved HunspellMatchTypeException up one directory to \HunspellPHP namespace. - Fixed an issue where not all `$match` values were returned from the command response resulting in PHP warnings. - Fixed a missing type `-` extraction from the matcher regex which resulted in PHP warnings and bad responses. --- .gitignore | 1 + CHANGELOG.md | 17 +++ README.MD | 15 -- README.md | 19 +++ composer.json | 8 +- composer.lock | 20 --- .../Exception/InvalidResultException.php | 8 -- .../Exception/WordNotFoundException.php | 8 -- src/HunspellPHP/Hunspell.php | 134 +++++++++--------- src/HunspellPHP/HunspellResponse.php | 33 +---- src/HunspellPHP/HunspellStemResponse.php | 14 +- .../InvalidMatchTypeException.php | 2 +- 12 files changed, 121 insertions(+), 158 deletions(-) create mode 100644 CHANGELOG.md delete mode 100644 README.MD create mode 100644 README.md delete mode 100644 composer.lock delete mode 100644 src/HunspellPHP/Exception/InvalidResultException.php delete mode 100644 src/HunspellPHP/Exception/WordNotFoundException.php rename src/HunspellPHP/{Exception => }/InvalidMatchTypeException.php (65%) diff --git a/.gitignore b/.gitignore index 331c58f..688d850 100644 --- a/.gitignore +++ b/.gitignore @@ -1,2 +1,3 @@ .idea +composer.lock vendor \ No newline at end of file diff --git a/CHANGELOG.md b/CHANGELOG.md new file mode 100644 index 0000000..d82323c --- /dev/null +++ b/CHANGELOG.md @@ -0,0 +1,17 @@ +## Changelog +### Version 2.0.0 +#### Added +- Added PHP8.0 typed class, +- Added constructor to main `HunspellPHP` class where the `$dictionary`, `$encoding` and `$dictionary_path` cal be set/overridden during initialization. +- Added `$dictionary_path` as a new argument were the dictionary files path may be specified (system default search locations are used otherwise). Additional `get()` and `set()`methods added. +- Added functionality to `findCommand` method via new `(bool)$stem_mode` argument. +#### Removed +- Removed `findStemCommand` method. +- Removed unused exception classes. +- Removed `HunspellPHP\Exceptions` namespace. +- Removed composer.lock from repo. +#### Fixed +- Renamed `$language` more appropriately `$dictionary` since that is what that property is referencing. +- Moved HunspellMatchTypeException up one directory to \HunspellPHP namespace. +- Fixed an issue where not all `$match` values were returned from the command response resulting in PHP warnings. +- Fixed a missing type `-` extraction from the matcher regex which resulted in PHP warnings and bad responses. \ No newline at end of file diff --git a/README.MD b/README.MD deleted file mode 100644 index 46be110..0000000 --- a/README.MD +++ /dev/null @@ -1,15 +0,0 @@ -##Hunspell PHP wrapper -This is a hunspell php wrapper. - -This fork changes the shell commands with windows and bash style shells. This will no longer work on the system the original author wrote it on. I'm not sure what type of shell they were using but the manner in which they were setting the environment variable does not work on windows or centos/bash. This fork addresses that problem by using the windows "set" command via powershell which supports piping. The command for non windows machines was changed to use "export" for setting the environment. - -An additional change was made to the parsing of the return value as the `PHP_EOL` value used in the original source was not working on my machine. This was changed to "\n" and it works great. - - -Example -=================== -```php -$hunspell = new \HunspellPHP\Hunspell(); - -var_dump($hunspell->find('otwórz')); -``` diff --git a/README.md b/README.md new file mode 100644 index 0000000..7f4322f --- /dev/null +++ b/README.md @@ -0,0 +1,19 @@ +# Hunspell PHP wrapper +Forked from [johnzuk/HunspellPHP](https://github.com/johnzuk/HunspellPHP) + +### Version 2.0.0 +Version 2.0.0 and above requires PHP ^8.0.0 and includes an important fix to the result matcher regex. If you need this for an older version of PHP I recommend that you fork 1.2 and update the regex matcher property of the Hunspell class to what is set in the current version of the code. + +[View Changelog](CHANGELOG.md) + +### The reason for this fork +This project was initially forked because the shell commands used were for a non-bash shell. This fork's main purpose was to convert the shell commands to a BASH compatible syntax and add support for Windows powershell. As such this fork will not work correctly outside of a bash or powershell environment. + +An additional change was made to the parsing of the return value as the `PHP_EOL` value used in the original source was not working in my testing. This was changed to "\n" which resolved the issue. + +Example +=================== +```php +$hunspell = new \HunspellPHP\Hunspell(); +var_dump($hunspell->find('otwórz')); +``` diff --git a/composer.json b/composer.json index a4eb186..76ee190 100644 --- a/composer.json +++ b/composer.json @@ -2,16 +2,16 @@ "name": "belniakmedia/hunspell-php", "description": "Hunspell PHP wrapper", "minimum-stability": "dev", - "version": "1.2", + "version": "2.0.0", "license": "MIT", "authors": [ { - "name": "Janusz Żukowicz", - "email": "john_zuk@wp.pl" + "name": "Richard Kukiela", + "email": "rick@belniakmedia.com" } ], "require": { - "php" : ">=5.6" + "php" : ">=8.0" }, "autoload": { "psr-4": { diff --git a/composer.lock b/composer.lock deleted file mode 100644 index 3397b9a..0000000 --- a/composer.lock +++ /dev/null @@ -1,20 +0,0 @@ -{ - "_readme": [ - "This file locks the dependencies of your project to a known state", - "Read more about it at https://getcomposer.org/doc/01-basic-usage.md#composer-lock-the-lock-file", - "This file is @generated automatically" - ], - "hash": "ee1117e1d6b15d290db1f1130656045a", - "content-hash": "d60c6568f84d2836865f7849ece2b5b1", - "packages": [], - "packages-dev": [], - "aliases": [], - "minimum-stability": "dev", - "stability-flags": [], - "prefer-stable": false, - "prefer-lowest": false, - "platform": { - "php": ">=5.6" - }, - "platform-dev": [] -} diff --git a/src/HunspellPHP/Exception/InvalidResultException.php b/src/HunspellPHP/Exception/InvalidResultException.php deleted file mode 100644 index 6ad7dfb..0000000 --- a/src/HunspellPHP/Exception/InvalidResultException.php +++ /dev/null @@ -1,8 +0,0 @@ - 'OK', Hunspell::ROOT => 'ROOT', Hunspell::MISS => 'MISS', Hunspell::NONE => 'NONE', - Hunspell::COMPOUND => 'COMPOUND', + Hunspell::COMPOUND => 'COMPOUND' ]; + protected string $encoding; + protected string $dictionary; + protected string $dictionary_path; + protected string $matcher = + '/(?P\*|\+|&|#|-)\s?(?P\w+)?\s?(?P\d+)?\s?(?P\d+)?:?\s?(?P.*+)?/u'; + /** - * @var string + * @param string $dictionary Dictionary name e.g.: 'en_US' (default) + * @param string $encoding Encoding e.g.: 'utf-8' (default) + * @param ?string $dictionary_path Specify the directory of the dictionary file (optional) */ - protected $language = "pl_PL"; + public function __construct( + string $dictionary = 'en_US', + string $encoding = 'en_US.utf-8', + ?string $dictionary_path = null + ) { + $this->dictionary = $this->clear($dictionary); + $this->encoding = $this->clear($encoding); + $this->dictionary_path = $dictionary_path; + } + /** - * @var string + * @return string */ - protected $encoding = "pl_PL.utf-8"; + public function getEncoding(): string + { + return $this->encoding; + } /** - * @var string + * @return string */ - protected $matcher = - "/(?P\*|\+|&|#)\s?(?P\w+)?\s?(?P\d+)?\s?(?P\d+)?:?\s?(?P.*+)?/u"; + public function getDictionary(): string + { + return $this->dictionary; + } /** * @return string */ - public function getLanguage() + public function getDictionaryPath(): string { - return $this->language; + return $this->dictionary_path; } /** - * @param string $language + * @param string $dictionary Language code e.g.: 'en_US' */ - public function setLanguage($language) + public function setDictionary(string $dictionary): void { - $this->language = $this->clear($language); + $this->dictionary = $this->clear($dictionary); } /** - * @return string + * @param string $dictionary_path The path to load the dictionary files from */ - public function getEncoding() + public function setDictionaryPath(string $dictionary_path): void { - return $this->encoding; + $this->dictionary_path = $dictionary_path; } + /** - * @param string $encoding + * @param string $encoding Encoding value (includes language code) e.g.: 'en_US.utf-8' */ - public function setEncoding($encoding) + public function setEncoding(string $encoding): void { $this->encoding = $this->clear($encoding); } /** - * @param $words + * @param string $words * @return array * @throws InvalidMatchTypeException */ - public function find($words) + public function find(string $words): array { - $results = $this->preParse((string)$this->findCommand($words), $words); + $results = $this->preParse($this->findCommand($words), $words); $response = []; foreach ($results as $word => $result) { $matches = ['type' => null]; preg_match($this->matcher, $result, $matches); $matches['input'] = $word; + $matches['type'] = $matches['type'] ?? null; + $matches['original'] = $matches['original'] ?? ''; + $matches['misses'] = $matches['misses'] ?? []; + $matches['offset'] = $matches['offset'] ?? null; + $matches['count'] = $matches['count'] ?? null; $response[] = $this->parse($matches); } - return $response; } /** * @param string $word word to find * @return HunspellStemResponse - * @throws InvalidMatchTypeException - * @throws WordNotFoundException */ - public function stem($word) + public function stem(string $word): HunspellStemResponse { - $result = explode(PHP_EOL, (string)$this->stemCommand($word)); + $result = explode(PHP_EOL, $this->findCommand($word, true)); $result['input'] = $word; return $this->stemParse($result); } /** * @param string $input - * @return mixed - */ - protected function clear($input) - { - return preg_replace('[^a-zA-Z0-9_\-.]', '', $input); - } - - /** - * @return null|string - * @param string $input + * @return string */ - protected function findCommand($input) + protected function clear(string $input): string { - - if (strtoupper(substr(PHP_OS, 0, 3)) === 'WIN') { - return shell_exec(sprintf("powershell \"set LANG='%s'; echo '%s' | hunspell -d %s\"", $this->encoding, $input, $this->language)); - } else { - return shell_exec(sprintf("export LANG='%s'; echo '%s' | hunspell -d %s", $this->encoding, $input, $this->language)); - } - + return (string)preg_replace('[^a-zA-Z0-9_-\.]', '', $input); } /** - * @return null|string * @param string $input + * @param bool $stem_mode + * @return string */ - protected function stemCommand($input) + protected function findCommand(string $input, bool $stem_mode = false): string { - + $stem_switch = $stem_mode ? ' -s' : ''; + $dictionary = $this->dictionary_path + ? rtrim($this->dictionary_path, DIRECTORY_SEPARATOR) . DIRECTORY_SEPARATOR . $this->dictionary + : $this->dictionary; if (strtoupper(substr(PHP_OS, 0, 3)) === 'WIN') { - return shell_exec(sprintf("powershell \"set LANG='%s'; echo '%s' | hunspell -d %s -s\"", $this->encoding, $input, $this->language)); + return (string)shell_exec(sprintf("powershell \"set LANG='%s'; echo '%s' | hunspell -d %s%s\"", $this->encoding, $input, $dictionary, $stem_switch)); } else { - return shell_exec(sprintf("export LANG='%s'; echo '%s' | hunspell -d %s -s", $this->encoding, $input, $this->language)); + return (string)shell_exec(sprintf("export LANG='%s'; echo '%s' | hunspell -d %s%s", $this->encoding, $input, $dictionary, $stem_switch)); } - } /** @@ -151,7 +155,7 @@ protected function stemCommand($input) * @param string $words * @return array */ - protected function preParse($input, $words) + protected function preParse(string $input, string $words): array { $result = explode("\n", trim($input)); array_shift($result); @@ -168,7 +172,7 @@ protected function preParse($input, $words) * @return HunspellResponse * @throws InvalidMatchTypeException */ - protected function parse(array $matches) + protected function parse(array $matches): HunspellResponse { if ($matches['type'] == Hunspell::OK || $matches['type'] == Hunspell::COMPOUND) { return new HunspellResponse( @@ -205,10 +209,8 @@ protected function parse(array $matches) /** * @param array $matches * @return HunspellStemResponse - * @throws InvalidMatchTypeException - * @throws WordNotFoundException */ - protected function stemParse(array $matches) + protected function stemParse(array $matches): HunspellStemResponse { $input = $matches['input']; unset($matches['input']); diff --git a/src/HunspellPHP/HunspellResponse.php b/src/HunspellPHP/HunspellResponse.php index 158df10..8b5a092 100644 --- a/src/HunspellPHP/HunspellResponse.php +++ b/src/HunspellPHP/HunspellResponse.php @@ -3,40 +3,21 @@ class HunspellResponse { - /** - * @var string - */ - public $root; - - /** - * @var string - */ - public $original; - - /** - * @var int - */ - public $offset; - - /** - * @var array - */ - public $misses = []; - - /** - * @var string - */ - public $type; + public string $root; + public string $original; + public ?int $offset; + public array $misses = []; + public string $type; /** * HunspellResponse constructor. * @param string $root * @param string $original - * @param int $offset + * @param ?int $offset * @param array $misses * @param string $type */ - public function __construct($root, $original, $type = '', $offset = null, array $misses = []) + public function __construct(string $root, string $original, string $type = '', ?int $offset = null, array $misses = []) { $this->root = $root; $this->original = $original; diff --git a/src/HunspellPHP/HunspellStemResponse.php b/src/HunspellPHP/HunspellStemResponse.php index 0c7a628..5dc28c0 100644 --- a/src/HunspellPHP/HunspellStemResponse.php +++ b/src/HunspellPHP/HunspellStemResponse.php @@ -3,22 +3,16 @@ class HunspellStemResponse { - /** - * @var string - */ - public $original; - - /** - * @var string[] - */ - public $stems; + public string $original; + /** @var string[] */ + public array $stems; /** * HunspellStemResponse constructor. * @param string $original * @param string[] $stems */ - public function __construct($original, $stems = []) + public function __construct(string $original, array $stems = []) { $this->original = $original; $this->stems = $stems; diff --git a/src/HunspellPHP/Exception/InvalidMatchTypeException.php b/src/HunspellPHP/InvalidMatchTypeException.php similarity index 65% rename from src/HunspellPHP/Exception/InvalidMatchTypeException.php rename to src/HunspellPHP/InvalidMatchTypeException.php index 5890554..6022b98 100644 --- a/src/HunspellPHP/Exception/InvalidMatchTypeException.php +++ b/src/HunspellPHP/InvalidMatchTypeException.php @@ -1,5 +1,5 @@ Date: Thu, 30 Oct 2025 18:31:57 -0500 Subject: [PATCH 09/14] - New optional constructor argument `$custom_words_file` which takes a path to a custom word list to be merged with the dictionary at runtime. - Windows/Linux environments now use the same process execution code. - Hunspell process invocation is now handled through `proc_open` instead of `shell_exec`. - Hunspell `stderr` output is now logged via `error_log()` call. #### Changed - Changed constructor argument `$encoding` default value from 'en_US.utf-8' to 'UTF-8'. --- CHANGELOG.md | 9 ++ README.md | 5 ++ composer.json | 2 +- src/HunspellPHP/Hunspell.php | 164 ++++++++++++++++++++++++++--------- 4 files changed, 139 insertions(+), 41 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index d82323c..82f9f34 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,4 +1,13 @@ ## Changelog +### Version 3.0.0 +#### Added +- New optional constructor argument `$custom_words_file` which takes a path to a custom word list to be merged with the dictionary at runtime. +- Windows/Linux environments now use the same process execution code. +- Hunspell process invocation is now handled through `proc_open` instead of `shell_exec`. +- Hunspell `stderr` output is now logged via `error_log()` call. +#### Changed +- Changed constructor argument `$encoding` default value from 'en_US.utf-8' to 'UTF-8'. + ### Version 2.0.0 #### Added - Added PHP8.0 typed class, diff --git a/README.md b/README.md index 7f4322f..dfd4369 100644 --- a/README.md +++ b/README.md @@ -1,6 +1,11 @@ # Hunspell PHP wrapper Forked from [johnzuk/HunspellPHP](https://github.com/johnzuk/HunspellPHP) +### Version 3.0.0 (Very minor backward breaking change) +This version updates the constructor signature with a different (better?) default value for `$encoding`, so if anyone was using that this would be a backward breaking change. Otherwise, a new constructor argument $custom_word_file (path) has been added and will bind your provided custom word list with your dictionary in real time. + +The other change this version takes care of is using `proc_open` and better env/encoding handling in general. We also now emmit an `error_log()` call so stderr output from the hunspell process are logged properly. + ### Version 2.0.0 Version 2.0.0 and above requires PHP ^8.0.0 and includes an important fix to the result matcher regex. If you need this for an older version of PHP I recommend that you fork 1.2 and update the regex matcher property of the Hunspell class to what is set in the current version of the code. diff --git a/composer.json b/composer.json index 76ee190..6f114e7 100644 --- a/composer.json +++ b/composer.json @@ -2,7 +2,7 @@ "name": "belniakmedia/hunspell-php", "description": "Hunspell PHP wrapper", "minimum-stability": "dev", - "version": "2.0.0", + "version": "3.0.0", "license": "MIT", "authors": [ { diff --git a/src/HunspellPHP/Hunspell.php b/src/HunspellPHP/Hunspell.php index e153fa7..7692a8c 100644 --- a/src/HunspellPHP/Hunspell.php +++ b/src/HunspellPHP/Hunspell.php @@ -1,7 +1,10 @@ \*|\+|&|#|-)\s?(?P\w+)?\s?(?P\d+)?\s?(?P\d+)?:?\s?(?P.*+)?/u'; /** * @param string $dictionary Dictionary name e.g.: 'en_US' (default) - * @param string $encoding Encoding e.g.: 'utf-8' (default) - * @param ?string $dictionary_path Specify the directory of the dictionary file (optional) + * @param string $encoding Encoding e.g.: 'UTF-8' (default) + * @param string|null $dictionary_path Specify the directory of the dictionary file (optional) + * @param string|null $custom_words_file Specify the path to the custom words file (optional) */ public function __construct( string $dictionary = 'en_US', - string $encoding = 'en_US.utf-8', - ?string $dictionary_path = null + string $encoding = 'UTF-8', + ?string $dictionary_path = null, + ?string $custom_words_file = null ) { $this->dictionary = $this->clear($dictionary); $this->encoding = $this->clear($encoding); - $this->dictionary_path = $dictionary_path; + $this->dictionary_path = $dictionary_path ?? ''; + $this->custom_words_file = $custom_words_file ?? ''; } @@ -132,6 +139,81 @@ protected function clear(string $input): string return (string)preg_replace('[^a-zA-Z0-9_-\.]', '', $input); } + protected function hunspellSuggest(string $input, bool $stemSwitch): array + { + $timeoutMs = 1000; + $encoding = strtoupper(trim($this->encoding)); + $dictionary_file = $this->dictionary_path + ? rtrim($this->dictionary_path, DIRECTORY_SEPARATOR) . DIRECTORY_SEPARATOR . $this->dictionary + : $this->dictionary; + + $cmd = ['hunspell', '-a', '-d', trim($dictionary_file), '-i', $encoding]; + if($stemSwitch) { $cmd[] = '-s'; } + + if($this->custom_words_file) { + if(!file_exists($this->custom_words_file)) { + error_log('WARNING: HunspellPHP - $custom_words_file "' . $this->custom_words_file . '" not found.'); + } else { + $cmd[] = '-p'; + $cmd[] = $this->custom_words_file; + } + } + + $descriptors = [ + 0 => ['pipe', 'r'], // stdin + 1 => ['pipe', 'w'], // stdout + 2 => ['pipe', 'w'], // stderr + ]; + + $env = getenv(); + $env['LC_ALL'] = $env['LANG'] = PHP_OS_FAMILY === 'Windows' ? "$this->dictionary.$encoding" : "C.$encoding"; + $proc = proc_open($cmd, $descriptors, $pipes, null, $env); + if (!is_resource($proc)) { + return ['', 'proc_open failed', 1]; + } + + // Write the input word(s) followed by newline, as hunspell expects one per line. + fwrite($pipes[0], $input . "\n"); + fclose($pipes[0]); + + // Simple, bounded read with a timeout. + stream_set_blocking($pipes[1], false); + stream_set_blocking($pipes[2], false); + $deadline = microtime(true) + ($timeoutMs / 1000); + $out = ''; + $err = ''; + do { + $read = [$pipes[1], $pipes[2]]; + $write = $except = []; + $left = max(0, (int)round(($deadline - microtime(true)) * 1_000_000)); + if ($left === 0) { + break; + } + if (@stream_select($read, $write, $except, 0, $left) !== false) { + foreach ($read as $r) { + if ($r === $pipes[1]) { + $out .= stream_get_contents($pipes[1]) ?: ''; + } + if ($r === $pipes[2]) { + $err .= stream_get_contents($pipes[2]) ?: ''; + } + } + } + } while (microtime(true) < $deadline); + + // Drain remaining data. + $out .= stream_get_contents($pipes[1]) ?: ''; + $err .= stream_get_contents($pipes[2]) ?: ''; + fclose($pipes[1]); + fclose($pipes[2]); + + $status = proc_get_status($proc); + $exit = $status['exitcode'] ?? 0; + proc_close($proc); + + return [$out, $err, $exit]; + } + /** * @param string $input * @param bool $stem_mode @@ -139,15 +221,11 @@ protected function clear(string $input): string */ protected function findCommand(string $input, bool $stem_mode = false): string { - $stem_switch = $stem_mode ? ' -s' : ''; - $dictionary = $this->dictionary_path - ? rtrim($this->dictionary_path, DIRECTORY_SEPARATOR) . DIRECTORY_SEPARATOR . $this->dictionary - : $this->dictionary; - if (strtoupper(substr(PHP_OS, 0, 3)) === 'WIN') { - return (string)shell_exec(sprintf("powershell \"set LANG='%s'; echo '%s' | hunspell -d %s%s\"", $this->encoding, $input, $dictionary, $stem_switch)); - } else { - return (string)shell_exec(sprintf("export LANG='%s'; echo '%s' | hunspell -d %s%s", $this->encoding, $input, $dictionary, $stem_switch)); - } + [$stdout, $stderr] = $this->hunspellSuggest($input, $stem_mode); + if($stderr !== '') { + error_log('hunspell stderr: ' . trim($stderr)); + } + return $stdout; } /** @@ -161,9 +239,9 @@ protected function preParse(string $input, string $words): array array_shift($result); $words = array_map('trim', preg_split('/\W/', $words)); - if(sizeof($result) != sizeof($words)) { - return []; - } + if (sizeof($result) != sizeof($words)) { + return []; + } return array_combine($words, $result); } @@ -180,27 +258,33 @@ protected function parse(array $matches): HunspellResponse $matches['input'], $matches['type'] ); - } else if ($matches['type'] == Hunspell::ROOT) { - return new HunspellResponse( - $matches['original'], - $matches['input'], - $matches['type'] - ); - } else if ($matches['type'] == Hunspell::MISS) { - return new HunspellResponse( - '', - $matches['original'], - $matches['type'], - $matches['offset'], - explode(", ", $matches['misses']) - ); - } else if ($matches['type'] == Hunspell::NONE) { - return new HunspellResponse( - '', - $matches['input'], - $matches['type'], - $matches['count'] - ); + } else { + if ($matches['type'] == Hunspell::ROOT) { + return new HunspellResponse( + $matches['original'], + $matches['input'], + $matches['type'] + ); + } else { + if ($matches['type'] == Hunspell::MISS) { + return new HunspellResponse( + '', + $matches['original'], + $matches['type'], + $matches['offset'], + explode(", ", $matches['misses']) + ); + } else { + if ($matches['type'] == Hunspell::NONE) { + return new HunspellResponse( + '', + $matches['input'], + $matches['type'], + $matches['count'] + ); + } + } + } } throw new InvalidMatchTypeException(sprintf("Match type %s is invalid", $matches['type'])); @@ -217,11 +301,11 @@ protected function stemParse(array $matches): HunspellStemResponse $stems = []; foreach ($matches as $match) { $stem = explode(' ', $match); - if (isset($stem[1]) && !empty($stem[1])) { + if (!empty($stem[1])) { if (!in_array($stem[1], $stems)) { $stems[] = $stem[1]; } - } elseif (isset($stem[0]) && !empty($stem[0])) { + } elseif (!empty($stem[0])) { if (!in_array($stem[0], $stems)) { $stems[] = $stem[0]; } From 21a9e4f7c27c9837a1e1c76e1b0f82594546cdb6 Mon Sep 17 00:00:00 2001 From: Rick Date: Fri, 30 Jan 2026 00:17:20 -0600 Subject: [PATCH 10/14] - Moved `getenv()` call to constructor and stored env data as a class property for caching to ensure the call is not made more than once per instance. - Refactored `hunspellSuggest()` to work with space-separated list of words and return parsed result batch - This avoids needing to invoke the proc for each word which was a major performance issue. --- src/HunspellPHP/Hunspell.php | 176 ++++++++++++++++++++++++++--------- 1 file changed, 132 insertions(+), 44 deletions(-) diff --git a/src/HunspellPHP/Hunspell.php b/src/HunspellPHP/Hunspell.php index 7692a8c..0dd6680 100644 --- a/src/HunspellPHP/Hunspell.php +++ b/src/HunspellPHP/Hunspell.php @@ -20,6 +20,8 @@ class Hunspell Hunspell::COMPOUND => 'COMPOUND' ]; + private array $env; + protected string $encoding; protected string $dictionary; protected string $dictionary_path; @@ -43,6 +45,8 @@ public function __construct( $this->encoding = $this->clear($encoding); $this->dictionary_path = $dictionary_path ?? ''; $this->custom_words_file = $custom_words_file ?? ''; + + $this->env = getenv(); } @@ -120,13 +124,13 @@ public function find(string $words): array } /** - * @param string $word word to find + * @param string $words word to find * @return HunspellStemResponse */ - public function stem(string $word): HunspellStemResponse + public function stem(string $words): HunspellStemResponse { - $result = explode(PHP_EOL, $this->findCommand($word, true)); - $result['input'] = $word; + $result = explode(PHP_EOL, $this->findCommand($words, true)); + $result['input'] = $words; return $this->stemParse($result); } @@ -142,74 +146,88 @@ protected function clear(string $input): string protected function hunspellSuggest(string $input, bool $stemSwitch): array { $timeoutMs = 1000; + $encoding = strtoupper(trim($this->encoding)); - $dictionary_file = $this->dictionary_path + $dictionaryFile = $this->dictionary_path ? rtrim($this->dictionary_path, DIRECTORY_SEPARATOR) . DIRECTORY_SEPARATOR . $this->dictionary : $this->dictionary; - $cmd = ['hunspell', '-a', '-d', trim($dictionary_file), '-i', $encoding]; - if($stemSwitch) { $cmd[] = '-s'; } + // Build command + $cmd = ['hunspell', '-a', '-d', trim($dictionaryFile), '-i', $encoding]; + if ($stemSwitch) { + $cmd[] = '-s'; + } - if($this->custom_words_file) { - if(!file_exists($this->custom_words_file)) { - error_log('WARNING: HunspellPHP - $custom_words_file "' . $this->custom_words_file . '" not found.'); - } else { - $cmd[] = '-p'; - $cmd[] = $this->custom_words_file; - } + // Only add -p if file exists + if (!empty($this->custom_words_file) && file_exists($this->custom_words_file)) { + $cmd[] = '-p'; + $cmd[] = $this->custom_words_file; + } elseif (!empty($this->custom_words_file)) { + error_log('WARNING: HunspellPHP - $custom_words_file "' . $this->custom_words_file . '" not found.'); } + $tokens = preg_split('/\R+|\s+/u', trim($input), -1, PREG_SPLIT_NO_EMPTY) ?: []; + if (empty($tokens)) { + return ['', '', 0]; + } + $batchedInput = implode("\n", $tokens) . "\n"; + $descriptors = [ 0 => ['pipe', 'r'], // stdin 1 => ['pipe', 'w'], // stdout 2 => ['pipe', 'w'], // stderr ]; - $env = getenv(); - $env['LC_ALL'] = $env['LANG'] = PHP_OS_FAMILY === 'Windows' ? "$this->dictionary.$encoding" : "C.$encoding"; - $proc = proc_open($cmd, $descriptors, $pipes, null, $env); + // Build minimal env with locale data to pass to proc + $this->env['LC_ALL'] = $this->env['LANG'] = PHP_OS_FAMILY === 'Windows' + ? $this->dictionary . '.' . $encoding + : 'C.UTF-8'; + + $proc = proc_open($cmd, $descriptors, $pipes, null, $this->env); if (!is_resource($proc)) { return ['', 'proc_open failed', 1]; } - // Write the input word(s) followed by newline, as hunspell expects one per line. - fwrite($pipes[0], $input . "\n"); + // Write all in one go and close stdin so hunspell can exit cleanly + fwrite($pipes[0], $batchedInput); fclose($pipes[0]); - // Simple, bounded read with a timeout. + // Non-blocking read stream_set_blocking($pipes[1], false); stream_set_blocking($pipes[2], false); + + // Enforce Deadline $deadline = microtime(true) + ($timeoutMs / 1000); $out = ''; $err = ''; - do { - $read = [$pipes[1], $pipes[2]]; - $write = $except = []; - $left = max(0, (int)round(($deadline - microtime(true)) * 1_000_000)); - if ($left === 0) { + + while (true) { + $out .= stream_get_contents($pipes[1]) ?: ''; + $err .= stream_get_contents($pipes[2]) ?: ''; + + $status = proc_get_status($proc); + if (!$status['running']) { break; } - if (@stream_select($read, $write, $except, 0, $left) !== false) { - foreach ($read as $r) { - if ($r === $pipes[1]) { - $out .= stream_get_contents($pipes[1]) ?: ''; - } - if ($r === $pipes[2]) { - $err .= stream_get_contents($pipes[2]) ?: ''; - } - } + + if (microtime(true) >= $deadline) { + // IMPORTANT: terminate, otherwise proc_close() can still block. + proc_terminate($proc); + break; } - } while (microtime(true) < $deadline); - // Drain remaining data. + // Avoid hammer locking cpu during loop + usleep(1000); + } + + // Drain the pipes $out .= stream_get_contents($pipes[1]) ?: ''; $err .= stream_get_contents($pipes[2]) ?: ''; + fclose($pipes[1]); fclose($pipes[2]); - $status = proc_get_status($proc); - $exit = $status['exitcode'] ?? 0; - proc_close($proc); + $exit = proc_close($proc); return [$out, $err, $exit]; } @@ -235,14 +253,84 @@ protected function findCommand(string $input, bool $stem_mode = false): string */ protected function preParse(string $input, string $words): array { - $result = explode("\n", trim($input)); - array_shift($result); - $words = array_map('trim', preg_split('/\W/', $words)); + $input = str_replace(["\r\n", "\r"], "\n", $input); + + // Tokenize words the same way the batched hunspell call does: whitespace/newlines. + $tokens = preg_split('/\s+/u', trim($words), -1, PREG_SPLIT_NO_EMPTY) ?: []; + $tokens = array_values(array_map('trim', $tokens)); - if (sizeof($result) != sizeof($words)) { + if (empty($tokens)) { return []; } - return array_combine($words, $result); + + // Split stdout into blocks separated by blank lines. + // Skip the hunspell banner/header lines starting with "@(#)". + $rawLines = preg_split('/\n/', $input); + $blocks = []; + $current = []; + + foreach ($rawLines as $line) { + $t = trim($line); + + if ($t !== '' && str_starts_with($t, '@(#)')) { + continue; + } + + if ($t === '') { + if (!empty($current)) { + $blocks[] = $current; + $current = []; + } + continue; + } + + $current[] = $t; + } + + if (!empty($current)) { + $blocks[] = $current; + } + + if (count($blocks) !== count($tokens)) { + return []; + } + + // Normalize each block to a single line compatible with the existing matcher. + $out = []; + foreach ($tokens as $i => $token) { + $lines = $blocks[$i]; + $first = $lines[0] ?? ''; + + // Merge any extra lines (e.g. ", ASTM") into the "misses" list. + // We strip a leading comma/space and append as additional misses. + $extras = []; + for ($j = 1; $j < count($lines); $j++) { + $extra = trim($lines[$j]); + if ($extra === '') { + continue; + } + + // Hunspell often prefixes extra suggestions with ",". + $extra = preg_replace('/^[,]\s*/u', '', $extra); + if ($extra !== '') { + $extras[] = $extra; + } + } + + if (!empty($extras) && preg_match('/^(?:&|#|\+|-)/u', $first)) { + // If the first line already has a ":" misses list, append to it. + if (str_contains($first, ':')) { + $first .= ', ' . implode(', ', $extras); + } else { + // Otherwise create a misses list. + $first .= ': ' . implode(', ', $extras); + } + } + + $out[$token] = $first; + } + + return $out; } /** From a4f3ca30deb51a5f677665e7fde24a448aaa41fc Mon Sep 17 00:00:00 2001 From: Rick Date: Fri, 30 Jan 2026 00:24:48 -0600 Subject: [PATCH 11/14] - Updated `stem()`, `stemParse()` and `hunspellSuggest()` to ensure the stem branch of this library works correctly after the batch improvement made in the previous commit. --- src/HunspellPHP/Hunspell.php | 69 +++++++++++++++++++++++++++--------- 1 file changed, 52 insertions(+), 17 deletions(-) diff --git a/src/HunspellPHP/Hunspell.php b/src/HunspellPHP/Hunspell.php index 0dd6680..81a6367 100644 --- a/src/HunspellPHP/Hunspell.php +++ b/src/HunspellPHP/Hunspell.php @@ -129,9 +129,28 @@ public function find(string $words): array */ public function stem(string $words): HunspellStemResponse { - $result = explode(PHP_EOL, $this->findCommand($words, true)); - $result['input'] = $words; - return $this->stemParse($result); + $raw = $this->findCommand($words, true); + + // Normalize newlines + $raw = str_replace(["\r\n", "\r"], "\n", $raw); + $lines = preg_split('/\n/', $raw) ?: []; + + // Keep only real stem result lines + $lines = array_values(array_filter(array_map('trim', $lines), static function (string $line): bool { + if ($line === '') { + return false; + } + if (str_starts_with($line, '@(#)')) { + return false; + } + // stem lines contain at least two tokens + return preg_match('/\S+\s+\S+/u', $line) === 1; + })); + + return $this->stemParse([ + 'input' => $words, + 'lines' => $lines, + ]); } /** @@ -153,12 +172,21 @@ protected function hunspellSuggest(string $input, bool $stemSwitch): array : $this->dictionary; // Build command - $cmd = ['hunspell', '-a', '-d', trim($dictionaryFile), '-i', $encoding]; + $cmd = ['hunspell']; + if ($stemSwitch) { + // Stem mode $cmd[] = '-s'; + } else { + // Spellcheck (interactive) mode + $cmd[] = '-a'; } - // Only add -p if file exists + $cmd[] = '-d'; + $cmd[] = trim($dictionaryFile); + $cmd[] = '-i'; + $cmd[] = $encoding; + if (!empty($this->custom_words_file) && file_exists($this->custom_words_file)) { $cmd[] = '-p'; $cmd[] = $this->custom_words_file; @@ -384,21 +412,28 @@ protected function parse(array $matches): HunspellResponse */ protected function stemParse(array $matches): HunspellStemResponse { - $input = $matches['input']; - unset($matches['input']); + $input = (string)($matches['input'] ?? ''); + $lines = $matches['lines'] ?? []; + $stems = []; - foreach ($matches as $match) { - $stem = explode(' ', $match); - if (!empty($stem[1])) { - if (!in_array($stem[1], $stems)) { - $stems[] = $stem[1]; - } - } elseif (!empty($stem[0])) { - if (!in_array($stem[0], $stems)) { - $stems[] = $stem[0]; - } + foreach ($lines as $line) { + $line = trim((string)$line); + if ($line === '') { + continue; + } + + // Split by any whitespace; hunspell can separate with multiple spaces/tabs. + $parts = preg_split('/\s+/u', $line, -1, PREG_SPLIT_NO_EMPTY) ?: []; + if (count($parts) < 2) { + continue; + } + + $stem = $parts[1] ?? ''; + if ($stem !== '' && !in_array($stem, $stems, true)) { + $stems[] = $stem; } } + return new HunspellStemResponse($input, $stems); } From bc842957d910675a0575fe99164add3af70617dc Mon Sep 17 00:00:00 2001 From: Rick Date: Fri, 30 Jan 2026 00:30:43 -0600 Subject: [PATCH 12/14] - Updated readme and changelog. - Bumped version in composer.json to 4.0.0. --- CHANGELOG.md | 5 +++++ README.md | 3 +++ composer.json | 2 +- 3 files changed, 9 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 82f9f34..a61470e 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,4 +1,9 @@ ## Changelog +### Version 4.0.0 +### Updated +- Moved `getenv()` call to constructor and stored env data as a class property for caching to ensure the call is not made more than once per instance. +- Refactored `hunspellSuggest()` to work with space-separated list of words and return parsed result batch - This avoids needing to invoke the proc for each word which was a major performance issue. +- Updated `stem()`, `stemParse()` and `hunspellSuggest()` to ensure the stem branch of this library works correctly after the batch improvement. ### Version 3.0.0 #### Added - New optional constructor argument `$custom_words_file` which takes a path to a custom word list to be merged with the dictionary at runtime. diff --git a/README.md b/README.md index dfd4369..69a97eb 100644 --- a/README.md +++ b/README.md @@ -1,6 +1,9 @@ # Hunspell PHP wrapper Forked from [johnzuk/HunspellPHP](https://github.com/johnzuk/HunspellPHP) +### Version 4.0.0 (Optimization +Batch Mode) +This version changes find() and possibly stem() (I'm not exactly sure how stem() functioned before as I did not use it, but I updated to be compatible with the changes made under the hood to `hunspellSuggest()`). The changes to `hunspellSuggest()` can now take a space-separated string of words to batch process. This change allows a single process call to handle many spell checks (and stems) rather than having to invoke the process once for each word. The update also ensures the 1000ms timeout "deadline" is not forcing the process to wait that time before ending which appeared to be the case in previous versions. + ### Version 3.0.0 (Very minor backward breaking change) This version updates the constructor signature with a different (better?) default value for `$encoding`, so if anyone was using that this would be a backward breaking change. Otherwise, a new constructor argument $custom_word_file (path) has been added and will bind your provided custom word list with your dictionary in real time. diff --git a/composer.json b/composer.json index 6f114e7..f623c66 100644 --- a/composer.json +++ b/composer.json @@ -2,7 +2,7 @@ "name": "belniakmedia/hunspell-php", "description": "Hunspell PHP wrapper", "minimum-stability": "dev", - "version": "3.0.0", + "version": "4.0.0", "license": "MIT", "authors": [ { From 7c708537c63ed9819863314c80f3da8c59eca47e Mon Sep 17 00:00:00 2001 From: Rick Date: Sun, 1 Feb 2026 17:36:42 -0600 Subject: [PATCH 13/14] - Fixed type due to possibly passing non-int value to the `$offset` parameter when instantiating `HunspellResponse` objects. - Updated readme/changelog and bumped version to 4.0.1 --- CHANGELOG.md | 5 +++++ README.md | 4 ++-- composer.json | 2 +- src/HunspellPHP/Hunspell.php | 2 +- 4 files changed, 9 insertions(+), 4 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index a61470e..4197ba3 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,9 +1,14 @@ ## Changelog +### Version 4.0.1 +### Fixed +- Fixed type due to possibly passing non-int value to the `$offset` parameter when instantiating `HunspellResponse` objects. + ### Version 4.0.0 ### Updated - Moved `getenv()` call to constructor and stored env data as a class property for caching to ensure the call is not made more than once per instance. - Refactored `hunspellSuggest()` to work with space-separated list of words and return parsed result batch - This avoids needing to invoke the proc for each word which was a major performance issue. - Updated `stem()`, `stemParse()` and `hunspellSuggest()` to ensure the stem branch of this library works correctly after the batch improvement. + ### Version 3.0.0 #### Added - New optional constructor argument `$custom_words_file` which takes a path to a custom word list to be merged with the dictionary at runtime. diff --git a/README.md b/README.md index 69a97eb..a7b63cd 100644 --- a/README.md +++ b/README.md @@ -1,7 +1,7 @@ # Hunspell PHP wrapper Forked from [johnzuk/HunspellPHP](https://github.com/johnzuk/HunspellPHP) -### Version 4.0.0 (Optimization +Batch Mode) +### Version 4.x (Optimization +Batch Mode) This version changes find() and possibly stem() (I'm not exactly sure how stem() functioned before as I did not use it, but I updated to be compatible with the changes made under the hood to `hunspellSuggest()`). The changes to `hunspellSuggest()` can now take a space-separated string of words to batch process. This change allows a single process call to handle many spell checks (and stems) rather than having to invoke the process once for each word. The update also ensures the 1000ms timeout "deadline" is not forcing the process to wait that time before ending which appeared to be the case in previous versions. ### Version 3.0.0 (Very minor backward breaking change) @@ -9,7 +9,7 @@ This version updates the constructor signature with a different (better?) defaul The other change this version takes care of is using `proc_open` and better env/encoding handling in general. We also now emmit an `error_log()` call so stderr output from the hunspell process are logged properly. -### Version 2.0.0 +### Version 2.x Version 2.0.0 and above requires PHP ^8.0.0 and includes an important fix to the result matcher regex. If you need this for an older version of PHP I recommend that you fork 1.2 and update the regex matcher property of the Hunspell class to what is set in the current version of the code. [View Changelog](CHANGELOG.md) diff --git a/composer.json b/composer.json index f623c66..e124499 100644 --- a/composer.json +++ b/composer.json @@ -2,7 +2,7 @@ "name": "belniakmedia/hunspell-php", "description": "Hunspell PHP wrapper", "minimum-stability": "dev", - "version": "4.0.0", + "version": "4.0.1", "license": "MIT", "authors": [ { diff --git a/src/HunspellPHP/Hunspell.php b/src/HunspellPHP/Hunspell.php index 81a6367..9aae744 100644 --- a/src/HunspellPHP/Hunspell.php +++ b/src/HunspellPHP/Hunspell.php @@ -387,7 +387,7 @@ protected function parse(array $matches): HunspellResponse '', $matches['original'], $matches['type'], - $matches['offset'], + intval($matches['offset']), explode(", ", $matches['misses']) ); } else { From f29491dd09ae5db23e697932c6d843e8feb7a181 Mon Sep 17 00:00:00 2001 From: Rick Date: Fri, 6 Feb 2026 11:56:12 -0600 Subject: [PATCH 14/14] - Fixed another type issue where int value was not always passed as the offest param to `HunspellResponse::__construct()`. - Updated changelog and bumped version to 4.0.2 --- CHANGELOG.md | 5 +++++ composer.json | 2 +- src/HunspellPHP/Hunspell.php | 2 +- 3 files changed, 7 insertions(+), 2 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 4197ba3..2fc5b48 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,4 +1,9 @@ ## Changelog + +### Version 4.0.2 +### Fixed +- Fixed another type issue where int value was not always passed as the offest param to `HunspellResponse::__construct()`. + ### Version 4.0.1 ### Fixed - Fixed type due to possibly passing non-int value to the `$offset` parameter when instantiating `HunspellResponse` objects. diff --git a/composer.json b/composer.json index e124499..8f7d353 100644 --- a/composer.json +++ b/composer.json @@ -2,7 +2,7 @@ "name": "belniakmedia/hunspell-php", "description": "Hunspell PHP wrapper", "minimum-stability": "dev", - "version": "4.0.1", + "version": "4.0.2", "license": "MIT", "authors": [ { diff --git a/src/HunspellPHP/Hunspell.php b/src/HunspellPHP/Hunspell.php index 9aae744..556b859 100644 --- a/src/HunspellPHP/Hunspell.php +++ b/src/HunspellPHP/Hunspell.php @@ -396,7 +396,7 @@ protected function parse(array $matches): HunspellResponse '', $matches['input'], $matches['type'], - $matches['count'] + intval($matches['count']) ); } }