create([ 'type' => $data['type'], 'slug' => $data['slug'], 'name' => $data['name'], 'summary' => $data['summary'] ?? '', 'description' => $data['description'] ?? '', 'author' => $data['author'] ?? '', 'homepage' => $data['homepage'] ?? '', 'icon_url' => $data['icon_url'] ?? '', 'license' => $data['license'] ?? '', 'status' => $data['status'] ?? 'draft', 'is_featured' => (bool) ($data['is_featured'] ?? false), 'sort_order' => (int) ($data['sort_order'] ?? 0), 'download_count' => 0, 'latest_version' => '', ]); $this->syncCategories($package, Arr::get($data, 'categories', [])); return $package->load(['categories', 'latestStableVersion']); }); } public function updatePackage(Package $package, array $data): Package { return DB::transaction(function () use ($package, $data) { $package->fill([ 'name' => $data['name'] ?? $package->name, 'summary' => $data['summary'] ?? $package->summary, 'description' => $data['description'] ?? $package->description, 'author' => $data['author'] ?? $package->author, 'homepage' => $data['homepage'] ?? $package->homepage, 'icon_url' => $data['icon_url'] ?? $package->icon_url, 'license' => $data['license'] ?? $package->license, 'status' => $data['status'] ?? $package->status, 'is_featured' => array_key_exists('is_featured', $data) ? (bool) $data['is_featured'] : $package->is_featured, 'sort_order' => array_key_exists('sort_order', $data) ? (int) $data['sort_order'] : $package->sort_order, ]); $package->save(); if (array_key_exists('categories', $data)) { $this->syncCategories($package, Arr::get($data, 'categories', [])); } $this->refreshLatestVersion($package->fresh()); return $package->fresh(['categories', 'latestStableVersion']); }); } public function updateStatus(Package $package, string $status): Package { $package->status = $status; $package->save(); return $package->fresh(['categories', 'latestStableVersion']); } public function createVersion(Package $package, array $data): Version { return DB::transaction(function () use ($package, $data) { $markAsLatest = array_key_exists('mark_as_latest', $data) ? (bool) $data['mark_as_latest'] : true; if ($markAsLatest) { $package->versions()->update(['is_latest' => false]); } $version = $package->versions()->create([ 'version' => $data['version'], 'changelog' => $data['changelog'] ?? '', 'typecho_min' => $data['typecho_min'] ?? '1.2.0', 'typecho_max' => $data['typecho_max'] ?? '', 'php_min' => $data['php_min'] ?? '7.4', 'php_max' => $data['php_max'] ?? '', 'php_extensions' => json_encode($data['php_extensions'] ?? [], JSON_UNESCAPED_UNICODE), 'package_url' => $data['package_url'], 'package_size' => (int) ($data['package_size'] ?? 0), 'sha256' => strtolower($data['sha256']), 'is_stable' => array_key_exists('is_stable', $data) ? (bool) $data['is_stable'] : true, 'is_latest' => $markAsLatest, 'download_count' => 0, 'published_at' => $data['published_at'] ?? now(), ]); $this->refreshLatestVersion($package->fresh()); return $version->fresh(); }); } public function deleteVersion(Version $version): void { DB::transaction(function () use ($version) { $package = $version->package; $version->delete(); if ($package) { $this->refreshLatestVersion($package->fresh()); } }); } public function deletePackage(Package $package): void { $storageDir = storage_path('app/packages/' . $package->type . '/' . $package->slug); DB::transaction(function () use ($package) { $package->delete(); }); if (is_dir($storageDir)) { File::deleteDirectory($storageDir); } } public function refreshLatestVersion(Package $package): void { $latestStable = $package->versions() ->where('is_stable', true) ->orderByDesc('is_latest') ->orderByDesc('published_at') ->orderByDesc('id') ->first(); if ($latestStable) { $package->latest_version = $latestStable->version; $package->save(); return; } $latestAny = $package->versions() ->orderByDesc('is_latest') ->orderByDesc('published_at') ->orderByDesc('id') ->first(); $package->latest_version = $latestAny?->version ?: ''; $package->save(); } private function syncCategories(Package $package, array $categorySlugs): void { $categorySlugs = collect($categorySlugs) ->map(fn ($item) => trim((string) $item)) ->filter() ->unique(fn (string $item) => mb_strtolower($item, 'UTF-8')) ->values() ->all(); if (empty($categorySlugs)) { $package->categories()->sync([]); return; } $categories = Category::query() ->where('type', $package->type) ->get(['id', 'type', 'slug', 'name']); $categoryIds = []; foreach ($categorySlugs as $token) { $normalizedToken = mb_strtolower($token, 'UTF-8'); $existing = $categories->first(function (Category $category) use ($token, $normalizedToken) { return $category->name === $token || mb_strtolower($category->slug, 'UTF-8') === $normalizedToken || mb_strtolower($category->name, 'UTF-8') === $normalizedToken; }); if ($existing) { $categoryIds[] = $existing->id; continue; } $slug = $this->generateCategorySlug($token, $categories); $category = Category::query()->create([ 'type' => $package->type, 'slug' => $slug, 'name' => $this->generateCategoryName($token), 'description' => '', 'sort_order' => 0, ]); $categories->push($category); $categoryIds[] = $category->id; } $package->categories()->sync(array_values(array_unique($categoryIds))); } private function generateCategorySlug(string $token, $categories): string { $base = (string) Str::of($token) ->trim() ->replace('_', '-') ->replaceMatches('/\s+/u', '-') ->replaceMatches('/[^\pL\pN-]+/u', '-') ->replaceMatches('/-+/u', '-') ->lower() ->trim('-'); if ($base === '') { $base = 'category'; } $slug = $base; $suffix = 2; while ($categories->contains(fn (Category $category) => mb_strtolower($category->slug, 'UTF-8') === mb_strtolower($slug, 'UTF-8'))) { $slug = $base . '-' . $suffix; $suffix++; } return $slug; } private function generateCategoryName(string $token): string { if (preg_match('/^[a-z0-9_-]+$/i', $token) === 1) { return (string) Str::of($token) ->replace(['-', '_'], ' ') ->title(); } return trim($token); } }