Tạo Laravel Package - MySql Search Fulltext với Laravel Scout
# Introduce:
Làm việc với Laravel thì việc sử dụng các package rất phổ biến. Đóng gói các thư viện thành package sẽ giúp cho việc phát triễn, maintain hay thay thế rất dễ dàng.
Các bước để tạo một package thì cũng không khó và trên document của Laravel (opens new window) cũng có hướng dẫn. Bài viết này sẽ hướng dẫn chi tiết hơn để tạo một Laravel package giúp search fulltext trong mysql (Một Engine search cho Laravel Scout).
Trước tiên cần tìm hiểu về Laravel Scout (opens new window). Mặc định, Laravel Scout có hổ trợ 2 search driver dùng service ngoài là Algolia (opens new window) và MeiliSearch (opens new window). Về hiệu suất search rất nhanh nhưng cũng có nhược điểm như cần đăng kí service của nó, nếu data ngày càng lớn sẽ tốn phí sử dụng, dữ liệu trong của project sẽ phải phải share lên server của algolia hay meiloSearch.
Package: https://packagist.org/packages/nin/mysql-ft-search
Package này sẽ tạo một Laravel Scout search engine ở local và sử dụng fulltext index trong mysql.
# Requirement:
- Môi trường để run 1 project Laravel. vd: apache, mysql,.
- Công cụ Composer (opens new window)
# Create package:
Install laravel: Sử dụng luôn laravel version mới nhất hiện tại
https://laravel.com/docs/8.x/installation
Tạo cấu trúc package:
Một package sẽ có dạng [Creator or Vendor]/[Package name].
Code sẽ nằm trong folder
src
, unit test trongtests
,config
vàdatabase
thì tùy vào package có thể không có.|- packages | |- nin /* Creator or Vendor */ | | |- config/ /* optional */ | | |- database/ /* optional */ | | |- src/ | | |- tests/ | | |- compose.json
1
2
3
4
5
6
7Init composer:
Bạn cần tạo các thư mục chính, file
composer.json
sẽ được tạo khi init composer.Tại
packages/nin
:$ composer init
1Sau khi nhập các thông tin file
composer.json
sẽ được tạo.{ "name": "nin/mysql-ft-search", "type": "library", "license": "MIT", "authors": [ { "name": "Nin", "email": "ninhnghia2@gmail.com" } ] }
1
2
3
4
5
6
7
8
9
10
11Đăng kí namespace:
Tại file
compose.json
của laravel app đã install."autoload": { "psr-4": { "App\\": "app/", "Database\\Factories\\": "database/factories/", "Database\\Seeders\\": "database/seeders/", "Nin\\MySqlFtSearch": "packages/nin/src" } },
1
2
3
4
5
6
7
8Run dump autoload.
composer dump-autoload
1Tạo file config và ServiceProvider:
Tạo file config
mysql-ft-search.php
trong folder /config và fileServiceProvider.php
extends classIlluminate\Support\ServiceProvider
trong folder /src chứa 2 method boot() và register().Để có thể publish và overide lại file config tại main app, cần thực hiện publish và merge config trong ServiceProvider
<?php namespace Nin\MySqlFtSearch; use Illuminate\Support\ServiceProvider as BaseServiceProvider; class ServiceProvider extends BaseServiceProvider { /** * Bootstrap any package services. * * @return void */ public function boot() { $this->publishes([ __DIR__ . '/../config/mysql-ft-search.php' => config_path('mysql-ft-search.php'), ]); } /** * Register any application services. * * @return void */ public function register() { $this->mergeConfigFrom( __DIR__ . '/../config/mysql-ft-search.php', 'mysql-ft-search' ); } }
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32Sau khi install package, file config sẽ được publish ra ngoài main app bạn chỉ cần run lệnh
$ php artisan vendor:publish --provider="Nin\MySqlFtSearch\ServiceProvider"
1Trong giai đoạn phát triển để Service Container có thể load qua ServiceProvider này bạn cần khai báo vào file
config/app.php
'providers' => [ ... Nin\MySqlFtSearch\ServiceProvider::class ]
1
2
3
4
5Còn khi sử dụng, với Laravel version > 5.5 thì không cần khai báo vào providers như trên nữa mà bạn cần khai báo trong composer.json
"extra": { "laravel": { "providers": [ "Nin\\MySqlFtSearch\\ServiceProvider" ], "aliases": { "FtSchema": "Nin\\MySqlFtSearch\\Facade" } } },
1
2
3
4
5
6
7
8
9
10Bao gồm các service provider và facade. Trong package này có tạo 1 facade sẽ đề cập chi tiết ở phần sau.
# Schema:
Class Illuminate\Support\Facades\Schema
của Laravel không có hổ trợ tạo column fulltext index. Để tạo được column fulltext có thể dùng DB::statement trong migration nhưng để code đẹp hơn và dễ maintain sau này mình tạo 1 class kế thừa lại Schema có thêm xử lí tại fulltext index.
DB::statement('ALTER TABLE posts ADD FULLTEXT fulltext_index (title)');
Blueprint:
Đây là class generate schema, xem chi tiết Blueprint (opens new window). Mình tạo class
Nin\MySqlFtSearch\MySqlFtBlueprint
kế thừaIlluminate\Database\Schema\Blueprint
và thêm methodfulltext
để tạo fulltext index.public function fulltext($columns, $name = null, $algorithm = null) { return $this->indexCommand('fulltext', $columns, $name, $algorithm); }
1
2
3
4Grammar:
Đây là class compile ra các command tương ứng với các db driver, xem chi tiết MySqlGrammar (opens new window). Tương tự, tạo class
Nin\MySqlFtSearch\MySqlFtGrammar
kế thừaIlluminate\Database\Schema\Grammars\MySqlGrammar
và thêm methodcompileFullText
public function compileFullText(Blueprint $blueprint, Fluent $command) { return $this->compileKey($blueprint, $command, 'FULLTEXT'); }
1
2
3
4Schema:
Tạo class builder schema
Nin\MySqlFtSearch\Schema\FtSchemaBuilder
và set các factory trên vào schema.use Nin\MySqlFtSearch\MySqlFtBlueprint as Blueprint; use Nin\MySqlFtSearch\MySqlFtGrammar; public function getSchema() { $connection = $this->getDbConnection(); $schema = $connection->getSchemaBuilder(); $schemaFtEnabled = $this->app['config']->get('mysql-ft-search.schema_ft_enabled', true); if ($schemaFtEnabled) { $this->checkDriver($connection->getDriverName()); // Set Schema Grammar. // Add method to compile a fulltext index command. $connection->setSchemaGrammar(new MySqlFtGrammar()); $schema = $connection->getSchemaBuilder(); // Set the Schema Blueprint resolver callback. // Add method to create fulltext index. $schema->blueprintResolver(function ($table, $callback) { return new Blueprint($table, $callback); }); } return $schema; }
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27Binding:
Tạo Facade FTSchema để dễ dàng sử dụng Schema trên. Facade của laravel sẽ get dependence trong Container nên ta binding Schema trong ServiceProvider.
<?php namespace Nin\MySqlFtSearch; use Illuminate\Support\ServiceProvider as BaseServiceProvider; use Nin\MySqlFtSearch\Schema\FtSchema; use Nin\MySqlFtSearch\Schema\FtSchemaBuilder; use Illuminate\Foundation\Application; class ServiceProvider extends BaseServiceProvider { /** * Register any application services. * * @return void */ public function register() { ... $this->app->singleton(FtSchema::class, function (Application $app) { $ftSchemaBuilder = new FtSchemaBuilder($app); return $ftSchemaBuilder->getSchema(); }); } }
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27Binding singleton schema builder cho interface
Nin\MySqlFtSearch\Schema\FtSchema
Tạo class
Nin\MySqlFtSearch\Facade
với facade accessor là FtSchemaprotected static function getFacadeAccessor() { return FtSchema::class; }
1
2
3
4Để sử dụng Facade này ở main app chỉ cần khai báo tại composer.json như ở trên.
Schema Usage:
Sử dụng Schema thì tương tự như class
Illuminate\Support\Facades\Schema
trong migrationuse Nin\MySqlFtSearch\Facade as FtSchema; public function up() { FtSchema::create('posts', function (Blueprint $table) { $table->id(); $table->string('title'); $table->text('description'); $table->fulltext(['title', 'description']); }); }
1
2
3
4
5
6
7
8
9
10
11
12
# Fulltext Search:
Tạo Engine kế thừa Laravel\Scout\Engines\Engine
chứa các abstract method
use Laravel\Scout\Builder;
abstract public function update($models);
abstract public function delete($models);
abstract public function search(Builder $builder);
abstract public function paginate(Builder $builder, $perPage, $page);
abstract public function mapIds($results);
abstract public function map(Builder $builder, $results, $model);
abstract public function getTotalCount($results);
abstract public function flush($model);
2
3
4
5
6
7
8
9
10
Xem chi tiết tại Nin\MySqlFtSearch\MySqlSearchEngine
và Nin\MySqlFtSearch\SearchBuilder
Để search fulltext index trong mysql dùng function MATCH
$builder->model->query()
->whereRaw("MATCH ({$columns}) AGAINST (? IN BOOLEAN MODE)", $this->fullTextWildcards($q))
2
Extending binding:
Extending binding Engine cho
Laravel\Scout\EngineManager
<?php namespace Nin\MySqlFtSearch; use Illuminate\Support\ServiceProvider as BaseServiceProvider; use Laravel\Scout\EngineManager; class ServiceProvider extends BaseServiceProvider { /** * Bootstrap any package services. * * @return void */ public function boot() { ... $scoutDriverName = $this->app['config']->get('mysql-ft-search.scout_driver_name', 'mysql'); resolve(EngineManager::class)->extend($scoutDriverName, function (Application $app) { return new MySqlSearchEngine($app); }); } }
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25Usage:
Set ENV:
SCOUT_DRIVER=mysql
1Model:
Khai báo property
$searchable
public $searchable = [ 'title', 'description' ];
1
2
3
4Sử dụng search
Post::search('foo')->get();
1
# Submit package:
Submit package lên https://packagist.org/ (opens new window)
- Tạo 1 git repository
- Tạo account ở Packagist, tại Submit package nhập Url của git repository là xong. Packagist sẽ tự động get branch của git repo, để tạo version thì dùng git tag. Khi thêm brach hoặc tag Packagist cũng tự động cập nhật hoặc cập nhật thủ công ở phần my packages.