Creating Health Checks
Learn how to create custom health checks for your Joomla extension.
Overview
A health check is a PHP class that:
- Extends
AbstractHealthCheck - Implements
performCheck()method - Returns a
HealthCheckResultwith status and description
Basic Health Check Structure
<?php
namespace YourVendor\Plugin\HealthChecker\YourPlugin\Checks;
use MySitesGuru\HealthChecker\Component\Administrator\Check\AbstractHealthCheck;
use MySitesGuru\HealthChecker\Component\Administrator\Check\HealthCheckResult;
defined('_JEXEC') or die;
final class ExampleCheck extends AbstractHealthCheck
{
public function getSlug(): string
{
return 'yourplugin.example_check';
}
public function getCategory(): string
{
return 'extensions'; // or your custom category
}
public function getProvider(): string
{
return 'yourplugin'; // matches your ProviderMetadata slug
}
protected function performCheck(): HealthCheckResult
{
// Your check logic here
if (/* everything is good */) {
return $this->good('Check passed successfully.');
}
if (/* needs attention */) {
return $this->warning('Issue found that should be addressed.');
}
return $this->critical('Critical issue requires immediate attention.');
}
}Documentation and Action URLs
Health checks can provide two optional URLs that display as buttons on the right side of each result row:
Documentation URL (getDocsUrl)
When implemented, displays a "Docs" button on the right side of the result row. Clicking the button opens the documentation URL in a new browser tab.
use MySitesGuru\HealthChecker\Component\Administrator\Check\HealthStatus;
public function getDocsUrl(?HealthStatus $healthStatus = null): ?string
{
return 'https://docs.yoursite.com/checks/example-check';
}The method receives an optional HealthStatus parameter, allowing you to conditionally show the docs button based on the check result.
Conditional Docs URLs:
public function getDocsUrl(?HealthStatus $healthStatus = null): ?string
{
// Only show docs button when there's something to investigate (not for Good status)
if ($healthStatus === HealthStatus::Good) {
return null;
}
return 'https://docs.yoursite.com/checks/example-check';
}Use cases:
- Link to detailed documentation explaining the check
- Link to troubleshooting guides
- Link to the source code on GitHub
- Hide the docs button when no further guidance is needed (Good status)
Action URL (getActionUrl)
When implemented, displays an "Explore" button on the right side of the result row. Clicking the button navigates to the action URL in the same window.
The method receives an optional HealthStatus parameter, allowing you to conditionally show the action button based on the check result. This is useful when an action is only needed for failed checks.
use MySitesGuru\HealthChecker\Component\Administrator\Check\HealthStatus;
public function getActionUrl(?HealthStatus $status = null): ?string
{
return '/administrator/index.php?option=com_yourplugin&view=settings';
}Conditional Action URLs:
You can return different URLs (or null) based on the check status:
public function getActionUrl(?HealthStatus $status = null): ?string
{
// Only show action button for failed checks, not for Good status
if ($status === HealthStatus::Good) {
return null;
}
return '/administrator/index.php?option=com_yourplugin&view=settings';
}Use cases:
- Link to the configuration page where users can fix the issue
- Link to the Joomla component that needs attention
- Link to the relevant admin panel section
- Hide the action button when no action is needed (Good status)
Example with both URLs:
use MySitesGuru\HealthChecker\Component\Administrator\Check\HealthStatus;
final class ApiConfigCheck extends AbstractHealthCheck
{
public function getSlug(): string
{
return 'yourplugin.api_config';
}
public function getCategory(): string
{
return 'extensions';
}
public function getProvider(): string
{
return 'yourplugin';
}
public function getDocsUrl(?HealthStatus $healthStatus = null): ?string
{
return 'https://docs.yoursite.com/configuration/api-settings';
}
public function getActionUrl(?HealthStatus $status = null): ?string
{
// Only show Explore button when there's an issue to fix
if ($status === HealthStatus::Good) {
return null;
}
return '/administrator/index.php?option=com_yourplugin&view=config';
}
protected function performCheck(): HealthCheckResult
{
// Check logic here
return $this->good('API is configured correctly.');
}
}Notes:
- Both methods return
?string- returnnull(or don't override) to hide the button getDocsUrl()opens in a new tab,getActionUrl()opens in the same window- Action URLs should be relative administrator paths (starting with
/administrator/) - Documentation URLs can be absolute URLs to external documentation
- Both
$healthStatusand$statusparameters are optional for backwards compatibility - existing checks without them will continue to work
Export Visibility
Every check has an export visibility setting that controls whether its result is included in JSON and HTML exports. The check always runs and appears in the admin UI — this setting only affects exports.
The ExportVisibility Enum
use MySitesGuru\HealthChecker\Component\Administrator\Check\ExportVisibility;
enum ExportVisibility: string
{
case Always = 'always'; // Always included in exports (default)
case IssuesOnly = 'issues'; // Only included when Warning or Critical
case Never = 'never'; // Never included in exports
}Setting the Developer Default
Override getExportVisibility() in your check class to change the default for that check:
use MySitesGuru\HealthChecker\Component\Administrator\Check\ExportVisibility;
final class InternalDiagnosticsCheck extends AbstractHealthCheck
{
public function getExportVisibility(): ExportVisibility
{
// This check is for internal use only — never include it in exports
return ExportVisibility::Never;
}
// ...
}final class CacheStatusCheck extends AbstractHealthCheck
{
public function getExportVisibility(): ExportVisibility
{
// Only worth exporting if there's a problem
return ExportVisibility::IssuesOnly;
}
// ...
}If you don't override getExportVisibility(), the default is ExportVisibility::Always.
Admin Override via Plugin Config
Admins can override the developer default through the plugin settings page (the export visibility dropdown next to each check). This is handled automatically by the framework — your plugin's event handler should call setExportVisibility() before adding the check to the event:
public function onCollectChecks(CollectChecksEvent $event): void
{
$check = new ApiConnectionCheck();
$check->setDatabase($this->getDatabase());
// Apply admin-configured export visibility (read from plugin params)
$rawValue = $this->params->get('export_yourplugin_api_connection', 'always');
$visibility = ExportVisibility::tryFrom($rawValue) ?? ExportVisibility::Always;
$check->setExportVisibility($visibility);
$event->addResult($check);
}The corresponding XML field in your plugin manifest:
<field name="export_yourplugin_api_connection" type="list"
label="PLG_HEALTHCHECKER_YOURPLUGIN_EXPORT_VISIBILITY"
default="always"
class="form-select-sm">
<option value="always">PLG_HEALTHCHECKER_CORE_EXPORT_ALWAYS</option>
<option value="issues">PLG_HEALTHCHECKER_CORE_EXPORT_ISSUES_ONLY</option>
<option value="never">PLG_HEALTHCHECKER_CORE_EXPORT_NEVER</option>
</field>Field naming convention: export_{provider}_{check_name} — mirrors the check_{provider}_{check_name} enable/disable field, with export_ prefix instead of check_.
The admin-configured value always takes precedence over the developer default returned by getExportVisibility().
Check Slug Format
Format: {provider}.{check_name}
Rules:
- Lowercase only
- Use underscores for spaces
- Must be unique across all checks
- Provider prefix helps avoid conflicts
Examples:
core.php_versionakeebabackup.last_backup_ageyourplugin.api_connection
Status Levels
Good (Green)
Everything is optimal. No action needed.
return $this->good('All settings configured correctly.');Use when:
- Feature is properly configured
- Values are within optimal range
- No issues detected
Warning (Yellow)
Should be addressed, but site still functions.
return $this->warning('Configuration could be improved.');Use when:
- Non-optimal configuration
- Recommended but not required features missing
- Values outside recommended range but still functional
Critical (Red)
Immediate attention required. Site may be broken or at serious risk.
return $this->critical('Required feature is not configured.');Use when:
- Required features not configured
- Site functionality broken
- Security vulnerability present
- Data loss risk
Accessing Database
Health checks can access the Joomla database:
final class DatabaseCheck extends AbstractHealthCheck
{
protected function performCheck(): HealthCheckResult
{
$db = $this->getDatabase();
$query = $db->getQuery(true)
->select('COUNT(*)')
->from($db->quoteName('#__yourextension_table'));
$db->setQuery($query);
$count = (int) $db->loadResult();
if ($count === 0) {
return $this->warning('No records found in database.');
}
return $this->good(sprintf('Found %d records.', $count));
}
}Note: Database is injected by your plugin's event handler:
public function onCollectChecks(CollectChecksEvent $event): void
{
$check = new YourCheck();
$check->setDatabase($this->getDatabase()); // Important!
$event->addResult($check);
}Accessing Configuration
Check Joomla Global Configuration:
use Joomla\CMS\Factory;
protected function performCheck(): HealthCheckResult
{
$config = Factory::getApplication()->get('debug');
if ($config) {
return $this->critical('Debug mode is enabled in production.');
}
return $this->good('Debug mode is disabled.');
}Check extension parameters:
use Joomla\CMS\Component\ComponentHelper;
protected function performCheck(): HealthCheckResult
{
$params = ComponentHelper::getParams('com_yourextension');
$apiKey = $params->get('api_key');
if (empty($apiKey)) {
return $this->critical('API key not configured.');
}
return $this->good('API key is configured.');
}Checking Files and Directories
use Joomla\CMS\Filesystem\File;
use Joomla\CMS\Filesystem\Folder;
protected function performCheck(): HealthCheckResult
{
$path = JPATH_ROOT . '/path/to/required/file.php';
if (!File::exists($path)) {
return $this->critical('Required file is missing.');
}
if (!is_readable($path)) {
return $this->warning('File exists but is not readable.');
}
return $this->good('Required file exists and is readable.');
}Check directory writability:
protected function performCheck(): HealthCheckResult
{
$dir = JPATH_ROOT . '/path/to/directory';
if (!Folder::exists($dir)) {
return $this->critical('Required directory does not exist.');
}
if (!is_writable($dir)) {
return $this->warning('Directory is not writable.');
}
return $this->good('Directory exists and is writable.');
}Making HTTP Requests
Check external API connectivity:
use Joomla\CMS\Http\HttpFactory;
protected function performCheck(): HealthCheckResult
{
try {
$http = HttpFactory::getHttp();
$response = $http->get('https://api.example.com/status');
if ($response->code !== 200) {
return $this->warning(sprintf(
'API returned status %d instead of 200.',
$response->code
));
}
return $this->good('API is responding correctly.');
} catch (\Exception $e) {
return $this->critical(sprintf(
'Cannot connect to API: %s',
$e->getMessage()
));
}
}Error Handling
The base AbstractHealthCheck class automatically catches exceptions:
protected function performCheck(): HealthCheckResult
{
// If this throws an exception, it will be caught automatically
// and returned as a critical status with the exception message
$db = $this->getDatabase();
$result = $db->loadResult(); // Could throw exception
return $this->good('Query succeeded.');
}You can also handle exceptions explicitly:
protected function performCheck(): HealthCheckResult
{
try {
// Risky operation
$result = $this->doSomethingRisky();
return $this->good('Operation succeeded.');
} catch (\RuntimeException $e) {
return $this->warning(sprintf(
'Operation failed but recoverable: %s',
$e->getMessage()
));
} catch (\Exception $e) {
// Let AbstractHealthCheck handle other exceptions
throw $e;
}
}Language Strings
Add language strings for your check title:
File: plugins/healthchecker/yourplugin/language/en-GB/plg_healthchecker_yourplugin.ini
PLG_HEALTHCHECKER_YOURPLUGIN_CHECK_YOURPLUGIN_EXAMPLE_CHECK_TITLE="Example Health Check"Format: PLG_HEALTHCHECKER_{PLUGIN}_{SLUG_UPPERCASE}_TITLE
Example:
- Slug:
yourplugin.api_connection - Key:
PLG_HEALTHCHECKER_YOURPLUGIN_YOURPLUGIN_API_CONNECTION_TITLE - Value:
API Connection Status
The getTitle() method automatically uses this language key.
Best Practices
1. Fast Execution
// ❌ Slow
protected function performCheck(): HealthCheckResult
{
sleep(10); // Don't do this!
return $this->good('Done');
}
// ✅ Fast
protected function performCheck(): HealthCheckResult
{
// Quick check only
return $this->good('Done');
}Target: Each check should complete in under 1 second.
2. Clear Descriptions
Health check descriptions support safe HTML formatting for better readability.
Allowed HTML Tags:
<br>- Line breaks<p>- Paragraphs<strong>,<b>- Bold text<em>,<i>- Italic/emphasis<u>- Underline<code>,<pre>- Code formatting<ul>,<ol>,<li>- Lists
NOT Allowed (automatically stripped):
<a>- Links (phishing risk)<script>,<style>,<iframe>- Script execution- Event handlers (
onclick,onerror, etc.) styleattributes
// ❌ Vague
return $this->warning('Something is wrong.');
// ✅ Specific with formatting
return $this->warning(
'<p>API key is configured but has not been tested.</p>' .
'<p>Visit <strong>Components → Your Extension → Settings</strong> to test the connection.</p>'
);
// ✅ Using lists for multiple issues
return $this->warning(
'<p>The following issues were found:</p>' .
'<ul>' .
'<li>Cache directory is not writable</li>' .
'<li>Temporary files older than 30 days exist</li>' .
'</ul>'
);
// ✅ Using code formatting for commands
return $this->critical(
'<p>Database connection failed.</p>' .
'<p>Run: <code>php bin/console doctrine:database:create</code></p>'
);3. Actionable Messages
// ❌ Not actionable
return $this->critical('Cache is broken.');
// ✅ Actionable with code formatting
return $this->critical(
'<p>Cache directory is not writable.</p>' .
'<pre>chmod 755 cache/</pre>'
);4. Appropriate Status
// ❌ Too severe
if ($cacheSize > 1000000) {
return $this->critical('Cache is large.'); // Not actually critical
}
// ✅ Appropriate
if ($cacheSize > 1000000) {
return $this->warning(
sprintf(
'Cache size is %s. Consider clearing cache.',
$this->formatBytes($cacheSize)
)
);
}5. Safe Checks
// ❌ Unsafe
protected function performCheck(): HealthCheckResult
{
unlink('/some/file'); // NEVER modify anything!
return $this->good('Deleted file.');
}
// ✅ Safe (read-only)
protected function performCheck(): HealthCheckResult
{
$exists = file_exists('/some/file');
return $exists
? $this->good('File exists.')
: $this->warning('File missing.');
}Rule: Health checks must be read-only. Never modify data, files, or configuration.
Complete Example
Here's a complete, production-ready health check:
<?php
namespace YourVendor\Plugin\HealthChecker\YourPlugin\Checks;
use Joomla\CMS\Component\ComponentHelper;
use Joomla\CMS\Http\HttpFactory;
use MySitesGuru\HealthChecker\Component\Administrator\Check\AbstractHealthCheck;
use MySitesGuru\HealthChecker\Component\Administrator\Check\HealthCheckResult;
use MySitesGuru\HealthChecker\Component\Administrator\Check\HealthStatus;
defined('_JEXEC') or die;
/**
* API Connection Health Check
*
* Verifies connectivity to external API service and validates API key.
*
* WHY THIS CHECK IS IMPORTANT:
* The API connection is required for all sync operations. If the connection
* fails, no data will sync and users will see errors.
*
* RESULT MEANINGS:
*
* GOOD: API key is valid and service is responding correctly.
*
* WARNING: API key is configured but untested, or API is slow to respond.
*
* CRITICAL: API key is missing, invalid, or service is unreachable.
*/
final class ApiConnectionCheck extends AbstractHealthCheck
{
private const API_ENDPOINT = 'https://api.example.com/v1/status';
private const TIMEOUT_SECONDS = 5;
public function getSlug(): string
{
return 'yourplugin.api_connection';
}
public function getCategory(): string
{
return 'extensions';
}
public function getProvider(): string
{
return 'yourplugin';
}
/**
* Link to documentation for this check.
*
* Displays a "Docs" button that opens this URL in a new tab.
*/
public function getDocsUrl(?HealthStatus $healthStatus = null): ?string
{
return 'https://docs.yoursite.com/health-checks/api-connection';
}
/**
* Link to the settings page where users can fix issues.
*
* Displays an "Explore" button that navigates to this URL.
* Only shown when check fails (Critical or Warning status).
*/
public function getActionUrl(?HealthStatus $status = null): ?string
{
// No action needed when everything is working
if ($status === HealthStatus::Good) {
return null;
}
return '/administrator/index.php?option=com_yourplugin&view=settings';
}
protected function performCheck(): HealthCheckResult
{
// Get API key from component config
$params = ComponentHelper::getParams('com_yourplugin');
$apiKey = $params->get('api_key', '');
if (empty($apiKey)) {
return $this->critical(
'API key is not configured. ' .
'Visit Components → Your Plugin → Settings to add your API key.'
);
}
// Test API connection
try {
$http = HttpFactory::getHttp();
$startTime = microtime(true);
$response = $http->get(
self::API_ENDPOINT,
['Authorization' => 'Bearer ' . $apiKey],
self::TIMEOUT_SECONDS
);
$duration = microtime(true) - $startTime;
if ($response->code !== 200) {
return $this->critical(sprintf(
'API returned HTTP %d. Check your API key in component settings.',
$response->code
));
}
$body = json_decode($response->body);
if (!$body || !isset($body->status)) {
return $this->warning(
'API responded but returned invalid data format.'
);
}
if ($duration > 2.0) {
return $this->warning(sprintf(
'API is responding slowly (%.2fs). Service may be experiencing issues.',
$duration
));
}
return $this->good(sprintf(
'API connection successful (%.2fs response time).',
$duration
));
} catch (\Exception $e) {
return $this->critical(sprintf(
'Cannot connect to API: %s',
$e->getMessage()
));
}
}
}Plugin Configuration (Enable/Disable Checks)
To allow users to enable or disable individual checks, add configuration fields to your plugin XML.
XML Configuration Structure
Add a <config> section to your plugin's XML manifest:
File: plugins/healthchecker/yourplugin/yourplugin.xml
<?xml version="1.0" encoding="utf-8"?>
<extension type="plugin" group="healthchecker" method="upgrade">
<name>plg_healthchecker_yourplugin</name>
<author>Your Name</author>
<version>1.0.0</version>
<description>PLG_HEALTHCHECKER_YOURPLUGIN_XML_DESCRIPTION</description>
<namespace path="src">YourVendor\Plugin\HealthChecker\YourPlugin</namespace>
<files>
<folder plugin="yourplugin">services</folder>
<folder>src</folder>
<folder>language</folder>
</files>
<!-- Plugin Configuration -->
<config>
<fields name="params">
<fieldset name="checks"
label="PLG_HEALTHCHECKER_YOURPLUGIN_FIELDSET_CHECKS_LABEL"
description="PLG_HEALTHCHECKER_YOURPLUGIN_FIELDSET_CHECKS_DESC">
<!-- Optional: Category Header -->
<field name="header_yourplugin" type="note"
label="PLG_HEALTHCHECKER_YOURPLUGIN_FIELDSET_YOURPLUGIN_LABEL"
description="PLG_HEALTHCHECKER_YOURPLUGIN_FIELDSET_YOURPLUGIN_DESC"
class="alert alert-info" />
<!-- Individual Check Toggle -->
<field name="check_yourplugin_api_connection" type="radio"
label="PLG_HEALTHCHECKER_YOURPLUGIN_CHECK_YOURPLUGIN_API_CONNECTION_TITLE"
class="btn-group btn-group-yesno"
default="1">
<option value="1">JENABLED</option>
<option value="0">JDISABLED</option>
</field>
<field name="check_yourplugin_database_sync" type="radio"
label="PLG_HEALTHCHECKER_YOURPLUGIN_CHECK_YOURPLUGIN_DATABASE_SYNC_TITLE"
class="btn-group btn-group-yesno"
default="1">
<option value="1">JENABLED</option>
<option value="0">JDISABLED</option>
</field>
</fieldset>
</fields>
</config>
</extension>Field Naming Convention
Format: check_{provider}_{check_name}
Examples:
- Check slug:
yourplugin.api_connection - Field name:
check_yourplugin_api_connection
Rules:
- Must start with
check_ - Use underscores (not dots)
- Lowercase only
- Must match your check slug (dots become underscores)
Checking Configuration in Your Plugin
Read the configuration value in your plugin's event handler:
<?php
namespace YourVendor\Plugin\HealthChecker\YourPlugin\Extension;
use Joomla\CMS\Plugin\CMSPlugin;
use Joomla\Event\SubscriberInterface;
use MySitesGuru\HealthChecker\Component\Administrator\Event\CollectChecksEvent;
final class YourPlugin extends CMSPlugin implements SubscriberInterface
{
public static function getSubscribedEvents(): array
{
return [
'onCollectChecks' => 'onCollectChecks',
];
}
public function onCollectChecks(CollectChecksEvent $event): void
{
// Check if the API connection check is enabled
$apiCheckEnabled = (bool) $this->params->get('check_yourplugin_api_connection', 1);
if ($apiCheckEnabled) {
$check = new \YourVendor\Plugin\HealthChecker\YourPlugin\Checks\ApiConnectionCheck();
$check->setDatabase($this->getDatabase());
$event->addResult($check);
}
// Check if the database sync check is enabled
$dbCheckEnabled = (bool) $this->params->get('check_yourplugin_database_sync', 1);
if ($dbCheckEnabled) {
$check = new \YourVendor\Plugin\HealthChecker\YourPlugin\Checks\DatabaseSyncCheck();
$check->setDatabase($this->getDatabase());
$event->addResult($check);
}
}
}Language Strings
Add language keys for your configuration:
File: language/en-GB/plg_healthchecker_yourplugin.ini
; Configuration fieldset
PLG_HEALTHCHECKER_YOURPLUGIN_FIELDSET_CHECKS_LABEL="Health Checks Configuration"
PLG_HEALTHCHECKER_YOURPLUGIN_FIELDSET_CHECKS_DESC="Enable or disable individual health checks."
; Category headers (optional)
PLG_HEALTHCHECKER_YOURPLUGIN_FIELDSET_YOURPLUGIN_LABEL="Your Plugin Checks"
PLG_HEALTHCHECKER_YOURPLUGIN_FIELDSET_YOURPLUGIN_DESC="Health checks for Your Plugin integration."
; Check titles (used in both config and results)
PLG_HEALTHCHECKER_YOURPLUGIN_CHECK_YOURPLUGIN_API_CONNECTION_TITLE="API Connection Status"
PLG_HEALTHCHECKER_YOURPLUGIN_CHECK_YOURPLUGIN_DATABASE_SYNC_TITLE="Database Synchronization"Organizing Multiple Categories
If your plugin provides checks in multiple categories, organize them with headers:
<config>
<fields name="params">
<fieldset name="checks"
label="PLG_HEALTHCHECKER_YOURPLUGIN_FIELDSET_CHECKS_LABEL">
<!-- System Category -->
<field name="header_system" type="note"
label="System & Hosting Checks"
class="alert alert-info" />
<field name="check_yourplugin_server_config" type="radio"
label="PLG_HEALTHCHECKER_YOURPLUGIN_CHECK_YOURPLUGIN_SERVER_CONFIG_TITLE"
class="btn-group btn-group-yesno"
default="1">
<option value="1">JENABLED</option>
<option value="0">JDISABLED</option>
</field>
<!-- Security Category -->
<field name="header_security" type="note"
label="Security Checks"
class="alert alert-info" />
<field name="check_yourplugin_api_security" type="radio"
label="PLG_HEALTHCHECKER_YOURPLUGIN_CHECK_YOURPLUGIN_API_SECURITY_TITLE"
class="btn-group btn-group-yesno"
default="1">
<option value="1">JENABLED</option>
<option value="0">JDISABLED</option>
</field>
</fieldset>
</fields>
</config>Default Value
The default="1" attribute sets the check to enabled by default.
default="1"- Check enabled on first installdefault="0"- Check disabled on first install
Recommendation: Keep all checks enabled by default (default="1") to ensure users see all available checks.
User Experience
After adding configuration:
- Users navigate to Extensions → Plugins
- Search for your plugin name
- Click to edit
- See organized list of all your checks
- Toggle individual checks on/off
- Save settings
The next health check run will respect these settings - disabled checks won't execute or appear in results.
Testing Your Check
Install your plugin
Navigate to: Components → Health Checker
Click: Run Health Check
Verify:
- Your check appears in correct category
- Title displays (not language key)
- Description is clear and actionable
- Status is appropriate
- Provider badge shows your attribution
Test Configuration:
- Go to Extensions → Plugins → Your Plugin
- Disable one of your checks
- Run Health Check again
- Verify disabled check doesn't appear
Next Steps
- Custom Categories - Create custom categories
- Provider Metadata - Add branding
- API Reference - Complete API documentation
- Examples - More code examples