From c109a03d5a22b908770ab41c7a61de17f8939bef Mon Sep 17 00:00:00 2001 From: Pavel Naumov Date: Thu, 19 May 2016 13:34:53 +0300 Subject: [PATCH] merge search --- .travis.yml | 8 + README.md | 134 +++++ composer.json | 5 +- composer.lock | 465 +++++++++++++----- src/Properties/Module.php | 49 +- src/Properties/views/manage/edit-property.php | 1 + src/behaviors/HasProperties.php | 18 +- src/commands/ElasticIndexController.php | 308 ++++++++++++ src/events/HasPropertiesEvent.php | 16 + src/helpers/PropertiesHelper.php | 1 - .../m160429_121414_add_in_search_column.php | 21 + src/models/Property.php | 5 +- .../AbstractPropertyStorage.php | 43 +- .../FiltrableStorageInterface.php | 53 -- src/propertyStorage/StaticValues.php | 126 ++++- src/search/base/AbstractSearch.php | 69 +++ src/search/base/AbstractWatch.php | 38 ++ src/search/common/Search.php | 121 +++++ src/search/elastic/Search.php | 219 +++++++++ src/search/elastic/Watch.php | 256 ++++++++++ src/search/elastic/helpers/IndexHelper.php | 69 +++ src/search/helpers/LanguageHelper.php | 36 ++ .../helpers/PropertiesFilterHelper.php | 35 +- src/search/interfaces/Filter.php | 103 ++++ src/search/interfaces/Search.php | 36 ++ src/search/interfaces/Watch.php | 37 ++ src/search/widgets/FilterFormWidget.php | 67 +++ src/search/widgets/views/default.php | 26 + src/translations/ru/app.php | 11 +- tests/CommonSearchTest.php | 156 ++++++ tests/ConfigTest.php | 85 ++++ tests/DSTCommonTestCase.php | 148 ++++++ tests/ElasticIndexControllerTest.php | 60 +++ tests/ElasticSearchFindEmptyStorageTest.php | 33 ++ tests/ElasticSearchFindTest.php | 29 ++ tests/ElasticSearchFindWrongModelTest.php | 23 + tests/ElasticSearchTest.php | 48 ++ tests/ElasticWatchDeleteTest.php | 34 ++ tests/ElasticWatchUpdateTest.php | 46 ++ tests/FilterTest.php | 8 +- tests/IndexHelperNoDbTest.php | 37 ++ tests/IndexHelperTest.php | 85 ++++ tests/LanguageHelperTest.php | 48 ++ tests/config/console.php | 22 +- tests/config/db.php | 7 + tests/data/filters.xml | 41 +- 46 files changed, 3053 insertions(+), 233 deletions(-) create mode 100644 src/commands/ElasticIndexController.php create mode 100644 src/events/HasPropertiesEvent.php create mode 100644 src/migrations/m160429_121414_add_in_search_column.php delete mode 100644 src/propertyStorage/FiltrableStorageInterface.php create mode 100644 src/search/base/AbstractSearch.php create mode 100644 src/search/base/AbstractWatch.php create mode 100644 src/search/common/Search.php create mode 100644 src/search/elastic/Search.php create mode 100644 src/search/elastic/Watch.php create mode 100644 src/search/elastic/helpers/IndexHelper.php create mode 100644 src/search/helpers/LanguageHelper.php rename src/{ => search}/helpers/PropertiesFilterHelper.php (74%) create mode 100644 src/search/interfaces/Filter.php create mode 100644 src/search/interfaces/Search.php create mode 100644 src/search/interfaces/Watch.php create mode 100644 src/search/widgets/FilterFormWidget.php create mode 100644 src/search/widgets/views/default.php create mode 100644 tests/CommonSearchTest.php create mode 100644 tests/ConfigTest.php create mode 100644 tests/DSTCommonTestCase.php create mode 100644 tests/ElasticIndexControllerTest.php create mode 100644 tests/ElasticSearchFindEmptyStorageTest.php create mode 100644 tests/ElasticSearchFindTest.php create mode 100644 tests/ElasticSearchFindWrongModelTest.php create mode 100644 tests/ElasticSearchTest.php create mode 100644 tests/ElasticWatchDeleteTest.php create mode 100644 tests/ElasticWatchUpdateTest.php create mode 100644 tests/IndexHelperNoDbTest.php create mode 100644 tests/IndexHelperTest.php create mode 100644 tests/LanguageHelperTest.php create mode 100644 tests/config/db.php diff --git a/.travis.yml b/.travis.yml index 12694c8..bb73882 100644 --- a/.travis.yml +++ b/.travis.yml @@ -6,11 +6,17 @@ php: - 7.0 - hhvm +sudo: true + matrix: fast_finish: true allow_failures: - php: hhvm +before_install: + - curl -L -O https://download.elastic.co/elasticsearch/release/org/elasticsearch/distribution/tar/elasticsearch/2.3.3/elasticsearch-2.3.3.tar.gz + - tar -xvf elasticsearch-2.3.3.tar.gz + install: - travis_retry composer self-update && composer --version - travis_retry composer global require fxp/composer-asset-plugin:~1.0 @@ -20,6 +26,8 @@ before_script: - mysql -e 'create database yii2_datastructure;' script: + - elasticsearch-2.3.3/bin/elasticsearch & + - sleep 10 - vendor/bin/phpunit --coverage-clover=coverage.xml --verbose $PHPUNIT_FLAGS after_script: diff --git a/README.md b/README.md index ae57e31..022b9c4 100644 --- a/README.md +++ b/README.md @@ -36,4 +36,138 @@ TBD Credits and inspiration sources ------------------------------- +Search concept +-------------- +*for now it works only with STATIC VALUES property storage* + +*Note, that search will work only with models with `DevGroup\DataStructure\traits\PropertiesTrait` trait +and `DevGroup\DataStructure\behaviors\HasProperties` behavior connected. See [how to connect (Russian for now)](/docs/ru/how-to-use.md)* + +Extension provides flexible system of search. Each property have configuration point that switches ability to use this property in search. + +Basic search will be done in two ways: + +- common search against regular databases e.g.: `mysql`, `mariadb` etc; +- elasticsearch indices search. + +Main feature is that if you want to use elasticseatch, and defined it in the app config, +but not still configure it your search will just work fine with auto fallback to simple mysql searching. And when your +elasticsearch will be properly started search will be automatically switched for elasticseatch. + +Preferred search mechanism you can define in the application configuration files, like this: +``` + 'modules' => [ + ... + 'properties' => [ + 'class' => 'DevGroup\DataStructure\Properties\Module', + 'searchClass' => \DevGroup\DataStructure\search\elastic\Search::class, + 'searchConfig' => [ + 'hosts' => ['host1:9200', 'https://host2:9200'], + 'watcherClass' => MyWatch::class, + ] + + ], + ... + ], +``` + +- `searchClass` - class to be used for search. If omit - there will be no search configured, +- `searchConfig` - array additional parameters to be applied for search object, except common search. + For elastic search following special keys may be set: + - `hosts` see [hosts config](https://www.elastic.co/guide/en/elasticsearch/client/php-api/current/_configuration.html), + - `watcherClass` - you can use your own watcher for elsticsearch if needed. + +If you want to start using elasticsearch, first of all you have to [install and configure it](https://www.elastic.co/guide/en/elasticsearch/reference/current/setup.html). + +Then, if you already have entries in your database you may want to generate and load start indices. For this run in console: +``` +./yii properties/elastic/fill-index +``` +This command will create indices for all properties that you allowed to search. + +# How to search +*For now only available to perform filtering against properties static values* +## At any place you want +``` + \app\models\Page::class, + 'filterRoute' => ['/url/to/filter'], + 'options' => [ + 'storage' => [ + EAV::class, + StaticValues::class, + ] + ] +]) ?> +``` +This will render basic filter form with all properties and values contained in the elasticsearch index +- `'modelClass'` - required param, any model class name you have in your app with assigned properties and their static values, +- `'filterRoute'` - required param, `action` attribute for rendered filter form, +- `'options'` - optional, additional array of config. Special key `storage` will be used for definition against what property storage +search will be proceed. If you omit it search will be work only against `StaticValues` storage by default + +## In your controller +``` +public function actionFilter() +{ + /** @var AbstractSearch $component */ + $search = \Yii::$app->getModule('properties')->getSearch(); + $config = ['storage' => [ + EAV::class, + StaticValues::class, + ] + ]; + $modelIds = $search->findInProperties(Page::class, $config); + $dataProvider = new ActiveDataProvider([ + //provider config + ]); + //other stuff here +} +``` +- `Page` - any model class name you have in your app with assigned properties and their static values +- `$modelIds` will contain all found model ids, according to selected property values in filter. Using them you can show anything you want, +- `'$config'` - optional, additional array of config. Special key `storage` will be used for definition against what property storage +search will be proceed. If you omit it search will be work only against `StaticValues` storage by default + +## Filtering logic + +Filters uses both intersection and union operations while search. + +Lets see, for example you have filter request like this: +``` +[ + 1 => [2,3], + 13 => [18,9,34] +] +``` +First of all this means that we want to find products that has property values assigned with id 2,3 from property with id 1, +and 18, 9, 34 from property with id 13. + +*What will filter do?* +- For now it will find all products with assigned values with ids IN(2,3); +- then it will find all products with assigned values with ids IN(12,9,34); +- and finally it will return to you result of intersection from both previous results. + +## How to extend and implement +For all `Search` and `Watch` mechanisms you can use your custom implementation. + +Actually you can create and use your own database connection, e.g.: `MongoDB`, `ArangoDB`. + +Or you can just use your custom `Watch` class for elasticsearch index actualization. + +Both `Search` and `Watch` classes are implements according interfaces and extends abstract classes +- `DevGroup\DataStructure\search\interfaces\Search` and `DevGroup\DataStructure\search\base\AbstractSearch` for `Search` +- `DevGroup\DataStructure\search\interfaces\Watch` and `DevGroup\DataStructure\search\base\AbstractWatch` for `Watch` + +Just extend your class from needed abstract class and define it in application config, like described upper. + +If you are realizing custom index, you probably need to create own controller for first time index initialization, like +`DevGroup\DataStructure\commands\ElasticIndexController` + +Define your own `Watch` class in your own `Search` class if necessary. + +Clearly created and defined `Watch` class will be automatically subscribed to according system events. + + + diff --git a/composer.json b/composer.json index 962b796..b084373 100644 --- a/composer.json +++ b/composer.json @@ -15,18 +15,19 @@ "php": ">=5.5.0", "yiisoft/yii2": "^2.0.6", "devgroup/yii2-tag-dependency-helper": "^1.3.0", - "yiisoft/yii2-elasticsearch": "^2.0@dev", "unclead/yii2-multiple-input": "~1.2.3", "devgroup/yii2-multilingual": "*", "arogachev/yii2-sortable": "^0.1.5", "kartik-v/yii2-widget-select2": "^2.0.0", "hector68/yii2-map-input-widget": "^2.0", + "elasticsearch/elasticsearch": "^2.1", "rmrevin/yii2-fontawesome": "~2.14" }, "require-dev": { "yiisoft/yii2-bootstrap": "^2.0@dev", "devgroup/yii2-admin-utils": "dev-master", - "devgroup/dotplant-dev": "~1.0.0" + "devgroup/dotplant-dev": "~1.0.0", + "elasticsearch/elasticsearch": "^2.1" }, "autoload": { "psr-4": { diff --git a/composer.lock b/composer.lock index 6d12245..88e11fc 100644 --- a/composer.lock +++ b/composer.lock @@ -372,12 +372,12 @@ "source": { "type": "git", "url": "https://github.com/lipis/flag-icon-css.git", - "reference": "9d82e6b083f136b43954eaa5f33b6bf17036ed9d" + "reference": "d9c140891b443b02acf7f2ebba9bb2121ea555a8" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/lipis/flag-icon-css/zipball/9d82e6b083f136b43954eaa5f33b6bf17036ed9d", - "reference": "9d82e6b083f136b43954eaa5f33b6bf17036ed9d", + "url": "https://api.github.com/repos/lipis/flag-icon-css/zipball/d9c140891b443b02acf7f2ebba9bb2121ea555a8", + "reference": "d9c140891b443b02acf7f2ebba9bb2121ea555a8", "shasum": "" }, "type": "component", @@ -402,7 +402,7 @@ } ], "description": "CSS for vector based country flags", - "time": "2016-04-16 11:47:36" + "time": "2016-05-08 12:24:15" }, { "name": "devgroup/yii2-multilingual", @@ -512,6 +512,60 @@ ], "time": "2016-01-15 09:29:24" }, + { + "name": "elasticsearch/elasticsearch", + "version": "v2.1.5", + "source": { + "type": "git", + "url": "https://github.com/elastic/elasticsearch-php.git", + "reference": "c1675245c0a6f789cbb80b3e0b333b9ef521a627" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/elastic/elasticsearch-php/zipball/c1675245c0a6f789cbb80b3e0b333b9ef521a627", + "reference": "c1675245c0a6f789cbb80b3e0b333b9ef521a627", + "shasum": "" + }, + "require": { + "guzzlehttp/ringphp": "~1.0", + "php": ">=5.4", + "psr/log": "~1.0" + }, + "require-dev": { + "athletic/athletic": "~0.1", + "cpliakas/git-wrapper": "~1.0", + "mockery/mockery": "0.9.4", + "phpunit/phpunit": "~4.7", + "symfony/yaml": "2.4.3 as 2.4.2", + "twig/twig": "1.*" + }, + "suggest": { + "ext-curl": "*", + "monolog/monolog": "Allows for client-level logging and tracing" + }, + "type": "library", + "autoload": { + "psr-4": { + "Elasticsearch\\": "src/Elasticsearch/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "Apache 2" + ], + "authors": [ + { + "name": "Zachary Tong" + } + ], + "description": "PHP Client for Elasticsearch", + "keywords": [ + "client", + "elasticsearch", + "search" + ], + "time": "2016-03-18 16:31:37" + }, { "name": "ezyang/htmlpurifier", "version": "v4.7.0", @@ -562,12 +616,12 @@ "source": { "type": "git", "url": "https://github.com/FortAwesome/Font-Awesome.git", - "reference": "06b2efcda0d4612eccdaf8b79ae428fd079f2dfb" + "reference": "97412d13845d680ab6fbc6fe5d933eec033fd696" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/FortAwesome/Font-Awesome/zipball/06b2efcda0d4612eccdaf8b79ae428fd079f2dfb", - "reference": "06b2efcda0d4612eccdaf8b79ae428fd079f2dfb", + "url": "https://api.github.com/repos/FortAwesome/Font-Awesome/zipball/97412d13845d680ab6fbc6fe5d933eec033fd696", + "reference": "97412d13845d680ab6fbc6fe5d933eec033fd696", "shasum": "" }, "require-dev": { @@ -577,7 +631,7 @@ "type": "library", "extra": { "branch-alias": { - "dev-master": "4.0.x-dev" + "dev-master": "4.6.x-dev" } }, "notification-url": "https://packagist.org/downloads/", @@ -602,7 +656,108 @@ "font", "icon" ], - "time": "2016-04-12 18:05:43" + "time": "2016-05-05 16:56:57" + }, + { + "name": "guzzlehttp/ringphp", + "version": "dev-master", + "source": { + "type": "git", + "url": "https://github.com/guzzle/RingPHP.git", + "reference": "9465032ac5d6beaa55f10923403e6e1c36018d9c" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/guzzle/RingPHP/zipball/9465032ac5d6beaa55f10923403e6e1c36018d9c", + "reference": "9465032ac5d6beaa55f10923403e6e1c36018d9c", + "shasum": "" + }, + "require": { + "guzzlehttp/streams": "~3.0", + "php": ">=5.4.0", + "react/promise": "~2.0" + }, + "require-dev": { + "ext-curl": "*", + "phpunit/phpunit": "~4.0" + }, + "suggest": { + "ext-curl": "Guzzle will use specific adapters if cURL is present" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "1.1-dev" + } + }, + "autoload": { + "psr-4": { + "GuzzleHttp\\Ring\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Michael Dowling", + "email": "mtdowling@gmail.com", + "homepage": "https://github.com/mtdowling" + } + ], + "description": "Provides a simple API and specification that abstracts away the details of HTTP into a single PHP function.", + "time": "2015-05-21 17:23:02" + }, + { + "name": "guzzlehttp/streams", + "version": "dev-master", + "source": { + "type": "git", + "url": "https://github.com/guzzle/streams.git", + "reference": "d99a261c616210618ab94fd319cb17eda458cc3e" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/guzzle/streams/zipball/d99a261c616210618ab94fd319cb17eda458cc3e", + "reference": "d99a261c616210618ab94fd319cb17eda458cc3e", + "shasum": "" + }, + "require": { + "php": ">=5.4.0" + }, + "require-dev": { + "phpunit/phpunit": "~4.0" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "3.0-dev" + } + }, + "autoload": { + "psr-4": { + "GuzzleHttp\\Stream\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Michael Dowling", + "email": "mtdowling@gmail.com", + "homepage": "https://github.com/mtdowling" + } + ], + "description": "Provides a simple abstraction over streams of data", + "homepage": "http://guzzlephp.org/", + "keywords": [ + "Guzzle", + "stream" + ], + "time": "2016-04-13 16:32:01" }, { "name": "hector68/yii2-map-input-widget", @@ -703,12 +858,12 @@ "source": { "type": "git", "url": "https://github.com/kartik-v/yii2-krajee-base.git", - "reference": "3115b09aeb15a5e06f38dc16860baf153d9bf70e" + "reference": "7f7a45ffe193fe673f72b9effc01b08499f2a454" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/kartik-v/yii2-krajee-base/zipball/3115b09aeb15a5e06f38dc16860baf153d9bf70e", - "reference": "3115b09aeb15a5e06f38dc16860baf153d9bf70e", + "url": "https://api.github.com/repos/kartik-v/yii2-krajee-base/zipball/7f7a45ffe193fe673f72b9effc01b08499f2a454", + "reference": "7f7a45ffe193fe673f72b9effc01b08499f2a454", "shasum": "" }, "require": { @@ -746,7 +901,7 @@ "widget", "yii2" ], - "time": "2016-04-11 09:07:40" + "time": "2016-05-07 19:45:52" }, { "name": "kartik-v/yii2-widget-select2", @@ -754,12 +909,12 @@ "source": { "type": "git", "url": "https://github.com/kartik-v/yii2-widget-select2.git", - "reference": "cb2a5992cb96bd2939e30ec1c76eba418d6a30af" + "reference": "6919d5096ca83ab4a1b163fcd09fb023814d1003" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/kartik-v/yii2-widget-select2/zipball/cb2a5992cb96bd2939e30ec1c76eba418d6a30af", - "reference": "cb2a5992cb96bd2939e30ec1c76eba418d6a30af", + "url": "https://api.github.com/repos/kartik-v/yii2-widget-select2/zipball/6919d5096ca83ab4a1b163fcd09fb023814d1003", + "reference": "6919d5096ca83ab4a1b163fcd09fb023814d1003", "shasum": "" }, "require": { @@ -799,7 +954,98 @@ "widget", "yii2" ], - "time": "2016-03-10 11:33:59" + "time": "2016-04-19 13:22:56" + }, + { + "name": "psr/log", + "version": "dev-master", + "source": { + "type": "git", + "url": "https://github.com/php-fig/log.git", + "reference": "d8e60a5619fff77f9669da8997697443ef1a1d7e" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/php-fig/log/zipball/d8e60a5619fff77f9669da8997697443ef1a1d7e", + "reference": "d8e60a5619fff77f9669da8997697443ef1a1d7e", + "shasum": "" + }, + "require": { + "php": ">=5.3.0" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "1.0.x-dev" + } + }, + "autoload": { + "psr-4": { + "Psr\\Log\\": "Psr/Log/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "PHP-FIG", + "homepage": "http://www.php-fig.org/" + } + ], + "description": "Common interface for logging libraries", + "homepage": "https://github.com/php-fig/log", + "keywords": [ + "log", + "psr", + "psr-3" + ], + "time": "2016-01-06 21:40:42" + }, + { + "name": "react/promise", + "version": "v2.4.1", + "source": { + "type": "git", + "url": "https://github.com/reactphp/promise.git", + "reference": "8025426794f1944de806618671d4fa476dc7626f" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/reactphp/promise/zipball/8025426794f1944de806618671d4fa476dc7626f", + "reference": "8025426794f1944de806618671d4fa476dc7626f", + "shasum": "" + }, + "require": { + "php": ">=5.4.0" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "2.0-dev" + } + }, + "autoload": { + "psr-4": { + "React\\Promise\\": "src/" + }, + "files": [ + "src/functions_include.php" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Jan Sorgalla", + "email": "jsorgalla@gmail.com" + } + ], + "description": "A lightweight implementation of CommonJS Promises/A for PHP", + "time": "2016-05-03 17:50:52" }, { "name": "rmrevin/yii2-fontawesome", @@ -855,16 +1101,16 @@ }, { "name": "unclead/yii2-multiple-input", - "version": "1.2.14", + "version": "1.2.15", "source": { "type": "git", "url": "https://github.com/unclead/yii2-multiple-input.git", - "reference": "9f2d4ea5f22c92a0b6023ce0201d901bcf76a7bb" + "reference": "56d92b27cb60f76a42bf994230a0f66e3f918339" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/unclead/yii2-multiple-input/zipball/9f2d4ea5f22c92a0b6023ce0201d901bcf76a7bb", - "reference": "9f2d4ea5f22c92a0b6023ce0201d901bcf76a7bb", + "url": "https://api.github.com/repos/unclead/yii2-multiple-input/zipball/56d92b27cb60f76a42bf994230a0f66e3f918339", + "reference": "56d92b27cb60f76a42bf994230a0f66e3f918339", "shasum": "" }, "require": { @@ -896,7 +1142,7 @@ "yii2 multiple input", "yii2 tabular input" ], - "time": "2016-04-18 04:31:29" + "time": "2016-05-03 09:45:21" }, { "name": "yii2tech/filedb", @@ -952,12 +1198,12 @@ "source": { "type": "git", "url": "https://github.com/yiisoft/yii2-framework.git", - "reference": "b7e62df2cfa1dfab4e70223770a99c3798d4a412" + "reference": "5ed762ebf18945ef3282d0910d1a8b03603c7a7b" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/yiisoft/yii2-framework/zipball/b7e62df2cfa1dfab4e70223770a99c3798d4a412", - "reference": "b7e62df2cfa1dfab4e70223770a99c3798d4a412", + "url": "https://api.github.com/repos/yiisoft/yii2-framework/zipball/5ed762ebf18945ef3282d0910d1a8b03603c7a7b", + "reference": "5ed762ebf18945ef3282d0910d1a8b03603c7a7b", "shasum": "" }, "require": { @@ -1038,7 +1284,7 @@ "framework", "yii2" ], - "time": "2016-04-18 09:49:22" + "time": "2016-05-15 22:11:47" }, { "name": "yiisoft/yii2-bootstrap", @@ -1046,12 +1292,12 @@ "source": { "type": "git", "url": "https://github.com/yiisoft/yii2-bootstrap.git", - "reference": "772b610ea7940059584f9220f7b87e4b2b1a0e78" + "reference": "61ff14445779b225baa8e60cf4eddbc46cc14504" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/yiisoft/yii2-bootstrap/zipball/772b610ea7940059584f9220f7b87e4b2b1a0e78", - "reference": "772b610ea7940059584f9220f7b87e4b2b1a0e78", + "url": "https://api.github.com/repos/yiisoft/yii2-bootstrap/zipball/61ff14445779b225baa8e60cf4eddbc46cc14504", + "reference": "61ff14445779b225baa8e60cf4eddbc46cc14504", "shasum": "" }, "require": { @@ -1088,7 +1334,7 @@ "bootstrap", "yii2" ], - "time": "2016-04-14 08:46:15" + "time": "2016-04-18 20:34:24" }, { "name": "yiisoft/yii2-composer", @@ -1137,55 +1383,6 @@ ], "time": "2016-04-14 08:46:37" }, - { - "name": "yiisoft/yii2-elasticsearch", - "version": "dev-master", - "source": { - "type": "git", - "url": "https://github.com/yiisoft/yii2-elasticsearch.git", - "reference": "735a93d3e72d1d599a95848fb6fd2406b7939c21" - }, - "dist": { - "type": "zip", - "url": "https://api.github.com/repos/yiisoft/yii2-elasticsearch/zipball/735a93d3e72d1d599a95848fb6fd2406b7939c21", - "reference": "735a93d3e72d1d599a95848fb6fd2406b7939c21", - "shasum": "" - }, - "require": { - "ext-curl": "*", - "yiisoft/yii2": "*" - }, - "type": "yii2-extension", - "extra": { - "branch-alias": { - "dev-master": "2.0.x-dev" - } - }, - "autoload": { - "psr-4": { - "yii\\elasticsearch\\": "" - } - }, - "notification-url": "https://packagist.org/downloads/", - "license": [ - "BSD-3-Clause" - ], - "authors": [ - { - "name": "Carsten Brandt", - "email": "mail@cebe.cc" - } - ], - "description": "Elasticsearch integration and ActiveRecord for the Yii framework", - "keywords": [ - "active-record", - "elasticsearch", - "fulltext", - "search", - "yii2" - ], - "time": "2016-04-14 08:47:17" - }, { "name": "yiisoft/yii2-jui", "version": "dev-master", @@ -1304,26 +1501,26 @@ }, { "name": "devgroup/dotplant-dev", - "version": "1.0.0", + "version": "1.0.1", "source": { "type": "git", "url": "https://github.com/DevGroup-ru/dotplant-dev.git", - "reference": "fbfec225fcfd75a30b6bcfffc0b66d4c986ac286" + "reference": "78c663dda6f381b483e9e6751ad1cbce0bf865e6" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/DevGroup-ru/dotplant-dev/zipball/fbfec225fcfd75a30b6bcfffc0b66d4c986ac286", - "reference": "fbfec225fcfd75a30b6bcfffc0b66d4c986ac286", + "url": "https://api.github.com/repos/DevGroup-ru/dotplant-dev/zipball/78c663dda6f381b483e9e6751ad1cbce0bf865e6", + "reference": "78c663dda6f381b483e9e6751ad1cbce0bf865e6", "shasum": "" }, "require": { "php": ">=5.5", "phpunit/dbunit": "~2.0.2", "phpunit/phpunit": "~4.8", - "yiisoft/yii2-codeception": "~2.0.4", - "yiisoft/yii2-debug": "~2.0.5", + "yiisoft/yii2-codeception": "~2.0.5", + "yiisoft/yii2-debug": "~2.0.6", "yiisoft/yii2-faker": "~2.0.3", - "yiisoft/yii2-gii": "~2.0.4" + "yiisoft/yii2-gii": "~2.0.5" }, "type": "metapackage", "notification-url": "https://packagist.org/downloads/", @@ -1336,11 +1533,12 @@ "email": "b37hr3z3n@gmail.com" } ], - "description": "DotPlant2 is an open-source E-Commerce CMS for shops build with Yii2", + "description": "DotPlant is an open-source CMS build with Yii2 including E-Commerce features for shops", "homepage": "http://dotplant.ru/", "keywords": [ "dotplant", "dotplant2", + "dotplant3", "yii", "yii cms", "yii e-commerce", @@ -1350,7 +1548,7 @@ "yii2 e-commerce", "yii2 shop" ], - "time": "2016-01-31 10:51:47" + "time": "2016-05-15 11:55:47" }, { "name": "devgroup/yii2-admin-utils", @@ -1358,12 +1556,12 @@ "source": { "type": "git", "url": "https://github.com/DevGroup-ru/yii2-admin-utils.git", - "reference": "146fed7dc6d185ee7dbc6dbe1598c4a8762858fe" + "reference": "e4bba84b06c9aac73c3e48978aa0fc107a6fd9fc" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/DevGroup-ru/yii2-admin-utils/zipball/146fed7dc6d185ee7dbc6dbe1598c4a8762858fe", - "reference": "146fed7dc6d185ee7dbc6dbe1598c4a8762858fe", + "url": "https://api.github.com/repos/DevGroup-ru/yii2-admin-utils/zipball/e4bba84b06c9aac73c3e48978aa0fc107a6fd9fc", + "reference": "e4bba84b06c9aac73c3e48978aa0fc107a6fd9fc", "shasum": "" }, "require": { @@ -1401,7 +1599,7 @@ "extension", "yii2" ], - "time": "2016-04-18 15:20:34" + "time": "2016-04-29 07:39:19" }, { "name": "devgroup/yii2-frontend-utils", @@ -1409,12 +1607,12 @@ "source": { "type": "git", "url": "https://github.com/DevGroup-ru/yii2-frontend-utils.git", - "reference": "aa192628d60f6bda5d36c3dfc6f9d2ce3f7b2034" + "reference": "eb1ba6f2c4ff5051cfb695e72698c5de496d8096" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/DevGroup-ru/yii2-frontend-utils/zipball/aa192628d60f6bda5d36c3dfc6f9d2ce3f7b2034", - "reference": "aa192628d60f6bda5d36c3dfc6f9d2ce3f7b2034", + "url": "https://api.github.com/repos/DevGroup-ru/yii2-frontend-utils/zipball/eb1ba6f2c4ff5051cfb695e72698c5de496d8096", + "reference": "eb1ba6f2c4ff5051cfb695e72698c5de496d8096", "shasum": "" }, "require": { @@ -1423,9 +1621,18 @@ "yiisoft/yii2": "^2.0.6" }, "require-dev": { - "devgroup/dotplant-dev": "*" + "devgroup/dotplant-dev": "~1.0" }, "type": "yii2-extension", + "extra": { + "yii2-extension": { + "name": "Frontend utils", + "name_ru": "Frontend утилиты", + "iconUrl": "https://st-1.dotplant.ru/images/ext-sample.png", + "description": "Various frontend components for yii2", + "description_ru": "Различные компоненты для yii2" + } + }, "autoload": { "psr-4": { "DevGroup\\Frontend\\": "src/" @@ -1448,7 +1655,7 @@ "frontend-monster", "yii2" ], - "time": "2016-01-31 10:25:08" + "time": "2016-05-15 12:10:24" }, { "name": "devgroup/yii2-polyglot", @@ -1561,12 +1768,12 @@ "source": { "type": "git", "url": "https://github.com/fzaninotto/Faker.git", - "reference": "1c33e894fbbad6cf65bd42871719cd33227ed6a7" + "reference": "191fc2b648e1b00ad4725fcae0ab0729537725c8" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/fzaninotto/Faker/zipball/1c33e894fbbad6cf65bd42871719cd33227ed6a7", - "reference": "1c33e894fbbad6cf65bd42871719cd33227ed6a7", + "url": "https://api.github.com/repos/fzaninotto/Faker/zipball/191fc2b648e1b00ad4725fcae0ab0729537725c8", + "reference": "191fc2b648e1b00ad4725fcae0ab0729537725c8", "shasum": "" }, "require": { @@ -1603,7 +1810,7 @@ "faker", "fixtures" ], - "time": "2016-04-13 06:45:05" + "time": "2016-05-10 09:22:13" }, { "name": "phpdocumentor/reflection-docblock", @@ -1698,12 +1905,12 @@ "source": { "type": "git", "url": "https://github.com/phpspec/prophecy.git", - "reference": "b02221e42163be673f9b44a0bc92a8b4907a7c6d" + "reference": "7ca4182b3acbd19ebbf3ced4dfee12487d9c05aa" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/phpspec/prophecy/zipball/b02221e42163be673f9b44a0bc92a8b4907a7c6d", - "reference": "b02221e42163be673f9b44a0bc92a8b4907a7c6d", + "url": "https://api.github.com/repos/phpspec/prophecy/zipball/7ca4182b3acbd19ebbf3ced4dfee12487d9c05aa", + "reference": "7ca4182b3acbd19ebbf3ced4dfee12487d9c05aa", "shasum": "" }, "require": { @@ -1752,7 +1959,7 @@ "spy", "stub" ], - "time": "2016-02-21 17:41:21" + "time": "2016-05-09 09:25:40" }, { "name": "phpunit/dbunit", @@ -1961,21 +2168,24 @@ }, { "name": "phpunit/php-timer", - "version": "1.0.7", + "version": "1.0.8", "source": { "type": "git", "url": "https://github.com/sebastianbergmann/php-timer.git", - "reference": "3e82f4e9fc92665fafd9157568e4dcb01d014e5b" + "reference": "38e9124049cf1a164f1e4537caf19c99bf1eb260" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/sebastianbergmann/php-timer/zipball/3e82f4e9fc92665fafd9157568e4dcb01d014e5b", - "reference": "3e82f4e9fc92665fafd9157568e4dcb01d014e5b", + "url": "https://api.github.com/repos/sebastianbergmann/php-timer/zipball/38e9124049cf1a164f1e4537caf19c99bf1eb260", + "reference": "38e9124049cf1a164f1e4537caf19c99bf1eb260", "shasum": "" }, "require": { "php": ">=5.3.3" }, + "require-dev": { + "phpunit/phpunit": "~4|~5" + }, "type": "library", "autoload": { "classmap": [ @@ -1998,7 +2208,7 @@ "keywords": [ "timer" ], - "time": "2015-06-21 08:01:12" + "time": "2016-05-12 18:03:57" }, { "name": "phpunit/php-token-stream", @@ -2055,12 +2265,12 @@ "source": { "type": "git", "url": "https://github.com/sebastianbergmann/phpunit.git", - "reference": "496745aeba741e63b7149da3e1f712d441751182" + "reference": "9f44734d4814d0a5632745bbd71ded5817a8b002" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/sebastianbergmann/phpunit/zipball/496745aeba741e63b7149da3e1f712d441751182", - "reference": "496745aeba741e63b7149da3e1f712d441751182", + "url": "https://api.github.com/repos/sebastianbergmann/phpunit/zipball/9f44734d4814d0a5632745bbd71ded5817a8b002", + "reference": "9f44734d4814d0a5632745bbd71ded5817a8b002", "shasum": "" }, "require": { @@ -2119,7 +2329,7 @@ "testing", "xunit" ], - "time": "2016-04-12 07:25:01" + "time": "2016-05-12 16:52:51" }, { "name": "phpunit/phpunit-mock-objects", @@ -2299,12 +2509,12 @@ "source": { "type": "git", "url": "https://github.com/sebastianbergmann/environment.git", - "reference": "dc7a29032cf72b54f36dac15a1ca5b3a1b6029bf" + "reference": "2292b116f43c272ff4328083096114f84ea46a56" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/sebastianbergmann/environment/zipball/dc7a29032cf72b54f36dac15a1ca5b3a1b6029bf", - "reference": "dc7a29032cf72b54f36dac15a1ca5b3a1b6029bf", + "url": "https://api.github.com/repos/sebastianbergmann/environment/zipball/2292b116f43c272ff4328083096114f84ea46a56", + "reference": "2292b116f43c272ff4328083096114f84ea46a56", "shasum": "" }, "require": { @@ -2341,7 +2551,7 @@ "environment", "hhvm" ], - "time": "2016-02-26 18:40:46" + "time": "2016-05-04 07:59:13" }, { "name": "sebastian/exporter", @@ -2555,12 +2765,12 @@ "source": { "type": "git", "url": "https://github.com/symfony/yaml.git", - "reference": "407e31ad9742ace5c3d01642f02a3b2e6062bae5" + "reference": "641bbd0fc1b671f896f7815169587dced3588f5c" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/yaml/zipball/407e31ad9742ace5c3d01642f02a3b2e6062bae5", - "reference": "407e31ad9742ace5c3d01642f02a3b2e6062bae5", + "url": "https://api.github.com/repos/symfony/yaml/zipball/641bbd0fc1b671f896f7815169587dced3588f5c", + "reference": "641bbd0fc1b671f896f7815169587dced3588f5c", "shasum": "" }, "require": { @@ -2569,7 +2779,7 @@ "type": "library", "extra": { "branch-alias": { - "dev-master": "3.1-dev" + "dev-master": "3.2-dev" } }, "autoload": { @@ -2596,7 +2806,7 @@ ], "description": "Symfony Yaml Component", "homepage": "https://symfony.com", - "time": "2016-03-30 14:44:34" + "time": "2016-05-14 15:06:20" }, { "name": "yiisoft/yii2-codeception", @@ -2743,12 +2953,12 @@ "source": { "type": "git", "url": "https://github.com/yiisoft/yii2-gii.git", - "reference": "70edab5a7938b5bf4b5dc3ad1e1c3ce673552f48" + "reference": "ad215b7f8c679e11e70ff3dc29c7306f28cef576" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/yiisoft/yii2-gii/zipball/70edab5a7938b5bf4b5dc3ad1e1c3ce673552f48", - "reference": "70edab5a7938b5bf4b5dc3ad1e1c3ce673552f48", + "url": "https://api.github.com/repos/yiisoft/yii2-gii/zipball/ad215b7f8c679e11e70ff3dc29c7306f28cef576", + "reference": "ad215b7f8c679e11e70ff3dc29c7306f28cef576", "shasum": "" }, "require": { @@ -2788,13 +2998,12 @@ "gii", "yii2" ], - "time": "2016-04-14 16:29:07" + "time": "2016-05-12 20:54:42" } ], "aliases": [], "minimum-stability": "dev", "stability-flags": { - "yiisoft/yii2-elasticsearch": 20, "yiisoft/yii2-bootstrap": 20, "devgroup/yii2-admin-utils": 20 }, diff --git a/src/Properties/Module.php b/src/Properties/Module.php index 3cddfa9..e939678 100644 --- a/src/Properties/Module.php +++ b/src/Properties/Module.php @@ -3,13 +3,17 @@ namespace DevGroup\DataStructure\Properties; use arogachev\sortable\controllers\SortController; -use DevGroup\AdminUtils\events\ModelEditAction; use DevGroup\AdminUtils\events\ModelEditForm; +use DevGroup\DataStructure\commands\ElasticIndexController; use DevGroup\DataStructure\Properties\actions\EditProperty; use DevGroup\DataStructure\propertyHandler\StaticValues; +use DevGroup\DataStructure\search\base\AbstractSearch; +use DevGroup\DataStructure\search\common\Search; +use DevGroup\DataStructure\search\elastic\Search as Elastic; use Yii; use yii\base\Application; use yii\base\BootstrapInterface; +use yii\base\InvalidConfigException; use yii\base\Module as BaseModule; use yii\web\View; @@ -26,6 +30,14 @@ class Module extends BaseModule implements BootstrapInterface { + /** @var string */ + public $searchClass; + + public $searchConfig = []; + + /** @var null | AbstractSearch */ + private $search = null; + public function init() { parent::init(); @@ -45,12 +57,31 @@ public function init() public function bootstrap($app) { - if (is_a($app, \yii\web\Application::className())) { + if ($app instanceof \yii\web\Application) { + $started = false; + $search = null; + if (false === empty($this->searchClass)) { + if (true === class_exists($this->searchClass)) { + $config = array_merge(['class' => $this->searchClass], $this->searchConfig); + $search = Yii::createObject($config); + $started = $search->getStarted(); + } + if (false === $started) { + $search = new Search; + } + $this->search = $search; + } $this->controllerMap['sort'] = [ 'class' => SortController::className(), ]; } - + if ($app instanceof \yii\console\Application) { + if ($this->searchClass === Elastic::class) { + $this->controllerMap['elastic'] = [ + 'class' => ElasticIndexController::class, + ]; + } + } $app->on(Application::EVENT_BEFORE_REQUEST, function () { Yii::$app->setAliases([ '@dataStructure' => '@vendor/devgroup/yii2-data-structure-tools/src/', @@ -77,6 +108,18 @@ public function registerTranslations() ]; } + /** + * @return AbstractSearch + * @throws InvalidConfigException + */ + public function getSearch() + { + if (null === $this->search || false === $this->search instanceof AbstractSearch) { + throw new InvalidConfigException("Before using 'Search' component you have to define it in the app config!"); + } + return $this->search; + } + /** * Add custom translations method */ diff --git a/src/Properties/views/manage/edit-property.php b/src/Properties/views/manage/edit-property.php index e622e36..3c7af7b 100644 --- a/src/Properties/views/manage/edit-property.php +++ b/src/Properties/views/manage/edit-property.php @@ -39,6 +39,7 @@ field($model, 'key') ?> field($model, 'is_internal')->checkbox() ?> + field($model, 'in_search')->checkbox() ?> field($model, 'allow_multiple_values')->checkbox() ?> field($model, 'data_type')->dropDownList(FrontendPropertiesHelper::dataTypeSelectOptions()) ?> field($model, 'property_handler_id')->dropDownList(FrontendPropertiesHelper::handlersSelectOptions()) ?> diff --git a/src/behaviors/HasProperties.php b/src/behaviors/HasProperties.php index 64eb7db..7058a18 100644 --- a/src/behaviors/HasProperties.php +++ b/src/behaviors/HasProperties.php @@ -2,6 +2,7 @@ namespace DevGroup\DataStructure\behaviors; +use DevGroup\DataStructure\events\HasPropertiesEvent; use DevGroup\DataStructure\helpers\PropertiesHelper; use DevGroup\DataStructure\helpers\PropertyStorageHelper; use DevGroup\DataStructure\models\Property; @@ -15,6 +16,11 @@ class HasProperties extends Behavior { + /** Events list to proceed search indexes actualization */ + const EVENT_AFTER_SAVE = 'afterSave'; + const EVENT_AFTER_UPDATE = 'afterUpdate'; + const EVENT_BEFORE_DELETE = 'beforeDelete'; + /** @var bool Should properties be automatically fetched after find */ public $autoFetchProperties = false; @@ -70,10 +76,12 @@ public function afterFind() */ public function beforeDelete() { - // properties assigned to this record /** @var \yii\db\ActiveRecord|\DevGroup\DataStructure\traits\PropertiesTrait $owner */ $owner = $this->owner; + $event = new HasPropertiesEvent(); + $event->model = $owner; + HasPropertiesEvent::trigger(self::class, self::EVENT_BEFORE_DELETE, $event); //! @todo add check if this object doesn't has related properties that we wish to delete(lower db queries) $array = [&$owner]; PropertiesHelper::deleteAllProperties($array); @@ -183,7 +191,9 @@ public function afterInsert() $owner = $this->owner; $groups = $owner->propertyGroupIds; $owner->propertyGroupIds = null; - + $event = new HasPropertiesEvent(); + $event->model = $owner; + HasPropertiesEvent::trigger(self::class, self::EVENT_AFTER_SAVE, $event); if (count($groups) > 0) { foreach ($groups as $group_id) { /** @var PropertyGroup $group */ @@ -212,7 +222,9 @@ public function afterSave() { /** @var \yii\db\ActiveRecord|\DevGroup\DataStructure\traits\PropertiesTrait $owner */ $owner = $this->owner; - + $event = new HasPropertiesEvent(); + $event->model = $owner; + HasPropertiesEvent::trigger(self::class, self::EVENT_AFTER_UPDATE, $event); if ($this->autoSaveProperties === true) { $models = [&$owner]; PropertiesHelper::storeValues($models); diff --git a/src/commands/ElasticIndexController.php b/src/commands/ElasticIndexController.php new file mode 100644 index 0000000..abe5754 --- /dev/null +++ b/src/commands/ElasticIndexController.php @@ -0,0 +1,308 @@ + '', + 'body' => [ + 'settings' => [ + 'number_of_shards' => 1, + 'number_of_replicas' => 0 + ], + 'mappings' => [ + 'static_values' => [ + 'properties' => [ + 'model_id' => [ + 'type' => 'long', + ], + 'propertyValues' => [ + 'type' => 'nested', + 'include_in_parent' => true, + 'properties' => [ + 'static_value_id' => [ + 'type' => 'long' + ], + 'prop_id' => [ + 'type' => 'long' + ], + 'prop_key' => [ + 'type' => 'string', + 'index' => "not_analyzed" + ], + //slug + //... + //values + //... + ] + ], + ], + ], + ] + ] + ]; + + /** + * @see https://www.elastic.co/guide/en/elasticsearch/reference/current/analysis-lang-analyzer.html + * @see https://en.wikipedia.org/wiki/List_of_ISO_639-2_codes + * + * @var array + */ + private static $langToAnalyzer = [ + 'ara' => 'arabic', + 'hye' => 'armenian', + 'eus' => 'basque', + //'' => 'brazilian', ? + 'bul' => 'bulgarian', + 'cat' => 'catalan', + //'' => 'cjk', + 'ces' => 'czech', + 'dan' => 'danish', + 'nld' => 'dutch', + 'eng' => 'english', + 'fin' => 'finnish', + 'fra' => 'french', + 'glg' => 'galician', + 'deu' => 'german', + 'grk' => 'greek', + 'hin' => 'hindi', + 'hun' => 'hungarian', + 'ind' => 'indonesian', + 'gle' => 'irish', + 'ita' => 'italian', + 'lav' => 'latvian', + 'lit' => 'lithuanian', + 'nor' => 'norwegian', + 'fas' => 'persian', + 'por' => 'portuguese', + 'ron' => 'romanian', + 'rus' => 'russian', + 'ckb' => 'sorani', + 'spa' => 'spanish', + 'swe' => 'swedish', + 'tur' => 'turkish', + 'tha' => 'thai', + ]; + + /** + * Part of language dependent index mapping for property value slug + * + * @var array + */ + private static $slugMap = [ + 'type' => 'string', + 'index' => 'not_analyzed' + ]; + + /** + * Part of language dependent index mapping for property value + * + * @var array + */ + private static $valueMap = [ + 'type' => 'string', + 'analyzer' => '', + 'fields' => [ + 'raw' => [ + 'type' => 'string', + 'index' => 'not_analyzed' + ], + ] + ]; + + /** + * @inheritdoc + */ + public function init() + { + self::$languages = LanguageHelper::getAll(); + self::prepareMapping(); + $apl = ApplicablePropertyModels::find()->asArray(true)->all(); + $client = ClientBuilder::create(); + /** @var Module $searchModule */ + $searchModule = Yii::$app->getModule('properties', false); + $hosts = empty($searchModule->searchConfig['hosts']) ? [] : $searchModule->searchConfig['hosts']; + if (true === is_array($hosts) && count($hosts) > 0) { + $client->setHosts($hosts); + } + $this->client = $client->build(); + foreach ($apl as $row) { + if (true === class_exists($row['class_name'])) { + $indexName = IndexHelper::classToIndex($row['class_name']); + $this->applicables[$indexName] = new $row['class_name']; + } + } + parent::init(); + } + + /** + * Creates and fills in indices + * + * @throws \yii\base\InvalidConfigException + */ + public function actionFillIndex() + { + try { + $this->client->ping(); + } catch (NoNodesAvailableException $e) { + $this->stderr($e->getMessage() . ', maybe you first need to configure and run elasticsearch' . PHP_EOL); + return; + } + $staticType = IndexHelper::storageClassToType(StaticValues::class); + /** @var HasProperties | PropertiesTrait $model */ + foreach ($this->applicables as $indexName => $model) { + if (true === $this->client->indices()->exists(['index' => $indexName])) { + $this->client->indices()->delete(['index' => $indexName]); + } + $config = self::$staticIndexConfig; + $config['index'] = $indexName; + $response = $this->client->indices()->create($config); + if (true === isset($response['acknowledged']) && $response['acknowledged'] == 1) { + $staticTable = $model->staticValuesBindingsTable(); + $staticIndexData = self::buildPsvData($staticTable, $indexName, $staticType); + if (false === empty($staticIndexData['body'])) { + $this->client->bulk($staticIndexData); + } + } + } + } + + /** + * Prepares language based index mappings according to languages defined in app config multilingual + */ + private static function prepareMapping() + { + foreach (self::$languages as $iso_639_2t) { + if (true === isset(self::$langToAnalyzer[$iso_639_2t])) { + self::$staticIndexConfig['body']['mappings']['static_values']['properties']['propertyValues']['properties']['slug_' . $iso_639_2t] = self::$slugMap; + self::$valueMap['analyzer'] = self::$langToAnalyzer[$iso_639_2t]; + self::$staticIndexConfig['body']['mappings']['static_values']['properties']['propertyValues']['properties']['value_' . $iso_639_2t] = self::$valueMap; + } + } + } + + /** + * Collects data and builds multidimensional array for store in elasticsearch index + * + * @param string $bindingsTable + * @param string $index + * @param string $type + * @return array + */ + private static function buildPsvData($bindingsTable, $index, $type) + { + $res = ['body' => []]; + $props = (new Query())->from(Property::tableName())->select(['id', 'key'])->where(['in_search' => 1])->all(); + $propIds = array_column($props, 'id'); + $propValues = (new Query())->from(StaticValue::tableName()) + ->select(['id', 'property_id']) + ->where(['property_id' => $propIds]) + ->distinct(true) + ->all(); + $valueIds = array_column($propValues, 'id'); + $assignedValues = (new Query())->from($bindingsTable) + ->select(['model_id', 'static_value_id']) + ->where(['static_value_id' => $valueIds]) + ->all(); + $assignedValueIds = array_column($assignedValues, 'static_value_id'); + $assignedValueIds = array_unique($assignedValueIds); + $propValuesMap = ArrayHelper::map($propValues, 'id', 'property_id'); + $props = ArrayHelper::map($props, 'id', 'key'); + $translations = StaticValueTranslation::find() + ->select(['model_id', 'language_id', 'name', 'slug']) + ->where(['model_id' => $assignedValueIds]) + ->asArray(true) + ->all(); + $mapped = []; + foreach ($translations as $one) { + if (false === isset($mapped[$one['model_id']])) { + $mapped[$one['model_id']] = [ + 'slug_' . self::$languages[$one['language_id']] => $one['slug'], + 'value_' . self::$languages[$one['language_id']] => $one['name'], + ]; + } else { + $mapped[$one['model_id']]['value_' . self::$languages[$one['language_id']]] = $one['name']; + $mapped[$one['model_id']]['slug_' . self::$languages[$one['language_id']]] = $one['slug']; + } + } + $data = []; + foreach ($assignedValues as $i => $value) { + $propId = isset($propValuesMap[$value['static_value_id']]) ? $propValuesMap[$value['static_value_id']] : null; + $propKey = isset($props[$propId]) ? $props[$propId] : null; + $staticValueId = $value['static_value_id']; + $propVals = [ + 'static_value_id' => $staticValueId, + 'prop_id' => $propId, + 'prop_key' => $propKey, + ]; + if (true === isset($mapped[$staticValueId])) { + $propVals = array_merge($propVals, $mapped[$staticValueId]); + } + if (false === isset($data[$value['model_id']])) { + $data[$value['model_id']] = [ + 'model_id' => $value['model_id'], + 'propertyValues' => [$propVals] + ]; + } else { + $data[$value['model_id']]['propertyValues'][] = $propVals; + } + } + foreach ($data as $id => $row) { + $res['body'][] = ['index' => [ + '_id' => $id, + '_index' => $index, + '_type' => $type, + ]]; + $res['body'][] = $row; + } + return $res; + } + + //TODO implement EAV collecting + /* + private static function buildEavData() + { + + } + */ +} \ No newline at end of file diff --git a/src/events/HasPropertiesEvent.php b/src/events/HasPropertiesEvent.php new file mode 100644 index 0000000..b46c945 --- /dev/null +++ b/src/events/HasPropertiesEvent.php @@ -0,0 +1,16 @@ +addColumn( + Property::tableName(), + 'in_search', + $this->boolean()->defaultValue(0) + ); + } + + public function down() + { + $this->dropColumn(Property::tableName(), 'in_search'); + } +} diff --git a/src/models/Property.php b/src/models/Property.php index 7876496..0467e6a 100644 --- a/src/models/Property.php +++ b/src/models/Property.php @@ -29,6 +29,7 @@ * @property boolean $allow_multiple_values * @property integer $storage_id * @property integer $property_handler_id + * @property integer $in_search * @property array $default_value * @property array $handler_config * @property string $name @@ -92,7 +93,7 @@ public function rules() ['key', 'required'], ['key', 'string', 'max' => 80], [['is_internal', 'allow_multiple_values'], 'filter', 'filter' => 'boolval'], - [['data_type'], 'integer',], + [['data_type', 'in_search'], 'integer',], [['data_type'], 'required',], [ 'storage_id', @@ -131,6 +132,7 @@ public function scenarios() 'storage_id', 'property_handler_id', 'name', + 'in_search', ]; return $scenarios; } @@ -150,6 +152,7 @@ public function attributeLabels() 'packed_json_default_value' => Module::t('app', 'Packed Json Default Value'), 'property_handler_id' => Module::t('app', 'Property Handler ID'), 'name' => Module::t('app', 'Name'), + 'in_search' => Module::t('app', 'Use in search'), ]; } diff --git a/src/propertyStorage/AbstractPropertyStorage.php b/src/propertyStorage/AbstractPropertyStorage.php index 73ceb91..d6f8864 100644 --- a/src/propertyStorage/AbstractPropertyStorage.php +++ b/src/propertyStorage/AbstractPropertyStorage.php @@ -8,6 +8,7 @@ use DevGroup\DataStructure\models\PropertyGroup; use DevGroup\DataStructure\models\PropertyPropertyGroup; use DevGroup\DataStructure\Properties\Module; +use DevGroup\DataStructure\search\interfaces\Filter; use DevGroup\DataStructure\traits\PropertiesTrait; use Yii; use yii\base\Exception; @@ -28,7 +29,7 @@ * * @package DevGroup\DataStructure\propertyStorage */ -abstract class AbstractPropertyStorage implements FiltrableStorageInterface +abstract class AbstractPropertyStorage implements Filter { /** * @var ActiveRecord[] | HasProperties[] | PropertiesTrait[] Applicable property model class names identity map by property id @@ -56,7 +57,7 @@ protected static function getApplicablePropertyModelClassNames($id) } return static::$applicablePropertyModelClassNames[$id]; } - + /** * @var int ID of storage in property_storage table */ @@ -89,9 +90,10 @@ protected static function valueByReturnType( $className, $dependency, $cacheLifetime = 86400 - ) { + ) + { switch ($returnType) { - case FiltrableStorageInterface::RETURN_COUNT: + case Filter::RETURN_COUNT: $result += $className::getDb()->cache( function ($db) use ($tmpQuery) { return $tmpQuery->count('*', $db); @@ -101,7 +103,7 @@ function ($db) use ($tmpQuery) { ); break; - case FiltrableStorageInterface::RETURN_QUERY: + case Filter::RETURN_QUERY: $result[$className] = $tmpQuery; break; default: @@ -356,7 +358,8 @@ public static function getPropertyValuesByParams( $customDependency = null, $customKey = '', $cacheLifetime = 86400 - ) { + ) + { return []; } @@ -369,7 +372,8 @@ public static function getModelsByPropertyValues( $returnType = self::RETURN_ALL, $customDependency = null, $cacheLifetime = 86400 - ) { + ) + { switch ($returnType) { case self::RETURN_COUNT: return 0; @@ -378,4 +382,29 @@ public static function getModelsByPropertyValues( return []; } } + + /** + * @inheritdoc + */ + public static function filterFormSet( + $modelClass, + $props, + $customDependency = null, + $cacheLifetime = 86400) + { + return []; + } + + /** + * @inheritdoc + */ + public static function getModelsByValueIds( + $modelClass, + $selections, + $customDependency = null, + $cacheLifetime = 86400 + ) + { + return []; + } } diff --git a/src/propertyStorage/FiltrableStorageInterface.php b/src/propertyStorage/FiltrableStorageInterface.php deleted file mode 100644 index 7ae71e3..0000000 --- a/src/propertyStorage/FiltrableStorageInterface.php +++ /dev/null @@ -1,53 +0,0 @@ -language]; $tags = [NamingHelper::getObjectTag(Property::className(), $propertyId)]; if (is_null($customDependency)) { $dependency = new TagDependency(['tags' => $tags]); @@ -248,11 +252,12 @@ public static function getModelsByPropertyValues( $returnType = self::RETURN_ALL, $customDependency = null, $cacheLifetime = 86400 - ) { + ) + { $result = $returnType === self::RETURN_COUNT ? 0 : []; $classNames = static::getApplicablePropertyModelClassNames($propertyId); $tags = [NamingHelper::getObjectTag(Property::className(), $propertyId)]; - $column = 'description'; + $column = 'name'; foreach ($classNames as $className) { $tmpQuery = $className::find()->innerJoin( $className::staticValuesBindingsTable() . ' MSV', @@ -331,4 +336,115 @@ public function afterPropertyDelete(Property &$property) $staticValue->delete(); } } + + /** + * @inheritdoc + */ + public static function getModelsByValueIds( + $modelClass, + $selections, + $customDependency = null, + $cacheLifetime = 86400 + ) + { + if(count($selections) == 0) { + return $modelClass::find()->select('id')->column(); + } + $keys = [$modelClass, 'Property', Json::encode($selections), Yii::$app->language]; + $tags = [NamingHelper::getCommonTag($modelClass)]; + foreach ($selections as $propertyId) { + $tags[] = NamingHelper::getObjectTag(Property::class, $propertyId); + } + if (is_null($customDependency)) { + $dependency = new TagDependency(['tags' => $tags]); + } elseif (is_string($customDependency)) { + $tags[] = $customDependency; + $dependency = new TagDependency(['tags' => $tags]); + } else { + $dependency = new ChainedDependency( + ['dependencies' => [$customDependency, new TagDependency(['tags' => $tags])]] + ); + } + /** @var ActiveRecord | HasProperties | PropertiesTrait $model */ + $model = new $modelClass; + $table = $model->staticValuesBindingsTable(); + $all = Yii::$app->cache->lazy( + function () use ($table, $selections) { + $all = []; + $q = (new Query())->from($table)->select('model_id')->distinct(true); + $start = true; + foreach ($selections as $propertyId => $values) { + $res = $q->where(['static_value_id' => $values])->column(); + if (true === $start) { + $all = $res; + } else { + $all = array_intersect($all, $res); + } + $start = false; + } + return $all; + }, + __METHOD__ . md5(implode(':', $keys)), + $cacheLifetime, + $dependency + ); + return $all; + } + + /** + * @inheritdoc + */ + public static function filterFormSet($modelClass, $props, $customDependency = null, $cacheLifetime = 86400) + { + $keys = [$modelClass, 'Property', Json::encode($props), Yii::$app->language]; + $tags = [NamingHelper::getCommonTag($modelClass)]; + foreach ($props as $propertyId) { + $tags[] = NamingHelper::getObjectTag(Property::className(), $propertyId); + } + if (is_null($customDependency)) { + $dependency = new TagDependency(['tags' => $tags]); + } elseif (is_string($customDependency)) { + $tags[] = $customDependency; + $dependency = new TagDependency(['tags' => $tags]); + } else { + $dependency = new ChainedDependency( + ['dependencies' => [$customDependency, new TagDependency(['tags' => $tags])]] + ); + } + /** @var ActiveRecord | HasProperties | PropertiesTrait $model */ + $model = new $modelClass; + $table = $model->staticValuesBindingsTable(); + $data = Yii::$app->cache->lazy( + function () use ($props, $table) { + $values = StaticValue::find() + ->select(['property_id', 'id']) + ->where(['property_id' => $props]) + ->asArray(true) + ->all(); + $availIds = array_column($values, 'id'); + $set = (new Query()) + ->from($table) + ->select('static_value_id') + ->where(['static_value_id' => $availIds]) + ->distinct(true) + ->column(); + $data = []; + foreach ($values as $row) { + if (false === in_array($row['id'], $set)) { + continue; + } + if (false === isset($data[$row['property_id']])) { + $data[$row['property_id']] = [$row['id'] => $row['defaultTranslation']['name']]; + } else { + $data[$row['property_id']][$row['id']] = $row['defaultTranslation']['name']; + } + } + return $data; + }, + __METHOD__ . md5(implode(':', $keys)), + $cacheLifetime, + $dependency + ); + return $data; + } } diff --git a/src/search/base/AbstractSearch.php b/src/search/base/AbstractSearch.php new file mode 100644 index 0000000..11a07b7 --- /dev/null +++ b/src/search/base/AbstractSearch.php @@ -0,0 +1,69 @@ +beforeInit()) { + if (true === class_exists($this->watcherClass)) { + $class = $this->watcherClass; + $this->watcher = new $class; + $this->watcher->init(); + $this->started = true; + } + parent::init(); + } + } + + /** + * Indicates module started successfully or not + * + * @return bool + */ + public function getStarted() + { + return $this->started; + } + + /** + * Performs some additional checks if necessary + * + * @return bool + */ + public function beforeInit() + { + return true; + } + + /** + * Returns according watcher + * + * @return AbstractWatch|null + */ + public function getWatcher() + { + return $this->watcher; + } +} \ No newline at end of file diff --git a/src/search/base/AbstractWatch.php b/src/search/base/AbstractWatch.php new file mode 100644 index 0000000..dd5c940 --- /dev/null +++ b/src/search/base/AbstractWatch.php @@ -0,0 +1,38 @@ +beforeInit()) { + HasPropertiesEvent::on(HasProperties::class, HasProperties::EVENT_AFTER_SAVE, [$this, 'onSave']); + HasPropertiesEvent::on(HasProperties::class, HasProperties::EVENT_AFTER_UPDATE, [$this, 'onUpdate']); + HasPropertiesEvent::on(HasProperties::class, HasProperties::EVENT_BEFORE_DELETE, [$this, 'onDelete']); + } + } + + /** + * Proceeds different pre initialization stuff if necessary. + * + * @return bool + */ + public function beforeInit() + { + return true; + } +} \ No newline at end of file diff --git a/src/search/common/Search.php b/src/search/common/Search.php new file mode 100644 index 0000000..dacc38d --- /dev/null +++ b/src/search/common/Search.php @@ -0,0 +1,121 @@ +request->get('filter', []); + $data = []; + /** @var AbstractPropertyStorage $one */ + foreach ($storage as $one) { + $data = array_merge($data, $one::getModelsByValueIds($modelClass, $params)); + } + return $data; + } + + /** + * Prepares list of applicable storage + * + * @param array $config + * @return array + */ + private static function prepareStorage($config) + { + $list = isset($config['storage']) ? $config['storage'] : []; + $list = is_array($list) ? $list : [$list]; + $query = PropertyStorage::find()->select('id')->where('class_name=:className'); + if (count($list) == 0) { + return [$query->params([':className' => StaticValues::class])->scalar() => StaticValues::class]; + } + $storage = []; + foreach ($list as $storageClass) { + if (false === $storageId = $query->params([':className' => $storageClass])->scalar()) { + continue; + } + $storage[$storageId] = $storageClass; + } + return $storage; + } + + /** + * @param array $config you can define it like this: + * [ + * 'modelClass' => Page::class, // models to search for + * 'storage' => StaticValues::class, //property storage or array of storages handler to search with + * ] + * For now and by default this works only with StaticValues property handler + * + * @return array + */ + public function filterFormData($config = []) + { + if (false === isset($config['modelClass']) || false === class_exists($config['modelClass'])) { + return []; + } + $class = $config['modelClass']; + /** @var ActiveRecord | HasProperties | PropertiesTrait $model */ + $model = new $class; + if (false === method_exists($model, 'ensurePropertyGroupIds')) { + return []; + } + $storage = self::prepareStorage($config); + $props = Property::find() + ->select(['id', 'name']) + ->where([ + 'in_search' => 1, + 'storage_id' => array_keys($storage) + ]) + ->asArray(true) + ->all(); + $props = ArrayHelper::map($props, 'id', 'name'); + $data = []; + /** @var AbstractPropertyStorage $one */ + foreach ($storage as $one) { + $data = ArrayHelper::merge($data, $one::filterFormSet($class, array_keys($props))); + } + return [ + 'data' => $data, + 'props' => $props, + 'selected' => Yii::$app->request->get('filter', []), + ]; + } +} diff --git a/src/search/elastic/Search.php b/src/search/elastic/Search.php new file mode 100644 index 0000000..671c6ef --- /dev/null +++ b/src/search/elastic/Search.php @@ -0,0 +1,219 @@ +hosts) && count($this->hosts) > 0) { + $client->setHosts($this->hosts); + } + $this->client = $client->build(); + try { + $e = $this->client->search(['index' => '_all', '_source' => false, 'body' => []]); + /** @var null | Watch $watcher */ + $watcher = null; + if (null !== $this->watcherClass) { + if (true === class_exists($this->watcherClass)) { + $class = $this->watcherClass; + $watcher = new $class; + } + } else { + $watcher = new $this->defaultWatcherClass; + } + if (null !== $watcher) { + $watcher->setClient($this->client); + $watcher->init(); + $this->watcher = $watcher; + $this->started = true; + } else { + Yii::warning('There is no correct class defined for elasticsearch Watch. Search rolls back to common.'); + } + } catch (NoNodesAvailableException $e) { + Yii::warning('Elasticsearch said:' . $e->getMessage()); + } + } + + /** + * @return Client|null + */ + public function getClient() + { + return $this->client; + } + + /** + * @inheritdoc + * @codeCoverageIgnore + */ + public function findInContent($modelClass = '') + { + // TODO: Implement findInContent() method. + } + + /** + * @inheritdoc + */ + public function findInProperties($modelClass = '', $config = []) + { + $params = Yii::$app->request->get('filter', []); + $index = IndexHelper::classToIndex($modelClass); + if (true === empty($index)) { + return []; + } + $query = self::buildQuery($params, $index); + $res = IndexHelper::primaryKeysByCondition($this->client, $query); + return array_keys($res); + } + + /** + * @inheritdoc + */ + public function filterFormData($config = []) + { + if (false === isset($config['modelClass']) || false === class_exists($config['modelClass'])) { + return []; + } + $class = $config['modelClass']; + /** @var ActiveRecord | HasProperties | PropertiesTrait $model */ + $model = new $class; + if (false === method_exists($model, 'ensurePropertyGroupIds')) { + return []; + } + $index = IndexHelper::classToIndex($config['modelClass']); + if (false === $this->client->indices()->exists(['index' => $index])) { + return []; + } + $condition = [ + 'index' => $index, + "size" => 0, + '_source' => false, + 'body' => [ + 'aggs' => [ + 'props' => [ + 'nested' => [ + 'path' => 'propertyValues', + ], + 'aggs' => [ + 'prop_id' => [ + 'terms' => [ + 'field' => 'propertyValues.prop_id', + ], + 'aggs' => [ + 'values' => [ + 'terms' => [ + 'field' => 'propertyValues.value_' . LanguageHelper::getCurrent() . '.raw' + ] + ] + ] + ] + ] + ] + ] + ] + ]; + $res = $this->client->search($condition); + $data = []; + if (false === empty($res['aggregations']['props']['prop_id']['buckets'])) { + foreach ($res['aggregations']['props']['prop_id']['buckets'] as $bucket) { + if (true === empty($bucket['key'])) { + continue; + } + if (true === empty($bucket['values']['buckets'])) { + continue; + } + foreach ($bucket['values']['buckets'] as $value) { + if (true === empty($value['key'])) { + continue; + } + if (false === isset($data[$bucket['key']])) { + $data[$bucket['key']] = [$value['key'] => $value['key']]; + } else { + $data[$bucket['key']][$value['key']] = $value['key']; + } + } + } + } + $props = PropertyTranslation::find()->select(['model_id', 'name']) + ->where(['model_id' => array_keys($data), 'language_id' => Yii::$app->multilingual->language_id]) + ->asArray(true) + ->all(); + $props = ArrayHelper::map($props, 'model_id', 'name'); + return [ + 'data' => $data, + 'props' => $props, + 'selected' => Yii::$app->request->get('filter', []), + ]; + } + + /** + * Prepares filter query + * + * @param $params + * @param $index + * @return array + */ + public static function buildQuery($params, $index) + { + $query = ['bool' => ['must' => []]]; + foreach ($params as $propId => $values) { + $q = ['bool' => ['should' => []]]; + foreach ($values as $val) { + $q['bool']['should'][] = [ + 'bool' => ['must' => [['term' => ['propertyValues.prop_id' => $propId]], + ['term' => ['propertyValues.value_' . LanguageHelper::getCurrent() . '.raw' => $val]]]] + ]; + } + $query['bool']['must'][] = $q; + } + //will search against all types in given index by default + return $a = [ + 'index' => $index, + 'body' => [ + 'query' => [ + 'constant_score' => [ + 'filter' => $query + ] + ] + ] + ]; + } +} \ No newline at end of file diff --git a/src/search/elastic/Watch.php b/src/search/elastic/Watch.php new file mode 100644 index 0000000..53002c4 --- /dev/null +++ b/src/search/elastic/Watch.php @@ -0,0 +1,256 @@ +client instanceof Client) { + return false; + } + return true; + } + return false; + } + + /** + * @param $client + */ + public function setClient(Client $client) + { + $this->client = $client; + } + + /** + * @inheritdoc + */ + public function onDelete($event) + { + $index = IndexHelper::classToIndex(get_class($event->model)); + $this->flushForModel($index, $event->model->id); + } + + /** + * @inheritdoc + */ + public function onSave($event) + { + $index = IndexHelper::classToIndex(get_class($event->model)); + $this->fillForModel($index, $event->model->id); + } + + /** + * @inheritdoc + */ + public function onUpdate($event) + { + $index = IndexHelper::classToIndex(get_class($event->model)); + $this->flushForModel($index, $event->model->id); + $this->fillForModel($index, $event->model); + } + + /** + * Flushes all elasticsearch indices for given model id + * + * @param string $index + * @param integer $modelId + */ + protected function flushForModel($index, $modelId) + { + $query = [ + 'index' => $index, + 'body' => [ + 'query' => [ + 'bool' => [ + 'filter' => [ + 'bool' => [ + 'must' => [ + 'term' => ['model_id' => $modelId] + ] + ] + ], + ] + ] + ] + ]; + $pks = IndexHelper::primaryKeysByCondition($this->client, $query); + if (count($pks) > 0) { + $params = ['body' => []]; + foreach ($pks as $id => $type) { + $params['body'][] = [ + 'delete' => [ + '_index' => $index, + '_type' => $type, + '_id' => $id + ] + ]; + } + $this->client->bulk($params); + } + } + + /** + * Performs model index filling + * + * @param string $index + * @param ActiveRecord | HasProperties | PropertiesTrait $model + */ + protected function fillForModel($index, $model) + { + if (false === empty($model->propertiesIds) && false === empty($model->propertiesValues)) { + //leave only not empty properties + $workingProps = array_filter($model->propertiesValues, function ($e) { + return false === empty($e); + }); + //TODO refactor. Work with all storages & possibly not yet created + //collecting applicable storages + $storage = (new Query())->from(PropertyStorage::tableName())->select(['class_name', 'id'])->where([ + 'class_name' => [ + StaticValues::class, + EAV::class + ] + ])->all(); + $storageClassToId = ArrayHelper::map($storage, 'class_name', 'id'); + $storageIdToIndexType = ArrayHelper::map($storage, 'id', function ($e) { + return IndexHelper::storageClassToType($e['class_name']); + }); + //selecting all applicable properties to work with + $props = (new Query())->from(Property::tableName())->select(['id', 'key', 'storage_id'])->where( + [ + 'id' => array_keys($workingProps), + 'storage_id' => array_keys($storageIdToIndexType), + 'in_search' => 1 + ] + )->all(); + //grouping them by storage id + $props = ArrayHelper::map($props, 'id', 'key', 'storage_id'); + if (false === empty($props[$storageClassToId[StaticValues::class]])) { + $staticBulk = self::prepareStatic( + $props[$storageClassToId[StaticValues::class]], + $model->id, + $workingProps, + $index, + $storageIdToIndexType[$storageClassToId[StaticValues::class]] + ); + $this->client->bulk($staticBulk); + } + //TODO implement EAV values filling + /* + if (false === empty($props[$storageClassToId[EAV::class]])) { + $eavBulk = self::prepareEav( + $props[$storageClassToId[EAV::class]], + $model->id, + $workingProps, + $index, + $storageIdToIndexType[$storageClassToId[EAV::class]] + ); + } + */ + } + } + + /** + * Prepares bulk data to store in elasticsearch index for model properties static values + * + * @param array $props + * @param integer $modelId + * @param array $workingProps + * @param string $index index name i.e.: page + * @param string $type index type i.e.: static_values + * @return array + */ + private static function prepareStatic($props, $modelId, $workingProps, $index, $type) + { + $languages = LanguageHelper::getAll(); + $res = $valIds = []; + //leave only applicable properties with according values + $workingProps = array_intersect_key($workingProps, $props); + $workingProps = array_flip($workingProps); + $values = (new Query())->from(StaticValueTranslation::tableName()) + ->select(['model_id', 'language_id', 'name', 'slug']) + ->where(['model_id' => array_keys($workingProps)]) + ->all(); + $propertyValues = []; + foreach ($values as $value) { + if (false === isset($propertyValues[$value['model_id']])) { + $propId = isset($workingProps[$value['model_id']]) ? $workingProps[$value['model_id']] : null; + $propKey = isset($props[$propId]) ? $props[$propId] : null; + $propertyValues[$value['model_id']] = [ + 'static_value_id' => $value['model_id'], + 'prop_id' => $propId, + 'prop_key' => $propKey, + 'value_' . $languages[$value['language_id']] => $value['name'], + 'slug_' . $languages[$value['language_id']] => $value['slug'], + ]; + } else { + $propertyValues[$value['model_id']]['value_' . $languages[$value['language_id']]] = $value['name']; + $propertyValues[$value['model_id']]['slug_' . $languages[$value['language_id']]] = $value['slug']; + } + } + $res['body'][] = ['index' => [ + '_id' => $modelId, + '_index' => $index, + '_type' => $type, + ]]; + $res['body'][] = [ + 'model_id' => $modelId, + 'propertyValues' => array_values($propertyValues), + ]; + return $res; + } + + /** + * Prepares bulk data to store in elasticsearch index for model properties eav values + * + * @param array $props + * @param integer $modelId + * @param array $workingProps + * @param string $index index name i.e.: page + * @param string $type index type i.e.: static_values + * @return array + */ + /* + private static function prepareEav($props, $modelId, $workingProps, $index, $type) + { + $bulk = []; + return $bulk; + } + */ +} \ No newline at end of file diff --git a/src/search/elastic/helpers/IndexHelper.php b/src/search/elastic/helpers/IndexHelper.php new file mode 100644 index 0000000..5b6a337 --- /dev/null +++ b/src/search/elastic/helpers/IndexHelper.php @@ -0,0 +1,69 @@ +count($condition); + $count = empty($count['count']) ? 10 : $count['count']; + $condition['size'] = $count; + $condition['_source'] = true; + $res = $client->search($condition); + if (false === empty($res['hits']['hits'])) { + foreach ($res['hits']['hits'] as $doc) { + $primaryKeys[$doc['_id']] = $doc['_type']; + } + } + return $primaryKeys; + } +} \ No newline at end of file diff --git a/src/search/helpers/LanguageHelper.php b/src/search/helpers/LanguageHelper.php new file mode 100644 index 0000000..953dca8 --- /dev/null +++ b/src/search/helpers/LanguageHelper.php @@ -0,0 +1,36 @@ +multilingual->getAllLanguages(); + $langs = ArrayHelper::map($langs, 'id', 'iso_639_2t'); + return $langs; + + } + + /** + * @return string + */ + public static function getCurrent() + { + $langs = self::getAll(); + $currentId = Yii::$app->multilingual->language_id; + return isset($langs[$currentId]) ? $langs[$currentId] : 'eng'; + } +} \ No newline at end of file diff --git a/src/helpers/PropertiesFilterHelper.php b/src/search/helpers/PropertiesFilterHelper.php similarity index 74% rename from src/helpers/PropertiesFilterHelper.php rename to src/search/helpers/PropertiesFilterHelper.php index 0d41ad0..ab5fbfa 100644 --- a/src/helpers/PropertiesFilterHelper.php +++ b/src/search/helpers/PropertiesFilterHelper.php @@ -1,21 +1,36 @@ $propertySelection) { $property = Property::findById($propertyId); @@ -23,7 +38,7 @@ public static function filterObjects($propertySelections = [], $returnType = Abs $selections[] = PropertiesHelper::getModelsByPropertyValues( $property, $propertySelection, - AbstractPropertyStorage::RETURN_QUERY + Filter::RETURN_QUERY ); } @@ -44,30 +59,30 @@ function ($result, $item) { /** @var ActiveQuery[] $prepareState */ foreach ($prepareState as $className => $query) { $prepareState[$className] = $className::find()->from(['t' => $query])->addGroupBy('t.id')->having( - "count(t.id)=" . (int) $selectionsCount + "count(t.id)=" . (int)$selectionsCount ); } switch ($returnType) { - case AbstractPropertyStorage::RETURN_COUNT: + case Filter::RETURN_COUNT: foreach ($prepareState as $className => $item) { $prepareState[$className] = $className::getDb()->cache( function ($db) use ($item) { return $item->count('*', $db); }, 86400, - new TagDependency(['tags' => ArrayHelper::merge($tags, (array) $className::commonTag())]) + new TagDependency(['tags' => ArrayHelper::merge($tags, (array)$className::commonTag())]) ); } break; - case AbstractPropertyStorage::RETURN_ALL: + case Filter::RETURN_ALL: foreach ($prepareState as $className => $item) { $prepareState[$className] = $className::getDb()->cache( function ($db) use ($item) { return $item->all($db); }, 86400, - new TagDependency(['tags' => ArrayHelper::merge($tags, (array) $className::commonTag())]) + new TagDependency(['tags' => ArrayHelper::merge($tags, (array)$className::commonTag())]) ); } break; diff --git a/src/search/interfaces/Filter.php b/src/search/interfaces/Filter.php new file mode 100644 index 0000000..d833e50 --- /dev/null +++ b/src/search/interfaces/Filter.php @@ -0,0 +1,103 @@ + [ + * 'property_1_value_1_id', + * 'property_1_value_2_id', + * 'property_1_value_3_id', + * ... + * ], + * 'property_2_Id' => [ + * 'property_2_value_1_id', + * 'property_2_value_2_id', + * 'property_2_value_3_id', + * ... + * ], + * ] + * @param null|string|\yii\caching\Dependency $customDependency + * @param int $cacheLifetime + * @return array of all found model ids + */ + public static function getModelsByValueIds( + $modelClass, + $selections, + $customDependency = null, + $cacheLifetime = 86400); + + /** + * Prepares all available properties values for use in filter form view. + * + * @param string $modelClass correct model class name. Model have to have PropertiesTrait + * @param array $config + * @param null|string|\yii\caching\Dependency $customDependency + * @param int $cacheLifetime + * @return array + */ + public static function filterFormSet( + $modelClass, + $config, + $customDependency = null, + $cacheLifetime = 86400 + ); +} diff --git a/src/search/interfaces/Search.php b/src/search/interfaces/Search.php new file mode 100644 index 0000000..04505f5 --- /dev/null +++ b/src/search/interfaces/Search.php @@ -0,0 +1,36 @@ +filterRoute)) { + throw new InvalidParamException("'filterRoute' must be set!"); + } + if (false === class_exists($this->modelClass)) { + throw new InvalidParamException("'modelClass' must be the correct class name!"); + } + $class = $this->modelClass; + $model = new $class; + if (false === method_exists($model, 'ensurePropertyGroupIds')) { + throw new InvalidParamException('Model class must has PropertiesTrait.'); + } + parent::init(); + } + + /** + * @inheritdoc + */ + public function run() + { + /** @var AbstractSearch $search */ + $search = Yii::$app->getModule('properties')->getSearch(); + $config = array_merge($this->config, ['modelClass' => $this->modelClass]); + $data = $search->filterFormData($config); + return $this->render( + $this->viewFile, + [ + 'data' => $data, + 'filterRoute' => $this->filterRoute, + ] + ); + } +} \ No newline at end of file diff --git a/src/search/widgets/views/default.php b/src/search/widgets/views/default.php new file mode 100644 index 0000000..3d5c10e --- /dev/null +++ b/src/search/widgets/views/default.php @@ -0,0 +1,26 @@ + 'props-filter']); +foreach ($data['data'] as $propId => $values) { + echo $data['props'][$propId] . "
"; + foreach ($values as $id => $value) { + $checked = false; + if (true === isset($data['selected'][$propId])) { + $checked = in_array($id, $data['selected'][$propId]); + } + echo Html::checkbox('filter[' . $propId . '][]', $checked, ['label' => $value, 'value' => $id]) . "
"; + } +} +?> + + \ No newline at end of file diff --git a/src/translations/ru/app.php b/src/translations/ru/app.php index cd7886a..3b49d2c 100644 --- a/src/translations/ru/app.php +++ b/src/translations/ru/app.php @@ -8,25 +8,26 @@ 'Group properties' => 'Группа свойств', 'Create property' => 'Создать свойство', 'New Static value' => 'Новое статичное значение', - 'Edit Static value' => 'Редактировать статичное значение', + 'Edit Static value' => 'Редактирование статичного значения ', 'Properties' => 'Свойства', 'Delete' => 'Удалить', 'Create property group' => 'Создать группу свойств', - 'Edit property group {id}' => 'Редактировать группу свойств {id}', + 'Edit property group {id}' => 'Редактирование группы свойст {id}', 'Name' => 'Название', 'Description' => 'Описание', 'Internal Name' => 'Внутреннее название', 'Applicable Property Model ID' => 'Свойство применимо к ид модели', 'Sort Order' => 'Сортировка', - 'Is Auto Added' => 'Автоматически добавлять', + 'Is Auto Added' => 'Добавляется автоматически', 'Slug' => 'Часть Url', 'Key' => 'Ключ', 'Data Type' => 'Тип данных', 'Is Internal' => 'Внутренняя', - 'Allow Multiple Values' => 'Разрешенно мультизначения', + 'Allow Multiple Values' => 'Разрешены мультизначения', 'Storage ID' => 'Хранилище', 'Property Handler ID' => 'Обработчик свойств', 'Params should be string or array' => 'Параметры должны быть строкой или массивом', - 'Nothing to union' => 'Нечего объедитять', + 'Nothing to union' => 'Нечего объединять', 'No empty array expected' => 'Ожидался не пустой массив', + 'Use in search' => 'Использовать в поиске', ]; diff --git a/tests/CommonSearchTest.php b/tests/CommonSearchTest.php new file mode 100644 index 0000000..0074377 --- /dev/null +++ b/tests/CommonSearchTest.php @@ -0,0 +1,156 @@ +getModule('properties')->getSearch(); + $data = $search->filterFormData([ + 'modelClass' => Product::class, + 'storage' => [ + EAV::class, + StaticValues::class, + ]]); + $this->assertArrayHasKey('props', $data); + $this->assertArrayHasKey('data', $data); + $this->assertArrayHasKey('selected', $data); + + $this->assertArraySubset([1 => 'material', 11 => 'Count'], $data['props']); + $this->assertEmpty($data['selected']); + $this->assertArrayHasKey(1, $data['data']); + $this->assertArrayHasKey(11, $data['data']); + $this->assertCount(2, $data['data'][1]); + $this->assertCount(3, $data['data'][11]); + return $search; + } + + /** + * @depends testFilterFormData + * @param Search $search + */ + public function testFilterFormDataEmptyModel($search) + { + $res = $search->filterFormData([]); + $this->assertEmpty($res); + } + + /** + * @depends testFilterFormData + * @param Search $search + */ + public function testFilterFormDataBadModel($search) + { + $res = $search->filterFormData(['modelClass' => StaticValue::class]); + $this->assertEmpty($res); + } + + /** + * @depends testFilterFormData + * @param Search $search + */ + public function testFilterFormDataEmptyStorage($search) + { + $data = $search->filterFormData(['modelClass' => Product::class]); + $this->assertArrayHasKey('props', $data); + $this->assertArrayHasKey('data', $data); + $this->assertArrayHasKey('selected', $data); + + $this->assertArraySubset([1 => 'material', 11 => 'Count'], $data['props']); + $this->assertEmpty($data['selected']); + $this->assertArrayHasKey(1, $data['data']); + $this->assertArrayHasKey(11, $data['data']); + $this->assertCount(2, $data['data'][1]); + $this->assertCount(3, $data['data'][11]); + } + + /** + * @depends testFilterFormData + * @param Search $search + */ + public function testFilterFormDataBadStorage($search) + { + $data = $search->filterFormData([ + 'modelClass' => Product::class, + 'storage' => [ + Category::class, + ]]); + $this->assertArrayHasKey('props', $data); + $this->assertArrayHasKey('data', $data); + $this->assertArrayHasKey('selected', $data); + + $this->assertEmpty($data['props']); + $this->assertEmpty($data['data']); + $this->assertEmpty($data['selected']); + } + + /** + * @depends testFilterFormData + * @param Search $search + */ + public function testFilterWithoutParams($search) + { + Yii::$app->request->setQueryParams([]); + $res = $search->findInProperties(Product::class, ['storage' => [StaticValues::class, EAV::class]]); + $this->assertCount(5, $res); + } + + /** + * @depends testFilterFormData + * @param Search $search + */ + public function testFilterWithParamsNoResults($search) + { + Yii::$app->request->setQueryParams(['filter' => [1 => [3], 11 => [11, 13]]]); + $res = $search->findInProperties(Product::class, ['storage' => [StaticValues::class, EAV::class]]); + $this->assertEmpty($res); + } + + /** + * @depends testFilterFormData + * @param Search $search + */ + public function testFilterWithParamsWithResults($search) + { + Yii::$app->request->setQueryParams(['filter' => [1 => [2], 11 => [11, 13]]]); + $res = $search->findInProperties(Product::class, ['storage' => [StaticValues::class, EAV::class]]); + $this->assertArraySubset([4, 5], $res); + } + + /** + * @depends testFilterFormData + * @param Search $search + */ + public function testFilterWithNoModel($search) + { + Yii::$app->request->setQueryParams([]); + $res = $search->findInProperties([]); + $this->assertEmpty($res); + } + + /** + * @depends testFilterFormData + * @param Search $search + */ + public function testFilterIncorrectModel($search) + { + Yii::$app->request->setQueryParams([]); + $res = $search->findInProperties(PropertyStorage::class); + $this->assertEmpty($res); + } +} \ No newline at end of file diff --git a/tests/ConfigTest.php b/tests/ConfigTest.php new file mode 100644 index 0000000..cedcbc4 --- /dev/null +++ b/tests/ConfigTest.php @@ -0,0 +1,85 @@ + 'DevGroup\DataStructure\Properties\Module', + ]; + if (false === empty($searchConfig)) { + $config['modules']['properties'] = array_merge($config['modules']['properties'], $searchConfig); + } + $app = new Application($config); + Yii::$app->cache->flush(); + } + + public function tearDown() + { + if (Yii::$app && Yii::$app->has('session', true)) { + Yii::$app->session->close(); + } + Yii::$app = null; + } + + /** + * @expectedException \yii\base\InvalidConfigException + */ + public function testEmptySearch() + { + $this->createApp(); + Yii::$app->getModule('properties')->getSearch(); + } + + public function testCommonSearch() + { + $this->createApp(['searchClass' => Common::class]); + $search = Yii::$app->getModule('properties')->getSearch(); + $this->assertInstanceOf(Common::class, $search); + } + + public function testBadElastic() + { + //try to run Elastic with no elastic configured (or badly configured like here) + $this->createApp(['searchClass' => Elastic::class, 'searchConfig' => ['hosts' => ['127.0.0.5:9455']]]); + $search = Yii::$app->getModule('properties')->getSearch(); + $this->assertInstanceOf(Common::class, $search); + } + + public function testElastic() + { + $this->createApp(['searchClass' => Elastic::class]); + $search = Yii::$app->getModule('properties')->getSearch(); + $this->assertInstanceOf(Elastic::class, $search); + } + + public function testElasticWithoutWatcher() + { + $this->createApp(['searchClass' => Elastic::class, 'searchConfig' => ['watcherClass' => 'MyNotExistingWatcher']]); + $search = Yii::$app->getModule('properties')->getSearch(); + $this->assertInstanceOf(Common::class, $search); + } + + public function testCorrectWatcher() + { + $this->createApp(['searchClass' => Elastic::class]); + $search = Yii::$app->getModule('properties')->getSearch(); + $this->assertInstanceOf(Watch::class, $search->getWatcher()); + } +} \ No newline at end of file diff --git a/tests/DSTCommonTestCase.php b/tests/DSTCommonTestCase.php new file mode 100644 index 0000000..51d924d --- /dev/null +++ b/tests/DSTCommonTestCase.php @@ -0,0 +1,148 @@ +createDefaultDBConnection(Yii::$app->getDb()->pdo); + } + + /** + * Returns the test dataset. + * + * @return \PHPUnit_Extensions_Database_DataSet_IDataSet + */ + protected function getDataSet() + { + return $this->createFlatXMLDataSet(__DIR__ . '/data/filters.xml'); + } + + public function setUp() + { + Property::$identityMap = []; + $config = include 'config/console.php'; + $config['bootstrap'] = ['properties']; + $config['modules']['properties'] = [ + 'class' => 'DevGroup\DataStructure\Properties\Module', + 'searchClass' => $this->searchClass, + ]; + $app = new Console($config); + try { + Yii::$app->runAction( + 'migrate/down', + [99999, 'interactive' => 0, 'migrationPath' => __DIR__ . '/../src/migrations/'] + ); + Yii::$app->runAction( + 'migrate/up', + ['interactive' => 0, 'migrationPath' => __DIR__ . '/../src/migrations/'] + ); + $this->importDump('models.sql'); + $generator = PropertiesTableGenerator::getInstance(); + $generator->generate(Product::className()); + $generator->generate(Category::className()); + } catch (\Exception $e) { + Yii::$app->clear('db'); + throw $e; + } + + if (Yii::$app->get('db', false) === null) { + $this->markTestSkipped(); + } else { + parent::setUp(); + } + if (true === $this->prepareIndex) { + $client = ClientBuilder::create()->build(); + if (true === $client->indices()->exists(['index' => 'product'])) { + $client->indices()->delete(['index' => 'product']); + } + Yii::$app->runAction('properties/elastic/fill-index'); + } + if (true === $this->runWeb) { + Yii::$app = null; + $config = include dirname(__DIR__) . '/testapp/config/web.php'; + $config['bootstrap'] = ['properties']; + $config['modules']['properties'] = [ + 'class' => 'DevGroup\DataStructure\Properties\Module', + 'searchClass' => $this->searchClass, + ]; + $config['components']['db'] = include 'config/db.php'; + $app = new Web($config); + } + Yii::$app->cache->flush(); + } + + private function importDump($filename) + { + $lines = explode(';', file_get_contents(__DIR__ . "/migrations/$filename")); + foreach ($lines as $line) { + if (trim($line) !== '') { + Yii::$app->getDb()->pdo->exec($line); + } + } + } + + public function tearDown() + { + if (true === $this->runWeb) { + Yii::$app->cache->flush(); + Yii::$app = null; + $config = include 'config/console.php'; + $app = new Console($config); + } + $generator = PropertiesTableGenerator::getInstance(); + $generator->drop(Product::className()); + $generator->drop(Category::className()); + Yii::$app->runAction( + 'migrate/down', + [99999, 'interactive' => 0, 'migrationPath' => __DIR__ . '/../src/migrations/'] + ); + if (true === $this->prepareIndex) { + $client = ClientBuilder::create()->build(); + if (true === $client->indices()->exists(['index' => 'product'])) { + $client->indices()->delete(['index' => 'product']); + } + } + // all identity map should be cleared + if (Yii::$app && Yii::$app->has('session', true)) { + Yii::$app->session->close(); + } + Yii::$app->cache->flush(); + Yii::$app = null; + } +} \ No newline at end of file diff --git a/tests/ElasticIndexControllerTest.php b/tests/ElasticIndexControllerTest.php new file mode 100644 index 0000000..05a3a18 --- /dev/null +++ b/tests/ElasticIndexControllerTest.php @@ -0,0 +1,60 @@ +cache->flush(); + } + + public function testGenerator() + { + $this->createApp([ + 'class' => 'DevGroup\DataStructure\Properties\Module', + 'searchClass' => Search::class, + ]); + $client = ClientBuilder::create()->build(); + if (true === $client->indices()->exists(['index' => 'product'])) { + $client->indices()->delete(['index' => 'product']); + } + Yii::$app->runAction('properties/elastic/fill-index'); + $this->assertTrue($client->indices()->exists(['index' => 'product'])); + return $client; + } + + /** + * @depends testGenerator + * @param Client $client + */ + public function testGeneratorNoElastic($client) + { + $this->createApp([ + 'class' => 'DevGroup\DataStructure\Properties\Module', + 'searchClass' => Search::class, + 'searchConfig' => [ + 'hosts' => ['localhost:9456'], + ] + ]); + if (true === $client->indices()->exists(['index' => 'product'])) { + $client->indices()->delete(['index' => 'product']); + } + Yii::$app->runAction('properties/elastic/fill-index'); + $this->assertFalse($client->indices()->exists(['index' => 'product'])); + } +} \ No newline at end of file diff --git a/tests/ElasticSearchFindEmptyStorageTest.php b/tests/ElasticSearchFindEmptyStorageTest.php new file mode 100644 index 0000000..91c5306 --- /dev/null +++ b/tests/ElasticSearchFindEmptyStorageTest.php @@ -0,0 +1,33 @@ +getModule('properties')->getSearch(); + $data = $search->filterFormData(['modelClass' => Product::class]); + $this->assertArrayHasKey('props', $data); + $this->assertArrayHasKey('data', $data); + $this->assertArrayHasKey('selected', $data); + + $this->assertArraySubset([1 => 'material', 11 => 'Count'], $data['props']); + $this->assertEmpty($data['selected']); + $this->assertArrayHasKey(1, $data['data']); + $this->assertArrayHasKey(11, $data['data']); + $this->assertCount(2, $data['data'][1]); + $this->assertCount(3, $data['data'][11]); + } +} \ No newline at end of file diff --git a/tests/ElasticSearchFindTest.php b/tests/ElasticSearchFindTest.php new file mode 100644 index 0000000..112ad98 --- /dev/null +++ b/tests/ElasticSearchFindTest.php @@ -0,0 +1,29 @@ +getModule('properties')->getSearch(); + Yii::$app->request->setQueryParams(['filter' => [1 => ['plastic'], 11 => ['15', '19']]]); + $res = $search->findInProperties(Product::class, ['storage' => [StaticValues::class, EAV::class]]); + $this->assertArraySubset([4, 5], $res); + } +} \ No newline at end of file diff --git a/tests/ElasticSearchFindWrongModelTest.php b/tests/ElasticSearchFindWrongModelTest.php new file mode 100644 index 0000000..a7b077e --- /dev/null +++ b/tests/ElasticSearchFindWrongModelTest.php @@ -0,0 +1,23 @@ +getModule('properties')->getSearch(); + $res = $search->filterFormData(['modelClass' => StaticValue::class]); + $this->assertEmpty($res); + } +} \ No newline at end of file diff --git a/tests/ElasticSearchTest.php b/tests/ElasticSearchTest.php new file mode 100644 index 0000000..d086c42 --- /dev/null +++ b/tests/ElasticSearchTest.php @@ -0,0 +1,48 @@ +getModule('properties')->getSearch(); + $data = $search->filterFormData([ + 'modelClass' => Product::class, + 'storage' => [ + EAV::class, + StaticValues::class, + ]]); + $this->assertArrayHasKey('props', $data); + $this->assertArrayHasKey('data', $data); + $this->assertArrayHasKey('selected', $data); + + $this->assertArraySubset([1 => 'material', 11 => 'Count'], $data['props']); + $this->assertEmpty($data['selected']); + $this->assertArrayHasKey(1, $data['data']); + $this->assertArrayHasKey(11, $data['data']); + $this->assertCount(2, $data['data'][1]); + $this->assertCount(3, $data['data'][11]); + return $search; + } +} \ No newline at end of file diff --git a/tests/ElasticWatchDeleteTest.php b/tests/ElasticWatchDeleteTest.php new file mode 100644 index 0000000..393245d --- /dev/null +++ b/tests/ElasticWatchDeleteTest.php @@ -0,0 +1,34 @@ +delete(); + + $client = ClientBuilder::create()->build(); + $params = [ + 'index' => 'product', + 'type' => 'static_values', + 'id' => '1' + ]; + $res = $client->get($params); + } + +} \ No newline at end of file diff --git a/tests/ElasticWatchUpdateTest.php b/tests/ElasticWatchUpdateTest.php new file mode 100644 index 0000000..fa75c2c --- /dev/null +++ b/tests/ElasticWatchUpdateTest.php @@ -0,0 +1,46 @@ +loadDefaultValues(); + $prod->autoSaveProperties = true; + $prod->name = 'sonOfAWitch'; + if (false === $prod->save()) { + $this->markTestSkipped(); + } + $pg = PropertyGroup::findOne(1); + if(null === $pg) { + $this->markTestSkipped(); + } + $prod->addPropertyGroup($pg); + $prod->material = 2; + if (false === $prod->save()) { + $this->markTestSkipped(); + } + sleep(4); + $client = ClientBuilder::create()->build(); + $params = [ + 'index' => 'product', + 'type' => 'static_values', + 'id' => $prod->id + ]; + $res = $client->get($params); + $this->assertArrayHasKey('found', $res); + $this->assertEquals(1, $res['found']); + } +} \ No newline at end of file diff --git a/tests/FilterTest.php b/tests/FilterTest.php index 0e57dfc..badda63 100644 --- a/tests/FilterTest.php +++ b/tests/FilterTest.php @@ -2,7 +2,7 @@ namespace DevGroup\DataStructure\tests; -use DevGroup\DataStructure\helpers\PropertiesFilterHelper; +use DevGroup\DataStructure\search\helpers\PropertiesFilterHelper; use DevGroup\DataStructure\helpers\PropertiesHelper; use DevGroup\DataStructure\helpers\PropertiesTableGenerator; use DevGroup\DataStructure\helpers\PropertyHandlerHelper; @@ -123,9 +123,10 @@ public function testFiltering() //single selections // static values $this->assertFilter(['1' => ['metal'],], 1, [1]); - $this->assertFilter(['1' => ['plastic'],], 1, [2]); + $this->assertFilter(['1' => ['plastic'],], 3, [2, 4, 5]); $this->assertFilter(['1' => ['glass']]); $this->assertFilter(['1' => ['stone']]); + // eav $this->assertFilter(['3' => ['138*67*7'],], 1, [1]); $this->assertFilter(['4' => ['2'],], 1, [1]); @@ -188,6 +189,9 @@ public function testFiltering() 1, [1] ); + //empty request + $this->assertEquals([], PropertiesFilterHelper::filterObjects([])); + $exception = false; try { PropertiesHelper::getPropertyValuesByParams( diff --git a/tests/IndexHelperNoDbTest.php b/tests/IndexHelperNoDbTest.php new file mode 100644 index 0000000..939f597 --- /dev/null +++ b/tests/IndexHelperNoDbTest.php @@ -0,0 +1,37 @@ +assertEquals('product', $index); + return $index; + } + + public function testClassToIndexBadClass() + { + $index = IndexHelper::classToIndex([Product::class]); + $this->assertEmpty($index); + } + + public function testStorageClassToType() + { + $type = IndexHelper::storageClassToType(StaticValues::class); + $this->assertEquals('static_values', $type); + } + + public function testStorageClassToTypeBadClass() + { + $type = IndexHelper::storageClassToType([StaticValues::class]); + $this->assertEmpty($type); + } +} \ No newline at end of file diff --git a/tests/IndexHelperTest.php b/tests/IndexHelperTest.php new file mode 100644 index 0000000..fa4e115 --- /dev/null +++ b/tests/IndexHelperTest.php @@ -0,0 +1,85 @@ +build(); + //because of elasticsearch works 'near real time' + sleep(2); + $query = [ + 'index' => $index, + 'body' => [ + 'query' => [ + 'constant_score' => [ + 'filter' => [ + 'bool' => [ + 'must' => [ + [ + 'bool' => [ + 'should' => [ + [ + 'bool' => [ + 'must' => [ + [ + 'term' => [ + 'propertyValues.prop_id' => 1 + ] + ], + [ + 'term' => [ + 'propertyValues.value_rus.raw' => 'plastic' + ] + ] + ] + ] + ] + ] + ] + ], + [ + 'bool' => [ + 'should' => [ + [ + 'bool' => [ + 'must' => [ + [ + 'term' => [ + 'propertyValues.prop_id' => 11 + ] + ], + [ + 'term' => [ + 'propertyValues.value_rus.raw' => '19' + ] + ] + ] + ] + ] + ] + ] + ] + ] + ] + ] + ] + ] + ] + ]; + $res = IndexHelper::primaryKeysByCondition($client, $query); + $this->assertArraySubset([4 => 'static_values', 5 => 'static_values'], $res); + } +} \ No newline at end of file diff --git a/tests/LanguageHelperTest.php b/tests/LanguageHelperTest.php new file mode 100644 index 0000000..c53dae5 --- /dev/null +++ b/tests/LanguageHelperTest.php @@ -0,0 +1,48 @@ + 'DevGroup\DataStructure\Properties\Module', + ]; + $app = new Application($config); + Yii::$app->cache->flush(); + } + + public function tearDown() + { + if (Yii::$app && Yii::$app->has('session', true)) { + Yii::$app->session->close(); + } + Yii::$app = null; + } + + public function testGetAll() + { + $expected = Yii::$app->multilingual->getAllLanguages(); + $expected = ArrayHelper::map($expected, 'id', 'iso_639_2t'); + $got = LanguageHelper::getAll(); + $this->assertEquals($expected, $got); + return $expected; + } + + /** + * @depends testGetAll + */ + public function testGetCurrent($langs) + { + $this->assertEquals(LanguageHelper::getCurrent(), $langs[Yii::$app->multilingual->language_id]); + } +} \ No newline at end of file diff --git a/tests/config/console.php b/tests/config/console.php index 5635acd..a6bd45d 100644 --- a/tests/config/console.php +++ b/tests/config/console.php @@ -23,14 +23,24 @@ ], ], 'multilingual' => [ - 'class' => 'DevGroup\Multilingual\Multilingual', + 'class' => \DevGroup\Multilingual\Multilingual::class, 'default_language_id' => 1, + 'handlers' => [ + [ + 'class' => \DevGroup\Multilingual\DefaultGeoProvider::class, + 'default' => [ + 'country' => [ + 'name' => 'English', + 'iso' => 'en', + ], + ], + ], + ], ], - 'db' => [ - 'class' => Connection::className(), - 'dsn' => 'mysql:host=localhost;dbname=yii2_datastructure', - 'username' => 'root', - 'password' => '', + 'db' => include 'db.php', + 'filedb' => [ + 'class' => 'yii2tech\filedb\Connection', + 'path' => dirname(dirname(__DIR__)) . '/testapp/config/data', ], ], ]; \ No newline at end of file diff --git a/tests/config/db.php b/tests/config/db.php new file mode 100644 index 0000000..64b0750 --- /dev/null +++ b/tests/config/db.php @@ -0,0 +1,7 @@ + \yii\db\Connection::class, + 'dsn' => 'mysql:host=localhost;dbname=yii2_datastructure', + 'username' => 'root', + 'password' => '', +]; \ No newline at end of file diff --git a/tests/data/filters.xml b/tests/data/filters.xml index 24f8cea..61a0b0c 100644 --- a/tests/data/filters.xml +++ b/tests/data/filters.xml @@ -11,7 +11,7 @@ - + @@ -21,6 +21,18 @@ + + + + + + + + + + + + @@ -50,6 +62,8 @@ + + @@ -60,24 +74,37 @@ + - - - - - - + + + + + + + + + + + + + + + + + +