php-spellcheck / spellcheck-core
Framework-agnostic PHP spell checking engine for source code and translation files. Hunspell, Aspell, pspell or pure PHP backends, character-accurate positions, PSR-6 caching.
Requires
- php: >=8.1
- ext-mbstring: *
- nikic/php-parser: ^4.18 || ^5.0
- psr/cache: ^1.0 || ^2.0 || ^3.0
- psr/log: ^1.1 || ^2.0 || ^3.0
- symfony/finder: ^5.4 || ^6.4 || ^7.0
- symfony/process: ^5.4 || ^6.4 || ^7.0
Requires (Dev)
- friendsofphp/php-cs-fixer: ^v3
- phpstan/phpstan: ^2.2
- phpunit/phpunit: ^9.6
- symfony/filesystem: ^5.4 || ^6.4 || ^7.0
- symfony/yaml: ^5.4 || ^6.4 || ^7.0
Suggests
- ext-pspell: To use the pspell backend without spawning external processes
- symfony/translation: To check Symfony translation catalogues (see php-spellcheck/spellcheck-bundle)
Provides
None
Conflicts
None
Replaces
None
This package is auto-updated.
Last update: 2026-09-07 09:10:50 UTC
README
Spell checking engine for PHP code and translation catalogues.
This is the framework agnostic core. For the Symfony integration (commands,
configuration, Translator support) use
php-spellcheck/spellcheck.
- PHP ≥ 8.1
- No mandatory
ext-intl(the ICU parser is built in) - Works without any system binary, through the pure PHP
wordlistbackend nikic/php-parser4 or 5
Why
cspell does not know that messages.it.yaml must be checked in Italian, that
%count% is a placeholder or that {n, plural, one {...}} has textual
branches. tigitz/php-spellchecker is an excellent backend abstraction but
knows nothing about the domain. This library is the missing middle layer: it
turns catalogues and PHP identifiers into checkable text, accurately, and hands
the words to whichever backend you have.
Install
composer require --dev php-spellcheck/spellcheck-core
Quick start
use PHPSpellcheck\Core\Checker\MisspellingFactory; use PHPSpellcheck\Core\Checker\RunConfiguration; use PHPSpellcheck\Core\Checker\RunStatisticsCollector; use PHPSpellcheck\Core\Checker\SpellcheckRunner; use PHPSpellcheck\Core\Diagnostics\DiagnosticCollector; use PHPSpellcheck\Core\Dictionary\BuiltinDictionaries; use PHPSpellcheck\Core\Dictionary\DictionaryLoader; use PHPSpellcheck\Core\Dictionary\LocaleDictionaryMap; use PHPSpellcheck\Core\Filter\DeduplicationFilter; use PHPSpellcheck\Core\Filter\DictionaryFilter; use PHPSpellcheck\Core\Filter\FilterChain; use PHPSpellcheck\Core\Php\IdentifierKind; use PHPSpellcheck\Core\Processor\HtmlProcessor; use PHPSpellcheck\Core\Processor\PlaceholderProcessor; use PHPSpellcheck\Core\Processor\ProcessorChain; use PHPSpellcheck\Core\Report\BufferedWriter; use PHPSpellcheck\Core\Report\TableReporter; use PHPSpellcheck\Core\Source\PhpFileSource; use PHPSpellcheck\Core\Speller\HunspellSpeller; use PHPSpellcheck\Core\Tokenizer\IdentifierSplitter; use PHPSpellcheck\Core\Tokenizer\ProseTokenizer; use PHPSpellcheck\Core\Tokenizer\TokenizerRegistry; $dictionary = (new DictionaryLoader())->loadAll( BuiltinDictionaries::paths(['technical', 'php', 'symfony']), ); $diagnostics = new DiagnosticCollector(); $statistics = new RunStatisticsCollector(); $runner = new SpellcheckRunner( new ProcessorChain([new PlaceholderProcessor(), new HtmlProcessor()]), new TokenizerRegistry([new IdentifierSplitter(4), new ProseTokenizer(4)]), new HunspellSpeller(), new FilterChain([new DictionaryFilter($dictionary), new DeduplicationFilter()]), new MisspellingFactory(), $diagnostics, $statistics, ); $source = new PhpFileSource( paths: [__DIR__.'/src'], exclude: ['vendor'], kinds: IdentifierKind::DEFAULTS, language: 'en_US', projectDir: __DIR__, diagnostics: $diagnostics, statistics: $statistics, ); $result = $runner->run([$source], new RunConfiguration(new LocaleDictionaryMap())); $writer = new BufferedWriter(); (new TableReporter())->report($result, $writer); echo $writer->getBuffer();
Architecture
Source ──► ProcessorChain ──► Tokenizer ──► Speller ──► FilterChain ──► RunResult
▲
CachingSpeller (PSR-6)
| Layer | Contract | Built-in implementations |
|---|---|---|
| Source | SourceInterface |
PhpFileSource, ArrayFragmentSource, ChainSource |
| Processor | TextProcessorInterface |
placeholders, sprintf, HTML, markdown, URLs, ICU, legacy plurals, docblocks, apostrophes |
| Tokenizer | TokenizerInterface |
IdentifierSplitter, ProseTokenizer |
| Speller | SpellerInterface |
HunspellSpeller, AspellSpeller, PspellSpeller, WordListSpeller, ChainSpeller, CachingSpeller, NullSpeller |
| Filter | MisspellingFilterInterface |
dictionary, pattern, deduplication, baseline |
| Reporter | ReporterInterface |
table, json, github, checkstyle, junit, gitlab, csv |
Everything is a generator: memory stays flat regardless of how many files or messages are processed.
Accurate positions
Every processor that removes text records the correspondence in an OffsetMap,
and maps compose associatively. That is what makes the reported column point at
the position in the original message:
// 'Ciao %name%, hai %count% messagi' -> the column of "messagi" is 26, // not the offset it would have in the cleaned-up string.
Ispell pipe protocol
PipeSpeller keeps one process alive per language for the whole run. Words are
sent one per line prefixed with ^, and the terminating blank line is the
synchronisation point. Terse mode (!) suppresses the * lines and cuts the
I/O by roughly 90%; if your binary does not emit the blank line in terse mode,
construct the backend with terseMode: false.
Baseline
The fingerprint is sha256(schema | contextSeed | lowercase word | type)
truncated to 16 hex characters. It deliberately excludes line and column, so
moving code around does not invalidate the baseline. Renaming a file does, and
that is documented behaviour.
Extending
Implement the relevant interface and pass it to the chain. In a Symfony application the bundle autoconfigures the six extension points; outside of it, compose the objects yourself as in the quick start above.
Tests
composer install vendor/bin/phpunit # unit tests, no binaries needed vendor/bin/phpunit --group integration # requires hunspell and aspell php tests/smoke.php # dependency free sanity check
tests/smoke.php runs a subset of the suite with a hand written autoloader and
no composer at all: useful to verify a checkout in a bare container.
License
MIT.