diff --git a/app/Contracts/BaseCommand.php b/app/Contracts/BaseCommand.php index 48d3c062..43c510ba 100644 --- a/app/Contracts/BaseCommand.php +++ b/app/Contracts/BaseCommand.php @@ -35,6 +35,13 @@ abstract class BaseCommand extends Command protected OutputInterface $output; protected SymfonyStyle $io; + /** + * Create a new BaseCommand with the application's services and repositories. + * + * The constructor accepts and stores dependencies (environment and inventory services, + * process and prompting helpers, server/site repositories, SSH service, and the DI container) + * used by this command and its subclasses. + */ public function __construct( // Framework protected readonly Container $container, @@ -80,7 +87,15 @@ protected function configure(): void } /** - * Initialize IO and services. + * Prepare console IO and initialize environment, inventory, and repositories. + * + * Sets the command's input/output properties, creates a SymfonyStyle IO helper, + * applies any custom paths provided via the `--env` and `--inventory` options, + * loads the corresponding files, and populates the servers and sites repositories + * from the loaded inventory. + * + * @param InputInterface $input The current console input. + * @param OutputInterface $output The current console output. */ protected function initialize(InputInterface $input, OutputInterface $output): void { @@ -142,4 +157,4 @@ protected function execute(InputInterface $input, OutputInterface $output): int return Command::SUCCESS; } -} +} \ No newline at end of file diff --git a/app/DTOs/SiteDTO.php b/app/DTOs/SiteDTO.php index d188de5e..4bb075f3 100644 --- a/app/DTOs/SiteDTO.php +++ b/app/DTOs/SiteDTO.php @@ -7,7 +7,12 @@ readonly class SiteDTO { /** - * @param array $servers + * Create a SiteDTO containing the site's domain, repository, branch, and associated servers. + * + * @param string $domain The site's domain name (e.g. example.com). + * @param string $repo The repository URL or identifier for the site. + * @param string $branch The repository branch to deploy (e.g. main). + * @param array $servers Ordered list of server hostnames or addresses associated with the site. */ public function __construct( public string $domain, @@ -16,4 +21,4 @@ public function __construct( public array $servers, ) { } -} +} \ No newline at end of file diff --git a/app/Repositories/SiteRepository.php b/app/Repositories/SiteRepository.php index 2cada281..4394bab6 100644 --- a/app/Repositories/SiteRepository.php +++ b/app/Repositories/SiteRepository.php @@ -26,7 +26,11 @@ final class SiteRepository // ------------------------------------------------------------------------------- /** - * Set the inventory service instance to use for storage operations. + * Configure the repository with an InventoryService and load site entries from storage. + * + * Loads the value stored under the repository's PREFIX key into the internal sites cache + * ($this->sites). If the stored value is not an array, an empty array is persisted under + * the PREFIX key and loaded into the cache. */ public function loadInventory(InventoryService $inventory): void { @@ -43,7 +47,10 @@ public function loadInventory(InventoryService $inventory): void } /** - * Create a new site in the inventory. + * Add a new site to the inventory storage ensuring the site's domain is unique. + * + * @param SiteDTO $site The site to store; its domain must not already exist in inventory. + * @throws \RuntimeException If the inventory has not been loaded or a site with the same domain already exists. */ public function create(SiteDTO $site): void { @@ -60,8 +67,11 @@ public function create(SiteDTO $site): void } /** - * Find a site by domain. - */ + * Retrieve the site matching the given domain. + * + * @throws \RuntimeException If the inventory has not been loaded via loadInventory(). + * @return SiteDTO|null The SiteDTO for the matching domain, or `null` if no match is found. + */ public function findByDomain(string $domain): ?SiteDTO { $this->assertInventoryLoaded(); @@ -76,9 +86,9 @@ public function findByDomain(string $domain): ?SiteDTO } /** - * Get all sites from the inventory. + * Retrieve all stored sites as SiteDTO objects. * - * @return array + * @return array An array of SiteDTO objects. */ public function all(): array { @@ -93,7 +103,11 @@ public function all(): array } /** - * Delete a site from the inventory. + * Remove the site with the given domain from the stored inventory. + * + * If no site matches the domain, the inventory remains unchanged. + * + * @param string $domain The domain of the site to remove. */ public function delete(string $domain): void { @@ -116,9 +130,9 @@ public function delete(string $domain): void // ------------------------------------------------------------------------------- /** - * Ensure inventory service is loaded before operations. + * Asserts that the repository's inventory service has been loaded. * - * @throws \RuntimeException If inventory is not set + * @throws \RuntimeException If the inventory service has not been loaded. * @phpstan-assert !null $this->inventory */ private function assertInventoryLoaded(): void @@ -129,9 +143,10 @@ private function assertInventoryLoaded(): void } /** - * Convert SiteDTO to array for storage. + * Serialize a SiteDTO into an associative array suitable for inventory storage. * - * @return array + * @param SiteDTO $site The site DTO to serialize. + * @return array Associative array with keys `domain`, `repo`, `branch`, and `servers`. */ private function dehydrateSiteDTO(SiteDTO $site): array { @@ -144,10 +159,11 @@ private function dehydrateSiteDTO(SiteDTO $site): array } /** - * Hydrate a SiteDTO from inventory data. - * - * @param array $data - */ + * Create a SiteDTO from raw inventory data. + * + * @param array $data Raw associative array from inventory. + * @return SiteDTO A SiteDTO where `domain`, `repo`, and `branch` are strings (empty string if missing or not a string) and `servers` is an array of strings (empty array if missing or invalid). + */ private function hydrateSiteDTO(array $data): SiteDTO { $domain = $data['domain'] ?? ''; @@ -162,4 +178,4 @@ private function hydrateSiteDTO(array $data): SiteDTO servers: is_array($servers) ? array_values(array_filter($servers, 'is_string')) : [], ); } -} +} \ No newline at end of file diff --git a/app/Services/ProcessService.php b/app/Services/ProcessService.php index 54042e9c..13d5052d 100644 --- a/app/Services/ProcessService.php +++ b/app/Services/ProcessService.php @@ -11,15 +11,24 @@ */ final readonly class ProcessService { + /** + * Initialize the service with a filesystem utility used for directory validation and inspection. + * + * @param FilesystemService $fs Filesystem utility used to validate working directories and perform filesystem checks. + */ public function __construct( private FilesystemService $fs, ) { } /** - * Execute a shell command and return the Process instance. + * Execute the given command in the specified working directory and return the executed Process instance. * - * @param list $command + * @param list $command The command and its arguments. + * @param string $cwd The working directory in which to execute the command. + * @param float $timeout Process timeout in seconds. + * @return Process The Symfony Process instance after execution. + * @throws \InvalidArgumentException If `$command` is empty or `$cwd` is not a directory. */ public function run(array $command, string $cwd, float $timeout = 3.0): Process { @@ -37,4 +46,4 @@ public function run(array $command, string $cwd, float $timeout = 3.0): Process return $process; } -} +} \ No newline at end of file diff --git a/app/Services/VersionService.php b/app/Services/VersionService.php index 98879d58..7271cdd4 100644 --- a/app/Services/VersionService.php +++ b/app/Services/VersionService.php @@ -17,6 +17,12 @@ */ class VersionService { + /** + * Create a VersionService configured with process and filesystem services and package/fallback version. + * + * @param string $packageName The Composer package name to query for version information (default 'bigpixelrocket/deployer-php'). + * @param string $fallbackVersion The version to return when no other source provides one (default 'dev-main'). + */ public function __construct( private readonly ProcessService $proc, private readonly FilesystemService $fs, @@ -109,7 +115,10 @@ public function isGitRepository(string $projectRoot): bool } /** - * Get exact git tag if HEAD is tagged. + * Retrieve the exact Git tag name that points to HEAD, if present. + * + * @param string $projectRoot Path to the Git repository root. + * @return string|null The exact tag name that points to HEAD, or `null` if HEAD is not tagged or an error occurs. */ public function getExactGitTag(string $projectRoot): ?string { @@ -127,7 +136,12 @@ public function getExactGitTag(string $projectRoot): ?string } /** - * Get git describe version (tag + commit info). + * Determine a human-readable Git reference for the repository at the given path. + * + * Attempts to run `git describe --tags --always` and returns the trimmed output on success. + * + * @param string $projectRoot Path to the repository root where the Git command will run. + * @return string|null The described reference (tag, tag+commit, or short commit) if available, `null` otherwise. */ public function getGitDescribeVersion(string $projectRoot): ?string { @@ -145,7 +159,14 @@ public function getGitDescribeVersion(string $projectRoot): ?string } /** - * Get current branch with short commit hash. + * Produce the current Git branch combined with the short commit hash. + * + * Returns a string in the format "branch@commit" where `branch` is the current branch name + * and `commit` is the short commit hash. Returns `null` if the repository information cannot + * be determined or an error occurs. + * + * @param string $projectRoot Path to the repository root. + * @return string|null The branch and short commit separated by '@', or `null` if unavailable. */ public function getBranchWithCommit(string $projectRoot): ?string { @@ -164,4 +185,4 @@ public function getBranchWithCommit(string $projectRoot): ?string return null; } -} +} \ No newline at end of file diff --git a/tests/Fixtures/TestConsoleCommand.php b/tests/Fixtures/TestConsoleCommand.php index aa274926..c5f1ee0a 100644 --- a/tests/Fixtures/TestConsoleCommand.php +++ b/tests/Fixtures/TestConsoleCommand.php @@ -31,6 +31,18 @@ class TestConsoleCommand extends BaseCommand private array $testArgs = []; + /** + * Create a TestConsoleCommand instance with the required service and repository dependencies. + * + * @param Container $container Dependency injection container. + * @param EnvService $env Environment service. + * @param InventoryService $inventory Inventory management service. + * @param ProcessService $proc Process execution service. + * @param PrompterService $prompter Interactive prompt service. + * @param ServerRepository $servers Repository for server records. + * @param SiteRepository $sites Repository for site records. + * @param SSHService $ssh SSH service for remote execution. + */ public function __construct( Container $container, EnvService $env, @@ -237,4 +249,4 @@ private function testPromptSearchWrapper(): void { $this->promptSearch('Test:', fn ($q) => ['a', 'b']); } -} +} \ No newline at end of file diff --git a/tests/TestHelpers.php b/tests/TestHelpers.php index 3c4c83c8..90efbd8e 100644 --- a/tests/TestHelpers.php +++ b/tests/TestHelpers.php @@ -130,28 +130,15 @@ function mockEnvService( if (!function_exists('mockInventoryService')) { /** - * Create a mock InventoryService for testing with configurable filesystem behavior. + * Create a mock InventoryService for tests with a configurable in-memory inventory file. * - * Accepts either array data (auto-converts to YAML) or raw string content. - * Use arrays for clean test setup, strings for testing YAML parsing edge cases. + * If $data is an array, it is dumped to YAML and used as the inventory file content; if it is a string, it is used verbatim. The filesystem mock can be configured to simulate missing files or read/write errors. * - * @example - * // Using array data (recommended) - * $service = mockInventoryService( - * fileExists: true, - * data: ['servers' => ['web1' => ['host' => '192.168.1.1']]] - * ); - * - * @example - * // Using raw YAML string - * $service = mockInventoryService( - * fileExists: true, - * data: "servers:\n web1:\n host: 192.168.1.1" - * ); - * - * @example - * // Test write failures - * $service = mockInventoryService(fileExists: true, throwOnWrite: true); + * @param bool $fileExists Whether the inventory file should appear to exist. + * @param array|string $data Array to be converted to YAML or raw YAML string to use as file content. + * @param bool $throwOnRead If true, the mocked filesystem will throw on read operations. + * @param bool $throwOnWrite If true, the mocked filesystem will throw on write/dump operations. + * @return InventoryService An InventoryService backed by a mocked FilesystemService. */ function mockInventoryService( bool $fileExists = true, @@ -174,14 +161,11 @@ function mockInventoryService( if (!function_exists('mockProcessService')) { /** - * Create a ProcessService for testing. + * Creates a ProcessService configured for tests. * - * Uses real Filesystem since directory validation requires is_dir() checks. - * Tests should use real directories (e.g., __DIR__, sys_get_temp_dir()). + * Uses a real FilesystemService (Symfony Filesystem) so directory validation relies on is_dir(); tests should provide real directories (e.g., __DIR__, sys_get_temp_dir()). * - * @example - * $proc = mockProcessService(); - * $process = $proc->run(['echo', 'test'], __DIR__); + * @return ProcessService A ProcessService backed by a FilesystemService using a real Filesystem. */ function mockProcessService(): ProcessService { @@ -272,20 +256,13 @@ function mockPrompter( if (!function_exists('mockVersionService')) { /** - * Create a VersionService for testing with configurable package name and fallback. + * Create a VersionService configured for tests with an optional package name and fallback version. * - * Uses real Filesystem and ProcessService since git operations require real directory checks. + * Uses a real Filesystem and ProcessService because version resolution may perform git/directory checks. * - * @example - * // Default configuration - * $service = mockVersionService(); - * - * @example - * // Custom package and fallback - * $service = mockVersionService( - * packageName: 'vendor/package', - * fallback: '1.0.0-dev' - * ); + * @param string|null $packageName Optional package name to use (e.g., "vendor/package"). If omitted the service uses its default discovery. + * @param string|null $fallback Optional fallback version string used when the package/version cannot be determined. + * @return VersionService The configured VersionService instance. */ function mockVersionService( ?string $packageName = null, @@ -308,23 +285,15 @@ function mockVersionService( if (!function_exists('mockServerRepository')) { /** - * Create a ServerRepository for testing with a loaded inventory service. - * - * Repository is returned fully initialized with inventory loaded and ready for use. + * Create a ServerRepository preloaded with inventory data for use in tests. * - * @example - * // Empty repository - * $repo = mockServerRepository(fileExists: true, data: ['servers' => []]); + * The returned repository has its inventory loaded from a mocked InventoryService and is ready for immediate use. * - * @example - * // Pre-populated with servers - * $repo = mockServerRepository( - * fileExists: true, - * data: ['servers' => [ - * 'web1' => ['host' => '192.168.1.1', 'port' => 22] - * ]] - * ); - * $repo->findByName('web1'); // Returns ServerDTO + * @param bool $fileExists Whether the mocked inventory file should exist. + * @param array|string $data Inventory content to load; an array will be converted to YAML, a string will be used as raw file content. + * @param bool $throwOnRead If true, the mocked filesystem will throw on read operations to simulate read errors. + * @param bool $throwOnWrite If true, the mocked filesystem will throw on write/dump operations to simulate write errors. + * @return ServerRepository A ServerRepository instance with inventory loaded from the mocked service. */ function mockServerRepository( bool $fileExists = true, @@ -344,23 +313,13 @@ function mockServerRepository( if (!function_exists('mockSiteRepository')) { /** - * Create a SiteRepository for testing with a loaded inventory service. - * - * Repository is returned fully initialized with inventory loaded and ready for use. + * Creates a SiteRepository for testing with its inventory loaded from a mocked InventoryService. * - * @example - * // Empty repository - * $repo = mockSiteRepository(fileExists: true, data: ['sites' => []]); - * - * @example - * // Pre-populated with sites - * $repo = mockSiteRepository( - * fileExists: true, - * data: ['sites' => [ - * ['domain' => 'example.com', 'repo' => 'git@github.com:user/repo.git', 'branch' => 'main', 'servers' => ['web1']] - * ]] - * ); - * $repo->findByDomain('example.com'); // Returns SiteDTO + * @param bool $fileExists Whether the underlying inventory file should appear to exist. + * @param array|string $data Inventory contents as an array (converted to YAML) or raw YAML string. + * @param bool $throwOnRead If true, the mocked inventory service will throw on read operations. + * @param bool $throwOnWrite If true, the mocked inventory service will throw on write operations. + * @return SiteRepository A repository instance with inventory loaded and ready for use. */ function mockSiteRepository( bool $fileExists = true, @@ -447,4 +406,4 @@ function mockCommandContainer( return $container; } -} +} \ No newline at end of file