envless / sdk
The official PHP SDK for the Envless API. Manage workspaces, products, projects, environments, variables, versions, members, roles, keys and webhooks from PHP, with every value encrypted on your machine.
Requires
- php: ^8.2
- ext-curl: *
- ext-json: *
- ext-openssl: *
Requires (Dev)
None
Suggests
- guzzlehttp/guzzle: Send the SDK's requests through Guzzle, wrapped in Envless\Http\Psr18HttpClient.
- nyholm/psr7: A PSR-17 implementation Envless\Http\Psr18HttpClient can build its requests with.
- php-http/discovery: Finds an installed PSR-17 implementation for Envless\Http\Psr18HttpClient when you do not pass the factories yourself.
Provides
None
Conflicts
None
Replaces
None
README
End-to-end encrypted environment variables for teams. Manage workspaces, projects, environments and secrets from code.
Website • Dashboard • Documentation • Services Status
Intro to the PHP SDK
The official PHP client for the Envless API. It has every method of the TypeScript SDK, all 113 across 13 namespaces, under the same names, and ships a PHPStan and Psalm type for every request body and response. It runs on PHP 8.2 and newer and has no Composer dependencies: requests go through cURL and the encryption is OpenSSL, both standard PHP extensions.
It carries an API key with write access, so it belongs on a server, in a job or in CI.
Installing
composer require envless/sdk
The package needs the curl, json and openssl extensions, which almost every PHP build ships with.
Using
use Envless\Envless; $envless = new Envless(); $passphrase = (string) getenv('ENVLESS_PASSPHRASE'); $workspaceId = $envless->me->get()['workspaceId']; $envless->variables->create('api', 'production', [ 'name' => 'STRIPE_SECRET_KEY', 'value' => Envless::encryptValue('sk_live_51H...', $passphrase, $workspaceId), ]);
new Envless() reads the API key from ENVLESS_TOKEN and the endpoint from ENVLESS_API_ENDPOINT, or takes them as token: and baseUrl:. Envless::init(...) builds a shared client once at startup and Envless::getClient() returns it anywhere else, building one from the environment on first use.
Values are encrypted on your machine, never by the server, so a plaintext value is refused. A request body is an array whose keys keep the API's own names, so defaultValue and projectIds read exactly as they do in the API reference. The options of a call are named arguments: product:, limit:, offset:, search:, order:, status:, reattempt: and timeout:. A response comes back as the decoded array, read with $variable['updatedAt'].
A client of your own
$envless = new Envless(timeout: 10, maxRetries: 2); foreach ($envless->variables->iterate('api', 'production', product: 'billing') as $variable) { echo $variable['name'], ' ', $variable['updatedAt'], PHP_EOL; } $envless->close();
A client is immutable once built. It keeps one connection alive between requests, opens its own after a fork(), and close() releases it. Requests that change data, POST and PATCH, always start on a fresh connection, so a connection that drops mid-request can never make one run twice. A client is not safe to use from two coroutines at the same time, so under Swoole or a similar runtime give each coroutine its own.
Pagination
Every collection has list for one page, listAll for every page at once as an array, and iterate, a Generator that fetches the next page only when it gets there, so a break stops the requests. A page is an Envless\Result\Page with items and pagination, plus hasMore() and totalCount(), and it can be counted and looped over. products->access->list returns an Envless\Result\ProductAccessPage, which also carries isPrivate.
Encryption
$key = Envless::deriveWorkspaceKey($passphrase, $workspaceId); $secrets = []; foreach ($envless->variables->listAll('api', 'production') as $variable) { if (Envless::isCiphertext($variable['value'])) { $secrets[$variable['name']] = Envless::decryptWithKey($key, $variable['value']); } }
encryptValue and decryptValue derive the key on every call, which costs 200,000 rounds of PBKDF2. To work with many values, derive the key once with deriveWorkspaceKey, or load the ENVLESS_KEY your CI holds with importWorkspaceKey, then use encryptWithKey and decryptWithKey. An Envless\Crypto\WorkspaceKey never prints its bytes and refuses to be serialised, and bytes() hands them over when you need them. Plaintext, passphrases and workspace ids must be valid UTF-8: binary data throws Envless\Exception\InvalidArgumentException rather than being changed silently, so encode it first, for example as base64. Ciphertext from this package and from the TypeScript, Python and Ruby SDKs, the CLI and the dashboard is interchangeable.
isCiphertext, passphraseStrength, variableNameValidation and the constants in Envless\Constants work exactly as they do in TypeScript. The two validators return an Envless\Result\Validation with valid and error.
Rotating the passphrase
$result = Envless::rotateWorkspacePassphrase( client: $envless, workspaceUniqueId: $workspaceId, from: $oldPassphrase, to: $newPassphrase, project: 'api', environment: 'production', ); echo $result->variables, ' ', $result->versionEntries, ' ', count($result->skipped), ' ', count($result->failures);
It re-encrypts an environment and its version history under the new passphrase and refuses one that passphraseStrength rejects. Values that are not ciphertext are left alone and listed in skipped, and anything the API refused is listed in failures, so a partial run can be retried.
Errors
use Envless\Envless; use Envless\Exception\ApiException; $envless = new Envless(); try { $envless->variables->get('api', 'production', 'MISSING'); } catch (ApiException $error) { if (!$error->isNotFound()) { throw $error; } error_log($error->errorCode . ' ' . $error->requestId); }
An API refusal throws Envless\Exception\ApiException, with status, errorCode, resource, field, requestId and retryAfterSeconds, plus isAuth(), isScopeMissing(), isValidation(), isNotFound(), isConflict(), isRateLimited() and needsUpgrade(). getCode() returns the HTTP status. No response at all throws Envless\Exception\NetworkException, with isTimeout() when the deadline passed. A value that cannot be decrypted throws Envless\Exception\DecryptionException, and a delivery that fails verification throws Envless\Exception\WebhookVerificationException. All of them implement Envless\Exception\EnvlessException. An unusable variable name is refused before anything is sent, with the same ApiException the API would answer with, and a mistake in the call itself, such as an empty slug, throws Envless\Exception\InvalidArgumentException.
Webhooks
use Envless\Envless; use Envless\Exception\WebhookVerificationException; try { $event = Envless::verifyWebhookSignature( payload: (string) file_get_contents('php://input'), headers: getallheaders(), secret: (string) getenv('ENVLESS_WEBHOOK_SECRET'), ); } catch (WebhookVerificationException) { http_response_code(400); exit; } error_log($event['type']); http_response_code(204);
Pass the raw body exactly as it arrived. headers takes an array in any letter case, $_SERVER, a PSR-7 request, or a Symfony or Laravel header bag such as $request->headers. Deliveries signed more than 300 seconds away from now are refused, and toleranceSeconds: changes that window.
HTTP clients
use Envless\Envless; use Envless\Http\Psr18HttpClient; $envless = new Envless(httpClient: new Psr18HttpClient(new GuzzleHttp\Client()));
Requests go through Envless\Http\CurlHttpClient unless you pass another client. It speaks HTTP/1.1, verifies TLS, takes caBundle:, proxy: and raw curlOptions:, and honours the usual proxy environment variables. Envless\Http\Psr18HttpClient wraps Guzzle or any other PSR-18 client, and finds PSR-17 factories with php-http/discovery or takes them as arguments. Anything implementing Envless\Http\HttpClient works too.
GET, PUT and DELETE requests are retried on 408, 429, 500, 502, 503 and 504, with exponential backoff or the wait Retry-After asks for, capped at 30 seconds. timeout bounds each attempt, redirects and the response body included, and defaults to 30 seconds; 0 turns it off. Redirects are followed the way fetch follows them, and the key is never sent to another origin. The timeout and the redirect rules hold for the bundled cURL client and for Guzzle; another PSR-18 client keeps its own timeout and redirect settings. $envless->raw->request('/path', 'GET', query: [...]) reaches an endpoint no method wraps yet, with the client's key, base URL, timeout and retries. Every request carries User-Agent: envless-sdk-php/<version>.
Types
Every body and response has a type alias on Envless\Types, written as a PHPStan and Psalm array shape, and every method declares the ones it takes and returns. Your own code can name them too:
use Envless\Types; /** * @psalm-import-type VariableResource from Types */ final class SecretsCache { /** @param list<VariableResource> $variables */ public function warm(array $variables): void {} }
When it runs in a terminal, the client checks Packagist once per process for a newer release, without blocking your code, and prints a one line notice. ENVLESS_DISABLE_UPDATE_NOTICE=1 or disableUpdateNotice: true turns that off.
For the full API reference, see the documentation.