totten/ca-config
totten/ca-config provides a small PHP API for reading, modifying, and writing configuration files, with helpers for environment-specific settings. It suits CiviCRM-style PHP applications that need safe, programmatic config updates.
Install via Composer (there are no runtime dependencies beyond PHP itself, and the package ships no certificates — it only discovers what the host already provides):
composer require totten/ca-config
The first thing to do is read the source. The whole library is essentially one class (src/CaConfig.php, namespace Civi\CaConfig) plus a handful of platform probes. Skim it end-to-end before copying any snippet, because a 1-star, zero-dependent package has no blog posts to fall back on and its API surface is intentionally tiny.
Your first use case will almost certainly be: "PHP can't find a CA bundle, so HTTPS calls fail with cURL error 60: unable to get local issuer certificate." Rather than hardcoding /etc/ssl/certs/ca-certificates.crt (wrong on macOS and Windows), ask the library to resolve it:
use Civi\CaConfig\CaConfig;
$ca = CaConfig::getDefault();
if ($ca && $ca->getCafile()) {
$client = new \GuzzleHttp\Client([
'verify' => $ca->getCafile(),
]);
}
For raw cURL, feed the same path into CURLOPT_CAINFO; for stream contexts, into ssl.cafile. Any other HTTP abstraction wants the same resolved path plumbed through.
A one-liner to prove it works on your machine before wiring anything up:
php -r 'require "vendor/autoload.php"; var_dump(\Civi\CaConfig\CaConfig::getDefault());'
If that returns null on a machine with working TLS, the heuristics didn't recognize your layout — that's your signal to go read the probe code and understand which paths it checks.
Resolve once, inject everywhere. The lookup touches the filesystem and environment; calling CaConfig::getDefault() on every outbound request is wasteful and makes tests host-dependent. Bind it once:
// Laravel: a service provider or config binding
$this->app->singleton(CaConfig::class, fn () => CaConfig::getDefault());
Wrap it behind your own adapter. Given the maintenance risk (single maintainer, no ecosystem validation), don't let application code call CaConfig directly. A thin interface — CaBundleResolver::path(): ?string — lets you swap in composer/ca-bundle or an internal fork without touching call sites:
final class CaBundle
{
public static function path(): ?string
{
return \Civi\CaConfig\CaConfig::getDefault()?->getCafile();
}
}
Explicit override beats discovery. Corporate MITM proxies and FIPS-certified trust stores need a pinned bundle. Support an env var and short-circuit the library:
$caPath = config('services.tls.ca_bundle')
?? env('CA_BUNDLE')
?? \Civi\CaConfig\CaConfig::getDefault()?->getCafile();
Wire it into Guzzle as a shared client factory. Every Guzzle call in your app should go through one place:
function http(): \GuzzleHttp\Client
{
$path = CaBundle::path();
return new \GuzzleHttp\Client([
// Never fall back to verify => false.
'verify' => $path ?? true,
]);
}
Laravel Http facade. The facade resolves its own Guzzle client, so a shared factory isn't enough. Either macro the facade at boot or set the underlying client:
// In a service provider's boot()
Http::macro('withCa', function () {
return Http::withOptions(['verify' => CaBundle::path() ?? true]);
});
Alternatively, configure guzzle defaults in config/services.php and read them from your factory.
Health-check / boot log. Resolve the CA config at boot and log the discovered path (or a warning if null). This converts "why is HTTPS broken in this container?" into a one-line grep:
Log::info('TLS CA bundle resolved', ['path' => CaBundle::path()]);
Test with a fixture. Inject the resolved path into your client factory; in tests, pass a fixture PEM bundle so CI doesn't depend on /etc/ssl/certs being populated (which it often isn't in slim php:8.x-cli images).
CiviCRM-adjacent code. This originates in the CiviCRM ecosystem; if you're working alongside CiviCRM, prefer this package over custom detection so behavior stays consistent with other CiviCRM calls.
getDefault(), but instance accessors (cafile, capath, and any verification-intent accessors) should be confirmed in src/CaConfig.php. Low-traffic library — no guarantee the class has a stable, documented surface beyond the entry point.ca-certificates upgrade) won't be picked up until the process restarts. Cache the object and re-resolve periodically if you need to observe rotations; otherwise document the restart requirement.'verify' => $path ?? false. If discovery returns null, fail closed (use true and let Guzzle/PHP defaults apply, or throw with an actionable message) rather than silently downgrading to insecure.ssl.cafile / ssl.capath), cURL (CURLOPT_CAINFO / CURLOPT_CAPATH), and Guzzle (verify) treat the same path differently — particularly around directories vs. files and on Windows. If you use more than one transport, test each.php:*-alpine, php:*-slim, and distroless bases ship without ca-certificates. The library can only discover what's on disk; if nothing is there, the real fix is apk add ca-certificates / apt-get install ca-certificates in your Dockerfile, not a library change.openssl.cafile and curl.cainfo set in php.ini (or via -d on the CLI) are a supported override path but may be ignored by your library's own probing order. If you rely on them, confirm the library respects them — and don't set both to different files./etc/ssl/certs can be unreadable to a non-root process user in a hardened container. Test with the real runtime user, not root.composer/ca-bundle first. In default Laravel, Guzzle already resolves a bundle via composer/ca-bundle, which is present in essentially every Composer project. Compare paths in shadow mode before replacing it — you may be re-implementing what's already there. Adopt this package only if its heuristics (INI probing, specific distro paths) genuinely surface a bundle that composer/ca-bundle misses on your install base.null or an unusable path. Log which source was selected (env var, INI, discovered path, fallback) so a TLS ticket can be triaged from logs alone.How can I help you explore Laravel packages today?