Category: Programación

DataTables: Tried to bind parameter number 65536. SQL Server supports a maximum of 2100 parameters.

Podemos encontrar este error en el método complex() de la librería ssp.class.php de DataTables. Esta función en casos normales debería funcionar, pero resulta que si utilizas el filtro de repente nos salta este error.

«error»:»An SQL error occurred: SQLSTATE[IMSSP]: Tried to bind parameter number 65536. SQL Server supports a maximum of 2100 parameters.

¿Qué está pasando?

Vamos a empezar con entender que hace la función sql_exec. Esta función ejecuta la sql utilizando variables Bind para realizar la ejecución de los filtros Where. La función que realiza la sustitución es bindValue que se ejecuta siempre que $bindings sea un array()

// Bind parameters
if ( is_array( $bindings ) ) {
    for ( $i=0, $ien=count($bindings) ; $i<$ien ; $i++ ) {
        $binding = $bindings[$i];
        $stmt->bindValue( $binding['key'], 
        $binding['val'], $binding['type'] );
    }
}

En la ejecución del método complex() hay 3 llamadas al sql_exec:

  1. La prímera (main query) que nos devuelve los datos. 
  2. La segunda (Data set length after filtering) que nos devuelve el número total de registros filtrados.
  3. La tercera (Total data set length) que es el total de registros sin filtrar.

La que nos da el error es la tercera llamada. En esta llamada intenta utilizar la variable $bindings que sustituiría los valores del Where.  Pero la variable Where que se utiliza es $whereAllSql que NO TIENE NINGÚN VALOR A SUSTITUIR.

$whereAllSql es un parámetro que le podemos pasar al método complex() para realizar la consulta por defecto. Por ejemplo un listado de alumnos donde siempre queremos que aparezcan los de la sección A. Añadiríamos a ese parámetro el texto correspondiente a la cláusula where:

SSP::complex($_GET, $sql_details, $table, $primaryKey, $columns, null, "(section='A')")

SOLUCIÓN

Por lo que el error lo podemos controlar de forma sencilla. La solución  es enviar el parámetro $bindings vacío a la función sql_exec que nos da el error:

Código Original

// Total data set length
$resTotalLength = self::sql_exec( $db, 
$bindings,
"SELECT COUNT({$primaryKey})
FROM   $table 
$whereAllSql"
);

Código Nuevo

// Total data set length
$resTotalLength = self::sql_exec( $db, 
"",
"SELECT COUNT({$primaryKey})
FROM   $table 
$whereAllSql"
);

Moodle: $CFG variables

La variable global $CFG y sus propiedades se definen en lib/setup.php

// Directorio raíz
$CFG->dirroot

// URL raíz
$CFG->wwwroot

// Directorio de Librerías
$CFG->libdir

// Directorio Temporal
$CFG->tempdir

// Directorio Caché
$CFG->cachedir

Moodle Error: Output can not be buffered before instantiating table_dataformat_export_format

Lo que nos dice este error es que no debe haber ningún tipo de Output si vamos a realizar una exportación.

Aún dejando limpio el HTML, no era capaz de eliminar este error por lo que buscando en el código eliminé la excepción y lo sustituí por un ob_clean().

En lib/table_lib.php buscamos el constructor de la case table_dataformat_export_format y realizamos la operación:

if (ob_get_length()) {
            // EXCEPTION was not helping
            // Is this the solution? It solves problem when exporting
            ob_clean();
            // throw new coding_exception("Output can not be buffered before instantiating table_dataformat_export_format");
        }

Por el momento nos dejará realizar la exportación sin problema, pero no sé si esta sea la mejor solución.

MOODLE Mostrar Tabla de Datos HTML con flexible_table

Vamos a partir de la base en la que no sé nada sobre la API de Moodle. Buscando información, he encontrado que NO HAY CASI NADA DE INFORMACIÓN / DOCUMENTACIÓN / EJEMPLOS.

Por lo que voy a compartir una mini guía personal y algunos recursos que he utilizado para poder mostrar una tabla dinámica que incluye paginación, ordenación y exportación de datos.

Para ello vamos a crear nuestra propia clase partiendo de la clase table_sql la cual extiende la clase flexible_table.

// Esta clase se encuentra en 
// lib\tablelib.php
class table_sql extends flexible_table{
}


// Crearemos nuestra propia clase en nuestra carpeta
// personal o en la misma carpeta lib
class table_sql_pers extends table_sql {
}

La idea de crear otra clase a partir de la table_sql es añadir un nuevo parámetro al método set_sql y personalizar así nuestra MySql Query sin tener que modificar la clase original.

En el mismo archivo he creado la clase para la gestión de alumnos creando una query personalizada dividiendo las partes de SELECT FROM WHERE y GROUP BY en variables para poder añadir los datos que necesitemos:

<?php

require "$CFG->libdir/tablelib.php";

/**
 * Clase de gestión personalizada de Alumnos
 * Métodos:
 * 		LIstado
 * 		Importación
 */

class GestionAlumnos{

	/**
	 * Variable FIELDS para SELECT
	 *
	 * @var string
	 */
	private $selectAlumnosFields = "
		{user}.`id`,
		DATE_FORMAT(
			FROM_UNIXTIME({user}.`timecreated`),
			'%d/%m/%Y %H:%i:%s'
		) AS `timecreated`,
		DATE_FORMAT(
			FROM_UNIXTIME({user}.`timemodified`),
			'%d/%m/%Y %H:%i:%s'
		) AS `timemodified`,
		{user}.`username` AS `username`
	";

	/**
	 * Variable FROM con las tablas y JOINS
	 *
	 * @var string
	 */
	private $selectAlumnosFrom = "
	(
		(
			(
				(
					(
						(
							(
								(
									(
										{role_assignments}
										LEFT JOIN {user} ON(
											(
												{user}.`id` = {role_assignments}.`userid`
											)
										)
									)
									LEFT JOIN {context} ON(
										(
											{role_assignments}.`contextid` = {context}.`id`
										)
									)
								)
							)
							JOIN {user_info_data} ON(
								(
									{user}.`id` = {user_info_data}.`userid`
								)
							)
						)
						JOIN {user_enrolments} ON(
							(
								{user_enrolments}.`userid` = {user}.`id`
							)
						)
					)
					JOIN {enrol} ON(
						(
							{enrol}.`id` = {user_enrolments}.`enrolid`
						)
					)
				)
				JOIN {course} ON(
					(
						{course}.`id` = {enrol}.`courseid`
					)
				)
			)
		)
	)
	";

	/**
	 * Variable WHERE
	 *
	 * @var string
	 */
	private $selectAlumnosWhere = "
		(
			NOT(
				(
					{user}.`firstname` LIKE '%\\_%'
				)
			)
		)
		AND (
			{user_info_data}.`fieldid` = 1
		)
		AND (
			{role_assignments}.`roleid` = 5
		)
	";

	/**
	 * Variable GROUP BY
	 *
	 * @var string
	 */
	private $selectAlumnosGroupBy = "
		{user}.`id`,
		{user}.`timecreated`,
		{user}.`timemodified`,
		{user}.`username`
	";

	/**
	 * Variable con las columnas que vamos a mostrar de la query original
	 *
	 * @var array
	 */
	protected $columns = array(
		'id',
		'username',
		'timecreated',
		'timemodified'
	);

	/**
	 * Cabeceras para el listado de la tabla HTML
	 *
	 * @var array
	 */
	protected $headers = array(
		'ID',
		'Usuario',
		'F. Crea.',
		'F. Modif.'
	);

	protected $database;
	protected $baseUrl;

	/**
	 * Constructor al que pasamos el recurso de BBDD
	 * y la URL para mostrar los links de paginación,
	 * ordenación en la tabla de datos
	 *
	 * @param [type] $DB
	 * @param [type] $baseUrl
	 */
	public function __construct($DB, $baseUrl)
	{
		$this->database = $DB;
		$this->baseUrl = $baseUrl;
	}

	/**
	 * Setea las columnas para mostrar solamente las que necesitamos
	 *
	 * @param array $columns
	 * @return void
	 */
	public function setColumns($columns = array()){
		$this->columns = $columns;
	}

	/**
	 * Undocumented function
	 *
	 * @param array $headers
	 * @return void
	 */
	public function setHeaders($headers = array()){
		$this->headers = $headers;
	}

	/**
	 * Crea la tabla para mostrar el listado de alumnos
	 * Devuelve el recurso $table que se podrá utilizar 
	 * para renderizar la tabla con el método out()
	 * Ejemplo: $table->out(15, true); 
	 * 
	 * Lo hacemos así para poder verificar cuando se hace un renderizado en HTML
	 * o una exportación ya que a la hora de exportar no debemos mostrar más que
	 * los datos. En la página donde llamamos esta clase y sus métodos debemos
	 * utilizar la siguiente condición antes de mostrar los headers / footers o
	 * cualquier tipo de $OUTPUT:
	 * if (!$table->is_downloading()) {
	 * 		echo $OUTPUT->header();
	 * 		echo $OUTPUT->footer();
	 * }
	 *
	 * @return $table
	 */
	public function getListAlumnosHTML(){

		$table = new table_sql_alumnos('listado-alumnos');

		$download = optional_param('download', '', PARAM_ALPHA);
		$table->is_downloading($download, 'Listado de Alumnos', 'Listado');

		$table->define_columns($this->columns);

		$table->define_headers($this->headers);

		$table->set_sql(
			$this->selectAlumnosFields,
			$this->selectAlumnosFrom,
			$this->selectAlumnosWhere,
			array(),
			$this->selectAlumnosGroupBy
		);

		$table->define_baseurl($this->baseUrl);

		return $table;
	}
}


/**
 * Extendemos la clase para modificar el método set_sql y añadir un nuevo parámetro $groupBy
 */
class table_sql_alumnos extends table_sql {

	/**
     * Set the sql to query the db. Query will be :
     *      SELECT $fields FROM $from WHERE $where
     * Of course you can use sub-queries, JOINS etc. by putting them in the
     * appropriate clause of the query.
     */

	/**
	 * MODIFICADO del original para añadir el parámetro GROUP BY
	 */
    function set_sql($fields, $from, $where, array $params = array(), $groupBy) {
        $this->sql = new stdClass();
        $this->sql->fields = $fields;
        $this->sql->from = $from;
        $this->sql->where = $where;
        $this->sql->params = $params;
        $this->sql->groupBy = $groupBy;
    }

    /**
     * Query the db. Store results in the table object for use by build_table.
     *
     * @param int $pagesize size of page for paginated displayed table.
     * @param bool $useinitialsbar do you want to use the initials bar. Bar
     * will only be used if there is a fullname column defined for the table.
     */
    function query_db($pagesize, $useinitialsbar=true) {
        global $DB;
        if (!$this->is_downloading()) {
            if ($this->countsql === NULL) {
                $this->countsql = 'SELECT COUNT(1) FROM '.$this->sql->from.' WHERE '.$this->sql->where;
                $this->countparams = $this->sql->params;
            }
            $grandtotal = $DB->count_records_sql($this->countsql, $this->countparams);
            if ($useinitialsbar && !$this->is_downloading()) {
                $this->initialbars($grandtotal > $pagesize);
            }

            list($wsql, $wparams) = $this->get_sql_where();
            if ($wsql) {
                $this->countsql .= ' AND '.$wsql;
                $this->countparams = array_merge($this->countparams, $wparams);

                $this->sql->where .= ' AND '.$wsql;
                $this->sql->params = array_merge($this->sql->params, $wparams);

                $total  = $DB->count_records_sql($this->countsql, $this->countparams);
            } else {
                $total = $grandtotal;
            }

            $this->pagesize($pagesize, $total);
        }

        // Fetch the attempts
        $sort = $this->get_sql_sort();
        if ($sort) {
            $sort = "ORDER BY $sort";
		}
		
		/**
		 * MODIFICADO del original para añadir GROUP BY
		 */
        $sql = "SELECT
                {$this->sql->fields}
                FROM {$this->sql->from}
                WHERE {$this->sql->where}
				GROUP BY {$this->sql->groupBy}
				{$sort}";

        if (!$this->is_downloading()) {
            $this->rawdata = $DB->get_records_sql($sql, $this->sql->params, $this->get_page_start(), $this->get_page_size());
        } else {
            $this->rawdata = $DB->get_records_sql($sql, $this->sql->params);
        }
    }
}

Este bloque es el que utilizaremos para renderizar nuestro HTML:

<?php

/**
 * Contiene la clase para la gestión de Alumnos
 */
require_once('./lib/GestionAlumnos.php');

$title = "Gestión de Alumnos";

$download = optional_param('download', '', PARAM_ALPHA);
$baseUrl = $CFG->wwwroot."/gestion_alumnos.php";
$gestAlumnos = new GestionAlumnos($DB,$baseUrl);
$table = $gestAlumnos->getListAlumnosHTML();

// Si no se está exportando, mostramos el HTML
if (!$table->is_downloading()) {

    $PAGE->set_title($title);
    $PAGE->set_heading($title);
    $PAGE->set_cacheable(false);
    $PAGE->navbar->ignore_active();
    $PAGE->set_url('/gestion_alumnos.php');

    $PAGE->navbar->add($title, new moodle_url(substr($PAGE->url, 0, strpos($PAGE->url, '?'))));
    $PAGE->set_pagelayout('frametop');

    echo $OUTPUT->header();
    echo '<link rel="stylesheet" type="text/css" href="https://code.jquery.com/ui/1.12.1/themes/base/jquery-ui.css">';

?>

<style>
    #tabs{
        min-height: 1150px;height:100%;margin-top:10px
    }
</style>

<h2><?= $title ?></h2>

<div id="tabs">
    <ul>
        <li><a href="#listado">Listado Alumnos</a></li>
	</ul>
	<div id="listado">
		<?php
			$table->out(15, true); 
		?>
    </div>
</div>

<script src="https://code.jquery.com/jquery-1.12.4.js"></script>
<script src="https://code.jquery.com/ui/1.12.1/jquery-ui.js"></script>
<script>
$(function() {
    $("#tabs").tabs();
});
</script>

<?php
    echo $OUTPUT->footer();
}else{
    $table->out(15, true); 
}

?>

RECURSOS

  • Moodle Docs: https://docs.moodle.org/dev/lib/tablelib.php
  • Moodle Forums (Entrar como Invitado): https://moodle.org/mod/forum/discuss.php?d=335786

Fatal error: Call to undefined function money_format()

Si estamos trabajando con WAMP o XAMPP puede que nos topemos con este error.

( ! ) Fatal error: Call to undefined function money_format()

Según la documentación de PHP La función money_format() sólo está definida si el sistema tiene capacidad strfmon. Por ejemplo, Windows no lo hace, así que money_format() no está definido en Windows.

Para solventarlo crearemos un archivo que adjuntaremos en nuestro php.ini

Abre el php.ini y busca la línea con la palabra «auto_prepend_file» y sustituye el valor vacío con

auto_prepend_file = "C:\wamp64\money_format.php"

Ahora crea el archivo en la misma ruta con la siguiente función de PHP

<?php

function money_format($formato, $valor)
{
    if (setlocale(LC_MONETARY, 0) == 'C') {
        return number_format($valor, 2);
    }

    $locale = localeconv();

    $regex = '/^' . // Inicio da Expressao
        '%' . // Caractere %
        '(?:' . // Inicio das Flags opcionais
        '\=([\w\040])' . // Flag =f
        '|' .
        '([\^])' . // Flag ^
        '|' .
        '(\+|\()' . // Flag + ou (
        '|' .
        '(!)' . // Flag !
        '|' .
        '(-)' . // Flag -
        ')*' . // Fim das flags opcionais
        '(?:([\d]+)?)' . // W Largura de campos
        '(?:#([\d]+))?' . // #n Precisao esquerda
        '(?:\.([\d]+))?' . // .p Precisao direita
        '([in%])' . // Caractere de conversao
        '$/'; // Fim da Expressao

    if (!preg_match($regex, $formato, $matches)) {
        trigger_error('Formato invalido: ' . $formato, E_USER_WARNING);
        return $valor;
    }

    $opcoes = array(
        'preenchimento' => ($matches[1] !== '') ? $matches[1] : ' ',
        'nao_agrupar' => ($matches[2] == '^'),
        'usar_sinal' => ($matches[3] == '+'),
        'usar_parenteses' => ($matches[3] == '('),
        'ignorar_simbolo' => ($matches[4] == '!'),
        'alinhamento_esq' => ($matches[5] == '-'),
        'largura_campo' => ($matches[6] !== '') ? (int) $matches[6] : 0,
        'precisao_esq' => ($matches[7] !== '') ? (int) $matches[7] : false,
        'precisao_dir' => ($matches[8] !== '') ? (int) $matches[8] : $locale['int_frac_digits'],
        'conversao' => $matches[9]
    );

    if ($opcoes['usar_sinal'] && $locale['n_sign_posn'] == 0) {
        $locale['n_sign_posn'] = 1;
    } elseif ($opcoes['usar_parenteses']) {
        $locale['n_sign_posn'] = 0;
    }
    if ($opcoes['precisao_dir']) {
        $locale['frac_digits'] = $opcoes['precisao_dir'];
    }
    if ($opcoes['nao_agrupar']) {
        $locale['mon_thousands_sep'] = '';
    }

    $tipo_sinal = $valor >= 0 ? 'p' : 'n';
    if ($opcoes['ignorar_simbolo']) {
        $simbolo = '';
    } else {
        $simbolo = $opcoes['conversao'] == 'n' ? $locale['currency_symbol']
            : $locale['int_curr_symbol'];
    }
    $numero = number_format(
        abs($valor),
        $locale['frac_digits'],
        $locale['mon_decimal_point'],
        $locale['mon_thousands_sep']
    );


    $sinal = $valor >= 0 ? $locale['positive_sign'] : $locale['negative_sign'];
    $simbolo_antes = $locale[$tipo_sinal . '_cs_precedes'];

    $espaco1 = $locale[$tipo_sinal . '_sep_by_space'] == 1 ? ' ' : '';

    $espaco2 = $locale[$tipo_sinal . '_sep_by_space'] == 2 ? ' ' : '';

    $formatado = '';
    switch ($locale[$tipo_sinal . '_sign_posn']) {
        case 0:
            if ($simbolo_antes) {
                $formatado = '(' . $simbolo . $espaco1 . $numero . ')';
            } else {
                $formatado = '(' . $numero . $espaco1 . $simbolo . ')';
            }
            break;
        case 1:
            if ($simbolo_antes) {
                $formatado = $sinal . $espaco2 . $simbolo . $espaco1 . $numero;
            } else {
                $formatado = $sinal . $numero . $espaco1 . $simbolo;
            }
            break;
        case 2:
            if ($simbolo_antes) {
                $formatado = $simbolo . $espaco1 . $numero . $sinal;
            } else {
                $formatado = $numero . $espaco1 . $simbolo . $espaco2 . $sinal;
            }
            break;
        case 3:
            if ($simbolo_antes) {
                $formatado = $sinal . $espaco2 . $simbolo . $espaco1 . $numero;
            } else {
                $formatado = $numero . $espaco1 . $sinal . $espaco2 . $simbolo;
            }
            break;
        case 4:
            if ($simbolo_antes) {
                $formatado = $simbolo . $espaco2 . $sinal . $espaco1 . $numero;
            } else {
                $formatado = $numero . $espaco1 . $simbolo . $espaco2 . $sinal;
            }
            break;
    }

    if ($opcoes['largura_campo'] > 0 && strlen($formatado) < $opcoes['largura_campo']) {
        $alinhamento = $opcoes['alinhamento_esq'] ? STR_PAD_RIGHT : STR_PAD_LEFT;
        $formatado = str_pad(
            $formatado,
            $opcoes['largura_campo'],
            $opcoes['preenchimento'],
            $alinhamento
        );
    }
    return $formatado;
}

Redirigir página WordPress a HTTPS

En WordPress existen 3 formas en las que se puede realizar la redirección de nuestra web al nombre de dominio que deseemos.

Esto con frecuencia se hace debido a la necesidad de habilitar la forma segura de mostrar nuestro dominio mediante HTTPS.

La mejor forma de hacerlo es modificando el archivo wpconfig.php porque así te evitas modificar la base de datos y que tengas un error de acceso a tu zona de administración.

En mi opinión creo que se debería hacer una combinación utilizando la primera o la segunda opción y la tercera opción, para asegurarnos que todas las páginas son redirigidas.

WpConfig.php

Editar wpconfig.php y añadir estas dos líneas con el nombre de dominio al que queremos redirigir.

define('WP_HOME','https://example.com');
define('WP_SITEURL','https://example.com');

WordPress Admin

Entrar a Configuración ó Settings -> General

Site Address (URL): https://example.com

WordPress Address (URL):’https://example.com

.Htaccess

<IfModule mod_rewrite.c>
RewriteEngine On
RewriteBase /

RewriteCond %{HTTPS} !=on
RewriteRule ^ https://%{HTTP_HOST}%{REQUEST_URI} [L,R=301]

# BEGIN WordPress
RewriteRule ^index\.php$ - [L]
RewriteCond %{REQUEST_FILENAME} !-f
RewriteCond %{REQUEST_FILENAME} !-d
RewriteRule . /index.php [L]
</IfModule>

Verificar si el archivo existe Vue Js

¿En qué casos nos podría interesar verificar que un archivo existe o no en nuestro servidor u otro?

Este es un caso real:

Tenemos una plataforma multilenguaje y queremos crear de forma dinámica los archivos en varios lenguajes. Para ello hemos creado un sistema de gestión de archivos en el que se puede utilizar el mismo registro para llamar a los distintos archivos en los distingos lenguajes. Es decir, podemos tener un manual en ES y EN.

Ahora bien, en Vue Js tenemos una estructura en la que no necesitamos decirle manualmente qué archivo escoger, si no que queremos que lo haga dinámicamente al cambiar de idioma. Aquí es donde nos vendrá de lujo esta función.

<a :href=»‘/’+locale+’/media/archivo.pdf'» class=»pdf-link»>{[ translations.label_download ]}</a>

Este es nuestro link que va a variar en los distintos idiomas.

¿Pero qué tal si hemos subido el archivo sólo en un idioma? Podemos evaluar si existe o no con la siguiente función.

He añadido además un caso en el que la evaluación puede estar condicionada a una acción. A la hora de hacer un clic, toggle se accionará y verificará que, para empezar esa estructura de html exista con el .length y a partir de allí empezar a buscar de cada registro encontrado si existe el archivo en el servidor.

Remplazar imagen con CSS

La técnica es sencilla, pero eficaz. Lo que tenemos que hacer es ocultar la imagen aplicando un ancho de 0px (width:0) y luego utilizando las propiedades de background (img y size) + padding para añadir la nueva imagen.

Esta es una buena técnica para realizar la acción remplazar una imagen utilizando sólo CSS, pero tenemos un «contra» y es que tendremos que ir jugando con @media-queries para poder adaptar la imagen a cada uno de los dispositivos.

Descargar PDF base64 con Symfony

Para hacer la recuperación de un archivo, en este caso un PDF, que está almacenado en nuestra base de datos como un BLOB para luego realizar el return añadiendo las cabeceras necesarias para realizar la acción de «Descargar» cuando llamemos a esta URL.

En este ejemplo sencillo, recuperamos el PDF utilizando una función ficticia en la que supuestamente recuperamos los datos del PDF en bruto (raw) y codificado en base64.

El segundo paso es decodificar ese raw utilizando la función base64_decode().

El último paso es crear la respuesta con los métodos que nos da la clase Response. A esta respuesta le vamos a añadir las cabeceras necesarias para poder decirle al navegador que es lo que tiene que hacer con este archivo.

En este caso lo que queremos es descargarlo. Por lo que añadiremos las cabeceras Content-Description y Content-Disposition:

$response->headers->set(‘Content-Description’, ‘File Transfer’);

$response->headers->set(‘Content-Disposition’, ‘attachment; filename=»Archivo.pdf»‘);
Con esto, al realizar una consulta a la ruta creada directamente nos descargará el PDF.
¡Importante! Si vas a realizar la consulta con un formulario, no te olvides de colocar el atributo enctype en tu etiqueta FORM.
<form id="pruebaPDF" method="post" enctype="multipart/form-data">
<?php
namespace App\Controller;
use Symfony\Component\HttpFoundation\Response;
    /**
    * @Route("/show-pdf", name="show_pdf")
    */
    public function getPDFDocument()
    {
        $result = getPdfFromRaw();
        $pdf = base64_decode($result->PDF);
        $response = new Response($pdf);
        $response->headers->set('Content-Type', 'application/octet-stream');
        $response->headers->set('Content-Description', 'File Transfer');
        $response->headers->set('Content-Disposition', 'attachment; filename="Archivo.pdf"');
        // $response->headers->set('Expires', '0');
        // $response->headers->set('Content-Transfer-Encoding', 'binary');
        $response->headers->set('Content-length', strlen($pdf));
        $response->headers->set('Cache-Control', 'no-cache private');
        // $response->headers->set('Pragma', 'public');
        // Send headers before outputting anything
        $response->sendHeaders();
        return $response;
    }

Sustituir icono radio button bootstrap 4

Vamos a sustituir el icono standard del radio button por algo más visible. En este caso práctico vamos a cambiar el modo de visualización de un listado, por lo que vamos a hacer que los iconos sean visiblemente más acorde a lo que se sugiere que es cambiar la vista a modo «Bloque» o modo «Listado».

Para realizar la estructura HTML he usado – bootstrap 4

Para los iconos – fontawesome 5

La estructura CSS tiene que estar bien anidada para no machacar otros estilos.