diff --git a/.github/workflows/test-suite.yaml b/.github/workflows/test-suite.yaml index a3abc65..ca77cc5 100644 --- a/.github/workflows/test-suite.yaml +++ b/.github/workflows/test-suite.yaml @@ -28,7 +28,7 @@ jobs: run: composer update - name: Run Static Analysis - run: vendor/bin/psalm + run: vendor/bin/phpstan tests: name: Tests diff --git a/Makefile b/Makefile index 7f91e4f..baeb3c2 100644 --- a/Makefile +++ b/Makefile @@ -6,7 +6,7 @@ prod production: # Build application for production test: # Run coding standards/static analysis checks and tests @vendor/bin/php-cs-fixer fix --diff --dry-run \ - && vendor/bin/psalm \ + && vendor/bin/phpstan \ && vendor/bin/phpunit --coverage-text coverage: # Generate an HTML coverage report diff --git a/composer.json b/composer.json index 394be37..d93c860 100644 --- a/composer.json +++ b/composer.json @@ -24,9 +24,9 @@ "yosymfony/toml": "^1.0" }, "require-dev": { - "phlak/coding-standards": "^2.0", + "phlak/coding-standards": "^2.2", + "phpstan/phpstan": "^1.10", "psy/psysh": "^0.11", - "vimeo/psalm": "^5.15", "yoast/phpunit-polyfills": "^2.0" }, "autoload": { diff --git a/phpstan-baseline.neon b/phpstan-baseline.neon new file mode 100644 index 0000000..e69de29 diff --git a/phpstan.neon.dist b/phpstan.neon.dist new file mode 100644 index 0000000..f6b0a56 --- /dev/null +++ b/phpstan.neon.dist @@ -0,0 +1,27 @@ +parameters: + + paths: + - src + - tests + + level: max + + checkFunctionNameCase: true + checkMissingIterableValueType: false + + reportUnmatchedIgnoredErrors: false + + exceptions: + implicitThrows: false + + check: + missingCheckedExceptionInThrows: true + tooWideThrowType: true + + uncheckedExceptionClasses: + - 'InvalidArgumentException' + - 'RuntimeException' + - 'PHPUnit\Framework\Exception' + +includes: + - phpstan-baseline.neon diff --git a/psalm.xml b/psalm.xml deleted file mode 100644 index 5bfb8e3..0000000 --- a/psalm.xml +++ /dev/null @@ -1,55 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/src/Config.php b/src/Config.php index 0933772..0d94877 100644 --- a/src/Config.php +++ b/src/Config.php @@ -7,10 +7,15 @@ use IteratorAggregate; use PHLAK\Config\Exceptions\InvalidContextException; use PHLAK\Config\Interfaces\ConfigInterface; +use PHLAK\Config\Loaders\Loader; use PHLAK\Config\Traits\Arrayable; use RuntimeException; use SplFileInfo; +/** + * @implements ArrayAccess + * @implements IteratorAggregate + */ class Config implements ConfigInterface, ArrayAccess, IteratorAggregate { use Arrayable; @@ -21,13 +26,13 @@ class Config implements ConfigInterface, ArrayAccess, IteratorAggregate /** * Create a new Config object. * - * @param mixed $context Raw array of configuration options or path to a - * configuration file or directory containing one or - * more configuration files - * @param string|array $prefix A key under which the loaded config will be nested + * @param array|string $context Raw array of configuration options or path to a configuration file + * or directory containing one or more configuration files + * @param string $prefix A key under which the loaded config will be nested + * * @throws InvalidContextException */ - public function __construct($context = null, string $prefix = null) + public function __construct(array|string $context = null, string $prefix = null) { switch (gettype($context)) { case 'NULL': @@ -50,11 +55,13 @@ public function __construct($context = null, string $prefix = null) * * @param string $path A path to a directory of configuration files * + * @throws InvalidContextException + * * @return \PHLAK\Config\Interfaces\ConfigInterface A new ConfigInterface object */ public static function fromDirectory(string $path): ConfigInterface { - $config = new self(); + $config = new self; foreach (new DirectoryIterator($path) as $file) { if ($file->isFile()) { @@ -93,7 +100,7 @@ public function set(string $key, mixed $value): bool * Retrieve a configuration option via a provided key. * * @param string $key Unique configuration option key - * @param mixed|null $default Default value to return if option does not exist + * @param mixed $default Default value to return if option does not exist * * @return mixed Stored config item or $default value */ @@ -138,10 +145,9 @@ public function has(string $key): bool * @param string $key Unique configuration option key * @param mixed $value Config item value * - * @return true - * * @throws RuntimeException * + * @return true */ public function append(string $key, mixed $value): bool { @@ -166,10 +172,9 @@ public function append(string $key, mixed $value): bool * @param string $key Unique configuration option key * @param mixed $value Config item value * - * @return true - * * @throws RuntimeException * + * @return true */ public function prepend(string $key, mixed $value): bool { @@ -221,6 +226,7 @@ public function load(string $path, string $prefix = null, bool $override = true) $className = $file->isDir() ? 'Directory' : ucfirst(strtolower($file->getExtension())); $classPath = 'PHLAK\\Config\\Loaders\\' . $className; + /** @var Loader $loader */ $loader = new $classPath($file->getRealPath()); $newConfig = $prefix ? [$prefix => $loader->getArray()] : $loader->getArray(); @@ -259,8 +265,9 @@ public function merge(ConfigInterface $config, bool $override = true): ConfigInt * * @param string $key Unique configuration option key * - * @return \PHLAK\Config\Interfaces\ConfigInterface A new ConfigInterface object * @throws InvalidContextException + * + * @return ConfigInterface A new ConfigInterface object */ public function split(string $key): ConfigInterface { diff --git a/src/Exceptions/ConfigException.php b/src/Exceptions/ConfigException.php index d9252c8..ce606ff 100644 --- a/src/Exceptions/ConfigException.php +++ b/src/Exceptions/ConfigException.php @@ -4,6 +4,4 @@ use Exception; -abstract class ConfigException extends Exception -{ -} +abstract class ConfigException extends Exception {} diff --git a/src/Exceptions/InvalidContextException.php b/src/Exceptions/InvalidContextException.php index 29a8466..44ccc9f 100644 --- a/src/Exceptions/InvalidContextException.php +++ b/src/Exceptions/InvalidContextException.php @@ -2,6 +2,4 @@ namespace PHLAK\Config\Exceptions; -class InvalidContextException extends ConfigException -{ -} +class InvalidContextException extends ConfigException {} diff --git a/src/Exceptions/InvalidFileException.php b/src/Exceptions/InvalidFileException.php index ad2eb1a..0084493 100644 --- a/src/Exceptions/InvalidFileException.php +++ b/src/Exceptions/InvalidFileException.php @@ -2,6 +2,4 @@ namespace PHLAK\Config\Exceptions; -class InvalidFileException extends ConfigException -{ -} +class InvalidFileException extends ConfigException {} diff --git a/src/Interfaces/ConfigInterface.php b/src/Interfaces/ConfigInterface.php index 87b96d1..908060e 100644 --- a/src/Interfaces/ConfigInterface.php +++ b/src/Interfaces/ConfigInterface.php @@ -12,7 +12,7 @@ interface ConfigInterface * more configuration files * @param string $prefix A key under which the loaded config will be nested */ - public function __construct($context = null, string $prefix = null); + public function __construct(array|string $context = null, string $prefix = null); /** * Create a new instance of a ConfigInterface objet from a directory with @@ -91,7 +91,6 @@ public function unset(string $key): bool; * @param string $prefix A key under which the loaded config will be nested * @param bool $override Whether to override existing options with * values from the loaded file - * @return ConfigInterface */ public function load(string $path, string $prefix = null, bool $override = true): self; diff --git a/src/Loaders/Directory.php b/src/Loaders/Directory.php index cb6a823..42494b2 100644 --- a/src/Loaders/Directory.php +++ b/src/Loaders/Directory.php @@ -3,7 +3,6 @@ namespace PHLAK\Config\Loaders; use DirectoryIterator; -use PHLAK\Config\Exceptions\InvalidFileException; class Directory extends Loader { @@ -12,8 +11,6 @@ class Directory extends Loader * and convert them to an array of configuration options. Any invalid files * will be silently ignored. * - * @throws \PHLAK\Config\Exceptions\InvalidFileException - * * @return array Array of configuration options */ public function getArray(): array @@ -28,13 +25,10 @@ public function getArray(): array $className = $file->isDir() ? 'Directory' : ucfirst(strtolower($file->getExtension())); $classPath = 'PHLAK\\Config\\Loaders\\' . $className; + /** @var Loader $loader */ $loader = new $classPath($file->getPathname()); - try { - $contents = array_merge($contents, $loader->getArray()); - } catch (InvalidFileException $e) { - // Ignore it and continue - } + $contents = array_merge($contents, $loader->getArray()); } return $contents; diff --git a/src/Loaders/Json.php b/src/Loaders/Json.php index e9552be..ef8bfed 100644 --- a/src/Loaders/Json.php +++ b/src/Loaders/Json.php @@ -18,6 +18,11 @@ public function getArray(): array { $contents = file_get_contents($this->context); + if ($contents === false) { + throw new InvalidFileException('Unable to parse invalid JSON file at ' . $this->context); + } + + /** @var array|null $parsed */ $parsed = json_decode($contents, true); if (is_null($parsed)) { diff --git a/src/Loaders/Toml.php b/src/Loaders/Toml.php index f4be8ea..0302bd1 100644 --- a/src/Loaders/Toml.php +++ b/src/Loaders/Toml.php @@ -19,6 +19,7 @@ class Toml extends Loader public function getArray(): array { try { + /** @var array $parsed */ $parsed = TomlParser::parseFile($this->context); } catch (ParseException $e) { throw new InvalidFileException($e->getMessage()); diff --git a/src/Loaders/Xml.php b/src/Loaders/Xml.php index dc6b892..fd1d3bb 100644 --- a/src/Loaders/Xml.php +++ b/src/Loaders/Xml.php @@ -2,6 +2,7 @@ namespace PHLAK\Config\Loaders; +use JsonException; use PHLAK\Config\Exceptions\InvalidFileException; class Xml extends Loader @@ -16,12 +17,21 @@ class Xml extends Loader */ public function getArray(): array { - $parsed = @simplexml_load_file($this->context); + $parsed = simplexml_load_file($this->context); - if (! $parsed) { + if ($parsed === false) { throw new InvalidFileException('Unable to parse invalid XML file at ' . $this->context); } - return json_decode(json_encode($parsed), true); + try { + $json = json_encode($parsed, flags: JSON_THROW_ON_ERROR); + } catch (JsonException $exception) { + throw new InvalidFileException(previous: $exception); + } + + /** @var array $array */ + $array = json_decode($json, true); + + return $array; } } diff --git a/src/Loaders/Yaml.php b/src/Loaders/Yaml.php index 705fb35..b7b98fc 100644 --- a/src/Loaders/Yaml.php +++ b/src/Loaders/Yaml.php @@ -18,10 +18,16 @@ class Yaml extends Loader */ public function getArray(): array { + $contents = file_get_contents($this->context); + + if ($contents === false) { + throw new InvalidFileException(sprintf('Unable to parse file [%s]', $this->context)); + } + try { - $parsed = YamlParser::parse(file_get_contents($this->context)); - } catch (ParseException $e) { - throw new InvalidFileException($e->getMessage()); + $parsed = YamlParser::parse($contents); + } catch (ParseException $exception) { + throw new InvalidFileException($exception->getMessage()); } if (! is_array($parsed)) { diff --git a/src/Traits/Arrayable.php b/src/Traits/Arrayable.php index 150ad17..b79fff9 100644 --- a/src/Traits/Arrayable.php +++ b/src/Traits/Arrayable.php @@ -20,11 +20,9 @@ public function getIterator(): Traversable /** * Determine whether an item exists at a specific offset. * - * @param int $offset Offset to check for existence - * - * @return bool + * @param mixed $offset Offset to check for existence */ - public function offsetExists($offset): bool + public function offsetExists(mixed $offset): bool { return isset($this->config[$offset]); } @@ -32,11 +30,9 @@ public function offsetExists($offset): bool /** * Retrieve an item at a specific offset. * - * @param int $offset Position of character to get - * - * @return mixed + * @param mixed $offset Position of character to get */ - public function offsetGet($offset): mixed + public function offsetGet(mixed $offset): mixed { return $this->config[$offset]; } diff --git a/tests/ConfigTest.php b/tests/ConfigTest.php index 776654a..1fa4c72 100644 --- a/tests/ConfigTest.php +++ b/tests/ConfigTest.php @@ -13,14 +13,14 @@ class ConfigTest extends TestCase { public function test_it_is_instantiable(): void { - $config = new Config(); + $config = new Config; $this->assertInstanceOf(ConfigInterface::class, $config); } public function test_it_can_set_and_retrieve_an_item() { - $config = new Config(); + $config = new Config; $this->assertTrue($config->set('name', 'John Pinkerton')); $this->assertEquals('John Pinkerton', $config->get('name')); @@ -28,7 +28,7 @@ public function test_it_can_set_and_retrieve_an_item() public function test_it_can_set_and_retrieve_an_item_by_dot_notation() { - $config = new Config(); + $config = new Config; $this->assertTrue($config->set('foo.bar.baz', 'foo-bar-baz')); $this->assertEquals('foo-bar-baz', $config->get('foo.bar.baz')); @@ -37,14 +37,14 @@ public function test_it_can_set_and_retrieve_an_item_by_dot_notation() public function test_it_returns_null_for_nonexistant_items() { - $config = new Config(); + $config = new Config; $this->assertNull($config->get('nonexistant-item')); } public function test_it_returns_a_default_value_for_nonexistant_items() { - $config = new Config(); + $config = new Config; $this->assertFalse($config->get('nonexistant-item', false)); } @@ -65,7 +65,7 @@ public function test_it_returns_true_if_it_has_a_boolean_false() public function test_it_returns_false_if_it_doesnt_have_an_item() { - $config = new Config(); + $config = new Config; $this->assertFalse($config->has('nonexistant-item')); } @@ -88,7 +88,7 @@ public function test_it_can_load_and_read_additional_files() public function test_it_can_load_additonal_files_with_a_prefix() { - $config = new Config(); + $config = new Config; $config->load(__DIR__ . '/files/php/config.php', 'database'); @@ -152,7 +152,7 @@ public function test_it_throws_an_exception_when_initialized_with_an_invalid_con public function test_it_can_set_and_retrieve_a_closure() { - $config = new Config(); + $config = new Config; $config->set('closure', function ($foo) { return ucwords($foo);