Arquitectura de dos niveles

En esta lección se describe una arquitectura de dos niveles que se puede utilizar para resolver los ejercicios del apartado "Con sesiones" de estos apuntes.

Introducción

La "arquitectura del software" es la rama de la informática que establece modelos generales para estructurar las aplicaciones o sistemas, de manera que se puedan desarrollar e implementar de forma coherente. Unos de los modelos más extendidos son los llamados modelos multinivel o multicapa, en los que cada nivel se ocupa de un aspecto de la aplicación.

Por ejemplo, una aplicación se podría estructurar mediante tres capas:

Por supuesto, existen muchos modelos alternativos con más o menos capas, definidas atenidiendo a distintos criterios y en los que determinadas partes pueden encontrarse concentradas o distribuidas en unas capas u otras. Y obviamente, seguir un modelo no garantiza que un programa sea correcto o que un programa no pueda ser correcto por seguir un determinado modelo o por no seguir ninguno. Es importante señalar que hay términos que en un modelo de arquitectura significan una cosa y en otro modelo significan otra, lo que a veces induce a la confusión.

Los ejercicios propuestos en este curso en los apartados de Sin formularios o Con formularios son programas cortos que consisten en una única página web (o como mucho dos, siendo una de las páginas un simple formulario) que se ocupa tanto de los cálculos como la presentación de los resultados. Estos programas son programas "monolíticos" en los que las capas no están claramente separadas y el programa va intercalando la realización de los cálculos y la presentación de los resultados, sin intervención del usuario (en el caso de los ejercicios Sin formularios) o con apenas intervención (en el caso de los ejercicios Con formularios).

En estos programas cortos seguramente no se obtendrían demasiados beneficios separando las capas, pero a medida que se va añadiendo interactividad a los programas o que los programas hacen más tareas sí que se pueden obtener beneficios, tanto al escribir el código como al reutilizarlo.

Gran parte de los ejercicios propuestos en este curso en el apartado Con sesiones se pueden resolver con una arquitectura de dos capas:

Estos ejercicios no conservan los datos a largo plazo por lo que no requieren la cada de persistencia.

Dependiendo del ejercicio, cada una de estas dos capas estará formada por una o varias páginas web, pero la idea es que las páginas de cada capa se ocupen solamente de una tarea: o bien mostrar contenido al usuario, o bien hacer cálculos para determinar el contenido que se mostrará al usuario. Para cumplir el ideal del modelo, las páginas de la capa de presentación no deberían contener nada de lógica, pero lo hacen eso no significa que la aplicación sea incorrecta, simplemente significa no que se ajusta estrictamente al modelo.

Ejemplo de arquitectura de dos capas

Como ejemplo de aplicación con una arquitectura de dos capas consideremos este programa:

Enlace a ejemplo

Este programa muestra un número entero que el usuario puede aumentar y disminuir o poner a cero haciendo clic en los botones.

Páginas y capas

El usuario visualiza siempre la misma página, pero esta página contiene un formulario que envía los datos a una segunda página. Si el usuario sólo ve la primera página es que la segunda página redirige automáticamente a la primera, como ilustra la imagen siguiente.

pagina-1.php formulario action Capa de presentación pagina-2.php header Capa lógica

Siguiendo el modelo de aplicación en dos capas, la página 1 contendrá la capa de presentación y la página 2 contendrá la capa lógica. Es decir, la página 1 es la que generará (mediante instrucciones print) la página web que verá el usuario, mientras que la página 2 es la que modificará el valor numérico y no se mostrará nunca al usuario (no contendrá ningún fragmento HTML ni ninguna instrucción print).

Sesiones

Para que ambas páginas puedan compartir el valor numérico utilizaremos sesiones. la sesión contendrá únicamente una variable de sesión, el número a mostrar.

Formulario

Con respecto al formulario, este no contiene controles específicos, sino tres botones de tipo submit con el mismo name y diferentes atributos value. Independientemente del botón que pulse el usuario, se enviará siempre el mimsmo control, con diferentes valores según el botón pulsado. Por tanto la página recibirá un control con tres posibles valores. Dependiendo del valor recibido, modificará la variable de sesión (aumentándola, reduciéndola o dejándola a 0).

Valores iniciales de las variables de sesión

Puede haber aplicaciones en los que no sea necesario disponer de unos valores iniciales en las variables de sesión, pero normalmente sí que son necesarios puesto que el interfaz de la aplicación se genera usando las variables de sesión. En la aplicación de ejemplo, la variable de sesión debería tener el valor 0.

Podemos asignar el valor de varias formas, pero la idea es que cualquier página debe asignar valores iniciales si las variables de sesión no están definidas, de manera que no se produzcan errores.

La manera más simple es hacerlo en todas las páginas que utilicen la sesión, como podríamos hacer en la aplicación de ejemplo.

página 1 (presentación)
página 2 (lógica)
<?php
// Accedemos a la sesión
session_name("arquitectura-1");
session_start();

// Si el número no está guardado en la sesión, ponemos el valor a cero
if (!isset($_SESSION["numero"])) {
    $_SESSION["numero"] = 0;
}

// ...
<?php
// Accedemos a la sesión
session_name("arquitectura-1");
session_start();

// Si el número no está guardado en la sesión, ponemos el valor a cero
if (!isset($_SESSION["numero"])) {
    $_SESSION["numero"] = 0;
}

// ...

Este código es correcto, pero la experiencia indica que hacerlo así puede ser una fuente de errores ya que si posteriormente se modifica la asignación de valores iniciales en una de las páginas hay que acordarse de cambiarlo en la otra y es fácil acabar con incilizaciones distintas. Por ello, puede ser aconsejable que las variables de sesión se inicialicen solamente en una página y que el resto de páginas redirijan a ella en caso necesario, como muestra el siguiente ejemplo.

página 1 (presentación)
página 2 (lógica)
<?php
// Accedemos a la sesión
session_name("arquitectura-1-1");
session_start();

// Si el número no está guardado en la sesión, ponemos el valor a cero
if (!isset($_SESSION["numero"])) {
    $_SESSION["numero"] = 0;
}

// ...
<?php
// Accedemos a la sesión
session_name("arquitectura-1-1");
session_start();

// Si el número no está guardado en la sesión, redirigimos a la primera página
if (!isset($_SESSION["numero"])) {
    header("Location:arquitectura-1-1.php");
    exit;
}

// ...

El hecho de que la página 2 redirija a la página 1 si las variables de sesión no están definidas nos proteje de paso frente ante atacantes que llamen directamente a la página 2 sin pasar por el formulario, que lo único que consiguen ahora es que se les muestre la página 1.

El único pero que se puede plantear a este código es que al establecer el valor inicial en la página 1, de alguna manera estamos incumpliendo el principio del modelo que dice que la capa de presentación sólo debe utilizar los datos definidos por la capa lógica, no establecerlos o modificarlos. Para evitar esa situación, lo tendríamos que hacer al revés, como muestra el siguiente ejemplo.

página 1 (presentación)
página 2 (lógica)
<?php
// Accedemos a la sesión
session_name("arquitectura-1-1");
session_start();

// Si el número no está guardado en la sesión, redirigimos a la segunda página
if (!isset($_SESSION["numero"])) {
    header("Location:arquitectura-1-2.php");
    exit;
}

// ...
<?php
// Accedemos a la sesión
session_name("arquitectura-1-1");
session_start();

// Si el número no está guardado en la sesión, ponemos el valor a cero
if (!isset($_SESSION["numero"])) {
    $_SESSION["numero"] = 0;
}

// ...

// Volvemos al formulario
header("Location:arquitectura-1-1.php");

Este código también es correcto, aunque la primera vez que se abra la página 1 (y por tanto no esté definida la variable de sesión), la página 1 redirigirá a la página 2, que establecerá la variable de sesión y finalmente redirigirá a la página 1, que ya podrá mostrar el interfaz.

Datos recibidos desde el formulario

Otro detalle importante es que la segunda página en este caso no necesita hacer ninguna comprobación de los datos recibidos, sino simplemente efectuar la operación solicitada si el valor recibido es correcto (es decir, uno de los esperados). En el caso de aplicaciones en las que la página 2 sí recibe datos que necesitan ser comprobados, la página 2 debe comprobar esos datos y modificar las variables de sesión como corresponda en cada caso. Si se quiere notificar el problema al usuario, debería hacerse también a través de variables de sesión para que sea siempre la página 1 la que presente la información.

Comprobación del funcionamiento de la aplicación

Por último, debemos comprobar siempre la corrección y robustez de la aplicación ante ataques de inyección, cerrando todas las ventanas del navegador y probando siempre qué ocurre:

Solución del ejemplo

La arquitectura comentada en los ejercicios anteriores se puede conseguir con una estructura del código en las dos páginas bastante sencilla, como la que se comenta a continuación.

Los fragmentos de código incluyen unos números situados en círculos que indican el orden en que se puede ir completando el programa. Los fragmentos del 1 al 6 implementan el funcionamiento básico de la aplicación, es decir, que al pulsar los botones se vuelva a mostrar la misma página, aunque sin modificaciones. Los fragmentos 7 y 8 implementan las modificaciones solicitadas con los diferentes botones.

Los comentarios de cada fragmento que se pueden leer antes del código también se pueden visualixzar situando el cursor encima del número situado en el círculo.

  1. La página 1 completa el interfaz de usuario mostrando el número. Como ese número se establece y modifica en la página 2, debe compartirse mediante sesiones, por ejemplo con el nombre $_SESSION["numero"].
  2. Para que la página 1 acceda a la sesión para recuperar el número que hay que mostrar en pantalla, debemos unir la página 1 a la sesión. Conviene que la sesión tenga nombre para que cada ejercicio utilice su propia matriz $_SESSION distinta del resto de ejercicios.
  3. Cuando se abre la página 1 por primera vez, $_SESSION no contiene ningún dato e intentar imprimir alguno provocaría un error. Por ello, en caso de que no exista la variable de sesión $_SESSION["numero"], redirigimos a la página 2 para que establezca su valor inicial.
  4. Para que la página 2 pueda establecer o modificar los datos guardados en la sesión, debemos unir la página a la misma sesión que la página 1.
  5. Si la variable de sesión $_SESSION["numero"] no está definida, establecemos su valor inicial, en este caso 0.
  6. La página 2 debe terminar redirigiendo siempre a la página 1.
  7. La página 2 debe recoger el control enviado por el formulario de la página 1. En la página 1 hay 3 botones, pero los tres tienen el mismo name, por lo que sólo hay que recoger un control.

    Como en cualquier página que recibe un control de un formulario, añadimos la función recoge() y definimos una variable para recoger el control.

  8. Dependiendo del dato recibido, aumentamos, reducimos o ponemos a cero la variable de sesión.

arquitectura-1-1.php

<?php
// Accedemos a la sesión 2
session_name("arquitectura-1-1");
session_start();

// Si el número no está guardado en la sesión, redirigimos a la segunda página 3
if (!isset($_SESSION["numero"])) {
    header("Location:arquitectura-1-2.php");
    exit;
}
?>
<!DOCTYPE html>
// ...
    <p>
      <button type="submit" name="accion" value="bajar" style="font-size: 4rem">-</button>
<?php
// Mostramos el número, guardado en la sesión 1
print "      <span style=\"font-size: 4rem\">$_SESSION[numero]</span>\n";
?>
      <button type="submit" name="accion" value="subir" style="font-size: 4rem">+</button>
    </p>
// ...

arquitectura-1-2.php

<?php
// Accedemos a la sesión 4
session_name("arquitectura-1-1");
session_start();

// Si el número no está guardado en la sesión, ponemos el valor a cero
if (!isset($_SESSION["numero"])) {5
    $_SESSION["numero"] = 0;
}

// Función de recogida de datos 7
function recoge($key, $type = "")
{
. . . if (!is_string($key) && !is_int($key) || $key == "") { trigger_error("Function recoge(): Argument #1 (\$key) must be a non-empty string or an integer", E_USER_ERROR); } elseif ($type !== "" && $type !== []) { trigger_error("Function recoge(): Argument #2 (\$type) is optional, but if provided, it must be an empty array or an empty string", E_USER_ERROR); } $tmp = $type; if (isset($_REQUEST[$key])) { if (!is_array($_REQUEST[$key]) && !is_array($type)) { $tmp = trim(htmlspecialchars($_REQUEST[$key])); } elseif (is_array($_REQUEST[$key]) && is_array($type)) { $tmp = $_REQUEST[$key]; array_walk_recursive($tmp, function (&$value) { $value = trim(htmlspecialchars($value)); }); } } return $tmp;
} // Recogemos accion $accion = recoge("accion"); // Dependiendo de la acción recibida, modificamos el número guardado 8 if ($accion == "cero") { $_SESSION["numero"] = 0; } elseif ($accion == "subir") { $_SESSION["numero"]++; } elseif ($accion == "bajar") { $_SESSION["numero"]--; } // Volvemos al formulario 6 header("Location:arquitectura-1-1.php");