Explore the best modern PHP alternatives to utf8_encode and utf8_decode with detailed guides on using mb_convert_encoding and iconv for robust character encoding in PHP 8.2.
You upgrade to PHP 8.2, run your test suite, and the logs fill up with deprecation notices pointing at utf8_encode() and utf8_decode(). If you’ve shipped PHP for any length of time, you’ve used them. They looked like the obvious way to “make a string UTF-8.” That’s exactly the problem, and it’s why they’re on the way out.
Here’s the short version, then the details. These two functions were deprecated in PHP 8.2. They don’t do what their names suggest, and the fix in almost every case is one line: mb_convert_encoding( $string, 'UTF-8', 'ISO-8859-1' ). The rest of this guide is about knowing which source encoding to name, and what to do when you don’t know it.
Why utf8_encode() and utf8_decode() got deprecated
The names are a lie, and that’s the real reason.
Despite what “utf8_encode” sounds like, the function does not encode arbitrary text as UTF-8. It only ever converted from ISO-8859-1 (Latin-1) to UTF-8. Its partner, utf8_decode(), only went the other way, UTF-8 back to ISO-8859-1. Feed it Windows-1252, Shift-JIS, or already-valid UTF-8, and you get silent corruption, not a conversion.
That mismatch between what the name promises and what the code does caused years of subtle bugs. So in PHP 8.2 both functions were deprecated. The replacements, mb_convert_encoding() and iconv(), make you say the source encoding out loud, which is the whole point. No hidden Latin-1 assumption.
Understanding Character Encoding
What Is Character Encoding?
A character encoding is the map between the characters you read and the bytes a computer stores. Computers only hold bytes, so every “é” or “ç” has to be written down as one or more byte values, and read back the same way.
The catch is that there’s more than one map. UTF-8 and ISO-8859-1 are two of the common ones. UTF-8 is variable-length and can represent every character in Unicode, which is why it runs the modern web. ISO-8859-1 is a single-byte, 256-slot map that only covers Western European characters. When the map you write with and the map you read with disagree, text turns to garbage.
Where the old functions fall short
The whole limitation is that single hardcoded pair: ISO-8859-1 in one direction, UTF-8 in the other. Nothing else.
If your string is actually Windows-1252 (which looks like Latin-1 but uses the 0x80 to 0x9F range for curly quotes, dashes, and the euro sign), the old functions get those characters wrong. They also can’t tell you when the input was malformed. They just hand back a string and let the corruption ride downstream. For anything touching user uploads, scraped data, or legacy databases, that’s not good enough.
Modern Alternatives to utf8_encode() and utf8_decode()
Two functions replace them, and you likely already have both:
mb_convert_encoding(): from the Multibyte String (mbstring) extension. Handles a wide range of encodings and is the natural default for multibyte work.iconv(): from the iconv extension. Also converts between encodings, with finer control over what happens to characters it can’t map.
Check that mbstring and iconv are available
Both extensions ship with most PHP builds, but don’t assume. A quick check before you rely on either:
<?php
// Check if mbstring is enabled
if ( extension_loaded( 'mbstring' ) ) {
echo 'mbstring is enabled';
} else {
echo 'mbstring is not enabled';
}
// Check if iconv is enabled
if ( extension_loaded( 'iconv' ) ) {
echo 'iconv is enabled';
} else {
echo 'iconv is not enabled';
}
?>Basic Usage of mb_convert_encoding()
This is the direct swap for the old functions. Arguments in order: the string, the target encoding, then the source encoding.
Example 1: Converting ISO-8859-1 to UTF-8
This is the exact drop-in for an old utf8_encode() call:
<?php
$iso_string = 'This is a string with ISO-8859-1 characters: é, ç, ü';
$utf8_string = mb_convert_encoding( $iso_string, 'UTF-8', 'ISO-8859-1' );
echo $utf8_string;
?>The old utf8_encode( $iso_string ) is now mb_convert_encoding( $iso_string, 'UTF-8', 'ISO-8859-1' ). Same result, but now the source encoding is stated, not assumed.
Example 2: Converting UTF-8 to ISO-8859-1
And the reverse, the stand-in for utf8_decode():
<?php
$utf8_string = 'This is a UTF-8 string with characters: é, ç, ü';
$iso_string = mb_convert_encoding( $utf8_string, 'ISO-8859-1', 'UTF-8' );
echo $iso_string;
?>Handling Multiple Encodings
When you don’t know the source encoding for sure, you can hand mb_convert_encoding() a list of candidates and let it detect.
Example 3: Handling Multiple Source Encodings
Say the input might be ISO-8859-1 or Windows-1252, and you want UTF-8 either way:
<?php
$input_string = 'This is a string with unknown encoding: é, ç, ü';
$encodings = ['ISO-8859-1', 'Windows-1252'];
$utf8_string = mb_convert_encoding( $input_string, 'UTF-8', $encodings );
echo $utf8_string;
?>Given a list, mb_convert_encoding() detects which candidate the string matches, then converts from that one. Order matters: detection isn’t perfect, so put the most likely encoding first.
A caveat on error handling with mb_convert_encoding()
Here’s a point most tutorials get wrong, so read it carefully. mb_convert_encoding() does not return false when your input contains characters it can’t map. For a bad character it substitutes a replacement (controlled by mb_substitute_character()) and keeps going. It only fails outright on an invalid encoding name, and on PHP 8 that throws a ValueError rather than returning false.
Example 4: The pattern you’ll see, and why it rarely fires
You’ll run into code like this in the wild:
<?php
$input_string = "This is a string with invalid characters: \x80\x81\x82";
$utf8_string = @mb_convert_encoding( $input_string, 'UTF-8', 'ISO-8859-1' );
if ( $utf8_string === false ) {
echo "Conversion failed due to invalid characters.";
} else {
echo $utf8_string;
}
?>Be honest about what this does. Converting from ISO-8859-1, every one of the 256 byte values is defined, so \x80\x81\x82 convert without complaint and the === false branch never runs. If you genuinely need to catch malformed input, set mb_substitute_character('none') and compare lengths, or reach for iconv(), which is stricter. The @ here just hides warnings; it doesn’t add real error handling.
Using iconv() for Character Conversion
iconv() covers the same ground with tighter control over unmappable characters. Note the argument order flips: source first, then target, then the string.
Example 5: Converting with iconv()
<?php
$iso_string = 'This is a string with ISO-8859-1 characters: é, ç, ü';
$utf8_string = iconv( 'ISO-8859-1', 'UTF-8', $iso_string );
echo $utf8_string;
?>Source encoding, target encoding, input string. That’s the order to remember, and it’s the opposite of mb_convert_encoding(), which trips people up.
Handling Errors with iconv()
Where iconv() earns its place is the suffix you can tack onto the target encoding: //IGNORE drops characters it can’t produce, and //TRANSLIT swaps them for a close approximation.
Example 6: Dropping unmappable characters
<?php
$iso_string = "This is a string with invalid characters: \x80\x81\x82";
$utf8_string = iconv( 'ISO-8859-1', 'UTF-8//IGNORE', $iso_string );
if ( $utf8_string === false ) {
echo "Conversion failed due to invalid characters.";
} else {
echo $utf8_string;
}
?>One honest note on this example: since every byte is valid ISO-8859-1, nothing actually gets dropped here. //IGNORE earns its keep in the harder direction, when the target can’t represent a character (say UTF-8 down to ASCII) or when the source really is malformed. Without //IGNORE, iconv() stops at the first character it can’t convert and returns false, which is the strictness you sometimes want.
Detecting the Encoding First
When the source is a mystery, detect before you convert. mb_detect_encoding() does the guessing.
Example 7: Detecting Encoding with mb_detect_encoding()
<?php
$string = 'This is a string with unknown encoding';
$encoding = mb_detect_encoding( $string, ['UTF-8', 'ISO-8859-1', 'Windows-1252'], true );
if ( $encoding ) {
echo "The detected encoding is: " . $encoding;
} else {
echo "Encoding could not be detected.";
}
?>The third argument, true, turns on strict mode, so a string that doesn’t cleanly match a candidate returns false instead of a hopeful guess. Keep it on. And keep your candidate list short and ordered, because Latin-1 and Windows-1252 overlap heavily and detection can’t always tell them apart.
Converting from Detected Encoding
Once you have a detected encoding, feed it straight into the conversion:
<?php
$detected_encoding = mb_detect_encoding( $string, ['UTF-8', 'ISO-8859-1', 'Windows-1252'], true );
if ( $detected_encoding ) {
$utf8_string = mb_convert_encoding( $string, 'UTF-8', $detected_encoding );
echo $utf8_string;
}
?>Which one should you reach for
Most days you want mb_convert_encoding(). Pick iconv() when you need its transliteration or the strict, fail-loud behavior.
- Reach for
mb_convert_encoding()for general multibyte work and when you want to hand it a list of candidate source encodings. It’s the closest match to the old functions and the one you’ll use most. - Reach for
iconv()when you want//TRANSLITto approximate unmappable characters,//IGNOREto drop them, or a hard failure on bad input.
A Fallback Helper
If you’re pulling data from sources you don’t control, wrap detection and conversion in one helper so callers don’t have to think about it:
/**
* Safely converts a given string to UTF-8 encoding.
*
* This function tries to detect the encoding of the input string using `mb_detect_encoding`
* and then attempts to convert it to UTF-8 using `mb_convert_encoding`. If encoding detection
* fails, it falls back to using `iconv` to convert the string from ISO-8859-1 to UTF-8.
*
* @param string $string The input string that needs to be converted to UTF-8.
*
* @return string The converted UTF-8 string, or an error message if the conversion fails.
*/
function safe_convert_to_utf8( $string ) {
// Try to detect the encoding first
$detected_encoding = mb_detect_encoding( $string, ['UTF-8', 'ISO-8859-1', 'Windows-1252'], true );
if ( $detected_encoding ) {
// Attempt to convert using mb_convert_encoding
$utf8_string = mb_convert_encoding( $string, 'UTF-8', $detected_encoding );
} else {
// Fallback to iconv if detection fails
$utf8_string = iconv( 'ISO-8859-1', 'UTF-8//IGNORE', $string );
}
if ( $utf8_string === false ) {
return "Conversion failed";
} else {
return $utf8_string;
}
}
// Example usage
$string = "This is a string that needs conversion.";
echo safe_convert_to_utf8( $string );
Wrapping Up
The deprecation isn’t PHP being fussy. utf8_encode() and utf8_decode() promised more than they delivered, and the name fooled people into shipping bugs. The replacements ask you to name your source encoding, which forces you to actually know your data.
For most of your old calls the migration is mechanical: utf8_encode($s) becomes mb_convert_encoding($s, 'UTF-8', 'ISO-8859-1'), and utf8_decode($s) becomes mb_convert_encoding($s, 'ISO-8859-1', 'UTF-8'). The work worth doing is checking whether your input was ever really Latin-1 in the first place. If it was Windows-1252 or something else, the old code was already quietly wrong, and this is your chance to fix it.
Test with real data, especially anything holding accented characters or currency symbols, and you’ll come out the other side with encoding handling you can trust.


