layout | title | share-description | gh-repo | gh-badge | tags | comments | head-extra | |||
---|---|---|---|---|---|---|---|---|---|---|
post |
Laravel Package Development - Create Searchable Package |
I will show you how I built a simple laravel package I named laravel-searchable. laravel-searchable is a trait for Laravel 6+ that adds a simple search function to Eloquent Models. laravel-searchable make it simple for you to perform searches in multiple columns. |
chebaby/blog |
|
|
false |
article_structured_data.html |
In the previous article, we have seen how we can create a searchable trait that adds searching functionality to our Eloquent Models. Today we are going to encapsulate this trait in a package so it can be used in different projects easily.
If you have followed along the previous article, you will know that we had two mandatory files:
app/Concerns/Models/Searchable
trait which contains the logic of the search query scope.
config/searchable.php
configuration file which contains default configuration options, in our case it’s just a request query key.
An easy way to quickly get started with Laravel Package Developing is using Laravel Package Boilerplate{:target="_blank"} it comes with pre-configured continuous integration services out of the box.
Since we are building specifically a Laravel package, select Laravel and click next button.
Fill in some basic informations about the package, and click next button.
Finally download the Zip file.
Extract the Zip to a folder, and open it in your preferred IDE.
Inside the src
folder is where we put our Searchable trait. But before we do that there are some updates I have to make to the default boilerplate files.
One of these is to remove LaravelSearchableFacade
we don’t need that.
We remove it also from the extra
section of the package's composer.json
file.
"extra": {
"laravel": {
"providers": [
"Chebaby\\LaravelSearchable\\LaravelSearchableServiceProvider"
],
"aliases": {
"LaravelSearchable": "Chebaby\\LaravelSearchable\\LaravelSearchableFacade"
}
}
}
Remove aliases
key.
"extra": {
"laravel": {
"providers": [
"Chebaby\\LaravelSearchable\\LaravelSearchableServiceProvider"
]
}
}
And then remove it's reference from register
method of LaravelSearchableServiceProvider
.
One more thing is to rename the LaravelSearchable
class to Searchable
trait.
Inside the config folder there is a config file where we can place custom package configurations. As stated previously, for now, it’s just a request query key.
<?php
return [
// query key
// e.g http://example.com/search?q=searchTerm
'key' => 'q',
];
For the Searchable logic we can copy it from tha last section in the previous article to the src/Searchable.php
file of our package:
<?php
namespace App\Concerns\Models;
use Illuminate\Support\Arr;
use Illuminate\Database\Eloquent\Builder;
trait Searchable
{
/**
* Scope a query to search for a term in the attributes
*
* @param \Illuminate\Database\Eloquent\Builder $query
* @return \Illuminate\Database\Eloquent\Builder
*/
protected function scopeSearch($query)
{
[$searchTerm, $attributes] = $this->parseArguments(func_get_args());
if (!$searchTerm || !$attributes) {
return $query;
}
return $query->where(function (Builder $query) use ($attributes, $searchTerm) {
foreach (Arr::wrap($attributes) as $attribute) {
$query->when(
str_contains($attribute, '.'),
function (Builder $query) use ($attribute, $searchTerm) {
[$relationName, $relationAttribute] = explode('.', $attribute);
$query->orWhereHas($relationName, function (Builder $query) use ($relationAttribute, $searchTerm) {
$query->where($relationAttribute, 'LIKE', "%{$searchTerm}%");
});
},
function (Builder $query) use ($attribute, $searchTerm) {
$query->orWhere($attribute, 'LIKE', "%{$searchTerm}%");
}
);
}
});
}
/**
* Parse search scope arguments
*
* @param array $arguments
* @return array
*/
private function parseArguments(array $arguments)
{
$args_count = count($arguments);
switch ($args_count) {
case 1:
return [request(config('searchable.key')), $this->searchableAttributes()];
break;
case 2:
return is_string($arguments[1])
? [$arguments[1], $this->searchableAttributes()]
: [request(config('searchable.key')), $arguments[1]];
break;
case 3:
return is_string($arguments[1])
? [$arguments[1], $arguments[2]]
: [$arguments[2], $arguments[1]];
break;
default:
return [null, []];
break;
}
}
/**
* Get searchable columns
*
* @return array
*/
public function searchableAttributes()
{
if (method_exists($this, 'searchable')) {
return $this->searchable();
}
return property_exists($this, 'searchable') ? $this->searchable : [];
}
}
When writing packages, your package will not typically have access to all of Laravel's testing helpers. If you would like to be able to write your package tests as if the package were installed inside a typical Laravel application, you may use the Orchestral Testbench package. Which is already in the composer.json
of the Laravel Package Boilerplate, so all we have to do is to run composer install
to install package dependencies.
To reduce setup configuration, you could use testing database connection (:memory:
with sqlite driver) via setting it up under getEnvironmentSetUp()
or by defining it under PHPUnit Configuration File phpunit.xml.dist
:
<phpunit>
// ...
<php>
<env name="DB_CONNECTION" value="testing"/>
</php>
</phpunit>
To run migrations that are only used for testing purposes and not part of your package, add the following to your base test class tests/TestCase.php
<?php
namespace Chebaby\LaravelSearchable\Tests;
use Illuminate\Foundation\Application;
use Illuminate\Support\Facades\Schema;
use Illuminate\Database\Schema\Blueprint;
use Orchestra\Testbench\TestCase as BaseTestCase;
class TestCase extends BaseTestCase
{
protected function setUp(): void
{
parent::setUp();
$this->setUpDatabase($this->app);
}
protected function setUpDatabase(Application $app)
{
Schema::create('countries', function (Blueprint $table) {
$table->increments('id');
$table->string('name')->nullable();
});
Schema::create('users', function (Blueprint $table) {
$table->increments('id');
$table->string('name')->nullable();
$table->string('email')->nullable();
$table->string('phone')->nullable();
$table->unsignedBigInteger('country_id')->nullable();
$table->foreign('country_id')->references('id')->on('countries')->onDelete('SET NULL');
});
}
}
We create migration to two table countries
and users
so we can test searching for an attribute in a related model.
Inside the tests
folder we create another folder with the name Models, you guessed it right, it is where our models live.
Rename ExampleTest to SearchableTest to start writing our tests.
In the last article, there is some code snippets showing the trait usage, based on those snippets, I'm whriting tests to cover those use cases.
User::search('searchTerm', ['name', 'email'])->get();
/**
* SELECT * FROM `users`
* WHERE `name` LIKE '%searchTerm%'
* OR `email` LIKE '%searchTerm%'
*/
/** @test */
public function it_can_search_for_a_term_in_multiple_attributes()
{
User::create([
'name' => 'john doe',
'email' => '[email protected]'
]);
$user = User::search('john', ['name', 'email'])->first();
$this->assertEquals('john doe', $user->name);
$this->assertEquals('[email protected]', $user->email);
}
User::search(['name', 'email'], 'searchTerm')->get();
/**
* SELECT * FROM `users`
* WHERE `name` LIKE '%searchTerm%'
* OR `email` LIKE '%searchTerm%'
*/
/** @test */
public function it_can_search_in_multiple_attributes_for_a_term()
{
User::create([
'name' => 'jane doe',
'email' => '[email protected]'
]);
$user = User::search(['name', 'email'], 'jane')->first();
$this->assertEquals('jane doe', $user->name);
$this->assertEquals('[email protected]', $user->email);
}
User::search('searchTerm')->get();
/**
* SELECT * FROM `users`
* WHERE `name` LIKE '%searchTerm%'
* OR `email` LIKE '%searchTerm%'
*/
/** @test */
public function it_can_search_for_a_term_without_passing_attributes_using_searchable_property()
{
UserWithSearchableProperty::create([
'name' => 'Johnny doe',
'email' => '[email protected]'
]);
$user = UserWithSearchableProperty::search('johnny')->first();
$this->assertEquals('Johnny doe', $user->name);
$this->assertEquals('[email protected]', $user->email);
}
/** @test */
public function it_can_search_for_a_term_without_passing_attributes_using_searchable_method()
{
UserWithSearchableMethod::create([
'name' => 'Richard Roe',
'email' => '[email protected]'
]);
$user = UserWithSearchableMethod::search('richard')->first();
$this->assertEquals('Richard Roe', $user->name);
$this->assertEquals('[email protected]', $user->email);
}
User::search(['name', 'email', 'phone'])->get();
/**
* SELECT * FROM `users`
* WHERE `name` LIKE '%searchTerm%'
* OR `email` LIKE '%searchTerm%'
* OR `phone` LIKE '%searchTerm%'
*/
/** @test */
public function it_can_search_in_multiple_attributes_without_passing_a_term()
{
User::create([
'name' => 'Janie Roe',
'email' => '[email protected]',
]);
// simulate GET request with query parameter "?q=janie"
request()->query->add(['q' => 'janie']);
$user = User::search(['name', 'email', 'phone'])->first();
$this->assertEquals('Janie Roe', $user->name);
$this->assertEquals('[email protected]', $user->email);
request()->query->remove('q');
// update request query key
config(['searchable.key' => 'keyword']);
// simulate GET request with query parameter "?keyword=janie"
request()->query->add(['keyword' => 'janie']);
$user = User::search(['name', 'email', 'phone'])->first();
$this->assertEquals('Janie Roe', $user->name);
$this->assertEquals('[email protected]', $user->email);
}