diff --git a/CHANGELOG.md b/CHANGELOG.md index c514283..58aa635 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -6,6 +6,7 @@ CHANGE LOG * Add PHP 8.5 support * Fixed the `Droplet::create` SSH keys parameter type documentation +* Fixed hydration of object-backed App Platform and project resource fields ## 5.0.5 (03/05/2025) diff --git a/src/Entity/App.php b/src/Entity/App.php index 8033932..32df3b5 100644 --- a/src/Entity/App.php +++ b/src/Entity/App.php @@ -49,4 +49,33 @@ final class App extends AbstractEntity public string $liveDomain; public array $domains; + + public function build(array $parameters): void + { + foreach ($parameters as $property => $value) { + if ( + \in_array(static::convertToCamelCase($property), ['spec', 'activeDeployment', 'inProgressDeployment', 'region', 'domains'], true) && + ($value instanceof \stdClass || \is_array($value)) + ) { + $parameters[$property] = self::normalizeArray($value); + } + } + + parent::build($parameters); + } + + private static function normalizeArray(array|\stdClass $value): array + { + if ($value instanceof \stdClass) { + $value = \get_object_vars($value); + } + + foreach ($value as $key => $subValue) { + if ($subValue instanceof \stdClass || \is_array($subValue)) { + $value[$key] = self::normalizeArray($subValue); + } + } + + return $value; + } } diff --git a/src/Entity/AppDeployment.php b/src/Entity/AppDeployment.php index 0b29bdc..752f3a7 100644 --- a/src/Entity/AppDeployment.php +++ b/src/Entity/AppDeployment.php @@ -47,4 +47,33 @@ final class AppDeployment extends AbstractEntity public string $phase; public string $tierSlug; + + public function build(array $parameters): void + { + foreach ($parameters as $property => $value) { + if ( + \in_array(static::convertToCamelCase($property), ['spec', 'services', 'staticSites', 'workers', 'jobs', 'progress'], true) && + ($value instanceof \stdClass || \is_array($value)) + ) { + $parameters[$property] = self::normalizeArray($value); + } + } + + parent::build($parameters); + } + + private static function normalizeArray(array|\stdClass $value): array + { + if ($value instanceof \stdClass) { + $value = \get_object_vars($value); + } + + foreach ($value as $key => $subValue) { + if ($subValue instanceof \stdClass || \is_array($subValue)) { + $value[$key] = self::normalizeArray($subValue); + } + } + + return $value; + } } diff --git a/src/Entity/ProjectResource.php b/src/Entity/ProjectResource.php index f308b08..f5067ee 100644 --- a/src/Entity/ProjectResource.php +++ b/src/Entity/ProjectResource.php @@ -27,4 +27,15 @@ final class ProjectResource extends AbstractEntity public array $links; public string $status; + + public function build(array $parameters): void + { + foreach ($parameters as $property => $value) { + if ('links' === static::convertToCamelCase($property) && $value instanceof \stdClass) { + $parameters[$property] = \get_object_vars($value); + } + } + + parent::build($parameters); + } } diff --git a/tests/Entity/AppDeploymentTest.php b/tests/Entity/AppDeploymentTest.php index ffe79b8..a61f0a8 100644 --- a/tests/Entity/AppDeploymentTest.php +++ b/tests/Entity/AppDeploymentTest.php @@ -32,6 +32,7 @@ public function testConstructor(): void 'staticSites' => [], 'workers' => [], 'jobs' => [], + 'phaseLastUpdatedAt' => '2021-02-10T17:05:30Z', 'createdAt' => '2021-02-10T17:05:30Z', 'updatedAt' => '2021-02-10T17:05:30Z', 'cause' => 'cause', @@ -51,6 +52,7 @@ public function testConstructor(): void self::assertSame($values['staticSites'], $entity->staticSites); self::assertSame($values['workers'], $entity->workers); self::assertSame($values['jobs'], $entity->jobs); + self::assertSame($values['phaseLastUpdatedAt'], $entity->phaseLastUpdatedAt); self::assertSame($values['createdAt'], $entity->createdAt); self::assertSame($values['updatedAt'], $entity->updatedAt); self::assertSame($values['cause'], $entity->cause); @@ -60,9 +62,172 @@ public function testConstructor(): void self::assertSame($values['tierSlug'], $entity->tierSlug); self::assertSame($values['staticSites'], $entity->static_sites); + self::assertSame($values['phaseLastUpdatedAt'], $entity->phase_last_updated_at); self::assertSame($values['createdAt'], $entity->created_at); self::assertSame($values['updatedAt'], $entity->updated_at); self::assertSame($values['clonedFrom'], $entity->cloned_from); self::assertSame($values['tierSlug'], $entity->tier_slug); } + + public function testConstructorAcceptsApiShapedObjects(): void + { + $payload = \json_decode(<<<'JSON' +{ + "id": "b6bdf840-2854-4f87-a36c-5f231c617c84", + "spec": { + "name": "sample-golang", + "services": [ + { + "name": "web", + "github": { + "repo": "digitalocean/sample-golang", + "branch": "main" + }, + "routes": [ + { + "path": "/" + } + ] + } + ] + }, + "services": [ + { + "name": "web", + "source_commit_hash": "abc123", + "alerts": [ + { + "rule": "CPU_UTILIZATION" + } + ] + } + ], + "static_sites": [ + { + "name": "docs", + "routes": [ + { + "path": "/docs" + } + ] + } + ], + "workers": [ + { + "name": "queue", + "instance_count": 1 + } + ], + "jobs": [ + { + "name": "migrate", + "kind": "POST_DEPLOY" + } + ], + "phase_last_updated_at": "2024-01-02T00:03:00Z", + "created_at": "2024-01-02T00:00:00Z", + "updated_at": "2024-01-02T00:04:00Z", + "cause": "manual", + "cloned_from": "dep-0", + "progress": { + "success_steps": 1, + "steps": [ + { + "name": "build", + "components": [ + { + "name": "web" + } + ] + } + ] + }, + "phase": "ACTIVE", + "tier_slug": "basic" +} +JSON, false, 512, \JSON_THROW_ON_ERROR); + + $expectedSpec = [ + 'name' => 'sample-golang', + 'services' => [ + [ + 'name' => 'web', + 'github' => [ + 'repo' => 'digitalocean/sample-golang', + 'branch' => 'main', + ], + 'routes' => [ + [ + 'path' => '/', + ], + ], + ], + ], + ]; + $expectedServices = [ + [ + 'name' => 'web', + 'source_commit_hash' => 'abc123', + 'alerts' => [ + [ + 'rule' => 'CPU_UTILIZATION', + ], + ], + ], + ]; + $expectedStaticSites = [ + [ + 'name' => 'docs', + 'routes' => [ + [ + 'path' => '/docs', + ], + ], + ], + ]; + $expectedWorkers = [ + [ + 'name' => 'queue', + 'instance_count' => 1, + ], + ]; + $expectedJobs = [ + [ + 'name' => 'migrate', + 'kind' => 'POST_DEPLOY', + ], + ]; + $expectedProgress = [ + 'success_steps' => 1, + 'steps' => [ + [ + 'name' => 'build', + 'components' => [ + [ + 'name' => 'web', + ], + ], + ], + ], + ]; + + $entity = new AppDeployment($payload); + + self::assertInstanceOf(AbstractEntity::class, $entity); + self::assertInstanceOf(AppDeployment::class, $entity); + self::assertSame('b6bdf840-2854-4f87-a36c-5f231c617c84', $entity->id); + self::assertSame($expectedSpec, $entity->spec); + self::assertSame($expectedServices, $entity->services); + self::assertSame($expectedStaticSites, $entity->staticSites); + self::assertSame($expectedWorkers, $entity->workers); + self::assertSame($expectedJobs, $entity->jobs); + self::assertSame($expectedProgress, $entity->progress); + self::assertSame('2024-01-02T00:03:00Z', $entity->phaseLastUpdatedAt); + self::assertSame('dep-0', $entity->clonedFrom); + self::assertSame('basic', $entity->tierSlug); + self::assertSame($expectedStaticSites, $entity->static_sites); + self::assertSame('2024-01-02T00:03:00Z', $entity->phase_last_updated_at); + self::assertSame('dep-0', $entity->cloned_from); + self::assertSame('basic', $entity->tier_slug); + } } diff --git a/tests/Entity/AppTest.php b/tests/Entity/AppTest.php index b77e9ab..b50774f 100644 --- a/tests/Entity/AppTest.php +++ b/tests/Entity/AppTest.php @@ -75,4 +75,133 @@ public function testConstructor(): void self::assertSame($values['liveUrlBase'], $entity->live_url_base); self::assertSame($values['liveDomain'], $entity->live_domain); } + + public function testConstructorAcceptsApiShapedObjects(): void + { + $payload = \json_decode(<<<'JSON' +{ + "id": "4f6c71e2-1e90-4762-9fee-6cc4a0a9f2cf", + "owner_uuid": "uuid", + "spec": { + "name": "sample-golang", + "services": [ + { + "name": "web", + "github": { + "repo": "digitalocean/sample-golang", + "branch": "main" + }, + "routes": [ + { + "path": "/" + } + ] + } + ] + }, + "default_ingress": "sample.ondigitalocean.app", + "created_at": "2024-01-01T00:00:00Z", + "updated_at": "2024-01-02T00:00:00Z", + "active_deployment": { + "id": "dep-active", + "phase": "ACTIVE", + "progress": { + "success_steps": 1, + "steps": [ + { + "name": "build", + "status": "SUCCESS" + } + ] + } + }, + "in_progress_deployment": { + "id": "dep-progress", + "phase": "BUILDING", + "spec": { + "name": "sample-golang" + } + }, + "last_deployment_created_at": "2024-01-02T00:01:00Z", + "live_url": "https://sample.ondigitalocean.app", + "region": { + "slug": "nyc", + "label": "New York" + }, + "tier_slug": "basic", + "live_url_base": "https://sample.ondigitalocean.app", + "live_domain": "sample.ondigitalocean.app", + "domains": [ + { + "domain": "example.com", + "type": "PRIMARY" + } + ] +} +JSON, false, 512, \JSON_THROW_ON_ERROR); + + $expectedSpec = [ + 'name' => 'sample-golang', + 'services' => [ + [ + 'name' => 'web', + 'github' => [ + 'repo' => 'digitalocean/sample-golang', + 'branch' => 'main', + ], + 'routes' => [ + [ + 'path' => '/', + ], + ], + ], + ], + ]; + $expectedActiveDeployment = [ + 'id' => 'dep-active', + 'phase' => 'ACTIVE', + 'progress' => [ + 'success_steps' => 1, + 'steps' => [ + [ + 'name' => 'build', + 'status' => 'SUCCESS', + ], + ], + ], + ]; + $expectedInProgressDeployment = [ + 'id' => 'dep-progress', + 'phase' => 'BUILDING', + 'spec' => [ + 'name' => 'sample-golang', + ], + ]; + $expectedRegion = [ + 'slug' => 'nyc', + 'label' => 'New York', + ]; + $expectedDomains = [ + [ + 'domain' => 'example.com', + 'type' => 'PRIMARY', + ], + ]; + + $entity = new App($payload); + + self::assertInstanceOf(AbstractEntity::class, $entity); + self::assertInstanceOf(App::class, $entity); + self::assertSame('4f6c71e2-1e90-4762-9fee-6cc4a0a9f2cf', $entity->id); + self::assertSame('uuid', $entity->ownerUuid); + self::assertSame($expectedSpec, $entity->spec); + self::assertSame($expectedActiveDeployment, $entity->activeDeployment); + self::assertSame($expectedInProgressDeployment, $entity->inProgressDeployment); + self::assertSame($expectedRegion, $entity->region); + self::assertSame($expectedDomains, $entity->domains); + self::assertSame($expectedActiveDeployment, $entity->active_deployment); + self::assertSame($expectedInProgressDeployment, $entity->in_progress_deployment); + self::assertSame('2024-01-02T00:01:00Z', $entity->last_deployment_created_at); + self::assertSame('basic', $entity->tier_slug); + } } diff --git a/tests/ProjectResourceEntityTest.php b/tests/ProjectResourceEntityTest.php index 388467d..7af8785 100644 --- a/tests/ProjectResourceEntityTest.php +++ b/tests/ProjectResourceEntityTest.php @@ -42,4 +42,23 @@ public function testConstructor(): void self::assertSame(['self' => 'https://api.digitalocean.com/v2/droplets/123456789'], $projectResource->links); self::assertSame('already_assigned', $projectResource->status); } + + public function testConstructorAcceptsApiShapedLinksObject(): void + { + $projectResource = new ProjectResource([ + 'urn' => 'do:droplet:123456789', + 'assigned_at' => '2022-08-04T04:26:24Z', + 'links' => (object) [ + 'self' => 'https://api.digitalocean.com/v2/droplets/123456789', + ], + 'status' => 'already_assigned', + ]); + + self::assertInstanceOf(AbstractEntity::class, $projectResource); + self::assertInstanceOf(ProjectResource::class, $projectResource); + self::assertSame('do:droplet:123456789', $projectResource->urn); + self::assertSame('2022-08-04T04:26:24Z', $projectResource->assignedAt); + self::assertSame(['self' => 'https://api.digitalocean.com/v2/droplets/123456789'], $projectResource->links); + self::assertSame('already_assigned', $projectResource->status); + } }