# PHP

> Learn how to integrate OpenTelemetry with Core PHP applications and send telemetry data to Last9

Source: https://last9.io/docs/integrations/languages/php/

This guide shows you how to instrument your Core PHP application with OpenTelemetry and send traces, metrics, and logs to Last9.

## Prerequisites

- PHP 8.1 or later
- Composer (for dependency management)
- Last9 account with OTLP endpoint configured

## Installation

1. **Install OpenTelemetry PHP extension**

   Install the PHP extension following the [OpenTelemetry PHP setup guide](https://opentelemetry.io/docs/zero-code/php/#install-the-opentelemetry-extension):

   ```bash
   # For Ubuntu/Debian
   sudo apt-get install php-dev
   pecl install opentelemetry

   # For macOS with Homebrew
   brew install php
   pecl install opentelemetry

   # Add to php.ini
   echo "extension=opentelemetry" >> $(php --ini | grep "Loaded Configuration" | sed -e "s|.*:\s*||")
   ```

2. **Create Composer project and install dependencies**

   ```bash
   # Initialize composer if not already done
   composer init

   # Install OpenTelemetry SDK and instrumentation packages
   composer require \
     open-telemetry/sdk \
     open-telemetry/exporter-otlp \
     open-telemetry/opentelemetry-auto-psr18 \
     open-telemetry/opentelemetry-auto-pdo \
     php-http/guzzle7-adapter \
     monolog/monolog
   ```

3. **Install additional HTTP client libraries (optional)**

   ```bash
   # For making HTTP requests
   composer require guzzlehttp/guzzle

   # For Redis support
   composer require predis/predis
   ```

## Configuration

### Environment Variables

**.env**

Create a `.env` file in your project root:

```bash
# OpenTelemetry Configuration
OTEL_PHP_AUTOLOAD_ENABLED=true
OTEL_SERVICE_NAME=core-php-app
OTEL_EXPORTER_OTLP_ENDPOINT=$last9_otlp_endpoint
OTEL_EXPORTER_OTLP_HEADERS="Authorization=$last9_otlp_auth_header"
OTEL_TRACES_EXPORTER=otlp
OTEL_METRICS_EXPORTER=otlp
OTEL_LOGS_EXPORTER=otlp
OTEL_PROPAGATORS=baggage,tracecontext
OTEL_TRACES_SAMPLER=always_on
OTEL_RESOURCE_ATTRIBUTES="deployment.environment=production,service.version=1.0.0"
OTEL_LOG_LEVEL=error

# Application Configuration
APP_ENV=production
APP_DEBUG=false

# Database
DB_HOST=localhost
DB_PORT=3306
DB_NAME=php_app_db
DB_USER=user
DB_PASSWORD=password

# Redis
REDIS_HOST=localhost
REDIS_PORT=6379
```

**config/config.php**

Create `config/config.php`:

```php
<?php

// Load environment variables
function loadEnv($path) {
    if (!file_exists($path)) {
        return;
    }

    $lines = file($path, FILE_IGNORE_NEW_LINES | FILE_SKIP_EMPTY_LINES);
    foreach ($lines as $line) {
        if (strpos($line, '#') === 0) continue;

        [$name, $value] = explode('=', $line, 2);
        $name = trim($name);
        $value = trim($value, '"\'');

        if (!array_key_exists($name, $_ENV)) {
            $_ENV[$name] = $value;
        }
    }
}

loadEnv(__DIR__ . '/../.env');

return [
    'app' => [
        'name' => $_ENV['OTEL_SERVICE_NAME'] ?? 'core-php-app',
        'env' => $_ENV['APP_ENV'] ?? 'production',
        'debug' => filter_var($_ENV['APP_DEBUG'] ?? false, FILTER_VALIDATE_BOOLEAN),
    ],

    'otel' => [
        'enabled' => filter_var($_ENV['OTEL_PHP_AUTOLOAD_ENABLED'] ?? true, FILTER_VALIDATE_BOOLEAN),
        'service_name' => $_ENV['OTEL_SERVICE_NAME'] ?? 'core-php-app',
        'endpoint' => $_ENV['OTEL_EXPORTER_OTLP_ENDPOINT'] ?? '',
        'headers' => $_ENV['OTEL_EXPORTER_OTLP_HEADERS'] ?? '',
        'resource_attributes' => [
            'service.name' => $_ENV['OTEL_SERVICE_NAME'] ?? 'core-php-app',
            'service.version' => '1.0.0',
            'deployment.environment' => $_ENV['APP_ENV'] ?? 'production',
            'framework.name' => 'core-php',
            'php.version' => PHP_VERSION,
        ],
    ],

    'database' => [
        'host' => $_ENV['DB_HOST'] ?? 'localhost',
        'port' => $_ENV['DB_PORT'] ?? 3306,
        'dbname' => $_ENV['DB_NAME'] ?? 'php_app_db',
        'username' => $_ENV['DB_USER'] ?? 'user',
        'password' => $_ENV['DB_PASSWORD'] ?? 'password',
        'charset' => 'utf8mb4',
    ],

    'redis' => [
        'host' => $_ENV['REDIS_HOST'] ?? 'localhost',
        'port' => $_ENV['REDIS_PORT'] ?? 6379,
        'timeout' => 2.5,
    ],
];
```

## OpenTelemetry Bootstrap

**src/OpenTelemetry/Bootstrap.php**

```php
<?php

namespace App\OpenTelemetry;

use OpenTelemetry\API\Globals;
use OpenTelemetry\API\Instrumentation\CachedInstrumentation;
use OpenTelemetry\API\Trace\TracerInterface;
use OpenTelemetry\Contrib\Otlp\OtlpHttpTransportFactory;
use OpenTelemetry\Contrib\Otlp\SpanExporter;
use OpenTelemetry\SDK\Common\Attribute\Attributes;
use OpenTelemetry\SDK\Common\Export\Stream\StreamTransportFactory;
use OpenTelemetry\SDK\Resource\ResourceInfo;
use OpenTelemetry\SDK\Resource\ResourceInfoFactory;
use OpenTelemetry\SDK\Sdk;
use OpenTelemetry\SDK\Trace\Sampler\AlwaysOnSampler;
use OpenTelemetry\SDK\Trace\Sampler\ParentBased;
use OpenTelemetry\SDK\Trace\SpanProcessor\BatchSpanProcessor;
use OpenTelemetry\SDK\Trace\TracerProvider;
use OpenTelemetry\SemConv\ResourceAttributes;

class Bootstrap
{
    private static ?TracerProvider $tracerProvider = null;
    private static ?TracerInterface $tracer = null;
    private static bool $initialized = false;

    public static function initialize(array $config): void
    {
        if (self::$initialized) {
            return;
        }

        if (!$config['otel']['enabled']) {
            return;
        }

        $resource = self::createResource($config['otel']['resource_attributes']);
        $tracerProvider = self::createTracerProvider($resource, $config);

        $sdk = Sdk::builder()
            ->setTracerProvider($tracerProvider)
            ->setAutoShutdown(true)
            ->build();

        self::$tracerProvider = $tracerProvider;
        self::$tracer = $tracerProvider->getTracer($config['otel']['service_name'], '1.0.0');
        self::$initialized = true;

        // Register shutdown handler
        register_shutdown_function([self::class, 'shutdown']);
    }

    public static function getTracer(): ?TracerInterface
    {
        return self::$tracer;
    }

    public static function getInstrumentation(string $name, string $version = '1.0.0'): CachedInstrumentation
    {
        return new CachedInstrumentation($name, $version);
    }

    public static function shutdown(): void
    {
        if (self::$tracerProvider) {
            self::$tracerProvider->shutdown();
        }
    }

    private static function createResource(array $attributes): ResourceInfo
    {
        return ResourceInfoFactory::create(
            Attributes::create($attributes),
            ResourceAttributes::SCHEMA_URL
        );
    }

    private static function createTracerProvider(ResourceInfo $resource, array $config): TracerProvider
    {
        $exporter = self::createExporter($config['otel']);

        $spanProcessor = new BatchSpanProcessor(
            $exporter,
            Globals::clockInterface(),
            512, // maxExportBatchSize
            5000, // scheduleDelayMillis
            30000, // exportTimeoutMillis
            2048 // maxQueueSize
        );

        return TracerProvider::builder()
            ->addSpanProcessor($spanProcessor)
            ->setResource($resource)
            ->setSampler(new ParentBased(new AlwaysOnSampler()))
            ->build();
    }

    private static function createExporter(array $config): SpanExporter
    {
        $endpoint = $config['endpoint'];
        $headers = [];

        if ($authHeader = $config['headers']) {
            if (str_starts_with($authHeader, 'Authorization=')) {
                $headers['Authorization'] = substr($authHeader, 14);
            }
        }

        if ($endpoint) {
            $transport = (new OtlpHttpTransportFactory())->create(
                $endpoint . '/v1/traces',
                'application/json',
                $headers
            );

            return new SpanExporter($transport);
        }

        // Fallback to console exporter for development
        return new SpanExporter(
            (new StreamTransportFactory())->create(STDOUT, 'application/json')
        );
    }
}
```

**src/TelemetryService.php**

```php
<?php

namespace App;

use App\OpenTelemetry\Bootstrap;
use OpenTelemetry\API\Trace\Span;
use OpenTelemetry\API\Trace\SpanKind;
use OpenTelemetry\API\Trace\StatusCode;
use OpenTelemetry\API\Trace\TracerInterface;

class TelemetryService
{
    private TracerInterface $tracer;

    public function __construct()
    {
        $this->tracer = Bootstrap::getTracer() ?? throw new \RuntimeException('OpenTelemetry not initialized');
    }

    public function traceOperation(string $operationName, callable $operation, array $attributes = []): mixed
    {
        $span = $this->tracer->spanBuilder($operationName)
            ->setSpanKind(SpanKind::KIND_INTERNAL)
            ->setAttributes($attributes)
            ->startSpan();

        $scope = $span->activate();

        try {
            $result = $operation($span);
            $span->setStatus(StatusCode::STATUS_OK);
            return $result;

        } catch (\Throwable $e) {
            $span->recordException($e);
            $span->setStatus(StatusCode::STATUS_ERROR, $e->getMessage());
            throw $e;

        } finally {
            $span->end();
            $scope->detach();
        }
    }

    public function traceHttpRequest(string $method, string $uri, callable $handler): mixed
    {
        $spanName = "{$method} {$uri}";

        $span = $this->tracer->spanBuilder($spanName)
            ->setSpanKind(SpanKind::KIND_SERVER)
            ->setAttributes([
                'http.request.method' => $method,
                'url.full' => $this->getCurrentUrl(),
                'url.path' => $uri,
                'user_agent.original' => $_SERVER['HTTP_USER_AGENT'] ?? '',
                'network.peer.address' => $this->getClientIp(),
                'http.request.body.size' => $_SERVER['CONTENT_LENGTH'] ?? 0,
            ])
            ->startSpan();

        $scope = $span->activate();
        $startTime = hrtime(true);

        try {
            $result = $handler($span);

            // If result has HTTP status code, use it
            $statusCode = 200;
            if (is_array($result) && isset($result['status_code'])) {
                $statusCode = $result['status_code'];
            }

            $duration = (hrtime(true) - $startTime) / 1_000_000; // Convert to milliseconds

            $span->setAttributes([
                'http.response.status_code' => $statusCode,
                'http.request.duration_ms' => $duration,
            ]);

            if ($statusCode >= 400) {
                $span->setStatus(StatusCode::STATUS_ERROR, "HTTP {$statusCode}");
            } else {
                $span->setStatus(StatusCode::STATUS_OK);
            }

            return $result;

        } catch (\Throwable $e) {
            $span->recordException($e);
            $span->setStatus(StatusCode::STATUS_ERROR, $e->getMessage());
            throw $e;

        } finally {
            $span->end();
            $scope->detach();
        }
    }

    public function getCurrentSpan(): ?Span
    {
        return Span::getCurrent();
    }

    public function addAttributes(array $attributes): void
    {
        $span = $this->getCurrentSpan();
        if ($span) {
            $span->setAttributes($attributes);
        }
    }

    public function addEvent(string $name, array $attributes = []): void
    {
        $span = $this->getCurrentSpan();
        if ($span) {
            $span->addEvent($name, $attributes);
        }
    }

    public function recordException(\Throwable $exception): void
    {
        $span = $this->getCurrentSpan();
        if ($span) {
            $span->recordException($exception);
        }
    }

    private function getCurrentUrl(): string
    {
        $protocol = isset($_SERVER['HTTPS']) && $_SERVER['HTTPS'] !== 'off' ? 'https' : 'http';
        $host = $_SERVER['HTTP_HOST'] ?? $_SERVER['SERVER_NAME'] ?? 'localhost';
        $uri = $_SERVER['REQUEST_URI'] ?? '/';

        return "{$protocol}://{$host}{$uri}";
    }

    private function getClientIp(): string
    {
        // Check for various headers
        $headers = [
            'HTTP_CF_CONNECTING_IP',     // Cloudflare
            'HTTP_CLIENT_IP',
            'HTTP_X_FORWARDED_FOR',
            'HTTP_X_FORWARDED',
            'HTTP_X_CLUSTER_CLIENT_IP',
            'HTTP_FORWARDED_FOR',
            'HTTP_FORWARDED',
            'REMOTE_ADDR'
        ];

        foreach ($headers as $header) {
            if (!empty($_SERVER[$header])) {
                $ips = explode(',', $_SERVER[$header]);
                return trim($ips[0]);
            }
        }

        return $_SERVER['REMOTE_ADDR'] ?? '127.0.0.1';
    }
}
```

## Database Integration

**src/Database/Connection.php**

```php
<?php

namespace App\Database;

use App\TelemetryService;
use OpenTelemetry\API\Trace\Span;
use PDO;
use PDOStatement;

class Connection
{
    private PDO $pdo;
    private TelemetryService $telemetry;

    public function __construct(array $config, TelemetryService $telemetry)
    {
        $this->telemetry = $telemetry;

        $dsn = "mysql:host={$config['host']};port={$config['port']};dbname={$config['dbname']};charset={$config['charset']}";

        $this->pdo = new PDO($dsn, $config['username'], $config['password'], [
            PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION,
            PDO::ATTR_DEFAULT_FETCH_MODE => PDO::FETCH_ASSOC,
            PDO::ATTR_EMULATE_PREPARES => false,
        ]);
    }

    public function query(string $sql, array $params = []): array
    {
        return $this->telemetry->traceOperation('database.query', function (Span $span) use ($sql, $params) {
            $span->setAttributes([
                'db.system' => 'mysql',
                'db.statement' => $this->sanitizeSql($sql),
                'db.operation.name' => $this->getOperationType($sql),
                'db.params.count' => count($params),
            ]);

            $stmt = $this->pdo->prepare($sql);

            $span->addEvent('query.prepared');

            $executeResult = $stmt->execute($params);

            if ($executeResult) {
                $results = $stmt->fetchAll();

                $span->setAttributes([
                    'db.rows_affected' => $stmt->rowCount(),
                    'db.results.count' => count($results),
                ]);

                $span->addEvent('query.executed', [
                    'rows_returned' => count($results),
                ]);

                return $results;
            }

            throw new \RuntimeException('Query execution failed');
        });
    }

    public function execute(string $sql, array $params = []): int
    {
        return $this->telemetry->traceOperation('database.execute', function (Span $span) use ($sql, $params) {
            $span->setAttributes([
                'db.system' => 'mysql',
                'db.statement' => $this->sanitizeSql($sql),
                'db.operation.name' => $this->getOperationType($sql),
                'db.params.count' => count($params),
            ]);

            $stmt = $this->pdo->prepare($sql);

            $span->addEvent('statement.prepared');

            $executeResult = $stmt->execute($params);

            if ($executeResult) {
                $rowCount = $stmt->rowCount();

                $span->setAttributes([
                    'db.rows_affected' => $rowCount,
                ]);

                $span->addEvent('statement.executed', [
                    'rows_affected' => $rowCount,
                ]);

                return $rowCount;
            }

            throw new \RuntimeException('Statement execution failed');
        });
    }

    public function lastInsertId(): string
    {
        return $this->pdo->lastInsertId();
    }

    public function beginTransaction(): bool
    {
        $this->telemetry->addEvent('transaction.begin');
        return $this->pdo->beginTransaction();
    }

    public function commit(): bool
    {
        $this->telemetry->addEvent('transaction.commit');
        return $this->pdo->commit();
    }

    public function rollback(): bool
    {
        $this->telemetry->addEvent('transaction.rollback');
        return $this->pdo->rollback();
    }

    private function sanitizeSql(string $sql): string
    {
        // Remove sensitive data and normalize for better span grouping
        $sql = preg_replace('/\s+/', ' ', $sql);
        $sql = preg_replace('/\'[^\']*\'/', '?', $sql);
        $sql = preg_replace('/\d+/', '?', $sql);
        return trim($sql);
    }

    private function getOperationType(string $sql): string
    {
        $sql = trim(strtoupper($sql));

        if (str_starts_with($sql, 'SELECT')) return 'SELECT';
        if (str_starts_with($sql, 'INSERT')) return 'INSERT';
        if (str_starts_with($sql, 'UPDATE')) return 'UPDATE';
        if (str_starts_with($sql, 'DELETE')) return 'DELETE';
        if (str_starts_with($sql, 'CREATE')) return 'CREATE';
        if (str_starts_with($sql, 'DROP')) return 'DROP';
        if (str_starts_with($sql, 'ALTER')) return 'ALTER';

        return 'UNKNOWN';
    }
}
```

**src/Models/User.php**

```php
<?php

namespace App\Models;

use App\Database\Connection;
use App\TelemetryService;

class User
{
    private Connection $db;
    private TelemetryService $telemetry;

    public function __construct(Connection $db, TelemetryService $telemetry)
    {
        $this->db = $db;
        $this->telemetry = $telemetry;
    }

    public function findAll(int $limit = 10, int $offset = 0): array
    {
        return $this->telemetry->traceOperation('user.find_all', function (Span $span) use ($limit, $offset) {
            $span->setAttributes([
                'model' => 'User',
                'operation' => 'findAll',
                'limit' => $limit,
                'offset' => $offset,
            ]);

            $sql = "SELECT id, name, email, created_at, updated_at FROM users ORDER BY id DESC LIMIT ? OFFSET ?";
            $users = $this->db->query($sql, [$limit, $offset]);

            $span->setAttributes([
                'users.count' => count($users),
            ]);

            return $users;
        });
    }

    public function findById(int $id): ?array
    {
        return $this->telemetry->traceOperation('user.find_by_id', function (Span $span) use ($id) {
            $span->setAttributes([
                'model' => 'User',
                'operation' => 'findById',
                'user.id' => $id,
            ]);

            $sql = "SELECT id, name, email, created_at, updated_at FROM users WHERE id = ?";
            $users = $this->db->query($sql, [$id]);

            $found = !empty($users);
            $span->setAttributes(['user.found' => $found]);

            return $found ? $users[0] : null;
        });
    }

    public function create(array $data): array
    {
        return $this->telemetry->traceOperation('user.create', function (Span $span) use ($data) {
            $span->setAttributes([
                'model' => 'User',
                'operation' => 'create',
                'user.email' => $data['email'] ?? 'unknown',
            ]);

            $this->db->beginTransaction();

            try {
                $sql = "INSERT INTO users (name, email, password, created_at, updated_at) VALUES (?, ?, ?, NOW(), NOW())";
                $params = [
                    $data['name'],
                    $data['email'],
                    isset($data['password']) ? password_hash($data['password'], PASSWORD_DEFAULT) : null,
                ];

                $this->db->execute($sql, $params);
                $userId = $this->db->lastInsertId();

                $this->db->commit();

                $span->setAttributes([
                    'user.created' => true,
                    'user.id' => $userId,
                ]);

                // Fetch the created user
                return $this->findById((int) $userId);

            } catch (\Exception $e) {
                $this->db->rollback();
                throw $e;
            }
        });
    }

    public function update(int $id, array $data): ?array
    {
        return $this->telemetry->traceOperation('user.update', function (Span $span) use ($id, $data) {
            $span->setAttributes([
                'model' => 'User',
                'operation' => 'update',
                'user.id' => $id,
                'fields_to_update' => array_keys($data),
            ]);

            $this->db->beginTransaction();

            try {
                $setParts = [];
                $params = [];

                if (isset($data['name'])) {
                    $setParts[] = 'name = ?';
                    $params[] = $data['name'];
                }

                if (isset($data['email'])) {
                    $setParts[] = 'email = ?';
                    $params[] = $data['email'];
                }

                if (isset($data['password'])) {
                    $setParts[] = 'password = ?';
                    $params[] = password_hash($data['password'], PASSWORD_DEFAULT);
                }

                if (empty($setParts)) {
                    $span->setAttributes(['user.updated' => false, 'reason' => 'no_fields_to_update']);
                    return $this->findById($id);
                }

                $setParts[] = 'updated_at = NOW()';
                $params[] = $id;

                $sql = "UPDATE users SET " . implode(', ', $setParts) . " WHERE id = ?";
                $rowsAffected = $this->db->execute($sql, $params);

                $this->db->commit();

                $span->setAttributes([
                    'user.updated' => $rowsAffected > 0,
                    'rows_affected' => $rowsAffected,
                ]);

                return $rowsAffected > 0 ? $this->findById($id) : null;

            } catch (\Exception $e) {
                $this->db->rollback();
                throw $e;
            }
        });
    }

    public function delete(int $id): bool
    {
        return $this->telemetry->traceOperation('user.delete', function (Span $span) use ($id) {
            $span->setAttributes([
                'model' => 'User',
                'operation' => 'delete',
                'user.id' => $id,
            ]);

            $sql = "DELETE FROM users WHERE id = ?";
            $rowsAffected = $this->db->execute($sql, [$id]);

            $deleted = $rowsAffected > 0;
            $span->setAttributes([
                'user.deleted' => $deleted,
                'rows_affected' => $rowsAffected,
            ]);

            return $deleted;
        });
    }

    public function count(): int
    {
        return $this->telemetry->traceOperation('user.count', function (Span $span) {
            $span->setAttributes([
                'model' => 'User',
                'operation' => 'count',
            ]);

            $result = $this->db->query("SELECT COUNT(*) as count FROM users");
            $count = (int) $result[0]['count'];

            $span->setAttributes(['users.total_count' => $count]);

            return $count;
        });
    }
}
```

## HTTP Handlers and Routing

**src/Http/Router.php**

```php
<?php

namespace App\Http;

use App\TelemetryService;

class Router
{
    private array $routes = [];
    private TelemetryService $telemetry;

    public function __construct(TelemetryService $telemetry)
    {
        $this->telemetry = $telemetry;
    }

    public function get(string $path, callable $handler): void
    {
        $this->addRoute('GET', $path, $handler);
    }

    public function post(string $path, callable $handler): void
    {
        $this->addRoute('POST', $path, $handler);
    }

    public function put(string $path, callable $handler): void
    {
        $this->addRoute('PUT', $path, $handler);
    }

    public function delete(string $path, callable $handler): void
    {
        $this->addRoute('DELETE', $path, $handler);
    }

    private function addRoute(string $method, string $path, callable $handler): void
    {
        $this->routes[] = [
            'method' => $method,
            'path' => $path,
            'handler' => $handler,
            'pattern' => $this->pathToPattern($path),
        ];
    }

    public function dispatch(): void
    {
        $method = $_SERVER['REQUEST_METHOD'];
        $uri = parse_url($_SERVER['REQUEST_URI'], PHP_URL_PATH);

        foreach ($this->routes as $route) {
            if ($route['method'] === $method && preg_match($route['pattern'], $uri, $matches)) {
                array_shift($matches); // Remove full match

                $this->telemetry->traceHttpRequest($method, $route['path'], function () use ($route, $matches) {
                    try {
                        $result = call_user_func($route['handler'], ...$matches);

                        if (is_array($result)) {
                            $this->sendJsonResponse($result);
                        } elseif (is_string($result)) {
                            $this->sendTextResponse($result);
                        }

                        return $result;

                    } catch (\Exception $e) {
                        $this->sendErrorResponse($e->getMessage(), 500);
                        throw $e;
                    }
                });

                return;
            }
        }

        $this->telemetry->traceHttpRequest($method, $uri, function () use ($uri) {
            $this->sendErrorResponse("Route not found: {$uri}", 404);
            return ['status_code' => 404];
        });
    }

    private function pathToPattern(string $path): string
    {
        // Convert {id} style parameters to regex groups
        $pattern = preg_replace('/\{(\w+)\}/', '([^/]+)', $path);
        return '#^' . $pattern . '$#';
    }

    private function sendJsonResponse(array $data, int $statusCode = 200): void
    {
        http_response_code($statusCode);
        header('Content-Type: application/json');
        echo json_encode($data);
    }

    private function sendTextResponse(string $text, int $statusCode = 200): void
    {
        http_response_code($statusCode);
        header('Content-Type: text/plain');
        echo $text;
    }

    private function sendErrorResponse(string $message, int $statusCode = 500): void
    {
        http_response_code($statusCode);
        header('Content-Type: application/json');
        echo json_encode(['error' => $message, 'status' => $statusCode]);
    }
}
```

**src/Http/Controllers/UserController.php**

```php
<?php

namespace App\Http\Controllers;

use App\Models\User;
use App\TelemetryService;

class UserController
{
    private User $userModel;
    private TelemetryService $telemetry;

    public function __construct(User $userModel, TelemetryService $telemetry)
    {
        $this->userModel = $userModel;
        $this->telemetry = $telemetry;
    }

    public function index(): array
    {
        $this->telemetry->addAttributes([
            'controller' => 'UserController',
            'action' => 'index',
        ]);

        $page = (int) ($_GET['page'] ?? 1);
        $perPage = (int) ($_GET['per_page'] ?? 10);
        $offset = ($page - 1) * $perPage;

        $this->telemetry->addAttributes([
            'pagination.page' => $page,
            'pagination.per_page' => $perPage,
        ]);

        $users = $this->userModel->findAll($perPage, $offset);
        $totalUsers = $this->userModel->count();

        $this->telemetry->addAttributes([
            'users.count' => count($users),
            'users.total' => $totalUsers,
        ]);

        return [
            'data' => $users,
            'pagination' => [
                'page' => $page,
                'per_page' => $perPage,
                'total' => $totalUsers,
                'pages' => ceil($totalUsers / $perPage),
            ],
        ];
    }

    public function show(string $id): array
    {
        $userId = (int) $id;

        $this->telemetry->addAttributes([
            'controller' => 'UserController',
            'action' => 'show',
            'user.id' => $userId,
        ]);

        $user = $this->userModel->findById($userId);

        if (!$user) {
            http_response_code(404);
            return ['error' => 'User not found', 'status' => 404];
        }

        $this->telemetry->addAttributes([
            'user.found' => true,
            'user.email' => $user['email'],
        ]);

        return ['data' => $user];
    }

    public function create(): array
    {
        $this->telemetry->addAttributes([
            'controller' => 'UserController',
            'action' => 'create',
        ]);

        // Parse JSON input
        $input = json_decode(file_get_contents('php://input'), true);

        if (!$input) {
            http_response_code(400);
            return ['error' => 'Invalid JSON input', 'status' => 400];
        }

        // Validate required fields
        $requiredFields = ['name', 'email'];
        $missingFields = [];

        foreach ($requiredFields as $field) {
            if (!isset($input[$field]) || empty($input[$field])) {
                $missingFields[] = $field;
            }
        }

        if (!empty($missingFields)) {
            $this->telemetry->addAttributes([
                'validation.failed' => true,
                'validation.missing_fields' => $missingFields,
            ]);

            http_response_code(422);
            return [
                'error' => 'Validation failed',
                'missing_fields' => $missingFields,
                'status' => 422,
            ];
        }

        if (isset($input['email']) && !filter_var($input['email'], FILTER_VALIDATE_EMAIL)) {
            http_response_code(422);
            return ['error' => 'Invalid email format', 'status' => 422];
        }

        $this->telemetry->addAttributes(['user.email' => $input['email']]);

        try {
            $user = $this->userModel->create($input);

            $this->telemetry->addAttributes([
                'user.created' => true,
                'user.id' => $user['id'],
            ]);

            http_response_code(201);
            return ['data' => $user, 'message' => 'User created successfully'];

        } catch (\Exception $e) {
            $this->telemetry->recordException($e);

            if (str_contains($e->getMessage(), 'Duplicate entry')) {
                http_response_code(409);
                return ['error' => 'Email already exists', 'status' => 409];
            }

            http_response_code(500);
            return ['error' => 'Failed to create user', 'status' => 500];
        }
    }

    public function update(string $id): array
    {
        $userId = (int) $id;

        $this->telemetry->addAttributes([
            'controller' => 'UserController',
            'action' => 'update',
            'user.id' => $userId,
        ]);

        // Parse JSON input
        $input = json_decode(file_get_contents('php://input'), true);

        if (!$input) {
            http_response_code(400);
            return ['error' => 'Invalid JSON input', 'status' => 400];
        }

        // Validate email if provided
        if (isset($input['email']) && !filter_var($input['email'], FILTER_VALIDATE_EMAIL)) {
            http_response_code(422);
            return ['error' => 'Invalid email format', 'status' => 422];
        }

        $this->telemetry->addAttributes(['fields_to_update' => array_keys($input)]);

        try {
            $user = $this->userModel->update($userId, $input);

            if (!$user) {
                http_response_code(404);
                return ['error' => 'User not found', 'status' => 404];
            }

            $this->telemetry->addAttributes([
                'user.updated' => true,
                'user.email' => $user['email'],
            ]);

            return ['data' => $user, 'message' => 'User updated successfully'];

        } catch (\Exception $e) {
            $this->telemetry->recordException($e);

            if (str_contains($e->getMessage(), 'Duplicate entry')) {
                http_response_code(409);
                return ['error' => 'Email already exists', 'status' => 409];
            }

            http_response_code(500);
            return ['error' => 'Failed to update user', 'status' => 500];
        }
    }

    public function delete(string $id): array
    {
        $userId = (int) $id;

        $this->telemetry->addAttributes([
            'controller' => 'UserController',
            'action' => 'delete',
            'user.id' => $userId,
        ]);

        $deleted = $this->userModel->delete($userId);

        if (!$deleted) {
            http_response_code(404);
            return ['error' => 'User not found', 'status' => 404];
        }

        $this->telemetry->addAttributes(['user.deleted' => true]);

        return ['message' => 'User deleted successfully'];
    }
}
```

## Application Entry Point

**public/index.php**

```php
<?php

// Include autoloader
require_once __DIR__ . '/../vendor/autoload.php';

use App\Database\Connection;
use App\Http\Controllers\UserController;
use App\Http\Router;
use App\Models\User;
use App\OpenTelemetry\Bootstrap;
use App\TelemetryService;

// Load configuration
$config = require __DIR__ . '/../config/config.php';

// Initialize OpenTelemetry
Bootstrap::initialize($config);

// Initialize services
$telemetry = new TelemetryService();
$database = new Connection($config['database'], $telemetry);

// Initialize models
$userModel = new User($database, $telemetry);

// Initialize controllers
$userController = new UserController($userModel, $telemetry);

// Initialize router
$router = new Router($telemetry);

// Define routes
$router->get('/', function () {
    return [
        'message' => 'Welcome to Core PHP API',
        'version' => '1.0.0',
        'timestamp' => date('c'),
    ];
});

$router->get('/health', function () {
    return [
        'status' => 'healthy',
        'timestamp' => date('c'),
        'memory_usage' => memory_get_usage(true),
        'peak_memory' => memory_get_peak_usage(true),
    ];
});

// User routes
$router->get('/api/users', [$userController, 'index']);
$router->post('/api/users', [$userController, 'create']);
$router->get('/api/users/{id}', [$userController, 'show']);
$router->put('/api/users/{id}', [$userController, 'update']);
$router->delete('/api/users/{id}', [$userController, 'delete']);

// Stats endpoint
$router->get('/api/stats', function () use ($userModel) {
    return [
        'users_count' => $userModel->count(),
        'memory_usage' => memory_get_usage(true),
        'peak_memory' => memory_get_peak_usage(true),
        'php_version' => PHP_VERSION,
    ];
});

// Dispatch the request
$router->dispatch();
```

**database/schema.sql**

```sql
-- Create database schema
CREATE TABLE IF NOT EXISTS users (
    id INT AUTO_INCREMENT PRIMARY KEY,
    name VARCHAR(255) NOT NULL,
    email VARCHAR(255) NOT NULL UNIQUE,
    password VARCHAR(255),
    created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
    updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,

    INDEX idx_email (email),
    INDEX idx_created_at (created_at)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;

-- Insert sample data
INSERT INTO users (name, email, password) VALUES
    ('John Doe', 'john@example.com', '$2y$10$92IXUNpkjO0rOQ5byMi.Ye4oKoEa3Ro9llC/.og/at2.uheWG/igi'),
    ('Jane Smith', 'jane@example.com', '$2y$10$92IXUNpkjO0rOQ5byMi.Ye4oKoEa3Ro9llC/.og/at2.uheWG/igi'),
    ('Bob Johnson', 'bob@example.com', '$2y$10$92IXUNpkjO0rOQ5byMi.Ye4oKoEa3Ro9llC/.og/at2.uheWG/igi')
ON DUPLICATE KEY UPDATE name=name;
```

## Production Deployment

### Docker Configuration

**Dockerfile**

```dockerfile
FROM php:8.2-fpm-alpine

# Install system dependencies
RUN apk add --no-cache \
    curl \
    git \
    icu-dev \
    libzip-dev \
    oniguruma-dev \
    autoconf \
    g++ \
    make \
    mysql-client

# Install PHP extensions
RUN docker-php-ext-install \
    pdo_mysql \
    mbstring \
    zip \
    intl \
    opcache

# Install OpenTelemetry extension
RUN pecl install opentelemetry && docker-php-ext-enable opentelemetry

# Install Composer
COPY --from=composer:latest /usr/bin/composer /usr/bin/composer

# Set working directory
WORKDIR /var/www/html

# Copy composer files
COPY composer.json composer.lock ./
RUN composer install --no-dev --optimize-autoloader

# Copy application files
COPY . .

# Set permissions
RUN chown -R www-data:www-data /var/www/html

# Copy PHP configuration
COPY docker/php/php.ini /usr/local/etc/php/php.ini
COPY docker/php/opcache.ini /usr/local/etc/php/conf.d/opcache.ini

USER www-data

EXPOSE 9000

CMD ["php-fpm"]
```

**docker-compose.yml**

```yaml
version: "3.8"

services:
  app:
    build: .
    container_name: core-php-app
    environment:
      # OpenTelemetry
      - OTEL_PHP_AUTOLOAD_ENABLED=true
      - OTEL_SERVICE_NAME=core-php-app
      - OTEL_EXPORTER_OTLP_ENDPOINT=$last9_otlp_endpoint
      - OTEL_EXPORTER_OTLP_HEADERS=Authorization=$last9_otlp_auth_header
      - OTEL_TRACES_EXPORTER=otlp
      - OTEL_RESOURCE_ATTRIBUTES=deployment.environment=production,service.version=1.0.0

      # Application
      - APP_ENV=production
      - APP_DEBUG=false
      - DB_HOST=mysql
      - DB_NAME=php_app_db
      - DB_USER=php_user
      - DB_PASSWORD=php_password
    depends_on:
      - mysql
      - redis

  nginx:
    image: nginx:alpine
    container_name: core-php-nginx
    ports:
      - "80:80"
    volumes:
      - .:/var/www/html
      - ./docker/nginx/default.conf:/etc/nginx/conf.d/default.conf
    depends_on:
      - app

  mysql:
    image: mysql:8.0
    container_name: core-php-mysql
    environment:
      MYSQL_ROOT_PASSWORD: root_password
      MYSQL_DATABASE: php_app_db
      MYSQL_USER: php_user
      MYSQL_PASSWORD: php_password
    ports:
      - "3306:3306"
    volumes:
      - mysql_data:/var/lib/mysql
      - ./database/schema.sql:/docker-entrypoint-initdb.d/schema.sql

  redis:
    image: redis:7-alpine
    container_name: core-php-redis
    volumes:
      - redis_data:/data

volumes:
  mysql_data:
  redis_data:
```

**docker/nginx/default.conf**

```nginx
server {
    listen 80;
    index index.php index.html;
    error_log  /var/log/nginx/error.log;
    access_log /var/log/nginx/access.log;
    root /var/www/html/public;

    location ~ \.php$ {
        try_files $uri =404;
        fastcgi_split_path_info ^(.+\.php)(/.+)$;
        fastcgi_pass app:9000;
        fastcgi_index index.php;
        include fastcgi_params;
        fastcgi_param SCRIPT_FILENAME $document_root$fastcgi_script_name;
        fastcgi_param PATH_INFO $fastcgi_path_info;
    }

    location / {
        try_files $uri $uri/ /index.php?$query_string;
        gzip_static on;
    }

    # Health check
    location /health {
        try_files $uri /index.php?$query_string;
        access_log off;
    }
}
```

## Testing the Integration

1. **Start your application**

   ```bash
   # Local development with PHP built-in server
   php -S localhost:8000 -t public

   # Or with Docker
   docker-compose up
   ```

2. **Test the endpoints**

   ```bash
   # Health check
   curl http://localhost:8000/health

   # API endpoints
   curl http://localhost:8000/api/users
   curl http://localhost:8000/api/users/1

   # Create a user
   curl -X POST http://localhost:8000/api/users \
     -H "Content-Type: application/json" \
     -d '{"name": "Test User", "email": "test@example.com", "password": "password123"}'

   # Update a user
   curl -X PUT http://localhost:8000/api/users/1 \
     -H "Content-Type: application/json" \
     -d '{"name": "Updated User"}'

   # Delete a user
   curl -X DELETE http://localhost:8000/api/users/1

   # Get stats
   curl http://localhost:8000/api/stats
   ```

3. **View telemetry in Last9**

   Check your Last9 dashboard for:

   - HTTP request traces with Core PHP specific attributes
   - Database operation spans with PDO instrumentation
   - Custom business logic spans and events
   - Error tracking and exception recording
