From d0b579943f83ce32b23d629f0b401d60f62dd18e Mon Sep 17 00:00:00 2001 From: Manoel Aranda Neto Date: Wed, 20 May 2026 13:19:37 +0200 Subject: [PATCH 1/2] Add PHP skill support --- basics/php/.env.example | 3 + basics/php/.gitignore | 4 + basics/php/README.md | 147 ++++++++ basics/php/composer.json | 13 + basics/php/todo.php | 349 ++++++++++++++++++ .../skills/error-tracking/config.yaml | 6 + .../skills/integration/config.yaml | 8 + .../instrument-error-tracking/config.yaml | 1 + .../instrument-error-tracking/description.md | 2 +- .../instrument-integration/config.yaml | 2 + .../instrument-integration/description.md | 2 +- .../instrument-product-analytics/config.yaml | 2 + .../description.md | 2 +- 13 files changed, 538 insertions(+), 3 deletions(-) create mode 100644 basics/php/.env.example create mode 100644 basics/php/.gitignore create mode 100644 basics/php/README.md create mode 100644 basics/php/composer.json create mode 100644 basics/php/todo.php diff --git a/basics/php/.env.example b/basics/php/.env.example new file mode 100644 index 00000000..6bbc2938 --- /dev/null +++ b/basics/php/.env.example @@ -0,0 +1,3 @@ +# PostHog configuration +POSTHOG_API_KEY=phc_your_api_key_here +POSTHOG_HOST=https://us.i.posthog.com diff --git a/basics/php/.gitignore b/basics/php/.gitignore new file mode 100644 index 00000000..9539b934 --- /dev/null +++ b/basics/php/.gitignore @@ -0,0 +1,4 @@ +vendor/ +.env +composer.lock +.todo_app.json diff --git a/basics/php/README.md b/basics/php/README.md new file mode 100644 index 00000000..32e00f70 --- /dev/null +++ b/basics/php/README.md @@ -0,0 +1,147 @@ +# PostHog PHP Example - CLI Todo App + +A simple command-line todo application built with plain PHP (no framework) demonstrating PostHog integration for CLIs, scripts, data pipelines, and non-web PHP applications. + +## Purpose + +This example serves as: +- **Verification** that the context-mill wizard works for plain PHP projects +- **Reference implementation** of PostHog best practices for non-framework PHP code +- **Working example** you can run and modify + +## Features Demonstrated + +- **SDK initialization** - Uses `PostHog::init(...)` once with environment-based configuration +- **Event tracking** - Captures user actions with `distinctId` and properties +- **User identification** - Associates properties with users via `PostHog::identify(...)` +- **Error tracking** - Enables automatic PHP error tracking and manually captures handled exceptions +- **Proper flushing** - Calls `PostHog::flush()` before CLI exit + +## Quick Start + +### 1. Install Dependencies + +```bash +composer install +``` + +### 2. Configure PostHog + +```bash +# Copy environment template +cp .env.example .env + +# Edit .env and add your PostHog API key +# POSTHOG_API_KEY=phc_your_api_key_here +# POSTHOG_HOST=https://us.i.posthog.com +``` + +### 3. Run the App + +```bash +# Add a todo +php todo.php add "Buy groceries" + +# List all todos +php todo.php list + +# Complete a todo +php todo.php complete 1 + +# Delete a todo +php todo.php delete 1 + +# Show statistics +php todo.php stats +``` + +## What Gets Tracked + +The app tracks these events in PostHog: + +| Event | Properties | Purpose | +|-------|-----------|---------| +| `todo_added` | `todo_id`, `todo_length`, `total_todos` | When user adds a new todo | +| `todos_viewed` | `total_todos`, `completed_todos` | When user lists todos | +| `todo_completed` | `todo_id`, `time_to_complete_hours` | When user completes a todo | +| `todo_deleted` | `todo_id`, `was_completed` | When user deletes a todo | +| `stats_viewed` | `total_todos`, `completed_todos`, `pending_todos` | When user views stats | +| `$exception` | exception details and command context | When handled errors occur | + +## Code Structure + +``` +basics/php/ +├── todo.php # Main CLI application +├── composer.json # PHP dependencies +├── .env.example # Environment variable template +├── .gitignore # Git ignore rules +└── README.md # This file +``` + +## Key Implementation Patterns + +### 1. Initialize Once + +```php +PostHog::init($apiKey, [ + 'host' => $host, + 'error_tracking' => [ + 'enabled' => true, + ], +]); +``` + +### 2. Event Tracking Pattern + +```php +PostHog::capture([ + 'distinctId' => 'user_123', + 'event' => 'event_name', + 'properties' => ['key' => 'value'], +]); +``` + +### 3. Identifying Users + +```php +PostHog::identify([ + 'distinctId' => 'user_123', + 'properties' => ['app_language' => 'php'], +]); +``` + +### 4. Exception Tracking + +```php +try { + riskyOperation(); +} catch (Throwable $e) { + PostHog::captureException($e, 'user_123', [ + 'command' => 'example_command', + ]); +} +``` + +### 5. Flush Before CLI Exit + +```php +PostHog::flush(); +``` + +## Running Without PostHog + +The app works fine without PostHog configured - it simply won't track analytics. You'll see a warning message but the app continues to function normally. + +## Next Steps + +- Modify `todo.php` to experiment with PostHog tracking +- Add new commands and track their usage +- Explore feature flags: `PostHog::isFeatureEnabled('flag-name', 'user_id')` +- Check your PostHog dashboard to see tracked events + +## Learn More + +- [PostHog PHP SDK Documentation](https://posthog.com/docs/libraries/php) +- [PostHog PHP Error Tracking](https://posthog.com/docs/error-tracking/installation/php) +- [PostHog Product Analytics PHP installation](https://posthog.com/docs/product-analytics/installation/php) diff --git a/basics/php/composer.json b/basics/php/composer.json new file mode 100644 index 00000000..10469696 --- /dev/null +++ b/basics/php/composer.json @@ -0,0 +1,13 @@ +{ + "name": "posthog/context-mill-php-example", + "description": "Plain PHP CLI todo app demonstrating PostHog integration", + "type": "project", + "license": "MIT", + "require": { + "php": ">=8.0", + "posthog/posthog-php": "^4.0" + }, + "scripts": { + "start": "php todo.php" + } +} diff --git a/basics/php/todo.php b/basics/php/todo.php new file mode 100644 index 00000000..a6e66978 --- /dev/null +++ b/basics/php/todo.php @@ -0,0 +1,349 @@ + getenv('POSTHOG_HOST') ?: 'https://us.i.posthog.com', + 'error_tracking' => [ + 'enabled' => true, + 'context_provider' => static function (array $payload): array { + return [ + 'distinctId' => getUserId(), + 'properties' => [ + 'app' => 'php_todo_cli', + 'runtime' => PHP_VERSION, + '$exception_source' => $payload['source'] ?? null, + ], + ]; + }, + ], + ]); + + return true; +} + +function getUserId(): string +{ + $path = dataFilePath(); + if (file_exists($path)) { + $data = json_decode((string) file_get_contents($path), true); + if (is_array($data) && isset($data['user_id'])) { + return (string) $data['user_id']; + } + } + + return 'user_' . bin2hex(random_bytes(4)); +} + +function loadTodos(): array +{ + $path = dataFilePath(); + if (!file_exists($path)) { + return ['user_id' => getUserId(), 'todos' => []]; + } + + $data = json_decode((string) file_get_contents($path), true); + if (!is_array($data)) { + return ['user_id' => getUserId(), 'todos' => []]; + } + + $data['todos'] = $data['todos'] ?? []; + $data['user_id'] = $data['user_id'] ?? getUserId(); + return $data; +} + +function saveTodos(array $data): void +{ + file_put_contents(dataFilePath(), json_encode($data, JSON_PRETTY_PRINT) . PHP_EOL); +} + +function identifyUser(bool $posthogEnabled): void +{ + if (!$posthogEnabled) { + return; + } + + PostHog::identify([ + 'distinctId' => getUserId(), + 'properties' => [ + 'app_language' => 'php', + 'app_type' => 'cli', + ], + ]); +} + +function trackEvent(bool $posthogEnabled, string $eventName, array $properties = []): void +{ + if (!$posthogEnabled) { + return; + } + + PostHog::capture([ + 'distinctId' => getUserId(), + 'event' => $eventName, + 'properties' => $properties, + ]); +} + +function cmdAdd(string $text, bool $posthogEnabled): void +{ + $data = loadTodos(); + + $todo = [ + 'id' => count($data['todos']) + 1, + 'text' => $text, + 'completed' => false, + 'created_at' => date(DATE_ATOM), + ]; + + $data['todos'][] = $todo; + saveTodos($data); + + echo "Added todo #{$todo['id']}: {$todo['text']}\n"; + + trackEvent($posthogEnabled, 'todo_added', [ + 'todo_id' => $todo['id'], + 'todo_length' => strlen($todo['text']), + 'total_todos' => count($data['todos']), + ]); +} + +function cmdList(bool $posthogEnabled): void +{ + $data = loadTodos(); + + if (count($data['todos']) === 0) { + echo "No todos yet! Add one with: php todo.php add 'Your task'\n"; + return; + } + + echo "\nYour Todos (" . count($data['todos']) . " total):\n\n"; + + foreach ($data['todos'] as $todo) { + $status = $todo['completed'] ? 'X' : ' '; + echo " [{$status}] #{$todo['id']}: {$todo['text']}\n"; + } + + echo "\n"; + + trackEvent($posthogEnabled, 'todos_viewed', [ + 'total_todos' => count($data['todos']), + 'completed_todos' => count(array_filter($data['todos'], static fn (array $todo): bool => (bool) $todo['completed'])), + ]); +} + +function cmdComplete(int $id, bool $posthogEnabled): void +{ + $data = loadTodos(); + + foreach ($data['todos'] as &$todo) { + if ((int) $todo['id'] !== $id) { + continue; + } + + if ($todo['completed']) { + echo "Todo #{$id} is already completed\n"; + return; + } + + $todo['completed'] = true; + $todo['completed_at'] = date(DATE_ATOM); + saveTodos($data); + + echo "Completed todo #{$todo['id']}: {$todo['text']}\n"; + + $timeToComplete = (strtotime($todo['completed_at']) - strtotime($todo['created_at'])) / 3600; + trackEvent($posthogEnabled, 'todo_completed', [ + 'todo_id' => $todo['id'], + 'time_to_complete_hours' => $timeToComplete, + ]); + return; + } + + echo "ERROR: Todo #{$id} not found\n"; +} + +function cmdDelete(int $id, bool $posthogEnabled): void +{ + $data = loadTodos(); + + foreach ($data['todos'] as $index => $todo) { + if ((int) $todo['id'] !== $id) { + continue; + } + + unset($data['todos'][$index]); + $data['todos'] = array_values($data['todos']); + saveTodos($data); + + echo "Deleted todo #{$id}\n"; + + trackEvent($posthogEnabled, 'todo_deleted', [ + 'todo_id' => $todo['id'], + 'was_completed' => $todo['completed'], + ]); + return; + } + + echo "ERROR: Todo #{$id} not found\n"; +} + +function cmdStats(bool $posthogEnabled): void +{ + $data = loadTodos(); + + $total = count($data['todos']); + $completed = count(array_filter($data['todos'], static fn (array $todo): bool => (bool) $todo['completed'])); + $pending = $total - $completed; + $rate = $total > 0 ? number_format($completed / $total * 100, 1) : '0.0'; + + echo "\nStats:\n\n"; + echo " Total todos: {$total}\n"; + echo " Completed: {$completed}\n"; + echo " Pending: {$pending}\n"; + echo " Completion rate: {$rate}%\n\n"; + + trackEvent($posthogEnabled, 'stats_viewed', [ + 'total_todos' => $total, + 'completed_todos' => $completed, + 'pending_todos' => $pending, + ]); +} + +function printUsage(): void +{ + echo << Mark todo as completed + php todo.php delete Delete a todo + php todo.php stats Show statistics +USAGE; +} + +$posthogEnabled = false; + +try { + $posthogEnabled = initializePostHog(); + identifyUser($posthogEnabled); + + $command = $argv[1] ?? null; + if (!$command) { + printUsage(); + exit(0); + } + + switch ($command) { + case 'add': + $text = $argv[2] ?? null; + if (!$text) { + echo "ERROR: Please provide todo text\n"; + echo "Usage: php todo.php add \"Your task\"\n"; + exit(1); + } + cmdAdd($text, $posthogEnabled); + break; + + case 'list': + cmdList($posthogEnabled); + break; + + case 'complete': + $id = (int) ($argv[2] ?? 0); + if ($id <= 0) { + echo "ERROR: Please provide a valid todo ID\n"; + echo "Usage: php todo.php complete \n"; + exit(1); + } + cmdComplete($id, $posthogEnabled); + break; + + case 'delete': + $id = (int) ($argv[2] ?? 0); + if ($id <= 0) { + echo "ERROR: Please provide a valid todo ID\n"; + echo "Usage: php todo.php delete \n"; + exit(1); + } + cmdDelete($id, $posthogEnabled); + break; + + case 'stats': + cmdStats($posthogEnabled); + break; + + default: + echo "ERROR: Unknown command '{$command}'\n"; + printUsage(); + exit(1); + } +} catch (Throwable $e) { + echo "ERROR: {$e->getMessage()}\n"; + + if ($posthogEnabled) { + PostHog::captureException($e, getUserId(), [ + 'command' => $argv[1] ?? null, + 'app' => 'php_todo_cli', + ]); + } + + exit(1); +} finally { + if ($posthogEnabled) { + PostHog::flush(); + } +} diff --git a/transformation-config/skills/error-tracking/config.yaml b/transformation-config/skills/error-tracking/config.yaml index d3a46624..141ae468 100644 --- a/transformation-config/skills/error-tracking/config.yaml +++ b/transformation-config/skills/error-tracking/config.yaml @@ -40,6 +40,12 @@ variants: docs_urls: - https://posthog.com/docs/error-tracking/installation/python.md + - id: php + display_name: PHP + tags: [php] + docs_urls: + - https://posthog.com/docs/error-tracking/installation/php.md + - id: ruby display_name: Ruby tags: [ruby] diff --git a/transformation-config/skills/integration/config.yaml b/transformation-config/skills/integration/config.yaml index f59e32ae..ac3ccfc6 100644 --- a/transformation-config/skills/integration/config.yaml +++ b/transformation-config/skills/integration/config.yaml @@ -132,6 +132,14 @@ variants: docs_urls: - https://posthog.com/docs/libraries/laravel.md + - id: php + example_paths: basics/php + display_name: PHP + description: PostHog integration for any PHP application using the PHP SDK + tags: [php] + docs_urls: + - https://posthog.com/docs/libraries/php.md + - id: ruby-on-rails example_paths: basics/ruby-on-rails display_name: Ruby on Rails diff --git a/transformation-config/skills/omnibus/instrument-error-tracking/config.yaml b/transformation-config/skills/omnibus/instrument-error-tracking/config.yaml index 3bc4fda9..80f8fe15 100644 --- a/transformation-config/skills/omnibus/instrument-error-tracking/config.yaml +++ b/transformation-config/skills/omnibus/instrument-error-tracking/config.yaml @@ -19,6 +19,7 @@ variants: - https://posthog.com/docs/error-tracking/installation/nextjs.md - https://posthog.com/docs/error-tracking/installation/node.md - https://posthog.com/docs/error-tracking/installation/python.md + - https://posthog.com/docs/error-tracking/installation/php.md - https://posthog.com/docs/error-tracking/installation/ruby.md - https://posthog.com/docs/error-tracking/installation/ruby-on-rails.md - https://posthog.com/docs/error-tracking/installation/go.md diff --git a/transformation-config/skills/omnibus/instrument-error-tracking/description.md b/transformation-config/skills/omnibus/instrument-error-tracking/description.md index 3dbd9ca2..cccc527a 100644 --- a/transformation-config/skills/omnibus/instrument-error-tracking/description.md +++ b/transformation-config/skills/omnibus/instrument-error-tracking/description.md @@ -2,7 +2,7 @@ Use this skill to add PostHog error tracking that captures and monitors exceptions in your application. Use it after implementing features or reviewing PRs to ensure errors are tracked with full stack traces and source maps. If PostHog is not yet installed, this skill also covers initial SDK setup. Supports any platform or language. -Supported platforms: React, Next.js, Web (JavaScript), Node.js, Python, Ruby, Ruby on Rails, Go, Angular, Svelte, Nuxt, React Native, Flutter, Android, and Hono. +Supported platforms: React, Next.js, Web (JavaScript), Node.js, Python, PHP, Ruby, Ruby on Rails, Go, Angular, Svelte, Nuxt, React Native, Flutter, Android, and Hono. ## Instructions diff --git a/transformation-config/skills/omnibus/instrument-integration/config.yaml b/transformation-config/skills/omnibus/instrument-integration/config.yaml index 6bdee4b9..44ffde82 100644 --- a/transformation-config/skills/omnibus/instrument-integration/config.yaml +++ b/transformation-config/skills/omnibus/instrument-integration/config.yaml @@ -40,6 +40,7 @@ variants: - basics/python # PHP - basics/laravel + - basics/php # Ruby - basics/ruby-on-rails - basics/ruby @@ -75,6 +76,7 @@ variants: - https://posthog.com/docs/references/posthog-python.md # PHP - https://posthog.com/docs/libraries/laravel.md + - https://posthog.com/docs/libraries/php.md # Ruby - https://posthog.com/docs/libraries/ruby-on-rails.md - https://posthog.com/docs/libraries/ruby.md diff --git a/transformation-config/skills/omnibus/instrument-integration/description.md b/transformation-config/skills/omnibus/instrument-integration/description.md index dcc4502d..58be1a06 100644 --- a/transformation-config/skills/omnibus/instrument-integration/description.md +++ b/transformation-config/skills/omnibus/instrument-integration/description.md @@ -2,7 +2,7 @@ Use this skill to add the PostHog SDK to an application. Use it when setting up PostHog for the first time, or reviewing PRs that need PostHog initialization. Covers SDK installation, provider setup, and basic configuration. Supports any framework or language. -Supported frameworks: Next.js, React, React Router, Vue, Nuxt, TanStack Start, SvelteKit, Astro, Angular, Django, Flask, FastAPI, Laravel, Ruby on Rails, Android, Swift, React Native, Expo, Node.js, and vanilla JavaScript. +Supported frameworks: Next.js, React, React Router, Vue, Nuxt, TanStack Start, SvelteKit, Astro, Angular, Django, Flask, FastAPI, Laravel, PHP, Ruby on Rails, Android, Swift, React Native, Expo, Node.js, and vanilla JavaScript. ## Instructions diff --git a/transformation-config/skills/omnibus/instrument-product-analytics/config.yaml b/transformation-config/skills/omnibus/instrument-product-analytics/config.yaml index 5c2d34c5..67706805 100644 --- a/transformation-config/skills/omnibus/instrument-product-analytics/config.yaml +++ b/transformation-config/skills/omnibus/instrument-product-analytics/config.yaml @@ -37,6 +37,7 @@ variants: - basics/python # PHP - basics/laravel + - basics/php # Ruby - basics/ruby-on-rails - basics/ruby @@ -66,6 +67,7 @@ variants: - https://posthog.com/docs/references/posthog-python.md # PHP - https://posthog.com/docs/libraries/laravel.md + - https://posthog.com/docs/libraries/php.md # Ruby - https://posthog.com/docs/libraries/ruby-on-rails.md - https://posthog.com/docs/libraries/ruby.md diff --git a/transformation-config/skills/omnibus/instrument-product-analytics/description.md b/transformation-config/skills/omnibus/instrument-product-analytics/description.md index 7e90fdcb..9ac07d62 100644 --- a/transformation-config/skills/omnibus/instrument-product-analytics/description.md +++ b/transformation-config/skills/omnibus/instrument-product-analytics/description.md @@ -2,7 +2,7 @@ Use this skill to add product analytics events (capture calls) that track meaningful user actions in new or changed code. Use it after implementing features or reviewing PRs to ensure key user behaviors are captured. If PostHog is not yet installed, this skill also covers initial SDK setup. Supports any framework or language. -Supported frameworks: Next.js, React Router, Nuxt, Vue, TanStack Start, SvelteKit, Astro, Angular, Django, Flask, FastAPI, Laravel, Ruby on Rails, Android, iOS, React Native, Expo, and more. +Supported frameworks: Next.js, React Router, Nuxt, Vue, TanStack Start, SvelteKit, Astro, Angular, Django, Flask, FastAPI, Laravel, PHP, Ruby on Rails, Android, iOS, React Native, Expo, and more. ## Instructions From 5e0903c2f7bb91a4b0960ff06684de51a64ffd20 Mon Sep 17 00:00:00 2001 From: Manoel Aranda Neto Date: Wed, 20 May 2026 13:32:57 +0200 Subject: [PATCH 2/2] Ignore audit reference in Semgrep --- .semgrepignore | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/.semgrepignore b/.semgrepignore index 65e56abd..860767d9 100644 --- a/.semgrepignore +++ b/.semgrepignore @@ -13,3 +13,7 @@ basics/react-native/android/app/src/main/AndroidManifest.xml basics/expo/android/app/src/main/AndroidManifest.xml basics/android/app/src/main/AndroidManifest.xml + +# Markdown audit reference contains HogQL examples with PostHog $host event property. +# Semgrep's nginx host-header rule misidentifies this docs-only SQL as nginx config. +transformation-config/skills/audit-events/references/3-events-optimize.md