Cómo crear un plugin para Moodle 5 con formulario, permisos y base de datos

Cómo crear un plugin para Moodle con formulario, permisos y base de datos

Publicado el

· Actualizado el

· Por

Crear un plugin para Moodle parece fácil durante los primeros diez minutos. Creas una carpeta en local/, añades version.php, visitas «Administración del sitio → Notificaciones» y aparece la confirmación de instalación. La pantalla está en verde. El plugin existe.

Entonces llega el siguiente requisito:

Queremos que cada estudiante valore el curso. Una sola respuesta por curso, aunque pueda cambiarla más adelante. El profesorado debe ver la media y los comentarios, pero no los nombres. Y, si una persona solicita eliminar sus datos, el plugin tiene que saber encontrarlos y borrarlos.

La petición cabe en tres frases. Sin embargo, obliga a decidir quién puede actuar, dentro de qué curso, qué datos acepta el formulario, cómo se evita una respuesta duplicada, quién consulta el informe y qué ocurre con la información personal. El «hola mundo» ya no sirve como mapa. Si seguimos añadiendo condiciones a index.php, pronto tendremos un archivo que hace de pantalla, formulario, regla de negocio, control de acceso y repositorio al mismo tiempo.

Ese es el momento en el que un plugin empieza a parecerse a software de verdad.

Este tutorial comienza ahí. Vamos a construir local_coursefeedback, un pequeño «pulso del curso» para Moodle 5.x: el alumnado podrá valorar de 1 a 5, añadir un comentario y actualizar su respuesta; el profesorado consultará un informe anonimizado; y Moodle podrá exportar o borrar los datos personales guardados.

El resultado seguirá siendo lo bastante pequeño para recorrerlo de principio a fin. No obstante, ya incluirá las piezas que separan una demostración de una funcionalidad mantenible: Form API, capacidades, contextos, XMLDB, DML, una capa mínima de acceso a datos y Privacy API. No se trata de copiar doce archivos. Se trata de entender qué decisión protege cada uno.

La idea que guiará todo el tutorial: un plugin no se vuelve real porque tenga muchas líneas, sino porque debe proteger un circuito completo: entrada, contexto, permisos, datos, lectura y privacidad.

Qué aprenderás al crear un plugin para Moodle

Al terminar no solo tendrás código instalable. Podrás reconocer las fronteras que se repiten en casi cualquier extensión de Moodle:

  • Elegir entre un plugin local, un módulo de actividad o un bloque según el comportamiento que necesitas.
  • Relacionar el nombre Frankenstyle, la carpeta y el componente sin inconsistencias.
  • Modelar una tabla con XMLDB y convertir «una respuesta por curso» en una restricción real.
  • Autorizar acciones con capacidades dentro del contexto del curso, sin depender de nombres de roles.
  • Recibir y validar datos con Form API, y persistirlos mediante la API DML.
  • Separar la coordinación HTTP, el formulario y el acceso a datos sin introducir una arquitectura ceremonial.
  • Construir una lectura diferente para el informe y minimizar los datos que muestra.
  • Incorporar Privacy API desde el diseño, no como una tarea pendiente para producción.

El resultado del tutorial

Así, un estudiante podrá enviar una valoración y modificarla más adelante. Un docente editor o gestor podrá consultar el total de respuestas, la media y los comentarios sin nombres. La base de datos garantizará una sola fila por persona y curso, y Moodle sabrá cómo localizar, exportar y eliminar los datos personales del plugin.

Cómo utilizar esta guía para crear un plugin para Moodle

Si estás en este puntoEmpieza porQué te llevarás
Es tu primera extensión de MoodleLee el modelo mental y la elección del tipo de pluginEntenderás el mapa antes de tocar código
Ya has creado un bloque sencilloVe a la estructura y recorre los doce pasosPasarás de renderizar contenido a gestionar datos y permisos
Ya desarrollas pluginsRevisa XMLDB, capacidades, repositorio y privacidadPodrás discutir las decisiones y adaptar el patrón
Quieres evaluar el ejemploSalta a instalación, matriz de pruebas y límitesSabrás qué valida el tutorial y qué falta para producción

Por ejemplo, si todavía no has creado ninguna extensión, puedes empezar por la guía para crear tu primer bloque en Moodle. Allí se presentan la estructura mínima, las cadenas de idioma y el ciclo de instalación con un ejemplo más corto.

Qué necesitas antes de crear un plugin para Moodle

  • Una instalación de desarrollo de Moodle 5.0 o posterior; nunca pruebes por primera vez en producción.
  • Acceso al código de Moodle y una cuenta administradora para instalar el componente.
  • Conocimientos básicos de PHP, clases, arrays y formularios.
  • Dos usuarios de prueba con perfiles distintos: estudiante y docente editor.

Por tanto, el ejemplo no usa JavaScript, servicios externos, colas ni plantillas Mustache. Esa renuncia es deliberada: aprenderemos el circuito esencial sin esconderlo bajo más infraestructura de la necesaria.

Crear un plugin para Moodle más allá del «hola mundo»

Un ejemplo mínimo suele detenerse después de imprimir una cadena con $OUTPUT. Aquí recorreremos un ciclo completo:

Flujo completo de un plugin de Moodle desde el usuario hasta la base de datos, el informe y Privacy API
El ciclo completo de local_coursefeedback: contexto, capacidad, formulario, repositorio, persistencia, informe y privacidad.

Sin embargo, la ruta de escritura y la de lectura no tienen los mismos permisos. El estudiante puede guardar su respuesta, pero no consultar las de los demás. El docente puede abrir el informe, pero el ejemplo no le entrega los identificadores de los participantes. Esta separación no es un detalle posterior: forma parte del diseño.

De este modo, podemos leer el circuito como cinco fronteras. Cada una responde a una pregunta diferente:

FronteraPreguntaPieza principal
Integración¿Cómo descubre Moodle el componente?version.php, nombres y callbacks
Autorización¿Quién puede hacer cada acción y dónde?Contexto y capacidades
Entrada¿Qué datos acepta el plugin?Form API y validación
Persistencia¿Qué regla deben cumplir los datos?XMLDB, DML y repositorio
Gobierno¿Quién puede leer, exportar o borrar?Informe y Privacy API

Modelo mental reutilizable: cada vez que añadas una pantalla, una tarea programada, un servicio web o un evento, vuelve a recorrer estas cinco fronteras. Te obligan a pensar en algo más que «dónde pongo este código».

Qué hará exactamente el plugin

ActorAcciónRegla
EstudianteValorar el curso del 1 al 5Solo una respuesta por curso
EstudianteAñadir un comentario opcionalMáximo 1000 caracteres
EstudianteVolver a guardarActualiza la respuesta anterior
Docente editor o gestorConsultar el informeVe resultados agregados y comentarios sin nombre
MoodleExportar o borrar datos de una personaEl plugin participa mediante Privacy API

Por supuesto, no estamos construyendo una herramienta de encuestas preparada para producción. Faltarían aspectos como ventanas de participación, escalas configurables, moderación, exportación CSV, eventos, pruebas automatizadas y análisis estadístico. El objetivo es aprender a crear un plugin para Moodle con una estructura suficientemente real como para poder evolucionarla.

Antes de crear un plugin para Moodle: elegir el tipo correcto

Antes de crear un plugin para Moodle debemos elegir el tipo más específico. Si quisiéramos que el profesorado añadiera varias encuestas como actividades dentro del curso, con fechas y configuración propia para cada instancia, probablemente deberíamos desarrollar un módulo de actividad en mod/.

En cambio, en este ejemplo la funcionalidad es transversal: aparece automáticamente en todos los cursos y no necesita que el docente cree una actividad. Por eso encaja razonablemente como plugin local. La carpeta será local/coursefeedback y, siguiendo la convención Frankenstyle, su nombre de componente será local_coursefeedback.

TipoEncaja cuando…No es la mejor elección cuando…
localLa función es transversal al sitio y no necesita instancias creadas por el docenteEl usuario debe añadir, configurar y calificar varias instancias dentro de un curso
modCada actividad tiene configuración, fechas, participación y ciclo de vida propiosSolo necesitas una utilidad global o una integración de apoyo
blockLa función se presenta como contenido auxiliar en una región del curso o del panelEl bloque sería únicamente una puerta hacia una aplicación con dominio propio
Comparación entre plugins mod, local y block de Moodle según el ciclo de vida de la funcionalidad
El tipo de plugin se elige por el comportamiento de la funcionalidad, no por la comodidad de la carpeta.

Una carpeta local no es un cajón de sastre. Que un plugin pueda guardar tablas, añadir páginas y escuchar eventos no significa que todo deba implementarse como local_. El comportamiento y el ciclo de vida de la funcionalidad deben decidir el tipo.

Si quieres profundizar en la alternativa visual, la segunda parte de la guía de bloques profesionales explica configuración, seguridad, plantillas y caché. La comparación ayuda a evitar que un bloque acabe absorbiendo responsabilidades que pertenecen a otro tipo de plugin.

La estructura de archivos para crear un plugin para Moodle

Para crear este plugin para Moodle utilizaremos los siguientes archivos:

RutaResponsabilidad
version.phpIdentidad, versión y compatibilidad
db/install.xmlEsquema inicial de la tabla
db/access.phpCapacidades y roles por defecto
lang/en/local_coursefeedback.phpCadenas base en inglés
lang/es/local_coursefeedback.phpTraducción al castellano
classes/form/feedback_form.phpDefinición y validación del formulario
classes/local/feedback_repository.phpLectura y escritura de valoraciones
index.phpPágina para enviar o editar la respuesta
report.phpInforme protegido para docentes y gestores
lib.phpCallback mínimo para añadir enlaces a la navegación
classes/privacy/provider.phpDescripción, exportación y borrado de datos personales
Árbol de archivos del plugin local_coursefeedback para Moodle
La estructura separa contratos de Moodle, lógica autocargada, páginas HTTP y privacidad.

Así, la estructura ya cuenta una historia. Sigue el mapa de archivos comunes de los plugins de Moodle: las páginas HTTP son finas, el formulario vive en una clase autocargada, la persistencia está aislada y lib.php solo contiene el callback que Moodle necesita descubrir.

Lee el árbol por responsabilidades. Los archivos de db/ declaran contratos instalables; lang/ concentra la interfaz traducible; classes/ contiene código autocargado; las páginas de la raíz coordinan peticiones; y lib.php expone únicamente los callbacks que Moodle busca por nombre.

1. Declarar el componente en version.php

PHP
<?php
defined('MOODLE_INTERNAL') || die();

$plugin->component = 'local_coursefeedback';
$plugin->version = 2026081000;
$plugin->requires = 2025041400;
$plugin->maturity = MATURITY_BETA;
$plugin->release = '1.0.0';

Como detalla la referencia oficial de version.php, component debe coincidir con el nombre Frankenstyle. version permite a Moodle detectar cambios. requires establece Moodle 5.0 como versión mínima; el identificador de Moodle 5.0.0 es 2025041400. maturity comunica el estado del componente y release ofrece una versión legible para administradores.

Además, incrementaremos version cuando Moodle necesite ejecutar el proceso de actualización o invalidar cachés asociadas al componente. La guía oficial de actualización de plugins lo exige, entre otros casos, tras cambios en db/, JavaScript, nuevas clases autocargadas, ajustes o cadenas de idioma. Un cambio puramente interno que no afecta a esos contratos puede no necesitarlo, pero publicar una nueva versión del plugin sin avanzar el número termina generando despliegues difíciles de diagnosticar.

Pregunta de control: si copias la carpeta en otra instalación, ¿Moodle puede identificar sin ambigüedad el componente, comprobar si es compatible y decidir si debe actualizarlo? Si la respuesta depende del nombre de la carpeta o de una nota externa, version.php todavía no está haciendo todo su trabajo.

2. Crear la tabla con XMLDB

Al crear un plugin para Moodle con datos propios no generamos la tabla mediante SQL específico. En nuestro caso almacenará el curso, el usuario, la valoración, el comentario y las fechas. El archivo db/install.xml, que conviene crear y mantener con el editor XMLDB de Moodle, describe su estado final para una instalación nueva.

XML
<?xml version="1.0" encoding="UTF-8" ?>
<XMLDB PATH="local/coursefeedback/db" VERSION="2026081000"
    COMMENT="XMLDB file for local_coursefeedback"
    xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
    xsi:noNamespaceSchemaLocation="../../../lib/xmldb/xmldb.xsd">
  <TABLES>
    <TABLE NAME="local_coursefeedback"
        COMMENT="Stores one feedback response per user and course">
      <FIELDS>
        <FIELD NAME="id" TYPE="int" LENGTH="10"
            NOTNULL="true" SEQUENCE="true"/>
        <FIELD NAME="courseid" TYPE="int" LENGTH="10"
            NOTNULL="true" SEQUENCE="false"/>
        <FIELD NAME="userid" TYPE="int" LENGTH="10"
            NOTNULL="true" SEQUENCE="false"/>
        <FIELD NAME="rating" TYPE="int" LENGTH="2"
            NOTNULL="true" SEQUENCE="false"/>
        <FIELD NAME="comment" TYPE="text"
            NOTNULL="false" SEQUENCE="false"/>
        <FIELD NAME="timecreated" TYPE="int" LENGTH="10"
            NOTNULL="true" SEQUENCE="false"/>
        <FIELD NAME="timemodified" TYPE="int" LENGTH="10"
            NOTNULL="true" SEQUENCE="false"/>
      </FIELDS>
      <KEYS>
        <KEY NAME="primary" TYPE="primary" FIELDS="id"/>
        <KEY NAME="courseid" TYPE="foreign" FIELDS="courseid"
            REFTABLE="course" REFFIELDS="id"/>
        <KEY NAME="userid" TYPE="foreign" FIELDS="userid"
            REFTABLE="user" REFFIELDS="id"/>
      </KEYS>
      <INDEXES>
        <INDEX NAME="courseuser" UNIQUE="true"
            FIELDS="courseid, userid"/>
      </INDEXES>
    </TABLE>
  </TABLES>
</XMLDB>

De este modo, el índice único courseuser convierte una regla funcional en una garantía de datos: una persona no puede tener dos filas para el mismo curso. La interfaz intentará actualizar el registro existente, pero la base de datos sigue protegiendo la invariancia.

Modelo de datos de local_coursefeedback con índice único por curso y usuario
El índice único sobre courseid y userid garantiza una sola respuesta por persona y curso.

No escribas el prefijo mdl_. En XMLDB y en la API DML utilizamos local_coursefeedback. Moodle aplica el prefijo real de cada instalación.

Por último, conviene generar y modificar este archivo con el editor XMLDB integrado en «Administración del sitio → Desarrollo → Editor XMLDB». Si alteramos la tabla después de publicar la primera versión, install.xml seguirá describiendo el estado final, pero las instalaciones existentes necesitarán los pasos correspondientes en db/upgrade.php.

3. Separar permisos con db/access.php

Iniciar sesión no basta. Para crear un plugin para Moodle que respete los roles del campus debemos preguntar qué puede hacer cada persona dentro del contexto correcto.

PHP
<?php
defined('MOODLE_INTERNAL') || die();

$capabilities = [
    'local/coursefeedback:submit' => [
        'riskbitmask' => RISK_SPAM,
        'captype' => 'write',
        'contextlevel' => CONTEXT_COURSE,
        'archetypes' => [
            'student' => CAP_ALLOW,
        ],
    ],
    'local/coursefeedback:viewreports' => [
        'riskbitmask' => RISK_PERSONAL,
        'captype' => 'read',
        'contextlevel' => CONTEXT_COURSE,
        'archetypes' => [
            'editingteacher' => CAP_ALLOW,
            'manager' => CAP_ALLOW,
        ],
    ],
];

Además, las dos capacidades viven en CONTEXT_COURSE. La Access API de Moodle permite así que una misma persona tenga permiso en un curso y no en otro. Los arquetipos solo proponen valores durante la instalación: el administrador puede modificar después las capacidades de cada rol.

En las páginas no comprobaremos si el usuario «es estudiante» o «es profesor». Comprobaremos capacidades. Los nombres de rol pueden cambiar, combinarse o personalizarse; la autorización debe apoyarse en lo que la persona puede hacer.

Capacidades submit y viewreports del plugin de Moodle dentro del contexto de curso
La autorización se expresa mediante capacidades dentro del contexto del curso, no mediante nombres de rol.

Checkpoint 1: ya existen tres contratos

Moodle puede identificar el componente, sabe qué tabla debe crear y conoce las acciones autorizables. Todavía no hay pantalla, pero ya hemos definido identidad, datos y permisos. Esta secuencia reduce el riesgo de diseñar primero una interfaz y preguntar después quién debería utilizarla.

4. Añadir las cadenas de idioma

Todo texto visible se recupera con get_string(). Según la documentación de los archivos comunes de un plugin, el archivo inglés es obligatorio aunque el campus principal esté en castellano.

Por ejemplo, el siguiente bloque es un extracto para mostrar el patrón. El paquete incluye también las cadenas de la escala, el informe, los errores de validación y la Privacy API. En ambos idiomas deben existir exactamente las mismas claves.

PHP
<?php
$string['pluginname'] = 'Course pulse';
$string['coursefeedback'] = 'Course pulse';
$string['rating'] = 'Rating';
$string['comment'] = 'Comment';
$string['comment_help'] = 'Optional. Maximum 1,000 characters.';
$string['savefeedback'] = 'Save feedback';
$string['feedbacksaved'] = 'Your feedback has been saved.';
$string['coursefeedback:submit'] = 'Submit course feedback';
$string['coursefeedback:viewreports'] = 'View course feedback reports';

La versión castellana usa las mismas claves en lang/es/local_coursefeedback.php. Las cadenas de las capacidades se llaman coursefeedback:submit y coursefeedback:viewreports, sin el prefijo local/.

No traduzcas dentro del código. La cadena en inglés es el contrato base; los paquetes de idioma aportan las traducciones. Esto permite que la misma instalación muestre el plugin en varios idiomas sin bifurcar la lógica.

5. Crear un plugin para Moodle: definir el formulario con Form API

Al crear un plugin para Moodle podríamos escribir un <form> manual y leer $_POST. También tendríamos que resolver la clave de sesión, la limpieza de parámetros, los mensajes de error, la accesibilidad y la integración visual. La Forms API de Moodle ya reúne esas preocupaciones.

PHP
<?php
namespace local_coursefeedback\form;

defined('MOODLE_INTERNAL') || die();
require_once($CFG->libdir . '/formslib.php');

final class feedback_form extends \moodleform {
    public function definition(): void {
        $mform = $this->_form;

        $ratings = [
            0 => get_string('chooserating', 'local_coursefeedback'),
            1 => get_string('rating1', 'local_coursefeedback'),
            2 => get_string('rating2', 'local_coursefeedback'),
            3 => get_string('rating3', 'local_coursefeedback'),
            4 => get_string('rating4', 'local_coursefeedback'),
            5 => get_string('rating5', 'local_coursefeedback'),
        ];

        $mform->addElement(
            'select',
            'rating',
            get_string('rating', 'local_coursefeedback'),
            $ratings
        );
        $mform->setType('rating', PARAM_INT);

        $mform->addElement(
            'textarea',
            'comment',
            get_string('comment', 'local_coursefeedback'),
            ['rows' => 6, 'maxlength' => 1000]
        );
        $mform->setType('comment', PARAM_TEXT);
        $mform->addHelpButton('comment', 'comment', 'local_coursefeedback');

        $this->add_action_buttons(
            false,
            get_string('savefeedback', 'local_coursefeedback')
        );
    }

    public function validation($data, $files): array {
        $errors = parent::validation($data, $files);
        $rating = (int) ($data['rating'] ?? 0);

        if ($rating < 1 || $rating > 5) {
            $errors['rating'] = get_string(
                'invalidrating',
                'local_coursefeedback'
            );
        }

        if (\core_text::strlen(trim($data['comment'] ?? '')) > 1000) {
            $errors['comment'] = get_string(
                'commentmaxlength',
                'local_coursefeedback'
            );
        }

        return $errors;
    }
}

setType() limpia el valor recibido según el tipo esperado. El atributo HTML maxlength mejora la experiencia, pero no sustituye la validación del servidor. Una petición se puede construir sin utilizar el navegador.

En la escala reservamos el valor 0 para «Elige una valoración» y la validación acepta únicamente del 1 al 5. El comentario pasa por PARAM_TEXT y vuelve a comprobarse con core_text::strlen(), que cuenta correctamente texto multibyte. Son decisiones pequeñas, pero convierten una expectativa de interfaz en un contrato verificable en el servidor.

La Form API no guarda nada. Define, presenta y valida el formulario. La decisión sobre insertar o actualizar pertenece a nuestra lógica de aplicación.

6. Encapsular el acceso a datos

En principio, para un ejemplo corto podríamos usar $DB directamente desde index.php. Sin embargo, crear una clase pequeña evita que la página termine mezclando autorización, procesamiento HTTP, reglas de negocio y consultas. Para mantenerla legible, mostraremos el archivo en dos fragmentos consecutivos.

PHP
<?php
namespace local_coursefeedback\local;

defined('MOODLE_INTERNAL') || die();

final class feedback_repository {
    public function get_for_user(int $courseid, int $userid): ?\stdClass {
        global $DB;

        $record = $DB->get_record('local_coursefeedback', [
            'courseid' => $courseid,
            'userid' => $userid,
        ]);

        return $record ?: null;
    }

    public function save(
        int $courseid,
        int $userid,
        int $rating,
        string $comment
    ): int {
        global $DB;

        if ($rating < 1 || $rating > 5) {
            throw new \coding_exception(
                'The rating must be between 1 and 5.'
            );
        }

        $now = time();
        $existing = $this->get_for_user($courseid, $userid);

        if ($existing) {
            $existing->rating = $rating;
            $existing->comment = trim($comment);
            $existing->timemodified = $now;
            $DB->update_record('local_coursefeedback', $existing);
            return (int) $existing->id;
        }

        return (int) $DB->insert_record(
            'local_coursefeedback',
            (object) [
                'courseid' => $courseid,
                'userid' => $userid,
                'rating' => $rating,
                'comment' => trim($comment),
                'timecreated' => $now,
                'timemodified' => $now,
            ]
        );
    }

A continuación, la misma clase añade la consulta de respuestas y el resumen que consumirá el informe. Este segundo fragmento continúa justo después del método save():

PHP

    public function get_course_responses(int $courseid): array {
        global $DB;

        return $DB->get_records(
            'local_coursefeedback',
            ['courseid' => $courseid],
            'timemodified DESC',
            'id, rating, comment, timemodified'
        );
    }

    public function summarise(array $responses): array {
        $total = count($responses);
        $sum = 0;

        foreach ($responses as $response) {
            $sum += (int) $response->rating;
        }

        return [
            'total' => $total,
            'average' => $total > 0 ? $sum / $total : 0.0,
        ];
    }
}

La API DML de Moodle permite que este código funcione con las bases de datos soportadas sin escribir una variante para MySQL y otra para PostgreSQL. Cuando solo necesitamos operaciones simples, get_record(), insert_record() y update_record() son preferibles a construir SQL manual.

Además, la comprobación de la valoración aparece tanto en el formulario como en la clase. No es una duplicación accidental: el formulario protege su entrada y la clase defiende su propio contrato si mañana se invoca desde otra ruta.

Dos defensas, dos responsabilidades. El formulario devuelve un error comprensible al usuario. El repositorio impide que una llamada futura —por ejemplo, un servicio web o una tarea— guarde una valoración imposible. Validar solo en la pantalla convierte la regla de negocio en una sugerencia.

7. Procesar la valoración en index.php

Esta página muestra otro patrón recurrente al crear un plugin para Moodle: recibe el identificador del curso, carga el contexto, exige la capacidad y delega el formulario y la persistencia.

PHP
<?php
require_once(__DIR__ . '/../../config.php');

$courseid = required_param('courseid', PARAM_INT);
$course = get_course($courseid);

require_login($course);

$context = context_course::instance($courseid);
require_capability('local/coursefeedback:submit', $context);

$url = new moodle_url(
    '/local/coursefeedback/index.php',
    ['courseid' => $courseid]
);

$PAGE->set_url($url);
$PAGE->set_context($context);
$PAGE->set_course($course);
$PAGE->set_pagelayout('incourse');
$PAGE->set_title(get_string('coursefeedback', 'local_coursefeedback'));
$PAGE->set_heading($course->fullname);

$repository = new \local_coursefeedback\local\feedback_repository();
$current = $repository->get_for_user($courseid, $USER->id);
$form = new \local_coursefeedback\form\feedback_form($url);

if ($data = $form->get_data()) {
    $repository->save(
        $courseid,
        $USER->id,
        (int) $data->rating,
        (string) $data->comment
    );

    redirect(
        $url,
        get_string('feedbacksaved', 'local_coursefeedback'),
        null,
        \core\output\notification::NOTIFY_SUCCESS
    );
}

if ($current) {
    $form->set_data($current);
}

echo $OUTPUT->header();
echo $OUTPUT->heading(
    get_string('coursefeedback', 'local_coursefeedback')
);
$form->display();
echo $OUTPUT->footer();

Por tanto, el orden importa. Antes de leer o modificar datos hemos identificado el curso, iniciado la sesión y comprobado local/coursefeedback:submit en su contexto. La URL se construye con moodle_url y la respuesta termina con una redirección para evitar reenviar el formulario al recargar la página.

Form API incorpora la protección de clave de sesión en sus formularios. Eso no elimina la necesidad de validar el parámetro, verificar el curso y exigir la capacidad.

Flujo de Form API, validación, repositorio y DML en un plugin de Moodle
index.php coordina, Form API valida y el repositorio decide entre insertar o actualizar mediante DML.

Checkpoint 2: la ruta de escritura ya está cerrada

Una petición válida recorre una secuencia completa: parámetro → curso → sesión → contexto → capacidad → formulario → validación → repositorio → DML → redirección. Si una nueva ruta permite guardar datos saltándose alguno de esos pasos, no es una alternativa equivalente: ha abierto otra frontera de seguridad.

8. Construir un informe protegido

report.php sigue la misma preparación de página, pero cambia la autorización:

PHP
$courseid = required_param('courseid', PARAM_INT);
$course = get_course($courseid);
require_login($course);

$context = context_course::instance($courseid);
require_capability(
    'local/coursefeedback:viewreports',
    $context
);

$repository = new \local_coursefeedback\local\feedback_repository();
$responses = $repository->get_course_responses($courseid);
$summary = $repository->summarise($responses);

Asimismo, la selección de campos del repositorio es intencionada. Aunque la tabla contiene userid para garantizar una respuesta por persona, get_course_responses() no lo recupera. Después summarise() calcula el total y la media, y la página construye una html_table con la valoración, el comentario y la fecha.

PHP
$table = new html_table();
$table->head = [
    get_string('rating', 'local_coursefeedback'),
    get_string('comment', 'local_coursefeedback'),
    get_string('lastmodified', 'local_coursefeedback'),
];

foreach ($responses as $response) {
    $comment = trim((string) $response->comment);

    $table->data[] = [
        (int) $response->rating . '/5',
        $comment !== ''
            ? format_text(
                $comment,
                FORMAT_PLAIN,
                ['context' => $context]
            )
            : get_string('nocomment', 'local_coursefeedback'),
        userdate((int) $response->timemodified),
    ];
}

echo html_writer::table($table);

FORMAT_PLAIN deja clara la decisión del ejemplo: el comentario no admite HTML enriquecido. Si utilizáramos un editor, necesitaríamos tratar el formato y los archivos asociados de otra forma, siguiendo las reglas de la Output API.

Minimiza desde la consulta. Omitir userid al renderizar es mejor que mostrarlo, pero no recuperarlo es aún más claro: la pantalla no recibe un dato que no necesita. La minimización empieza en la selección de campos.

9. Añadir enlaces sin llenar lib.php de lógica

Además, el plugin puede añadir enlaces contextuales cuando el usuario está dentro de un curso. El callback consulta las mismas capacidades que las páginas:

PHP
<?php
defined('MOODLE_INTERNAL') || die();

function local_coursefeedback_extend_navigation(
    global_navigation $navigation
): void {
    global $PAGE;

    if (empty($PAGE->course) ||
            (int) $PAGE->course->id === SITEID) {
        return;
    }

    $courseid = (int) $PAGE->course->id;
    $context = context_course::instance($courseid);

    if (has_capability('local/coursefeedback:submit', $context)) {
        $navigation->add(
            get_string('coursefeedback', 'local_coursefeedback'),
            new moodle_url(
                '/local/coursefeedback/index.php',
                ['courseid' => $courseid]
            ),
            navigation_node::TYPE_CUSTOM
        );
    }

    if (has_capability(
            'local/coursefeedback:viewreports',
            $context
    )) {
        $navigation->add(
            get_string('viewreport', 'local_coursefeedback'),
            new moodle_url(
                '/local/coursefeedback/report.php',
                ['courseid' => $courseid]
            ),
            navigation_node::TYPE_CUSTOM
        );
    }
}

Moodle carga muchos archivos lib.php. Por eso, como recomienda la documentación de plugins locales, debe actuar como puente: detectar el callback, realizar comprobaciones baratas y delegar cualquier lógica interna a clases autocargadas.

Un enlace no protege una página. Ocultar «Ver informe» a quien no tiene permiso mejora la experiencia, pero un usuario puede escribir la URL directamente. Por eso report.php vuelve a ejecutar require_capability(). La navegación orienta; la página autoriza.

10. No olvidar la privacidad

La tabla contiene un identificador de usuario y contenido escrito por esa persona. Por tanto, el plugin almacena datos personales y debe integrarse con la Privacy API de Moodle. Añadir una tabla y terminar el tutorial aquí dejaría fuera una responsabilidad importante.

La clase classes/privacy/provider.php debe cubrir tres grupos de operaciones:

  • Metadatos: declarar qué tabla y campos contienen datos personales.
  • Localización y exportación: encontrar los contextos de curso donde una persona tiene respuestas y exportarlas.
  • Eliminación: borrar los datos de una persona, de varias personas aprobadas o de todo un contexto.
PHP
final class provider implements
    \core_privacy\local\metadata\provider,
    \core_privacy\local\request\plugin\provider,
    \core_privacy\local\request\core_userlist_provider {

    public static function get_metadata(
        collection $collection
    ): collection {
        $collection->add_database_table(
            'local_coursefeedback',
            [
                'courseid' => 'privacy:metadata:courseid',
                'userid' => 'privacy:metadata:userid',
                'rating' => 'privacy:metadata:rating',
                'comment' => 'privacy:metadata:comment',
                'timecreated' => 'privacy:metadata:timecreated',
                'timemodified' => 'privacy:metadata:timemodified',
            ],
            'privacy:metadata:table'
        );

        return $collection;
    }

    // get_contexts_for_userid()
    // get_users_in_context()
    // export_user_data()
    // delete_data_for_all_users_in_context()
    // delete_data_for_user()
    // delete_data_for_users()
}

Por eso, el código completo acompaña al ejemplo. Aquí lo importante es entender el contrato: Moodle no puede exportar ni eliminar correctamente datos que un plugin guarda si ese plugin no explica dónde están y cómo tratarlos.

«El informe es anónimo» no significa «no hay datos personales». El plugin conserva userid para controlar la unicidad y permitir que la persona edite su respuesta. La privacidad se evalúa sobre los datos almacenados, no solo sobre lo que muestra una pantalla.

Separación entre informe anonimizado y operaciones de privacidad en local_coursefeedback
El informe minimiza los datos mostrados; Privacy API permite exportar y eliminar los registros del interesado.

Checkpoint 3: lectura y gobierno ya son rutas distintas

En cambio, el informe responde a una necesidad docente y devuelve solo los campos necesarios. Privacy API responde a los derechos sobre los datos y sí puede localizar registros por usuario. Que ambas rutas consulten la misma tabla no significa que deban compartir propósito, permisos ni salida.

11. Crear un plugin para Moodle: instalarlo y comprobarlo

Necesitas un entorno desechable donde puedas instalar, actualizar y restaurar el componente sin miedo. Si aún no lo tienes, la guía para montar Moodle con Docker te ofrece una base local reproducible.

  1. Copia la carpeta del ejemplo en local/coursefeedback.
  2. Accede como administrador a «Administración del sitio → Notificaciones».
  3. Comprueba que Moodle detecta local_coursefeedback.
  4. Confirma la actualización para crear la tabla y registrar las capacidades.
  5. Purga las cachés.
  6. Entra en un curso con un estudiante y abre «Pulso del curso».
  7. Entra con un docente editor y abre el informe.

Durante el desarrollo, crear un plugin para Moodle también implica repetir instalaciones y actualizaciones. Desde consola podemos ejecutar:

Bash
php admin/cli/upgrade.php --non-interactive
php admin/cli/purge_caches.php

Si modificas db/access.php, db/install.xml o las definiciones de privacidad después de instalar, incrementa la versión cuando corresponda y ejecuta de nuevo la actualización. Borrar la tabla a mano no es un flujo de actualización.

12. Probar el comportamiento, no solo la pantalla

PruebaResultado esperado
Acceso sin sesiónMoodle redirige al inicio de sesión
Usuario sin capacidad de envíoRecibe una excepción de permisos
Valoración 0, 6 o manipuladaLa validación la rechaza
Comentario de más de 1000 caracteresEl servidor devuelve un error de formulario
Segundo envío del mismo usuarioActualiza la fila; no crea otra
Estudiante abre report.phpNo puede consultar el informe
Docente editor abre el informeVe total, media y comentarios
Comentario con etiquetas HTMLSe trata como texto plano
Exportación de privacidadIncluye valoración, comentario y fechas
Solicitud de borradoElimina las filas aprobadas

La siguiente mejora al crear un plugin para Moodle sería automatizar esta matriz con PHPUnit y Behat. El repositorio permite probar inserción y actualización; el formulario puede probar su validación; las páginas y permisos encajan bien en escenarios Behat.

Proceso para instalar, probar y evolucionar un plugin de Moodle
Un plugin mantenible se instala de forma repetible, se prueba por comportamiento y evoluciona mediante contratos explícitos.

Checkpoint final: una prueba por cada decisión

La matriz no intenta comprobar que «la página se ve». Comprueba contratos: sesión, capacidad, rango, longitud, unicidad, minimización, exportación y borrado. Esa correspondencia entre decisión y prueba es la base de una suite automatizada útil.

Qué hemos separado y por qué

CapaDecisiónEvita
PáginaContexto, autorización y flujo HTTPAccesos sin permiso y controladores gigantes
FormularioCampos, limpieza y validaciónLeer $_POST y repetir seguridad básica
RepositorioOperaciones DML y regla de actualizaciónConsultas dispersas por varias pantallas
XMLDBEsquema e índice únicoDependencia de un motor y duplicados
CapabilitiesAutorización por contextoComprobar nombres de roles
Privacy APIExportación y borradoDatos invisibles para el sistema de privacidad

No hace falta convertir un plugin de doce archivos en una arquitectura ceremonial. Sí conviene impedir que toda la funcionalidad termine en index.php. La separación elegida es pequeña, visible y fácil de extender.

Errores frecuentes al crear un plugin para Moodle

Al crear un plugin para Moodle, muchos fallos no aparecen como errores de PHP. La pantalla carga, pero la autorización es demasiado amplia, la regla de datos solo vive en el formulario o una actualización deja instalaciones antiguas en otro estado. Estas son las señales que conviene buscar:

  • Confiar solo en require_login(): autenticar no es autorizar.
  • Comprobar roles: un rol personalizado puede romper la lógica. Comprueba capacidades.
  • Usar el contexto de sistema: perderías la posibilidad de asignar permisos por curso.
  • Guardar dos respuestas y deduplicar después: expresa la unicidad en el esquema.
  • Escribir SQL específico de MySQL: usa DML y los placeholders de Moodle.
  • Mostrar directamente el comentario: limpia la entrada y formatea la salida.
  • Guardar userid y omitir Privacy API: el plugin almacena datos personales.
  • Introducir toda la lógica en lib.php: Moodle lo carga con frecuencia.
  • Editar install.xml en una versión ya instalada: las instalaciones existentes necesitan upgrade.php.

Cómo evolucionar un plugin de Moodle sin perder el mapa

A partir de ahí, es tentador añadir diez funcionalidades a la vez. Sin embargo, resulta más útil evolucionar por capas y comprobar qué frontera cambia en cada una:

CapaMejoras posiblesDecisión nueva
Control de participaciónFechas de apertura y cierre, activación por categorías o cursos, umbral mínimo antes de mostrar comentariosDónde vive la configuración y quién puede modificarla
Integración y operaciónExportación CSV, eventos al crear o actualizar, observador al eliminar un cursoQué consumidores existen y cómo se conserva la consistencia
Presentación y volumenPlantillas Mustache, filtros y paginación con table_sqlCómo separar renderizado, consulta y rendimiento
Producto de encuestasPreguntas configurables, varias escalas, moderación y análisisSi la funcionalidad sigue siendo transversal o debe convertirse en mod_
Calidad automatizadaPHPUnit, Behat, pruebas de privacidad y comprobaciones de estiloQué contratos deben bloquear una regresión

Vuelve a preguntar «¿sigue siendo un plugin local?» Si cada docente necesita crear varias encuestas con configuración, fechas y resultados independientes, el dominio ya se parece a una actividad. Evolucionar también significa reconocer cuándo la primera decisión ha dejado de encajar.

Conclusión: crear un plugin para Moodle no empieza por el formulario

Crear un plugin para Moodle no consiste solo en colocar PHP dentro de local/. En cuanto la funcionalidad recibe datos aparecen decisiones sobre contexto, permisos, validación, persistencia, salida, actualización y privacidad.

Además, el «pulso del curso» es deliberadamente pequeño, pero recorre el circuito completo. Un estudiante envía una respuesta. Form API la valida. Una capacidad autoriza la acción en un curso concreto. El repositorio inserta o actualiza. XMLDB protege la unicidad. El docente utiliza otra lectura. Privacy API mantiene esos registros dentro del gobierno de datos de Moodle.

La lección al crear un plugin para Moodle no es que todo componente necesite exactamente estos doce archivos. Es que cada responsabilidad debe tener un lugar reconocible y cada regla importante debe existir en algo más sólido que la intención del desarrollador.

Qué conviene recordar: Moodle detecta carpetas con facilidad. Lo difícil —y lo verdaderamente reutilizable— es diseñar las fronteras del componente antes de que todo termine mezclado en una sola página.

Preguntas frecuentes sobre plugins de Moodle

Decisiones de diseño al crear un plugin para Moodle

¿Por qué no utilizar el módulo Feedback que ya incluye Moodle?

Para una necesidad real, primero deberías evaluar las funciones estándar y los plugins existentes. Aquí desarrollamos una versión mínima porque el objetivo es aprender las APIs y la estructura de un componente, no sustituir una herramienta madura. Si la actividad Feedback incluida en Moodle cubre el requisito, mantener menos código propio suele ser la mejor decisión.

¿Por qué guardar el usuario si el informe es anónimo?

Porque el ejemplo permite una sola respuesta por usuario y curso, y que cada persona la actualice. El informe no muestra la identidad, pero la tabla sí la conserva. Por eso implementamos Privacy API y evitamos afirmar que la recogida de datos es anónima.

¿Podría eliminar userid y hacer la encuesta completamente anónima?

Sí, pero cambiarían las reglas: no podrías garantizar de forma sencilla una única respuesta por persona ni permitir que se editara después. Además, un comentario libre puede contener información personal aunque no exista una columna userid. Anonimizar el esquema no garantiza que el contenido lo sea.

Integridad, actualizaciones y paso a producción

¿El índice único resuelve también dos envíos simultáneos?

Protege la integridad e impide que queden dos filas para la misma pareja de curso y usuario. Sin embargo, dos inserciones casi simultáneas podrían hacer que una recibiera una excepción por clave duplicada. Un plugin de producción debería decidir cómo tratar esa carrera —por ejemplo, capturando el conflicto y reintentando como actualización— y cubrirla con una prueba.

¿Debo escribir install.xml a mano?

No es lo recomendable. Utiliza el editor XMLDB de Moodle: reduce errores de formato y ayuda a generar las definiciones necesarias para futuras actualizaciones. El XML del artículo sirve para entender el resultado, no para sustituir la herramienta.

¿Cuándo necesito db/upgrade.php?

Cuando una versión ya instalada debe pasar de un esquema anterior a uno nuevo. install.xml describe el estado final para instalaciones nuevas; upgrade.php aplica pasos incrementales sobre instalaciones existentes. La guía de actualización de plugins detalla este ciclo: cambiar solo install.xml no actualiza una tabla que ya fue creada.

¿Es este plugin apto para producción?

Es un ejemplo educativo funcional. Antes de desplegarlo deberías adaptar requisitos, añadir pruebas automatizadas, revisar accesibilidad y privacidad, definir retención, resolver carreras de escritura, gestionar la eliminación de cursos y ejecutar Moodle Code Checker y las comprobaciones de compatibilidad que correspondan a tu entorno.

Documentación oficial para desarrollar plugins de Moodle

Tu siguiente paso: prueba el circuito completo

Instala el ejemplo en un entorno local, ejecuta la matriz con dos usuarios y modifica una sola regla —por ejemplo, el rango de valoración o la longitud del comentario— en todas las capas que la protegen. Ese ejercicio enseña más que añadir otra pantalla copiando código.

Deja una respuesta

Tu dirección de correo electrónico no será publicada. Los campos obligatorios están marcados con *

Sobre mí

Soy Andrés Martínez Soto, CTO y consultor EdTech especializado en Moodle, LTI, IA educativa, arquitectura de plataformas e integración de sistemas educativos.

Ver perfil profesional

¿Necesitas ordenar tu ecosistema EdTech?

Te ayudo a revisar plataformas, LMS, integraciones, automatizaciones, datos e IA educativa con una visión técnica y pedagógica.

Buscar