2018-07-16 14:13:07 -07:00
|
|
|
<?php
|
|
|
|
|
|
|
|
namespace App\Models\Traits;
|
|
|
|
|
|
|
|
use App\Models\Asset;
|
|
|
|
use App\Models\CustomField;
|
|
|
|
use Illuminate\Database\Eloquent\Builder;
|
|
|
|
|
|
|
|
/**
|
|
|
|
* This trait allows for cleaner searching of models,
|
|
|
|
* moving from complex queries to an easier declarative syntax.
|
|
|
|
*
|
|
|
|
* @author Till Deeke <kontakt@tilldeeke.de>
|
|
|
|
*/
|
|
|
|
trait Searchable {
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Performs a search on the model, using the provided search terms
|
|
|
|
*
|
|
|
|
* @param Illuminate\Database\Eloquent\Builder $query The query to start the search on
|
|
|
|
* @param string $search
|
|
|
|
* @return Illuminate\Database\Eloquent\Builder A query with added "where" clauses
|
|
|
|
*/
|
|
|
|
public function scopeTextSearch($query, $search)
|
|
|
|
{
|
|
|
|
$terms = $this->prepeareSearchTerms($search);
|
|
|
|
|
2018-07-16 17:44:31 -07:00
|
|
|
/**
|
|
|
|
* Search the attributes of this model
|
|
|
|
*/
|
|
|
|
$query = $this->searchAttributes($query, $terms);
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Search through the custom fields of the model
|
|
|
|
*/
|
|
|
|
$query = $this->searchCustomFields($query, $terms);
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Search through the relations of the model
|
|
|
|
*/
|
|
|
|
$query = $this->searchRelations($query, $terms);
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Search for additional attributes defined by the model
|
|
|
|
*/
|
|
|
|
$query = $this->advancedTextSearch($query, $terms);
|
2018-07-16 14:13:07 -07:00
|
|
|
|
|
|
|
return $query;
|
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Prepares the search term, splitting and cleaning it up
|
|
|
|
* @param string $search The search term
|
|
|
|
* @return array An array of search terms
|
|
|
|
*/
|
|
|
|
private function prepeareSearchTerms(string $search) {
|
|
|
|
return explode(' OR ', $search);
|
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
2018-07-16 17:44:31 -07:00
|
|
|
* Searches the models attributes for the search terms
|
2018-07-16 14:13:07 -07:00
|
|
|
*
|
|
|
|
* @param Illuminate\Database\Eloquent\Builder $query
|
2018-07-16 17:44:31 -07:00
|
|
|
* @param array $terms
|
2018-07-16 14:13:07 -07:00
|
|
|
* @return Illuminate\Database\Eloquent\Builder
|
|
|
|
*/
|
2018-07-16 17:44:31 -07:00
|
|
|
private function searchAttributes(Builder $query, array $terms) {
|
|
|
|
|
|
|
|
$table = $this->getTable();
|
2018-07-16 14:13:07 -07:00
|
|
|
|
2018-07-20 13:23:29 -07:00
|
|
|
$firstConditionAdded = false;
|
|
|
|
|
2018-07-16 14:13:07 -07:00
|
|
|
foreach($this->getSearchableAttributes() as $column) {
|
|
|
|
|
2018-07-16 17:44:31 -07:00
|
|
|
foreach($terms as $term) {
|
|
|
|
/**
|
|
|
|
* Making sure to only search in date columns if the search term consists of characters that can make up a MySQL timestamp!
|
|
|
|
*
|
|
|
|
* @see https://github.com/snipe/snipe-it/issues/4590
|
|
|
|
*/
|
|
|
|
if (!preg_match('/^[0-9 :-]++$/', $term) && in_array($column, $this->getDates())) {
|
|
|
|
continue;
|
|
|
|
}
|
2018-07-16 14:13:07 -07:00
|
|
|
|
2018-07-20 13:23:29 -07:00
|
|
|
/**
|
|
|
|
* We need to form the query properly, starting with a "where",
|
|
|
|
* otherwise the generated select is wrong.
|
|
|
|
*
|
|
|
|
* @todo This does the job, but is inelegant and fragile
|
|
|
|
*/
|
|
|
|
if (!$firstConditionAdded) {
|
|
|
|
$query = $query->where($table . '.' . $column, 'LIKE', '%'.$term.'%');
|
|
|
|
|
|
|
|
$firstConditionAdded = true;
|
|
|
|
continue;
|
|
|
|
}
|
|
|
|
|
2018-07-16 17:44:31 -07:00
|
|
|
$query = $query->orWhere($table . '.' . $column, 'LIKE', '%'.$term.'%');
|
|
|
|
}
|
2018-07-16 14:13:07 -07:00
|
|
|
}
|
|
|
|
|
|
|
|
return $query;
|
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
2018-07-16 17:44:31 -07:00
|
|
|
* Searches the models custom fields for the search terms
|
2018-07-16 14:13:07 -07:00
|
|
|
*
|
|
|
|
* @param Illuminate\Database\Eloquent\Builder $query
|
2018-07-16 17:44:31 -07:00
|
|
|
* @param array $terms
|
2018-07-16 14:13:07 -07:00
|
|
|
* @return Illuminate\Database\Eloquent\Builder
|
|
|
|
*/
|
2018-07-16 17:44:31 -07:00
|
|
|
private function searchCustomFields(Builder $query, array $terms) {
|
2018-07-16 14:13:07 -07:00
|
|
|
|
|
|
|
/**
|
|
|
|
* If we are searching on something other that an asset, skip custom fields.
|
|
|
|
*/
|
|
|
|
if (! $this instanceof Asset) {
|
|
|
|
return $query;
|
|
|
|
}
|
|
|
|
|
2018-07-16 17:44:31 -07:00
|
|
|
$customFields = CustomField::all();
|
|
|
|
|
|
|
|
foreach ($customFields as $field) {
|
|
|
|
foreach($terms as $term) {
|
|
|
|
$query->orWhere($this->getTable() . '.'. $field->db_column_name(), 'LIKE', '%'.$term.'%');
|
|
|
|
}
|
2018-07-16 14:13:07 -07:00
|
|
|
}
|
|
|
|
|
|
|
|
return $query;
|
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
2018-07-16 17:44:31 -07:00
|
|
|
* Searches the models relations for the search terms
|
2018-07-16 14:13:07 -07:00
|
|
|
*
|
|
|
|
* @param Illuminate\Database\Eloquent\Builder $query
|
2018-07-16 17:44:31 -07:00
|
|
|
* @param array $terms
|
2018-07-16 14:13:07 -07:00
|
|
|
* @return Illuminate\Database\Eloquent\Builder
|
|
|
|
*/
|
2018-07-16 17:44:31 -07:00
|
|
|
private function searchRelations(Builder $query, array $terms) {
|
2018-07-16 14:13:07 -07:00
|
|
|
|
|
|
|
foreach($this->getSearchableRelations() as $relation => $columns) {
|
|
|
|
|
2018-07-16 17:44:31 -07:00
|
|
|
$query = $query->orWhereHas($relation, function($query) use ($relation, $columns, $terms) {
|
2018-07-16 14:13:07 -07:00
|
|
|
|
|
|
|
$table = $this->getRelationTable($relation);
|
2018-07-16 17:44:31 -07:00
|
|
|
|
2018-07-16 14:13:07 -07:00
|
|
|
/**
|
|
|
|
* We need to form the query properly, starting with a "where",
|
|
|
|
* otherwise the generated nested select is wrong.
|
|
|
|
*
|
2018-07-16 17:44:31 -07:00
|
|
|
* @todo This does the job, but is inelegant and fragile
|
2018-07-16 14:13:07 -07:00
|
|
|
*/
|
2018-07-16 17:44:31 -07:00
|
|
|
$firstConditionAdded = false;
|
2018-07-16 14:13:07 -07:00
|
|
|
|
|
|
|
foreach($columns as $column) {
|
2018-07-16 17:44:31 -07:00
|
|
|
foreach($terms as $term) {
|
|
|
|
if (!$firstConditionAdded) {
|
|
|
|
$query->where($table . '.' . $column, 'LIKE', '%'.$term.'%');
|
|
|
|
$firstConditionAdded = true;
|
|
|
|
continue;
|
|
|
|
}
|
|
|
|
|
|
|
|
$query->orWhere($table . '.' . $column, 'LIKE', '%'.$term.'%');
|
|
|
|
}
|
2018-07-16 14:13:07 -07:00
|
|
|
}
|
|
|
|
});
|
2018-07-16 17:44:31 -07:00
|
|
|
|
2018-07-16 14:13:07 -07:00
|
|
|
}
|
|
|
|
|
|
|
|
return $query;
|
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Run additional, advanced searches that can't be done using the attributes or relations.
|
|
|
|
*
|
|
|
|
* This is a noop in this trait, but can be overridden in the implementing model, to allow more advanced searches
|
|
|
|
*
|
|
|
|
* @param Illuminate\Database\Eloquent\Builder $query
|
2018-07-16 17:44:31 -07:00
|
|
|
* @param array $terms The search terms
|
2018-07-16 14:13:07 -07:00
|
|
|
* @return Illuminate\Database\Eloquent\Builder
|
|
|
|
*
|
|
|
|
* @SuppressWarnings(PHPMD.UnusedFormalParameter)
|
|
|
|
*/
|
2018-07-16 17:44:31 -07:00
|
|
|
public function advancedTextSearch(Builder $query, array $terms) {
|
2018-07-16 14:13:07 -07:00
|
|
|
return $query;
|
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Get the searchable attributes, if defined. Otherwise it returns an empty array
|
|
|
|
*
|
|
|
|
* @return array The attributes to search in
|
|
|
|
*/
|
|
|
|
private function getSearchableAttributes() {
|
|
|
|
return isset($this->searchableAttributes) ? $this->searchableAttributes : [];
|
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Get the searchable relations, if defined. Otherwise it returns an empty array
|
|
|
|
*
|
|
|
|
* @return array The relations to search in
|
|
|
|
*/
|
|
|
|
private function getSearchableRelations() {
|
|
|
|
return isset($this->searchableRelations) ? $this->searchableRelations : [];
|
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Get the table name of a relation.
|
|
|
|
*
|
|
|
|
* This method loops over a relation name,
|
|
|
|
* getting the table name of the last relation in the series.
|
|
|
|
* So "category" would get the table name for the Category model,
|
|
|
|
* "model.manufacturer" would get the tablename for the Manufacturer model.
|
|
|
|
*
|
|
|
|
* @param string $relation
|
|
|
|
* @return string The table name
|
|
|
|
*/
|
|
|
|
private function getRelationTable($relation) {
|
|
|
|
$related = $this;
|
|
|
|
|
|
|
|
foreach(explode('.', $relation) as $relationName) {
|
|
|
|
$related = $related->{$relationName}()->getRelated();
|
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Are we referencing the model that called?
|
|
|
|
* Then get the internal join-tablename, since laravel
|
|
|
|
* has trouble selecting the correct one in this type of
|
|
|
|
* parent-child self-join.
|
|
|
|
*
|
|
|
|
* @todo Does this work with deeply nested resources? Like "category.assets.model.category" or something like that?
|
|
|
|
*/
|
|
|
|
if ($this instanceof $related) {
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Since laravel increases the counter on the hash on retrieval, we have to count it down again.
|
|
|
|
*
|
|
|
|
* This causes side effects! Every time we access this method, laravel increases the counter!
|
|
|
|
*
|
|
|
|
* Format: laravel_reserved_XXX
|
|
|
|
*/
|
|
|
|
$relationCountHash = $this->{$relationName}()->getRelationCountHash();
|
|
|
|
|
|
|
|
$parts = collect(explode('_', $relationCountHash));
|
|
|
|
|
|
|
|
$counter = $parts->pop();
|
|
|
|
|
|
|
|
$parts->push($counter - 1);
|
|
|
|
|
|
|
|
return implode('_', $parts->toArray());
|
|
|
|
}
|
|
|
|
|
|
|
|
return $related->getTable();
|
|
|
|
}
|
|
|
|
}
|