Hacer un sistema de puntuación con PHP y jQuery
Introducción
En muchas páginas se utilizan sistemas de puntuación. Normalmente suelen ser de 5 estrellas, en los que la gente vota que le ha parecido del 1 al 5. Desde el punto de vista de los desarrolladores plantéa muchas cuestiones interesantes, algunas más complejas que otras. En cualquier caso no es demasiado difícil, por lo que podemos cubrirlo en un solo tutorial.
Aquí vamos a diseñar, implementar y sobre todo explicar como hacer un sistema de puntuación de 5 estrellas con decimales intermedios, por lo que la gente puede votar cosas como 4.5 o 1.5. Además un mismo usuario no podrá votar más de una vez, por lo que intentaremos hacer este sistema de votación lo más acorde con los gustos reales de la media de los usuarios.
Paso 1. Diseño
Antes que nada tenemos que hacer el maquetado del sistema de puntuación. Tenemos que pensar en términos de medias estrellas, porque vamos a admitir la votación de 4.5, por ejemplo, como hemos comentado en la introducción. Esto plantéa la primera dificultad, porque ¿cómo hacemos para unirlas todas? o ¿cómo sabemos que el usuario está seleccionando una u otra parte de la estrella? Podrías pensar en utilizar 5 imágenes, de tres tipos, estrellas vacías, medias estrellas y estrellas enteras. Pero entonces no podrías diferenciar fácilmente si un usuario está en la parte izquierda o en la derecha. Así que es un poco más complicado que eso.
En realidad gran parte de la solución vendrá con jQuery, pero de momento hay un hecho a remarcar. Tenemos que usar sprites. Sprite es una imagen compuesta de varias, que se envía una vez, y con CSS se selecciona la imagen dentro del archivo que queremos mostrar. Normalmente se usa para hacer más rápida la carga de la página, porque el navegador y el servidor sólo se tienen que conectar una vez para pasar todas las imágenes en un solo archivo. Pero en nuestro caso no es esa cuestión la más importante, lo importante es que si no tuviéramos sprites, cuando cambiáramos parte de una estrella tendríamos que enviar el archivo, después de finalizar la carga de la página. Esto supone una carga innecesaria en el ancho de banda fuera de tiempo.
Echemos un vistazo al código HTML, será algo bastante simple, basándonos en nuestra implementación concreta y dado que solo queremos 5 estrellas.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 | <div id="1" class="puntuacion"> <div class="estrella estrella_izq"><a title="0.5/5" href="#0.5">0.5</a></div> <div class="estrella estrella_der"><a title="1/5" href="#1">1</a></div> <div class="estrella estrella_izq"><a title="1.5/5" href="#1.5">1.5</a></div> <div class="estrella estrella_der"><a title="2/5" href="#2">2</a></div> <div class="estrella estrella_izq"><a title="2.5/5" href="#2.5">2.5</a></div> <div class="estrella estrella_der"><a title="3/5" href="#3">3</a></div> <div class="estrella estrella_izq"><a title="3.5/5" href="#3.5">3.5</a></div> <div class="estrella estrella_der"><a title="4/5" href="#4">4</a></div> <div class="estrella estrella_izq"><a title="4.5/5" href="#4.5">4.5</a></div> <div class="estrella estrella_der"><a title="5/5" href="#5">5</a></div> </div> <!-- fin puntuacion #1 --> <div id="num_votos"> <p>Votos: </p> </div> <!-- fin #num_votos --> |
Cada línea dentro del div con id 1 representa media estrella, probablemente te parezca raro que no haya ningún tag , pero no te preocupes, verás la razón en el código jQuery. Además, para codificar el CSS debemos pensar en cuantos “estados” van a tener nuestras estrellas. La respuesta es 3, uno para cuando esté vacía, otro para cuando esté establecida y otro para el estado hover, cuando un usuario esté poniendo el ratón por encima. Mira este gráfico para aclararte un poco:
Por tanto, en el CSS tendremos en cuenta esto. Y ya que estamos, recordaremos que tenemos sprites, por lo que el fondo, se va a realizar simplemente con CSS sobre la misma imagen. En concreto debemos tener un sprite para la parte derecha de las distintas estrellas y uno para la parte izquierda. Adicionalmente, añadimos un sprite para las estrellas enteras, pero esto es una mera formalidad y en realidad no se usará. Nuestro CSS quedará así:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 | .puntuacion { cursor: pointer; margin-right: 5px; clear: both; display: block; float: right; overflow: visible; } .puntuacion:after { content: '.'; display: block; height: 0; width: 0; clear: both; visibility: hidden; } .estrella { float: left; width: 35px; height: 35px; overflow: hidden; display: inline-block; text-indent: -999em; cursor: pointer; margin-top: 1em; } .estrella_izq, .estrella_der { width: 18px } .estrella, .estrella a { background: url('imagenes/estrella_sprite.png') no-repeat 0 0px; } .estrella_izq, .estrella_izq a { background: url('imagenes/estrella_sprite_izq.png') no-repeat 0 0px; } .estrella_der, .estrella_der a { background: url('imagenes/estrella_sprite_der.png') no-repeat 0 0px; } .estrella a { display: block; width: 100%; height: 100%; background-position: 0 0px; /* Estrella Vacía */ } div.puntuacion div.on a { background-position: 0px -45px; /* Estrella Establecida */ } div.puntuacion div.hover a, div.puntuacion div a:hover { background-position: 0px -90px; /* Estrella Hover */ } |
Los detalles de como maquetarlo para que quede lo mejor posible no son demasiado importantes, quizá destacar que nos movemos por el sprite para seleccionar los diferentes tipos de estrellas. Además cuando establezcas un elemento con una imagen como background, debes tener en cuenta que si el elemento no tiene ancho definido, debes hacerlo para que el fondo se vea correctamente.
Paso 2. Plugin en jQuery
Para realizar toda la funcionalidad que necesitamos podremos simplemente codificarlo en jQuery, pero de cara a una posible mejora en el futuro, meteremos todo el código en un plugin hecho por nosotros. Así que veamos en primer lugar como se crea un plugin muy rápidamente:
1 2 3 4 5 6 7 | // Declaración del plugin jQuery.fn.plugin_tonto = function(param1, param2) { alert('Soy un plugin y estoy funcionando'); }; //Llamada al plugin $('#elemento').plugin_tonto('valor1', 'valor2'); |
Como puedes ver es muy simple, es importante resaltar, como probablemente ya supongas, que en $(this) dentro del plugin tendremos el elemento que lo ha llamado, en este caso $(‘#elemento’).
Así que ahora empezemos a codificar nuestro plugin de verdad. Debemos tener 1 parámetro, la url donde vamos a mandar los datos para que se procesen (será un script en PHP, que veremos luego). Además este plugin se va a encargar de mucha cosas, por lo que mejor vemos parte por parte desde arriba del código hasta el final.
Funciones con Cookies y estableciendo variables
Aquí vemos como establecemos una serie de variables necesarias para que el plugin funcione, como la url a dónde mandaremos la petición POST, o la serie de estrellas que tenemos en nuestro HTML. Además utilizamos dos funciones para manejar las cookies en javascript plano.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 | function createCookie(name,value,days) { if (days) { var date = new Date(); date.setTime(date.getTime()+(days*24*60*60*1000)); var expires = "; expires="+date.toGMTString(); } else var expires = ""; document.cookie = name+"="+value+expires+"; path=/"; } function readCookie(name) { var nameEQ = name + "="; var ca = document.cookie.split(';'); for(var i=0;i < ca.length;i++) { var c = ca[i]; while (c.charAt(0)==' ') c = c.substring(1,c.length); if (c.indexOf(nameEQ) == 0) return c.substring(nameEQ.length,c.length); } return null; } |
Este par de funciones se sale del objetivo del tutorial, pero básicamente sirven para establecer y guardar cookies, usando javascript. Si quieres más información la tienes por aquí.
1 2 3 4 5 6 7 8 9 | if(url == null) return; var container = jQuery(this); jQuery.extend(container, { puntos: 0, url: url }); var estrellas = jQuery(container).children('.estrella'); |
Línea 6. Establecemos los atributos puntos y url, como si pertenecieran a container. De tal forma que cuando queramos acceder a ellos simplemente haremos container.url o container.puntos
Línea 9. Seleccionamos todas las estrellas (en realidad serán media estrellas) y tendremos un total de 10 elementos en la variable estrellas.
Definiendo eventos sobre las estrellas
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 | estrellas .mouseover(function(){ eventos.quitar(); eventos.llenar(this); }) .mouseout(function(){ eventos.quitar(); eventos.reiniciar(); }) .focus(function(){ eventos.quitar(); eventos.llenar(this); }) .blur(function(){ eventos.quitar(); eventos.reiniciar(); }); //Cuando se hace click en una estrella estrellas.click(function(){ if (readCookie('votado') == null){ container.puntos = (estrellas.index(this) * .5) + .5; $.post(container.url, { "id": $(this).parent().attr('id'), "puntos": $(this).children('a')[0].href.split('#')[1] }); createCookie("votado", 1, 365); eventos.establecer_de_BD(); } }); |
En la primera parte del código establecemos las acciones a realizar cuando pase una de esos eventos predefinidos en jQuery. Todas las llamadas que son eventos.algo pertenecen a un array que definiremos después.
Además definimos el evento click sobre cualquiera de las estrellas. Esto generará que se calcule la puntuación seleccionada y se envía a través del método POST a la url pasada al plugin. Antes de hacer nada para no molestar al servidor si realmente el usuario no puede votar porque ya lo ha hecho, miramos si tiene la cookie que pusimos en una ejecución anterior.
Si realmente es su primera vez votando, se manda la actualización, se establece una cookie para que no vote de nuevo y cuando se ha finalizado el envío se hace una consulta a la BD para actualizar la nueva puntuación. Esto resulta un poco ineficiente pero no quería perder el lado docente del tutorial, y creo que es más claro. En cualquier caso debes tener en mente que se podría devolver la nueva puntuación directamente desde la primera petición POST.
Algunas funciones útiles e inicio de la puntuación
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 | var eventos = { // llenar hasta la posición del ratón. llenar: function(el) { var index = estrellas.index(el) + 1; estrellas .children('a').css('width', '100%').end() .slice(0,index).addClass('hover').end(); }, // quitar todas las estrellas. quitar: function() { estrellas .filter('.on').removeClass('on').end() .filter('.hover').removeClass('hover').end(); }, // reiniciar el sistema. reiniciar: function() { var unidad = parseInt(container.puntos); var decimales = container.puntos - unidad; var num_estrellas = (unidad * 2) + 1; if (decimales >= .5) num_estrellas += 1; estrellas.slice(0, num_estrellas).addClass('on').end(); }, establecer_de_BD: function() { //Establecemos la puntuación desde la BD $.post(container.url, { "peticion": 1, "id": estrellas.parent().attr('id') }, function(data){ $('#num_votos p').html("Votos: " + data.votos); container.puntos = data.puntuacion; eventos.reiniciar(); }, "json"); } }; //Al principio establecemos el sistema con el valor que corresponde eventos.establecer_de_BD(); return(this); |
Línea 3. Ahora creamos un array del que antes hemos hablado, llamado eventos, es un array asociativo compuesto por funciones. En esta línea definimos la primera función, que se encarga de ir componiendo las estrellas con la clase hover, para dar el efecto de llenado progresivo según se mueve el ratón.
Línea 11. Esta función se encarga de quitar el efecto hover cuando dejamos de poner el ratón encima de las estrellas.
Línea 18. Reiniciar es una funcíón para establecer el número de medias estrellas vacías que tenemos, para ello se “parte” el número entre la unidad y los decimales, y se opera con ellos. Los detalles los puedo explicar en los comentarios, pero es relativamente sencillo, teniendo en cuenta que tenemos siempre el doble de elementos por ser medias estrellas. Así que he establecido que si se tiene de X a X.5 se marca la mitad de una estrella, y si se tiene de X.5 a X+1 se tiene otra nueva estrella completa.
Línea 29. Para que el sistema de puntuación funcione necesitamos ver el número de votos y la puntuación de la base de datos y establecerla con la funcion reiniciar. Así que se hace una petición POST y con el resultado en json se establece el resultado.
En la parte final del plugin ejecutamos por primera vez establecer_de_BD para poner el número de estrellas cuando se carga la página así como el número de votos.
Paso 3. Parte servidor con PHP y MySQL
Hasta ahora tenemos una serie de estrellas que al hacer click en ellas nos mandan los datos actualizados y cuando se cargan nos lo piden. Pero de momento no tenemos un script que se encargue de recibir esas peticiones y actuar en consecuencia.
Es por ello que necesitamos pensar en dos cosas, qué vamos a hacer y cómo vamos a guardar los datos de las puntuaciones. Contestemos primero al problema de almacenar las puntuaciones.
MySQL es la solución
Contradiciendo a la navaja de Ockham, en este caso la solución más compleja es la mejor. Y es que podríamos planternos la posibilidad de guardar los votos en un archivo de texto, por ejemplo, pero esto resultaría en problemas de concurrencia, que MySQL resuelve por sí mismo, sin ni siquiera tener que saber como desarrollador que algo llamado concurrencia existe (uno de los temas más complejos en programación, por cierto).
Así que creemos una tabla realmente simple donde meteremos nuestros datos:
Para crearla haríamos lo siguiente con un IDE de bases de datos o desde un terminal:
1 2 3 4 5 6 7 | CREATE TABLE puntuaciones ( id INT(11) NOT NULL AUTO_INCREMENT, puntos DOUBLE PRECISION NOT NULL, votos INT(11) DEFAULT NULL, PRIMARY KEY (id) ) CHARACTER SET=utf8; |
Completando el sistema con PHP
Por último necesitamos un archivo de configuración para simplificar la defición de constantes, una clase para facilitar el acceso a la BD y un script (post.php) para recibir y atender las peticiones.
Nuestro esquema de comunicación se puede resumir en algo así, siendo el plugin de jQuery el encargado de comunicarse con el servidor y de lidiar con las votaciones repetidas.
Así que vamos con el primer archivo PHP.Este archivo de configuración será simplemente:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 | <?php /** El nombre de tu base de datos */ define('DB_NAME', 'nombre_base_de_datos'); /** Tu nombre de usuario de MySQL */ define('DB_USERNAME', 'usuario'); /** Tu contraseña de MySQL */ define('DB_PASSWORD', 'pass'); define('DB_HOSTNAME', 'hostname'); /** Codificación de caracteres para la base de datos. */ define('DB_CHARSET', 'utf8'); ?> |
La clase, llamada puntos.class.php será bastante sencilla también:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 | <?php require_once 'config.inc.php'; class Puntos { private $db; private $id; function __construct($id) { $this->db = mysql_connect(DB_HOSTNAME, DB_USERNAME, DB_PASSWORD); mysql_set_charset(DB_CHARSET, $this->db); mysql_select_db(DB_NAME, $this->db); $this->id = mysql_real_escape_string($id); $query = "INSERT IGNORE INTO puntuaciones (id, votos, puntos) " ."VALUES ($this->id,0,0)"; mysql_query($query); } |
Incluimos el archivo de configuración y en el constructor hacemos tres cosas:
- Establecemos la conexión con la base de datos.
- Establecemos como atributo privado de la clase el id del sistema de puntuación que nos pasan.
- Insertamos una nueva fila con votos y puntuación a 0 si no existía ya.
1 2 3 4 5 6 7 8 9 10 | function getPuntuacion() { $query = "SELECT puntos as puntuacion, votos " ."FROM puntuaciones " ."WHERE id = $this->id"; $resultado = mysql_query($query, $this->db); return mysql_fetch_assoc($resultado); } |
Aquí recogemos en un array asociativo la consulta de el número de votos y los puntos que tiene un determinado sistema. Otra vez, lo sabremos por el id que nos pasaron en el constructor y que ya tenemos guardado como atributo.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 | function setPuntuacion($puntos) { if (isset($_COOKIE['votado'])) { return true; } else { $puntos = mysql_real_escape_string($puntos); $query = "UPDATE puntuaciones ". "SET puntos = ((puntos * votos) + $puntos) / (votos + 1), ". "votos = votos + 1 ". "WHERE id = $this->id"; return mysql_query($query); } } } //Fin de la clase ?> |
Este último método mira si se tiene una cookie previamente establecida y si no es así actualiza la base de datos con los puntos pasados como parámetro, aumentando también el número de votos a uno más de los que había. La comprobación de la cookie no es estrictamente necesaria, porque previamente el plugin se ha encargado de ver eso, pero no está de más y no supone casi carga al servidor.
El sistema de establecer una cookie cuando un usuario vote para que no lo haga más no es una solución perfecta, porque se puede saltar fácilmente, pero es una primera aproximación y quizá en un futuro enseñe como mejorar esto. En cualquier caso se puede ver un uso práctico de las cookies con PHP y una aplicación real.
Script que recibe las peticiones
Por último tenemos nuestro archivo post.php en el que recibimos las peticiones:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 | <?php //Incluimos la clase puntos require_once 'puntos.class.php'; if (!empty($_POST) && !empty($_POST['id'])) { $pt = new Puntos($_POST['id']); if (!empty($_POST['peticion']) && $_POST['peticion']) { echo json_encode($pt->getPuntuacion()); } elseif (!empty($_POST['puntos'])) { echo $pt->setPuntuacion($_POST['puntos']); } } ?> |
Línea 8. Nos han hecho una petición para saber el número de votos y la puntuación. Así que llamamos al método definido antes que se encarga de manejar esa petición.
Línea 11. Establecemos la nueva puntuación y aumentamos el número de votos en 1. Nuestro método ya se encarga de comprobar la cookie y de limpiar la entrada de datos, usando mysql_real_escape_string, como hemos comentado antes.
Conclusiones
Si bien es cierto que hay cosas que pulir, como por ejemplo en términos de accesibilidad si no se tiene javascript activado, o de seguridad, para evitar que un usuario pueda saltarse el control que hemos hecho con la cookie, este tutorial cubre muchos puntos, que espero, te hayan sido útiles.
El plugin está basado en uno mucho más complejo y grande que ya va por su versión 3.13. Lo puedes encontrar aquí.
Las estrellas usadas (aunque estaban enteras) son de Customicon Design.
Si tienes cualquier duda, no dudes en preguntar en la sección de comentarios.

4 Comentarios
Muy buen articulo, y muy buen blog, hoy lo descubri y pasare a visitarlo diariamente. Saludos!
@mitulian. Muchas gracias, espero verte por aquí!
muy bueno ya lo estoy por probar, de antemano muchas gracias
exelente articulo, me gustaria si pudieras ayudarme con un ejercicio de como hacer un diagrama de clases para un sistema de votacion.gracias