diff --git a/composer.json b/composer.json index 90b0adfe..e24ba755 100644 --- a/composer.json +++ b/composer.json @@ -2,16 +2,13 @@ "name": "picamator/transfer-object", "description": "A modern Symfony-compatible Transfer Object Generator with property hooks, FixedArray, and asymmetric visibilities.", "keywords": [ - "transfer-object", "data-transfer-object", - "transfer-object-generator", + "data-transfer-object-generator", "dto", "dto-generator", - "definition-generator", - "symfony", - "symfony-console", "generator", - "automation" + "symfony", + "symfony-console" ], "license": "MIT", "authors": [ @@ -78,25 +75,28 @@ }, "prefer-stable": true, "scripts": { - "phpunit": "./vendor/bin/phpunit --no-progress", - "phpunit-group": "./vendor/bin/phpunit --testdox --group", - "phpstan": "./vendor/bin/phpstan analyse -c phpstan.neon --no-progress --memory-limit=512M", - "transfer-generate": "php ./bin/transfer-generate", - "transfer-generate-bulk": "php ./bin/transfer-generate-bulk", - "definition-generate": "php ./bin/definition-generate", - "captainhook": "./vendor/bin/captainhook", - "phpcs": "./vendor/bin/phpcs -q --colors", - "phpcbf": "./vendor/bin/phpcbf -q --colors" + "captainhook": "captainhook", + "definition-generate": "@php ./bin/definition-generate", + "phpcbf": "phpcbf -q --colors", + "phpcs": "phpcs -q --colors", + "phpstan": "phpstan analyse -c phpstan.neon --no-progress --memory-limit=512M", + "phpunit": "phpunit --no-progress", + "phpunit-group": "@phpunit --testdox --group", + "transfer-generate": "@php ./bin/transfer-generate", + "transfer-generate-bulk": "@php ./bin/transfer-generate-bulk" + }, + "scripts-aliases": { + "phpunit": ["test"] }, "scripts-descriptions": { + "captainhook": "Manage and execute Git hooks.", + "definition-generate": "Create Transfer Object definition files based on JSON blueprints.", + "phpcbf": "Automatically fix coding style issues with PHP Code Beautifier and Fixer.", + "phpcs": "Analyze code style using PHP CodeSniffer.", + "phpstan": "Perform static code analysis using PHPStan.", "phpunit": "Run unit and integration tests.", "phpunit-group": "Run unit and integration tests assigned to the group.", - "phpstan": "Perform static code analysis using PHPStan.", "transfer-generate": "Generate Transfer Objects from YML definition templates.", - "transfer-generate-bulk": "Generate Transfer Objects based on configuration list.", - "definition-generate": "Create Transfer Object definition files based on JSON blueprints.", - "captainhook": "Manage and execute Git hooks.", - "phpcs": "Analyze code style using PHP CodeSniffer.", - "phpcbf": "Automatically fix coding style issues with PHP Code Beautifier and Fixer." + "transfer-generate-bulk": "Generate Transfer Objects based on configuration list." } } diff --git a/composer.lock b/composer.lock index b7268916..7ca34b0b 100644 --- a/composer.lock +++ b/composer.lock @@ -945,16 +945,16 @@ "packages-dev": [ { "name": "captainhook/captainhook", - "version": "5.28.0", + "version": "5.28.3", "source": { "type": "git", "url": "https://github.com/captainhook-git/captainhook.git", - "reference": "9d0f34f2e9d112553ea342463371807e9864a87c" + "reference": "5d35b249f3843ef36ead119f4347e649278ad6d8" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/captainhook-git/captainhook/zipball/9d0f34f2e9d112553ea342463371807e9864a87c", - "reference": "9d0f34f2e9d112553ea342463371807e9864a87c", + "url": "https://api.github.com/repos/captainhook-git/captainhook/zipball/5d35b249f3843ef36ead119f4347e649278ad6d8", + "reference": "5d35b249f3843ef36ead119f4347e649278ad6d8", "shasum": "" }, "require": { @@ -1017,7 +1017,7 @@ ], "support": { "issues": "https://github.com/captainhook-git/captainhook/issues", - "source": "https://github.com/captainhook-git/captainhook/tree/5.28.0" + "source": "https://github.com/captainhook-git/captainhook/tree/5.28.3" }, "funding": [ { @@ -1025,7 +1025,7 @@ "type": "github" } ], - "time": "2026-02-08T14:15:15+00:00" + "time": "2026-02-16T14:08:58+00:00" }, { "name": "captainhook/secrets", @@ -1950,16 +1950,16 @@ }, { "name": "phpunit/phpunit", - "version": "13.0.2", + "version": "13.0.5", "source": { "type": "git", "url": "https://github.com/sebastianbergmann/phpunit.git", - "reference": "87f0ca577a7056016741af70e7a7de9246c52e67" + "reference": "d57826e8921a534680c613924bfd921ded8047f4" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/sebastianbergmann/phpunit/zipball/87f0ca577a7056016741af70e7a7de9246c52e67", - "reference": "87f0ca577a7056016741af70e7a7de9246c52e67", + "url": "https://api.github.com/repos/sebastianbergmann/phpunit/zipball/d57826e8921a534680c613924bfd921ded8047f4", + "reference": "d57826e8921a534680c613924bfd921ded8047f4", "shasum": "" }, "require": { @@ -2028,7 +2028,7 @@ "support": { "issues": "https://github.com/sebastianbergmann/phpunit/issues", "security": "https://github.com/sebastianbergmann/phpunit/security/policy", - "source": "https://github.com/sebastianbergmann/phpunit/tree/13.0.2" + "source": "https://github.com/sebastianbergmann/phpunit/tree/13.0.5" }, "funding": [ { @@ -2052,7 +2052,7 @@ "type": "tidelift" } ], - "time": "2026-02-10T12:33:24+00:00" + "time": "2026-02-18T12:40:03+00:00" }, { "name": "sebastian/cli-parser", diff --git a/docker/bash/sdk-autocomplete.sh b/docker/bash/sdk-autocomplete.sh index 6e873038..48af3ebd 100644 --- a/docker/bash/sdk-autocomplete.sh +++ b/docker/bash/sdk-autocomplete.sh @@ -8,12 +8,12 @@ _docker_sdk_autocomplete() { prev="${COMP_WORDS[COMP_CWORD-1]}" # define the main commands for docker/sdk - opts="build start stop cli composer phpstan phpunit phpunit-group phpcs phpcbf hook-install hook to-generate to-generate-bulk df-generate" + opts="install build start stop cli composer phpstan phpunit phpunit-group phpcs phpcbf hook-install hook to-generate to-generate-bulk df-generate" case "${prev}" in composer) # define subcommands for 'composer' - COMPREPLY=( $(compgen -W "install update why phpunit phpunit-group phpstan transfer-generate transfer-generate-bulk definition-generate captainhook phpcs phpcbf list" -- "${cur}") ) + COMPREPLY=( $(compgen -W "install update why test phpunit phpunit-group phpstan transfer-generate transfer-generate-bulk definition-generate captainhook phpcs phpcbf list" -- "${cur}") ) return 0 ;; hook) diff --git a/docker/php/bash/composer-autocomplete.sh b/docker/php/bash/composer-autocomplete.sh index 92249ae1..a049a43f 100644 --- a/docker/php/bash/composer-autocomplete.sh +++ b/docker/php/bash/composer-autocomplete.sh @@ -6,7 +6,7 @@ _composer_autocomplete() { COMPREPLY=() cur="${COMP_WORDS[COMP_CWORD]}" prev="${COMP_WORDS[COMP_CWORD-1]}" - opts="install update why phpunit phpunit-group phpstan transfer-generate transfer-generate-bulk definition-generate captainhook phpcs phpcbf list" + opts="install update why test phpunit phpunit-group phpstan transfer-generate transfer-generate-bulk definition-generate captainhook phpcs phpcbf list" if [[ ${prev} == "composer" ]]; then COMPREPLY=( $(compgen -W "${opts}" -- ${cur}) ) diff --git a/docker/sdk b/docker/sdk index 9cde8ec9..882a8978 100755 --- a/docker/sdk +++ b/docker/sdk @@ -10,6 +10,16 @@ function wrap_in_yellow() { echo -e "\e[33m$1\e[0m" } +# function to print a command/option with its description +function print_command() { + printf " %-35s %s\n" "$(wrap_in_yellow "$1")" "$2" +} + +# function to print an example with its description +function print_example() { + printf " %-40s %s\n" "$(wrap_in_yellow "$1")" "$2" +} + # function to display usage function show_usage() { wrap_in_green "===================================" @@ -19,30 +29,31 @@ function show_usage() { wrap_in_yellow "$0 [-x] [-p] [args]" echo echo "Options:" - echo -e " $(wrap_in_yellow "-x") Enable Xdebug with debug + coverage mode when starting containers" - echo -e " $(wrap_in_yellow "-p") Enable Xdebug with profiler mode when starting containers" + print_command "-x" "Enable Xdebug (debug, coverage)" + print_command "-p" "Enable Xdebug (profiler)" echo echo "Commands:" - echo -e " $(wrap_in_yellow "build") Build Docker containers" - echo -e " $(wrap_in_yellow "start") Start Docker containers (honors -x / -p)" - echo -e " $(wrap_in_yellow "stop") Stop Docker containers" - echo -e " $(wrap_in_yellow "cli") Open container shell or execute the PHP script when provided as an argument" - echo -e " $(wrap_in_yellow "composer ") Run composer command" - echo -e " $(wrap_in_yellow "phpstan") Run PHPStan" - echo -e " $(wrap_in_yellow "phpunit") Run all PHPUnit tests or concrete test case when full qualified name provided as an argument" - echo -e " $(wrap_in_yellow "phpunit-group ") Run PHPUnit tests assigned to the group" - echo -e " $(wrap_in_yellow "phpcs") Run PHP CodeSniffer" - echo -e " $(wrap_in_yellow "phpcbf") Run PHP Code Beautifier and Fixer" - echo -e " $(wrap_in_yellow "hook-install") Install CaptainHook" - echo -e " $(wrap_in_yellow "hook ") Run CaptainHook command" - echo -e " $(wrap_in_yellow "to-generate") Generate internal transfer objects or externals when configuration path provided as an argument" - echo -e " $(wrap_in_yellow "to-generate-bulk") Generate bulk internal transfer objects or externals when configuration path provided as an argument" - echo -e " $(wrap_in_yellow "df-generate") Generate definition" + print_command "install" "Install development environment" + print_command "build" "Build Docker containers" + print_command "start" "Start Docker containers (honors -x / -p)" + print_command "stop" "Stop Docker containers" + print_command "cli [script]" "Open container shell or execute PHP script" + print_command "composer " "Run composer command" + print_command "phpstan" "Run PHPStan" + print_command "phpunit [filter]" "Run PHPUnit tests" + print_command "phpunit-group " "Run PHPUnit tests by group" + print_command "phpcs" "Run PHP CodeSniffer" + print_command "phpcbf" "Run PHP Code Beautifier and Fixer" + print_command "hook-install" "Install CaptainHook" + print_command "hook " "Run CaptainHook command" + print_command "to-generate [c]" "Generate transfer objects" + print_command "to-generate-bulk [b]" "Generate bulk transfer objects" + print_command "df-generate" "Generate definitions" echo echo "Examples:" - echo -e " $(wrap_in_yellow "$0 -x start") Start containers with Xdebug (debug + coverage)" - echo -e " $(wrap_in_yellow "$0 -p start") Start containers with Xdebug profiler" - echo -e " $(wrap_in_yellow "$0 composer install") Run composer install inside container" + print_example "$0 -x start" "Start containers (Xdebug: debug, coverage)" + print_example "$0 -p start" "Start containers (Xdebug: profiler)" + print_example "$0 composer install" "Run composer install inside container" } # vars @@ -70,6 +81,9 @@ shift $((OPTIND -1)) # arguments case $1 in + install) + $0 build && $0 start && $0 composer install + ;; build) USER_ID=$(id -u) GROUP_ID=$(id -g) docker compose build ;; @@ -84,41 +98,27 @@ case $1 in docker compose stop ;; cli) - if [ -n "$5" ]; then - $DOCKER_EXEC php "$2" "$3" "$4" "$5" - elif [ -n "$4" ]; then - $DOCKER_EXEC php "$2" "$3" "$4" - elif [ -n "$3" ]; then - $DOCKER_EXEC php "$2" "$3" - elif [ -n "$2" ]; then - $DOCKER_EXEC php $2 - else + if [ -z "$2" ]; then $DOCKER_EXEC bash + else + $DOCKER_EXEC php "${@:2}" fi ;; composer) - if [ -n "$5" ]; then - $DOCKER_EXEC composer "$2" "$3" "$4" "$5" - elif [ -n "$4" ]; then - $DOCKER_EXEC composer "$2" "$3" "$4" - elif [ -n "$3" ]; then - $DOCKER_EXEC composer "$2" "$3" - else - $DOCKER_EXEC composer "$2" - fi + $DOCKER_EXEC composer "${@:2}" ;; phpstan) $DOCKER_EXEC composer phpstan ;; phpunit) if [ -n "$2" ]; then - $DOCKER_EXEC composer phpunit -- --filter $2 + $DOCKER_EXEC composer phpunit -- --filter "$2" else $DOCKER_EXEC composer phpunit fi ;; phpunit-group) - $DOCKER_EXEC composer phpunit-group $2 + $DOCKER_EXEC composer phpunit-group "$2" ;; phpcs) $DOCKER_EXEC composer phpcs @@ -130,21 +130,13 @@ case $1 in $DOCKER_EXEC composer captainhook install --only-enabled --run-mode=docker --run-exec="docker exec -i $DOCKER_CONTAINER_NAME" ;; hook) - $DOCKER_EXEC composer captainhook $2 + $DOCKER_EXEC composer captainhook "${@:2}" ;; to-generate) - if [ -n "$2" ]; then - $DOCKER_EXEC composer transfer-generate -- -c $2 -v - else - $DOCKER_EXEC composer transfer-generate -- -c ./config/generator.config.yml -v - fi + $DOCKER_EXEC composer transfer-generate -- -c "${2:-./config/generator.config.yml}" -v ;; to-generate-bulk) - if [ -n "$2" ]; then - $DOCKER_EXEC composer transfer-generate-bulk -- -b $2 - else - $DOCKER_EXEC composer transfer-generate-bulk -- -b ./var/config/config.list.txt - fi + $DOCKER_EXEC composer transfer-generate-bulk -- -b "${2:-./var/config/config.list.txt}" ;; df-generate) $DOCKER_EXEC composer definition-generate diff --git a/src/Shared/Filesystem/FileReader.php b/src/Shared/Filesystem/FileReader.php index 9a7127d7..98a99e66 100644 --- a/src/Shared/Filesystem/FileReader.php +++ b/src/Shared/Filesystem/FileReader.php @@ -72,6 +72,12 @@ private function assertEndOfFile($file, string $filename, int $fileLine): void */ private function getFile(string $filename) { + if (!$this->fileExists($filename)) { + throw new FileReaderException( + sprintf('File "%s" does not exist.', $filename), + ); + } + $file = $this->fopen($filename); if ($file === false) { throw new FileReaderException( @@ -87,13 +93,14 @@ private function getFile(string $filename) */ protected function fopen(string $filename) { - if (!file_exists($filename)) { - return false; - } - return fopen($filename, 'r'); } + protected function fileExists(string $filename): bool + { + return file_exists($filename); + } + /** * @param resource $file */ diff --git a/src/TransferGenerator/Config/Environment/ConfigEnvironmentRender.php b/src/TransferGenerator/Config/Environment/ConfigEnvironmentRender.php index e16515ab..7cf6ed53 100644 --- a/src/TransferGenerator/Config/Environment/ConfigEnvironmentRender.php +++ b/src/TransferGenerator/Config/Environment/ConfigEnvironmentRender.php @@ -37,14 +37,19 @@ private function getProjectRoot(): string return $this->projectRootCache = $projectRoot; } - protected function getWorkingDir(): string + private function getWorkingDir(): string { - return getcwd() ?: ''; + return $this->getcwd() ?: ''; } - protected function getEnvironment(): string + protected function getcwd(): false|string { - $envValue = getenv(static::ENVIRONMENT_KEY); + return getcwd(); + } + + private function getEnvironment(): string + { + $envValue = $this->getenv(static::ENVIRONMENT_KEY); if (!is_string($envValue)) { return ''; } @@ -54,6 +59,14 @@ protected function getEnvironment(): string |> $this->rtrimPath(...); } + /** + * @return array|false|string + */ + protected function getenv(string $name): array|false|string + { + return getenv($name); + } + private function rtrimPath(string $path): string { return rtrim($path, '\/'); diff --git a/tests/unit/Shared/Filesystem/FileReaderTest.php b/tests/unit/Shared/Filesystem/FileReaderTest.php index 207157f5..0b69d5e4 100644 --- a/tests/unit/Shared/Filesystem/FileReaderTest.php +++ b/tests/unit/Shared/Filesystem/FileReaderTest.php @@ -25,6 +25,7 @@ protected function setUp(): void { $this->fileReaderMock = $this->getMockBuilder(FileReader::class) ->onlyMethods([ + 'fileExists', 'fopen', 'fgets', 'feof', @@ -37,10 +38,44 @@ public static function tearDownAfterClass(): void self::closeFile(); } + #[TestDox('File does not exist should throw exception')] + public function testFileDoesNotExistShouldThrowException(): void + { + // Expect + $this->fileReaderMock->expects($this->once()) + ->method('fileExists') + ->with(self::FILE_NAME) + ->willReturn(false); + + $this->fileReaderMock->expects($this->never()) + ->method('fopen') + ->with(self::FILE_NAME); + + $this->fileReaderMock->expects($this->never()) + ->method('fgets'); + + $this->fileReaderMock->expects($this->never()) + ->method('feof'); + + $this->fileReaderMock->expects($this->never()) + ->method('fclose') + ->seal(); + + $this->expectException(FileReaderException::class); + + // Act + $this->fileReaderMock->readFile(self::FILE_NAME)->current(); + } + #[TestDox('Failed to open file should throw exception')] public function testFailedToOpenFileShouldThrowException(): void { // Expect + $this->fileReaderMock->expects($this->once()) + ->method('fileExists') + ->with(self::FILE_NAME) + ->willReturn(true); + $this->fileReaderMock->expects($this->once()) ->method('fopen') ->with(self::FILE_NAME) @@ -69,6 +104,11 @@ public function testFailedToReadFileShouldThrowException(): void $file = self::openFile(); // Expect + $this->fileReaderMock->expects($this->once()) + ->method('fileExists') + ->with(self::FILE_NAME) + ->willReturn(true); + $this->fileReaderMock->expects($this->once()) ->method('fopen') ->with(self::FILE_NAME) @@ -103,6 +143,11 @@ public function testFailedToCloseFileShouldThrowException(): void $file = self::openFile(); // Expect + $this->fileReaderMock->expects($this->once()) + ->method('fileExists') + ->with(self::FILE_NAME) + ->willReturn(true); + $this->fileReaderMock->expects($this->once()) ->method('fopen') ->with(self::FILE_NAME) @@ -138,6 +183,11 @@ public function testReadFileShouldSkipEmptyLines(): void $expected = ['some.config.yml']; // Expect + $this->fileReaderMock->expects($this->once()) + ->method('fileExists') + ->with(self::FILE_NAME) + ->willReturn(true); + $this->fileReaderMock->expects($this->once()) ->method('fopen') ->with(self::FILE_NAME) diff --git a/tests/unit/TransferGenerator/Config/Environment/ConfigEnvironmentRenderTest.php b/tests/unit/TransferGenerator/Config/Environment/ConfigEnvironmentRenderTest.php index 1fe31908..5128840a 100644 --- a/tests/unit/TransferGenerator/Config/Environment/ConfigEnvironmentRenderTest.php +++ b/tests/unit/TransferGenerator/Config/Environment/ConfigEnvironmentRenderTest.php @@ -19,8 +19,8 @@ protected function setUp(): void { $this->renderMock = $this->getMockBuilder(ConfigEnvironmentRender::class) ->onlyMethods([ - 'getWorkingDir', - 'getEnvironment', + 'getenv', + 'getcwd', ]) ->getMock(); } @@ -35,11 +35,11 @@ public function testEnvironmentVariableIsSet(): void // Expect $this->renderMock->expects($this->once()) - ->method('getEnvironment') + ->method('getenv') ->willReturn($envProjectRoot); $this->renderMock->expects($this->never()) - ->method('getWorkingDir') + ->method('getcwd') ->seal(); // Act @@ -60,11 +60,11 @@ public function testEnvironmentVariableIsNotSetShouldFallbackToWorkingDirectory( // Expect $this->renderMock->expects($this->once()) - ->method('getEnvironment') - ->willReturn(''); + ->method('getenv') + ->willReturn(false); $this->renderMock->expects($this->once()) - ->method('getWorkingDir') + ->method('getcwd') ->willReturn($workingDirectory) ->seal(); @@ -74,4 +74,28 @@ public function testEnvironmentVariableIsNotSetShouldFallbackToWorkingDirectory( // Assert $this->assertSame($expected, $actual); } + + #[TestDox('Environment variable working directory is not set and failed to get working directory')] + public function testEnvironmentVariableIsNotSetAndWorkingDirectoryIsFailedShouldFallbackToEmpty(): void + { + // Arrange + $configPath = '${PROJECT_ROOT}/some-config-path.yml'; + $expected = '/some-config-path.yml'; + + // Expect + $this->renderMock->expects($this->once()) + ->method('getenv') + ->willReturn(false); + + $this->renderMock->expects($this->once()) + ->method('getcwd') + ->willReturn(false) + ->seal(); + + // Act + $actual = $this->renderMock->renderProjectRoot($configPath); + + // Assert + $this->assertSame($expected, $actual); + } }