Skip to content
Angel Fernando Quiroz Campos edited this page Apr 3, 2026 · 1 revision

Plugin development guide for Chamilo LMS 2.x

Table of contents

  1. Introduction
  2. Directory structure
  3. Required files
  4. Main plugin class
  5. Configuration and settings
  6. Installation and uninstallation
  7. Doctrine entities
  8. Symfony integration (Event Subscribers)
  9. Display regions (Twig)
  10. Translations
  11. Course-oriented plugins
  12. Lifecycle hooks
  13. PluginHelper service
  14. Core file reference

Introduction

The Chamilo 2.x plugin system allows extending the platform without modifying the core codebase. Plugins live under public/plugin/MyPlugin/ and are discovered automatically when that directory is scanned.

There are two integration layers:

  • Legacy (plain PHP): the Plugin class manages settings, translations, and the administration interface.
  • Modern Symfony: automatic compiler passes register event subscribers and Doctrine entities located inside the plugin's src/ directory.

Directory structure

public/plugin/MyPlugin/
├── plugin.php              # Plugin metadata (REQUIRED)
├── index.php               # Entry point / region rendering
├── install.php             # Installation script
├── uninstall.php           # Uninstallation script
├── admin.php               # Administration interface (optional)
├── lang/                   # Translations
│   ├── en_US.php
│   └── es.php
├── src/
│   ├── MyPluginPlugin.php        # Main plugin class
│   ├── Entity/                   # Doctrine entities
│   │   └── MyEntity.php
│   ├── Repository/               # Doctrine repositories
│   │   └── MyEntityRepository.php
│   └── EventSubscriber/          # Symfony event subscribers
│       └── MyPluginEventSubscriber.php
├── templates/              # Twig templates
│   └── my_view.html.twig
└── resources/              # Static CSS / JS
    └── MyPlugin.css

Required files

plugin.php

This is the only truly required file. AppPlugin uses it to discover the plugin and display its information in the administration panel.

<?php
// public/plugin/MyPlugin/plugin.php

$plugin_info = MyPluginPlugin::create()->get_info();

For very simple plugins without a dedicated class, the $plugin_info array can be defined inline:

$plugin_info = [
    'title'   => 'My Plugin',
    'comment' => 'Short description',
    'version' => '1.0',
    'author'  => 'Your Name',
];

Main plugin class

The main class extends Plugin (located at public/main/inc/lib/plugin.class.php) and follows the static factory (singleton) pattern.

<?php
// public/plugin/MyPlugin/src/MyPluginPlugin.php

declare(strict_types=1);

class MyPluginPlugin extends Plugin
{
    // Optional: name of the plugin's own database table
    public const TABLE_LOG = 'my_plugin_log';

    protected function __construct()
    {
        // Settings define the fields of the configuration form.
        // Available types: 'boolean', 'text', 'select', 'wysiwyg', 'html', 'checkbox', 'user'
        $settings = [
            'enabled'      => 'boolean',
            'max_items'    => 'text',
            'display_mode' => 'select',
        ];

        parent::__construct(
            '1.0',           // Version
            'Your Name',     // Author
            $settings
        );
    }

    public static function create(): self
    {
        static $instance = null;

        return $instance ??= new self();
    }
}

Relevant base-class properties

Property Type Effect
$isCoursePlugin bool Registers the plugin as a course tool
$isAdminPlugin bool Indicates the plugin has its own administration interface
$isMailPlugin bool Indicates the plugin integrates with the mail system
$addCourseTool bool Shows an icon on the course home page
$hasPersonalEvents bool Indicates the plugin provides personal calendar events
$course_settings array Additional per-course settings

Useful methods

Method Description
get_lang(string $key) Returns a translated string from the lang/ directory
get(string $name) Reads a saved setting from the database
get_settings() Returns all plugin settings for the current access URL
getFieldNames() Returns the names of all declared settings fields
isEnabled(bool $checkEnabled) Checks whether the plugin is installed (and optionally active)
renderRegion(string $region) Override to render plugin content in a Twig region
getAdminUrl() Returns the URL of the administration panel (admin.php or start.php)
addTab(bool $installInHomePage) Adds the plugin as a tab in the main navigation menu
deleteTab(bool $uninstallFromHomePage) Removes the plugin tab from the main navigation menu
getToolIconVisibilityPerUserStatus() Override to restrict the course icon to students or teachers only

Tab filter constants

Use these constants with getToolIconVisibilityPerUserStatus() to control course-tool icon visibility:

Constant Value Effect
Plugin::TAB_FILTER_NO_STUDENT '::no-student' Icon visible only for teachers/admins
Plugin::TAB_FILTER_ONLY_STUDENT '::only-student' Icon visible only for students
public function getToolIconVisibilityPerUserStatus(): string
{
    return self::TAB_FILTER_NO_STUDENT; // hides the icon from students
}

Configuration and settings

Settings are declared in the constructor as a name => type array. Chamilo automatically generates the configuration form from that array.

$settings = [
    'api_key'       => 'text',      // Plain text field
    'show_sidebar'  => 'boolean',   // Radio Yes/No
    'welcome_text'  => 'wysiwyg',   // WYSIWYG editor
    'layout'        => 'select',    // Drop-down list
    'notify_me'     => 'checkbox',  // Checkbox
    'responsible'   => 'user',      // Ajax user selector
];

For select fields with dynamic or translated options, use array notation:

$settings = [
    'mode' => [
        'type'             => 'select',
        'options'          => ['auto' => 'Automatic', 'manual' => 'Manual'],
        'translate_options' => true,  // calls get_lang() on each option label
    ],
];

To read a setting inside the plugin code:

$plugin = MyPluginPlugin::create();

// Single setting
$apiKey = $plugin->get('api_key');

// All settings
$config = $plugin->get_settings();

Settings are stored in the access_url_rel_plugin table (JSON configuration column), which allows different configurations per access URL (multi-tenant installations). get_settings() automatically reads the configuration for the current access URL.

Help text for a field is loaded automatically if a translation key {field_name}_help exists in the plugin's language files.


Installation and uninstallation

install.php

<?php
// public/plugin/MyPlugin/install.php

MyPluginPlugin::create()->install();

uninstall.php

<?php
// public/plugin/MyPlugin/uninstall.php

MyPluginPlugin::create()->uninstall();

Implementation inside the plugin class

use Doctrine\ORM\Tools\SchemaTool;
use Chamilo\PluginBundle\MyPlugin\Entity\MyEntity;

public function install(): void
{
    $em = Database::getManager();
    $schemaManager = $em->getConnection()->createSchemaManager();

    // Avoid creating tables that already exist
    if ($schemaManager->tablesExist([self::TABLE_LOG])) {
        return;
    }

    $schemaTool = new SchemaTool($em);
    $schemaTool->createSchema([
        $em->getClassMetadata(MyEntity::class),
    ]);
}

public function uninstall(): void
{
    $em = Database::getManager();

    if (!$em->getConnection()->createSchemaManager()->tablesExist([self::TABLE_LOG])) {
        return;
    }

    $schemaTool = new SchemaTool($em);
    $schemaTool->dropSchema([
        $em->getClassMetadata(MyEntity::class),
    ]);
}

Doctrine entities

The PluginEntityPass compiler pass automatically scans each plugin's Entity/ and src/Entity/ directories and registers entities under the Chamilo\PluginBundle\{PluginName} namespace.

No manual registration in config/ is needed. Entities must use PHP attributes (not XML or YAML mapping).

<?php
// public/plugin/MyPlugin/src/Entity/MyEntity.php

declare(strict_types=1);

namespace Chamilo\PluginBundle\MyPlugin\Entity;

use Doctrine\ORM\Mapping as ORM;

#[ORM\Table(name: 'my_plugin_entity')]
#[ORM\Entity(repositoryClass: MyEntityRepository::class)]
class MyEntity
{
    #[ORM\Id]
    #[ORM\GeneratedValue]
    #[ORM\Column(type: 'integer')]
    private ?int $id = null;

    #[ORM\Column(type: 'string', length: 255)]
    private string $title = '';

    #[ORM\Column(type: 'datetime_immutable')]
    private \DateTimeImmutable $createdAt;

    public function __construct()
    {
        $this->createdAt = new \DateTimeImmutable();
    }

    public function getId(): ?int
    {
        return $this->id;
    }

    public function getTitle(): string
    {
        return $this->title;
    }

    public function setTitle(string $title): self
    {
        $this->title = $title;

        return $this;
    }
}
<?php
// public/plugin/MyPlugin/src/Entity/MyEntityRepository.php

declare(strict_types=1);

namespace Chamilo\PluginBundle\MyPlugin\Entity;

use Doctrine\Bundle\DoctrineBundle\Repository\ServiceEntityRepository;
use Doctrine\Persistence\ManagerRegistry;

class MyEntityRepository extends ServiceEntityRepository
{
    public function __construct(ManagerRegistry $registry)
    {
        parent::__construct($registry, MyEntity::class);
    }
}

Table naming convention: use a unique prefix to avoid collisions, e.g. my_plugin_*.


Symfony integration (Event Subscribers)

The PluginEventSubscriberPass compiler pass scans public/plugin/*/src/ looking for files whose name ends in EventSubscriber.php and automatically registers them as Symfony services tagged with kernel.event_subscriber.

<?php
// public/plugin/MyPlugin/src/EventSubscriber/MyPluginEventSubscriber.php

declare(strict_types=1);

namespace Chamilo\PluginBundle\MyPlugin\EventSubscriber;

use Chamilo\CoreBundle\Event\CourseCreatedEvent;
use Chamilo\CoreBundle\Event\Events;
use Doctrine\ORM\EntityManagerInterface;
use Symfony\Component\EventDispatcher\EventSubscriberInterface;

readonly class MyPluginEventSubscriber implements EventSubscriberInterface
{
    private \MyPluginPlugin $plugin;

    public function __construct(
        private EntityManagerInterface $entityManager,
    ) {
        $this->plugin = \MyPluginPlugin::create();
    }

    public static function getSubscribedEvents(): array
    {
        return [
            Events::COURSE_CREATED => 'onCourseCreated',
        ];
    }

    public function onCourseCreated(CourseCreatedEvent $event): void
    {
        // Always check that the plugin is active
        if (!$this->plugin->isEnabled(true)) {
            return;
        }

        $course = $event->getCourse();
        // plugin logic...
    }
}

Available events

Events are defined in src/CoreBundle/Event/Events.php.

Users

Constant When fired
Events::USER_CREATED When a user is created
Events::USER_UPDATED When a user is updated

Courses and sessions

Constant When fired
Events::COURSE_CREATED When a course is created
Events::SESSION_RESUBSCRIPTION When a user is re-subscribed to a session

Authentication

Constant When fired
Events::LOGIN_CREDENTIALS_CHECKED When login credentials are verified
Events::LOGIN_CONDITION_CHECKED When additional login conditions are evaluated

Documents

Constant When fired
Events::DOCUMENT_ITEM_ACTION When an action is performed on a document
Events::DOCUMENT_ACTION When a general document action is executed
Events::DOCUMENT_ITEM_VIEW When a document is viewed

Learning paths

Constant When fired
Events::LP_CREATED When a learning path is created
Events::LP_ITEM_VIEWED When a learning path item is viewed
Events::LP_ENDED When a learning path is completed

Exercises

Constant When fired
Events::EXERCISE_QUESTION_ANSWERED When an exercise question is answered
Events::EXERCISE_ENDED When an exercise is finished
Events::EXERCISE_REPORT_ACTION When an action is performed on an exercise report

Student tracking

Constant When fired
Events::MY_STUDENTS_EXERCISE_TRACKING When a student's exercise tracking is queried
Events::MY_STUDENTS_LP_TRACKING When a student's learning path tracking is queried

Portfolio

Constant When fired
Events::PORTFOLIO_ITEM_ADDED When a portfolio item is added
Events::PORTFOLIO_ITEM_EDITED When a portfolio item is edited
Events::PORTFOLIO_ITEM_VIEWED When a portfolio item is viewed
Events::PORTFOLIO_ITEM_DELETED When a portfolio item is deleted
Events::PORTFOLIO_ITEM_VISIBILITY_CHANGED When the visibility of a portfolio item changes
Events::PORTFOLIO_ITEM_COMMENTED When a portfolio item is commented on
Events::PORTFOLIO_ITEM_HIGHLIGHTED When a portfolio item is highlighted
Events::PORTFOLIO_DOWNLOADED When the portfolio is downloaded
Events::PORTFOLIO_ITEM_SCORED When a portfolio item is scored
Events::PORTFOLIO_COMMENT_SCORED When a portfolio comment is scored
Events::PORTFOLIO_COMMENT_EDITED When a portfolio comment is edited

Notifications

Constant When fired
Events::NOTIFICATION_CONTENT_FORMATTED When a notification's content is formatted
Events::NOTIFICATION_TITLE_FORMATTED When a notification's title is formatted

Administration

Constant When fired
Events::ADMIN_BLOCK_DISPLAYED When an administration panel block is rendered

Display regions (Vue)

Plugins can inject content into 18 predefined regions of Chamilo's Vue frontend. Each region is rendered by the PluginRegion.vue component, which calls the GET /plugin-regions/{region} endpoint (PluginRegionController). That controller iterates over the active plugins, calls AppPlugin::loadRegion() (and Plugin::renderRegion() for course plugins), and returns the resulting HTML as JSON. Override the renderRegion() method in the plugin class to return the HTML to inject.

Available regions

content_bottom    content_top       course_tool_plugin
footer_center     footer_left       footer_right
header_center     header_left       header_main
header_right      login_bottom      login_top
main_bottom       main_top          menu_administrator
menu_bottom       menu_top          pre_footer

How a region reaches the browser

  1. The Vue layout includes <PluginRegion region="header_right" /> (or whichever region).
  2. The usePluginRegion composable fetches GET /plugin-regions/header_right.
  3. PluginRegionController returns { blocks: [{ pluginName, region, html }] }.
  4. The component renders each block's HTML via v-html.

Override renderRegion() in the plugin class

public function renderRegion(string $region): string
{
    if ('header_right' !== $region) {
        return '';
    }

    // Return the HTML to inject
    return '<div class="my-plugin-widget">Hello from MyPlugin!</div>';
}

Translations

Create one PHP file per language inside lang/. The fallback language is en_US.php; if the user's language file does not exist, the English strings are used.

<?php
// public/plugin/MyPlugin/lang/en_US.php

$strings['plugin_title']   = 'My Plugin';
$strings['plugin_comment'] = 'Short description of my plugin.';
$strings['welcome']        = 'Welcome to My Plugin!';
$strings['items_count']    = 'Number of items';
// Optional help text for a setting field (key: {field_name}_help)
$strings['api_key_help']   = 'Enter the API key provided by the service.';
<?php
// public/plugin/MyPlugin/lang/es_ES.php

$strings['plugin_title']   = 'Mi Plugin';
$strings['plugin_comment'] = 'Descripción breve de mi plugin.';
$strings['welcome']        = '¡Bienvenido a Mi Plugin!';
$strings['items_count']    = 'Número de elementos';
// Usage inside the plugin code
$plugin = MyPluginPlugin::create();

echo $plugin->get_lang('welcome');        // Welcome to My Plugin!
echo $plugin->get_lang('items_count');    // Number of items

The language is selected automatically based on the user's interface setting. Language files are loaded in order: en_US.php first (as base), then the user's language file (which overrides matching keys).


Course-oriented plugins

A plugin can be added as a tool in courses by setting $isCoursePlugin = true.

class MyPluginPlugin extends Plugin
{
    public bool $isCoursePlugin = true;
    public bool $addCourseTool = true;   // Shows an icon on the course home page

    // Additional per-course settings (shown in the course settings form)
    public array $course_settings = [
        [
            'name'    => 'my_plugin_enabled',
            'type'    => 'checkbox',
            'default' => false,
        ],
    ];

    // Called when course settings are saved
    public function course_settings_updated(array $values): void
    {
        // React to changes in the course configuration
    }
}

Installing / uninstalling in courses

// Install in all existing courses
$plugin->install_course_fields_in_all_courses(add_tool_link: true);

// Install in a single course
$plugin->course_install(courseId: 42, addToolLink: true);

// Uninstall from a single course
$plugin->uninstall_course_fields(courseId: 42);

// Uninstall from all courses
$plugin->uninstall_course_fields_in_all_courses();

Lifecycle hooks

Override these methods in the plugin class to react to platform events:

Method When called
install() When the plugin is installed from the admin panel
uninstall() When the plugin is uninstalled
performActionsAfterConfigure() After the plugin configuration form is saved
course_settings_updated(array $values) After course-level settings are saved (requires $course_settings_callback = true)
validateCourseSetting(string $variable) To validate a course setting value (return false to reject)
doWhenDeletingUser(int $userId) When a user is deleted — clean up user-related plugin data
doWhenDeletingCourse(int $courseId) When a course is deleted — clean up course-related plugin data
doWhenDeletingSession(int $sessionId) When a session is deleted — clean up session-related plugin data
public function performActionsAfterConfigure(): static
{
    // e.g., flush an external API cache when settings change
    $apiKey = $this->get('api_key');
    MyApiClient::clearCache($apiKey);

    return $this;
}

public function doWhenDeletingUser(int $userId): void
{
    $em = Database::getManager();
    // remove plugin records linked to this user
}

PluginHelper service

PluginHelper (src/CoreBundle/Helpers/PluginHelper.php) is a Symfony service that provides a safe way to query plugin state from core Symfony code (controllers, services, event subscribers of other bundles).

use Chamilo\CoreBundle\Helpers\PluginHelper;

class SomeService
{
    public function __construct(
        private readonly PluginHelper $pluginHelper
    ) {}

    public function doSomething(): void
    {
        // Check if a plugin is installed and active for the current access URL
        if ($this->pluginHelper->isPluginEnabled('MyPlugin')) {
            $value = $this->pluginHelper->getPluginConfigValue('MyPlugin', 'api_key');
        }
    }
}

Available methods

Method Description
isPluginEnabled(string $pluginName) Returns true if the plugin is installed and active for the current access URL
loadLegacyPlugin(string $pluginName) Loads and returns the plugin singleton object
getPluginSetting(string $pluginName, string $settingKey) Returns a single setting value via the legacy plugin object
getPluginConfiguration(string $pluginName) Returns the full configuration array for the current access URL
getPluginConfigValue(string $pluginName, string $key, mixed $default) Returns a configuration value, falling back to the legacy get() method

getPluginConfigValue tries both the plain key and a legacy-prefixed key ({pluginName}_{key}) for backwards compatibility.


Core file reference

Component Path
Plugin base class public/main/inc/lib/plugin.class.php
AppPlugin manager public/main/inc/lib/plugin.lib.php
Plugin entity (Doctrine) src/CoreBundle/Entity/Plugin.php
AccessUrlRelPlugin entity src/CoreBundle/Entity/AccessUrlRelPlugin.php
PluginHelper src/CoreBundle/Helpers/PluginHelper.php
Event subscriber compiler pass src/CoreBundle/DependencyInjection/Compiler/PluginEventSubscriberPass.php
Entity compiler pass src/CoreBundle/DependencyInjection/Compiler/PluginEntityPass.php
Chamilo Twig extension src/CoreBundle/Twig/Extension/ChamiloExtension.php
Plugin region controller src/CoreBundle/Controller/PluginRegionController.php
Plugin region Vue component assets/vue/components/layout/PluginRegion.vue
Plugin region composable assets/vue/composables/pluginRegion.js
Administration controller src/CoreBundle/Controller/Admin/PluginsController.php
Event constants src/CoreBundle/Event/Events.php
Example plugin (simple) public/plugin/HelloWorld/
Example plugin (complex) public/plugin/TopLinks/

Checklist for creating a new plugin

  • Create directory public/plugin/MyPlugin/
  • Create plugin.php with the get_info() call
  • Create src/MyPluginPlugin.php with the class extending Plugin
  • Declare settings in the constructor
  • Create install.php and uninstall.php
  • Implement install() and uninstall() in the class (if the plugin has its own tables)
  • Create Doctrine entities in src/Entity/ (if applicable)
  • Create an event subscriber in src/EventSubscriber/ (if applicable)
  • Add translations in lang/en_US.php (minimum)
  • Override lifecycle hooks as needed (doWhenDeletingUser, performActionsAfterConfigure, etc.)
  • Test installation from Administration > Plugins

Clone this wiki locally