mirror of
https://gitee.com/hyperf/hyperf.git
synced 2024-11-29 18:27:44 +08:00
Add Scout Component
This commit is contained in:
parent
4faa34aec9
commit
012e89d7d2
@ -176,6 +176,7 @@
|
||||
"Hyperf\\RpcClient\\": "src/rpc-client/src/",
|
||||
"Hyperf\\RpcServer\\": "src/rpc-server/src/",
|
||||
"Hyperf\\Rpc\\": "src/rpc/src/",
|
||||
"Hyperf\\Scout\\": "src/scout/src/",
|
||||
"Hyperf\\Server\\": "src/server/src/",
|
||||
"Hyperf\\ServiceGovernance\\": "src/service-governance/src/",
|
||||
"Hyperf\\Session\\": "src/session/src/",
|
||||
@ -240,6 +241,7 @@
|
||||
"HyperfTest\\Redis\\": "src/redis/tests/",
|
||||
"HyperfTest\\Retry\\": "src/retry/tests/",
|
||||
"HyperfTest\\Rpc\\": "src/rpc/tests/",
|
||||
"HyperfTest\\Scout\\": "src/scout/tests/",
|
||||
"HyperfTest\\Server\\": "src/server/tests/",
|
||||
"HyperfTest\\ServiceGovernance\\": "src/service-governance/tests/",
|
||||
"HyperfTest\\Session\\": "src/session/tests/",
|
||||
@ -303,6 +305,7 @@
|
||||
"Hyperf\\Retry\\ConfigProvider",
|
||||
"Hyperf\\RpcClient\\ConfigProvider",
|
||||
"Hyperf\\RpcServer\\ConfigProvider",
|
||||
"Hyperf\\Scout\\ConfigProvider",
|
||||
"Hyperf\\Server\\ConfigProvider",
|
||||
"Hyperf\\ServiceGovernance\\ConfigProvider",
|
||||
"Hyperf\\Session\\ConfigProvider",
|
||||
|
1
src/scout/.gitattributes
vendored
Normal file
1
src/scout/.gitattributes
vendored
Normal file
@ -0,0 +1 @@
|
||||
/tests export-ignore
|
4
src/scout/.gitignore
vendored
Normal file
4
src/scout/.gitignore
vendored
Normal file
@ -0,0 +1,4 @@
|
||||
/vendor/
|
||||
composer.lock
|
||||
*.cache
|
||||
*.log
|
87
src/scout/.php_cs
Normal file
87
src/scout/.php_cs
Normal file
@ -0,0 +1,87 @@
|
||||
<?php
|
||||
|
||||
$header = <<<'EOF'
|
||||
This file is part of Hyperf.
|
||||
|
||||
@link https://www.hyperf.io
|
||||
@document https://doc.hyperf.io
|
||||
@contact group@hyperf.io
|
||||
@license https://github.com/hyperf/hyperf/blob/master/LICENSE
|
||||
EOF;
|
||||
|
||||
return PhpCsFixer\Config::create()
|
||||
->setRiskyAllowed(true)
|
||||
->setRules([
|
||||
'@PSR2' => true,
|
||||
'@Symfony' => true,
|
||||
'@DoctrineAnnotation' => true,
|
||||
'@PhpCsFixer' => true,
|
||||
'header_comment' => [
|
||||
'commentType' => 'PHPDoc',
|
||||
'header' => $header,
|
||||
'separate' => 'none',
|
||||
'location' => 'after_declare_strict',
|
||||
],
|
||||
'array_syntax' => [
|
||||
'syntax' => 'short'
|
||||
],
|
||||
'list_syntax' => [
|
||||
'syntax' => 'short'
|
||||
],
|
||||
'concat_space' => [
|
||||
'spacing' => 'one'
|
||||
],
|
||||
'blank_line_before_statement' => [
|
||||
'statements' => [
|
||||
'declare',
|
||||
],
|
||||
],
|
||||
'general_phpdoc_annotation_remove' => [
|
||||
'annotations' => [
|
||||
'author'
|
||||
],
|
||||
],
|
||||
'ordered_imports' => [
|
||||
'imports_order' => [
|
||||
'class', 'function', 'const',
|
||||
],
|
||||
'sort_algorithm' => 'alpha',
|
||||
],
|
||||
'single_line_comment_style' => [
|
||||
'comment_types' => [
|
||||
],
|
||||
],
|
||||
'yoda_style' => [
|
||||
'always_move_variable' => false,
|
||||
'equal' => false,
|
||||
'identical' => false,
|
||||
],
|
||||
'phpdoc_align' => [
|
||||
'align' => 'left',
|
||||
],
|
||||
'multiline_whitespace_before_semicolons' => [
|
||||
'strategy' => 'no_multi_line',
|
||||
],
|
||||
'class_attributes_separation' => true,
|
||||
'combine_consecutive_unsets' => true,
|
||||
'declare_strict_types' => true,
|
||||
'linebreak_after_opening_tag' => true,
|
||||
'lowercase_constants' => true,
|
||||
'lowercase_static_reference' => true,
|
||||
'no_useless_else' => true,
|
||||
'no_unused_imports' => true,
|
||||
'not_operator_with_successor_space' => true,
|
||||
'not_operator_with_space' => false,
|
||||
'ordered_class_elements' => true,
|
||||
'php_unit_strict' => false,
|
||||
'phpdoc_separation' => false,
|
||||
'single_quote' => true,
|
||||
'standardize_not_equals' => true,
|
||||
'multiline_comment_opening_closing' => true,
|
||||
])
|
||||
->setFinder(
|
||||
PhpCsFixer\Finder::create()
|
||||
->exclude('vendor')
|
||||
->in(__DIR__)
|
||||
)
|
||||
->setUsingCache(false);
|
6
src/scout/.phpstorm.meta.php
Normal file
6
src/scout/.phpstorm.meta.php
Normal file
@ -0,0 +1,6 @@
|
||||
<?php
|
||||
|
||||
namespace PHPSTORM_META {
|
||||
// Reflect
|
||||
override(\Psr\Container\ContainerInterface::get(0), map('@'));
|
||||
}
|
40
src/scout/.travis.yml
Normal file
40
src/scout/.travis.yml
Normal file
@ -0,0 +1,40 @@
|
||||
language: php
|
||||
|
||||
sudo: required
|
||||
|
||||
matrix:
|
||||
include:
|
||||
- php: 7.2
|
||||
env: SW_VERSION="4.4.7"
|
||||
- php: 7.3
|
||||
env: SW_VERSION="4.4.7"
|
||||
- php: master
|
||||
env: SW_VERSION="4.4.7"
|
||||
|
||||
allow_failures:
|
||||
- php: master
|
||||
|
||||
services:
|
||||
- mysql
|
||||
- redis-server
|
||||
- docker
|
||||
|
||||
before_install:
|
||||
- export PHP_MAJOR="$(`phpenv which php` -r 'echo phpversion();' | cut -d '.' -f 1)"
|
||||
- export PHP_MINOR="$(`phpenv which php` -r 'echo phpversion();' | cut -d '.' -f 2)"
|
||||
- echo $PHP_MAJOR
|
||||
- echo $PHP_MINOR
|
||||
|
||||
install:
|
||||
- cd $TRAVIS_BUILD_DIR
|
||||
- bash ./tests/swoole.install.sh
|
||||
- phpenv config-rm xdebug.ini || echo "xdebug not available"
|
||||
- phpenv config-add ./tests/ci.ini
|
||||
|
||||
before_script:
|
||||
- cd $TRAVIS_BUILD_DIR
|
||||
- composer config -g process-timeout 900 && composer update
|
||||
|
||||
script:
|
||||
- composer analyze
|
||||
- composer test
|
5
src/scout/README.md
Normal file
5
src/scout/README.md
Normal file
@ -0,0 +1,5 @@
|
||||
# component-creater
|
||||
|
||||
```
|
||||
composer create-project hyperf/component-creater
|
||||
```
|
43
src/scout/composer.json
Normal file
43
src/scout/composer.json
Normal file
@ -0,0 +1,43 @@
|
||||
{
|
||||
"name": "hyperf/scout",
|
||||
"type": "library",
|
||||
"license": "MIT",
|
||||
"keywords": [
|
||||
"php",
|
||||
"hyperf"
|
||||
],
|
||||
"description": "Hyperf Scout provides a driver based solution to searching your Eloquent models. Inspired By Laravel Scout.",
|
||||
"autoload": {
|
||||
"psr-4": {
|
||||
"Hyperf\\Scout\\": "src/"
|
||||
}
|
||||
},
|
||||
"autoload-dev": {
|
||||
"psr-4": {
|
||||
"HyperfTest\\Scout\\": "tests/"
|
||||
}
|
||||
},
|
||||
"require": {
|
||||
"php": ">=7.2",
|
||||
"ext-swoole": ">=4.4"
|
||||
},
|
||||
"require-dev": {
|
||||
"friendsofphp/php-cs-fixer": "^2.14",
|
||||
"phpstan/phpstan": "^0.10.5",
|
||||
"hyperf/testing": "1.1.*",
|
||||
"swoft/swoole-ide-helper": "dev-master"
|
||||
},
|
||||
"config": {
|
||||
"sort-packages": true
|
||||
},
|
||||
"scripts": {
|
||||
"test": "co-phpunit -c phpunit.xml --colors=always",
|
||||
"analyze": "phpstan analyse --memory-limit 300M -l 0 ./src",
|
||||
"cs-fix": "php-cs-fixer fix $1"
|
||||
},
|
||||
"extra": {
|
||||
"hyperf": {
|
||||
"config": "Hyperf\\Scout\\ConfigProvider"
|
||||
}
|
||||
}
|
||||
}
|
15
src/scout/phpunit.xml
Normal file
15
src/scout/phpunit.xml
Normal file
@ -0,0 +1,15 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<phpunit bootstrap="tests/bootstrap.php"
|
||||
backupGlobals="false"
|
||||
backupStaticAttributes="false"
|
||||
verbose="true"
|
||||
colors="true"
|
||||
convertErrorsToExceptions="true"
|
||||
convertNoticesToExceptions="true"
|
||||
convertWarningsToExceptions="true"
|
||||
processIsolation="false"
|
||||
stopOnFailure="false">
|
||||
<testsuite name="Testsuite">
|
||||
<directory>./tests/</directory>
|
||||
</testsuite>
|
||||
</phpunit>
|
31
src/scout/publish/scout.php
Normal file
31
src/scout/publish/scout.php
Normal file
@ -0,0 +1,31 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
/**
|
||||
* This file is part of Hyperf.
|
||||
*
|
||||
* @link https://www.hyperf.io
|
||||
* @document https://doc.hyperf.io
|
||||
* @contact group@hyperf.io
|
||||
* @license https://github.com/hyperf/hyperf/blob/master/LICENSE
|
||||
*/
|
||||
|
||||
return [
|
||||
'default' => env('SCOUT_ENGINE', 'elasticsearch'),
|
||||
'chunk' => [
|
||||
'searchable' => 500,
|
||||
'unsearchable' => 500,
|
||||
],
|
||||
'prefix' => env('SCOUT_PREFIX', ''),
|
||||
'soft_delete' => false,
|
||||
'concurrency' => 100,
|
||||
'engine' => [
|
||||
'elasticsearch' => [
|
||||
'driver' => \Hyperf\Scout\Provider\ElasticsearchProvider::class,
|
||||
'index' => env('ELASTICSEARCH_INDEX', 'hyperf'),
|
||||
'hosts' => [
|
||||
env('ELASTICSEARCH_HOST', 'http://localhost'),
|
||||
],
|
||||
],
|
||||
],
|
||||
];
|
271
src/scout/src/Builder.php
Normal file
271
src/scout/src/Builder.php
Normal file
@ -0,0 +1,271 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
/**
|
||||
* This file is part of Hyperf.
|
||||
*
|
||||
* @link https://www.hyperf.io
|
||||
* @document https://doc.hyperf.io
|
||||
* @contact group@hyperf.io
|
||||
* @license https://github.com/hyperf/hyperf/blob/master/LICENSE
|
||||
*/
|
||||
|
||||
namespace Hyperf\Scout;
|
||||
|
||||
use Hyperf\Database\Model\Collection;
|
||||
use Hyperf\Database\Model\Model;
|
||||
use Hyperf\Paginator\LengthAwarePaginator;
|
||||
use Hyperf\Paginator\Paginator;
|
||||
use Hyperf\Utils\Collection as BaseCollection;
|
||||
use Hyperf\Utils\Traits\Macroable;
|
||||
|
||||
class Builder
|
||||
{
|
||||
use Macroable;
|
||||
|
||||
/**
|
||||
* The model instance.
|
||||
*
|
||||
* @var Model
|
||||
*/
|
||||
public $model;
|
||||
|
||||
/**
|
||||
* The query expression.
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
public $query;
|
||||
|
||||
/**
|
||||
* Optional callback before search execution.
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
public $callback;
|
||||
|
||||
/**
|
||||
* Optional callback before model query execution.
|
||||
*
|
||||
* @var null|\Closure
|
||||
*/
|
||||
public $queryCallback;
|
||||
|
||||
/**
|
||||
* The custom index specified for the search.
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
public $index;
|
||||
|
||||
/**
|
||||
* The "where" constraints added to the query.
|
||||
*
|
||||
* @var array
|
||||
*/
|
||||
public $wheres = [];
|
||||
|
||||
/**
|
||||
* The "limit" that should be applied to the search.
|
||||
*
|
||||
* @var int
|
||||
*/
|
||||
public $limit;
|
||||
|
||||
/**
|
||||
* The "order" that should be applied to the search.
|
||||
*
|
||||
* @var array
|
||||
*/
|
||||
public $orders = [];
|
||||
|
||||
/**
|
||||
* Create a new search builder instance.
|
||||
*/
|
||||
public function __construct(Model $model, string $query, ?\Closure $callback = null, ?bool $softDelete = false)
|
||||
{
|
||||
$this->model = $model;
|
||||
$this->query = $query;
|
||||
$this->callback = $callback;
|
||||
if ($softDelete) {
|
||||
$this->wheres['__soft_deleted'] = 0;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Specify a custom index to perform this search on.
|
||||
*/
|
||||
public function within(string $index): Builder
|
||||
{
|
||||
$this->index = $index;
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Add a constraint to the search query.
|
||||
*
|
||||
* @param mixed $value
|
||||
* @return $this
|
||||
*/
|
||||
public function where(string $field, $value): Builder
|
||||
{
|
||||
$this->wheres[$field] = $value;
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Include soft deleted records in the results.
|
||||
*
|
||||
* @return $this
|
||||
*/
|
||||
public function withTrashed(): Builder
|
||||
{
|
||||
unset($this->wheres['__soft_deleted']);
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Include only soft deleted records in the results.
|
||||
*
|
||||
* @return $this
|
||||
*/
|
||||
public function onlyTrashed(): Builder
|
||||
{
|
||||
return tap($this->withTrashed(), function () {
|
||||
$this->wheres['__soft_deleted'] = 1;
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Set the "limit" for the search query.
|
||||
*
|
||||
* @return $this
|
||||
*/
|
||||
public function take(int $limit): Builder
|
||||
{
|
||||
$this->limit = $limit;
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Add an "order" for the search query.
|
||||
*/
|
||||
public function orderBy(string $column, ?string $direction = 'asc'): Builder
|
||||
{
|
||||
$this->orders[] = [
|
||||
'column' => $column,
|
||||
'direction' => strtolower($direction) == 'asc' ? 'asc' : 'desc',
|
||||
];
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Apply the callback's query changes if the given "value" is true.
|
||||
* @param mixed $value
|
||||
*/
|
||||
public function when($value, callable $callback, ?callable $default = null): Builder
|
||||
{
|
||||
if ($value) {
|
||||
return $callback($this, $value) ?: $this;
|
||||
}
|
||||
if ($default) {
|
||||
return $default($this, $value) ?: $this;
|
||||
}
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Pass the query to a given callback.
|
||||
*/
|
||||
public function tap(\Closure $callback): Builder
|
||||
{
|
||||
return $this->when(true, $callback);
|
||||
}
|
||||
|
||||
/**
|
||||
* Set the callback that should have an opportunity to modify the database query.
|
||||
*/
|
||||
public function query(\Closure $callback): Builder
|
||||
{
|
||||
$this->queryCallback = $callback;
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the raw results of the search.
|
||||
*
|
||||
* @return mixed
|
||||
*/
|
||||
public function raw()
|
||||
{
|
||||
return $this->engine()->search($this);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the keys of search results.
|
||||
*/
|
||||
public function keys(): BaseCollection
|
||||
{
|
||||
return $this->engine()->keys($this);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the first result from the search.
|
||||
*/
|
||||
public function first(): Model
|
||||
{
|
||||
return $this->get()->first();
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the results of the search.
|
||||
*/
|
||||
public function get(): Collection
|
||||
{
|
||||
return $this->engine()->get($this);
|
||||
}
|
||||
|
||||
/**
|
||||
* Paginate the given query into a simple paginator.
|
||||
*/
|
||||
public function paginate(?int $perPage = null, ?string $pageName = 'page', ?int $page = null): LengthAwarePaginator
|
||||
{
|
||||
$engine = $this->engine();
|
||||
$page = $page ?: Paginator::resolveCurrentPage($pageName);
|
||||
$perPage = $perPage ?: $this->model->getPerPage();
|
||||
$results = $this->model->newCollection($engine->map(
|
||||
$this,
|
||||
$rawResults = $engine->paginate($this, $perPage, $page),
|
||||
$this->model
|
||||
)->all());
|
||||
$paginator = (new LengthAwarePaginator($results, $engine->getTotalCount($rawResults), $perPage, $page, [
|
||||
'path' => Paginator::resolveCurrentPath(),
|
||||
'pageName' => $pageName,
|
||||
]));
|
||||
return $paginator->appends('query', $this->query);
|
||||
}
|
||||
|
||||
/**
|
||||
* Paginate the given query into a simple paginator with raw data.
|
||||
*/
|
||||
public function paginateRaw(?int $perPage = null, ?string $pageName = 'page', ?int $page = null): LengthAwarePaginator
|
||||
{
|
||||
$engine = $this->engine();
|
||||
$page = $page ?: Paginator::resolveCurrentPage($pageName);
|
||||
$perPage = $perPage ?: $this->model->getPerPage();
|
||||
$results = $engine->paginate($this, $perPage, $page);
|
||||
$paginator = (new LengthAwarePaginator($results, $engine->getTotalCount($results), $perPage, $page, [
|
||||
'path' => Paginator::resolveCurrentPath(),
|
||||
'pageName' => $pageName,
|
||||
]));
|
||||
return $paginator->appends('query', $this->query);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the engine that should handle the query.
|
||||
*/
|
||||
protected function engine()
|
||||
{
|
||||
return $this->model->searchableUsing();
|
||||
}
|
||||
}
|
33
src/scout/src/ConfigProvider.php
Normal file
33
src/scout/src/ConfigProvider.php
Normal file
@ -0,0 +1,33 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
/**
|
||||
* This file is part of Hyperf.
|
||||
*
|
||||
* @link https://www.hyperf.io
|
||||
* @document https://doc.hyperf.io
|
||||
* @contact group@hyperf.io
|
||||
* @license https://github.com/hyperf/hyperf/blob/master/LICENSE
|
||||
*/
|
||||
|
||||
namespace Hyperf\Scout;
|
||||
|
||||
use Hyperf\Scout\Console\FlushCommand;
|
||||
use Hyperf\Scout\Console\ImportCommand;
|
||||
use Hyperf\Scout\Engine\Engine;
|
||||
|
||||
class ConfigProvider
|
||||
{
|
||||
public function __invoke(): array
|
||||
{
|
||||
return [
|
||||
'dependencies' => [
|
||||
Engine::class => EngineFactory::class,
|
||||
],
|
||||
'commands' => [
|
||||
ImportCommand::class,
|
||||
FlushCommand::class,
|
||||
],
|
||||
];
|
||||
}
|
||||
}
|
55
src/scout/src/Console/FlushCommand.php
Normal file
55
src/scout/src/Console/FlushCommand.php
Normal file
@ -0,0 +1,55 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
/**
|
||||
* This file is part of Hyperf.
|
||||
*
|
||||
* @link https://www.hyperf.io
|
||||
* @document https://doc.hyperf.io
|
||||
* @contact group@hyperf.io
|
||||
* @license https://github.com/hyperf/hyperf/blob/master/LICENSE
|
||||
*/
|
||||
|
||||
namespace Hyperf\Scout\Console;
|
||||
|
||||
use Hyperf\Command\Annotation\Command;
|
||||
use Hyperf\Command\Command as HyperfCommand;
|
||||
use Symfony\Component\Console\Input\InputArgument;
|
||||
|
||||
/**
|
||||
* @Command
|
||||
*/
|
||||
class FlushCommand extends HyperfCommand
|
||||
{
|
||||
/**
|
||||
* The name and signature of the console command.
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
protected $name = 'scout:flush';
|
||||
|
||||
/**
|
||||
* The console command description.
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
protected $description = "Flush all of the model's records from the index";
|
||||
|
||||
/**
|
||||
* Execute the console command.
|
||||
*/
|
||||
public function handle()
|
||||
{
|
||||
$class = $this->input->getArgument('model');
|
||||
$model = new $class();
|
||||
$model::removeAllFromSearch();
|
||||
$this->info('All [' . $class . '] records have been flushed.');
|
||||
}
|
||||
|
||||
protected function getArguments()
|
||||
{
|
||||
return [
|
||||
['model', InputArgument::REQUIRED, 'fully qualified class name of the model'],
|
||||
];
|
||||
}
|
||||
}
|
58
src/scout/src/Console/ImportCommand.php
Normal file
58
src/scout/src/Console/ImportCommand.php
Normal file
@ -0,0 +1,58 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
/**
|
||||
* This file is part of Hyperf.
|
||||
*
|
||||
* @link https://www.hyperf.io
|
||||
* @document https://doc.hyperf.io
|
||||
* @contact group@hyperf.io
|
||||
* @license https://github.com/hyperf/hyperf/blob/master/LICENSE
|
||||
*/
|
||||
|
||||
namespace Hyperf\Scout\Console;
|
||||
|
||||
use Hyperf\Command\Command;
|
||||
use Hyperf\Event\ListenerProvider;
|
||||
use Hyperf\Scout\Event\ModelsImported;
|
||||
use Hyperf\Utils\ApplicationContext;
|
||||
|
||||
class ImportCommand extends Command
|
||||
{
|
||||
/**
|
||||
* The name and signature of the console command.
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
protected $name = 'scout:import';
|
||||
|
||||
/**
|
||||
* The console command description.
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
protected $description = 'Import the given model into the search index';
|
||||
|
||||
/**
|
||||
* Execute the console command.
|
||||
*/
|
||||
public function handle()
|
||||
{
|
||||
$class = $this->input->getArgument('model');
|
||||
$model = new $class();
|
||||
$provider = ApplicationContext::getContainer()->get(ListenerProvider::class);
|
||||
$provider->on(ModelsImported::class, function ($event) use ($class) {
|
||||
$key = $event->models->last()->getScoutKey();
|
||||
$this->line('<comment>Imported [' . $class . '] models up to ID:</comment> ' . $key);
|
||||
});
|
||||
$model::makeAllSearchable();
|
||||
$this->info('All [' . $class . '] records have been imported.');
|
||||
}
|
||||
|
||||
protected function getArguments()
|
||||
{
|
||||
return [
|
||||
['model', InputArgument::REQUIRED, 'fully qualified class name of the model'],
|
||||
];
|
||||
}
|
||||
}
|
249
src/scout/src/Engine/ElasticsearchEngine.php
Normal file
249
src/scout/src/Engine/ElasticsearchEngine.php
Normal file
@ -0,0 +1,249 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
/**
|
||||
* This file is part of Hyperf.
|
||||
*
|
||||
* @link https://www.hyperf.io
|
||||
* @document https://doc.hyperf.io
|
||||
* @contact group@hyperf.io
|
||||
* @license https://github.com/hyperf/hyperf/blob/master/LICENSE
|
||||
*/
|
||||
|
||||
namespace Hyperf\Scout\Engine;
|
||||
|
||||
use Elasticsearch\Client;
|
||||
use Elasticsearch\Client as Elastic;
|
||||
use Hyperf\Database\Model\Collection;
|
||||
use Hyperf\Database\Model\Model;
|
||||
use Hyperf\Scout\Builder;
|
||||
|
||||
class ElasticsearchEngine extends Engine
|
||||
{
|
||||
/**
|
||||
* Index where the models will be saved.
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
protected $index;
|
||||
|
||||
/**
|
||||
* Elastic where the instance of Elastic|\Elasticsearch\Client is stored.
|
||||
*
|
||||
* @var object
|
||||
*/
|
||||
protected $elastic;
|
||||
|
||||
/**
|
||||
* Create a new engine instance.
|
||||
*
|
||||
* @param $index
|
||||
*/
|
||||
public function __construct(Client $client, $index)
|
||||
{
|
||||
$this->elastic = $client;
|
||||
$this->index = $index;
|
||||
}
|
||||
|
||||
/**
|
||||
* Update the given model in the index.
|
||||
*
|
||||
* @param Collection $models
|
||||
*/
|
||||
public function update($models): void
|
||||
{
|
||||
$params['body'] = [];
|
||||
$models->each(function ($model) use (&$params) {
|
||||
$params['body'][] = [
|
||||
'update' => [
|
||||
'_id' => $model->getKey(),
|
||||
'_index' => $this->index,
|
||||
'_type' => $model->searchableAs(),
|
||||
],
|
||||
];
|
||||
$params['body'][] = [
|
||||
'doc' => $model->toSearchableArray(),
|
||||
'doc_as_upsert' => true,
|
||||
];
|
||||
});
|
||||
$this->elastic->bulk($params);
|
||||
}
|
||||
|
||||
/**
|
||||
* Remove the given model from the index.
|
||||
*
|
||||
* @param Collection $models
|
||||
*/
|
||||
public function delete($models): void
|
||||
{
|
||||
$params['body'] = [];
|
||||
$models->each(function ($model) use (&$params) {
|
||||
$params['body'][] = [
|
||||
'delete' => [
|
||||
'_id' => $model->getKey(),
|
||||
'_index' => $this->index,
|
||||
'_type' => $model->searchableAs(),
|
||||
],
|
||||
];
|
||||
});
|
||||
$this->elastic->bulk($params);
|
||||
}
|
||||
|
||||
/**
|
||||
* Perform the given search on the engine.
|
||||
*
|
||||
* @return mixed
|
||||
*/
|
||||
public function search(Builder $builder)
|
||||
{
|
||||
return $this->performSearch($builder, array_filter([
|
||||
'numericFilters' => $this->filters($builder),
|
||||
'size' => $builder->limit,
|
||||
]));
|
||||
}
|
||||
|
||||
/**
|
||||
* Perform the given search on the engine.
|
||||
*
|
||||
* @param int $perPage
|
||||
* @param int $page
|
||||
* @return mixed
|
||||
*/
|
||||
public function paginate(Builder $builder, $perPage, $page)
|
||||
{
|
||||
$result = $this->performSearch($builder, [
|
||||
'numericFilters' => $this->filters($builder),
|
||||
'from' => (($page * $perPage) - $perPage),
|
||||
'size' => $perPage,
|
||||
]);
|
||||
$result['nbPages'] = $result['hits']['total'] / $perPage;
|
||||
return $result;
|
||||
}
|
||||
|
||||
/**
|
||||
* Pluck and return the primary keys of the given results.
|
||||
*
|
||||
* @param mixed $results
|
||||
* @return \Illuminate\Support\Collection
|
||||
*/
|
||||
public function mapIds($results): Collection
|
||||
{
|
||||
return collect($results['hits']['hits'])->pluck('_id')->values();
|
||||
}
|
||||
|
||||
/**
|
||||
* Map the given results to instances of the given model.
|
||||
*
|
||||
* @param \Laravel\Scout\Builder $builder
|
||||
* @param mixed $results
|
||||
* @param \Illuminate\Database\Eloquent\Model $model
|
||||
*/
|
||||
public function map(Builder $builder, $results, $model): Collection
|
||||
{
|
||||
if ($results['hits']['total'] === 0) {
|
||||
return $model->newCollection();
|
||||
}
|
||||
$keys = collect($results['hits']['hits'])->pluck('_id')->values()->all();
|
||||
return $model->getScoutModelsByIds(
|
||||
$builder,
|
||||
$keys
|
||||
)->filter(function ($model) use ($keys) {
|
||||
return in_array($model->getScoutKey(), $keys);
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the total count from a raw result returned by the engine.
|
||||
*
|
||||
* @param mixed $results
|
||||
*/
|
||||
public function getTotalCount($results): int
|
||||
{
|
||||
return $results['hits']['total'];
|
||||
}
|
||||
|
||||
/**
|
||||
* Flush all of the model's records from the engine.
|
||||
*/
|
||||
public function flush(Model $model): void
|
||||
{
|
||||
$model->newQuery()
|
||||
->orderBy($model->getKeyName())
|
||||
->unsearchable();
|
||||
}
|
||||
|
||||
/**
|
||||
* Perform the given search on the engine.
|
||||
*
|
||||
* @return mixed
|
||||
*/
|
||||
protected function performSearch(Builder $builder, array $options = [])
|
||||
{
|
||||
$params = [
|
||||
'index' => $this->index,
|
||||
'type' => $builder->index ?: $builder->model->searchableAs(),
|
||||
'body' => [
|
||||
'query' => [
|
||||
'bool' => [
|
||||
'must' => [['query_string' => ['query' => "*{$builder->query}*"]]],
|
||||
],
|
||||
],
|
||||
],
|
||||
];
|
||||
if ($sort = $this->sort($builder)) {
|
||||
$params['body']['sort'] = $sort;
|
||||
}
|
||||
if (isset($options['from'])) {
|
||||
$params['body']['from'] = $options['from'];
|
||||
}
|
||||
if (isset($options['size'])) {
|
||||
$params['body']['size'] = $options['size'];
|
||||
}
|
||||
if (isset($options['numericFilters']) && count($options['numericFilters'])) {
|
||||
$params['body']['query']['bool']['must'] = array_merge(
|
||||
$params['body']['query']['bool']['must'],
|
||||
$options['numericFilters']
|
||||
);
|
||||
}
|
||||
if ($builder->callback) {
|
||||
return call_user_func(
|
||||
$builder->callback,
|
||||
$this->elastic,
|
||||
$builder->query,
|
||||
$params
|
||||
);
|
||||
}
|
||||
return $this->elastic->search($params);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the filter array for the query.
|
||||
*
|
||||
* @return array
|
||||
*/
|
||||
protected function filters(Builder $builder)
|
||||
{
|
||||
return collect($builder->wheres)->map(function ($value, $key) {
|
||||
if (is_array($value)) {
|
||||
return ['terms' => [$key => $value]];
|
||||
}
|
||||
return ['match_phrase' => [$key => $value]];
|
||||
})->values()->all();
|
||||
}
|
||||
|
||||
/**
|
||||
* Generates the sort if theres any.
|
||||
*
|
||||
* @param Builder $builder
|
||||
* @return null|array
|
||||
*/
|
||||
protected function sort($builder)
|
||||
{
|
||||
if (count($builder->orders) == 0) {
|
||||
return null;
|
||||
}
|
||||
return collect($builder->orders)->map(function ($order) {
|
||||
return [$order['column'] => $order['direction']];
|
||||
})->toArray();
|
||||
}
|
||||
}
|
83
src/scout/src/Engine/Engine.php
Normal file
83
src/scout/src/Engine/Engine.php
Normal file
@ -0,0 +1,83 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
/**
|
||||
* This file is part of Hyperf.
|
||||
*
|
||||
* @link https://www.hyperf.io
|
||||
* @document https://doc.hyperf.io
|
||||
* @contact group@hyperf.io
|
||||
* @license https://github.com/hyperf/hyperf/blob/master/LICENSE
|
||||
*/
|
||||
|
||||
namespace Hyperf\Scout\Engine;
|
||||
|
||||
use Hyperf\Database\Model\Collection;
|
||||
use Hyperf\Database\Model\Model;
|
||||
use Hyperf\Scout\Builder;
|
||||
|
||||
abstract class Engine
|
||||
{
|
||||
/**
|
||||
* Update the given model in the index.
|
||||
*/
|
||||
abstract public function update(Collection $models): void;
|
||||
|
||||
/**
|
||||
* Remove the given model from the index.
|
||||
*/
|
||||
abstract public function delete(Collection $models): void;
|
||||
|
||||
/**
|
||||
* Perform the given search on the engine.
|
||||
*/
|
||||
abstract public function search(Builder $builder);
|
||||
|
||||
/**
|
||||
* Perform the given search on the engine.
|
||||
*/
|
||||
abstract public function paginate(Builder $builder, int $perPage, int $page);
|
||||
|
||||
/**
|
||||
* Pluck and return the primary keys of the given results.
|
||||
* @param mixed $results
|
||||
*/
|
||||
abstract public function mapIds($results): Collection;
|
||||
|
||||
/**
|
||||
* Map the given results to instances of the given model.
|
||||
* @param mixed $results
|
||||
*/
|
||||
abstract public function map(Builder $builder, $results, Model $model): Collection;
|
||||
|
||||
/**
|
||||
* Get the total count from a raw result returned by the engine.
|
||||
* @param mixed $results
|
||||
*/
|
||||
abstract public function getTotalCount($results): int;
|
||||
|
||||
/**
|
||||
* Flush all of the model's records from the engine.
|
||||
*/
|
||||
abstract public function flush(Model $model): void;
|
||||
|
||||
/**
|
||||
* Get the results of the query as a Collection of primary keys.
|
||||
*/
|
||||
public function keys(Builder $builder): Collection
|
||||
{
|
||||
return $this->mapIds($this->search($builder));
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the results of the given query mapped onto models.
|
||||
*/
|
||||
public function get(Builder $builder): Collection
|
||||
{
|
||||
return $this->map(
|
||||
$builder,
|
||||
$this->search($builder),
|
||||
$builder->model
|
||||
);
|
||||
}
|
||||
}
|
84
src/scout/src/Engine/NullEngine.php
Normal file
84
src/scout/src/Engine/NullEngine.php
Normal file
@ -0,0 +1,84 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
/**
|
||||
* This file is part of Hyperf.
|
||||
*
|
||||
* @link https://www.hyperf.io
|
||||
* @document https://doc.hyperf.io
|
||||
* @contact group@hyperf.io
|
||||
* @license https://github.com/hyperf/hyperf/blob/master/LICENSE
|
||||
*/
|
||||
|
||||
namespace Hyperf\Scout\Engine;
|
||||
|
||||
use Hyperf\Database\Model\Collection;
|
||||
use Hyperf\Database\Model\Model;
|
||||
use Hyperf\Utils\Collection as BaseCollection;
|
||||
|
||||
class NullEngine extends Engine
|
||||
{
|
||||
/**
|
||||
* Update the given model in the index.
|
||||
*/
|
||||
public function update(Collection $models): void
|
||||
{
|
||||
}
|
||||
|
||||
/**
|
||||
* Remove the given model from the index.
|
||||
*/
|
||||
public function delete(Collection $models): void
|
||||
{
|
||||
}
|
||||
|
||||
/**
|
||||
* Perform the given search on the engine.
|
||||
*/
|
||||
public function search(Builder $builder)
|
||||
{
|
||||
return [];
|
||||
}
|
||||
|
||||
/**
|
||||
* Perform the given search on the engine.
|
||||
*/
|
||||
public function paginate(Builder $builder, int $perPage, int $page)
|
||||
{
|
||||
return [];
|
||||
}
|
||||
|
||||
/**
|
||||
* Pluck and return the primary keys of the given results.
|
||||
* @param mixed $results
|
||||
*/
|
||||
public function mapIds($results): Collection
|
||||
{
|
||||
return BaseCollection::make();
|
||||
}
|
||||
|
||||
/**
|
||||
* Map the given results to instances of the given model.
|
||||
* @param mixed $results
|
||||
*/
|
||||
public function map(Builder $builder, $results, Model $model): Collection
|
||||
{
|
||||
return Collection::make();
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the total count from a raw result returned by the engine.
|
||||
* @param mixed $results
|
||||
*/
|
||||
public function getTotalCount($results): int
|
||||
{
|
||||
return count($results);
|
||||
}
|
||||
|
||||
/**
|
||||
* Flush all of the model's records from the engine.
|
||||
*/
|
||||
public function flush(Model $model): void
|
||||
{
|
||||
}
|
||||
}
|
28
src/scout/src/EngineFactory.php
Normal file
28
src/scout/src/EngineFactory.php
Normal file
@ -0,0 +1,28 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
/**
|
||||
* This file is part of Hyperf.
|
||||
*
|
||||
* @link https://www.hyperf.io
|
||||
* @document https://doc.hyperf.io
|
||||
* @contact group@hyperf.io
|
||||
* @license https://github.com/hyperf/hyperf/blob/master/LICENSE
|
||||
*/
|
||||
|
||||
namespace Hyperf\Scout;
|
||||
|
||||
use Hyperf\Contract\ConfigInterface;
|
||||
use Hyperf\Contract\ContainerInterface;
|
||||
|
||||
class EngineFactory
|
||||
{
|
||||
public function __invoke(ContainerInterface $container)
|
||||
{
|
||||
$config = $container->get(ConfigInterface::class);
|
||||
$name = $config->get('scout.default');
|
||||
$driver = $config->get("scout.engine.{$name}.driver");
|
||||
$driverInstance = make($driver);
|
||||
return $driverInstance->make($name);
|
||||
}
|
||||
}
|
30
src/scout/src/Event/ModelsFlushed.php
Normal file
30
src/scout/src/Event/ModelsFlushed.php
Normal file
@ -0,0 +1,30 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
/**
|
||||
* This file is part of Hyperf.
|
||||
*
|
||||
* @link https://www.hyperf.io
|
||||
* @document https://doc.hyperf.io
|
||||
* @contact group@hyperf.io
|
||||
* @license https://github.com/hyperf/hyperf/blob/master/LICENSE
|
||||
*/
|
||||
|
||||
namespace Hyperf\Scout\Event;
|
||||
|
||||
use Hyperf\Database\Model\Collection;
|
||||
|
||||
class ModelsFlushed
|
||||
{
|
||||
/**
|
||||
* The model collection.
|
||||
*
|
||||
* @var Collection
|
||||
*/
|
||||
public $models;
|
||||
|
||||
public function __construct(Collection $models)
|
||||
{
|
||||
$this->models = $models;
|
||||
}
|
||||
}
|
33
src/scout/src/Event/ModelsImported.php
Normal file
33
src/scout/src/Event/ModelsImported.php
Normal file
@ -0,0 +1,33 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
/**
|
||||
* This file is part of Hyperf.
|
||||
*
|
||||
* @link https://www.hyperf.io
|
||||
* @document https://doc.hyperf.io
|
||||
* @contact group@hyperf.io
|
||||
* @license https://github.com/hyperf/hyperf/blob/master/LICENSE
|
||||
*/
|
||||
|
||||
namespace Hyperf\Scout\Event;
|
||||
|
||||
use Hyperf\Database\Model\Collection;
|
||||
|
||||
class ModelsImported
|
||||
{
|
||||
/**
|
||||
* The model collection.
|
||||
*
|
||||
* @var Collection
|
||||
*/
|
||||
public $models;
|
||||
|
||||
/**
|
||||
* Create a new event instance.
|
||||
*/
|
||||
public function __construct(Collection $models)
|
||||
{
|
||||
$this->models = $models;
|
||||
}
|
||||
}
|
123
src/scout/src/ModelObserver.php
Normal file
123
src/scout/src/ModelObserver.php
Normal file
@ -0,0 +1,123 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
/**
|
||||
* This file is part of Hyperf.
|
||||
*
|
||||
* @link https://www.hyperf.io
|
||||
* @document https://doc.hyperf.io
|
||||
* @contact group@hyperf.io
|
||||
* @license https://github.com/hyperf/hyperf/blob/master/LICENSE
|
||||
*/
|
||||
|
||||
namespace Hyperf\Scout;
|
||||
|
||||
use Hyperf\Database\Model\Events\Deleted;
|
||||
use Hyperf\Database\Model\Events\ForceDeleted;
|
||||
use Hyperf\Database\Model\Events\Restored;
|
||||
use Hyperf\Database\Model\Events\Saved;
|
||||
use Hyperf\Database\Model\Model;
|
||||
use Hyperf\Database\Model\SoftDeletes;
|
||||
use Hyperf\Utils\Context;
|
||||
|
||||
class ModelObserver
|
||||
{
|
||||
/**
|
||||
* Enable syncing for the given class.
|
||||
*/
|
||||
public static function enableSyncingFor(string $class): void
|
||||
{
|
||||
Context::override('syncing_disabled', function ($syncingDisabled) use ($class) {
|
||||
unset($syncingDisabled[$class]);
|
||||
return $syncingDisabled;
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Disable syncing for the given class.
|
||||
*/
|
||||
public static function disableSyncingFor(string $class): void
|
||||
{
|
||||
Context::override('syncing_disabled', function ($syncingDisabled) use ($class) {
|
||||
$syncingDisabled[$class] = true;
|
||||
return $syncingDisabled;
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Determine if syncing is disabled for the given class or model.
|
||||
*
|
||||
* @param object|string $class
|
||||
*/
|
||||
public static function syncingDisabledFor($class): bool
|
||||
{
|
||||
$class = is_object($class) ? get_class($class) : $class;
|
||||
$syncingDisabled = (array)Context::get('syncing_disabled', []);
|
||||
return array_key_exists($class, $syncingDisabled);
|
||||
}
|
||||
|
||||
/**
|
||||
* Handle the saved event for the model.
|
||||
*/
|
||||
public function saved(Saved $event): void
|
||||
{
|
||||
$model = $event->getModel();
|
||||
|
||||
if (static::syncingDisabledFor($model)) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (! $model->shouldBeSearchable()) {
|
||||
$model->unsearchable();
|
||||
return;
|
||||
}
|
||||
$model->searchable();
|
||||
}
|
||||
|
||||
/**
|
||||
* Handle the deleted event for the model.
|
||||
*/
|
||||
public function deleted(Deleted $event)
|
||||
{
|
||||
$model = $event->getModel();
|
||||
|
||||
if (static::syncingDisabledFor($model)) {
|
||||
return;
|
||||
}
|
||||
if ($this->usesSoftDelete($model) && config('scout.soft_delete', false)) {
|
||||
$this->saved($model);
|
||||
} else {
|
||||
$model->unsearchable();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Handle the force deleted event for the model.
|
||||
*/
|
||||
public function forceDeleted(ForceDeleted $event)
|
||||
{
|
||||
$model = $event->getModel();
|
||||
|
||||
if (static::syncingDisabledFor($model)) {
|
||||
return;
|
||||
}
|
||||
$model->unsearchable();
|
||||
}
|
||||
|
||||
/**
|
||||
* Handle the restored event for the model.
|
||||
*/
|
||||
public function restored(Restored $event)
|
||||
{
|
||||
$model = $event->getModel();
|
||||
$this->saved(new Saved($model));
|
||||
}
|
||||
|
||||
/**
|
||||
* Determine if the given model uses soft deletes.
|
||||
*/
|
||||
protected function usesSoftDelete(Model $model): bool
|
||||
{
|
||||
return in_array(SoftDeletes::class, class_uses_recursive($model));
|
||||
}
|
||||
}
|
40
src/scout/src/Provider/ElasticsearchProvider.php
Normal file
40
src/scout/src/Provider/ElasticsearchProvider.php
Normal file
@ -0,0 +1,40 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
/**
|
||||
* This file is part of Hyperf.
|
||||
*
|
||||
* @link https://www.hyperf.io
|
||||
* @document https://doc.hyperf.io
|
||||
* @contact group@hyperf.io
|
||||
* @license https://github.com/hyperf/hyperf/blob/master/LICENSE
|
||||
*/
|
||||
|
||||
namespace Hyperf\Scout\Provider;
|
||||
|
||||
use Hyperf\Contract\ContainerInterface;
|
||||
use Hyperf\Elasticsearch\ClientBuilderFactory;
|
||||
use Hyperf\Scout\Engine\ElasticsearchEngine;
|
||||
use Hyperf\Scout\Engine\Engine;
|
||||
|
||||
class ElasticsearchProvider implements ProviderInterface
|
||||
{
|
||||
/**
|
||||
* @var ContainerInterface
|
||||
*/
|
||||
private $container;
|
||||
|
||||
public function __construct(ContainerInterface $container)
|
||||
{
|
||||
$this->container = $container;
|
||||
}
|
||||
|
||||
public function make(string $name): Engine
|
||||
{
|
||||
$config = $this->container->get(ConfigInterface::class);
|
||||
$builder = $this->container->get(ClientBuilderFactory::class)->create();
|
||||
$client = $builder->setHosts($config->get("scout.{$name}.hosts"))->build();
|
||||
$index = $config->get("scout.{$name}.index");
|
||||
return new ElasticsearchEngine($client, $index);
|
||||
}
|
||||
}
|
20
src/scout/src/Provider/ProviderInterface.php
Normal file
20
src/scout/src/Provider/ProviderInterface.php
Normal file
@ -0,0 +1,20 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
/**
|
||||
* This file is part of Hyperf.
|
||||
*
|
||||
* @link https://www.hyperf.io
|
||||
* @document https://doc.hyperf.io
|
||||
* @contact group@hyperf.io
|
||||
* @license https://github.com/hyperf/hyperf/blob/master/LICENSE
|
||||
*/
|
||||
|
||||
namespace Hyperf\Scout\Provider;
|
||||
|
||||
use Hyperf\Scout\Engine\Engine;
|
||||
|
||||
interface ProviderInterface
|
||||
{
|
||||
public function make(string $name): Engine;
|
||||
}
|
297
src/scout/src/Searchable.php
Normal file
297
src/scout/src/Searchable.php
Normal file
@ -0,0 +1,297 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
/**
|
||||
* This file is part of Hyperf.
|
||||
*
|
||||
* @link https://www.hyperf.io
|
||||
* @document https://doc.hyperf.io
|
||||
* @contact group@hyperf.io
|
||||
* @license https://github.com/hyperf/hyperf/blob/master/LICENSE
|
||||
*/
|
||||
|
||||
namespace Hyperf\Scout;
|
||||
|
||||
use Hyperf\Database\Model\Collection;
|
||||
use Hyperf\ModelListener\Collector\ListenerCollector;
|
||||
use Hyperf\Scout\Engine\Engine;
|
||||
use Hyperf\Utils\ApplicationContext;
|
||||
use Hyperf\Utils\Collection as BaseCollection;
|
||||
use Hyperf\Utils\Coroutine;
|
||||
|
||||
trait Searchable
|
||||
{
|
||||
/**
|
||||
* Additional metadata attributes managed by Scout.
|
||||
*
|
||||
* @var array
|
||||
*/
|
||||
protected $scoutMetadata = [];
|
||||
|
||||
/**
|
||||
* @var Coroutine\Concurrent
|
||||
*/
|
||||
protected static $scoutRunner;
|
||||
|
||||
/**
|
||||
* Boot the trait.
|
||||
*/
|
||||
public static function bootSearchable()
|
||||
{
|
||||
static::addGlobalScope(make(SearchableScope::class));
|
||||
ListenerCollector::register(static::class, ModelObserver::class);
|
||||
if (! (static::$scoutRunner instanceof Coroutine\Concurrent)) {
|
||||
static::$scoutRunner = new Coroutine\Concurrent((new static())->syncWithSearchUsingConcurency());
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Dispatch the coroutine to make the given models searchable.
|
||||
*/
|
||||
public function queueMakeSearchable(Collection $models): void
|
||||
{
|
||||
if ($models->isEmpty()) {
|
||||
return;
|
||||
}
|
||||
if (Coroutine::inCoroutine()) {
|
||||
Coroutine::defer(function () use ($models) {
|
||||
$models->first()->searchableUsing()->update($models);
|
||||
});
|
||||
} else {
|
||||
self::$scoutRunner->create(function () use ($models) {
|
||||
$models->first()->searchableUsing()->update($models);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Dispatch the coroutine to make the given models unsearchable.
|
||||
* @param mixed $models
|
||||
*/
|
||||
public function queueRemoveFromSearch($models)
|
||||
{
|
||||
if ($models->isEmpty()) {
|
||||
return;
|
||||
}
|
||||
if (Coroutine::inCoroutine()) {
|
||||
Coroutine::defer(function () use ($models) {
|
||||
$models->first()->searchableUsing()->delete($models);
|
||||
});
|
||||
} else {
|
||||
self::$scoutRunner->create(function () use ($models) {
|
||||
$models->first()->searchableUsing()->delete($models);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Determine if the model should be searchable.
|
||||
*
|
||||
* @return bool
|
||||
*/
|
||||
public function shouldBeSearchable()
|
||||
{
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Perform a search against the model's indexed data.
|
||||
*/
|
||||
public static function search(?string $query = '', ?\Closure $callback = null)
|
||||
{
|
||||
return make(Builder::class, [
|
||||
'model' => new static(),
|
||||
'query' => $query,
|
||||
'callback' => $callback,
|
||||
'softDelete' => static::usesSoftDelete() && config('scout.soft_delete', false),
|
||||
]);
|
||||
}
|
||||
|
||||
/**
|
||||
* Make all instances of the model searchable.
|
||||
*/
|
||||
public static function makeAllSearchable()
|
||||
{
|
||||
$self = new static();
|
||||
$softDelete = static::usesSoftDelete() && config('scout.soft_delete', false);
|
||||
$self->newQuery()
|
||||
->when($softDelete, function ($query) {
|
||||
$query->withTrashed();
|
||||
})
|
||||
->orderBy($self->getKeyName())
|
||||
->searchable();
|
||||
}
|
||||
|
||||
/**
|
||||
* Make the given model instance searchable.
|
||||
*/
|
||||
public function searchable(): void
|
||||
{
|
||||
$this->newCollection([$this])->searchable();
|
||||
}
|
||||
|
||||
/**
|
||||
* Remove all instances of the model from the search index.
|
||||
*/
|
||||
public static function removeAllFromSearch(): void
|
||||
{
|
||||
$self = new static();
|
||||
$self->searchableUsing()->flush($self);
|
||||
}
|
||||
|
||||
/**
|
||||
* Remove the given model instance from the search index.
|
||||
*/
|
||||
public function unsearchable(): void
|
||||
{
|
||||
$this->newCollection([$this])->unsearchable();
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the requested models from an array of object IDs.
|
||||
*/
|
||||
public function getScoutModelsByIds(Builder $builder, array $ids)
|
||||
{
|
||||
$query = static::usesSoftDelete()
|
||||
? $this->withTrashed() : $this->newQuery();
|
||||
if ($builder->queryCallback) {
|
||||
call_user_func($builder->queryCallback, $query);
|
||||
}
|
||||
return $query->whereIn(
|
||||
$this->getScoutKeyName(),
|
||||
$ids
|
||||
)->get();
|
||||
}
|
||||
|
||||
/**
|
||||
* Enable search syncing for this model.
|
||||
*/
|
||||
public static function enableSearchSyncing(): void
|
||||
{
|
||||
ModelObserver::enableSyncingFor(get_called_class());
|
||||
}
|
||||
|
||||
/**
|
||||
* Disable search syncing for this model.
|
||||
*/
|
||||
public static function disableSearchSyncing(): void
|
||||
{
|
||||
ModelObserver::disableSyncingFor(get_called_class());
|
||||
}
|
||||
|
||||
/**
|
||||
* Temporarily disable search syncing for the given callback.
|
||||
*
|
||||
* @return mixed
|
||||
*/
|
||||
public static function withoutSyncingToSearch(callable $callback)
|
||||
{
|
||||
static::disableSearchSyncing();
|
||||
try {
|
||||
return $callback();
|
||||
} finally {
|
||||
static::enableSearchSyncing();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the index name for the model.
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
public function searchableAs()
|
||||
{
|
||||
return config('scout.prefix') . $this->getTable();
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the indexable data array for the model.
|
||||
*
|
||||
* @return array
|
||||
*/
|
||||
public function toSearchableArray()
|
||||
{
|
||||
return $this->toArray();
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the Scout engine for the model.
|
||||
*
|
||||
* @return mixed
|
||||
*/
|
||||
public function searchableUsing()
|
||||
{
|
||||
return ApplicationContext::getContainer()->get(EngineManager::class)->engine();
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the concurrency that should be used when syncing.
|
||||
*/
|
||||
public function syncWithSearchUsingConcurency(): int
|
||||
{
|
||||
return (int)config('scout.concurrency', 100);
|
||||
}
|
||||
|
||||
/**
|
||||
* Sync the soft deleted status for this model into the metadata.
|
||||
*
|
||||
* @return $this
|
||||
*/
|
||||
public function pushSoftDeleteMetadata()
|
||||
{
|
||||
return $this->withScoutMetadata('__soft_deleted', $this->trashed() ? 1 : 0);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get all Scout related metadata.
|
||||
*
|
||||
* @return array
|
||||
*/
|
||||
public function scoutMetadata()
|
||||
{
|
||||
return $this->scoutMetadata;
|
||||
}
|
||||
|
||||
/**
|
||||
* Set a Scout related metadata.
|
||||
*
|
||||
* @param string $key
|
||||
* @param mixed $value
|
||||
* @return $this
|
||||
*/
|
||||
public function withScoutMetadata($key, $value)
|
||||
{
|
||||
$this->scoutMetadata[$key] = $value;
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the value used to index the model.
|
||||
*
|
||||
* @return mixed
|
||||
*/
|
||||
public function getScoutKey()
|
||||
{
|
||||
return $this->getKey();
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the key name used to index the model.
|
||||
*
|
||||
* @return mixed
|
||||
*/
|
||||
public function getScoutKeyName()
|
||||
{
|
||||
return $this->getQualifiedKeyName();
|
||||
}
|
||||
|
||||
/**
|
||||
* Determine if the current class should use soft deletes with searching.
|
||||
*
|
||||
* @return bool
|
||||
*/
|
||||
protected static function usesSoftDelete()
|
||||
{
|
||||
return in_array(SoftDeletes::class, class_uses_recursive(get_called_class()));
|
||||
}
|
||||
}
|
68
src/scout/src/SearchableScope.php
Normal file
68
src/scout/src/SearchableScope.php
Normal file
@ -0,0 +1,68 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
/**
|
||||
* This file is part of Hyperf.
|
||||
*
|
||||
* @link https://www.hyperf.io
|
||||
* @document https://doc.hyperf.io
|
||||
* @contact group@hyperf.io
|
||||
* @license https://github.com/hyperf/hyperf/blob/master/LICENSE
|
||||
*/
|
||||
|
||||
namespace Hyperf\Scout;
|
||||
|
||||
use Hyperf\Database\Model\Builder as EloquentBuilder;
|
||||
use Hyperf\Database\Model\Model;
|
||||
use Hyperf\Database\Model\Scope;
|
||||
use Hyperf\Scout\Event\ModelsFlushed;
|
||||
use Hyperf\Scout\Event\ModelsImported;
|
||||
use Hyperf\Utils\ApplicationContext;
|
||||
use Psr\EventDispatcher\EventDispatcherInterface;
|
||||
|
||||
class SearchableScope implements Scope
|
||||
{
|
||||
/**
|
||||
* @var EventDispatcherInterface
|
||||
*/
|
||||
private $dispatcher;
|
||||
|
||||
public function __construct(?EventDispatcherInterface $dispatcher = null)
|
||||
{
|
||||
if (ApplicationContext::hasContainer()) {
|
||||
$this->dispatcher = $dispatcher ?? ApplicationContext::getContainer()->get(EventDispatcherInterface::class);
|
||||
} else {
|
||||
$this->dispatcher = $dispatcher;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Apply the scope to a given Eloquent query builder.
|
||||
*/
|
||||
public function apply(EloquentBuilder $builder, Model $model)
|
||||
{
|
||||
}
|
||||
|
||||
/**
|
||||
* Extend the query builder with the needed functions.
|
||||
*/
|
||||
public function extend(EloquentBuilder $builder)
|
||||
{
|
||||
$builder->macro('searchable', function (EloquentBuilder $builder, $chunk = null) {
|
||||
$builder->chunk($chunk ?: config('scout.chunk.searchable', 500), function ($models) {
|
||||
$models->filter->shouldBeSearchable()->searchable();
|
||||
if ($this->dispatcher !== null) {
|
||||
$this->dispatcher->dispatch(new ModelsImported($models));
|
||||
}
|
||||
});
|
||||
});
|
||||
$builder->macro('unsearchable', function (EloquentBuilder $builder, $chunk = null) {
|
||||
$builder->chunk($chunk ?: config('scout.chunk.unsearchable', 500), function ($models) {
|
||||
$models->unsearchable();
|
||||
if ($this->dispatcher !== null) {
|
||||
$this->dispatcher->dispatch(new ModelsFlushed($models));
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
}
|
58
src/scout/tests/Cases/BuilderTest.php
Normal file
58
src/scout/tests/Cases/BuilderTest.php
Normal file
@ -0,0 +1,58 @@
|
||||
<?php
|
||||
|
||||
|
||||
namespace HyperfTest\Scout\Cases;
|
||||
|
||||
use Hyperf\Database\Model\Collection;
|
||||
use Hyperf\Database\Model\Model;
|
||||
use Hyperf\Paginator\Paginator;
|
||||
use Hyperf\Scout\Builder;
|
||||
use Mockery as m;
|
||||
use PHPUnit\Framework\TestCase;
|
||||
use stdClass;
|
||||
|
||||
class BuilderTest extends TestCase
|
||||
{
|
||||
protected function tearDown(): void
|
||||
{
|
||||
m::close();
|
||||
$this->assertTrue(true);
|
||||
}
|
||||
public function test_pagination_correctly_handles_paginated_results()
|
||||
{
|
||||
Paginator::currentPageResolver(function () {
|
||||
return 1;
|
||||
});
|
||||
Paginator::currentPathResolver(function () {
|
||||
return 'http://localhost/foo';
|
||||
});
|
||||
$builder = new Builder($model = m::mock(Model::class), 'zonda');
|
||||
$model->shouldReceive('getPerPage')->andReturn(15);
|
||||
$model->shouldReceive('searchableUsing')->andReturn($engine = m::mock());
|
||||
$engine->shouldReceive('paginate');
|
||||
$engine->shouldReceive('map')->andReturn($results = Collection::make([new stdClass]));
|
||||
$engine->shouldReceive('getTotalCount');
|
||||
$model->shouldReceive('newCollection')->andReturn($results);
|
||||
$builder->paginate();
|
||||
}
|
||||
public function test_macroable()
|
||||
{
|
||||
Builder::macro('foo', function () {
|
||||
return 'bar';
|
||||
});
|
||||
$builder = new Builder($model = m::mock(Model::class), 'zonda');
|
||||
$this->assertEquals(
|
||||
'bar', $builder->foo()
|
||||
);
|
||||
}
|
||||
public function test_hard_delete_doesnt_set_wheres()
|
||||
{
|
||||
$builder = new Builder($model = m::mock(Model::class), 'zonda', null, false);
|
||||
$this->assertArrayNotHasKey('__soft_deleted', $builder->wheres);
|
||||
}
|
||||
public function test_soft_delete_sets_wheres()
|
||||
{
|
||||
$builder = new Builder($model = m::mock(Model::class), 'zonda', null, true);
|
||||
$this->assertEquals(0, $builder->wheres['__soft_deleted']);
|
||||
}
|
||||
}
|
123
src/scout/tests/Cases/ElasticsearchEngineTest.php
Normal file
123
src/scout/tests/Cases/ElasticsearchEngineTest.php
Normal file
@ -0,0 +1,123 @@
|
||||
<?php
|
||||
|
||||
namespace HyperfTest\Scout\Cases;
|
||||
use Hyperf\Database\Model\Collection;
|
||||
use Hyperf\Scout\Builder;
|
||||
use Hyperf\Scout\Engine\ElasticsearchEngine;
|
||||
use HyperfTest\Scout\Stub\ElasticsearchEngineTestModel;
|
||||
use Mockery;
|
||||
use PHPUnit\Framework\TestCase;
|
||||
|
||||
class ElasticsearchEngineTest extends TestCase
|
||||
{
|
||||
public function tearDown()
|
||||
{
|
||||
Mockery::close();
|
||||
$this->assertTrue(true);
|
||||
}
|
||||
public function test_update_adds_objects_to_index()
|
||||
{
|
||||
$client = Mockery::mock('Elasticsearch\Client');
|
||||
$client->shouldReceive('bulk')->with([
|
||||
'body' => [
|
||||
[
|
||||
'update' => [
|
||||
'_id' => 1,
|
||||
'_index' => 'scout',
|
||||
'_type' => 'table',
|
||||
]
|
||||
],
|
||||
[
|
||||
'doc' => ['id' => 1 ],
|
||||
'doc_as_upsert' => true
|
||||
]
|
||||
]
|
||||
]);
|
||||
$engine = new ElasticsearchEngine($client, 'scout');
|
||||
$engine->update(Collection::make([new ElasticsearchEngineTestModel]));
|
||||
}
|
||||
public function test_delete_removes_objects_to_index()
|
||||
{
|
||||
$client = Mockery::mock('Elasticsearch\Client');
|
||||
$client->shouldReceive('bulk')->with([
|
||||
'body' => [
|
||||
[
|
||||
'delete' => [
|
||||
'_id' => 1,
|
||||
'_index' => 'scout',
|
||||
'_type' => 'table',
|
||||
]
|
||||
],
|
||||
]
|
||||
]);
|
||||
$engine = new ElasticsearchEngine($client, 'scout');
|
||||
$engine->delete(Collection::make([new ElasticsearchEngineTestModel]));
|
||||
}
|
||||
public function test_search_sends_correct_parameters_to_elasticsearch()
|
||||
{
|
||||
$client = Mockery::mock('Elasticsearch\Client');
|
||||
$client->shouldReceive('search')->with([
|
||||
'index' => 'scout',
|
||||
'type' => 'table',
|
||||
'body' => [
|
||||
'query' => [
|
||||
'bool' => [
|
||||
'must' => [
|
||||
['query_string' => ['query' => '*zonda*']],
|
||||
['match_phrase' => ['foo' => 1]],
|
||||
['terms' => ['bar' => [1, 3]]],
|
||||
]
|
||||
]
|
||||
],
|
||||
'sort' => [
|
||||
['id' => 'desc']
|
||||
]
|
||||
]
|
||||
]);
|
||||
$engine = new ElasticsearchEngine($client, 'scout');
|
||||
$builder = new Builder(new ElasticsearchEngineTestModel, 'zonda');
|
||||
$builder->where('foo', 1);
|
||||
$builder->where('bar', [1, 3]);
|
||||
$builder->orderBy('id', 'desc');
|
||||
$engine->search($builder);
|
||||
}
|
||||
public function test_builder_callback_can_manipulate_search_parameters_to_elasticsearch()
|
||||
{
|
||||
/** @var \Elasticsearch\Client|\Mockery\MockInterface $client */
|
||||
$client = Mockery::mock(\Elasticsearch\Client::class);
|
||||
$client->shouldReceive('search')->with('modified_by_callback');
|
||||
$engine = new ElasticsearchEngine($client, 'scout');
|
||||
$builder = new Builder(
|
||||
new ElasticsearchEngineTestModel(),
|
||||
'huayra',
|
||||
function (\Elasticsearch\Client $client, $query, $params) {
|
||||
$this->assertNotEmpty($params);
|
||||
$this->assertEquals('huayra', $query);
|
||||
$params = 'modified_by_callback';
|
||||
return $client->search($params);
|
||||
}
|
||||
);
|
||||
$engine->search($builder);
|
||||
}
|
||||
public function test_map_correctly_maps_results_to_models()
|
||||
{
|
||||
$client = Mockery::mock('Elasticsearch\Client');
|
||||
$engine = new ElasticsearchEngine($client, 'scout');
|
||||
$builder = Mockery::mock(Builder::class);
|
||||
$model = Mockery::mock('Illuminate\Database\Eloquent\Model');
|
||||
$model->shouldReceive('getScoutKey')->andReturn('1');
|
||||
$model->shouldReceive('getScoutModelsByIds')->once()->with($builder, ['1'])->andReturn($models = Collection::make([$model]));
|
||||
$model->shouldReceive('newCollection')->andReturn($models);
|
||||
$results = $engine->map($builder, [
|
||||
'hits' => [
|
||||
'total' => '1',
|
||||
'hits' => [
|
||||
[
|
||||
'_id' => '1'
|
||||
]
|
||||
]
|
||||
]
|
||||
], $model);
|
||||
$this->assertEquals(1, count($results));
|
||||
}
|
||||
}
|
63
src/scout/tests/Cases/ModelObserverTest.php
Normal file
63
src/scout/tests/Cases/ModelObserverTest.php
Normal file
@ -0,0 +1,63 @@
|
||||
<?php
|
||||
|
||||
|
||||
namespace HyperfTest\Scout\Cases;
|
||||
|
||||
|
||||
use Hyperf\Database\Model\Events\Deleted;
|
||||
use Hyperf\Database\Model\Events\Restored;
|
||||
use Hyperf\Database\Model\Events\Saved;
|
||||
use Hyperf\Database\Model\Model;
|
||||
use Hyperf\Scout\ModelObserver;
|
||||
use Mockery as m;
|
||||
use PHPUnit\Framework\TestCase;
|
||||
|
||||
class ModelObserverTest extends TestCase
|
||||
{
|
||||
protected function tearDown(): void
|
||||
{
|
||||
m::close();
|
||||
$this->assertTrue(true);
|
||||
}
|
||||
public function test_saved_handler_makes_model_searchable()
|
||||
{
|
||||
$observer = new ModelObserver;
|
||||
$model = m::mock(Model::class);
|
||||
$model->shouldReceive('shouldBeSearchable')->andReturn(true);
|
||||
$model->shouldReceive('searchable');
|
||||
|
||||
$observer->saved(new Saved($model));
|
||||
}
|
||||
public function test_saved_handler_doesnt_make_model_searchable_when_disabled()
|
||||
{
|
||||
$observer = new ModelObserver;
|
||||
$model = m::mock(Model::class);
|
||||
$observer->disableSyncingFor(get_class($model));
|
||||
$model->shouldReceive('searchable')->never();
|
||||
$observer->saved(new Saved($model));
|
||||
}
|
||||
public function test_saved_handler_makes_model_unsearchable_when_disabled_per_model_rule()
|
||||
{
|
||||
$observer = new ModelObserver;
|
||||
$model = m::mock(Model::class);
|
||||
$model->shouldReceive('shouldBeSearchable')->andReturn(false);
|
||||
$model->shouldReceive('searchable')->never();
|
||||
$model->shouldReceive('unsearchable');
|
||||
$observer->saved(new Saved($model));
|
||||
}
|
||||
public function test_deleted_handler_makes_model_unsearchable()
|
||||
{
|
||||
$observer = new ModelObserver;
|
||||
$model = m::mock(Model::class);
|
||||
$model->shouldReceive('unsearchable');
|
||||
$observer->deleted(new Deleted($model));
|
||||
}
|
||||
public function test_restored_handler_makes_model_searchable()
|
||||
{
|
||||
$observer = new ModelObserver;
|
||||
$model = m::mock(Model::class);
|
||||
$model->shouldReceive('shouldBeSearchable')->andReturn(true);
|
||||
$model->shouldReceive('searchable');
|
||||
$observer->restored(new Restored($model));
|
||||
}
|
||||
}
|
79
src/scout/tests/Cases/SearchableTest.php
Normal file
79
src/scout/tests/Cases/SearchableTest.php
Normal file
@ -0,0 +1,79 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
/**
|
||||
* This file is part of Hyperf.
|
||||
*
|
||||
* @link https://www.hyperf.io
|
||||
* @document https://doc.hyperf.io
|
||||
* @contact group@hyperf.io
|
||||
* @license https://github.com/hyperf/hyperf/blob/master/LICENSE
|
||||
*/
|
||||
|
||||
namespace HyperfTest\Scout\Cases;
|
||||
|
||||
use HyperfTest\Scout\Stub\ModelStubForMakeAllSearchable;
|
||||
use HyperfTest\Scout\Stub\SearchableModel;
|
||||
use Mockery as m;
|
||||
use PHPUnit\Framework\TestCase;
|
||||
|
||||
/**
|
||||
* @internal
|
||||
* @coversNothing
|
||||
*/
|
||||
class SearchableTest extends TestCase
|
||||
{
|
||||
protected function tearDown(): void
|
||||
{
|
||||
m::close();
|
||||
$this->assertTrue(true);
|
||||
}
|
||||
|
||||
public function testSearchableUsingUpdateIsCalledOnCollection()
|
||||
{
|
||||
$collection = m::mock(\Hyperf\Database\Model\Collection::class);
|
||||
$collection->shouldReceive('isEmpty')->andReturn(false);
|
||||
$collection->shouldReceive('first->searchableUsing->update')->with($collection);
|
||||
$model = new SearchableModel();
|
||||
$model->queueMakeSearchable($collection);
|
||||
}
|
||||
|
||||
public function testSearchableUsingUpdateIsNotCalledOnEmptyCollection()
|
||||
{
|
||||
$collection = m::mock(\Hyperf\Database\Model\Collection::class);
|
||||
$collection->shouldReceive('isEmpty')->andReturn(true);
|
||||
$collection->shouldNotReceive('first->searchableUsing->update');
|
||||
$model = new SearchableModel();
|
||||
$model->queueMakeSearchable($collection);
|
||||
}
|
||||
|
||||
public function testSearchableUsingDeleteIsCalledOnCollection()
|
||||
{
|
||||
$collection = m::mock(\Hyperf\Database\Model\Collection::class);
|
||||
$collection->shouldReceive('isEmpty')->andReturn(false);
|
||||
$collection->shouldReceive('first->searchableUsing->delete')->with($collection);
|
||||
$model = new SearchableModel();
|
||||
$model->queueRemoveFromSearch($collection);
|
||||
}
|
||||
|
||||
public function testSearchableUsingDeleteIsNotCalledOnEmptyCollection()
|
||||
{
|
||||
$collection = m::mock(\Hyperf\Database\Model\Collection::class);
|
||||
$collection->shouldReceive('isEmpty')->andReturn(true);
|
||||
$collection->shouldNotReceive('first->searchableUsing->delete');
|
||||
$model = new SearchableModel();
|
||||
$model->queueRemoveFromSearch($collection);
|
||||
}
|
||||
|
||||
public function testMakeAllSearchableUsesOrderBy()
|
||||
{
|
||||
ModelStubForMakeAllSearchable::makeAllSearchable();
|
||||
}
|
||||
}
|
||||
|
||||
namespace Hyperf\Scout;
|
||||
|
||||
function config($arg)
|
||||
{
|
||||
return false;
|
||||
}
|
27
src/scout/tests/Stub/ElasticsearchEngineTestModel.php
Normal file
27
src/scout/tests/Stub/ElasticsearchEngineTestModel.php
Normal file
@ -0,0 +1,27 @@
|
||||
<?php
|
||||
|
||||
|
||||
namespace HyperfTest\Scout\Stub;
|
||||
|
||||
|
||||
use Hyperf\Database\Model\Model;
|
||||
|
||||
class ElasticsearchEngineTestModel extends Model
|
||||
{
|
||||
public function getIdAttribute()
|
||||
{
|
||||
return 1;
|
||||
}
|
||||
public function searchableAs()
|
||||
{
|
||||
return 'table';
|
||||
}
|
||||
public function getKey()
|
||||
{
|
||||
return '1';
|
||||
}
|
||||
public function toSearchableArray()
|
||||
{
|
||||
return ['id' => 1];
|
||||
}
|
||||
}
|
30
src/scout/tests/Stub/ModelStubForMakeAllSearchable.php
Normal file
30
src/scout/tests/Stub/ModelStubForMakeAllSearchable.php
Normal file
@ -0,0 +1,30 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
/**
|
||||
* This file is part of Hyperf.
|
||||
*
|
||||
* @link https://www.hyperf.io
|
||||
* @document https://doc.hyperf.io
|
||||
* @contact group@hyperf.io
|
||||
* @license https://github.com/hyperf/hyperf/blob/master/LICENSE
|
||||
*/
|
||||
|
||||
namespace HyperfTest\Scout\Stub;
|
||||
|
||||
use Hyperf\Scout\Builder;
|
||||
use Mockery as m;
|
||||
|
||||
class ModelStubForMakeAllSearchable extends SearchableModel
|
||||
{
|
||||
public function newQuery()
|
||||
{
|
||||
$mock = m::mock(Builder::class);
|
||||
$mock->shouldReceive('orderBy')
|
||||
->with('id')
|
||||
->andReturnSelf()
|
||||
->shouldReceive('searchable');
|
||||
$mock->shouldReceive('when')->andReturnSelf();
|
||||
return $mock;
|
||||
}
|
||||
}
|
38
src/scout/tests/Stub/SearchableModel.php
Normal file
38
src/scout/tests/Stub/SearchableModel.php
Normal file
@ -0,0 +1,38 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
/**
|
||||
* This file is part of Hyperf.
|
||||
*
|
||||
* @link https://www.hyperf.io
|
||||
* @document https://doc.hyperf.io
|
||||
* @contact group@hyperf.io
|
||||
* @license https://github.com/hyperf/hyperf/blob/master/LICENSE
|
||||
*/
|
||||
|
||||
namespace HyperfTest\Scout\Stub;
|
||||
|
||||
use Hyperf\Database\Model\Model;
|
||||
use Hyperf\Scout\Searchable;
|
||||
|
||||
class SearchableModel extends Model
|
||||
{
|
||||
use Searchable;
|
||||
|
||||
/**
|
||||
* The attributes that are mass assignable.
|
||||
*
|
||||
* @var array
|
||||
*/
|
||||
protected $fillable = ['id'];
|
||||
|
||||
public function searchableAs()
|
||||
{
|
||||
return 'table';
|
||||
}
|
||||
|
||||
public function scoutMetadata()
|
||||
{
|
||||
return [];
|
||||
}
|
||||
}
|
13
src/scout/tests/bootstrap.php
Normal file
13
src/scout/tests/bootstrap.php
Normal file
@ -0,0 +1,13 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
/**
|
||||
* This file is part of Hyperf.
|
||||
*
|
||||
* @link https://www.hyperf.io
|
||||
* @document https://doc.hyperf.io
|
||||
* @contact group@hyperf.io
|
||||
* @license https://github.com/hyperf/hyperf/blob/master/LICENSE
|
||||
*/
|
||||
|
||||
require_once dirname(dirname(__FILE__)) . '/vendor/autoload.php';
|
8
src/scout/tests/ci.ini
Normal file
8
src/scout/tests/ci.ini
Normal file
@ -0,0 +1,8 @@
|
||||
[opcache]
|
||||
opcache.enable_cli=1
|
||||
|
||||
[redis]
|
||||
extension = "redis.so"
|
||||
|
||||
[swoole]
|
||||
extension = "swoole.so"
|
10
src/scout/tests/swoole.install.sh
Normal file
10
src/scout/tests/swoole.install.sh
Normal file
@ -0,0 +1,10 @@
|
||||
#!/usr/bin/env bash
|
||||
wget https://github.com/swoole/swoole-src/archive/v"${SW_VERSION}".tar.gz -O swoole.tar.gz
|
||||
mkdir -p swoole
|
||||
tar -xf swoole.tar.gz -C swoole --strip-components=1
|
||||
rm swoole.tar.gz
|
||||
cd swoole || exit
|
||||
phpize
|
||||
./configure --enable-openssl --enable-mysqlnd
|
||||
make -j "$(nproc)"
|
||||
make install
|
Loading…
Reference in New Issue
Block a user