---
title: "PHP"
description: "A moderation helper for PHP 8.1 or later with cURL, and no dependency."
canonical_url: "https://www.openshield.dev/docs/integration-guides/php"
---
# PHP

> A moderation helper for PHP 8.1 or later with cURL, and no dependency.

This guide needs PHP 8.1 or later and the cURL extension, which ships with most PHP installations.

## Set the key

Put the key in the environment of your server, never in the code. For PHP-FPM, set it in the pool's `env[OPENSHIELD_API_KEY]` or in your framework's secret store, and read it with `getenv()`.

## The helper

```php [moderation.php]
<?php

declare(strict_types=1);

const OPENMOD_BASE = 'https://api.openshield.dev';

final class ModerationError extends RuntimeException
{
    public function __construct(public readonly int $status, public readonly string $type, string $message, public readonly ?int $retryAfter = null)
    {
        parent::__construct($message, $status);
    }
}

function openmod_send(string $path, string $body, array $headers = []): array
{
    $key = getenv('OPENSHIELD_API_KEY');
    if ($key === false || $key === '') {
        throw new RuntimeException('OPENSHIELD_API_KEY is not set');
    }

    $curl = curl_init(OPENMOD_BASE . $path);
    curl_setopt_array($curl, [
        CURLOPT_POST => true,
        CURLOPT_POSTFIELDS => $body,
        CURLOPT_HTTPHEADER => array_merge(['Authorization: Bearer ' . $key], $headers),
        CURLOPT_RETURNTRANSFER => true,
        CURLOPT_HEADER => false,
        CURLOPT_TIMEOUT => 10,
    ]);
    $raw = curl_exec($curl);
    $status = (int) curl_getinfo($curl, CURLINFO_RESPONSE_CODE);

    if ($raw === false) {
        throw new RuntimeException('openshield could not be reached: ' . curl_error($curl));
    }

    $answer = json_decode($raw, true, 512, JSON_THROW_ON_ERROR);
    if ($status !== 200) {
        $error = $answer['error'] ?? [];
        throw new ModerationError($status, $error['type'] ?? 'unknown', $error['message'] ?? 'The check failed');
    }

    return [
        'action' => $answer['summary']['action'],
        'reasons' => array_merge($answer['summary']['reject_reason'], $answer['summary']['review_reason']),
        'request_id' => $answer['request_id'],
    ];
}

function openmod_check_text(string $text, ?string $ref = null): array
{
    return openmod_send('/v1/text', json_encode(['text' => $text, 'ref' => $ref], JSON_THROW_ON_ERROR), ['Content-Type: application/json']);
}

function openmod_check_image(string $bytes, ?string $ref = null): array
{
    return openmod_send('/v1/image' . ($ref === null ? '' : '?ref=' . rawurlencode($ref)), $bytes);
}
```

## Use it, and fail closed

```php
<?php

require 'moderation.php';

function moderate_comment(string $text, string $commentId): string
{
    try {
        $decision = openmod_check_text($text, $commentId);
    } catch (ModerationError $error) {
        // The content cannot be accepted as it is: refuse it.
        return in_array($error->status, [400, 413, 415], true) ? 'refuse' : 'hold';
    } catch (Throwable) {
        // A timeout or a network error: keep it hidden until someone looks at it.
        return 'hold';
    }

    return match ($decision['action']) {
        'accept' => 'publish',
        'review' => 'hold',
        default => 'refuse',
    };
}
```

For rejected content, show your user a message chosen from the `id` of the first reason, in your own words and your own languages. [Best practices](https://www.openshield.dev/docs/best-practices) says more.


## Sitemap

See the full [sitemap](https://www.openshield.dev/sitemap.md) for all pages.
