diff --git a/.editorconfig b/.editorconfig new file mode 100644 index 0000000..5b2278e --- /dev/null +++ b/.editorconfig @@ -0,0 +1,16 @@ +# The IDE is assumed to use PSR-2 for PHP files. + +root = true + +[*] +indent_style = space +indent_size = 4 +tab_width = 4 +end_of_line = lf +charset = utf-8 +trim_trailing_whitespace = true +insert_final_newline = true + +[*.{yml,yaml}] +indent_size = 2 +tab_width = 2 diff --git a/Classes/Command/SentryCommandController.php b/Classes/Command/SentryCommandController.php index 4bc9265..5405953 100644 --- a/Classes/Command/SentryCommandController.php +++ b/Classes/Command/SentryCommandController.php @@ -3,8 +3,8 @@ namespace Netlogix\Sentry\Command; -use Neos\Flow\Cli\CommandController; use Neos\Flow\Annotations as Flow; +use Neos\Flow\Cli\CommandController; use Netlogix\Sentry\Exception\Test; use Netlogix\Sentry\Scope\ScopeProvider; diff --git a/Classes/ExceptionHandler/ExceptionRenderingOptionsResolver.php b/Classes/ExceptionHandler/ExceptionRenderingOptionsResolver.php index 29d0dcf..8ee601b 100644 --- a/Classes/ExceptionHandler/ExceptionRenderingOptionsResolver.php +++ b/Classes/ExceptionHandler/ExceptionRenderingOptionsResolver.php @@ -3,8 +3,9 @@ namespace Netlogix\Sentry\ExceptionHandler; -use Neos\Flow\Error\AbstractExceptionHandler; use Neos\Flow\Annotations as Flow; +use Neos\Flow\Error\AbstractExceptionHandler; +use RuntimeException; use Throwable; /** @@ -38,14 +39,14 @@ protected function echoExceptionWeb($exception) self::throwWhenUsed(); } - protected function echoExceptionCli(\Throwable $exception) + protected function echoExceptionCli(Throwable $exception) { self::throwWhenUsed(); } private static function throwWhenUsed(): void { - throw new \RuntimeException('This Exception Handler should not be used!', 1612044864); + throw new RuntimeException('This Exception Handler should not be used!', 1612044864); } } diff --git a/Classes/Integration/NetlogixIntegration.php b/Classes/Integration/NetlogixIntegration.php index f55f525..7a928f1 100644 --- a/Classes/Integration/NetlogixIntegration.php +++ b/Classes/Integration/NetlogixIntegration.php @@ -62,7 +62,7 @@ private static function configureScopeForEvent(Event $event, EventHint $hint): v return; } - $configureEvent = function() use ($event, $scopeProvider) { + $configureEvent = function () use ($event, $scopeProvider) { $event->setEnvironment($scopeProvider->collectEnvironment()); $event->setExtra($scopeProvider->collectExtra()); $event->setRelease($scopeProvider->collectRelease()); diff --git a/Classes/Package.php b/Classes/Package.php index c5b72cb..1f74683 100644 --- a/Classes/Package.php +++ b/Classes/Package.php @@ -15,16 +15,21 @@ public function boot(Bootstrap $bootstrap) { $dispatcher = $bootstrap->getSignalSlotDispatcher(); - $dispatcher->connect(ConfigurationManager::class, 'configurationManagerReady', static function(ConfigurationManager $configurationManager) { - $dsn = $configurationManager->getConfiguration(ConfigurationManager::CONFIGURATION_TYPE_SETTINGS, 'Netlogix.Sentry.dsn'); + $dispatcher->connect( + ConfigurationManager::class, + 'configurationManagerReady', + static function (ConfigurationManager $configurationManager) { + $dsn = $configurationManager->getConfiguration(ConfigurationManager::CONFIGURATION_TYPE_SETTINGS, + 'Netlogix.Sentry.dsn'); - init([ - 'dsn' => $dsn, - 'integrations' => [ - new NetlogixIntegration(), - ] - ]); - }); + init([ + 'dsn' => $dsn, + 'integrations' => [ + new NetlogixIntegration(), + ] + ]); + } + ); } } diff --git a/Classes/Scope/ScopeProvider.php b/Classes/Scope/ScopeProvider.php index 426c290..69c98d9 100644 --- a/Classes/Scope/ScopeProvider.php +++ b/Classes/Scope/ScopeProvider.php @@ -148,9 +148,8 @@ public function withThrowable(Throwable $t, callable $do): void } /** - * @api - * * @return Throwable|null + * @api */ public function getCurrentThrowable(): ?Throwable { diff --git a/Classes/ThrowableStorage/CompoundStorage.php b/Classes/ThrowableStorage/CompoundStorage.php index b1f401e..256213a 100644 --- a/Classes/ThrowableStorage/CompoundStorage.php +++ b/Classes/ThrowableStorage/CompoundStorage.php @@ -3,11 +3,14 @@ namespace Netlogix\Sentry\ThrowableStorage; +use Closure; use InvalidArgumentException; +use Neos\Flow\Annotations as Flow; use Neos\Flow\Configuration\ConfigurationManager; use Neos\Flow\Core\Bootstrap; use Neos\Flow\Log\ThrowableStorageInterface; -use Neos\Flow\Annotations as Flow; +use RuntimeException; +use Throwable; /** * @Flow\Proxy(false) @@ -22,17 +25,17 @@ final class CompoundStorage implements ThrowableStorageInterface private $initialized = false; /** - * @var \Closure + * @var Closure */ private $initializeStoragesClosure; /** - * @var \Closure + * @var Closure */ protected $requestInformationRenderer; /** - * @var \Closure + * @var Closure */ protected $backtraceRenderer; @@ -56,10 +59,12 @@ public static function createWithOptions(array $options): ThrowableStorageInterf foreach ($storagesFromOptions as $storageClassName) { if (!is_a($storageClassName, ThrowableStorageInterface::class, true)) { - throw new InvalidArgumentException(sprintf('Class "%s" must implement ThrowableStorageInterface', $storageClassName), 1612095174); + throw new InvalidArgumentException(sprintf('Class "%s" must implement ThrowableStorageInterface', + $storageClassName), 1612095174); } if (is_a($storageClassName, CompoundStorage::class, true)) { - throw new InvalidArgumentException('Cannot use CompoundStorage as Storage for CompoundStorage', 1612096699); + throw new InvalidArgumentException('Cannot use CompoundStorage as Storage for CompoundStorage', + 1612096699); } $storageClassNames[] = $storageClassName; } @@ -69,7 +74,7 @@ public static function createWithOptions(array $options): ThrowableStorageInterf return new CompoundStorage($primaryStorageClassName, ... $storageClassNames); } - private function __construct(string $primaryStorageClassName, string ... $additionalStorageClassNames) + private function __construct(string $primaryStorageClassName, string ...$additionalStorageClassNames) { $this->initializeStoragesClosure = function () use ($primaryStorageClassName, $additionalStorageClassNames) { $this->primaryStorage = $this->createStorage($primaryStorageClassName); @@ -81,7 +86,7 @@ private function __construct(string $primaryStorageClassName, string ... $additi }; } - public function logThrowable(\Throwable $throwable, array $additionalData = []) + public function logThrowable(Throwable $throwable, array $additionalData = []) { if (!$this->initializeStorages()) { // could not initialize storages, throw exception @@ -90,19 +95,20 @@ public function logThrowable(\Throwable $throwable, array $additionalData = []) $message = $this->primaryStorage->logThrowable($throwable, $additionalData); - array_walk($this->additionalStorages, static function(ThrowableStorageInterface $storage) use ($throwable, $additionalData) { - $storage->logThrowable($throwable, $additionalData); - }); + array_walk($this->additionalStorages, + static function (ThrowableStorageInterface $storage) use ($throwable, $additionalData) { + $storage->logThrowable($throwable, $additionalData); + }); return $message; } - public function setRequestInformationRenderer(\Closure $requestInformationRenderer) + public function setRequestInformationRenderer(Closure $requestInformationRenderer) { $this->requestInformationRenderer = $requestInformationRenderer; } - public function setBacktraceRenderer(\Closure $backtraceRenderer) + public function setBacktraceRenderer(Closure $backtraceRenderer) { $this->backtraceRenderer = $backtraceRenderer; } @@ -110,13 +116,14 @@ public function setBacktraceRenderer(\Closure $backtraceRenderer) private function createStorage(string $storageClassName): ThrowableStorageInterface { if (!Bootstrap::$staticObjectManager) { - throw new \RuntimeException('Bootstrap::$staticObjectManager is not set yet', 1612434395); + throw new RuntimeException('Bootstrap::$staticObjectManager is not set yet', 1612434395); } assert(is_a($storageClassName, ThrowableStorageInterface::class, true)); $bootstrap = Bootstrap::$staticObjectManager->get(Bootstrap::class); $configurationManager = $bootstrap->getEarlyInstance(ConfigurationManager::class); - $settings = $configurationManager->getConfiguration(ConfigurationManager::CONFIGURATION_TYPE_SETTINGS, 'Neos.Flow'); + $settings = $configurationManager->getConfiguration(ConfigurationManager::CONFIGURATION_TYPE_SETTINGS, + 'Neos.Flow'); $storageOptions = $settings['log']['throwables']['optionsByImplementation'][$storageClassName] ?? []; $storage = $storageClassName::createWithOptions($storageOptions); @@ -138,7 +145,7 @@ private function initializeStorages(): bool try { ($this->initializeStoragesClosure)(); - } catch (\Throwable $t) { + } catch (Throwable $t) { return false; } diff --git a/Classes/ThrowableStorage/SentryStorage.php b/Classes/ThrowableStorage/SentryStorage.php index 871b6bc..aaed5f6 100644 --- a/Classes/ThrowableStorage/SentryStorage.php +++ b/Classes/ThrowableStorage/SentryStorage.php @@ -3,8 +3,9 @@ namespace Netlogix\Sentry\ThrowableStorage; -use Neos\Flow\Log\ThrowableStorageInterface; +use Closure; use Neos\Flow\Annotations as Flow; +use Neos\Flow\Log\ThrowableStorageInterface; use Sentry\State\Scope; use Throwable; use function Sentry\captureException; @@ -24,7 +25,7 @@ public static function createWithOptions(array $options): ThrowableStorageInterf public function logThrowable(Throwable $throwable, array $additionalData = []) { - withScope(function(Scope $scope) use (&$eventId, $throwable, $additionalData) { + withScope(function (Scope $scope) use (&$eventId, $throwable, $additionalData) { $scope->setExtras($additionalData); $eventId = captureException($throwable); }); @@ -36,11 +37,11 @@ public function logThrowable(Throwable $throwable, array $additionalData = []) return ''; } - public function setRequestInformationRenderer(\Closure $requestInformationRenderer) + public function setRequestInformationRenderer(Closure $requestInformationRenderer) { } - public function setBacktraceRenderer(\Closure $backtraceRenderer) + public function setBacktraceRenderer(Closure $backtraceRenderer) { } diff --git a/README.md b/README.md index 8df387c..0dcfea5 100644 --- a/README.md +++ b/README.md @@ -1,11 +1,13 @@ # Netlogix.Sentry ## About -This package provides a Flow integration for the [sentry.io](https://sentry.io) PHP SDK. Some basic -information about the Flow application is added to the sentry Event by default, but you can easily -configure and extend this package to fit your needs. + +This package provides a Flow integration for the [sentry.io](https://sentry.io) PHP SDK. Some basic information about +the Flow application is added to the sentry Event by default, but you can easily configure and extend this package to +fit your needs. ## Installation + `composer require netlogix/sentry` Currently the following Flow versions are supported: @@ -17,29 +19,32 @@ Currently the following Flow versions are supported: ## Setup The sentry DSN Client Key has to be configured. Get it from your project settings (SDK Setup -> Client Keys (DSN)). + ```yaml Netlogix: Sentry: dsn: 'https://fd5c649e6e4d41dd8ca729b15cc5d1c7@o01392.ingest.sentry.io/123456789' ``` -Then simply run `./flow sentry:test` to log an exception to sentry. -While this is technically all you **have to** do, you might want to adjust the providers - see below. +Then simply run `./flow sentry:test` to log an exception to sentry. While this is technically all you **have to** do, +you might want to adjust the providers - see below. ## Configuration -This package allows you to configure which data should be added to the sentry event by changing the providers -for each scope. Currently, the available scopes are `environment`, `extra`, `release`, `tags` and `user`. - -Providers can be sorted using the [PositionalArraySorter](https://github.com/neos/utility-arrays/blob/master/Classes/PositionalArraySorter.php#L15) position strings. -For the scopes `extra`, `tags` and `user`, all data provided will be merged together. The scopes `environment` and `release` only support a **single** value (you can still configure more than one provider, but the last one wins). +This package allows you to configure which data should be added to the sentry event by changing the providers for each +scope. Currently, the available scopes are `environment`, `extra`, `release`, `tags` and `user`. +Providers can be sorted using +the [PositionalArraySorter](https://github.com/neos/utility-arrays/blob/master/Classes/PositionalArraySorter.php#L15) +position strings. For the scopes `extra`, `tags` and `user`, all data provided will be merged together. The +scopes `environment` and `release` only support a **single** value (you can still configure more than one provider, but +the last one wins). ```yaml Netlogix: Sentry: scope: - extra: [] + extra: [ ] release: # If you don't need a specific order, you can simply set the provider to true @@ -57,8 +62,10 @@ Netlogix: ``` ## Environments -The sentry SDK will search for the environment variable `SENTRY_ENVIRONMENT` and use it's value as the current environment. This is still the default, however -you can configure the `Netlogix\Sentry\Scope\Environment\FlowSettings` provider to use a different value: + +The sentry SDK will search for the environment variable `SENTRY_ENVIRONMENT` and use it's value as the current +environment. This is still the default, however you can configure the `Netlogix\Sentry\Scope\Environment\FlowSettings` +provider to use a different value: ```yaml Netlogix: @@ -68,10 +75,11 @@ Netlogix: ``` ## Release tracking -You can use the `Netlogix\Sentry\Scope\Release\PathPattern` `ReleaseProvider` to extract your current release from -the app directory. By default, the configured `pathPattern` is matched against the `FLOW_PATH_ROOT` constant: -````yaml +You can use the `Netlogix\Sentry\Scope\Release\PathPattern` `ReleaseProvider` to extract your current release from the +app directory. By default, the configured `pathPattern` is matched against the `FLOW_PATH_ROOT` constant: + +```yaml Netlogix: Sentry: @@ -83,10 +91,10 @@ Netlogix: # Pattern to extract current release from file path # This pattern is matched against pathToMatch pathPattern: '~/releases/(\d{14})$~' -```` +``` -You can also use the `Netlogix\Sentry\Scope\Release\FlowSettings` to set the Release -through Flow Configuration (`Netlogix.Sentry.release.setting`, set to `%env:SENTRY_RELEASE%` by default). +You can also use the `Netlogix\Sentry\Scope\Release\FlowSettings` to set the Release through Flow +Configuration (`Netlogix.Sentry.release.setting`, set to `%env:SENTRY_RELEASE%` by default). ## Custom Providers @@ -101,6 +109,7 @@ For each scope, you can implement your own providers. Each scope requires it's o Then simply add them to the configuration. If you need access to the thrown exception, you can check `Netlogix\Sentry\Scope\ScopeProvider::getCurrentThrowable()`: + ```php setRequestInformationRenderer($requestInformationRenderer); $storage->setBacktraceRenderer($backtraceRenderer); diff --git a/Tests/Functional/ThrowableStorage/TestThrowableStorage.php b/Tests/Functional/ThrowableStorage/TestThrowableStorage.php index 47255f9..19dd9f0 100644 --- a/Tests/Functional/ThrowableStorage/TestThrowableStorage.php +++ b/Tests/Functional/ThrowableStorage/TestThrowableStorage.php @@ -3,7 +3,9 @@ namespace Netlogix\Sentry\Tests\Functional\ThrowableStorage; +use Closure; use Neos\Flow\Log\ThrowableStorageInterface; +use Throwable; abstract class TestThrowableStorage implements ThrowableStorageInterface { @@ -18,19 +20,19 @@ public static function createWithOptions(array $options): ThrowableStorageInterf return new static($options); } - public function logThrowable(\Throwable $throwable, array $additionalData = []) + public function logThrowable(Throwable $throwable, array $additionalData = []) { if (is_callable(static::$logThrowableClosure)) { (static::$logThrowableClosure)($throwable, $additionalData); } } - public function setRequestInformationRenderer(\Closure $requestInformationRenderer) + public function setRequestInformationRenderer(Closure $requestInformationRenderer) { static::$requestInformationRenderer = $requestInformationRenderer; } - public function setBacktraceRenderer(\Closure $backtraceRenderer) + public function setBacktraceRenderer(Closure $backtraceRenderer) { static::$backtraceRenderer = $backtraceRenderer; } diff --git a/Tests/Unit/Scope/ScopeProviderTest.php b/Tests/Unit/Scope/ScopeProviderTest.php index 2a45c3e..d87187b 100644 --- a/Tests/Unit/Scope/ScopeProviderTest.php +++ b/Tests/Unit/Scope/ScopeProviderTest.php @@ -3,6 +3,7 @@ namespace Netlogix\Sentry\Tests\Functional\Scope; +use Exception; use Neos\Flow\ObjectManagement\ObjectManagerInterface; use Neos\Flow\Tests\UnitTestCase; use Netlogix\Sentry\Exception\InvalidProviderType; @@ -140,33 +141,75 @@ public function providers_must_be_of_the_correct_type(string $scope, object $pro public function provideInvalidProviderTypes(): iterable { yield 'environment stdClass' => ['scope' => 'environment', 'provider' => new stdClass()]; - yield 'environment Extra' => ['scope' => 'environment', 'provider' => $this->getMockBuilder(ExtraProvider::class)->getMock()]; - yield 'environment Release' => ['scope' => 'environment', 'provider' => $this->getMockBuilder(ReleaseProvider::class)->getMock()]; - yield 'environment Tag' => ['scope' => 'environment', 'provider' => $this->getMockBuilder(TagProvider::class)->getMock()]; - yield 'environment User' => ['scope' => 'environment', 'provider' => $this->getMockBuilder(UserProvider::class)->getMock()]; + yield 'environment Extra' => [ + 'scope' => 'environment', + 'provider' => $this->getMockBuilder(ExtraProvider::class)->getMock() + ]; + yield 'environment Release' => [ + 'scope' => 'environment', + 'provider' => $this->getMockBuilder(ReleaseProvider::class)->getMock() + ]; + yield 'environment Tag' => [ + 'scope' => 'environment', + 'provider' => $this->getMockBuilder(TagProvider::class)->getMock() + ]; + yield 'environment User' => [ + 'scope' => 'environment', + 'provider' => $this->getMockBuilder(UserProvider::class)->getMock() + ]; yield 'extra stdClass' => ['scope' => 'extra', 'provider' => new stdClass()]; - yield 'extra Environment' => ['scope' => 'extra', 'provider' => $this->getMockBuilder(EnvironmentProvider::class)->getMock()]; - yield 'extra Release' => ['scope' => 'extra', 'provider' => $this->getMockBuilder(ReleaseProvider::class)->getMock()]; + yield 'extra Environment' => [ + 'scope' => 'extra', + 'provider' => $this->getMockBuilder(EnvironmentProvider::class)->getMock() + ]; + yield 'extra Release' => [ + 'scope' => 'extra', + 'provider' => $this->getMockBuilder(ReleaseProvider::class)->getMock() + ]; yield 'extra Tag' => ['scope' => 'extra', 'provider' => $this->getMockBuilder(TagProvider::class)->getMock()]; yield 'extra User' => ['scope' => 'extra', 'provider' => $this->getMockBuilder(UserProvider::class)->getMock()]; yield 'release stdClass' => ['scope' => 'release', 'provider' => new stdClass()]; - yield 'release Environment' => ['scope' => 'release', 'provider' => $this->getMockBuilder(EnvironmentProvider::class)->getMock()]; - yield 'release Extra' => ['scope' => 'release', 'provider' => $this->getMockBuilder(ExtraProvider::class)->getMock()]; - yield 'release Tag' => ['scope' => 'release', 'provider' => $this->getMockBuilder(TagProvider::class)->getMock()]; - yield 'release User' => ['scope' => 'release', 'provider' => $this->getMockBuilder(UserProvider::class)->getMock()]; + yield 'release Environment' => [ + 'scope' => 'release', + 'provider' => $this->getMockBuilder(EnvironmentProvider::class)->getMock() + ]; + yield 'release Extra' => [ + 'scope' => 'release', + 'provider' => $this->getMockBuilder(ExtraProvider::class)->getMock() + ]; + yield 'release Tag' => [ + 'scope' => 'release', + 'provider' => $this->getMockBuilder(TagProvider::class)->getMock() + ]; + yield 'release User' => [ + 'scope' => 'release', + 'provider' => $this->getMockBuilder(UserProvider::class)->getMock() + ]; yield 'tags stdClass' => ['scope' => 'tags', 'provider' => new stdClass()]; - yield 'tags Environment' => ['scope' => 'tags', 'provider' => $this->getMockBuilder(EnvironmentProvider::class)->getMock()]; + yield 'tags Environment' => [ + 'scope' => 'tags', + 'provider' => $this->getMockBuilder(EnvironmentProvider::class)->getMock() + ]; yield 'tags Extra' => ['scope' => 'tags', 'provider' => $this->getMockBuilder(ExtraProvider::class)->getMock()]; - yield 'tags Release' => ['scope' => 'tags', 'provider' => $this->getMockBuilder(ReleaseProvider::class)->getMock()]; + yield 'tags Release' => [ + 'scope' => 'tags', + 'provider' => $this->getMockBuilder(ReleaseProvider::class)->getMock() + ]; yield 'tags User' => ['scope' => 'tags', 'provider' => $this->getMockBuilder(UserProvider::class)->getMock()]; yield 'user stdClass' => ['scope' => 'user', 'provider' => new stdClass()]; - yield 'user Environment' => ['scope' => 'user', 'provider' => $this->getMockBuilder(EnvironmentProvider::class)->getMock()]; + yield 'user Environment' => [ + 'scope' => 'user', + 'provider' => $this->getMockBuilder(EnvironmentProvider::class)->getMock() + ]; yield 'user Extra' => ['scope' => 'user', 'provider' => $this->getMockBuilder(ExtraProvider::class)->getMock()]; - yield 'user Release' => ['scope' => 'user', 'provider' => $this->getMockBuilder(ReleaseProvider::class)->getMock()]; + yield 'user Release' => [ + 'scope' => 'user', + 'provider' => $this->getMockBuilder(ReleaseProvider::class)->getMock() + ]; yield 'user Tag' => ['scope' => 'user', 'provider' => $this->getMockBuilder(TagProvider::class)->getMock()]; } @@ -380,7 +423,7 @@ public function user_is_merged_recursively(): void */ public function withThrowable_triggers_the_callback(): void { - $throwable = new \Exception('foo', 123); + $throwable = new Exception('foo', 123); $makeSureCallbackWasCalled = self::getMockBuilder(stdClass::class) @@ -399,7 +442,7 @@ public function withThrowable_triggers_the_callback(): void */ public function While_the_callable_runs_the_current_throwable_is_set(): void { - $throwable = new \Exception('foo', 123); + $throwable = new Exception('foo', 123); self::assertNull($this->provider->getCurrentThrowable()); @@ -411,7 +454,7 @@ public function While_the_callable_runs_the_current_throwable_is_set(): void ->expects(self::once()) ->method('foo'); - $this->provider->withThrowable($throwable, function() use ($throwable, $makeSureCallbackWasCalled) { + $this->provider->withThrowable($throwable, function () use ($throwable, $makeSureCallbackWasCalled) { self::assertSame($throwable, $this->provider->getCurrentThrowable()); [$makeSureCallbackWasCalled, 'foo'](); }); diff --git a/Tests/Unit/ThrowableStorage/CompoundStorageTest.php b/Tests/Unit/ThrowableStorage/CompoundStorageTest.php index c201011..b1ca241 100644 --- a/Tests/Unit/ThrowableStorage/CompoundStorageTest.php +++ b/Tests/Unit/ThrowableStorage/CompoundStorageTest.php @@ -3,6 +3,7 @@ namespace Netlogix\Sentry\Tests\Unit\ThrowableStorage; +use InvalidArgumentException; use Neos\Flow\Tests\UnitTestCase; use Netlogix\Sentry\ThrowableStorage\CompoundStorage; @@ -14,7 +15,7 @@ class CompoundStorageTest extends UnitTestCase */ public function if_no_storages_are_given_an_exception_is_thrown(): void { - $this->expectException(\InvalidArgumentException::class); + $this->expectException(InvalidArgumentException::class); CompoundStorage::createWithOptions([]); } @@ -24,7 +25,7 @@ public function if_no_storages_are_given_an_exception_is_thrown(): void */ public function if_another_CompoundStorage_is_given_as_storage_an_exception_is_thrown(): void { - $this->expectException(\InvalidArgumentException::class); + $this->expectException(InvalidArgumentException::class); CompoundStorage::createWithOptions([ 'storages' => [