diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index b319eea2..9c85cf28 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -158,10 +158,10 @@ jobs: - name: Generate Transfer Objects env: - PROJECT_ROOT: ${{ github.workspace }} + PICAMATOR_TRANSFER_OBJECT_PROJECT_ROOT: ${{ github.workspace }} run: composer transfer-generate -- -c ./config/generator.config.yml - name: Run PHPUnit env: - PROJECT_ROOT: ${{ github.workspace }} + PICAMATOR_TRANSFER_OBJECT_PROJECT_ROOT: ${{ github.workspace }} run: composer phpunit diff --git a/docker-compose.yml b/docker-compose.yml index d5cd9840..e9a244cb 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -5,7 +5,7 @@ services: - "host.docker.internal:host-gateway" environment: XDEBUG_MODE: ${XDEBUG_MODE:-off} - PROJECT_ROOT: /home/transfer/transfer-object + PICAMATOR_TRANSFER_OBJECT_PROJECT_ROOT: ${PICAMATOR_TRANSFER_OBJECT_PROJECT_ROOT:-/home/transfer/transfer-object} build: context: docker/php args: diff --git a/src/Shared/Environment/Enum/EnvironmentEnum.php b/src/Shared/Environment/Enum/EnvironmentEnum.php new file mode 100644 index 00000000..716b8e44 --- /dev/null +++ b/src/Shared/Environment/Enum/EnvironmentEnum.php @@ -0,0 +1,42 @@ +name => '', + self::PROJECT_ROOT_ALIAS->name => '', + self::MAX_FILE_SIZE_MB->name => '10', + ]; + + public function getDefault(): string + { + return self::DEFAULT_VALUES[$this->name]; + } +} diff --git a/src/Shared/Environment/EnvironmentReader.php b/src/Shared/Environment/EnvironmentReader.php new file mode 100644 index 00000000..b9039a46 --- /dev/null +++ b/src/Shared/Environment/EnvironmentReader.php @@ -0,0 +1,52 @@ +getEnvironment(EnvironmentEnum::PROJECT_ROOT) + ?: $this->getEnvironment(EnvironmentEnum::PROJECT_ROOT_ALIAS); + + if ($projectRoot === '') { + return $this->getcwd() ?: ''; + } + + $projectRoot = trim($projectRoot); + + return rtrim($projectRoot, '\/'); + } + + public function getMaxFileSizeMegabytes(): string + { + return $this->getEnvironment(EnvironmentEnum::MAX_FILE_SIZE_MB); + } + + private function getEnvironment(EnvironmentEnum $environment): string + { + $value = $this->getenv($environment->value); + if (is_string($value)) { + return $value; + } + + return $environment->getDefault(); + } + + /** + * @return array|false|string + */ + protected function getenv(string $name): array|false|string + { + return getenv($name); + } + + protected function getcwd(): false|string + { + return getcwd(); + } +} diff --git a/src/Shared/Environment/EnvironmentReaderInterface.php b/src/Shared/Environment/EnvironmentReaderInterface.php new file mode 100644 index 00000000..aac44eb2 --- /dev/null +++ b/src/Shared/Environment/EnvironmentReaderInterface.php @@ -0,0 +1,12 @@ +getCached( key: 'shared:FileSizeValidator', - factory: fn(): FileSizeValidatorInterface => new FileSizeValidator(), + factory: fn(): FileSizeValidatorInterface => new FileSizeValidator($this->createEnvironmentReader()), ); } @@ -102,4 +104,12 @@ final protected function createFileReader(): FileReaderInterface factory: fn(): FileReaderInterface => new FileReader(), ); } + + final protected function createEnvironmentReader(): EnvironmentReaderInterface + { + return $this->getCached( + key: 'shared:EnvironmentReader', + factory: fn(): EnvironmentReaderInterface => new EnvironmentReader(), + ); + } } diff --git a/src/Shared/Validator/FileSizeValidator.php b/src/Shared/Validator/FileSizeValidator.php index 4001eca4..9fe2e232 100644 --- a/src/Shared/Validator/FileSizeValidator.php +++ b/src/Shared/Validator/FileSizeValidator.php @@ -5,6 +5,7 @@ namespace Picamator\TransferObject\Shared\Validator; use Picamator\TransferObject\Generated\ValidatorMessageTransfer; +use Picamator\TransferObject\Shared\Environment\EnvironmentReaderInterface; readonly class FileSizeValidator implements FileSizeValidatorInterface { @@ -15,7 +16,11 @@ File "%s" ("%d" bytes) exceeds the maximum allowed size of "%d" bytes. Please split the file into smaller parts. TEMPLATE; - private const int MAX_FILE_SIZE_BYTES = 10_000_000; + private const int MAX_FILE_SIZE_BYTE_MULTIPLIER = 1_000_000; + + public function __construct(private EnvironmentReaderInterface $environmentReader) + { + } public function validate(string $path): ?ValidatorMessageTransfer { @@ -26,12 +31,14 @@ public function validate(string $path): ?ValidatorMessageTransfer return $this->createErrorMessageTransfer($errorMessage); } - if ($fileSize > self::MAX_FILE_SIZE_BYTES) { + $maxFileSizeBytes = $this->getMaxFileSizeBytes(); + + if ($fileSize > $maxFileSizeBytes) { $errorMessage = sprintf( self::MAX_SIZE_ERROR_MESSAGE_TEMPLATE, $path, $fileSize, - self::MAX_FILE_SIZE_BYTES + $maxFileSizeBytes, ); return $this->createErrorMessageTransfer($errorMessage); @@ -40,6 +47,11 @@ public function validate(string $path): ?ValidatorMessageTransfer return null; } + private function getMaxFileSizeBytes(): int + { + return (int)$this->environmentReader->getMaxFileSizeMegabytes() * self::MAX_FILE_SIZE_BYTE_MULTIPLIER; + } + protected function filesize(string $path): int|false { return @filesize($path); diff --git a/src/Transfer/AbstractTransfer.php b/src/Transfer/AbstractTransfer.php index 1b21b514..02cd5e7e 100644 --- a/src/Transfer/AbstractTransfer.php +++ b/src/Transfer/AbstractTransfer.php @@ -10,6 +10,8 @@ /** * @api + * + * @phpstan-consistent-constructor */ abstract class AbstractTransfer implements TransferInterface { diff --git a/src/TransferGenerator/Config/ConfigFactory.php b/src/TransferGenerator/Config/ConfigFactory.php index 421e55b1..530fb2d3 100644 --- a/src/TransferGenerator/Config/ConfigFactory.php +++ b/src/TransferGenerator/Config/ConfigFactory.php @@ -6,8 +6,6 @@ use ArrayObject; use Picamator\TransferObject\Shared\SharedFactoryTrait; -use Picamator\TransferObject\TransferGenerator\Config\Environment\ConfigEnvironmentRender; -use Picamator\TransferObject\TransferGenerator\Config\Environment\ConfigEnvironmentRenderInterface; use Picamator\TransferObject\TransferGenerator\Config\Loader\ConfigLoader; use Picamator\TransferObject\TransferGenerator\Config\Loader\ConfigLoaderInterface; use Picamator\TransferObject\TransferGenerator\Config\Parser\Builder\ConfigBuilder; @@ -134,11 +132,6 @@ className: ConfigParser::class, protected function createConfigContentBuilder(): ConfigContentBuilderInterface { - return new ConfigContentBuilder($this->createConfigEnvironmentRender()); - } - - protected function createConfigEnvironmentRender(): ConfigEnvironmentRenderInterface - { - return new ConfigEnvironmentRender(); + return new ConfigContentBuilder($this->createEnvironmentReader()); } } diff --git a/src/TransferGenerator/Config/Environment/ConfigEnvironmentRender.php b/src/TransferGenerator/Config/Environment/ConfigEnvironmentRender.php deleted file mode 100644 index 7cf6ed53..00000000 --- a/src/TransferGenerator/Config/Environment/ConfigEnvironmentRender.php +++ /dev/null @@ -1,74 +0,0 @@ -getProjectRoot(); - $path = $this->rtrimPath($path); - - return str_replace(static::PLACEHOLDER, $projectRoot, $path); - } - - public function renderRelativeProjectRoot(string $path): string - { - $path = $this->rtrimPath($path); - - return str_replace(static::PLACEHOLDER, '', $path); - } - - private function getProjectRoot(): string - { - if (isset($this->projectRootCache)) { - return $this->projectRootCache; - } - - $projectRoot = $this->getEnvironment() ?: $this->getWorkingDir(); - - return $this->projectRootCache = $projectRoot; - } - - private function getWorkingDir(): string - { - return $this->getcwd() ?: ''; - } - - protected function getcwd(): false|string - { - return getcwd(); - } - - private function getEnvironment(): string - { - $envValue = $this->getenv(static::ENVIRONMENT_KEY); - if (!is_string($envValue)) { - return ''; - } - - return $envValue - |> trim(...) - |> $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/src/TransferGenerator/Config/Environment/ConfigEnvironmentRenderInterface.php b/src/TransferGenerator/Config/Environment/ConfigEnvironmentRenderInterface.php deleted file mode 100644 index 6366ee43..00000000 --- a/src/TransferGenerator/Config/Environment/ConfigEnvironmentRenderInterface.php +++ /dev/null @@ -1,12 +0,0 @@ -renderPathKeys($contentTransfer); - } + $relativeDefinitionPath = $this->filterPath($contentTransfer->definitionPath); + $contentTransfer->relativeDefinitionPath = $this->replacePlaceholder($relativeDefinitionPath, ''); - private function renderPathKeys(ConfigContentTransfer $contentTransfer): ConfigContentTransfer - { - $contentTransfer->relativeDefinitionPath = - $this->environmentRender->renderRelativeProjectRoot($contentTransfer->definitionPath); + $projectRoot = $this->environmentReader->getProjectRoot(); - $contentTransfer->definitionPath = - $this->environmentRender->renderProjectRoot($contentTransfer->definitionPath); + $definitionPath = $this->replacePlaceholder($contentTransfer->definitionPath, $projectRoot); + $contentTransfer->definitionPath = $definitionPath; - $contentTransfer->transferPath = - $this->environmentRender->renderProjectRoot($contentTransfer->transferPath); + $transferPath = $this->replacePlaceholder($contentTransfer->transferPath, $projectRoot); + $contentTransfer->transferPath = $transferPath; return $contentTransfer; } + + private function filterPath(string $path): string + { + return rtrim($path, '\/'); + } + + private function replacePlaceholder(string $path, string $replace): string + { + return str_replace(static::PLACEHOLDER, $replace, $path); + } } diff --git a/src/TransferGenerator/Definition/DefinitionFactory.php b/src/TransferGenerator/Definition/DefinitionFactory.php index d39f2f66..99d4e5b7 100644 --- a/src/TransferGenerator/Definition/DefinitionFactory.php +++ b/src/TransferGenerator/Definition/DefinitionFactory.php @@ -48,6 +48,7 @@ protected function createDefinitionFinder(): DefinitionFinderInterface { return new DefinitionFinder( $this->createFinder(), + $this->createEnvironmentReader(), $this->getConfig(), ); } diff --git a/src/TransferGenerator/Definition/Filesystem/DefinitionFinder.php b/src/TransferGenerator/Definition/Filesystem/DefinitionFinder.php index f54d0e9a..e2d07230 100644 --- a/src/TransferGenerator/Definition/Filesystem/DefinitionFinder.php +++ b/src/TransferGenerator/Definition/Filesystem/DefinitionFinder.php @@ -8,19 +8,19 @@ use Generator; use IteratorAggregate; use Picamator\TransferObject\Dependency\Finder\FinderInterface; +use Picamator\TransferObject\Shared\Environment\EnvironmentReaderInterface; use Picamator\TransferObject\TransferGenerator\Config\Config\ConfigInterface; use Picamator\TransferObject\TransferGenerator\Exception\TransferGeneratorDefinitionException; readonly class DefinitionFinder implements DefinitionFinderInterface { - private const string MAX_FILE_SIZE = '10M'; - private const string FILE_NAME_PATTERN = '*.transfer.yml'; private const string DEFINITIONS_NOT_FOUND_ERROR_MESSAGE = 'Missing Transfer Object definitions.'; public function __construct( private FinderInterface $finder, + private EnvironmentReaderInterface $environmentReader, private ConfigInterface $config, ) { } @@ -44,10 +44,12 @@ public function getDefinitionFiles(): Generator */ private function findDefinitionFiles(): IteratorAggregate&Countable { + $maxFileSize = $this->environmentReader->getMaxFileSizeMegabytes() . 'M'; + $definitionFinder = $this->finder->findFilesInDirectory( filePattern: self::FILE_NAME_PATTERN, dirName: $this->config->getDefinitionPath(), - maxFileSize: self::MAX_FILE_SIZE, + maxFileSize: $maxFileSize, ); $fileCount = $definitionFinder->count(); diff --git a/tests/unit/Shared/Environment/EnvironmentReaderTest.php b/tests/unit/Shared/Environment/EnvironmentReaderTest.php new file mode 100644 index 00000000..e036b09c --- /dev/null +++ b/tests/unit/Shared/Environment/EnvironmentReaderTest.php @@ -0,0 +1,122 @@ +readerMock = $this->getMockBuilder(EnvironmentReader::class) + ->onlyMethods([ + 'getenv', + 'getcwd', + ]) + ->getMock(); + } + + #[TestDox('Environment variable project root "$projectRoot" is set with "$expected"')] + #[TestWith(['/home/my-user', '/home/my-user'])] + #[TestWith([' /home/my-user/ ', '/home/my-user'])] + public function testProjectRootVariableIsSet(string $projectRoot, string $expected): void + { + // Expect + $this->readerMock->expects($this->once()) + ->method('getenv') + ->with(EnvironmentEnum::PROJECT_ROOT->value) + ->willReturn($projectRoot); + + $this->readerMock->expects($this->never()) + ->method('getcwd') + ->seal(); + + // Act + $actual = $this->readerMock->getProjectRoot(); + + // Assert + $this->assertSame($expected, $actual); + } + + #[TestDox('Environment variable project root is not set should fallback to Alias')] + public function testProjectRootVariableIsNotSetShouldFallbackToAlias(): void + { + // Arrange + $projectRoot = '/home/my-user'; + + // Expect + $this->readerMock->expects($this->exactly(2)) + ->method('getenv') + ->willReturnOnConsecutiveCalls(false, $projectRoot); + + $this->readerMock->expects($this->never()) + ->method('getcwd') + ->seal(); + + // Act + $actual = $this->readerMock->getProjectRoot(); + + // Assert + $this->assertSame($projectRoot, $actual); + } + + #[TestDox('Environment variable project root and Alias are not set should fallback to current working directory')] + public function testProjectRootVariableAndAliasAreNotSetShouldFallbackToCurrentWorkingDirectory(): void + { + // Arrange + $workingDirectory = '/home/work-dir/my-user'; + + // Expect + $this->readerMock->expects($this->exactly(2)) + ->method('getenv') + ->willReturn(false); + + $this->readerMock->expects($this->once()) + ->method('getcwd') + ->willReturn($workingDirectory) + ->seal(); + + // Act + $actual = $this->readerMock->getProjectRoot(); + + // Assert + $this->assertSame($workingDirectory, $actual); + } + + #[TestDox('Environment variable project root and alias are not set and current working directory is failed')] + public function testProjectRootVariableIsNotSetAndWorkingDirectoryIsFailed(): void + { + // Arrange + $expected = ''; + + // Expect + $this->readerMock->expects($this->exactly(2)) + ->method('getenv') + ->willReturn(false); + + $this->readerMock->expects($this->once()) + ->method('getcwd') + ->willReturn(false) + ->seal(); + + // Act + $actual = $this->readerMock->getProjectRoot(); + + // Assert + $this->assertSame($expected, $actual); + } +} diff --git a/tests/unit/Shared/Validator/FileSizeValidatorTest.php b/tests/unit/Shared/Validator/FileSizeValidatorTest.php index c3d364f4..01a8f114 100644 --- a/tests/unit/Shared/Validator/FileSizeValidatorTest.php +++ b/tests/unit/Shared/Validator/FileSizeValidatorTest.php @@ -7,20 +7,25 @@ use PHPUnit\Framework\Attributes\TestDox; use PHPUnit\Framework\MockObject\MockObject; use PHPUnit\Framework\TestCase; +use Picamator\TransferObject\Shared\Environment\EnvironmentReaderInterface; use Picamator\TransferObject\Shared\Validator\FileSizeValidator; final class FileSizeValidatorTest extends TestCase { - /** - * @see \Picamator\TransferObject\Shared\Validator\FileSizeValidator::MAX_FILE_SIZE_BYTES - */ - private const int MAX_FILE_SIZE_BYTES = 10_000_000; + private const string MAX_FILE_SIZE_MEGABYTES = '1'; private FileSizeValidator&MockObject $validatorMock; protected function setUp(): void { + $environmentReaderStub = $this->createStub(EnvironmentReaderInterface::class); + $environmentReaderStub + ->method('getMaxFileSizeMegabytes') + ->willReturn(self::MAX_FILE_SIZE_MEGABYTES) + ->seal(); + $this->validatorMock = $this->getMockBuilder(FileSizeValidator::class) + ->setConstructorArgs([$environmentReaderStub]) ->onlyMethods(['filesize']) ->getMock(); } @@ -51,12 +56,13 @@ public function testMaxFileSizeExceeded(): void { // Arrange $path = '/some-path.txt'; + $fileSize = (int)self::MAX_FILE_SIZE_MEGABYTES * 1_000_000 + 10; // Expect $this->validatorMock->expects($this->once()) ->method('filesize') ->with($path) - ->willReturn(self::MAX_FILE_SIZE_BYTES + 10) + ->willReturn($fileSize) ->seal(); // Act diff --git a/tests/unit/TransferGenerator/Config/Environment/ConfigEnvironmentRenderTest.php b/tests/unit/TransferGenerator/Config/Environment/ConfigEnvironmentRenderTest.php deleted file mode 100644 index 5128840a..00000000 --- a/tests/unit/TransferGenerator/Config/Environment/ConfigEnvironmentRenderTest.php +++ /dev/null @@ -1,101 +0,0 @@ -renderMock = $this->getMockBuilder(ConfigEnvironmentRender::class) - ->onlyMethods([ - 'getenv', - 'getcwd', - ]) - ->getMock(); - } - - #[TestDox('Environment variable working directory is set')] - public function testEnvironmentVariableIsSet(): void - { - // Arrange - $configPath = '${PROJECT_ROOT}/some-config-path.yml'; - $envProjectRoot = '/home/my-user'; - $expected = '/home/my-user/some-config-path.yml'; - - // Expect - $this->renderMock->expects($this->once()) - ->method('getenv') - ->willReturn($envProjectRoot); - - $this->renderMock->expects($this->never()) - ->method('getcwd') - ->seal(); - - // Act - $this->renderMock->renderProjectRoot($configPath); - $actual = $this->renderMock->renderProjectRoot($configPath); // duplicate run to test internal cache - - // Assert - $this->assertSame($expected, $actual); - } - - #[TestDox('Environment variable working directory is not set should use fallback')] - public function testEnvironmentVariableIsNotSetShouldFallbackToWorkingDirectory(): void - { - // Arrange - $configPath = '${PROJECT_ROOT}/some-config-path.yml'; - $workingDirectory = '/home/my-user'; - $expected = '/home/my-user/some-config-path.yml'; - - // Expect - $this->renderMock->expects($this->once()) - ->method('getenv') - ->willReturn(false); - - $this->renderMock->expects($this->once()) - ->method('getcwd') - ->willReturn($workingDirectory) - ->seal(); - - // Act - $actual = $this->renderMock->renderProjectRoot($configPath); - - // 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); - } -}