diff --git a/src/Attributes/Mapping/DataFromArray.php b/src/Attributes/Mapping/DataFromArray.php new file mode 100644 index 0000000..8867878 --- /dev/null +++ b/src/Attributes/Mapping/DataFromArray.php @@ -0,0 +1,21 @@ + + * Alle Rechte vorbehalten. + */ + +declare(strict_types=1); + +namespace App\Attributes\Mapping; + +use Attribute; + +#[Attribute(Attribute::TARGET_PROPERTY)] +final readonly class DataFromArray +{ + public function __construct( + public string|null $key, + ){} +} diff --git a/src/Entity/.gitignore b/src/Entity/.gitignore deleted file mode 100644 index e69de29..0000000 diff --git a/src/Entity/Project.php b/src/Entity/Project.php new file mode 100644 index 0000000..f5551aa --- /dev/null +++ b/src/Entity/Project.php @@ -0,0 +1,74 @@ + + * Alle Rechte vorbehalten. + */ + +namespace App\Entity; + +use App\Attributes\Mapping\DataFromArray; +use App\Service\Deployment\DeploymentInterface; +use ReflectionClass; +use ReflectionException; + +class Project +{ + #[DataFromArray(key: 'name')] + public readonly string $name; + + #[DataFromArray(key: 'deployment')] + public readonly array $deployment; + + protected DeploymentInterface $deploymentService; + + + public function __construct( + array $data, + public string $projectDir, + public string $deployedVersion, + \Closure $deploymentServiceResolver, + ){ + $this->autoMapData($data); + + $this->deploymentService = ($deploymentServiceResolver)()->init($this); + } + + + public function deploy(string $version = '', string $step = ''): void + { + $this->deploymentService->handle($this, $version, $step); + } + + + /** + * Automatically maps data from the provided array to object properties + * based on attributes defined in the class. + * + * @param array $data The input data array containing key-value pairs to map. + */ + protected function autoMapData(array $data): void + { + try + { + $class = new ReflectionClass($this); + + foreach($class->getProperties() as $property) + { + foreach($property->getAttributes() as $attribute) + { + if($attribute->getName() === DataFromArray::class) + { + $attributeArguments = $attribute->getArguments(); + $this->{$property->getName()} = $data[$attributeArguments['key']]; + } + } + } + } + catch(ReflectionException $e) + { + + } + } +} diff --git a/src/Repository/.gitignore b/src/Repository/.gitignore deleted file mode 100644 index e69de29..0000000 diff --git a/src/Repository/ProjectRepository.php b/src/Repository/ProjectRepository.php new file mode 100644 index 0000000..1ff6661 --- /dev/null +++ b/src/Repository/ProjectRepository.php @@ -0,0 +1,57 @@ + + * Alle Rechte vorbehalten. + */ + +namespace App\Repository; + +use App\Entity\Project; +use App\Service\Deployment\DeploymentServiceResolver; +use App\Service\ProjectConfigDirService; +use Generator; +use Symfony\Component\DependencyInjection\Attribute\AutowireServiceClosure; +use Symfony\Component\Yaml\Yaml; + +class ProjectRepository +{ + /** @var Project[] */ + private array $projects; + + public function __construct( + protected ProjectConfigDirService $configDirService, + #[AutowireServiceClosure(DeploymentServiceResolver::class)] \Closure $deploymentServiceResolver, + ){ + foreach($this->configDirService->getProjectDirContents() as $projectFile) + { + $deployedVersion = $this->configDirService->getDeployedVersion($projectFile->getPath()); + + $this->projects[$projectFile->getRelativePath()] = new Project( + data: Yaml::parseFile($projectFile->getRealPath()), + projectDir: $projectFile->getPath(), + deployedVersion: $deployedVersion, + deploymentServiceResolver: $deploymentServiceResolver, + ); + } + } + + + public function getAll(): Generator + { + yield from $this->projects; + } + + + public function findById($id): Project|null + { + return $this->projects[$id] ?? null; + } + + + public function exists(string $id): bool + { + return array_any($this->projects, fn($project) => $project['id'] === $id); + } +} diff --git a/src/Service/ProjectConfigDirService.php b/src/Service/ProjectConfigDirService.php new file mode 100644 index 0000000..0475c02 --- /dev/null +++ b/src/Service/ProjectConfigDirService.php @@ -0,0 +1,125 @@ + + * Alle Rechte vorbehalten. + */ + +namespace App\Service; + +use App\Traits\LoggerTrait; +use Symfony\Component\Filesystem\Exception\IOExceptionInterface; +use Symfony\Component\Filesystem\Filesystem; +use Symfony\Component\Filesystem\Path; +use Symfony\Component\Finder\Exception\DirectoryNotFoundException; +use Symfony\Component\Finder\Finder; +use Symfony\Component\Finder\SplFileInfo; + +class ProjectConfigDirService +{ + use LoggerTrait; + + const string FILE_NAME_DEPLOYED_VERSION = '.deployed-version'; + const string FILE_NAME_DEPLOYMENT_STEP = '.deployment-step'; + + public function __construct( + protected string $dataDir, + ){} + + + /** + * @return SplFileInfo[] + */ + public function getProjectDirContents(): array + { + return $this->loadProjectDirContents(); + } + + + public function getDeployedVersion(string $projectDir): string + { + $filesystem = new Filesystem(); + if(!$filesystem->exists($projectDir . DIRECTORY_SEPARATOR . self::FILE_NAME_DEPLOYED_VERSION)) + { + return ''; + } + + $version = $filesystem->readFile($projectDir . DIRECTORY_SEPARATOR . self::FILE_NAME_DEPLOYED_VERSION); + + return trim($version); + } + + + public function setDeployedVersion(string $projectDir, string $version): void + { + $filesystem = new Filesystem(); + $filesystem->dumpFile($projectDir . DIRECTORY_SEPARATOR . self::FILE_NAME_DEPLOYED_VERSION, $version); + } + + + public function getDeploymentStep(string $projectDir): string + { + $filesystem = new Filesystem(); + if(!$filesystem->exists($projectDir . DIRECTORY_SEPARATOR . self::FILE_NAME_DEPLOYMENT_STEP)) + { + return 'not-started'; + } + + $stage = $filesystem->readFile($projectDir . DIRECTORY_SEPARATOR . self::FILE_NAME_DEPLOYMENT_STEP); + + return trim($stage); + } + + + public function setDeploymentStep(string $projectDir, string $step): void + { + $filesystem = new Filesystem(); + $filesystem->dumpFile($projectDir . DIRECTORY_SEPARATOR . self::FILE_NAME_DEPLOYMENT_STEP, $step); + } + + + /** + * @return SplFileInfo[] + */ + protected function loadProjectDirContents(): array + { + $finder = new Finder(); + $content = []; + + // find all files in the current directory + try + { + $finder->files()->in($this->dataDir)->depth(1)->name('project.yaml'); + + foreach($finder as $file) + { + $content[] = $file; + } + + return $content; + } + catch(DirectoryNotFoundException $e) + { + $this->logger->error('Project directory not found: ' . $this->dataDir . '. creating ...'); + + $filesystem = new Filesystem(); + + try + { + $filesystem->mkdir(Path::normalize($this->dataDir)); + $this->loadProjectDirContents(); + } + catch(IOExceptionInterface $exception) + { + echo "An error occurred while creating your directory at ".$exception->getPath(); + } + } + catch(\Exception $e) + { + throw new \RuntimeException('Failed to load project directory contents', 500, $e); + } + + return []; + } +} diff --git a/src/Traits/LoggerTrait.php b/src/Traits/LoggerTrait.php new file mode 100644 index 0000000..a7af7d2 --- /dev/null +++ b/src/Traits/LoggerTrait.php @@ -0,0 +1,23 @@ + + * Alle Rechte vorbehalten. + */ + +namespace App\Traits; + +use Psr\Log\LoggerInterface; +use Symfony\Contracts\Service\Attribute\Required; + +trait LoggerTrait +{ + protected LoggerInterface $logger; + + #[Required] + public function setLogger(LoggerInterface $logger): void + { + $this->logger = $logger; + } +}