diff --git a/.github/ci.yml b/.github/ci.yml new file mode 100644 index 00000000..ae161fac --- /dev/null +++ b/.github/ci.yml @@ -0,0 +1,22 @@ +name: Package CI + +on: + pull_request: + +jobs: + checks: + name: Checks + runs-on: ubuntu-latest + strategy: + matrix: + php: [ 7.4 ] + version: [ lowest, standard ] + + steps: + - uses: actions/checkout@v2 + - uses: shivammathur/setup-php@v2 + with: + php-version: ${{ matrix.php }} + + - name: Run a one-line script + run: echo Hello, world! diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml new file mode 100644 index 00000000..c403febb --- /dev/null +++ b/.github/workflows/ci.yml @@ -0,0 +1,56 @@ +name: Package CI + +on: + pull_request: + +jobs: + checks: + name: Checks + runs-on: ubuntu-latest + strategy: + matrix: + php: [ 7.4, 8.0, 8.1 ] + version: [ lowest, standard ] + elastic: [ 7.12.1 ] + + steps: + - uses: actions/checkout@v2 + - uses: shivammathur/setup-php@v2 + with: + php-version: ${{ matrix.php }} + + - name: Configure sysctl limits + run: | + sudo swapoff -a + sudo sysctl -w vm.swappiness=1 + sudo sysctl -w fs.file-max=262144 + sudo sysctl -w vm.max_map_count=262144 + + - name: Start Elasticsearch + uses: elastic/elastic-github-actions/elasticsearch@master + with: + stack-version: ${{ matrix.elastic }} + + - if: matrix.version == 'lowest' + name: Composer lowest + run: make composer-lowest + + - if: matrix.version == 'standard' + name: Composer standard + run: make composer + + - if: matrix.php == '7.4' + name: Coding standard + run: make cs + + - if: matrix.php == '7.4' && matrix.version == 'lowest' + name: PHPStan lowest + run: make phpstan-lowest + + - if: matrix.php == '7.4' && matrix.version == 'standard' + name: PHPStan standard + run: make phpstan + + - name: Tests + run: make tests + diff --git a/.travis.yml b/.travis.yml deleted file mode 100644 index 4fb36b71..00000000 --- a/.travis.yml +++ /dev/null @@ -1,46 +0,0 @@ -language: php - -cache: - directories: - - $HOME/.composer/cache - - $HOME/phpcs-cache - -php: - - 7.1 - - 7.2 - - 7.3 - -services: - - elasticsearch - -env: - matrix: - - RUN_TESTS=1 COMPOSER_EXTRA_ARGS="" - - RUN_TESTS=1 COMPOSER_EXTRA_ARGS="--prefer-lowest" - -matrix: - fast_finish: true - include: - - php: 7.3 - env: COMPOSER_EXTRA_ARGS="" COVERAGE="--coverage ./coverage.xml --coverage-src ./src" - - php: 7.3 - env: COMPOSER_EXTRA_ARGS="" PHPSTAN=1 RUN_TESTS=0 - - php: 7.3 - env: COMPOSER_EXTRA_ARGS="" CODING_STANDARD=1 RUN_TESTS=0 - -before_install: - - travis_retry composer self-update - -install: - - travis_retry composer update --no-interaction --no-suggest --no-progress --prefer-dist --prefer-stable $COMPOSER_EXTRA_ARGS # update because we may need --prefer-lowest option - -script: - - if [ "$PHPSTAN" = "1" ]; then vendor/bin/phpstan analyse -l 7 -c phpstan.neon src tests; fi - - if [ "$CODING_STANDARD" = "1" ]; then vendor/bin/phpcs --standard=vendor/spameri/coding-standard/src/ruleset.xml --cache=$HOME/phpcs-cache/.phpcs-cache src tests; fi - - if [ "$RUN_TESTS" = "1" ]; then vendor/bin/tester $COVERAGE -s -c ./tests/php.ini-unix ./tests; fi - -after_script: - - if [ "$COVERAGE" != "" ]; then vendor/bin/php-coveralls --verbose --config tests/.coveralls.yml || true; fi - -after_failure: - - 'for i in $(find ./tests -name \*.actual); do echo "--- $i"; cat $i; echo; echo; done' diff --git a/composer.json b/composer.json index 2957ffe2..7f92e9ba 100644 --- a/composer.json +++ b/composer.json @@ -24,27 +24,28 @@ } ], "require": { - "php": ">=7.1", + "php": ">=7.4", "ext-json": "*", - "nette/di": "^2.4.10", - "nette/security": "^2.4.3", - "nette/utils": "^2.5.3", - "spameri/elastic-query": "^0.5.0", - "elasticsearch/elasticsearch": "^6.0", - "kdyby/console": "^2.7.1", - "kdyby/datetime-provider": "v1.0.0", - "kdyby/monolog": "^1.3.2", - "tracy/tracy": "^2.6.0" + "ext-curl": "*", + "spameri/elastic-query": "^v1.0.0", + "elasticsearch/elasticsearch": "^7.12.0", + "nette/di": "^3.0.5", + "nette/utils": "^3.2.5", + "symfony/console": "^4.4.26", + "tracy/tracy": "^2.6.7", + "monolog/monolog": "^2.0.2", + "ezimuel/ringphp": "^1.2" }, "require-dev": { "spameri/coding-standard": "dev-master", "spameri/dependency-mocker": "^1.3", - "nette/tester": "^2.2.0", - "phpstan/phpstan-shim": "^0.11.5", + "nette/tester": "^2.4", + "phpstan/phpstan": "0.12.84", "php-coveralls/php-coveralls": "^2.1", - "nette/bootstrap": "2.4", - "nette/http": "^2.4.7", - "mockery/mockery": "^1.2" + "nette/bootstrap": "^3.0", + "nette/http": "^3.0.7", + "kdyby/console": "^4.0.0", + "mockery/mockery": "^1.5" }, "autoload": { "psr-4": { @@ -55,5 +56,10 @@ "psr-4": { "SpameriTests\\": "tests/SpameriTests" } + }, + "config": { + "allow-plugins": { + "dealerdirect/phpcodesniffer-composer-installer": true + } } } diff --git a/doc/00_quick_start.md b/doc/00_quick_start.md new file mode 100644 index 00000000..89af1190 --- /dev/null +++ b/doc/00_quick_start.md @@ -0,0 +1,459 @@ +# Quick Start + +## 1. Install + +Use composer to install this library. +```bash +composer require spameri/elastic +``` + +--- + +## 2. Configure + +You need to set up few things first, before you can dive into ElasticSearch. + +### I. Register extension + +In your configuration neon file you need to add these lines to `extension:` section. + +```yaml +extensions: + spameriElasticSearch: \Spameri\Elastic\DI\SpameriElasticSearchExtension +``` + +Optionaly you need some Symfony Console implementation ie: + +`````yaml +extensions: + console: Kdyby\Console\DI\ConsoleExtension +````` + +### II. Configure + +Now you need to tell library where is ElasticSearch running. Default values are **localhost** +and port **9200**. That means if you are running ElasticSearch locally with default port, no +need to configure anything. + +```yaml +spameriElasticSearch: + host: 192.168.0.14 + port: 9200 +``` + +### III. Configure Entity + +Next step is to configure your first entity. In this example, entity is for e-shop product. + +In neon configuration you need just index name. In this example it is just under parameters.elasticsSearch key, but can be anywhere. + +```yaml +parameters: + elasticSearch: + SimpleProductIndex: spameri_simple_product +``` + +--- + +## 3. Create entity class + +Mainly you need entity class, which implements \Spameri\Elastic\Entity\ElasticEntityInterface interface. +```php +id; +} + + +public function entityVariables(): array +{ + return \get_object_vars($this); +} +``` + +To build entity from Elasticsearch and not use black magic🪄 you have to explicitly make factory class for that entity. And from hit object which represents result from Elasticsearch you have to extract all parameters. No magic, typed 🎖️ + +### Factory +````php +class SimpleProductFactory implements \Spameri\Elastic\Factory\EntityFactoryInterface +{ + + public function create(\Spameri\ElasticQuery\Response\Result\Hit $hit) : \Generator + { + yield new \App\ProductModule\Entity\SimpleProduct( + new \Spameri\Elastic\Entity\Property\ElasticId($hit->id()), + $hit->getValue('databaseId'), + $hit->getValue('name'), + $hit->getValue('content'), + $hit->getValue('alias'), + $hit->getValue('image'), + $hit->getValue('price'), + $hit->getValue('availability'), + $hit->getValue('tags'), + $hit->getValue('categories') + ); + } + +} +```` + +In collection factory you need to specify our entity. + +### CollectionFactory +````php +class SimpleProductCollectionFactory implements \Spameri\Elastic\Factory\CollectionFactoryInterface +{ + + public function create( + \Spameri\Elastic\Model\ServiceInterface $service, + array $elasticIds = [], + \Spameri\Elastic\Entity\ElasticEntityInterface ... $entityCollection + ) : \Spameri\Elastic\Entity\ElasticEntityCollectionInterface + { + return new \App\ProductModule\Entity\ProductCollection($service, $elasticIds, ... $entityCollection); + } + +} +```` + +--- + +## 4. Index Configuring +````php +class SimpleProductConfig implements \Spameri\Elastic\Settings\IndexConfigInterface +{ + public function __construct( + string $indexName + ) + { + $this->indexName = $indexName; + } +} +```` + +`public function provide(): \Spameri\ElasticQuery\Mapping\Settings` + +````php +$settings = new \Spameri\ElasticQuery\Mapping\Settings($this->indexName); +$czechDictionary = new \Spameri\ElasticQuery\Mapping\Analyzer\Custom\CzechDictionary(); +$settings->addAnalyzer($czechDictionary); + +$lowerCase = new \Spameri\ElasticQuery\Mapping\Analyzer\Custom\Lowercase(); +$settings->addAnalyzer($lowerCase); +```` + +````php +$settings->addMappingField( + new \Spameri\ElasticQuery\Mapping\Settings\Mapping\Field( + 'databaseId', + \Spameri\Elastic\Model\ValidateMapping\AllowedValues::TYPE_KEYWORD + ) +); +$settings->addMappingField( + new \Spameri\ElasticQuery\Mapping\Settings\Mapping\Field( + 'name', + \Spameri\Elastic\Model\ValidateMapping\AllowedValues::TYPE_TEXT, + $czechDictionary + ) +); +$settings->addMappingField( + new \Spameri\ElasticQuery\Mapping\Settings\Mapping\Field( + 'content', + \Spameri\Elastic\Model\ValidateMapping\AllowedValues::TYPE_TEXT, + $czechDictionary + ) +); +```` + + +````php +$settings->addMappingField( + new \Spameri\ElasticQuery\Mapping\Settings\Mapping\Field( + 'tags', + \Spameri\Elastic\Model\ValidateMapping\AllowedValues::TYPE_TEXT, + $lowerCase + ) +); +```` + + +## 5. Export data to ElasticSearch + +````php +class ExportToElastic extends \Spameri\Elastic\Import\Run +{ + + public function __construct( + string $logDir = 'log', + \Symfony\Component\Console\Output\ConsoleOutput $output, + \Spameri\Elastic\Import\Run\NullLoggerHandler $loggerHandler, + \Spameri\Elastic\Import\Lock\NullLock $lock, + \Spameri\Elastic\Import\RunHandler\NullHandler $runHandler, + + \App\ProductModule\Model\ExportToElastic\DataProvider $dataProvider, + \App\ProductModule\Model\ExportToElastic\PrepareImportData $prepareImportData, + \App\ProductModule\Model\ExportToElastic\DataImport $dataImport, + + \Spameri\Elastic\Import\AfterImport\NullAfterImport $afterImport + ) + { + parent::__construct($logDir, $output, $loggerHandler, $lock, $runHandler, $dataProvider, $prepareImportData, $dataImport, $afterImport); + } + +} +```` + + +````php +class DataProvider implements \Spameri\Elastic\Import\DataProviderInterface +{ + public function provide(\Spameri\Elastic\Import\Run\Options $options): \Generator + { + $query = $this->connection->select('*')->from('table'); + + while ($hasResults) { + $items = $query->fetchAll($offset, $limit); + + yield from $items; + + if ( ! \count($items)) { + $hasResults = FALSE; + + } else { + $offset += $limit; + } + } + } +} +```` + + +````php + +class PrepareImportData implements \Spameri\Elastic\Import\PrepareImportDataInterface +{ + + public function prepare($entityData): \Spameri\Elastic\Entity\AbstractImport + { + $imageSrc = '//via.placeholder.com/150x150'; + $elasticId = NULL; + $tags = []; + $categories = []; + return new \App\ProductModule\Entity\SimpleProduct( + $elasticId, + $entityData['id'], + $entityData['name'], + $entityData['content_description'], + $entityData['alias'], + $imageSrc, + $entityData['amount'], + $entityData['availability_id'] === 1 ? 'Skladem' : 'Nedostupné', + $tags, + $categories + ); + } + +} +```` + +````php +class DataImport implements \Spameri\Elastic\Import\DataImportInterface +{ + + /** + * @param \App\ProductModule\Entity\SimpleProduct $entity + */ + public function import( + \Spameri\Elastic\Entity\AbstractImport $entity + ): \Spameri\Elastic\Import\ResponseInterface + { + $id = $this->productService->insert($entity); + + return new \Spameri\Elastic\Import\Response\SimpleResponse( + $id, + $entity + ); + } + +} +```` + +````php +$options = new \Spameri\Elastic\Import\Run\Options(600); + +// Clear index +try { + $this->delete->execute($this->simpleProductConfig->provide()->indexName()); +} catch (\Spameri\Elastic\Exception\AbstractElasticSearchException $exception) {} + +// Create index +$this->create->execute( + $this->simpleProductConfig->provide()->indexName(), + $this->simpleProductConfig->provide()->toArray() +); + +// Export +$this->exportToElastic->execute($options); +```` + +## 6. Presenter, Form, Template +````php +class SimpleProductListPresenter extends \App\Presenter\BasePresenter +{ + + public function renderDefault($queryString): void + { + $query = $this->buildQuery($queryString); + + try { + $products = $this->productService->getAllBy($query); + + } catch (\Spameri\Elastic\Exception\AbstractElasticSearchException $exception) { + $products = []; + } + + $this->getTemplate()->add( + 'products', + $products + ); + $this->getTemplate()->add( + 'queryString', + $queryString + ); + } + +} +```` + +````php +public function createComponentSearchForm() :\Nette\Application\UI\Form +{ + $form = new \Nette\Application\UI\Form(); + $form->addText('queryString', 'query') + ->setAttribute('class', 'inp-text suggest') + ; + + $form->addSubmit('search', 'Search'); + + $form->onSuccess[] = function () use ($form) { + $this->redirect( + 301, + ':Product:SimpleProductList:default', + [ + 'queryString' => $form->getValues()->queryString, + ] + ); + }; + + return $form; +} +```` + +````php +{control searchForm} +

You have searched: {$queryString}

+ +
+