# PRAutoBlogger — Conventions This document codifies the naming patterns, coding conventions, and step-by-step guides for extending PRAutoBlogger. Update this file whenever a new pattern is introduced. --- ## Naming Conventions ### PHP Classes - **Format:** `PRAutoBlogger_{PascalCase}` (e.g., `PRAutoBlogger_Content_Generator`) - **File naming:** `class-{kebab-case}.php` (e.g., `class-content-generator.php`) - **Interfaces:** `interface-{kebab-case}.php` with `PRAutoBlogger_{Name}_Interface` class name ### Hooks (Actions & Filters) - **Prefix:** All hooks start with `prautoblogger_` - **Actions:** `prautoblogger_{event}` (e.g., `prautoblogger_before_content_generation`) - **Filters:** `prautoblogger_filter_{what}` (e.g., `prautoblogger_filter_article_idea_score`) - **Hook callbacks registered in:** `class-prautoblogger.php` (central loader), never in the classes themselves ### Database - **Table prefix:** `{$wpdb->prefix}prautoblogger_` (e.g., `wp_prautoblogger_source_data`) - **Option prefix:** `prautoblogger_` (e.g., `prautoblogger_openrouter_api_key`) - **Post meta prefix:** `_prautoblogger_` (underscore prefix hides from custom fields UI) - **Transient prefix:** `prautoblogger_` (e.g., `prautoblogger_reddit_token`) ### Constants - **Format:** `PRAUTOBLOGGER_{UPPER_SNAKE}` (e.g., `PRAUTOBLOGGER_MAX_RETRIES`) - **Defined in:** `prautoblogger.php` (bootstrap file) for global constants ### Hook Callback Methods - **Action callbacks:** Prefixed with `on_` (e.g., `on_daily_generation_triggered`) - **Filter callbacks:** Prefixed with `filter_` (e.g., `filter_content_quality`) --- ## Error Handling & Logging ### Structured Logger All logging uses `PRAutoBlogger_Logger` (singleton) instead of raw `error_log()`. The logger writes to the `prab_event_log` custom table and forwards errors/warnings to PHP's `error_log()` for server-level monitoring. The verbosity threshold is user-configurable via Settings → Publishing → Log Level. **Never call `error_log()` directly.** Always use the Logger: ```php $logger = PRAutoBlogger_Logger::instance(); $logger->error( 'API call failed: ' . $error_message, 'openrouter' ); $logger->warning( 'Budget nearing limit.', 'cost-tracker' ); $logger->info( 'Pipeline complete: 3 posts generated.', 'pipeline' ); $logger->debug( 'Token counts: prompt=1200, completion=800', 'openrouter' ); ``` ### Log levels (ascending verbosity) | Level | Numeric | Use for | |---------|---------|---------| | error | 0 | Failures: API errors, parse failures, DB write failures | | warning | 1 | Degraded behavior: budget approaching, missing providers, fallback pricing | | info | 2 | Key events: pipeline start/stop, articles generated, metrics collected | | debug | 3 | Verbose detail: token counts, timing, skipped posts, intermediate values | ### Context strings The second argument to every log call is a short context tag identifying the origin: `pipeline`, `scheduler`, `collector`, `analyzer`, `editor`, `openrouter`, `reddit`, `ga4`, `metrics`, `cost-tracker`, `encryption`. ### What gets shown to the user - API key issues → admin notice (persistent until resolved) - Budget exceeded → admin notice + email to site admin - Generation failures → logged to `prab_generation_log` with `response_status = 'error'` - Generation failures also shown in the admin dashboard widget - All log entries visible in PRAutoBlogger → Activity Log (filterable by level) ### What gets retried - LLM API calls: exponential backoff, max 3 retries, then fail loudly - Reddit API calls: exponential backoff, max 3 retries, then skip this collection cycle - **Never retry silently.** Every retry is logged. Every final failure is surfaced. ### Error severity levels 1. **Fatal:** Plugin cannot function (missing API key, budget exhausted) → admin notice, stop all scheduled jobs 2. **Retriable:** Transient API failure → retry with backoff, log each attempt 3. **Skippable:** Single post generation failed → log, skip this post, continue with others --- ## How To: Add a New LLM Provider 1. Create `includes/providers/class-{name}-provider.php` 2. Implement `PRAutoBlogger_LLM_Provider_Interface` (see `interface-llm-provider.php`) 3. Required methods: `send_chat_completion()`, `get_available_models()`, `estimate_cost()` 4. Add provider option to the settings page in `admin/class-admin-page.php` 5. Register the provider in `class-prautoblogger.php` loader 6. Update ARCHITECTURE.md external API integrations table ### Interface contract: ```php interface PRAutoBlogger_LLM_Provider_Interface { public function send_chat_completion(array $messages, string $model, array $options = []): array; public function get_available_models(): array; public function estimate_cost(string $model, int $prompt_tokens, int $completion_tokens): float; public function get_provider_name(): string; } ``` --- ## How To: Add a New Source Provider (Social Platform) 1. Create `includes/providers/class-{platform}-provider.php` 2. Implement `PRAutoBlogger_Source_Provider_Interface` (see `interface-source-provider.php`) 3. Required methods: `collect_data()`, `get_source_type()`, `validate_credentials()` 4. Add API credentials fields to the settings page in `admin/class-admin-page.php` 5. Register the provider in `class-prautoblogger.php` loader 6. Add the source type string to the `ab_source_data.source_type` allowed values 7. Update ARCHITECTURE.md file tree and external API integrations table ### Interface contract: ```php interface PRAutoBlogger_Source_Provider_Interface { public function collect_data(array $config): array; // Returns PRAutoBlogger_Source_Data[] public function get_source_type(): string; // e.g., 'reddit', 'tiktok' public function validate_credentials(): bool; public function get_rate_limit_status(): array; } ``` --- ## How To: Add a New Image Provider 1. Create `includes/providers/class-{name}-image-provider.php` 2. Implement `PRAutoBlogger_Image_Provider_Interface` (see `interface-image-provider.php`) 3. Required methods: `generate_image()`, `estimate_cost()`, `get_provider_name()`, `validate_credentials_detailed()` 4. Split helpers into companions if you'd exceed the 300-line cap. The Cloudflare provider splits into provider + pricing + validator — mirror that shape. 5. Add API credentials fields to `admin/class-settings-fields.php` under the `prautoblogger_images` section 6. Register the provider in `class-prautoblogger.php` loader once the image pipeline lands (commit 1b of the image workstream) 7. Update `ARCHITECTURE.md` file tree, external API integrations table, and the options table ### Interface contract: ```php interface PRAutoBlogger_Image_Provider_Interface { public function generate_image( string $prompt, int $width, int $height, array $options = [] ): array; // {bytes, mime_type, width, height, model, seed, cost_usd, latency_ms} public function estimate_cost( int $width, int $height, array $options = [] ): float; public function get_provider_name(): string; // e.g., 'cloudflare_workers_ai' public function validate_credentials_detailed(): array; // {status, message, debug?} } ``` ### Retry + cost rules (non-negotiable) - 5xx / 429 / network errors: retry with exponential backoff up to `PRAUTOBLOGGER_MAX_RETRIES`, respect `Retry-After` when present. - 4xx other than 429: fail loudly on the first response — never retry a deterministic bad request. - Validation check must be non-destructive (no image generation). - Every attempt logs via `PRAutoBlogger_Logger` with context `{provider-slug}-image`. --- ## How To: Tune the Image Composer (v0.17.0+) The deterministic composer (ARCHITECTURE.md #21) renders branded variants between provider bytes and sideload. Three rules when touching it: 1. **Geometry lives in code, not settings.** All sizes/colors/opacities come from `PRAutoBlogger_Image_Composer_Layout::defaults()` and can be overridden via the `prautoblogger_image_compose_layout` filter: ```php add_filter( 'prautoblogger_image_compose_layout', function ( array $layout ): array { $layout['featured']['mark_opacity'] = 0.40; return $layout; } ); ``` Promote a value to a settings field only on explicit CEO request. 2. **Keep renderer classes WP-free.** `Image_Composer_Imagick`, `Image_Composer_Canvas`, and `Image_Composer_Layout` must not call WordPress functions — they are exercised by a standalone render harness (and the determinism test) without a WP bootstrap. Only the orchestrator (`Image_Composer`) and the GD rung (`Image_Composer_Editor`) may touch WP APIs. 3. **Never fatal a publish.** Any new render path needs a try/catch that degrades to pass-through and logs at most one WARNING per run (`warn_once()`). Capability checks go through the cached probe (`prautoblogger_image_compose_capability` option + same-named override filter) — never re-probe per image. Adding a new variant role: extend `Layout::defaults()` + the renderer, whitelist the role in `Image_Composer::configured_variants()` and in `Image_Attacher::ROLE_POST_META` (new `_prautoblogger_{role}_image_id` post meta keeps the uninstall prefix-sweep effective), then cover it in the unit ladder tests. --- ## How To: Add a New Admin Setting 1. Define the option key in the settings array in `admin/class-admin-page.php` → `get_settings_fields()` method 2. Format: add an entry to the appropriate section array: ```php [ 'id' => 'prautoblogger_new_setting', 'label' => __('Setting Label', 'prautoblogger'), 'type' => 'text', // text, number, select, textarea, checkbox, password 'default' => '', 'description' => __('Help text shown below the field.', 'prautoblogger'), 'sanitize' => 'sanitize_text_field', // sanitization callback ] ``` 3. The settings page renderer handles registration, rendering, and sanitization automatically from this array. 4. Access the setting anywhere with: `get_option('prautoblogger_new_setting', $default)` 5. For encrypted settings (API keys): use `PRAutoBlogger_Encryption::encrypt()` / `decrypt()` --- ## How To: Add a New Pipeline Stage The writing pipeline is an ordered array of stages. Each stage is a class method on `Content_Generator`. 1. Add the stage method to `class-content-generator.php`: `protected function stage_{name}(PRAutoBlogger_Content_Request $request): string` 2. Register the stage in the `get_pipeline_stages()` method for the appropriate mode (single_pass or multi_step) 3. Each stage receives the accumulated context and returns its output 4. Each stage logs its cost via `Cost_Tracker::log_api_call()` 5. If a stage fails after retries, the entire pipeline fails (no partial publishes) --- ## How To: Add a New Frontend Component Frontend components use `wp.element` (WordPress-bundled React) — no JSX, no build step. 1. Create the PHP class in `includes/frontend/class-{name}.php`: - Register a shortcode via `add_shortcode()` in an `on_register_shortcode()` method - Register a REST endpoint if the component needs async data (in `on_register_rest_route()`) - Enqueue JS (`wp-element` dependency) and CSS via `wp_enqueue_script/style()` - Pass config to JS via `wp_localize_script()` 2. Create JS in `assets/js/{name}.js`: - Use `wp.element.createElement` (aliased as `el`) for rendering - Import hooks: `var useState = wp.element.useState;` - Read config from `window.{localizedObjectName}` - Handle all states: loading, loaded, empty, error - Mount into the shortcode's `