diff --git a/config/data.php b/config/data.php index 441bf6e0..72cd2e4a 100644 --- a/config/data.php +++ b/config/data.php @@ -1,6 +1,7 @@ \Spatie\LaravelData\Support\Creation\ValidationStrategy::OnlyRequests->value, + /** + * The default name mapping strategy for data objects' keys. + * This has to be a class implementing the `Spatie\LaravelData\Mappers\NameMapper` interface. + */ + 'naming_strategy' => [ + 'input' => null, + 'output' => null, + ], + /** * When using an invalid include, exclude, only or except partial, the package will * throw an exception. You can disable this behaviour by setting this option to true. diff --git a/src/Resolvers/NameMappersResolver.php b/src/Resolvers/NameMappersResolver.php index 943726d1..64881ac4 100644 --- a/src/Resolvers/NameMappersResolver.php +++ b/src/Resolvers/NameMappersResolver.php @@ -34,7 +34,8 @@ protected function resolveInputNameMapper( ): ?NameMapper { /** @var \Spatie\LaravelData\Attributes\MapInputName|\Spatie\LaravelData\Attributes\MapName|null $mapper */ $mapper = $attributes->first(fn (object $attribute) => $attribute instanceof MapInputName) - ?? $attributes->first(fn (object $attribute) => $attribute instanceof MapName); + ?? $attributes->first(fn (object $attribute) => $attribute instanceof MapName) + ?? $this->resolveDefaultNameMapper(input: true); if ($mapper) { return $this->resolveMapper($mapper->input); @@ -48,7 +49,8 @@ protected function resolveOutputNameMapper( ): ?NameMapper { /** @var \Spatie\LaravelData\Attributes\MapOutputName|\Spatie\LaravelData\Attributes\MapName|null $mapper */ $mapper = $attributes->first(fn (object $attribute) => $attribute instanceof MapOutputName) - ?? $attributes->first(fn (object $attribute) => $attribute instanceof MapName); + ?? $attributes->first(fn (object $attribute) => $attribute instanceof MapName) + ?? $this->resolveDefaultNameMapper(input: false); if ($mapper) { return $this->resolveMapper($mapper->output); @@ -86,4 +88,18 @@ protected function resolveMapperClass(int|string|NameMapper $value): NameMapper return new ProvidedNameMapper($value); } + + private function resolveDefaultNameMapper(bool $input): null|MapInputName|MapOutputName + { + $nameMapper = $input ? config('data.naming_strategy.input') : config('data.naming_strategy.output'); + + if ($nameMapper === null) { + return null; + } + + return match ($input) { + true => new MapInputName($nameMapper), + false => new MapOutputName($nameMapper), + }; + } } diff --git a/tests/Resolvers/NameMappersResolverTest.php b/tests/Resolvers/NameMappersResolverTest.php index 3fa9c46d..46333909 100644 --- a/tests/Resolvers/NameMappersResolverTest.php +++ b/tests/Resolvers/NameMappersResolverTest.php @@ -5,6 +5,7 @@ use Spatie\LaravelData\Attributes\MapName; use Spatie\LaravelData\Attributes\MapOutputName; use Spatie\LaravelData\Mappers\CamelCaseMapper; +use Spatie\LaravelData\Mappers\StudlyCaseMapper; use Spatie\LaravelData\Mappers\ProvidedNameMapper; use Spatie\LaravelData\Mappers\SnakeCaseMapper; use Spatie\LaravelData\Resolvers\NameMappersResolver; @@ -102,6 +103,50 @@ function getAttributes(object $class): Collection ]); }); +it('can have default mappers', function () { + config()->set('data.naming_strategy.input', CamelCaseMapper::class); + config()->set('data.naming_strategy.output', SnakeCaseMapper::class); + + $attributes = getAttributes(new class () { + public $property; + }); + + expect($this->resolver->execute($attributes))->toMatchArray([ + 'inputNameMapper' => new CamelCaseMapper(), + 'outputNameMapper' => new SnakeCaseMapper(), + ]); +}); + +it('input name mappers only work when no mappers are specified', function () { + config()->set('data.naming_strategy.input', CamelCaseMapper::class); + config()->set('data.naming_strategy.output', SnakeCaseMapper::class); + + $attributes = getAttributes(new class () { + #[MapInputName(StudlyCaseMapper::class)] + public $property; + }); + + expect($this->resolver->execute($attributes))->toMatchArray([ + 'inputNameMapper' => new StudlyCaseMapper(), + 'outputNameMapper' => new SnakeCaseMapper(), + ]); +}); + +it('output name mappers only work when no mappers are specified', function () { + config()->set('data.naming_strategy.input', CamelCaseMapper::class); + config()->set('data.naming_strategy.output', SnakeCaseMapper::class); + + $attributes = getAttributes(new class () { + #[MapOutputName(StudlyCaseMapper::class)] + public $property; + }); + + expect($this->resolver->execute($attributes))->toMatchArray([ + 'inputNameMapper' => new CamelCaseMapper(), + 'outputNameMapper' => new StudlyCaseMapper(), + ]); +}); + it('can ignore certain mapper types', function () { $attributes = getAttributes(new class () { #[MapInputName('input'), MapOutputName(CamelCaseMapper::class)]