Ir al contenido principal

Lección 4 · Autoevaluación

Cómo se usa

Esta página cubre todo lo de la lección 4: rutas y permisos, lectura y escritura de archivos, la importación del catálogo, la subida de imágenes, los errores de PHP y las excepciones.

Hay 24 preguntas de opción múltiple y 10 ejercicios. La corrección de las preguntas es inmediata: al elegir una opción se marca la correcta y aparece la explicación.

Los ejercicios de esta lección se prueban con archivos de verdad: conviene armar un CSV con una línea rota a propósito y ver qué informa la importación. Ahí es donde se entiende por qué el número de línea y el continue importan tanto.

Preguntas

Agrupadas por tema, en el orden en que se dieron en la lección. Una sola opción es correcta en cada una.

Todavía sin contestar · 24 preguntas

Rutas, permisos y lectura

1. ¿Por qué conviene armar la ruta de un archivo con __DIR__?

Elegí una opción para verla

Una ruta relativa como 'datos/catalogo.csv' se resuelve contra el directorio de trabajo del proceso, que cambia según cómo se llame al script.

Con __DIR__ la ruta es absoluta en ejecución y relativa al archivo al escribirla, así que el proyecto se puede mover sin tocar nada.

2. El sistema devuelve «Permission denied» al guardar un archivo. ¿Cuál es la causa?

Elegí una opción para verla

PHP corre como el usuario del servidor web —www-data en Debian y Ubuntu—, y para escribir necesita permiso sobre el archivo y sobre la carpeta.

Se corrige dándole la carpeta a ese usuario. Lo que no se hace nunca es chmod 777: deja el archivo escribible por cualquiera.

3. ¿Qué devuelve file_get_contents() si el archivo no existe?

Elegí una opción para verla

false, y además un warning. Por eso la comprobación correcta es === false: un archivo vacío y un archivo que no se pudo abrir son cosas distintas.

4. ¿Para qué sirven las banderas FILE_IGNORE_NEW_LINES y FILE_SKIP_EMPTY_LINES en file()?

Elegí una opción para verla

Sin FILE_IGNORE_NEW_LINES, cada elemento del array termina en \n, y eso arruina las comparaciones con ===: "1042\n" !== "1042".

La segunda descarta las líneas vacías, que en un archivo generado a mano suelen sobrar.

5. ¿Cuándo conviene fgets() en lugar de file_get_contents()?

Elegí una opción para verla

Con fgets() se lee línea por línea y el archivo grande nunca se carga entero en memoria. file_get_contents() devuelve todo de una vez, y sirve para archivos chicos: una plantilla, un JSON de configuración.

Escribir e importar

6. ¿Qué hace LOCK_EX al escribir un archivo?

Elegí una opción para verla

Bloquea el archivo durante la escritura. Sin eso, dos peticiones simultáneas pueden escribir al mismo tiempo y dejar una línea mezclada con otra.

Se usa junto con FILE_APPEND en cualquier archivo de registro.

7. ¿Qué bandera hay que usar para agregar una línea al final de un archivo de registro sin borrar lo anterior?

Elegí una opción para verla

FILE_APPEND. Sin esa bandera, file_put_contents() reemplaza todo el contenido del archivo.

PHP_EOL es solo el salto de línea del sistema; LOCK_EX es el bloqueo.

8. Antes de desestructurar [$codigo, $nombre, $precio] = $campos en una importación, ¿qué hay que comprobar?

Elegí una opción para verla

Que count($campos) sea el esperado. Si la línea tiene menos campos, la desestructuración avisa; si tiene más, el sobrante se pierde sin que nadie lo note.

En una importación de 200 líneas, ese chequeo es la diferencia entre detectar el problema y publicar datos incompletos.

9. Una línea del CSV tiene el precio inválido. ¿Qué corresponde hacer?

Elegí una opción para verla

Saltarla con continue y anotarla. Una línea mala no tiene que invalidar las otras 199.

Y el informe tiene que decir en qué línea fue: es lo único que le permite a quien tiene el archivo encontrarla.

10. El archivo viene de Windows y cada campo trae un carácter invisible al final. ¿Cuál es la solución?

Elegí una opción para verla

Las líneas de Windows terminan en \r\n, así que el último campo de cada fila queda con un \r. Se ve como un espacio invisible y rompe las comparaciones.

array_map('trim', $campos) resuelve el problema de una vez.

Subir archivos

11. ¿Qué le falta a un formulario que sube un archivo si $_FILES llega vacío?

Elegí una opción para verla

Sin enctype="multipart/form-data" el archivo no viaja: en el servidor llega solo el nombre como texto.

Es el primer error de todo el que empieza con subidas, y el formulario se ve perfecto igual.

12. ¿Cuál de los datos de $_FILES es confiable?

Elegí una opción para verla

size lo mide el servidor, así que ese dato sirve. name y type los manda el cliente y se pueden falsear: alguien puede subir un .php diciendo que es image/jpeg.

El tipo real se comprueba leyendo el contenido con getimagesize() o finfo_file().

13. ¿Para qué sirve is_uploaded_file()?

Elegí una opción para verla

Confirma que la ruta es de un archivo subido en esta petición y no cualquier archivo del servidor. Sin esa comprobación, alguien podría hacer que el programa copie /etc/passwd.

Se usa junto con move_uploaded_file(), que hace una verificación parecida por dentro.

14. ¿Por qué el nombre del archivo guardado lo decide el servidor?

Elegí una opción para verla

El nombre original lo escribió el cliente: puede tener ../ para salir de la carpeta, caracteres raros o una extensión ejecutable.

Generar el nombre en el servidor —por ejemplo, el código del producto más la extensión deducida del contenido— elimina el problema.

15. ¿Por qué no se aceptan SVG en una subida de imágenes de usuario?

Elegí una opción para verla

Un SVG es un documento con etiquetas y puede incluir <script>. Al abrirlo, el navegador lo ejecuta en el contexto del sitio.

Para fotos de producto alcanza con JPEG y PNG.

Errores y excepciones

16. ¿Cómo se configuran los errores en producción?

Elegí una opción para verla

No se muestran, pero se registran todos. Un mensaje en pantalla revela rutas, nombres de tablas y hasta fragmentos de claves.

Lo que cambia es dónde va el mensaje, no si se guarda.

17. Un E_WARNING por un archivo que no existe, ¿se atrapa con try/catch?

Elegí una opción para verla

No. Los warning no son excepciones: se imprimen, el programa sigue y el valor de retorno es false.

Por eso hay que comprobar lo que devuelve la función. Desde PHP 8 los errores graves sí se lanzan como Error, que sí se puede atrapar.

18. ¿Para qué sirve finally?

Elegí una opción para verla

Se ejecuta siempre: tanto si el try terminó bien como si saltó a un catch. Sirve para cerrar un archivo o liberar un recurso.

19. ¿Por qué el catch más específico va primero?

Elegí una opción para verla

Se evalúan en orden y se ejecuta el primero que coincide. Como toda excepción hereda de Exception, un catch (Exception $e) puesto primero atrapa todo y los demás nunca se ejecutan.

Lo específico primero, Throwable al final.

Manejar el error y registrar

20. ¿Qué hace throw?

Elegí una opción para verla

Lanza la excepción: la ejecución normal se corta en ese punto y salta al catch que corresponda, o termina con un error fatal si nadie la atrapa.

No confundir con return false: eso no avisa nada, y quien llama tiene que acordarse de comprobarlo.

21. ¿Qué tiene de malo catch (Exception $e) { }?

Elegí una opción para verla

El error desaparece: no se registra, no se muestra y el programa continúa con datos que no son válidos. El síntoma aparece mucho después y lejos de la causa.

Lo correcto es registrar el detalle y decidir qué se le muestra al usuario.

22. ¿Por qué no se muestra $e->getMessage() directamente en pantalla?

Elegí una opción para verla

El mensaje de una excepción está escrito para quien programa, no para el usuario: suele traer rutas absolutas, nombres de tablas y hasta fragmentos de consultas.

Va al registro; a la pantalla va un texto que se entienda, como «No se pudo cargar el catálogo, probá más tarde».

23. ¿Qué información no debe escribirse nunca en un archivo de registro?

Elegí una opción para verla

Un registro se copia, se manda por correo para revisar y queda en el servidor durante meses: cualquier cosa que se escriba ahí deja de estar protegida.

La fecha, la línea y el archivo, en cambio, son justamente lo que sirve para diagnosticar.

24. ¿Qué devuelve file_put_contents() si falla?

Elegí una opción para verla

false, y emite un aviso. Si escribe bien, devuelve la cantidad de bytes escritos.

Comprobar ese valor es lo que separa un sistema que avisa «no se pudo guardar» de uno que pierde el dato en silencio.

Ejercicios de práctica

Diez ejercicios. Los tres primeros son de lectura y registro; del cuarto al sexto, de importación y subidas; los últimos, de excepciones y diagnóstico.

Ejercicio 1

Escribir una función que reciba una ruta y devuelva cuántas líneas no vacías tiene, o null si el archivo no se puede leer. Sin avisos en pantalla.

function contarLineas(string $ruta): ?int
{
    if (!is_readable($ruta)) {
        return null;
    }

    $lineas = file($ruta, FILE_IGNORE_NEW_LINES | FILE_SKIP_EMPTY_LINES);

    return $lineas === false ? null : count($lineas);
}

is_readable() evita el warning. Y el retorno de file() se comprueba igual, porque entre una cosa y la otra el archivo puede desaparecer.

Ejercicio 2

Escribir registrar(), que agrega una línea con fecha y mensaje a registro.log sin que dos peticiones se pisen.

function registrar(string $mensaje): void
{
    $linea = date('Y-m-d H:i:s') . ' ' . $mensaje . PHP_EOL;

    file_put_contents(
        __DIR__ . '/../datos/registro.log',
        $linea,
        FILE_APPEND | LOCK_EX
    );
}

FILE_APPEND para no borrar lo anterior y LOCK_EX para que dos escrituras simultáneas no se mezclen.

Ejercicio 3

Leer un CSV con código, nombre y precio, saltear el encabezado y devolver los productos válidos. Informar cuántos quedaron afuera.

function importar(string $ruta): array
{
    $manejador = fopen($ruta, 'r');

    if ($manejador === false) {
        throw new RuntimeException('No se pudo abrir el archivo.');
    }

    $productos = [];
    $rechazados = [];
    $numero = 0;

    while (($campos = fgetcsv($manejador, 0, ';')) !== false) {
        $numero++;

        if ($numero === 1) {
            continue;
        }

        if (count($campos) !== 3) {
            $rechazados[] = "Línea $numero: se esperaban 3 campos.";
            continue;
        }

        [$codigo, $nombre, $precio] = array_map('trim', $campos);

        if (!is_numeric($precio) || (float) $precio < 0) {
            $rechazados[] = "Línea $numero: precio inválido ($precio).";
            continue;
        }

        $productos[] = [
            'codigo' => (int) $codigo,
            'nombre' => $nombre,
            'precio' => (float) $precio,
        ];
    }

    fclose($manejador);

    return [$productos, $rechazados];
}

Ejercicio 4

Validar la subida de una foto de producto: que exista, que no pase de 1 MB, que sea JPEG o PNG de verdad, y guardarla con un nombre armado en el servidor.

const MAX_BYTES = 1024 * 1024;

$archivo = $_FILES['foto'] ?? null;
$codigo  = (int) ($_POST['codigo'] ?? 0);

if ($archivo === null || $archivo['error'] === UPLOAD_ERR_NO_FILE) {
    exit('Elegí una foto.');
}

if ($archivo['error'] !== UPLOAD_ERR_OK) {
    exit('La subida falló.');
}

if (!is_uploaded_file($archivo['tmp_name']) || $archivo['size'] > MAX_BYTES) {
    exit('La imagen no es válida o pasa de 1 MB.');
}

$info = getimagesize($archivo['tmp_name']);

if ($info === false || !in_array($info[2], [IMAGETYPE_JPEG, IMAGETYPE_PNG], true)) {
    exit('La imagen tiene que ser JPEG o PNG.');
}

$extension = $info[2] === IMAGETYPE_PNG ? 'png' : 'jpg';
$destino = __DIR__ . '/../public/img/productos/' . $codigo . '.' . $extension;

if (!move_uploaded_file($archivo['tmp_name'], $destino)) {
    exit('No se pudo guardar la imagen.');
}

Las tres comprobaciones que no se saltean: is_uploaded_file, getimagesize sobre el contenido y el nombre generado en el servidor.

Ejercicio 5

Crear una excepción propia que lleve el código del producto y lanzarla cuando el precio sea negativo.

class ProductoInvalidoException extends RuntimeException
{
    public function __construct(
        string $mensaje,
        private int $codigoProducto,
    ) {
        parent::__construct($mensaje);
    }

    public function codigoProducto(): int
    {
        return $this->codigoProducto;
    }
}

function validarPrecio(int $codigo, float $precio): void
{
    if ($precio < 0) {
        throw new ProductoInvalidoException('Precio negativo.', $codigo);
    }
}

La propiedad se declara en el constructor y se expone con un método: así quien atrapa la excepción tiene el dato que necesita para informar.

Ejercicio 6

Escribir el try/catch de una importación: si el archivo falta, avisar en pantalla y registrar el detalle; si el error es de otro tipo, registrar y mostrar un mensaje genérico.

try {
    importarCatalogo(__DIR__ . '/../datos/catalogo.csv');
} catch (RuntimeException $e) {
    registrar('Catálogo: ' . $e->getMessage());
    exit('No se pudo leer el archivo del catálogo.');
} catch (Throwable $e) {
    registrar('Catálogo, error inesperado: ' . $e->getMessage());
    exit('Ocurrió un error inesperado. Probá más tarde.');
}

El específico primero, el general al final. Y en los dos casos el mensaje de pantalla es distinto del que se registra.

Ejercicio 7

Exportar un catálogo a CSV con encabezados, usando fputcsv.

$manejador = fopen($ruta, 'w');

if ($manejador === false) {
    exit('No se pudo crear el archivo.');
}

fputcsv($manejador, ['codigo', 'nombre', 'precio'], ';');

foreach ($catalogo as $producto) {
    fputcsv($manejador, [
        $producto['codigo'],
        $producto['nombre'],
        $producto['precio'],
    ], ';');
}

fclose($manejador);

Modo 'w' crea o reemplaza; 'a' agregaría al final. fputcsv pone las comillas donde hacen falta, si un nombre tiene el separador adentro.

Ejercicio 8

Leer un config.json con {"iva": 0.21} y mostrar el IVA, distinguiendo «no existe el archivo» de «el JSON es inválido».

$texto = file_get_contents(__DIR__ . '/config.json');

if ($texto === false) {
    exit('No se encontró la configuración.');
}

$config = json_decode($texto, true);

if (json_last_error() !== JSON_ERROR_NONE) {
    exit('La configuración tiene un error de formato: ' . json_last_error_msg());
}

echo 'IVA: ' . (float) $config['iva'];

No se comprueba si el resultado es null, porque null también es un JSON válido. Se comprueba json_last_error().

Ejercicio 9

Escribir un try/finally que abra un archivo, procese sus líneas y garantice que se cierra aunque falle en el medio.

$manejador = fopen($ruta, 'r');

if ($manejador === false) {
    exit('No se pudo abrir el archivo.');
}

try {
    while (($linea = fgets($manejador)) !== false) {
        procesar(trim($linea));
    }
} finally {
    fclose($manejador);
}

finally se ejecuta siempre: si procesar() lanza una excepción, el archivo se cierra igual y la excepción sigue su camino hacia arriba.

Ejercicio 10

Este código lee el catálogo. Encontrar los cuatro problemas y corregirlos: $lineas = file('catalogo.csv');, [$codigo, $nombre, $precio] = explode(';', $linea);, if ($precio < 0) y count($lineas) como total de productos.

  • La ruta es relativa: depende del directorio de trabajo. Se arma con __DIR__.
  • No se comprueba si file() pudo leer: hay que comparar con false.
  • explode no respeta comillas ni comprueba la cantidad de campos: mejor fgetcsv con count().
  • $precio llega como texto con salto de línea, así que $precio < 0 compara un texto: hay que trim() y validar con is_numeric antes de convertir.

Y el total incluye el encabezado y las líneas vacías, así que count($lineas) tampoco es el número de productos.

Cómo saber si está bien

Si fallaste… Volvé a
Las preguntas 1 a 5Archivos en el servidor y Leer archivos.
Las preguntas 6 a 10Escribir archivos e Importar el catálogo.
Las preguntas 11 a 15Subir archivos y Lo que puede salir mal con una subida.
Las preguntas 16 a 19Los errores de PHP y Excepciones.
Las preguntas 20 a 24Excepciones propias y Atrapar sin tragarse el error.
Los ejercicios 9 y 10Errores frecuentes: son los que juntan todas las trampas en un solo lugar.

Con 18 o más respuestas correctas, la lección está firme. Entre 12 y 17, conviene releer los temas que fallaron. Con menos de 12, la lección 5 va a costar: las clases se apoyan en el manejo de errores que se ve acá.

Lo que hay que poder explicar

Idea La respuesta en una línea
Rutas__DIR__ siempre: la ruta no depende de dónde se ejecute el script.
PermisosEl usuario del servidor tiene que poder escribir. Nunca chmod 777.
LeerTodo de una vez para archivos chicos; línea por línea para los grandes.
EscribirFILE_APPEND y LOCK_EX, y comprobar que no devolvió false.
ImportarEncabezado, cantidad de campos, seguir después del error e informar la línea.
SubirEl tipo se comprueba en el contenido; el nombre lo genera el servidor.
ErroresSe muestran en desarrollo y se registran en producción. Siempre E_ALL.
Excepcionestry, catch específico primero, finally siempre.
RegistroEl detalle al archivo, el mensaje entendible a la pantalla, ningún dato sensible.

La lección 5 da el salto a los objetos: las clases Producto, Cliente y Venta, con sus datos y sus reglas adentro.

Licencia. El material de esta página es elaboración propia de Prof Mauro Bobyk —a partir de las fuentes que detalla el pie del sitio— y se publica bajo una licencia Creative Commons BY-NC-SA 4.0.