Skip to content

Commit ee8d78c

Browse files
committed
refactor: create dedicated YamlParser class
1 parent 5767f53 commit ee8d78c

7 files changed

Lines changed: 188 additions & 27 deletions

File tree

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
<?php
2+
3+
declare(strict_types=1);
4+
5+
/*
6+
* This file is part of Ymir command-line tool.
7+
*
8+
* (c) Carl Alexander <support@ymirapp.com>
9+
*
10+
* For the full copyright and license information, please view the LICENSE
11+
* file that was distributed with this source code.
12+
*/
13+
14+
namespace Ymir\Cli\Exception;
15+
16+
class YamlParseException extends RuntimeException
17+
{
18+
}

src/Project/ProjectConfiguration.php

Lines changed: 15 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -21,10 +21,12 @@
2121
use Ymir\Cli\Exception\ConfigurationException;
2222
use Ymir\Cli\Exception\InvalidArgumentException;
2323
use Ymir\Cli\Exception\Project\UnsupportedProjectException;
24+
use Ymir\Cli\Exception\YamlParseException;
2425
use Ymir\Cli\Project\Configuration\ConfigurationChangeInterface;
2526
use Ymir\Cli\Project\Type\ProjectTypeInterface;
2627
use Ymir\Cli\Resource\Model\Project;
2728
use Ymir\Cli\Support\Arr;
29+
use Ymir\Cli\YamlParser;
2830

2931
class ProjectConfiguration implements Arrayable
3032
{
@@ -49,6 +51,13 @@ class ProjectConfiguration implements Arrayable
4951
*/
5052
private $filesystem;
5153

54+
/**
55+
* The YAML parser.
56+
*
57+
* @var YamlParser
58+
*/
59+
private $parser;
60+
5261
/**
5362
* All supported project types.
5463
*
@@ -59,9 +68,10 @@ class ProjectConfiguration implements Arrayable
5968
/**
6069
* Constructor.
6170
*/
62-
public function __construct(Filesystem $filesystem, iterable $projectTypes, string $configurationFilePath = '')
71+
public function __construct(Filesystem $filesystem, YamlParser $parser, iterable $projectTypes, string $configurationFilePath = '')
6372
{
6473
$this->filesystem = $filesystem;
74+
$this->parser = $parser;
6575

6676
$this->loadConfiguration($configurationFilePath);
6777

@@ -228,18 +238,10 @@ public function hasEnvironment(string $environment): bool
228238
*/
229239
public function loadConfiguration(string $configurationFilePath): void
230240
{
231-
$configuration = [];
232-
233-
if ($this->filesystem->exists($configurationFilePath)) {
234-
try {
235-
$configuration = Yaml::parse((string) file_get_contents($configurationFilePath));
236-
} catch (\Throwable $exception) {
237-
throw new ConfigurationException(sprintf('Error parsing Ymir project configuration file: %s', $exception->getMessage()));
238-
}
239-
}
240-
241-
if (!empty($configuration) && !is_array($configuration)) {
242-
throw new ConfigurationException('Error parsing Ymir project configuration file');
241+
try {
242+
$configuration = $this->parser->parse($configurationFilePath) ?? [];
243+
} catch (YamlParseException $exception) {
244+
throw new ConfigurationException(sprintf('Error parsing Ymir project configuration file: %s', $exception->getMessage()));
243245
}
244246

245247
$this->configuration = $configuration;

src/YamlParser.php

Lines changed: 50 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,50 @@
1+
<?php
2+
3+
declare(strict_types=1);
4+
5+
/*
6+
* This file is part of Ymir command-line tool.
7+
*
8+
* (c) Carl Alexander <support@ymirapp.com>
9+
*
10+
* For the full copyright and license information, please view the LICENSE
11+
* file that was distributed with this source code.
12+
*/
13+
14+
namespace Ymir\Cli;
15+
16+
use Symfony\Component\Yaml\Yaml;
17+
use Ymir\Cli\Exception\YamlParseException;
18+
19+
class YamlParser
20+
{
21+
/**
22+
* Parse the given YAML file.
23+
*/
24+
public function parse(string $filePath): ?array
25+
{
26+
if (!file_exists($filePath)) {
27+
return null;
28+
}
29+
30+
$contents = file_get_contents($filePath);
31+
32+
if (false === $contents) {
33+
throw new YamlParseException(sprintf('Unable to read the YAML file at "%s"', $filePath));
34+
}
35+
36+
try {
37+
$configuration = Yaml::parse($contents);
38+
} catch (\Throwable $exception) {
39+
throw new YamlParseException(sprintf('Error parsing YAML file at "%s": %s', $filePath, $exception->getMessage()));
40+
}
41+
42+
if (null === $configuration) {
43+
return [];
44+
} elseif (!is_array($configuration)) {
45+
throw new YamlParseException(sprintf('Error parsing YAML file at "%s"', $filePath));
46+
}
47+
48+
return $configuration;
49+
}
50+
}

tests/Integration/Command/Docker/DeleteDockerImagesCommandTest.php

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,7 @@
2222
use Ymir\Cli\Resource\ResourceCollection;
2323
use Ymir\Cli\Tests\Factory\ProjectFactory;
2424
use Ymir\Cli\Tests\Integration\Command\TestCase;
25+
use Ymir\Cli\YamlParser;
2526

2627
class DeleteDockerImagesCommandTest extends TestCase
2728
{
@@ -97,7 +98,7 @@ public function testDeleteAllImagesIgnoresLocalProject(): void
9798
$this->projectTypeMock = \Mockery::mock(ProjectTypeInterface::class);
9899
$this->projectTypeMock->shouldReceive('getSlug')->andReturn('wordpress');
99100

100-
$this->projectConfiguration = new ProjectConfiguration($this->filesystem, [$this->projectTypeMock], $this->tempDir.'/ymir.yml');
101+
$this->projectConfiguration = new ProjectConfiguration($this->filesystem, new YamlParser(), [$this->projectTypeMock], $this->tempDir.'/ymir.yml');
101102
$this->projectConfiguration->createNew($project, collect(), $this->projectTypeMock);
102103

103104
$this->dockerExecutable->shouldReceive('removeImagesMatchingPattern')->with('dkr.ecr')->once();
@@ -158,7 +159,7 @@ public function testDeleteProjectImagesInProjectDirectory(): void
158159
$this->projectTypeMock = \Mockery::mock(ProjectTypeInterface::class);
159160
$this->projectTypeMock->shouldReceive('getSlug')->andReturn('wordpress');
160161

161-
$this->projectConfiguration = new ProjectConfiguration($this->filesystem, [$this->projectTypeMock], $this->tempDir.'/ymir.yml');
162+
$this->projectConfiguration = new ProjectConfiguration($this->filesystem, new YamlParser(), [$this->projectTypeMock], $this->tempDir.'/ymir.yml');
162163
$this->projectConfiguration->createNew($project, collect(), $this->projectTypeMock);
163164

164165
$this->dockerExecutable->shouldReceive('removeImagesMatchingPattern')->with($project->getRepositoryUri())->once();

tests/Integration/Command/TestCase.php

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -36,6 +36,7 @@
3636
use Ymir\Cli\Tests\Factory\ProjectFactory;
3737
use Ymir\Cli\Tests\Factory\TeamFactory;
3838
use Ymir\Cli\Tests\TestCase as BaseTestCase;
39+
use Ymir\Cli\YamlParser;
3940

4041
abstract class TestCase extends BaseTestCase
4142
{
@@ -90,7 +91,7 @@ protected function setUp(): void
9091

9192
$this->apiClient = \Mockery::mock(ApiClient::class);
9293
$this->cliConfiguration = new CliConfiguration($this->homeDir.'/.ymir/config.json', $this->filesystem);
93-
$this->projectConfiguration = new ProjectConfiguration($this->filesystem, [], $this->tempDir.'/ymir.yml');
94+
$this->projectConfiguration = new ProjectConfiguration($this->filesystem, new YamlParser(), [], $this->tempDir.'/ymir.yml');
9495
}
9596

9697
/**
@@ -186,7 +187,7 @@ protected function setupValidProject(int $projectId = 1, string $projectName = '
186187
$this->projectTypeMock->shouldReceive('getSlug')->andReturn($projectTypeSlug);
187188
$this->projectTypeMock->shouldReceive('getDefaultPhpVersion')->andReturn($this->resolveDefaultPhpVersion($projectTypeSlug));
188189

189-
$this->projectConfiguration = new ProjectConfiguration($this->filesystem, [$this->projectTypeMock], $this->tempDir.'/ymir.yml');
190+
$this->projectConfiguration = new ProjectConfiguration($this->filesystem, new YamlParser(), [$this->projectTypeMock], $this->tempDir.'/ymir.yml');
190191
$this->projectConfiguration->createNew($project, collect(), $this->projectTypeMock);
191192

192193
foreach ($environments as $name => $config) {

tests/Unit/Project/ProjectConfigurationTest.php

Lines changed: 16 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@
1919
use Ymir\Cli\Project\EnvironmentConfiguration;
2020
use Ymir\Cli\Project\ProjectConfiguration;
2121
use Ymir\Cli\Tests\TestCase;
22+
use Ymir\Cli\YamlParser;
2223

2324
class ProjectConfigurationTest extends TestCase
2425
{
@@ -41,19 +42,19 @@ protected function tearDown(): void
4142

4243
public function testExistsReturnsFalseIfFileDoesNotExist(): void
4344
{
44-
$this->assertFalse((new ProjectConfiguration(new Filesystem(), [], 'non-existent-file'))->exists());
45+
$this->assertFalse($this->createProjectConfiguration('non-existent-file')->exists());
4546
}
4647

4748
public function testExistsReturnsTrueIfFileExists(): void
4849
{
49-
$this->assertTrue((new ProjectConfiguration(new Filesystem(), [], $this->tempFile))->exists());
50+
$this->assertTrue($this->createProjectConfiguration($this->tempFile)->exists());
5051
}
5152

5253
public function testGetEnvironmentConfigurationReturnsConfiguration(): void
5354
{
5455
file_put_contents($this->tempFile, "environments:\n prod:\n foo: bar");
5556

56-
$configuration = (new ProjectConfiguration(new Filesystem(), [], $this->tempFile))->getEnvironmentConfiguration('prod');
57+
$configuration = $this->createProjectConfiguration($this->tempFile)->getEnvironmentConfiguration('prod');
5758

5859
$this->assertInstanceOf(EnvironmentConfiguration::class, $configuration);
5960
$this->assertSame('prod', $configuration->getName());
@@ -67,14 +68,14 @@ public function testGetEnvironmentConfigurationThrowsExceptionIfEnvironmentDoesN
6768
$this->expectException(InvalidArgumentException::class);
6869
$this->expectExceptionMessage('Environment "staging" not found in Ymir project configuration file');
6970

70-
(new ProjectConfiguration(new Filesystem(), [], $this->tempFile))->getEnvironmentConfiguration('staging');
71+
$this->createProjectConfiguration($this->tempFile)->getEnvironmentConfiguration('staging');
7172
}
7273

7374
public function testGetProjectIdReturnsId(): void
7475
{
7576
file_put_contents($this->tempFile, 'id: 123');
7677

77-
$this->assertSame(123, (new ProjectConfiguration(new Filesystem(), [], $this->tempFile))->getProjectId());
78+
$this->assertSame(123, $this->createProjectConfiguration($this->tempFile)->getProjectId());
7879
}
7980

8081
public function testGetProjectIdThrowsExceptionIfIdIsMissing(): void
@@ -84,14 +85,14 @@ public function testGetProjectIdThrowsExceptionIfIdIsMissing(): void
8485
$this->expectException(ConfigurationException::class);
8586
$this->expectExceptionMessage('No "id" found in Ymir project configuration file');
8687

87-
(new ProjectConfiguration(new Filesystem(), [], $this->tempFile))->getProjectId();
88+
$this->createProjectConfiguration($this->tempFile)->getProjectId();
8889
}
8990

9091
public function testGetProjectNameReturnsName(): void
9192
{
9293
file_put_contents($this->tempFile, 'name: foo');
9394

94-
$this->assertSame('foo', (new ProjectConfiguration(new Filesystem(), [], $this->tempFile))->getProjectName());
95+
$this->assertSame('foo', $this->createProjectConfiguration($this->tempFile)->getProjectName());
9596
}
9697

9798
public function testGetProjectNameThrowsExceptionIfNameIsMissing(): void
@@ -101,7 +102,7 @@ public function testGetProjectNameThrowsExceptionIfNameIsMissing(): void
101102
$this->expectException(ConfigurationException::class);
102103
$this->expectExceptionMessage('No "name" found in Ymir project configuration file');
103104

104-
(new ProjectConfiguration(new Filesystem(), [], $this->tempFile))->getProjectName();
105+
$this->createProjectConfiguration($this->tempFile)->getProjectName();
105106
}
106107

107108
public function testGetProjectTypeThrowsExceptionIfTypeIsMissing(): void
@@ -111,7 +112,7 @@ public function testGetProjectTypeThrowsExceptionIfTypeIsMissing(): void
111112
$this->expectException(ConfigurationException::class);
112113
$this->expectExceptionMessage('No "type" found in Ymir project configuration file');
113114

114-
(new ProjectConfiguration(new Filesystem(), [], $this->tempFile))->getProjectType();
115+
$this->createProjectConfiguration($this->tempFile)->getProjectType();
115116
}
116117

117118
public function testLoadConfigurationThrowsExceptionIfParsingFails(): void
@@ -121,6 +122,11 @@ public function testLoadConfigurationThrowsExceptionIfParsingFails(): void
121122
$this->expectException(ConfigurationException::class);
122123
$this->expectExceptionMessage('Error parsing Ymir project configuration file');
123124

124-
new ProjectConfiguration(new Filesystem(), [], $this->tempFile);
125+
$this->createProjectConfiguration($this->tempFile);
126+
}
127+
128+
private function createProjectConfiguration(string $configurationFilePath): ProjectConfiguration
129+
{
130+
return new ProjectConfiguration(new Filesystem(), new YamlParser(), [], $configurationFilePath);
125131
}
126132
}

tests/Unit/YamlParserTest.php

Lines changed: 83 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,83 @@
1+
<?php
2+
3+
declare(strict_types=1);
4+
5+
/*
6+
* This file is part of Ymir command-line tool.
7+
*
8+
* (c) Carl Alexander <support@ymirapp.com>
9+
*
10+
* For the full copyright and license information, please view the LICENSE
11+
* file that was distributed with this source code.
12+
*/
13+
14+
namespace Ymir\Cli\Tests\Unit;
15+
16+
use Ymir\Cli\Exception\YamlParseException;
17+
use Ymir\Cli\Tests\TestCase;
18+
use Ymir\Cli\YamlParser;
19+
20+
class YamlParserTest extends TestCase
21+
{
22+
/**
23+
* @var string
24+
*/
25+
private $tempFile;
26+
27+
protected function setUp(): void
28+
{
29+
parent::setUp();
30+
31+
$this->tempFile = tempnam(sys_get_temp_dir(), 'ymir-yaml-parser-test');
32+
}
33+
34+
protected function tearDown(): void
35+
{
36+
if (file_exists($this->tempFile)) {
37+
unlink($this->tempFile);
38+
}
39+
40+
parent::tearDown();
41+
}
42+
43+
public function testParseReturnsArray(): void
44+
{
45+
file_put_contents($this->tempFile, "foo: bar\nbaz: 123");
46+
47+
$this->assertSame(['foo' => 'bar', 'baz' => 123], (new YamlParser())->parse($this->tempFile));
48+
}
49+
50+
public function testParseReturnsEmptyArrayWhenFileIsEmpty(): void
51+
{
52+
file_put_contents($this->tempFile, '');
53+
54+
$this->assertSame([], (new YamlParser())->parse($this->tempFile));
55+
}
56+
57+
public function testParseReturnsNullWhenFileDoesNotExist(): void
58+
{
59+
unlink($this->tempFile);
60+
61+
$this->assertNull((new YamlParser())->parse($this->tempFile));
62+
}
63+
64+
public function testParseThrowsExceptionWhenParsedYamlIsNotArray(): void
65+
{
66+
file_put_contents($this->tempFile, 'foo');
67+
68+
$this->expectException(YamlParseException::class);
69+
$this->expectExceptionMessage('Error parsing YAML file at');
70+
71+
(new YamlParser())->parse($this->tempFile);
72+
}
73+
74+
public function testParseThrowsExceptionWhenYamlIsInvalid(): void
75+
{
76+
file_put_contents($this->tempFile, 'invalid yaml: [');
77+
78+
$this->expectException(YamlParseException::class);
79+
$this->expectExceptionMessage('Error parsing YAML file at');
80+
81+
(new YamlParser())->parse($this->tempFile);
82+
}
83+
}

0 commit comments

Comments
 (0)