# PHP Development Skill

> **Technology:** PHP 8.0+ with strict typing for WordPress plugin development

---

## Purpose

This skill covers PHP-specific patterns, WordPress integration, and backend logic for the Comments Press Zone plugin.

---

## PHP Standards

### File Structure

```php
<?php
/**
 * [Description]
 *
 * @package CommentsPressZone\[Subpackage]
 */

declare(strict_types=1);

namespace CommentsPressZone\[Subpackage];

if (!defined('ABSPATH')) {
    exit;
}

class ClassName {
    // Class implementation
}
```

### Namespace Hierarchy

```
CommentsPressZone\
├── Core\              # Plugin initialization, setup
├── Comments\          # Comment handling, engagement
├── Database\          # Schema, migrations
└── Api\              # REST endpoints, AJAX handlers
```

### Strict Typing (MANDATORY)

```php
declare(strict_types=1);

public function get_vote_count(int $comment_id): int {
    // Type declarations enforce correctness
}
```

---

## WordPress Integration Patterns

### Plugin Initialization

```php
namespace CommentsPressZone\Core;

class Plugin {
    private static ?Plugin $instance = null;

    public static function get_instance(): Plugin {
        if (null === self::$instance) {
            self::$instance = new self();
        }
        return self::$instance;
    }

    private function __construct() {
        $this->register_hooks();
    }

    private function register_hooks(): void {
        add_action('plugins_loaded', [$this, 'init']);
        add_action('wp_enqueue_scripts', [$this, 'enqueue_assets']);
    }
}
```

### Hook Registration

```php
// Actions
add_action('init', [$this, 'register_post_types']);
add_action('wp_ajax_presszone_comments_submit', [$this, 'handle_submit']);
add_action('wp_ajax_nopriv_presszone_comments_submit', [$this, 'handle_submit']);

// Filters
add_filter('comment_text', [$this, 'filter_comment_text'], 10, 2);
add_filter('preprocess_comment', [$this, 'validate_comment']);
```

---

## AJAX Handler Pattern

```php
namespace CommentsPressZone\Comments;

class Actions {
    public function handle_vote(): void {
        // 1. Verify nonce
        $nonce = isset($_POST['nonce']) ? sanitize_text_field(wp_unslash($_POST['nonce'])) : '';
        if (!wp_verify_nonce($nonce, 'presszone_comments_nonce')) {
            wp_send_json_error(['message' => esc_html__('Security check failed.', 'comments-press-zone')]);
        }

        // 2. Check authentication
        if (!is_user_logged_in()) {
            wp_send_json_error(['message' => esc_html__('You must be logged in.', 'comments-press-zone')]);
        }

        // 3. Sanitize input
        $comment_id = isset($_POST['comment_id']) ? absint($_POST['comment_id']) : 0;
        $type = isset($_POST['type']) ? sanitize_key($_POST['type']) : '';

        // 4. Validate
        if ($comment_id === 0 || !in_array($type, ['upvote', 'downvote'], true)) {
            wp_send_json_error(['message' => esc_html__('Invalid data.', 'comments-press-zone')]);
        }

        // 5. Process
        $result = $this->process_vote($comment_id, $type);

        // 6. Respond
        wp_send_json_success($result);
    }
}
```

---

## Database Query Patterns

### Using $wpdb (with prepare)

```php
global $wpdb;
$table = $wpdb->prefix . 'presszone_comments_likes';

// SELECT with placeholders
$count = $wpdb->get_var($wpdb->prepare(
    "SELECT COUNT(*) FROM $table WHERE comment_id = %d AND type = %s",
    $comment_id,
    'upvote'
));

// INSERT
$wpdb->insert(
    $table,
    [
        'comment_id' => $comment_id,
        'user_id' => get_current_user_id(),
        'type' => $type,
        'ip_address' => $this->get_user_ip(),
    ],
    ['%d', '%d', '%s', '%s']
);

// UPDATE
$wpdb->update(
    $table,
    ['type' => $new_type],
    ['id' => $vote_id],
    ['%s'],
    ['%d']
);

// DELETE
$wpdb->delete(
    $table,
    ['id' => $vote_id],
    ['%d']
);
```

### Complex Queries

```php
// Join with wp_users
$results = $wpdb->get_results($wpdb->prepare(
    "SELECT u.display_name, COUNT(l.id) as vote_count
     FROM {$wpdb->users} u
     INNER JOIN {$wpdb->prefix}presszone_comments_likes l ON u.ID = l.user_id
     WHERE l.type = %s
     GROUP BY u.ID
     ORDER BY vote_count DESC
     LIMIT %d",
    'upvote',
    10
), ARRAY_A);
```

---

## Options and Settings

```php
// Get option with default
$settings = get_option('presszone_comments_settings', [
    'enable_voting' => true,
    'enable_reports' => true,
    'max_comment_length' => 2000,
]);

// Update option
update_option('presszone_comments_settings', $settings);

// Delete option
delete_option('presszone_comments_settings');
```

---

## User Meta Patterns

```php
// Get user ban status
$is_banned = get_user_meta($user_id, '_presszone_comments_banned', true);

// Set mute expiration
update_user_meta($user_id, '_presszone_comments_muted_until', time() + (24 * HOUR_IN_SECONDS));

// Increment warning count
$warnings = (int) get_user_meta($user_id, '_presszone_comments_warnings', true);
update_user_meta($user_id, '_presszone_comments_warnings', $warnings + 1);

// Delete user meta
delete_user_meta($user_id, '_presszone_comments_banned');
```

---

## Permission Checking

```php
// Basic capability check
if (!current_user_can('moderate_comments')) {
    wp_send_json_error(['message' => esc_html__('Unauthorized.', 'comments-press-zone')]);
}

// Check if user can edit specific comment
if (!current_user_can('edit_comment', $comment_id)) {
    wp_send_json_error(['message' => esc_html__('You cannot edit this comment.', 'comments-press-zone')]);
}

// Protect administrators
if (user_can($user_id, 'administrator')) {
    // Don't allow banning admins
    return;
}
```

---

## Template Loading

```php
namespace CommentsPressZone\Core;

class Plugin {
    public function load_template(string $template_name, array $args = []): void {
        $template_path = PRESSZONE_COMMENTS_PATH . "templates/{$template_name}.php";
        
        if (!file_exists($template_path)) {
            return;
        }

        // Extract args to variables
        extract($args, EXTR_SKIP);

        // Load template
        include $template_path;
    }
}
```

---

## Enqueue Assets

```php
public function enqueue_frontend_assets(): void {
    // Only enqueue on single posts/pages
    if (!is_singular()) {
        return;
    }

    // Styles
    wp_enqueue_style(
        'presszone-comments-frontend',
        PRESSZONE_COMMENTS_URL . 'assets/css/frontend.css',
        [],
        PRESSZONE_COMMENTS_VERSION
    );

    // Scripts
    wp_enqueue_script(
        'presszone-comments-frontend',
        PRESSZONE_COMMENTS_URL . 'assets/js/frontend.js',
        [],
        PRESSZONE_COMMENTS_VERSION,
        true
    );

    // Localize script
    wp_localize_script(
        'presszone-comments-frontend',
        'presszoneCommentsData',
        [
            'ajaxUrl' => admin_url('admin-ajax.php'),
            'nonce' => wp_create_nonce('presszone_comments_nonce'),
            'i18n' => [
                'confirm' => esc_html__('Are you sure?', 'comments-press-zone'),
            ],
        ]
    );
}
```

---

## Error Handling

```php
try {
    $result = $this->process_data($data);
    wp_send_json_success($result);
} catch (\Exception $e) {
    error_log('Comments Press Zone Error: ' . $e->getMessage());
    wp_send_json_error([
        'message' => esc_html__('An error occurred.', 'comments-press-zone'),
    ]);
}
```

---

## Common Patterns

### Singleton Pattern

```php
class Service {
    private static ?Service $instance = null;

    public static function get_instance(): Service {
        if (null === self::$instance) {
            self::$instance = new self();
        }
        return self::$instance;
    }

    private function __construct() {}
}
```

### IP Address Helper

```php
private function get_user_ip(): string {
    if (!empty($_SERVER['HTTP_CLIENT_IP'])) {
        $ip = sanitize_text_field(wp_unslash($_SERVER['HTTP_CLIENT_IP']));
    } elseif (!empty($_SERVER['HTTP_X_FORWARDED_FOR'])) {
        $ip = sanitize_text_field(wp_unslash($_SERVER['HTTP_X_FORWARDED_FOR']));
    } else {
        $ip = isset($_SERVER['REMOTE_ADDR']) ? sanitize_text_field(wp_unslash($_SERVER['REMOTE_ADDR'])) : '';
    }
    return $ip;
}
```

---

## Internationalization (i18n) Best Practices

### CRITICAL Rule: No Variables in Gettext Functions

```php
// ❌ FORBIDDEN - WordPress.org REJECTION
__($variable, 'comments-press-zone')
__($email_template, 'comments-press-zone')
_e($dynamic_text, 'comments-press-zone')

// ✅ CORRECT - Static strings with dynamic values via printf
printf(
    /* translators: %s: User's first name */
    esc_html__('Hello %s, how are you?', 'comments-press-zone'),
    esc_html($user_firstname)
);

// ✅ CORRECT - Multiple placeholders
printf(
    /* translators: 1: Comment count, 2: Post title */
    esc_html__('There are %1$d comments on "%2$s"', 'comments-press-zone'),
    $count,
    esc_html($post_title)
);
```

**User-Configurable Content:** Admin-defined email templates or database values should NOT be translated:

```php
// ❌ WRONG - $template is from database
private function get_email_template(): string {
    $template = get_option('presszone_comments_email_template');
    return __($template, 'comments-press-zone'); // FORBIDDEN
}

// ✅ CORRECT - Return user data as-is
private function get_email_template(): string {
    return get_option('presszone_comments_email_template', 'Default template');
}
```

---

## Common Mistakes to Avoid

| Mistake | Fix |
|---------|-----|
| Missing `declare(strict_types=1)` | Add to every PHP file |
| No `defined('ABSPATH')` check | Add after opening tag |
| Not using `wp_unslash()` before sanitizing `$_POST` | Always unslash first |
| Hardcoded file paths | Use `PRESSZONE_COMMENTS_PATH` constant |
| Hardcoded URLs | Use `PRESSZONE_COMMENTS_URL` constant |
| Direct `$_POST` access | Always sanitize and validate |
| Missing type hints | Use strict types everywhere |
| No error handling | Wrap risky code in try/catch |
| **Variables in `__()`** | **Use static strings with printf** |

---

## WordPress.org Compliance Checklist

- [ ] All SQL uses `$wpdb->prepare()`
- [ ] All output is escaped
- [ ] All nonces verified
- [ ] All input sanitized
- [ ] All permissions checked
- [ ] Text domain in all strings
- [ ] **NO variables in gettext functions** (`__($var, ...)` forbidden)
- [ ] No direct file access allowed
- [ ] 4+ character prefixes everywhere
