PHP SDK
The PHP SDK (cachix/secretspec) is a thin client over the same Rust resolver
every other SecretSpec SDK uses, so it inherits every provider, chain, profile,
and generator with no PHP-side logic. It reaches the resolver through one of two
native backends over an identical JSON contract:
- The
secretspecPHP extension (built with ext-php-rs) embeds the resolver the waypdoorredisdo. It needs noffi.enableand works under PHP-FPM and the web SAPI out of the box — the recommended path for Laravel and Symfony. ext-ffidlopens thesecretspec-ffishared library at runtime. Nothing to compile, ideal for CLI tools and local development; requires the FFI extension enabled.
The SDK prefers the extension whenever it is loaded and transparently falls back
to ext-ffi, so your application code is the same either way.
Install
Section titled “Install”composer require cachix/secretspecThat installs the pure-PHP client. Then provide the native resolver with one of the backends below.
Option A — the PHP extension (recommended for web / FPM)
Section titled “Option A — the PHP extension (recommended for web / FPM)”The secretspec-php-native extension embeds the resolver, so it works under
PHP-FPM with no ffi.enable and nothing to locate at runtime — the same
operational model as ext-redis or ext-imagick (the binary is provisioned at
the image/host level, not by Composer). Install it one of three ways:
-
Prebuilt binary — download the
secretspec-php-native-<php>-nts-<target>shared object for your PHP version and platform from the GitHub release, then enable it inphp.ini:extension=/path/to/secretspec-php-native.soIn an official PHP Docker image, drop it into the extension dir and
docker-php-ext-enable secretspec-php-native. -
Build from source (needs the Rust toolchain,
php-config, and libclang):Terminal window cargo build --release -p secretspec-php-native# then point extension= at target/release/libsecretspec_php_native.so
Once loaded, php -m lists secretspec-php-native and the SDK uses it
automatically.
Option B — ext-ffi (quick start / CLI)
Section titled “Option B — ext-ffi (quick start / CLI)”The FFI backend dlopens the secretspec-ffi library at runtime. Enable the
bundled FFI extension — in CLI it is on by default; for the web SAPI set:
extension=ffiffi.enable=trueThen fetch the native library for your platform (a one-time step; Composer does not run it automatically):
vendor/bin/secretspec-install-libThat downloads the right secretspec-ffi library from the matching GitHub
release into the package. Alternatively, point SECRETSPEC_FFI_LIB at a library
you built or placed yourself. The SDK looks at SECRETSPEC_FFI_LIB first, then
the downloaded copy, then a local Cargo target/ directory.
Quick start
Section titled “Quick start”<?php
use Secretspec\SecretSpec;
$resolved = SecretSpec::builder() ->withProvider('keyring://') ->withProfile('production') ->withReason('boot web app') ->load();
echo $resolved->provider, ' ', $resolved->profile, PHP_EOL;
$db = $resolved->secrets['DATABASE_URL'];echo $db->get(); // the value, or the file path for as_path secrets
$resolved->setAsEnv(); // export everything into getenv()/$_ENV/$_SERVERA missing required secret throws Secretspec\MissingRequiredException (with a
->missing list); any other failure throws Secretspec\SecretSpecException
(with a stable ->kind).
There is also a one-shot form using named arguments:
$resolved = SecretSpec::resolve(provider: 'keyring://', reason: 'boot');Laravel
Section titled “Laravel”Resolve your secrets early and export them so Laravel’s env() and config see
them. A service provider is a natural home:
<?php
namespace App\Providers;
use Illuminate\Support\ServiceProvider;use Secretspec\SecretSpec;
class SecretSpecServiceProvider extends ServiceProvider{ public function register(): void { SecretSpec::builder() ->withProfile(app()->environment()) // "production", "local", ... ->withReason('laravel boot') ->load() ->setAsEnv(); }}Register it first in bootstrap/providers.php so the secrets are present before
other providers read configuration. Because setAsEnv() also populates $_ENV
and $_SERVER, the env() helper and any config/*.php that calls env(...)
resolve normally.
If you run
php artisan config:cache, configuration is frozen at cache time andenv()is not read per request. Either resolve before caching, or bind theResolvedinto the container and read secrets from it directly where you need them.
Symfony
Section titled “Symfony”Export the secrets in the front controller and bin/console, before the kernel
boots, so %env(DATABASE_URL)% in your config resolves:
// public/index.php (and bin/console)use Secretspec\SecretSpec;
require_once dirname(__DIR__).'/vendor/autoload.php';
SecretSpec::builder() ->withProfile($_SERVER['APP_ENV'] ?? 'dev') ->withReason('symfony boot') ->load() ->setAsEnv();setAsEnv() sets $_ENV, $_SERVER, and putenv(), all three of which
Symfony’s env-var processors read, so no bundle or extra configuration is needed.
Plain PHP
Section titled “Plain PHP”Point the builder at a specific manifest and provider and read the values back:
use Secretspec\SecretSpec;
$resolved = SecretSpec::builder() ->withPath(__DIR__.'/secretspec.toml') ->withProvider('dotenv://.env.production') ->withReason('cron job') ->load();
foreach ($resolved->secrets as $name => $secret) { // $secret->get() is the value, or a readable file path for as_path secrets. printf("%s=%s\n", $name, $secret->get());}Typed access (codegen)
Section titled “Typed access (codegen)”Generate a typed class with secretspec schema plus
quicktype, then build it from $resolved->fields():
secretspec schema | quicktype -s schema --top-level SecretSpec --lang php -o SecretSpecTyped.php// $resolved->fields() is a [SECRET_NAME => value] map; quicktype's `from`// wants an object, so cast it.$typed = SecretSpec::from((object) $resolved->fields());echo $typed->getDatabaseURL();Files (as_path)
Section titled “Files (as_path)”Secrets declared as_path are materialized to a temporary file and come back as
a readable path; $secret->get() returns the path. The SDK persists the file
(mode 0400) so the path stays valid after load() returns — you own its
lifetime. Call $resolved->close() when done to remove those temp files:
$resolved = SecretSpec::builder()->withReason('tls')->load();try { $certPath = $resolved->secrets['TLS_CERT']->get(); // ... use the file ...} finally { $resolved->close();}Native backends
Section titled “Native backends”The SDK chooses a backend automatically: if the secretspec-php-native extension
is loaded it is used directly (no ffi.enable, no library to locate); otherwise
the SDK dlopens the secretspec-ffi library via ext-ffi, looking first at
SECRETSPEC_FFI_LIB, then the copy vendor/bin/secretspec-install-lib places in
the package, then a local Cargo target/ directory. Both backends call the
identical Rust resolve_json, so the result is the same — a cross-language
conformance suite asserts it.