Preparación para entrevista de PHP

Preguntas de entrevista de PHP

15 preguntas seleccionadas para entrevistas backend de PHP, agrupadas por nivel de experiencia. Úsalas para repasar fundamentos, compromisos prácticos y el razonamiento de producción a nivel senior.

Iniciar una entrevista de IA de PHPNo se requiere tarjeta de crédito. 1 sesión gratuita disponible.
Práctica de entrevistas técnicas en inglésDiseñado para hablantes no nativos que desean practicar entrevistas técnicas en inglés.

Preguntas para Junior

1¿Cómo distingue PHP el tipado débil del estricto y qué cambia al habilitar declare(strict_types=1)?

PHP normalmente permite conversiones escalares compatibles para los tipos declarados de parámetros y retornos, como pasar una cadena numérica a un parámetro int. declare(strict_types=1) activa comprobaciones estrictas: un argumento o retorno incompatible provoca TypeError, aunque int sigue siendo válido para float. Para argumentos de funciones de usuario, manda el archivo que realiza la llamada; las comprobaciones del retorno dependen del archivo que define la función. Las llamadas realizadas desde funciones internas de PHP tienen particularidades y no dependen simplemente del strict_types del llamador externo. La directiva debe aparecer al principio de su archivo PHP. No convierte PHP en un lenguaje de tipado estático ni desactiva las conversiones de operadores en todo el lenguaje.

// THREE SEPARATE FILES: save each PHP block under its indicated filename.
// File: lib.php
<?php
function takesInt(int $x): int { return $x; }
?>

// File: weak.php
<?php
require __DIR__ . '/lib.php';
echo takesInt('123'); // 123: coercive caller
?>

// File: strict.php
<?php
declare(strict_types=1);
require __DIR__ . '/lib.php';
try {
    echo takesInt('123');
} catch (TypeError $e) {
    echo 'TypeError';
}
?>
Probar responder esta pregunta con un coach de IA

2Explica la diferencia entre == y === en PHP, incluidas las dificultades con cadenas numéricas, arrays, objetos e in_array().

En PHP, == realiza una comparación no estricta, con conversión de tipos cuando corresponde; === exige que coincidan los tipos y los valores, sin coerción de escalares. Las cadenas numéricas pueden compararse numéricamente, por lo que no conviene usar comparaciones no estrictas cuando se necesita una coincidencia exacta que tenga en cuenta el tipo. Dos arrays son iguales con == si contienen los mismos pares clave/valor según las reglas de comparación, independientemente del orden de los pares. Con === también deben coincidir el orden y los tipos de las claves y los valores. En objetos, == compara las propiedades de objetos de la misma clase; === exige que se trate de la misma instancia. in_array compara de forma no estricta por defecto; pasa true como tercer argumento para una comprobación estricta. El comportamiento de las comparaciones no estrictas entre números y cadenas cambió en PHP 8, así que evita basarte en ejemplos históricos de coerción sin indicar la versión.

<?php
var_dump(123 == '123');      // loose: numeric string is converted
var_dump(123 === '123');     // strict: int !== string

$values = [0, 1, 2];
var_dump(in_array('0', $values));       // loose
var_dump(in_array('0', $values, true)); // strict
Probar responder esta pregunta con un coach de IA

3¿En qué se diferencian mixed, void y never como tipos de retorno de PHP y qué garantías ofrecen?

mixed permite devolver cualquier valor, incluido null. void declara que una función no devuelve un valor significativo: puede llegar al final o usar return;, pero no puede devolver una expresión. Una llamada normal a una función void se evalúa como null. never, disponible desde PHP 8.1, significa que la función nunca devuelve el control normalmente a quien la llama. Puede lanzar una excepción, terminar la ejecución con exit o permanecer en un bucle infinito; no está obligada a terminar el proceso en algún momento. Una función never no puede contener una sentencia return. La diferencia fundamental es que una función void puede finalizar normalmente y permitir que continúe quien la llamó, mientras que una función never no tiene una continuación normal.

<?php
function parse(string $raw): mixed {
    return json_decode($raw, true);
}

function logMessage(string $message): void {
    error_log($message);
    return; // allowed
}

function fail(string $message): never {
    throw new RuntimeException($message);
}

logMessage('starting');
fail('stop');
echo 'unreachable';
Probar responder esta pregunta con un coach de IA

4¿Qué es iterable en PHP y cómo se relaciona con array, Traversable, Iterator y Generator?

iterable significa array|Traversable. Generator implementa Iterator, que extiende Traversable, por lo que cumple el tipo iterable. Las clases definidas por el usuario ofrecen el comportamiento de Traversable mediante Iterator o IteratorAggregate, en lugar de implementar Traversable directamente en una clase concreta. Iterator expone métodos que controlan la iteración; IteratorAggregate proporciona un iterador. Una función generadora que contiene yield devuelve un Generator. Hay una distinción importante: foreach también puede recorrer las propiedades visibles de un objeto ordinario que no implemente Traversable, pero eso no hace que dicho objeto cumpla una declaración de parámetro iterable.

function printItems(iterable $items): void {
    foreach ($items as $item) {
        echo $item, PHP_EOL;
    }
}

printItems(['a', 'b']);

function letters(): Generator {
    yield 'x';
    yield 'y';
}

printItems(letters());
Probar responder esta pregunta con un coach de IA

5¿Cuáles son las reglas de visibilidad public, protected y private en PHP y cómo interactúan con la herencia y los traits?

Los miembros public son accesibles desde cualquier lugar donde se pueda acceder al objeto o a la clase. Los miembros protected son accesibles dentro de la clase que los declara y de su jerarquía relacionada de clases padre e hijas, pero no desde código externo arbitrario. Los miembros private son accesibles dentro de la clase que los declara; una subclase no puede usar un miembro privado del padre como parte de su interfaz heredada. Un método ordinario que sobrescribe otro no puede reducir la visibilidad heredada, aunque los constructores tienen reglas especiales de herencia y no deben tratarse como un ejemplo incondicional de esa regla. Los miembros de un trait se incorporan a la clase que lo usa y siguen sus reglas de visibilidad. La adaptación as puede crear un alias, ajustar la visibilidad de un método del trait o ambas cosas; añadir un alias no elimina por sí mismo el nombre original.

<?php
class Base {
    public function a() {}
    protected function b() {}
    private function c() {}
}

trait Logs {
    public function log(string $message): void {}
}

class Child extends Base {
    use Logs { log as protected writeLog; }

    public function test(): void {
        $this->a();              // OK
        $this->b();              // OK
        // $this->c();           // Not accessible: private to Base
        $this->writeLog('ok');   // OK inside class, protected alias
    }
}

$child = new Child();
$child->a();
// $child->b();           // Not accessible from outside
// $child->writeLog('x'); // Not accessible from outside
Probar responder esta pregunta con un coach de IA

6¿Cómo funcionan los traits en PHP y cómo se resuelven los conflictos cuando varios traits definen el mismo método?

Los traits permiten reutilizar código de forma horizontal: una clase importa sus miembros con use, y un trait no puede instanciarse por sí solo. Un método declarado en la clase tiene prioridad sobre los métodos de los traits, que a su vez tienen prioridad sobre los métodos heredados del padre. Si varios traits proporcionan el mismo método y un método de la clase no resuelve ya el conflicto, utiliza insteadof para elegir la implementación. Utiliza as para dar a un método de un trait un nombre adicional y, opcionalmente, otra visibilidad. Un alias no cambia ni elimina el nombre original del método. Resolver las colisiones y mantener clara la interfaz pública resultante son responsabilidades distintas.

<?php
trait JsonLogger {
    public function log(string $message): void {
        echo "json:$message\n";
    }
}

trait FileLogger {
    public function log(string $message): void {
        echo "file:$message\n";
    }
}

class Service {
    use JsonLogger, FileLogger {
        JsonLogger::log insteadof FileLogger;
        FileLogger::log as logToFile;
        JsonLogger::log as protected logJson;
    }
}

$service = new Service();
$service->log('hello');
$service->logToFile('hello');
// $service->logJson('hello'); // not accessible from outside; protected alias
Probar responder esta pregunta con un coach de IA

7¿Qué son las enums de PHP, incluyendo las enums puras y las enums respaldadas, y cómo mejoran el modelado de dominio?

Las enums de PHP definen un conjunto cerrado de casos con nombre para un tipo, como estados de pedido o roles de usuario. Las enums puras solo tienen casos con nombre; las enums respaldadas asignan a cada caso un valor único de cadena (string) o entero (int), útil para el almacenamiento, las API (Application Programming Interface) o la interoperabilidad. Las enums mejoran el modelado de dominio al reemplazar cadenas (strings) sin procesar frágiles, enteros o constantes con valores seguros en cuanto a tipos y autodocumentados. Las enums de PHP también pueden tener métodos e implementar interfaces; todas las enums admiten la lista de sus casos, y las enums respaldadas admiten la conversión hacia y desde sus valores escalares.

enum OrderState
{
    case Draft;
    case Paid;
    case Shipped;
}

enum PaymentStatus: string
{
    case Pending = 'pending';
    case Complete = 'complete';
    case Failed = 'failed';
}

$status = PaymentStatus::tryFrom('complete');
var_dump($status === PaymentStatus::Complete);
Probar responder esta pregunta con un coach de IA

Preguntas para Middle

8¿Cómo expresan los tipos escalares, anulables, de unión, de intersección y DNF los contratos de las interfaces de PHP, y cuándo debe utilizarse cada uno?

Las declaraciones de PHP describen los valores aceptados y devueltos. Las declaraciones escalares, como int, float, string y bool, seleccionan una categoría de valores; que se permita la coerción entre escalares compatibles también depende de strict_types y del contexto. ?T o T|null admite null. Las uniones, disponibles desde PHP 8.0, admiten alternativas como int|string. Las intersecciones puras, disponibles desde PHP 8.1, exigen que un objeto cumpla todos los tipos de clase o interfaz indicados, como Cacheable&JsonSerializable; int&string no es una intersección válida. Los tipos DNF (forma normal disyuntiva), disponibles desde PHP 8.2, combinan alternativas e intersecciones entre paréntesis, por ejemplo (Cacheable&JsonSerializable)|string. Elige el contrato más acotado y comprensible que represente las entradas válidas, sin añadir alternativas innecesarias.

<?php
interface Cacheable {}

class Report implements Cacheable, JsonSerializable {
    public function jsonSerialize(): mixed { return []; }
}

function findName(int $id): ?string {
    return $id > 0 ? 'Alice' : null;
}

function normalize(int|string $id): string {
    return (string) $id;
}

function store(Cacheable&JsonSerializable $item): void {
    // must satisfy both interfaces
}

function handle((Cacheable&JsonSerializable)|string $input): void {
    // either an object satisfying both contracts, or a string
}
Probar responder esta pregunta con un coach de IA

9¿Cuál es la diferencia entre callable, Closure, los invocables de primera clase, los objetos invocables y los arrays callable en PHP?

callable describe valores que pueden invocarse, incluidas cadenas con nombres de funciones, objetos Closure, referencias a métodos accesibles y objetos con __invoke. Closure es un objeto invocable concreto: puede proceder de una función anónima o de una función o método con nombre convertido en Closure. La sintaxis de invocables de primera clase, como strlen(...) o $object->method(...), crea un Closure sin invocar el destino. Un array callable [$object, 'method'] hace referencia a un método de instancia accesible; [ClassName::class, 'method'] exige un método estático adecuado y accesible en las versiones modernas de PHP. is_callable comprueba si el valor es invocable en el ámbito correspondiente. callable es un contrato amplio, mientras que Closure es una representación específica.

class Greeter {
    public function hello(string $name): string { return "Hello $name"; }
    public function __invoke(string $name): string { return "Hi $name"; }
}

$g = new Greeter();

$closure = fn(string $name) => "Hey $name";
$arrayCallable = [$g, 'hello'];
$firstClass = $g->hello(...);
$invokable = $g;

foreach ([$closure, $arrayCallable, $firstClass, $invokable] as $cb) {
    echo $cb('Sam'), PHP_EOL;
}
Probar responder esta pregunta con un coach de IA

10¿Cómo maneja PHP el desbordamiento de enteros, la precisión de punto flotante y los cálculos decimales exactos?

Los enteros de PHP tienen un tamaño que depende de la plataforma, con límites expuestos por constantes como `PHP_INT_MAX`. PHP no mantiene automáticamente los resultados de enteros nativos como enteros de precisión arbitraria después de un desbordamiento; la aritmética de enteros con desbordamiento puede convertirse en un `float` o perder la semántica exacta de los enteros. Los números flotantes de PHP son valores de punto flotante binarios, por lo que muchas fracciones decimales, como `0.1`, son aproximadas. Para cálculos decimales exactos o de dinero, evite los flotantes y utilice un enfoque exacto como unidades menores enteras cuando sea apropiado, o herramientas decimales de precisión fija/arbitraria como BCMath. GMP es útil para la aritmética de enteros de precisión arbitraria.

var_dump(0.1 + 0.2 === 0.3);
printf('%.17f\n', 0.1 + 0.2);
Probar responder esta pregunta con un coach de IA

11Explique las referencias de PHP (`&`), la copia en escritura (*copy-on-write*) y cómo las referencias pueden crear efectos secundarios inesperados.

Las variables de PHP están representadas internamente por *zvals*. Para valores como arrays y strings, PHP utiliza la copia en escritura (*copy-on-write*): asignar `$b = $a` usualmente comparte el mismo valor subyacente hasta que una variable es modificada, momento en el cual PHP la separa/copia. Una referencia creada con `&` convierte las variables en alias del mismo contenedor, por lo que cambiar una cambia la otra y puede eludir el comportamiento de separación que se espera de una asignación normal. Pasar por referencia permite que una función modifique la variable del llamador. Las referencias pueden causar efectos secundarios sorprendentes, especialmente con `foreach ($array as &$value)`: la variable del bucle sigue siendo una referencia al último elemento después del bucle, por lo que reutilizarla más tarde puede sobrescribir accidentalmente ese elemento a menos que se use `unset($value)`.

$a = [1, 2];
$b = $a;      // shared until modification
$b[0] = 99;   // separates; $a is unchanged
var_dump($a, $b);

$c =& $a;     // alias/reference
$c[0] = 42;   // modifies $a too
var_dump($a, $c);
Probar responder esta pregunta con un coach de IA

12¿Cómo funcionan los manejadores de objetos, la asignación, la clonación (`clone`), `__clone`, la copia superficial y la copia profunda en PHP?

En PHP, una variable de objeto almacena una referencia a un objeto, no una copia completa del objeto. Asignar `$b = $a` copia la referencia, por lo que ambas variables se refieren al mismo objeto; modificar el objeto a través de cualquiera de las variables es visible a través de la otra, sin usar `&`. `clone $a` crea una nueva instancia de objeto con una copia superficial de las propiedades del objeto original. Si una propiedad es en sí misma un objeto, la propiedad del objeto clonado seguirá apuntando al mismo objeto anidado a menos que lo clones explícitamente. Se llama a `__clone()` en el nuevo objeto después de la copia superficial y es el lugar para restablecer IDs, desvincular recursos o realizar una copia profunda de objetos anidados. Una copia profunda significa copiar recursivamente los objetos mutables anidados que no deben compartirse.

class Address {
    public function __construct(public string $city) {}
}

class User {
    public function __construct(public string $name, public Address $address) {}

    public function __clone() {
        $this->address = clone $this->address; // make nested object independent
    }
}

$u1 = new User('Ann', new Address('Paris'));
$u2 = $u1;          // same object handle
$u2->name = 'Beth';
echo $u1->name, PHP_EOL;

$u3 = clone $u1;    // new User object; __clone deep-clones address
$u3->address->city = 'Rome';
echo $u1->address->city, PHP_EOL;
echo $u3->address->city, PHP_EOL;
Probar responder esta pregunta con un coach de IA

Preguntas para Senior

13¿Cómo solucionarías el agotamiento de conexiones a la base de datos y diseñarías la gestión de conexiones para PHP-FPM y workers de larga duración?

Para solucionar el agotamiento de conexiones, compararía los límites de conexiones de la base de datos y las sesiones actuales con la concurrencia total de PHP: cada proceso hijo activo de PHP-FPM (FastCGI Process Manager) puede mantener su propia conexión a la base de datos, y múltiples pools de aplicaciones, hosts, tareas cron, workers de cola, migraciones y despliegues multiplican la demanda. Inspeccionaría las vistas de sesión de la base de datos, los estados de conexión, las sesiones inactivas en transacción, las consultas lentas, las esperas de bloqueo, los registros de errores, las métricas del pool y los cambios recientes de tráfico o de número de workers. Para PHP-FPM, mantendría `max_children` y la concurrencia por host dentro del presupuesto de la base de datos, usaría transacciones/conexiones de ámbito de solicitud cortas, confirmaría (commit) o revertiría (roll back) rápidamente, y evitaría mantener conexiones durante llamadas externas lentas. Las conexiones PDO persistentes pueden reducir la sobrecarga de conexión, pero pueden mantener muchas conexiones inactivas por proceso FPM y retener el estado de la sesión, por lo que requieren precaución. Un 'pooler' o 'proxy' como PgBouncer puede multiplexar muchos clientes PHP en menos conexiones de servidor de base de datos cuando la aplicación es compatible. Los workers de larga duración necesitan un manejo explícito del ciclo de vida de la conexión: comprobaciones de estado, reconexión después de tiempos de espera de inactividad/reinicios/conmutaciones por error (failover)/bifurcaciones (forks), restablecimiento del estado de la sesión y reintentar solo el trabajo seguro o idempotente.

Connection-budget example:
- 6 application servers
- 2 PHP-FPM pools on EACH server
- pm.max_children = 40 for EACH pool
- Assume at most 1 database connection per child

Potential request connections = 6 * 2 * 40 = 480.
If 40 were the TOTAL children per server across both pools, it would be 240.
Budget separately for queue workers, cron, deployments and administrators.
A limit of 300 database connections cannot accommodate 480 simultaneous connections.
Probar responder esta pregunta con un coach de IA

14¿Cómo gestionarías las réplicas de lectura, la separación de lecturas y escrituras, el retraso de replicación y la conmutación por error de la base de datos principal en PHP?

Separa las conexiones a la principal y a las réplicas según la semántica de la operación, no solo según si el SQL empieza por SELECT: las lecturas con bloqueo, las escrituras y las transacciones que requieren la consistencia de la principal deben usarla. Las transacciones de solo lectura pueden usar una réplica adecuada si sus requisitos de consistencia lo permiten. La replicación asíncrona puede retrasarse. La opción sticky de Laravel dirige las lecturas posteriores a la conexión de escritura durante la solicitud actual después de una escritura; mantener esa afinidad durante un intervalo fijo entre solicitudes solo mitiga el problema, sin garantizar incondicionalmente la lectura de las propias escrituras. Para garantizarla, lee de la principal o espera a que una réplica haya aplicado la posición de escritura requerida, con un tiempo límite y recurso a la principal si se supera. El comportamiento de los proxies varía: RDS Proxy utiliza puntos de conexión separados para lectores y escritores y no es un separador automático general de lecturas y escrituras por sentencia. Durante una conmutación por error, descarta las conexiones obsoletas y vuelve a conectar con una espera progresiva acotada. Si se pierde la respuesta de una confirmación, el resultado es ambiguo: no repitas a ciegas escrituras no idempotentes; usa claves de idempotencia o comprueba primero el resultado.

<?php
// config/database.php
return [
    'mysql' => [
        'read' => [
            'host' => [env('DB_REPLICA_HOST_1'), env('DB_REPLICA_HOST_2')],
        ],
        'write' => [
            'host' => [env('DB_PRIMARY_HOST')],
        ],
        'sticky'    => true,
        'driver'    => 'mysql',
        'database'  => env('DB_DATABASE'),
        'username'  => env('DB_USERNAME'),
        'password'  => env('DB_PASSWORD'),
        'charset'   => 'utf8mb4',
        'collation' => 'utf8mb4_unicode_ci',
    ],
];
Probar responder esta pregunta con un coach de IA

15¿Cómo se despliegan de forma segura las migraciones de bases de datos y los cambios de esquema en aplicaciones PHP sin interrupciones o con interrupciones mínimas?

Utiliza cambios de expansión y contracción que toleren las versiones antigua y nueva de la aplicación durante un despliegue gradual. Primero añade estructuras compatibles y después despliega código capaz de utilizar ambas representaciones. Si necesitas escritura doble, mantenla coherente y diseña procesos de carga de datos históricos reanudables y con ritmo limitado que no sobrescriban escrituras más recientes. Valida los datos copiados y cambia las lecturas antes de retirar las rutas de código antiguas. El DDL (lenguaje de definición de datos) en línea reduce ciertos riesgos de bloqueo, pero no está siempre libre de bloqueos: examina los bloqueos específicos de la base de datos y su versión, las restricciones de transacciones, el consumo de recursos y el impacto en la replicación. Por ejemplo, CREATE INDEX CONCURRENTLY de PostgreSQL tiene restricciones y puede dejar un índice no válido si falla. Elimina las columnas antiguas solo cuando ya no queden instancias antiguas de la aplicación y haya terminado el plazo requerido para poder revertir. Tras cambios destructivos de contracción, volver a un código que necesita los datos eliminados deja de ser automáticamente seguro.

<?php
// Step 1: Migration (Expand) -> Add new column as nullable
Schema::table('users', function (Blueprint $table) {
    $table->string('full_name')->nullable()->after('name');
});

// Step 2: Code deployment -> Write to both, read with fallback
class User extends Model {
    public function setNameAttribute($value) {
        $this->attributes['name'] = $value;
        $this->attributes['full_name'] = $value; // Dual write
    }
}

// Step 3: Background worker backfills existing rows in small batches
// Step 4: Code deployment -> Read/write exclusively from 'full_name'
// Step 5: Migration (Contract) -> Drop legacy 'name' column
Probar responder esta pregunta con un coach de IA