# portable-utf8: Unicode string handling for PHP when mbstring is not there

> voku/portable-utf8 is a PHP 7.1+ library of Unicode string functions that runs without mbstring, iconv or intl, using whichever of those the server has and falling back to Symfony Polyfills. It is easy to bundle, but swapping native calls for its static helpers is not a mechanical change.

**voku/portable-utf8** — 🉑 Portable UTF-8 library - performance optimized (unicode) string functions for PHP.

- Repository: https://github.com/voku/portable-utf8
- Website: https://voku.github.io/portable-utf8_docs/
- Stars: 518 · Forks: 89
- Language: PHP
- License: Apache-2.0
- Published: 2026-10-22 · Updated: 2026-10-22 · Language: en
- Canonical page: https://hysenlabs.com/projects/voku-portable-utf8

## PHP string functions count bytes, and that is the whole problem

The standard library in PHP treats a string as a byte sequence. A six letter word whose last four letters carry diacritics therefore reports a length of 10, and strtoupper only rewrites ASCII letters, so the accented characters come back unchanged while the multibyte aware mb_strtoupper uppercases them and mb_strlen reports 6. The usual fix is to require mbstring, iconv or intl on the server. The problem the author describes is that on a given machine you often cannot guarantee which of those is present, and that becomes acute for open source applications that have to run on servers with different configurations. The project positions itself as the bridge for that case: a single API that keeps working when the extensions are missing, and that switches to the fast native path when they are there.

## One API over three backends, chosen at runtime

The library is written in PHP for PHP 7.1 and up, and it auto-detects the server environment. If mbstring, iconv or another encoding extension is installed, the work is delegated to it, which the documentation frames as the best possible performance for that server. When nothing usable is installed, the implementation falls back to Symfony Polyfills. A shim for mbstring, iconv and intl is also included, so the library can be used on shared webspace where an extension cannot be added. The only hard requirement is the PCRE library, which ships by default and cannot be turned off in supported PHP versions. That architecture is the reason the package is easy to bundle: it has no extension requirement of its own, yet it does not reimplement everything from scratch when a native implementation is sitting on the same server.

## Installing through Composer and dropping the polyfills you do not need

The install path is a single Composer command, and the documentation recommends that you trim the polyfill packages you have no use for, because a removed package leaves no runtime overhead in your project:

```bash
composer require voku/portable-utf8
```

The `replace` section of your own composer.json is where that trimming happens:

```json
{
  "replace": {
    "symfony/polyfill-php72": "1.99",
    "symfony/polyfill-iconv": "1.99",
    "symfony/polyfill-intl-grapheme": "1.99",
    "symfony/polyfill-intl-normalizer": "1.99",
    "symfony/polyfill-mbstring": "1.99"
  }
}
```

Replacing a polyfill with a dummy version only works if your code genuinely does not call into it, so this step belongs after you know which parts of the library your project touches. The same Composer channel is also how older PHP versions are handled: the documentation states that PHP 7.1 is the minimum and that Composer will resolve an older Portable UTF-8 release when you need support for older PHP versions.

## UTF8::cleanup and UTF8::fix_utf8 for input that was encoded twice

The most concrete part of the API deals with broken input. UTF8::cleanup is documented with a mojibake example, taking a string that was stored as double encoded bytes and returning readable text again. UTF8::fix_utf8 does a narrower repair on a single mis-encoded string. Both matter in practice because upstream systems that claim UTF-8 often hand you latin1 bytes labelled as UTF-8, and the failure shows up much later as garbled names in a database or a PDF. The library also offers length and tag handling, where UTF8::strlen and UTF8::strip_tags return character aware counts compared with the byte based results of the native functions. One warning is worth repeating from the documentation: most of the time a native function is all you need, and replacing working calls before you know the reason and the consequences is the path to subtle behaviour changes.

## strtoupper and strlen do not map one to one onto UTF8

The documentation is unusually direct about the switching cost. Moving from the native API to Portable UTF-8 may not be straight-forward for everyone, and it is highly recommended not to update scripts to include the library, or replace anything, before you know the reason and the consequences. The difference shows up in one behaviour: the standard library leaves accented characters alone when uppercasing, while the UTF8 variant maps them to their uppercase forms. Code that relied on the first behaviour, for example a comparison against a mixed case label, can change results without any error being raised. The same applies to length checks, where byte counts become character counts. A migration is therefore a review of call sites rather than a search and replace, and the project itself advises that most of the time the native function is enough.

## A different approach for object oriented string work

For code that prefers an object wrapper, the project points to voku/Stringy, a fork of danielstjules/Stringy that is built on the Portable UTF-8 class and adds extra methods. The shape of the API differs completely: where portable-utf8 exposes static calls such as UTF8::strtoupper and UTF8::strlen, the Stringy route creates an instance from a string and then asks the instance for an uppercase version or a length. The result of the operations is the same, but the call style and the amount of chaining available differ. Another angle on the same problem is the mbstring, iconv and intl shim, which lets existing code keep calling extension style functions while the implementation underneath is portable, at the cost of shipping more of the library than a direct dependency needs.

## Apache-2.0 code, a GPL file and a release channel without tags

The library is released under Apache-2.0, and the repository root also carries LICENSE-APACHE and LICENSE-GPL files alongside a SECURITY.md and a CHANGELOG.md. The repository is not archived and the last push was on 2026-10-19, but it publishes no GitHub releases, so the versioned artefacts arrive through Packagist and its Composer tags rather than through tagged GitHub releases with notes. Quality tooling is broad for a library of this size: phpunit.xml for tests, phpstan.neon and a separate phpstan-php7.neon, psalm.xml, infection.json5 for mutation testing, phpcs.php_cs, scrutinizer and codacy configuration, CI workflows for GitHub Actions, AppVeyor, Travis and Circle, plus renovate.json for dependency updates. That setup suggests a mature but low drama project, where the change of a dependency, not a new feature, is what shows up in the log.

## Conclusion

portable-utf8 earns a place in shared hosting, CLI tools and libraries that cannot assume mbstring is installed, and its cleanup and fix_utf8 helpers are useful anywhere double encoded input shows up. It is the wrong choice when the target environment is known to have mbstring, iconv or intl, and it is the wrong choice for teams that want the native API untouched. Check three things before adopting it: that the PHP version in production is 7.1 or newer, whether the polyfill packages you intend to drop are listed in the replace section of composer.json, and how your own test suite behaves when strtoupper and strlen are replaced by their UTF8 counterparts. The last push was on 2026-10-19 and the repository publishes no GitHub releases, so upgrades come through Packagist.

## FAQ

### Does portable-utf8 need the mbstring or iconv extension to run?

No. The library needs only the PCRE library, which is available by default, and works on PHP 7.1 or newer without any extra encoding extension. If mbstring, iconv or intl happens to be installed, the library detects them and uses them instead.

### How do I install portable-utf8 in a PHP project?

Composer pulls it in with the command `composer require voku/portable-utf8`. You can then remove the Symfony polyfills you do not need by listing them in the `replace` section of your composer.json.

### What is the difference between UTF8::strlen and strlen in portable-utf8?

strlen counts bytes, so a string of accented characters counts each multi-byte character more than once. UTF8::strlen counts characters instead, and the documentation example shows 70 for strlen and 67 for UTF8::strlen on the same string.

### Can portable-utf8 repair text that was encoded twice?

Yes. UTF8::cleanup is given as the function for turning badly encoded input back into readable text, and UTF8::fix_utf8 handles a single mis-encoded string.

### Does portable-utf8 work with older PHP versions?

PHP 7.1 is the minimum requirement, and Composer resolves an older Portable UTF-8 release for projects that need to support older PHP versions. PHP 8.0 support is described as adapting the behaviours of the native functions.

## Sources

- [Issues](https://github.com/voku/portable-utf8/issues)
- [License: Apache-2.0](https://github.com/voku/portable-utf8/blob/master/LICENSE)
- [Project website](https://voku.github.io/portable-utf8_docs/)
- [README](https://github.com/voku/portable-utf8/blob/master/README.md)
- [voku/portable-utf8 on GitHub](https://github.com/voku/portable-utf8)

---

Hysen Labs editorial analysis, written from the project's own repository and release notes. Cite the canonical page: https://hysenlabs.com/projects/voku-portable-utf8
