From baa3be728d1a857d2ba9d87055fb3d26d0f10ce4 Mon Sep 17 00:00:00 2001 From: Till Deeke Date: Mon, 16 Jul 2018 23:13:07 +0200 Subject: [PATCH] Refactoring: A nicer and easier syntax for searching models (#5841) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * Adds the ability to search by dates Adding extra „where“-conditions to the „TextSearch“ queries, allowing the users to search by dates * Adds missing dates to $dates in models * Removes duplicated „where“ conditions * Adds the Searchable trait to models, defining the searchable attributes and relations * Removes the old text search methods * Adds back additional conditions to the search These conditions could not be modeled in the „attributes“ or „relations“, so we include them here * Removes unnecessary check for the deleted_at attribute * Fixes typo in comments * suppresses errors from Codacy We can safely ignore the error codacy is throwing here, since this method is a standin/noop for models who need to implement more advanced searches --- .../Controllers/Api/AccessoriesController.php | 2 +- .../Controllers/Api/ComponentsController.php | 2 +- .../Controllers/Api/ConsumablesController.php | 1 - .../Controllers/Api/SuppliersController.php | 2 +- app/Models/Accessory.php | 57 ++--- app/Models/Actionlog.php | 46 ++-- app/Models/Asset.php | 157 ++++++------ app/Models/AssetMaintenance.php | 44 ++-- app/Models/AssetModel.php | 54 ++-- app/Models/Category.php | 34 ++- app/Models/Company.php | 32 +-- app/Models/Component.php | 65 ++--- app/Models/Consumable.php | 66 ++--- app/Models/Department.php | 35 +-- app/Models/Depreciation.php | 36 ++- app/Models/Group.php | 33 ++- app/Models/License.php | 66 +++-- app/Models/Location.php | 52 ++-- app/Models/Manufacturer.php | 34 +-- app/Models/Statuslabel.php | 34 +-- app/Models/Supplier.php | 35 +-- app/Models/Traits/Searchable.php | 238 ++++++++++++++++++ app/Models/User.php | 93 ++++--- 23 files changed, 679 insertions(+), 539 deletions(-) create mode 100644 app/Models/Traits/Searchable.php diff --git a/app/Http/Controllers/Api/AccessoriesController.php b/app/Http/Controllers/Api/AccessoriesController.php index 8590accfd4..fcdc52d210 100644 --- a/app/Http/Controllers/Api/AccessoriesController.php +++ b/app/Http/Controllers/Api/AccessoriesController.php @@ -24,7 +24,7 @@ class AccessoriesController extends Controller $this->authorize('view', Accessory::class); $allowed_columns = ['id','name','model_number','eol','notes','created_at','min_amt','company_id']; - $accessories = Accessory::whereNull('accessories.deleted_at')->with('category', 'company', 'manufacturer', 'users', 'location'); + $accessories = Accessory::with('category', 'company', 'manufacturer', 'users', 'location'); if ($request->has('search')) { $accessories = $accessories->TextSearch($request->input('search')); diff --git a/app/Http/Controllers/Api/ComponentsController.php b/app/Http/Controllers/Api/ComponentsController.php index 5530fdbfcf..00b4cd9e5a 100644 --- a/app/Http/Controllers/Api/ComponentsController.php +++ b/app/Http/Controllers/Api/ComponentsController.php @@ -24,7 +24,7 @@ class ComponentsController extends Controller public function index(Request $request) { $this->authorize('view', Component::class); - $components = Company::scopeCompanyables(Component::select('components.*')->whereNull('components.deleted_at') + $components = Company::scopeCompanyables(Component::select('components.*') ->with('company', 'location', 'category')); if ($request->has('search')) { diff --git a/app/Http/Controllers/Api/ConsumablesController.php b/app/Http/Controllers/Api/ConsumablesController.php index bfd233e2a1..faf485f882 100644 --- a/app/Http/Controllers/Api/ConsumablesController.php +++ b/app/Http/Controllers/Api/ConsumablesController.php @@ -24,7 +24,6 @@ class ConsumablesController extends Controller $this->authorize('index', Consumable::class); $consumables = Company::scopeCompanyables( Consumable::select('consumables.*') - ->whereNull('consumables.deleted_at') ->with('company', 'location', 'category', 'users', 'manufacturer') ); diff --git a/app/Http/Controllers/Api/SuppliersController.php b/app/Http/Controllers/Api/SuppliersController.php index 4de32bc41b..27cee15216 100644 --- a/app/Http/Controllers/Api/SuppliersController.php +++ b/app/Http/Controllers/Api/SuppliersController.php @@ -26,7 +26,7 @@ class SuppliersController extends Controller $suppliers = Supplier::select( array('id','name','address','address2','city','state','country','fax', 'phone','email','contact','created_at','updated_at','deleted_at','image','notes') - )->withCount('assets')->withCount('licenses')->withCount('accessories')->whereNull('deleted_at'); + )->withCount('assets')->withCount('licenses')->withCount('accessories'); if ($request->has('search')) { diff --git a/app/Models/Accessory.php b/app/Models/Accessory.php index 1c78b03a58..944304d806 100755 --- a/app/Models/Accessory.php +++ b/app/Models/Accessory.php @@ -1,6 +1,7 @@ 'boolean' ]; + use Searchable; + + /** + * The attributes that should be included when searching the model. + * + * @var array + */ + protected $searchableAttributes = ['name', 'model_number', 'order_number', 'purchase_date']; + + /** + * The relations and their attributes that should be included when searching the model. + * + * @var array + */ + protected $searchableRelations = [ + 'category' => ['name'], + 'company' => ['name'], + 'manufacturer' => ['name'], + 'supplier' => ['name'], + 'location' => ['name'] + ]; + /** * Set static properties to determine which checkout/checkin handlers we should use */ @@ -171,40 +194,6 @@ class Accessory extends SnipeModel return $remaining; } - /** - * Query builder scope to search on text - * - * @param \Illuminate\Database\Query\Builder $query Query builder instance - * @param text $search Search term - * - * @return \Illuminate\Database\Query\Builder Modified query builder - */ - public function scopeTextSearch($query, $search) - { - - return $query->where(function ($query) use ($search) { - - $query->whereHas('category', function ($query) use ($search) { - $query->where('categories.name', 'LIKE', '%'.$search.'%'); - })->orWhere(function ($query) use ($search) { - $query->whereHas('company', function ($query) use ($search) { - $query->where('companies.name', 'LIKE', '%'.$search.'%'); - }); - })->orWhere(function ($query) use ($search) { - $query->whereHas('manufacturer', function ($query) use ($search) { - $query->where('manufacturers.name', 'LIKE', '%'.$search.'%'); - }); - })->orWhere(function ($query) use ($search) { - $query->whereHas('location', function ($query) use ($search) { - $query->where('locations.name', 'LIKE', '%'.$search.'%'); - }); - })->orWhere('accessories.name', 'LIKE', '%'.$search.'%') - ->orWhere('accessories.model_number', 'LIKE', '%'.$search.'%') - ->orWhere('accessories.order_number', 'LIKE', '%'.$search.'%'); - - }); - } - /** * Query builder scope to order on company * diff --git a/app/Models/Actionlog.php b/app/Models/Actionlog.php index e8e823a488..b2945b8d13 100755 --- a/app/Models/Actionlog.php +++ b/app/Models/Actionlog.php @@ -1,5 +1,6 @@ ['name'] + ]; + // Overridden from Builder to automatically add the company public static function boot() { @@ -200,31 +219,4 @@ class Actionlog extends SnipeModel ->orderBy('created_at', 'asc') ->get(); } - - /** - * Query builder scope to search on text for complex Bootstrap Tables API - * - * @param Illuminate\Database\Query\Builder $query Query builder instance - * @param text $search Search term - * - * @return Illuminate\Database\Query\Builder Modified query builder - */ - public function scopeTextSearch($query, $search) - { - $search = explode(' OR ', $search); - - return $query->where(function ($query) use ($search) { - - foreach ($search as $search) { - $query->where(function ($query) use ($search) { - $query->whereHas('company', function ($query) use ($search) { - $query->where('companies.name', 'LIKE', '%'.$search.'%'); - }); - })->orWhere('action_type', 'LIKE', '%'.$search.'%') - ->orWhere('note', 'LIKE', '%'.$search.'%') - ->orWhere('log_meta', 'LIKE', '%'.$search.'%'); - } - - }); - } } diff --git a/app/Models/Asset.php b/app/Models/Asset.php index 984ca0e085..85a4499a2e 100644 --- a/app/Models/Asset.php +++ b/app/Models/Asset.php @@ -4,11 +4,13 @@ namespace App\Models; use App\Exceptions\CheckoutNotAllowed; use App\Http\Traits\UniqueSerialTrait; use App\Http\Traits\UniqueUndeletedTrait; +use App\Models\Traits\Searchable; use App\Presenters\Presentable; use AssetPresenter; use Auth; use Carbon\Carbon; use Config; +use Illuminate\Database\Eloquent\Builder; use Illuminate\Database\Eloquent\SoftDeletes; use Log; use Watson\Validating\ValidatingTrait; @@ -111,7 +113,42 @@ class Asset extends Depreciable 'warranty_months', ]; + use Searchable; + /** + * The attributes that should be included when searching the model. + * + * @var array + */ + protected $searchableAttributes = [ + 'name', + 'asset_tag', + 'serial', + 'order_number', + 'purchase_cost', + 'notes', + 'created_at', + 'updated_at', + 'purchase_date', + 'expected_checkin', + 'next_audit_date', + 'last_audit_date' + ]; + + /** + * The relations and their attributes that should be included when searching the model. + * + * @var array + */ + protected $searchableRelations = [ + 'assetstatus' => ['name'], + 'supplier' => ['name'], + 'company' => ['name'], + 'defaultLoc' => ['name'], + 'model' => ['name', 'model_number'], + 'model.category' => ['name'], + 'model.manufacturer' => ['name'], + ]; public function getDisplayNameAttribute() { @@ -580,6 +617,49 @@ class Asset extends Depreciable } } + /** + * Run additional, advanced searches. + * + * @param Illuminate\Database\Eloquent\Builder $query + * @param string $term The search term + * @return Illuminate\Database\Eloquent\Builder + */ + public function advancedTextSearch(Builder $query, string $term) { + /** + * Assigned user + */ + $query = $query->leftJoin('users as assets_users',function ($leftJoin) { + $leftJoin->on("assets_users.id", "=", "assets.assigned_to") + ->where("assets.assigned_type", "=", User::class); + }); + $query = $query + ->orWhere('assets_users.first_name', 'LIKE', '%'.$term.'%') + ->orWhere('assets_users.last_name', 'LIKE', '%'.$term.'%') + ->orWhere('assets_users.username', 'LIKE', '%'.$term.'%') + ->orWhereRaw('CONCAT('.DB::getTablePrefix().'assets_users.first_name," ",'.DB::getTablePrefix().'assets_users.last_name) LIKE ?', ["%$term%", "%$term%"]); + + /** + * Assigned location + */ + $query = $query->leftJoin('locations as assets_locations',function ($leftJoin) { + $leftJoin->on("assets_locations.id","=","assets.assigned_to") + ->where("assets.assigned_type","=",Location::class); + }); + + $query = $query->orWhere('assets_locations.name', 'LIKE', '%'.$term.'%'); + + /** + * Assigned assets + */ + $query = $query->leftJoin('assets as assigned_assets',function ($leftJoin) { + $leftJoin->on('assigned_assets.id', '=', 'assets.assigned_to') + ->where('assets.assigned_type', '=', Asset::class); + }); + + $query = $query->orWhere('assigned_assets.name', 'LIKE', '%'.$term.'%'); + + return $query; + } /** * ----------------------------------------------- @@ -813,83 +893,6 @@ class Asset extends Depreciable return $query->where("accepted", "=", "accepted"); } - - /** - * Query builder scope to search on text for complex Bootstrap Tables API. - * This is really horrible, but I can't think of a less-awful way to do it. - * - * @param \Illuminate\Database\Query\Builder $query Query builder instance - * @param text $search Search term - * - * @return \Illuminate\Database\Query\Builder Modified query builder - */ - public function scopeTextSearch($query, $search) - { - $search = explode(' OR ', $search); - - return $query->leftJoin('users as assets_users',function ($leftJoin) { - $leftJoin->on("assets_users.id", "=", "assets.assigned_to") - ->where("assets.assigned_type", "=", User::class); - })->leftJoin('locations as assets_locations',function ($leftJoin) { - $leftJoin->on("assets_locations.id","=","assets.assigned_to") - ->where("assets.assigned_type","=",Location::class); - })->leftJoin('assets as assigned_assets',function ($leftJoin) { - $leftJoin->on('assigned_assets.id', '=', 'assets.assigned_to') - ->where('assets.assigned_type', '=', Asset::class); - })->where(function ($query) use ($search) { - foreach ($search as $search) { - $query->whereHas('model', function ($query) use ($search) { - $query->whereHas('category', function ($query) use ($search) { - $query->where(function ($query) use ($search) { - $query->where('categories.name', 'LIKE', '%'.$search.'%') - ->orWhere('models.name', 'LIKE', '%'.$search.'%') - ->orWhere('models.model_number', 'LIKE', '%'.$search.'%'); - }); - }); - })->orWhereHas('model', function ($query) use ($search) { - $query->whereHas('manufacturer', function ($query) use ($search) { - $query->where(function ($query) use ($search) { - $query->where('manufacturers.name', 'LIKE', '%'.$search.'%'); - }); - }); - - })->orWhere(function ($query) use ($search) { - $query->whereHas('assetstatus', function ($query) use ($search) { - $query->where('status_labels.name', 'LIKE', '%'.$search.'%'); - }); - })->orWhere(function ($query) use ($search) { - $query->whereHas('supplier', function ($query) use ($search) { - $query->where('suppliers.name', 'LIKE', '%'.$search.'%'); - }); - })->orWhere(function ($query) use ($search) { - $query->whereHas('company', function ($query) use ($search) { - $query->where('companies.name', 'LIKE', '%' . $search . '%'); - }); - })->orWhere(function ($query) use ($search) { - $query->whereHas('defaultLoc', function ($query) use ($search) { - $query->where('locations.name', 'LIKE', '%'.$search.'%'); - }); - })->orWhere(function ($query) use ($search) { - $query->where('assets_users.first_name', 'LIKE', '%'.$search.'%') - ->orWhere('assets_users.last_name', 'LIKE', '%'.$search.'%') - ->orWhereRaw('CONCAT('.DB::getTablePrefix().'assets_users.first_name," ",'.DB::getTablePrefix().'assets_users.last_name) LIKE ?', ["%$search%", "%$search%"]) - ->orWhere('assets_users.username', 'LIKE', '%'.$search.'%') - ->orWhere('assets_locations.name', 'LIKE', '%'.$search.'%') - ->orWhere('assigned_assets.name', 'LIKE', '%'.$search.'%'); - })->orWhere('assets.name', 'LIKE', '%'.$search.'%') - ->orWhere('assets.asset_tag', 'LIKE', '%'.$search.'%') - ->orWhere('assets.serial', 'LIKE', '%'.$search.'%') - ->orWhere('assets.order_number', 'LIKE', '%'.$search.'%') - ->orWhere('assets.purchase_cost', 'LIKE', '%'.$search.'%') - ->orWhere('assets.notes', 'LIKE', '%'.$search.'%'); - } - foreach (CustomField::all() as $field) { - $query->orWhere('assets.'.$field->db_column_name(), 'LIKE', "%$search%"); - } - })->withTrashed()->whereNull("assets.deleted_at"); //workaround for laravel bug - } - - /** * Query builder scope to search on text for complex Bootstrap Tables API. * diff --git a/app/Models/AssetMaintenance.php b/app/Models/AssetMaintenance.php index a99fa4ebd6..83ecd8a177 100644 --- a/app/Models/AssetMaintenance.php +++ b/app/Models/AssetMaintenance.php @@ -2,6 +2,7 @@ namespace App\Models; use App\Helpers\Helper; +use App\Models\Traits\Searchable; use Illuminate\Database\Eloquent\Model; use Illuminate\Support\Facades\Lang; use Illuminate\Database\Eloquent\SoftDeletes; @@ -19,7 +20,7 @@ class AssetMaintenance extends Model implements ICompanyableChild use ValidatingTrait; - protected $dates = [ 'deleted_at' ]; + protected $dates = [ 'deleted_at', 'start_date' , 'completion_date']; protected $table = 'asset_maintenances'; // Declaring rules for form validation protected $rules = [ @@ -34,6 +35,23 @@ class AssetMaintenance extends Model implements ICompanyableChild 'cost' => 'numeric|nullable' ]; + use Searchable; + + /** + * The attributes that should be included when searching the model. + * + * @var array + */ + protected $searchableAttributes = ['title', 'notes', 'asset_maintenance_type', 'cost', 'start_date', 'completion_date']; + + /** + * The relations and their attributes that should be included when searching the model. + * + * @var array + */ + protected $searchableRelations = []; + + public function getCompanyableParents() { return [ 'asset' ]; @@ -139,30 +157,8 @@ class AssetMaintenance extends Model implements ICompanyableChild * ----------------------------------------------- * BEGIN QUERY SCOPES * ----------------------------------------------- - **/ - + **/ - /** - * Query builder scope to search on text - * - * @param Illuminate\Database\Query\Builder $query Query builder instance - * @param text $search Search term - * - * @return Illuminate\Database\Query\Builder Modified query builder - */ - public function scopeTextSearch($query, $search) - { - - return $query->where(function ($query) use ($search) { - - $query->where('asset_maintenances.title', 'LIKE', '%'.$search.'%') - ->orWhere('asset_maintenances.notes', 'LIKE', '%'.$search.'%') - ->orWhere('asset_maintenances.asset_maintenance_type', 'LIKE', '%'.$search.'%') - ->orWhere('asset_maintenances.cost', 'LIKE', '%'.$search.'%') - ->orWhere('asset_maintenances.start_date', 'LIKE', '%'.$search.'%') - ->orWhere('asset_maintenances.completion_date', 'LIKE', '%'.$search.'%'); - }); - } /** * Query builder scope to order on admin user diff --git a/app/Models/AssetModel.php b/app/Models/AssetModel.php index dde11702ac..9c18f522a5 100755 --- a/app/Models/AssetModel.php +++ b/app/Models/AssetModel.php @@ -3,6 +3,7 @@ namespace App\Models; use App\Models\Requestable; use App\Models\SnipeModel; +use App\Models\Traits\Searchable; use App\Presenters\Presentable; use Illuminate\Database\Eloquent\Model; use Illuminate\Database\Eloquent\SoftDeletes; @@ -68,6 +69,26 @@ class AssetModel extends SnipeModel 'user_id', ]; + use Searchable; + + /** + * The attributes that should be included when searching the model. + * + * @var array + */ + protected $searchableAttributes = ['name', 'model_number', 'notes', 'eol']; + + /** + * The relations and their attributes that should be included when searching the model. + * + * @var array + */ + protected $searchableRelations = [ + 'depreciation' => ['name'], + 'category' => ['name'], + 'manufacturer' => ['name'], + ]; + public function assets() { return $this->hasMany('\App\Models\Asset', 'model_id'); @@ -160,38 +181,7 @@ class AssetModel extends SnipeModel { return $query->where('requestable', '1'); - } - - /** - * Query builder scope to search on text - * - * @param Illuminate\Database\Query\Builder $query Query builder instance - * @param text $search Search term - * - * @return Illuminate\Database\Query\Builder Modified query builder - */ - public function scopeTextSearch($query, $search) - { - - return $query->where('models.name', 'LIKE', "%$search%") - ->orWhere('model_number', 'LIKE', "%$search%") - ->orWhere(function ($query) use ($search) { - $query->whereHas('depreciation', function ($query) use ($search) { - $query->where('depreciations.name', 'LIKE', '%'.$search.'%'); - }); - }) - ->orWhere(function ($query) use ($search) { - $query->whereHas('category', function ($query) use ($search) { - $query->where('categories.name', 'LIKE', '%'.$search.'%'); - }); - }) - ->orWhere(function ($query) use ($search) { - $query->whereHas('manufacturer', function ($query) use ($search) { - $query->where('manufacturers.name', 'LIKE', '%'.$search.'%'); - }); - }); - - } + } /** * Query builder scope to search on text, including catgeory and manufacturer name diff --git a/app/Models/Category.php b/app/Models/Category.php index 8a598e9b10..7650cdde76 100755 --- a/app/Models/Category.php +++ b/app/Models/Category.php @@ -3,6 +3,7 @@ namespace App\Models; use App\Http\Traits\UniqueUndeletedTrait; use App\Models\SnipeModel; +use App\Models\Traits\Searchable; use App\Presenters\Presentable; use Illuminate\Database\Eloquent\Model; use Illuminate\Database\Eloquent\SoftDeletes; @@ -63,6 +64,21 @@ class Category extends SnipeModel 'user_id', ]; + use Searchable; + + /** + * The attributes that should be included when searching the model. + * + * @var array + */ + protected $searchableAttributes = ['name', 'category_type']; + + /** + * The relations and their attributes that should be included when searching the model. + * + * @var array + */ + protected $searchableRelations = []; public function has_models() { @@ -143,22 +159,4 @@ class Category extends SnipeModel return $query->where('require_acceptance', '=', true); } - - /** - * Query builder scope to search on text - * - * @param Illuminate\Database\Query\Builder $query Query builder instance - * @param text $search Search term - * - * @return Illuminate\Database\Query\Builder Modified query builder - */ - public function scopeTextSearch($query, $search) - { - - return $query->where(function ($query) use ($search) { - - $query->where('name', 'LIKE', '%'.$search.'%') - ->orWhere('category_type', 'LIKE', '%'.$search.'%'); - }); - } } diff --git a/app/Models/Company.php b/app/Models/Company.php index 76260311f7..391a79d6b2 100644 --- a/app/Models/Company.php +++ b/app/Models/Company.php @@ -2,6 +2,7 @@ namespace App\Models; use App\Models\SnipeModel; +use App\Models\Traits\Searchable; use App\Presenters\Presentable; use Auth; use DB; @@ -35,6 +36,21 @@ final class Company extends SnipeModel protected $injectUniqueIdentifier = true; use ValidatingTrait; + use Searchable; + + /** + * The attributes that should be included when searching the model. + * + * @var array + */ + protected $searchableAttributes = ['name', 'created_at', 'updated_at']; + + /** + * The relations and their attributes that should be included when searching the model. + * + * @var array + */ + protected $searchableRelations = []; /** * The attributes that are mass assignable. @@ -192,20 +208,4 @@ final class Company extends SnipeModel { return $this->hasMany(Component::class, 'company_id'); } - - /** - * Query builder scope to search on text - * - * @param Illuminate\Database\Query\Builder $query Query builder instance - * @param text $search Search term - * - * @return Illuminate\Database\Query\Builder Modified query builder - */ - public function scopeTextSearch($query, $search) - { - - return $query->where(function ($query) use ($search) { - $query->where('name', 'LIKE', '%'.$search.'%'); - }); - } } diff --git a/app/Models/Component.php b/app/Models/Component.php index f7b9bb9fcd..cc973c6bf5 100644 --- a/app/Models/Component.php +++ b/app/Models/Component.php @@ -1,6 +1,7 @@ ['name'], + 'company' => ['name'], + 'location' => ['name'], + ]; + public function location() { return $this->belongsTo('\App\Models\Location', 'location_id'); @@ -114,49 +135,7 @@ class Component extends SnipeModel $total = $this->qty; $remaining = $total - $checkedout; return $remaining; - } - - - /** - * Query builder scope to search on text - * - * @param Illuminate\Database\Query\Builder $query Query builder instance - * @param text $search Search term - * - * @return Illuminate\Database\Query\Builder Modified query builder - */ - /** - * Query builder scope to search on text - * - * @param Illuminate\Database\Query\Builder $query Query builder instance - * @param text $search Search term - * - * @return Illuminate\Database\Query\Builder Modified query builder - */ - public function scopeTextSearch($query, $search) - { - $search = explode(' ', $search); - - return $query->where(function ($query) use ($search) { - - foreach ($search as $search) { - $query->whereHas('category', function ($query) use ($search) { - $query->where('categories.name', 'LIKE', '%'.$search.'%'); - })->orWhere(function ($query) use ($search) { - $query->whereHas('company', function ($query) use ($search) { - $query->where('companies.name', 'LIKE', '%'.$search.'%'); - }); - })->orWhere(function ($query) use ($search) { - $query->whereHas('location', function ($query) use ($search) { - $query->where('locations.name', 'LIKE', '%'.$search.'%'); - }); - })->orWhere('components.name', 'LIKE', '%'.$search.'%') - ->orWhere('components.order_number', 'LIKE', '%'.$search.'%') - ->orWhere('components.serial', 'LIKE', '%'.$search.'%') - ->orWhere('components.purchase_cost', 'LIKE', '%'.$search.'%'); - } - }); - } + } /** * Query builder scope to order on company diff --git a/app/Models/Consumable.php b/app/Models/Consumable.php index b4addc7f87..23487c5ba8 100644 --- a/app/Models/Consumable.php +++ b/app/Models/Consumable.php @@ -1,6 +1,7 @@ ['name'], + 'company' => ['name'], + 'location' => ['name'], + 'manufacturer' => ['name'], + ]; + public function setRequestableAttribute($value) { if ($value == '') { @@ -163,50 +185,6 @@ class Consumable extends SnipeModel return $remaining; } - /** - * Query builder scope to search on text - * - * @param Illuminate\Database\Query\Builder $query Query builder instance - * @param text $search Search term - * - * @return Illuminate\Database\Query\Builder Modified query builder - */ - /** - * Query builder scope to search on text - * - * @param Illuminate\Database\Query\Builder $query Query builder instance - * @param text $search Search term - * - * @return Illuminate\Database\Query\Builder Modified query builder - */ - public function scopeTextSearch($query, $search) - { - $search = explode(' ', $search); - - return $query->where(function ($query) use ($search) { - - foreach ($search as $search) { - $query->whereHas('category', function ($query) use ($search) { - $query->where('categories.name', 'LIKE', '%'.$search.'%'); - })->orWhere(function ($query) use ($search) { - $query->whereHas('company', function ($query) use ($search) { - $query->where('companies.name', 'LIKE', '%'.$search.'%'); - }); - })->orWhere(function ($query) use ($search) { - $query->whereHas('location', function ($query) use ($search) { - $query->where('locations.name', 'LIKE', '%'.$search.'%'); - }); - })->orWhere(function ($query) use ($search) { - $query->whereHas('manufacturer', function ($query) use ($search) { - $query->where('manufacturers.name', 'LIKE', '%'.$search.'%'); - }); - })->orWhere('consumables.name', 'LIKE', '%'.$search.'%') - ->orWhere('consumables.order_number', 'LIKE', '%'.$search.'%') - ->orWhere('consumables.purchase_cost', 'LIKE', '%'.$search.'%'); - } - }); - } - /** * Query builder scope to order on company * diff --git a/app/Models/Department.php b/app/Models/Department.php index 1ed97ecc76..d9cab7a738 100644 --- a/app/Models/Department.php +++ b/app/Models/Department.php @@ -3,6 +3,7 @@ namespace App\Models; use App\Http\Traits\UniqueUndeletedTrait; +use App\Models\Traits\Searchable; use Illuminate\Database\Eloquent\Model; use Log; use Watson\Validating\ValidatingTrait; @@ -45,6 +46,22 @@ class Department extends SnipeModel 'notes', ]; + use Searchable; + + /** + * The attributes that should be included when searching the model. + * + * @var array + */ + protected $searchableAttributes = ['name', 'notes']; + + /** + * The relations and their attributes that should be included when searching the model. + * + * @var array + */ + protected $searchableRelations = []; + public function company() { @@ -76,23 +93,7 @@ class Department extends SnipeModel { return $this->belongsTo('\App\Models\Location', 'location_id'); } - - - /** - * Query builder scope to search on text - * - * @param Illuminate\Database\Query\Builder $query Query builder instance - * @param text $search Search term - * - * @return Illuminate\Database\Query\Builder Modified query builder - */ - public function scopeTextsearch($query, $search) - { - return $query->where('name', 'LIKE', "%$search%") - ->orWhere('notes', 'LIKE', "%$search%"); - - } - + /** * Query builder scope to order on location name * diff --git a/app/Models/Depreciation.php b/app/Models/Depreciation.php index c9b90e4da4..e67d4d7fa4 100755 --- a/app/Models/Depreciation.php +++ b/app/Models/Depreciation.php @@ -1,6 +1,7 @@ hasMany('\App\Models\License', 'depreciation_id')->count(); - } - - /** - * Query builder scope to search on text - * - * @param \Illuminate\Database\Query\Builder $query Query builder instance - * @param text $search Search term - * - * @return \Illuminate\Database\Query\Builder Modified query builder - */ - public function scopeTextSearch($query, $search) - { - - return $query->where(function ($query) use ($search) { - - $query->where('name', 'LIKE', '%'.$search.'%') - ->orWhere('months', 'LIKE', '%'.$search.'%'); - }); - } + } } diff --git a/app/Models/Group.php b/app/Models/Group.php index 1938d6b965..1003e0d03f 100755 --- a/app/Models/Group.php +++ b/app/Models/Group.php @@ -2,6 +2,7 @@ namespace App\Models; use App\Models\SnipeModel; +use App\Models\Traits\Searchable; use Watson\Validating\ValidatingTrait; class Group extends SnipeModel @@ -22,6 +23,21 @@ class Group extends SnipeModel protected $injectUniqueIdentifier = true; use ValidatingTrait; + use Searchable; + + /** + * The attributes that should be included when searching the model. + * + * @var array + */ + protected $searchableAttributes = ['name', 'created_at']; + + /** + * The relations and their attributes that should be included when searching the model. + * + * @var array + */ + protected $searchableRelations = []; /** * Get user groups @@ -36,21 +52,4 @@ class Group extends SnipeModel { return json_decode($this->permissions, true); } - - /** - * Query builder scope to search on text - * - * @param Illuminate\Database\Query\Builder $query Query builder instance - * @param text $search Search term - * - * @return Illuminate\Database\Query\Builder Modified query builder - */ - public function scopeTextSearch($query, $search) - { - - return $query->where(function ($query) use ($search) { - - $query->where('name', 'LIKE', '%'.$search.'%'); - }); - } } diff --git a/app/Models/License.php b/app/Models/License.php index 5959819c22..559b48ebe8 100755 --- a/app/Models/License.php +++ b/app/Models/License.php @@ -5,6 +5,7 @@ use App\Models\Actionlog; use App\Models\Company; use App\Models\LicenseSeat; use App\Models\Loggable; +use App\Models\Traits\Searchable; use App\Presenters\Presentable; use Carbon\Carbon; use DB; @@ -35,7 +36,9 @@ class License extends Depreciable 'created_at', 'updated_at', 'deleted_at', - 'purchase_date' + 'purchase_date', + 'expiration_date', + 'termination_date' ]; @@ -81,6 +84,34 @@ class License extends Depreciable 'user_id', ]; + use Searchable; + + /** + * The attributes that should be included when searching the model. + * + * @var array + */ + protected $searchableAttributes = [ + 'name', + 'serial', + 'notes', + 'order_number', + 'purchase_order', + 'purchase_cost', + 'purchase_date', + 'expiration_date', + ]; + + /** + * The relations and their attributes that should be included when searching the model. + * + * @var array + */ + protected $searchableRelations = [ + 'manufacturer' => ['name'], + 'company' => ['name'], + ]; + public static function boot() { parent::boot(); @@ -414,39 +445,6 @@ class License extends Depreciable } - /** - * Query builder scope to search on text - * - * @param Illuminate\Database\Query\Builder $query Query builder instance - * @param text $search Search term - * - * @return Illuminate\Database\Query\Builder Modified query builder - */ - public function scopeTextSearch($query, $search) - { - - return $query->where(function ($query) use ($search) { - - $query->where('licenses.name', 'LIKE', '%'.$search.'%') - ->orWhere('licenses.serial', 'LIKE', '%'.$search.'%') - ->orWhere('licenses.notes', 'LIKE', '%'.$search.'%') - ->orWhere('licenses.order_number', 'LIKE', '%'.$search.'%') - ->orWhere('licenses.purchase_order', 'LIKE', '%'.$search.'%') - ->orWhere('licenses.purchase_date', 'LIKE', '%'.$search.'%') - ->orWhere('licenses.purchase_cost', 'LIKE', '%'.$search.'%') - ->orWhereHas('manufacturer', function ($query) use ($search) { - $query->where(function ($query) use ($search) { - $query->where('manufacturers.name', 'LIKE', '%'.$search.'%'); - }); - }) - ->orWhereHas('company', function ($query) use ($search) { - $query->where(function ($query) use ($search) { - $query->where('companies.name', 'LIKE', '%'.$search.'%'); - }); - }); - }); - } - /** * Query builder scope to order on manufacturer * diff --git a/app/Models/Location.php b/app/Models/Location.php index 2da77b7ff5..1e676854c3 100755 --- a/app/Models/Location.php +++ b/app/Models/Location.php @@ -4,6 +4,7 @@ namespace App\Models; use App\Http\Traits\UniqueUndeletedTrait; use App\Models\Asset; use App\Models\SnipeModel; +use App\Models\Traits\Searchable; use App\Models\User; use App\Presenters\Presentable; use Illuminate\Database\Eloquent\Model; @@ -60,6 +61,24 @@ class Location extends SnipeModel ]; protected $hidden = ['user_id']; + use Searchable; + + /** + * The attributes that should be included when searching the model. + * + * @var array + */ + protected $searchableAttributes = ['name', 'address', 'city', 'state', 'zip', 'created_at']; + + /** + * The relations and their attributes that should be included when searching the model. + * + * @var array + */ + protected $searchableRelations = [ + 'parent' => ['name'] + ]; + public function users() { return $this->hasMany('\App\Models\User', 'location_id'); @@ -171,39 +190,6 @@ class Location extends SnipeModel return $location_options; } - /** - * Query builder scope to search on text - * - * @param Illuminate\Database\Query\Builder $query Query builder instance - * @param text $search Search term - * - * @return Illuminate\Database\Query\Builder Modified query builder - */ - public function scopeTextsearch($query, $search) - { - - return $query->where('name', 'LIKE', "%$search%") - ->orWhere('address', 'LIKE', "%$search%") - ->orWhere('city', 'LIKE', "%$search%") - ->orWhere('state', 'LIKE', "%$search%") - ->orWhere('zip', 'LIKE', "%$search%") - - // This doesn't actually work - need to use a table alias maybe? - ->orWhere(function ($query) use ($search) { - $query->whereHas('parent', function ($query) use ($search) { - $query->where(function ($query) use ($search) { - $query->where('name', 'LIKE', '%'.$search.'%'); - }); - }) - // Ugly, ugly code because Laravel sucks at self-joins - ->orWhere(function ($query) use ($search) { - $query->whereRaw("parent_id IN (select id from ".DB::getTablePrefix()."locations where name LIKE '%".$search."%') "); - }); - }); - - } - - /** * Query builder scope to order on parent * diff --git a/app/Models/Manufacturer.php b/app/Models/Manufacturer.php index 5cd7bcc15d..81dbfdac8b 100755 --- a/app/Models/Manufacturer.php +++ b/app/Models/Manufacturer.php @@ -1,6 +1,7 @@ hasMany('\App\Models\Consumable', 'manufacturer_id'); } - - /** - * Query builder scope to search on text - * - * @param Illuminate\Database\Query\Builder $query Query builder instance - * @param text $search Search term - * - * @return Illuminate\Database\Query\Builder Modified query builder - */ - public function scopeTextSearch($query, $search) - { - - return $query->where(function ($query) use ($search) { - - $query->where('name', 'LIKE', '%'.$search.'%'); - }); - } } diff --git a/app/Models/Statuslabel.php b/app/Models/Statuslabel.php index 6a700b973e..fa6f6df1ba 100755 --- a/app/Models/Statuslabel.php +++ b/app/Models/Statuslabel.php @@ -3,6 +3,7 @@ namespace App\Models; use App\Http\Traits\UniqueUndeletedTrait; use App\Models\SnipeModel; +use App\Models\Traits\Searchable; use Illuminate\Database\Eloquent\Model; use Illuminate\Database\Eloquent\SoftDeletes; use Watson\Validating\ValidatingTrait; @@ -35,6 +36,22 @@ class Statuslabel extends SnipeModel 'pending', ]; + use Searchable; + + /** + * The attributes that should be included when searching the model. + * + * @var array + */ + protected $searchableAttributes = ['name']; + + /** + * The relations and their attributes that should be included when searching the model. + * + * @var array + */ + protected $searchableRelations = []; + /** * Get assets with associated status label @@ -108,21 +125,4 @@ class Statuslabel extends SnipeModel return $statustype; } - - /** - * Query builder scope to search on text - * - * @param Illuminate\Database\Query\Builder $query Query builder instance - * @param text $search Search term - * - * @return Illuminate\Database\Query\Builder Modified query builder - */ - public function scopeTextSearch($query, $search) - { - - return $query->where(function ($query) use ($search) { - - $query->where('name', 'LIKE', '%'.$search.'%'); - }); - } } diff --git a/app/Models/Supplier.php b/app/Models/Supplier.php index d44f0bec29..82c3a0770c 100755 --- a/app/Models/Supplier.php +++ b/app/Models/Supplier.php @@ -3,6 +3,7 @@ namespace App\Models; use App\Http\Traits\UniqueUndeletedTrait; use App\Models\SnipeModel; +use App\Models\Traits\Searchable; use Illuminate\Database\Eloquent\Model; use Illuminate\Database\Eloquent\SoftDeletes; use Watson\Validating\ValidatingTrait; @@ -40,6 +41,23 @@ class Supplier extends SnipeModel use ValidatingTrait; use UniqueUndeletedTrait; + use Searchable; + + /** + * The attributes that should be included when searching the model. + * + * @var array + */ + protected $searchableAttributes = ['name']; + + /** + * The relations and their attributes that should be included when searching the model. + * + * @var array + */ + protected $searchableRelations = []; + + /** * The attributes that are mass assignable. * @@ -104,21 +122,4 @@ class Supplier extends SnipeModel } return $url; } - - /** - * Query builder scope to search on text - * - * @param Illuminate\Database\Query\Builder $query Query builder instance - * @param text $search Search term - * - * @return Illuminate\Database\Query\Builder Modified query builder - */ - public function scopeTextSearch($query, $search) - { - - return $query->where(function ($query) use ($search) { - - $query->where('name', 'LIKE', '%'.$search.'%'); - }); - } } diff --git a/app/Models/Traits/Searchable.php b/app/Models/Traits/Searchable.php new file mode 100644 index 0000000000..3039e291d8 --- /dev/null +++ b/app/Models/Traits/Searchable.php @@ -0,0 +1,238 @@ + + */ +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); + + foreach($terms as $term) { + + /** + * Search the attributes of this model + */ + $query = $this->searchAttributes($query, $term); + + /** + * Search through the custom fields of the model + */ + $query = $this->searchCustomFields($query, $term); + + /** + * Search through the relations of the model + */ + $query = $this->searchRelations($query, $term); + + /** + * Search for additional attributes defined by the model + */ + $query = $this->advancedTextSearch($query, $term); + } + + 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); + } + + /** + * Searches the models attributes for the search term + * + * @param Illuminate\Database\Eloquent\Builder $query + * @param string $term + * @return Illuminate\Database\Eloquent\Builder + */ + private function searchAttributes(Builder $query, string $term) { + + foreach($this->getSearchableAttributes() as $column) { + + /** + * 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; + } + + $table = $this->getTable(); + + $query = $query->orWhere($table . '.' . $column, 'LIKE', '%'.$term.'%'); + } + + return $query; + } + + /** + * Searches the models custom fields for the search term + * + * @param Illuminate\Database\Eloquent\Builder $query + * @param string $term + * @return Illuminate\Database\Eloquent\Builder + */ + private function searchCustomFields(Builder $query, string $term) { + + /** + * If we are searching on something other that an asset, skip custom fields. + */ + if (! $this instanceof Asset) { + return $query; + } + + foreach (CustomField::all() as $field) { + $query->orWhere($this->getTable() . '.'. $field->db_column_name(), 'LIKE', '%'.$term.'%'); + } + + return $query; + } + + /** + * Searches the models relations for the search term + * + * @param Illuminate\Database\Eloquent\Builder $query + * @param string $term + * @return Illuminate\Database\Eloquent\Builder + */ + private function searchRelations(Builder $query, string $term) { + + foreach($this->getSearchableRelations() as $relation => $columns) { + + /** + * Make the columns into a collection, + * for easier handling further down + * + * @var Illuminate\Support\Collection + */ + $columns = collect($columns); + + $query = $query->orWhereHas($relation, function($query) use ($relation, $columns, $term) { + + $table = $this->getRelationTable($relation); + + /** + * We need to form the query properly, starting with a "where", + * otherwise the generated nested select is wrong. + * + * We can just choose the last column, since they all get "and where"d in the end. + * (And because using pop saves us from handling the removal of the first element) + */ + $last = $columns->pop(); + $query->where($table . '.' . $last, 'LIKE', '%'.$term.'%'); + + foreach($columns as $column) { + $query->orWhere($table . '.' . $column, 'LIKE', '%'.$term.'%'); + } + + }); + } + + 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 + * @param string $term The search term + * @return Illuminate\Database\Eloquent\Builder + * + * @SuppressWarnings(PHPMD.UnusedFormalParameter) + */ + public function advancedTextSearch(Builder $query, string $term) { + 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(); + } +} diff --git a/app/Models/User.php b/app/Models/User.php index c2ccd35be6..a915a53d58 100755 --- a/app/Models/User.php +++ b/app/Models/User.php @@ -1,6 +1,7 @@ 'max:10|nullable', ]; + use Searchable; + + /** + * The attributes that should be included when searching the model. + * + * @var array + */ + protected $searchableAttributes = [ + 'first_name', + 'last_name', + 'email', + 'username', + 'notes', + 'phone', + 'jobtitle', + 'employee_num' + ]; + + /** + * The relations and their attributes that should be included when searching the model. + * + * @var array + */ + protected $searchableRelations = [ + 'userloc' => ['name'], + 'department' => ['name'], + 'groups' => ['name'], + 'manager' => ['first_name', 'last_name', 'username'] + ]; public function hasAccess($section) { @@ -405,6 +436,19 @@ class User extends SnipeModel implements AuthenticatableContract, CanResetPasswo return json_decode($this->permissions, true); } + /** + * Run additional, advanced searches. + * + * @param Illuminate\Database\Eloquent\Builder $query + * @param string $term The search term + * @return Illuminate\Database\Eloquent\Builder + */ + public function advancedTextSearch(Builder $query, string $term) { + $query = $query->orWhereRaw('CONCAT('.DB::getTablePrefix().'users.first_name," ",'.DB::getTablePrefix().'users.last_name) LIKE ?', ["%$term%", "%$term%"]); + + return $query; + } + public function scopeByGroup($query, $id) { return $query->whereHas('groups', function ($query) use ($id) { @@ -412,55 +456,6 @@ class User extends SnipeModel implements AuthenticatableContract, CanResetPasswo }); } - - /** - * Query builder scope to search on text - * - * @param Illuminate\Database\Query\Builder $query Query builder instance - * @param text $search Search term - * - * @return Illuminate\Database\Query\Builder Modified query builder - */ - public function scopeTextsearch($query, $search) - { - - return $query->where(function ($query) use ($search) { - $query->where('users.first_name', 'LIKE', "%$search%") - ->orWhere('users.last_name', 'LIKE', "%$search%") - ->orWhere('users.email', 'LIKE', "%$search%") - ->orWhere('users.username', 'LIKE', "%$search%") - ->orWhere('users.notes', 'LIKE', "%$search%") - ->orWhere('users.phone', 'LIKE', "%$search%") - ->orWhere('users.jobtitle', 'LIKE', "%$search%") - ->orWhere('users.employee_num', 'LIKE', "%$search%") - ->orWhereRaw('CONCAT('.DB::getTablePrefix().'users.first_name," ",'.DB::getTablePrefix().'users.last_name) LIKE ?', ["%$search%", "%$search%"]) - ->orWhere(function ($query) use ($search) { - $query->whereHas('userloc', function ($query) use ($search) { - $query->where('locations.name', 'LIKE', '%'.$search.'%'); - }); - }) - ->orWhere(function ($query) use ($search) { - $query->whereHas('department', function ($query) use ($search) { - $query->where('departments.name', 'LIKE', '%'.$search.'%'); - }); - }) - ->orWhere(function ($query) use ($search) { - $query->whereHas('groups', function ($query) use ($search) { - $query->where('groups.name', 'LIKE', '%'.$search.'%'); - }); - }) - - //Ugly, ugly code because Laravel sucks at self-joins - ->orWhere(function ($query) use ($search) { - $query->whereRaw(DB::getTablePrefix()."users.manager_id IN (select id from ".DB::getTablePrefix()."users where first_name LIKE ? OR last_name LIKE ?)", ["%$search%", "%$search%"]); - }); - - - }); - - } - - /** * Query builder scope for Deleted users *