cms/app/Services/ThemeService.php
Funky Waddle 37ed997989 feat(cms): initialize Laravel project structure and core CMS files
- Added standard Laravel directory structure and configuration.

- Included Svelte and Tailwind configuration for the admin interface.

- Added core PHPUnit and testing scripts.
2026-04-13 12:48:06 -05:00

159 lines
5 KiB
PHP

<?php
namespace App\Services;
use App\Models\Setting;
use App\Support\ThemeManager;
use Illuminate\Http\UploadedFile;
use Illuminate\Support\Facades\File;
use Exception;
/**
* Service to handle Theme-related operations.
*/
class ThemeService
{
/**
* @var ThemeManager
*/
protected ThemeManager $themeManager;
/**
* ThemeService constructor.
*
* @param ThemeManager $themeManager
*/
public function __construct(ThemeManager $themeManager)
{
$this->themeManager = $themeManager;
}
/**
* Upload and install a new theme from a ZIP file.
*
* @param UploadedFile $zipFile The uploaded theme ZIP file.
* @return array Result containing success status and theme metadata.
* @throws Exception If ZIP extraction or validation fails.
*/
public function upload(UploadedFile $zipFile): array
{
/**
* Initialize temporary extraction directory.
* Using a unique ID ensures no collisions if multiple themes are uploaded simultaneously.
*/
$tempPath = storage_path('app/temp/theme_upload_' . uniqid());
File::makeDirectory($tempPath, 0755, true);
try {
$zipFilePath = $zipFile->getRealPath();
/**
* 1. ZIP Extraction.
* We prioritize the native PHP ZipArchive class for performance.
* If not available, we fall back to the system 'unzip' command (common on Linux).
*/
if (class_exists('ZipArchive')) {
$zip = new \ZipArchive();
if ($zip->open($zipFilePath) === true) {
$zip->extractTo($tempPath);
$zip->close();
} else {
throw new Exception('Failed to open ZIP file via ZipArchive.');
}
} else {
$command = "unzip -q " . escapeshellarg($zipFilePath) . " -d " . escapeshellarg($tempPath);
$output = [];
$returnVar = 0;
exec($command, $output, $returnVar);
if ($returnVar !== 0) {
throw new Exception('Failed to extract ZIP file via system unzip command.');
}
}
/**
* 2. Path Discovery.
* We check if the ZIP contains a single root folder or if the files are at the root.
* This handles different ZIP packaging styles (folders vs flat).
*/
$rootContents = File::directories($tempPath);
$rootFiles = File::files($tempPath);
$extractDir = $tempPath;
$themeSlug = null;
if (count($rootContents) === 1 && count($rootFiles) === 0) {
// ZIP has a single top-level folder
$extractDir = $rootContents[0];
$themeSlug = basename($extractDir);
} else {
// ZIP is flat (files are at root)
$themeSlug = pathinfo($zipFile->getClientOriginalName(), PATHINFO_FILENAME);
}
// Sanitize theme slug to prevent filesystem issues
$themeSlug = strtolower(preg_replace('/[^a-z0-9_-]/i', '-', $themeSlug));
/**
* 3. Validation.
* A valid theme MUST contain a 'theme.md' file.
*/
if (! File::exists($extractDir . '/theme.md')) {
File::deleteDirectory($tempPath);
throw new Exception('Invalid theme: theme.md is missing.');
}
/**
* 4. Final Deployment.
* Move the extracted files to the project-wide 'themes' directory.
* Existing themes with the same slug are overwritten.
*/
$finalPath = base_path('themes/' . $themeSlug);
if (File::exists($finalPath)) {
File::deleteDirectory($finalPath);
}
File::copyDirectory($extractDir, $finalPath);
File::deleteDirectory($tempPath);
return [
'success' => true,
'slug' => $themeSlug,
'metadata' => $this->themeManager->getMetadata($themeSlug),
];
} catch (Exception $e) {
File::deleteDirectory($tempPath);
throw $e;
}
}
/**
* Activate a theme.
*
* @param string $themeSlug The slug of the theme to activate.
* @return bool True if activation was successful.
* @throws Exception If the theme is not found.
*/
public function activate(string $themeSlug): bool
{
if (! file_exists(base_path('themes/' . $themeSlug))) {
throw new Exception('Theme not found.');
}
Setting::set('active_theme', $themeSlug, 'cms');
return true;
}
/**
* List all installed themes with their metadata.
*
* @return array List of installed themes.
*/
public function list(): array
{
return $this->themeManager->getThemes();
}
}