This guide covers setting up your development environment, running tests, and contributing to Sybgo.
- WordPress 5.0+
- PHP 7.4+
- Composer
- MySQL 5.7+ or MariaDB 10.2+
- Local by Flywheel (recommended)
- MAMP/WAMP/XAMPP
- Docker (wp-env, Lando, etc.)
# 1. Clone repository
cd /path/to/wordpress/wp-content/plugins
git clone <repo-url> sybgo
cd sybgo
# 2. Install dependencies
composer install
# 3. Activate in WordPress
# WP Admin → Plugins → Activate "Sybgo"
# 4. Verify installation
wp plugin list | grep sybgo# Code standards
composer require --dev squizlabs/php_codesniffer
composer require --dev wp-coding-standards/wpcs
# Testing
composer require --dev phpunit/phpunit
composer require --dev brain/monkey
composer require --dev mockery/mockerySybgo follows WordPress Coding Standards and group.one technical standards.
# Check all PHP files
composer phpcs
# Check specific file
composer phpcs -- database/class-event-repository.php# Fix all fixable issues
composer phpcs:fix
# Fix specific file
composer phpcs:fix -- database/class-event-repository.phpType Declarations:
declare(strict_types=1);Namespacing:
namespace Rocket\Sybgo\Events\Trackers;Security:
// Nonces
wp_nonce_field( 'sybgo_action', 'sybgo_nonce' );
check_admin_referer( 'sybgo_action', 'sybgo_nonce' );
// Output escaping
echo esc_html( $text );
echo esc_url( $url );
echo esc_attr( $attr );
// Input sanitization
$clean = sanitize_text_field( $_POST['field'] );
$email = sanitize_email( $_POST['email'] );
// SQL preparation
$wpdb->prepare( "SELECT * FROM table WHERE id = %d", $id );See CLAUDE.md at the repo root for the exact commands to run unit tests, PHPCS, and PHPStan across lib/ and wp-plugin/, including the symlink fix for PHPStan.
Unit Test Example:
namespace Rocket\Sybgo\Tests\Unit\Events;
use Brain\Monkey;
use Brain\Monkey\Functions;
use Mockery;
use PHPUnit\Framework\TestCase;
use Rocket\Sybgo\Database\Event_Repository;
use Rocket\Sybgo\Events\Trackers\Post_Tracker;
class PostTrackerTest extends TestCase {
protected function setUp(): void {
parent::setUp();
Monkey\setUp();
// Mock WordPress functions
Functions\when( 'get_current_user_id' )->justReturn( 1 );
Functions\when( 'get_permalink' )->alias( function( $id ) {
return "https://example.com/post-{$id}";
} );
}
protected function tearDown(): void {
Monkey\tearDown();
Mockery::close();
parent::tearDown();
}
public function test_track_post_publish() {
$event_repo = Mockery::mock( Event_Repository::class );
$event_repo->shouldReceive( 'create' )
->once()
->with( Mockery::type( 'array' ) )
->andReturn( 123 );
$tracker = new Post_Tracker( $event_repo );
// Test logic here
$this->assertTrue( true );
}
}When adding new features:
- Write unit tests for all new classes
- Mock WordPress functions with Brain\Monkey
- Test both success and error cases
- Ensure 100% pass rate before committing
- Run code standards check
# 1. Create feature branch
git checkout -b feature/my-feature
# 2. Make changes
# Edit files...
# 3. Check code standards
composer phpcs
# 4. Fix any issues
composer phpcs:fix
# 5. Run tests
composer run-tests
# 6. Commit changes
git add .
git commit -m "Add feature: description"
# 7. Push and create PR
git push origin feature/my-featureEnable WordPress Debug Mode:
// In wp-config.php
define( 'WP_DEBUG', true );
define( 'WP_DEBUG_LOG', true );
define( 'WP_DEBUG_DISPLAY', false );Add Debug Logging:
error_log( 'Sybgo: Event tracked - ' . $event_type );
error_log( print_r( $event_data, true ) );Check Debug Log:
tail -f wp-content/debug.log# View recent events
wp db query "SELECT * FROM wp_sybgo_events ORDER BY event_timestamp DESC LIMIT 10"
# Pretty print JSON
wp db query "SELECT id, event_type, JSON_PRETTY(event_data) FROM wp_sybgo_events LIMIT 1"
# Count events by type
wp db query "SELECT event_type, COUNT(*) as total FROM wp_sybgo_events GROUP BY event_type"
# View reports
wp db query "SELECT * FROM wp_sybgo_reports ORDER BY period_end DESC"# Publish a post
wp post create --post_title="Test Post" --post_status=publish
# Verify event created
wp db query "SELECT * FROM wp_sybgo_events WHERE event_type='post_published' ORDER BY created_at DESC LIMIT 1"
# Edit the post
wp post update 1 --post_content="Updated content"
# Check edit event
wp db query "SELECT event_type, JSON_EXTRACT(event_data, '$.metadata.edit_magnitude') FROM wp_sybgo_events WHERE event_type='post_edited' ORDER BY created_at DESC LIMIT 1"# Manual freeze
wp cron event run sybgo_freeze_weekly_report
# Verify report frozen
wp db query "SELECT * FROM wp_sybgo_reports WHERE status='frozen' ORDER BY period_end DESC LIMIT 1"
# Check events assigned
wp db query "SELECT COUNT(*) FROM wp_sybgo_events WHERE report_id IS NOT NULL"# Manual send
wp cron event run sybgo_send_report_emails
# Check email log
wp db query "SELECT * FROM wp_sybgo_email_log ORDER BY created_at DESC LIMIT 5"
# View email content (from log)
wp db query "SELECT recipient, status, error_message FROM wp_sybgo_email_log WHERE status='failed'"sybgo/
├── sybgo.php # Main plugin file (WordPress header)
├── class-sybgo.php # Lifecycle orchestrator (activate, deactivate, init)
├── class-ability-manager.php # WP7 Ability API registration utility
├── class-cron-manager.php # WP-Cron registration utility
├── class-factory.php # Dependency injection
│
├── modules/ # Feature modules (one per domain area)
│ ├── interface-module.php # Module_Interface contract
│ ├── class-event-module.php # Event tracking wiring
│ ├── class-report-module.php # Reporting UI and freeze cron
│ ├── class-email-module.php # Email delivery crons
│ ├── class-ai-module.php # AI ability registration
│ └── class-settings-module.php # Settings UI, cleanup cron
│
├── database/ # Data layer
│ ├── class-databasemanager.php
│ ├── class-event-repository.php
│ └── class-report-repository.php
│
├── events/ # Event tracking
│ ├── class-event-tracker.php
│ ├── class-event-registry.php
│ └── trackers/
│ ├── class-post-tracker.php
│ ├── class-user-tracker.php
│ ├── class-comment-tracker.php
│ └── class-update-tracker.php
│
├── reports/ # Report generation
│ ├── class-report-manager.php
│ └── class-report-generator.php
│
├── admin/ # WordPress admin
│ ├── class-admin-manager.php # Admin registration utility
│ ├── class-dashboard-widget.php
│ ├── class-settings-page.php
│ ├── class-reports-page.php
│ └── class-uninstaller.php # Plugin cleanup on uninstall
│
├── uninstall.php # WP uninstall entry point
│
├── email/ # Email system
│ ├── class-email-manager.php
│ └── class-email-template.php
│
├── ai/ # AI integration
│ └── class-ai-summarizer.php
│
├── api/ # Extensibility
│ └── functions.php
│
├── docs/ # Documentation
│ ├── event-tracking.md
│ ├── report-lifecycle.md
│ ├── extension-api.md
│ └── development.md (this file)
│
└── Tests/ # Test suite
├── Unit/
│ ├── Events/
│ ├── Reports/
│ ├── Email/
│ └── Admin/
└── Integration/
New domain wiring goes into a feature module under wp-plugin/modules/, not directly into class-sybgo.php. Each module implements Module_Interface (a single boot(): void method) and receives only the dependencies it needs — a subset of Factory, Cron_Manager, Admin_Manager, and Ability_Manager.
Sybgo::init() orchestrates the startup sequence:
- All five modules have
boot()called on them in order. Eachboot()only registers on managers — it never executes domain logic directly (e.g.$this->cron->register(...),$this->admin->register_page(...)). Cron_Manager::init()is called — schedules all registered cron events and wires theiradd_actioncallbacks.Admin_Manager::init()is called (admin context only) — callsinit()on each registered page and wires the cleanup handler and asset enqueuer.Ability_Manager::init()is deferred to theinitWordPress action at priority 20 — modules register abilities at priority 5 on the same hook, so their registrations complete before the manager wires them into WP.
Callback methods on a module (e.g. freeze_report_callback(), cleanup_old_events_callback()) are public named methods, making them independently testable without running init().
| Module | Managers | Responsibilities |
|---|---|---|
Event_Module |
Ability_Manager |
Event Tracker init, sybgo_init_api(), sybgo/track-events ability |
Report_Module |
Cron_Manager, Admin_Manager |
Dashboard_Widget, Reports_Page, freeze cron |
Email_Module |
Cron_Manager |
Send and retry email crons |
AI_Module |
Ability_Manager |
sybgo/generate-summary ability |
Settings_Module |
Cron_Manager, Admin_Manager |
Settings_Page, cleanup cron, asset enqueuer, cleanup form handler |
When adding a new domain feature, identify the appropriate existing module or create a new one. Do not add hooks or domain logic to class-sybgo.php.
Sybgo registers two abilities with the WordPress 7 Ability API via Ability_Manager. Both are registered at init priority 5 so the sybgo text domain is loaded before __() evaluates, while Ability_Manager::init() (which wires them into WP) runs at priority 20.
| Ability name | Registered by | Permission |
|---|---|---|
sybgo/track-events |
Event_Module |
manage_options |
sybgo/generate-summary |
AI_Module |
manage_options |
sybgo/track-events confirms that the Event Tracker is active. sybgo/generate-summary proxies the AI summariser; its execute_callback returns null when the summariser is unavailable (WP < 7 or no AI transport configured).
The dashboard widget registers two AJAX actions, both protected by the sybgo_widget_nonce nonce (key: nonce in the POST body).
| Action | Handler | Capability | Success response |
|---|---|---|---|
sybgo_filter_events |
Dashboard_Widget::ajax_filter_events() |
read |
{html: string, count: int} |
sybgo_widget_ai_summary |
Dashboard_Widget::ajax_widget_ai_summary() |
manage_options |
{summary: string} |
The nonce value and ajaxUrl are available in the sybgoWidget JS object (localized by Dashboard_Widget::enqueue_assets()). See ajax-actions.md for full parameter and response documentation.
Reports_Page accepts Aggregated_Event_Repository as its 7th constructor argument. This repository is used by render_php_errors_table() to query PHP error rows for a report's date range. When instantiating Reports_Page directly (e.g., in tests), pass an Aggregated_Event_Repository instance as the final argument.
Similarly, Dashboard_Widget accepts Aggregated_Event_Repository as its 6th constructor argument for the PHP Errors widget section.
When a user deletes the plugin from the WordPress admin, WordPress calls uninstall.php at the plugin root. This file bootstraps the autoloader and delegates all cleanup to Sybgo\Admin\Uninstaller::run().
Uninstaller performs three steps in order:
- Drop database tables — calls
DatabaseManager::get_table_names()(static, no side effects) and issuesDROP TABLE IF EXISTSfor each table. - Clear cron events — calls
Cron_Manager::get_hooks()and passes each hook towp_clear_scheduled_hook(). - Delete options — calls
Settings_Page::get_option_names()and passes each name todelete_option().
Each of those static methods is the single source of truth for its identifiers, so adding a new table, hook, or option to the appropriate method is enough to ensure it is also cleaned up on uninstall.
The deactivation hook (Sybgo::deactivate()) only clears cron events. Full data removal (tables, options) happens only on uninstall, not on deactivation.
Create events/trackers/class-media-tracker.php:
namespace Rocket\Sybgo\Events\Trackers;
use Rocket\Sybgo\Database\Event_Repository;
class Media_Tracker {
private Event_Repository $event_repo;
public function __construct( Event_Repository $event_repo ) {
$this->event_repo = $event_repo;
add_filter( 'sybgo_event_types', array( $this, 'register_event_types' ) );
}
public function register_hooks(): void {
add_action( 'add_attachment', array( $this, 'on_media_upload' ) );
}
public function register_event_types( array $types ): array {
$types['media_uploaded'] = array(
'icon' => '📎',
'stat_label' => __( 'Media Uploads', 'sybgo' ),
'short_title' => function ( array $event_data ): string {
return $event_data['object']['filename'];
},
'detailed_title' => function ( array $event_data ): string {
return 'Uploaded: ' . $event_data['object']['filename'];
},
'ai_description' => function ( array $object, array $metadata ): string {
return "File uploaded: {$object['filename']} ({$metadata['mime_type']})";
},
'describe' => function ( array $event_data ): string {
return "Event Type: Media Uploaded\nData: Filename, size, MIME type";
},
);
return $types;
}
public function on_media_upload( int $attachment_id ): void {
$event_data = [
'action' => 'uploaded',
'object' => [
'type' => 'media',
'id' => $attachment_id,
'filename' => basename( get_attached_file( $attachment_id ) )
],
'context' => [
'user_id' => get_current_user_id(),
'user_name' => wp_get_current_user()->display_name,
],
'metadata' => [
'file_size' => filesize( get_attached_file( $attachment_id ) ),
'mime_type' => get_post_mime_type( $attachment_id )
]
];
$this->event_repo->create( array(
'event_type' => 'media_uploaded',
'event_data' => $event_data,
) );
}
}Add to events/class-event-tracker.php in the load_trackers() method:
$this->trackers = array(
'post' => new Trackers\Post_Tracker( $this->event_repo ),
'user' => new Trackers\User_Tracker( $this->event_repo ),
'update' => new Trackers\Update_Tracker( $this->event_repo ),
'comment' => new Trackers\Comment_Tracker( $this->event_repo ),
'media' => new Trackers\Media_Tracker( $this->event_repo ),
);Event types registered via the sybgo_event_types filter are automatically picked up by the Report_Generator for totals and trends — no manual updates needed.
Create Tests/Unit/Events/MediaTrackerTest.php:
class MediaTrackerTest extends TestCase {
public function test_track_media_upload() {
// Test implementation
}
}Check PHP version:
php -v # Must be 7.4+Update dependencies:
composer updateClear cache:
composer dump-autoloadCommon issues:
- Missing type declarations
- Incorrect escaping functions
- Wrong indentation (tabs vs spaces)
- Missing DocBlocks
Fix automatically:
composer phpcs:fixCheck schedule:
wp cron event listTest manually:
wp cron event run sybgo_freeze_weekly_report
wp cron event run sybgo_send_report_emailsEnable system cron:
# In crontab -e
*/15 * * * * wget -q -O - https://yoursite.local/wp-cron.php?doing_wp_cron# Generate test events
for i in {1..1000}; do
wp post create --post_title="Test Post $i" --post_status=publish
done
# Check performance
time wp cron event run sybgo_freeze_weekly_report-- Explain slow queries
EXPLAIN SELECT * FROM wp_sybgo_events WHERE report_id IS NULL;
-- Check index usage
SHOW INDEXES FROM wp_sybgo_events;- All tests passing (
composer run-tests) - Code standards passing (
composer phpcs) - Documentation updated if needed
- Commit messages are descriptive
- No debug code left in
Add feature: Brief description
- Detailed point 1
- Detailed point 2
Closes #123
- Event Tracking - Understanding event system
- Report Lifecycle - How reports work
- Extension API - Plugin integration