diff --git a/app/Config/Modules.php b/app/Config/Modules.php index 8d4bf56544b0..8a442fb0a6d1 100644 --- a/app/Config/Modules.php +++ b/app/Config/Modules.php @@ -62,6 +62,14 @@ class Modules extends BaseModules */ public $composerPackages = []; + /** + * If set to `true`, Registrars may have previous config values as an array. + * This is useful when updating arrays or checking properties. + * + * NOTE: Enable this option only for trusted modules/packages. + */ + public bool $registrarHasData = false; + /** * -------------------------------------------------------------------------- * Auto-Discovery Rules diff --git a/system/Config/BaseConfig.php b/system/Config/BaseConfig.php index ce6594d45d36..4c8532df6c7d 100644 --- a/system/Config/BaseConfig.php +++ b/system/Config/BaseConfig.php @@ -276,6 +276,19 @@ protected function registerProperties() $shortName = (new ReflectionClass($this))->getShortName(); + if (static::$moduleConfig->registrarHasData) { + // Get all public properties for this config + $worker = new class () { + /** + * @return array + */ + public function getProperties(BaseConfig $obj): array + { + return get_object_vars($obj); + } + }; + } + // Check the registrar class for a method named after this class' shortName foreach (static::$registrars as $callable) { // ignore non-applicable registrars @@ -283,14 +296,16 @@ protected function registerProperties() continue; // @codeCoverageIgnore } - $properties = $callable::$shortName(); + $currentProps = static::$moduleConfig->registrarHasData ? $worker->getProperties($this) : []; + $properties = $callable::$shortName($currentProps); if (! is_array($properties)) { throw new RuntimeException('Registrars must return an array of properties and their values.'); } foreach ($properties as $property => $value) { - if (isset($this->{$property}) && is_array($this->{$property}) && is_array($value)) { + // TODO: The array check can be removed if the option `registrarHasData` is accepted. + if (isset($this->{$property}) && is_array($this->{$property}) && is_array($value) && ! static::$moduleConfig->registrarHasData) { $this->{$property} = array_merge($this->{$property}, $value); } else { $this->{$property} = $value; diff --git a/tests/_support/Config/TestRegistrar.php b/tests/_support/Config/TestRegistrar.php index 806fd1518402..07b8e0957e70 100644 --- a/tests/_support/Config/TestRegistrar.php +++ b/tests/_support/Config/TestRegistrar.php @@ -20,13 +20,53 @@ */ class TestRegistrar { - public static function RegistrarConfig() + /** + * @param array $previous + */ + public static function RegistrarConfig(array $previous = []) { + if ($previous === []) { + return [ + 'bar' => [ + 'first', + 'second', + ], + 'cars' => [ + 'Trucks' => [ + 'Volvo' => [ + 'year' => 2019, + 'color' => 'dark blue', + ], + ], + 'Sedans Lux' => [ + 'Toyota' => [ + 'year' => 2025, + 'color' => 'silver', + ], + ], + ], + ]; + } + return [ 'bar' => [ 'first', 'second', ], + 'cars' => array_replace_recursive($previous['cars'], [ + 'Trucks' => [ + 'Volvo' => [ + 'year' => 2019, + 'color' => 'dark blue', + ], + ], + 'Sedans Lux' => [ + 'Toyota' => [ + 'year' => 2025, + 'color' => 'silver', + ], + ], + ]), ]; } } diff --git a/tests/system/Config/BaseConfigTest.php b/tests/system/Config/BaseConfigTest.php index 0ba70346de4c..751ff2007e02 100644 --- a/tests/system/Config/BaseConfigTest.php +++ b/tests/system/Config/BaseConfigTest.php @@ -252,18 +252,87 @@ public function testRecognizesLooseValues(): void $this->assertFalse($config->QFALSE); } - public function testRegistrars(): void + public function testRegistrarsWithDisabledRegistrarHasData(): void { + $modules = new Modules(); + + $modules->registrarHasData = false; + BaseConfig::setModules($modules); + $config = new RegistrarConfig(); $config::$registrars = [TestRegistrar::class]; + $this->setPrivateProperty($config, 'didDiscovery', true); $method = self::getPrivateMethodInvoker($config, 'registerProperties'); $method(); + $cars = [ + 'Sedans' => [ + 'Toyota' => [ + 'year' => 2018, + 'color' => 'silver', + ], + ], + 'Trucks' => [ + 'Volvo' => [ + 'year' => 2019, + 'color' => 'dark blue', + ], + ], + 'Sedans Lux' => [ + 'Toyota' => [ + 'year' => 2025, + 'color' => 'silver', + ], + ], + ]; + // no change to unmodified property $this->assertSame('bar', $config->foo); // add to an existing array property $this->assertSame(['baz', 'first', 'second'], $config->bar); + // replace some of the keys with another value + $this->assertSame($cars, $config->cars); + } + + public function testRegistrarsWithEnabledRegistrarHasData(): void + { + $modules = new Modules(); + + $modules->registrarHasData = true; + BaseConfig::setModules($modules); + + $config = new RegistrarConfig(); + $config::$registrars = [TestRegistrar::class]; + + $this->setPrivateProperty($config, 'didDiscovery', true); + $method = $this->getPrivateMethodInvoker($config, 'registerProperties'); + $method(); + + $cars = [ + 'Sedans' => [ + 'Toyota' => [ + 'year' => 2018, + 'color' => 'silver', + ], + ], + 'Trucks' => [ + 'Volvo' => [ + 'year' => 2019, + 'color' => 'dark blue', + ], + ], + 'Sedans Lux' => [ + 'Toyota' => [ + 'year' => 2025, + 'color' => 'silver', + ], + ], + ]; + + $this->assertSame('bar', $config->foo); + $this->assertSame(['first', 'second'], $config->bar); + $this->assertSame($cars, $config->cars); } public function testBadRegistrar(): void diff --git a/tests/system/Config/fixtures/RegistrarConfig.php b/tests/system/Config/fixtures/RegistrarConfig.php index 4f6e1b493eed..9417084f396b 100644 --- a/tests/system/Config/fixtures/RegistrarConfig.php +++ b/tests/system/Config/fixtures/RegistrarConfig.php @@ -17,4 +17,22 @@ class RegistrarConfig extends CodeIgniter\Config\BaseConfig public $bar = [ 'baz', ]; + + /** + * @var array> + */ + public $cars = [ + 'Sedans' => [ + 'Toyota' => [ + 'year' => 2018, + 'color' => 'silver', + ], + ], + 'Trucks' => [ + 'Volvo' => [ + 'year' => 2019, + 'color' => 'blue', + ], + ], + ]; } diff --git a/utils/phpstan-baseline/property.readOnlyByPhpDocAssignOutOfClass.neon b/utils/phpstan-baseline/property.readOnlyByPhpDocAssignOutOfClass.neon index b26204669407..c78e93e7b0ba 100644 --- a/utils/phpstan-baseline/property.readOnlyByPhpDocAssignOutOfClass.neon +++ b/utils/phpstan-baseline/property.readOnlyByPhpDocAssignOutOfClass.neon @@ -1,4 +1,4 @@ -# total 33 errors +# total 35 errors parameters: ignoreErrors: @@ -42,6 +42,11 @@ parameters: count: 1 path: ../../tests/system/CommonFunctionsTest.php + - + message: '#^@readonly property Config\\Modules\:\:\$registrarHasData is assigned outside of its declaring class\.$#' + count: 2 + path: ../../tests/system/Config/BaseConfigTest.php + - message: '#^@readonly property Config\\Modules\:\:\$aliases is assigned outside of its declaring class\.$#' count: 1 diff --git a/utils/phpstan-baseline/property.readOnlyByPhpDocDefaultValue.neon b/utils/phpstan-baseline/property.readOnlyByPhpDocDefaultValue.neon index 6bd85ccd63d8..034941df9372 100644 --- a/utils/phpstan-baseline/property.readOnlyByPhpDocDefaultValue.neon +++ b/utils/phpstan-baseline/property.readOnlyByPhpDocDefaultValue.neon @@ -1,4 +1,4 @@ -# total 20 errors +# total 21 errors parameters: ignoreErrors: @@ -19,7 +19,7 @@ parameters: - message: '#^@readonly property cannot have a default value\.$#' - count: 4 + count: 5 path: ../../app/Config/Modules.php -