Search code examples
laravelapi

Laravel REST API: how to map database column name with another name for parameters?


I have a column named location_relationship_id which is a relationship that belongs to Location model.

In postman, this parameter works: http://localhost/api/v1/traffic-violations?location_relationship_id=25

Results:

 "data": [
        {
            "id": 1,
            "violationType": "Pass traffic light",
            "location": "Al-Morooj",
            "workingShift": "(B) Evening",
        }
    ],

In the json, the naming/mapping is correct. As you can see it's named location rather than location_relationship_id.

How can I replace the url to be something like this: http://localhost/api/v1/traffic-violations?location=Al-Morooj? Or should I leave it as is and not bother?

When I do this I get a column doesn't exist error (obviously).

I am using this package to return the data itsrennyman/laravel-rest-filters

This is my controller:

/**
     * Display a listing of the resource.
     * @return \Illuminate\Http\Response
     */
    public function index(Request $request)
    {
        $violations = TrafficViolation::withRestFilters();
        return new TrafficViolationCollection($violations->paginate());
    }

    /**
     * Display a specified resource
     * @param \App\Models\TrafficViolation $violation
     * @return Illuminate\Http\Response
     */
    public function show($id)
    {
        return new TrafficViolationResource(TrafficViolation::findOrFail($id));
    }

And this is my Resource class:

class TrafficViolationResource extends JsonResource
{
    // Define the mapping configuration for each field
    private const MAPPING = [
        'location_relationship_id' => ['key' => 'location', 'method' => 'mapLocationField'],
        'violation_type_relationship_id' => ['key' => 'violationType', 'method' => 'mapViolationTypeField'],
        'working_shift_relationship_id' => ['key' => 'workingShift', 'method' => 'mapWorkingShiftField']
    ];

    public function toArray($request)
    {
        return $this->transformData(parent::toArray($request));
    }

    /**
     * Transform the data by excluding specified fields and mapping the remaining fields.
     *
     */
    private function transformData(array $data): array
    {
        return collect($data)
            ->except('created_at', 'updated_at')
            ->mapWithKeys(function ($value, $key) {
                // Check if the field has a mapping configuration
                if (array_key_exists($key, self::MAPPING)) {
                    $mapping = self::MAPPING[$key];
                    // Invoke the corresponding mapping method and pass the value and output key
                    return $this->{$mapping['method']}($value, $mapping['key']);
                }
                // If no mapping exists, convert the field to camel case
                return [Str::camel($key) => $value];
            })
            ->toArray();
    }

    /**
     * Map the 'location_relationship_id' field by accessing the relationship and transforming the value.
     */
    private function mapLocationField($value, $key): array
    {
        $name = $this->locationRelationship->name;
        return [$key => $name];
    }

    /**
     * Map the 'violation_type_relationship_id' field by accessing the relationship and transforming the value.
     */
    private function mapViolationTypeField($value, $key): array
    {
        $name = $this->violationTypeRelationship->name;
        return [$key => $name];
    }

    /**
     * Map the 'working_shift_relationship_id' field by accessing the relationship and transforming the value.
     */
    private function mapWorkingShiftField($value, $key): array
    {
        $name = $this->workingShiftRelationship->name;
        return [$key => $name];
    }
}

Solution

  • Sadly, the plugin you are using does not support this out of the box. But good news is that the plugin itself is very simple ( just two classes ), so lets ditch it for now and try implementing it ourself in your application

    So there are two classes, Attribute and RestServiceProvider

    RestServiceProvider.php will mostly also be identical. Let's change its namespace and add option to map fields, like

    // This is where we will map field names
    if (property_exists($model, 'fieldMapping')) {
        $field = $model->fieldMapping[$field] ?? $field;
    }
    

    So app/Providers/RestServiceProvider.php

    <?php
    
    namespace App\Providers;
    
    use App\RestFilters\Helpers\Attribute;
    use Illuminate\Support\Str;
    use Illuminate\Support\ServiceProvider;
    use Illuminate\Database\Eloquent\Builder;
    
    class RestServiceProvider extends ServiceProvider
    {
        /**
         * Register bindings in the container.
         *
         * @return void
         */
        public function register()
        {
            //
        }
    
        /**
         * Bootstrap any application services.
         *
         * @return void
         */
        public function boot()
        {
            Builder::macro('withRestFilters', function () {
                // Model Instance
                $model = $this->getModel();
                // Filtering
                $query = request()->query->all();
    
                // Needs to exclude key items like "sort"
                collect($query)->except(['sort', 'fields', 'embed', 'page'])
                    ->except($model->bannedFields ?: [])
                    ->each(function ($value, $field) use ($model) {
    
                        // This is where we will map field names
                        if (property_exists($model, 'fieldMapping')) {
                            $field = $model->fieldMapping[$field] ?? $field;
                        }
    
                        // Check has Multiple Filters
                        if (Str::contains($value, ',')) {
                            $this->whereIn($field, Str::of($value)->explode(','));
                        } else {
                            // Empty value. Skip.
                            if (empty($value)) {
                                return true;
                            }
    
                            // Check has an extra attribute
                            if (Str::contains($value, ':')) {
                                $attributeAndValue = Str::of($value)->explode(':');
    
                                // Check Banned Attributes
                                if ($model->bannedAttributes && in_array($attributeAndValue[0], $model->bannedAttributes)) {
                                    return true;
                                }
    
                                // Replace Attribute
                                $replacedAttribute = Attribute::sobstitute($attributeAndValue[0]);
    
                                // If not exists returns false, so not considered.
                                if (!$replacedAttribute) {
                                    return true;
                                }
    
                                $this->where($field, $replacedAttribute, $attributeAndValue[1]);
                            } else {
                                $this->where($field, '=', $value);
                            }
                        }
                    });
    
                // Selecting Fields
                if (request()->filled('fields')) {
                    $fields = Str::of(request()->fields)->explode(',');
    
                    $fields->each(function ($field) {
                        $this->addSelect($field);
                    });
                }
    
                // Sorting
                if (request()->filled('sort')) {
                    $fields = Str::of(request()->sort)->explode(',');
    
                    $fields->each(function ($field) {
                        $sortDirection = Str::startsWith($field, '-') ? 'DESC' : 'ASC';
    
                        $this->orderBy(Str::replaceFirst('-', '', $field), $sortDirection);
                    });
                }
    
                // Embedding (Needs to add id field if filtered.)
                if (request()->filled('embed')) {
                    $fields = Str::of(request()->embed)->explode(',');
    
                    $fields->each(function ($field) {
                        $this->with($field);
                    });
                }
    
                return $this;
            });
        }
    }
    
    

    Attribute.php. This can be left unchanged, lets just change the namespace /app/RestFilters/Helpers/Attribute.php

    <?php
    
    namespace App\RestFilters\Helpers;
    
    class Attribute 
    {
        public static function sobstitute($attribute)
        {
            $hashMap = [
                'gt' => '>',
                'gte' => '>=',
                'lt' => '<',
                'lte' => '<=',
                'like' => 'like',
                'ilike' => 'ilike'
            ];
    
            return array_key_exists($attribute, $hashMap) ?
                $hashMap[$attribute] :
                false;
        }
    }
    

    To make it work, you can now add the $fieldMapping property to your model

    // App/Models/TrafficViolation.php
    
    class TrafficViolation extends Model
    {
        public array $fieldMapping = [
            'location' => 'location_relationship_id'
        ];
    }
    

    And don't forget to register your new ServiceProvider in app.php

    // config/app.php
    
    $providers = [
        ...
        \App\Providers\RestServiceProvider::class,
    ]
    

    This should allow you to now define mappings on each of your models