La cadena vacía en PHP: usos correctos, antipatrones y alternativas

La cadena vacía ("") en PHP tiene más usos de los que parece, y no todos son correctos. Este tutorial recorre siete situaciones reales —valor por defecto, centinela de error, cast implícito, acumulador en loop, comparaciones con ==, funciones que no la aceptan— y muestra para cada una el antipatrón habitual y la alternativa limpia con código listo para copiar.
				<?php

/**
 * La cadena vacía en PHP: usos correctos, antipatrones y alternativas
 *
 * PHP trata "" como un string válido en casi todos los contextos, pero
 * eso no significa que sea el valor correcto en todos ellos. Este archivo
 * recorre los siete usos más comunes —algunos legítimos, otros peligrosos—
 * y muestra la alternativa correcta en cada caso.
 */

declare(strict_types=1);


// ============================================================
// 1. VALOR POR DEFECTO EN PROPIEDADES Y PARÁMETROS
// ============================================================

// CORRECTO: "" como default cuando el campo es siempre un string
// y "vacío" tiene sentido de negocio (p.ej. un nickname opcional).
class UserConDefault
{
    public string $nickname = '';

    public function hasNickname(): bool
    {
        return $this->nickname !== '';
    }
}

// MEJOR cuando necesitas distinguir "nunca asignado" de "vacío deliberado".
// Si el usuario no rellenó el campo, $nickname es null.
// Si lo dejó en blanco a propósito, $nickname es ''.
class UserConNullable
{
    public ?string $nickname = null;

    public function hasNickname(): bool
    {
        return $this->nickname !== null && $this->nickname !== '';
    }
}

// Mismo criterio en parámetros de función:
function saludar(string $nombre = ''): string
{
    return $nombre !== '' ? "Hola, $nombre." : 'Hola, desconocido.';
}

function saludarNullable(?string $nombre = null): string
{
    if ($nombre === null) {
        return 'Nombre no proporcionado.';
    }
    return $nombre !== '' ? "Hola, $nombre." : 'El nombre está vacío.';
}

echo saludar();            // Hola, desconocido.
echo saludar('Ana');       // Hola, Ana.
echo saludarNullable();    // Nombre no proporcionado.
echo saludarNullable('');  // El nombre está vacío.


// ============================================================
// 2. CENTINELA DE ERROR: "" PARA INDICAR FALLO — ANTIPATRÓN
// ============================================================

// ANTIPATRÓN: la función devuelve "" cuando falla y el token real
// cuando tiene éxito. Pero un token puede ser legítimamente vacío,
// o el desarrollador puede olvidar comprobar el resultado.
function extraerToken_mal(string $header): string
{
    if (!str_contains($header, 'Bearer ')) {
        return ''; // ¿fallo o token vacío válido?
    }
    return substr($header, 7);
}

$token = extraerToken_mal('Basic abc123');
if ($token === '') {
    // Ambigüedad: ¿la cabecera no tenía Bearer, o el token era ''?
}

// CORRECTO: lanzar excepción cuando la entrada es inválida,
// o devolver null para indicar "ausencia de resultado".
function extraerToken(string $header): ?string
{
    if (!str_contains($header, 'Bearer ')) {
        return null; // explícito: no hay token
    }
    $token = substr($header, 7);
    return $token !== '' ? $token : null;
}

$token = extraerToken('Authorization: Bearer eyJ0...');
if ($token === null) {
    // Aquí sí sabemos con certeza que no había token.
}


// ============================================================
// 3. CENTINELA DE ÉXITO INVERTIDO: "" = OK — ANTIPATRÓN
// ============================================================

// ANTIPATRÓN: la función devuelve "" si todo está bien
// y el mensaje de error si algo falla. Semántica invertida.
function validarEmail_mal(string $email): string
{
    if (!filter_var($email, FILTER_VALIDATE_EMAIL)) {
        return 'Email no válido.';
    }
    return ''; // silencio = éxito, confuso para el lector
}

$error = validarEmail_mal($input ?? '');
if ($error !== '') {
    echo $error;
}

// CORRECTO: separar el resultado booleano del mensaje de error.
// Opción A — bool + parámetro por referencia para el mensaje:
function validarEmail(string $email, string &$error = ''): bool
{
    if (!filter_var($email, FILTER_VALIDATE_EMAIL)) {
        $error = 'Email no válido.';
        return false;
    }
    return true;
}

if (!validarEmail($input ?? '', $error)) {
    echo $error;
}

// Opción B — lanzar excepción (preferible en flujos críticos):
function validarEmailOException(string $email): void
{
    if (!filter_var($email, FILTER_VALIDATE_EMAIL)) {
        throw new InvalidArgumentException("Email no válido: $email");
    }
}

try {
    validarEmailOException('no-es-un-email');
} catch (InvalidArgumentException $e) {
    echo $e->getMessage();
}


// ============================================================
// 4. CAST IMPLÍCITO CON '' . $valor — FRÁGIL
// ============================================================

// ANTIPATRÓN: usar "" como operador de conversión de tipo.
// Con objetos sin __toString() lanza un TypeError en PHP 8+.
class SinToString {}

$obj = new SinToString();
// $texto = '' . $obj; // TypeError: Object of class SinToString could not be converted to string

// Con valores primitivos funciona, pero es opaco:
$numero = 42;
$texto  = '' . $numero; // '42' — funciona pero no es obvio

// CORRECTO: cast explícito siempre.
$texto = (string) $numero;  // '42'
$texto = (string) null;     // ''
$texto = (string) false;    // ''
$texto = (string) true;     // '1'

// Con objetos: garantizar que implementan Stringable o usar un método dedicado.
class Producto implements Stringable
{
    public function __construct(private string $nombre) {}

    public function __toString(): string
    {
        return $this->nombre;
    }
}

$p = new Producto('Teclado');
echo (string) $p; // 'Teclado'


// ============================================================
// 5. ACUMULADOR EN LOOP CON .= — INEFICIENTE
// ============================================================

$items = ['manzana', 'pera', 'naranja', 'kiwi'];

// ANTIPATRÓN: concatenar en cada iteración.
// PHP crea una nueva cadena en memoria en cada vuelta.
$resultado = '';
foreach ($items as $item) {
    $resultado .= strtoupper($item) . ', ';
}
echo rtrim($resultado, ', '); // hay que limpiar la coma final a mano

// CORRECTO: acumular en array y unir al final.
// Una sola operación de memoria, separador explícito, sin rtrim.
$partes = [];
foreach ($items as $item) {
    $partes[] = strtoupper($item);
}
echo implode(', ', $partes); // MANZANA, PERA, NARANJA, KIWI

// BONUS: para salidas HTML complejas con lógica condicional dentro del loop,
// ob_start() evita la concatenación sin perder claridad:
ob_start();
foreach ($items as $i => $item) {
    if ($i > 0) {
        echo ', ';
    }
    echo '' . htmlspecialchars(strtoupper($item)) . '';
}
$html = ob_get_clean();
echo $html;


// ============================================================
// 6. COMPARACIONES PELIGROSAS CON == — TRAMPA CLÁSICA
// ============================================================

// PHP tiene comparación débil (==) y estricta (===).
// Con "" los resultados de == sorprenden:

var_dump('' == false);  // true  — "" es falsy
var_dump('' == null);   // true  — "" es falsy
var_dump('' == 0);      // false en PHP 8 (era true en PHP 7!)
var_dump('' == '0');    // false
var_dump('' == []);     // false

// Tabla de valores que == trata como iguales a "":
// "" == false  ? true
// "" == null   ? true
// "" == 0      ? FALSE en PHP 8 (cambio importante respecto a PHP 7)

// REGLA: usar siempre === cuando se compara con strings.
var_dump('' === false); // false — correcto
var_dump('' === null);  // false — correcto
var_dump('' === 0);     // false — correcto
var_dump('' === '');    // true  — el único caso esperado

// Ejemplo real donde == falla silenciosamente:
function buscarUsuario_mal(string $id): ?string
{
    $usuarios = ['admin' => 'Ana', '' => 'Fantasma'];
    return $usuarios[$id] ?? null;
}

$id_recibido = null; // viene del request sin validar
// Con ==, null == '' es true, por lo que podrías servir el usuario 'Fantasma'.
// Con ===, null !== '', y el acceso se deniega correctamente.
if ($id_recibido === '') {
    echo 'ID vacío';
}


// ============================================================
// 7. FUNCIONES QUE NO ACEPTAN CADENA VACÍA
// ============================================================

// ord() necesita exactamente un carácter.
// Desde PHP 8.5 emite E_WARNING con "".
// En versiones anteriores devolvía 0 sin aviso.

// ANTIPATRÓN:
$codigo = ord(''); // Warning en PHP 8.5+, resultado 0 (falso negativo)

// CORRECTO: validar antes de llamar.
function codigoAscii(string $char): int
{
    if (strlen($char) !== 1) {
        throw new LengthException(
            'Se esperaba exactamente un carácter, recibido: ' . strlen($char)
        );
    }
    return ord($char);
}

echo codigoAscii('A'); // 65
// codigoAscii('');    // LengthException

// Otras funciones que asumen string no vacío:
// - mb_substr_count('', 'a') ? 0 (no falla, pero el resultado puede confundir)
// - preg_match('/^.+$/', '') ? 0 (no coincide, resultado correcto)
// - base64_decode('')        ? '' (silencioso, pero ¿esperabas eso?)
// - json_decode('')          ? null + JSON_ERROR_SYNTAX (falla silenciosamente sin verificar json_last_error())

// PATRÓN DEFENSIVO para cualquier función que asuma string no vacío:
function procesarCadena(string $input): string
{
    if ($input === '') {
        throw new InvalidArgumentException('La cadena no puede estar vacía.');
    }
    // ... lógica real ...
    return strtoupper($input);
}

			
Descargar adjuntos
COMPARTE ESTE TUTORIAL

COMPARTIR EN FACEBOOK
COMPARTIR EN TWITTER
COMPARTIR EN LINKEDIN
COMPARTIR EN WHATSAPP