Hace un tiempo te mostré un contador de visitas simple en PHP. Ahora te traigo una versión avanzada que brinda además gráficas y reportes.
Como bien sabes, anteriormente hicimos un script para contar los visitantes y visitas, pero fue algo muy simple y no contaba con total precisión.
En el sistema que te mostraré ahora se van a contar las visitas y visitantes, además de mostrar una gráfica con la cantidad de visitas y visitantes que se hayan tenido en un período.
Del mismo modo se van a mostrar las páginas más vistas según determinada fecha, y del mismo modo vamos a ver cómo han visitado determinada página en un rango de fechas.
Todo el sistema que te presentaré es gratuito y open source, así que a través del post colocaré el código fuente.
Reporte de visitas
Comencemos viendo el dashboard en donde podemos ver un resumen de las visitas de los usuarios. Tenemos una gráfica que muestra, a lo largo de un período de tiempo, las visitas y visitantes que hemos tenido.
Nosotros podemos cambiar el período de tiempo ya que por defecto el período es el mes actual de inicio a fin.
Además de ello, tenemos una segunda tarjeta en donde se muestran las páginas más visitadas en una fecha (por defecto hoy) junto con el total de visitas y visitantes. Igualmente podemos cambiar esta fecha para ver el reporte de otro día.
Si queremos podemos ver las estadísticas de una página en específico, filtrando además por fecha:
Registrar visita
Para registrar una visita y contarla con PHP vamos a usar un poco de JavaScript. Los bots que indexan el contenido o generan la vista previa no ejecutan JavaScript, pero los usuarios legítimos sí. Así que vamos a hacer que el conteo se haga con JavaScript.
Lo único que tienes que hacer es incluir el siguiente código en todos los lugares donde quieras llevar el registro. Solo recuerda configurar correctamente la URL en donde se encuentra registrar_visita.php
.
El código hace una petición al servidor con PHP para registrar la visita, usando la función nativa fetch.
(() => {
/*
Script que cuenta la visita y la envía al servidor con PHP
Solo tienes que incluir este script o código en todas las páginas en donde quieras registrar las visitas
y los visitantes
https://parzibyte.me/blog
*/
document.addEventListener("DOMContentLoaded", async () => {
try {
// Preferiblemente debería ser la URL absoluta
// Ejemplo: http://localhost/contador_visitas_php_avanzado/contador/registrar_visita.php
const url = "./contador/registrar_visita.php";
const payload = {
pagina: document.title,
url: window.location.href,
};
const respuestaRaw = await fetch(url, {
method: "POST",
body: JSON.stringify(payload),
});
const respuesta = await respuestaRaw.json();
if (!respuesta) {
console.log("Error registrando visita");
}
} catch (e) {
console.log("Error registrando visita: " + e);
}
});
})();
En mi caso tengo el script de JS y el contador de PHP en el mismo directorio, por lo que puedo referirme al directorio hermano con ./
.
Lo que se está guardando en nuestra base de datos es la IP del usuario, la fecha de la visita, el título de la página visitada y la URL de la misma. Pero no todos los datos vienen de un solo lugar.
Del lado del cliente tomamos el título y la URL, pero en el servidor guardamos la fecha junto con la IP del usuario en el siguiente script:
<?php
$payload = json_decode(file_get_contents("php://input"));
if (!$payload) {
exit("");
}
include_once "funciones.php";
$ok = registrarVisita($payload->pagina, $payload->url);
echo json_encode($ok);
Y la función registrarVisita
es la siguiente:
<?php
function registrarVisita($pagina, $url)
{
$fecha = date("Y-m-d");
$ip = $_SERVER["REMOTE_ADDR"] ?? "";
$bd = obtenerConexion();
$sentencia = $bd->prepare("INSERT INTO visitas(fecha, ip, pagina, url) VALUES(?, ?, ?, ?)");
return $sentencia->execute([$fecha, $ip, $pagina, $url]);
}
Ejemplo para contar visita
Por si no quedó claro, te muestro un ejemplo. Yo tengo el código de JavaScript en un archivo llamado contador.js
. Luego, en mi página en donde quiero llevar el registro, lo incluyo:
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Página de contacto</title>
</head>
<body>
<h1>Contacto</h1>
<a href="index.html">Ir al inicio</a>
<p>Lorem ipsum dolor sit amet consectetur adipisicing elit. Suscipit dolorum officiis ab dolores ipsam porro tenetur
eius soluta nobis voluptatibus debitis nulla, totam eveniet ipsa, saepe natus voluptate fuga. Exercitationem.
</p>
</body>
<!--
Incluir el script para contar la visita. Así de simple
-->
<script src="./contador.js"></script>
</html>
Y de este modo se va a registrar la visita cada que un usuario la visite, suponiendo que usa JavaScript y un navegador web decente.
Reportes de visitas y visitantes con PHP
Ya vimos cómo registrar la visita, ahora veamos lo necesario para obtener los datos. Lo único que necesitamos es hacer consultas a la tabla que registra las visitas. A continuación veremos las funciones que permiten:
- Obtener el conteo de visitas y visitantes
- Saber cuáles son las páginas más visitadas en un período de tiempo
- Conocer cuántas visitas y visitantes hay en un período de tiempo, día con día
- Obtener el registro de visitantes y visitas de una página específica dentro de un período de fechas
Repito que todos son consultas usando GROUP BY, COUNT, ORDER BY y consultas para filtrar las fechas lexicográficamente.
<?php
function fechaInicioYFinDeMes()
{
$inicio = date("Y-m-01");
$fin = date("Y-m-t");
return [$inicio, $fin];
}
function fechaHoy()
{
return date("Y-m-d");
}
/*
Nota: está limitado a solo traer los 10 primeros registros, ordenados por las veces que se visitaron
*/
function obtenerPaginasVisitadasEnFecha($fecha)
{
$consulta = "SELECT COUNT(*) AS conteo_visitas, count(distinct ip) as conteo_visitantes, url, pagina
from visitas where fecha = ?
group by url, pagina
ORDER BY conteo_visitas DESC
LIMIT 10;";
$bd = obtenerConexion();
$sentencia = $bd->prepare($consulta);
$sentencia->execute([$fecha]);
return $sentencia->fetchAll();
}
function obtenerConteoVisitasYVisitantesDePaginaEnRango($fechaInicio, $fechaFin, $url)
{
return (object)[
"visitantes" => obtenerConteoVisitantesDePaginaEnRango($fechaInicio, $fechaFin, $url),
"visitas" => obtenerConteoVisitasDePaginaEnRango($fechaInicio, $fechaFin, $url),
];
}
function obtenerConteoVisitantesDePaginaEnRango($fechaInicio, $fechaFin, $url)
{
$bd = obtenerConexion();
$sentencia = $bd->prepare("SELECT COUNT(DISTINCT ip) AS conteo FROM visitas WHERE fecha >= ? AND fecha <= ? AND url = ?");
$sentencia->execute([$fechaInicio, $fechaFin, $url]);
return $sentencia->fetchObject()->conteo;
}
function obtenerConteoVisitasDePaginaEnRango($fechaInicio, $fechaFin, $url)
{
$bd = obtenerConexion();
$sentencia = $bd->prepare("SELECT COUNT(*) AS conteo FROM visitas WHERE fecha >= ? AND fecha <= ? AND url = ?");
$sentencia->execute([$fechaInicio, $fechaFin, $url]);
return $sentencia->fetchObject()->conteo;
}
function obtenerVisitantesDePaginaEnRango($fechaInicio, $fechaFin, $url)
{
$bd = obtenerConexion();
$sentencia = $bd->prepare("SELECT fecha, COUNT(DISTINCT ip) AS conteo FROM visitas WHERE fecha >= ? AND fecha <= ? AND url = ? GROUP BY fecha");
$sentencia->execute([$fechaInicio, $fechaFin, $url]);
return $sentencia->fetchAll();
}
function obtenerVisitasDePaginaEnRango($fechaInicio, $fechaFin, $url)
{
$bd = obtenerConexion();
$sentencia = $bd->prepare("SELECT fecha, COUNT(*) AS conteo FROM visitas WHERE fecha >= ? AND fecha <= ? AND url = ? GROUP BY fecha");
$sentencia->execute([$fechaInicio, $fechaFin, $url]);
return $sentencia->fetchAll();
}
function obtenerConteoVisitasYVisitantesEnRango($fechaInicio, $fechaFin)
{
return (object)[
"visitantes" => obtenerConteoVisitantesEnRango($fechaInicio, $fechaFin),
"visitas" => obtenerConteoVisitasEnRango($fechaInicio, $fechaFin),
];
}
function obtenerConteoVisitantesEnRango($fechaInicio, $fechaFin)
{
$bd = obtenerConexion();
$sentencia = $bd->prepare("SELECT COUNT(DISTINCT ip) AS conteo FROM visitas WHERE fecha >= ? AND fecha <= ?");
$sentencia->execute([$fechaInicio, $fechaFin]);
return $sentencia->fetchObject()->conteo;
}
function obtenerConteoVisitasEnRango($fechaInicio, $fechaFin)
{
$bd = obtenerConexion();
$sentencia = $bd->prepare("SELECT COUNT(*) AS conteo FROM visitas WHERE fecha >= ? AND fecha <= ?");
$sentencia->execute([$fechaInicio, $fechaFin]);
return $sentencia->fetchObject()->conteo;
}
function obtenerVisitantesEnRango($fechaInicio, $fechaFin)
{
$bd = obtenerConexion();
$sentencia = $bd->prepare("SELECT fecha, COUNT(DISTINCT ip) AS conteo FROM visitas WHERE fecha >= ? AND fecha <= ? GROUP BY fecha");
$sentencia->execute([$fechaInicio, $fechaFin]);
return $sentencia->fetchAll();
}
function obtenerVisitasEnRango($fechaInicio, $fechaFin)
{
$bd = obtenerConexion();
$sentencia = $bd->prepare("SELECT fecha, COUNT(*) AS conteo FROM visitas WHERE fecha >= ? AND fecha <= ? GROUP BY fecha");
$sentencia->execute([$fechaInicio, $fechaFin]);
return $sentencia->fetchAll();
}
function registrarVisita($pagina, $url)
{
$fecha = date("Y-m-d");
$ip = $_SERVER["REMOTE_ADDR"] ?? "";
$bd = obtenerConexion();
$sentencia = $bd->prepare("INSERT INTO visitas(fecha, ip, pagina, url) VALUES(?, ?, ?, ?)");
return $sentencia->execute([$fecha, $ip, $pagina, $url]);
}
function obtenerVariableDelEntorno($key)
{
if (defined("_ENV_CACHE")) {
$vars = _ENV_CACHE;
} else {
$file = "env.php";
if (!file_exists($file)) {
throw new Exception("El archivo de las variables de entorno ($file) no existe. Favor de crearlo");
}
$vars = parse_ini_file($file);
define("_ENV_CACHE", $vars);
}
if (isset($vars[$key])) {
return $vars[$key];
} else {
throw new Exception("La clave especificada (" . $key . ") no existe en el archivo de las variables de entorno");
}
}
function obtenerConexion()
{
$password = obtenerVariableDelEntorno("MYSQL_PASSWORD");
$user = obtenerVariableDelEntorno("MYSQL_USER");
$dbName = obtenerVariableDelEntorno("MYSQL_DATABASE_NAME");
$database = new PDO('mysql:host=localhost;dbname=' . $dbName, $user, $password);
$database->query("set names utf8;");
$database->setAttribute(PDO::ATTR_EMULATE_PREPARES, FALSE);
$database->setAttribute(PDO::ATTR_ERRMODE, PDO::ERRMODE_EXCEPTION);
$database->setAttribute(PDO::ATTR_DEFAULT_FETCH_MODE, PDO::FETCH_OBJ);
return $database;
}
Como puedes ver en este código de PHP, tenemos varias consultas (ya sea INSERT o SELECT) a MySQL / MariaDB. Además, existen algunas funciones ayudantes que leen variables del archivo de entorno o trabajan con fechas.
Por cierto, cada IP distinta cuenta como un visitante distinto. Ya que un visitante siempre puede visitar distintas páginas, pero seguirá siendo un solo visitante aunque lleve varias visitas.
Código fuente del dashboard
La página principal en donde se muestra el reporte de visitas y visitantes está hecha con Bulma. El código es el siguiente:
<?php include_once "encabezado.php"; ?>
<?php
include_once "funciones.php";
$hoy = fechaHoy();
list($inicio, $fin) = fechaInicioYFinDeMes();
if (isset($_GET["inicio"])) {
$inicio = $_GET["inicio"];
}
if (isset($_GET["fin"])) {
$fin = $_GET["fin"];
}
if (isset($_GET["hoy"])) {
$hoy = $_GET["hoy"];
}
$visitasYVisitantes = obtenerConteoVisitasYVisitantesEnRango($hoy, $hoy);
$paginas = obtenerPaginasVisitadasEnFecha($hoy);
$visitantes = obtenerVisitantesEnRango($inicio, $fin);
$visitas = obtenerVisitasEnRango($inicio, $fin);
?>
<section class="section">
<div class="columns">
<div class="column">
<div class="card">
<header class="card-header">
<p class="card-header-title">
Estadísticas entre <?php echo $inicio ?> y <?php echo $fin ?>
</p>
</header>
<div class="card-content">
<div class="content">
<form action="dashboard.php">
<input type="hidden" name="hoy" value="<?php echo $hoy ?>">
<div class="field is-grouped">
<p class="control is-expanded">
<label>Desde: </label>
<input class="input" type="date" name="inicio" value="<?php echo $inicio ?>">
</p>
<p class="control is-expanded">
<label>Hasta: </label>
<input class="input" type="date" name="fin" value="<?php echo $fin ?>">
</p>
<p class="control">
<!--La etiqueta es invisible a propósito para que tome el espacio y alinee el botón-->
<label style="color: white;">ª</label>
<input type="submit" value="OK" class="button is-success input">
</p>
</div>
</form>
<canvas id="grafica"></canvas>
</div>
</div>
<footer class="card-footer">
<small class="mx-2 my-2">By parzibyte</small>
</footer>
</div>
</div>
<div class="column is-one-third ">
<div class="card">
<header class="card-header">
<p class="card-header-title">
Estadísticas de <?php echo $hoy ?>
</p>
</header>
<div class="card-content">
<div class="content">
<form action="dashboard.php" class="mb-2">
<input type="hidden" name="inicio" value="<?php echo $inicio ?>">
<input type="hidden" name="fin" value="<?php echo $fin ?>">
<div class="field is-grouped">
<p class="control is-expanded">
<label>Fecha: </label>
<input class="input" type="date" name="hoy" value="<?php echo $hoy ?>">
</p>
<p class="control">
<!--La etiqueta es invisible a propósito para que tome el espacio y alinee el botón-->
<label style="color: white;">ª</label>
<input type="submit" value="OK" class="button is-success input">
</p>
</div>
</form>
<div class="field is-grouped is-grouped-multiline">
<div class="control">
<div class="tags has-addons">
<span class="tag is-success is-large">Visitas</span>
<span class="tag is-info is-large"><?php echo $visitasYVisitantes->visitas ?></span>
</div>
</div>
<div class="control">
<div class="tags has-addons">
<span class="tag is-warning is-large">Visitantes</span>
<span class="tag is-info is-large"><?php echo $visitasYVisitantes->visitantes ?></span>
</div>
</div>
</div>
<table class="table">
<thead>
<tr>
<th>Página</th>
<th>Visitas</th>
<th>Visitantes</th>
<th>Estadísticas</th>
</tr>
</thead>
<tbody>
<?php foreach ($paginas as $pagina) { ?>
<tr>
<td><a target="_blank" href="<?php echo $pagina->url ?>"><?php echo $pagina->pagina ?></a></td>
<td><?php echo $pagina->conteo_visitas ?></td>
<td><?php echo $pagina->conteo_visitantes ?></td>
<td>
<a class="button is-info" href="visitas_url.php?url=<?php echo urlencode($pagina->url) ?>">
<i class="fa fa-chart-area"></i>
</a>
</td>
</tr>
<?php } ?>
</tbody>
</table>
</div>
</div>
<footer class="card-footer">
<small class="mx-2 my-2">By parzibyte</small>
</footer>
</div>
</div>
</div>
</section>
<script type="text/javascript">
// Pasar variable de PHP a JS
const visitantes = <?php echo json_encode($visitantes) ?>;
const visitas = <?php echo json_encode($visitas) ?>;
// Obtener una referencia al elemento canvas del DOM
const $grafica = document.querySelector("#grafica");
// Las etiquetas son las que van en el eje X.
// Podemos mapear visitas o visitantes, ya que solo necesitamos las fechas
const etiquetas = visitas.map(visita => visita.fecha);
// Podemos tener varios conjuntos de datos
const datosVisitas = {
label: "Visitas",
data: visitas.map(visita => visita.conteo),
backgroundColor: 'rgba(237,78,136, 0.2)', // Color de fondo
borderColor: 'rgba(237,78,136, 1)', // Color del borde
borderWidth: 1, // Ancho del borde
};
const datosVisitantes = {
label: "Visitantes",
data: visitantes.map(visitante => visitante.conteo),
backgroundColor: 'rgba(93,82,247, 0.2)', // Color de fondo
borderColor: 'rgba(93,82,247,1)', // Color del borde
borderWidth: 1, // Ancho del borde
};
new Chart($grafica, {
type: 'line', // Tipo de gráfica
data: {
labels: etiquetas,
datasets: [
datosVisitas,
datosVisitantes,
// Aquí más datos...
]
},
options: {
scales: {
yAxes: [{
ticks: {
beginAtZero: true
}
}],
},
}
});
</script>
<?php include_once "pie.php" ?>
Para lograr que se muestre la gráfica y la lista de páginas visitadas estamos invocando a las funciones definidas en el archivo presentado con anterioridad y renderizando los datos a partir de lo que los métodos devuelvan.
Por cierto, el código que muestra las estadísticas de visitas de una página en un período de tiempo es el siguiente:
<?php include_once "encabezado.php"; ?>
<?php
if (!isset($_GET["url"])) {
exit("No hay URL");
}
$url = urldecode($_GET["url"]);
include_once "funciones.php";
$hoy = fechaHoy();
list($inicio, $fin) = fechaInicioYFinDeMes();
if (isset($_GET["inicio"])) {
$inicio = $_GET["inicio"];
}
if (isset($_GET["fin"])) {
$fin = $_GET["fin"];
}
$visitasYVisitantes = obtenerConteoVisitasYVisitantesDePaginaEnRango($inicio, $fin, $url);
$visitasYVisitantes = obtenerConteoVisitasYVisitantesEnRango($hoy, $hoy);
$visitantes = obtenerVisitantesDePaginaEnRango($inicio, $fin, $url);
$visitas = obtenerVisitasDePaginaEnRango($inicio, $fin, $url);
?>
<section class="section">
<div class="columns">
<div class="column">
<div class="card">
<header class="card-header">
<p class="card-header-title">
Estadísticas para <?php echo $url ?> entre <?php echo $inicio ?> y <?php echo $fin ?>
</p>
</header>
<div class="card-content">
<div class="content">
<a class="button is-info mb-2" href="dashboard.php">
<i class="fa fa-arrow-left"></i>
Volver</a>
<form action="visitas_url.php">
<input type="hidden" value="<?php echo $url ?>" name="url">
<div class="field is-grouped">
<p class="control is-expanded">
<label>Desde: </label>
<input class="input" type="date" name="inicio" value="<?php echo $inicio ?>">
</p>
<p class="control is-expanded">
<label>Hasta: </label>
<input class="input" type="date" name="fin" value="<?php echo $fin ?>">
</p>
<p class="control">
<!--La etiqueta es invisible a propósito para que tome el espacio y alinee el botón-->
<label style="color: white;">ª</label>
<input type="submit" value="Filtrar" class="button is-success input">
</p>
</div>
</form>
<canvas id="grafica"></canvas>
</div>
</div>
<footer class="card-footer">
<small class="mx-2 my-2">By parzibyte</small>
</footer>
</div>
</div>
</div>
</section>
<script type="text/javascript">
// Pasar variable de PHP a JS
const visitantes = <?php echo json_encode($visitantes) ?>;
const visitas = <?php echo json_encode($visitas) ?>;
// Obtener una referencia al elemento canvas del DOM
const $grafica = document.querySelector("#grafica");
// Las etiquetas son las que van en el eje X.
// Podemos mapear visitas o visitantes, ya que solo necesitamos las fechas
const etiquetas = visitas.map(visita => visita.fecha);
// Podemos tener varios conjuntos de datos
const datosVisitas = {
label: "Visitas",
data: visitas.map(visita => visita.conteo),
backgroundColor: 'rgba(237,78,136, 0.2)', // Color de fondo
borderColor: 'rgba(237,78,136, 1)', // Color del borde
borderWidth: 1, // Ancho del borde
};
const datosVisitantes = {
label: "Visitantes",
data: visitantes.map(visitante => visitante.conteo),
backgroundColor: 'rgba(93,82,247, 0.2)', // Color de fondo
borderColor: 'rgba(93,82,247,1)', // Color del borde
borderWidth: 1, // Ancho del borde
};
new Chart($grafica, {
type: 'line', // Tipo de gráfica
data: {
labels: etiquetas,
datasets: [
datosVisitas,
datosVisitantes,
// Aquí más datos...
]
},
options: {
scales: {
yAxes: [{
ticks: {
beginAtZero: true
}
}],
},
}
});
</script>
<?php include_once "pie.php" ?>
Instalación del software
Te dejo el código fuente completo en GitHub. Lo verdaderamente importante es el script que hace el registro de la visita y todo lo que está dentro de la carpeta llamada contador
.
Una vez que tengas el código, las instrucciones son:
- Dentro de
contador
, crea el archivo llamadoenv.php
basándote en el archivoenv.ejemplo.php
- Crea una base de datos en MySQL / MariaDB y ten a la mano el usuario y contraseña para acceder a la misma
- Coloca las credenciales de acceso a la base de datos dentro de
env.php
- Importa las tablas que están en
esquema.sql
y crea los índices - Ahora solo tienes que incluir a
contador.js
en cualquier lugar en donde quieras llevar registro de las visitas y visitantes. Recuerda configurar la ruta en caso de que coloques los archivos de manera distinta - Disfruta
como lo implementarias en una web diseñada con laravel?
Escribiendo el código necesario para adaptarlo