Cómo convertir un sitio web desarrollado para PHP 4 y 5 a un sitio web con PHP 8. El sitio web de ejemplo incluye acceso a MySQL/MariaDB y contiene programación orientada a objetos.
- Requisitos para hacer compatible un sitio web de PHP 4 en PHP 8.
- Convertir sitio web PHP 4 a PHP 8.
Requisitos para hacer compatible un sitio web de PHP 4 en PHP 8
Aunque existen algunos mecanismos semiautomatizados para la conversión de código PHP 4 a PHP 8, en general, transformar un sitio web que se desarrolló para PHP versiones 4 y 5 para hacerlo compatible con PHP 8 es una labor prácticamente manual. Deberemos revisar todo el código (o parte de él). Si es un sitio web con muchas líneas de código y tenemos cierta urgencia, al menos, revisaremos las líneas que arrojen errores o warnings.
Para que el sitio web «antiguo» siga funcionando en producción mientras hacemos la conversión, lo recomendable es hacer una réplica del sitio web en otro servidor que implemente PHP 8. En este caso hemos usado un despliegue rápido de Apache + PHP + MySQL en Docker, como indicamos en este tutorial:
Réplica del sitio web PHP de producción en un servidor de desarrollo
La réplica de un sitio web en PHP con acceso a MariaDB/MySQL requerirá de dos componentes:
- Ficheros del sitio web: ficheros PHP, ficheros HTML, ficheros CSS, ficheros de imágenes, pdf, etc. y cualquier otro fichero necesario para el funcionamiento del sitio web.
- Base de datos: la base de datos MariaDB/MySQL (o cualquier otro motor de BD como PostgreSQL, Oracle, SQL Server). El sitio web obtendrá los datos dinámicos desde la base de datos.
A continuación, indicamos cómo descargar/exportar tanto los ficheros como la base de datos para hacer una réplica en un servidor de desarrollo.
Descarga y transferencia de los ficheros que componen el sitio web al servidor de desarrollo
Descargaremos los ficheros que componen el sitio web en PHP 4 en producción (usando cualquier cliente SFTP o FTP, como Filezilla) a un equipo «intermedio» (que tenga acceso tanto al servidor de producción actual como al nuevo servidor con PHP 8 para desarrollo):
Y los subiremos al equipo de desarrollo con PHP + Apache + MySQL/MariaDB:
Exportar e importar base de datos MariaDB/MySQL del servidor de producción al de desarrollo
Realizaremos la importación de la base de datos MariaDB o MySQL del servidor de producción. Esta exportación podemos hacerla de diferentes formas. Quizá la más rápida sea usando phpMyAdmin, con la opción de «Exportar»:
En caso de no disponer de phpMyAdmin, podremos hacer la exportación mediante MySQL Workbench, herramienta gratuita de MySQL que es compatible con MariaDB. También podremos realizar la exportación desde la línea de comandos con la herramienta mysqldump, lo explicamos en el siguiente tutorial:
En cuanto dispongamos del fichero de exportación de la BD de producción, realizaremos el proceso inverso, consistente en hacer la importación al servidor MySQL/MariaDB de desarrollo. Para ello, al igual que hemos hecho la exportación, seleccionaremos la base de datos de destino en phpMyAdmin del servidor de desarrollo (es importante seleccionar primero la base de datos en la que se realizará la importación):
Y pulsaremos en «Importar». Seleccionaremos el archivo SQL exportando anteriormente y realizaremos la importanción.
NOTA IMPORTANTE: si el archivo de exportación es mayor de 40MB, el proceso fallará. En este caso, lo comprimiremos con zip o gzip y seleccionaremos el fichero comprimido (phpMyAdmin admite ficheros comprimidos en zip, gzip o bzip2).
Una vez concluido el proceso, tendremos todas las tablas y registros de la base de datos de producción en la base de datos de desarrollo:
Configuración del sitio web en el servidor de desarrollo
Configuraremos el servidor de desarrollo para que pueda ejecutar el sitio web. Probablemente necesitemos agregar la ruta del sitio web (donde hayamos subido los ficheros) al fichero httpd.conf correspondiente (o en el fichero de Virtual Host correspondiente), para indicar que ejecute los ficheros PHP y que no los descargue. Esto lo explicamos en este tutorial:
Por otro lado, configuraremos el acceso a la base de datos. Según el sitio web y según el desarrollo, buscaremos el fichero o ficheros de configuración donde se definen el usuario, la contraseña y el servidor de base de datos MariaDB/MySQL. El ejemplo que vamos a usar para la conversión es una vieja web desarrollada con el gestor PHP-Nuke para PHP 4 y 5. Esta web incluye un fichero llamado «config.php» que contiene los datos de conexión al servidor de base de datos. Lo modificaremos para introducir los nuevos datos de conexión al MySQL/MariaDB del servidor de desarrollo. Para editar los ficheros PHP (y el resto de ficheros js, HTML o CSS que necesitemos) podemos usar el «Editar» del propio Filezilla, que descargará el fichero a editar en el equipo local y lo abrirá con el editor predeterminado. Filezilla, una vez editado y guardado el fichero local, nos mostrará un mensaje para subirlo al servidor de desarrollo:
O bien, podemos editarlo directamente accediendo al servidor mediante SSH y usando algún editor que incorpore. También podemos editarlo en el equipo local (si previamente lo hemos descargado) y, una vez editado, subirlo con un cliente SFTP como Filezilla al servidor de desarrollo.
Editaremos el fichero config.php (en nuestro caso), estableciendo los parámetros de conexión a la base de datos:
- $dbhost: IP o nombre DNS del servidor con MySQL/MariaDB de desarrollo. En este caso, «localhost».
- $dbuname: nombre de usuario de la base de datos MySQL/MariaDB de desarrollo. En este caso «root».
- $dbpass: contraseña del usuario anterior.
- $dbname: nombre de la base de datos (catálogo) que contiene las tablas de la aplicación o sitio web. En este caso «ajpdsoft».
Cada sitio o aplicación web, según el desarrollo realizado, tendrá estos datos de conexión en un fichero diferente. Es importante definir el acceso al servidor de MySQL/MariaDB de desarrollo, para que las pruebas que realicemos no afecten al de producción.
Convertir sitio web PHP 4 a PHP 8
Una vez desplegado y configurado el servidor de producción y el sitio web, podremos empezar, o bien a grosso modo o bien estudiando el código de todos los ficheros PHP que componen el sitio web. En este caso, dado que son muchos ficheros y muchas líneas de código, lo que haremos será probar el acceso «a lo bruto». Es decir, intentaremos acceder al sitio web de forma normal e iremos anotando y revisando los errores o avisos que se vayan produciendo.
Empezaremos por la página de inicio «index.php». Intentaremos acceder a la página de inicio del sitio web:
http://localhost:41063/www/ajpdsoft/index.php
Puesto que, por defecto, tenemos activada la opción de mostrar warning y errores en PHP, nos encontramos con estos primeros warings y errores:
Warning: Undefined variable $HTTP_USER_AGENT in /www/ajpdsoft/mainfile.php on line 10
Fatal error: Uncaught Error: Call to undefined function import_request_variables() in /www/ajpdsoft/mainfile.php:51 Stack trace: #0 /www/ajpdsoft/index.php(3): require_once() #1 {main} thrown in /www/ajpdsoft/mainfile.php on line 51
Afortunadamente, en los avisos y errores viene indicado el fichero y la línea de código que lo produce, así como el motivo. Por ello, analizaremos cada uno. Por ejemplo, el primer warning lo generan estas líneas de código PHP:
1 2 3 4 5 |
$phpver = phpversion(); if ($phpver >= '4.0.4pl1' && strstr($HTTP_USER_AGENT,'compatible')) { if (extension_loaded('zlib')) { ob_end_clean(); ob_start('ob_gzhandler'); |
Analizaremos el código de cada caso y lo convertiremos/modificaremos/transformaremos para que sea compatible con PHP 8. Para el ejemplo anterior, modificamos la línea, quedando:
1 2 3 4 5 6 |
$phpver = phpversion(); //Modificado por Alonso para compatibilidad PHP 8 if ($phpver >= '4.0.4pl1' && strstr($_SERVER["HTTP_USER_AGENT"],'compatible')) { if (extension_loaded('zlib')) { ob_end_clean(); ob_start('ob_gzhandler'); |
En este caso, para solucionar el aviso, hemos cambiado $HTTP_USER_AGENT por $_SERVER[«HTTP_USER_AGENT»].
Con cada modificación, guardaremos el fichero correspondiente y lo subiremos al servidor de desarrollo:
Y volveremos a cargar el sitio web para pasar al siguiente aviso o error. En este caso, el siguiente error era:
Fatal error: Uncaught Error: Call to undefined function import_request_variables() in /www/ajpdsoft/mainfile.php:51 Stack trace: #0 /www/ajpdsoft/index.php(3): require_once() #1 {main} thrown in /www/ajpdsoft/mainfile.php on line 51
Este error, para PHP-Nuke lo vamos a obviar, ya que no necesitamos la línea de código que lo genera, por lo tanto la comentamos:
Volvemos a guardar, subir y cargar el sitio web. El siguiente error que se produce:
Fatal error: Uncaught Error: Call to undefined function eregi() in /www/ajpdsoft/mainfile.php:76 Stack trace: #0 /www/ajpdsoft/index.php(3): require_once() #1 {main} thrown in /www/ajpdsoft/mainfile.php on line 76
Comprobamos que es debido a que PHP 8 no incluye la función eregi(). Este será un error habitual en la conversión de sitios web de PHP 4 a 8, dado que muchas funciones de PHP 4 han sido declaradas obsoletas en PHP 8 y ya no están disponibles.
Para este caso, el error se produce en las líneas de código PHP (seguimos con el fichero mainfile.php, en la línea 76):
1 2 3 4 |
if (eregi("mainfile.php",$PHP_SELF)) { Header("Location: https://www.ajpdsoft.com"); die(); } |
Que, en este caso, cambiaremos la función eregi() (obsoleta desde PHP 5 y eliminada a partir de PHP 7) por la función strcasecmp():
1 2 3 4 |
if (strcasecmp("mainfile.php", $PHP_SELF) == 0) { Header("Location: https://www.ajpdsoft.com"); die(); } |
Analizaremos bien las comprobaciones de seguridad, como la anterior. Porque si en alguna comprobamos la URL raíz (por ejemplo) y no coincide al ser un sitio web de desarrollo, podría redirigirnos al sitio web de producción. Por ello, si nos encontramos con alguna de estas comprobaciones, de forma excepcional, en el servidor de desarrollo comentaremos estas líneas para que no haga esa comprobación. Luego, una vez en producción, podremos descomentarlas.
Continuando con la depuración, nos encontramos nuevamente con un error de función eregi() no definida. En este caso en el fichero /includes/sql_layer.php, en la línea 17:
Fatal error: Uncaught Error: Call to undefined function eregi() in /www/ajpdsoft/includes/sql_layer.php:17 Stack trace: #0 /www/ajpdsoft/mainfile.php(97): require_once() #1 /www/ajpdsoft/index.php(3): require_once(‘/www/ajpdsoft/m…’) #2 {main} thrown in /www/ajpdsoft/includes/sql_layer.php on line 17
1 2 3 4 |
if (eregi("sql_layer.php",$_SERVER['PHP_SELF'])) { Header("Location: ../index.php"); die(); } |
Procederemos de la misma forma que anteriormente, reemplazando la función eregi() por strcasecmp():
1 2 3 4 |
if (strcasecmp("sql_layer.php",$_SERVER['PHP_SELF']) == 0) { Header("Location: ../index.php"); die(); } |
Para abreviar este tutorial, nos centraremos en errores diferentes y obviaremos los que son comunes, dado que una vez que sepamos como resolver uno, el resto similares se resolverán igual.
Siguiendo con la depuración, ahora nos encontramos con este otro error:
Fatal error: Uncaught Error: Call to undefined function mysql_connect() in /www/ajpdsoft/includes/sql_layer.php:79 Stack trace: #0 /www/ajpdsoft/mainfile.php(96): sql_connect(‘localhost’, ‘root’, ‘ContrseñaRoot’, ‘ajpdsoft’) #1 /www/ajpdsoft/index.php(3): require_once(‘/www/ajpdsoft/m…’) #2 {main} thrown in /www/ajpdsoft/includes/sql_layer.php on line 79
Este error es debido al mismo motivo que la función eregi() anterior, en este caso, la función mysql_connect() está marcada como obsoleta desde PHP 5 y eliminada desde PHP 7. Por lo tanto tendremos que sustituirla (en todos los ficheros donde la usemos) por la función mysqli_connect(). Por ejemplo, para las líneas que dan error:
1 2 3 |
case "MySQL": $dbi=@mysql_connect($host, $user, $password); mysql_select_db($db); |
Las sustituiremos por:
1 2 3 |
case "MySQL": $dbi=@mysqli_connect($host, $user, $password, $db); //mysql_select_db($db); |
Comentamos la línea mysql_select_db() dado que en la función mysqli_connect() ya le hemos indicado la base de datos.
Volvemos a acceder al sitio, y ahora nos encontramos con el error:
Fatal error: Uncaught Error: Call to undefined function mysql_query() in /www/ajpdsoft/db/mysql.php:113 Stack trace: #0 /www/ajpdsoft/mainfile.php(101): sql_db->sql_query(‘SELECT sitename…’) #1 /www/ajpdsoft/index.php(3): require_once(‘/www/ajpdsoft/m…’) #2 {main} thrown in /www/ajpdsoft/db/mysql.php on line 113
Similar al caso anterior, se trata de la función mysql_query() que ha sido declarada obsoleta a partir de PHP 5 y eliminada a partir de PHP 7. Por lo tanto la sustituiremos por mysqli_query():
1 |
$this->query_result = @mysqli_query($this->db_connect_id, $query); |
Al acceder al sitio web, sigue mostrándose el error en la misma línea y mismo fichero. Pero ahora el error es diferente, indicando que la conexión (link) requerido por la función mysqli_query() está a null:
Fatal error: Uncaught TypeError: mysqli_query(): Argument #1 ($mysql) must be of type mysqli, null given in /www/ajpdsoft/db/mysql.php:113 Stack trace: #0 /www/ajpdsoft/db/mysql.php(113): mysqli_query(NULL, ‘SELECT sitename…’) #1 /www/ajpdsoft/mainfile.php(101): sql_db->sql_query(‘SELECT sitename…’) #2 /www/ajpdsoft/index.php(3): require_once(‘/www/ajpdsoft/m…’) #3 {main} thrown in /www/ajpdsoft/db/mysql.php on line 113
Por lo tanto, en este caso, realizaremos ingeniería inversa para ver desde dónde nos llega el link de conexión, dado que llega vacío (null). Revisando el código, vemos que el fichero mysql.php contiene una clase llamada «sql_db» y un constructor con el código:
1 2 3 4 5 6 7 |
function sql_db($sqlserver, $sqluser, $sqlpassword, $database, $persistency = true) { $this->persistency = $persistency; $this->user = $sqluser; $this->password = $sqlpassword; $this->server = $sqlserver; $this->dbname = $database; |
Pues bien, en PHP 8 los constructores se definen con __construct, por lo tanto, modificaremos el código anterior por:
1 2 3 4 5 6 7 |
function __construct ($sqlserver, $sqluser, $sqlpassword, $database, $persistency = true) { $this->persistency = $persistency; $this->user = $sqluser; $this->password = $sqlpassword; $this->server = $sqlserver; $this->dbname = $database; |
Ahora nos llegará correctamente el link de la conexión, definido en el atributo $db_connect_id de la clase sql_db, dado que ahora sí se llama correctamente al constructor.
Continuamos con los errores, ahora para la función mysql_select_db(), que es sustituida por mysqli_select_db():
Fatal error: Uncaught Error: Call to undefined function mysql_select_db() in /www/ajpdsoft/db/mysql.php:61 Stack trace: #0 /www/ajpdsoft/db/db.php(49): sql_db->__construct(‘localhost’, ‘root’, ‘ContraseñaRoot’, ‘ajpdsoft’, false) #1 /www/ajpdsoft/mainfile.php(92): require_once(‘/www/ajpdsoft/d…’) #2 /www/ajpdsoft/index.php(3): require_once(‘/www/ajpdsoft/m…’) #3 {main} thrown in /www/ajpdsoft/db/mysql.php on line 61
Por lo tanto, las líneas que dan error:
1 2 3 4 |
$this->dbname = $database; $dbselect = @mysql_select_db($this->dbname); if(!$dbselect) { |
Se sustituyen por:
1 2 3 4 |
$this->dbname = $database; $dbselect = @mysqli_select_db($this->db_connect_id, $this->dbname); if(!$dbselect) { |
Teniendo en cuenta que la nueva función mysqli_select_db() requiere de dos argumentos (link y nombre de la base de datos).
Otro error habitual, si usamos la función obsoleta mysql_fetch_array(), la cambiaremos por mysqli_fetch_array().
Fatal error: Uncaught Error: Call to undefined function mysql_fetch_array() in /www/ajpdsoft/db/mysql.php:215 Stack trace: #0 /www/ajpdsoft/mainfile.php(102): sql_db->sql_fetchrow(Object(mysqli_result)) #1 /www/ajpdsoft/index.php(3): require_once(‘/www/ajpdsoft/m…’) #2 {main} thrown in /www/ajpdsoft/db/mysql.php on line 215
Sustituiremos, en este caso, las líneas:
1 2 3 4 5 |
if($query_id) { $this->row[$query_id] = @mysql_fetch_array($query_id); return $this->row[$query_id]; } |
Por:
1 2 3 4 5 |
if($query_id) { $this->row[$query_id] = @mysqli_fetch_array($query_id); return $this->row[$query_id]; } |
Otro error común, si usamos la función eregi_replace(), la sustituiremos por str_replace():
Fatal error: Uncaught Error: Call to undefined function eregi_replace() in /www/ajpdsoft/mainfile.php:158 Stack trace: #0 /www/ajpdsoft/index.php(3): require_once() #1 {main} thrown in /www/ajpdsoft/mainfile.php on line 158
La línea que da el error:
1 |
$domain = eregi_replace("http://", "", $nukeurl); |
La dejaremos con:
1 |
$domain = str_replace("http://", "", $nukeurl); |
Otro error que se puede producir:
Fatal error: Uncaught Error: Call to undefined function mysql_num_rows() in /www/ajpdsoft/db/mysql.php:119 Stack trace: #0 /www/ajpdsoft/mainfile.php(369): sql_db->sql_numrows(Object(mysqli_result)) #1 /www/ajpdsoft/header.php(301): message_box() #2 /www/ajpdsoft/modules/News/index.php(19): include(‘/www/ajpdsoft/h…’) #3 /www/ajpdsoft/modules/News/index.php(223): theindex(0) #4 /www/ajpdsoft/index.php(66): include(‘/www/ajpdsoft/m…’) #5 {main} thrown in /www/ajpdsoft/db/mysql.php on line 119
En este caso debido al uso de la función mysql_num_rows() obsoleta, sustituida por mysqli_num_rows(), quedando las líneas de código corregidas como:
1 2 3 4 5 |
if($query_id) { $result = @mysqli_num_rows($query_id); return $result; } |
Si nos encontramos algún error como este:
Fatal error: Uncaught Error: Undefined constant «mid» in /www/ajpdsoft/mainfile.php:373 Stack trace: #0 /www/ajpdsoft/header.php(301): message_box() #1 /www/ajpdsoft/modules/News/index.php(19): include(‘/www/ajpdsoft/h…’) #2 /www/ajpdsoft/modules/News/index.php(223): theindex(0) #3 /www/ajpdsoft/index.php(66): include(‘/www/ajpdsoft/m…’) #4 {main} thrown in /www/ajpdsoft/mainfile.php on line 373
Puede ser debido a que en PHP 4 se admite el código:
1 2 3 4 5 6 7 8 9 10 11 12 13 |
if ($numrows = $db->sql_numrows($result) == 0) { return; } else { while ($row = $db->sql_fetchrow($result)) { $mid = $row[mid]; $title = $row[title]; $content = $row[content]; $mdate = $row[date]; $expire = $row[expire]; $view = $row[view]; |
En PHP 8 deberemos añadir comillas dobles a la asociación de los array, el código anterior quedaría:
1 2 3 4 5 6 7 8 9 10 11 12 13 |
if ($numrows = $db->sql_numrows($result) == 0) { return; } else { while ($row = $db->sql_fetchrow($result)) { $mid = $row["mid"]; $title = $row["title"]; $content = $row["content"]; $mdate = $row["date"]; $expire = $row["expire"]; $view = $row["view"]; |