diff --git a/app/Services/DigitalOcean/DigitalOceanAccountService.php b/app/Services/DigitalOcean/DigitalOceanAccountService.php new file mode 100644 index 00000000..99934f6d --- /dev/null +++ b/app/Services/DigitalOcean/DigitalOceanAccountService.php @@ -0,0 +1,189 @@ +api = $api; + } + + /** + * Get available DigitalOcean regions. + * + * @return array Array of region slug => description + */ + public function getAvailableRegions(): array + { + $client = $this->getAPI(); + + try { + $regionApi = $client->region(); + $regions = $regionApi->getAll(); + + $options = []; + foreach ($regions as $region) { + /** @var RegionEntity $region */ + if ($region->available) { + $options[$region->slug] = "{$region->name} ({$region->slug})"; + } + } + + return $options; + } catch (\Throwable $e) { + throw new \RuntimeException('Failed to fetch regions: ' . $e->getMessage(), 0, $e); + } + } + + /** + * Get available droplet sizes. + * + * @return array Array of size slug => description + */ + public function getAvailableSizes(): array + { + $client = $this->getAPI(); + + try { + $sizeApi = $client->size(); + $sizes = $sizeApi->getAll(); + + $options = []; + foreach ($sizes as $size) { + /** @var SizeEntity $size */ + if ($size->available) { + $vcpus = $size->vcpus; + $memory = $size->memory / 1024; // Convert MB to GB + $disk = $size->disk; + $price = $size->priceMonthly; + + $options[$size->slug] = "{$size->slug} - {$vcpus} vCPU, {$memory}GB RAM, {$disk}GB SSD (\${$price}/mo)"; + } + } + + return $options; + } catch (\Throwable $e) { + throw new \RuntimeException('Failed to fetch sizes: ' . $e->getMessage(), 0, $e); + } + } + + /** + * Get available OS images (filtered to Ubuntu and Debian only). + * + * @return array Array of image slug => description + */ + public function getAvailableImages(): array + { + $client = $this->getAPI(); + + try { + $imageApi = $client->image(); + + // Get distribution images (not snapshots or backups) + $images = $imageApi->getAll(['type' => 'distribution']); + + $options = []; + foreach ($images as $image) { + /** @var ImageEntity $image */ + // Filter to only Ubuntu and Debian distributions + if ($image->status === 'available' && $image->public === true) { + $distribution = strtolower($image->distribution ?? ''); + + if (in_array($distribution, ['ubuntu', 'debian'], true)) { + $slug = $image->slug; + if ($slug !== null && $slug !== '') { + $options[$slug] = "{$image->distribution} {$image->name}"; + } + } + } + } + + return $options; + } catch (\Throwable $e) { + throw new \RuntimeException('Failed to fetch images: ' . $e->getMessage(), 0, $e); + } + } + + /** + * Get user's VPCs for a specific region. + * + * @return array Array of VPC UUID => name + */ + public function getUserVpcs(string $region): array + { + $client = $this->getAPI(); + + try { + $vpcApi = $client->vpc(); + $vpcs = $vpcApi->getAll(); + + $options = ['default' => 'Use default VPC']; + foreach ($vpcs as $vpc) { + if ($vpc->region === $region) { + $options[$vpc->id] = "{$vpc->name} ({$vpc->id})"; + } + } + + return $options; + } catch (\Throwable $e) { + throw new \RuntimeException('Failed to fetch VPCs: ' . $e->getMessage(), 0, $e); + } + } + + /** + * Get user's SSH keys. + * + * @return array Array of key ID => description + */ + public function getUserSshKeys(): array + { + $client = $this->getAPI(); + + try { + $keyApi = $client->key(); + $keys = $keyApi->getAll(); + + $options = []; + foreach ($keys as $key) { + $fingerprint = substr($key->fingerprint, 0, 16) . '...'; + $options[$key->id] = "{$key->name} ({$fingerprint})"; + } + + return $options; + } catch (\Throwable $e) { + throw new \RuntimeException('Failed to fetch SSH keys: ' . $e->getMessage(), 0, $e); + } + } + + /** + * Get the configured DigitalOcean API client. + * + * @throws \RuntimeException If client not configured + */ + private function getAPI(): Client + { + if ($this->api === null) { + throw new \RuntimeException('DigitalOcean API client not configured. Call setAPI() first.'); + } + + return $this->api; + } +} diff --git a/app/Services/DigitalOcean/DigitalOceanDropletService.php b/app/Services/DigitalOcean/DigitalOceanDropletService.php new file mode 100644 index 00000000..c994ddae --- /dev/null +++ b/app/Services/DigitalOcean/DigitalOceanDropletService.php @@ -0,0 +1,211 @@ +api = $api; + } + + /** + * Create a new droplet with the specified configuration. + * + * @param string $name Droplet name + * @param string $region Region slug (e.g., nyc3) + * @param string $size Size slug (e.g., s-1vcpu-1gb) + * @param string $image Image slug or ID (e.g., ubuntu-22-04-x64) + * @param array $sshKeys SSH key IDs + * @param bool $backups Enable backups + * @param bool $monitoring Enable monitoring + * @param bool $ipv6 Enable IPv6 + * @param string|null $vpcUuid VPC UUID (null for default) + * + * @return array{id: int, name: string, status: string} Droplet data + * + * @throws \RuntimeException If creation fails + */ + public function createDroplet( + string $name, + string $region, + string $size, + string $image, + array $sshKeys = [], + bool $backups = false, + bool $monitoring = false, + bool $ipv6 = false, + ?string $vpcUuid = null + ): array { + $client = $this->getAPI(); + + try { + $dropletApi = $client->droplet(); + + // Prepare VPC parameter (API expects string|bool, false for default) + $vpcParam = $vpcUuid ?? false; + + /** @var DropletEntity $droplet */ + $droplet = $dropletApi->create( + $name, + $region, + $size, + $image, + $backups, + $ipv6, + $vpcParam, // VPC UUID or false for default + $sshKeys, + '', // user_data + $monitoring, + [], // volumes + ); + + return [ + 'id' => $droplet->id, + 'name' => $droplet->name, + 'status' => $droplet->status, + ]; + } catch (\Throwable $e) { + throw new \RuntimeException('Failed to create droplet: ' . $e->getMessage(), 0, $e); + } + } + + /** + * Get the current status of a droplet. + * + * @throws \RuntimeException If status check fails + */ + public function getDropletStatus(int $dropletId): string + { + $client = $this->getAPI(); + + try { + $dropletApi = $client->droplet(); + + /** @var DropletEntity $droplet */ + $droplet = $dropletApi->getById($dropletId); + + return $droplet->status; + } catch (\Throwable $e) { + throw new \RuntimeException("Failed to get droplet status: {$e->getMessage()}", 0, $e); + } + } + + /** + * Wait for a droplet to become active. + * + * @param int $dropletId Droplet ID + * @param int $timeoutSeconds Maximum time to wait (default: 300 = 5 minutes) + * @param int $pollIntervalSeconds Time between status checks (default: 2) + * + * @throws \RuntimeException If timeout is reached or polling fails + */ + public function waitForDropletReady( + int $dropletId, + int $timeoutSeconds = 300, + int $pollIntervalSeconds = 2 + ): void { + $startTime = time(); + + while (true) { + $status = $this->getDropletStatus($dropletId); + + if ($status === 'active') { + return; + } + + if ((time() - $startTime) >= $timeoutSeconds) { + throw new \RuntimeException( + "Timeout waiting for droplet (ID: {$dropletId}) to become active (current status: {$status})" + ); + } + + sleep($pollIntervalSeconds); + } + } + + /** + * Get the public IPv4 address of a droplet. + * + * @throws \RuntimeException If IP retrieval fails or no public IP found + */ + public function getDropletIp(int $dropletId): string + { + $client = $this->getAPI(); + + try { + $dropletApi = $client->droplet(); + + /** @var DropletEntity $droplet */ + $droplet = $dropletApi->getById($dropletId); + + // Find public IPv4 network + foreach ($droplet->networks as $network) { + if ($network->type === 'public' && $network->version === 4) { + return $network->ipAddress; + } + } + + throw new \RuntimeException('No public IPv4 address found for droplet'); + } catch (\Throwable $e) { + throw new \RuntimeException("Failed to get droplet IP: {$e->getMessage()}", 0, $e); + } + } + + /** + * Destroy a droplet by ID. + * + * Silently succeeds if droplet doesn't exist (404). + * + * @param int $dropletId Droplet ID to destroy + * + * @throws \RuntimeException If destruction fails (non-404 errors) + */ + public function destroyDroplet(int $dropletId): void + { + $client = $this->getAPI(); + + try { + $dropletApi = $client->droplet(); + $dropletApi->remove($dropletId); + } catch (\Throwable $e) { + // Check if 404 (already deleted) - silently succeed + $message = strtolower($e->getMessage()); + if (str_contains($message, '404') || str_contains($message, 'not found')) { + return; + } + + // Other errors - throw + throw new \RuntimeException("Failed to destroy droplet: {$e->getMessage()}", 0, $e); + } + } + + /** + * Get the configured DigitalOcean API client. + * + * @throws \RuntimeException If client not configured + */ + private function getAPI(): Client + { + if ($this->api === null) { + throw new \RuntimeException('DigitalOcean API client not configured. Call setAPI() first.'); + } + + return $this->api; + } +} diff --git a/app/Services/DigitalOcean/DigitalOceanKeyService.php b/app/Services/DigitalOcean/DigitalOceanKeyService.php new file mode 100644 index 00000000..5bed88b9 --- /dev/null +++ b/app/Services/DigitalOcean/DigitalOceanKeyService.php @@ -0,0 +1,121 @@ +api = $api; + } + + /** + * Upload a local SSH public key to DigitalOcean account. + * + * @param string $publicKeyPath Path to public key file (should be already expanded) + * @param string $keyName Name for the key in DO account + * + * @return int The new SSH key ID + * + * @throws \RuntimeException If upload fails + */ + public function uploadKey(string $publicKeyPath, string $keyName): int + { + // Check if file exists + if (!$this->fs->exists($publicKeyPath)) { + throw new \RuntimeException("SSH public key file not found: {$publicKeyPath}"); + } + + // Read public key content + $publicKey = $this->fs->readFile($publicKeyPath); + $publicKey = trim($publicKey); + + // Validate key format (should start with ssh-rsa, ssh-ed25519, ecdsa-sha2-nistp256, etc.) + // except 'ssh-dss' which is effectively obsolete: + $validPrefixes = ['ssh-rsa', 'ssh-ed25519', 'ecdsa-sha2-nistp256', 'ecdsa-sha2-nistp384', 'ecdsa-sha2-nistp521', 'ssh-dss']; + $isValid = false; + foreach ($validPrefixes as $prefix) { + if (str_starts_with($publicKey, $prefix)) { + $isValid = true; + break; + } + } + + if (!$isValid) { + throw new \RuntimeException("Invalid SSH public key format in {$publicKeyPath}"); + } + + $client = $this->getAPI(); + + try { + $keyApi = $client->key(); + $key = $keyApi->create($keyName, $publicKey); + + return $key->id; + } catch (\Throwable $e) { + throw new \RuntimeException("Failed to upload SSH key: {$e->getMessage()}", 0, $e); + } + } + + /** + * Delete an SSH key from DigitalOcean account. + * + * Silently succeeds if key doesn't exist (404). + * + * @param int $keyId SSH key ID to delete + * + * @throws \RuntimeException If deletion fails (non-404 errors) + */ + public function deleteKey(int $keyId): void + { + $client = $this->getAPI(); + + try { + $keyApi = $client->key(); + $keyApi->remove((string) $keyId); + } catch (\Throwable $e) { + // Check if 404 (already deleted) - silently succeed + $message = strtolower($e->getMessage()); + if (str_contains($message, '404') || str_contains($message, 'not found')) { + return; + } + + // Other errors - throw + throw new \RuntimeException("Failed to delete SSH key: {$e->getMessage()}", 0, $e); + } + } + + /** + * Get the configured DigitalOcean API client. + * + * @throws \RuntimeException If client not configured + */ + private function getAPI(): Client + { + if ($this->api === null) { + throw new \RuntimeException('DigitalOcean API client not configured. Call setAPI() first.'); + } + + return $this->api; + } +} diff --git a/app/Services/DigitalOceanService.php b/app/Services/DigitalOceanService.php new file mode 100644 index 00000000..68217fd2 --- /dev/null +++ b/app/Services/DigitalOceanService.php @@ -0,0 +1,148 @@ + */ + private array $cache = []; + + public function __construct( + public readonly DigitalOceanAccountService $account, + public readonly DigitalOceanKeyService $key, + public readonly DigitalOceanDropletService $droplet, + ) { + } + + // + // API + // ------------------------------------------------------------------------------- + + /** + * Initialize the DigitalOcean API with the given token. + * + * @param string $token The DigitalOcean API token + * + * @throws \RuntimeException If authentication fails or API is unreachable + */ + public function initialize(string $token): void + { + $this->setToken($token); + $this->initializeAPI(); + $this->verifyAuthentication(); + } + + /** + * Set the DigitalOcean API token. + * + * Must be called before making any API calls. + */ + public function setToken(string $token): void + { + $this->token = $token; + + // Reset client every time a new token is set + $this->api = null; + } + + /** + * Verify API token authentication by making a lightweight API call. + * + * @throws \RuntimeException If authentication fails or API is unreachable + */ + public function verifyAuthentication(): void + { + $api = $this->initializeAPI(); + + try { + // Use account endpoint - lightweight and verifies token validity + $api->account()->getUserInformation(); + } catch (\Throwable $e) { + throw new \RuntimeException('Failed to authenticate with DigitalOcean API: ' . $e->getMessage(), 0, $e); + } + } + + // + // Cache management + // ------------------------------------------------------------------------------- + + /** + * Check if a cache key exists. + */ + public function hasCache(string $key): bool + { + return isset($this->cache[$key]); + } + + /** + * Get a cached value. + */ + public function getCache(string $key): mixed + { + return $this->cache[$key] ?? null; + } + + /** + * Set a cache value. + */ + public function setCache(string $key, mixed $value): void + { + $this->cache[$key] = $value; + } + + /** + * Clear a specific cache key. + */ + public function clearCache(string $key): void + { + unset($this->cache[$key]); + } + + // + // Client access + // ------------------------------------------------------------------------------- + + /** + * Get or initialize the DigitalOcean API client. + * + * @throws \RuntimeException If API token is not configured + */ + private function initializeAPI(): Client + { + if ($this->api !== null) { + return $this->api; + } + + if ($this->token === null || $this->token === '') { + throw new \RuntimeException( + 'DigitalOcean API token not set. '. + 'Set API token before making API requests.' + ); + } + + $this->api = new Client(); + $this->api->authenticate($this->token); + + $this->account->setAPI($this->api); + $this->key->setAPI($this->api); + $this->droplet->setAPI($this->api); + + return $this->api; + } +} diff --git a/composer.json b/composer.json index f8897464..a258354e 100644 --- a/composer.json +++ b/composer.json @@ -23,7 +23,8 @@ "symfony/console": "^7.3", "symfony/dotenv": "^7.3", "symfony/filesystem": "^7.3", - "symfony/yaml": "^7.3" + "symfony/yaml": "^7.3", + "toin0u/digitalocean-v2": "^5.0" }, "require-dev": { "laravel/pint": "^1.25", @@ -67,7 +68,8 @@ "preferred-install": "dist", "sort-packages": true, "allow-plugins": { - "pestphp/pest-plugin": true + "pestphp/pest-plugin": true, + "php-http/discovery": true } }, "minimum-stability": "stable", diff --git a/composer.lock b/composer.lock index 085d0996..b1196d69 100644 --- a/composer.lock +++ b/composer.lock @@ -4,8 +4,74 @@ "Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies", "This file is @generated automatically" ], - "content-hash": "4455525da0a41afde3f0f00e010a7d86", + "content-hash": "68f2fa4e1a07a32b57aef442161c2118", "packages": [ + { + "name": "clue/stream-filter", + "version": "v1.7.0", + "source": { + "type": "git", + "url": "https://github.com/clue/stream-filter.git", + "reference": "049509fef80032cb3f051595029ab75b49a3c2f7" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/clue/stream-filter/zipball/049509fef80032cb3f051595029ab75b49a3c2f7", + "reference": "049509fef80032cb3f051595029ab75b49a3c2f7", + "shasum": "" + }, + "require": { + "php": ">=5.3" + }, + "require-dev": { + "phpunit/phpunit": "^9.6 || ^5.7 || ^4.8.36" + }, + "type": "library", + "autoload": { + "files": [ + "src/functions_include.php" + ], + "psr-4": { + "Clue\\StreamFilter\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Christian Lück", + "email": "christian@clue.engineering" + } + ], + "description": "A simple and modern approach to stream filtering in PHP", + "homepage": "https://github.com/clue/stream-filter", + "keywords": [ + "bucket brigade", + "callback", + "filter", + "php_user_filter", + "stream", + "stream_filter_append", + "stream_filter_register" + ], + "support": { + "issues": "https://github.com/clue/stream-filter/issues", + "source": "https://github.com/clue/stream-filter/tree/v1.7.0" + }, + "funding": [ + { + "url": "https://clue.engineering/support", + "type": "custom" + }, + { + "url": "https://github.com/clue", + "type": "github" + } + ], + "time": "2023-12-20T15:40:13+00:00" + }, { "name": "guzzlehttp/guzzle", "version": "7.10.0", @@ -509,6 +575,332 @@ }, "time": "2020-10-15T08:29:30+00:00" }, + { + "name": "php-http/client-common", + "version": "2.7.2", + "source": { + "type": "git", + "url": "https://github.com/php-http/client-common.git", + "reference": "0cfe9858ab9d3b213041b947c881d5b19ceeca46" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/php-http/client-common/zipball/0cfe9858ab9d3b213041b947c881d5b19ceeca46", + "reference": "0cfe9858ab9d3b213041b947c881d5b19ceeca46", + "shasum": "" + }, + "require": { + "php": "^7.1 || ^8.0", + "php-http/httplug": "^2.0", + "php-http/message": "^1.6", + "psr/http-client": "^1.0", + "psr/http-factory": "^1.0", + "psr/http-message": "^1.0 || ^2.0", + "symfony/options-resolver": "~4.0.15 || ~4.1.9 || ^4.2.1 || ^5.0 || ^6.0 || ^7.0", + "symfony/polyfill-php80": "^1.17" + }, + "require-dev": { + "doctrine/instantiator": "^1.1", + "guzzlehttp/psr7": "^1.4", + "nyholm/psr7": "^1.2", + "phpspec/phpspec": "^5.1 || ^6.3 || ^7.1", + "phpspec/prophecy": "^1.10.2", + "phpunit/phpunit": "^7.5.20 || ^8.5.33 || ^9.6.7" + }, + "suggest": { + "ext-json": "To detect JSON responses with the ContentTypePlugin", + "ext-libxml": "To detect XML responses with the ContentTypePlugin", + "php-http/cache-plugin": "PSR-6 Cache plugin", + "php-http/logger-plugin": "PSR-3 Logger plugin", + "php-http/stopwatch-plugin": "Symfony Stopwatch plugin" + }, + "type": "library", + "autoload": { + "psr-4": { + "Http\\Client\\Common\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Márk Sági-Kazár", + "email": "mark.sagikazar@gmail.com" + } + ], + "description": "Common HTTP Client implementations and tools for HTTPlug", + "homepage": "http://httplug.io", + "keywords": [ + "client", + "common", + "http", + "httplug" + ], + "support": { + "issues": "https://github.com/php-http/client-common/issues", + "source": "https://github.com/php-http/client-common/tree/2.7.2" + }, + "time": "2024-09-24T06:21:48+00:00" + }, + { + "name": "php-http/discovery", + "version": "1.20.0", + "source": { + "type": "git", + "url": "https://github.com/php-http/discovery.git", + "reference": "82fe4c73ef3363caed49ff8dd1539ba06044910d" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/php-http/discovery/zipball/82fe4c73ef3363caed49ff8dd1539ba06044910d", + "reference": "82fe4c73ef3363caed49ff8dd1539ba06044910d", + "shasum": "" + }, + "require": { + "composer-plugin-api": "^1.0|^2.0", + "php": "^7.1 || ^8.0" + }, + "conflict": { + "nyholm/psr7": "<1.0", + "zendframework/zend-diactoros": "*" + }, + "provide": { + "php-http/async-client-implementation": "*", + "php-http/client-implementation": "*", + "psr/http-client-implementation": "*", + "psr/http-factory-implementation": "*", + "psr/http-message-implementation": "*" + }, + "require-dev": { + "composer/composer": "^1.0.2|^2.0", + "graham-campbell/phpspec-skip-example-extension": "^5.0", + "php-http/httplug": "^1.0 || ^2.0", + "php-http/message-factory": "^1.0", + "phpspec/phpspec": "^5.1 || ^6.1 || ^7.3", + "sebastian/comparator": "^3.0.5 || ^4.0.8", + "symfony/phpunit-bridge": "^6.4.4 || ^7.0.1" + }, + "type": "composer-plugin", + "extra": { + "class": "Http\\Discovery\\Composer\\Plugin", + "plugin-optional": true + }, + "autoload": { + "psr-4": { + "Http\\Discovery\\": "src/" + }, + "exclude-from-classmap": [ + "src/Composer/Plugin.php" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Márk Sági-Kazár", + "email": "mark.sagikazar@gmail.com" + } + ], + "description": "Finds and installs PSR-7, PSR-17, PSR-18 and HTTPlug implementations", + "homepage": "http://php-http.org", + "keywords": [ + "adapter", + "client", + "discovery", + "factory", + "http", + "message", + "psr17", + "psr7" + ], + "support": { + "issues": "https://github.com/php-http/discovery/issues", + "source": "https://github.com/php-http/discovery/tree/1.20.0" + }, + "time": "2024-10-02T11:20:13+00:00" + }, + { + "name": "php-http/httplug", + "version": "2.4.1", + "source": { + "type": "git", + "url": "https://github.com/php-http/httplug.git", + "reference": "5cad731844891a4c282f3f3e1b582c46839d22f4" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/php-http/httplug/zipball/5cad731844891a4c282f3f3e1b582c46839d22f4", + "reference": "5cad731844891a4c282f3f3e1b582c46839d22f4", + "shasum": "" + }, + "require": { + "php": "^7.1 || ^8.0", + "php-http/promise": "^1.1", + "psr/http-client": "^1.0", + "psr/http-message": "^1.0 || ^2.0" + }, + "require-dev": { + "friends-of-phpspec/phpspec-code-coverage": "^4.1 || ^5.0 || ^6.0", + "phpspec/phpspec": "^5.1 || ^6.0 || ^7.0" + }, + "type": "library", + "autoload": { + "psr-4": { + "Http\\Client\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Eric GELOEN", + "email": "geloen.eric@gmail.com" + }, + { + "name": "Márk Sági-Kazár", + "email": "mark.sagikazar@gmail.com", + "homepage": "https://sagikazarmark.hu" + } + ], + "description": "HTTPlug, the HTTP client abstraction for PHP", + "homepage": "http://httplug.io", + "keywords": [ + "client", + "http" + ], + "support": { + "issues": "https://github.com/php-http/httplug/issues", + "source": "https://github.com/php-http/httplug/tree/2.4.1" + }, + "time": "2024-09-23T11:39:58+00:00" + }, + { + "name": "php-http/message", + "version": "1.16.2", + "source": { + "type": "git", + "url": "https://github.com/php-http/message.git", + "reference": "06dd5e8562f84e641bf929bfe699ee0f5ce8080a" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/php-http/message/zipball/06dd5e8562f84e641bf929bfe699ee0f5ce8080a", + "reference": "06dd5e8562f84e641bf929bfe699ee0f5ce8080a", + "shasum": "" + }, + "require": { + "clue/stream-filter": "^1.5", + "php": "^7.2 || ^8.0", + "psr/http-message": "^1.1 || ^2.0" + }, + "provide": { + "php-http/message-factory-implementation": "1.0" + }, + "require-dev": { + "ergebnis/composer-normalize": "^2.6", + "ext-zlib": "*", + "guzzlehttp/psr7": "^1.0 || ^2.0", + "laminas/laminas-diactoros": "^2.0 || ^3.0", + "php-http/message-factory": "^1.0.2", + "phpspec/phpspec": "^5.1 || ^6.3 || ^7.1", + "slim/slim": "^3.0" + }, + "suggest": { + "ext-zlib": "Used with compressor/decompressor streams", + "guzzlehttp/psr7": "Used with Guzzle PSR-7 Factories", + "laminas/laminas-diactoros": "Used with Diactoros Factories", + "slim/slim": "Used with Slim Framework PSR-7 implementation" + }, + "type": "library", + "autoload": { + "files": [ + "src/filters.php" + ], + "psr-4": { + "Http\\Message\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Márk Sági-Kazár", + "email": "mark.sagikazar@gmail.com" + } + ], + "description": "HTTP Message related tools", + "homepage": "http://php-http.org", + "keywords": [ + "http", + "message", + "psr-7" + ], + "support": { + "issues": "https://github.com/php-http/message/issues", + "source": "https://github.com/php-http/message/tree/1.16.2" + }, + "time": "2024-10-02T11:34:13+00:00" + }, + { + "name": "php-http/promise", + "version": "1.3.1", + "source": { + "type": "git", + "url": "https://github.com/php-http/promise.git", + "reference": "fc85b1fba37c169a69a07ef0d5a8075770cc1f83" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/php-http/promise/zipball/fc85b1fba37c169a69a07ef0d5a8075770cc1f83", + "reference": "fc85b1fba37c169a69a07ef0d5a8075770cc1f83", + "shasum": "" + }, + "require": { + "php": "^7.1 || ^8.0" + }, + "require-dev": { + "friends-of-phpspec/phpspec-code-coverage": "^4.3.2 || ^6.3", + "phpspec/phpspec": "^5.1.2 || ^6.2 || ^7.4" + }, + "type": "library", + "autoload": { + "psr-4": { + "Http\\Promise\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Joel Wurtz", + "email": "joel.wurtz@gmail.com" + }, + { + "name": "Márk Sági-Kazár", + "email": "mark.sagikazar@gmail.com" + } + ], + "description": "Promise used for asynchronous HTTP requests", + "homepage": "http://httplug.io", + "keywords": [ + "promise" + ], + "support": { + "issues": "https://github.com/php-http/promise/issues", + "source": "https://github.com/php-http/promise/tree/1.3.1" + }, + "time": "2024-03-15T13:55:21+00:00" + }, { "name": "phpseclib/phpseclib", "version": "3.0.47", @@ -781,16 +1173,16 @@ }, { "name": "psr/http-message", - "version": "2.0", + "version": "1.1", "source": { "type": "git", "url": "https://github.com/php-fig/http-message.git", - "reference": "402d35bcb92c70c026d1a6a9883f06b2ead23d71" + "reference": "cb6ce4845ce34a8ad9e68117c10ee90a29919eba" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/php-fig/http-message/zipball/402d35bcb92c70c026d1a6a9883f06b2ead23d71", - "reference": "402d35bcb92c70c026d1a6a9883f06b2ead23d71", + "url": "https://api.github.com/repos/php-fig/http-message/zipball/cb6ce4845ce34a8ad9e68117c10ee90a29919eba", + "reference": "cb6ce4845ce34a8ad9e68117c10ee90a29919eba", "shasum": "" }, "require": { @@ -799,7 +1191,7 @@ "type": "library", "extra": { "branch-alias": { - "dev-master": "2.0.x-dev" + "dev-master": "1.1.x-dev" } }, "autoload": { @@ -814,7 +1206,7 @@ "authors": [ { "name": "PHP-FIG", - "homepage": "https://www.php-fig.org/" + "homepage": "http://www.php-fig.org/" } ], "description": "Common interface for HTTP messages", @@ -828,9 +1220,9 @@ "response" ], "support": { - "source": "https://github.com/php-fig/http-message/tree/2.0" + "source": "https://github.com/php-fig/http-message/tree/1.1" }, - "time": "2023-04-04T09:54:51+00:00" + "time": "2023-04-04T09:50:52+00:00" }, { "name": "ralouphie/getallheaders", @@ -1189,6 +1581,77 @@ ], "time": "2025-07-07T08:17:47+00:00" }, + { + "name": "symfony/options-resolver", + "version": "v7.3.3", + "source": { + "type": "git", + "url": "https://github.com/symfony/options-resolver.git", + "reference": "0ff2f5c3df08a395232bbc3c2eb7e84912df911d" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/options-resolver/zipball/0ff2f5c3df08a395232bbc3c2eb7e84912df911d", + "reference": "0ff2f5c3df08a395232bbc3c2eb7e84912df911d", + "shasum": "" + }, + "require": { + "php": ">=8.2", + "symfony/deprecation-contracts": "^2.5|^3" + }, + "type": "library", + "autoload": { + "psr-4": { + "Symfony\\Component\\OptionsResolver\\": "" + }, + "exclude-from-classmap": [ + "/Tests/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Fabien Potencier", + "email": "fabien@symfony.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Provides an improved replacement for the array_replace PHP function", + "homepage": "https://symfony.com", + "keywords": [ + "config", + "configuration", + "options" + ], + "support": { + "source": "https://github.com/symfony/options-resolver/tree/v7.3.3" + }, + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://github.com/nicolas-grekas", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" + } + ], + "time": "2025-08-05T10:16:07+00:00" + }, { "name": "symfony/polyfill-ctype", "version": "v1.33.0", @@ -1524,6 +1987,90 @@ ], "time": "2024-12-23T08:48:59+00:00" }, + { + "name": "symfony/polyfill-php80", + "version": "v1.33.0", + "source": { + "type": "git", + "url": "https://github.com/symfony/polyfill-php80.git", + "reference": "0cc9dd0f17f61d8131e7df6b84bd344899fe2608" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/polyfill-php80/zipball/0cc9dd0f17f61d8131e7df6b84bd344899fe2608", + "reference": "0cc9dd0f17f61d8131e7df6b84bd344899fe2608", + "shasum": "" + }, + "require": { + "php": ">=7.2" + }, + "type": "library", + "extra": { + "thanks": { + "url": "https://github.com/symfony/polyfill", + "name": "symfony/polyfill" + } + }, + "autoload": { + "files": [ + "bootstrap.php" + ], + "psr-4": { + "Symfony\\Polyfill\\Php80\\": "" + }, + "classmap": [ + "Resources/stubs" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Ion Bazan", + "email": "ion.bazan@gmail.com" + }, + { + "name": "Nicolas Grekas", + "email": "p@tchwork.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Symfony polyfill backporting some PHP 8.0+ features to lower PHP versions", + "homepage": "https://symfony.com", + "keywords": [ + "compatibility", + "polyfill", + "portable", + "shim" + ], + "support": { + "source": "https://github.com/symfony/polyfill-php80/tree/v1.33.0" + }, + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://github.com/nicolas-grekas", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" + } + ], + "time": "2025-01-02T08:10:11+00:00" + }, { "name": "symfony/service-contracts", "version": "v3.6.0", @@ -1772,6 +2319,87 @@ } ], "time": "2025-08-27T11:34:33+00:00" + }, + { + "name": "toin0u/digitalocean-v2", + "version": "5.0.5", + "source": { + "type": "git", + "url": "https://github.com/DigitalOceanPHP/Client.git", + "reference": "be8658cb864c1af8d43fcd64c94b5f39e6af2c53" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/DigitalOceanPHP/Client/zipball/be8658cb864c1af8d43fcd64c94b5f39e6af2c53", + "reference": "be8658cb864c1af8d43fcd64c94b5f39e6af2c53", + "shasum": "" + }, + "require": { + "ext-json": "*", + "php": "^8.1", + "php-http/client-common": "^2.7.2", + "php-http/discovery": "^1.20.0", + "php-http/httplug": "^2.4.1", + "psr/http-client-implementation": "^1.0", + "psr/http-factory-implementation": "^1.0", + "psr/http-message": "^1.1 || ^2.0" + }, + "require-dev": { + "bamarni/composer-bin-plugin": "^1.8.2", + "guzzlehttp/guzzle": "^7.9.2" + }, + "type": "library", + "extra": { + "bamarni-bin": { + "bin-links": true, + "forward-command": false + } + }, + "autoload": { + "psr-4": { + "DigitalOceanV2\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Graham Campbell", + "email": "hello@gjcampbell.co.uk", + "homepage": "https://github.com/GrahamCampbell" + }, + { + "name": "Yassir Hannoun", + "email": "yassir.hannoun@gmail.com", + "homepage": "https://github.com/yassirh" + }, + { + "name": "Antoine Kirk", + "email": "contact@sbin.dk", + "homepage": "https://github.com/toin0u" + } + ], + "description": "DigitalOcean API v2 client for PHP", + "keywords": [ + "Cloud Hosting", + "SSD", + "api", + "digitalocean", + "vps" + ], + "support": { + "issues": "https://github.com/DigitalOceanPHP/Client/issues", + "source": "https://github.com/DigitalOceanPHP/Client/tree/5.0.5" + }, + "funding": [ + { + "url": "https://github.com/GrahamCampbell", + "type": "github" + } + ], + "time": "2025-05-03T20:44:33+00:00" } ], "packages-dev": [