diff --git a/sdk/php/.gitignore b/sdk/php/.gitignore
new file mode 100644
index 0000000000..762e67278e
--- /dev/null
+++ b/sdk/php/.gitignore
@@ -0,0 +1,4 @@
+/vendor/
+composer.lock
+.phpunit.cache/
+
diff --git a/sdk/php/composer.json b/sdk/php/composer.json
new file mode 100644
index 0000000000..9c4aa164a0
--- /dev/null
+++ b/sdk/php/composer.json
@@ -0,0 +1,22 @@
+{
+ "name": "sst/sdk",
+ "description": "PHP SDK for SST",
+ "type": "library",
+ "license": "MIT",
+ "autoload": {
+ "psr-4": {
+ "Sst\\Sdk\\": "src/"
+ }
+ },
+ "autoload-dev": {
+ "psr-4": {
+ "Sst\\Sdk\\Tests\\": "tests/"
+ }
+ },
+ "require-dev": {
+ "phpunit/phpunit": "^12.4"
+ },
+ "scripts": {
+ "test": "phpunit"
+ }
+}
diff --git a/sdk/php/phpunit.xml b/sdk/php/phpunit.xml
new file mode 100644
index 0000000000..3bdd603607
--- /dev/null
+++ b/sdk/php/phpunit.xml
@@ -0,0 +1,20 @@
+
+
+
+
+ tests
+
+
+
+
+ src
+
+
+
+
diff --git a/sdk/php/src/Resource.php b/sdk/php/src/Resource.php
new file mode 100644
index 0000000000..cefa99d6c2
--- /dev/null
+++ b/sdk/php/src/Resource.php
@@ -0,0 +1,129 @@
+ $value) {
+ if (strpos($key, 'SST_RESOURCE_') === 0) {
+ $resourceName = substr($key, strlen('SST_RESOURCE_'));
+ if (!isset(self::$resources[$resourceName])) {
+ $decoded = json_decode($value, true);
+ if ($decoded === null && json_last_error() !== JSON_ERROR_NONE) {
+ continue;
+ }
+ self::$resources[$resourceName] = $decoded;
+ }
+ }
+ }
+ }
+
+ public static function get(string $name, string ...$path) {
+ if (self::$resources === null) {
+ self::init();
+ }
+
+ if (!isset(self::$resources[$name])) {
+ if (!isset(self::$resources['App'])) {
+ throw new \RuntimeException(
+ 'It does not look like SST links are active. If this is in local development and you are not starting this process through the multiplexer, wrap your command with `sst dev -- `'
+ );
+ }
+
+ $msg = "\"$name\" is not linked in your sst.config.ts";
+ if (getenv('AWS_LAMBDA_FUNCTION_NAME') !== false) {
+ $msg .= ' to ' . getenv('AWS_LAMBDA_FUNCTION_NAME');
+ }
+
+ throw new \RuntimeException($msg);
+ }
+
+ if (empty($path)) {
+ return self::$resources[$name];
+ }
+
+ return self::getByPath(self::$resources[$name], $path);
+ }
+
+ public static function all(): array {
+ if (self::$resources === null) {
+ self::init();
+ }
+
+ return self::$resources;
+ }
+
+ private static function getByPath($input, array $path) {
+ if (empty($path)) {
+ return $input;
+ }
+
+ if (!is_array($input)) {
+ throw new \RuntimeException('Resource path not found');
+ }
+
+ $key = array_shift($path);
+ if (!isset($input[$key])) {
+ throw new \RuntimeException('Resource path not found');
+ }
+
+ return self::getByPath($input[$key], $path);
+ }
+}
diff --git a/sdk/php/tests/ResourceTest.php b/sdk/php/tests/ResourceTest.php
new file mode 100644
index 0000000000..292d405e2f
--- /dev/null
+++ b/sdk/php/tests/ResourceTest.php
@@ -0,0 +1,335 @@
+getProperty('resources');
+ $property->setAccessible(true);
+ $property->setValue(null, null);
+ }
+
+ protected function tearDown(): void
+ {
+ parent::tearDown();
+ // Clean up environment variables
+ putenv('SST_RESOURCE_App');
+ putenv('SST_RESOURCE_MyBucket');
+ putenv('SST_RESOURCE_MyTable');
+ putenv('SST_KEY');
+ putenv('SST_KEY_FILE');
+ putenv('AWS_LAMBDA_FUNCTION_NAME');
+
+ // Also clean $_ENV
+ unset($_ENV['SST_RESOURCE_App']);
+ unset($_ENV['SST_RESOURCE_MyBucket']);
+ unset($_ENV['SST_RESOURCE_MyTable']);
+ unset($_ENV['SST_KEY']);
+ unset($_ENV['SST_KEY_FILE']);
+ unset($_ENV['AWS_LAMBDA_FUNCTION_NAME']);
+ }
+
+ public function testGetResourceFromEnvironmentVariable(): void
+ {
+ $_ENV['SST_RESOURCE_App'] = json_encode(['name' => 'my-app', 'stage' => 'dev']);
+ $_ENV['SST_RESOURCE_MyBucket'] = json_encode(['name' => 'my-bucket', 'type' => 'sst.aws.Bucket']);
+
+ $app = Resource::get('App');
+ $this->assertEquals(['name' => 'my-app', 'stage' => 'dev'], $app);
+
+ $bucket = Resource::get('MyBucket');
+ $this->assertEquals(['name' => 'my-bucket', 'type' => 'sst.aws.Bucket'], $bucket);
+ }
+
+ public function testGetResourceFromGetenv(): void
+ {
+ putenv('SST_RESOURCE_App=' . json_encode(['name' => 'test-app', 'stage' => 'prod']));
+ putenv('SST_RESOURCE_MyTable=' . json_encode(['name' => 'test-table', 'type' => 'sst.aws.Dynamo']));
+
+ $app = Resource::get('App');
+ $this->assertEquals(['name' => 'test-app', 'stage' => 'prod'], $app);
+
+ $table = Resource::get('MyTable');
+ $this->assertEquals(['name' => 'test-table', 'type' => 'sst.aws.Dynamo'], $table);
+ }
+
+ public function testThrowsErrorWhenResourceNotFound(): void
+ {
+ $_ENV['SST_RESOURCE_App'] = json_encode(['name' => 'my-app', 'stage' => 'dev']);
+
+ $this->expectException(\RuntimeException::class);
+ $this->expectExceptionMessage('"NonExistentResource" is not linked in your sst.config.ts');
+
+ Resource::get('NonExistentResource');
+ }
+
+ public function testThrowsErrorWithLambdaFunctionName(): void
+ {
+ $_ENV['SST_RESOURCE_App'] = json_encode(['name' => 'my-app', 'stage' => 'dev']);
+ putenv('AWS_LAMBDA_FUNCTION_NAME=my-function');
+
+ $this->expectException(\RuntimeException::class);
+ $this->expectExceptionMessage('"MissingResource" is not linked in your sst.config.ts to my-function');
+
+ Resource::get('MissingResource');
+ }
+
+ public function testThrowsErrorWhenSSTLinksNotActive(): void
+ {
+ // Don't set SST_RESOURCE_App
+ $_ENV['SST_RESOURCE_MyBucket'] = json_encode(['name' => 'bucket']);
+
+ $this->expectException(\RuntimeException::class);
+ $this->expectExceptionMessage('It does not look like SST links are active');
+
+ Resource::get('SomeResource');
+ }
+
+ public function testDecryptFromKeyFile(): void
+ {
+ // Create a temporary encrypted file for testing
+ // This uses the same encryption as the other SDKs
+ $key = random_bytes(32);
+ $nonce = str_repeat("\0", 12);
+ $data = json_encode([
+ 'App' => ['name' => 'encrypted-app', 'stage' => 'test'],
+ 'SecretBucket' => ['name' => 'secret-bucket', 'encrypted' => true]
+ ]);
+
+ $ciphertext = openssl_encrypt(
+ $data,
+ 'aes-256-gcm',
+ $key,
+ OPENSSL_RAW_DATA,
+ $nonce,
+ $tag
+ );
+
+ $encryptedData = $ciphertext . $tag;
+
+ // Write to temporary file
+ $tempFile = tempnam(sys_get_temp_dir(), 'sst_test_');
+ file_put_contents($tempFile, $encryptedData);
+
+ // Set environment variables
+ putenv('SST_KEY=' . base64_encode($key));
+ putenv('SST_KEY_FILE=' . $tempFile);
+
+ try {
+ $app = Resource::get('App');
+ $this->assertEquals(['name' => 'encrypted-app', 'stage' => 'test'], $app);
+
+ $bucket = Resource::get('SecretBucket');
+ $this->assertEquals(['name' => 'secret-bucket', 'encrypted' => true], $bucket);
+ } finally {
+ // Clean up
+ unlink($tempFile);
+ }
+ }
+
+ public function testEncryptedFileWithEnvironmentVariableOverride(): void
+ {
+ // Create encrypted file with one resource
+ $key = random_bytes(32);
+ $nonce = str_repeat("\0", 12);
+ $data = json_encode([
+ 'App' => ['name' => 'encrypted-app', 'stage' => 'test'],
+ 'EncryptedResource' => ['source' => 'encrypted']
+ ]);
+
+ $ciphertext = openssl_encrypt(
+ $data,
+ 'aes-256-gcm',
+ $key,
+ OPENSSL_RAW_DATA,
+ $nonce,
+ $tag
+ );
+
+ $encryptedData = $ciphertext . $tag;
+ $tempFile = tempnam(sys_get_temp_dir(), 'sst_test_');
+ file_put_contents($tempFile, $encryptedData);
+
+ putenv('SST_KEY=' . base64_encode($key));
+ putenv('SST_KEY_FILE=' . $tempFile);
+
+ // Add an environment variable resource
+ $_ENV['SST_RESOURCE_EnvResource'] = json_encode(['source' => 'environment']);
+
+ try {
+ $encrypted = Resource::get('EncryptedResource');
+ $this->assertEquals(['source' => 'encrypted'], $encrypted);
+
+ $env = Resource::get('EnvResource');
+ $this->assertEquals(['source' => 'environment'], $env);
+ } finally {
+ unlink($tempFile);
+ }
+ }
+
+ public function testFallbackToEnvWhenDecryptionFails(): void
+ {
+ // Set invalid key file
+ $tempFile = tempnam(sys_get_temp_dir(), 'sst_test_');
+ file_put_contents($tempFile, 'invalid encrypted data');
+
+ putenv('SST_KEY=' . base64_encode(random_bytes(32)));
+ putenv('SST_KEY_FILE=' . $tempFile);
+
+ // Should fall back to environment variables
+ $_ENV['SST_RESOURCE_App'] = json_encode(['name' => 'fallback-app', 'stage' => 'dev']);
+ $_ENV['SST_RESOURCE_MyBucket'] = json_encode(['name' => 'env-bucket']);
+
+ try {
+ $app = Resource::get('App');
+ $this->assertEquals(['name' => 'fallback-app', 'stage' => 'dev'], $app);
+
+ $bucket = Resource::get('MyBucket');
+ $this->assertEquals(['name' => 'env-bucket'], $bucket);
+ } finally {
+ unlink($tempFile);
+ }
+ }
+
+ public function testLazyInitialization(): void
+ {
+ // Verify resources are only loaded on first access
+ $reflection = new \ReflectionClass(Resource::class);
+ $property = $reflection->getProperty('resources');
+ $property->setAccessible(true);
+
+ // Before any access, resources should be null
+ $this->assertNull($property->getValue());
+
+ // Set an environment variable
+ $_ENV['SST_RESOURCE_App'] = json_encode(['name' => 'test', 'stage' => 'dev']);
+
+ // After first access, resources should be initialized
+ Resource::get('App');
+ $this->assertIsArray($property->getValue());
+ }
+
+ public function testInvalidJsonInEnvironmentVariableIsSkipped(): void
+ {
+ $_ENV['SST_RESOURCE_App'] = json_encode(['name' => 'my-app', 'stage' => 'dev']);
+ $_ENV['SST_RESOURCE_InvalidJson'] = 'not-valid-json{';
+ $_ENV['SST_RESOURCE_ValidResource'] = json_encode(['valid' => true]);
+
+ // Should not throw, just skip the invalid JSON
+ $app = Resource::get('App');
+ $this->assertEquals(['name' => 'my-app', 'stage' => 'dev'], $app);
+
+ $valid = Resource::get('ValidResource');
+ $this->assertEquals(['valid' => true], $valid);
+
+ // InvalidJson should not be accessible
+ $this->expectException(\RuntimeException::class);
+ Resource::get('InvalidJson');
+ }
+
+ public function testEmptyEnvironmentVariablesAreHandled(): void
+ {
+ putenv('SST_KEY=');
+ putenv('SST_KEY_FILE=');
+ $_ENV['SST_RESOURCE_App'] = json_encode(['name' => 'test', 'stage' => 'dev']);
+
+ // Should still work with just environment variables
+ $app = Resource::get('App');
+ $this->assertEquals(['name' => 'test', 'stage' => 'dev'], $app);
+ }
+
+ public function testGetResourceWithPath(): void
+ {
+ $_ENV['SST_RESOURCE_App'] = json_encode(['name' => 'my-app', 'stage' => 'dev']);
+ $_ENV['SST_RESOURCE_MyBucket'] = json_encode(['name' => 'my-bucket', 'type' => 'sst.aws.Bucket']);
+
+ // Get nested property directly
+ $bucketName = Resource::get('MyBucket', 'name');
+ $this->assertEquals('my-bucket', $bucketName);
+
+ $bucketType = Resource::get('MyBucket', 'type');
+ $this->assertEquals('sst.aws.Bucket', $bucketType);
+ }
+
+ public function testGetResourceWithNestedPath(): void
+ {
+ $_ENV['SST_RESOURCE_App'] = json_encode(['name' => 'my-app', 'stage' => 'dev']);
+ $_ENV['SST_RESOURCE_MyResource'] = json_encode([
+ 'config' => [
+ 'nested' => [
+ 'value' => 'deep-value'
+ ]
+ ]
+ ]);
+
+ // Get deeply nested property
+ $value = Resource::get('MyResource', 'config', 'nested', 'value');
+ $this->assertEquals('deep-value', $value);
+ }
+
+ public function testGetResourceWithInvalidPath(): void
+ {
+ $_ENV['SST_RESOURCE_App'] = json_encode(['name' => 'my-app', 'stage' => 'dev']);
+ $_ENV['SST_RESOURCE_MyBucket'] = json_encode(['name' => 'my-bucket']);
+
+ $this->expectException(\RuntimeException::class);
+ $this->expectExceptionMessage('Resource path not found');
+
+ Resource::get('MyBucket', 'nonexistent');
+ }
+
+ public function testGetResourceWithPathOnNonArray(): void
+ {
+ $_ENV['SST_RESOURCE_App'] = json_encode(['name' => 'my-app', 'stage' => 'dev']);
+ $_ENV['SST_RESOURCE_MyBucket'] = json_encode(['name' => 'my-bucket']);
+
+ $this->expectException(\RuntimeException::class);
+ $this->expectExceptionMessage('Resource path not found');
+
+ // Try to traverse a string value
+ Resource::get('MyBucket', 'name', 'invalid');
+ }
+
+ public function testAllMethod(): void
+ {
+ $_ENV['SST_RESOURCE_App'] = json_encode(['name' => 'my-app', 'stage' => 'dev']);
+ $_ENV['SST_RESOURCE_MyBucket'] = json_encode(['name' => 'my-bucket']);
+ $_ENV['SST_RESOURCE_MyTable'] = json_encode(['name' => 'my-table']);
+
+ $all = Resource::all();
+
+ $this->assertIsArray($all);
+ $this->assertArrayHasKey('App', $all);
+ $this->assertArrayHasKey('MyBucket', $all);
+ $this->assertArrayHasKey('MyTable', $all);
+ $this->assertEquals(['name' => 'my-app', 'stage' => 'dev'], $all['App']);
+ $this->assertEquals(['name' => 'my-bucket'], $all['MyBucket']);
+ $this->assertEquals(['name' => 'my-table'], $all['MyTable']);
+ }
+
+ public function testGetAndPathWorkTogether(): void
+ {
+ $_ENV['SST_RESOURCE_App'] = json_encode(['name' => 'my-app', 'stage' => 'dev']);
+ $_ENV['SST_RESOURCE_MyBucket'] = json_encode(['name' => 'my-bucket', 'region' => 'us-east-1']);
+
+ // Get whole resource
+ $bucket = Resource::get('MyBucket');
+ $this->assertEquals(['name' => 'my-bucket', 'region' => 'us-east-1'], $bucket);
+
+ // Get specific property
+ $name = Resource::get('MyBucket', 'name');
+ $this->assertEquals('my-bucket', $name);
+
+ // Both should work
+ $this->assertEquals($bucket['name'], $name);
+ }
+}
+
diff --git a/www/src/content/docs/docs/reference/sdk.mdx b/www/src/content/docs/docs/reference/sdk.mdx
index 25260801a7..3f89aa8b9e 100644
--- a/www/src/content/docs/docs/reference/sdk.mdx
+++ b/www/src/content/docs/docs/reference/sdk.mdx
@@ -11,7 +11,7 @@ You can use the SDK in your **functions**, **frontends**, and **container applic
Check out the _SDK_ section in a component's API reference doc.
:::
-Currently, the SDK is only available for JS/TS, Python, Golang, and Rust. Support for other languages is on the roadmap.
+Currently, the SDK is available for JS/TS, Python, Golang, Rust, and PHP. Support for other languages is on the roadmap.
---
@@ -208,3 +208,53 @@ new sst.aws.Function("MyFunction", {
```
Client functions are currently **not supported** in the Rust SDK.
+
+---
+
+## PHP
+
+Use the SST PHP SDK package in your PHP container applications.
+
+```bash
+composer require sst/sdk
+```
+
+In your runtime code, use the `Resource::get()` function to access the linked resources.
+
+```php title="index.php"
+