DESARROLLO MOODLE · PARTE 2
El bloque de soporte ya aparece en el curso. Entonces cambia el correo de ayuda, un profesor pide ocultar la FAQ y el equipo de seguridad pregunta quién puede ver cada acción. En ese momento descubres la diferencia entre un bloque que funciona y uno que puede mantenerse. En esta segunda parte aprenderás a crear bloques profesionales en Moodle 4.5 mientras convertimos block_soporte_curso en un desarrollo configurable, seguro y preparado para crecer.
En la Parte 1 construimos la estructura mínima, registramos el plugin, añadimos sus cadenas de idioma, definimos quién podía incorporarlo a un curso y separamos la vista con Mustache. Ahora continuamos sobre ese mismo código: no empezamos otro ejemplo ni cambiamos de componente a mitad del tutorial.

Qué vas a conseguir en esta Parte 2
Al terminar, block_soporte_curso ya no dependerá de correos ni URLs escritos en el código. Tendrá una configuración global controlada por administración, ajustes propios por instancia, capacidades específicas y una salida preparada mediante una clase templatable y Mustache. Es la base que comparten los bloques profesionales en Moodle, aunque cada dominio añada después sus propias reglas.
- Configuración multinivel: una política global y excepciones controladas por curso.
- Seguridad por capacidades: comprobar qué puede hacer el usuario en el contexto exacto del bloque.
- Caché con criterio: usar MUC solo para trabajo costoso y con claves que no mezclen datos.
- Arquitectura mantenible: separar obtención de datos, preparación de la vista y HTML.
- Despliegue verificable: incrementar versión, actualizar Moodle y probar con varios roles.
Qué cambia al crear bloques profesionales en Moodle
La primera versión del plugin utilizaba cadenas de idioma para el correo de contacto y la URL de ayuda. Servía para aprender el flujo de un bloque, pero obliga a modificar archivos y desplegar una nueva versión cada vez que cambia un dato operativo. En esta entrega moveremos esos valores al lugar que les corresponde.
blocks/soporte_curso/
├── block_soporte_curso.php
├── edit_form.php
├── settings.php
├── version.php
├── classes/
│ ├── local/
│ │ └── support_service.php
│ └── output/
│ └── content.php
├── db/
│ ├── access.php
│ └── caches.php
├── lang/
│ ├── en/block_soporte_curso.php
│ └── es/block_soporte_curso.php
└── templates/
└── content.mustache
| Archivo | Responsabilidad | ¿Se ejecuta en cada render? |
|---|---|---|
settings.php | Ajustes globales gestionados por administración. | No. Forma parte del árbol de administración. |
edit_form.php | Configuración de una instancia concreta del bloque. | Solo al abrir o guardar el formulario. |
db/access.php | Definición inicial de capacidades. | No. Moodle procesa sus cambios durante la actualización. |
db/caches.php | Definiciones de caché MUC. | No. Requiere actualización de versión. |
classes/local/ | Servicios y obtención de datos. | Cuando los invoca la clase principal. |
classes/output/ y templates/ | Preparación y renderizado de la salida. | Sí, pero sin consultas ni reglas de negocio en la plantilla. |
No copies archivos aislados
Los fragmentos de esta Parte 2 sustituyen o amplían archivos de la Parte 1. Si actualizas db/access.php, copia el archivo completo o fusiona conscientemente las capacidades. Sobrescribirlo con dos capacidades nuevas y perder addinstance hará que el bloque deje de estar disponible en el selector.
1. Configuración de bloques profesionales en Moodle: quién decide cada valor
Un ajuste no es global o local por comodidad del desarrollador. Lo es por gobernanza. La institución debe controlar aquello que afecta a todos los cursos; el profesor solo debería adaptar lo que pertenece a su instancia.

| Decisión | Nivel adecuado | Motivo |
|---|---|---|
| Correo de soporte institucional | Global, con posible sustitución controlada | Debe existir un valor coherente para todo el sitio. |
| URL de la base de conocimiento | Global | Evita enlaces distintos o no aprobados en cada curso. |
| Título visible del bloque | Instancia | Puede adaptarse al lenguaje de cada curso. |
| Mostrar u ocultar la FAQ | Global + instancia | Administración habilita la función; el profesor decide si aporta valor en su curso. |
1.1. Ajustes globales con settings.php
Los ajustes con un nombre como block_soporte_curso/supportemail se almacenan en config_plugins y se recuperan con get_config(). La barra del nombre no es decorativa: mantiene el valor dentro del espacio del plugin y evita contaminar la configuración global de Moodle.
<?php
defined('MOODLE_INTERNAL') || die();
if ($ADMIN->fulltree) {
$settings->add(new admin_setting_configtext(
'block_soporte_curso/supportemail',
get_string('supportemail', 'block_soporte_curso'),
get_string('supportemail_desc', 'block_soporte_curso'),
'',
PARAM_EMAIL
));
$settings->add(new admin_setting_configtext(
'block_soporte_curso/faqurl',
get_string('faqurl', 'block_soporte_curso'),
get_string('faqurl_desc', 'block_soporte_curso'),
'',
PARAM_URL
));
$settings->add(new admin_setting_configcheckbox(
'block_soporte_curso/showfaq',
get_string('showfaq', 'block_soporte_curso'),
get_string('showfaq_desc', 'block_soporte_curso'),
1
));
}
La clase de cada ajuste define cómo se muestra y el parámetro PARAM_* sanea el valor al guardarlo. Aquí usamos PARAM_EMAIL y PARAM_URL porque describen mejor el contrato de los datos que un filtro genérico.
La configuración no es un gestor de secretos
Un token guardado mediante los ajustes estándar termina en la base de datos. Ocultar el texto en el formulario no equivale a cifrarlo. Si el plugin necesita credenciales externas, limita quién puede ver el ajuste, evita mostrarlas de nuevo, revisa copias de seguridad y registros, y valora un almacén de secretos de infraestructura.
Referencia oficial
La documentación de ajustes de administración de Moodle 4.5 explica por qué los nombres con componente se guardan en config_plugins y recomienda no crear nuevas opciones globales en $CFG.
1.2. Ajustes por instancia con edit_form.php
Cada copia del bloque tiene su propio configdata dentro de block_instances. Moodle serializa y recupera esos valores por nosotros, pero exige que los nombres del formulario comiencen por config_.
<?php
defined('MOODLE_INTERNAL') || die();
class block_soporte_curso_edit_form extends block_edit_form {
protected function specific_definition($mform): void {
$mform->addElement(
'header',
'config_header',
get_string('blocksettings', 'block_soporte_curso')
);
$mform->addElement(
'text',
'config_title',
get_string('configtitle', 'block_soporte_curso')
);
$mform->setType('config_title', PARAM_TEXT);
$mform->addElement(
'text',
'config_contactemail',
get_string('contactemail', 'block_soporte_curso')
);
$mform->setType('config_contactemail', PARAM_EMAIL);
$mform->addHelpButton(
'config_contactemail',
'contactemail',
'block_soporte_curso'
);
$mform->addElement(
'advcheckbox',
'config_showfaq',
get_string('showfaqinstance', 'block_soporte_curso')
);
$mform->setDefault('config_showfaq', 1);
}
}
El prefijo desaparece al leer el valor. Por tanto, config_contactemail se convierte en $this->config->contactemail. Esa asimetría es pequeña, pero explica muchos formularios que aparentemente guardan bien y después devuelven null.
La presencia de edit_form.php basta para que Moodle reconozca la configuración por instancia. En cambio, los ajustes globales de un plugin de tipo bloque sí exigen declarar has_config(). Añade ese método y utiliza specialization() para aplicar el título una vez que $this->config ya está disponible:
<?php
public function has_config(): bool {
return true;
}
public function specialization(): void {
if (!empty($this->config->title)) {
$this->title = format_string(
$this->config->title,
true,
['context' => $this->context]
);
}
}
Define la precedencia antes de programarla
En este tutorial, el correo de la instancia prevalece sobre el correo global; si ambos están vacíos, usamos el correo de soporte del sitio. Para la FAQ aplicamos otra regla: administración debe habilitarla y la instancia puede ocultarla, pero nunca reactivarla contra la política global. Escribe estas reglas antes de llenar el código de operadores ??.
Comprobación de avance
- Administración cambia el correo global y una instancia sin personalizar refleja el nuevo valor.
- Un profesor modifica el título y el correo de un único bloque sin afectar a otros cursos.
- Al desactivar globalmente la FAQ, ninguna instancia puede volver a mostrarla.
2. Seguridad de bloques profesionales en Moodle: capacidades y contextos
Moodle no pregunta si alguien “es profesor” para decidir qué puede hacer. Calcula capacidades a lo largo de una jerarquía de contextos. Un usuario puede tener permisos distintos en el sistema, una categoría, un curso, una actividad o una instancia concreta de bloque.

2.1. Actualizar db/access.php sin perder addinstance
La Parte 1 ya definía quién podía añadir el bloque. Ahora incorporamos dos capacidades: una para verlo y otra para acceder a futuras acciones de gestión. Como el bloque solo se admite en páginas de curso, eliminamos la capacidad myaddinstance, que ya no aportaba nada.
<?php
defined('MOODLE_INTERNAL') || die();
$capabilities = [
'block/soporte_curso:addinstance' => [
'riskbitmask' => RISK_SPAM | RISK_XSS,
'captype' => 'write',
'contextlevel' => CONTEXT_BLOCK,
'archetypes' => [
'editingteacher' => CAP_ALLOW,
'manager' => CAP_ALLOW,
],
'clonepermissionsfrom' => 'moodle/site:manageblocks',
],
'block/soporte_curso:view' => [
'captype' => 'read',
'contextlevel' => CONTEXT_BLOCK,
'archetypes' => [
'student' => CAP_ALLOW,
'teacher' => CAP_ALLOW,
'editingteacher' => CAP_ALLOW,
'manager' => CAP_ALLOW,
],
],
'block/soporte_curso:manage' => [
'captype' => 'write',
'contextlevel' => CONTEXT_BLOCK,
'archetypes' => [
'editingteacher' => CAP_ALLOW,
'manager' => CAP_ALLOW,
],
],
];
Los arquetipos no reescriben permisos existentes
Los valores de archetypes sirven como configuración inicial. Si una institución ya modificó permisos, una actualización del plugin no debe restaurarlos silenciosamente. Prueba la actualización en un clon y revisa la matriz de roles real, no solo lo que declara el archivo.
2.2. has_capability() para mostrar; require_capability() para actuar
Dentro del renderizado podemos ocultar el bloque o preparar una variante de la vista. La comprobación se hace contra el contexto de esa instancia, no contra el curso de forma genérica:
<?php
$context = context_block::instance($this->instance->id);
if (!has_capability('block/soporte_curso:view', $context)) {
return null;
}
$canmanage = has_capability(
'block/soporte_curso:manage',
$context
);
Ocultar un botón no protege una acción
Si más adelante añades un formulario, una página de gestión o una llamada AJAX, el endpoint debe ejecutar require_login() y require_capability(). Para cambios de estado, valida además sesskey. La interfaz solo comunica permisos; la autorización real vive en el servidor.
Referencia oficial
La Access API de Moodle 4.5 insiste en preguntar “¿puede este usuario hacer esta acción?” y no “¿qué rol tiene?”. También documenta la jerarquía de contextos y la diferencia entre has_capability() y require_capability().
3. Rendimiento de bloques profesionales en Moodle: cuándo usar MUC
Un bloque puede renderizarse varias veces durante la construcción de una página y vuelve a ejecutarse en cada carga de una página donde esté permitido. Nuestra versión se limita a course-view; si más adelante la amplías a actividades o al Área personal, el número de ejecuciones crecerá. Al crear bloques profesionales en Moodle, una operación moderadamente cara importa por la cantidad de veces que se multiplica.

Imagina que la siguiente versión del bloque consulta una base de conocimiento externa para recuperar las preguntas frecuentes asociadas al curso. Llamar a esa API en cada render añade latencia, aumenta la probabilidad de error y traslada la carga a un servicio que quizá no controlas.
MUC no es obligatoria por usar Moodle
No caches el título, tres cadenas de idioma ni una llamada sencilla a get_config(). La caché introduce invalidación, consumo de memoria y posibilidades de servir datos obsoletos. Úsala para agregaciones costosas, consultas repetidas o servicios externos medidos previamente.
3.1. Elegir el modo de caché
| Modo | Alcance | Ejemplo adecuado |
|---|---|---|
MODE_REQUEST | Una petición PHP. | Evitar repetir el mismo cálculo dentro de una carga. |
MODE_SESSION | La sesión del usuario. | Datos temporales propios de esa sesión. |
MODE_APPLICATION | Compartida entre peticiones y usuarios. | Datos comunes de una API o una agregación no personalizada. |
Para una lista común de FAQ por curso usaremos caché de aplicación. El plugin no decide si el almacenamiento físico es Redis, Memcached o el sistema de archivos: administración realiza ese mapeo y nuestro código sigue usando la misma API.
3.2. Registrar la definición en db/caches.php
<?php
defined('MOODLE_INTERNAL') || die();
$definitions = [
'faqentries' => [
'mode' => cache_store::MODE_APPLICATION,
'simplekeys' => true,
'staticacceleration' => true,
'staticaccelerationsize' => 10,
],
];
simplekeys solo es correcto si las claves contienen letras ASCII, números y guiones bajos. No activamos simpledata porque una lista de FAQ suele ser una estructura anidada; esa opción se reserva para escalares o arrays simples de escalares. Tampoco guardes en la caché objetos moodle_url, contextos ni renderables.
3.3. Encapsular el patrón de caché en un servicio
La clase principal del bloque no debería saber cómo se obtiene una FAQ ni cómo se invalida. Concentramos esa responsabilidad en classes/local/support_service.php:
<?php
namespace block_soporte_cursolocal;
defined('MOODLE_INTERNAL') || die();
final class support_service {
private cache $cache;
public function __construct() {
$this->cache = cache::make(
'block_soporte_curso',
'faqentries'
);
}
public function get_faq_entries(int $courseid): array {
$key = 'course_' . $courseid;
$entries = $this->cache->get($key);
if ($entries === false) {
$entries = $this->load_faq_entries($courseid);
$this->cache->set($key, $entries);
}
return $entries;
}
public function invalidate_course(int $courseid): void {
$this->cache->delete('course_' . $courseid);
}
private function load_faq_entries(int $courseid): array {
// Sustituye este método por tu repositorio o cliente de API.
return [];
}
}

La clave forma parte de la seguridad
Si el resultado depende del curso, la clave necesita el curso. Si depende además del usuario, grupo, idioma o permiso, incluye esas dimensiones o elige otro modo de caché. Una clave demasiado amplia no solo devuelve datos incorrectos: puede exponer información entre usuarios.
Invalida cerca de la escritura
Cuando tu plugin cambie la fuente de datos, elimina la entrada afectada con delete(). Evita purgar toda la definición o toda la caché del sitio por comodidad. Un ttl puede ser un último recurso para una fuente externa sin eventos, pero la documentación de Moodle recomienda una invalidación dirigida siempre que sea posible.
Referencia oficial
La Cache API de Moodle 4.5 documenta los modos, las restricciones de simplekeys y simpledata, y recuerda que una definición nueva requiere incrementar la versión del plugin.
4. Arquitectura limpia para bloques profesionales en Moodle
Mustache no está para ejecutar consultas ni decidir permisos. Su trabajo es transformar un contexto simple en HTML. Cuanto menos sabe la plantilla, más fácil resulta probarla, sobrescribirla desde un tema y revisar su seguridad.

4.1. Crear un objeto templatable
La clase de salida define de forma explícita qué datos puede consumir la plantilla. Devuelve tipos simples y evita pasar objetos internos de Moodle al navegador:
<?php
namespace block_soporte_cursooutput;
defined('MOODLE_INTERNAL') || die();
final class content implements
enderable, emplatable {
public function __construct(
private string $title,
private string $helptext,
private string $supportemail,
private string $faqurl,
private bool $showfaq,
private bool $canmanage
) {
}
public function export_for_template(
enderer_base $output): stdClass {
return (object) [
'title' => $this->title,
'help_text' => $this->helptext,
'support_email' => $this->supportemail,
'has_email' => $this->supportemail !== '',
'faq_url' => $this->faqurl,
'show_faq' => $this->showfaq,
'can_manage' => $this->canmanage,
];
}
}
4.2. Reescribir templates/content.mustache
{{!
@template block_soporte_curso/content
Example context (json):
{
"title": "¿Necesitas ayuda?",
"help_text": "Estamos aquí para ayudarte.",
"support_email": "soporte@example.edu",
"has_email": true,
"faq_url": "https://example.edu/ayuda",
"show_faq": true,
"can_manage": false
}
}}
<section class="block-soporte-curso p-3 text-center" aria-label="{{title}}">
<p class="h5 mb-3">{{title}}</p>
<p class="mb-3 text-muted">{{help_text}}</p>
{{#has_email}}
<p class="mb-3">
<a href="mailto:{{support_email}}">
<strong>{{support_email}}</strong>
</a>
</p>
{{/has_email}}
{{#show_faq}}
<a href="{{faq_url}}" class="btn btn-primary w-100"
target="_blank" rel="noopener">
{{#str}} viewfaq, block_soporte_curso {{/str}}
</a>
{{/show_faq}}
{{#can_manage}}
<p class="small text-muted mt-3 mb-0">
{{#str}} managehint, block_soporte_curso {{/str}}
</p>
{{/can_manage}}
</section>
Mantén el autoescapado
Las variables con dobles llaves, como {{title}}, se escapan. Las triples llaves, {{{html}}}, insertan HTML sin escapar y cambian por completo el modelo de riesgo. No las uses para títulos, correos ni texto introducido desde la configuración.
4.3. Integrar configuración, capacidades y salida
Con las responsabilidades separadas, la clase principal coordina el flujo sin mezclar HTML. Esta es la versión completa de block_soporte_curso.php al finalizar la Parte 2:
<?php
defined('MOODLE_INTERNAL') || die();
class block_soporte_curso extends block_base {
public function init(): void {
$this->title = get_string('pluginname', 'block_soporte_curso');
}
public function specialization(): void {
if (!empty($this->config->title)) {
$this->title = format_string(
$this->config->title,
true,
['context' => $this->context]
);
}
}
public function get_content(): ?stdClass {
global $CFG, $OUTPUT;
if ($this->content !== null) {
return $this->content;
}
$context = context_block::instance($this->instance->id);
if (!has_capability('block/soporte_curso:view', $context)) {
return null;
}
$supportemail = (string) get_config(
'block_soporte_curso',
'supportemail'
);
if ($supportemail === '') {
$supportemail = (string) ($CFG->supportemail ?? '');
}
if (!empty($this->config->contactemail)) {
$supportemail = $this->config->contactemail;
}
$faqurl = (string) get_config(
'block_soporte_curso',
'faqurl'
);
$globalfaqsetting = get_config(
'block_soporte_curso',
'showfaq'
);
$globalfaq = $globalfaqsetting === false
? true
: (bool) $globalfaqsetting;
$instancefaq = !isset($this->config->showfaq)
|| (bool) $this->config->showfaq;
$showfaq = $globalfaq && $instancefaq && $faqurl !== '';
$view = new \block_soporte_curso\output\content(
$this->title,
get_string('help_text', 'block_soporte_curso'),
$supportemail,
$showfaq ? (new moodle_url($faqurl))->out(false) : '',
$showfaq,
has_capability('block/soporte_curso:manage', $context)
);
$this->content = new stdClass();
$this->content->text = $OUTPUT->render_from_template(
'block_soporte_curso/content',
$view->export_for_template($OUTPUT)
);
$this->content->footer = '';
return $this->content;
}
public function applicable_formats(): array {
return [
'all' => false,
'course-view' => true,
];
}
public function instance_allow_multiple(): bool {
return false;
}
public function has_config(): bool {
return true;
}
}
¿Dónde entra el servicio de caché?
La clase support_service es una ampliación para cuando el bloque recupere FAQ dinámicas. No la instancies todavía si solo muestras una URL configurada: añadir MUC sin una operación costosa real solo complica el ejemplo. Cuando conectes la API, el servicio entregará datos simples a la clase output\content.
Referencia oficial
La Output API de Moodle 4.5 recomienda exportar tipos simples para las plantillas y documenta el uso de clases renderable, templatable y render_from_template().
5. Internacionalización: las etiquetas se traducen; la configuración no
La Parte 1 utilizaba las cadenas contact_email y faq_url como datos. Ahora desaparecen: un correo operativo y una URL no son traducciones. Los archivos de idioma conservan etiquetas, descripciones, ayudas y mensajes visibles.
5.1. Idioma base en inglés
<?php
defined('MOODLE_INTERNAL') || die();
$string['pluginname'] = 'Course support';
$string['soporte_curso:addinstance'] = 'Add a course support block';
$string['soporte_curso:view'] = 'View the course support block';
$string['soporte_curso:manage'] = 'Manage the course support block';
$string['help_text'] = 'If you have technical issues or questions about the course, we are here to help.';
$string['supportemail'] = 'Default support email';
$string['supportemail_desc'] = 'Email used when a block instance has no specific contact.';
$string['faqurl'] = 'Knowledge base URL';
$string['faqurl_desc'] = 'Approved URL for frequently asked questions.';
$string['showfaq'] = 'Enable the knowledge base link';
$string['showfaq_desc'] = 'Allows block instances to display the configured knowledge base link.';
$string['blocksettings'] = 'Course support settings';
$string['configtitle'] = 'Custom title';
$string['contactemail'] = 'Course contact email';
$string['contactemail_help'] = 'Leave empty to use the site-wide support email.';
$string['showfaqinstance'] = 'Show the knowledge base link in this block';
$string['viewfaq'] = 'View frequently asked questions';
$string['managehint'] = 'You can configure this block from its actions menu.';
$string['cachedef_faqentries'] = 'Course support FAQ entries';
5.2. Traducción al castellano
<?php
defined('MOODLE_INTERNAL') || die();
$string['pluginname'] = 'Soporte del curso';
$string['soporte_curso:addinstance'] = 'Añadir un bloque de soporte del curso';
$string['soporte_curso:view'] = 'Ver el bloque de soporte del curso';
$string['soporte_curso:manage'] = 'Gestionar el bloque de soporte del curso';
$string['help_text'] = 'Si tienes problemas técnicos o dudas sobre el curso, estamos aquí para ayudarte.';
$string['supportemail'] = 'Correo de soporte predeterminado';
$string['supportemail_desc'] = 'Correo utilizado cuando una instancia no tiene un contacto específico.';
$string['faqurl'] = 'URL de la base de conocimiento';
$string['faqurl_desc'] = 'URL aprobada para las preguntas frecuentes.';
$string['showfaq'] = 'Habilitar el enlace a la base de conocimiento';
$string['showfaq_desc'] = 'Permite que las instancias muestren el enlace configurado.';
$string['blocksettings'] = 'Ajustes del soporte del curso';
$string['configtitle'] = 'Título personalizado';
$string['contactemail'] = 'Correo de contacto del curso';
$string['contactemail_help'] = 'Déjalo vacío para utilizar el correo global de soporte.';
$string['showfaqinstance'] = 'Mostrar la base de conocimiento en este bloque';
$string['viewfaq'] = 'Consultar preguntas frecuentes';
$string['managehint'] = 'Puedes configurar este bloque desde su menú de acciones.';
$string['cachedef_faqentries'] = 'Preguntas frecuentes del soporte del curso';
El inglés sigue siendo el idioma base
Aunque el artículo y la interfaz objetivo estén en castellano, lang/en/block_soporte_curso.php debe contener todas las cadenas. Si falta una traducción en otro idioma, Moodle utiliza el paquete inglés como respaldo.
6. Actualizar el plugin sin convertir el despliegue en una prueba de fe
Hemos añadido capacidades y una definición MUC dentro de db/. Moodle solo procesa esos cambios cuando detecta una versión superior. Incrementa $plugin->version; no cambies $plugin->requires, porque Moodle 4.5 sigue siendo la versión mínima.
<?php
defined('MOODLE_INTERNAL') || die();
$plugin->component = 'block_soporte_curso';
$plugin->version = 2026081101;
$plugin->requires = 2024100700; // Moodle 4.5.
$plugin->maturity = MATURITY_STABLE;
$plugin->release = '1.1.0';
Después copia el código al entorno de pruebas y ejecuta la actualización. En servidores administrados, la vía CLI evita depender de timeouts del navegador:
php admin/cli/upgrade.php --non-interactive
php admin/cli/purge_caches.php
No edites primero en producción
Prueba la actualización sobre una copia con datos y roles representativos. Un plugin puede instalarse bien desde cero y fallar al actualizar una instalación real por configuraciones antiguas, permisos personalizados o una caché no procesada.
7. Pruebas mínimas por rol y por estado
Probar como administrador solo demuestra que el camino con más permisos funciona. Para validar la Parte 2 necesitas cambiar de usuario, contexto y configuración.

Checklist funcional
- Administrador: guarda correo, URL y política global de FAQ sin errores de validación.
- Profesor editor: cambia el título y el correo de una instancia concreta.
- Profesor no editor: ve el bloque, pero no recibe controles de gestión.
- Estudiante: ve correo y FAQ sin acceder a ninguna acción administrativa.
- Invitado: no ve el bloque mientras la capacidad
viewno se le conceda explícitamente. - Dos cursos: una personalización de instancia no se filtra al otro curso.
- FAQ global desactivada: el enlace desaparece incluso en instancias que lo tenían activado.
- Título hostil: un valor como
<script>alert(1)</script>se muestra como texto y nunca se ejecuta. - Caché opcional: dos cursos generan claves distintas y la invalidación elimina solo la entrada afectada.
Checklist técnica
- La actualización detecta
2026081101y registra capacidades y definición MUC. - No aparecen avisos con el nivel de depuración
DEVELOPER. - Los archivos PHP pasan el comprobador de sintaxis y el estándar de codificación del proyecto.
- No hay consultas SQL ni llamadas HTTP dentro de la plantilla Mustache.
- Los endpoints de escritura futuros usan
require_login(),require_capability()ysesskey. - La documentación indica qué configuración prevalece y cómo se invalida cualquier dato cacheado.
Criterio de salida
La Parte 2 está completa cuando el comportamiento cambia de forma predecible según configuración y capacidades, el HTML no contiene lógica de negocio y una actualización desde la versión de la Parte 1 termina sin avisos.
8. Errores habituales al profesionalizar un bloque Moodle
| Síntoma | Causa probable | Corrección |
|---|---|---|
| El formulario guarda, pero el valor no aparece. | El campo no empieza por config_ o se lee con el prefijo. | Guardar como config_title y leer $this->config->title. |
| La nueva capacidad no existe. | No se incrementó la versión después de modificar db/access.php. | Subir $plugin->version y ejecutar la actualización. |
| El bloque desaparece para estudiantes. | La capacidad view no está concedida en su contexto. | Revisar permisos efectivos y sobrescrituras del curso o bloque. |
| La caché devuelve datos de otro curso. | La clave no incluye todas las dimensiones del resultado. | Rediseñar la clave o usar otro modo de caché. |
| La FAQ sigue mostrando datos antiguos. | No existe una ruta de invalidación. | Eliminar la entrada cuando cambia la fuente; usar TTL solo si no hay mejor señal. |
| El título ejecuta o rompe HTML. | Se usaron triples llaves o HTML construido en PHP. | Volver al autoescapado de Mustache y preparar datos simples. |
| El bloque funciona en instalación limpia, pero no al actualizar. | La prueba no cubrió configuración ni permisos históricos. | Ensayar el upgrade sobre una copia representativa. |
9. Lo que todavía falta para llamarlo “listo para producción”
Configuración, capacidades, MUC y Mustache mejoran mucho el diseño, pero no convierten automáticamente el plugin en un producto empresarial. Los bloques profesionales en Moodle se validan también por sus pruebas, privacidad, observabilidad y comportamiento durante una actualización. El siguiente nivel depende de lo que realmente haga el bloque.
No confundas arquitectura con garantía
- Pruebas automatizadas: PHPUnit para reglas y servicios; Behat para permisos y formularios.
- Privacidad: declarar si almacena o transmite datos personales mediante la Privacy API.
- Backup y restauración: imprescindible si añades tablas, archivos o configuración con referencias internas.
- Servicios externos: timeouts, reintentos limitados, circuit breaker, logs sin secretos y degradación controlada.
- Observabilidad: medir latencia, errores y aciertos de caché antes de prometer rendimiento.
- Compatibilidad: probar Moodle 4.5 y cada rama adicional que declares, incluida Moodle 5.x.
Si el bloque solo muestra configuración y enlaces, una implementación de privacidad nula puede ser suficiente. Si envía correos, consulta una plataforma externa, almacena preferencias o registra interacciones, la evaluación cambia. La Privacy API de Moodle 4.5 explica esa frontera.
Preguntas frecuentes
¿Necesita MUC cualquier bloque que vaya a producción?
No. Un bloque ligero que lee configuración y renderiza una plantilla no obtiene un beneficio relevante. MUC tiene sentido cuando existe una operación cara y repetida, y solo después de medirla.
¿Dónde guarda Moodle la configuración de cada instancia?
En el campo configdata de block_instances. Moodle se ocupa de serializarla y exponerla mediante $this->config. No necesitas una tabla propia para estos ajustes sencillos.
¿contextlevel limita dónde puede comprobarse una capacidad?
Declara el nivel típico de la capacidad, pero la evaluación considera la jerarquía de contextos. La comprobación debe realizarse en el contexto que representa la acción o el recurso protegido.
¿Puedo guardar una API key en settings.php?
Técnicamente sí, pero los ajustes estándar se almacenan en la base de datos y no forman un almacén de secretos. Debes diseñar la protección de credenciales según el riesgo, la infraestructura y el modelo de soporte.
¿Cuándo tengo que incrementar $plugin->version?
Siempre que Moodle deba procesar un cambio de esquema, capacidades, cachés, tareas, eventos u otros archivos de db/. También es recomendable hacerlo en cada entrega desplegable para que la versión instalada sea trazable.
Conclusión: el bloque ya no depende de que nada cambie
En la Parte 1 aprendimos a encajar una necesidad pequeña dentro de la estructura de Moodle. En esta Parte 2 hemos trabajado lo que suele quedar fuera de una demo: quién controla cada ajuste, quién puede hacer cada acción, qué operaciones merecen caché y qué datos llegan a la plantilla. Ese es el salto real hacia los bloques profesionales en Moodle 4.5.
Un plugin mantenible no es el que tiene más capas. Es el que hace explícitas sus decisiones y coloca cada responsabilidad en un lugar predecible.
Nuestro block_soporte_curso puede cambiar de correo sin desplegar código, adaptarse por curso, respetar permisos en el contexto de bloque y crecer hacia datos dinámicos sin mezclar consultas con HTML. Sigue siendo un ejemplo pequeño. Precisamente por eso permite ver la arquitectura sin que el dominio la oculte.
DE “FUNCIONA” A “PODEMOS MANTENERLO”
La incidencia más cara suele empezar en una decisión que nadie dejó escrita
Una capacidad comprobada en el contexto equivocado, una consulta repetida miles de veces o una caché con una clave demasiado amplia pueden pasar semanas desapercibidas. Si tu Moodle depende de plugins propios, podemos revisar el código, el flujo de actualización y su comportamiento bajo roles y carga reales antes de que llegue el periodo crítico.
- Arquitectura y mantenimiento: responsabilidades, dependencias y estrategia de evolución.
- Seguridad: capacidades, contextos, validación, privacidad y exposición de datos.
- Rendimiento: consultas, integraciones externas, caché e invalidación.
- Actualización: compatibilidad con Moodle 4.5 y preparación del salto a 5.x.
¿Todavía estás aprendiendo? Ejecuta primero la checklist con cuatro roles y dos cursos. Esa prueba sencilla descubre más errores que volver a leer el código como administrador.

Deja una respuesta