diff --git a/UPGRADE.md b/UPGRADE.md index 4ae4f1cd..2b483ca6 100644 --- a/UPGRADE.md +++ b/UPGRADE.md @@ -106,6 +106,26 @@ occurs before validation, so it makes more sense for the arguments to be this wa The method signature for the `errors` method has changed. To maintain the old behaviour use `error` instead (it accepts an error collection as well as a single error). +### Schemas + +The JSON API specification recommends using hyphens for member names, e.g. `foo-bar`. The Eloquent schema now uses +this recommendation as its default behaviour. E.g. a model key of `foo_bar` will be serialized as `foo-bar`. + +To maintain the old behaviour (of using the model key for the member key), set the `$hyphenated` property on your +schema to `false`. If you want to implement your own logic, overload the `keyForAttribute` method. + +If you have irregular mappings of model keys to attribute keys, you can define these in your `$attributes` property, +e.g. + +``` php +protected $attributes = [ + 'foo', + 'bar', + 'foo_bar' // will be dasherized to 'foo-bar' by default + 'baz' => 'bat' // model key `baz` will be serialized to attribute key `bat` +]; +``` + ### Sort We've merged the abstract sorted search class into the abstract search class, to simplify things. @@ -130,6 +150,17 @@ class Search extends AbstractSearch } ``` +#### Sort Parameter Field Conversion + +When working out what column to use for a sort parameter, the sort parameter will automatically be snake cased if +your model uses snake attributes. If it does not use snake attributes, the parameter will be camel cased. + +If you have irregular mappings of search params to column names, add that mapping to your `$sortColumns` property, +e.g. `$sortColumns = ['foo-bar' => 'baz_bat']`. + +If you want to use a complete different logic for converting search params to column names, overload the +`columnForField()` method. + ### Validator Providers The classes that provide validators for individual resource types generally extend `AbstractValidatorProvider`. This diff --git a/src/Schema/EloquentSchema.php b/src/Schema/EloquentSchema.php index c0fec8cc..7474ee62 100644 --- a/src/Schema/EloquentSchema.php +++ b/src/Schema/EloquentSchema.php @@ -19,7 +19,8 @@ namespace CloudCreativity\LaravelJsonApi\Schema; use Carbon\Carbon; -use CloudCreativity\JsonApi\Exceptions\SchemaException; +use CloudCreativity\JsonApi\Exceptions\RuntimeException; +use CloudCreativity\LaravelJsonApi\Utils\Str; use DateTime; use Illuminate\Database\Eloquent\Model; @@ -83,6 +84,17 @@ abstract class EloquentSchema extends AbstractSchema */ protected $attributes = []; + /** + * Whether resource member names are hyphenated + * + * The JSON API spec recommends using hyphens for resource member names, so this package + * uses this as the default. If you do not want to follow the recommendation, set this + * to `false`. + * + * @var bool + */ + protected $hyphenated = true; + /** * @param object $resource * @return mixed @@ -90,7 +102,7 @@ abstract class EloquentSchema extends AbstractSchema public function getId($resource) { if (!$resource instanceof Model) { - throw new SchemaException('Expecting an Eloquent model.'); + throw new RuntimeException('Expecting an Eloquent model.'); } $key = $this->idName ?: $resource->getKeyName(); @@ -105,7 +117,7 @@ public function getId($resource) public function getAttributes($resource) { if (!$resource instanceof Model) { - throw new SchemaException('Expecting an Eloquent model to serialize.'); + throw new RuntimeException('Expecting an Eloquent model to serialize.'); } return array_merge( @@ -172,7 +184,7 @@ protected function getModelAttributes(Model $model) */ protected function keyForAttribute($modelKey) { - return $modelKey; + return $this->hyphenated ? Str::dasherize($modelKey) : $modelKey; } /** diff --git a/src/Search/AbstractSearch.php b/src/Search/AbstractSearch.php index 80203673..e6dc37ef 100644 --- a/src/Search/AbstractSearch.php +++ b/src/Search/AbstractSearch.php @@ -23,6 +23,7 @@ use CloudCreativity\JsonApi\Contracts\Pagination\PaginatorInterface; use CloudCreativity\LaravelJsonApi\Contracts\Search\SearchInterface; use CloudCreativity\LaravelJsonApi\Pagination\Page; +use CloudCreativity\LaravelJsonApi\Utils\Str; use Illuminate\Database\Eloquent\Builder; use Illuminate\Database\Eloquent\Collection as EloquentCollection; use Illuminate\Database\Eloquent\Model; @@ -277,7 +278,7 @@ protected function sortBy(Builder $builder, SortParameterInterface $param) */ protected function getQualifiedSortColumn(Builder $builder, $field) { - $key = $this->columnForField($field); + $key = $this->columnForField($field, $builder->getModel()); if (!str_contains('.', $key)) { $key = sprintf('%s.%s', $builder->getModel()->getTable(), $key); @@ -290,11 +291,17 @@ protected function getQualifiedSortColumn(Builder $builder, $field) * Get the table column to use for the specified search field. * * @param string $field + * @param Model $model * @return string */ - protected function columnForField($field) + protected function columnForField($field, Model $model) { - return isset($this->sortColumns[$field]) ? $this->sortColumns[$field] : $field; + /** If there is a custom mapping, return that */ + if (isset($this->sortColumns[$field])) { + return $this->sortColumns[$field]; + } + + return $model::$snakeAttributes ? Str::snake($field) : Str::camel($field); } /** diff --git a/src/Utils/Str.php b/src/Utils/Str.php new file mode 100644 index 00000000..a9d7212c --- /dev/null +++ b/src/Utils/Str.php @@ -0,0 +1,70 @@ + 'foo', + 'foo_bar' => 'foo-bar', + 'fooBar' => 'foo-bar', + ]; + + foreach ($values as $value => $expected) { + $actual = Str::dasherize($value); + $this->assertSame($expected, $actual, "Did not dasherize '$value' correctly"); + } + } + + public function testCamel() + { + $values = [ + 'foo' => 'foo', + 'foo-bar' => 'fooBar', + 'fooBar' => 'fooBar', + 'foo_bar' => 'fooBar', + ]; + + foreach ($values as $value => $expected) { + $actual = Str::camel($value); + $this->assertSame($expected, $actual, "Did not camel case '$value' correctly"); + } + } + + public function testSnake() + { + $values = [ + 'foo' => 'foo', + 'foo-bar' => 'foo_bar', + 'fooBar' => 'foo_bar', + 'foo_bar' => 'foo_bar', + ]; + + foreach ($values as $value => $expected) { + $actual = Str::snake($value); + $this->assertSame($expected, $actual, "Did not snake case '$value' correctly"); + } + } +}