Ir al contenido principal

Lección 1 · Fundamentos del lenguaje

Qué es PHP

PHP es un lenguaje de programación que se ejecuta en el servidor. Cuando alguien escribe una dirección en el navegador, la petición viaja hasta una computadora que está siempre encendida; esa computadora ejecuta el programa, arma el resultado —casi siempre HTML— y lo devuelve. Lo que llega al navegador es el resultado, nunca el código. El nombre es un acrónimo recursivo: PHP: Hypertext Preprocessor, o sea, «preprocesador de hipertexto».

Nació en 1994, cuando Rasmus Lerdorf juntó unos scripts en C para saber quién visitaba su currículum, y con los años se convirtió en el lenguaje con el que está hecho buena parte de la web. Hoy es un lenguaje de propósito general —tiene variables, funciones, objetos, excepciones y tipos declarados— que conserva de su origen lo que mejor hace: recibir una petición, armar una respuesta y devolverla.

La versión que se usa

Todo el curso se escribe para PHP 8.4. No hace falta conocer versiones anteriores, ni lo que se explicaba en su momento: lo que aparece en estas páginas es el lenguaje tal como se escribe hoy, y cada cosa que se muestra funciona en una instalación nueva.

Herramienta del lenguaje Dónde aparece en el curso
Tipos declarados en parámetros y en el valor de retorno Desde esta lección, en todas las funciones del sistema.
declare(strict_types=1) En la cabecera de cada archivo.
Argumentos con nombre Lección 2, en funciones con varios parámetros opcionales.
match Esta lección, para elegir entre valores conocidos.
Funciones flecha y ... Lección 2, para trabajar con arrays y con argumentos variables.
?? y ??= En toda lectura de datos que llegan de afuera.
str_contains() y sus variantes Lección 2, para buscar dentro de un texto.

Se instala PHP 8.4 o una versión posterior. Un número de parche más nuevo no cambia nada de lo que se explica acá; al revés, usar una versión anterior no es una opción del curso: el código que se escribe aprovecha el lenguaje actual y no está pensado para funcionar en instalaciones viejas.

El caso que vamos a seguir

Toda la lección —y todo el curso— trabaja sobre el mismo sistema: la Tienda La Esquina, un almacén de barrio con productos, stock, clientes y ventas. En esta primera lección es apenas un conjunto de archivos que muestran datos escritos a mano, pero son los datos que va a usar el sistema completo. A medida que avancen las lecciones van a venir de un formulario y después de una base de datos, sin que cambie la forma de nombrarlos.

Un producto del catálogo, por ejemplo, se representa con estos datos:

código:     1042
nombre:     Yerba mate 1 kg
categoría:  Almacén
precio:     1850.50
stock:      24
Lección Qué le agrega al sistema
1Los datos y la lógica, en archivos sueltos.
2Funciones para calcular totales y ordenar el catálogo.
3El login del vendedor y el alta de productos.
4La importación del catálogo desde un archivo y el registro de errores.
5Las clases Producto, Categoría, Cliente y Venta.
6La base de datos, con consultas preparadas y transacciones.
7El sistema ordenado en carpetas, con un único punto de entrada.
8Permisos por rol y todo lo que protege los datos de los clientes.
9La tienda completa, funcionando en XAMPP.

Los productos, precios y clientes que aparecen en el material son inventados. No hace falta más que eso: alcanza con no copiar una lista real de clientes a un archivo de práctica.

Cómo se ejecuta

La confusión más común al empezar es de dónde corre el código. Conviene tenerlo claro desde el principio, porque cambia la forma de pensar los programas:

Tecnología Dónde se ejecuta Qué ve el usuario
HTML En ninguna parte: es texto El documento
CSS En el navegador El documento con estilos
JavaScript En el navegador El código, si lo abre; el efecto, si no
PHP En el servidor Solo el resultado

El recorrido de una petición

  1. El navegador pide una dirección, por ejemplo http://localhost/tienda/precios.php.
  2. El servidor web —Apache— recibe la petición y ve que el archivo termina en .php.
  3. En lugar de entregarlo tal cual, se lo pasa al intérprete de PHP.
  4. El intérprete lee el archivo de arriba hacia abajo, ejecuta las instrucciones y produce texto.
  5. Apache devuelve ese texto al navegador, junto con las cabeceras de la respuesta.
  6. El navegador recibe HTML común y corriente, y lo muestra. Nunca supo que había PHP del otro lado.

Si en el navegador se ve el código en lugar del resultado, PHP no está corriendo. Las tres causas de siempre: el archivo se abrió como archivo local —la dirección del navegador empieza con file:///—, quedó fuera de la carpeta del sitio, o el servidor está apagado. En los tres casos el archivo se entrega tal cual, con las etiquetas <?php a la vista. No es un error de sintaxis: es que no hay nadie ejecutándolo.

Dos formas de ejecutarlo

La primera es la de la web, que ya se describió: el archivo va dentro de la carpeta del sitio —/var/www/html en una instalación nativa, /opt/lampp/htdocs con XAMPP— y se pide con http://localhost/…. Si XAMPP todavía no está instalado, el procedimiento completo está en el Anexo · XAMPP. La segunda es la línea de comandos, que sirve para aprender y para probar:

php total.php

En ese caso no hay navegador ni HTML: el resultado sale por la terminal. Muchos de los ejemplos de esta lección se pueden probar así, y es más rápido que abrir el navegador cada vez. Además, PHP trae un servidor propio para desarrollo, que evita tener que configurar Apache: php -S localhost:8000 dentro de la carpeta del proyecto, y las páginas quedan en http://localhost:8000.

El primer programa

Se guarda el archivo como hola.php dentro de la carpeta del sitio —por ejemplo /var/www/html/tienda/— y se abre en el navegador con http://localhost/tienda/hola.php:

<?php

echo 'Tienda La Esquina';

El resultado es una página con una sola línea de texto. Tres cosas para mirar:

  • El archivo empieza con <?php. Esa etiqueta le avisa al intérprete que lo que sigue es código y no texto para copiar tal cual.
  • Cada instrucción termina con punto y coma. Es el error número uno de quien empieza.
  • echo no es una función sino una construcción del lenguaje: escribe lo que recibe en la salida.

Mezclar PHP con HTML

Un archivo .php puede tener HTML y PHP mezclados. Todo lo que está fuera de las etiquetas se copia tal cual a la respuesta; lo que está dentro se ejecuta. El primer archivo del sistema es el encabezado que van a compartir todas las pantallas:

<?php
date_default_timezone_set('America/Argentina/Buenos_Aires');
?>
<!DOCTYPE html>
<html lang="es">
<head>
    <meta charset="utf-8">
    <title>Tienda La Esquina</title>
</head>
<body>
    <h1>Tienda La Esquina</h1>
    <p>Lista de precios · <?= date('d/m/Y') ?></p>

El navegador recibe HTML terminado, con la fecha ya escrita adentro. Si se mira el código fuente desde el navegador, aparece la fecha pero no el echo: el programa corrió en el servidor y no dejó rastro.

date_default_timezone_set() fija la zona horaria. Sin esa línea, PHP usa la del servidor —o avisa que no hay ninguna configurada— y la fecha del ticket puede salir corrida. Para Argentina la zona es America/Argentina/Buenos_Aires.

La etiqueta corta de salida

<p>Lista de precios · <?= date('d/m/Y') ?></p>

<?= es equivalente a <?php echo. Es la forma que se usa en las plantillas, porque evita cerrar y volver a abrir el bloque para cada dato. La etiqueta <? a secas —sin el php— no se usa: depende de una configuración del servidor, así que el mismo archivo funciona en una instalación y en otra no.

Sintaxis básica

PHP toma mucho de C y de Perl. Las reglas generales son pocas:

  • Punto y coma al final de cada instrucción. El único caso en que se puede omitir es cuando la instrucción es lo último antes de cerrar el bloque con ?>, pero no conviene aprovecharse: es una fuente de errores.
  • Mayúsculas y minúsculas. Las variables distinguen mayúsculas: $precio y $Precio son dos variables distintas. Las palabras reservadas y las funciones no: echo, ECHO y Echo son lo mismo. Las constantes sí distinguen. Esa mezcla de criterios es histórica; la convención es escribir todo en minúsculas salvo las constantes, que van en mayúsculas.
  • Los bloques van entre llaves, y la indentación no le importa al intérprete: es para quien lee. La convención del ecosistema —PSR-12— pide cuatro espacios.
  • Los espacios en blanco sobran en casi todos lados. Se pueden partir las expresiones en varias líneas para que se lean mejor.

Comentarios

// Comentario de una línea, al estilo de JavaScript
# Otra forma de una línea, heredada de los scripts de consola
/* Comentario de
   varias líneas */
/**
 * Docblock: la forma de documentar una función o una clase.
 * Los editores lo muestran mientras se escribe.
 */

El docblock no es decoración: es información que el editor usa para mostrar la firma de una función, sus parámetros y lo que devuelve.

La etiqueta de cierre

En un archivo que contiene solo PHP, la etiqueta de cierre ?> se omite. El motivo es concreto: cualquier carácter que quede después de ella —un espacio, una línea en blanco, el salto de línea final que agrega el editor— se envía al navegador. Si ese archivo se incluye en otro que después quiere mandar una cabecera, el error es «headers already sent», y encontrar el espacio culpable puede llevar horas. Sin etiqueta de cierre, el problema no existe.

Variables

Una variable es un nombre que apunta a un valor. En PHP se reconoce por el signo pesos delante, y no hay que declararla antes de usarla: se crea al asignarle algo por primera vez. Un producto del catálogo se guarda así:

<?php

$codigo    = 1042;
$nombre    = 'Yerba mate 1 kg';
$categoria = 'Almacén';
$precio    = 1850.50;
$stock     = 24;

echo 'Producto: ' . $nombre . ' — $' . $precio;

Reglas de los nombres

  • Empiezan con una letra o con un guion bajo, y siguen con letras, números o guiones bajos.
  • Distinguen mayúsculas y minúsculas: $precio y $Precio conviven sin problema, y eso es una fuente de errores.
  • La convención es camelCase en minúscula inicial: $precioFinal, $totalVenta. Los nombres de una o dos letras —$i, $n— se reservan para contadores.
  • El nombre tiene que decir qué contiene. $dato no dice nada; $precioLista sí.

Tipado dinámico

La misma variable puede contener un número hoy y un texto mañana. El tipo lo determina el valor, no el nombre:

$precio = 1850.50;     // decimal
$precio = '1850.50';   // ahora es texto, y no hubo error
$precio = 1850;        // ahora es entero

Esto es cómodo para escribir y peligroso para leer: si una función espera un número y recibe un texto, el error aparece lejos de donde se originó. Un precio es siempre el mismo dato: conviene que sea siempre del mismo tipo. Por eso las funciones del sistema declaran el tipo de sus parámetros y de su valor de retorno: si un precio llega como texto, el error salta ahí y no en la suma.

Interpolación: comillas simples y dobles

Las comillas dobles interpolan: reemplazan las variables por su valor. Las simples no: muestran el texto literal.

$nombre = 'Yerba mate 1 kg';
$precio = 1850.50;

echo "Producto: $nombre";     // Producto: Yerba mate 1 kg
echo 'Producto: $nombre';     // Producto: $nombre
echo 'Producto: ' . $nombre;  // Producto: Yerba mate 1 kg

Cuando después del nombre de la variable sigue un carácter que podría confundirse con parte del nombre —una letra, un guion bajo, una llave—, se lo encierra entre llaves:

$codigo = '1042';

echo "Código $codigoA";     // busca $codigoA, que no existe
echo "Código {$codigo}A";   // Código 1042A

En las comillas dobles también funcionan los escapes: \n es un salto de línea, \t una tabulación, \\ una barra y \" una comilla doble. En las simples, en cambio, solo \\ y \' tienen significado; lo demás se escribe tal cual.

Saber si una variable existe

Función Devuelve verdadero cuando…
isset($x) La variable existe y no es null.
empty($x) La variable no existe, o su valor es «vacío»: 0, "0", "", [], null, false.
is_null($x) La variable vale exactamente null. No avisa si no existe.

El caso que muerde es empty("0"): devuelve verdadero, porque el texto «0» se considera vacío. Un producto de obsequio tiene $precio = 0, y ese cero es un precio válido. Si se valida con empty($precio), el obsequio se rechaza como si el campo estuviera en blanco. Cuando el cero es un valor legítimo, hay que comparar con === '' y no con empty.

Constantes

Una constante se define una vez y no cambia. Se declaran sin el signo pesos y, por convención, en mayúsculas:

const IVA = 0.21;
const STOCK_MINIMO = 5;

$precioConIva = $precio * (1 + IVA);

echo 'Stock mínimo: ' . STOCK_MINIMO;   // Stock mínimo: 5
echo "Stock mínimo: STOCK_MINIMO";      // no se interpolan

Se escriben sin el signo pesos y tampoco se interpolan dentro de las comillas dobles: dentro de un texto, PHP busca una variable. Por eso se concatenan. Se declaran con const, que es la forma que se usa en todo el curso, funciona dentro de una clase y se resuelve al compilar el archivo.

Ámbito

Una variable creada al principio del archivo no está disponible dentro de una función: cada función tiene su propio ámbito. Para acceder a una global hay que declararlo de forma explícita, y conviene evitarlo siempre que se pueda:

$productosCargados = 0;

function contar()
{
    global $productosCargados;   // sin esta línea, la función ve una variable nueva
    $productosCargados++;
}

La solución sana es pasar el valor como parámetro y devolver el resultado, que es lo que se ve en la lección 2.

Tipos de datos

PHP maneja ocho tipos, y para lo que hace falta en este curso alcanza con conocer los seis primeros:

Tipo Qué guarda Ejemplo en el sistema
intNúmeros enteros$codigo = 1042;
floatNúmeros con decimales$precio = 1850.50;
stringTexto$nombre = 'Yerba mate';
boolVerdadero o falso$disponible = true;
arrayConjunto ordenado de valores, con clave$carrito = ['Yerba mate' => 2];
nullAusencia de valorUn producto sin precio asignado.
objectInstancia de una claseSe ve en la lección 5.
resourceReferencia a un recurso externo, como un archivo abiertoLo devuelve fopen().

Cómo averiguar el tipo

var_dump() muestra el tipo y el valor; es la herramienta de diagnóstico que más se usa:

var_dump(1042);        // int(1042)
var_dump("1042");      // string(4) "1042"
var_dump(1850.50);     // float(1850.5)
var_dump(null);        // NULL

echo gettype(1042);       // integer
var_dump(is_int(1042));   // bool(true)

Para ver un array no sirve echo: PHP avisa «Array to string conversion» y escribe la palabra Array. Se usa print_r() o var_dump(), y en el medio de una página conviene envolverlos en <pre> para que se lea.

Enteros y decimales

Los enteros son de 64 bits en cualquier plataforma moderna, así que el máximo es enorme —PHP_INT_MAX—. Si una cuenta los supera, PHP pasa el resultado a decimal sin avisar. Con los decimales hay dos advertencias que conviene tener presentes, y las dos aparecen apenas se trabaja con plata:

var_dump(0.1 + 0.2);        // float(0.30000000000000004)
var_dump(0.1 + 0.2 == 0.3); // bool(false)

La primera: no se comparan con ==, porque 0.1 no se puede representar de forma exacta en binario. Se compara con una tolerancia, abs($a - $b) < 0.00001.

La segunda: un total de venta nunca se guarda en un float si se puede evitar. En la base de datos el precio va como DECIMAL, y en el código se redondea recién al final, con round($total, 2) o number_format(). Los centavos que se pierden por redondear en el medio, en un comercio, se notan.

Booleanos y valores «falsy»

Cualquier valor se puede evaluar como condición. Estos son los que cuentan como falso:

  • false
  • 0 y 0.0
  • "" y "0"
  • [], el array vacío
  • null

Todo lo demás es verdadero, incluido el texto "false" —que no es false— y el número -1. El stock 0 y el texto "0" están en la lista: un producto sin stock se evalúa como falso, lo cual viene bien para if ($stock), pero es una trampa cuando lo que se quiere saber es si el dato llegó.

Arrays, apenas una presentación

El array es la estructura más usada del lenguaje y merece una lección entera, pero conviene verlo ahora porque aparece en todos los ejemplos. En PHP hay un solo tipo de array, que hace de lista y de diccionario a la vez:

// Los precios de tres productos, en una lista
$precios = [1850.50, 1200, 640.75];

// Los datos de un producto, con claves
$producto = [
    'codigo'    => 1042,
    'nombre'    => 'Yerba mate 1 kg',
    'categoria' => 'Almacén',
    'precio'    => 1850.50,
];

echo $precios[1];              // 1200
echo $producto['nombre'];      // Yerba mate 1 kg
echo count($precios);          // 3

Las claves se asignan solas cuando no se indican, y pueden ser números o texto. Un array puede contener otros arrays: el catálogo es un array de arrays, uno por producto.

Conversión de tipos

Se puede forzar el tipo con una conversión explícita, o con las funciones que lo hacen. En el sistema aparece todo el tiempo, porque lo que llega por la dirección siempre es texto:

$codigo = (int) ($_GET['codigo'] ?? 0);   // '1042' pasa a 1042
$precio = (float) '1850.50';               // 1850.5
$activo = (bool) '0';                      // false
$texto  = (string) 1042;                   // '1042'

$numero = intval('1042abc');   // 1042: corta en el primer carácter no numérico

La conversión implícita —«type juggling»— también existe y es la que produce sorpresas al comparar. La forma de defenderse es la de la próxima sección: comparar con ===.

Declarar los tipos

Las funciones declaran qué reciben y qué devuelven. Con strict_types activado, el intérprete no convierte por su cuenta y tira un error si el tipo no coincide:

<?php

declare(strict_types=1);

function hayStock(int $stock): bool
{
    return $stock > 0;
}

echo hayStock(24) ? 'Sí' : 'No';    // Sí
echo hayStock('24');                // TypeError con strict_types activado

declare(strict_types=1) tiene que ser la primera instrucción del archivo, antes de cualquier otra cosa. Se pone en todos los archivos del proyecto: si un precio llega como texto en lugar de número, conviene que el programa avise ahí mismo y no tres pantallas después.

Operadores

Aritméticos

Operador Qué hace Ejemplo
+Suma1850.50 + 1200 → 3050.5
-Resta1850.50 - 150 → 1700.5
*Producto1850.50 * 2 → 3701
/División1850.50 / 2 → 925.25
%Resto de la división7 % 2 → 1
**Potencia2 ** 10 → 1024

Atención con la división: en PHP / devuelve un decimal siempre que la cuenta no sea exacta, aunque los dos operandos sean enteros. Para quedarse con la parte entera está intdiv(7, 2), que da 3.

Asignación

$total = 0;
$total += 1850.50;   // 1850.5
$total += 1200;      // 3050.5
$total -= 100;       // 2950.5

$ticket  = "Yerba mate 1 kg\n";
$ticket .= "Azúcar 1 kg\n";      // agrega una línea al texto

$codigo = $_GET['codigo'] ?? 0;   // solo si no vino, usa 0
$codigo ??= 0;                    // equivalente y más corto

El operador ?? —null coalescing— es la forma corta de «usá este valor si existe y no es nulo, y si no este otro». Con $_GET['codigo'] ?? 0 se lee un parámetro sin que aparezca un aviso si no vino.

Comparación

Operador Qué pregunta
==¿Son iguales después de convertir al mismo tipo?
===¿Son iguales y del mismo tipo?
!= o <>¿Son distintos, con conversión?
!==¿Son distintos, o de distinto tipo?
<, <=, >, >=Orden.
<=>Comparación de tres vías: devuelve -1, 0 o 1.

La regla práctica es simple: usar siempre ===, salvo que haya un motivo concreto para no hacerlo. En el sistema hay dos lugares donde esto decide qué ve cada usuario: comparar el código de producto que llega por la dirección y comparar el rol del que está logueado. Estos son los casos que explican por qué:

Expresión Resultado Por qué
10 == "10"trueEl texto se convierte a número.
10 === "10"falseUno es entero y el otro texto.
"1" == "01"trueLos dos son textos numéricos: se comparan como números.
null == falsetrueAmbos se consideran vacíos.
"" == 0falseEl texto vacío no se convierte en cero.
0 == "a"falseUn número no se compara con un texto que no es numérico.

Las dos últimas filas son las que justifican toda la insistencia: la comparación floja hace cosas que nadie espera, y siempre en el borde. La regla práctica no cambia, y es la de esta página: ===, salvo un motivo concreto para no usarlo.

Lógicos

Se combinan condiciones: && (y), || (o), ! (no). Un producto se puede vender si tiene stock y tiene precio:

$sePuedeVender = $stock > 0 && $precio > 0;

Existen también and, or y xor, que significan lo mismo pero tienen menor precedencia que la asignación. Esa diferencia produce un error clásico:

$a = true and false;   // $a vale true: primero se asigna, después se evalúa
$b = true && false;    // $b vale false: la asignación va última

Por eso se usan siempre && y ||. Las variantes con palabra se reservan para el estilo de las plantillas: if ($logueado and $esAdmin).

Concatenación, ternario y orden

echo 'Producto: ' . $nombre . ' — $' . $precio;

$estado = $stock > 0 ? 'Disponible' : 'Sin stock';

$estado = $stock > 0 ?: 'Sin stock';   // versión corta de lo mismo

$precioConDescuento = $precio * (1 - 0.10);

// -1 si la izquierda es menor, 0 si son iguales, 1 si es mayor
$orden = $precio1 <=> $precio2;

El operador . no es una suma: '2' + '3' da 5 y '2' . '3' da '23'. Confundirlos es un error frecuente al armar un código a partir de partes, como el código de barras compuesto por rubro y número.

Precedencia

Los operadores se evalúan en un orden establecido: primero la potenciación, después el producto y la división, después la suma y la resta, después la concatenación, después las comparaciones y al final los lógicos. No hace falta memorizarlo: se usan paréntesis. Un paréntesis de más no cuesta nada y un error de precedencia cuesta una tarde.

Un detalle que se cobra caro: la concatenación tiene menos precedencia que la suma, así que 'Total: ' . 1850 + 2 no da 'Total: 1852' sino 'Total: 18502'. No es un error de PHP: es la regla del lenguaje. Se resuelve con paréntesis, y conviene ponerlos siempre que se mezclen texto y cuentas.

Estructuras de control

Un programa lineal ejecuta todo siempre. Las estructuras de control permiten decidir y repetir, y son las mismas que en cualquier lenguaje de la familia de C, con dos agregados propios.

Condicionales

La primera regla del sistema es la que decide si un producto se puede ofrecer:

$stock = 3;

if ($stock > 10) {
    echo 'Hay stock';
} elseif ($stock > 0) {
    echo 'Últimas unidades';
} else {
    echo 'Sin stock';
}

Las condiciones se evalúan de arriba hacia abajo y se ejecuta la primera que se cumple. Por eso el orden importa: si la primera condición fuera $stock > 0, cualquier cantidad caería ahí y nunca llegaría a la rama de «hay stock».

La forma con match

Cuando lo que hay que hacer es elegir entre varios valores conocidos, match es más corto que switch y no permite olvidarse del caso por defecto:

$formaPago = 2;

$recargo = match ($formaPago) {
    1 => 0,          // efectivo
    2 => 0.10,       // tarjeta
    3 => 0.05,       // transferencia
    default => 0,
};

echo 'Recargo: ' . ($recargo * 100) . ' %';   // Recargo: 10 %

Dos diferencias con switch que conviene saber: match compara con ===, así que no hay conversiones raras —y el formulario manda el número de forma como texto, así que hay que convertirlo antes—, y devuelve un valor, así que se puede asignar.

Repetición

// while: se pregunta antes de entrar
$i = 1;
while ($i <= 3) {
    echo "Cuota $i\n";
    $i++;
}

// do-while: se ejecuta al menos una vez
$i = 1;
do {
    echo "Cuota $i\n";
    $i++;
} while ($i <= 3);

// for: cuando se sabe cuántas veces
for ($i = 1; $i <= 3; $i++) {
    echo "Cuota $i\n";
}

El for tiene tres partes separadas por punto y coma: inicialización, condición y avance. Se usa cuando la cantidad de vueltas se conoce de antemano, como las tres cuotas de un pago.

foreach, el que más se usa

Para recorrer un array no hace falta contar nada: foreach pasa por todos los elementos y deja el valor en una variable. Así se recorre el carrito de una venta:

$carrito = [
    'Yerba mate 1 kg' => 1850.50,
    'Azúcar 1 kg'     => 1200,
    'Yerba 500 g'     => 990.75,
];

foreach ($carrito as $producto => $precio) {
    echo "$producto: $precio\n";
}

$total = 0;
foreach ($carrito as $precio) {
    $total += $precio;
}

echo 'Total: ' . number_format($total, 2, ',', '.');

Si se necesita modificar los valores del array, se recorre por referencia con as &$precio. Es una herramienta delicada: deja la variable del foreach apuntando al último elemento, así que después hay que liberarla con unset($precio) o el próximo foreach va a pisar el último valor sin avisar.

Cortar y saltear

break sale del bucle; continue salta a la vuelta siguiente. En el sistema sirven para ignorar los productos que todavía no tienen precio —null— sin cortar el listado:

foreach ($catalogo as $producto => $precio) {
    if ($precio === null) {
        continue;      // todavía no tiene precio: sigue con el próximo
    }

    echo "$producto: $precio\n";
}

Los dos aceptan un número que indica cuántos niveles cortar, aunque el uso de varios niveles suele ser señal de que el código se puede ordenar mejor. Estas estructuras también tienen una sintaxis alternativa con dos puntos —if (...): ... endif;, foreach (...): ... endforeach;— que se usa cuando el código está mezclado con HTML, porque evita llaves sueltas en medio del marcado. Aparece en la lección 3, con las plantillas.

Entrada y salida

La salida de un programa es lo que se envía al navegador —o a la terminal—. echo es lo más directo, y acepta varios argumentos separados por coma:

echo 'Producto: ', $nombre;

print 'Producto';                        // parecida a echo, pero devuelve 1

printf('%s: %.2f', 'Total', 3050.5);     // Total: 3050.50
$codigo = sprintf('%05d', 42);           // '00042'

printf y sprintf sirven para armar texto con formato: %d es un entero, %s un texto y %.2f un decimal con dos cifras. Para precios conviene number_format(1850.5, 2, ',', '.'), que devuelve 1.850,50 con coma decimal y punto de miles, como se escribe en Argentina.

Mostrar datos del usuario

Este es el punto donde se cruzan la salida y la seguridad. La pantalla del buscador recibe el texto por la dirección: buscar.php?q=yerba. Lo primero que hace el programa es leerlo:

$busqueda = $_GET['q'] ?? '';

echo "Resultados para: $busqueda";

Funciona, pero está mal. Ese texto lo escribió cualquiera: si en la dirección va un fragmento de HTML o de JavaScript, el navegador lo interpreta. Es el ataque llamado XSS, y en una tienda no es teórico: alguien podría armar un enlace que, al abrirlo el administrador, muestre un formulario falso de cambio de precio. Se evita escapando el texto antes de escribirlo:

$busqueda = $_GET['q'] ?? '';

echo 'Resultados para: ' . htmlspecialchars($busqueda, ENT_QUOTES, 'UTF-8');

htmlspecialchars convierte < en &lt;, > en &gt; y las comillas en sus entidades. El navegador muestra el texto tal cual en lugar de interpretarlo. La regla es corta y vale para todo el curso: todo dato que venga de afuera se escapa al mostrarlo. Escapar al guardar no es la solución: ensucia la base con entidades y rompe cualquier otro uso del dato.

Entrada desde la terminal

En un script de consola la entrada se lee de otra manera. Los argumentos llegan en $argv:

<?php

// php total.php 1850.50 2
$precio   = (float) ($argv[1] ?? 0);
$cantidad = (int) ($argv[2] ?? 1);

echo 'Total: ' . number_format($precio * $cantidad, 2, ',', '.') . "\n";

Los scripts de consola son útiles para probar ideas sin pasar por el navegador, y son el mejor lugar para experimentar con el lenguaje: el ciclo es más corto y los errores se ven completos, sin HTML alrededor.

Las superglobales

Hay variables que PHP crea por su cuenta y que están disponibles en cualquier parte del programa, sin declararlas ni pasarlas como parámetro. Se las llama superglobales y todas son arrays:

Variable Qué contiene En el sistema
$_GETLos parámetros que vienen en la dirección, después del ?.El código que se pide en producto.php?codigo=1042.
$_POSTLos datos que llegan en el cuerpo de un formulario enviado por POST.El alta de un producto.
$_COOKIELas cookies que mandó el navegador.El carrito de compras guardado en el navegador.
$_FILESLos archivos subidos con un formulario.La foto del producto.
$_SESSIONLos datos de la sesión del usuario, guardados en el servidor.Quién está logueado y con qué rol.
$_SERVERInformación del servidor y de la petición.El método, la ruta y la IP que hizo la petición.
$_ENVVariables de entorno.El usuario y la clave de la base de datos.
$GLOBALSTodas las variables del ámbito global.Se usa muy poco y conviene evitarla.

Tres datos de $_SERVER aparecen en casi todos los programas:

echo $_SERVER['REQUEST_METHOD'];   // GET, POST...
echo $_SERVER['SCRIPT_NAME'];      // /tienda/productos.php
echo $_SERVER['HTTP_USER_AGENT'];  // qué navegador hace la petición

Un caso particular: $_SERVER['PHP_SELF'] devuelve la ruta del script tal como la pidió el navegador. Si alguien pide /productos.php/<script>..., eso aparece en la variable, y usarlo sin escapar en el atributo action de un formulario es un XSS servido en bandeja. Por eso se usa $_SERVER['SCRIPT_NAME'], que devuelve solo la ruta del archivo.

Incluir archivos

El sistema no puede ser un archivo de mil líneas. PHP permite partir el código en archivos y pegarlos en tiempo de ejecución con cuatro instrucciones:

Instrucción Si el archivo no está
includeAvisa y sigue ejecutando.
include_onceIgual, pero no lo incluye dos veces.
requireFrena el programa con un error fatal.
require_onceIgual, y solo una vez.

La regla es simple: require para lo que el programa necesita para funcionar —la configuración, la conexión, una clase— y include para lo que es opcional. _once se usa siempre que el archivo defina funciones o clases, porque definirlas dos veces es un error fatal.

Cómo se arma el sistema con esto

El primer paso para ordenar el sistema es separar lo que se repite en cada pantalla. El encabezado con el nombre de la tienda y el pie con el horario van en archivos aparte:

<?php
// encabezado.php
?>
<!DOCTYPE html>
<html lang="es">
<head>
    <meta charset="utf-8">
    <title><?= $titulo ?> · Tienda La Esquina</title>
</head>
<body>
    <h1>Tienda La Esquina</h1>
<?php
// producto.php
$titulo = 'Ficha de producto';
$nombre = 'Yerba mate 1 kg';

require __DIR__ . '/encabezado.php';
?>
    <h2><?= htmlspecialchars($nombre, ENT_QUOTES, 'UTF-8') ?></h2>
<?php require __DIR__ . '/pie.php'; ?>

El archivo incluido se ejecuta en el mismo ámbito que el que lo incluye: por eso $titulo existe dentro de encabezado.php. Esa es también la razón por la que conviene usar __DIR__ para armar la ruta: es la carpeta del archivo actual, así que la inclusión funciona sin importar desde dónde se ejecute el programa. Con una ruta relativa como 'encabezado.php', el resultado depende del directorio de trabajo y cambia según cómo se llame al script.

Este patrón —un encabezado, un pie y el contenido en el medio— es el antecesor directo de las plantillas y del modelo MVC, y tiene un problema que se ve en la lección 7: si cada pantalla decide qué incluir, el orden y las dependencias quedan repartidos por todo el proyecto.

Errores frecuentes

Estos son los que aparecen una y otra vez al empezar. Reconocerlos ahorra tiempo, porque el mensaje de PHP no siempre dice dónde está el problema:

Lo que pasa La causa Cómo se arregla
Parse error: syntax error, unexpected end of file Falta un punto y coma o una llave de cierre. El error se informa al final del archivo, pero está antes: revisar el último bloque que se tocó.
Warning: Undefined variable Se usa una variable que nunca se asignó. Asignarla antes, o leerla con ??. No es inofensivo: el valor es null y las cuentas siguen.
La pantalla muestra el código PHP El archivo no se está ejecutando: se abrió como archivo local, está fuera de la carpeta del sitio o el servidor está apagado. Pedirlo por http://localhost/… con Apache y PHP encendidos.
Array to string conversion Se hace echo del array de un producto. print_r() o var_dump() para verlo; implode() para unirlo en un texto.
Headers already sent Se intenta mandar una cabecera después de que ya salió texto —por ejemplo, un espacio antes de <?php. Quitar la etiqueta de cierre en los archivos que son solo PHP y no dejar espacios antes de la de apertura.
La comparación da verdadero cuando no debería Se usó == y PHP convirtió los tipos. Usar ===.
Trying to access array offset on value of type null Se accede a una clave de algo que no existe, como $_GET['codigo'] cuando no vino. $_GET['codigo'] ?? '', y validar antes de usar.
El total sale 0 o se muestra Array Se sumaron textos en lugar de números, o se usó . en lugar de +. Convertir con (int) o (float) antes de operar.

Una recomendación para todo el curso: al desarrollar, mostrar todos los errores. En el php.ini de desarrollo se ponen display_errors = On y error_reporting = E_ALL. En el servidor de producción van al revés: los errores se registran en un archivo y no se muestran, porque un mensaje de error puede revelar rutas, nombres de tablas y hasta claves. En GNU/Linux hay un php.ini por cada forma de ejecución —/etc/php/8.4/apache2/php.ini para el servidor y /etc/php/8.4/cli/php.ini para la terminal—; php --ini dice cuál se está usando, y después de modificarlo hay que reiniciar Apache con sudo systemctl restart apache2.

Ejercicios

Nueve ejercicios, todos sobre la Tienda La Esquina y resolubles con lo que se vio en esta página. Conviene intentarlos antes de mirar las respuestas.

1. Del HTML al servidor

Explicar con palabras propias qué diferencia hay entre estas dos direcciones, y qué ve el usuario en cada caso:

  1. http://localhost/tienda/precios.html
  2. http://localhost/tienda/precios.php

2. Predecir la salida

Sin ejecutarlo, escribir qué imprime cada línea:

$precios = [1850, 1200, 640];
$cantidad = 3;

echo array_sum($precios) / $cantidad;
echo $precios[1] % 2;
echo $precios[0] . $precios[2];
echo 'Total: ' . $precios[0] + $precios[1];

3. Comillas

Indicar el resultado de cada echo y explicar por qué:

$producto = 'Yerba mate';

echo 'Producto: $producto';
echo "Producto: $producto";
echo 'Producto: ' . $producto;
echo "Producto: {$producto}s";

4. Comparaciones

El login del sistema compara el rol del usuario, que llega como texto. Decidir si cada expresión es verdadera o falsa, y justificar:

  1. "vendedor" == "vendedor"
  2. 1042 == "1042"
  3. 1042 === "1042"
  4. null == false
  5. 0 == "admin"

5. El producto de obsequio

El formulario de alta de productos tiene que rechazar el precio vacío, pero aceptar un precio 0 para los obsequios. Escribir la condición correcta y explicar qué pasaría con empty().

6. Lista de precios con IVA

Armar una tabla HTML con cinco productos y dos columnas: precio de lista y precio con IVA. Usar un for para los productos y la constante IVA.

7. El ticket

Dado el carrito ['Yerba mate' => 1850.50, 'Azúcar' => 1200, 'Fideos' => null], mostrar cuántos productos tienen precio, el total y cuáles quedaron sin precio. Los que no tienen precio no se suman.

8. Buscar los errores

El siguiente programa da de alta un producto y tiene cinco errores. Encontrarlos y escribir la versión corregida:

<?php

$nombre = $_POST['nombre']
$codigo = '1042';

if ($codigo = 1042) {
    echo 'Ya existe';
}

echo "Producto: " . $nombre . $codigo + 1;

echo $nombre;

9. Inclusión y escape

Armar dos archivos: encabezado.php que reciba un $titulo y muestre el comienzo del documento, y producto.php que lo incluya, muestre el nombre que llega por la dirección y cierre el HTML. El nombre tiene que estar escapado.

Respuestas sugeridas

1. Con precios.html el servidor entrega el archivo tal como está y el navegador lo interpreta: lo que se ve es exactamente lo que hay en el archivo. Con precios.php el servidor lo ejecuta primero y entrega el resultado; lo que se ve es la salida del programa, y el código quedó en el servidor. Por eso, en el segundo caso, «ver el código fuente» en el navegador no muestra PHP.

2.

  • array_sum($precios) / $cantidad → 1230. Suma 3690 y divide por 3.
  • $precios[1] % 2 → 0. El resto de dividir 1200 por 2.
  • $precios[0] . $precios[2] → 1850640. El punto concatena: junta los dos números como texto.
  • 'Total: ' . $precios[0] + $precios[1] → 3050. La concatenación tiene menos precedencia que la suma, así que primero se hace 1850 + 1200 y después se concatena con el texto. Como el texto no es numérico, el resultado final es 3050. Para el total con texto hay que escribir 'Total: ' . ($precios[0] + $precios[1]).

3.

  • 'Producto: $producto' → Producto: $producto. Las comillas simples no interpolan.
  • "Producto: $producto" → Producto: Yerba mate. Las dobles sí.
  • 'Producto: ' . $producto → Producto: Yerba mate. Concatenación explícita.
  • "Producto: {$producto}s" → Producto: Yerba mates. Las llaves delimitan el nombre de la variable para que la s no se confunda con parte de él.

4.

  • a) Verdadera: son el mismo texto.
  • b) Verdadera: el texto "1042" es numérico y se compara como número.
  • c) Falsa: son iguales en valor pero no en tipo. Esta es la comparación que hay que usar en el login.
  • d) Verdadera: los dos valores se consideran «vacíos».
  • e) Falsa: comparar un número con un texto no numérico no los iguala. Y como la comparación floja tiene estos bordes, en el sistema se usa ===.

5. La condición tiene que distinguir «no llegó nada» de «llegó cero»:

$precio = $_POST['precio'] ?? '';

if ($precio === '' || !is_numeric($precio)) {
    echo 'Precio inválido';
} else {
    $valor = (float) $precio;

    if ($valor < 0) {
        echo 'El precio no puede ser negativo';
    }
}

Con empty() el producto de obsequio se rechazaría como si el campo estuviera vacío, porque empty("0") es verdadero. Es el ejemplo típico de una validación que parece correcta y falla justo en el borde.

6.

<?php
const IVA = 0.21;
$precios = [1850.50, 1200, 640.75, 2300, 890];
?>
<table class="table">
    <thead>
        <tr><th>Producto</th><th>Precio</th><th>Con IVA</th></tr>
    </thead>
    <tbody>
        <?php for ($i = 0; $i < count($precios); $i++): ?>
            <tr>
                <td><?= $i + 1 ?></td>
                <td><?= number_format($precios[$i], 2, ',', '.') ?></td>
                <td><?= number_format($precios[$i] * (1 + IVA), 2, ',', '.') ?></td>
            </tr>
        <?php endfor; ?>
    </tbody>
</table>

Acá se ve la sintaxis alternativa con dos puntos, que deja el HTML limpio: el for recorre los precios y cada vuelta imprime una fila. El IVA se calcula, no se escribe a mano en cada celda.

7.

$carrito = ['Yerba mate' => 1850.50, 'Azúcar' => 1200, 'Fideos' => null];

$conPrecio = 0;
$total = 0;
$sinPrecio = [];

foreach ($carrito as $producto => $precio) {
    if ($precio === null) {
        $sinPrecio[] = $producto;
        continue;                  // no se suma
    }

    $conPrecio++;
    $total += $precio;
}

echo 'Productos con precio: ' . $conPrecio . "\n";
echo 'Total: ' . number_format($total, 2, ',', '.') . "\n";
echo 'Sin precio: ' . implode(', ', $sinPrecio);

La clave está en no sumar el null como si fuera cero: un producto sin precio no es un producto gratis. Es la misma distinción que aparece en la base de datos, donde NULL no es lo mismo que 0.

8. Los errores son: falta el punto y coma después de $nombre; $codigo se guarda como texto y después se usa en una cuenta; la condición usa = —asignación— en lugar de ===; la concatenación se mezcla con la suma sin paréntesis, así que el resultado no es el esperado; y el nombre se imprime sin escapar. La versión corregida:

<?php

declare(strict_types=1);

$nombre = $_POST['nombre'] ?? '';
$codigo = (int) ($_POST['codigo'] ?? 0);

if ($codigo === 1042) {
    echo 'Ya existe';
}

echo 'Producto: ' . htmlspecialchars($nombre, ENT_QUOTES, 'UTF-8') . ' ' . ($codigo + 1);

9. Dos archivos. El primero recibe el título y abre el documento:

<?php
// encabezado.php
?>
<!DOCTYPE html>
<html lang="es">
<head>
    <meta charset="utf-8">
    <title><?= htmlspecialchars($titulo, ENT_QUOTES, 'UTF-8') ?> · Tienda La Esquina</title>
</head>
<body>

El segundo lo usa, muestra el nombre escapado y cierra:

<?php
// producto.php
$titulo = 'Ficha de producto';
$nombre = $_GET['nombre'] ?? 'sin nombre';

require __DIR__ . '/encabezado.php';
?>
    <h1>Producto: <?= htmlspecialchars($nombre, ENT_QUOTES, 'UTF-8') ?></h1>
</body>
</html>

El título también se escapa aunque lo escriba el programador: si mañana ese título viene de una base de datos, la página ya está protegida. Escapar de más no molesta; escapar de menos, sí.

Para leer más

El manual oficial de PHP está en español, es gratuito y suele tener la respuesta exacta a lo que se busca. Conviene consultarlo por tema y no de principio a fin:

  • Tipos de datos, con la tabla de comparaciones entre tipos.
  • Operadores de comparación: la referencia de lo que se explicó con las tablas de esta página.
  • Seguridad: el capítulo que conviene leer temprano, no al final.
  • PSR-12: el estándar de estilo que usa el ecosistema.

Lo que este curso no cubre

Para que no se busque en el lugar equivocado, estas cosas quedan afuera del recorrido de nueve lecciones, aunque la tienda las necesite el día que se use en serio:

  • Administración del servidor MySQL: usuarios, privilegios, copias de seguridad y restauración.
  • Envío de correo: avisos de compra, recuperación de contraseña.
  • Generación de PDF e imágenes: la factura imprimible y las fotos del catálogo.
  • Pruebas automatizadas: verificar que el total sigue dando bien después de cada cambio.
  • Publicación en producción: HTTPS, dominio, certificados y copias de seguridad programadas.

Resumen

Si de toda la lección queda una sola idea, que sea esta: PHP se ejecuta en el servidor y devuelve texto. Todo lo demás se apoya en eso, incluido el hecho de que no haya memoria entre una página y la siguiente.

Tema Lo que hay que retener
El caso Todo el curso trabaja sobre la Tienda La Esquina. Los datos del catálogo y de los clientes son inventados.
Ejecución El código corre en el servidor; el navegador recibe solo el resultado. Cada petición empieza de cero.
Etiquetas <?php ... ?> para el código, <?= ... ?> para escribir un valor. Fuera de ellas, el texto se copia tal cual.
Variables Llevan $, no se declaran y cambian de tipo sin avisar. Distinguen mayúsculas.
Strings Las comillas dobles interpolan y las simples no. Se concatenan con ..
Comparaciones Usar ===. == convierte tipos, y esa conversión es la fuente de la mayoría de los errores lógicos y de varias fallas de seguridad.
Control if, match, while, for y foreach. El último es el que más se usa: recorre arrays sin contar índices.
Superglobales Llegan datos sin validar. Se comprueba que existan, se valida el formato y se escapa al mostrarlos.
Inclusión require_once para lo indispensable, con rutas armadas con __DIR__. Es el primer paso hacia separar el proyecto en partes.

En la próxima lección el sistema empieza a ordenarse: funciones para calcular totales, arrays para el catálogo y las funciones de texto que hacen falta para normalizar nombres y ordenar el listado de productos.

Autoevaluación de la lección 1

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.