Add Scout Component

This commit is contained in:
Reasno 2019-12-23 15:34:00 +08:00
parent 4faa34aec9
commit 012e89d7d2
35 changed files with 2156 additions and 0 deletions

View File

@ -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
View File

@ -0,0 +1 @@
/tests export-ignore

4
src/scout/.gitignore vendored Normal file
View File

@ -0,0 +1,4 @@
/vendor/
composer.lock
*.cache
*.log

87
src/scout/.php_cs Normal file
View 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);

View File

@ -0,0 +1,6 @@
<?php
namespace PHPSTORM_META {
// Reflect
override(\Psr\Container\ContainerInterface::get(0), map('@'));
}

40
src/scout/.travis.yml Normal file
View 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
View File

@ -0,0 +1,5 @@
# component-creater
```
composer create-project hyperf/component-creater
```

43
src/scout/composer.json Normal file
View 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
View 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>

View 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
View 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();
}
}

View 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,
],
];
}
}

View 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'],
];
}
}

View 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'],
];
}
}

View 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();
}
}

View 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
);
}
}

View 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
{
}
}

View 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);
}
}

View 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;
}
}

View 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;
}
}

View 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));
}
}

View 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);
}
}

View 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;
}

View 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()));
}
}

View 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));
}
});
});
}
}

View 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']);
}
}

View 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));
}
}

View 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));
}
}

View 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;
}

View 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];
}
}

View 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;
}
}

View 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 [];
}
}

View 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
View File

@ -0,0 +1,8 @@
[opcache]
opcache.enable_cli=1
[redis]
extension = "redis.so"
[swoole]
extension = "swoole.so"

View 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