strpos en PHP: buscar texto en cadenas sin confundir el cero

strpos() devuelve la posición de la primera ocurrencia de una subcadena dentro de otra. El problema viene cuando esa posición es 0 (el texto buscado está justo al principio), porque en PHP el entero 0 es falso en comparaciones débiles. Si no tienes esto claro, la mitad de tus búsquedas van a romperse sin que el intérprete te avise de nada.

Aquí vemos también stripos() para búsquedas sin distinguir mayúsculas, strrpos() para encontrar la última ocurrencia y cuándo tiene más sentido pasarse a str_contains() si ya corres PHP 8.

Sintaxis y parámetros

<?php
strpos(string $haystack, string $needle, int $offset = 0): int|false
?>

$haystack es la cadena donde buscas y $needle lo que buscas. Si no hay coincidencia devuelve false; si la hay, devuelve un entero con la posición del primer carácter (empezando desde 0). El tercer parámetro, $offset, es opcional e indica a PHP que empiece a buscar desde esa posición.

El antipatrón clásico: usar ! con strpos

Este error vive en proyectos legacy desde hace décadas:

<?php
// MAL: strpos devuelve 0 cuando 'https' está al principio.
// 0 es falsy, así que !strpos() es true y el mensaje se muestra cuando no debería.
$url = 'https://tienda.ejemplo.com/producto/zapatillas';

if (!strpos($url, 'https')) {
    echo "La URL no usa HTTPS"; // Se ejecuta aunque SÍ usa HTTPS
}
?>

La posición 0 no es un error, simplemente indica que el texto está en el primer carácter. Para distinguirla de false necesitas igualdad estricta:

<?php
$url = 'https://tienda.ejemplo.com/producto/zapatillas';

if (strpos($url, 'https') === false) {
    echo "La URL no usa HTTPS";
} else {
    $posicion = strpos($url, 'https');
    echo "HTTPS encontrado en posición: $posicion"; // posición: 0
}
?>

El operador === compara valor y tipo, así que 0 === false devuelve false pero 0 == false devuelve true. Una sola letra entre == y === cambia el resultado por completo.

Buscar a partir de una posición con $offset

Cuando procesas rutas de fichero puede que necesites localizar el segundo punto para distinguir vendor.bundle.min.js de vendor.js. Con $offset puedes saltar la primera coincidencia:

<?php
$fichero = 'scripts/vendor.bundle.min.js';

$primerPunto  = strpos($fichero, '.');
$segundoPunto = strpos($fichero, '.', $primerPunto + 1);

echo "Primer punto en: $primerPunton";   // 14
echo "Segundo punto en: $segundoPunton"; // 21
?>

El tercer argumento desplaza el punto de inicio sin modificar los índices devueltos; siguen siendo relativos al inicio de la cadena original.

Última ocurrencia con strrpos()

Para extraer el nombre de fichero de una ruta completa lo más directo es buscar la última barra con strrpos():

<?php
$ruta = '/var/www/html/uploads/2024/junio/imagen-portada.jpg';

$ultimaBarra   = strrpos($ruta, '/');
$nombreFichero = substr($ruta, $ultimaBarra + 1);

echo $nombreFichero; // imagen-portada.jpg
?>

El valor de retorno es idéntico al de strpos(), entero o false, así que el === false sigue siendo obligatorio aquí también.

Búsqueda sin distinguir mayúsculas con stripos()

En un buscador interno el usuario puede escribir "PHP", "php" o "Php". stripos() resuelve eso sin que tengas que pasar la cadena por strtolower() antes:

<?php
$titulos = [
    'Introducción a PHP moderno',
    'Patrones de diseño en JavaScript',
    'APIs REST con Python y Flask',
    'Testing en PHPUnit: guía práctica',
];

foreach ($titulos as $titulo) {
    if (stripos($titulo, 'php') !== false) {
        echo "Relacionado con PHP: $titulon";
    }
}
// Relacionado con PHP: Introducción a PHP moderno
// Relacionado con PHP: Testing en PHPUnit: guía práctica
?>

Uso !== false porque quiero actuar cuando la cadena aparece; tanto === false como !== false son válidos según el caso. Lo que no debes usar nunca es el operador ! solo.

Cuándo str_contains() es mejor opción

PHP 8.0 añadió str_contains(), que sirve cuando solo necesitas saber si una cadena contiene otra sin que te importe la posición:

<?php
$email = '[email protected]';

// Con strpos: necesario en PHP 7, o cuando necesites la posición
if (strpos($email, '@') !== false) {
    echo "El email tiene arroba";
}

// Con str_contains: PHP 8+, más claro de leer
if (str_contains($email, '@')) {
    echo "El email tiene arroba";
}
?>

str_contains() devuelve un booleano real, así que el problema del cero desaparece. Si mantienes PHP 7 en el proyecto, strpos() !== false sigue siendo la opción correcta; en PHP 8 puedes elegir según lo que necesites.

Un detalle que pasa desapercibido: el needle vacío

Si el needle puede llegar vacío (por ejemplo, desde un campo de formulario sin validar), recuerda que strpos($cadena, '') lanza un ValueError en PHP 8, mientras que en PHP 7 devolvía false. Valida el needle antes de llamar a la función si no controlas su origen:

<?php
$termino = trim($_GET['q'] ?? '');

if ($termino !== '' && strpos($descripcion, $termino) !== false) {
    // solo buscamos si hay algo que buscar
    echo "Encontrado en la descripción";
}
?>

La documentación oficial detalla el historial de cambios entre versiones; merece la pena echarle un vistazo si trabajas con bases de código que tienen que correr en PHP 7 y 8 a la vez.

COMPARTE ESTE ARTÍCULO

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