Be part of JetBrains PHPverse 2026 on June 9 – a free online event bringing PHP devs worldwide together.

jamesaps's avatar

How to create a Laravel RESTful API allowing requests with URL params?

I want to create an API that gives me more functionality than simply extracting all users. I want to be able to make complex queries on these users for example extract all users who registered after a certain date or all users who have more than 10 posts etc.

How do you implement this RESTfully in Laravel? I watched the API series on here but that didn't detail how to pass through URL params and use them.

0 likes
21 replies
thepsion5's avatar

You can access query parameters using the Input facades. So for example, if you had this url:

/api/users/?registered_after=2014-01-01&min_posts=10

You can extract those parameters in your controller like so:

public function index()
{
    //array( 'registered_after' => '2014-01-01', 'min_posts' => 10)
    $filters = Input::only('registered_after', 'min_posts');
    $users = $this->userRepo->applyFilters($filters)->all();
    return $users->toJSON();
}
3 likes
jamesaps's avatar

Fantastic! Thanks for the reply. So are you suggesting that filters should be used to handle refining the results according to the URL parameters?

thepsion5's avatar
Level 25

That's always been my approach, at least. If I needed to filter results, I would have a chainable method on my repository that would apply a relatively simple key-value filter. This trait is ripped directly from a current project:

trait FilterableTrait
{

    protected $validFilterableFields = [];

    protected $filters = [];

    protected function addFilter($field, $value)
    {
        if(!in_array($field, $this->validFilterableFields)) {
            return false;
        }
        $filterMethod = 'filterBy' . camel_case($field);
        if( method_exists( $this, $filterMethod ) ) {
            $this->$filterMethod($value);
        } else {
            $this->filters[$field] = $value;
        }
        return true;
    }

    protected function applyFiltersToQuery($query)
    {
        foreach($this->filters as $field => $value) {
            $query->where($field, $value);
        }
        return $query;
    }
}

Typically, I'd add it to a repository and use that to do the filtering:

//not ripped from a current project, sadly :'(
class EloquentAvengersRepository extends EloquentRepository implements AvengersRepository
{
    use FilterableTrait;

    $this->validFilterableFields = ['is_genius', 'owns_hammer', 'loves_freedom', 'unpopular_when_angry'];

    public function filterBy($field, $value)
    {
        $this->addFilter($field, $value);
        return $this;
    }

    public function all()
    {
        return $this->applyFiltersToQuery( $this->query() )->paginate(10);
    }

}

So now I can use the repository like so:

$this->avengersRepo->filterBy($foo, $bar)->all();
20 likes
jamesaps's avatar

Thanks for the reply. Sorry I've taken so long to thank you - I've frantically been learning what traits and repositories are :D

I notice that all of the fields you're filtering against can be modelled as booleans (more generally discrete mutually exclusive variables). How would I create a filter that retrieved all avengers that were created after a certain date for example?

jamesaps's avatar

This blog suggests using inequalities and + / - signs in URLS.

E.g. http://url/users/?age>3&sort=-age

Which would retrieve all users older than 3 sorted by age in descending order.

Is a URL like this easily parseable?

1 like
jamesaps's avatar

Sorry that this is a stupid question but where does $this->query() come from?

jamesaps's avatar

I just realised that your Repositories are different to mine. I am not extending an EloquentRepository class. What would you put in that class?

Do you have an example set of files that shows the controller, traits and repositories working together? Sorry that's a big ask :/

alfonsan's avatar

I think this concept deserves a video from @JeffreyWay.

It's the difference between the Eloquent model, the Illuminate\Database\Eloquent\Builder (the one jamesaps is asking for), and the final collection/result from the query.

There is some "magic" going on behind the scenes, and it's somehow confusing to understand when you are dealing with each object.

3 likes
jamesaps's avatar

@alfonsan thanks :) yeah a video would be amazing. I think maybe I've made a mistake learning PHP in the context of Laravel but I watched Jeffrey's video on Repositories and it made sense and then an IRL example is just far more complicated :/

Ionut's avatar

Most likely EloquentRepository is an abstract class that has methods common to all models/repositories, like get, findById, findByAnyOtherAtribute, create, update, destroy.... This kinds of methods. You don't need to repeat that for every repository.

Extending EloquentAvengersRepository you inherit the methods from EloquentRepository plus you can add custom methods just for avengers.

Like a colleague of mine says: try and stay DRY as much as possible even when it's raining!

Less copy & paste, more abstraction, more time to think the best for your current and future features.

jamesaps's avatar

@ionut thanks ... god there's so much to learn aha - this whole abstraction concept is mind boggling

jamesaps's avatar

@alfonsan thanks ... it's that I understand it when everything is taught in isolation. It's just that bringing everything together is a different story

alfonsan's avatar

You cannot learn this in one month, or two. Programming is not a course you learn in X hours, it's a discipline.

The main difference is that learning a course is about spending some time and then keeping that knowledge for some time. Math equations, color theory, reading a Game of Thrones novel, etc...

Disciplines is more about lifestyle. You keep yourself in a constant learning and improving state. Instead of going through 400 pages in one week, and trying to grasp all that knowledge, you learn a tiny bit of knowledge, 10 minute video per week for example, and then you stick with that for a month until it becomes part of your programming style, until it becomes natural.

It's also useful that instead of writing the exact same code as Jeffrey writes, you take one of your old projects, and you refactor it with the new knowledge you had learned. Do it with 2 or 3 projects, and then keep the new ones going with that technique. Practice refactoring of old code and writing new one.

Neuroscientist says that in average, you need to keep trying for 90 days until the habit becomes part of yourself and you encounter less inner resistance, feel less lazy about it, or you simply cannot understand coding without it.

Think about it like space shuttles, 90% of the energy used for the space voyage is wasted during the first minute taking off.

3 likes
thepsion5's avatar

@alfonsan Yep, I've retyped these examples enough times that I figured out it would be a good idea to put them in gist, haha

@jamesaps For cases more complex than simple equality, I usually have a custom method that that adds the correct filter conditions. See this code block here:

$filterMethod = 'filterBy' . camel_case($field);
if( method_exists( $this, $filterMethod ) ) {
    $this->$filterMethod($value);
} else { 
    $this->filters[$field] = $value; 
}

So, if $field is equal to "name" and I have a filterByName() method on the repository, that method will be called instead of just adding the filter field.

For example, if I had a form field called name_starts_with, I would have a filterByNameStartsWith($value) method. With that particular example, there's not a good way to handle that, but I've got other versions of the same thing that runs the filter method in the applyFilters($query) method, which would make that easy.

1 like
jamesaps's avatar

@thepsion5

thanks! That way sounds like you could end up with tonnes of different methods. Is this a possible way to handle it:


<?php namespace Storage; use StdClass; trait FilterableTrait { protected $validFilterableFields = ['sort', 'page', 'created_at', 'modified_at']; protected $filters = []; protected $model; protected function addFilter($field, $value, $operator='=') { if($value == null) return false; $this->filters[$field] = array( 'value' => $value, 'operator' => $operator ); return true; } public function applyFiltersToQuery() { foreach ($this->filters as $field => $value_operator) { $value = $value_operator['value']; $operator = $value_operator['operator']; if($operator == 'page') continue; if(is_array($value)) { $this->model = $this->model->where(function($query) use($value, $field, $operator) { foreach ($value as $val) { $controls = $this->applyFilterToQuery($field, $val, $operator, 'OR', true); if(isset($controls['operator'])) { $query->$controls['queryType']($controls['field'], $controls['operator'], $controls['value']); } else { $query->$controls['queryType']($controls['field'], $controls['value']); } } }); } else { $this->model = $this->applyFilterToQuery($field, $value, $operator); } } return $this; } public function applyFilterToQuery($field, $val, $operator, $connector = 'AND', $returnArray = false) { $clause = $connector == 'OR' ? 'orWhere' : 'where'; switch($operator) { case '=' : case '>' : case '<' : case '<=' : case '>=' : case '<>' : if($returnArray) { return [ 'queryType' => $clause, 'operator' => $operator, 'field' => $field, 'value' => $val ]; } return $this->model = $this->model->$clause($field, $operator, $val); break; case 'sort' : $direction = substr($val, 0, 4) == 'asc_' ? 'asc' : 'desc'; if(substr($val, 0, 5) == 'desc_' or $direction == 'asc') { $orderAgainst = substr($val, strlen($direction) + 1); if(!in_array($orderAgainst, $this->validFilterableFields)) return $this->model; if($returnArray) { return [ 'queryType' => 'orderBy', 'field' => $orderAgainst, 'value' => $direction ]; } return $this->model = $this->model->orderBy($orderAgainst, $direction); } else { if(!in_array($val, $this->validFilterableFields)) return $this->model; if($returnArray) { return [ 'queryType' => 'orderBy', 'field' => $val, 'value' => $direction ]; } return $this->model = $this->model->orderBy($val, $direction); } break; } } public function addFilters($filters) { foreach ($filters as $field => $value) { $operator = '='; $fieldPrefix = substr($field, 0, 3); if(substr($field, 3, 1) == '_') { switch($fieldPrefix) { case 'gtn' : //greater than $operator = '>'; $field = substr($field, 4); break; case 'gte' : //greater than or equal to $operator = '>='; $field = substr($field, 4); break; case 'gtd' : //greater than date $operator = '>'; $field = substr($field, 4); $value = date('Y-m-d H:i:s', strtotime($value)); break; case 'ltn' : //less than $operator = '<'; $field = substr($field, 4); break; case 'lte' : //less than or equal to $operator = '<='; $field = substr($field, 4); break; case 'ltd' : //less than date $operator = '<'; $field = substr($field, 4); $value = date('Y-m-d H:i:s', strtotime($value)); break; case 'not' : //not equal to $operator = '<>'; $field = substr($field, 4); break; } } if($operator <> '=') { if(!in_array($field, $this->validFilterableFields)) return $this->model; } if($field == 'sort') $operator = 'sort'; if($field == 'page') $operator = 'page'; $this->addFilter($field, $value, $operator); } return $this->applyFiltersToQuery(); } public function all() { return $this->model = $this->applyFiltersToQuery(); } public function find($id) { return $this->model = $this->model->find($id)->get(); } public function getByPage($page = 1, $limit = 5) { $results = new StdClass; $results->page = $page; $results->limit = $limit; $results->totalItems = 0; $results->items = array(); $this->model = $this->model->skip($limit * ($page - 1))->take($limit)->get(); $results->totalItems = $this->model->count(); $results->items = $this->model; return $results; } }

Basically, ?lte_age=18&not_name=james&sort=asc_name

Would return all results:

WHERE
age <= 18 AND
name <> 'james' 
ORDER BY name
ASC
2 likes
thepsion5's avatar

@jamesapps You could handle it that way, yes, but I'm not sure I would. Relying so heavily on parsing the name of search fields and string checking makes me wary. But I haven't had any use cases where I need to filter on more than 3-4 separate fields, so if you're dealing with a report where you have 15+ fields that all need to be filtered that's preferable to having 2+ methods for each field.

I'd probably try and compromise by using multiple value to define a range of filters.

So for age = 18: ?age=18&sort=name&dir=asc For age <= 18: ?age=-1,18&sort=name&dir=asc For 12 <= age <= 18: ?age=12,18&sort=name&dir=asc

And then I could parse that value in the filterByAge method.

FroimsonM's avatar

Could somebody explain why keep everything in trait if we have parent class "EloquentRepository" ?

Also, do I get it right that filterBy method is included in AvengersRepository interface? If so, why not pass the whole input, so that we can simply write something like

public function index() 
{
    $companies = $this->company->setFilters(\Request::all())->get();
}

Instead of looping through the input in controller and call filterBy each time?

Please or to participate in this conversation.