# PHP Skill

> **Purpose:** PHP-specific patterns and WordPress PHP API usage
> **When to use:** Any task involving PHP code
> **Related skills:** wordpress-plugin-foundation-skill.md (always applies)

---

## Quick Reference

```php
// Namespace
namespace PresszoneForumPlugin;

// Type hints (PHP 8.0+)
public function getPost(int $postId): ?array
public function updatePost(int $postId, array $data): bool

// Null coalescing
$value = $_POST['field'] ?? 'default';
$value = $array['key'] ?? null;

// Null safe operator (PHP 8.0+)
$name = $user?->display_name ?? 'Guest';

// Array destructuring
[$width, $height] = $dimensions;
['name' => $name, 'email' => $email] = $user;

// Spread operator
$merged = [...$array1, ...$array2];
$result = $this->method(...$args);
```

---

## WordPress PHP APIs

### Post Functions

```php
// Get post
$post = get_post($post_id);
if (!$post || $post->post_status !== 'publish') {
    return null;
}

// Insert post
$post_id = wp_insert_post([
    'post_title' => sanitize_text_field($title),
    'post_content' => wp_kses_post($content),
    'post_status' => 'publish',
    'post_type' => 'post',
    'post_author' => get_current_user_id(),
]);

// Update post
wp_update_post([
    'ID' => $post_id,
    'post_title' => sanitize_text_field($title),
]);

// Delete post (soft delete)
wp_trash_post($post_id);

// Delete post (permanent)
wp_delete_post($post_id, true);
```

### User Functions

```php
// Get user
$user = get_userdata($user_id);
if (!$user) {
    return new WP_Error('user_not_found', 'User not found');
}

// Get current user
$current_user_id = get_current_user_id();
$current_user = wp_get_current_user();

// User meta
update_user_meta($user_id, 'meta_key', $value);
$value = get_user_meta($user_id, 'meta_key', true);
delete_user_meta($user_id, 'meta_key');

// User roles
$user->add_role('role_name');
$user->remove_role('role_name');
$user->set_role('role_name');  // Replaces all roles
```

### Options API

```php
// Get option with default
$value = get_option('option_name', 'default_value');

// Update option
update_option('option_name', $value);

// Delete option
delete_option('option_name');

// Autoload option (loaded on every page)
add_option('option_name', $value, '', 'yes');  // Autoload
add_option('option_name', $value, '', 'no');   // Don't autoload
```

### Transients (Caching)

```php
// Set transient (expires after time)
set_transient('key', $value, HOUR_IN_SECONDS);
set_transient('key', $value, DAY_IN_SECONDS);

// Get transient
$value = get_transient('key');
if ($value === false) {
    // Cache miss - regenerate
    $value = $this->expensiveOperation();
    set_transient('key', $value, HOUR_IN_SECONDS);
}

// Delete transient
delete_transient('key');
```

### WordPress Cache

```php
// Set cache
wp_cache_set('key', $value, 'group', 3600);

// Get cache
$value = wp_cache_get('key', 'group');
if ($value === false) {
    // Cache miss
    $value = $this->getData();
    wp_cache_set('key', $value, 'group', 3600);
}

// Delete cache
wp_cache_delete('key', 'group');

// Flush group
wp_cache_flush();
```

---

## Type Declarations (PHP 8.0+)

### Strict Types

```php
<?php
declare(strict_types=1);  // REQUIRED at top of every file

namespace PresszoneForumPlugin;
```

### Return Types

```php
// Scalar types
public function getId(): int
public function getName(): string
public function getScore(): float
public function isActive(): bool

// Nullable types
public function getUser(int $id): ?array
public function findPost(int $id): ?object

// Array types
public function getPosts(): array
public function getSettings(): array

// Mixed type (PHP 8.0+)
public function getValue(string $key): mixed

// Void (no return)
public function logAction(string $action): void

// Never (always throws or exits)
public function abort(string $message): never
```

### Parameter Types

```php
public function updatePost(
    int $postId,
    string $title,
    ?string $content = null,
    array $meta = [],
    bool $publish = true
): bool {
    // Implementation
}
```

---

## Error Handling

### WP_Error Pattern

```php
// Return WP_Error on failure
public function createPost(array $data): int|WP_Error
{
    if (empty($data['title'])) {
        return new WP_Error(
            'missing_title',
            __('Title is required', 'forum-press-zone'),
            ['status' => 400]
        );
    }
    
    $post_id = wp_insert_post($data);
    
    if (is_wp_error($post_id)) {
        return $post_id;
    }
    
    return $post_id;
}

// Check for errors
$result = $this->createPost($data);
if (is_wp_error($result)) {
    return $result;  // Propagate error
}
```

### Exception Handling

```php
// Use exceptions for validation
public function validatePost(array $data): void
{
    if (empty($data['title'])) {
        throw new \InvalidArgumentException(
            __('Title is required', 'forum-press-zone')
        );
    }
    
    if (strlen($data['title']) > 200) {
        throw new \InvalidArgumentException(
            __('Title is too long', 'forum-press-zone')
        );
    }
}

// Catch exceptions
try {
    $this->validatePost($data);
    $post_id = $this->createPost($data);
} catch (\InvalidArgumentException $e) {
    return new WP_Error('validation_error', $e->getMessage());
}
```

---

## WordPress Hooks

### Actions

```php
// Add action
add_action('init', [$this, 'initialize']);
add_action('wp_enqueue_scripts', [$this, 'enqueueScripts']);

// Action with priority and args
add_action('save_post', [$this, 'onSavePost'], 10, 2);

public function onSavePost(int $post_id, \WP_Post $post): void
{
    // Handle post save
}

// Fire custom action
do_action('presszone_forum_post_created', $post_id, $post_data);
do_action('presszone_forum_user_banned', $user_id, $reason);
```

### Filters

```php
// Add filter
add_filter('the_content', [$this, 'filterContent']);
add_filter('wp_mail', [$this, 'filterEmail'], 10, 1);

// Filter with return
public function filterContent(string $content): string
{
    return $content . '<p>Footer text</p>';
}

// Apply custom filter
$value = apply_filters('presszone_forum_post_content', $content, $post_id);
$title = apply_filters('presszone_forum_thread_title', $title, $thread_id);
```

---

## WordPress Filesystem API

### File Operations

```php
// Get WordPress filesystem
global $wp_filesystem;
if (!function_exists('WP_Filesystem')) {
    require_once ABSPATH . 'wp-admin/includes/file.php';
}
WP_Filesystem();

// Write file
$wp_filesystem->put_contents(
    $file_path,
    $content,
    FS_CHMOD_FILE
);

// Read file
$content = $wp_filesystem->get_contents($file_path);

// Check if file exists
if ($wp_filesystem->exists($file_path)) {
    // File exists
}

// Delete file
$wp_filesystem->delete($file_path);

// Create directory
$wp_filesystem->mkdir($dir_path, FS_CHMOD_DIR);
```

### Upload Directory

```php
// Get upload directory info
$upload_dir = wp_upload_dir();
$base_dir = $upload_dir['basedir'];  // /path/to/wp-content/uploads
$base_url = $upload_dir['baseurl'];  // https://site.com/wp-content/uploads

// Plugin-specific upload directory
$plugin_dir = $base_dir . '/presszone-forum';
if (!file_exists($plugin_dir)) {
    wp_mkdir_p($plugin_dir);
}
```

---

## Date & Time

### Current Time

```php
// Current time (site timezone)
$time = current_time('mysql');        // 2025-01-15 14:30:00
$timestamp = current_time('timestamp'); // Unix timestamp

// Current time (UTC)
$time_utc = current_time('mysql', true);

// Format date
$formatted = date_i18n('F j, Y', $timestamp);  // January 15, 2025
```

### Date Calculations

```php
// Add time
$expires = strtotime('+1 week');
$expires = strtotime('+30 days');
$expires = time() + HOUR_IN_SECONDS;
$expires = time() + DAY_IN_SECONDS;

// Time constants
MINUTE_IN_SECONDS  // 60
HOUR_IN_SECONDS    // 3600
DAY_IN_SECONDS     // 86400
WEEK_IN_SECONDS    // 604800
MONTH_IN_SECONDS   // 2592000
YEAR_IN_SECONDS    // 31536000
```

---

## WordPress Query

### WP_Query

```php
$query = new WP_Query([
    'post_type' => 'post',
    'posts_per_page' => 10,
    'post_status' => 'publish',
    'orderby' => 'date',
    'order' => 'DESC',
]);

if ($query->have_posts()) {
    while ($query->have_posts()) {
        $query->the_post();
        // Use template tags: the_title(), the_content(), etc.
    }
    wp_reset_postdata();
}
```

### WP_User_Query

```php
$user_query = new WP_User_Query([
    'role' => 'subscriber',
    'number' => 50,
    'orderby' => 'registered',
    'order' => 'DESC',
]);

$users = $user_query->get_results();
foreach ($users as $user) {
    echo esc_html($user->display_name);
}
```

---

## Email

### wp_mail

```php
// Simple email
wp_mail(
    $to,
    $subject,
    $message
);

// Email with headers
$headers = [
    'Content-Type: text/html; charset=UTF-8',
    'From: ' . get_bloginfo('name') . ' <' . get_option('admin_email') . '>',
];

wp_mail(
    $to,
    $subject,
    $message,
    $headers
);

// Email with attachments
wp_mail(
    $to,
    $subject,
    $message,
    $headers,
    [$attachment_path]
);
```

---

## Localization

### Translation Functions

```php
// Simple translation
__('Text', 'forum-press-zone')

// Translation with echo
_e('Text', 'forum-press-zone')

// Translation with escaping
esc_html__('Text', 'forum-press-zone')
esc_attr__('Text', 'forum-press-zone')
esc_html_e('Text', 'forum-press-zone')
esc_attr_e('Text', 'forum-press-zone')

// Plural forms
_n('1 item', '%d items', $count, 'forum-press-zone')
sprintf(_n('1 item', '%d items', $count, 'forum-press-zone'), $count)

// Context (same text, different meaning)
_x('Post', 'noun', 'forum-press-zone')
_x('Post', 'verb', 'forum-press-zone')
```

---

## WordPress Constants

### Useful Constants

```php
ABSPATH                    // /path/to/wordpress/
WP_CONTENT_DIR            // /path/to/wp-content
WP_PLUGIN_DIR             // /path/to/wp-content/plugins
WP_CONTENT_URL            // https://site.com/wp-content
WP_PLUGIN_URL             // https://site.com/wp-content/plugins

// Plugin-specific
plugin_dir_path(__FILE__) // /path/to/plugin/
plugin_dir_url(__FILE__)  // https://site.com/wp-content/plugins/plugin-name/
plugin_basename(__FILE__) // plugin-name/plugin-file.php
```

---

## Common Patterns

### Singleton Pattern

```php
class MyClass
{
    private static ?self $instance = null;
    
    private function __construct()
    {
        // Private constructor
    }
    
    public static function getInstance(): self
    {
        if (self::$instance === null) {
            self::$instance = new self();
        }
        return self::$instance;
    }
    
    // Prevent cloning
    private function __clone() {}
    
    // Prevent unserialization
    public function __wakeup()
    {
        throw new \Exception('Cannot unserialize singleton');
    }
}
```

### Dependency Injection

```php
class PostCreator
{
    private Query $query;
    private Validator $validator;
    
    public function __construct(Query $query, Validator $validator)
    {
        $this->query = $query;
        $this->validator = $validator;
    }
    
    public function create(array $data): int
    {
        $this->validator->validate($data);
        return $this->query->insert($data);
    }
}

// Usage
$creator = new PostCreator(
    new Query($wpdb),
    new Validator()
);
```

---

## Performance Optimization

### Object Caching

```php
public function getPost(int $postId): ?array
{
    // Try cache first
    $cache_key = "post_{$postId}";
    $post = wp_cache_get($cache_key, 'presszone_forum_posts');
    
    if ($post !== false) {
        return $post;
    }
    
    // Cache miss - query database
    $post = $this->queryPost($postId);
    
    // Cache for 1 hour
    wp_cache_set($cache_key, $post, 'presszone_forum_posts', 3600);
    
    return $post;
}
```

### Batch Operations

```php
// Process in batches to avoid memory issues
public function processAllPosts(): void
{
    $batch_size = 100;
    $offset = 0;
    
    do {
        $posts = $this->getPosts($batch_size, $offset);
        
        foreach ($posts as $post) {
            $this->processPost($post);
        }
        
        $offset += $batch_size;
        
        // Free memory
        wp_cache_flush();
        
    } while (count($posts) === $batch_size);
}
```

---

## Common Mistakes

| Mistake | Fix |
|---------|-----|
| Missing `declare(strict_types=1)` | Add at top of every PHP file |
| Not using type hints | Add parameter and return types |
| Using `file_put_contents()` | Use WordPress Filesystem API |
| Using `date()` instead of `current_time()` | Use `current_time()` for site timezone |
| Not checking `is_wp_error()` | Always check WP function returns |
| Using `$_SERVER['DOCUMENT_ROOT']` | Use `ABSPATH` constant |
| Hardcoding paths | Use `plugin_dir_path()`, `WP_CONTENT_DIR` |
| Not escaping in translation | Use `esc_html__()`, `esc_attr__()` |
| Missing text domain | Always use `'forum-press-zone'` |
| Using `echo` in functions | Return values, let caller echo |
| Not using `wp_reset_postdata()` | Always reset after `WP_Query` loop |
| Modifying global `$post` | Use local variables |

---

## Integration with Other Skills

- **wordpress-plugin-foundation-skill.md** - Security and compliance (always applies)
- **sql-skill.md** - Database queries with `$wpdb`
- **rest-api-skill.md** - REST endpoint callbacks
- **ajax-skill.md** - AJAX action handlers
