From 0305666acabcd4b384033e4315517e7bf2254552 Mon Sep 17 00:00:00 2001 From: Prem Palanisamy Date: Fri, 8 May 2026 08:25:33 +0100 Subject: [PATCH 01/10] feat: bump appwrite/appwrite to 23.* (Backups service, breaking changes) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Adds typed Backups SDK service support (cloud uses this). Fixes PHPStan errors against SDK 23: - Replace removed Runtime::DENO121/124/135 (kept ::DENO140 onward) - Functions::create / Sites::create: specification → buildSpecification + runtimeSpecification (SDK 21 split) - importPasswordUser return type: array → \Appwrite\Models\User|null - API::getRow: call ->toArray() since SDK 22+ returns typed Row NOTE: many other call sites in Sources/Destinations still use array offset access on typed responses. Those paths will break at runtime under SDK 23. This commit is the minimum to ship typed-Backups support in cloud; a follow-up refactor will convert all array-offset access to property access. --- composer.json | 2 +- composer.lock | 70 +++++++++++-------- src/Migration/Destinations/Appwrite.php | 16 ++--- src/Migration/Sources/Appwrite/Reader/API.php | 2 +- 4 files changed, 47 insertions(+), 43 deletions(-) diff --git a/composer.json b/composer.json index a52b23a9..8649e3be 100644 --- a/composer.json +++ b/composer.json @@ -25,7 +25,7 @@ "php": ">=8.1", "ext-curl": "*", "ext-openssl": "*", - "appwrite/appwrite": "19.*", + "appwrite/appwrite": "23.*", "utopia-php/database": "5.*", "utopia-php/storage": "2.*", "utopia-php/dsn": "0.2.*", diff --git a/composer.lock b/composer.lock index 15882491..c32afe70 100644 --- a/composer.lock +++ b/composer.lock @@ -4,29 +4,29 @@ "Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies", "This file is @generated automatically" ], - "content-hash": "38981f8df096cfbc9dd34487643b7ed6", + "content-hash": "577cd7fb43f3f832869661126e0a5090", "packages": [ { "name": "appwrite/appwrite", - "version": "19.1.0", + "version": "23.0.0", "source": { "type": "git", "url": "https://github.com/appwrite/sdk-for-php.git", - "reference": "8738e812062f899c85b2598eef43d6a247f08a56" + "reference": "d22b36167931294bec4259f0803aba14537030ec" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/appwrite/sdk-for-php/zipball/8738e812062f899c85b2598eef43d6a247f08a56", - "reference": "8738e812062f899c85b2598eef43d6a247f08a56", + "url": "https://api.github.com/repos/appwrite/sdk-for-php/zipball/d22b36167931294bec4259f0803aba14537030ec", + "reference": "d22b36167931294bec4259f0803aba14537030ec", "shasum": "" }, "require": { "ext-curl": "*", "ext-json": "*", - "php": ">=7.1.0" + "php": ">=8.2.0" }, "require-dev": { - "mockery/mockery": "^1.6.12", + "mockery/mockery": "1.6.12", "phpunit/phpunit": "^10" }, "type": "library", @@ -39,14 +39,14 @@ "license": [ "BSD-3-Clause" ], - "description": "Appwrite is an open-source self-hosted backend server that abstract and simplify complex and repetitive development tasks behind a very simple REST API", + "description": "Appwrite is an open-source self-hosted backend server that abstracts and simplifies complex and repetitive development tasks behind a very simple REST API", "support": { "email": "team@appwrite.io", "issues": "https://github.com/appwrite/sdk-for-php/issues", - "source": "https://github.com/appwrite/sdk-for-php/tree/19.1.0", + "source": "https://github.com/appwrite/sdk-for-php/tree/23.0.0", "url": "https://appwrite.io/support" }, - "time": "2025-12-18T08:07:43+00:00" + "time": "2026-04-16T12:38:44+00:00" }, { "name": "brick/math", @@ -1420,16 +1420,16 @@ }, { "name": "symfony/deprecation-contracts", - "version": "v3.6.0", + "version": "v3.7.0", "source": { "type": "git", "url": "https://github.com/symfony/deprecation-contracts.git", - "reference": "63afe740e99a13ba87ec199bb07bbdee937a5b62" + "reference": "50f59d1f3ca46d41ac911f97a78626b6756af35b" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/deprecation-contracts/zipball/63afe740e99a13ba87ec199bb07bbdee937a5b62", - "reference": "63afe740e99a13ba87ec199bb07bbdee937a5b62", + "url": "https://api.github.com/repos/symfony/deprecation-contracts/zipball/50f59d1f3ca46d41ac911f97a78626b6756af35b", + "reference": "50f59d1f3ca46d41ac911f97a78626b6756af35b", "shasum": "" }, "require": { @@ -1442,7 +1442,7 @@ "name": "symfony/contracts" }, "branch-alias": { - "dev-main": "3.6-dev" + "dev-main": "3.7-dev" } }, "autoload": { @@ -1467,7 +1467,7 @@ "description": "A generic function and convention to trigger deprecation notices", "homepage": "https://symfony.com", "support": { - "source": "https://github.com/symfony/deprecation-contracts/tree/v3.6.0" + "source": "https://github.com/symfony/deprecation-contracts/tree/v3.7.0" }, "funding": [ { @@ -1478,12 +1478,16 @@ "url": "https://github.com/fabpot", "type": "github" }, + { + "url": "https://github.com/nicolas-grekas", + "type": "github" + }, { "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", "type": "tidelift" } ], - "time": "2024-09-25T14:21:43+00:00" + "time": "2026-04-13T15:52:40+00:00" }, { "name": "symfony/http-client", @@ -1588,16 +1592,16 @@ }, { "name": "symfony/http-client-contracts", - "version": "v3.6.0", + "version": "v3.7.0", "source": { "type": "git", "url": "https://github.com/symfony/http-client-contracts.git", - "reference": "75d7043853a42837e68111812f4d964b01e5101c" + "reference": "4a2d00c37651c0bdc2b9e1c773487a8bf4edb12d" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/http-client-contracts/zipball/75d7043853a42837e68111812f4d964b01e5101c", - "reference": "75d7043853a42837e68111812f4d964b01e5101c", + "url": "https://api.github.com/repos/symfony/http-client-contracts/zipball/4a2d00c37651c0bdc2b9e1c773487a8bf4edb12d", + "reference": "4a2d00c37651c0bdc2b9e1c773487a8bf4edb12d", "shasum": "" }, "require": { @@ -1610,7 +1614,7 @@ "name": "symfony/contracts" }, "branch-alias": { - "dev-main": "3.6-dev" + "dev-main": "3.7-dev" } }, "autoload": { @@ -1646,7 +1650,7 @@ "standards" ], "support": { - "source": "https://github.com/symfony/http-client-contracts/tree/v3.6.0" + "source": "https://github.com/symfony/http-client-contracts/tree/v3.7.0" }, "funding": [ { @@ -1657,12 +1661,16 @@ "url": "https://github.com/fabpot", "type": "github" }, + { + "url": "https://github.com/nicolas-grekas", + "type": "github" + }, { "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", "type": "tidelift" } ], - "time": "2025-04-29T11:18:49+00:00" + "time": "2026-03-06T13:17:50+00:00" }, { "name": "symfony/polyfill-mbstring", @@ -1991,16 +1999,16 @@ }, { "name": "symfony/service-contracts", - "version": "v3.6.1", + "version": "v3.7.0", "source": { "type": "git", "url": "https://github.com/symfony/service-contracts.git", - "reference": "45112560a3ba2d715666a509a0bc9521d10b6c43" + "reference": "d25d82433a80eba6aa0e6c24b61d7370d99e444a" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/service-contracts/zipball/45112560a3ba2d715666a509a0bc9521d10b6c43", - "reference": "45112560a3ba2d715666a509a0bc9521d10b6c43", + "url": "https://api.github.com/repos/symfony/service-contracts/zipball/d25d82433a80eba6aa0e6c24b61d7370d99e444a", + "reference": "d25d82433a80eba6aa0e6c24b61d7370d99e444a", "shasum": "" }, "require": { @@ -2018,7 +2026,7 @@ "name": "symfony/contracts" }, "branch-alias": { - "dev-main": "3.6-dev" + "dev-main": "3.7-dev" } }, "autoload": { @@ -2054,7 +2062,7 @@ "standards" ], "support": { - "source": "https://github.com/symfony/service-contracts/tree/v3.6.1" + "source": "https://github.com/symfony/service-contracts/tree/v3.7.0" }, "funding": [ { @@ -2074,7 +2082,7 @@ "type": "tidelift" } ], - "time": "2025-07-15T11:30:57+00:00" + "time": "2026-03-28T09:44:51+00:00" }, { "name": "tbachert/spi", diff --git a/src/Migration/Destinations/Appwrite.php b/src/Migration/Destinations/Appwrite.php index 312f7e3f..c4271c66 100644 --- a/src/Migration/Destinations/Appwrite.php +++ b/src/Migration/Destinations/Appwrite.php @@ -2151,11 +2151,11 @@ public function importAuthResource(Resource $resource): Resource /** * @param User $user - * @return array|null + * @return \Appwrite\Models\User|null * @throws AppwriteException * @throws \Exception */ - public function importPasswordUser(User $user): ?array + public function importPasswordUser(User $user): ?\Appwrite\Models\User { $hash = $user->getPasswordHash(); $result = null; @@ -2277,9 +2277,6 @@ public function importFunctionResource(Resource $resource): Resource 'dart-2.17' => Runtime::DART217(), 'dart-2.18' => Runtime::DART218(), 'dart-2.19' => Runtime::DART219(), - 'deno-1.21' => Runtime::DENO121(), - 'deno-1.24' => Runtime::DENO124(), - 'deno-1.35' => Runtime::DENO135(), 'deno-1.40' => Runtime::DENO140(), 'deno-1.46' => Runtime::DENO146(), 'deno-2.0' => Runtime::DENO20(), @@ -2322,7 +2319,8 @@ public function importFunctionResource(Resource $resource): Resource $resource->getEntrypoint(), $resource->getCommands(), $resource->getScopes(), - specification: $resource->getSpecification() ?: null, + buildSpecification: $resource->getSpecification() ?: null, + runtimeSpecification: $resource->getSpecification() ?: null, ); break; case Resource::TYPE_ENVIRONMENT_VARIABLE: @@ -2498,9 +2496,6 @@ public function importSiteResource(Resource $resource): Resource 'dart-2.17' => BuildRuntime::DART217(), 'dart-2.18' => BuildRuntime::DART218(), 'dart-2.19' => BuildRuntime::DART219(), - 'deno-1.21' => BuildRuntime::DENO121(), - 'deno-1.24' => BuildRuntime::DENO124(), - 'deno-1.35' => BuildRuntime::DENO135(), 'deno-1.40' => BuildRuntime::DENO140(), 'deno-1.46' => BuildRuntime::DENO146(), 'deno-2.0' => BuildRuntime::DENO20(), @@ -2573,7 +2568,8 @@ public function importSiteResource(Resource $resource): Resource $resource->getOutputDirectory(), $adapter, fallbackFile: $resource->getFallbackFile(), - specification: $resource->getSpecification(), + buildSpecification: $resource->getSpecification() ?: null, + runtimeSpecification: $resource->getSpecification() ?: null, ); break; case Resource::TYPE_SITE_VARIABLE: diff --git a/src/Migration/Sources/Appwrite/Reader/API.php b/src/Migration/Sources/Appwrite/Reader/API.php index 47a52f9d..f08a1f5f 100644 --- a/src/Migration/Sources/Appwrite/Reader/API.php +++ b/src/Migration/Sources/Appwrite/Reader/API.php @@ -199,7 +199,7 @@ public function getRow(Table $resource, string $rowId, array $queries = []): arr $resource->getId(), $rowId, $queries - ); + )->toArray(); } /** From a5195bad3e7f56c40d45fdf03166e6e59a170c02 Mon Sep 17 00:00:00 2001 From: Prem Palanisamy Date: Fri, 8 May 2026 12:11:49 +0100 Subject: [PATCH 02/10] refactor: convert SDK array-access to property access for typed models SDK 22+ returns typed Models without ArrayAccess, so existing \$response['key'] patterns fail at runtime ('Cannot use object of type Appwrite\Models\\\*List as array'). Convert all SDK call sites in Sources and Destinations to property access: - \$list['total'] / ['users'|'teams'|...] -> \$list->total / ->users - \$user['\$id'] -> \$user->id (SDK 22+ drops \$ prefix) - \$bucket['\$permissions'] -> \$bucket->permissions API Reader keeps its array contract (matches Database reader, used by existing callers): converts SDK typed responses via array_map ->toArray at the I/O boundary. \Appwrite\Models\Deployment param types added to exportDeploymentData and exportSiteDeploymentData. PHPStan level 7 finds zero remaining 'Cannot access offset on Appwrite\Models\\\*' errors. Level 3 (project default) clean. --- src/Migration/Destinations/Appwrite.php | 2 +- src/Migration/Sources/Appwrite.php | 386 +++++++++--------- src/Migration/Sources/Appwrite/Reader/API.php | 87 ++-- 3 files changed, 242 insertions(+), 233 deletions(-) diff --git a/src/Migration/Destinations/Appwrite.php b/src/Migration/Destinations/Appwrite.php index c4271c66..34866141 100644 --- a/src/Migration/Destinations/Appwrite.php +++ b/src/Migration/Destinations/Appwrite.php @@ -1999,7 +1999,7 @@ public function importFileResource(Resource $resource): Resource $resource->getTransformations() ); - $resource->setId($response['$id']); + $resource->setId($response->id); } $resource->setStatus(Resource::STATUS_SUCCESS); diff --git a/src/Migration/Sources/Appwrite.php b/src/Migration/Sources/Appwrite.php index 48dc03d9..9c92fce7 100644 --- a/src/Migration/Sources/Appwrite.php +++ b/src/Migration/Sources/Appwrite.php @@ -288,7 +288,7 @@ private function reportAuth(array $resources, array &$report, array $resourceIds limit: 1 ); $userList = $this->users->list($userQueries); - $report[Resource::TYPE_USER] = $userList['total']; + $report[Resource::TYPE_USER] = $userList->total; } if ($needTeams) { @@ -304,11 +304,11 @@ private function reportAuth(array $resources, array &$report, array $resourceIds ); $teamList = $this->teams->list($params); - $totalTeams = $teamList['total']; - $currentTeams = $teamList['teams']; + $totalTeams = $teamList->total; + $currentTeams = $teamList->teams; $allTeams = array_merge($allTeams, $currentTeams); - $lastTeam = $currentTeams[count($currentTeams) - 1]['$id'] ?? null; + $lastTeam = end($currentTeams)?->id ?? null; if (count($currentTeams) < self::DEFAULT_PAGE_LIMIT) { break; @@ -322,7 +322,7 @@ private function reportAuth(array $resources, array &$report, array $resourceIds limit: 1 ); $teamList = $this->teams->list($params); - $teams = ['total' => $teamList['total'], 'teams' => []]; + $teams = ['total' => $teamList->total, 'teams' => []]; } } @@ -334,9 +334,9 @@ private function reportAuth(array $resources, array &$report, array $resourceIds $report[Resource::TYPE_MEMBERSHIP] = 0; foreach ($teams['teams'] as $team) { $report[Resource::TYPE_MEMBERSHIP] += $this->teams->listMemberships( - $team['$id'], + $team->id, [Query::limit(1)] - )['total']; + )->total; } } } @@ -364,7 +364,7 @@ private function reportStorage(array $resources, array &$report, array $resource resourceIds: $resourceIds, limit: 1 ); - $report[Resource::TYPE_BUCKET] = $this->storage->listBuckets($bucketQueries)['total']; + $report[Resource::TYPE_BUCKET] = $this->storage->listBuckets($bucketQueries)->total; } if (\in_array(Resource::TYPE_FILE, $resources)) { @@ -379,10 +379,10 @@ private function reportStorage(array $resources, array &$report, array $resource resourceIds: $resourceIds, cursor: $lastBucket, ); - $currentBuckets = $this->storage->listBuckets($queries)['buckets']; + $currentBuckets = $this->storage->listBuckets($queries)->buckets; $buckets = array_merge($buckets, $currentBuckets); - $lastBucket = $buckets[count($buckets) - 1]['$id'] ?? null; + $lastBucket = $buckets[count($buckets) - 1]->id ?? null; if (count($currentBuckets) < self::DEFAULT_PAGE_LIMIT) { break; @@ -391,12 +391,12 @@ private function reportStorage(array $resources, array &$report, array $resource foreach ($buckets as $bucket) { $filesResponse = $this->storage->listFiles( - $bucket['$id'], + $bucket->id, [Query::limit(1)] ); - $report['size'] += $bucket['totalSize'] ?? 0; - $report[Resource::TYPE_FILE] += $filesResponse['total']; + $report['size'] += $bucket->totalSize ?? 0; + $report[Resource::TYPE_FILE] += $filesResponse->total; } $report['size'] = $report['size'] / 1000 / 1000; // MB @@ -419,7 +419,7 @@ private function reportFunctions(array $resources, array &$report, array $resour resourceIds: $resourceIds, limit: 1 ); - $report[Resource::TYPE_FUNCTION] = $this->functions->list($functionQueries)['total']; + $report[Resource::TYPE_FUNCTION] = $this->functions->list($functionQueries)->total; return; } @@ -433,11 +433,11 @@ private function reportFunctions(array $resources, array &$report, array $resour ); $funcList = $this->functions->list($params); - $totalFunctions = $funcList['total']; - $currentFunctions = $funcList['functions']; + $totalFunctions = $funcList->total; + $currentFunctions = $funcList->functions; $functions = array_merge($functions, $currentFunctions); - $lastFunction = $currentFunctions[count($currentFunctions) - 1]['$id'] ?? null; + $lastFunction = $currentFunctions[count($currentFunctions) - 1]->id ?? null; if (count($currentFunctions) < self::DEFAULT_PAGE_LIMIT) { break; } @@ -451,7 +451,7 @@ private function reportFunctions(array $resources, array &$report, array $resour if (\in_array(Resource::TYPE_DEPLOYMENT, $resources)) { $report[Resource::TYPE_DEPLOYMENT] = 0; foreach ($functions as $function) { - if (!empty($function['deploymentId'])) { + if (!empty($function->deploymentId)) { $report[Resource::TYPE_DEPLOYMENT] += 1; } } @@ -461,7 +461,7 @@ private function reportFunctions(array $resources, array &$report, array $resour $report[Resource::TYPE_ENVIRONMENT_VARIABLE] = 0; foreach ($functions as $function) { // function model contains `vars`, we don't need to fetch the list again. - $report[Resource::TYPE_ENVIRONMENT_VARIABLE] += count($function['vars'] ?? []); + $report[Resource::TYPE_ENVIRONMENT_VARIABLE] += count($function->vars ?? []); } } } @@ -482,7 +482,7 @@ private function reportSites(array $resources, array &$report, array $resourceId resourceIds: $resourceIds, limit: 1 ); - $report[Resource::TYPE_SITE] = $this->sites->list($siteQueries)['total']; + $report[Resource::TYPE_SITE] = $this->sites->list($siteQueries)->total; return; } @@ -496,15 +496,15 @@ private function reportSites(array $resources, array &$report, array $resourceId ); $siteList = $this->sites->list($params); - $totalSites = $siteList['total']; - $currentSites = $siteList['sites']; + $totalSites = $siteList->total; + $currentSites = $siteList->sites; $sites = array_merge($sites, $currentSites); if (count($currentSites) === 0 || count($currentSites) < self::DEFAULT_PAGE_LIMIT) { break; } - $lastSite = $currentSites[count($currentSites) - 1]['$id']; + $lastSite = $currentSites[count($currentSites) - 1]->id; } } @@ -515,7 +515,7 @@ private function reportSites(array $resources, array &$report, array $resourceId if (\in_array(Resource::TYPE_SITE_DEPLOYMENT, $resources)) { $report[Resource::TYPE_SITE_DEPLOYMENT] = 0; foreach ($sites as $site) { - if (!empty($site['deploymentId'])) { + if (!empty($site->deploymentId)) { $report[Resource::TYPE_SITE_DEPLOYMENT] += 1; } } @@ -524,8 +524,8 @@ private function reportSites(array $resources, array &$report, array $resourceId if (\in_array(Resource::TYPE_SITE_VARIABLE, $resources)) { $report[Resource::TYPE_SITE_VARIABLE] = 0; foreach ($sites as $site) { - $variables = $this->sites->listVariables($site['$id']); - $report[Resource::TYPE_SITE_VARIABLE] += $variables['total'] ?? 0; + $variables = $this->sites->listVariables($site->id); + $report[Resource::TYPE_SITE_VARIABLE] += $variables->total ?? 0; } } } @@ -603,27 +603,27 @@ private function exportUsers(int $batchSize): void } $response = $this->users->list($queries); - if ($response['total'] == 0) { + if ($response->total == 0) { break; } - foreach ($response['users'] as $user) { + foreach ($response->users as $user) { $users[] = new User( - $user['$id'], - empty($user['email']) ? null : $user['email'], - empty($user['name']) ? null : $user['name'], - $user['password'] ? new Hash($user['password'], algorithm: $user['hash']) : null, - empty($user['phone']) ? null : $user['phone'], - $user['labels'] ?? [], + $user->id, + empty($user->email) ? null : $user->email, + empty($user->name) ? null : $user->name, + $user->password ? new Hash($user->password, algorithm: $user->hash) : null, + empty($user->phone) ? null : $user->phone, + $user->labels ?? [], '', - $user['emailVerification'] ?? false, - $user['phoneVerification'] ?? false, - !$user['status'], - $user['prefs'] ?? [], - $user['targets'] ?? [], + $user->emailVerification ?? false, + $user->phoneVerification ?? false, + !$user->status, + $user->prefs ?? [], + $user->targets ?? [], ); - $lastDocument = $user['$id']; + $lastDocument = $user->id; } $this->callback($users); @@ -657,18 +657,18 @@ private function exportTeams(int $batchSize): void } $response = $this->teams->list($queries); - if ($response['total'] == 0) { + if ($response->total == 0) { break; } - foreach ($response['teams'] as $team) { + foreach ($response->teams as $team) { $teams[] = new Team( - $team['$id'], - $team['name'], - $team['prefs'], + $team->id, + $team->name, + $team->prefs, ); - $lastDocument = $team['$id']; + $lastDocument = $team->id; } $this->callback($teams); @@ -709,25 +709,25 @@ private function exportMemberships(int $batchSize): void $response = $this->teams->listMemberships($team->getId(), $queries); - if ($response['total'] == 0) { + if ($response->total == 0) { break; } - foreach ($response['memberships'] as $membership) { - $user = $cacheUsers[$membership['userId']] ?? null; + foreach ($response->memberships as $membership) { + $user = $cacheUsers[$membership->userId] ?? null; if ($user === null) { throw new \Exception('User not found', Exception::CODE_NOT_FOUND); } $memberships[] = new Membership( - $membership['$id'], + $membership->id, $team, $user, - $membership['roles'], - $membership['confirm'] + $membership->roles, + $membership->confirm ); - $lastDocument = $membership['$id']; + $lastDocument = $membership->id; } $this->callback($memberships); @@ -1245,20 +1245,20 @@ private function exportBuckets(int $batchSize): void $convertedBuckets = []; - foreach ($buckets['buckets'] as $bucket) { + foreach ($buckets->buckets as $bucket) { $bucket = new Bucket( - $bucket['$id'], - $bucket['name'], - $bucket['$permissions'], - $bucket['fileSecurity'], - $bucket['enabled'], - $bucket['maximumFileSize'], - $bucket['allowedFileExtensions'], - $bucket['compression'], - $bucket['encryption'], - $bucket['antivirus'], + $bucket->id, + $bucket->name, + $bucket->permissions, + $bucket->fileSecurity, + $bucket->enabled, + $bucket->maximumFileSize, + $bucket->allowedFileExtensions, + $bucket->compression, + $bucket->encryption, + $bucket->antivirus, false, - $bucket['transformations'] ?? false, + $bucket->transformations ?? false, ); $convertedBuckets[] = $bucket; } @@ -1293,31 +1293,31 @@ private function exportFiles(int $batchSize): void $queries ); - foreach ($response['files'] as $file) { + foreach ($response->files as $file) { try { $this->exportFileData(new File( - $file['$id'], + $file->id, $bucket, - $file['name'], - $file['signature'], - $file['mimeType'], - $file['$permissions'], - $file['sizeOriginal'], + $file->name, + $file->signature, + $file->mimeType, + $file->permissions, + $file->sizeOriginal, )); } catch (\Throwable $e) { $this->addError(new Exception( resourceName: Resource::TYPE_FILE, resourceGroup: Transfer::GROUP_STORAGE, - resourceId: $file['$id'], + resourceId: $file->id, message: $e->getMessage(), code: $e->getCode() )); } - $lastDocument = $file['$id']; + $lastDocument = $file->id; } - if (count($response['files']) < $batchSize) { + if (count($response->files) < $batchSize) { break; } } @@ -1463,40 +1463,40 @@ private function exportFunctions(int $batchSize): void $response = $this->functions->list($queries); - if ($response['total'] === 0) { + if ($response->total === 0) { return; } $functions = []; $convertedResources = []; - foreach ($response['functions'] as $function) { + foreach ($response->functions as $function) { $convertedFunc = new Func( - $function['$id'], - $function['name'], - $function['runtime'], - $function['execute'], - $function['enabled'], - $function['events'], - $function['schedule'], - $function['timeout'], - $function['deploymentId'] ?? '', - $function['entrypoint'], - $function['commands'] ?? '', - $function['logging'] ?? true, - $function['scopes'] ?? [], - $function['specification'] ?? '', + $function->id, + $function->name, + $function->runtime, + $function->execute, + $function->enabled, + $function->events, + $function->schedule, + $function->timeout, + $function->deploymentId ?? '', + $function->entrypoint, + $function->commands ?? '', + $function->logging ?? true, + $function->scopes ?? [], + $function->specification ?? '', ); $functions[] = $convertedFunc; $convertedResources[] = $convertedFunc; - foreach ($function['vars'] as $var) { + foreach ($function->vars as $var) { $convertedResources[] = new EnvVar( - $var['$id'], + $var->id, $convertedFunc, - $var['key'], - $var['value'], + $var->key, + $var->value, ); } } @@ -1551,17 +1551,17 @@ private function exportDeployments(int $batchSize, bool $exportOnlyActive = fals $queries ); - foreach ($response['deployments'] as $deployment) { + foreach ($response->deployments as $deployment) { try { $this->exportDeploymentData($func, $deployment); } catch (\Throwable $e) { $func->setStatus(Resource::STATUS_ERROR, $e->getMessage()); } - $lastDocument = $deployment['$id']; + $lastDocument = $deployment->id; } - if (count($response['deployments']) < $batchSize) { + if (count($response->deployments) < $batchSize) { break; } } @@ -1571,7 +1571,7 @@ private function exportDeployments(int $batchSize, bool $exportOnlyActive = fals /** * @throws \Exception */ - private function exportDeploymentData(Func $func, array $deployment): void + private function exportDeploymentData(Func $func, \Appwrite\Models\Deployment $deployment): void { // Set the chunk size (5MB) $start = 0; @@ -1582,7 +1582,7 @@ private function exportDeploymentData(Func $func, array $deployment): void $this->call( 'HEAD', - "/functions/{$func->getId()}/deployments/{$deployment['$id']}/download", + "/functions/{$func->getId()}/deployments/{$deployment->id}/download", [], [], $responseHeaders @@ -1592,7 +1592,7 @@ private function exportDeploymentData(Func $func, array $deployment): void if (!array_key_exists('content-length', $responseHeaders)) { $file = $this->call( 'GET', - "/functions/{$func->getId()}/deployments/{$deployment['$id']}/download", + "/functions/{$func->getId()}/deployments/{$deployment->id}/download", [], [], $responseHeaders @@ -1605,14 +1605,14 @@ private function exportDeploymentData(Func $func, array $deployment): void } $deployment = new Deployment( - $deployment['$id'], + $deployment->id, $func, $size, - $deployment['entrypoint'], + $deployment->entrypoint, $start, $end, $file, - $deployment['activate'] + $deployment->activate ); $deployment->setSequence($deployment->getId()); @@ -1628,14 +1628,14 @@ private function exportDeploymentData(Func $func, array $deployment): void } $deployment = new Deployment( - $deployment['$id'], + $deployment->id, $func, $fileSize, - $deployment['entrypoint'], + $deployment->entrypoint, $start, $end, '', - $deployment['activate'] + $deployment->activate ); $deployment->setSequence($deployment->getId()); @@ -1680,7 +1680,7 @@ private function reportMessaging(array $resources, array &$report, array $resour resourceIds: $resourceIds, limit: 1 ); - $report[Resource::TYPE_PROVIDER] = $this->messaging->listProviders($providerQueries)['total']; + $report[Resource::TYPE_PROVIDER] = $this->messaging->listProviders($providerQueries)->total; } if (\in_array(Resource::TYPE_TOPIC, $resources)) { @@ -1689,7 +1689,7 @@ private function reportMessaging(array $resources, array &$report, array $resour resourceIds: $resourceIds, limit: 1 ); - $report[Resource::TYPE_TOPIC] = $this->messaging->listTopics($topicQueries)['total']; + $report[Resource::TYPE_TOPIC] = $this->messaging->listTopics($topicQueries)->total; } if (\in_array(Resource::TYPE_SUBSCRIBER, $resources)) { @@ -1703,16 +1703,16 @@ private function reportMessaging(array $resources, array &$report, array $resour } $topicResponse = $this->messaging->listTopics($topicQueries); - if ($topicResponse['total'] == 0 || empty($topicResponse['topics'])) { + if ($topicResponse->total == 0 || empty($topicResponse->topics)) { break; } - foreach ($topicResponse['topics'] as $topic) { - $subscriberTotal += $this->messaging->listSubscribers($topic['$id'], [Query::limit(1)])['total']; - $lastTopic = $topic['$id']; + foreach ($topicResponse->topics as $topic) { + $subscriberTotal += $this->messaging->listSubscribers($topic->id, [Query::limit(1)])->total; + $lastTopic = $topic->id; } - if (\count($topicResponse['topics']) < self::DEFAULT_PAGE_LIMIT) { + if (\count($topicResponse->topics) < self::DEFAULT_PAGE_LIMIT) { break; } } @@ -1726,7 +1726,7 @@ private function reportMessaging(array $resources, array &$report, array $resour resourceIds: $resourceIds, limit: 1 ); - $report[Resource::TYPE_MESSAGE] = $this->messaging->listMessages($messageQueries)['total']; + $report[Resource::TYPE_MESSAGE] = $this->messaging->listMessages($messageQueries)->total; } } @@ -1812,24 +1812,24 @@ private function exportProviders(int $batchSize): void $response = $this->messaging->listProviders($queries); - if ($response['total'] == 0) { + if ($response->total == 0) { break; } - foreach ($response['providers'] as $provider) { + foreach ($response->providers as $provider) { $providers[] = new Provider( - $provider['$id'], - $provider['name'], - $provider['provider'], - $provider['type'], - $provider['enabled'], - $provider['credentials'] ?? [], - $provider['options'] ?? [], - $provider['$createdAt'] ?? '', - $provider['$updatedAt'] ?? '', + $provider->id, + $provider->name, + $provider->provider, + $provider->type, + $provider->enabled, + $provider->credentials ?? [], + $provider->options ?? [], + $provider->createdAt ?? '', + $provider->updatedAt ?? '', ); - $lastDocument = $provider['$id']; + $lastDocument = $provider->id; } $this->callback($providers); @@ -1863,20 +1863,20 @@ private function exportTopics(int $batchSize): void $response = $this->messaging->listTopics($queries); - if ($response['total'] == 0) { + if ($response->total == 0) { break; } - foreach ($response['topics'] as $topic) { + foreach ($response->topics as $topic) { $topics[] = new Topic( - $topic['$id'], - $topic['name'], - $topic['subscribe'] ?? [], - $topic['$createdAt'] ?? '', - $topic['$updatedAt'] ?? '', + $topic->id, + $topic->name, + $topic->subscribe ?? [], + $topic->createdAt ?? '', + $topic->updatedAt ?? '', ); - $lastDocument = $topic['$id']; + $lastDocument = $topic->id; } $this->callback($topics); @@ -1909,23 +1909,23 @@ private function exportSubscribers(int $batchSize): void $response = $this->messaging->listSubscribers($topic->getId(), $queries); - if ($response['total'] == 0) { + if ($response->total == 0) { break; } - foreach ($response['subscribers'] as $subscriber) { + foreach ($response->subscribers as $subscriber) { $subscribers[] = new Subscriber( - $subscriber['$id'], - $subscriber['topicId'], - $subscriber['targetId'], - $subscriber['userId'] ?? '', - $subscriber['userName'] ?? '', - $subscriber['providerType'] ?? '', - $subscriber['$createdAt'] ?? '', - $subscriber['$updatedAt'] ?? '', + $subscriber->id, + $subscriber->topicId, + $subscriber->targetId, + $subscriber->userId ?? '', + $subscriber->userName ?? '', + $subscriber->providerType ?? '', + $subscriber->createdAt ?? '', + $subscriber->updatedAt ?? '', ); - $lastDocument = $subscriber['$id']; + $lastDocument = $subscriber->id; } $this->callback($subscribers); @@ -1960,28 +1960,28 @@ private function exportMessages(int $batchSize): void $response = $this->messaging->listMessages($queries); - if ($response['total'] == 0) { + if ($response->total == 0) { break; } - foreach ($response['messages'] as $message) { + foreach ($response->messages as $message) { $messages[] = new Message( - $message['$id'], - $message['providerType'] ?? '', - $message['topics'] ?? [], - $message['users'] ?? [], - $message['targets'] ?? [], - $message['data'] ?? [], - $message['status'] ?? '', - $message['scheduledAt'] ?? '', - $message['deliveredAt'] ?? '', - $message['deliveryErrors'] ?? [], - $message['deliveredTotal'] ?? 0, - $message['$createdAt'] ?? '', - $message['$updatedAt'] ?? '', + $message->id, + $message->providerType ?? '', + $message->topics ?? [], + $message->users ?? [], + $message->targets ?? [], + $message->data ?? [], + $message->status ?? '', + $message->scheduledAt ?? '', + $message->deliveredAt ?? '', + $message->deliveryErrors ?? [], + $message->deliveredTotal ?? 0, + $message->createdAt ?? '', + $message->updatedAt ?? '', ); - $lastDocument = $message['$id']; + $lastDocument = $message->id; } $this->callback($messages); @@ -2015,40 +2015,40 @@ private function exportSites(int $batchSize): void $response = $this->sites->list($queries); - if ($response['total'] === 0) { + if ($response->total === 0) { return; } $sites = []; $convertedResources = []; - foreach ($response['sites'] as $site) { + foreach ($response->sites as $site) { $convertedSite = new Site( - $site['$id'], - $site['name'], - $site['framework'], - $site['buildRuntime'], - $site['enabled'], - $site['logging'], - $site['timeout'], - $site['installCommand'] ?? '', - $site['buildCommand'] ?? '', - $site['outputDirectory'] ?? '', - $site['adapter'] ?? 'static', - $site['fallbackFile'] ?? '', - $site['specification'] ?? '', - $site['deploymentId'] ?? '' + $site->id, + $site->name, + $site->framework, + $site->buildRuntime, + $site->enabled, + $site->logging, + $site->timeout, + $site->installCommand ?? '', + $site->buildCommand ?? '', + $site->outputDirectory ?? '', + $site->adapter ?? 'static', + $site->fallbackFile ?? '', + $site->specification ?? '', + $site->deploymentId ?? '' ); $sites[] = $convertedSite; $convertedResources[] = $convertedSite; - $variables = $this->sites->listVariables($site['$id']); - foreach ($variables['variables'] ?? [] as $var) { + $variables = $this->sites->listVariables($site->id); + foreach ($variables->variables ?? [] as $var) { $convertedResources[] = new SiteEnvVar( - $var['$id'], + $var->id, $convertedSite, - $var['key'], - $var['value'] + $var->key, + $var->value ); } } @@ -2104,17 +2104,17 @@ private function exportSiteDeployments(int $batchSize, bool $exportOnlyActive = $queries ); - foreach ($response['deployments'] as $deployment) { + foreach ($response->deployments as $deployment) { try { $this->exportSiteDeploymentData($site, $deployment); } catch (\Throwable $e) { $site->setStatus(Resource::STATUS_ERROR, $e->getMessage()); } - $lastDocument = $deployment['$id']; + $lastDocument = $deployment->id; } - if (count($response['deployments']) < $batchSize) { + if (count($response->deployments) < $batchSize) { break; } } @@ -2124,7 +2124,7 @@ private function exportSiteDeployments(int $batchSize, bool $exportOnlyActive = /** * @throws \Exception */ - private function exportSiteDeploymentData(Site $site, array $deployment): void + private function exportSiteDeploymentData(Site $site, \Appwrite\Models\Deployment $deployment): void { $start = 0; $end = Transfer::STORAGE_MAX_CHUNK_SIZE - 1; @@ -2133,7 +2133,7 @@ private function exportSiteDeploymentData(Site $site, array $deployment): void $this->call( 'HEAD', - "/sites/{$site->getId()}/deployments/{$deployment['$id']}/download", + "/sites/{$site->getId()}/deployments/{$deployment->id}/download", [], [], $responseHeaders @@ -2142,7 +2142,7 @@ private function exportSiteDeploymentData(Site $site, array $deployment): void if (!\array_key_exists('content-length', $responseHeaders)) { $file = $this->call( 'GET', - "/sites/{$site->getId()}/deployments/{$deployment['$id']}/download", + "/sites/{$site->getId()}/deployments/{$deployment->id}/download", [], [], $responseHeaders @@ -2155,13 +2155,13 @@ private function exportSiteDeploymentData(Site $site, array $deployment): void } $siteDeployment = new SiteDeployment( - $deployment['$id'], + $deployment->id, $site, $size, $start, $end, $file, - $deployment['$id'] === $site->getActiveDeployment() + $deployment->id === $site->getActiveDeployment() ); $siteDeployment->setSequence($siteDeployment->getId()); @@ -2177,13 +2177,13 @@ private function exportSiteDeploymentData(Site $site, array $deployment): void } $siteDeployment = new SiteDeployment( - $deployment['$id'], + $deployment->id, $site, $fileSize, $start, $end, '', - $deployment['$id'] === $site->getActiveDeployment() + $deployment->id === $site->getActiveDeployment() ); $siteDeployment->setSequence($siteDeployment->getId()); diff --git a/src/Migration/Sources/Appwrite/Reader/API.php b/src/Migration/Sources/Appwrite/Reader/API.php index f08a1f5f..a187e8c9 100644 --- a/src/Migration/Sources/Appwrite/Reader/API.php +++ b/src/Migration/Sources/Appwrite/Reader/API.php @@ -11,6 +11,10 @@ use Utopia\Migration\Sources\Appwrite\Reader; /** + * Adapts Appwrite SDK typed models to the Reader's plain-array contract so that + * downstream callers can use array-offset syntax interchangeably with the + * Database reader (which returns utopia-php Document objects). + * * @implements Reader */ class API implements Reader @@ -51,10 +55,10 @@ public function report(array $resources, array &$report, array $resourceIds = [] } $databasesResponse = $this->database->list($databaseQueries); - $databases = $databasesResponse['databases']; + $databases = \array_map(fn ($database) => $database->toArray(), $databasesResponse->databases); if (in_array(Resource::TYPE_DATABASE, $resources)) { - $report[Resource::TYPE_DATABASE] = $databasesResponse['total']; + $report[Resource::TYPE_DATABASE] = $databasesResponse->total; } if (count(array_intersect($resources, $relevantResources)) === 1 && @@ -71,12 +75,13 @@ public function report(array $resources, array &$report, array $resourceIds = [] $lastTable = null; while (true) { - $currentTables = $this->database->listTables( + $tablesResponse = $this->database->listTables( $databaseId, $lastTable ? [Query::cursorAfter($lastTable)] : [Query::limit($pageLimit)] - )['tables']; + ); + $currentTables = \array_map(fn ($table) => $table->toArray(), $tablesResponse->tables); $tables = \array_merge($tables, $currentTables); $lastTable = $tables[count($tables) - 1]['$id'] ?? null; @@ -111,7 +116,7 @@ public function report(array $resources, array &$report, array $resourceIds = [] [Query::limit(1)] ); - $report[Resource::TYPE_ROW] += $rowsResponse['total']; + $report[Resource::TYPE_ROW] += $rowsResponse->total; } } } @@ -121,75 +126,79 @@ public function report(array $resources, array &$report, array $resourceIds = [] } /** + * @return array> * @throws AppwriteException */ public function listDatabases(array $queries = []): array { - return $this->database->list($queries)['databases']; + return \array_map( + fn ($database) => $database->toArray(), + $this->database->list($queries)->databases + ); } /** + * @return array> * @throws AppwriteException */ public function listTables(Database $resource, array $queries = []): array { - return $this->database->listTables( - $resource->getId(), - $queries - )['tables']; + return \array_map( + fn ($table) => $table->toArray(), + $this->database->listTables($resource->getId(), $queries)->tables + ); } /** - * @param Table $resource - * @param array $queries - * @return array + * @return array> * @throws AppwriteException */ public function listColumns(Table $resource, array $queries = []): array { - return $this->database->listColumns( - $resource->getDatabase()->getId(), - $resource->getId(), - $queries - )['columns']; + return \array_map( + fn ($column) => $column->toArray(), + $this->database->listColumns( + $resource->getDatabase()->getId(), + $resource->getId(), + $queries + )->columns + ); } /** - * @param Table $resource - * @param array $queries - * @return array + * @return array> * @throws AppwriteException */ public function listIndexes(Table $resource, array $queries = []): array { - return $this->database->listIndexes( - $resource->getDatabase()->getId(), - $resource->getId(), - $queries - )['indexes']; + return \array_map( + fn ($index) => $index->toArray(), + $this->database->listIndexes( + $resource->getDatabase()->getId(), + $resource->getId(), + $queries + )->indexes + ); } - /** - * @param Table $resource - * @param array $queries - * @return array + * @return array> * @throws AppwriteException */ public function listRows(Table $resource, array $queries = []): array { - return $this->database->listRows( - $resource->getDatabase()->getId(), - $resource->getId(), - $queries - )['rows']; + return \array_map( + fn ($row) => $row->toArray(), + $this->database->listRows( + $resource->getDatabase()->getId(), + $resource->getId(), + $queries + )->rows + ); } /** - * @param Table $resource - * @param string $rowId - * @param array $queries - * @return array + * @return array * @throws AppwriteException */ public function getRow(Table $resource, string $rowId, array $queries = []): array From 80e9a0498e1a1a2667607fa4b4aca44a7b4590ab Mon Sep 17 00:00:00 2001 From: Prem Palanisamy Date: Fri, 8 May 2026 16:29:18 +0100 Subject: [PATCH 03/10] fix: handle SDK 23 nested typed objects (Preferences, Target, MessageStatus) Five runtime bugs missed in the previous regex pass; PHPStan level 8 caught them when checking against the SDK Models. Migration's resource constructors expect plain arrays / strings, not the nested typed objects SDK 23 returns: - \$user->prefs is Preferences object -> use ->data array - \$user->targets is array -> array_map(fn(\$t) => \$t->toArray()) - \$team->prefs same Preferences fix - \$function->specification doesn't exist on FunctionModel any more; read runtimeSpecification ?: buildSpecification (SDK 21 split) - \$site->specification same Site fix - \$message->status is MessageStatus enum -> cast to string via __toString PHPStan level 3 (project default) clean. Level 8 has no remaining SDK type issues across Sources/Destinations/Reader. --- src/Migration/Sources/Appwrite.php | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/src/Migration/Sources/Appwrite.php b/src/Migration/Sources/Appwrite.php index 9c92fce7..2b5906d8 100644 --- a/src/Migration/Sources/Appwrite.php +++ b/src/Migration/Sources/Appwrite.php @@ -619,8 +619,8 @@ private function exportUsers(int $batchSize): void $user->emailVerification ?? false, $user->phoneVerification ?? false, !$user->status, - $user->prefs ?? [], - $user->targets ?? [], + $user->prefs->data ?? [], + \array_map(fn ($target) => $target->toArray(), $user->targets ?? []), ); $lastDocument = $user->id; @@ -665,7 +665,7 @@ private function exportTeams(int $batchSize): void $teams[] = new Team( $team->id, $team->name, - $team->prefs, + $team->prefs->data, ); $lastDocument = $team->id; @@ -1485,7 +1485,7 @@ private function exportFunctions(int $batchSize): void $function->commands ?? '', $function->logging ?? true, $function->scopes ?? [], - $function->specification ?? '', + $function->runtimeSpecification ?: $function->buildSpecification ?: '', ); $functions[] = $convertedFunc; @@ -1972,7 +1972,7 @@ private function exportMessages(int $batchSize): void $message->users ?? [], $message->targets ?? [], $message->data ?? [], - $message->status ?? '', + (string) $message->status, $message->scheduledAt ?? '', $message->deliveredAt ?? '', $message->deliveryErrors ?? [], @@ -2036,7 +2036,7 @@ private function exportSites(int $batchSize): void $site->outputDirectory ?? '', $site->adapter ?? 'static', $site->fallbackFile ?? '', - $site->specification ?? '', + $site->runtimeSpecification ?: $site->buildSpecification ?: '', $site->deploymentId ?? '' ); $sites[] = $convertedSite; From 0e88268d10fef0475de1fa292cbb571ce64e4837 Mon Sep 17 00:00:00 2001 From: Prem Palanisamy Date: Sat, 9 May 2026 01:23:29 +0100 Subject: [PATCH 04/10] fix: pass variableId to Functions::createVariable and Sites::createVariable SDK 23.1 added a required \$variableId 2nd parameter: - Sites::createVariable(\$siteId, \$variableId, \$key, \$value, ?\$secret) - Functions::createVariable(\$functionId, \$variableId, \$key, \$value, ?\$secret) Pass the migration resource's id as \$variableId so the destination preserves the source variable id. Bumps appwrite/appwrite to 23.1.0 in composer.lock. --- composer.lock | 12 ++++++------ src/Migration/Destinations/Appwrite.php | 2 ++ 2 files changed, 8 insertions(+), 6 deletions(-) diff --git a/composer.lock b/composer.lock index c32afe70..bd8c429b 100644 --- a/composer.lock +++ b/composer.lock @@ -8,16 +8,16 @@ "packages": [ { "name": "appwrite/appwrite", - "version": "23.0.0", + "version": "23.1.0", "source": { "type": "git", "url": "https://github.com/appwrite/sdk-for-php.git", - "reference": "d22b36167931294bec4259f0803aba14537030ec" + "reference": "2f275921f10ceb7cff99f2d463f7328b296234fa" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/appwrite/sdk-for-php/zipball/d22b36167931294bec4259f0803aba14537030ec", - "reference": "d22b36167931294bec4259f0803aba14537030ec", + "url": "https://api.github.com/repos/appwrite/sdk-for-php/zipball/2f275921f10ceb7cff99f2d463f7328b296234fa", + "reference": "2f275921f10ceb7cff99f2d463f7328b296234fa", "shasum": "" }, "require": { @@ -43,10 +43,10 @@ "support": { "email": "team@appwrite.io", "issues": "https://github.com/appwrite/sdk-for-php/issues", - "source": "https://github.com/appwrite/sdk-for-php/tree/23.0.0", + "source": "https://github.com/appwrite/sdk-for-php/tree/23.1.0", "url": "https://appwrite.io/support" }, - "time": "2026-04-16T12:38:44+00:00" + "time": "2026-05-08T13:44:58+00:00" }, { "name": "brick/math", diff --git a/src/Migration/Destinations/Appwrite.php b/src/Migration/Destinations/Appwrite.php index 34866141..29a68634 100644 --- a/src/Migration/Destinations/Appwrite.php +++ b/src/Migration/Destinations/Appwrite.php @@ -2327,6 +2327,7 @@ public function importFunctionResource(Resource $resource): Resource /** @var EnvVar $resource */ $this->functions->createVariable( $resource->getFunc()->getId(), + $resource->getId(), $resource->getKey(), $resource->getValue() ); @@ -2576,6 +2577,7 @@ public function importSiteResource(Resource $resource): Resource /** @var SiteEnvVar $resource */ $this->sites->createVariable( $resource->getSite()->getId(), + $resource->getId(), $resource->getKey(), $resource->getValue() ); From 6deabc665472639d5615e923cc43bb3d9c8aadca Mon Sep 17 00:00:00 2001 From: Prem Palanisamy Date: Sun, 10 May 2026 05:07:41 +0100 Subject: [PATCH 05/10] fix: use named arguments for Sites/Functions create + createVariable MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit SDK 23.1 inserted \$startCommand between \$buildCommand and \$outputDirectory in Sites::create, which silently shifted positional calls — outputDirectory landed in startCommand and adapter landed in outputDirectory, causing testAppwriteMigrationSite to migrate sites with empty adapter. Fixed by switching to named arguments. Apply the same to: - Functions::create - Functions::createVariable - Sites::createVariable Named args are robust against future positional insertions in the generated SDK. --- src/Migration/Destinations/Appwrite.php | 62 ++++++++++++------------- 1 file changed, 31 insertions(+), 31 deletions(-) diff --git a/src/Migration/Destinations/Appwrite.php b/src/Migration/Destinations/Appwrite.php index 29a68634..71ab9da8 100644 --- a/src/Migration/Destinations/Appwrite.php +++ b/src/Migration/Destinations/Appwrite.php @@ -2307,18 +2307,18 @@ public function importFunctionResource(Resource $resource): Resource }; $this->functions->create( - $resource->getId(), - $resource->getFunctionName(), - $runtime, - $resource->getExecute(), - $resource->getEvents(), - $resource->getSchedule(), - $resource->getTimeout(), - $resource->getEnabled(), - $resource->getLogging(), - $resource->getEntrypoint(), - $resource->getCommands(), - $resource->getScopes(), + functionId: $resource->getId(), + name: $resource->getFunctionName(), + runtime: $runtime, + execute: $resource->getExecute(), + events: $resource->getEvents(), + schedule: $resource->getSchedule(), + timeout: $resource->getTimeout(), + enabled: $resource->getEnabled(), + logging: $resource->getLogging(), + entrypoint: $resource->getEntrypoint(), + commands: $resource->getCommands(), + scopes: $resource->getScopes(), buildSpecification: $resource->getSpecification() ?: null, runtimeSpecification: $resource->getSpecification() ?: null, ); @@ -2326,10 +2326,10 @@ public function importFunctionResource(Resource $resource): Resource case Resource::TYPE_ENVIRONMENT_VARIABLE: /** @var EnvVar $resource */ $this->functions->createVariable( - $resource->getFunc()->getId(), - $resource->getId(), - $resource->getKey(), - $resource->getValue() + functionId: $resource->getFunc()->getId(), + variableId: $resource->getId(), + key: $resource->getKey(), + value: $resource->getValue(), ); break; case Resource::TYPE_DEPLOYMENT: @@ -2557,17 +2557,17 @@ public function importSiteResource(Resource $resource): Resource }; $this->sites->create( - $resource->getId(), - $resource->getSiteName(), - $framework, - $buildRuntime, - $resource->getEnabled(), - $resource->getLogging(), - $resource->getTimeout(), - $resource->getInstallCommand(), - $resource->getBuildCommand(), - $resource->getOutputDirectory(), - $adapter, + siteId: $resource->getId(), + name: $resource->getSiteName(), + framework: $framework, + buildRuntime: $buildRuntime, + enabled: $resource->getEnabled(), + logging: $resource->getLogging(), + timeout: $resource->getTimeout(), + installCommand: $resource->getInstallCommand(), + buildCommand: $resource->getBuildCommand(), + outputDirectory: $resource->getOutputDirectory(), + adapter: $adapter, fallbackFile: $resource->getFallbackFile(), buildSpecification: $resource->getSpecification() ?: null, runtimeSpecification: $resource->getSpecification() ?: null, @@ -2576,10 +2576,10 @@ public function importSiteResource(Resource $resource): Resource case Resource::TYPE_SITE_VARIABLE: /** @var SiteEnvVar $resource */ $this->sites->createVariable( - $resource->getSite()->getId(), - $resource->getId(), - $resource->getKey(), - $resource->getValue() + siteId: $resource->getSite()->getId(), + variableId: $resource->getId(), + key: $resource->getKey(), + value: $resource->getValue(), ); break; case Resource::TYPE_SITE_DEPLOYMENT: From e5dc657423bea96941cd71e5f0b060fd8b047b56 Mon Sep 17 00:00:00 2001 From: Prem Palanisamy Date: Mon, 11 May 2026 02:20:10 +0100 Subject: [PATCH 06/10] fix: fallback deprecated deno-1.21/1.24/1.35 runtimes to DENO140 Migrations from older Appwrite servers may still have functions registered with deno-1.21 / 1.24 / 1.35. SDK 23 dropped those Runtime factory methods. Rather than throwing 'Invalid Runtime' and breaking the migration, transparently bump these deprecated runtimes to deno-1.40, the nearest still-supported version (already mapped). --- src/Migration/Destinations/Appwrite.php | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/Migration/Destinations/Appwrite.php b/src/Migration/Destinations/Appwrite.php index 71ab9da8..840df6f6 100644 --- a/src/Migration/Destinations/Appwrite.php +++ b/src/Migration/Destinations/Appwrite.php @@ -2277,7 +2277,7 @@ public function importFunctionResource(Resource $resource): Resource 'dart-2.17' => Runtime::DART217(), 'dart-2.18' => Runtime::DART218(), 'dart-2.19' => Runtime::DART219(), - 'deno-1.40' => Runtime::DENO140(), + 'deno-1.21', 'deno-1.24', 'deno-1.35', 'deno-1.40' => Runtime::DENO140(), 'deno-1.46' => Runtime::DENO146(), 'deno-2.0' => Runtime::DENO20(), 'dotnet-6.0' => Runtime::DOTNET60(), @@ -2497,7 +2497,7 @@ public function importSiteResource(Resource $resource): Resource 'dart-2.17' => BuildRuntime::DART217(), 'dart-2.18' => BuildRuntime::DART218(), 'dart-2.19' => BuildRuntime::DART219(), - 'deno-1.40' => BuildRuntime::DENO140(), + 'deno-1.21', 'deno-1.24', 'deno-1.35', 'deno-1.40' => BuildRuntime::DENO140(), 'deno-1.46' => BuildRuntime::DENO146(), 'deno-2.0' => BuildRuntime::DENO20(), 'dotnet-6.0' => BuildRuntime::DOTNET60(), From a9bdfba24cd1f069a0c4f28a6060999e5741fc28 Mon Sep 17 00:00:00 2001 From: Prem Palanisamy Date: Mon, 11 May 2026 02:27:17 +0100 Subject: [PATCH 07/10] Revert "fix: fallback deprecated deno-1.21/1.24/1.35 runtimes to DENO140" This reverts commit e5dc657423bea96941cd71e5f0b060fd8b047b56. --- src/Migration/Destinations/Appwrite.php | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/Migration/Destinations/Appwrite.php b/src/Migration/Destinations/Appwrite.php index 840df6f6..71ab9da8 100644 --- a/src/Migration/Destinations/Appwrite.php +++ b/src/Migration/Destinations/Appwrite.php @@ -2277,7 +2277,7 @@ public function importFunctionResource(Resource $resource): Resource 'dart-2.17' => Runtime::DART217(), 'dart-2.18' => Runtime::DART218(), 'dart-2.19' => Runtime::DART219(), - 'deno-1.21', 'deno-1.24', 'deno-1.35', 'deno-1.40' => Runtime::DENO140(), + 'deno-1.40' => Runtime::DENO140(), 'deno-1.46' => Runtime::DENO146(), 'deno-2.0' => Runtime::DENO20(), 'dotnet-6.0' => Runtime::DOTNET60(), @@ -2497,7 +2497,7 @@ public function importSiteResource(Resource $resource): Resource 'dart-2.17' => BuildRuntime::DART217(), 'dart-2.18' => BuildRuntime::DART218(), 'dart-2.19' => BuildRuntime::DART219(), - 'deno-1.21', 'deno-1.24', 'deno-1.35', 'deno-1.40' => BuildRuntime::DENO140(), + 'deno-1.40' => BuildRuntime::DENO140(), 'deno-1.46' => BuildRuntime::DENO146(), 'deno-2.0' => BuildRuntime::DENO20(), 'dotnet-6.0' => BuildRuntime::DOTNET60(), From 4dc72708497729f1aacb133a814bb5b0adab5a8d Mon Sep 17 00:00:00 2001 From: Prem Palanisamy Date: Mon, 11 May 2026 02:37:10 +0100 Subject: [PATCH 08/10] fix(source): guard against empty teams page when computing cursor end([]) returns false, and PHP's nullsafe operator (?->) only short-circuits on null. end($empty)?->id therefore throws TypeError when the teams response is empty, breaking reportAuth for accounts without teams. Replace with an explicit empty check. --- src/Migration/Sources/Appwrite.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Migration/Sources/Appwrite.php b/src/Migration/Sources/Appwrite.php index 2b5906d8..94368e5e 100644 --- a/src/Migration/Sources/Appwrite.php +++ b/src/Migration/Sources/Appwrite.php @@ -308,7 +308,7 @@ private function reportAuth(array $resources, array &$report, array $resourceIds $currentTeams = $teamList->teams; $allTeams = array_merge($allTeams, $currentTeams); - $lastTeam = end($currentTeams)?->id ?? null; + $lastTeam = empty($currentTeams) ? null : end($currentTeams)->id; if (count($currentTeams) < self::DEFAULT_PAGE_LIMIT) { break; From 447a98706d752254a1cc7829a6d76f3a201b799c Mon Sep 17 00:00:00 2001 From: Prem Palanisamy Date: Mon, 11 May 2026 02:47:15 +0100 Subject: [PATCH 09/10] fix(deps): require PHP >=8.2 and fix composer config.platform MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit appwrite/appwrite 23.x (via SDK 22.0.0) requires PHP >=8.2.0, but this lib's require.php was >=8.1 — composer install would fail on PHP 8.1 consumers. The previous top-level 'platform' key was silently ignored by Composer (must live under 'config.platform'), which hid the inconsistency locally. Move it under 'config.platform' and bump to 8.2 to match the require constraint. --- composer.json | 9 +++++---- composer.lock | 19 +++++++++++-------- 2 files changed, 16 insertions(+), 12 deletions(-) diff --git a/composer.json b/composer.json index 8649e3be..896a388c 100644 --- a/composer.json +++ b/composer.json @@ -22,9 +22,10 @@ "check": "./vendor/bin/phpstan analyse --level 3 src tests --memory-limit 2G" }, "require": { - "php": ">=8.1", + "php": ">=8.2", "ext-curl": "*", "ext-openssl": "*", + "appwrite/appwrite": "23.*", "utopia-php/database": "5.*", "utopia-php/storage": "2.*", @@ -38,10 +39,10 @@ "laravel/pint": "1.*", "phpstan/phpstan": "1.*" }, - "platform": { - "php": "8.1" - }, "config": { + "platform": { + "php": "8.2" + }, "allow-plugins": { "php-http/discovery": true, "tbachert/spi": true diff --git a/composer.lock b/composer.lock index bd8c429b..246268b6 100644 --- a/composer.lock +++ b/composer.lock @@ -4,7 +4,7 @@ "Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies", "This file is @generated automatically" ], - "content-hash": "577cd7fb43f3f832869661126e0a5090", + "content-hash": "b437ef18d3106e157d1d9363962137a4", "packages": [ { "name": "appwrite/appwrite", @@ -2138,16 +2138,16 @@ }, { "name": "utopia-php/cache", - "version": "1.0.1", + "version": "1.0.2", "source": { "type": "git", "url": "https://github.com/utopia-php/cache.git", - "reference": "05ceba981436a4022553f7aaa2a05fa049d0f71c" + "reference": "d36f9050c39c02e09a7763389c9e71258e74af1f" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/utopia-php/cache/zipball/05ceba981436a4022553f7aaa2a05fa049d0f71c", - "reference": "05ceba981436a4022553f7aaa2a05fa049d0f71c", + "url": "https://api.github.com/repos/utopia-php/cache/zipball/d36f9050c39c02e09a7763389c9e71258e74af1f", + "reference": "d36f9050c39c02e09a7763389c9e71258e74af1f", "shasum": "" }, "require": { @@ -2184,9 +2184,9 @@ ], "support": { "issues": "https://github.com/utopia-php/cache/issues", - "source": "https://github.com/utopia-php/cache/tree/1.0.1" + "source": "https://github.com/utopia-php/cache/tree/1.0.2" }, - "time": "2026-03-12T03:39:09+00:00" + "time": "2026-05-08T11:40:20+00:00" }, { "name": "utopia-php/console", @@ -4964,12 +4964,15 @@ "prefer-stable": false, "prefer-lowest": false, "platform": { - "php": ">=8.1", + "php": ">=8.2", "ext-curl": "*", "ext-openssl": "*" }, "platform-dev": { "ext-pdo": "*" }, + "platform-overrides": { + "php": "8.2" + }, "plugin-api-version": "2.9.0" } From 2681028af256838e4d291e6cd539aeb427d5fc6e Mon Sep 17 00:00:00 2001 From: Prem Palanisamy Date: Mon, 11 May 2026 08:13:31 +0100 Subject: [PATCH 10/10] chore(deps): drop config.platform pin Per review: pinning config.platform.php to 8.2 forces composer to resolve against 8.2, which can hold back deps that need 8.3+. The "php": ">=8.2" require constraint is the signal that matters. --- composer.json | 3 --- composer.lock | 5 +---- 2 files changed, 1 insertion(+), 7 deletions(-) diff --git a/composer.json b/composer.json index 896a388c..6bd64041 100644 --- a/composer.json +++ b/composer.json @@ -40,9 +40,6 @@ "phpstan/phpstan": "1.*" }, "config": { - "platform": { - "php": "8.2" - }, "allow-plugins": { "php-http/discovery": true, "tbachert/spi": true diff --git a/composer.lock b/composer.lock index 246268b6..ab0a68c7 100644 --- a/composer.lock +++ b/composer.lock @@ -4,7 +4,7 @@ "Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies", "This file is @generated automatically" ], - "content-hash": "b437ef18d3106e157d1d9363962137a4", + "content-hash": "44746ecb1183e23d963fc90b1481541a", "packages": [ { "name": "appwrite/appwrite", @@ -4971,8 +4971,5 @@ "platform-dev": { "ext-pdo": "*" }, - "platform-overrides": { - "php": "8.2" - }, "plugin-api-version": "2.9.0" }