Skip to content

Commit

Permalink
feat: add support for casting parameters (#7)
Browse files Browse the repository at this point in the history
* feat: add support for casting parameters

* fix: pacify static analysis

* feat: use Symfony ParameterAccess to cast nested arrays

* fix: allow `symfony/property-access` version 5.4
  • Loading branch information
JoshuaLicense authored Nov 17, 2023
1 parent 1cec4e8 commit 29929b5
Show file tree
Hide file tree
Showing 8 changed files with 134 additions and 9 deletions.
30 changes: 26 additions & 4 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -9,15 +9,32 @@ This Composer library facilitates the use of Laminas config placeholders, enabli

```php
<?php

use Dvsa\LaminasConfigCloudParameters\Provider\SecretsManager;
use Dvsa\LaminasConfigCloudParameters\Cast\Boolean;
use Dvsa\LaminasConfigCloudParameters\Cast\Integer;

return [
'config' => [
'providers' => [
FQCN::class => [
// Ids to retreive from the cloud variable storage service.
SecretsManager::class => [
'example-secret',
// ...
],

// ...
],

'casts' => [
// Uses `symfony/property-access` to access the property. See https://symfony.com/doc/current/components/property_access.html#reading-from-arrays.
'[foo]' => Boolean::class,
'[bar][nested]' => Integer::class,

// ...
],
],
// ...
];
```
1. Register the module with the Laminas ModuleManager:

Expand All @@ -26,16 +43,19 @@ This Composer library facilitates the use of Laminas config placeholders, enabli
// module.config.php

return [
'Laminas\Router',
'Dvsa\LaminasConfigCloudParameters',
// ...
];
```

1. Placeholders can be then added to the Laminas config:

```php
return [
'foo' => '%bar%', // Will be replaced by a parameter with the key 'bar' from the cloud variable storage service.
'foo' => '%bar%',
'bar' => [
'nested' => '%baz%',
],
];
```

Expand Down Expand Up @@ -74,6 +94,8 @@ Parameters will be loaded recursively by path. The key will be parameter name wi
```php
<?php

use Dvsa\LaminasConfigCloudParameters\Provider\ParameterStore;

return [
'config' => [
'providers' => [
Expand Down
3 changes: 2 additions & 1 deletion composer.json
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,8 @@
"laminas/laminas-modulemanager": "^2.4|^3.0",
"laminas/laminas-config": "^2.0|^3.0",
"laminas/laminas-config-aggregator": "^1.7",
"symfony/dependency-injection": "^5.4"
"symfony/dependency-injection": "^5.4",
"symfony/property-access": "^5.4|^6.3"
},
"suggest": {
"aws/aws-sdk-php": "To use the AWS parameter providers"
Expand Down
11 changes: 11 additions & 0 deletions src/Cast/Boolean.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
<?php

namespace Dvsa\LaminasConfigCloudParameters\Cast;

class Boolean implements CastInterface
{
public function __invoke(string $value): bool
{
return filter_var($value, FILTER_VALIDATE_BOOLEAN);
}
}
11 changes: 11 additions & 0 deletions src/Cast/CastInterface.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
<?php

namespace Dvsa\LaminasConfigCloudParameters\Cast;

interface CastInterface
{
/**
* @return mixed
*/
public function __invoke(string $value);
}
11 changes: 11 additions & 0 deletions src/Cast/Integer.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
<?php

namespace Dvsa\LaminasConfigCloudParameters\Cast;

class Integer implements CastInterface
{
public function __invoke(string $value): int
{
return intval($value);
}
}
11 changes: 11 additions & 0 deletions src/Exception/InvalidCastException.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
<?php

declare(strict_types=1);

namespace Dvsa\LaminasConfigCloudParameters\Exception;

use InvalidArgumentException;

class InvalidCastException extends InvalidArgumentException
{
}
40 changes: 38 additions & 2 deletions src/Module.php
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@

namespace Dvsa\LaminasConfigCloudParameters;

use Dvsa\LaminasConfigCloudParameters\Exception\InvalidCastException;
use Dvsa\LaminasConfigCloudParameters\ParameterProvider\ParameterProviderInterface;
use Laminas\ConfigAggregator\ArrayProvider;
use Laminas\ConfigAggregator\ConfigAggregator;
Expand All @@ -12,6 +13,7 @@
use Laminas\ModuleManager\ModuleManager;
use Symfony\Component\DependencyInjection\Exception\ParameterNotFoundException as SymfonyParameterNotFoundException;
use Symfony\Component\DependencyInjection\ParameterBag\ParameterBag;
use Symfony\Component\PropertyAccess\PropertyAccess;

/**
* @psalm-api
Expand Down Expand Up @@ -53,6 +55,10 @@ public function onMergeConfig(ModuleEvent $e): void

$resolved = $bag->resolveValue($config);

if (!empty($config['config_parameters']['casts'])) {
$this->applyCasts($resolved, $config['config_parameters']['casts']);
}

return $bag->unescapeValue($resolved);
} catch (SymfonyParameterNotFoundException $e) {
throw new Exception\ParameterNotFoundException($e->getMessage(), $e->getCode(), $e);
Expand All @@ -67,14 +73,44 @@ public function onMergeConfig(ModuleEvent $e): void
/**
* @return array<string, mixed>
*
* @psalm-return array{config_parameters: array{providers: array<string, string[]>}}
* @psalm-return array{config_parameters: array{providers: array<string, string[]>, casts: array<string, class-string<Cast\CastInterface>>}}
*/
public function getConfig(): array
{
return [
'config_parameters' => [
'providers' => [],
]
'casts' => [],
],
];
}

/**
* @psalm-param array<string, mixed> $config
* @psalm-param array<string, class-string<Cast\CastInterface>> $casts
*/
private function applyCasts(array &$config, array $casts): void
{
$propertyAccessor = PropertyAccess::createPropertyAccessor();

foreach ($casts as $key => $type) {
if (!is_a($type, Cast\CastInterface::class, true)) {
throw new InvalidCastException("Class {$type} must implement " . Cast\CastInterface::class . " interface.");
}

$property = $key;

$exists = $propertyAccessor->isReadable($config, $property);

if (!$exists) {
continue;
}

$value = $propertyAccessor->getValue($config, $property);

if (isset($value) && is_string($value)) {
$propertyAccessor->setValue($config, $property, (new $type())($value));
}
}
}
}
26 changes: 24 additions & 2 deletions test/Functional/ModuleTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,8 @@

use Aws\MockHandler;
use Aws\Result;
use Dvsa\LaminasConfigCloudParameters\Cast\Boolean;
use Dvsa\LaminasConfigCloudParameters\Cast\Integer;
use Dvsa\LaminasConfigCloudParameters\Exception\ParameterNotFoundException;
use Dvsa\LaminasConfigCloudParameters\ParameterProvider\Aws\ParameterStore;
use Dvsa\LaminasConfigCloudParameters\ParameterProvider\Aws\SecretsManager;
Expand Down Expand Up @@ -33,6 +35,14 @@ public function testProcessParameters(): void
[
'Name' => '/EXAMPLE/PATH/PARAMETER_VALUE_1',
'Value' => 'parameter',
],
[
'Name' => '/EXAMPLE/PATH/PARAMETER_VALUE_2',
'Value' => 'TRUE',
],
[
'Name' => '/EXAMPLE/PATH/PARAMETER_VALUE_3',
'Value' => '42',
]
]
])
Expand All @@ -54,17 +64,29 @@ public function testProcessParameters(): void
'/EXAMPLE/PATH',
],
],
'casts' => [
'[parameter_2]' => Boolean::class,
'[parameter_3][nested][deep]' => new Integer(),
],
],
'secret' => '%SECRET_VALUE_1%',
'parameter' => '%PARAMETER_VALUE_1%',
'parameter_2' => '%PARAMETER_VALUE_2%',
'parameter_3' => [
'nested' => [
'deep' => '%PARAMETER_VALUE_3%',
],
],
];

$application = $this->createApplication($config);

$config = $application->getConfig();

$this->assertEquals('secret', $config['secret'] ?? null);
$this->assertEquals('parameter', $config['parameter'] ?? null);
$this->assertSame('secret', $config['secret'] ?? null);
$this->assertSame('parameter', $config['parameter'] ?? null);
$this->assertSame(true, $config['parameter_2'] ?? null);
$this->assertSame(42, $config['parameter_3']['nested']['deep'] ?? null);
}

public function testMissingParametersThrowException(): void
Expand Down

0 comments on commit 29929b5

Please sign in to comment.