PHPackages                             php-aspell/php-aspell - PHPackages - PHPackages  [Skip to content](#main-content)[PHPackages](/)[Directory](/)[Categories](/categories)[Trending](/trending)[Leaderboard](/leaderboard)[Changelog](/changelog)[Analyze](/analyze)[Collections](/collections)[Log in](/login)[Sign up](/register)

1. [Directory](/)
2. /
3. php-aspell/php-aspell

ActiveLibrary

php-aspell/php-aspell
=====================

A pure PHP 8.4 port of GNU Aspell with native LaTeX-aware spell checking

v1.6.0(1mo ago)027↓75%LGPL-2.1-or-laterCommon Workflow LanguagePHP &gt;=8.4

Since Jul 17Pushed 1mo agoCompare

[ Source](https://github.com/snorky22/php-aspell)[ Packagist](https://packagist.org/packages/php-aspell/php-aspell)[ Docs](https://github.com/snorky22/php-aspell)[ RSS](/packages/php-aspell-php-aspell/feed)WikiDiscussions master Synced 1w ago

READMEChangelogDependencies (2)Versions (14)Used By (0)

PHP-Aspell
==========

[](#php-aspell)

A pure PHP 8.4 port of GNU Aspell.

Goal
----

[](#goal)

To provide a faceless, high-performance spelling engine that can be easily integrated into PHP applications (e.g., Symfony controllers) without external binary dependencies.

Features
--------

[](#features)

- **Pure PHP implementation** (Targeting PHP 8.4+).
- **Multi-byte Support**: Full UTF-8 support for diverse languages including Arabic, Russian, German, Hebrew and French. Handles legacy encodings (ISO-8859-1, ISO-8859-8, KOI8-R, CP1256, etc.) by converting to UTF-8 internally.
- **Phonetic Engine**: Full implementation of the Aspell "Phonet" algorithm for soundslike transformations, now multi-byte aware.
- **Dictionary Support**:
    - **Binary Parser**: Supports standard `.aspell` and `.rws` binary files.
    - **Compressed Support**: Built-in decompression for `.cwl` (prezip) format.
    - **Multi-file Support**: Recursively parses `.multi` files for combining multiple word lists.
    - **Affix Compression**: Recognizes inflected forms of affix-compressed dictionaries (German, Hebrew, Russian, Arabic) by applying the prefix/suffix rules in `_affix.dat` at lookup time — so `schöner`, `Häuser` or Hebrew clitic-prefixed forms are accepted without expanding the whole language into memory.
    - **Auto-discovery &amp; Manifest**: Discovers the bundled dictionaries by their canonical `.multi` entry point and persists the resulting language → label → path table to a JSON manifest that is restored at runtime.
    - **Phonetic Rules**: Automatically loads language-specific phonetic rules from `_phonet.dat` files.
    - **Custom Dictionaries**: Writable personal word lists, persistable to GNU Aspell's `personal_ws-1.1` file format or serialized to/from a JSON string for database storage.
- **Suggestion Engine**: Integrated a weighted Damerau-Levenshtein edit distance algorithm for ranking spelling suggestions.
- **LaTeX Filtering**: Advanced state-machine-based filter for LaTeX documents (ported from GNU Aspell's `tex.cpp`).
- **Modern PHP 8.4 Features**: Utilizes property hooks, readonly classes, and asymmetric visibility for performance and safety.

Installation
------------

[](#installation)

```
composer require php-aspell/php-aspell
```

Usage
-----

[](#usage)

### Speller Engine (High-Level API)

[](#speller-engine-high-level-api)

The `Speller` class is the primary entry point for spell checking documents. It supports specialized modes like `latex`, powered by a robust state-machine port of the official GNU Aspell LaTeX filter.

#### Example: Setting up the Speller with the English Dictionary

[](#example-setting-up-the-speller-with-the-english-dictionary)

```
use Aspell\Config\AspellConfig;
use Aspell\Engine\Speller;

$config = new AspellConfig();
$speller = new Speller($config);

// Load a dictionary (can be .multi, .rws, or .cwl)
// It will automatically look for phonetic rules (en_phonet.dat) in the same directory.
$speller->loadDictionary('path/to/dictionaries/en.multi');

// Check a single word
if (!$speller->check('nait')) {
    $suggestions = $speller->suggest('nait');
    // Result: ['night', 'knight', ...]
}

// Check a LaTeX document
$latexContent = file_get_contents('paper.tex');
$misspelled = $speller->checkDocument($latexContent, 'latex');
```

#### Example: Discovering bundled dictionaries and persisting a manifest

[](#example-discovering-bundled-dictionaries-and-persisting-a-manifest)

Instead of hand-maintaining a table of which dictionaries ship with your application, the `Speller` can **auto-discover** them and **persist** the result as a small JSON manifest. Discovery scans a directory tree for each language's canonical entry point — a file named exactly `.multi`, where the two letters before `.multi` are trusted as the language code. Regional and variant multis (`en_US.multi`, `en-variant_0.multi`, `fr_FR.multi`, …) are the alternatives that the canonical `en.multi` / `fr.multi` already `add`s internally, so they are not listed as separate languages.

```
use Aspell\Engine\Speller;

// Discover the dictionaries under a root (defaults to the bundled ./dictionaries).
$dictionaries = Speller::discoverDictionaries('path/to/dictionaries');
// => [
//   'de' => ['label' => 'German — Deutsch',      'path' => '/abs/…/de.multi'],
//   'en' => ['label' => 'English',               'path' => '/abs/…/en.multi'],
//   'he' => ['label' => 'Hebrew — עברית',        'path' => '/abs/…/he.multi'],
//   … ordered by code, with absolute paths ready for loadDictionary()
// ]
```

Rather than run discovery on every request, generate the manifest once (e.g. at build/deploy time) and load it at runtime:

```
// --- Build time: write the manifest --------------------------------------
// Paths are stored RELATIVE to the root, so the file stays valid wherever the
// package is installed. Returns the discovered manifest for convenience.
Speller::saveDictionaryManifest('path/to/dictionaries/dictionaries.json', 'path/to/dictionaries');

// --- Runtime: restore the $DICTIONARIES table ----------------------------
// The source may be a path to the JSON file OR the JSON string itself; relative
// paths are resolved back to absolute against the given root.
$dictionaries = Speller::loadDictionaryManifest(
    'path/to/dictionaries/dictionaries.json',
    'path/to/dictionaries'
);

$speller->loadDictionary($dictionaries['de']['path']);
```

Both `discoverDictionaries()` and `loadDictionaryManifest()` return the same shape — `['' => ['label' => string, 'path' => string], …]` — so callers can treat them interchangeably (discover live when no manifest exists, load the manifest otherwise). `Speller::defaultDictionaryRoot()` returns the bundled `dictionaries/` directory, which is the default root when the argument is omitted. Labels come from a built-in table of common languages (falling back to the uppercased code for unknown ones).

A ready-made CLI script regenerates the manifest — run it after adding or removing a dictionary:

```
php bin/build-dictionaries.php
# or with an explicit root / output path:
php bin/build-dictionaries.php path/to/dictionaries path/to/out.json
```

```
{
    "dictionaries": {
        "de": { "label": "German — Deutsch",      "path": "aspell6-de-20161207-7-0/de.multi" },
        "en": { "label": "English",               "path": "aspell6-en-2026.02.25-0/en.multi" },
        "he": { "label": "Hebrew — עברית",         "path": "aspell6-he-1.0-0/he.multi" }
    }
}
```

`public/index.php` uses exactly this flow: it restores `$DICTIONARIES` from `dictionaries/dictionaries.json` when present, and falls back to live discovery otherwise — so the web UI's language selector is populated automatically.

#### Example: Custom (personal) dictionary

[](#example-custom-personal-dictionary)

You can load a writable custom dictionary that is checked *in addition to* the language dictionary, and add words to it at runtime. When backed by a file, the words are persisted in GNU Aspell's plain-text personal word list format (`personal_ws-1.1   utf-8`, one word per line) so they survive across requests.

```
$speller->loadDictionary('path/to/dictionaries/en.multi');

// Bind a persistent custom dictionary (created if it does not yet exist).
$speller->loadCustomDictionary('path/to/user.pws');

$speller->check('Kubernetes');   // false
$speller->addWord('Kubernetes'); // true (added + written to user.pws)
$speller->check('Kubernetes');   // true — now accepted alongside the language dict

// addWord() also works without a bound file (in-memory only, not persisted):
$speller->addWord('Symfony');
```

`addWord()` returns `false` if the word was already known to the custom dictionary. Matching is case-insensitive, and added words also feed the suggestion engine.

#### Example: Serializing a custom dictionary to a string (e.g. a database)

[](#example-serializing-a-custom-dictionary-to-a-string-eg-a-database)

When you would rather store the personal word list in a database (or any other text store) than in a `.pws` file, the custom dictionary can be serialized to and from a JSON string. The JSON is UTF-8 and human-readable — multibyte words such as `café` are kept literal, not `\uXXXX`-escaped — so it drops straight into a `TEXT`/`LONGTEXT` (`utf8mb4`) column:

```
// --- Restore before a spelling session -------------------------------------
$json = $row['dictionary']; // JSON string read from your DB (may be empty)
if ($json !== '') {
    $speller->setCustomDictionaryFromJson($json);
}

// ... run the session; addWord() etc. work exactly as above ...
$speller->addWord('Symfony');
$speller->addWord('café');

// --- Persist after the session ---------------------------------------------
$row['dictionary'] = $speller->getCustomDictionaryAsJson();
// e.g. {"lang":"fr","words":["Symfony","café"]}
// UPDATE ... SET dictionary = :dictionary
```

A dictionary set from JSON is **in-memory only** — nothing is written to disk, so the database stays the single source of truth (and there is no per-`addWord()`file rewrite during the session). `getCustomDictionaryAsJson()` always returns valid JSON, even when no custom dictionary is configured (an empty `words` list).

The same round-trip is available on the dictionary itself via `CustomDictionary::toJson()` and `CustomDictionary::fromJson()`. `fromJson()`accepts either the full `{"lang":…, "words":[…]}` object or a bare `["word", …]` array, and takes an optional path to bind the result to a file:

```
use Aspell\Dictionary\CustomDictionary;

$dict = CustomDictionary::fromJson($json);          // in-memory
$dict = CustomDictionary::fromJson($json, $path);   // also persisted to $path
$json = $dict->toJson();
```

#### Example: Finding and correcting misspellings with `misspellingRegex()`

[](#example-finding-and-correcting-misspellings-with-misspellingregex)

`checkDocument()` tells you *which* words are misspelled; `misspellingRegex()`turns that word list into a single compiled PCRE pattern that matches every whole-word occurrence of those words in the text. With it you can count occurrences, highlight them, or replace them — the same pattern drives plain-text correction and HTML highlighting alike.

The example below builds a corrector in clearly labelled steps: identify the misspelled words, build the pattern, choose a suggestion per word, then replace every occurrence while preserving its capitalisation. Because matching is whole-word, correction never needs character offsets — the identical approach works in PHP and in client-side JavaScript.

First, a small helper that transfers the capitalisation of the word found in the text onto the (lower-cased) suggestion, so `Teh` becomes `The` and `TEH`becomes `THE`:

```
/**
 * Transfer the capitalisation of $model (the word as it appeared in the text)
 * onto $replacement: ALL CAPS, Titlecase, or left as-is.
 */
function matchCase(string $model, string $replacement): string
{
    // "WORD" -> replacement entirely in upper case.
    if (mb_strtoupper($model, 'UTF-8') === $model && mb_strtolower($model, 'UTF-8') !== $model) {
        return mb_strtoupper($replacement, 'UTF-8');
    }

    // "Word" -> capitalise only the first letter of the replacement.
    $firstChar = mb_substr($model, 0, 1, 'UTF-8');
    if (mb_strtoupper($firstChar, 'UTF-8') === $firstChar && mb_strtolower($firstChar, 'UTF-8') !== $firstChar) {
        $head = mb_strtoupper(mb_substr($replacement, 0, 1, 'UTF-8'), 'UTF-8');
        $tail = mb_substr($replacement, 1, null, 'UTF-8');
        return $head . $tail;
    }

    // "word" -> leave the replacement untouched.
    return $replacement;
}
```

Now the corrector itself:

```
use Aspell\Engine\Speller;

/**
 * Replace every misspelled word in $text with its top suggestion, preserving
 * each occurrence's capitalisation. Returns the corrected text.
 */
function correctText(Speller $speller, string $text): string
{
    // --- Step 1: identify the unique misspelled words --------------------
    // checkDocument() returns one entry per occurrence; reduce to the set.
    $found = $speller->checkDocument($text);
    $words = array_values(array_unique(array_column($found, 'word')));

    if ($words === []) {
        return $text; // nothing to correct
    }

    // --- Step 2: build one whole-word pattern for all those words ---------
    $rx = $speller->misspellingRegex($words);

    // --- Step 3: choose the best suggestion for each word ----------------
    // Key by the lower-cased word so any capitalisation ("sentance",
    // "Sentance", "SENTANCE") resolves to the same suggestion in Step 4.
    $best = [];
    foreach ($words as $word) {
        $suggestions = $speller->suggest($word);
        if ($suggestions !== []) {
            $best[mb_strtolower($word, 'UTF-8')] = $suggestions[0];
        }
    }

    // --- Step 4: replace every occurrence, preserving its capitalisation -
    $callback = function (array $match) use ($best): string {
        $original = $match[1];
        $key      = mb_strtolower($original, 'UTF-8');

        // No suggestion for this word: leave it untouched.
        if (!isset($best[$key])) {
            return $original;
        }

        // Transfer the original word's capitalisation onto the suggestion.
        return matchCase($original, $best[$key]);
    };

    return preg_replace_callback($rx, $callback, $text) ?? $text;
}

// Correct the whole document — the case of each word is preserved:
echo correctText($speller, 'Sentance has a fwe MISSPELED words.');
// => 'Sentence has a few MISSPELLED words.'
```

Counting is just as direct — `preg_match_all()` returns the number of matches, so you never need the match positions:

```
$rx    = $speller->misspellingRegex($words);
$total = preg_match_all($rx, $text); // total misspelled-word occurrences
```

The pattern matches **whole words only** and treats apostrophes as word-internal — consistent with `checkDocument()`'s tokenizer — so contractions such as `don't` are matched as a unit rather than as `don`. It is built with the case-insensitive (`i`) and Unicode (`u`) flags, so matches are found regardless of capitalisation and across scripts. Each candidate word is passed through `preg_quote()`, so words containing regex metacharacters are matched literally. `public/index.php` uses this method to build both its corrected-text output and its highlighted HTML preview from a single pattern.

Because the match is whole-word, replacing a suggestion needs no character positions, so the same approach runs unchanged in the browser — the web UI applies a suggestion to every occurrence client-side with an equivalent regex:

```
const rx = new RegExp("(?Word found!";
}
```

### Suggestion Engine

[](#suggestion-engine)

The `SuggestionEngine` calculates weighted edit distance to rank spelling suggestions.

```
use Aspell\Engine\SuggestionEngine;
use Aspell\Engine\EditDistanceWeights;

$engine = new SuggestionEngine();
$weights = new EditDistanceWeights(del1: 1, del2: 1, swap: 1, sub: 1);
$distance = $engine->editDistance('nait', 'night', $weights);
```

### Configuration

[](#configuration)

The `AspellConfig` class handles all speller settings.

```
use Aspell\Config\AspellConfig;

$config = new AspellConfig();
$config->replace('lang', 'fr');
echo $config->retrieve('lang'); // fr
```

Web demo
--------

[](#web-demo)

A minimal browser UI for trying the checker interactively lives in `public/index.php`. It provides a text area for LaTeX/plain input, a dictionary selector populated automatically from the discovered dictionaries (currently English, French, German, Hebrew, Russian and Arabic), a corrected-text box with a copy button, a highlighted preview, and per-word suggestions (click a suggestion to apply it).

### Running the server

[](#running-the-server)

Start PHP's built-in web server from the project root (the `php-aspell` directory), then open the printed URL in your browser:

```
cd php-aspell
php -S 127.0.0.1:8080 public/index.php
# then open http://127.0.0.1:8080/
```

Notes:

- **Keep the terminal open** — the server runs in the foreground. Press `Ctrl+C` to stop it.
- **Run it from the `php-aspell` directory** so it can find `vendor/` and `dictionaries/`(both must be present; run `composer install` first if `vendor/` is missing).
- **Port already in use?** Pick another one, e.g. `php -S 127.0.0.1:8137 public/index.php`.
- **First check load time**: the chosen dictionary is loaded into memory on each request. French/English/German/Russian are fast (~0.2–0.8 s); Hebrew is quick too despite its size because inflected forms are matched on demand rather than expanded; Arabic (≈ 1M words) takes a few seconds.

The page renders on `GET`; submitting posts the text as JSON and runs the checker server-side.

Roadmap
-------

[](#roadmap)

- Phase I: Core Models &amp; Configuration
- Phase II: Phonetic Engine
- Phase III: Dictionary Parser (Multi-file &amp; Lookups)
- Phase IV: Suggestion Engine (Weighted Levenshtein)
- Phase V: Packagist Release

Testing
-------

[](#testing)

To run the test suite:

```
vendor/bin/phpunit tests/
```

###  Health Score

44

—

FairBetter than 90% of packages

Maintenance92

Actively maintained with recent releases

Popularity9

Limited adoption so far

Community6

Small or concentrated contributor base

Maturity58

Maturing project, gaining track record

 Bus Factor1

Top contributor holds 100% of commits — single point of failure

How is this calculated?**Maintenance (25%)** — Last commit recency, latest release date, and issue-to-star ratio. Uses a 2-year decay window.

**Popularity (30%)** — Total and monthly downloads, GitHub stars, and forks. Logarithmic scaling prevents top-heavy scores.

**Community (15%)** — Contributors, dependents, forks, watchers, and maintainers. Measures real ecosystem engagement.

**Maturity (30%)** — Project age, version count, PHP version support, and release stability.

###  Release Activity

Cadence

Every ~0 days

Total

13

Last Release

41d ago

### Community

Maintainers

![](https://avatars.githubusercontent.com/u/3802603?v=4)[snorky22](/maintainers/snorky22)[@snorky22](https://github.com/snorky22)

---

Top Contributors

[![snorky22](https://avatars.githubusercontent.com/u/3802603?v=4)](https://github.com/snorky22 "snorky22 (24 commits)")

---

Tags

spellingaspelldictionarylatexspellcheckspellcheckerspell-checkphoneticcwl

###  Code Quality

TestsPHPUnit

### Embed Badge

![Health badge](/badges/php-aspell-php-aspell/health.svg)

```
[![Health](https://phpackages.com/badges/php-aspell-php-aspell/health.svg)](https://phpackages.com/packages/php-aspell-php-aspell)
```

###  Alternatives

[tigitz/php-spellchecker

Provides an easy way to spellcheck multiple text source by many spellcheckers, directly from PHP

312570.0k1](/packages/tigitz-php-spellchecker)[mathjax/mathjax

MathJax is an open-source JavaScript display engine for LaTeX, MathML, and AsciiMath notation that works in all modern browsers.

10.9k96.0k1](/packages/mathjax-mathjax)[wapmorgan/morphos

A morphological solution for Russian and English language written completely in PHP. Provides classes to inflect personal names, geographical names, decline and pluralize nouns, generate cardinal and ordinal numerals, spell out money amounts and time.

8281.5M10](/packages/wapmorgan-morphos)[peckphp/peck

Peck is a powerful CLI tool designed to identify pure wording or spelling (grammar) mistakes in your codebase.

475491.5k143](/packages/peckphp-peck)[mekras/php-speller

PHP spell check library

68415.3k](/packages/mekras-php-speller)[knplabs/dictionary-bundle

Are you often tired to repeat static choices like gender or civility in your apps ?

90289.1k](/packages/knplabs-dictionary-bundle)

PHPackages © 2026

[Directory](/)[Categories](/categories)[Trending](/trending)[Changelog](/changelog)[Analyze](/analyze)
