diff --git a/packages/core/src/Application/Configuration/Configuration.php b/packages/core/src/Application/Configuration/Configuration.php new file mode 100644 index 000000000..0cacb446c --- /dev/null +++ b/packages/core/src/Application/Configuration/Configuration.php @@ -0,0 +1,110 @@ + + */ +abstract class Configuration implements ArrayAccess { + protected function __construct() { + // empty + } + + /** + * @param array $array + */ + public static function __set_state(array $array): static { + return new static(...$array); // @phpstan-ignore new.static (this is developer responsibility) + } + + #[Override] + public function offsetExists(mixed $offset): bool { + throw new LogicException('Not supported.'); + } + + #[Override] + public function offsetGet(mixed $offset): mixed { + throw new LogicException('Not supported.'); + } + + #[Override] + public function offsetSet(mixed $offset, mixed $value): void { + throw new LogicException('Not supported.'); + } + + #[Override] + public function offsetUnset(mixed $offset): void { + throw new LogicException('Not supported.'); + } + + /** + * @deprecated %{VERSION} Array-based config is deprecated. Please migrate to object-based config. + * + * @param array $array + */ + public static function fromArray(array $array): static { + $properties = []; + + foreach ($array as $key => $value) { + $property = static::fromArrayGetPropertyName($key); + $properties[$property] = static::fromArrayGetPropertyValue($property, $value); + } + + return static::__set_state($properties); + } + + /** + * @deprecated %{VERSION} + */ + protected static function fromArrayGetPropertyName(int|string $property): string { + if (!is_string($property)) { + throw new LogicException( + sprintf( + 'The `%s::$%s` is not a valid property name.', + static::class, + $property, + ), + ); + } + + if (str_contains($property, '_')) { + $property = Str::camel($property); + } + + return $property; + } + + /** + * @deprecated %{VERSION} + */ + protected static function fromArrayGetPropertyValue(string $property, mixed $value): mixed { + if (is_array($value) && (!array_is_list($value) || $value === [])) { + $property = new ReflectionProperty(static::class, $property); + $type = $property->getType(); + + if ($type instanceof ReflectionNamedType && !$type->isBuiltin()) { + $name = $type->getName(); + + if (is_a($name, self::class, true)) { + $value = $name::fromArray($value); + } + } + } + + return $value; + } +} diff --git a/packages/core/src/Application/Configuration/ConfigurationResolver.php b/packages/core/src/Application/Configuration/ConfigurationResolver.php new file mode 100644 index 000000000..74e3a1e92 --- /dev/null +++ b/packages/core/src/Application/Configuration/ConfigurationResolver.php @@ -0,0 +1,31 @@ + + */ +abstract class ConfigurationResolver extends Resolver { + public function __construct(ConfigResolver $config) { + parent::__construct( + static function () use ($config): Configuration { + /** @var TConfiguration $configuration */ + $configuration = $config->getInstance()->get(static::getName()); + + return $configuration; + }, + ); + } + + abstract protected static function getName(): string; + + /** + * @return TConfiguration + */ + abstract public static function getDefaultConfig(): Configuration; +} diff --git a/packages/core/src/Application/Configuration/ConfigurationTest.php b/packages/core/src/Application/Configuration/ConfigurationTest.php new file mode 100644 index 000000000..502735bdb --- /dev/null +++ b/packages/core/src/Application/Configuration/ConfigurationTest.php @@ -0,0 +1,103 @@ +a = 321; + $expected->b = new ConfigurationTest_ConfigurationB(); + $actual = ConfigurationTest_ConfigurationA::__set_state([ + 'a' => 321, + 'b' => new ConfigurationTest_ConfigurationB(), + ]); + + self::assertEquals($expected, $actual); + } + + public function testOffsetGet(): void { + self::expectException(LogicException::class); + + $config = new ConfigurationTest_ConfigurationA(); + + $config['a']; // @phpstan-ignore expr.resultUnused (for test) + } + + public function testOffsetExists(): void { + self::expectException(LogicException::class); + + $config = new ConfigurationTest_ConfigurationA(); + + isset($config['a']); // @phpstan-ignore expr.resultUnused (for test) + } + + public function testOffsetUnset(): void { + self::expectException(LogicException::class); + + $config = new ConfigurationTest_ConfigurationA(); + + unset($config['a']); + } + + public function testOffsetSet(): void { + self::expectException(LogicException::class); + + $config = new ConfigurationTest_ConfigurationA(); + + $config['a'] = 123; + } + + public function testFromArray(): void { + $expected = new ConfigurationTest_ConfigurationA(); + $expected->a = 321; + $expected->b = new ConfigurationTest_ConfigurationB(); + $expected->b->b = true; + $expected->b->bA = 'cba'; + $actual = ConfigurationTest_ConfigurationA::fromArray([ + 'a' => 321, + 'b' => [ + 'b' => true, + 'b_a' => 'cba', + ], + ]); + + self::assertEquals($expected, $actual); + } +} + +// @phpcs:disable PSR1.Classes.ClassDeclaration.MultipleClasses +// @phpcs:disable Squiz.Classes.ValidClassName.NotCamelCaps + +/** + * @internal + * @noinspection PhpMultipleClassesDeclarationsInOneFile + */ +class ConfigurationTest_ConfigurationA extends Configuration { + public function __construct( + public int $a = 123, + public ?ConfigurationTest_ConfigurationB $b = null, + ) { + parent::__construct(); + } +} + +/** + * @internal + * @noinspection PhpMultipleClassesDeclarationsInOneFile + */ +class ConfigurationTest_ConfigurationB extends Configuration { + public function __construct( + public bool $b = false, + public string $bA = 'abc', + ) { + parent::__construct(); + } +} diff --git a/packages/core/src/Provider/WithConfig.php b/packages/core/src/Provider/WithConfig.php index 08055bc4b..d596d6920 100644 --- a/packages/core/src/Provider/WithConfig.php +++ b/packages/core/src/Provider/WithConfig.php @@ -5,15 +5,32 @@ use Illuminate\Contracts\Config\Repository; use Illuminate\Contracts\Foundation\CachesConfiguration; use Illuminate\Support\ServiceProvider; +use LastDragon_ru\LaraASP\Core\Application\Configuration\Configuration; +use LastDragon_ru\LaraASP\Core\Application\Configuration\ConfigurationResolver; +use LastDragon_ru\LaraASP\Core\Package; use LastDragon_ru\LaraASP\Core\Utils\ConfigMerger; +use function is_array; +use function trigger_deprecation; + /** + * @see Configuration + * * @phpstan-require-extends ServiceProvider */ trait WithConfig { use Helper; + /** + * @deprecated %{VERSION} Please migrate to {@see self::registerConfig()} and object-based config. + */ protected function bootConfig(): void { + trigger_deprecation( + Package::Name, + '%{VERSION}', + 'Please migrate to `self::registerConfig()` and object-based config.', + ); + $package = $this->getName(); $path = $this->getPath('../defaults/config.php'); @@ -35,4 +52,50 @@ protected function loadConfigFrom(string $path, string $key): void { ]); } } + + /** + * @template C of Configuration + * @template T of ConfigurationResolver + * + * @param class-string $resolver + */ + protected function registerConfig(string $resolver): void { + $package = $this->getName(); + + $this->app->singletonIf($resolver); + $this->loadPackageConfig($resolver); + $this->publishes([ + $this->getPath('../defaults/config.php') => $this->app->configPath("{$package}.php"), + ], 'config'); + } + + /** + * @param class-string> $resolver + */ + private function loadPackageConfig(string $resolver): void { + if (!($this->app instanceof CachesConfiguration && $this->app->configurationIsCached())) { + $repository = $this->app->make(Repository::class); + $package = $this->getName(); + $current = $repository->get($package, null); + + if ($current === null) { + $repository->set([ + $package => $resolver::getDefaultConfig(), + ]); + } elseif (is_array($current)) { + // todo(core): Remove somewhere in v9 or later. + trigger_deprecation( + Package::Name, + '%{VERSION}', + 'Array-based config is deprecated. Please migrate to object-based config.', + ); + + $repository->set([ + $package => $resolver::getDefaultConfig()::fromArray((new ConfigMerger())->merge($current, [])), + ]); + } else { + // empty + } + } + } } diff --git a/packages/core/src/Provider/WithConfigTest.php b/packages/core/src/Provider/WithConfigTest.php new file mode 100644 index 000000000..494af4d25 --- /dev/null +++ b/packages/core/src/Provider/WithConfigTest.php @@ -0,0 +1,190 @@ +shouldReceive('make') + ->with(Repository::class) + ->once() + ->andReturn($repository); + + $provider = new WithConfigTest_Provider($application); + $config = $provider->getName(); + $actual = null; + + $repository + ->shouldReceive('get') + ->with($config, null) + ->once() + ->andReturn(null); + $repository + ->shouldReceive('set') + ->once() + ->andReturnUsing(static function (mixed ...$args) use (&$actual): void { + $actual = $args; + }); + + $provider->loadPackageConfig(WithConfigTest_ConfigurationResolver::class); + + self::assertEquals( + [ + [ + $config => WithConfigTest_ConfigurationResolver::getDefaultConfig(), + ], + ], + $actual, + ); + } + + public function testLoadPackageConfigDefined(): void { + $repository = Mockery::mock(Repository::class); + $application = Mockery::mock(Application::class); + $application + ->shouldReceive('make') + ->with(Repository::class) + ->once() + ->andReturn($repository); + + $provider = new WithConfigTest_Provider($application); + $config = $provider->getName(); + + $repository + ->shouldReceive('get') + ->with($config, null) + ->once() + ->andReturn( + WithConfigTest_ConfigurationResolver::getDefaultConfig(), + ); + $repository + ->shouldReceive('set') + ->never(); + + $provider->loadPackageConfig(WithConfigTest_ConfigurationResolver::class); + } + + public function testLoadPackageConfigLegacy(): void { + $repository = Mockery::mock(Repository::class); + $application = Mockery::mock(Application::class); + $application + ->shouldReceive('make') + ->with(Repository::class) + ->once() + ->andReturn($repository); + + $provider = new WithConfigTest_Provider($application); + $config = $provider->getName(); + $actual = null; + $expected = new WithConfigTest_ConfigurationA(); + $expected->a = 321; + $expected->b = new WithConfigTest_ConfigurationB(); + $expected->b->b = true; + $expected->b->bA = 'cba'; + + $repository + ->shouldReceive('get') + ->with($config, null) + ->once() + ->andReturn([ + 'a' => 321, + 'b' => [ + 'b' => true, + 'b_a' => 'cba', + ], + ]); + $repository + ->shouldReceive('set') + ->once() + ->andReturnUsing(static function (mixed ...$args) use (&$actual): void { + $actual = $args; + }); + + $provider->loadPackageConfig(WithConfigTest_ConfigurationResolver::class); + + self::assertEquals( + [ + [ + $config => $expected, + ], + ], + $actual, + ); + } +} + +// @phpcs:disable PSR1.Classes.ClassDeclaration.MultipleClasses +// @phpcs:disable Squiz.Classes.ValidClassName.NotCamelCaps + +/** + * @internal + */ +class WithConfigTest_Provider extends ServiceProvider { + use WithConfig { + loadPackageConfig as public; + } + + #[Override] + public function getName(): string { + return __METHOD__; + } +} + +/** + * @internal + * @extends ConfigurationResolver + */ +class WithConfigTest_ConfigurationResolver extends ConfigurationResolver { + #[Override] + protected static function getName(): string { + return Package::Name; + } + + #[Override] + public static function getDefaultConfig(): Configuration { + return new WithConfigTest_ConfigurationA(); + } +} + +/** + * @internal + * @noinspection PhpMultipleClassesDeclarationsInOneFile + */ +class WithConfigTest_ConfigurationA extends Configuration { + public function __construct( + public int $a = 123, + public ?WithConfigTest_ConfigurationB $b = null, + ) { + parent::__construct(); + } +} + +/** + * @internal + * @noinspection PhpMultipleClassesDeclarationsInOneFile + */ +class WithConfigTest_ConfigurationB extends Configuration { + public function __construct( + public bool $b = false, + public string $bA = 'abc', + ) { + parent::__construct(); + } +} diff --git a/packages/core/src/Utils/ConfigMerger.php b/packages/core/src/Utils/ConfigMerger.php index ef4543db1..92aeca1ce 100644 --- a/packages/core/src/Utils/ConfigMerger.php +++ b/packages/core/src/Utils/ConfigMerger.php @@ -3,6 +3,7 @@ namespace LastDragon_ru\LaraASP\Core\Utils; use InvalidArgumentException; +use LastDragon_ru\LaraASP\Core\Package; use function array_key_exists; use function array_values; @@ -11,9 +12,16 @@ use function is_scalar; use function is_string; use function key; +use function trigger_deprecation; + +// phpcs:disable PSR1.Files.SideEffects + +trigger_deprecation(Package::Name, '%{VERSION}', 'Please migrate to object-based config.'); /** * The merger for array-based configs. + * + * @deprecated %{VERSION} Please migrate to object-based config. */ class ConfigMerger { /** diff --git a/packages/core/src/Utils/ConfigMergerTest.php b/packages/core/src/Utils/ConfigMergerTest.php index 711c20e7c..c72b9b14e 100644 --- a/packages/core/src/Utils/ConfigMergerTest.php +++ b/packages/core/src/Utils/ConfigMergerTest.php @@ -11,6 +11,7 @@ /** * @internal + * @deprecated %{VERSION} */ #[CoversClass(ConfigMerger::class)] final class ConfigMergerTest extends TestCase { diff --git a/packages/graphql/README.md b/packages/graphql/README.md index c86717dad..22c725d8c 100644 --- a/packages/graphql/README.md +++ b/packages/graphql/README.md @@ -134,7 +134,7 @@ For Implicit type, the following rules are applied (in this order; concrete dire * Otherwise - include * Ignored (if supported)? - exclude -When converting the field, some of the original directives will be copied into the newly generated field. For the Explicit type, all directives except operators of other directives will be copied. For Implicit type, you can use [`builder.allowed_directives`](defaults/config.php) setting to control. Be aware of directive locations - the package doesn't perform any checks to ensure that the copied directive allowed on `INPUT_FIELD_DEFINITION`, it just copies it as is. +When converting the field, some of the original directives will be copied into the newly generated field. For the Explicit type, all directives except operators of other directives will be copied. For Implicit type, you can use [`Config::$allowedDirectives`][code-links/2537184413e9d748] setting to control. Be aware of directive locations - the package doesn't perform any checks to ensure that the copied directive allowed on `INPUT_FIELD_DEFINITION`, it just copies it as is. # Builder field/column name @@ -605,3 +605,11 @@ Please follow [Upgrade Guide](UPGRADE.md). This package is the part of Awesome Set of Packages for Laravel. Please use the [main repository](https://github.com/LastDragon-ru/lara-asp) to [report issues](https://github.com/LastDragon-ru/lara-asp/issues), send [pull requests](https://github.com/LastDragon-ru/lara-asp/pulls), or [ask questions](https://github.com/LastDragon-ru/lara-asp/discussions). [//]: # (end: preprocess/c4ba75080f5a48b7) + +[//]: # (start: code-links) +[//]: # (warning: Generated automatically. Do not edit.) + +[code-links/2537184413e9d748]: src/Builder/Config.php#L11-L29 + "\LastDragon_ru\LaraASP\GraphQL\Builder\Config::$allowedDirectives" + +[//]: # (end: code-links) diff --git a/packages/graphql/UPGRADE.md b/packages/graphql/UPGRADE.md index af1ac6a48..ad6d661be 100644 --- a/packages/graphql/UPGRADE.md +++ b/packages/graphql/UPGRADE.md @@ -47,6 +47,8 @@ Please also see [changelog](https://github.com/LastDragon-ru/lara-asp/releases) [//]: # (end: preprocess/9679e76379216855) +* [ ] Package config now uses objects instead of an array, it is recommended to migrate to the new format. 🤝 + * [ ] The [`JsonStringType`][code-links/9ad31c571587f0f4] is not implement [`TypeDefinition`][code-links/3c9ddc100b69df14] anymore. To add the scalar into the Schema, you can use `@type`/`@scalar` directive, or create a custom implementation of `TypeDefinition` contract to use with `Builder`/`Manipulator`. ## Tests @@ -352,7 +354,7 @@ This section is actual only if you are extending the package. Please review and [code-links/ab92ab72ccf08721]: src/SearchBy/Definitions/SearchByOperatorFieldDirective.php "\LastDragon_ru\LaraASP\GraphQL\SearchBy\Definitions\SearchByOperatorFieldDirective" -[code-links/5f93528c6eb9dc8f]: src/SearchBy/Operators.php#L60 +[code-links/5f93528c6eb9dc8f]: src/SearchBy/Operators.php#L58 "\LastDragon_ru\LaraASP\GraphQL\SearchBy\Operators::Object" [code-links/b26bb0f7b2034eb1]: src/SortBy/Definitions/SortByOperatorFieldDirective.php diff --git a/packages/graphql/composer.json b/packages/graphql/composer.json index 233d1d687..149221480 100644 --- a/packages/graphql/composer.json +++ b/packages/graphql/composer.json @@ -23,7 +23,6 @@ "ext-mbstring": "*", "composer/semver": "^3.2", "illuminate/collections": "^10.34.0|^11.0.0", - "illuminate/container": "^10.34.0|^11.0.0", "illuminate/contracts": "^10.34.0|^11.0.0", "illuminate/database": "^10.34.0|^11.0.0", "illuminate/support": "^10.34.0|^11.0.0", diff --git a/packages/graphql/defaults/config.php b/packages/graphql/defaults/config.php index a3610260c..e584fc6d8 100644 --- a/packages/graphql/defaults/config.php +++ b/packages/graphql/defaults/config.php @@ -1,146 +1,16 @@ >>, - * }, - * sort_by: array{ - * operators: array>>, - * nulls: Nulls|non-empty-array, Nulls>|null, - * }, - * stream: array{ - * search: array{ - * name: string, - * enabled: bool, - * }, - * sort: array{ - * name: string, - * enabled: bool, - * }, - * limit: array{ - * name: string, - * default: int<1, max>, - * max: int<1, max>, - * }, - * offset: array{ - * name: string, - * } - * }, - * builder: array{ - * allowed_directives: list, - * }, - * } $settings */ -$settings = [ - /** - * Settings for {@see \LastDragon_ru\LaraASP\GraphQL\SearchBy\Definitions\SearchByDirective @searchBy} directive. - */ - 'search_by' => [ - /** - * Operators - * --------------------------------------------------------------------- - * - * You can redefine operators for exiting (=default) types OR define own - * types here. Note that directives is the recommended way and have - * priority over the array. Please see the documentation for more - * details. - */ - 'operators' => [ - // empty - ], - ], - - /** - * Settings for {@see \LastDragon_ru\LaraASP\GraphQL\SortBy\Definitions\SortByDirective @sortBy} directive. - */ - 'sort_by' => [ - /** - * Operators - * --------------------------------------------------------------------- - * - * You can redefine operators for exiting (=default) types OR define own - * types here. Note that directives is the recommended way and have - * priority over the array. Please see the documentation for more - * details. - */ - 'operators' => [ - // empty - ], - - /** - * NULLs - * - * --------------------------------------------------------------------- - * - * Determines how the `NULL` values should be treatment. By default, - * there is no any processing, so the order of `NULL` depends on the - * database. It may be set for all (if single value) or for each - * direction (if array). Not all databases/builders may be supported. - * Please check the documentation for more details. - * - * @see Nulls - */ - 'nulls' => null, - ], - - /** - * Settings for {@see \LastDragon_ru\LaraASP\GraphQL\Stream\Definitions\StreamDirective @stream} directive. - */ - 'stream' => [ - 'search' => [ - 'name' => 'where', - 'enabled' => true, - ], - 'sort' => [ - 'name' => 'order', - 'enabled' => true, - ], - 'limit' => [ - 'name' => 'limit', - 'default' => 25, - 'max' => 100, - ], - 'offset' => [ - 'name' => 'offset', - ], - ], +$config = PackageConfig::getDefaultConfig(); - /** - * General settings for all `Builder` directives like `@searchBy`/`@sortBy`/etc. - */ - 'builder' => [ - /** - * The list of the directives which should be copied from the original - * field into the generated `input` field. - * - * Important notes: - * - All other directives except {@see Operator} (for the current - * directive) will be ignored. - * - There are no any checks that directive can be used on - * `INPUT_FIELD_DEFINITION`. - * - The `instanceof` operator is used to check. - * - Applies for Implicit types only. - */ - 'allowed_directives' => [ - RenameDirective::class, - ], - ], -]; +// ... -return $settings; +return $config; diff --git a/packages/graphql/docs/Directives/@searchBy.md b/packages/graphql/docs/Directives/@searchBy.md index 4f57aa2d5..f4bf209e6 100644 --- a/packages/graphql/docs/Directives/@searchBy.md +++ b/packages/graphql/docs/Directives/@searchBy.md @@ -254,52 +254,44 @@ on ### Schema +[include:example]: @searchByConfigOperators.php +[//]: # (start: preprocess/95312ea5dfacf197) +[//]: # (warning: Generated automatically. Do not edit.) + ```php >> - * } - * } $settings - */ -$settings = [ - 'search_by' => [ - 'operators' => [ - // You can define a list of operators for each type - 'Date' => [ - SearchByOperatorEqualDirective::class, - SearchByOperatorBetweenDirective::class, - MyCustomOperator::class, - ], - - // Or re-use existing type - 'DateTime' => [ - 'Date', - ], - - // Or re-use built-in type - 'Int' => [ - 'Int', // built-in operators for `Int` will be used - MyCustomOperator::class, - ], - - // You can also use enum name to redefine default operators for it: - 'MyEnum' => [ - 'Boolean', - ], - ], - ], +$config = PackageConfig::getDefaultConfig(); + +// You can define a list of operators for each type +$config->searchBy->operators['Date'] = [ + SearchByOperatorEqualDirective::class, + SearchByOperatorBetweenDirective::class, + // MyCustomOperator::class, +]; + +// Or re-use existing type +$config->searchBy->operators['DateTime'] = [ + 'Date', ]; -return $settings; +// Or re-use built-in type +$config->searchBy->operators['Int'] = [ + 'Int', // built-in operators for `Int` will be used + // MyCustomOperator::class, // the custom operator will be added +]; + +// You can also use enum name to redefine default operators for it: +$config->searchBy->operators['MyEnum'] = [ + 'Boolean', +]; + +// Return +return $config; ``` + +[//]: # (end: preprocess/95312ea5dfacf197) diff --git a/packages/graphql/docs/Directives/@searchByConfigOperators.php b/packages/graphql/docs/Directives/@searchByConfigOperators.php new file mode 100644 index 000000000..813f35f3a --- /dev/null +++ b/packages/graphql/docs/Directives/@searchByConfigOperators.php @@ -0,0 +1,33 @@ +searchBy->operators['Date'] = [ + SearchByOperatorEqualDirective::class, + SearchByOperatorBetweenDirective::class, + // MyCustomOperator::class, +]; + +// Or re-use existing type +$config->searchBy->operators['DateTime'] = [ + 'Date', +]; + +// Or re-use built-in type +$config->searchBy->operators['Int'] = [ + 'Int', // built-in operators for `Int` will be used + // MyCustomOperator::class, // the custom operator will be added +]; + +// You can also use enum name to redefine default operators for it: +$config->searchBy->operators['MyEnum'] = [ + 'Boolean', +]; + +// Return +return $config; diff --git a/packages/graphql/docs/Directives/@sortBy.md b/packages/graphql/docs/Directives/@sortBy.md index 305d27cfc..7e85f3498 100644 --- a/packages/graphql/docs/Directives/@sortBy.md +++ b/packages/graphql/docs/Directives/@sortBy.md @@ -115,37 +115,28 @@ extend scalar SortByOperatorsExtra or via config +[include:example]: @sortByConfigOrderByRandom.php +[//]: # (start: preprocess/d2d497ec780cf493) +[//]: # (warning: Generated automatically. Do not edit.) + ```php >> - * }, - * } $settings - */ -$settings = [ - 'sort_by' => [ - 'operators' => [ - SortByOperators::Extra => [ - SortByOperatorRandomDirective::class, - ], - ], - ], +$config = PackageConfig::getDefaultConfig(); + +$config->sortBy->operators[Operators::Extra] = [ + SortByOperatorRandomDirective::class, ]; -return $settings; +return $config; ``` +[//]: # (end: preprocess/d2d497ec780cf493) + And after this, you can 🎉 ```graphql @@ -163,54 +154,50 @@ query { Default ordering can be changed via config. You may set it for all directions if single value used, in this case NULL always be first/last: +[include:example]: @sortByConfigNullsSingleValue.php +[//]: # (start: preprocess/0a3d52d172342702) +[//]: # (warning: Generated automatically. Do not edit.) + ```php , Nulls>|null, - * }, - * } $settings - */ -$settings = [ - 'sort_by' => [ - 'nulls' => Nulls::First, - ], -]; +$config = PackageConfig::getDefaultConfig(); -return $settings; +$config->sortBy->nulls = Nulls::First; + +return $config; ``` +[//]: # (end: preprocess/0a3d52d172342702) + Or individually for each direction: +[include:example]: @sortByConfigNullsArrayValue.php +[//]: # (start: preprocess/d7692bfa2035b990) +[//]: # (warning: Generated automatically. Do not edit.) + ```php , Nulls>|null, - * }, - * } $settings - */ -$settings = [ - 'sort_by' => [ - 'nulls' => [ - Direction::Asc->value => Nulls::First, - Direction::Desc->value => Nulls::Last, - ], - ], +$config = PackageConfig::getDefaultConfig(); + +$config->sortBy->nulls = [ + Direction::Asc->value => Nulls::First, + Direction::Desc->value => Nulls::Last, ]; -return $settings; +return $config; ``` +[//]: # (end: preprocess/d7692bfa2035b990) + The query is also supported and have highest priority (will override default settings): ```graphql diff --git a/packages/graphql/docs/Directives/@sortByConfigNullsArrayValue.php b/packages/graphql/docs/Directives/@sortByConfigNullsArrayValue.php new file mode 100644 index 000000000..409191460 --- /dev/null +++ b/packages/graphql/docs/Directives/@sortByConfigNullsArrayValue.php @@ -0,0 +1,14 @@ +sortBy->nulls = [ + Direction::Asc->value => Nulls::First, + Direction::Desc->value => Nulls::Last, +]; + +return $config; diff --git a/packages/graphql/docs/Directives/@sortByConfigNullsSingleValue.php b/packages/graphql/docs/Directives/@sortByConfigNullsSingleValue.php new file mode 100644 index 000000000..85adcb2ac --- /dev/null +++ b/packages/graphql/docs/Directives/@sortByConfigNullsSingleValue.php @@ -0,0 +1,10 @@ +sortBy->nulls = Nulls::First; + +return $config; diff --git a/packages/graphql/docs/Directives/@sortByConfigOrderByRandom.php b/packages/graphql/docs/Directives/@sortByConfigOrderByRandom.php new file mode 100644 index 000000000..d4649258b --- /dev/null +++ b/packages/graphql/docs/Directives/@sortByConfigOrderByRandom.php @@ -0,0 +1,13 @@ +sortBy->operators[Operators::Extra] = [ + SortByOperatorRandomDirective::class, +]; + +return $config; diff --git a/packages/graphql/src/Builder/Config.php b/packages/graphql/src/Builder/Config.php new file mode 100644 index 000000000..ca2eb1ea1 --- /dev/null +++ b/packages/graphql/src/Builder/Config.php @@ -0,0 +1,33 @@ + + */ + public array $allowedDirectives = [ + RenameDirective::class, + ], + ) { + parent::__construct(); + } +} diff --git a/packages/graphql/src/Builder/Types/InputObject.php b/packages/graphql/src/Builder/Types/InputObject.php index d2faf4d0a..e392caa6e 100644 --- a/packages/graphql/src/Builder/Types/InputObject.php +++ b/packages/graphql/src/Builder/Types/InputObject.php @@ -13,7 +13,6 @@ use GraphQL\Type\Definition\FieldDefinition; use GraphQL\Type\Definition\InputObjectField; use GraphQL\Type\Definition\Type; -use LastDragon_ru\LaraASP\Core\Application\ConfigResolver; use LastDragon_ru\LaraASP\GraphQL\Builder\Context\HandlerContextImplicit; use LastDragon_ru\LaraASP\GraphQL\Builder\Context\HandlerContextOperators; use LastDragon_ru\LaraASP\GraphQL\Builder\Contracts\Context; @@ -29,7 +28,7 @@ use LastDragon_ru\LaraASP\GraphQL\Builder\Sources\InterfaceSource; use LastDragon_ru\LaraASP\GraphQL\Builder\Sources\ObjectFieldSource; use LastDragon_ru\LaraASP\GraphQL\Builder\Sources\ObjectSource; -use LastDragon_ru\LaraASP\GraphQL\Package; +use LastDragon_ru\LaraASP\GraphQL\PackageConfig; use Nuwave\Lighthouse\Schema\Directives\RelationDirective; use Nuwave\Lighthouse\Schema\Directives\RenameDirective; use Nuwave\Lighthouse\Support\Contracts\Directive; @@ -38,12 +37,11 @@ use function count; use function is_a; -use function is_string; use function trim; abstract class InputObject implements TypeDefinition { public function __construct( - protected readonly ConfigResolver $config, + protected readonly PackageConfig $config, ) { // empty } @@ -443,10 +441,10 @@ protected function isFieldDirectiveAllowed( // Allowed? $isAllowed = false; - $allowed = (array) $this->config->getInstance()->get(Package::Name.'.builder.allowed_directives'); + $allowed = $this->config->getInstance()->builder->allowedDirectives; foreach ($allowed as $class) { - if (is_string($class) && $directive instanceof $class) { + if ($directive instanceof $class) { $isAllowed = true; break; } diff --git a/packages/graphql/src/Builder/Types/InputObjectTest.php b/packages/graphql/src/Builder/Types/InputObjectTest.php index 78c7be4c9..04f417bfc 100644 --- a/packages/graphql/src/Builder/Types/InputObjectTest.php +++ b/packages/graphql/src/Builder/Types/InputObjectTest.php @@ -3,7 +3,6 @@ namespace LastDragon_ru\LaraASP\GraphQL\Builder\Types; use Exception; -use LastDragon_ru\LaraASP\Core\Application\ConfigResolver; use LastDragon_ru\LaraASP\GraphQL\Builder\Context; use LastDragon_ru\LaraASP\GraphQL\Builder\Context\HandlerContextImplicit; use LastDragon_ru\LaraASP\GraphQL\Builder\Contracts\Context as ContextContract; @@ -19,7 +18,8 @@ use LastDragon_ru\LaraASP\GraphQL\Builder\Sources\InterfaceSource; use LastDragon_ru\LaraASP\GraphQL\Builder\Sources\ObjectFieldSource; use LastDragon_ru\LaraASP\GraphQL\Builder\Sources\ObjectSource; -use LastDragon_ru\LaraASP\GraphQL\Package; +use LastDragon_ru\LaraASP\GraphQL\Config\Config; +use LastDragon_ru\LaraASP\GraphQL\PackageConfig; use LastDragon_ru\LaraASP\GraphQL\Testing\Package\Directives\TestDirective; use LastDragon_ru\LaraASP\GraphQL\Testing\Package\TestCase; use Mockery; @@ -48,15 +48,15 @@ public function testIsFieldDirectiveAllowed( ContextContract $context, ): void { if ($allowed !== null) { - $this->setConfig([ - Package::Name.'.builder.allowed_directives' => $allowed, - ]); + $this->setConfiguration(PackageConfig::class, static function (Config $config) use ($allowed): void { + $config->builder->allowedDirectives = $allowed; + }); } $manipulator = Mockery::mock(Manipulator::class); $field = Mockery::mock(ObjectFieldSource::class); $input = new InputObjectTest__InputObject( - $this->app()->make(ConfigResolver::class), + $this->app()->make(PackageConfig::class), ); self::assertEquals($expected, $input->isFieldDirectiveAllowed($manipulator, $field, $context, $directive)); diff --git a/packages/graphql/src/Config/Config.php b/packages/graphql/src/Config/Config.php new file mode 100644 index 000000000..1d2ff55bf --- /dev/null +++ b/packages/graphql/src/Config/Config.php @@ -0,0 +1,32 @@ + + */ +class PackageConfig extends ConfigurationResolver { + #[Override] + protected static function getName(): string { + return Package::Name; + } + + #[Override] + public static function getDefaultConfig(): Config { + return new Config(); + } +} diff --git a/packages/graphql/src/Provider.php b/packages/graphql/src/Provider.php index 6e840d359..750f68ea7 100644 --- a/packages/graphql/src/Provider.php +++ b/packages/graphql/src/Provider.php @@ -38,7 +38,6 @@ class Provider extends ServiceProvider { use WithConfig; public function boot(Dispatcher $dispatcher): void { - $this->bootConfig(); $this->bootDirectives($dispatcher); } @@ -46,6 +45,7 @@ public function boot(Dispatcher $dispatcher): void { public function register(): void { parent::register(); + $this->registerConfig(PackageConfig::class); $this->registerBindings(); $this->registerSchemaPrinter(); } diff --git a/packages/graphql/src/ProviderTest.php b/packages/graphql/src/ProviderTest.php new file mode 100644 index 000000000..3173f3717 --- /dev/null +++ b/packages/graphql/src/ProviderTest.php @@ -0,0 +1,64 @@ +app(); + $config = $app->make(Repository::class); + $legacy = (array) require self::getTestData()->path('~LegacyConfig.php'); + $package = Package::Name; + + $config->set($package, $legacy); + + self::assertIsArray($config->get($package)); + + (new Provider($app))->register(); + + // Test + $expected = new Config(); + $expected->searchBy->operators['Date'] = [ + SearchByOperatorEqualDirective::class, + SearchByOperatorBetweenDirective::class, + ]; + $expected->searchBy->operators['DateTime'] = ['Date']; + $expected->sortBy->operators[SortByOperators::Extra] = [ + SortByOperatorRandomDirective::class, + ]; + $expected->stream->search->name = 'custom_where'; + $expected->stream->sort->name = 'custom_order'; + $expected->stream->limit->name = 'custom_limit'; + $expected->stream->limit->default = 5; + $expected->stream->limit->max = 10; + $expected->stream->offset->name = 'custom_offset'; + $expected->builder->allowedDirectives = [ + RenameDirective::class, + ValidateDirective::class, + ]; + + self::assertEquals($expected, $config->get($package)); + } +} diff --git a/packages/graphql/src/ProviderTest~LegacyConfig.php b/packages/graphql/src/ProviderTest~LegacyConfig.php new file mode 100644 index 000000000..79793b952 --- /dev/null +++ b/packages/graphql/src/ProviderTest~LegacyConfig.php @@ -0,0 +1,160 @@ +>>, + * }, + * sort_by: array{ + * operators: array>>, + * nulls: Nulls|non-empty-array, Nulls>|null, + * }, + * stream: array{ + * search: array{ + * name: string, + * enabled: bool, + * }, + * sort: array{ + * name: string, + * enabled: bool, + * }, + * limit: array{ + * name: string, + * default: int<1, max>, + * max: int<1, max>, + * }, + * offset: array{ + * name: string, + * } + * }, + * builder: array{ + * allowed_directives: list, + * }, + * } $settings + */ +$settings = [ + /** + * Settings for {@see \LastDragon_ru\LaraASP\GraphQL\SearchBy\Definitions\SearchByDirective @searchBy} directive. + */ + 'search_by' => [ + /** + * Operators + * --------------------------------------------------------------------- + * + * You can redefine operators for exiting (=default) types OR define own + * types here. Note that directives is the recommended way and have + * priority over the array. Please see the documentation for more + * details. + */ + 'operators' => [ + 'Date' => [ + SearchByOperatorEqualDirective::class, + SearchByOperatorBetweenDirective::class, + ], + 'DateTime' => [ + 'Date', + ], + ], + ], + + /** + * Settings for {@see \LastDragon_ru\LaraASP\GraphQL\SortBy\Definitions\SortByDirective @sortBy} directive. + */ + 'sort_by' => [ + /** + * Operators + * --------------------------------------------------------------------- + * + * You can redefine operators for exiting (=default) types OR define own + * types here. Note that directives is the recommended way and have + * priority over the array. Please see the documentation for more + * details. + */ + 'operators' => [ + SortByOperators::Extra => [ + SortByOperatorRandomDirective::class, + ], + ], + + /** + * NULLs + * + * --------------------------------------------------------------------- + * + * Determines how the `NULL` values should be treatment. By default, + * there is no any processing, so the order of `NULL` depends on the + * database. It may be set for all (if single value) or for each + * direction (if array). Not all databases/builders may be supported. + * Please check the documentation for more details. + * + * @see Nulls + */ + 'nulls' => null, + ], + + /** + * Settings for {@see \LastDragon_ru\LaraASP\GraphQL\Stream\Definitions\StreamDirective @stream} directive. + */ + 'stream' => [ + 'search' => [ + 'name' => 'custom_where', + 'enabled' => true, + ], + 'sort' => [ + 'name' => 'custom_order', + 'enabled' => true, + ], + 'limit' => [ + 'name' => 'custom_limit', + 'default' => 5, + 'max' => 10, + ], + 'offset' => [ + 'name' => 'custom_offset', + ], + ], + + /** + * General settings for all `Builder` directives like `@searchBy`/`@sortBy`/etc. + */ + 'builder' => [ + /** + * The list of the directives which should be copied from the original + * field into the generated `input` field. + * + * Important notes: + * - All other directives except {@see Operator} (for the current + * directive) will be ignored. + * - There are no any checks that directive can be used on + * `INPUT_FIELD_DEFINITION`. + * - The `instanceof` operator is used to check. + * - Applies for Implicit types only. + */ + 'allowed_directives' => [ + RenameDirective::class, + ValidateDirective::class, + ], + ], +]; + +return $settings; diff --git a/packages/graphql/src/SearchBy/Config.php b/packages/graphql/src/SearchBy/Config.php new file mode 100644 index 000000000..2744de0e9 --- /dev/null +++ b/packages/graphql/src/SearchBy/Config.php @@ -0,0 +1,22 @@ +>> + */ + public array $operators = [], + ) { + parent::__construct(); + } +} diff --git a/packages/graphql/src/SearchBy/Directives/DirectiveTest.php b/packages/graphql/src/SearchBy/Directives/DirectiveTest.php index 8465e4190..00e1083dc 100644 --- a/packages/graphql/src/SearchBy/Directives/DirectiveTest.php +++ b/packages/graphql/src/SearchBy/Directives/DirectiveTest.php @@ -28,8 +28,9 @@ use LastDragon_ru\LaraASP\GraphQL\Builder\Field; use LastDragon_ru\LaraASP\GraphQL\Builder\Manipulator; use LastDragon_ru\LaraASP\GraphQL\Builder\Property; +use LastDragon_ru\LaraASP\GraphQL\Config\Config; use LastDragon_ru\LaraASP\GraphQL\Exceptions\TypeDefinitionUnknown; -use LastDragon_ru\LaraASP\GraphQL\Package; +use LastDragon_ru\LaraASP\GraphQL\PackageConfig; use LastDragon_ru\LaraASP\GraphQL\SearchBy\Contracts\Ignored; use LastDragon_ru\LaraASP\GraphQL\SearchBy\Definitions\SearchByOperatorBetweenDirective; use LastDragon_ru\LaraASP\GraphQL\SearchBy\Definitions\SearchByOperatorEqualDirective; @@ -118,11 +119,11 @@ public function testManipulateArgDefinition(string $expected, string $graphql, ? #[RequiresLaravelScout] public function testManipulateArgDefinitionScoutBuilder(): void { - $this->setConfig([ - Package::Name.'.search_by.operators.Date' => [ + $this->setConfiguration(PackageConfig::class, static function (Config $config): void { + $config->searchBy->operators['Date'] = [ SearchByOperatorEqualDirective::class, - ], - ]); + ]; + }); $this->app()->make(DirectiveLocator::class) ->setResolved('search', SearchDirective::class); @@ -158,11 +159,11 @@ public function testManipulateArgDefinitionScoutBuilderV5Compat(): void { ->andReturn(false); }); - $this->setConfig([ - Package::Name.'.search_by.operators.Date' => [ + $this->setConfiguration(PackageConfig::class, static function (Config $config): void { + $config->searchBy->operators['Date'] = [ SearchByOperatorEqualDirective::class, - ], - ]); + ]; + }); $this->app()->make(DirectiveLocator::class) ->setResolved('search', SearchDirective::class); @@ -688,32 +689,38 @@ public static function definition(): string { $locator->setResolved('allowed', $allowed::class); $locator->setResolved('forbidden', $forbidden::class); - $test->setConfig([ - Package::Name.'.builder.allowed_directives' => [ - RenameDirective::class, - $allowed::class, - ], - Package::Name.'.search_by.operators.String' => [ - SearchByOperatorEqualDirective::class, - ], - Package::Name.'.search_by.operators.'.Operators::Extra => [ - SearchByOperatorFieldDirective::class, - ], - ]); + $test->setConfiguration( + PackageConfig::class, + static function (Config $config) use ($allowed): void { + $config->builder->allowedDirectives = [ + RenameDirective::class, + $allowed::class, + ]; + $config->searchBy->operators['String'] = [ + SearchByOperatorEqualDirective::class, + ]; + $config->searchBy->operators[Operators::Extra] = [ + SearchByOperatorFieldDirective::class, + ]; + }, + ); }, ], 'Ignored' => [ 'Ignored.expected.graphql', 'Ignored.schema.graphql', static function (TestCase $test): void { - $test->setConfig([ - Package::Name.'.search_by.operators.String' => [ - SearchByOperatorEqualDirective::class, - ], - Package::Name.'.search_by.operators.'.Operators::Extra => [ - SearchByOperatorFieldDirective::class, - ], - ]); + $test->setConfiguration( + PackageConfig::class, + static function (Config $config): void { + $config->searchBy->operators['String'] = [ + SearchByOperatorEqualDirective::class, + ]; + $config->searchBy->operators[Operators::Extra] = [ + SearchByOperatorFieldDirective::class, + ]; + }, + ); $test->app()->make(TypeRegistry::class)->register( new class([ @@ -734,25 +741,31 @@ static function (TestCase $test): void { 'Example.expected.graphql', 'Example.schema.graphql', static function (TestCase $test): void { - $test->setConfig([ - Package::Name.'.search_by.operators.Date' => [ - SearchByOperatorBetweenDirective::class, - ], - ]); + $test->setConfiguration( + PackageConfig::class, + static function (Config $config): void { + $config->searchBy->operators['Date'] = [ + SearchByOperatorBetweenDirective::class, + ]; + }, + ); }, ], 'InterfaceUpdate' => [ 'InterfaceUpdate.expected.graphql', 'InterfaceUpdate.schema.graphql', static function (TestCase $test): void { - $test->setConfig([ - Package::Name.'.search_by.operators.'.Operators::ID => [ - SearchByOperatorEqualDirective::class, - ], - Package::Name.'.search_by.operators.'.Operators::Extra => [ - SearchByOperatorFieldDirective::class, - ], - ]); + $test->setConfiguration( + PackageConfig::class, + static function (Config $config): void { + $config->searchBy->operators[Operators::ID] = [ + SearchByOperatorEqualDirective::class, + ]; + $config->searchBy->operators[Operators::Extra] = [ + SearchByOperatorFieldDirective::class, + ]; + }, + ); }, ], 'V5Compat' => [ diff --git a/packages/graphql/src/SearchBy/Operators.php b/packages/graphql/src/SearchBy/Operators.php index f150274db..207c103de 100644 --- a/packages/graphql/src/SearchBy/Operators.php +++ b/packages/graphql/src/SearchBy/Operators.php @@ -3,11 +3,9 @@ namespace LastDragon_ru\LaraASP\GraphQL\SearchBy; use GraphQL\Type\Definition\Type; -use LastDragon_ru\LaraASP\Core\Application\ConfigResolver; use LastDragon_ru\LaraASP\Core\Application\ContainerResolver; -use LastDragon_ru\LaraASP\GraphQL\Builder\Contracts\Operator as BuilderOperator; use LastDragon_ru\LaraASP\GraphQL\Builder\Operators as BuilderOperators; -use LastDragon_ru\LaraASP\GraphQL\Package; +use LastDragon_ru\LaraASP\GraphQL\PackageConfig; use LastDragon_ru\LaraASP\GraphQL\SearchBy\Contracts\Scope; use LastDragon_ru\LaraASP\GraphQL\SearchBy\Definitions\SearchByOperatorAllOfDirective; use LastDragon_ru\LaraASP\GraphQL\SearchBy\Definitions\SearchByOperatorAnyOfDirective; @@ -169,13 +167,9 @@ class Operators extends BuilderOperators { public function __construct( ContainerResolver $container, - protected readonly ConfigResolver $config, + protected readonly PackageConfig $config, ) { - /** @var array|string>> $operators */ - $operators = (array) $this->config->getInstance() - ->get(Package::Name.'.search_by.operators'); - - parent::__construct($container, $operators); + parent::__construct($container, $this->config->getInstance()->searchBy->operators); } #[Override] diff --git a/packages/graphql/src/SearchBy/OperatorsTest.php b/packages/graphql/src/SearchBy/OperatorsTest.php index 30792cdb7..5cb72c7c3 100644 --- a/packages/graphql/src/SearchBy/OperatorsTest.php +++ b/packages/graphql/src/SearchBy/OperatorsTest.php @@ -9,7 +9,8 @@ use LastDragon_ru\LaraASP\GraphQL\Builder\Contracts\TypeSource; use LastDragon_ru\LaraASP\GraphQL\Builder\Field; use LastDragon_ru\LaraASP\GraphQL\Builder\Manipulator; -use LastDragon_ru\LaraASP\GraphQL\Package; +use LastDragon_ru\LaraASP\GraphQL\Config\Config; +use LastDragon_ru\LaraASP\GraphQL\PackageConfig; use LastDragon_ru\LaraASP\GraphQL\SearchBy\Operators\Operator; use LastDragon_ru\LaraASP\GraphQL\Testing\Package\TestCase; use Mockery; @@ -26,16 +27,19 @@ final class OperatorsTest extends TestCase { // // ========================================================================= public function testConstructor(): void { - $this->setConfig([ - Package::Name.'.search_by.operators' => [ - Operators::ID => [ - OperatorsTest__Operator::class, - ], - Operators::Int => [ - OperatorsTest__Operator::class, - ], - ], - ]); + $this->setConfiguration( + PackageConfig::class, + static function (Config $config): void { + $config->searchBy->operators = [ + Operators::ID => [ + OperatorsTest__Operator::class, + ], + Operators::Int => [ + OperatorsTest__Operator::class, + ], + ]; + }, + ); $source = Mockery::mock(TypeSource::class); $context = Mockery::mock(Context::class); diff --git a/packages/graphql/src/SortBy/Config.php b/packages/graphql/src/SortBy/Config.php new file mode 100644 index 000000000..05dc7a09f --- /dev/null +++ b/packages/graphql/src/SortBy/Config.php @@ -0,0 +1,34 @@ +>> + */ + public array $operators = [], + /** + * Determines how the `NULL` values should be treatment. By default, + * there is no any processing, so the order of `NULL` depends on the + * database. It may be set for all (if single value) or for each + * direction (if array). Not all databases/builders may be supported. + * Please check the documentation for more details. + * + * @var Nulls|non-empty-array, Nulls>|null + */ + public Nulls|array|null $nulls = null, + ) { + parent::__construct(); + } +} diff --git a/packages/graphql/src/SortBy/Directives/DirectiveTest.php b/packages/graphql/src/SortBy/Directives/DirectiveTest.php index 8d1a22b1a..16db318ae 100644 --- a/packages/graphql/src/SortBy/Directives/DirectiveTest.php +++ b/packages/graphql/src/SortBy/Directives/DirectiveTest.php @@ -19,7 +19,8 @@ use LastDragon_ru\LaraASP\GraphQL\Builder\Exceptions\TypeDefinitionImpossibleToCreateType; use LastDragon_ru\LaraASP\GraphQL\Builder\Field; use LastDragon_ru\LaraASP\GraphQL\Builder\Property; -use LastDragon_ru\LaraASP\GraphQL\Package; +use LastDragon_ru\LaraASP\GraphQL\Config\Config; +use LastDragon_ru\LaraASP\GraphQL\PackageConfig; use LastDragon_ru\LaraASP\GraphQL\SortBy\Contracts\Ignored; use LastDragon_ru\LaraASP\GraphQL\SortBy\Definitions\SortByOperatorFieldDirective; use LastDragon_ru\LaraASP\GraphQL\SortBy\Definitions\SortByOperatorRandomDirective; @@ -103,14 +104,14 @@ public function testManipulateArgDefinition(string $expected, string $graphql, ? #[RequiresLaravelScout] public function testManipulateArgDefinitionScoutBuilder(): void { - $this->setConfig([ - Package::Name.'.sort_by.operators' => [ + $this->setConfiguration(PackageConfig::class, static function (Config $config): void { + $config->sortBy->operators = [ Operators::Extra => [ Operators::Extra, SortByOperatorRandomDirective::class, ], - ], - ]); + ]; + }); $this->app()->make(DirectiveLocator::class) ->setResolved('search', SearchDirective::class); @@ -146,13 +147,13 @@ public function testManipulateArgDefinitionScoutBuilderV5Compat(): void { } public function testManipulateArgDefinitionTypeRegistryEmpty(): void { - $this->setConfig([ - Package::Name.'.sort_by.operators' => [ + $this->setConfiguration(PackageConfig::class, static function (Config $config): void { + $config->sortBy->operators = [ Operators::Extra => [ SortByOperatorFieldDirective::class, ], - ], - ]); + ]; + }); $type = new ObjectType([ 'name' => 'TestType', @@ -626,26 +627,29 @@ public static function definition(): string { $locator->setResolved('allowed', $allowed::class); $locator->setResolved('forbidden', $forbidden::class); - $test->setConfig([ - Package::Name.'.builder.allowed_directives' => [ - RenameDirective::class, - $allowed::class, - ], - Package::Name.'.sort_by.operators.'.Operators::Extra => [ - SortByOperatorFieldDirective::class, - ], - ]); + $test->setConfiguration( + PackageConfig::class, + static function (Config $config) use ($allowed): void { + $config->builder->allowedDirectives = [ + RenameDirective::class, + $allowed::class, + ]; + $config->sortBy->operators[Operators::Extra] = [ + SortByOperatorFieldDirective::class, + ]; + }, + ); }, ], 'Ignored' => [ 'Ignored.expected.graphql', 'Ignored.schema.graphql', static function (TestCase $test): void { - $test->setConfig([ - Package::Name.'.sort_by.operators.'.Operators::Extra => [ + $test->setConfiguration(PackageConfig::class, static function (Config $config): void { + $config->sortBy->operators[Operators::Extra] = [ SortByOperatorFieldDirective::class, - ], - ]); + ]; + }); $test->app()->make(TypeRegistry::class)->register( new class([ @@ -666,11 +670,11 @@ static function (TestCase $test): void { 'InterfaceUpdate.expected.graphql', 'InterfaceUpdate.schema.graphql', static function (TestCase $test): void { - $test->setConfig([ - Package::Name.'.sort_by.operators.'.Operators::Extra => [ + $test->setConfiguration(PackageConfig::class, static function (Config $config): void { + $config->sortBy->operators[Operators::Extra] = [ SortByOperatorFieldDirective::class, - ], - ]); + ]; + }); }, ], 'TypeRegistry' => [ @@ -881,16 +885,14 @@ public static function dataProviderHandleBuilder(): array { ], ], static function (TestCase $test): void { - $package = Package::Name; - - $test->setConfig([ - "{$package}.sort_by.operators" => [ + $test->setConfiguration(PackageConfig::class, static function (Config $config): void { + $config->sortBy->operators = [ Operators::Extra => [ SortByOperatorFieldDirective::class, SortByOperatorRandomDirective::class, ], - ], - ]); + ]; + }); }, ], 'nulls ordering' => [ @@ -920,14 +922,12 @@ static function (TestCase $test): void { ], ], static function (TestCase $test): void { - $package = Package::Name; - - $test->setConfig([ - "{$package}.sort_by.nulls" => [ + $test->setConfiguration(PackageConfig::class, static function (Config $config): void { + $config->sortBy->nulls = [ Direction::Asc->value => Nulls::Last, Direction::Desc->value => Nulls::First, - ], - ]); + ]; + }); }, ], 'nullsFirst' => [ @@ -1084,16 +1084,14 @@ public static function dataProviderHandleBuilderV5Compat(): array { ], ], static function (TestCase $test): void { - $package = Package::Name; - - $test->setConfig([ - "{$package}.sort_by.operators" => [ + $test->setConfiguration(PackageConfig::class, static function (Config $config): void { + $config->sortBy->operators = [ Operators::Extra => [ SortByOperatorFieldDirective::class, SortByOperatorRandomDirective::class, ], - ], - ]); + ]; + }); }, ], 'nulls ordering' => [ @@ -1119,14 +1117,12 @@ static function (TestCase $test): void { ], ], static function (TestCase $test): void { - $package = Package::Name; - - $test->setConfig([ - "{$package}.sort_by.nulls" => [ + $test->setConfiguration(PackageConfig::class, static function (Config $config): void { + $config->sortBy->nulls = [ Direction::Asc->value => Nulls::Last, Direction::Desc->value => Nulls::First, - ], - ]); + ]; + }); }, ], 'nullsFirst' => [ diff --git a/packages/graphql/src/SortBy/Operators.php b/packages/graphql/src/SortBy/Operators.php index 14ca53498..94c561b71 100644 --- a/packages/graphql/src/SortBy/Operators.php +++ b/packages/graphql/src/SortBy/Operators.php @@ -2,11 +2,9 @@ namespace LastDragon_ru\LaraASP\GraphQL\SortBy; -use LastDragon_ru\LaraASP\Core\Application\ConfigResolver; use LastDragon_ru\LaraASP\Core\Application\ContainerResolver; -use LastDragon_ru\LaraASP\GraphQL\Builder\Contracts\Operator as BuilderOperator; use LastDragon_ru\LaraASP\GraphQL\Builder\Operators as BuilderOperators; -use LastDragon_ru\LaraASP\GraphQL\Package; +use LastDragon_ru\LaraASP\GraphQL\PackageConfig; use LastDragon_ru\LaraASP\GraphQL\SortBy\Contracts\Scope; use LastDragon_ru\LaraASP\GraphQL\SortBy\Definitions\SortByOperatorChildDirective; use LastDragon_ru\LaraASP\GraphQL\SortBy\Definitions\SortByOperatorFieldDirective; @@ -40,13 +38,9 @@ class Operators extends BuilderOperators { public function __construct( ContainerResolver $container, - protected readonly ConfigResolver $config, + protected readonly PackageConfig $config, ) { - /** @var array|string>> $operators */ - $operators = (array) $this->config->getInstance() - ->get(Package::Name.'.sort_by.operators'); - - parent::__construct($container, $operators); + parent::__construct($container, $this->config->getInstance()->sortBy->operators); } #[Override] diff --git a/packages/graphql/src/SortBy/Operators/Sort.php b/packages/graphql/src/SortBy/Operators/Sort.php index ceb8b259a..6ccc957bb 100644 --- a/packages/graphql/src/SortBy/Operators/Sort.php +++ b/packages/graphql/src/SortBy/Operators/Sort.php @@ -2,7 +2,6 @@ namespace LastDragon_ru\LaraASP\GraphQL\SortBy\Operators; -use LastDragon_ru\LaraASP\Core\Application\ConfigResolver; use LastDragon_ru\LaraASP\GraphQL\Builder\Contracts\BuilderFieldResolver; use LastDragon_ru\LaraASP\GraphQL\Builder\Contracts\Context; use LastDragon_ru\LaraASP\GraphQL\Builder\Contracts\Handler; @@ -10,7 +9,7 @@ use LastDragon_ru\LaraASP\GraphQL\Builder\Contracts\TypeSource; use LastDragon_ru\LaraASP\GraphQL\Builder\Exceptions\OperatorUnsupportedBuilder; use LastDragon_ru\LaraASP\GraphQL\Builder\Field; -use LastDragon_ru\LaraASP\GraphQL\Package; +use LastDragon_ru\LaraASP\GraphQL\PackageConfig; use LastDragon_ru\LaraASP\GraphQL\SortBy\Contracts\Sorter; use LastDragon_ru\LaraASP\GraphQL\SortBy\Contracts\SorterFactory; use LastDragon_ru\LaraASP\GraphQL\SortBy\Enums\Direction; @@ -26,7 +25,7 @@ class Sort extends Operator { * @param SorterFactory $factory */ public function __construct( - protected readonly ConfigResolver $config, + protected readonly PackageConfig $config, protected readonly SorterFactory $factory, BuilderFieldResolver $resolver, ) { @@ -107,7 +106,7 @@ protected function getNulls(Sorter $sorter, Context $context, Direction $directi // Default $nulls = null; - $config = $this->config->getInstance()->get(Package::Name.'.sort_by.nulls'); + $config = $this->config->getInstance()->sortBy->nulls; $direction = match ($direction) { Direction::asc => Direction::Asc, Direction::desc => Direction::Desc, diff --git a/packages/graphql/src/SortBy/Operators/SortTest.php b/packages/graphql/src/SortBy/Operators/SortTest.php index b270096b8..24c7a1cd8 100644 --- a/packages/graphql/src/SortBy/Operators/SortTest.php +++ b/packages/graphql/src/SortBy/Operators/SortTest.php @@ -7,11 +7,12 @@ use Illuminate\Database\Eloquent\Builder as EloquentBuilder; use Illuminate\Database\Query\Builder as QueryBuilder; use Laravel\Scout\Builder as ScoutBuilder; -use LastDragon_ru\LaraASP\Core\Application\ConfigResolver; use LastDragon_ru\LaraASP\GraphQL\Builder\Context; use LastDragon_ru\LaraASP\GraphQL\Builder\Contracts\BuilderFieldResolver; use LastDragon_ru\LaraASP\GraphQL\Builder\Field; -use LastDragon_ru\LaraASP\GraphQL\Package; +use LastDragon_ru\LaraASP\GraphQL\Config\Config; +use LastDragon_ru\LaraASP\GraphQL\PackageConfig; +use LastDragon_ru\LaraASP\GraphQL\SortBy\Config as SortByConfig; use LastDragon_ru\LaraASP\GraphQL\SortBy\Contracts\Sorter; use LastDragon_ru\LaraASP\GraphQL\SortBy\Contracts\SorterFactory; use LastDragon_ru\LaraASP\GraphQL\SortBy\Directives\Directive; @@ -156,23 +157,26 @@ public function testCallScoutBuilder(): void { } /** - * @param array $config * @param Closure(static): Sorter $sorterFactory * @param Closure(static): Context $contextFactory */ #[DataProvider('dataProviderGetNulls')] public function testGetNulls( ?Nulls $expected, - ?array $config, + ?SortByConfig $configuration, Closure $sorterFactory, Closure $contextFactory, Direction $direction, ): void { - $this->setConfig($config); + if ($configuration !== null) { + $this->setConfiguration(PackageConfig::class, static function (Config $config) use ($configuration): void { + $config->sortBy = $configuration; + }); + } $sorter = $sorterFactory($this); $context = $contextFactory($this); - $config = $this->app()->make(ConfigResolver::class); + $config = $this->app()->make(PackageConfig::class); $factory = Mockery::mock(SorterFactory::class); $resolver = Mockery::mock(BuilderFieldResolver::class); $operator = Mockery::mock(Sort::class, [$config, $factory, $resolver]); @@ -261,14 +265,13 @@ static function (object $builder, Field $field): string { /** * @return array, + * ?SortByConfig, * Closure(static): Sorter, * Closure(static): Context, * Direction, * }> */ public static function dataProviderGetNulls(): array { - $key = Package::Name.'.sort_by.nulls'; $contextFactory = static function (): Context { return new Context(); }; @@ -309,78 +312,62 @@ public function sort( ], 'nulls are not sortable' => [ null, - [ - $key => Nulls::First, - ], + new SortByConfig(nulls: Nulls::First), $getSorterFactory(false), $contextFactory, Direction::Asc, ], 'nulls are sortable (asc)' => [ Nulls::Last, - [ - $key => Nulls::Last, - ], + new SortByConfig(nulls: Nulls::Last), $getSorterFactory(true), $contextFactory, Direction::Asc, ], 'nulls are sortable (desc)' => [ Nulls::Last, - [ - $key => Nulls::Last, - ], + new SortByConfig(nulls: Nulls::Last), $getSorterFactory(true), $contextFactory, Direction::Desc, ], 'nulls are sortable (separate)' => [ Nulls::First, - [ - $key => [ - Direction::Asc->value => Nulls::Last, - Direction::Desc->value => Nulls::First, - ], - ], + new SortByConfig(nulls: [ + Direction::Asc->value => Nulls::Last, + Direction::Desc->value => Nulls::First, + ]), $getSorterFactory(true), $contextFactory, Direction::Desc, ], '(deprecated) nulls are sortable (asc)' => [ Nulls::Last, - [ - $key => Nulls::Last, - ], + new SortByConfig(nulls: Nulls::Last), $getSorterFactory(true), $contextFactory, Direction::asc, ], '(deprecated) nulls are sortable (desc)' => [ Nulls::Last, - [ - $key => Nulls::Last, - ], + new SortByConfig(nulls: Nulls::Last), $getSorterFactory(true), $contextFactory, Direction::desc, ], '(deprecated) nulls are sortable (separate)' => [ Nulls::First, - [ - $key => [ - Direction::Asc->value => Nulls::Last, - Direction::Desc->value => Nulls::First, - ], - ], + new SortByConfig(nulls: [ + Direction::Asc->value => Nulls::Last, + Direction::Desc->value => Nulls::First, + ]), $getSorterFactory(true), $contextFactory, Direction::desc, ], 'nulls are sortable (Context null)' => [ null, - [ - $key => Nulls::Last, - ], + new SortByConfig(nulls: Nulls::Last), $getSorterFactory(true), static function (): Context { return (new Context())->override([ @@ -391,9 +378,7 @@ static function (): Context { ], 'nulls are sortable (Context first)' => [ Nulls::First, - [ - $key => Nulls::Last, - ], + new SortByConfig(nulls: Nulls::Last), $getSorterFactory(true), static function (): Context { return (new Context())->override([ diff --git a/packages/graphql/src/SortBy/OperatorsTest.php b/packages/graphql/src/SortBy/OperatorsTest.php index 4669ef625..4ab47ee15 100644 --- a/packages/graphql/src/SortBy/OperatorsTest.php +++ b/packages/graphql/src/SortBy/OperatorsTest.php @@ -9,7 +9,8 @@ use LastDragon_ru\LaraASP\GraphQL\Builder\Contracts\TypeSource; use LastDragon_ru\LaraASP\GraphQL\Builder\Field; use LastDragon_ru\LaraASP\GraphQL\Builder\Manipulator; -use LastDragon_ru\LaraASP\GraphQL\Package; +use LastDragon_ru\LaraASP\GraphQL\Config\Config; +use LastDragon_ru\LaraASP\GraphQL\PackageConfig; use LastDragon_ru\LaraASP\GraphQL\SortBy\Operators\Operator; use LastDragon_ru\LaraASP\GraphQL\Testing\Package\TestCase; use Mockery; @@ -26,13 +27,13 @@ final class OperatorsTest extends TestCase { // // ========================================================================= public function testConstructor(): void { - $this->setConfig([ - Package::Name.'.sort_by.operators' => [ + $this->setConfiguration(PackageConfig::class, static function (Config $config): void { + $config->sortBy->operators = [ Operators::Extra => [ OperatorsTest__Operator::class, ], - ], - ]); + ]; + }); $source = Mockery::mock(TypeSource::class); $context = Mockery::mock(Context::class); diff --git a/packages/graphql/src/Stream/Config.php b/packages/graphql/src/Stream/Config.php new file mode 100644 index 000000000..98cb3086c --- /dev/null +++ b/packages/graphql/src/Stream/Config.php @@ -0,0 +1,20 @@ + + */ + public int $default = 25, + /** + * @var int<1, max> + */ + public int $max = 100, + ) { + parent::__construct(); + } +} diff --git a/packages/graphql/src/Stream/Config/Offset.php b/packages/graphql/src/Stream/Config/Offset.php new file mode 100644 index 000000000..f3c2228cb --- /dev/null +++ b/packages/graphql/src/Stream/Config/Offset.php @@ -0,0 +1,16 @@ +config->getInstance(); $manipulator = $this->manipulatorFactory->create($documentAST); $source = $this->getFieldSource($manipulator, $parentType, $fieldDefinition); - $prefix = self::Settings; + $config = $this->config->getInstance(); // Updated? if ($this->streamType->is($source->getTypeName())) { @@ -223,35 +222,27 @@ public function manipulateFieldDefinition( $this->detector->getFieldBuilderInfo($documentAST, $parentType, $fieldDefinition); // Searchable? - $searchable = Cast::toBool( - $this->directiveArgValue(self::ArgSearchable) - ?? $repository->get("{$prefix}.search.enabled") - ?? false, - ); + $searchable = Cast::toBool($this->directiveArgValue(self::ArgSearchable) ?? $config->stream->search->enabled); if ($searchable) { $this->addArgument( $manipulator, $source, SearchByDirective::class, - Cast::toString($repository->get("{$prefix}.search.name") ?? 'where'), + $config->stream->search->name, $manipulator::Placeholder, ); } // Sortable? - $sortable = Cast::toBool( - $this->directiveArgValue(self::ArgSortable) - ?? $repository->get("{$prefix}.sort.enabled") - ?? false, - ); + $sortable = Cast::toBool($this->directiveArgValue(self::ArgSortable) ?? $config->stream->sort->enabled); if ($sortable) { $this->addArgument( $manipulator, $source, SortByDirective::class, - Cast::toString($repository->get("{$prefix}.sort.name") ?? 'order'), + $config->stream->sort->name, $manipulator::Placeholder, ); } @@ -261,7 +252,7 @@ public function manipulateFieldDefinition( $manipulator, $source, StreamLimitDirective::class, - StreamLimitDirective::settings()['name'], + $config->stream->limit->name, $manipulator::Placeholder, null, $this->directiveArgValue(self::ArgLimit) !== null @@ -274,7 +265,7 @@ public function manipulateFieldDefinition( $manipulator, $source, StreamOffsetDirective::class, - StreamOffsetDirective::settings()['name'], + $config->stream->offset->name, $manipulator::Placeholder, ); diff --git a/packages/graphql/src/Stream/Directives/DirectiveTest.php b/packages/graphql/src/Stream/Directives/DirectiveTest.php index 102e9038c..ebeffc0cc 100644 --- a/packages/graphql/src/Stream/Directives/DirectiveTest.php +++ b/packages/graphql/src/Stream/Directives/DirectiveTest.php @@ -15,7 +15,6 @@ use Illuminate\Database\Eloquent\Model as EloquentModel; use Illuminate\Database\Query\Builder as QueryBuilder; use Laravel\Scout\Builder as ScoutBuilder; -use LastDragon_ru\LaraASP\Core\Application\ConfigResolver; use LastDragon_ru\LaraASP\GraphQL\Builder\BuilderInfo; use LastDragon_ru\LaraASP\GraphQL\Builder\BuilderInfoDetector; use LastDragon_ru\LaraASP\GraphQL\Builder\Exceptions\BuilderUnknown; @@ -23,8 +22,9 @@ use LastDragon_ru\LaraASP\GraphQL\Builder\Sources\InterfaceFieldSource; use LastDragon_ru\LaraASP\GraphQL\Builder\Sources\ObjectFieldSource; use LastDragon_ru\LaraASP\GraphQL\Builder\Sources\ObjectSource; +use LastDragon_ru\LaraASP\GraphQL\Config\Config; use LastDragon_ru\LaraASP\GraphQL\Exceptions\ArgumentAlreadyDefined; -use LastDragon_ru\LaraASP\GraphQL\Package; +use LastDragon_ru\LaraASP\GraphQL\PackageConfig; use LastDragon_ru\LaraASP\GraphQL\SearchBy\Definitions\SearchByDirective; use LastDragon_ru\LaraASP\GraphQL\SearchBy\Definitions\SearchByOperatorEqualDirective; use LastDragon_ru\LaraASP\GraphQL\SearchBy\Operators; @@ -282,14 +282,16 @@ public function testDirectiveScout( } public function testManipulateFieldDefinition(): void { - $this->setConfig([ - 'lighthouse.namespaces.models' => [ - (new ReflectionClass(TestObject::class))->getNamespaceName(), - ], - Package::Name.'.search_by.operators' => [ + $this->setConfiguration(PackageConfig::class, static function (Config $config): void { + $config->searchBy->operators = [ Operators::ID => [ SearchByOperatorEqualDirective::class, ], + ]; + }); + $this->setConfig([ + 'lighthouse.namespaces.models' => [ + (new ReflectionClass(TestObject::class))->getNamespaceName(), ], ]); @@ -302,14 +304,16 @@ public function testManipulateFieldDefinition(): void { #[RequiresLaravelScout] public function testManipulateFieldDefinitionScoutBuilder(): void { - $this->setConfig([ - 'lighthouse.namespaces.models' => [ - (new ReflectionClass(TestObject::class))->getNamespaceName(), - ], - Package::Name.'.search_by.operators' => [ + $this->setConfiguration(PackageConfig::class, static function (Config $config): void { + $config->searchBy->operators = [ Operators::ID => [ SearchByOperatorEqualDirective::class, ], + ]; + }); + $this->setConfig([ + 'lighthouse.namespaces.models' => [ + (new ReflectionClass(TestObject::class))->getNamespaceName(), ], ]); @@ -333,7 +337,7 @@ public function testManipulateFieldDefinitionBuilderUnknown(): void { $directive ->shouldUseProperty('config') ->value( - $this->app()->make(ConfigResolver::class), + $this->app()->make(PackageConfig::class), ); $directive ->shouldUseProperty('detector') diff --git a/packages/graphql/src/Stream/Directives/Limit.php b/packages/graphql/src/Stream/Directives/Limit.php index cfab140c7..6ab57e5c9 100644 --- a/packages/graphql/src/Stream/Directives/Limit.php +++ b/packages/graphql/src/Stream/Directives/Limit.php @@ -10,10 +10,9 @@ use GraphQL\Language\Parser; use GraphQL\Type\Definition\Type; use GraphQL\Utils\AST; -use Illuminate\Container\Container; -use Illuminate\Contracts\Config\Repository; use LastDragon_ru\LaraASP\Core\Utils\Cast; use LastDragon_ru\LaraASP\GraphQL\Builder\ManipulatorFactory; +use LastDragon_ru\LaraASP\GraphQL\PackageConfig; use LastDragon_ru\LaraASP\GraphQL\Stream\Contracts\FieldArgumentDirective; use Nuwave\Lighthouse\Execution\ResolveInfo; use Nuwave\Lighthouse\Schema\AST\DocumentAST; @@ -35,25 +34,12 @@ class Limit extends BaseDirective implements ArgManipulator, FieldArgumentDirect final public const ArgMax = 'max'; public function __construct( + protected readonly PackageConfig $config, private readonly ManipulatorFactory $manipulatorFactory, ) { // empty } - /** - * @return array{name: string, default: int, max: int} - */ - final public static function settings(): array { - $repository = Container::getInstance()->make(Repository::class); - $settings = (array) $repository->get(Directive::Settings.'.limit'); - - return [ - 'name' => Cast::toString($settings['name'] ?? 'limit'), - 'default' => Cast::toInt($settings['default'] ?? 25), - 'max' => Cast::toInt($settings['max'] ?? 100), - ]; - } - #[Override] public static function definition(): string { $name = DirectiveLocator::directiveName(static::class); @@ -119,7 +105,8 @@ public function manipulateArgDefinition( * @return int<1, max> */ protected function getArgMax(): int { - $value = Cast::toInt($this->directiveArgValue(self::ArgMax) ?? static::settings()['max']); + $value = $this->directiveArgValue(self::ArgMax) ?? $this->config->getInstance()->stream->limit->max; + $value = Cast::toInt($value); $value = max(1, $value); return $value; @@ -130,7 +117,8 @@ protected function getArgMax(): int { */ protected function getArgDefault(): int { $max = $this->getArgMax(); - $value = Cast::toInt($this->directiveArgValue(self::ArgDefault) ?? static::settings()['default']); + $value = $this->directiveArgValue(self::ArgDefault) ?? $this->config->getInstance()->stream->limit->default; + $value = Cast::toInt($value); $value = min($max, max(1, $value)); return $value; diff --git a/packages/graphql/src/Stream/Directives/Offset.php b/packages/graphql/src/Stream/Directives/Offset.php index 366ef270f..f7af1aa8f 100644 --- a/packages/graphql/src/Stream/Directives/Offset.php +++ b/packages/graphql/src/Stream/Directives/Offset.php @@ -7,8 +7,6 @@ use GraphQL\Language\AST\InterfaceTypeDefinitionNode; use GraphQL\Language\AST\ObjectTypeDefinitionNode; use GraphQL\Language\Parser; -use Illuminate\Container\Container; -use Illuminate\Contracts\Config\Repository; use LastDragon_ru\LaraASP\Core\Utils\Cast; use LastDragon_ru\LaraASP\GraphQL\Builder\Context; use LastDragon_ru\LaraASP\GraphQL\Builder\ManipulatorFactory; @@ -41,18 +39,6 @@ public function __construct( // empty } - /** - * @return array{name: string} - */ - final public static function settings(): array { - $repository = Container::getInstance()->make(Repository::class); - $settings = (array) $repository->get(Directive::Settings.'.offset'); - - return [ - 'name' => Cast::toString($settings['name'] ?? 'offset'), - ]; - } - #[Override] public static function definition(): string { $name = DirectiveLocator::directiveName(static::class); diff --git a/packages/serializer/UPGRADE.md b/packages/serializer/UPGRADE.md index 459ae6655..0b5232fc8 100644 --- a/packages/serializer/UPGRADE.md +++ b/packages/serializer/UPGRADE.md @@ -39,6 +39,8 @@ Please also see [changelog](https://github.com/LastDragon-ru/lara-asp/releases) [//]: # (end: preprocess/9679e76379216855) +* [ ] Package config now uses objects instead of an array, it is recommended to migrate to the new format. 🤝 + # Upgrade from v5 [include:file]: ../../docs/Shared/Upgrade/FromV5.md diff --git a/packages/serializer/defaults/config.php b/packages/serializer/defaults/config.php index d57c78df5..1d4fb550e 100644 --- a/packages/serializer/defaults/config.php +++ b/packages/serializer/defaults/config.php @@ -1,58 +1,9 @@ , - * array - * >, - * normalizers: array< - * class-string, - * array|null - * >, - * context: array, - * } $settings - */ -$settings = [ - /** - * Default format. The `null` means "built-in default" (=json). - */ - 'default' => null, +$config = PackageConfig::getDefaultConfig(); - /** - * Additional encoders and their context options. By default, only - * {@see \Symfony\Component\Serializer\Encoder\JsonEncoder} available. - */ - 'encoders' => [ - // empty - ], +// ... - /** - * Additional normalizers/denormalizers and their context options. The `null` - * value can be used to remove the built-in normalizer/denormalizer. - */ - 'normalizers' => [ - // empty - ], - - /** - * Additional context options. - */ - 'context' => [ - // empty - ], -]; - -return $settings; +return $config; diff --git a/packages/serializer/src/Config/Config.php b/packages/serializer/src/Config/Config.php new file mode 100644 index 000000000..18ce21394 --- /dev/null +++ b/packages/serializer/src/Config/Config.php @@ -0,0 +1,44 @@ +, array> + */ + public array $encoders = [], + /** + * Additional normalizers/denormalizers and their context options. The `null` + * value can be used to remove the built-in normalizer/denormalizer. + * + * @var array, array|null> + */ + public array $normalizers = [], + /** + * Additional context options. + * + * @var array + */ + public array $context = [], + ) { + parent::__construct(); + } +} diff --git a/packages/serializer/src/Factory.php b/packages/serializer/src/Factory.php index 21f4ccf68..d1693d2cf 100644 --- a/packages/serializer/src/Factory.php +++ b/packages/serializer/src/Factory.php @@ -2,7 +2,6 @@ namespace LastDragon_ru\LaraASP\Serializer; -use LastDragon_ru\LaraASP\Core\Application\ConfigResolver; use LastDragon_ru\LaraASP\Core\Application\ContainerResolver; use LastDragon_ru\LaraASP\Serializer\Contracts\Serializer as SerializerContract; use LastDragon_ru\LaraASP\Serializer\Normalizers\DateTimeNormalizer; @@ -38,7 +37,7 @@ class Factory { public function __construct( protected readonly ContainerResolver $container, - protected readonly ConfigResolver $config, + protected readonly PackageConfig $config, ) { // empty } @@ -54,12 +53,12 @@ public function create( array $normalizers = [], array $context = [], ?string $format = null, - ?string $config = Package::Name, ): SerializerContract { - $format = $format ?? $this->getConfigFormat($config) ?? JsonEncoder::FORMAT; - $context = $context + $this->getConfigContext($config); - $encoders = $this->getEncoders($encoders, $context, $config); - $normalizers = $this->getNormalizers($normalizers, $context, $config); + $config = $this->config->getInstance(); + $format ??= $config->default; + $context = $context + $config->context; + $encoders = $this->getEncoders($encoders, $context); + $normalizers = $this->getNormalizers($normalizers, $context); $serializer = $this->make($encoders, $normalizers, $context, $format); return $serializer; @@ -97,35 +96,14 @@ protected function make( ); } - protected function getConfigFormat(?string $config): ?string { - /** @var ?string $format */ - $format = $config !== null && $config !== '' - ? $this->config->getInstance()->get("{$config}.default") - : null; - - return $format; - } - - /** - * @return array - */ - protected function getConfigContext(?string $config): array { - /** @var array $context */ - $context = $config !== null && $config !== '' - ? (array) $this->config->getInstance()->get("{$config}.context") - : []; - - return $context; - } - /** * @param array, array> $encoders * @param array $context * * @return list> */ - protected function getEncoders(array $encoders, array &$context, ?string $config): array { - $groups = [$encoders, $this->getConfigEncoders($config), $this->getDefaultEncoders()]; + protected function getEncoders(array $encoders, array &$context): array { + $groups = [$encoders, $this->config->getInstance()->encoders, $this->getDefaultEncoders()]; $list = []; foreach ($groups as $group) { @@ -141,18 +119,6 @@ protected function getEncoders(array $encoders, array &$context, ?string $config return array_keys($list); } - /** - * @return array, array> - */ - protected function getConfigEncoders(?string $config): array { - /** @var array, array> $encoders */ - $encoders = $config !== null && $config !== '' - ? (array) $this->config->getInstance()->get("{$config}.encoders") - : []; - - return $encoders; - } - /** * @return array, array> */ @@ -180,8 +146,8 @@ protected function getDefaultEncoders(): array { * * @return list> */ - protected function getNormalizers(array $normalizers, array &$context, ?string $config): array { - $groups = [$normalizers, $this->getConfigNormalizers($config), $this->getDefaultNormalizers()]; + protected function getNormalizers(array $normalizers, array &$context): array { + $groups = [$normalizers, $this->config->getInstance()->normalizers, $this->getDefaultNormalizers()]; $list = []; foreach ($groups as $group) { @@ -203,18 +169,6 @@ protected function getNormalizers(array $normalizers, array &$context, ?string $ return array_keys(array_filter($list)); } - /** - * @return array, array|null> - */ - protected function getConfigNormalizers(?string $config): array { - /** @var array, array|null> $normalizers */ - $normalizers = $config !== null && $config !== '' - ? (array) $this->config->getInstance()->get("{$config}.normalizers") - : $config; - - return $normalizers; - } - /** * @return array, array> */ diff --git a/packages/serializer/src/FactoryTest.php b/packages/serializer/src/FactoryTest.php index c51947264..a7e30a3a9 100644 --- a/packages/serializer/src/FactoryTest.php +++ b/packages/serializer/src/FactoryTest.php @@ -2,8 +2,8 @@ namespace LastDragon_ru\LaraASP\Serializer; -use LastDragon_ru\LaraASP\Core\Application\ConfigResolver; use LastDragon_ru\LaraASP\Core\Application\ContainerResolver; +use LastDragon_ru\LaraASP\Serializer\Config\Config; use LastDragon_ru\LaraASP\Serializer\Contracts\Serializer; use LastDragon_ru\LaraASP\Serializer\Normalizers\DateTimeNormalizer; use LastDragon_ru\LaraASP\Serializer\Normalizers\SerializableNormalizer; @@ -26,34 +26,32 @@ #[CoversClass(Factory::class)] final class FactoryTest extends TestCase { public function testCreate(): void { - $this->setConfig([ - Package::Name => [ - 'default' => 'format from config', - 'encoders' => [ - XmlEncoder::class => [], - JsonEncoder::class => [ - 'context option from config' => 'encoder', - 'encoder option from config' => 'encoder', - 'encoder option' => 'encoder', - ], - ], - 'normalizers' => [ - SerializableNormalizer::class => null, - DateTimeNormalizer::class => [ - 'context option from config' => 'normalizer', - 'normalizer option from config' => 'normalizer', - 'normalizer option' => 'normalizer', - ], - DataUriNormalizer::class => [], + $this->setConfiguration(PackageConfig::class, static function (Config $config): void { + $config->default = 'format from config'; + $config->encoders = [ + XmlEncoder::class => [], + JsonEncoder::class => [ + 'context option from config' => 'encoder', + 'encoder option from config' => 'encoder', + 'encoder option' => 'encoder', ], - 'context' => [ - 'context option from config' => 'context', - 'context option' => 'context', + ]; + $config->normalizers = [ + SerializableNormalizer::class => null, + DateTimeNormalizer::class => [ + 'context option from config' => 'normalizer', + 'normalizer option from config' => 'normalizer', + 'normalizer option' => 'normalizer', ], - ], - ]); + DataUriNormalizer::class => [], + ]; + $config->context = [ + 'context option from config' => 'context', + 'context option' => 'context', + ]; + }); - $config = $this->app()->make(ConfigResolver::class); + $config = $this->app()->make(PackageConfig::class); $container = $this->app()->make(ContainerResolver::class); $factory = Mockery::mock(Factory::class, [$container, $config]) ->shouldAllowMockingProtectedMethods() diff --git a/packages/serializer/src/PackageConfig.php b/packages/serializer/src/PackageConfig.php new file mode 100644 index 000000000..86ea6911e --- /dev/null +++ b/packages/serializer/src/PackageConfig.php @@ -0,0 +1,22 @@ + + */ +class PackageConfig extends ConfigurationResolver { + #[Override] + protected static function getName(): string { + return Package::Name; + } + + #[Override] + public static function getDefaultConfig(): Config { + return new Config(); + } +} diff --git a/packages/serializer/src/Provider.php b/packages/serializer/src/Provider.php index 8f67ba1a0..24fb4540e 100644 --- a/packages/serializer/src/Provider.php +++ b/packages/serializer/src/Provider.php @@ -15,15 +15,12 @@ class Provider extends ServiceProvider { public function register(): void { parent::register(); + $this->registerConfig(PackageConfig::class); $this->app->scopedIf(Serializer::class, static function (Container $container): Serializer { return $container->make(Factory::class)->create(); }); } - public function boot(): void { - $this->bootConfig(); - } - #[Override] protected function getName(): string { return Package::Name; diff --git a/packages/serializer/src/ProviderTest.php b/packages/serializer/src/ProviderTest.php index 8184c7319..36842a039 100644 --- a/packages/serializer/src/ProviderTest.php +++ b/packages/serializer/src/ProviderTest.php @@ -4,8 +4,10 @@ use DateTimeInterface; use Exception; +use Illuminate\Contracts\Config\Repository; use Illuminate\Support\Carbon; use JsonSerializable; +use LastDragon_ru\LaraASP\Serializer\Config\Config; use LastDragon_ru\LaraASP\Serializer\Contracts\Partial; use LastDragon_ru\LaraASP\Serializer\Contracts\Serializable; use LastDragon_ru\LaraASP\Serializer\Contracts\Serializer as SerializerContract; @@ -20,10 +22,13 @@ use Stringable; use Symfony\Component\Serializer\Attribute\DiscriminatorMap; use Symfony\Component\Serializer\Attribute\SerializedName; +use Symfony\Component\Serializer\Encoder\JsonDecode; +use Symfony\Component\Serializer\Encoder\XmlEncoder; use Symfony\Component\Serializer\Exception\CircularReferenceException; use Symfony\Component\Serializer\Exception\ExtraAttributesException; use Symfony\Component\Serializer\Exception\MissingConstructorArgumentsException; use Symfony\Component\Serializer\Exception\NotNormalizableValueException; +use Symfony\Component\Serializer\Normalizer\ObjectNormalizer; use function get_debug_type; use function is_string; @@ -93,6 +98,44 @@ public function testDeserialization(Exception|Serializable $expected, string $cl } } } + + public function testConfig(): void { + self::assertConfigurationExportable(PackageConfig::class); + } + + /** + * @deprecated %{VERSION} Array-base config is deprecated. + */ + public function testLegacyConfig(): void { + // Prepare + $app = $this->app(); + $config = $app->make(Repository::class); + $legacy = (array) require self::getTestData()->path('~LegacyConfig.php'); + $package = Package::Name; + + $config->set($package, $legacy); + + self::assertIsArray($config->get($package)); + + (new Provider($app))->register(); + + // Test + $expected = new Config(); + $expected->default = 'xml'; + $expected->encoders = [ + XmlEncoder::class => [ + XmlEncoder::REMOVE_EMPTY_TAGS => false, + ], + ]; + $expected->normalizers = [ + ObjectNormalizer::class => null, + ]; + $expected->context = [ + JsonDecode::ASSOCIATIVE => true, + ]; + + self::assertEquals($expected, $config->get($package)); + } // // diff --git a/packages/serializer/src/ProviderTest~LegacyConfig.php b/packages/serializer/src/ProviderTest~LegacyConfig.php new file mode 100644 index 000000000..bfb0ff34e --- /dev/null +++ b/packages/serializer/src/ProviderTest~LegacyConfig.php @@ -0,0 +1,63 @@ +, + * array + * >, + * normalizers: array< + * class-string, + * array|null + * >, + * context: array, + * } $settings + */ +$settings = [ + /** + * Default format. The `null` means "built-in default" (=json). + */ + 'default' => 'xml', + + /** + * Additional encoders and their context options. By default, only + * {@see \Symfony\Component\Serializer\Encoder\JsonEncoder} available. + */ + 'encoders' => [ + XmlEncoder::class => [ + XmlEncoder::REMOVE_EMPTY_TAGS => false, + ], + ], + + /** + * Additional normalizers/denormalizers and their context options. The `null` + * value can be used to remove the built-in normalizer/denormalizer. + */ + 'normalizers' => [ + ObjectNormalizer::class => null, + ], + + /** + * Additional context options. + */ + 'context' => [ + JsonDecode::ASSOCIATIVE => true, + ], +]; + +return $settings; diff --git a/packages/spa/UPGRADE.md b/packages/spa/UPGRADE.md index 459ae6655..0b5232fc8 100644 --- a/packages/spa/UPGRADE.md +++ b/packages/spa/UPGRADE.md @@ -39,6 +39,8 @@ Please also see [changelog](https://github.com/LastDragon-ru/lara-asp/releases) [//]: # (end: preprocess/9679e76379216855) +* [ ] Package config now uses objects instead of an array, it is recommended to migrate to the new format. 🤝 + # Upgrade from v5 [include:file]: ../../docs/Shared/Upgrade/FromV5.md diff --git a/packages/spa/defaults/config.php b/packages/spa/defaults/config.php index ab413927d..9422637ce 100644 --- a/packages/spa/defaults/config.php +++ b/packages/spa/defaults/config.php @@ -1,30 +1,9 @@ [ - 'enabled' => false, - 'middleware' => 'web', - 'prefix' => null, - ], +$config = PackageConfig::getDefaultConfig(); - /** - * SPA Settings - * --------------------------------------------------------------------- - * You can define settings that should be available for SPA. - */ - 'spa' => [ - // This value has no effect inside the published config. - ConfigMerger::Strict => false, - ], -]; +// ... + +return $config; diff --git a/packages/spa/src/Config/Config.php b/packages/spa/src/Config/Config.php new file mode 100644 index 000000000..ef97c1119 --- /dev/null +++ b/packages/spa/src/Config/Config.php @@ -0,0 +1,24 @@ + + */ + public array $spa = [], + ) { + parent::__construct(); + } +} diff --git a/packages/spa/src/Config/Routes.php b/packages/spa/src/Config/Routes.php new file mode 100644 index 000000000..6a5de2b83 --- /dev/null +++ b/packages/spa/src/Config/Routes.php @@ -0,0 +1,15 @@ + */ protected function getSettings(): array { - $repository = $this->config->getInstance(); - $package = Package::Name; - $default = [ + $config = $this->config->getInstance(); + $default = [ ConfigMerger::Strict => false, - 'title' => $repository->get('app.name'), + 'title' => $config->get('app.name'), 'upload' => [ 'max' => UploadedFile::getMaxFilesize(), ], ]; - $custom = $repository->get("{$package}.spa"); - $settings = (new ConfigMerger())->merge($default, $custom); + $custom = $this->configuration->getInstance()->spa; + $settings = (new ConfigMerger())->merge($default, $custom); return $settings; } diff --git a/packages/spa/src/Http/Controllers/SpaControllerTest.php b/packages/spa/src/Http/Controllers/SpaControllerTest.php index 7a3ad7b17..02e1fc621 100644 --- a/packages/spa/src/Http/Controllers/SpaControllerTest.php +++ b/packages/spa/src/Http/Controllers/SpaControllerTest.php @@ -2,7 +2,8 @@ namespace LastDragon_ru\LaraASP\Spa\Http\Controllers; -use LastDragon_ru\LaraASP\Spa\Package; +use LastDragon_ru\LaraASP\Spa\Config\Config; +use LastDragon_ru\LaraASP\Spa\PackageConfig; use LastDragon_ru\LaraASP\Spa\Provider; use LastDragon_ru\LaraASP\Spa\Testing\Package\TestCase; use LastDragon_ru\LaraASP\Testing\Constraints\Json\JsonSchemaFile; @@ -37,11 +38,14 @@ public function testSettings( array $headers = [], array $settings = [], ): void { - $this->setConfig([ - Package::Name.'.routes.enabled' => $routes, - Package::Name.'.routes.prefix' => $prefix, - Package::Name.'.spa' => $settings, - ]); + $this->setConfiguration( + PackageConfig::class, + static function (Config $config) use ($routes, $prefix, $settings): void { + $config->routes->enabled = $routes; + $config->routes->prefix = $prefix; + $config->spa = $settings; + }, + ); $provider = new class($this->app()) extends Provider { // empty diff --git a/packages/spa/src/PackageConfig.php b/packages/spa/src/PackageConfig.php new file mode 100644 index 000000000..b96c7313f --- /dev/null +++ b/packages/spa/src/PackageConfig.php @@ -0,0 +1,22 @@ + + */ +class PackageConfig extends ConfigurationResolver { + #[Override] + protected static function getName(): string { + return Package::Name; + } + + #[Override] + public static function getDefaultConfig(): Config { + return new Config(); + } +} diff --git a/packages/spa/src/Provider.php b/packages/spa/src/Provider.php index 5123ef09e..549d0c9e6 100644 --- a/packages/spa/src/Provider.php +++ b/packages/spa/src/Provider.php @@ -2,12 +2,10 @@ namespace LastDragon_ru\LaraASP\Spa; -use Illuminate\Contracts\Config\Repository; use Illuminate\Support\ServiceProvider; use LastDragon_ru\LaraASP\Core\Provider\WithConfig; use LastDragon_ru\LaraASP\Core\Provider\WithRoutes; use LastDragon_ru\LaraASP\Core\Provider\WithTranslations; -use LastDragon_ru\LaraASP\Core\Utils\Cast; use Override; class Provider extends ServiceProvider { @@ -17,17 +15,22 @@ class Provider extends ServiceProvider { // // ========================================================================= + #[Override] + public function register(): void { + parent::register(); + + $this->registerConfig(PackageConfig::class); + } + public function boot(): void { - $this->bootConfig(); $this->bootTranslations(); $this->bootRoutes(function (): array { - $package = Package::Name; - $config = (array) $this->app->make(Repository::class)->get("{$package}.routes"); + $config = $this->app->make(PackageConfig::class)->getInstance(); $settings = [ - 'enabled' => Cast::toBool($config['enabled'] ?? false), + 'enabled' => $config->routes->enabled, 'attributes' => [ - 'prefix' => Cast::toString($config['prefix'] ?? ''), - 'middleware' => Cast::toString($config['middleware'] ?? ''), + 'prefix' => $config->routes->prefix, + 'middleware' => $config->routes->middleware, ], ]; diff --git a/packages/spa/src/ProviderTest.php b/packages/spa/src/ProviderTest.php new file mode 100644 index 000000000..bc85b88d8 --- /dev/null +++ b/packages/spa/src/ProviderTest.php @@ -0,0 +1,45 @@ +app(); + $config = $app->make(Repository::class); + $legacy = (array) require self::getTestData()->path('~LegacyConfig.php'); + $package = Package::Name; + + $config->set($package, $legacy); + + self::assertIsArray($config->get($package)); + + (new Provider($app))->register(); + + // Test + $expected = new Config(); + $expected->routes->enabled = true; + $expected->routes->prefix = 'spa_'; + $expected->spa = [ + 'property' => 'value', + ]; + + self::assertEquals($expected, $config->get($package)); + } +} diff --git a/packages/spa/src/ProviderTest~LegacyConfig.php b/packages/spa/src/ProviderTest~LegacyConfig.php new file mode 100644 index 000000000..10e9f6082 --- /dev/null +++ b/packages/spa/src/ProviderTest~LegacyConfig.php @@ -0,0 +1,31 @@ + [ + 'enabled' => true, + 'middleware' => 'web', + 'prefix' => 'spa_', + ], + + /** + * SPA Settings + * --------------------------------------------------------------------- + * You can define settings that should be available for SPA. + */ + 'spa' => [ + // This value has no effect inside the published config. + ConfigMerger::Strict => false, + 'property' => 'value', + ], +]; diff --git a/packages/testing/src/Testing/WithConfig.php b/packages/testing/src/Testing/WithConfig.php index 68df8f850..1e058b6da 100644 --- a/packages/testing/src/Testing/WithConfig.php +++ b/packages/testing/src/Testing/WithConfig.php @@ -3,9 +3,11 @@ namespace LastDragon_ru\LaraASP\Testing\Testing; use Illuminate\Contracts\Config\Repository; -use Illuminate\Contracts\Foundation\Application; +use LastDragon_ru\LaraASP\Core\Application\Configuration\Configuration; +use LastDragon_ru\LaraASP\Core\Application\Configuration\ConfigurationResolver; use function is_callable; +use function var_export; /** * Allows replacing settings for Laravel. @@ -17,10 +19,10 @@ * @phpstan-type Settings array * @phpstan-type SettingsCallback callable(static, Repository): Settings * @phpstan-type SettingsFactory SettingsCallback|Settings|null + * + * @phpstan-require-extends TestCase */ trait WithConfig { - abstract protected function app(): Application; - /** * @param SettingsFactory $settings */ @@ -32,4 +34,30 @@ public function setConfig(callable|array|null $settings): void { $repository->set($settings); } } + + /** + * @template T of Configuration + * + * @param class-string> $resolver + * @param callable(T): void|null $callback + */ + public function setConfiguration(string $resolver, ?callable $callback): void { + if ($callback !== null) { + $callback($this->app()->make($resolver)->getInstance()); + } + } + + /** + * @template T of Configuration + * + * @param class-string> $resolver + */ + public function assertConfigurationExportable(string $resolver): void { + $config = $resolver::getDefaultConfig(); + $exported = var_export($config, true); + $exported = "getPathname(); + + self::assertEquals($config, $imported); + } } diff --git a/phpstan-baseline.neon b/phpstan-baseline.neon index 2086313bf..766abeb7d 100644 --- a/phpstan-baseline.neon +++ b/phpstan-baseline.neon @@ -31,7 +31,7 @@ parameters: path: packages/formatter/src/Formatter.php - - message: "#^Method class@anonymous/packages/graphql/src/Stream/Directives/Directive\\.php\\:480\\:\\:enhanceEloquentBuilder\\(\\) return type with generic class Laravel\\\\Scout\\\\Builder does not specify its types\\: TModel$#" + message: "#^Method class@anonymous/packages/graphql/src/Stream/Directives/Directive\\.php\\:471\\:\\:enhanceEloquentBuilder\\(\\) return type with generic class Laravel\\\\Scout\\\\Builder does not specify its types\\: TModel$#" count: 1 path: packages/graphql/src/Stream/Directives/Directive.php @@ -50,11 +50,6 @@ parameters: count: 3 path: packages/serializer/src/Casts/SerializedAttributeTest.php - - - message: "#^Parameter \\#2 \\.\\.\\.\\$configs of method LastDragon_ru\\\\LaraASP\\\\Core\\\\Utils\\\\ConfigMerger\\:\\:merge\\(\\) expects array, mixed given\\.$#" - count: 1 - path: packages/spa/src/Http/Controllers/SpaController.php - - message: "#^Parameter \\#3 \\$path of method LastDragon_ru\\\\LaraASP\\\\Spa\\\\Http\\\\Resources\\\\Resource\\:\\:mapResourceValue\\(\\) expects array\\, array given\\.$#" count: 1