Skip to main content

Plugin Events API

GPS Tools provides a comprehensive event system that allows developers to extend and customize the component's functionality through Joomla plugins. This guide covers all available events, how to create plugins, and best practices.

Overview

The event system allows you to:

  • Validate data before operations (and optionally cancel them)
  • Modify data before it's saved
  • React to completed operations (notifications, external sync)
  • Log and audit user activities
  • Integrate with third-party services

All events use the com_gpstools. prefix followed by the entity and action:

com_gpstools.{entity}.{action}

Quick Start

1. Create Plugin Structure

plg_gpstools_myplugin/
├── services/
│ └── provider.php
├── src/
│ └── Extension/
│ └── MyPlugin.php
├── language/
│ └── en-GB/
│ └── plg_gpstools_myplugin.ini
└── myplugin.xml

2. Plugin Manifest

myplugin.xml
<?xml version="1.0" encoding="utf-8"?>
<extension type="plugin" group="gpstools" method="upgrade">
<name>PLG_GPSTOOLS_MYPLUGIN</name>
<author>Your Name</author>
<creationDate>2025-01-01</creationDate>
<copyright>Copyright (C) 2025 Your Company</copyright>
<license>GNU General Public License version 2 or later</license>
<version>1.0.0</version>
<description>PLG_GPSTOOLS_MYPLUGIN_DESC</description>
<namespace path="src">YourVendor\Plugin\GpsTools\MyPlugin</namespace>
<files>
<folder plugin="myplugin">services</folder>
<folder>src</folder>
<folder>language</folder>
</files>
</extension>

3. Service Provider

services/provider.php
<?php

defined('_JEXEC') or die;

use Joomla\CMS\Extension\PluginInterface;
use Joomla\CMS\Factory;
use Joomla\CMS\Plugin\PluginHelper;
use Joomla\DI\Container;
use Joomla\DI\ServiceProviderInterface;
use Joomla\Event\DispatcherInterface;
use YourVendor\Plugin\GpsTools\MyPlugin\Extension\MyPlugin;

return new class implements ServiceProviderInterface
{
public function register(Container $container): void
{
$container->set(
PluginInterface::class,
function (Container $container) {
$dispatcher = $container->get(DispatcherInterface::class);
$plugin = new MyPlugin(
$dispatcher,
(array) PluginHelper::getPlugin('gpstools', 'myplugin')
);
$plugin->setApplication(Factory::getApplication());
return $plugin;
}
);
}
};

4. Plugin Class

src/Extension/MyPlugin.php
<?php

namespace YourVendor\Plugin\GpsTools\MyPlugin\Extension;

use Joomla\CMS\Plugin\CMSPlugin;
use Joomla\Component\GpsTools\Administrator\Event\Track\BeforeTrackCreateEvent;
use Joomla\Component\GpsTools\Administrator\Event\Track\AfterTrackCreateEvent;
use Joomla\Event\SubscriberInterface;

class MyPlugin extends CMSPlugin implements SubscriberInterface
{
/**
* Returns an array of events this subscriber will listen to.
*/
public static function getSubscribedEvents(): array
{
return [
'com_gpstools.track.beforeCreate' => 'onBeforeTrackCreate',
'com_gpstools.track.afterCreate' => 'onAfterTrackCreate',
];
}

public function onBeforeTrackCreate(BeforeTrackCreateEvent $event): void
{
$data = $event->getTrackData();

// Validate or modify data
if (empty($data['title'])) {
$event->cancel('Track title is required');
return;
}

// Modify data before save
$data['title'] = ucwords($data['title']);
$event->setTrackData($data);
}

public function onAfterTrackCreate(AfterTrackCreateEvent $event): void
{
$trackId = $event->getTrackId();
$track = $event->getTrack();

// Send notification, sync, log, etc.
$this->getApplication()->enqueueMessage(
"Track #{$trackId} created: {$track->title}",
'success'
);
}
}

Available Events

Track Events

BeforeTrackCreate

Fired before a new track is created. Cancellable.

PropertyTypeDescription
Event Namecom_gpstools.track.beforeCreate
Event ClassBeforeTrackCreateEvent
Cancellable✅ Yes

Methods:

MethodReturnDescription
getTrackData()arrayGet the track data being saved
setTrackData(array $data)voidModify the track data
cancel(?string $reason)voidCancel the operation
isCancelled()boolCheck if cancelled
getCancelReason()?stringGet cancellation reason

Example - Content Moderation:

public function onBeforeTrackCreate(BeforeTrackCreateEvent $event): void
{
$data = $event->getTrackData();

// Check for prohibited content
if ($this->containsProfanity($data['title'] . ' ' . $data['description'])) {
$event->cancel('Your submission contains prohibited content');
return;
}

// Auto-assign category based on activity type
if (empty($data['catid']) && !empty($data['activity_type'])) {
$data['catid'] = $this->getCategoryByActivityType($data['activity_type']);
$event->setTrackData($data);
}
}

AfterTrackCreate

Fired after a track is successfully created.

PropertyTypeDescription
Event Namecom_gpstools.track.afterCreate
Event ClassAfterTrackCreateEvent
Cancellable❌ No

Methods:

MethodReturnDescription
getTrackId()intGet the new track ID
getTrackData()arrayGet the saved track data
getTrack()?objectGet the full track object

Example - Social Sharing:

public function onAfterTrackCreate(AfterTrackCreateEvent $event): void
{
$track = $event->getTrack();

// Only share published tracks
if ($track->published != 1) {
return;
}

// Post to social media
$this->socialService->shareTrack([
'title' => $track->title,
'url' => $this->getTrackUrl($event->getTrackId()),
'image' => $this->getTrackThumbnail($track),
]);
}

BeforeTrackUpdate

Fired before a track is updated. Cancellable.

PropertyTypeDescription
Event Namecom_gpstools.track.beforeUpdate
Event ClassBeforeTrackUpdateEvent
Cancellable✅ Yes

Methods:

MethodReturnDescription
getTrackId()intGet the track ID
getTrackData()arrayGet the new track data
setTrackData(array $data)voidModify the track data
getOldTrack()objectGet the track before changes
cancel(?string $reason)voidCancel the operation

Example - Change Protection:

public function onBeforeTrackUpdate(BeforeTrackUpdateEvent $event): void
{
$oldTrack = $event->getOldTrack();
$data = $event->getTrackData();

// Prevent changing category of featured tracks
if ($oldTrack->featured && isset($data['catid']) && $data['catid'] != $oldTrack->catid) {
$event->cancel('Cannot change the category of featured tracks');
}

// Prevent editing tracks older than 30 days
$createdDate = new \DateTime($oldTrack->created);
$daysDiff = (new \DateTime())->diff($createdDate)->days;

if ($daysDiff > 30 && !$this->isAdmin()) {
$event->cancel('Tracks older than 30 days cannot be edited');
}
}

AfterTrackUpdate

Fired after a track is successfully updated.

PropertyTypeDescription
Event Namecom_gpstools.track.afterUpdate
Event ClassAfterTrackUpdateEvent
Cancellable❌ No

Methods:

MethodReturnDescription
getTrackId()intGet the track ID
getTrackData()arrayGet the updated data
getTrack()?objectGet the updated track
getOldTrack()objectGet track before changes
wasFieldChanged(string $field)boolCheck if field changed
getChangedFields()arrayGet all changed field names

Example - Changelog Logging:

public function onAfterTrackUpdate(AfterTrackUpdateEvent $event): void
{
$changes = $event->getChangedFields();

if (empty($changes)) {
return;
}

$oldTrack = $event->getOldTrack();
$newTrack = $event->getTrack();

// Log each changed field
foreach ($changes as $field) {
$this->logChange([
'track_id' => $event->getTrackId(),
'field' => $field,
'old_value' => $oldTrack->$field ?? null,
'new_value' => $newTrack->$field ?? null,
'changed_by' => $this->getApplication()->getIdentity()->id,
'changed_at' => date('Y-m-d H:i:s'),
]);
}

// Special handling for state changes
if ($event->wasFieldChanged('published')) {
if ($newTrack->published == 1) {
$this->notifyFollowers($event->getTrackId(), 'track_published');
}
}
}

BeforeTrackDelete

Fired before a track is deleted. Cancellable.

PropertyTypeDescription
Event Namecom_gpstools.track.beforeDelete
Event ClassBeforeTrackDeleteEvent
Cancellable✅ Yes

Methods:

MethodReturnDescription
getTrackId()intGet the track ID
getTrack()objectGet the track being deleted
cancel(?string $reason)voidCancel the deletion

Example - Deletion Protection:

public function onBeforeTrackDelete(BeforeTrackDeleteEvent $event): void
{
$track = $event->getTrack();

// Prevent deletion of featured tracks
if ($track->featured) {
$event->cancel('Featured tracks cannot be deleted. Please unfeature the track first.');
return;
}

// Prevent deletion of tracks with comments
if ($track->comment_count > 0) {
$event->cancel('Tracks with comments cannot be deleted. Please remove comments first.');
}
}

AfterTrackDelete

Fired after a track is successfully deleted.

PropertyTypeDescription
Event Namecom_gpstools.track.afterDelete
Event ClassAfterTrackDeleteEvent
Cancellable❌ No

Methods:

MethodReturnDescription
getTrackId()intGet the deleted track ID
getTrack()objectGet snapshot of deleted track

Example - Cleanup External Resources:

public function onAfterTrackDelete(AfterTrackDeleteEvent $event): void
{
$track = $event->getTrack();
$trackId = $event->getTrackId();

// Remove from CDN
if (!empty($track->file_name)) {
$this->cdnService->delete("tracks/{$track->file_name}");
}

// Remove from search index
$this->searchIndex->remove('track', $trackId);

// Clean up cached thumbnails
$this->cacheService->deletePattern("track_thumb_{$trackId}_*");

// Notify external systems
$this->webhookService->send('track.deleted', [
'track_id' => $trackId,
'title' => $track->title,
]);
}

AfterTrackView

Fired when a track is viewed on the frontend.

PropertyTypeDescription
Event Namecom_gpstools.track.afterView
Event ClassAfterTrackViewEvent
Cancellable❌ No

Methods:

MethodReturnDescription
getTrackId()intGet the track ID
getTrack()objectGet the track object
getUserId()intGet the viewing user ID
isGuest()boolCheck if viewer is guest

Example - Analytics Tracking:

public function onAfterTrackView(AfterTrackViewEvent $event): void
{
$this->analyticsService->record([
'event' => 'track_view',
'track_id' => $event->getTrackId(),
'track_title' => $event->getTrack()->title,
'user_id' => $event->getUserId(),
'is_guest' => $event->isGuest(),
'timestamp' => time(),
'referrer' => $_SERVER['HTTP_REFERER'] ?? null,
'user_agent' => $_SERVER['HTTP_USER_AGENT'] ?? null,
]);
}

Comment Events

BeforeCommentCreate

Fired before a comment is created. Cancellable.

PropertyTypeDescription
Event Namecom_gpstools.comment.beforeCreate
Event ClassBeforeCommentCreateEvent
Cancellable✅ Yes

Methods:

MethodReturnDescription
getCommentData()arrayGet the comment data
setCommentData(array $data)voidModify comment data
cancel(?string $reason)voidCancel creation

Example - Spam Filtering:

public function onBeforeCommentCreate(BeforeCommentCreateEvent $event): void
{
$data = $event->getCommentData();

// Check with spam service
$spamScore = $this->akismetService->check([
'content' => $data['content'],
'author' => $data['user_name'],
'email' => $data['user_email'],
'ip' => $data['ip_address'],
]);

if ($spamScore > 0.8) {
// Definitely spam - reject
$event->cancel('Your comment was identified as spam');
} elseif ($spamScore > 0.5) {
// Possible spam - hold for moderation
$data['published'] = 0;
$data['spam'] = 1;
$event->setCommentData($data);
}
}

AfterCommentCreate

Fired after a comment is successfully created.

PropertyTypeDescription
Event Namecom_gpstools.comment.afterCreate
Event ClassAfterCommentCreateEvent
Cancellable❌ No

Methods:

MethodReturnDescription
getCommentId()intGet the new comment ID
getCommentData()arrayGet the comment data
getTrackId()intGet the track ID
getUserId()intGet commenter's user ID
isReply()boolCheck if this is a reply
getParentId()intGet parent comment ID

Example - Notification System:

public function onAfterCommentCreate(AfterCommentCreateEvent $event): void
{
$data = $event->getCommentData();

// Don't notify for unpublished comments
if (($data['published'] ?? 1) != 1) {
return;
}

// Notify track owner
$track = $this->trackService->getTrack($event->getTrackId());
if ($track && $track->created_by != $event->getUserId()) {
$this->notificationService->send($track->created_by, [
'type' => 'new_comment',
'track_id' => $event->getTrackId(),
'comment_id' => $event->getCommentId(),
'commenter' => $data['user_name'],
]);
}

// Notify parent comment author for replies
if ($event->isReply()) {
$parentComment = $this->commentService->getComment($event->getParentId());
if ($parentComment && $parentComment->user_id != $event->getUserId()) {
$this->notificationService->send($parentComment->user_id, [
'type' => 'comment_reply',
'comment_id' => $event->getCommentId(),
'replier' => $data['user_name'],
]);
}
}
}

BeforeCommentDelete / AfterCommentDelete

Similar to track delete events. Available methods:

  • getCommentId() - Get the comment ID
  • getComment() - Get the comment object
  • cancel(?string $reason) - Cancel deletion (before only)

Waypoint Events

AfterWaypointsCreate

Fired after waypoints are created (batch operation).

PropertyTypeDescription
Event Namecom_gpstools.waypoint.afterCreate
Event ClassAfterWaypointsCreateEvent
Cancellable❌ No

Methods:

MethodReturnDescription
getTrackId()intGet the track ID
getWaypoints()arrayGet all created waypoints
getCount()intGet waypoint count

Example - Geocoding:

public function onAfterWaypointsCreate(AfterWaypointsCreateEvent $event): void
{
foreach ($event->getWaypoints() as $waypoint) {
// Skip waypoints with names
if (!empty($waypoint['name'])) {
continue;
}

// Reverse geocode to get location name
$location = $this->geocoder->reverse(
$waypoint['latitude'],
$waypoint['longitude']
);

if ($location) {
$this->waypointService->updateName(
$waypoint['id'],
$location->getFormattedAddress()
);
}
}
}

Rating Events

AfterTrackRate

Fired after a track or comment is rated (like/dislike).

PropertyTypeDescription
Event Namecom_gpstools.rating.afterRate
Event ClassAfterTrackRateEvent
Cancellable❌ No

Methods:

MethodReturnDescription
getItemId()intGet the rated item ID
getItemType()intGet item type (1=track, 2=comment)
isTrackRating()boolCheck if track rating
isCommentRating()boolCheck if comment rating
getUserId()intGet the rater's user ID
getAction()?stringGet action (like/dislike/null)
wasRemoved()boolCheck if rating removed
isLike()boolCheck if like action
isDislike()boolCheck if dislike action
getLikes()intGet current like count
getDislikes()intGet current dislike count
getTotalVotes()intGet total votes

Example - Achievement System:

public function onAfterTrackRate(AfterTrackRateEvent $event): void
{
// Only process track likes
if (!$event->isTrackRating() || !$event->isLike() || $event->wasRemoved()) {
return;
}

$totalLikes = $event->getLikes();
$track = $this->trackService->getTrack($event->getItemId());

// Award badges based on likes
if ($totalLikes == 10) {
$this->badgeService->award($track->created_by, 'rising_star');
} elseif ($totalLikes == 100) {
$this->badgeService->award($track->created_by, 'popular_track');
} elseif ($totalLikes == 1000) {
$this->badgeService->award($track->created_by, 'legendary_explorer');
}
}

File Upload Events

BeforeFileParse

Fired before a GPS file is parsed. Cancellable.

PropertyTypeDescription
Event Namecom_gpstools.upload.beforeParse
Event ClassBeforeFileParseEvent
Cancellable✅ Yes

Methods:

MethodReturnDescription
getFilePath()stringGet the file path
getFileType()stringGet file type (gpx/kml/tcx)
getFileSize()intGet file size in bytes
getFileName()stringGet original file name
cancel(?string $reason)voidCancel parsing

Example - Security Scan:

public function onBeforeFileParse(BeforeFileParseEvent $event): void
{
// Check file size limit
$maxSize = 50 * 1024 * 1024; // 50MB
if ($event->getFileSize() > $maxSize) {
$event->cancel('File size exceeds maximum allowed (50MB)');
return;
}

// Virus scan
$scanResult = $this->virusScanner->scan($event->getFilePath());
if (!$scanResult->isClean()) {
$event->cancel('Security scan failed: ' . $scanResult->getThreatName());
}
}

AfterFileParse

Fired after a GPS file is successfully parsed.

PropertyTypeDescription
Event Namecom_gpstools.upload.afterParse
Event ClassAfterFileParseEvent
Cancellable❌ No

Methods:

MethodReturnDescription
getFilePath()stringGet the file path
getFileType()stringGet file type
getParsedData()arrayGet parsed track data
setParsedData(array $data)voidModify parsed data
getPointCount()intGet point count
getWaypointCount()intGet waypoint count

Example - Data Enrichment:

public function onAfterFileParse(AfterFileParseEvent $event): void
{
$data = $event->getParsedData();

// Add weather data for each point
foreach ($data['points'] as $index => &$point) {
// Only process every 10th point to avoid API limits
if ($index % 10 !== 0) {
continue;
}

$weather = $this->weatherService->getHistorical(
$point['latitude'],
$point['longitude'],
$point['time']
);

if ($weather) {
$point['weather'] = [
'temperature' => $weather->temperature,
'conditions' => $weather->conditions,
'wind_speed' => $weather->windSpeed,
];
}
}

$event->setParsedData($data);
}

Event Reference Table

Event NameCancellableEntityWhen Fired
com_gpstools.track.beforeCreateTrackBefore creating a new track
com_gpstools.track.afterCreateTrackAfter track is created
com_gpstools.track.beforeUpdateTrackBefore updating a track
com_gpstools.track.afterUpdateTrackAfter track is updated
com_gpstools.track.beforeDeleteTrackBefore deleting a track
com_gpstools.track.afterDeleteTrackAfter track is deleted
com_gpstools.track.afterViewTrackWhen track is viewed
com_gpstools.comment.beforeCreateCommentBefore creating a comment
com_gpstools.comment.afterCreateCommentAfter comment is created
com_gpstools.comment.beforeDeleteCommentBefore deleting a comment
com_gpstools.comment.afterDeleteCommentAfter comment is deleted
com_gpstools.waypoint.afterCreateWaypointAfter waypoints are created
com_gpstools.rating.afterRateRatingAfter a like/dislike
com_gpstools.upload.beforeParseUploadBefore parsing GPS file
com_gpstools.upload.afterParseUploadAfter parsing GPS file

Best Practices

1. Keep Handlers Fast

Events run synchronously. For heavy operations, queue them for background processing:

public function onAfterTrackCreate(AfterTrackCreateEvent $event): void
{
// DON'T: Heavy inline processing
// $this->generateThumbnails($event->getTrackId()); // Slow!

// DO: Queue for background processing
$this->jobQueue->dispatch(new GenerateThumbnailsJob([
'track_id' => $event->getTrackId()
]));
}

2. Handle Exceptions Gracefully

Your plugin should never break the main operation:

public function onAfterTrackCreate(AfterTrackCreateEvent $event): void
{
try {
$this->externalService->sync($event->getTrackId());
} catch (\Exception $e) {
// Log but don't propagate
$this->logger->error('Sync failed: ' . $e->getMessage());

// Optionally notify user
$this->getApplication()->enqueueMessage(
'External sync will retry later',
'notice'
);
}
}

3. Use Cancellation Responsibly

Only cancel operations when absolutely necessary. Always provide clear reasons:

// Good - Clear, actionable message
$event->cancel('Tracks with more than 100,000 points require Pro plan. Upgrade at /pricing');

// Bad - Vague message
$event->cancel('Operation not allowed');

4. Avoid Infinite Loops

Don't trigger the same event from within a handler:

public function onAfterTrackUpdate(AfterTrackUpdateEvent $event): void
{
// DON'T: This triggers another afterUpdate event
// $this->trackService->updateTrack($event->getTrackId(), [...]);

// DO: Use direct database operations
$db = Factory::getContainer()->get(DatabaseInterface::class);
$db->updateObject('#__gpstools_tracks', $record, 'id');
}

5. Check Event Context

Always verify the event is relevant to your use case:

public function onAfterTrackRateEvent(AfterTrackRateEvent $event): void
{
// Only process track likes, not comment likes
if (!$event->isTrackRating()) {
return;
}

// Only process new likes, not removals
if ($event->wasRemoved() || !$event->isLike()) {
return;
}

// Now process...
}

Debugging

View Registered Listeners

use Joomla\CMS\Factory;

$dispatcher = Factory::getApplication()->getDispatcher();
$listeners = $dispatcher->getListeners('com_gpstools.track.afterCreate');

foreach ($listeners as $listener) {
echo get_class($listener[0]) . '::' . $listener[1] . "\n";
}

Debug Event Data

public function onBeforeTrackCreate(BeforeTrackCreateEvent $event): void
{
// Dump event data during development
if (JDEBUG) {
error_log(print_r([
'name' => $event->getName(),
'data' => $event->getTrackData(),
], true));
}
}

Migration from Legacy Events

If upgrading from older GPS Tools versions using onContent* events:

Before (legacy):

public function onContentAfterSave($context, $article, $isNew)
{
if ($context !== 'com_gpstools.track') return;
// ...
}

After (new):

public static function getSubscribedEvents(): array
{
return [
'com_gpstools.track.afterCreate' => 'onAfterTrackCreate',
'com_gpstools.track.afterUpdate' => 'onAfterTrackUpdate',
];
}

public function onAfterTrackCreate(AfterTrackCreateEvent $event): void
{
// Type-safe, with full IDE support
}

Benefits of the new system:

  • Type safety - Full IDE autocompletion
  • Better encapsulation - Access only what you need
  • Cleaner code - No context checking
  • More granular - Separate events for create/update
  • Cancellable - Before events can prevent operations