En cuanto le dedicas algo de tiempo a PHP, al trabajar en el lado del servidor, sabes que tiene sus limitaciones y que hay que apoyarse en JavaScript para resolver algunos problemas. En este caso nos vamos a olvidar de frameworks como Laravel + Inertia y otros, y vamos a buscar una solución solo con PHP y JavaScript «básicos».

Una de las primeras situaciones en las que nos damos cuenta de estas limitaciones de PHP es al intentar crear un Select (Combo en otros lenguajes) de categorías y otro de subcategorías que vaya cambiando según la categoría seleccionada.

Ejemplo de uso de 2 select en https://iagolast.github.io/pselect/

La solución clásica es usar Ajax para recargar el combo de la subcategoría (población en el ejemplo) cada vez que cambia el de la categoría (provincia en el ejemplo). En esta ocasión vamos a crear una solución en la que con PHP vamos a «escribir» 2 objetos JavaScript (categorías y subcategorías) y vamos a añadir código JavaScript que haga el cambio de los valores de las subcategorías al cambiar la categoría seleccionada.

Para que sea más fácil entenderlo, primero vamos a ver un ejemplo en el que únicamente se utiliza JavaScript. ESTA PARTE ES SOLO PARA COMPRENDER MEJOR EL EJEMPLO, NO ES NECESARIA PARA QUE FUNCIONE NUESTRO PHP.

(Se omiten voluntariamente los acentos en los nombres de variables)

Este es el fichero selectdoble.html:

<!--Creo los controles SELECT para incluir categorias y subcategorias-->
<!--La clave es llamar a la función cargaSubcategoria cuando cambia la categoria-->
<select class="form-control" id="selectcat" onchange="cargaSubcategoria()">
</select>
<select class="form-control" id="selectsubcat">
</select>

<script>
    //Creo los arrays con categorías y subcategorías
    var categorias=[
        {id: 1, n:"cat1"},
        {id:2, n: "cat2"}
        ];
    var sub=[
        {idc: 1, c: "cat1", ids:1, s: "subcat1 de 1"}, 
        {idc: 1, c: "cat1", ids:2, s: "subcat2 de 1"}, 
        {idc: 2, c: "cat2", ids:3, s: "subcat1 de 2"},
        ];
    
    //Esta función carga las categorías del objeto en el SELECT
    function cargaCategoria(){
        select = document.getElementById('selectcat');

        for (var i = 0; i<categorias.length; i++){
            var opt = document.createElement('option');
            opt.value = categorias[i].id;
            opt.innerHTML = categorias[i].n;
            select.appendChild(opt);
        }
    }

    //Para cargar las subcategorías de la categoría seleccionada
    function cargaSubcategoria(){
        select = document.getElementById('selectcat');
        //guardo en una variable la categoría seleccionada
        categoriaseleccionada=select.value;
        select2=document.getElementById("selectsubcat");
        select2.innerHTML="";  //Así borramos las subcategorías
        //Podría usar filter, pero para hacerlo más comprensible para mis alumnos...
        for (var i = 0; i<sub.length; i++){
            //Elijo solo las subcategorías de la categoría seleccionada
            if(sub[i].idc==categoriaseleccionada){
                var opt = document.createElement('option');
                opt.value = sub[i].ids;
                opt.innerHTML = sub[i].s;
                select2.appendChild(opt);
            }
        }
    }

    //Al cargar la página, pongo las categorías y subcategorías
    cargaCategoria();
    cargaSubcategoria();
</script>

Puntos importantes en el JavaScript del ejemplo:

  • En el primer SELECT asignamos la función cargaSubcategoria() al evento onChange
  • Creamos un «option» para cada categoría y lo añadimos al «select» de categorías. El «option» tendrá el id como valor (value) y el nombre como texto a mostrar (innerHTML)
  • En la función de cargar las subcategorías, lo primero que hacemos es borrar las subcategorías antes cargadas, si las hubiera, con la línea: select2.innerHTML=»»;
  • Recorremos todas las subcategorías y solo «cargamos» en el «select» las subcategorías que pertenezcan a la categoría seleccionada.
  • Al final del código JavaScript llamamos a las dos funciones creadas para que al cargarse la página, los «select» tengan el contenido necesario.

Las categorías y subcategorías las tenemos en 2 tablas de la base de datos. Usamos un Script para conectarnos al servidor de base de datos. Si no existe la base de datos, la creamos y si no existen las tablas, las creamos y añadimos registros de ejemplo. Veamos conexion.php:

<?php

    $conexion=new mysqli("localhost", "root", "");
    $conexion->query("CREATE DATABASE IF NOT EXISTS dobleselect DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci;");
    //$conexion=new mysqli("localhost", "root", "", "dobleselect");
    $conexion->query("USE dobleselect");
    
    //Si no existen, creamos las tablas
    $conexion->query("CREATE TABLE IF NOT EXISTS categorias(
        idcategoria INT NOT NULL PRIMARY KEY AUTO_INCREMENT,
        nombre VARCHAR(100) NOT NULL UNIQUE)"
    );

    $conexion->query("CREATE TABLE IF NOT EXISTS subcategorias(
        idsubcategoria INT NOT NULL PRIMARY KEY AUTO_INCREMENT,
        nombre VARCHAR(100) NOT NULL UNIQUE,
        categoria INT,
        FOREIGN KEY (categoria) REFERENCES categorias (idcategoria))"
    );

    //Si no hay registros, añadimos 2
    $resultado=$conexion->query("SELECT * FROM categorias");
    if(!$resultado->fetch_object()){
        $conexion->query("INSERT INTO categorias values (null, 'categoria1')");
        $conexion->query("INSERT INTO categorias values (null, 'categoria2')");
        $conexion->query("INSERT INTO subcategorias values (null, 'subcat1 de 1', 1)");
        $conexion->query("INSERT INTO subcategorias values (null, 'subcat2 de 1', 1)");
        $conexion->query("INSERT INTO subcategorias values (null, 'subcat1 de 2', 2)");
    }
?>

El c´ódigo en PHP lo que hace simplemente es generar los objetos «categorias» y «subcategorías» con «echo» y el resto funciona igual

...
<script>
    <?php
        require_once("conexion.php");
        $categorias=$conexion->query("SELECT * from categorias");

        // "Escribo" con PHP el array de categorías de JavaScript
        echo "var categorias=[";        
        while($fila=$categorias->fetch_object()){
            echo "{id:" . $fila->idcategoria;
            echo ", n: '" . $fila->nombre . "'} , ";
        }
        echo "];";

        //"Escribo" con PHP el array de subcategorías de JavaScript
        echo "var sub=[";
        $subcategorias=$conexion->query("select s.idsubcategoria as ids, s.nombre as subcat, c.idcategoria as idc, c.nombre as cat from subcategorias s left join categorias c on s.categoria = c.idcategoria");
        while($fila=$subcategorias->fetch_object()){
            //Consigo el nombre de la categoría
            echo "{idc: " . $fila->idc . ", c: '" . $fila->cat . "', ids: " . $fila->ids . ", s:'" . $fila->subcat . "'},";
        }
        echo "];"

    ?>
    
    function cargaCategoria(){
        select = document.getElementById('selectcat');

        for (var i = 0; i<categorias.length; i++){
...

Puntos importantes en el código PHP:

  • Hacemos una consulta: «SELECT * from categorias» para crear el objeto de JavaScript con la lista de categorías.
  • Después tenemos que hacer un SELECT para cargar las subcategorías, pero este es un poco más complicado porque tenemos que usar un «left join» para unir las dos tablas y conseguir el nombre de las categorías a partir del «idcategoria» que tenemos en la tabla de subcategorías.
  • Una vez que tenemos los dos «objetos» con categorías y subcategorías, el resto del código es igual que en el ejemplo de JavaScript.

Otra posibilidad sería la de generar los SELECT directamente desde PHP, pero creo que de esta manera resulta más comprensible el ejemplo.

Por último, hay que tener claro que esta forma de hacerlo sin AJAX, tiene ventajas e inconvenientes. La ventaja es que nos ahorramos una llamada al servidor, pero si la lista total de subcategorías es muy grande, acabaría ralentizando la carga de la página.

Puedes descargar aquí el código de los ejemplos incluyendo un volcado de una base de datos de MySQL con los municipios y provincias con el que podrás ponerlo a prueba. Necesitarás un servidor MySQL.

Para el listado de provincias y municipios he adaptado el que aparece en https://www.harecoded.com/volcado-mysql-municipios-provincias-espanolas-territorios-ue-1963492/ cambiando solo el nombre de algunos campos y poco más