From 99ed07171abfbde20619e62797c8f79c15dfdc01 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Lukas=20K=C3=A4mmerling?= Date: Mon, 13 Oct 2025 20:52:05 +0200 Subject: [PATCH 01/14] Implement basic DNS Zones client --- src/HetznerAPIClient.php | 27 ++- src/Models/Zones/AuthoritativeNameservers.php | 30 +++ src/Models/Zones/PrimaryNameserver.php | 25 ++ src/Models/Zones/RRSet.php | 69 ++++++ src/Models/Zones/RRSetProtection.php | 49 ++++ src/Models/Zones/Zone.php | 219 +++++++++++++++++ src/Models/Zones/ZoneMode.php | 9 + src/Models/Zones/ZoneRequestOpts.php | 37 +++ src/Models/Zones/Zones.php | 221 ++++++++++++++++++ tests/Unit/Models/Zones/ZoneTest.php | 61 +++++ tests/Unit/Models/Zones/ZonesTest.php | 132 +++++++++++ tests/Unit/Models/Zones/fixtures/zone.json | 44 ++++ .../zone_action_change_protection.json | 17 ++ .../Zones/fixtures/zone_action_generic.json | 17 ++ .../Models/Zones/fixtures/zone_create.json | 47 ++++ tests/Unit/Models/Zones/fixtures/zones.json | 56 +++++ 16 files changed, 1050 insertions(+), 10 deletions(-) create mode 100644 src/Models/Zones/AuthoritativeNameservers.php create mode 100644 src/Models/Zones/PrimaryNameserver.php create mode 100644 src/Models/Zones/RRSet.php create mode 100644 src/Models/Zones/RRSetProtection.php create mode 100644 src/Models/Zones/Zone.php create mode 100644 src/Models/Zones/ZoneMode.php create mode 100644 src/Models/Zones/ZoneRequestOpts.php create mode 100644 src/Models/Zones/Zones.php create mode 100644 tests/Unit/Models/Zones/ZoneTest.php create mode 100644 tests/Unit/Models/Zones/ZonesTest.php create mode 100644 tests/Unit/Models/Zones/fixtures/zone.json create mode 100644 tests/Unit/Models/Zones/fixtures/zone_action_change_protection.json create mode 100644 tests/Unit/Models/Zones/fixtures/zone_action_generic.json create mode 100644 tests/Unit/Models/Zones/fixtures/zone_create.json create mode 100644 tests/Unit/Models/Zones/fixtures/zones.json diff --git a/src/HetznerAPIClient.php b/src/HetznerAPIClient.php index e44e814..8938bff 100644 --- a/src/HetznerAPIClient.php +++ b/src/HetznerAPIClient.php @@ -21,6 +21,7 @@ use LKDev\HetznerCloud\Models\Servers\Types\ServerTypes; use LKDev\HetznerCloud\Models\SSHKeys\SSHKeys; use LKDev\HetznerCloud\Models\Volumes\Volumes; +use LKDev\HetznerCloud\Models\Zones\Zones; use Psr\Http\Message\ResponseInterface; /** @@ -63,9 +64,9 @@ class HetznerAPIClient protected GuzzleClient $httpClient; /** - * @param string $apiToken - * @param string $baseUrl - * @param string $userAgent + * @param string $apiToken + * @param string $baseUrl + * @param string $userAgent */ public function __construct(string $apiToken, $baseUrl = 'https://api.hetzner.cloud/v1/', $userAgent = '') { @@ -101,7 +102,7 @@ public function getBaseUrl(): string } /** - * @param string $userAgent + * @param string $userAgent * @return HetznerAPIClient */ public function setUserAgent(string $userAgent): self @@ -112,7 +113,7 @@ public function setUserAgent(string $userAgent): self } /** - * @param string $baseUrl + * @param string $baseUrl * @return HetznerAPIClient */ public function setBaseUrl(string $baseUrl): self @@ -141,13 +142,13 @@ public function setHttpClient(GuzzleClient $client): self } /** - * @param \Psr\Http\Message\ResponseInterface $response + * @param \Psr\Http\Message\ResponseInterface $response * * @throws \LKDev\HetznerCloud\APIException */ public static function throwError(ResponseInterface $response) { - $body = (string) $response->getBody(); + $body = (string)$response->getBody(); if (strlen($body) > 0) { $error = \GuzzleHttp\json_decode($body); throw new APIException(APIResponse::create([ @@ -160,15 +161,15 @@ public static function throwError(ResponseInterface $response) } /** - * @param \Psr\Http\Message\ResponseInterface $response + * @param \Psr\Http\Message\ResponseInterface $response * @return bool * * @throws \LKDev\HetznerCloud\APIException */ public static function hasError(ResponseInterface $response) { - $responseDecoded = json_decode((string) $response->getBody()); - if (strlen((string) $response->getBody()) > 0) { + $responseDecoded = json_decode((string)$response->getBody()); + if (strlen((string)$response->getBody()) > 0) { if (property_exists($responseDecoded, 'error')) { self::throwError($response); @@ -327,6 +328,12 @@ public function loadBalancerTypes() return new LoadBalancerTypes($this->httpClient); } + + public function zones() + { + return new Zones($this->httpClient); + } + /** * @return GuzzleClient */ diff --git a/src/Models/Zones/AuthoritativeNameservers.php b/src/Models/Zones/AuthoritativeNameservers.php new file mode 100644 index 0000000..56a7985 --- /dev/null +++ b/src/Models/Zones/AuthoritativeNameservers.php @@ -0,0 +1,30 @@ +assigned = $assigned; + $this->delegated = $delegated; + $this->delegation_last_check = $delegation_last_check; + $this->delegation_status = $delegation_status; + } + + public static function fromResponse(array $response): AuthoritativeNameservers + { + return new self($response['assigned'], $response['delegated'], $response['delegation_last_check'], $response['delegation_status']); + } +} diff --git a/src/Models/Zones/PrimaryNameserver.php b/src/Models/Zones/PrimaryNameserver.php new file mode 100644 index 0000000..9676555 --- /dev/null +++ b/src/Models/Zones/PrimaryNameserver.php @@ -0,0 +1,25 @@ +port = $port; + $this->address = $address; + } + + + public static function fromResponse(array $response): PrimaryNameserver + { + return new self($response['address'], $response['port']); + } +} diff --git a/src/Models/Zones/RRSet.php b/src/Models/Zones/RRSet.php new file mode 100644 index 0000000..b7360dc --- /dev/null +++ b/src/Models/Zones/RRSet.php @@ -0,0 +1,69 @@ +id = $id; + $this->name = $name; + $this->type = $type; + $this->ttl = $ttl; + $this->records = $records; + $this->labels = $labels; + $this->protection = $protection; + } + + public static function fromResponse(array $data): RRSet + { + return new self($data['id'], $data['name'], $data['type'], $data['ttl'], $data['records'], $data['labels'], RRSetProtection::parse($data['protection'])); + } + + public function __toRequest(): array + { + $r = [ + "name" => $this->name, + 'type' => $this->type, + 'ttl' => $this->ttl, + 'records' => $this->records, + ]; + if (!empty($this->labels)) { + $r['labels'] = $this->labels; + } + return $r; + } +} + + +class Record +{ + public string $value; + public string $comment; + + + public function __construct(string $value, string $comment) + { + $this->value = $value; + $this->comment = $comment; + } +} diff --git a/src/Models/Zones/RRSetProtection.php b/src/Models/Zones/RRSetProtection.php new file mode 100644 index 0000000..c17dc7c --- /dev/null +++ b/src/Models/Zones/RRSetProtection.php @@ -0,0 +1,49 @@ +change = $delete; + // Force getting the default http client + parent::__construct(null); + } + + /** + * @param array $input + * @return ?RRSetProtection + */ + public static function parse($input) + { + if ($input == null) { + return null; + } + if (!is_array($input)) { + return null; + } + return new self($input['change'] ?? false); + } +} diff --git a/src/Models/Zones/Zone.php b/src/Models/Zones/Zone.php new file mode 100644 index 0000000..2839b0c --- /dev/null +++ b/src/Models/Zones/Zone.php @@ -0,0 +1,219 @@ + + * + */ + public array $primary_nameservers; + + + /** + * @var array|\LKDev\HetznerCloud\Models\Protection + */ + public Protection|array $protection; + + /** + * @var object + */ + public array $labels; + + /** + * @var int + */ + public int $ttl; + + /** + * @var int + */ + public int $record_count; + + /** + * @var string + * + */ + public string $registrar; + + /** + * @var AuthoritativeNameservers + */ + public AuthoritativeNameservers $authoritative_nameservers; + + /** + * @param int $zoneId + * @param GuzzleClient|null $httpClient + */ + public function __construct(int $zoneId, ?GuzzleClient $httpClient = null) + { + $this->id = $zoneId; + parent::__construct($httpClient); + } + + /** + * @param $data + * @return \LKDev\HetznerCloud\Models\Zones\Zone + */ + public function setAdditionalData($data) + { + $this->name = $data->name; + $this->status = $data->status ?: null; + $this->mode = $data->mode ?: null; + $this->created = $data->created; + $this->protection = $data->protection ? Protection::parse($data->protection) : new Protection(false); + $this->labels = get_object_vars($data->labels); + $this->record_count = $data->record_count; + $this->ttl = $data->ttl; + $this->registrar = $data->registrar; + $this->authoritative_nameservers = AuthoritativeNameservers::fromResponse(get_object_vars($data->authoritative_nameservers)); + if (property_exists($data, 'primary_nameservers')) { + $this->primary_nameservers = []; + foreach ($data->primary_nameservers as $primary_nameserver) { + $this->primary_nameservers[] = PrimaryNameserver::fromResponse(get_object_vars($primary_nameserver)); + } + } + + return $this; + } + + /** + * Reload the data of the zone. + * + * @return zone + * + * @throws \LKDev\HetznerCloud\APIException + */ + public function reload() + { + return HetznerAPIClient::$instance->zones()->get($this->id); + } + + /** + * Deletes a zone. This immediately removes the zone from your account, and it is no longer accessible. + * + * @see https://docs.hetzner.cloud/reference/cloud#zones-delete-a-zone + * + * @return APIResponse|null + * + * @throws \LKDev\HetznerCloud\APIException + */ + public function delete(): ?APIResponse + { + $response = $this->httpClient->delete($this->replaceZoneIdInUri('zones/{id}')); + if (!HetznerAPIClient::hasError($response)) { + return APIResponse::create([ + 'action' => Action::parse(json_decode((string)$response->getBody())->action), + ], $response->getHeaders()); + } + + return null; + } + + /** + * Update a zone with new meta data. + * + * @see https://docs.hetzner.cloud/reference/cloud#zones-update-a-zone + * + * @param array $data + * @return APIResponse|null + * + * @throws \LKDev\HetznerCloud\APIException + */ + public function update(array $data) + { + $response = $this->httpClient->put($this->replaceZoneIdInUri('zones/{id}'), [ + 'json' => $data, + ]); + if (!HetznerAPIClient::hasError($response)) { + return APIResponse::create([ + 'zone' => self::parse(json_decode((string)$response->getBody())->zone), + ], $response->getHeaders()); + } + + return null; + } + + /** + * Changes the protection configuration of the zone. + * + * @see https://docs.hetzner.cloud/#zone-actions-change-zone-protection + * + * @param bool $delete + * @return APIResponse|null + * + * @throws \LKDev\HetznerCloud\APIException + */ + public function changeProtection(bool $delete = true): ?APIResponse + { + $response = $this->httpClient->post('zones/' . $this->id . '/actions/change_protection', [ + 'json' => [ + 'delete' => $delete, + ], + ]); + if (!HetznerAPIClient::hasError($response)) { + return APIResponse::create([ + 'action' => Action::parse(json_decode((string)$response->getBody())->action), + ], $response->getHeaders()); + } + + return null; + } + + /** + * @param string $uri + * @return string + */ + protected function replaceZoneIdInUri(string $uri): string + { + return str_replace('{id}', $this->id, $uri); + } + + /** + * @param $input + * @return \LKDev\HetznerCloud\Models\Zones\Zone|static |null + */ + public static function parse($input) + { + if ($input == null) { + return null; + } + + return (new self($input->id))->setAdditionalData($input); + } +} + + diff --git a/src/Models/Zones/ZoneMode.php b/src/Models/Zones/ZoneMode.php new file mode 100644 index 0000000..c5e2b3f --- /dev/null +++ b/src/Models/Zones/ZoneMode.php @@ -0,0 +1,9 @@ +name = $name; + $this->mode = $mode; + parent::__construct($perPage, $page, $labelSelector); + } +} diff --git a/src/Models/Zones/Zones.php b/src/Models/Zones/Zones.php new file mode 100644 index 0000000..95d9601 --- /dev/null +++ b/src/Models/Zones/Zones.php @@ -0,0 +1,221 @@ + $primary_nameservers + * @param array $rrsets + * @param string|null $zonefile + * @return APIResponse|null + * @throws APIException + */ + public function create(string $name, string $mode, ?int $ttl = null, ?array $labels = [], ?array $primary_nameservers = [], ?array $rrsets = [], ?string $zonefile = '') + { + $parameters = [ + 'name' => $name, + 'mode' => $mode, + ]; + if($ttl !== null) { + $parameters['ttl'] = $ttl; + } + if (!empty($labels)) { + $parameters['labels'] = $labels; + } + if (!empty($rrsets)) { + $parameters['rrsets'] = []; + foreach ($rrsets as $rrset) { + $parameters['rrsets'][] = $rrset->__toRequest(); + } + } + if (!empty($primary_nameservers)) { + $parameters['primary_nameservers'] = $primary_nameservers; + } + + if (!empty($zonefile)) { + $parameters['zonefile'] = $zonefile; + } + $response = $this->httpClient->post('zones', [ + 'json' => $parameters, + ]); + + if (!HetznerAPIClient::hasError($response)) { + $payload = json_decode((string)$response->getBody()); + + return APIResponse::create([ + 'action' => Action::parse($payload->action), + 'zone' => Zone::parse($payload->zone), + ], $response->getHeaders()); + } + + return null; + } + + /** + * Returns all existing zone objects. + * + * @see https://docs.hetzner.cloud/#resources-zones-get + * + * @param RequestOpts|null $requestOpts + * @return array + * + * @throws \LKDev\HetznerCloud\APIException + */ + public function all(?RequestOpts $requestOpts = null): array + { + if ($requestOpts == null) { + $requestOpts = new ZoneRequestOpts(); + } + + return $this->_all($requestOpts); + } + + /** + * List zone objects. + * + * @see https://docs.hetzner.cloud/#resources-zones-get + * + * @param RequestOpts|null $requestOpts + * @return APIResponse|null + * + * @throws \LKDev\HetznerCloud\APIException + */ + public function list(?RequestOpts $requestOpts = null): ?APIResponse + { + if ($requestOpts == null) { + $requestOpts = new ZoneRequestOpts(); + } + $response = $this->httpClient->get('zones' . $requestOpts->buildQuery()); + if (!HetznerAPIClient::hasError($response)) { + $resp = json_decode((string)$response->getBody()); + + return APIResponse::create([ + 'meta' => Meta::parse($resp->meta), + $this->_getKeys()['many'] => self::parse($resp->{$this->_getKeys()['many']})->{$this->_getKeys()['many']}, + ], $response->getHeaders()); + } + + return null; + } + + /** + * Returns a specific zone object by its name. The zone must exist inside the project. + * + * @see https://docs.hetzner.cloud/#resources-zones-get + * + * @param string $zoneName + * @return \LKDev\HetznerCloud\Models\Zones\Zone|null + * + * @throws \LKDev\HetznerCloud\APIException + */ + public function getByName(string $zoneName): ?Zone + { + $response = $this->httpClient->get('zones/' . $zoneName); + if (!HetznerAPIClient::hasError($response)) { + return Zone::parse(json_decode((string)$response->getBody())->{$this->_getKeys()['one']}); + } + + return null; + } + + /** + * Returns a specific zone object by its id. The zone must exist inside the project. + * + * @see https://docs.hetzner.cloud/#resources-zones-get + * + * @param int $zoneId + * @return \LKDev\HetznerCloud\Models\Zones\Zone|null + * + * @throws \LKDev\HetznerCloud\APIException + */ + public function getById(int $zoneId): ?Zone + { + $response = $this->httpClient->get('zones/' . $zoneId); + if (!HetznerAPIClient::hasError($response)) { + return Zone::parse(json_decode((string)$response->getBody())->{$this->_getKeys()['one']}); + } + + return null; + } + + /** + * Deletes a specific zone object by its id. The zone must exist inside the project. + * + * @see https://docs.hetzner.cloud/#zones-delete-a-zone + * + * @param int $zoneId + * @return \LKDev\HetznerCloud\Models\Actions\Action|null + * + * @throws \LKDev\HetznerCloud\APIException + */ + public function deleteById(int $zoneId): ?Action + { + $response = $this->httpClient->delete('zones/' . $zoneId); + if (!HetznerAPIClient::hasError($response)) { + $payload = json_decode((string)$response->getBody()); + + return Action::parse($payload->action); + } + + return null; + } + + + /** + * @param $input + * @return $this + */ + public function setAdditionalData($input) + { + $this->zones = collect($input) + ->map(function ($zone) { + if ($zone != null) { + return Zone::parse($zone); + } + + return null; + }) + ->toArray(); + + return $this; + } + + /** + * @param $input + * @return static + */ + public static function parse($input) + { + return (new self())->setAdditionalData($input); + } + + /** + * @return array + */ + public function _getKeys(): array + { + return ['one' => 'zone', 'many' => 'zones']; + } +} diff --git a/tests/Unit/Models/Zones/ZoneTest.php b/tests/Unit/Models/Zones/ZoneTest.php new file mode 100644 index 0000000..3ce2f02 --- /dev/null +++ b/tests/Unit/Models/Zones/ZoneTest.php @@ -0,0 +1,61 @@ +hetznerApi->getHttpClient()); + + $this->mockHandler->append(new Response(200, [], file_get_contents(__DIR__.'/fixtures/zone.json'))); + $this->zone = $tmp->getById(4711); + } + + + public function testDelete() + { + $this->mockHandler->append(new Response(200, [], $this->getGenericActionResponse('delete_zone'))); + $resp = $this->zone->delete(); + + $this->assertEquals('delete_zone', $resp->action->command); + $this->assertEquals($this->zone->id, $resp->action->resources[0]->id); + $this->assertEquals('zone', $resp->action->resources[0]->type); + $this->assertLastRequestEquals('DELETE', '/zones/4711'); + } + + public function testUpdate() + { + $this->mockHandler->append(new Response(200, [], file_get_contents(__DIR__.'/fixtures/zone.json'))); + $this->zone->update(['name' => 'new-name']); + $this->assertLastRequestEquals('PUT', '/zones/4711'); + $this->assertLastRequestBodyParametersEqual(['name' => 'new-name']); + } + + public function testChangeProtection() + { + $this->mockHandler->append(new Response(200, [], file_get_contents(__DIR__.'/fixtures/zone_action_change_protection.json'))); + $apiResponse = $this->zone->changeProtection(true); + $this->assertEquals('change_protection', $apiResponse->action->command); + $this->assertEquals($this->zone->id, $apiResponse->action->resources[0]->id); + $this->assertEquals('zone', $apiResponse->action->resources[0]->type); + $this->assertLastRequestEquals('POST', '/zones/4711/actions/change_protection'); + $this->assertLastRequestBodyParametersEqual(['delete' => true]); + } + + protected function getGenericActionResponse(string $command) + { + return str_replace('$command', $command, file_get_contents(__DIR__.'/fixtures/zone_action_generic.json')); + } +} diff --git a/tests/Unit/Models/Zones/ZonesTest.php b/tests/Unit/Models/Zones/ZonesTest.php new file mode 100644 index 0000000..c68c029 --- /dev/null +++ b/tests/Unit/Models/Zones/ZonesTest.php @@ -0,0 +1,132 @@ +zones = new zones($this->hetznerApi->getHttpClient()); + } + + public function testCreatePrimarySimple() + { + $this->mockHandler->append(new Response(200, [], file_get_contents(__DIR__ . '/fixtures/zone_create.json'))); + $resp = $this->zones->create('example.com', ZoneMode::PRIMARY); + + $Zone = $resp->getResponsePart('zone'); + $this->assertEquals($Zone->id, 4711); + $this->assertEquals($Zone->name, 'example.com'); + + $this->assertNotNull($resp->action); + + $this->assertLastRequestEquals('POST', '/zones'); + $this->assertLastRequestBodyParametersEqual(['name' => 'example.com', 'mode' => 'primary']); + } + + public function testCreatePrimaryFull() + { + $this->mockHandler->append(new Response(200, [], file_get_contents(__DIR__ . '/fixtures/zone_create.json'))); + $resp = $this->zones->create('example.com', ZoneMode::PRIMARY, 10, ["key" => "value"], [], [ + new RRSet("", "@", "A", 3600, [ + new Record("192.0.2.1", "my comment") + ], [], RRSetProtection::parse(['change' => false])), + ]); + + $Zone = $resp->getResponsePart('zone'); + $this->assertEquals($Zone->id, 4711); + $this->assertEquals($Zone->name, 'example.com'); + + $this->assertNotNull($resp->action); + + $this->assertLastRequestEquals('POST', '/zones'); + $this->assertLastRequestBodyParametersEqual(['name' => 'example.com', + 'mode' => 'primary', + 'ttl' => 10, + "labels" => ["key" => "value"], + "rrsets" => [["type" => "A", "name" => "@", "ttl" => 3600, "records" => [["value" => "192.0.2.1", "comment" => "my comment"]]]]]); + } + + public function testCreateSecondary() + { + $this->mockHandler->append(new Response(200, [], file_get_contents(__DIR__ . '/fixtures/zone_create.json'))); + $resp = $this->zones->create('example.com', ZoneMode::SECONDARY, 10, ["key" => "value"], [ + new PrimaryNameserver("192.168.178.1", 53) + ],); + + $Zone = $resp->getResponsePart('zone'); + $this->assertEquals($Zone->id, 4711); + $this->assertEquals($Zone->name, 'example.com'); + + $this->assertNotNull($resp->action); + + $this->assertLastRequestEquals('POST', '/zones'); + $this->assertLastRequestBodyParametersEqual([ + 'name' => 'example.com', + 'mode' => 'secondary', + 'ttl' => 10, + "labels" => ["key" => "value"], + "primary_nameservers" => [["address" => "192.168.178.1", "port" => 53]]]);; + } + + public function testGetByName() + { + $this->mockHandler->append(new Response(200, [], file_get_contents(__DIR__ . '/fixtures/zone.json'))); + $Zone = $this->zones->getByName('example.com'); + $this->assertEquals(4711, $Zone->id); + $this->assertEquals('example.com', $Zone->name); + + $this->assertLastRequestEquals('GET', '/zones/example.com'); + } + + public function testGet() + { + $this->mockHandler->append(new Response(200, [], file_get_contents(__DIR__ . '/fixtures/zone.json'))); + $Zone = $this->zones->get(4711); + $this->assertEquals($Zone->id, 4711); + $this->assertEquals($Zone->name, 'example.com'); + + $this->assertLastRequestEquals('GET', '/zones/4711'); + } + + public function testAll() + { + $this->mockHandler->append(new Response(200, [], file_get_contents(__DIR__ . '/fixtures/zones.json'))); + $zones = $this->zones->all(); + $this->assertCount(1, $zones); + $Zone = $zones[0]; + $this->assertEquals($Zone->id, 4711); + $this->assertEquals($Zone->name, 'example.com'); + + $this->assertLastRequestEquals('GET', '/zones'); + } + + public function testList() + { + $this->mockHandler->append(new Response(200, [], file_get_contents(__DIR__ . '/fixtures/zones.json'))); + $zones = $this->zones->list()->zones; + $this->assertCount(1, $zones); + $Zone = $zones[0]; + $this->assertEquals($Zone->id, 4711); + $this->assertEquals($Zone->name, 'example.com'); + + $this->assertLastRequestEquals('GET', '/zones'); + } +} diff --git a/tests/Unit/Models/Zones/fixtures/zone.json b/tests/Unit/Models/Zones/fixtures/zone.json new file mode 100644 index 0000000..9f38eed --- /dev/null +++ b/tests/Unit/Models/Zones/fixtures/zone.json @@ -0,0 +1,44 @@ +{ + "zone": { + "id": 4711, + "name": "example.com", + "created": "2016-01-30T23:55:00+00:00", + "mode": "primary", + "primary_nameservers": [ + { + "address": "198.51.100.1", + "port": 53 + }, + { + "address": "203.0.113.1", + "port": 53 + } + ], + "labels": { + "environment": "prod", + "example.com/my": "label", + "just-a-key": "" + }, + "protection": { + "delete": false + }, + "ttl": 10800, + "status": "ok", + "record_count": 0, + "authoritative_nameservers": { + "assigned": [ + "hydrogen.ns.hetzner.com.", + "oxygen.ns.hetzner.com.", + "helium.ns.hetzner.de." + ], + "delegated": [ + "hydrogen.ns.hetzner.com.", + "oxygen.ns.hetzner.com.", + "helium.ns.hetzner.de." + ], + "delegation_last_check": "2016-01-30T23:55:00+00:00", + "delegation_status": "valid" + }, + "registrar": "hetzner" + } +} diff --git a/tests/Unit/Models/Zones/fixtures/zone_action_change_protection.json b/tests/Unit/Models/Zones/fixtures/zone_action_change_protection.json new file mode 100644 index 0000000..9207027 --- /dev/null +++ b/tests/Unit/Models/Zones/fixtures/zone_action_change_protection.json @@ -0,0 +1,17 @@ +{ + "action": { + "id": 1, + "command": "change_protection", + "status": "running", + "progress": 50, + "started": "2016-01-30T23:55:00+00:00", + "finished": null, + "resources": [ + { + "id": 4711, + "type": "zone" + } + ], + "error": null + } +} diff --git a/tests/Unit/Models/Zones/fixtures/zone_action_generic.json b/tests/Unit/Models/Zones/fixtures/zone_action_generic.json new file mode 100644 index 0000000..e070683 --- /dev/null +++ b/tests/Unit/Models/Zones/fixtures/zone_action_generic.json @@ -0,0 +1,17 @@ +{ + "action": { + "id": 13, + "command": "$command", + "status": "running", + "progress": 0, + "started": "2016-01-30T23:50:00+00:00", + "finished": null, + "resources": [ + { + "id": 4711, + "type": "zone" + } + ], + "error": null + } +} diff --git a/tests/Unit/Models/Zones/fixtures/zone_create.json b/tests/Unit/Models/Zones/fixtures/zone_create.json new file mode 100644 index 0000000..e7c46fb --- /dev/null +++ b/tests/Unit/Models/Zones/fixtures/zone_create.json @@ -0,0 +1,47 @@ +{ + "zone": { + "id": 4711, + "name": "example.com", + "mode": "primary", + "status": "ok", + "ttl": 3600, + "record_count": 4, + "protection": { + "delete": false + }, + "labels": { + "key": "value" + }, + "created": "2016-01-30T23:50:00+00:00", + "registrar": "hetzner", + "authoritative_nameservers": { + "assigned": [ + "hydrogen.ns.hetzner.com.", + "oxygen.ns.hetzner.com.", + "helium.ns.hetzner.de." + ], + "delegated": [ + "hydrogen.ns.hetzner.com.", + "oxygen.ns.hetzner.com.", + "helium.ns.hetzner.de." + ], + "delegation_last_check": "2016-01-30T23:50:00+00:00", + "delegation_status": "valid" + } + }, + "action": { + "id": 1, + "command": "create_zone", + "status": "running", + "progress": 50, + "started": "2016-01-30T23:50:00+00:00", + "finished": null, + "resources": [ + { + "id": 42, + "type": "zone" + } + ], + "error": null + } +} diff --git a/tests/Unit/Models/Zones/fixtures/zones.json b/tests/Unit/Models/Zones/fixtures/zones.json new file mode 100644 index 0000000..3700d59 --- /dev/null +++ b/tests/Unit/Models/Zones/fixtures/zones.json @@ -0,0 +1,56 @@ +{ + "zones": [ + { + "id": 4711, + "name": "example.com", + "created": "2016-01-30T23:55:00+00:00", + "mode": "primary", + "primary_nameservers": [ + { + "address": "198.51.100.1", + "port": 53 + }, + { + "address": "203.0.113.1", + "port": 53 + } + ], + "labels": { + "environment": "prod", + "example.com/my": "label", + "just-a-key": "" + }, + "protection": { + "delete": false + }, + "ttl": 10800, + "status": "ok", + "record_count": 0, + "authoritative_nameservers": { + "assigned": [ + "hydrogen.ns.hetzner.com.", + "oxygen.ns.hetzner.com.", + "helium.ns.hetzner.de." + ], + "delegated": [ + "hydrogen.ns.hetzner.com.", + "oxygen.ns.hetzner.com.", + "helium.ns.hetzner.de." + ], + "delegation_last_check": "2016-01-30T23:55:00+00:00", + "delegation_status": "valid" + }, + "registrar": "hetzner" + } + ], + "meta": { + "pagination": { + "page": 1, + "per_page": 25, + "previous_page": null, + "next_page": null, + "last_page": 1, + "total_entries": 1 + } + } +} From 7b7b8c7aadfbfe52de09e381229f215d29795ca2 Mon Sep 17 00:00:00 2001 From: StyleCI Bot Date: Mon, 13 Oct 2025 18:52:21 +0000 Subject: [PATCH 02/14] Apply fixes from StyleCI --- src/HetznerAPIClient.php | 21 +++--- src/Models/Zones/AuthoritativeNameservers.php | 8 +-- src/Models/Zones/PrimaryNameserver.php | 5 +- src/Models/Zones/RRSet.php | 23 +++---- src/Models/Zones/RRSetProtection.php | 8 +-- src/Models/Zones/Zone.php | 29 ++++----- src/Models/Zones/ZoneRequestOpts.php | 10 +-- src/Models/Zones/Zones.php | 65 +++++++++---------- tests/Unit/Models/Zones/ZoneTest.php | 1 - tests/Unit/Models/Zones/ZonesTest.php | 36 +++++----- 10 files changed, 96 insertions(+), 110 deletions(-) diff --git a/src/HetznerAPIClient.php b/src/HetznerAPIClient.php index 8938bff..fc004fa 100644 --- a/src/HetznerAPIClient.php +++ b/src/HetznerAPIClient.php @@ -64,9 +64,9 @@ class HetznerAPIClient protected GuzzleClient $httpClient; /** - * @param string $apiToken - * @param string $baseUrl - * @param string $userAgent + * @param string $apiToken + * @param string $baseUrl + * @param string $userAgent */ public function __construct(string $apiToken, $baseUrl = 'https://api.hetzner.cloud/v1/', $userAgent = '') { @@ -102,7 +102,7 @@ public function getBaseUrl(): string } /** - * @param string $userAgent + * @param string $userAgent * @return HetznerAPIClient */ public function setUserAgent(string $userAgent): self @@ -113,7 +113,7 @@ public function setUserAgent(string $userAgent): self } /** - * @param string $baseUrl + * @param string $baseUrl * @return HetznerAPIClient */ public function setBaseUrl(string $baseUrl): self @@ -142,13 +142,13 @@ public function setHttpClient(GuzzleClient $client): self } /** - * @param \Psr\Http\Message\ResponseInterface $response + * @param \Psr\Http\Message\ResponseInterface $response * * @throws \LKDev\HetznerCloud\APIException */ public static function throwError(ResponseInterface $response) { - $body = (string)$response->getBody(); + $body = (string) $response->getBody(); if (strlen($body) > 0) { $error = \GuzzleHttp\json_decode($body); throw new APIException(APIResponse::create([ @@ -161,15 +161,15 @@ public static function throwError(ResponseInterface $response) } /** - * @param \Psr\Http\Message\ResponseInterface $response + * @param \Psr\Http\Message\ResponseInterface $response * @return bool * * @throws \LKDev\HetznerCloud\APIException */ public static function hasError(ResponseInterface $response) { - $responseDecoded = json_decode((string)$response->getBody()); - if (strlen((string)$response->getBody()) > 0) { + $responseDecoded = json_decode((string) $response->getBody()); + if (strlen((string) $response->getBody()) > 0) { if (property_exists($responseDecoded, 'error')) { self::throwError($response); @@ -328,7 +328,6 @@ public function loadBalancerTypes() return new LoadBalancerTypes($this->httpClient); } - public function zones() { return new Zones($this->httpClient); diff --git a/src/Models/Zones/AuthoritativeNameservers.php b/src/Models/Zones/AuthoritativeNameservers.php index 56a7985..bb46fd1 100644 --- a/src/Models/Zones/AuthoritativeNameservers.php +++ b/src/Models/Zones/AuthoritativeNameservers.php @@ -10,10 +10,10 @@ class AuthoritativeNameservers public string $delegation_status; /** - * @param array $assigned - * @param array $delegated - * @param string $delegation_last_check - * @param string $delegation_status + * @param array $assigned + * @param array $delegated + * @param string $delegation_last_check + * @param string $delegation_status */ public function __construct(array $assigned, array $delegated, string $delegation_last_check, string $delegation_status) { diff --git a/src/Models/Zones/PrimaryNameserver.php b/src/Models/Zones/PrimaryNameserver.php index 9676555..ba25289 100644 --- a/src/Models/Zones/PrimaryNameserver.php +++ b/src/Models/Zones/PrimaryNameserver.php @@ -8,8 +8,8 @@ class PrimaryNameserver public int $port; /** - * @param string $address - * @param int $port + * @param string $address + * @param int $port */ public function __construct(string $address, int $port) { @@ -17,7 +17,6 @@ public function __construct(string $address, int $port) $this->address = $address; } - public static function fromResponse(array $response): PrimaryNameserver { return new self($response['address'], $response['port']); diff --git a/src/Models/Zones/RRSet.php b/src/Models/Zones/RRSet.php index b7360dc..11d5bd3 100644 --- a/src/Models/Zones/RRSet.php +++ b/src/Models/Zones/RRSet.php @@ -2,8 +2,6 @@ namespace LKDev\HetznerCloud\Models\Zones; -use LKDev\HetznerCloud\Models\Protection; - class RRSet { public string $id; @@ -15,13 +13,13 @@ class RRSet public RRSetProtection $protection; /** - * @param string $id - * @param string $name - * @param string $type - * @param int $ttl - * @param array $records - * @param array|null $labels - * @param RRSetProtection|null $protection + * @param string $id + * @param string $name + * @param string $type + * @param int $ttl + * @param array $records + * @param array|null $labels + * @param RRSetProtection|null $protection */ public function __construct(string $id, string $name, string $type, int $ttl, array $records, ?array $labels, ?RRSetProtection $protection) { @@ -42,25 +40,24 @@ public static function fromResponse(array $data): RRSet public function __toRequest(): array { $r = [ - "name" => $this->name, + 'name' => $this->name, 'type' => $this->type, 'ttl' => $this->ttl, 'records' => $this->records, ]; - if (!empty($this->labels)) { + if (! empty($this->labels)) { $r['labels'] = $this->labels; } + return $r; } } - class Record { public string $value; public string $comment; - public function __construct(string $value, string $comment) { $this->value = $value; diff --git a/src/Models/Zones/RRSetProtection.php b/src/Models/Zones/RRSetProtection.php index c17dc7c..f721933 100644 --- a/src/Models/Zones/RRSetProtection.php +++ b/src/Models/Zones/RRSetProtection.php @@ -19,11 +19,10 @@ class RRSetProtection extends Model */ public $change; - /** * Protection constructor. * - * @param bool $delete + * @param bool $delete */ public function __construct(bool $delete) { @@ -33,7 +32,7 @@ public function __construct(bool $delete) } /** - * @param array $input + * @param array $input * @return ?RRSetProtection */ public static function parse($input) @@ -41,9 +40,10 @@ public static function parse($input) if ($input == null) { return null; } - if (!is_array($input)) { + if (! is_array($input)) { return null; } + return new self($input['change'] ?? false); } } diff --git a/src/Models/Zones/Zone.php b/src/Models/Zones/Zone.php index 2839b0c..68fcd5c 100644 --- a/src/Models/Zones/Zone.php +++ b/src/Models/Zones/Zone.php @@ -38,11 +38,9 @@ class Zone extends Model implements Resource public string $mode; /** * @var array - * */ public array $primary_nameservers; - /** * @var array|\LKDev\HetznerCloud\Models\Protection */ @@ -65,7 +63,6 @@ class Zone extends Model implements Resource /** * @var string - * */ public string $registrar; @@ -75,8 +72,8 @@ class Zone extends Model implements Resource public AuthoritativeNameservers $authoritative_nameservers; /** - * @param int $zoneId - * @param GuzzleClient|null $httpClient + * @param int $zoneId + * @param GuzzleClient|null $httpClient */ public function __construct(int $zoneId, ?GuzzleClient $httpClient = null) { @@ -134,9 +131,9 @@ public function reload() public function delete(): ?APIResponse { $response = $this->httpClient->delete($this->replaceZoneIdInUri('zones/{id}')); - if (!HetznerAPIClient::hasError($response)) { + if (! HetznerAPIClient::hasError($response)) { return APIResponse::create([ - 'action' => Action::parse(json_decode((string)$response->getBody())->action), + 'action' => Action::parse(json_decode((string) $response->getBody())->action), ], $response->getHeaders()); } @@ -148,7 +145,7 @@ public function delete(): ?APIResponse * * @see https://docs.hetzner.cloud/reference/cloud#zones-update-a-zone * - * @param array $data + * @param array $data * @return APIResponse|null * * @throws \LKDev\HetznerCloud\APIException @@ -158,9 +155,9 @@ public function update(array $data) $response = $this->httpClient->put($this->replaceZoneIdInUri('zones/{id}'), [ 'json' => $data, ]); - if (!HetznerAPIClient::hasError($response)) { + if (! HetznerAPIClient::hasError($response)) { return APIResponse::create([ - 'zone' => self::parse(json_decode((string)$response->getBody())->zone), + 'zone' => self::parse(json_decode((string) $response->getBody())->zone), ], $response->getHeaders()); } @@ -172,21 +169,21 @@ public function update(array $data) * * @see https://docs.hetzner.cloud/#zone-actions-change-zone-protection * - * @param bool $delete + * @param bool $delete * @return APIResponse|null * * @throws \LKDev\HetznerCloud\APIException */ public function changeProtection(bool $delete = true): ?APIResponse { - $response = $this->httpClient->post('zones/' . $this->id . '/actions/change_protection', [ + $response = $this->httpClient->post('zones/'.$this->id.'/actions/change_protection', [ 'json' => [ 'delete' => $delete, ], ]); - if (!HetznerAPIClient::hasError($response)) { + if (! HetznerAPIClient::hasError($response)) { return APIResponse::create([ - 'action' => Action::parse(json_decode((string)$response->getBody())->action), + 'action' => Action::parse(json_decode((string) $response->getBody())->action), ], $response->getHeaders()); } @@ -194,7 +191,7 @@ public function changeProtection(bool $delete = true): ?APIResponse } /** - * @param string $uri + * @param string $uri * @return string */ protected function replaceZoneIdInUri(string $uri): string @@ -215,5 +212,3 @@ public static function parse($input) return (new self($input->id))->setAdditionalData($input); } } - - diff --git a/src/Models/Zones/ZoneRequestOpts.php b/src/Models/Zones/ZoneRequestOpts.php index bd7dc8c..33d9991 100644 --- a/src/Models/Zones/ZoneRequestOpts.php +++ b/src/Models/Zones/ZoneRequestOpts.php @@ -22,11 +22,11 @@ class ZoneRequestOpts extends RequestOpts /** * RequestOpts constructor. * - * @param string|null $name - * @param string|null $mode - * @param int|null $perPage - * @param int|null $page - * @param string|null $labelSelector + * @param string|null $name + * @param string|null $mode + * @param int|null $perPage + * @param int|null $page + * @param string|null $labelSelector */ public function __construct(?string $name = null, ?string $mode = null, ?int $perPage = null, ?int $page = null, ?string $labelSelector = null) { diff --git a/src/Models/Zones/Zones.php b/src/Models/Zones/Zones.php index 95d9601..eb224d3 100644 --- a/src/Models/Zones/Zones.php +++ b/src/Models/Zones/Zones.php @@ -8,7 +8,6 @@ use LKDev\HetznerCloud\Models\Actions\Action; use LKDev\HetznerCloud\Models\Meta; use LKDev\HetznerCloud\Models\Model; -use LKDev\HetznerCloud\Models\Servers\Server; use LKDev\HetznerCloud\RequestOpts; use LKDev\HetznerCloud\Traits\GetFunctionTrait; @@ -22,14 +21,15 @@ class Zones extends Model protected $zones; /** - * @param string $name - * @param string $mode - * @param int|null $ttl - * @param array|null $labels - * @param array $primary_nameservers - * @param array $rrsets - * @param string|null $zonefile + * @param string $name + * @param string $mode + * @param int|null $ttl + * @param array|null $labels + * @param array $primary_nameservers + * @param array $rrsets + * @param string|null $zonefile * @return APIResponse|null + * * @throws APIException */ public function create(string $name, string $mode, ?int $ttl = null, ?array $labels = [], ?array $primary_nameservers = [], ?array $rrsets = [], ?string $zonefile = '') @@ -38,31 +38,31 @@ public function create(string $name, string $mode, ?int $ttl = null, ?array $lab 'name' => $name, 'mode' => $mode, ]; - if($ttl !== null) { + if ($ttl !== null) { $parameters['ttl'] = $ttl; } - if (!empty($labels)) { + if (! empty($labels)) { $parameters['labels'] = $labels; } - if (!empty($rrsets)) { + if (! empty($rrsets)) { $parameters['rrsets'] = []; foreach ($rrsets as $rrset) { $parameters['rrsets'][] = $rrset->__toRequest(); } } - if (!empty($primary_nameservers)) { + if (! empty($primary_nameservers)) { $parameters['primary_nameservers'] = $primary_nameservers; } - if (!empty($zonefile)) { + if (! empty($zonefile)) { $parameters['zonefile'] = $zonefile; } $response = $this->httpClient->post('zones', [ 'json' => $parameters, ]); - if (!HetznerAPIClient::hasError($response)) { - $payload = json_decode((string)$response->getBody()); + if (! HetznerAPIClient::hasError($response)) { + $payload = json_decode((string) $response->getBody()); return APIResponse::create([ 'action' => Action::parse($payload->action), @@ -78,7 +78,7 @@ public function create(string $name, string $mode, ?int $ttl = null, ?array $lab * * @see https://docs.hetzner.cloud/#resources-zones-get * - * @param RequestOpts|null $requestOpts + * @param RequestOpts|null $requestOpts * @return array * * @throws \LKDev\HetznerCloud\APIException @@ -97,7 +97,7 @@ public function all(?RequestOpts $requestOpts = null): array * * @see https://docs.hetzner.cloud/#resources-zones-get * - * @param RequestOpts|null $requestOpts + * @param RequestOpts|null $requestOpts * @return APIResponse|null * * @throws \LKDev\HetznerCloud\APIException @@ -107,9 +107,9 @@ public function list(?RequestOpts $requestOpts = null): ?APIResponse if ($requestOpts == null) { $requestOpts = new ZoneRequestOpts(); } - $response = $this->httpClient->get('zones' . $requestOpts->buildQuery()); - if (!HetznerAPIClient::hasError($response)) { - $resp = json_decode((string)$response->getBody()); + $response = $this->httpClient->get('zones'.$requestOpts->buildQuery()); + if (! HetznerAPIClient::hasError($response)) { + $resp = json_decode((string) $response->getBody()); return APIResponse::create([ 'meta' => Meta::parse($resp->meta), @@ -125,16 +125,16 @@ public function list(?RequestOpts $requestOpts = null): ?APIResponse * * @see https://docs.hetzner.cloud/#resources-zones-get * - * @param string $zoneName + * @param string $zoneName * @return \LKDev\HetznerCloud\Models\Zones\Zone|null * * @throws \LKDev\HetznerCloud\APIException */ public function getByName(string $zoneName): ?Zone { - $response = $this->httpClient->get('zones/' . $zoneName); - if (!HetznerAPIClient::hasError($response)) { - return Zone::parse(json_decode((string)$response->getBody())->{$this->_getKeys()['one']}); + $response = $this->httpClient->get('zones/'.$zoneName); + if (! HetznerAPIClient::hasError($response)) { + return Zone::parse(json_decode((string) $response->getBody())->{$this->_getKeys()['one']}); } return null; @@ -145,16 +145,16 @@ public function getByName(string $zoneName): ?Zone * * @see https://docs.hetzner.cloud/#resources-zones-get * - * @param int $zoneId + * @param int $zoneId * @return \LKDev\HetznerCloud\Models\Zones\Zone|null * * @throws \LKDev\HetznerCloud\APIException */ public function getById(int $zoneId): ?Zone { - $response = $this->httpClient->get('zones/' . $zoneId); - if (!HetznerAPIClient::hasError($response)) { - return Zone::parse(json_decode((string)$response->getBody())->{$this->_getKeys()['one']}); + $response = $this->httpClient->get('zones/'.$zoneId); + if (! HetznerAPIClient::hasError($response)) { + return Zone::parse(json_decode((string) $response->getBody())->{$this->_getKeys()['one']}); } return null; @@ -165,16 +165,16 @@ public function getById(int $zoneId): ?Zone * * @see https://docs.hetzner.cloud/#zones-delete-a-zone * - * @param int $zoneId + * @param int $zoneId * @return \LKDev\HetznerCloud\Models\Actions\Action|null * * @throws \LKDev\HetznerCloud\APIException */ public function deleteById(int $zoneId): ?Action { - $response = $this->httpClient->delete('zones/' . $zoneId); - if (!HetznerAPIClient::hasError($response)) { - $payload = json_decode((string)$response->getBody()); + $response = $this->httpClient->delete('zones/'.$zoneId); + if (! HetznerAPIClient::hasError($response)) { + $payload = json_decode((string) $response->getBody()); return Action::parse($payload->action); } @@ -182,7 +182,6 @@ public function deleteById(int $zoneId): ?Action return null; } - /** * @param $input * @return $this diff --git a/tests/Unit/Models/Zones/ZoneTest.php b/tests/Unit/Models/Zones/ZoneTest.php index 3ce2f02..276ca6a 100644 --- a/tests/Unit/Models/Zones/ZoneTest.php +++ b/tests/Unit/Models/Zones/ZoneTest.php @@ -23,7 +23,6 @@ public function setUp(): void $this->zone = $tmp->getById(4711); } - public function testDelete() { $this->mockHandler->append(new Response(200, [], $this->getGenericActionResponse('delete_zone'))); diff --git a/tests/Unit/Models/Zones/ZonesTest.php b/tests/Unit/Models/Zones/ZonesTest.php index c68c029..38b09f3 100644 --- a/tests/Unit/Models/Zones/ZonesTest.php +++ b/tests/Unit/Models/Zones/ZonesTest.php @@ -3,8 +3,6 @@ namespace LKDev\Tests\Unit\Models\zones; use GuzzleHttp\Psr7\Response; -use LKDev\HetznerCloud\Models\Locations\Location; -use LKDev\HetznerCloud\Models\Protection; use LKDev\HetznerCloud\Models\Zones\PrimaryNameserver; use LKDev\HetznerCloud\Models\Zones\Record; use LKDev\HetznerCloud\Models\Zones\RRSet; @@ -28,7 +26,7 @@ public function setUp(): void public function testCreatePrimarySimple() { - $this->mockHandler->append(new Response(200, [], file_get_contents(__DIR__ . '/fixtures/zone_create.json'))); + $this->mockHandler->append(new Response(200, [], file_get_contents(__DIR__.'/fixtures/zone_create.json'))); $resp = $this->zones->create('example.com', ZoneMode::PRIMARY); $Zone = $resp->getResponsePart('zone'); @@ -43,10 +41,10 @@ public function testCreatePrimarySimple() public function testCreatePrimaryFull() { - $this->mockHandler->append(new Response(200, [], file_get_contents(__DIR__ . '/fixtures/zone_create.json'))); - $resp = $this->zones->create('example.com', ZoneMode::PRIMARY, 10, ["key" => "value"], [], [ - new RRSet("", "@", "A", 3600, [ - new Record("192.0.2.1", "my comment") + $this->mockHandler->append(new Response(200, [], file_get_contents(__DIR__.'/fixtures/zone_create.json'))); + $resp = $this->zones->create('example.com', ZoneMode::PRIMARY, 10, ['key' => 'value'], [], [ + new RRSet('', '@', 'A', 3600, [ + new Record('192.0.2.1', 'my comment'), ], [], RRSetProtection::parse(['change' => false])), ]); @@ -60,16 +58,16 @@ public function testCreatePrimaryFull() $this->assertLastRequestBodyParametersEqual(['name' => 'example.com', 'mode' => 'primary', 'ttl' => 10, - "labels" => ["key" => "value"], - "rrsets" => [["type" => "A", "name" => "@", "ttl" => 3600, "records" => [["value" => "192.0.2.1", "comment" => "my comment"]]]]]); + 'labels' => ['key' => 'value'], + 'rrsets' => [['type' => 'A', 'name' => '@', 'ttl' => 3600, 'records' => [['value' => '192.0.2.1', 'comment' => 'my comment']]]]]); } public function testCreateSecondary() { - $this->mockHandler->append(new Response(200, [], file_get_contents(__DIR__ . '/fixtures/zone_create.json'))); - $resp = $this->zones->create('example.com', ZoneMode::SECONDARY, 10, ["key" => "value"], [ - new PrimaryNameserver("192.168.178.1", 53) - ],); + $this->mockHandler->append(new Response(200, [], file_get_contents(__DIR__.'/fixtures/zone_create.json'))); + $resp = $this->zones->create('example.com', ZoneMode::SECONDARY, 10, ['key' => 'value'], [ + new PrimaryNameserver('192.168.178.1', 53), + ], ); $Zone = $resp->getResponsePart('zone'); $this->assertEquals($Zone->id, 4711); @@ -82,13 +80,13 @@ public function testCreateSecondary() 'name' => 'example.com', 'mode' => 'secondary', 'ttl' => 10, - "labels" => ["key" => "value"], - "primary_nameservers" => [["address" => "192.168.178.1", "port" => 53]]]);; + 'labels' => ['key' => 'value'], + 'primary_nameservers' => [['address' => '192.168.178.1', 'port' => 53]]]); } public function testGetByName() { - $this->mockHandler->append(new Response(200, [], file_get_contents(__DIR__ . '/fixtures/zone.json'))); + $this->mockHandler->append(new Response(200, [], file_get_contents(__DIR__.'/fixtures/zone.json'))); $Zone = $this->zones->getByName('example.com'); $this->assertEquals(4711, $Zone->id); $this->assertEquals('example.com', $Zone->name); @@ -98,7 +96,7 @@ public function testGetByName() public function testGet() { - $this->mockHandler->append(new Response(200, [], file_get_contents(__DIR__ . '/fixtures/zone.json'))); + $this->mockHandler->append(new Response(200, [], file_get_contents(__DIR__.'/fixtures/zone.json'))); $Zone = $this->zones->get(4711); $this->assertEquals($Zone->id, 4711); $this->assertEquals($Zone->name, 'example.com'); @@ -108,7 +106,7 @@ public function testGet() public function testAll() { - $this->mockHandler->append(new Response(200, [], file_get_contents(__DIR__ . '/fixtures/zones.json'))); + $this->mockHandler->append(new Response(200, [], file_get_contents(__DIR__.'/fixtures/zones.json'))); $zones = $this->zones->all(); $this->assertCount(1, $zones); $Zone = $zones[0]; @@ -120,7 +118,7 @@ public function testAll() public function testList() { - $this->mockHandler->append(new Response(200, [], file_get_contents(__DIR__ . '/fixtures/zones.json'))); + $this->mockHandler->append(new Response(200, [], file_get_contents(__DIR__.'/fixtures/zones.json'))); $zones = $this->zones->list()->zones; $this->assertCount(1, $zones); $Zone = $zones[0]; From 2143ca26e1825888053cba800eb961a8dbbf8405 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Lukas=20K=C3=A4mmerling?= Date: Mon, 13 Oct 2025 21:03:18 +0200 Subject: [PATCH 03/14] Implement remaining zone actions --- src/Models/Zones/Zone.php | 65 +++++++++++++++++++ tests/Unit/Models/Zones/ZoneTest.php | 53 +++++++++++++-- .../Models/Zones/fixtures/zone_zonefile.json | 3 + 3 files changed, 117 insertions(+), 4 deletions(-) create mode 100644 tests/Unit/Models/Zones/fixtures/zone_zonefile.json diff --git a/src/Models/Zones/Zone.php b/src/Models/Zones/Zone.php index 68fcd5c..b1b1f34 100644 --- a/src/Models/Zones/Zone.php +++ b/src/Models/Zones/Zone.php @@ -190,6 +190,50 @@ public function changeProtection(bool $delete = true): ?APIResponse return null; } + public function exportZonefile(): ?APIResponse + { + $response = $this->httpClient->get('zones/' . $this->id . '/zonefile'); + if (!HetznerAPIClient::hasError($response)) { + return APIResponse::create([ + 'zonefile' => json_decode((string)$response->getBody())->zonefile, + ], $response->getHeaders()); + } + + return null; + } + + public function changeTTL(int $ttl): ?APIResponse + { + $response = $this->httpClient->post('zones/' . $this->id . '/actions/change_ttl', [ + 'json' => [ + 'ttl' => $ttl, + ], + ]); + if (!HetznerAPIClient::hasError($response)) { + return APIResponse::create([ + 'action' => Action::parse(json_decode((string)$response->getBody())->action), + ], $response->getHeaders()); + } + + return null; + } + + public function importZonefile(string $zonefile): ?APIResponse + { + $response = $this->httpClient->post('zones/' . $this->id . '/actions/import_zonefile', [ + 'json' => [ + 'zonefile' => $zonefile, + ], + ]); + if (!HetznerAPIClient::hasError($response)) { + return APIResponse::create([ + 'action' => Action::parse(json_decode((string)$response->getBody())->action), + ], $response->getHeaders()); + } + + return null; + } + /** * @param string $uri * @return string @@ -199,6 +243,7 @@ protected function replaceZoneIdInUri(string $uri): string return str_replace('{id}', $this->id, $uri); } + /** * @param $input * @return \LKDev\HetznerCloud\Models\Zones\Zone|static |null @@ -211,4 +256,24 @@ public static function parse($input) return (new self($input->id))->setAdditionalData($input); } + + /** + * @param array $primary_nameservers + * @return void# + */ + public function changePrimaryNameservers(array $primary_nameservers) + { + $response = $this->httpClient->post('zones/' . $this->id . '/actions/change_primary_nameservers', [ + 'json' => [ + 'primary_nameservers' => $primary_nameservers, + ], + ]); + if (!HetznerAPIClient::hasError($response)) { + return APIResponse::create([ + 'action' => Action::parse(json_decode((string)$response->getBody())->action), + ], $response->getHeaders()); + } + + return null; + } } diff --git a/tests/Unit/Models/Zones/ZoneTest.php b/tests/Unit/Models/Zones/ZoneTest.php index 276ca6a..5a4e033 100644 --- a/tests/Unit/Models/Zones/ZoneTest.php +++ b/tests/Unit/Models/Zones/ZoneTest.php @@ -3,6 +3,7 @@ namespace LKDev\Tests\Unit\Models\Zones; use GuzzleHttp\Psr7\Response; +use LKDev\HetznerCloud\Models\Zones\PrimaryNameserver; use LKDev\HetznerCloud\Models\Zones\Zone; use LKDev\HetznerCloud\Models\Zones\Zones; use LKDev\Tests\TestCase; @@ -19,7 +20,7 @@ public function setUp(): void parent::setUp(); $tmp = new Zones($this->hetznerApi->getHttpClient()); - $this->mockHandler->append(new Response(200, [], file_get_contents(__DIR__.'/fixtures/zone.json'))); + $this->mockHandler->append(new Response(200, [], file_get_contents(__DIR__ . '/fixtures/zone.json'))); $this->zone = $tmp->getById(4711); } @@ -36,7 +37,7 @@ public function testDelete() public function testUpdate() { - $this->mockHandler->append(new Response(200, [], file_get_contents(__DIR__.'/fixtures/zone.json'))); + $this->mockHandler->append(new Response(200, [], file_get_contents(__DIR__ . '/fixtures/zone.json'))); $this->zone->update(['name' => 'new-name']); $this->assertLastRequestEquals('PUT', '/zones/4711'); $this->assertLastRequestBodyParametersEqual(['name' => 'new-name']); @@ -44,7 +45,7 @@ public function testUpdate() public function testChangeProtection() { - $this->mockHandler->append(new Response(200, [], file_get_contents(__DIR__.'/fixtures/zone_action_change_protection.json'))); + $this->mockHandler->append(new Response(200, [], file_get_contents(__DIR__ . '/fixtures/zone_action_change_protection.json'))); $apiResponse = $this->zone->changeProtection(true); $this->assertEquals('change_protection', $apiResponse->action->command); $this->assertEquals($this->zone->id, $apiResponse->action->resources[0]->id); @@ -53,8 +54,52 @@ public function testChangeProtection() $this->assertLastRequestBodyParametersEqual(['delete' => true]); } + public function testChangeTTL() + { + $this->mockHandler->append(new Response(200, [], $this->getGenericActionResponse('change_ttl'))); + $apiResponse = $this->zone->changeTTL(50); + $this->assertEquals('change_ttl', $apiResponse->action->command); + $this->assertEquals($this->zone->id, $apiResponse->action->resources[0]->id); + $this->assertEquals('zone', $apiResponse->action->resources[0]->type); + $this->assertLastRequestEquals('POST', '/zones/4711/actions/change_ttl'); + $this->assertLastRequestBodyParametersEqual(['ttl' => 50]); + } + + public function testImportZonefile() + { + $this->mockHandler->append(new Response(200, [], $this->getGenericActionResponse('import_zonefile'))); + $apiResponse = $this->zone->importZonefile("zonefile_content"); + $this->assertEquals('import_zonefile', $apiResponse->action->command); + $this->assertEquals($this->zone->id, $apiResponse->action->resources[0]->id); + $this->assertEquals('zone', $apiResponse->action->resources[0]->type); + $this->assertLastRequestEquals('POST', '/zones/4711/actions/import_zonefile'); + $this->assertLastRequestBodyParametersEqual(['zonefile' => "zonefile_content"]); + } + + public function testTestChangePrimaryNameservers() + { + $this->mockHandler->append(new Response(200, [], $this->getGenericActionResponse('import_zonefile'))); + $apiResponse = $this->zone->changePrimaryNameservers([ + new PrimaryNameserver("192.168.178.1", 53) + ]); + $this->assertEquals('import_zonefile', $apiResponse->action->command); + $this->assertEquals($this->zone->id, $apiResponse->action->resources[0]->id); + $this->assertEquals('zone', $apiResponse->action->resources[0]->type); + $this->assertLastRequestEquals('POST', '/zones/4711/actions/change_primary_nameservers'); + $this->assertLastRequestBodyParametersEqual(['primary_nameservers' => [["address" => "192.168.178.1", "port" => 53]]]); + } + + + public function testExportZonefile() + { + $this->mockHandler->append(new Response(200, [], file_get_contents(__DIR__ . '/fixtures/zone_zonefile.json'))); + $apiResponse = $this->zone->exportZonefile(); + $this->assertNotEmpty($apiResponse->zonefile); + $this->assertLastRequestEquals('GET', '/zones/4711/zonefile'); + } + protected function getGenericActionResponse(string $command) { - return str_replace('$command', $command, file_get_contents(__DIR__.'/fixtures/zone_action_generic.json')); + return str_replace('$command', $command, file_get_contents(__DIR__ . '/fixtures/zone_action_generic.json')); } } diff --git a/tests/Unit/Models/Zones/fixtures/zone_zonefile.json b/tests/Unit/Models/Zones/fixtures/zone_zonefile.json new file mode 100644 index 0000000..7ce3906 --- /dev/null +++ b/tests/Unit/Models/Zones/fixtures/zone_zonefile.json @@ -0,0 +1,3 @@ +{ + "zonefile": "$ORIGIN\texample.com.\n$TTL\t3600\n\n@\tIN\tSOA\thydrogen.ns.hetzner.com. dns.hetzner.com. 2024010100 86400 10800 3600000 3600\n\n@\tIN\t10800\tNS\thydrogen.ns.hetzner.com. ; Some comment.\n@\tIN\t10800\tNS\toxygen.ns.hetzner.com.\n@\tIN\t10800\tNS\thelium.ns.hetzner.de.\n" +} From a2c33e2fdea5139bf955db1989b1ea00be33884c Mon Sep 17 00:00:00 2001 From: StyleCI Bot Date: Mon, 13 Oct 2025 19:03:27 +0000 Subject: [PATCH 04/14] Apply fixes from StyleCI --- src/Models/Zones/Zone.php | 27 +++++++++++++-------------- tests/Unit/Models/Zones/ZoneTest.php | 19 +++++++++---------- 2 files changed, 22 insertions(+), 24 deletions(-) diff --git a/src/Models/Zones/Zone.php b/src/Models/Zones/Zone.php index b1b1f34..cadf381 100644 --- a/src/Models/Zones/Zone.php +++ b/src/Models/Zones/Zone.php @@ -192,10 +192,10 @@ public function changeProtection(bool $delete = true): ?APIResponse public function exportZonefile(): ?APIResponse { - $response = $this->httpClient->get('zones/' . $this->id . '/zonefile'); - if (!HetznerAPIClient::hasError($response)) { + $response = $this->httpClient->get('zones/'.$this->id.'/zonefile'); + if (! HetznerAPIClient::hasError($response)) { return APIResponse::create([ - 'zonefile' => json_decode((string)$response->getBody())->zonefile, + 'zonefile' => json_decode((string) $response->getBody())->zonefile, ], $response->getHeaders()); } @@ -204,14 +204,14 @@ public function exportZonefile(): ?APIResponse public function changeTTL(int $ttl): ?APIResponse { - $response = $this->httpClient->post('zones/' . $this->id . '/actions/change_ttl', [ + $response = $this->httpClient->post('zones/'.$this->id.'/actions/change_ttl', [ 'json' => [ 'ttl' => $ttl, ], ]); - if (!HetznerAPIClient::hasError($response)) { + if (! HetznerAPIClient::hasError($response)) { return APIResponse::create([ - 'action' => Action::parse(json_decode((string)$response->getBody())->action), + 'action' => Action::parse(json_decode((string) $response->getBody())->action), ], $response->getHeaders()); } @@ -220,14 +220,14 @@ public function changeTTL(int $ttl): ?APIResponse public function importZonefile(string $zonefile): ?APIResponse { - $response = $this->httpClient->post('zones/' . $this->id . '/actions/import_zonefile', [ + $response = $this->httpClient->post('zones/'.$this->id.'/actions/import_zonefile', [ 'json' => [ 'zonefile' => $zonefile, ], ]); - if (!HetznerAPIClient::hasError($response)) { + if (! HetznerAPIClient::hasError($response)) { return APIResponse::create([ - 'action' => Action::parse(json_decode((string)$response->getBody())->action), + 'action' => Action::parse(json_decode((string) $response->getBody())->action), ], $response->getHeaders()); } @@ -243,7 +243,6 @@ protected function replaceZoneIdInUri(string $uri): string return str_replace('{id}', $this->id, $uri); } - /** * @param $input * @return \LKDev\HetznerCloud\Models\Zones\Zone|static |null @@ -258,19 +257,19 @@ public static function parse($input) } /** - * @param array $primary_nameservers + * @param array $primary_nameservers * @return void# */ public function changePrimaryNameservers(array $primary_nameservers) { - $response = $this->httpClient->post('zones/' . $this->id . '/actions/change_primary_nameservers', [ + $response = $this->httpClient->post('zones/'.$this->id.'/actions/change_primary_nameservers', [ 'json' => [ 'primary_nameservers' => $primary_nameservers, ], ]); - if (!HetznerAPIClient::hasError($response)) { + if (! HetznerAPIClient::hasError($response)) { return APIResponse::create([ - 'action' => Action::parse(json_decode((string)$response->getBody())->action), + 'action' => Action::parse(json_decode((string) $response->getBody())->action), ], $response->getHeaders()); } diff --git a/tests/Unit/Models/Zones/ZoneTest.php b/tests/Unit/Models/Zones/ZoneTest.php index 5a4e033..db57eb1 100644 --- a/tests/Unit/Models/Zones/ZoneTest.php +++ b/tests/Unit/Models/Zones/ZoneTest.php @@ -20,7 +20,7 @@ public function setUp(): void parent::setUp(); $tmp = new Zones($this->hetznerApi->getHttpClient()); - $this->mockHandler->append(new Response(200, [], file_get_contents(__DIR__ . '/fixtures/zone.json'))); + $this->mockHandler->append(new Response(200, [], file_get_contents(__DIR__.'/fixtures/zone.json'))); $this->zone = $tmp->getById(4711); } @@ -37,7 +37,7 @@ public function testDelete() public function testUpdate() { - $this->mockHandler->append(new Response(200, [], file_get_contents(__DIR__ . '/fixtures/zone.json'))); + $this->mockHandler->append(new Response(200, [], file_get_contents(__DIR__.'/fixtures/zone.json'))); $this->zone->update(['name' => 'new-name']); $this->assertLastRequestEquals('PUT', '/zones/4711'); $this->assertLastRequestBodyParametersEqual(['name' => 'new-name']); @@ -45,7 +45,7 @@ public function testUpdate() public function testChangeProtection() { - $this->mockHandler->append(new Response(200, [], file_get_contents(__DIR__ . '/fixtures/zone_action_change_protection.json'))); + $this->mockHandler->append(new Response(200, [], file_get_contents(__DIR__.'/fixtures/zone_action_change_protection.json'))); $apiResponse = $this->zone->changeProtection(true); $this->assertEquals('change_protection', $apiResponse->action->command); $this->assertEquals($this->zone->id, $apiResponse->action->resources[0]->id); @@ -68,31 +68,30 @@ public function testChangeTTL() public function testImportZonefile() { $this->mockHandler->append(new Response(200, [], $this->getGenericActionResponse('import_zonefile'))); - $apiResponse = $this->zone->importZonefile("zonefile_content"); + $apiResponse = $this->zone->importZonefile('zonefile_content'); $this->assertEquals('import_zonefile', $apiResponse->action->command); $this->assertEquals($this->zone->id, $apiResponse->action->resources[0]->id); $this->assertEquals('zone', $apiResponse->action->resources[0]->type); $this->assertLastRequestEquals('POST', '/zones/4711/actions/import_zonefile'); - $this->assertLastRequestBodyParametersEqual(['zonefile' => "zonefile_content"]); + $this->assertLastRequestBodyParametersEqual(['zonefile' => 'zonefile_content']); } public function testTestChangePrimaryNameservers() { $this->mockHandler->append(new Response(200, [], $this->getGenericActionResponse('import_zonefile'))); $apiResponse = $this->zone->changePrimaryNameservers([ - new PrimaryNameserver("192.168.178.1", 53) + new PrimaryNameserver('192.168.178.1', 53), ]); $this->assertEquals('import_zonefile', $apiResponse->action->command); $this->assertEquals($this->zone->id, $apiResponse->action->resources[0]->id); $this->assertEquals('zone', $apiResponse->action->resources[0]->type); $this->assertLastRequestEquals('POST', '/zones/4711/actions/change_primary_nameservers'); - $this->assertLastRequestBodyParametersEqual(['primary_nameservers' => [["address" => "192.168.178.1", "port" => 53]]]); + $this->assertLastRequestBodyParametersEqual(['primary_nameservers' => [['address' => '192.168.178.1', 'port' => 53]]]); } - public function testExportZonefile() { - $this->mockHandler->append(new Response(200, [], file_get_contents(__DIR__ . '/fixtures/zone_zonefile.json'))); + $this->mockHandler->append(new Response(200, [], file_get_contents(__DIR__.'/fixtures/zone_zonefile.json'))); $apiResponse = $this->zone->exportZonefile(); $this->assertNotEmpty($apiResponse->zonefile); $this->assertLastRequestEquals('GET', '/zones/4711/zonefile'); @@ -100,6 +99,6 @@ public function testExportZonefile() protected function getGenericActionResponse(string $command) { - return str_replace('$command', $command, file_get_contents(__DIR__ . '/fixtures/zone_action_generic.json')); + return str_replace('$command', $command, file_get_contents(__DIR__.'/fixtures/zone_action_generic.json')); } } From 7aa26f6702d5db4442bce92101048f57b0146278 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Lukas=20K=C3=A4mmerling?= Date: Thu, 16 Oct 2025 20:29:20 +0200 Subject: [PATCH 05/14] Implement first RRSet methods --- src/Models/Zones/RRSet.php | 16 +- src/Models/Zones/RRSetProtection.php | 2 +- src/Models/Zones/RRSetRequestOpts.php | 37 +++++ src/Models/Zones/Record.php | 15 ++ src/Models/Zones/Zone.php | 144 +++++++++++++++--- tests/Unit/Models/Zones/ZoneTest.php | 49 +++++- .../Zones/fixtures/zone_create_rrset.json | 36 +++++ .../Models/Zones/fixtures/zone_rrsets.json | 35 +++++ 8 files changed, 289 insertions(+), 45 deletions(-) create mode 100644 src/Models/Zones/RRSetRequestOpts.php create mode 100644 src/Models/Zones/Record.php create mode 100644 tests/Unit/Models/Zones/fixtures/zone_create_rrset.json create mode 100644 tests/Unit/Models/Zones/fixtures/zone_rrsets.json diff --git a/src/Models/Zones/RRSet.php b/src/Models/Zones/RRSet.php index 11d5bd3..51be0d0 100644 --- a/src/Models/Zones/RRSet.php +++ b/src/Models/Zones/RRSet.php @@ -10,7 +10,7 @@ class RRSet public int $ttl; public array $records; public array $labels; - public RRSetProtection $protection; + public ?RRSetProtection $protection; /** * @param string $id @@ -34,7 +34,7 @@ public function __construct(string $id, string $name, string $type, int $ttl, ar public static function fromResponse(array $data): RRSet { - return new self($data['id'], $data['name'], $data['type'], $data['ttl'], $data['records'], $data['labels'], RRSetProtection::parse($data['protection'])); + return new self($data['id'], $data['name'], $data['type'], $data['ttl'], $data['records'], get_object_vars($data['labels']), RRSetProtection::parse($data['protection'])); } public function __toRequest(): array @@ -52,15 +52,3 @@ public function __toRequest(): array return $r; } } - -class Record -{ - public string $value; - public string $comment; - - public function __construct(string $value, string $comment) - { - $this->value = $value; - $this->comment = $comment; - } -} diff --git a/src/Models/Zones/RRSetProtection.php b/src/Models/Zones/RRSetProtection.php index f721933..c6fd9da 100644 --- a/src/Models/Zones/RRSetProtection.php +++ b/src/Models/Zones/RRSetProtection.php @@ -41,7 +41,7 @@ public static function parse($input) return null; } if (! is_array($input)) { - return null; + $input = get_object_vars($input); } return new self($input['change'] ?? false); diff --git a/src/Models/Zones/RRSetRequestOpts.php b/src/Models/Zones/RRSetRequestOpts.php new file mode 100644 index 0000000..3f1a18b --- /dev/null +++ b/src/Models/Zones/RRSetRequestOpts.php @@ -0,0 +1,37 @@ +name = $name; + $this->type = $type; + parent::__construct($perPage, $page, $labelSelector); + } +} diff --git a/src/Models/Zones/Record.php b/src/Models/Zones/Record.php new file mode 100644 index 0000000..767a6b1 --- /dev/null +++ b/src/Models/Zones/Record.php @@ -0,0 +1,15 @@ +value = $value; + $this->comment = $comment; + } +} diff --git a/src/Models/Zones/Zone.php b/src/Models/Zones/Zone.php index cadf381..490c579 100644 --- a/src/Models/Zones/Zone.php +++ b/src/Models/Zones/Zone.php @@ -2,13 +2,16 @@ namespace LKDev\HetznerCloud\Models\Zones; +use LKDev\HetznerCloud\APIException; use LKDev\HetznerCloud\APIResponse; use LKDev\HetznerCloud\Clients\GuzzleClient; use LKDev\HetznerCloud\HetznerAPIClient; use LKDev\HetznerCloud\Models\Actions\Action; use LKDev\HetznerCloud\Models\Contracts\Resource; +use LKDev\HetznerCloud\Models\Meta; use LKDev\HetznerCloud\Models\Model; use LKDev\HetznerCloud\Models\Protection; +use LKDev\HetznerCloud\RequestOpts; class Zone extends Model implements Resource { @@ -72,8 +75,8 @@ class Zone extends Model implements Resource public AuthoritativeNameservers $authoritative_nameservers; /** - * @param int $zoneId - * @param GuzzleClient|null $httpClient + * @param int $zoneId + * @param GuzzleClient|null $httpClient */ public function __construct(int $zoneId, ?GuzzleClient $httpClient = null) { @@ -131,9 +134,9 @@ public function reload() public function delete(): ?APIResponse { $response = $this->httpClient->delete($this->replaceZoneIdInUri('zones/{id}')); - if (! HetznerAPIClient::hasError($response)) { + if (!HetznerAPIClient::hasError($response)) { return APIResponse::create([ - 'action' => Action::parse(json_decode((string) $response->getBody())->action), + 'action' => Action::parse(json_decode((string)$response->getBody())->action), ], $response->getHeaders()); } @@ -145,7 +148,7 @@ public function delete(): ?APIResponse * * @see https://docs.hetzner.cloud/reference/cloud#zones-update-a-zone * - * @param array $data + * @param array $data * @return APIResponse|null * * @throws \LKDev\HetznerCloud\APIException @@ -155,9 +158,9 @@ public function update(array $data) $response = $this->httpClient->put($this->replaceZoneIdInUri('zones/{id}'), [ 'json' => $data, ]); - if (! HetznerAPIClient::hasError($response)) { + if (!HetznerAPIClient::hasError($response)) { return APIResponse::create([ - 'zone' => self::parse(json_decode((string) $response->getBody())->zone), + 'zone' => self::parse(json_decode((string)$response->getBody())->zone), ], $response->getHeaders()); } @@ -169,21 +172,21 @@ public function update(array $data) * * @see https://docs.hetzner.cloud/#zone-actions-change-zone-protection * - * @param bool $delete + * @param bool $delete * @return APIResponse|null * * @throws \LKDev\HetznerCloud\APIException */ public function changeProtection(bool $delete = true): ?APIResponse { - $response = $this->httpClient->post('zones/'.$this->id.'/actions/change_protection', [ + $response = $this->httpClient->post('zones/' . $this->id . '/actions/change_protection', [ 'json' => [ 'delete' => $delete, ], ]); - if (! HetznerAPIClient::hasError($response)) { + if (!HetznerAPIClient::hasError($response)) { return APIResponse::create([ - 'action' => Action::parse(json_decode((string) $response->getBody())->action), + 'action' => Action::parse(json_decode((string)$response->getBody())->action), ], $response->getHeaders()); } @@ -192,10 +195,10 @@ public function changeProtection(bool $delete = true): ?APIResponse public function exportZonefile(): ?APIResponse { - $response = $this->httpClient->get('zones/'.$this->id.'/zonefile'); - if (! HetznerAPIClient::hasError($response)) { + $response = $this->httpClient->get('zones/' . $this->id . '/zonefile'); + if (!HetznerAPIClient::hasError($response)) { return APIResponse::create([ - 'zonefile' => json_decode((string) $response->getBody())->zonefile, + 'zonefile' => json_decode((string)$response->getBody())->zonefile, ], $response->getHeaders()); } @@ -204,14 +207,14 @@ public function exportZonefile(): ?APIResponse public function changeTTL(int $ttl): ?APIResponse { - $response = $this->httpClient->post('zones/'.$this->id.'/actions/change_ttl', [ + $response = $this->httpClient->post('zones/' . $this->id . '/actions/change_ttl', [ 'json' => [ 'ttl' => $ttl, ], ]); - if (! HetznerAPIClient::hasError($response)) { + if (!HetznerAPIClient::hasError($response)) { return APIResponse::create([ - 'action' => Action::parse(json_decode((string) $response->getBody())->action), + 'action' => Action::parse(json_decode((string)$response->getBody())->action), ], $response->getHeaders()); } @@ -220,14 +223,14 @@ public function changeTTL(int $ttl): ?APIResponse public function importZonefile(string $zonefile): ?APIResponse { - $response = $this->httpClient->post('zones/'.$this->id.'/actions/import_zonefile', [ + $response = $this->httpClient->post('zones/' . $this->id . '/actions/import_zonefile', [ 'json' => [ 'zonefile' => $zonefile, ], ]); - if (! HetznerAPIClient::hasError($response)) { + if (!HetznerAPIClient::hasError($response)) { return APIResponse::create([ - 'action' => Action::parse(json_decode((string) $response->getBody())->action), + 'action' => Action::parse(json_decode((string)$response->getBody())->action), ], $response->getHeaders()); } @@ -235,7 +238,7 @@ public function importZonefile(string $zonefile): ?APIResponse } /** - * @param string $uri + * @param string $uri * @return string */ protected function replaceZoneIdInUri(string $uri): string @@ -257,19 +260,110 @@ public static function parse($input) } /** - * @param array $primary_nameservers + * @param array $primary_nameservers * @return void# + * @throws APIException */ public function changePrimaryNameservers(array $primary_nameservers) { - $response = $this->httpClient->post('zones/'.$this->id.'/actions/change_primary_nameservers', [ + $response = $this->httpClient->post('zones/' . $this->id . '/actions/change_primary_nameservers', [ 'json' => [ 'primary_nameservers' => $primary_nameservers, ], ]); - if (! HetznerAPIClient::hasError($response)) { + if (!HetznerAPIClient::hasError($response)) { return APIResponse::create([ - 'action' => Action::parse(json_decode((string) $response->getBody())->action), + 'action' => Action::parse(json_decode((string)$response->getBody())->action), + ], $response->getHeaders()); + } + + return null; + } + + /** + * @param RRSetRequestOpts|null $requestOpts + * @return array + * @throws APIException + */ + public function allRRSets(?RRSetRequestOpts $requestOpts = null): array + { + if ($requestOpts == null) { + $requestOpts = new RRSetRequestOpts(); + } + $entities = []; + $requestOpts->per_page = HetznerAPIClient::MAX_ENTITIES_PER_PAGE; + $max_pages = PHP_INT_MAX; + for ($i = 1; $i < $max_pages; $i++) { + $requestOpts->page = $i; + $_f = $this->listRRSets($requestOpts); + $entities = array_merge($entities, $_f->rrsets); + if ($_f->meta->pagination->page === $_f->meta->pagination->last_page || $_f->meta->pagination->last_page === null) { + $max_pages = 0; + } + } + + return $entities; + } + + /** + * @param RRSetRequestOpts|null $requestOpts + * @return APIResponse|null + * @throws \LKDev\HetznerCloud\APIException + */ + public function listRRSets(?RRSetRequestOpts $requestOpts = null): ?APIResponse + { + if ($requestOpts == null) { + $requestOpts = new RRSetRequestOpts(); + } + $response = $this->httpClient->get('zones/' . $this->id . "/rrsets" . $requestOpts->buildQuery()); + if (!HetznerAPIClient::hasError($response)) { + $resp = json_decode((string)$response->getBody()); + $rrsets = []; + foreach ($resp->rrsets as $rrset) { + $rrsets[] = RRSet::fromResponse(get_object_vars($rrset)); + } + return APIResponse::create([ + 'meta' => Meta::parse($resp->meta), + 'rrsets' => $rrsets, + ], $response->getHeaders()); + } + + return null; + } + + /** + * @param string $name + * @param string $type + * @param array $records + * @param int|null $ttl + * @param array|null $labels + * @return void + * @throws APIException + */ + public function createRRSet(string $name, string $type, array $records, ?int $ttl = null, ?array $labels = []) + { + $parameters = [ + 'name' => $name, + 'type' => $type, + 'records' => $records, + ]; + if ($ttl !== null) { + $parameters['ttl'] = $ttl; + } + if (!empty($labels)) { + $parameters['labels'] = $labels; + } + + $response = $this->httpClient->post('zones/' . $this->id . '/rrsets', [ + 'json' => $parameters, + ]); + + if (!HetznerAPIClient::hasError($response)) { + $payload = json_decode((string)$response->getBody()); + + return APIResponse::create([ + 'action' => Action::parse($payload->action), + 'rrset' => RRSet::fromResponse(get_object_vars($payload->rrset)), ], $response->getHeaders()); } diff --git a/tests/Unit/Models/Zones/ZoneTest.php b/tests/Unit/Models/Zones/ZoneTest.php index db57eb1..e816527 100644 --- a/tests/Unit/Models/Zones/ZoneTest.php +++ b/tests/Unit/Models/Zones/ZoneTest.php @@ -4,6 +4,7 @@ use GuzzleHttp\Psr7\Response; use LKDev\HetznerCloud\Models\Zones\PrimaryNameserver; +use LKDev\HetznerCloud\Models\Zones\Record; use LKDev\HetznerCloud\Models\Zones\Zone; use LKDev\HetznerCloud\Models\Zones\Zones; use LKDev\Tests\TestCase; @@ -20,7 +21,7 @@ public function setUp(): void parent::setUp(); $tmp = new Zones($this->hetznerApi->getHttpClient()); - $this->mockHandler->append(new Response(200, [], file_get_contents(__DIR__.'/fixtures/zone.json'))); + $this->mockHandler->append(new Response(200, [], file_get_contents(__DIR__ . '/fixtures/zone.json'))); $this->zone = $tmp->getById(4711); } @@ -37,7 +38,7 @@ public function testDelete() public function testUpdate() { - $this->mockHandler->append(new Response(200, [], file_get_contents(__DIR__.'/fixtures/zone.json'))); + $this->mockHandler->append(new Response(200, [], file_get_contents(__DIR__ . '/fixtures/zone.json'))); $this->zone->update(['name' => 'new-name']); $this->assertLastRequestEquals('PUT', '/zones/4711'); $this->assertLastRequestBodyParametersEqual(['name' => 'new-name']); @@ -45,7 +46,7 @@ public function testUpdate() public function testChangeProtection() { - $this->mockHandler->append(new Response(200, [], file_get_contents(__DIR__.'/fixtures/zone_action_change_protection.json'))); + $this->mockHandler->append(new Response(200, [], file_get_contents(__DIR__ . '/fixtures/zone_action_change_protection.json'))); $apiResponse = $this->zone->changeProtection(true); $this->assertEquals('change_protection', $apiResponse->action->command); $this->assertEquals($this->zone->id, $apiResponse->action->resources[0]->id); @@ -91,14 +92,52 @@ public function testTestChangePrimaryNameservers() public function testExportZonefile() { - $this->mockHandler->append(new Response(200, [], file_get_contents(__DIR__.'/fixtures/zone_zonefile.json'))); + $this->mockHandler->append(new Response(200, [], file_get_contents(__DIR__ . '/fixtures/zone_zonefile.json'))); $apiResponse = $this->zone->exportZonefile(); $this->assertNotEmpty($apiResponse->zonefile); $this->assertLastRequestEquals('GET', '/zones/4711/zonefile'); } + public function testAllRRSets() + { + $this->mockHandler->append(new Response(200, [], file_get_contents(__DIR__ . '/fixtures/zone_rrsets.json'))); + $rrsets = $this->zone->allRRSets(); + $this->assertCount(1, $rrsets); + $rrset = $rrsets[0]; + $this->assertEquals($rrset->id, "www/A"); + $this->assertEquals($rrset->name, 'www'); + + $this->assertLastRequestEquals('GET', '/zones/' . $this->zone->id . '/rrsets'); + } + public function testListRRSets() + { + $this->mockHandler->append(new Response(200, [], file_get_contents(__DIR__ . '/fixtures/zone_rrsets.json'))); + $rrsets = $this->zone->listRRSets()->rrsets; + $this->assertCount(1, $rrsets); + $rrset = $rrsets[0]; + $this->assertEquals($rrset->id, "www/A"); + $this->assertEquals($rrset->name, 'www'); + + $this->assertLastRequestEquals('GET', '/zones/' . $this->zone->id . '/rrsets'); + } + + public function testCreateRRSet() + { + $this->mockHandler->append(new Response(201, [], file_get_contents(__DIR__ . '/fixtures/zone_create_rrset.json'))); + $apiResponse = $this->zone->createRRSet("www", "A", [new Record("198.51.100.1", "my webserver at Hetzner Cloud")], 3600, ["environment" => "prod"]); + $this->assertNotEmpty($apiResponse->rrset); + $this->assertNotEmpty($apiResponse->action); + + $this->assertLastRequestEquals('POST', '/zones/4711/rrsets'); + $this->assertLastRequestBodyParametersEqual([ + 'name' => 'www', + 'type' => 'A', + 'ttl' => 3600, + 'labels' => ['environment' => 'prod'], + 'records' => [['value' => "198.51.100.1", 'comment' => "my webserver at Hetzner Cloud"]]]); + } protected function getGenericActionResponse(string $command) { - return str_replace('$command', $command, file_get_contents(__DIR__.'/fixtures/zone_action_generic.json')); + return str_replace('$command', $command, file_get_contents(__DIR__ . '/fixtures/zone_action_generic.json')); } } diff --git a/tests/Unit/Models/Zones/fixtures/zone_create_rrset.json b/tests/Unit/Models/Zones/fixtures/zone_create_rrset.json new file mode 100644 index 0000000..3ddac28 --- /dev/null +++ b/tests/Unit/Models/Zones/fixtures/zone_create_rrset.json @@ -0,0 +1,36 @@ +{ + "rrset": { + "id": "www/A", + "name": "www", + "type": "A", + "ttl": 3600, + "labels": { + "key": "value" + }, + "protection": { + "change": false + }, + "records": [ + { + "value": "198.51.100.1", + "comment": "My web server at Hetzner Cloud." + } + ], + "zone": 42 + }, + "action": { + "id": 1, + "command": "create_rrset", + "status": "running", + "progress": 50, + "started": "2016-01-30T23:55:00+00:00", + "finished": null, + "resources": [ + { + "id": 42, + "type": "zone" + } + ], + "error": null + } +} diff --git a/tests/Unit/Models/Zones/fixtures/zone_rrsets.json b/tests/Unit/Models/Zones/fixtures/zone_rrsets.json new file mode 100644 index 0000000..c53723f --- /dev/null +++ b/tests/Unit/Models/Zones/fixtures/zone_rrsets.json @@ -0,0 +1,35 @@ +{ + "rrsets": [ + { + "id": "www/A", + "name": "www", + "type": "A", + "ttl": 3600, + "labels": { + "environment": "prod", + "example.com/my": "label", + "just-a-key": "" + }, + "protection": { + "change": false + }, + "records": [ + { + "value": "198.51.100.1", + "comment": "My web server at Hetzner Cloud." + } + ], + "zone": 4711 + } + ], + "meta": { + "pagination": { + "page": 1, + "per_page": 25, + "previous_page": null, + "next_page": null, + "last_page": 1, + "total_entries": 1 + } + } +} From 6848aeab4fa068506de279cf8c317229653b34d7 Mon Sep 17 00:00:00 2001 From: StyleCI Bot Date: Thu, 16 Oct 2025 18:29:32 +0000 Subject: [PATCH 06/14] Apply fixes from StyleCI --- src/Models/Zones/Zone.php | 84 +++++++++++++++------------- tests/Unit/Models/Zones/ZoneTest.php | 30 +++++----- 2 files changed, 60 insertions(+), 54 deletions(-) diff --git a/src/Models/Zones/Zone.php b/src/Models/Zones/Zone.php index 490c579..9160722 100644 --- a/src/Models/Zones/Zone.php +++ b/src/Models/Zones/Zone.php @@ -11,7 +11,6 @@ use LKDev\HetznerCloud\Models\Meta; use LKDev\HetznerCloud\Models\Model; use LKDev\HetznerCloud\Models\Protection; -use LKDev\HetznerCloud\RequestOpts; class Zone extends Model implements Resource { @@ -75,8 +74,8 @@ class Zone extends Model implements Resource public AuthoritativeNameservers $authoritative_nameservers; /** - * @param int $zoneId - * @param GuzzleClient|null $httpClient + * @param int $zoneId + * @param GuzzleClient|null $httpClient */ public function __construct(int $zoneId, ?GuzzleClient $httpClient = null) { @@ -134,9 +133,9 @@ public function reload() public function delete(): ?APIResponse { $response = $this->httpClient->delete($this->replaceZoneIdInUri('zones/{id}')); - if (!HetznerAPIClient::hasError($response)) { + if (! HetznerAPIClient::hasError($response)) { return APIResponse::create([ - 'action' => Action::parse(json_decode((string)$response->getBody())->action), + 'action' => Action::parse(json_decode((string) $response->getBody())->action), ], $response->getHeaders()); } @@ -148,7 +147,7 @@ public function delete(): ?APIResponse * * @see https://docs.hetzner.cloud/reference/cloud#zones-update-a-zone * - * @param array $data + * @param array $data * @return APIResponse|null * * @throws \LKDev\HetznerCloud\APIException @@ -158,9 +157,9 @@ public function update(array $data) $response = $this->httpClient->put($this->replaceZoneIdInUri('zones/{id}'), [ 'json' => $data, ]); - if (!HetznerAPIClient::hasError($response)) { + if (! HetznerAPIClient::hasError($response)) { return APIResponse::create([ - 'zone' => self::parse(json_decode((string)$response->getBody())->zone), + 'zone' => self::parse(json_decode((string) $response->getBody())->zone), ], $response->getHeaders()); } @@ -172,21 +171,21 @@ public function update(array $data) * * @see https://docs.hetzner.cloud/#zone-actions-change-zone-protection * - * @param bool $delete + * @param bool $delete * @return APIResponse|null * * @throws \LKDev\HetznerCloud\APIException */ public function changeProtection(bool $delete = true): ?APIResponse { - $response = $this->httpClient->post('zones/' . $this->id . '/actions/change_protection', [ + $response = $this->httpClient->post('zones/'.$this->id.'/actions/change_protection', [ 'json' => [ 'delete' => $delete, ], ]); - if (!HetznerAPIClient::hasError($response)) { + if (! HetznerAPIClient::hasError($response)) { return APIResponse::create([ - 'action' => Action::parse(json_decode((string)$response->getBody())->action), + 'action' => Action::parse(json_decode((string) $response->getBody())->action), ], $response->getHeaders()); } @@ -195,10 +194,10 @@ public function changeProtection(bool $delete = true): ?APIResponse public function exportZonefile(): ?APIResponse { - $response = $this->httpClient->get('zones/' . $this->id . '/zonefile'); - if (!HetznerAPIClient::hasError($response)) { + $response = $this->httpClient->get('zones/'.$this->id.'/zonefile'); + if (! HetznerAPIClient::hasError($response)) { return APIResponse::create([ - 'zonefile' => json_decode((string)$response->getBody())->zonefile, + 'zonefile' => json_decode((string) $response->getBody())->zonefile, ], $response->getHeaders()); } @@ -207,14 +206,14 @@ public function exportZonefile(): ?APIResponse public function changeTTL(int $ttl): ?APIResponse { - $response = $this->httpClient->post('zones/' . $this->id . '/actions/change_ttl', [ + $response = $this->httpClient->post('zones/'.$this->id.'/actions/change_ttl', [ 'json' => [ 'ttl' => $ttl, ], ]); - if (!HetznerAPIClient::hasError($response)) { + if (! HetznerAPIClient::hasError($response)) { return APIResponse::create([ - 'action' => Action::parse(json_decode((string)$response->getBody())->action), + 'action' => Action::parse(json_decode((string) $response->getBody())->action), ], $response->getHeaders()); } @@ -223,14 +222,14 @@ public function changeTTL(int $ttl): ?APIResponse public function importZonefile(string $zonefile): ?APIResponse { - $response = $this->httpClient->post('zones/' . $this->id . '/actions/import_zonefile', [ + $response = $this->httpClient->post('zones/'.$this->id.'/actions/import_zonefile', [ 'json' => [ 'zonefile' => $zonefile, ], ]); - if (!HetznerAPIClient::hasError($response)) { + if (! HetznerAPIClient::hasError($response)) { return APIResponse::create([ - 'action' => Action::parse(json_decode((string)$response->getBody())->action), + 'action' => Action::parse(json_decode((string) $response->getBody())->action), ], $response->getHeaders()); } @@ -238,7 +237,7 @@ public function importZonefile(string $zonefile): ?APIResponse } /** - * @param string $uri + * @param string $uri * @return string */ protected function replaceZoneIdInUri(string $uri): string @@ -260,20 +259,21 @@ public static function parse($input) } /** - * @param array $primary_nameservers + * @param array $primary_nameservers * @return void# + * * @throws APIException */ public function changePrimaryNameservers(array $primary_nameservers) { - $response = $this->httpClient->post('zones/' . $this->id . '/actions/change_primary_nameservers', [ + $response = $this->httpClient->post('zones/'.$this->id.'/actions/change_primary_nameservers', [ 'json' => [ 'primary_nameservers' => $primary_nameservers, ], ]); - if (!HetznerAPIClient::hasError($response)) { + if (! HetznerAPIClient::hasError($response)) { return APIResponse::create([ - 'action' => Action::parse(json_decode((string)$response->getBody())->action), + 'action' => Action::parse(json_decode((string) $response->getBody())->action), ], $response->getHeaders()); } @@ -281,8 +281,9 @@ public function changePrimaryNameservers(array $primary_nameservers) } /** - * @param RRSetRequestOpts|null $requestOpts + * @param RRSetRequestOpts|null $requestOpts * @return array + * * @throws APIException */ public function allRRSets(?RRSetRequestOpts $requestOpts = null): array @@ -306,8 +307,9 @@ public function allRRSets(?RRSetRequestOpts $requestOpts = null): array } /** - * @param RRSetRequestOpts|null $requestOpts + * @param RRSetRequestOpts|null $requestOpts * @return APIResponse|null + * * @throws \LKDev\HetznerCloud\APIException */ public function listRRSets(?RRSetRequestOpts $requestOpts = null): ?APIResponse @@ -315,13 +317,14 @@ public function listRRSets(?RRSetRequestOpts $requestOpts = null): ?APIResponse if ($requestOpts == null) { $requestOpts = new RRSetRequestOpts(); } - $response = $this->httpClient->get('zones/' . $this->id . "/rrsets" . $requestOpts->buildQuery()); - if (!HetznerAPIClient::hasError($response)) { - $resp = json_decode((string)$response->getBody()); + $response = $this->httpClient->get('zones/'.$this->id.'/rrsets'.$requestOpts->buildQuery()); + if (! HetznerAPIClient::hasError($response)) { + $resp = json_decode((string) $response->getBody()); $rrsets = []; foreach ($resp->rrsets as $rrset) { $rrsets[] = RRSet::fromResponse(get_object_vars($rrset)); } + return APIResponse::create([ 'meta' => Meta::parse($resp->meta), 'rrsets' => $rrsets, @@ -332,12 +335,13 @@ public function listRRSets(?RRSetRequestOpts $requestOpts = null): ?APIResponse } /** - * @param string $name - * @param string $type - * @param array $records - * @param int|null $ttl - * @param array|null $labels + * @param string $name + * @param string $type + * @param array $records + * @param int|null $ttl + * @param array|null $labels * @return void + * * @throws APIException */ public function createRRSet(string $name, string $type, array $records, ?int $ttl = null, ?array $labels = []) @@ -350,16 +354,16 @@ public function createRRSet(string $name, string $type, array $records, ?int $tt if ($ttl !== null) { $parameters['ttl'] = $ttl; } - if (!empty($labels)) { + if (! empty($labels)) { $parameters['labels'] = $labels; } - $response = $this->httpClient->post('zones/' . $this->id . '/rrsets', [ + $response = $this->httpClient->post('zones/'.$this->id.'/rrsets', [ 'json' => $parameters, ]); - if (!HetznerAPIClient::hasError($response)) { - $payload = json_decode((string)$response->getBody()); + if (! HetznerAPIClient::hasError($response)) { + $payload = json_decode((string) $response->getBody()); return APIResponse::create([ 'action' => Action::parse($payload->action), diff --git a/tests/Unit/Models/Zones/ZoneTest.php b/tests/Unit/Models/Zones/ZoneTest.php index e816527..fb38737 100644 --- a/tests/Unit/Models/Zones/ZoneTest.php +++ b/tests/Unit/Models/Zones/ZoneTest.php @@ -21,7 +21,7 @@ public function setUp(): void parent::setUp(); $tmp = new Zones($this->hetznerApi->getHttpClient()); - $this->mockHandler->append(new Response(200, [], file_get_contents(__DIR__ . '/fixtures/zone.json'))); + $this->mockHandler->append(new Response(200, [], file_get_contents(__DIR__.'/fixtures/zone.json'))); $this->zone = $tmp->getById(4711); } @@ -38,7 +38,7 @@ public function testDelete() public function testUpdate() { - $this->mockHandler->append(new Response(200, [], file_get_contents(__DIR__ . '/fixtures/zone.json'))); + $this->mockHandler->append(new Response(200, [], file_get_contents(__DIR__.'/fixtures/zone.json'))); $this->zone->update(['name' => 'new-name']); $this->assertLastRequestEquals('PUT', '/zones/4711'); $this->assertLastRequestBodyParametersEqual(['name' => 'new-name']); @@ -46,7 +46,7 @@ public function testUpdate() public function testChangeProtection() { - $this->mockHandler->append(new Response(200, [], file_get_contents(__DIR__ . '/fixtures/zone_action_change_protection.json'))); + $this->mockHandler->append(new Response(200, [], file_get_contents(__DIR__.'/fixtures/zone_action_change_protection.json'))); $apiResponse = $this->zone->changeProtection(true); $this->assertEquals('change_protection', $apiResponse->action->command); $this->assertEquals($this->zone->id, $apiResponse->action->resources[0]->id); @@ -92,38 +92,40 @@ public function testTestChangePrimaryNameservers() public function testExportZonefile() { - $this->mockHandler->append(new Response(200, [], file_get_contents(__DIR__ . '/fixtures/zone_zonefile.json'))); + $this->mockHandler->append(new Response(200, [], file_get_contents(__DIR__.'/fixtures/zone_zonefile.json'))); $apiResponse = $this->zone->exportZonefile(); $this->assertNotEmpty($apiResponse->zonefile); $this->assertLastRequestEquals('GET', '/zones/4711/zonefile'); } + public function testAllRRSets() { - $this->mockHandler->append(new Response(200, [], file_get_contents(__DIR__ . '/fixtures/zone_rrsets.json'))); + $this->mockHandler->append(new Response(200, [], file_get_contents(__DIR__.'/fixtures/zone_rrsets.json'))); $rrsets = $this->zone->allRRSets(); $this->assertCount(1, $rrsets); $rrset = $rrsets[0]; - $this->assertEquals($rrset->id, "www/A"); + $this->assertEquals($rrset->id, 'www/A'); $this->assertEquals($rrset->name, 'www'); - $this->assertLastRequestEquals('GET', '/zones/' . $this->zone->id . '/rrsets'); + $this->assertLastRequestEquals('GET', '/zones/'.$this->zone->id.'/rrsets'); } + public function testListRRSets() { - $this->mockHandler->append(new Response(200, [], file_get_contents(__DIR__ . '/fixtures/zone_rrsets.json'))); + $this->mockHandler->append(new Response(200, [], file_get_contents(__DIR__.'/fixtures/zone_rrsets.json'))); $rrsets = $this->zone->listRRSets()->rrsets; $this->assertCount(1, $rrsets); $rrset = $rrsets[0]; - $this->assertEquals($rrset->id, "www/A"); + $this->assertEquals($rrset->id, 'www/A'); $this->assertEquals($rrset->name, 'www'); - $this->assertLastRequestEquals('GET', '/zones/' . $this->zone->id . '/rrsets'); + $this->assertLastRequestEquals('GET', '/zones/'.$this->zone->id.'/rrsets'); } public function testCreateRRSet() { - $this->mockHandler->append(new Response(201, [], file_get_contents(__DIR__ . '/fixtures/zone_create_rrset.json'))); - $apiResponse = $this->zone->createRRSet("www", "A", [new Record("198.51.100.1", "my webserver at Hetzner Cloud")], 3600, ["environment" => "prod"]); + $this->mockHandler->append(new Response(201, [], file_get_contents(__DIR__.'/fixtures/zone_create_rrset.json'))); + $apiResponse = $this->zone->createRRSet('www', 'A', [new Record('198.51.100.1', 'my webserver at Hetzner Cloud')], 3600, ['environment' => 'prod']); $this->assertNotEmpty($apiResponse->rrset); $this->assertNotEmpty($apiResponse->action); @@ -133,11 +135,11 @@ public function testCreateRRSet() 'type' => 'A', 'ttl' => 3600, 'labels' => ['environment' => 'prod'], - 'records' => [['value' => "198.51.100.1", 'comment' => "my webserver at Hetzner Cloud"]]]); + 'records' => [['value' => '198.51.100.1', 'comment' => 'my webserver at Hetzner Cloud']]]); } protected function getGenericActionResponse(string $command) { - return str_replace('$command', $command, file_get_contents(__DIR__ . '/fixtures/zone_action_generic.json')); + return str_replace('$command', $command, file_get_contents(__DIR__.'/fixtures/zone_action_generic.json')); } } From 2d1e27722c96cd00a780eb552b902c88e7edb711 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Lukas=20K=C3=A4mmerling?= Date: Thu, 16 Oct 2025 20:40:59 +0200 Subject: [PATCH 07/14] WIP --- src/HetznerAPIClient.php | 1 + src/Models/Zones/RRSet.php | 90 +++++++++++++++---- src/Models/Zones/Zone.php | 19 +++- tests/Unit/Models/Zones/ZoneTest.php | 11 +++ .../Models/Zones/fixtures/zone_rrset.json | 23 +++++ 5 files changed, 124 insertions(+), 20 deletions(-) create mode 100644 tests/Unit/Models/Zones/fixtures/zone_rrset.json diff --git a/src/HetznerAPIClient.php b/src/HetznerAPIClient.php index fc004fa..eee4a56 100644 --- a/src/HetznerAPIClient.php +++ b/src/HetznerAPIClient.php @@ -21,6 +21,7 @@ use LKDev\HetznerCloud\Models\Servers\Types\ServerTypes; use LKDev\HetznerCloud\Models\SSHKeys\SSHKeys; use LKDev\HetznerCloud\Models\Volumes\Volumes; +use LKDev\HetznerCloud\Models\Zones\RRSet; use LKDev\HetznerCloud\Models\Zones\Zones; use Psr\Http\Message\ResponseInterface; diff --git a/src/Models/Zones/RRSet.php b/src/Models/Zones/RRSet.php index 51be0d0..23a0414 100644 --- a/src/Models/Zones/RRSet.php +++ b/src/Models/Zones/RRSet.php @@ -2,7 +2,12 @@ namespace LKDev\HetznerCloud\Models\Zones; -class RRSet +use LKDev\HetznerCloud\Clients\GuzzleClient; +use LKDev\HetznerCloud\HetznerAPIClient; +use LKDev\HetznerCloud\Models\Contracts\Resource; +use LKDev\HetznerCloud\Models\Model; + +class RRSet extends Model implements Resource { public string $id; public string $name; @@ -12,29 +17,38 @@ class RRSet public array $labels; public ?RRSetProtection $protection; + public int $zone; + /** - * @param string $id - * @param string $name - * @param string $type - * @param int $ttl - * @param array $records - * @param array|null $labels - * @param RRSetProtection|null $protection + * @param string $id + * @param GuzzleClient|null $client */ - public function __construct(string $id, string $name, string $type, int $ttl, array $records, ?array $labels, ?RRSetProtection $protection) + public function __construct(string $id, ?GuzzleClient $client = null) { $this->id = $id; - $this->name = $name; - $this->type = $type; - $this->ttl = $ttl; - $this->records = $records; - $this->labels = $labels; - $this->protection = $protection; + + parent::__construct($client); + } + + /** + * @param $data + * @return \LKDev\HetznerCloud\Models\Zones\RRSet + */ + public function setAdditionalData($data) + { + $this->name = $data->name; + $this->type = $data->type; + $this->ttl = $data->ttl; + $this->records = $data->records; + $this->labels = get_object_vars($data->labels); + $this->protection = RRSetProtection::parse($data->protection); + $this->zone = $data->zone; + return $this; } - public static function fromResponse(array $data): RRSet + public static function parse($input): RRSet { - return new self($data['id'], $data['name'], $data['type'], $data['ttl'], $data['records'], get_object_vars($data['labels']), RRSetProtection::parse($data['protection'])); + return (new self($input->id))->setAdditionalData($input); } public function __toRequest(): array @@ -45,10 +59,50 @@ public function __toRequest(): array 'ttl' => $this->ttl, 'records' => $this->records, ]; - if (! empty($this->labels)) { + if (!empty($this->labels)) { $r['labels'] = $this->labels; } return $r; } + + public function reload() + { + return HetznerAPIClient::$instance->zones()->getById($this->zone)->getRRSet($this->id); + } + + public function delete() + { + // TODO: Implement delete() method. + } + + public function update(array $data) + { + // TODO: Implement update() method. + } + + public function changeProtection(RRSetProtection $protection) + { + // TODO: Implement changeProtection() method. + } + + public function changeTTL(int $ttl) + { + // TODO: Implement changeTTL() method. + } + + public function setRecords(array $records) + { +// TODO: Implement setRecords() method. + } + + public function addRecords(array $records) + { + // TODO: Implement addRecords() method. + } + + public function removeRecords(array $records) + { + // TODO: Implement removeRecords() method. + } } diff --git a/src/Models/Zones/Zone.php b/src/Models/Zones/Zone.php index 9160722..1d4293c 100644 --- a/src/Models/Zones/Zone.php +++ b/src/Models/Zones/Zone.php @@ -322,7 +322,7 @@ public function listRRSets(?RRSetRequestOpts $requestOpts = null): ?APIResponse $resp = json_decode((string) $response->getBody()); $rrsets = []; foreach ($resp->rrsets as $rrset) { - $rrsets[] = RRSet::fromResponse(get_object_vars($rrset)); + $rrsets[] = RRSet::parse(get_object_vars($rrset)); } return APIResponse::create([ @@ -367,10 +367,25 @@ public function createRRSet(string $name, string $type, array $records, ?int $tt return APIResponse::create([ 'action' => Action::parse($payload->action), - 'rrset' => RRSet::fromResponse(get_object_vars($payload->rrset)), + 'rrset' => RRSet::parse(get_object_vars($payload->rrset)), ], $response->getHeaders()); } return null; } + + /** + * @param string $id + * @return RRSet|null + * @throws APIException + */ + public function getRRSetById(string $id): ?RRSet + { + $response = $this->httpClient->get('zones/' . $this->id . "/rrsets/" . $id);; + if (!HetznerAPIClient::hasError($response)) { + return RRSet::parse(json_decode((string)$response->getBody())->rrset); + } + + return null; + } } diff --git a/tests/Unit/Models/Zones/ZoneTest.php b/tests/Unit/Models/Zones/ZoneTest.php index fb38737..1ae79c3 100644 --- a/tests/Unit/Models/Zones/ZoneTest.php +++ b/tests/Unit/Models/Zones/ZoneTest.php @@ -138,6 +138,17 @@ public function testCreateRRSet() 'records' => [['value' => '198.51.100.1', 'comment' => 'my webserver at Hetzner Cloud']]]); } + public function testGetById() + { + $this->mockHandler->append(new Response(200, [], file_get_contents(__DIR__.'/fixtures/zone_rrset.json'))); + $rrset = $this->zone->getRRSetById("www/A"); + $this->assertEquals($rrset->id, "www/A"); + $this->assertEquals($rrset->name, 'www'); + + $this->assertLastRequestEquals('GET', '/zones/4711/rrsets/www/A'); + } + + protected function getGenericActionResponse(string $command) { return str_replace('$command', $command, file_get_contents(__DIR__.'/fixtures/zone_action_generic.json')); diff --git a/tests/Unit/Models/Zones/fixtures/zone_rrset.json b/tests/Unit/Models/Zones/fixtures/zone_rrset.json new file mode 100644 index 0000000..f425563 --- /dev/null +++ b/tests/Unit/Models/Zones/fixtures/zone_rrset.json @@ -0,0 +1,23 @@ +{ + "rrset": { + "id": "www/A", + "name": "www", + "type": "A", + "ttl": 3600, + "labels": { + "environment": "prod", + "example.com/my": "label", + "just-a-key": "" + }, + "protection": { + "change": false + }, + "records": [ + { + "value": "198.51.100.1", + "comment": "My web server at Hetzner Cloud." + } + ], + "zone": 4711 + } +} From 0d8bf769004df14bcfba51fea64569c15f98d10e Mon Sep 17 00:00:00 2001 From: StyleCI Bot Date: Thu, 16 Oct 2025 18:41:09 +0000 Subject: [PATCH 08/14] Apply fixes from StyleCI --- src/HetznerAPIClient.php | 1 - src/Models/Zones/RRSet.php | 9 +++++---- src/Models/Zones/Zone.php | 9 +++++---- tests/Unit/Models/Zones/ZoneTest.php | 5 ++--- 4 files changed, 12 insertions(+), 12 deletions(-) diff --git a/src/HetznerAPIClient.php b/src/HetznerAPIClient.php index eee4a56..fc004fa 100644 --- a/src/HetznerAPIClient.php +++ b/src/HetznerAPIClient.php @@ -21,7 +21,6 @@ use LKDev\HetznerCloud\Models\Servers\Types\ServerTypes; use LKDev\HetznerCloud\Models\SSHKeys\SSHKeys; use LKDev\HetznerCloud\Models\Volumes\Volumes; -use LKDev\HetznerCloud\Models\Zones\RRSet; use LKDev\HetznerCloud\Models\Zones\Zones; use Psr\Http\Message\ResponseInterface; diff --git a/src/Models/Zones/RRSet.php b/src/Models/Zones/RRSet.php index 23a0414..decf340 100644 --- a/src/Models/Zones/RRSet.php +++ b/src/Models/Zones/RRSet.php @@ -20,8 +20,8 @@ class RRSet extends Model implements Resource public int $zone; /** - * @param string $id - * @param GuzzleClient|null $client + * @param string $id + * @param GuzzleClient|null $client */ public function __construct(string $id, ?GuzzleClient $client = null) { @@ -43,6 +43,7 @@ public function setAdditionalData($data) $this->labels = get_object_vars($data->labels); $this->protection = RRSetProtection::parse($data->protection); $this->zone = $data->zone; + return $this; } @@ -59,7 +60,7 @@ public function __toRequest(): array 'ttl' => $this->ttl, 'records' => $this->records, ]; - if (!empty($this->labels)) { + if (! empty($this->labels)) { $r['labels'] = $this->labels; } @@ -93,7 +94,7 @@ public function changeTTL(int $ttl) public function setRecords(array $records) { -// TODO: Implement setRecords() method. + // TODO: Implement setRecords() method. } public function addRecords(array $records) diff --git a/src/Models/Zones/Zone.php b/src/Models/Zones/Zone.php index 1d4293c..ba1c990 100644 --- a/src/Models/Zones/Zone.php +++ b/src/Models/Zones/Zone.php @@ -375,15 +375,16 @@ public function createRRSet(string $name, string $type, array $records, ?int $tt } /** - * @param string $id + * @param string $id * @return RRSet|null + * * @throws APIException */ public function getRRSetById(string $id): ?RRSet { - $response = $this->httpClient->get('zones/' . $this->id . "/rrsets/" . $id);; - if (!HetznerAPIClient::hasError($response)) { - return RRSet::parse(json_decode((string)$response->getBody())->rrset); + $response = $this->httpClient->get('zones/'.$this->id.'/rrsets/'.$id); + if (! HetznerAPIClient::hasError($response)) { + return RRSet::parse(json_decode((string) $response->getBody())->rrset); } return null; diff --git a/tests/Unit/Models/Zones/ZoneTest.php b/tests/Unit/Models/Zones/ZoneTest.php index 1ae79c3..9fab56f 100644 --- a/tests/Unit/Models/Zones/ZoneTest.php +++ b/tests/Unit/Models/Zones/ZoneTest.php @@ -141,14 +141,13 @@ public function testCreateRRSet() public function testGetById() { $this->mockHandler->append(new Response(200, [], file_get_contents(__DIR__.'/fixtures/zone_rrset.json'))); - $rrset = $this->zone->getRRSetById("www/A"); - $this->assertEquals($rrset->id, "www/A"); + $rrset = $this->zone->getRRSetById('www/A'); + $this->assertEquals($rrset->id, 'www/A'); $this->assertEquals($rrset->name, 'www'); $this->assertLastRequestEquals('GET', '/zones/4711/rrsets/www/A'); } - protected function getGenericActionResponse(string $command) { return str_replace('$command', $command, file_get_contents(__DIR__.'/fixtures/zone_action_generic.json')); From d27e1c731b4dadaa6abac4bb03efb06e91f20879 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Lukas=20K=C3=A4mmerling?= Date: Thu, 16 Oct 2025 20:56:37 +0200 Subject: [PATCH 09/14] Add all currently available endpoints --- src/Models/Zones/RRSet.php | 147 ++++++++++++++++++++++---- src/Models/Zones/Zone.php | 4 +- tests/Unit/Models/Zones/RRSetTest.php | 127 ++++++++++++++++++++++ 3 files changed, 258 insertions(+), 20 deletions(-) create mode 100644 tests/Unit/Models/Zones/RRSetTest.php diff --git a/src/Models/Zones/RRSet.php b/src/Models/Zones/RRSet.php index decf340..76f2edf 100644 --- a/src/Models/Zones/RRSet.php +++ b/src/Models/Zones/RRSet.php @@ -2,8 +2,10 @@ namespace LKDev\HetznerCloud\Models\Zones; +use LKDev\HetznerCloud\APIResponse; use LKDev\HetznerCloud\Clients\GuzzleClient; use LKDev\HetznerCloud\HetznerAPIClient; +use LKDev\HetznerCloud\Models\Actions\Action; use LKDev\HetznerCloud\Models\Contracts\Resource; use LKDev\HetznerCloud\Models\Model; @@ -20,8 +22,8 @@ class RRSet extends Model implements Resource public int $zone; /** - * @param string $id - * @param GuzzleClient|null $client + * @param string $id + * @param GuzzleClient|null $client */ public function __construct(string $id, ?GuzzleClient $client = null) { @@ -43,7 +45,6 @@ public function setAdditionalData($data) $this->labels = get_object_vars($data->labels); $this->protection = RRSetProtection::parse($data->protection); $this->zone = $data->zone; - return $this; } @@ -60,50 +61,160 @@ public function __toRequest(): array 'ttl' => $this->ttl, 'records' => $this->records, ]; - if (! empty($this->labels)) { + if (!empty($this->labels)) { $r['labels'] = $this->labels; } return $r; } + /** + * @return RRSet|null + * @throws \LKDev\HetznerCloud\APIException + */ public function reload() { - return HetznerAPIClient::$instance->zones()->getById($this->zone)->getRRSet($this->id); + return (new Zone($this->zone))->getRRSetById($this->id); } - public function delete() + /** + * @return APIResponse|null + * @throws \LKDev\HetznerCloud\APIException + */ + public function delete(): ?APIResponse { - // TODO: Implement delete() method. + $response = $this->httpClient->delete('zones/' . $this->zone . '/rrsets/' . $this->id); + if (!HetznerAPIClient::hasError($response)) { + return APIResponse::create([ + 'action' => Action::parse(json_decode((string)$response->getBody())->action), + ], $response->getHeaders()); + } + + return null; } - public function update(array $data) + /** + * @param array $data + * @return APIResponse|null + * @throws \LKDev\HetznerCloud\APIException + */ + public function update(array $data): ?APIResponse { - // TODO: Implement update() method. + $response = $this->httpClient->put('zones/' . $this->zone . '/rrsets/' . $this->id, [ + 'json' => $data, + ]); + if (!HetznerAPIClient::hasError($response)) { + return APIResponse::create([ + 'rrset' => self::parse(json_decode((string)$response->getBody())->rrset), + ], $response->getHeaders()); + } + + return null; } - public function changeProtection(RRSetProtection $protection) + /** + * @param bool $change + * @return APIResponse|null + * @throws \LKDev\HetznerCloud\APIException + */ + public function changeProtection(bool $change): ?APIResponse { - // TODO: Implement changeProtection() method. + $response = $this->httpClient->post('zones/' . $this->zone . '/rrsets/' . $this->id . '/actions/change_protection', [ + 'json' => [ + 'change' => $change, + ], + ]); + if (!HetznerAPIClient::hasError($response)) { + return APIResponse::create([ + 'action' => Action::parse(json_decode((string)$response->getBody())->action), + ], $response->getHeaders()); + } + return null; } - public function changeTTL(int $ttl) + /** + * @param int $ttl + * @return APIResponse|null + * @throws \LKDev\HetznerCloud\APIException + */ + public function changeTTL(int $ttl): ?APIResponse { - // TODO: Implement changeTTL() method. + $response = $this->httpClient->post('zones/' . $this->zone . '/rrsets/' . $this->id . '/actions/change_ttl', [ + 'json' => [ + 'ttl' => $ttl, + ], + ]); + if (!HetznerAPIClient::hasError($response)) { + return APIResponse::create([ + 'action' => Action::parse(json_decode((string)$response->getBody())->action), + ], $response->getHeaders()); + } + + return null; } - public function setRecords(array $records) + /** + * @param array $records + * @return APIResponse|null + * @throws \LKDev\HetznerCloud\APIException + */ + public function setRecords(array $records): ?APIResponse { - // TODO: Implement setRecords() method. + $response = $this->httpClient->post('zones/' . $this->zone . '/rrsets/' . $this->id . '/actions/set_records', [ + 'json' => [ + 'records' => $records, + ], + ]); + if (!HetznerAPIClient::hasError($response)) { + return APIResponse::create([ + 'action' => Action::parse(json_decode((string)$response->getBody())->action), + ], $response->getHeaders()); + } + + return null; } - public function addRecords(array $records) + /** + * @param array $records + * @param int|null $ttl + * @return APIResponse|null + * @throws \LKDev\HetznerCloud\APIException + */ + public function addRecords(array $records, ?int $ttl = null): ?APIResponse { - // TODO: Implement addRecords() method. + $response = $this->httpClient->post('zones/' . $this->zone . '/rrsets/' . $this->id . '/actions/add_records', [ + 'json' => [ + 'records' => $records, + 'ttl' => $ttl, + ], + ]); + if (!HetznerAPIClient::hasError($response)) { + return APIResponse::create([ + 'action' => Action::parse(json_decode((string)$response->getBody())->action), + ], $response->getHeaders()); + } + + return null; } + /** + * @param array $records + * @return APIResponse|null + * @throws \LKDev\HetznerCloud\APIException + */ public function removeRecords(array $records) { - // TODO: Implement removeRecords() method. + $response = $this->httpClient->post('zones/' . $this->zone . '/rrsets/' . $this->id . '/actions/remove_records', [ + 'json' => [ + 'records' => $records, + ], + ]); + if (!HetznerAPIClient::hasError($response)) { + return APIResponse::create([ + 'action' => Action::parse(json_decode((string)$response->getBody())->action), + ], $response->getHeaders()); + } + + return null; } } diff --git a/src/Models/Zones/Zone.php b/src/Models/Zones/Zone.php index ba1c990..dae2ee2 100644 --- a/src/Models/Zones/Zone.php +++ b/src/Models/Zones/Zone.php @@ -322,7 +322,7 @@ public function listRRSets(?RRSetRequestOpts $requestOpts = null): ?APIResponse $resp = json_decode((string) $response->getBody()); $rrsets = []; foreach ($resp->rrsets as $rrset) { - $rrsets[] = RRSet::parse(get_object_vars($rrset)); + $rrsets[] = RRSet::parse($rrset); } return APIResponse::create([ @@ -367,7 +367,7 @@ public function createRRSet(string $name, string $type, array $records, ?int $tt return APIResponse::create([ 'action' => Action::parse($payload->action), - 'rrset' => RRSet::parse(get_object_vars($payload->rrset)), + 'rrset' => RRSet::parse($payload->rrset), ], $response->getHeaders()); } diff --git a/tests/Unit/Models/Zones/RRSetTest.php b/tests/Unit/Models/Zones/RRSetTest.php new file mode 100644 index 0000000..f198c1c --- /dev/null +++ b/tests/Unit/Models/Zones/RRSetTest.php @@ -0,0 +1,127 @@ +mockHandler->append(new Response(200, [], file_get_contents(__DIR__ . '/fixtures/zone_rrset.json'))); + $this->rrset = (new Zone(4711))->getRRSetById('www/A');; + } + + public function testDelete() + { + $this->mockHandler->append(new Response(200, [], $this->getGenericActionResponse('delete_rrset'))); + $resp = $this->rrset->delete(); + + $this->assertEquals('delete_rrset', $resp->action->command); + $this->assertEquals($this->rrset->zone, $resp->action->resources[0]->id); + $this->assertEquals('zone', $resp->action->resources[0]->type); + $this->assertLastRequestEquals('DELETE', '/zones/4711/rrsets/www/A'); + } + + public function testUpdate() + { + $this->mockHandler->append(new Response(200, [], file_get_contents(__DIR__ . '/fixtures/zone_rrset.json'))); + $this->rrset->update(['labels' => ['environment' => 'prod']]);; + $this->assertLastRequestEquals('PUT', '/zones/4711/rrsets/www/A'); + $this->assertLastRequestBodyParametersEqual(['labels' => ['environment' => 'prod']]); + } + + public function testChangeProtection() + { + $this->mockHandler->append(new Response(200, [], $this->getGenericActionResponse('change_rrset_protection')));; + $apiResponse = $this->rrset->changeProtection(true); + $this->assertEquals('change_rrset_protection', $apiResponse->action->command); + $this->assertEquals($this->rrset->zone, $apiResponse->action->resources[0]->id); + $this->assertEquals('zone', $apiResponse->action->resources[0]->type); + $this->assertLastRequestEquals('POST', '/zones/4711/rrsets/www/A/actions/change_protection'); + $this->assertLastRequestBodyParametersEqual(['change' => true]); + } + + public function testChangeTTL() + { + $this->mockHandler->append(new Response(200, [], $this->getGenericActionResponse('change_rrset_ttl'))); + $apiResponse = $this->rrset->changeTTL(50); + $this->assertEquals('change_rrset_ttl', $apiResponse->action->command); + $this->assertEquals($this->rrset->zone, $apiResponse->action->resources[0]->id); + $this->assertEquals('zone', $apiResponse->action->resources[0]->type); + $this->assertLastRequestEquals('POST', '/zones/4711/rrsets/www/A/actions/change_ttl'); + $this->assertLastRequestBodyParametersEqual(['ttl' => 50]); + } + + public function testSetRecords() + { + $this->mockHandler->append(new Response(200, [], $this->getGenericActionResponse('set_rrset_records'))); + $apiResponse = $this->rrset->setRecords([ + new Record('198.51.100.1', 'my webserver at Hetzner Cloud'), + ]); + $this->assertEquals('set_rrset_records', $apiResponse->action->command); + $this->assertEquals($this->rrset->zone, $apiResponse->action->resources[0]->id); + $this->assertEquals('zone', $apiResponse->action->resources[0]->type); + $this->assertLastRequestEquals('POST', '/zones/4711/rrsets/www/A/actions/set_records'); + $this->assertLastRequestBodyParametersEqual(['records' => [ + [ + "value" => "198.51.100.1", + "comment" => "my webserver at Hetzner Cloud" + ] + ]]); + } + + public function testAddRecords() + { + $this->mockHandler->append(new Response(200, [], $this->getGenericActionResponse('add_rrset_records'))); + $apiResponse = $this->rrset->addRecords([ + new Record('198.51.100.1', 'my webserver at Hetzner Cloud'), + ], 3600); + $this->assertEquals('add_rrset_records', $apiResponse->action->command); + $this->assertEquals($this->rrset->zone, $apiResponse->action->resources[0]->id); + $this->assertEquals('zone', $apiResponse->action->resources[0]->type); + $this->assertLastRequestEquals('POST', '/zones/4711/rrsets/www/A/actions/add_records'); + $this->assertLastRequestBodyParametersEqual(['ttl' => 3600, 'records' => [ + [ + "value" => "198.51.100.1", + "comment" => "my webserver at Hetzner Cloud" + ] + ]]); + } + + public function testRemoveRecords() + { + $this->mockHandler->append(new Response(200, [], $this->getGenericActionResponse('remove_rrset_records'))); + $apiResponse = $this->rrset->removeRecords([ + new Record('198.51.100.1', 'my webserver at Hetzner Cloud'), + ]); + $this->assertEquals('remove_rrset_records', $apiResponse->action->command); + $this->assertEquals($this->rrset->zone, $apiResponse->action->resources[0]->id); + $this->assertEquals('zone', $apiResponse->action->resources[0]->type); + $this->assertLastRequestEquals('POST', '/zones/4711/rrsets/www/A/actions/remove_records'); + $this->assertLastRequestBodyParametersEqual(['records' => [ + [ + "value" => "198.51.100.1", + "comment" => "my webserver at Hetzner Cloud" + ] + ]]); + } + + + protected function getGenericActionResponse(string $command) + { + return str_replace('$command', $command, file_get_contents(__DIR__ . '/fixtures/zone_action_generic.json')); + } +} From 4a6b3857cb71bdc9f3b8c708055f38df8d94e75b Mon Sep 17 00:00:00 2001 From: StyleCI Bot Date: Thu, 16 Oct 2025 18:57:03 +0000 Subject: [PATCH 10/14] Add all currently available endpoints --- src/Models/Zones/RRSet.php | 95 ++++++++++++++++++--------- tests/Unit/Models/Zones/RRSetTest.php | 33 +++++----- tests/Unit/Models/Zones/ZonesTest.php | 23 ++++--- 3 files changed, 90 insertions(+), 61 deletions(-) diff --git a/src/Models/Zones/RRSet.php b/src/Models/Zones/RRSet.php index 76f2edf..567a63b 100644 --- a/src/Models/Zones/RRSet.php +++ b/src/Models/Zones/RRSet.php @@ -22,8 +22,31 @@ class RRSet extends Model implements Resource public int $zone; /** - * @param string $id - * @param GuzzleClient|null $client + * Creates a new RRSet. This is useful if you want to create a new RRSet to pass to the createRRSet method of a Zone. + * + * @param string $name + * @param string $type + * @param array $records + * @param int|null $ttl + * @param array|null $labels + * @return RRSet|null + */ + public static function create(string $name, string $type, array $records, ?int $ttl = null, ?array $labels = []): ?RRSet + { + return (new RRset(""))->setAdditionalData((object)[ + 'name' => $name, + 'type' => $type, + 'ttl' => $ttl, + 'records' => $records, + 'labels' => (object)$labels, + 'protection' => (object)[], + 'zone' => 0, + ]); + } + + /** + * @param string $id + * @param GuzzleClient|null $client */ public function __construct(string $id, ?GuzzleClient $client = null) { @@ -45,6 +68,7 @@ public function setAdditionalData($data) $this->labels = get_object_vars($data->labels); $this->protection = RRSetProtection::parse($data->protection); $this->zone = $data->zone; + return $this; } @@ -61,7 +85,7 @@ public function __toRequest(): array 'ttl' => $this->ttl, 'records' => $this->records, ]; - if (!empty($this->labels)) { + if (! empty($this->labels)) { $r['labels'] = $this->labels; } @@ -70,6 +94,7 @@ public function __toRequest(): array /** * @return RRSet|null + * * @throws \LKDev\HetznerCloud\APIException */ public function reload() @@ -79,14 +104,15 @@ public function reload() /** * @return APIResponse|null + * * @throws \LKDev\HetznerCloud\APIException */ public function delete(): ?APIResponse { - $response = $this->httpClient->delete('zones/' . $this->zone . '/rrsets/' . $this->id); - if (!HetznerAPIClient::hasError($response)) { + $response = $this->httpClient->delete('zones/'.$this->zone.'/rrsets/'.$this->id); + if (! HetznerAPIClient::hasError($response)) { return APIResponse::create([ - 'action' => Action::parse(json_decode((string)$response->getBody())->action), + 'action' => Action::parse(json_decode((string) $response->getBody())->action), ], $response->getHeaders()); } @@ -94,18 +120,19 @@ public function delete(): ?APIResponse } /** - * @param array $data + * @param array $data * @return APIResponse|null + * * @throws \LKDev\HetznerCloud\APIException */ public function update(array $data): ?APIResponse { - $response = $this->httpClient->put('zones/' . $this->zone . '/rrsets/' . $this->id, [ + $response = $this->httpClient->put('zones/'.$this->zone.'/rrsets/'.$this->id, [ 'json' => $data, ]); - if (!HetznerAPIClient::hasError($response)) { + if (! HetznerAPIClient::hasError($response)) { return APIResponse::create([ - 'rrset' => self::parse(json_decode((string)$response->getBody())->rrset), + 'rrset' => self::parse(json_decode((string) $response->getBody())->rrset), ], $response->getHeaders()); } @@ -113,40 +140,43 @@ public function update(array $data): ?APIResponse } /** - * @param bool $change + * @param bool $change * @return APIResponse|null + * * @throws \LKDev\HetznerCloud\APIException */ public function changeProtection(bool $change): ?APIResponse { - $response = $this->httpClient->post('zones/' . $this->zone . '/rrsets/' . $this->id . '/actions/change_protection', [ + $response = $this->httpClient->post('zones/'.$this->zone.'/rrsets/'.$this->id.'/actions/change_protection', [ 'json' => [ 'change' => $change, ], ]); - if (!HetznerAPIClient::hasError($response)) { + if (! HetznerAPIClient::hasError($response)) { return APIResponse::create([ - 'action' => Action::parse(json_decode((string)$response->getBody())->action), + 'action' => Action::parse(json_decode((string) $response->getBody())->action), ], $response->getHeaders()); } + return null; } /** - * @param int $ttl + * @param int $ttl * @return APIResponse|null + * * @throws \LKDev\HetznerCloud\APIException */ public function changeTTL(int $ttl): ?APIResponse { - $response = $this->httpClient->post('zones/' . $this->zone . '/rrsets/' . $this->id . '/actions/change_ttl', [ + $response = $this->httpClient->post('zones/'.$this->zone.'/rrsets/'.$this->id.'/actions/change_ttl', [ 'json' => [ 'ttl' => $ttl, ], ]); - if (!HetznerAPIClient::hasError($response)) { + if (! HetznerAPIClient::hasError($response)) { return APIResponse::create([ - 'action' => Action::parse(json_decode((string)$response->getBody())->action), + 'action' => Action::parse(json_decode((string) $response->getBody())->action), ], $response->getHeaders()); } @@ -154,20 +184,21 @@ public function changeTTL(int $ttl): ?APIResponse } /** - * @param array $records + * @param array $records * @return APIResponse|null + * * @throws \LKDev\HetznerCloud\APIException */ public function setRecords(array $records): ?APIResponse { - $response = $this->httpClient->post('zones/' . $this->zone . '/rrsets/' . $this->id . '/actions/set_records', [ + $response = $this->httpClient->post('zones/'.$this->zone.'/rrsets/'.$this->id.'/actions/set_records', [ 'json' => [ 'records' => $records, ], ]); - if (!HetznerAPIClient::hasError($response)) { + if (! HetznerAPIClient::hasError($response)) { return APIResponse::create([ - 'action' => Action::parse(json_decode((string)$response->getBody())->action), + 'action' => Action::parse(json_decode((string) $response->getBody())->action), ], $response->getHeaders()); } @@ -175,22 +206,23 @@ public function setRecords(array $records): ?APIResponse } /** - * @param array $records - * @param int|null $ttl + * @param array $records + * @param int|null $ttl * @return APIResponse|null + * * @throws \LKDev\HetznerCloud\APIException */ public function addRecords(array $records, ?int $ttl = null): ?APIResponse { - $response = $this->httpClient->post('zones/' . $this->zone . '/rrsets/' . $this->id . '/actions/add_records', [ + $response = $this->httpClient->post('zones/'.$this->zone.'/rrsets/'.$this->id.'/actions/add_records', [ 'json' => [ 'records' => $records, 'ttl' => $ttl, ], ]); - if (!HetznerAPIClient::hasError($response)) { + if (! HetznerAPIClient::hasError($response)) { return APIResponse::create([ - 'action' => Action::parse(json_decode((string)$response->getBody())->action), + 'action' => Action::parse(json_decode((string) $response->getBody())->action), ], $response->getHeaders()); } @@ -198,20 +230,21 @@ public function addRecords(array $records, ?int $ttl = null): ?APIResponse } /** - * @param array $records + * @param array $records * @return APIResponse|null + * * @throws \LKDev\HetznerCloud\APIException */ public function removeRecords(array $records) { - $response = $this->httpClient->post('zones/' . $this->zone . '/rrsets/' . $this->id . '/actions/remove_records', [ + $response = $this->httpClient->post('zones/'.$this->zone.'/rrsets/'.$this->id.'/actions/remove_records', [ 'json' => [ 'records' => $records, ], ]); - if (!HetznerAPIClient::hasError($response)) { + if (! HetznerAPIClient::hasError($response)) { return APIResponse::create([ - 'action' => Action::parse(json_decode((string)$response->getBody())->action), + 'action' => Action::parse(json_decode((string) $response->getBody())->action), ], $response->getHeaders()); } diff --git a/tests/Unit/Models/Zones/RRSetTest.php b/tests/Unit/Models/Zones/RRSetTest.php index f198c1c..5e9f40e 100644 --- a/tests/Unit/Models/Zones/RRSetTest.php +++ b/tests/Unit/Models/Zones/RRSetTest.php @@ -3,11 +3,9 @@ namespace LKDev\Tests\Unit\Models\Zones; use GuzzleHttp\Psr7\Response; -use LKDev\HetznerCloud\Models\Zones\PrimaryNameserver; use LKDev\HetznerCloud\Models\Zones\Record; use LKDev\HetznerCloud\Models\Zones\RRSet; use LKDev\HetznerCloud\Models\Zones\Zone; -use LKDev\HetznerCloud\Models\Zones\Zones; use LKDev\Tests\TestCase; class RRSetTest extends TestCase @@ -20,8 +18,8 @@ class RRSetTest extends TestCase public function setUp(): void { parent::setUp(); - $this->mockHandler->append(new Response(200, [], file_get_contents(__DIR__ . '/fixtures/zone_rrset.json'))); - $this->rrset = (new Zone(4711))->getRRSetById('www/A');; + $this->mockHandler->append(new Response(200, [], file_get_contents(__DIR__.'/fixtures/zone_rrset.json'))); + $this->rrset = (new Zone(4711))->getRRSetById('www/A'); } public function testDelete() @@ -37,15 +35,15 @@ public function testDelete() public function testUpdate() { - $this->mockHandler->append(new Response(200, [], file_get_contents(__DIR__ . '/fixtures/zone_rrset.json'))); - $this->rrset->update(['labels' => ['environment' => 'prod']]);; + $this->mockHandler->append(new Response(200, [], file_get_contents(__DIR__.'/fixtures/zone_rrset.json'))); + $this->rrset->update(['labels' => ['environment' => 'prod']]); $this->assertLastRequestEquals('PUT', '/zones/4711/rrsets/www/A'); $this->assertLastRequestBodyParametersEqual(['labels' => ['environment' => 'prod']]); } public function testChangeProtection() { - $this->mockHandler->append(new Response(200, [], $this->getGenericActionResponse('change_rrset_protection')));; + $this->mockHandler->append(new Response(200, [], $this->getGenericActionResponse('change_rrset_protection'))); $apiResponse = $this->rrset->changeProtection(true); $this->assertEquals('change_rrset_protection', $apiResponse->action->command); $this->assertEquals($this->rrset->zone, $apiResponse->action->resources[0]->id); @@ -77,9 +75,9 @@ public function testSetRecords() $this->assertLastRequestEquals('POST', '/zones/4711/rrsets/www/A/actions/set_records'); $this->assertLastRequestBodyParametersEqual(['records' => [ [ - "value" => "198.51.100.1", - "comment" => "my webserver at Hetzner Cloud" - ] + 'value' => '198.51.100.1', + 'comment' => 'my webserver at Hetzner Cloud', + ], ]]); } @@ -95,9 +93,9 @@ public function testAddRecords() $this->assertLastRequestEquals('POST', '/zones/4711/rrsets/www/A/actions/add_records'); $this->assertLastRequestBodyParametersEqual(['ttl' => 3600, 'records' => [ [ - "value" => "198.51.100.1", - "comment" => "my webserver at Hetzner Cloud" - ] + 'value' => '198.51.100.1', + 'comment' => 'my webserver at Hetzner Cloud', + ], ]]); } @@ -113,15 +111,14 @@ public function testRemoveRecords() $this->assertLastRequestEquals('POST', '/zones/4711/rrsets/www/A/actions/remove_records'); $this->assertLastRequestBodyParametersEqual(['records' => [ [ - "value" => "198.51.100.1", - "comment" => "my webserver at Hetzner Cloud" - ] + 'value' => '198.51.100.1', + 'comment' => 'my webserver at Hetzner Cloud', + ], ]]); } - protected function getGenericActionResponse(string $command) { - return str_replace('$command', $command, file_get_contents(__DIR__ . '/fixtures/zone_action_generic.json')); + return str_replace('$command', $command, file_get_contents(__DIR__.'/fixtures/zone_action_generic.json')); } } diff --git a/tests/Unit/Models/Zones/ZonesTest.php b/tests/Unit/Models/Zones/ZonesTest.php index 38b09f3..f8f28a9 100644 --- a/tests/Unit/Models/Zones/ZonesTest.php +++ b/tests/Unit/Models/Zones/ZonesTest.php @@ -1,12 +1,11 @@ mockHandler->append(new Response(200, [], file_get_contents(__DIR__.'/fixtures/zone_create.json'))); + $this->mockHandler->append(new Response(200, [], file_get_contents(__DIR__ . '/fixtures/zone_create.json'))); $resp = $this->zones->create('example.com', ZoneMode::PRIMARY); $Zone = $resp->getResponsePart('zone'); @@ -41,11 +40,11 @@ public function testCreatePrimarySimple() public function testCreatePrimaryFull() { - $this->mockHandler->append(new Response(200, [], file_get_contents(__DIR__.'/fixtures/zone_create.json'))); + $this->mockHandler->append(new Response(200, [], file_get_contents(__DIR__ . '/fixtures/zone_create.json'))); $resp = $this->zones->create('example.com', ZoneMode::PRIMARY, 10, ['key' => 'value'], [], [ - new RRSet('', '@', 'A', 3600, [ + (RRSet::create('@', 'A', [ new Record('192.0.2.1', 'my comment'), - ], [], RRSetProtection::parse(['change' => false])), + ], 3600, [])), ]); $Zone = $resp->getResponsePart('zone'); @@ -64,10 +63,10 @@ public function testCreatePrimaryFull() public function testCreateSecondary() { - $this->mockHandler->append(new Response(200, [], file_get_contents(__DIR__.'/fixtures/zone_create.json'))); + $this->mockHandler->append(new Response(200, [], file_get_contents(__DIR__ . '/fixtures/zone_create.json'))); $resp = $this->zones->create('example.com', ZoneMode::SECONDARY, 10, ['key' => 'value'], [ new PrimaryNameserver('192.168.178.1', 53), - ], ); + ],); $Zone = $resp->getResponsePart('zone'); $this->assertEquals($Zone->id, 4711); @@ -86,7 +85,7 @@ public function testCreateSecondary() public function testGetByName() { - $this->mockHandler->append(new Response(200, [], file_get_contents(__DIR__.'/fixtures/zone.json'))); + $this->mockHandler->append(new Response(200, [], file_get_contents(__DIR__ . '/fixtures/zone.json'))); $Zone = $this->zones->getByName('example.com'); $this->assertEquals(4711, $Zone->id); $this->assertEquals('example.com', $Zone->name); @@ -96,7 +95,7 @@ public function testGetByName() public function testGet() { - $this->mockHandler->append(new Response(200, [], file_get_contents(__DIR__.'/fixtures/zone.json'))); + $this->mockHandler->append(new Response(200, [], file_get_contents(__DIR__ . '/fixtures/zone.json'))); $Zone = $this->zones->get(4711); $this->assertEquals($Zone->id, 4711); $this->assertEquals($Zone->name, 'example.com'); @@ -106,7 +105,7 @@ public function testGet() public function testAll() { - $this->mockHandler->append(new Response(200, [], file_get_contents(__DIR__.'/fixtures/zones.json'))); + $this->mockHandler->append(new Response(200, [], file_get_contents(__DIR__ . '/fixtures/zones.json'))); $zones = $this->zones->all(); $this->assertCount(1, $zones); $Zone = $zones[0]; @@ -118,7 +117,7 @@ public function testAll() public function testList() { - $this->mockHandler->append(new Response(200, [], file_get_contents(__DIR__.'/fixtures/zones.json'))); + $this->mockHandler->append(new Response(200, [], file_get_contents(__DIR__ . '/fixtures/zones.json'))); $zones = $this->zones->list()->zones; $this->assertCount(1, $zones); $Zone = $zones[0]; From 758566413757b8597f1b4a16b9f1da0f949492fe Mon Sep 17 00:00:00 2001 From: StyleCI Bot Date: Thu, 16 Oct 2025 19:05:50 +0000 Subject: [PATCH 11/14] Apply fixes from StyleCI --- src/Models/Zones/RRSet.php | 16 ++++++++-------- tests/Unit/Models/Zones/ZonesTest.php | 20 ++++++++++---------- 2 files changed, 18 insertions(+), 18 deletions(-) diff --git a/src/Models/Zones/RRSet.php b/src/Models/Zones/RRSet.php index 567a63b..b8482a4 100644 --- a/src/Models/Zones/RRSet.php +++ b/src/Models/Zones/RRSet.php @@ -24,22 +24,22 @@ class RRSet extends Model implements Resource /** * Creates a new RRSet. This is useful if you want to create a new RRSet to pass to the createRRSet method of a Zone. * - * @param string $name - * @param string $type - * @param array $records - * @param int|null $ttl - * @param array|null $labels + * @param string $name + * @param string $type + * @param array $records + * @param int|null $ttl + * @param array|null $labels * @return RRSet|null */ public static function create(string $name, string $type, array $records, ?int $ttl = null, ?array $labels = []): ?RRSet { - return (new RRset(""))->setAdditionalData((object)[ + return (new RRset(''))->setAdditionalData((object) [ 'name' => $name, 'type' => $type, 'ttl' => $ttl, 'records' => $records, - 'labels' => (object)$labels, - 'protection' => (object)[], + 'labels' => (object) $labels, + 'protection' => (object) [], 'zone' => 0, ]); } diff --git a/tests/Unit/Models/Zones/ZonesTest.php b/tests/Unit/Models/Zones/ZonesTest.php index f8f28a9..e795257 100644 --- a/tests/Unit/Models/Zones/ZonesTest.php +++ b/tests/Unit/Models/Zones/ZonesTest.php @@ -25,7 +25,7 @@ public function setUp(): void public function testCreatePrimarySimple() { - $this->mockHandler->append(new Response(200, [], file_get_contents(__DIR__ . '/fixtures/zone_create.json'))); + $this->mockHandler->append(new Response(200, [], file_get_contents(__DIR__.'/fixtures/zone_create.json'))); $resp = $this->zones->create('example.com', ZoneMode::PRIMARY); $Zone = $resp->getResponsePart('zone'); @@ -40,11 +40,11 @@ public function testCreatePrimarySimple() public function testCreatePrimaryFull() { - $this->mockHandler->append(new Response(200, [], file_get_contents(__DIR__ . '/fixtures/zone_create.json'))); + $this->mockHandler->append(new Response(200, [], file_get_contents(__DIR__.'/fixtures/zone_create.json'))); $resp = $this->zones->create('example.com', ZoneMode::PRIMARY, 10, ['key' => 'value'], [], [ - (RRSet::create('@', 'A', [ + RRSet::create('@', 'A', [ new Record('192.0.2.1', 'my comment'), - ], 3600, [])), + ], 3600, []), ]); $Zone = $resp->getResponsePart('zone'); @@ -63,10 +63,10 @@ public function testCreatePrimaryFull() public function testCreateSecondary() { - $this->mockHandler->append(new Response(200, [], file_get_contents(__DIR__ . '/fixtures/zone_create.json'))); + $this->mockHandler->append(new Response(200, [], file_get_contents(__DIR__.'/fixtures/zone_create.json'))); $resp = $this->zones->create('example.com', ZoneMode::SECONDARY, 10, ['key' => 'value'], [ new PrimaryNameserver('192.168.178.1', 53), - ],); + ], ); $Zone = $resp->getResponsePart('zone'); $this->assertEquals($Zone->id, 4711); @@ -85,7 +85,7 @@ public function testCreateSecondary() public function testGetByName() { - $this->mockHandler->append(new Response(200, [], file_get_contents(__DIR__ . '/fixtures/zone.json'))); + $this->mockHandler->append(new Response(200, [], file_get_contents(__DIR__.'/fixtures/zone.json'))); $Zone = $this->zones->getByName('example.com'); $this->assertEquals(4711, $Zone->id); $this->assertEquals('example.com', $Zone->name); @@ -95,7 +95,7 @@ public function testGetByName() public function testGet() { - $this->mockHandler->append(new Response(200, [], file_get_contents(__DIR__ . '/fixtures/zone.json'))); + $this->mockHandler->append(new Response(200, [], file_get_contents(__DIR__.'/fixtures/zone.json'))); $Zone = $this->zones->get(4711); $this->assertEquals($Zone->id, 4711); $this->assertEquals($Zone->name, 'example.com'); @@ -105,7 +105,7 @@ public function testGet() public function testAll() { - $this->mockHandler->append(new Response(200, [], file_get_contents(__DIR__ . '/fixtures/zones.json'))); + $this->mockHandler->append(new Response(200, [], file_get_contents(__DIR__.'/fixtures/zones.json'))); $zones = $this->zones->all(); $this->assertCount(1, $zones); $Zone = $zones[0]; @@ -117,7 +117,7 @@ public function testAll() public function testList() { - $this->mockHandler->append(new Response(200, [], file_get_contents(__DIR__ . '/fixtures/zones.json'))); + $this->mockHandler->append(new Response(200, [], file_get_contents(__DIR__.'/fixtures/zones.json'))); $zones = $this->zones->list()->zones; $this->assertCount(1, $zones); $Zone = $zones[0]; From 226483db3a866d8c881bbb43bf58d1f3e98ac24b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Lukas=20K=C3=A4mmerling?= Date: Thu, 16 Oct 2025 21:08:57 +0200 Subject: [PATCH 12/14] Add all currently available endpoints --- src/Models/Zones/RRSet.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Models/Zones/RRSet.php b/src/Models/Zones/RRSet.php index b8482a4..5bf26e4 100644 --- a/src/Models/Zones/RRSet.php +++ b/src/Models/Zones/RRSet.php @@ -33,7 +33,7 @@ class RRSet extends Model implements Resource */ public static function create(string $name, string $type, array $records, ?int $ttl = null, ?array $labels = []): ?RRSet { - return (new RRset(''))->setAdditionalData((object) [ + return (new RRSet(""))->setAdditionalData((object)[ 'name' => $name, 'type' => $type, 'ttl' => $ttl, From 5cff1157d79b6220802a16109ce43deefc3f422f Mon Sep 17 00:00:00 2001 From: StyleCI Bot Date: Thu, 16 Oct 2025 19:09:06 +0000 Subject: [PATCH 13/14] Apply fixes from StyleCI --- src/Models/Zones/RRSet.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Models/Zones/RRSet.php b/src/Models/Zones/RRSet.php index 5bf26e4..a4ccd46 100644 --- a/src/Models/Zones/RRSet.php +++ b/src/Models/Zones/RRSet.php @@ -33,7 +33,7 @@ class RRSet extends Model implements Resource */ public static function create(string $name, string $type, array $records, ?int $ttl = null, ?array $labels = []): ?RRSet { - return (new RRSet(""))->setAdditionalData((object)[ + return (new RRSet(''))->setAdditionalData((object) [ 'name' => $name, 'type' => $type, 'ttl' => $ttl, From d18e3787f9bdccd95063a1e6a53f20d8b8e88eaf Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Lukas=20K=C3=A4mmerling?= Date: Thu, 16 Oct 2025 21:10:38 +0200 Subject: [PATCH 14/14] Apply suggestions from code review Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> --- src/Models/Zones/RRSetProtection.php | 2 +- src/Models/Zones/RRSetRequestOpts.php | 2 +- src/Models/Zones/Zone.php | 10 +++++----- src/Models/Zones/ZoneRequestOpts.php | 2 +- 4 files changed, 8 insertions(+), 8 deletions(-) diff --git a/src/Models/Zones/RRSetProtection.php b/src/Models/Zones/RRSetProtection.php index c6fd9da..33049e2 100644 --- a/src/Models/Zones/RRSetProtection.php +++ b/src/Models/Zones/RRSetProtection.php @@ -22,7 +22,7 @@ class RRSetProtection extends Model /** * Protection constructor. * - * @param bool $delete + * @param bool $change */ public function __construct(bool $delete) { diff --git a/src/Models/Zones/RRSetRequestOpts.php b/src/Models/Zones/RRSetRequestOpts.php index 3f1a18b..794d172 100644 --- a/src/Models/Zones/RRSetRequestOpts.php +++ b/src/Models/Zones/RRSetRequestOpts.php @@ -5,7 +5,7 @@ use LKDev\HetznerCloud\RequestOpts; /** - * Class ServerRequestOpts. + * Class RRSetRequestOpts. */ class RRSetRequestOpts extends RequestOpts { diff --git a/src/Models/Zones/Zone.php b/src/Models/Zones/Zone.php index dae2ee2..e00d899 100644 --- a/src/Models/Zones/Zone.php +++ b/src/Models/Zones/Zone.php @@ -49,7 +49,7 @@ class Zone extends Model implements Resource public Protection|array $protection; /** - * @var object + * @var array */ public array $labels; @@ -112,7 +112,7 @@ public function setAdditionalData($data) /** * Reload the data of the zone. * - * @return zone + * @return Zone * * @throws \LKDev\HetznerCloud\APIException */ @@ -247,7 +247,7 @@ protected function replaceZoneIdInUri(string $uri): string /** * @param $input - * @return \LKDev\HetznerCloud\Models\Zones\Zone|static |null + * @return \LKDev\HetznerCloud\Models\Zones\Zone|static|null */ public static function parse($input) { @@ -260,7 +260,7 @@ public static function parse($input) /** * @param array $primary_nameservers - * @return void# + * @return APIResponse|null * * @throws APIException */ @@ -340,7 +340,7 @@ public function listRRSets(?RRSetRequestOpts $requestOpts = null): ?APIResponse * @param array $records * @param int|null $ttl * @param array|null $labels - * @return void + * @return APIResponse|null * * @throws APIException */ diff --git a/src/Models/Zones/ZoneRequestOpts.php b/src/Models/Zones/ZoneRequestOpts.php index 33d9991..f08a7cb 100644 --- a/src/Models/Zones/ZoneRequestOpts.php +++ b/src/Models/Zones/ZoneRequestOpts.php @@ -5,7 +5,7 @@ use LKDev\HetznerCloud\RequestOpts; /** - * Class ServerRequestOpts. + * Class ZoneRequestOpts. */ class ZoneRequestOpts extends RequestOpts {