Git Product home page Git Product logo

searchable's Introduction

Searchable trait for Laravel's Eloquent models

This package adds search/filtering functionality to Eloquent models in Laravel 4/5/6.

You could also find those packages useful:

  • Withable - Allows eager loading of relations using request parameters
  • Sortable - Allows sorting your models using request parameters
  • Pimpable - A meta package that combines Sortable, Searchable and Withable behaviours

Composer install

Add the following line to composer.json file in your project:

"jedrzej/searchable": "0.0.17"

or run the following in the commandline in your project's root folder:

composer require "jedrzej/searchable" "0.0.17"

Setting up searchable models

In order to make an Eloquent model searchable, add the trait to the model and define a list of fields that the model can be filtered by. You can either define a $searchable property or implement a getSearchableAttributes method if you want to execute some logic to define list of searchable fields.

use Jedrzej\Searchable\SearchableTrait;

class Post extends Eloquent
{
	use SearchableTrait;
	
	// either a property holding a list of searchable fields...
	public $searchable = ['title', 'forum_id', 'user_id', 'created_at'];
	
	// ...or a method that returns a list of searchable fields
	public function getSearchableAttributes()
	{
	    return ['title', 'forum_id', 'user_id', 'created_at'];
	}
}

In order to make all fields searchable put an asterisk * in the list of searchable fields:

public $searchable = ['*'];

It is also possible to blacklist model's attributes to prevent it from being filtered on.

You can either define a $notSearchable property or implement a getNotSearchableAttributes method if you want to execute some logic to define list of searchable fields.

use Jedrzej\Searchable\SearchableTrait;

class Post extends Eloquent
{
	use SearchableTrait;
	
	// either a property holding a list of not searchable fields...
	public $notSearchable = ['created_at'];
	
	// ...or a method that returns a list of not searchable fields
	public function getNotSearchableAttributes()
	{
	    return ['created_at'];
	}
}

If you define both lists - searchable and not searchable columns - the resulting set of searchable fields will contain all whitelisted attributes except all blacklisted attributes.

Searching models

SearchableTrait adds a filtered() scope to the model - you can pass it a query being an array of filter conditions:

// return all posts with forum_id equal to $forum_id
Post::filtered(['forum_id' => $forum_id])->get();

// return all posts with with <operator> applied to forum_id
Post::filtered(['forum_id' => <operator>])->get();

or it will use Request::all() as default:

// if you append ?forum_id=<operator> to the URL, you'll get all Posts with <operator> applied to forum_id
Post::filtered()->get();

Choosing query mode

The default query mode is to apply conjunction (AND) of all queries to searchable model. It can be changed to disjunction (OR) by setting value of mode query paramter to or. If the mode query parameter is already in use, name returned by getQueryMode method will be used.

Building a query

The SearchableTrait supports the following operators:

Comparison operators

Comparison operators allow filtering based on result of comparison of model's attribute and query value. They work for strings, numbers and dates. They have the following format:

(<operator>)<value>

The following comparison operators are available:

  • gt for greater than comparison
  • ge for greater than or equal comparison
  • lt for less than comparison, e.g
  • le for les than or equal comparison

In order to filter posts from 2015 and newer, the following query should be used:

?created_at=(ge)2015-01-01

Equals/In operators

Searchable trait allows filtering by exact value of an attribute or by a set of values, depending on the type of value passed as query parameter. If the value contains commas, the parameter is split on commas and used as array input for IN filtering, otherwise exact match is applied.

In order to filter posts from user with id 42, the following query should be used:

?user_id=42

In order to filter posts from forums with id 7 or 8, the following query should be used:

?forum_id=7,8

Like operators

Like operators allow filtering using LIKE query. This operator is triggered if exact match operator is used, but value contains % sign as first or last character.

In order to filter posts that start with How, the following query should be used:

?title=How%

Notice: percentage character is used to encode special characters in URLs, so when sending the request make sure the tools you use properly encode the % character as %25

Null operator

Null operator (null) allows filtering models whose attribute is null.

In order to filter posts that have no attachment, the following query should be used:

?attachment_id=(null)

Negation operator

It is possible to get negated results of a query by prepending the operator with !.

Some examples:

//filter posts from all forums except those with id 7 or 8
?forum_id=!7,8

//filter posts older than 2015
?created_at=!(ge)2015

//filter posts with attachment
?attachment_id=!(null)

Multiple constraints for single attribute

It is possible to apply multiple constraints for a single model's attribute. In order to achieve that provide an array of query filters instead of a single filter:

// filter all posts from year 20** except 2013
?created_at[]=20%&created_at[]=!2013%

Filtering by relation attributes

It is possible to filter by attributes of model's relations - Eloquent's whereHas() will be applied. In order to filter by relation, add the relation attribute to the list of searchable fields in the form relation:attribute. The same string should be used in the query to filter by that relation's attribute, e.g.:

 // allow filtering on user's active field
 protected $searchable = ['user:active'];

 // filter only posts of active users
 ?user:active=1

It's also possible to negate the search and filter objects that don't have a matching relation by applying Eloquent's whereDoesntHave(). In order to do that, prefix relation name with !:

 // allow filtering on comment's approved field
 protected $searchable = ['comments:approved'];

 // filter only posts that have approved comments
 ?comments:approved=1
      
 // filter only posts that have not-approved comments
 ?comments:approved=1
 
 // filter only posts that do not have approved comments
 ?!comments:approved=1

If you want to filter by a nested relation, just provide the nested relation's name with the dot replaced by colon. It's necessary as PHP automatically replaces dots with underscores in the URL.

 // filter only posts that have comments from active authors
 ?comments:author:active=1

Overriding default filter logic

It is possible to process selected filters with your own logic, e.g. when filter name doesn't match the name of attribute that is used for filtering or some custom operations need to be executed. In order to override logic for filter xyz, you'll need to define a method in your model called processXyzFilter. This method should return true, if filter has been processed and default logic should no longer happen.

 // use one filter to search in multiple columns
 protected function processNameFilter(Builder $builder, Constraint $constraint)
 {
     // this logic should happen for LIKE/EQUAL operators only
     if ($constraint->getOperator() === Constraint::OPERATOR_LIKE || $constraint->getOperator() === Constraint::OPERATOR_EQUAL) {
         $builder->where(function ($query) use ($constraint) {
             $query->where('first_name', $constraint->getOperator(), $constraint->getValue())
                 ->orWhere('last_name', $constraint->getOperator(), $constraint->getValue());
         });

         return true;
     }

     // default logic should be executed otherwise
     return false;
 }

In order to override a filter for relation search, replace the colon in the filter name with underscore. If you wanted to override logic for user:active, you'd need to define processUser_ActiveFilter method.

searchable's People

Contributors

jadjoubran avatar jedrzej avatar jedrzej-x-team-com avatar morloderex avatar pablomaurer avatar

Stargazers

 avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar

Watchers

 avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar

searchable's Issues

Allow switching to OR search instead of AND search

Current version of the package allows only searching using AND operator on all criteria - meaning that all criteria need to be met in order to record to be returned. The scope of this issue is to allow switching to OR search - the result of such search are records that meet any of given criteria. One example of such feature being useful is searching among multiple text fields (email, username, first/last name) for some kind of user autosuggest.

Searching Related Models Using OR

When using this package to search multiple relations using the OR mode the query generated is not produced as expected. For example, consider the following if we have a one to one relation between "providers" and "users" where the users table contains first_name and last_name and the providers table has a user_id column:

?user:first_name&user:last_name&mode=or will generate the following query

 where exists (select * from `users` where `providers`.`user_id` = `users`.`id` and (`last_name` = ?)) **and** 
exists (select * from `users` where `providers`.`user_id` = `users`.`id` and (`first_name` = ?))

The expected result would be

 where exists (select * from `users` where `providers`.`user_id` = `users`.`id` and (`last_name` = ?)) **OR** 
exists (select * from `users` where `providers`.`user_id` = `users`.`id` and (`first_name` = ?))

As a side note, I believe this query should actually group the two queries under the same subquery like this:

 where exists (select * from `users` where `providers`.`user_id` = `users`.`id` and (`last_name` = ? OR `first_name` = ?)) 

however I believe this is a separate issue. I propose the following changes in Constraint.php

 public function apply(Builder $builder, $field, $mode = Constraint::MODE_AND)
    {
        if ($this->isRelation($field)) {
            list($relation, $field) = $this->splitRelationField($field);
            if (static::parseIsNegation($relation)) {
                $builder->doesntHave($relation, $mode, function (Builder $builder) use ($field, $mode) {
                    $this->doApply($builder, $field, $mode);
                });
            } else {
                $builder->has($relation,'>=',1,$mode, function (Builder $builder) use ($field, $mode) {
                    $this->doApply($builder, $field, $mode);
                });
            }
        } else {
            $this->doApply($builder, $field, $mode);
        }
    }

Where the ->whereDoesntHave() is replaced with doesntHave() passing in the $mode and the whereHas() is replaced with has(). This could actully simplify the doApply() function as well instead of resolving the method name 'whereIn' 'orWhereIn' etc, the $mode could be passed in directly the function.

Using OR and AND at the same query

For example, we have a small service for blog posts management and a blog itself.

At our search, we want to do the following query:

            'query' => [
                'mode' => 'or',
                'city' => "%$query%",
                'state' => "%$query%",
                'store_name' => "%$query%",
                'title' => "%$query%",
            ],

Thats nice, it will search for everything we need but we want to filter by published posts also. But, Its not possible since we're using OR mode.

Is there a way to make something like?

            'query' => [
                'mode' => 'and',
                'status' => 'published',
                [
                    'mode' => 'or',
                    'city' => "%$query%",
                    'state' => "%$query%",
                    'store_name' => "%$query%",
                    'title' => "%$query%",
                ],
            ],

Search operator position is better at param key

Usually users do not input operators like (ge)2018-04-16
It make view code little hacky like change values before send

I think operators should be in query keys, not values like below

Now
<input name="foo" value="(ge)2018-04-16">
My opinion
<input name="(ge)foo" value="2018-04-16">

Where between?

Hello i have noticed there is no way to do a where between.

And overriding the filter doesn't work either sense the filter method is beeing hit twice and not once with an array of the constraints.

Consider this:
date[]=2016-07-26&date[]=2016-07-29

This makes the dateProgressFilter beeing hit but its beeing hitted twice.

So i have a single value to work with..

Is it possible to somehow get both constraint objects a once so i can do something like the fallowing:

$builder->whereBetween()

ILIKE instead of LIKE

I am using postgres, where LIKE is case sensitive. This does not provide best experience with filters. ILIKE should be used in this case.

Can this be fixed?

Ambiguous columns

I'm filtering on a model but a filed like created_at appears in many of my models so this is causing an error whereby created_at is ambiguous. I believe this occurs because I am running pimp() on a relation instead on the model itself. How can I fix this?

$user->transactions()->pimp()

SimplePaginate with "page" attribute stop working.

I installed pimpable package, but I think the problem is with searchable package.
When I have something like this:
Model::pimp()->simplePaginate();
It gives me:
SQLSTATE[42S22]: Column not found: 1054 Unknown column 'page' in 'where clause' (SQL: select * fromsectionspage = 1 limit 16 offset 0)
I found a way to allow everything except 'page' attribute, but it would be nice to have this effect in package.

My implementation in Model;
protected function getSearchableAttributes() { return array_except(Input::all(), 'page'); } // or something like this public $searchableExcept = ['page'];

BETWEEN on relations not grouped

Hi,

?start_date[]=(ge)2018-08-01&start_date[]=(le)2018-08-02

translates to the following MySQL query:

WHERE `start_date` >= 2018-08-01 
AND 
`start_date` <= 2018-08-02

Which is the expected result. However,

?instances:start_date[]=(ge)2018-08-01&instances:start_date[]=(le)2018-08-02

becomes

WHERE EXISTS 
( 
       SELECT * 
       FROM   `expedition_instances` 
       WHERE  `expeditions`.`id` = `expedition_instances`.`expedition_id` 
       AND    `start_date` >= '2018-08-01') 
AND 
EXISTS 
( 
       SELECT * 
       FROM   `expedition_instances` 
       WHERE  `expeditions`.`id` = `expedition_instances`.`expedition_id` 
       AND    `start_date` <= '2018-08-02')

The expected result is

WHERE EXISTS 
( 
       SELECT * 
       FROM   `expedition_instances` 
       WHERE  `expeditions`.`id` = `expedition_instances`.`expedition_id` 
       WHERE  `start_date` >= 2018-08-01 
       AND    `start_date` <= 2018-08-02 )

Feel free to change the issue's title, I wasn't able to think of any better name for it atm.

Edit 1

The constraints looks correctly grouped into instances:start_date:

/expeditions?with[]=instances&instances:start_date[]=(ge)2018-08-01&instances:start_date[]=(le)2018-08-02

array(1) {
  ["instances:start_date"]=>
  array(2) {
    [0]=>
    object(Jedrzej\Searchable\Constraint)#715 (3) {
      ["operator":protected]=>
      string(2) ">="
      ["value":protected]=>
      string(10) "2018-08-01"
      ["is_negation":protected]=>
      bool(false)
    }
    [1]=>
    object(Jedrzej\Searchable\Constraint)#716 (3) {
      ["operator":protected]=>
      string(2) "<="
      ["value":protected]=>
      string(10) "2018-08-02"
      ["is_negation":protected]=>
      bool(false)
    }
  }
}

Edit 2

I think that all constraints of the group should be applied when doing

$builder->whereHas($relation, function (Builder $builder) use ($field, $mode) {
    $this->doApply($builder, $field, $mode);
});

Something like

$builder->whereHas($relation, function (Builder $builder) use ($constraints, $mode) {
    foreach($constraints as $constraint){
        $this->doApply($builder, $constraint->field, $mode);
    }
});

Of course that's just hypothetical, there is no access to the group of the same constraints as they are isolated in buildConstraints (Jedrzej/Searchable/SearchableTrait.php#L120)

Allow negative relation search

At the moment it's possible to do positive search by relation - find records that have matching related records. It's needed to allow also negative search - find records that don't have matching related records.

Allow overriding relation filter

Now it's possible to override a filter by implementing processAttributeFilter method in the model. Similar functionality should exist for relation filters.

Multiple columns

Hello, pleasure, first of all to thank the developers for the beautiful work. I've been looking for something like this for some time.

Sorry I'm Brazilian so my English isn't very good, it's just technical.

I would like to know if with this tool I can do the research of several fields through just one attribute.

Example:

posts?search=%searchstring%

colums search: [title, desc, keyword]

Problem with LIKE operator

I have this problem, when i use LIKE operator. I want to filter %Carlos%, but dont work because Builder resolve this: "value" => b"Êrlos%".

screenshot_1

` protected function processSearchFilter(Builder $builder, Constraint $constraint)
{
// this logic should happen for LIKE/EQUAL operators only

    if ($constraint->getOperator() === Constraint::OPERATOR_LIKE || $constraint->getOperator() === Constraint::OPERATOR_EQUAL) {
        $builder->whereHas('tercero',function ($query) use ($constraint) {
                $query->where('nombre1', $constraint->getOperator(), $constraint->getValue())
                    ->orWhere('nombre2', $constraint->getOperator(), $constraint->getValue())
                    ->orWhere('apellido1', $constraint->getOperator(), $constraint->getValue())
                    ->orWhere('apellido2', $constraint->getOperator(), $constraint->getValue())
                    ->orWhere('identificacion', $constraint->getOperator(), $constraint->getValue());

            });
        return true;
    }


    // default logic should be executed otherwise
    return false;
}`

No search for existance of a relation is available

Some of users of model Student have foreign key to model User. I create relation student() in model User with hasOne() to join them. Now in model User I need to filter those who are student. How can I do that?

I tried
public $searchable = ['name', 'mobile', 'email', 'id', 'student:id'];

and in string query I set

student:id=(null)

and doesn't work because the relation doesn't exist in the first place.

Allow filtering by fields of related models

The package now allows to search only among fiields of given searchable models. The scope of this issue is to add possibility to filter also fields of related models (one-to-one, many-to-one, many-to-many relations)

Search for content containg comma

Is it possible to search for content, like "Smith, John"?
Right now, it is looking for "Smith" OR "John".
Is there some kind of config to disable "OR" functionality for comma, or change it to another character?

Recommend Projects

  • React photo React

    A declarative, efficient, and flexible JavaScript library for building user interfaces.

  • Vue.js photo Vue.js

    🖖 Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.

  • Typescript photo Typescript

    TypeScript is a superset of JavaScript that compiles to clean JavaScript output.

  • TensorFlow photo TensorFlow

    An Open Source Machine Learning Framework for Everyone

  • Django photo Django

    The Web framework for perfectionists with deadlines.

  • D3 photo D3

    Bring data to life with SVG, Canvas and HTML. 📊📈🎉

Recommend Topics

  • javascript

    JavaScript (JS) is a lightweight interpreted programming language with first-class functions.

  • web

    Some thing interesting about web. New door for the world.

  • server

    A server is a program made to process requests and deliver data to clients.

  • Machine learning

    Machine learning is a way of modeling and interpreting data that allows a piece of software to respond intelligently.

  • Game

    Some thing interesting about game, make everyone happy.

Recommend Org

  • Facebook photo Facebook

    We are working to build community through open source technology. NB: members must have two-factor auth.

  • Microsoft photo Microsoft

    Open source projects and samples from Microsoft.

  • Google photo Google

    Google ❤️ Open Source for everyone.

  • D3 photo D3

    Data-Driven Documents codes.