diff --git a/.editorconfig b/.editorconfig index 47e735b6..c75f6f91 100644 --- a/.editorconfig +++ b/.editorconfig @@ -11,7 +11,10 @@ indent_style = space indent_size = 4 max_line_length = 120 -[*.{yml,json}] +[*.{yml,yaml}] +indent_size = 4 + +[*.json] indent_size = 2 [*.{neon,neon.dist}] diff --git a/.github/ISSUE_TEMPLATE/config.yml b/.github/ISSUE_TEMPLATE/config.yml new file mode 100644 index 00000000..de409cb2 --- /dev/null +++ b/.github/ISSUE_TEMPLATE/config.yml @@ -0,0 +1,5 @@ +blank_issues_enabled: false +contact_links: + - name: Support questions & other + url: https://discord.meilisearch.com/ + about: Support is not handled here but on our Discord diff --git a/.github/ISSUE_TEMPLATE/feature_request.md b/.github/ISSUE_TEMPLATE/feature_request.md index 39bb8f02..8a916518 100644 --- a/.github/ISSUE_TEMPLATE/feature_request.md +++ b/.github/ISSUE_TEMPLATE/feature_request.md @@ -1,5 +1,5 @@ --- -name: Feature Request πŸ’‘ +name: Feature Request & Enhancement πŸ’‘ about: Suggest a new idea for the project. title: '' labels: '' diff --git a/.github/ISSUE_TEMPLATE/other.md b/.github/ISSUE_TEMPLATE/other.md deleted file mode 100644 index b7a939a9..00000000 --- a/.github/ISSUE_TEMPLATE/other.md +++ /dev/null @@ -1,7 +0,0 @@ ---- -name: Other -about: Any other topic you want to talk about. -title: '' -labels: '' -assignees: '' ---- diff --git a/.github/dependabot.yml b/.github/dependabot.yml index 0b7f847a..b13403fa 100644 --- a/.github/dependabot.yml +++ b/.github/dependabot.yml @@ -4,12 +4,12 @@ updates: - package-ecosystem: composer directory: "/" schedule: - interval: daily + interval: "monthly" time: "04:00" open-pull-requests-limit: 10 labels: - - dependencies - - skip-changelog + - dependencies + - skip-changelog rebase-strategy: disabled - package-ecosystem: "github-actions" diff --git a/.github/release-draft-template.yml b/.github/release-draft-template.yml index 4953c783..e93aa1a0 100644 --- a/.github/release-draft-template.yml +++ b/.github/release-draft-template.yml @@ -1,33 +1,37 @@ name-template: 'v$RESOLVED_VERSION 🎡' tag-template: 'v$RESOLVED_VERSION' exclude-labels: - - 'skip-changelog' + - 'skip-changelog' version-resolver: - minor: - labels: - - 'breaking-change' - default: patch + minor: + labels: + - 'breaking-change' + default: patch categories: - - title: '⚠️ Breaking changes' - label: 'breaking-change' - - title: 'πŸš€ Enhancements' - label: 'enhancement' - - title: 'πŸ› Bug Fixes' - label: 'bug' - - title: 'πŸ”’ Security' - label: 'security' + - title: '⚠️ Breaking changes' + label: 'breaking-change' + - title: 'πŸš€ Enhancements' + label: 'enhancement' + - title: 'πŸ› Bug Fixes' + label: 'bug' + - title: 'πŸ”’ Security' + label: 'security' + - title: 'βš™οΈ Maintenance/misc' + label: + - 'maintenance' + - 'documentation' template: | - $CHANGES + $CHANGES - Thanks again to $CONTRIBUTORS! πŸŽ‰ + Thanks again to $CONTRIBUTORS! πŸŽ‰ no-changes-template: 'Changes are coming soon 😎' sort-direction: 'ascending' replacers: - - search: '/(?:and )?@dependabot-preview(?:\[bot\])?,?/g' - replace: '' - - search: '/(?:and )?@dependabot(?:\[bot\])?,?/g' - replace: '' - - search: '/(?:and )?@bors(?:\[bot\])?,?/g' - replace: '' - - search: '/(?:and )?@meili-bot,?/g' - replace: '' + - search: '/(?:and )?@dependabot-preview(?:\[bot\])?,?/g' + replace: '' + - search: '/(?:and )?@dependabot(?:\[bot\])?,?/g' + replace: '' + - search: '/(?:and )?@bors(?:\[bot\])?,?/g' + replace: '' + - search: '/(?:and )?@meili-bot,?/g' + replace: '' diff --git a/.github/workflows/pre-release-tests.yml b/.github/workflows/pre-release-tests.yml index 50b88b0b..57f64302 100644 --- a/.github/workflows/pre-release-tests.yml +++ b/.github/workflows/pre-release-tests.yml @@ -3,33 +3,33 @@ name: Pre-Release Tests # Will only run for PRs and pushes to bump-meilisearch-v* on: - push: - branches: - - bump-meilisearch-v* - pull_request: - branches: - - bump-meilisearch-v* + push: + branches: + - bump-meilisearch-v* + pull_request: + branches: + - bump-meilisearch-v* jobs: - integration-tests: - runs-on: ubuntu-latest - strategy: - matrix: - php-versions: ['7.4', '8.0'] - name: integration-tests-against-rc (PHP ${{ matrix.php-versions }}) - steps: - - uses: actions/checkout@v3 - - name: Install PHP - uses: shivammathur/setup-php@v2 - with: - php-version: ${{ matrix.php-versions }} - - name: Validate composer.json and composer.lock - run: composer validate - - name: Install dependencies - run: composer install --prefer-dist --no-progress --quiet - - name: Get the latest Meilisearch RC - run: echo "MEILISEARCH_VERSION=$(curl https://raw.githubusercontent.com/meilisearch/integration-guides/main/scripts/get-latest-meilisearch-rc.sh | bash)" >> $GITHUB_ENV - - name: Meilisearch (${{ env.MEILISEARCH_VERSION }}) setup with Docker - run: docker run -d -p 7700:7700 getmeili/meilisearch:${{ env.MEILISEARCH_VERSION }} meilisearch --master-key=masterKey --no-analytics - - name: Run test suite - run: composer test:unit + integration-tests: + runs-on: ubuntu-latest + strategy: + matrix: + php-versions: ['7.4', '8.0'] + name: integration-tests-against-rc (PHP ${{ matrix.php-versions }}) + steps: + - uses: actions/checkout@v4 + - name: Install PHP + uses: shivammathur/setup-php@v2 + with: + php-version: ${{ matrix.php-versions }} + - name: Validate composer.json and composer.lock + run: composer validate + - name: Install dependencies + run: composer install --prefer-dist --no-progress --quiet + - name: Get the latest Meilisearch RC + run: echo "MEILISEARCH_VERSION=$(curl https://raw.githubusercontent.com/meilisearch/integration-guides/main/scripts/get-latest-meilisearch-rc.sh | bash)" >> $GITHUB_ENV + - name: Meilisearch (${{ env.MEILISEARCH_VERSION }}) setup with Docker + run: docker run -d -p 7700:7700 getmeili/meilisearch:${{ env.MEILISEARCH_VERSION }} meilisearch --master-key=masterKey --no-analytics + - name: Run test suite + run: composer test:unit diff --git a/.github/workflows/release-drafter.yml b/.github/workflows/release-drafter.yml index 477c50ff..20f2d83f 100644 --- a/.github/workflows/release-drafter.yml +++ b/.github/workflows/release-drafter.yml @@ -1,16 +1,16 @@ name: Release Drafter on: - push: - branches: - - main + push: + branches: + - main jobs: - update_release_draft: - runs-on: ubuntu-latest - steps: - - uses: release-drafter/release-drafter@v5 - with: - config-name: release-draft-template.yml - env: - GITHUB_TOKEN: ${{ secrets.RELEASE_DRAFTER_TOKEN }} + update_release_draft: + runs-on: ubuntu-latest + steps: + - uses: release-drafter/release-drafter@v6 + with: + config-name: release-draft-template.yml + env: + GITHUB_TOKEN: ${{ secrets.RELEASE_DRAFTER_TOKEN }} diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml index 72ddc103..528a5b6f 100644 --- a/.github/workflows/tests.yml +++ b/.github/workflows/tests.yml @@ -9,68 +9,102 @@ on: - staging - main +env: + fail-fast: true + jobs: integration-tests: # Will not run if the event is a PR to bump-meilisearch-v* (so a pre-release PR) # Will still run for each push to bump-meilisearch-v* if: github.event_name != 'pull_request' || !startsWith(github.base_ref, 'bump-meilisearch-v') runs-on: ubuntu-latest + services: + meilisearch: + image: getmeili/meilisearch:latest + ports: + - 7700:7700 + env: + MEILI_MASTER_KEY: masterKey + MEILI_NO_ANALYTICS: true strategy: matrix: - php-version: ['7.4', '8.0', '8.1', '8.2'] - include: + php-version: ['7.4', '8.1', '8.2', '8.3'] + sf-version: ['5.4', '6.4', '7.0', '7.1'] + exclude: - php-version: '7.4' - sf-version: '4.4.*' + sf-version: '6.4' - php-version: '7.4' - sf-version: '5.4.*' - - php-version: '8.0' - sf-version: '6.0.*' + sf-version: '7.0' + - php-version: '7.4' + sf-version: '7.1' + - php-version: '8.1' + sf-version: '5.4' - php-version: '8.1' - sf-version: '6.0.*' + sf-version: '7.0' - php-version: '8.1' - sf-version: '6.1.*' + sf-version: '7.1' - php-version: '8.2' - sf-version: '6.2.*' + sf-version: '5.4' + - php-version: '8.3' + sf-version: '5.4' + - php-version: '8.3' + sf-version: '5.4' - name: integration-tests (PHP ${{ matrix.php-version }}) (Symfony ${{ matrix.sf-version }}) + name: integration-tests (PHP ${{ matrix.php-version }}) (Symfony ${{ matrix.sf-version }}.*) steps: - - uses: actions/checkout@v3 + - name: Checkout code + uses: actions/checkout@v4 + - name: Install PHP uses: shivammathur/setup-php@v2 with: php-version: ${{ matrix.php-version }} - tools: composer:v2, flex + tools: composer, flex + - name: Validate composer.json and composer.lock run: composer validate + + - name: Remove doctrine/annotations + if: matrix.php-version != '7.4' + run: sed -i '/doctrine\/annotations/d' composer.json + - name: Install dependencies + uses: ramsey/composer-install@v3 env: - SYMFONY_REQUIRE: ${{ matrix.sf-version }} - run: composer install --prefer-dist --no-progress --quiet - - name: "Remove doctrine/annotations" - if: matrix.php-version != '7.4' - run: | - composer remove --dev doctrine/annotations - - name: Meilisearch setup with Docker - run: docker run -d -p 7700:7700 getmeili/meilisearch:latest meilisearch --master-key=masterKey --no-analytics + SYMFONY_REQUIRE: ${{ matrix.sf-version }}.* + with: + dependency-versions: 'highest' + - name: Run test suite - run: composer test:unit + run: composer test:unit -- --coverage-clover coverage.xml + + - name: Upload coverage file + uses: actions/upload-artifact@v4 + with: + name: 'phpunit-${{ matrix.php-version }}-${{ matrix.sf-version }}-coverage' + path: 'coverage.xml' code-style: runs-on: ubuntu-latest name: 'Code style' steps: - - uses: actions/checkout@v3 + - uses: actions/checkout@v4 - name: Install PHP uses: shivammathur/setup-php@v2 with: - php-version: '8.1' + php-version: 8.3 - name: Validate composer.json and composer.lock run: composer validate - name: Install dependencies - run: composer install --prefer-dist --no-progress --quiet + uses: ramsey/composer-install@v3 + env: + SYMFONY_REQUIRE: 7.1.* + with: + composer-options: '--no-progress --quiet' + dependency-versions: 'highest' - name: PHP CS Fixer run: composer lint:check @@ -79,7 +113,39 @@ jobs: run: composer phpmd continue-on-error: true - - name: PHPstan + - name: PHPStan run: | vendor/bin/simple-phpunit --version composer phpstan + + yaml-lint: + name: Yaml linting check + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + - name: Yaml lint check + uses: ibiqlik/action-yamllint@v3 + with: + config_file: .yamllint.yml + + upload-coverage: + name: Upload coverage to Codecov + runs-on: ubuntu-latest + needs: + - integration-tests + + steps: + - name: Checkout + uses: actions/checkout@v4 + with: + fetch-depth: 2 + + - name: Download coverage files + uses: actions/download-artifact@v4 + with: + path: reports + + - name: Upload to Codecov + uses: codecov/codecov-action@v4 + with: + directory: reports diff --git a/.gitignore b/.gitignore index a0b80ceb..427199d6 100644 --- a/.gitignore +++ b/.gitignore @@ -3,6 +3,7 @@ composer.lock /vendor/ /var/ +phpstan.neon # Meilisearch /data.ms/* diff --git a/.php-cs-fixer.dist.php b/.php-cs-fixer.dist.php index 9f927c45..5ea2c32b 100644 --- a/.php-cs-fixer.dist.php +++ b/.php-cs-fixer.dist.php @@ -6,21 +6,17 @@ ->in(__DIR__.'/src') ->in(__DIR__.'/tests') ->append([__FILE__]); -$config = new PhpCsFixer\Config(); -$config->setRules([ - '@Symfony' => true, - '@PHP80Migration:risky' => true, - 'global_namespace_import' => [ - 'import_classes' => false, - 'import_functions' => false, - 'import_constants' => false, - ], - 'no_superfluous_phpdoc_tags' => false, - ] -) +return (new PhpCsFixer\Config()) ->setRiskyAllowed(true) ->setFinder($finder) -; - -return $config; + ->setRules([ + '@Symfony' => true, + '@PHP80Migration:risky' => true, + 'global_namespace_import' => [ + 'import_classes' => false, + 'import_functions' => false, + 'import_constants' => false, + ], + 'no_superfluous_phpdoc_tags' => false, + ]); diff --git a/.yamllint.yml b/.yamllint.yml new file mode 100644 index 00000000..35c43988 --- /dev/null +++ b/.yamllint.yml @@ -0,0 +1,9 @@ +extends: default +ignore: | + vendor +rules: + comments-indentation: disable + line-length: disable + document-start: disable + brackets: disable + truthy: disable diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 3f666fb4..5951e5bd 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -16,7 +16,7 @@ First of all, thank you for contributing to Meilisearch! The goal of this docume 1. **You're familiar with [GitHub](https://github.com) and the [Pull Request](https://help.github.com/en/github/collaborating-with-issues-and-pull-requests/about-pull-requests) (PR) workflow.** 2. **You've read the Meilisearch [documentation](https://docs.meilisearch.com) and the [README](/README.md).** -3. **You know about the [Meilisearch community](https://docs.meilisearch.com/learn/what_is_meilisearch/contact.html). Please use this for help.** +3. **You know about the [Meilisearch community](https://www.meilisearch.com/docs/learn/what_is_meilisearch/contact.html). Please use this for help.** ## How to Contribute diff --git a/LICENSE b/LICENSE index b1f0d22c..971d3878 100644 --- a/LICENSE +++ b/LICENSE @@ -1,6 +1,6 @@ MIT License -Copyright (c) 2019-2022 Meili SAS +Copyright (c) 2019-2024 Meili SAS Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal diff --git a/README.md b/README.md index ed2b2655..1323a7d4 100644 --- a/README.md +++ b/README.md @@ -6,14 +6,16 @@

Meilisearch | + Meilisearch Cloud | Documentation | Discord | Roadmap | Website | - FAQ + FAQ

+ Codecov coverage Latest Stable Version Test License @@ -26,9 +28,10 @@ **Meilisearch** is an open-source search engine. [Discover what Meilisearch is!](https://github.com/meilisearch/meilisearch) -## Table of Contents +## Table of Contents - [πŸ“– Documentation](#-documentation) +- [⚑ Supercharge your Meilisearch experience](#-supercharge-your-meilisearch-experience) - [πŸ“ Requirements](#-requirements) - [πŸ€– Compatibility with Meilisearch](#-compatibility-with-meilisearch) - [πŸ’‘ Learn More](#-learn-more) @@ -38,13 +41,19 @@ Check out the [Wiki](https://github.com/meilisearch/meilisearch-symfony/wiki) of this repository to get started! πŸš€ -Also, see our [Documentation](https://docs.meilisearch.com/learn/tutorials/getting_started.html) or our [API References](https://docs.meilisearch.com/reference/api/). +Also, see our [Documentation](https://www.meilisearch.com/docs/learn/getting_started/installation) or our [API References](https://www.meilisearch.com/docs/reference/api/overview). + +## ⚑ Supercharge your Meilisearch experience + +Say goodbye to server deployment and manual updates with [Meilisearch Cloud](https://www.meilisearch.com/pricing?utm_campaign=oss&utm_source=integration&utm_medium=meilisearch-symfony). No credit card required. ## πŸ“ Requirements * **Require** PHP 7.4 and later. -* **Compatible** with Symfony 4.0 and later. -* **Support** Doctrine ORM and Doctrine MongoDB. +* **Compatible** with Symfony 5.4 and later. +* **Support** Doctrine ORM. + +For support of older versions, see older versions of this bundle. ## πŸ€– Compatibility with Meilisearch @@ -54,10 +63,10 @@ This package guarantees compatibility with [version v1.x of Meilisearch](https:/ The following sections may interest you: -- **Manipulate documents**: see the [API references](https://docs.meilisearch.com/reference/api/documents.html) or read more about [documents](https://docs.meilisearch.com/learn/core_concepts/documents.html). -- **Search**: see the [API references](https://docs.meilisearch.com/reference/api/search.html) or follow our guide on [search parameters](https://docs.meilisearch.com/reference/features/search_parameters.html). -- **Manage the indexes**: see the [API references](https://docs.meilisearch.com/reference/api/indexes.html) or read more about [indexes](https://docs.meilisearch.com/learn/core_concepts/indexes.html). -- **Configure the index settings**: see the [API references](https://docs.meilisearch.com/reference/api/settings.html) or follow our guide on [settings parameters](https://docs.meilisearch.com/reference/features/settings.html). +- **Manipulate documents**: see the [API references](https://www.meilisearch.com/docs/reference/api/documents) or read more about [documents](https://www.meilisearch.com/docs/learn/core_concepts/documents). +- **Search**: see the [API references](https://www.meilisearch.com/docs/reference/api/search) or follow our guide on [search parameters](https://www.meilisearch.com/docs/reference/api/search#search-parameters). +- **Manage the indexes**: see the [API references](https://www.meilisearch.com/docs/reference/api/indexes) or read more about [indexes](https://www.meilisearch.com/docs/learn/core_concepts/indexes). +- **Configure the index settings**: see the [API references](https://www.meilisearch.com/docs/reference/api/settings) or follow our guide on [settings parameters](https://www.meilisearch.com/docs/reference/api/settings#settings_parameters). πŸ“– Also, check out the [Wiki](https://github.com/meilisearch/meilisearch-symfony/wiki) of this repository! diff --git a/bors.toml b/bors.toml index 330d47fe..45afcecb 100644 --- a/bors.toml +++ b/bors.toml @@ -1,8 +1,12 @@ status = [ 'integration-tests (PHP 7.4) (Symfony 5.4.*)', - 'integration-tests (PHP 8.0) (Symfony 6.0.*)', - 'integration-tests (PHP 8.1) (Symfony 6.1.*)', - 'integration-tests (PHP 8.2) (Symfony 6.2.*)', + 'integration-tests (PHP 8.1) (Symfony 6.4.*)', + 'integration-tests (PHP 8.2) (Symfony 6.4.*)', + 'integration-tests (PHP 8.3) (Symfony 6.4.*)', + 'integration-tests (PHP 8.2) (Symfony 7.0.*)', + 'integration-tests (PHP 8.2) (Symfony 7.1.*)', + 'integration-tests (PHP 8.3) (Symfony 7.0.*)', + 'integration-tests (PHP 8.3) (Symfony 7.1.*)', 'Code style' ] # 1 hour timeout diff --git a/composer.json b/composer.json index cea92a73..c04da0b6 100644 --- a/composer.json +++ b/composer.json @@ -20,29 +20,35 @@ "require": { "php": "^7.4|^8.0", "ext-json": "*", - "doctrine/doctrine-bundle": "^2.4", + "doctrine/doctrine-bundle": "^2.10", "meilisearch/meilisearch-php": "^1.0.0", - "symfony/filesystem": "^4.4 || ^5.0 || ^6.0", - "symfony/property-access": "^4.4 || ^5.0 || ^6.0", - "symfony/serializer": "^4.4 || ^5.0 || ^6.0" + "symfony/config": "^5.4 || ^6.0 || ^7.0", + "symfony/dependency-injection": "^5.4.17 || ^6.0 || ^7.0", + "symfony/event-dispatcher": "^5.4 || ^6.0 || ^7.0", + "symfony/http-kernel": "^5.4 || ^6.0 || ^7.0", + "symfony/polyfill-php80": "^1.27", + "symfony/property-access": "^5.4 || ^6.0 || ^7.0", + "symfony/serializer": "^5.4 || ^6.0 || ^7.0" }, "require-dev": { - "doctrine/annotations": "^2.0", - "doctrine/orm": "^2.9", - "phpmd/phpmd": "^2.13", - "matthiasnoback/symfony-dependency-injection-test": "^4.3", - "nyholm/psr7": "^1.5.1", - "php-cs-fixer/shim": "^3.14", - "phpstan/extension-installer": "^1.2", - "phpstan/phpstan": "^1.10.6", - "phpstan/phpstan-doctrine": "^1.3.33", - "phpstan/phpstan-phpunit": "^1.3.10", - "phpstan/phpstan-symfony": "^1.2.23", - "phpunit/php-code-coverage": "^9.2.26", - "symfony/doctrine-bridge": "^4.4 || ^5.0 || ^6.0", - "symfony/http-client": "^4.4 || ^5.0 || ^6.0", - "symfony/phpunit-bridge": "^4.4 || ^5.0 || ^6.0", - "symfony/yaml": "^4.4 || ^5.0 || ^6.0" + "doctrine/annotations": "^2.0.0", + "doctrine/orm": "^2.12 || ^3.0", + "matthiasnoback/symfony-dependency-injection-test": "^4.3 || ^5.0", + "nyholm/psr7": "^1.8.1", + "php-cs-fixer/shim": "^3.58.1", + "phpmd/phpmd": "^2.15", + "phpstan/extension-installer": "^1.4.1", + "phpstan/phpstan": "^1.11.4", + "phpstan/phpstan-doctrine": "^1.4.3", + "phpstan/phpstan-phpunit": "^1.4.0", + "phpstan/phpstan-symfony": "^1.4.4", + "phpunit/php-code-coverage": "^9.2.31", + "symfony/doctrine-bridge": "^5.4.19 || ^6.0.7 || ^7.0", + "symfony/filesystem": "^5.4 || ^6.0 || ^7.0", + "symfony/framework-bundle": "^5.4.17 || ^6.0 || ^7.0", + "symfony/http-client": "^5.4 || ^6.0 || ^7.0", + "symfony/phpunit-bridge": "^6.4 || ^7.0", + "symfony/yaml": "^5.4 || ^6.0 || ^7.0" }, "autoload": { "psr-4": { @@ -63,7 +69,7 @@ }, "scripts": { "phpmd": "./vendor/bin/phpmd src text phpmd.xml", - "phpstan": "./vendor/bin/phpstan --memory-limit=1G --ansi", + "phpstan": "./vendor/bin/phpstan", "test:unit": "SYMFONY_DEPRECATIONS_HELPER='ignoreFile=./tests/baseline-ignore' ./vendor/bin/simple-phpunit --colors=always --verbose", "test:unit:coverage": "SYMFONY_DEPRECATIONS_HELPER='ignoreFile=./tests/baseline-ignore' XDEBUG_MODE=coverage ./vendor/bin/simple-phpunit --colors=always --coverage-html=tests/coverage", "lint:check": "./vendor/bin/php-cs-fixer fix -v --using-cache=no --dry-run", diff --git a/config/services.xml b/config/services.xml index cf25ac1e..414cfb31 100644 --- a/config/services.xml +++ b/config/services.xml @@ -4,13 +4,22 @@ xsi:schemaLocation="http://symfony.com/schema/dic/services https://symfony.com/schema/dic/services/services-1.0.xsd"> - + + + - + + + + + + + + + The "%alias_id%" service alias is deprecated. Use "meilisearch.service" instead. + - + @@ -18,12 +27,12 @@ - %meili_url% - %meili_api_key% - - - %meili_symfony_version% - + + + + null + + null The "%alias_id%" service alias is deprecated. Use "meilisearch.client" instead. @@ -34,5 +43,43 @@ The "%alias_id%" service alias is deprecated. Use "meilisearch.client" instead. + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/phpstan.neon b/phpstan.neon.dist similarity index 50% rename from phpstan.neon rename to phpstan.neon.dist index 5246556d..28e5679a 100644 --- a/phpstan.neon +++ b/phpstan.neon.dist @@ -5,3 +5,5 @@ parameters: paths: - src - tests + ignoreErrors: + - '#Call to static method getClass\(\) on an unknown class Doctrine\\Common\\Util\\ClassUtils#' diff --git a/phpunit.xml.dist b/phpunit.xml.dist index 25bcf37c..d50518cb 100644 --- a/phpunit.xml.dist +++ b/phpunit.xml.dist @@ -14,6 +14,7 @@ + diff --git a/src/Collection.php b/src/Collection.php index e3c0569d..c9a90651 100644 --- a/src/Collection.php +++ b/src/Collection.php @@ -65,7 +65,7 @@ public function map(callable $callback) * * @return static */ - public function filter(callable $callback = null) + public function filter(?callable $callback = null) { if (null !== $callback) { return new self(array_filter($this->items, $callback, ARRAY_FILTER_USE_BOTH)); @@ -143,7 +143,7 @@ public function unique($key = null, bool $strict = false) /** * Get the first item from the collection passing the given truth test. */ - public function first(callable $callback = null, $default = null) + public function first(?callable $callback = null, $default = null) { if (is_null($callback)) { if (empty($this->items)) { @@ -339,7 +339,7 @@ private static function existsInArray($array, $key): bool return array_key_exists($key, $array); } - private function operatorForWhere(string $key, string $operator = null, $value = null): \Closure + private function operatorForWhere(string $key, ?string $operator = null, $value = null): \Closure { if (1 === func_num_args()) { $value = true; diff --git a/src/Command/IndexCommand.php b/src/Command/IndexCommand.php index 5b54d373..e9f37d2b 100644 --- a/src/Command/IndexCommand.php +++ b/src/Command/IndexCommand.php @@ -10,14 +10,14 @@ use Symfony\Component\Console\Input\InputInterface; use Symfony\Component\Console\Output\OutputInterface; -/** - * Class IndexCommand. - */ abstract class IndexCommand extends Command { - private string $prefix; + protected const DEFAULT_RESPONSE_TIMEOUT = 5000; + protected SearchService $searchService; + private string $prefix; + public function __construct(SearchService $searchService) { $this->searchService = $searchService; diff --git a/src/Command/MeilisearchClearCommand.php b/src/Command/MeilisearchClearCommand.php index ffbb465d..54a35caa 100644 --- a/src/Command/MeilisearchClearCommand.php +++ b/src/Command/MeilisearchClearCommand.php @@ -8,14 +8,11 @@ use Symfony\Component\Console\Input\InputOption; use Symfony\Component\Console\Output\OutputInterface; -/** - * Class MeilisearchClearCommand. - */ final class MeilisearchClearCommand extends IndexCommand { public static function getDefaultName(): string { - return 'meili:clear'; + return 'meilisearch:clear|meili:clear'; } public static function getDefaultDescription(): string diff --git a/src/Command/MeilisearchCreateCommand.php b/src/Command/MeilisearchCreateCommand.php index d2c32157..7ec47a1b 100644 --- a/src/Command/MeilisearchCreateCommand.php +++ b/src/Command/MeilisearchCreateCommand.php @@ -5,30 +5,35 @@ namespace Meilisearch\Bundle\Command; use Meilisearch\Bundle\Collection; -use Meilisearch\Bundle\Exception\InvalidSettingName; -use Meilisearch\Bundle\Exception\TaskException; +use Meilisearch\Bundle\EventListener\ConsoleOutputSubscriber; use Meilisearch\Bundle\Model\Aggregator; use Meilisearch\Bundle\SearchService; -use Meilisearch\Bundle\SettingsProvider; +use Meilisearch\Bundle\Services\SettingsUpdater; use Meilisearch\Client; use Symfony\Component\Console\Input\InputInterface; use Symfony\Component\Console\Input\InputOption; use Symfony\Component\Console\Output\OutputInterface; +use Symfony\Component\Console\Style\SymfonyStyle; +use Symfony\Component\EventDispatcher\EventDispatcherInterface; final class MeilisearchCreateCommand extends IndexCommand { private Client $searchClient; + private SettingsUpdater $settingsUpdater; + private EventDispatcherInterface $eventDispatcher; - public function __construct(SearchService $searchService, Client $searchClient) + public function __construct(SearchService $searchService, Client $searchClient, SettingsUpdater $settingsUpdater, EventDispatcherInterface $eventDispatcher) { parent::__construct($searchService); $this->searchClient = $searchClient; + $this->settingsUpdater = $settingsUpdater; + $this->eventDispatcher = $eventDispatcher; } public static function getDefaultName(): string { - return 'meili:create'; + return 'meilisearch:create|meili:create'; } public static function getDefaultDescription(): string @@ -40,33 +45,32 @@ protected function configure(): void { $this ->setDescription(self::getDefaultDescription()) - ->addOption('indices', 'i', InputOption::VALUE_OPTIONAL, 'Comma-separated list of index names'); - } - - private function entitiesToIndex($indexes): array - { - foreach ($indexes as $key => $index) { - $entityClassName = $index['class']; - if (is_subclass_of($entityClassName, Aggregator::class)) { - $indexes->forget($key); - - $indexes = new Collection(array_merge( - $indexes->all(), - array_map( - static fn ($entity) => ['name' => $index['name'], 'class' => $entity], - $entityClassName::getEntities() - ) - )); - } - } - - return array_unique($indexes->all(), SORT_REGULAR); + ->addOption('indices', 'i', InputOption::VALUE_OPTIONAL, 'Comma-separated list of index names') + ->addOption( + 'update-settings', + null, + InputOption::VALUE_NEGATABLE, + 'Update settings related to indices to the search engine', + true + ) + ->addOption( + 'response-timeout', + 't', + InputOption::VALUE_REQUIRED, + 'Timeout (in ms) to get response from the search engine', + self::DEFAULT_RESPONSE_TIMEOUT + ) + ; } protected function execute(InputInterface $input, OutputInterface $output): int { + $this->eventDispatcher->addSubscriber(new ConsoleOutputSubscriber(new SymfonyStyle($input, $output))); + $indexes = $this->getEntitiesFromArgs($input, $output); $entitiesToIndex = $this->entitiesToIndex($indexes); + $updateSettings = $input->getOption('update-settings'); + $responseTimeout = ((int) $input->getOption('response-timeout')) ?: self::DEFAULT_RESPONSE_TIMEOUT; /** @var array $index */ foreach ($entitiesToIndex as $index) { @@ -79,39 +83,38 @@ protected function execute(InputInterface $input, OutputInterface $output): int $output->writeln('Creating index '.$index['name'].' for '.$entityClassName.''); $task = $this->searchClient->createIndex($index['name']); - $this->searchClient->waitForTask($task['taskUid']); - $indexInstance = $this->searchClient->index($index['name']); + $this->searchClient->waitForTask($task['taskUid'], $responseTimeout); - if (isset($index['settings']) && is_array($index['settings'])) { - foreach ($index['settings'] as $variable => $value) { - $method = sprintf('update%s', ucfirst($variable)); + if ($updateSettings) { + $this->settingsUpdater->update($index['name'], $responseTimeout); + } + } - if (!method_exists($indexInstance, $method)) { - throw new InvalidSettingName(sprintf('Invalid setting name: "%s"', $variable)); - } + $output->writeln('Done!'); - if (isset($value['_service']) && $value['_service'] instanceof SettingsProvider) { - $value = $value['_service'](); - } + return 0; + } - // Update - $task = $indexInstance->{$method}($value); + private function entitiesToIndex($indexes): array + { + foreach ($indexes as $key => $index) { + $entityClassName = $index['class']; - // Get task information using uid - $indexInstance->waitForTask($task['taskUid']); - $task = $indexInstance->getTask($task['taskUid']); + if (!is_subclass_of($entityClassName, Aggregator::class)) { + continue; + } - if ('failed' === $task['status']) { - throw new TaskException($task['error']); - } + $indexes->forget($key); - $output->writeln('Settings updated of "'.$index['name'].'".'); - } - } + $indexes = new Collection(array_merge( + $indexes->all(), + array_map( + static fn ($entity) => ['name' => $index['name'], 'prefixed_name' => $index['prefixed_name'], 'class' => $entity], + $entityClassName::getEntities() + ) + )); } - $output->writeln('Done!'); - - return 0; + return array_unique($indexes->all(), SORT_REGULAR); } } diff --git a/src/Command/MeilisearchDeleteCommand.php b/src/Command/MeilisearchDeleteCommand.php index a316ed19..04bffc27 100644 --- a/src/Command/MeilisearchDeleteCommand.php +++ b/src/Command/MeilisearchDeleteCommand.php @@ -10,14 +10,11 @@ use Symfony\Component\Console\Input\InputOption; use Symfony\Component\Console\Output\OutputInterface; -/** - * Class MeilisearchDeleteCommand. - */ final class MeilisearchDeleteCommand extends IndexCommand { public static function getDefaultName(): string { - return 'meili:delete'; + return 'meilisearch:delete|meili:delete'; } public static function getDefaultDescription(): string diff --git a/src/Command/MeilisearchImportCommand.php b/src/Command/MeilisearchImportCommand.php index 8bf1a2e7..fe0a0555 100644 --- a/src/Command/MeilisearchImportCommand.php +++ b/src/Command/MeilisearchImportCommand.php @@ -6,38 +6,39 @@ use Doctrine\Persistence\ManagerRegistry; use Meilisearch\Bundle\Collection; -use Meilisearch\Bundle\Exception\InvalidSettingName; +use Meilisearch\Bundle\EventListener\ConsoleOutputSubscriber; use Meilisearch\Bundle\Exception\TaskException; use Meilisearch\Bundle\Model\Aggregator; use Meilisearch\Bundle\SearchService; -use Meilisearch\Bundle\SettingsProvider; +use Meilisearch\Bundle\Services\SettingsUpdater; use Meilisearch\Client; use Meilisearch\Exceptions\TimeOutException; use Symfony\Component\Console\Input\InputInterface; use Symfony\Component\Console\Input\InputOption; use Symfony\Component\Console\Output\OutputInterface; +use Symfony\Component\Console\Style\SymfonyStyle; +use Symfony\Component\EventDispatcher\EventDispatcherInterface; -/** - * Class MeilisearchImportCommand. - */ final class MeilisearchImportCommand extends IndexCommand { - private const DEFAULT_RESPONSE_TIMEOUT = 5000; + private Client $searchClient; + private ManagerRegistry $managerRegistry; + private SettingsUpdater $settingsUpdater; + private EventDispatcherInterface $eventDispatcher; - protected Client $searchClient; - protected ManagerRegistry $managerRegistry; - - public function __construct(SearchService $searchService, ManagerRegistry $managerRegistry, Client $searchClient) + public function __construct(SearchService $searchService, ManagerRegistry $managerRegistry, Client $searchClient, SettingsUpdater $settingsUpdater, EventDispatcherInterface $eventDispatcher) { parent::__construct($searchService); $this->managerRegistry = $managerRegistry; $this->searchClient = $searchClient; + $this->settingsUpdater = $settingsUpdater; + $this->eventDispatcher = $eventDispatcher; } public static function getDefaultName(): string { - return 'meili:import'; + return 'meilisearch:import|meili:import'; } public static function getDefaultDescription(): string @@ -53,8 +54,9 @@ protected function configure(): void ->addOption( 'update-settings', null, - InputOption::VALUE_NONE, - 'Update settings related to indices to the search engine' + InputOption::VALUE_NEGATABLE, + 'Update settings related to indices to the search engine', + true ) ->addOption('batch-size', null, InputOption::VALUE_REQUIRED) ->addOption( @@ -76,25 +78,12 @@ protected function configure(): void protected function execute(InputInterface $input, OutputInterface $output): int { + $this->eventDispatcher->addSubscriber(new ConsoleOutputSubscriber(new SymfonyStyle($input, $output))); + $indexes = $this->getEntitiesFromArgs($input, $output); + $entitiesToIndex = $this->entitiesToIndex($indexes); $config = $this->searchService->getConfiguration(); - - foreach ($indexes as $key => $index) { - $entityClassName = $index['class']; - if (is_subclass_of($entityClassName, Aggregator::class)) { - $indexes->forget($key); - - $indexes = new Collection(array_merge( - $indexes->all(), - array_map( - fn ($entity) => ['class' => $entity], - $entityClassName::getEntities() - ) - )); - } - } - - $entitiesToIndex = array_unique($indexes->all(), SORT_REGULAR); + $updateSettings = $input->getOption('update-settings'); $batchSize = $input->getOption('batch-size') ?? ''; $batchSize = ctype_digit($batchSize) ? (int) $batchSize : $config->get('batchSize'); $responseTimeout = ((int) $input->getOption('response-timeout')) ?: self::DEFAULT_RESPONSE_TIMEOUT; @@ -102,6 +91,7 @@ protected function execute(InputInterface $input, OutputInterface $output): int /** @var array $index */ foreach ($entitiesToIndex as $index) { $entityClassName = $index['class']; + if (!$this->searchService->isSearchable($entityClassName)) { continue; } @@ -151,34 +141,8 @@ protected function execute(InputInterface $input, OutputInterface $output): int ); } - if (isset($index['settings']) - && is_array($index['settings']) - && count($index['settings']) > 0) { - $indexInstance = $this->searchClient->index($index['name']); - foreach ($index['settings'] as $variable => $value) { - $method = sprintf('update%s', ucfirst($variable)); - - if (!method_exists($indexInstance, $method)) { - throw new InvalidSettingName(sprintf('Invalid setting name: "%s"', $variable)); - } - - if (isset($value['_service']) && $value['_service'] instanceof SettingsProvider) { - $value = $value['_service'](); - } - - // Update - $task = $indexInstance->{$method}($value); - - // Get task information using uid - $indexInstance->waitForTask($task['taskUid'], $responseTimeout); - $task = $indexInstance->getTask($task['taskUid']); - - if ('failed' === $task['status']) { - throw new TaskException($task['error']); - } - - $output->writeln('Settings updated of "'.$index['name'].'".'); - } + if ($updateSettings) { + $this->settingsUpdater->update($index['prefixed_name'], $responseTimeout); } ++$page; @@ -221,4 +185,27 @@ private function formatIndexingResponse(array $batch, int $responseTimeout): arr return $formattedResponse; } + + private function entitiesToIndex($indexes): array + { + foreach ($indexes as $key => $index) { + $entityClassName = $index['class']; + + if (!is_subclass_of($entityClassName, Aggregator::class)) { + continue; + } + + $indexes->forget($key); + + $indexes = new Collection(array_merge( + $indexes->all(), + array_map( + static fn ($entity) => ['name' => $index['name'], 'prefixed_name' => $index['prefixed_name'], 'class' => $entity], + $entityClassName::getEntities() + ) + )); + } + + return array_unique($indexes->all(), SORT_REGULAR); + } } diff --git a/src/DataCollector/MeilisearchDataCollector.php b/src/DataCollector/MeilisearchDataCollector.php new file mode 100644 index 00000000..73c8fb3f --- /dev/null +++ b/src/DataCollector/MeilisearchDataCollector.php @@ -0,0 +1,40 @@ + + */ +final class MeilisearchDataCollector extends AbstractDataCollector +{ + private TraceableMeilisearchService $meilisearchService; + + public function __construct(TraceableMeilisearchService $meilisearchService) + { + $this->meilisearchService = $meilisearchService; + } + public function collect(Request $request, Response $response, \Throwable $exception = null): void + { + $data = $this->meilisearchService->getData(); + + $this->data[$this->getName()] = !empty($data) ? $this->cloneVar($data) : null; + } + + public function getName(): string + { + return 'meilisearch'; + } + + /** @internal used in the DataCollector view template */ + public function getMeilisearch(): mixed + { + return $this->data[$this->getName()] ?? null; + } +} diff --git a/src/Debug/TraceableMeilisearchService.php b/src/Debug/TraceableMeilisearchService.php new file mode 100644 index 00000000..f23875b2 --- /dev/null +++ b/src/Debug/TraceableMeilisearchService.php @@ -0,0 +1,110 @@ + + */ +final class TraceableMeilisearchService implements SearchService +{ + private SearchService $searchService; + private Stopwatch $stopwatch; + private array $data = []; + + public function __construct(SearchService $searchService, Stopwatch $stopwatch) + { + $this->searchService = $searchService; + $this->stopwatch = $stopwatch; + } + + public function index(ObjectManager $objectManager, $searchable): array + { + return $this->innerSearchService(__FUNCTION__, \func_get_args()); + } + + public function remove(ObjectManager $objectManager, $searchable): array + { + return $this->innerSearchService(__FUNCTION__, \func_get_args()); + } + + public function clear(string $className): array + { + return $this->innerSearchService(__FUNCTION__, \func_get_args()); + } + + public function deleteByIndexName(string $indexName): ?array + { + return $this->innerSearchService(__FUNCTION__, \func_get_args()); + } + + public function delete(string $className): ?array + { + return $this->innerSearchService(__FUNCTION__, \func_get_args()); + } + + public function search(ObjectManager $objectManager, string $className, string $query = '', array $searchParams = []): array + { + return $this->innerSearchService(__FUNCTION__, \func_get_args()); + } + + public function rawSearch(string $className, string $query = '', array $searchParams = []): array + { + return $this->innerSearchService(__FUNCTION__, \func_get_args()); + } + + public function count(string $className, string $query = '', array $searchParams = []): int + { + return $this->innerSearchService(__FUNCTION__, \func_get_args()); + } + + public function isSearchable($className): bool + { + return $this->searchService->isSearchable($className); + } + + public function getSearchable(): array + { + return $this->searchService->getSearchable(); + } + + public function getConfiguration(): Collection + { + return $this->searchService->getConfiguration(); + } + + public function searchableAs(string $className): string + { + return $this->searchService->searchableAs($className); + } + + /** @internal used in the DataCollector class */ + public function getData(): array + { + return $this->data; + } + + private function innerSearchService(string $function, array $args): mixed + { + $this->stopwatch->start($function); + + $result = $this->searchService->{$function}(...$args); + + $event = $this->stopwatch->stop($function); + + $this->data[$function] = [ + '_params' => $args, + '_results' => $result, + '_duration' => $event->getDuration(), + '_memory' => $event->getMemory(), + ]; + + return $result; + } +} diff --git a/src/DependencyInjection/Configuration.php b/src/DependencyInjection/Configuration.php index 4999bfb8..83464d59 100644 --- a/src/DependencyInjection/Configuration.php +++ b/src/DependencyInjection/Configuration.php @@ -17,15 +17,15 @@ public function getConfigTreeBuilder(): TreeBuilder $rootNode ->children() - ->scalarNode('url')->end() - ->scalarNode('api_key')->end() + ->scalarNode('url')->defaultValue('http://localhost:7700')->end() + ->scalarNode('api_key')->defaultNull()->end() ->scalarNode('prefix') - ->defaultValue(null) + ->defaultNull() ->end() - ->scalarNode('nbResults') + ->integerNode('nbResults') ->defaultValue(20) ->end() - ->scalarNode('batchSize') + ->integerNode('batchSize') ->defaultValue(500) ->end() ->arrayNode('doctrineSubscribedEvents') @@ -60,7 +60,21 @@ public function getConfigTreeBuilder(): TreeBuilder ->defaultNull() ->end() ->arrayNode('settings') - ->info('Configure indices settings, see: https://docs.meilisearch.com/guides/advanced_guides/settings.html') + ->info('Configure indices settings, see: https://www.meilisearch.com/docs/reference/api/settings') + ->beforeNormalization() + ->always() + ->then(static function (array $value) { + $stringSettings = ['distinctAttribute', 'proximityPrecision', 'searchCutoffMs']; + + foreach ($stringSettings as $setting) { + if (isset($value[$setting]) && !is_array($value[$setting])) { + $value[$setting] = (array) $value[$setting]; + } + } + + return $value; + }) + ->end() ->arrayPrototype() ->variablePrototype()->end() ->end() diff --git a/src/DependencyInjection/MeilisearchExtension.php b/src/DependencyInjection/MeilisearchExtension.php index bc07379a..002977d0 100644 --- a/src/DependencyInjection/MeilisearchExtension.php +++ b/src/DependencyInjection/MeilisearchExtension.php @@ -4,19 +4,18 @@ namespace Meilisearch\Bundle\DependencyInjection; -use Meilisearch\Bundle\Engine; +use Meilisearch\Bundle\DataCollector\MeilisearchDataCollector; +use Meilisearch\Bundle\Debug\TraceableMeilisearchService; use Meilisearch\Bundle\MeilisearchBundle; -use Meilisearch\Bundle\Services\MeilisearchService; +use Meilisearch\Bundle\SearchService; +use Meilisearch\Bundle\Services\UnixTimestampNormalizer; use Symfony\Component\Config\FileLocator; use Symfony\Component\DependencyInjection\ContainerBuilder; -use Symfony\Component\DependencyInjection\Definition; +use Symfony\Component\DependencyInjection\Extension\Extension; use Symfony\Component\DependencyInjection\Loader; use Symfony\Component\DependencyInjection\Reference; -use Symfony\Component\HttpKernel\DependencyInjection\Extension; +use Symfony\Component\HttpKernel\Kernel; -/** - * Class MeilisearchExtension. - */ final class MeilisearchExtension extends Extension { public function load(array $configs, ContainerBuilder $container): void @@ -32,6 +31,7 @@ public function load(array $configs, ContainerBuilder $container): void } foreach ($config['indices'] as $index => $indice) { + $config['indices'][$index]['prefixed_name'] = $config['prefix'].$indice['name']; $config['indices'][$index]['settings'] = $this->findReferences($config['indices'][$index]['settings']); } @@ -50,15 +50,31 @@ public function load(array $configs, ContainerBuilder $container): void $container->removeDefinition('meilisearch.search_indexer_subscriber'); } - $engineDefinition = new Definition(Engine::class, [new Reference('meilisearch.client')]); + $container->findDefinition('meilisearch.client') + ->replaceArgument(0, $config['url']) + ->replaceArgument(1, $config['api_key']) + ->replaceArgument(4, [MeilisearchBundle::qualifiedVersion()]); - $searchDefinition = (new Definition( - MeilisearchService::class, - [new Reference($config['serializer']), $engineDefinition, $config] - )); + $container->findDefinition('meilisearch.service') + ->replaceArgument(0, new Reference($config['serializer'])) + ->replaceArgument(2, $config); - $container->setDefinition('meilisearch.service', $searchDefinition->setPublic(true)); - $container->setAlias('search.service', 'meilisearch.service')->setPublic(true); + if ($container->getParameter('kernel.debug')) { + $container->register('debug.meilisearch.service', TraceableMeilisearchService::class) + ->setDecoratedService(SearchService::class) + ->addArgument(new Reference('debug.meilisearch.service.inner')) + ->addArgument(new Reference('debug.stopwatch')); + $container->register('data_collector.meilisearch', MeilisearchDataCollector::class) + ->addArgument(new Reference('debug.meilisearch.service')) + ->addTag('data_collector', [ + 'id' => 'meilisearch', + 'template' => '@Meilisearch/DataCollector/meilisearch.html.twig', + ]); + } + + if (Kernel::VERSION_ID >= 70100) { + $container->removeDefinition(UnixTimestampNormalizer::class); + } } /** diff --git a/src/Document/Aggregator.php b/src/Document/Aggregator.php index 0f45cdf7..336e3bcb 100644 --- a/src/Document/Aggregator.php +++ b/src/Document/Aggregator.php @@ -6,9 +6,6 @@ use Meilisearch\Bundle\Model\Aggregator as BaseAggregator; -/** - * Class Aggregator. - */ abstract class Aggregator extends BaseAggregator { } diff --git a/src/Engine.php b/src/Engine.php index 9fb960ed..1bd47662 100644 --- a/src/Engine.php +++ b/src/Engine.php @@ -7,9 +7,6 @@ use Meilisearch\Client; use Meilisearch\Exceptions\ApiException; -/** - * Class Engine. - */ final class Engine { private Client $client; @@ -104,7 +101,7 @@ public function remove($searchableEntities): array /** * Clear the records of an index. * This method enables you to delete an index’s contents (records). - * Will fail if the index does not exists. + * Will fail if the index does not exist. * * @throws ApiException */ diff --git a/src/Entity/Aggregator.php b/src/Entity/Aggregator.php index 0d89d70a..6bad4094 100644 --- a/src/Entity/Aggregator.php +++ b/src/Entity/Aggregator.php @@ -6,9 +6,6 @@ use Meilisearch\Bundle\Model\Aggregator as BaseAggregator; -/** - * Class Aggregator. - */ abstract class Aggregator extends BaseAggregator { } diff --git a/src/Event/SettingsUpdatedEvent.php b/src/Event/SettingsUpdatedEvent.php new file mode 100644 index 00000000..0c77670c --- /dev/null +++ b/src/Event/SettingsUpdatedEvent.php @@ -0,0 +1,46 @@ +index = $index; + $this->class = $class; + } + + /** + * @return class-string + */ + public function getClass(): string + { + return $this->class; + } + + /** + * @return non-empty-string + */ + public function getIndex(): string + { + return $this->index; + } +} diff --git a/src/EventListener/ConsoleOutputSubscriber.php b/src/EventListener/ConsoleOutputSubscriber.php new file mode 100644 index 00000000..5aaf51f7 --- /dev/null +++ b/src/EventListener/ConsoleOutputSubscriber.php @@ -0,0 +1,31 @@ +io = $io; + } + + public function afterSettingsUpdate(SettingsUpdatedEvent $event): void + { + $this->io->writeln('Settings updated of "'.$event->getIndex().'".'); + } + + public static function getSubscribedEvents(): array + { + return [ + SettingsUpdatedEvent::class => 'afterSettingsUpdate', + ]; + } +} diff --git a/src/Exception/EntityNotFoundInObjectID.php b/src/Exception/EntityNotFoundInObjectID.php index f8081660..d9e1b16a 100644 --- a/src/Exception/EntityNotFoundInObjectID.php +++ b/src/Exception/EntityNotFoundInObjectID.php @@ -4,9 +4,6 @@ namespace Meilisearch\Bundle\Exception; -/** - * Class EntityNotFoundInObjectID. - */ final class EntityNotFoundInObjectID extends \LogicException { } diff --git a/src/Exception/InvalidIndiceException.php b/src/Exception/InvalidIndiceException.php new file mode 100644 index 00000000..a7ce9aef --- /dev/null +++ b/src/Exception/InvalidIndiceException.php @@ -0,0 +1,13 @@ + $this->objectID], $normalizer->normalize($this->entity, $format, $context)); } diff --git a/src/SearchService.php b/src/SearchService.php index 8e5e023b..9fbdcfef 100644 --- a/src/SearchService.php +++ b/src/SearchService.php @@ -6,9 +6,6 @@ use Doctrine\Persistence\ObjectManager; -/** - * Interface SearchService. - */ interface SearchService { public const RESULT_KEY_HITS = 'hits'; diff --git a/src/Searchable.php b/src/Searchable.php index 47e4dd35..b4e3da0a 100644 --- a/src/Searchable.php +++ b/src/Searchable.php @@ -4,9 +4,6 @@ namespace Meilisearch\Bundle; -/** - * Class Searchable. - */ final class Searchable { public const NORMALIZATION_FORMAT = 'searchableArray'; diff --git a/src/SearchableEntity.php b/src/SearchableEntity.php index 85f77048..7b072d55 100644 --- a/src/SearchableEntity.php +++ b/src/SearchableEntity.php @@ -6,13 +6,12 @@ use Doctrine\ORM\Mapping\ClassMetadata; use Symfony\Component\Config\Definition\Exception\Exception; +use Symfony\Component\HttpKernel\Kernel; use Symfony\Component\Serializer\Exception\ExceptionInterface; +use Symfony\Component\Serializer\Normalizer\DateTimeNormalizer; use Symfony\Component\Serializer\Normalizer\NormalizableInterface; use Symfony\Component\Serializer\Normalizer\NormalizerInterface; -/** - * Class SearchableEntity. - */ final class SearchableEntity { private string $indexUid; @@ -34,8 +33,6 @@ final class SearchableEntity private $id; /** - * SearchableEntity constructor. - * * @param object $entity * @param ClassMetadata $entityMetadata */ @@ -43,7 +40,7 @@ public function __construct( string $indexUid, $entity, ClassMetadata $entityMetadata, - NormalizerInterface $normalizer = null, + ?NormalizerInterface $normalizer = null, array $extra = [] ) { $this->indexUid = $indexUid; @@ -66,6 +63,7 @@ public function getIndexUid(): string public function getSearchableArray(): array { $context = [ + 'meilisearch' => true, 'fieldsMapping' => $this->entityMetadata->fieldMappings, ]; @@ -73,6 +71,11 @@ public function getSearchableArray(): array $context['groups'] = $this->normalizationGroups; } + if (Kernel::VERSION_ID >= 70100) { + $context[DateTimeNormalizer::FORMAT_KEY] = 'U'; + $context[DateTimeNormalizer::CAST_KEY] = 'int'; + } + if ($this->entity instanceof NormalizableInterface && null !== $this->normalizer) { return $this->entity->normalize($this->normalizer, Searchable::NORMALIZATION_FORMAT, $context); } diff --git a/src/Services/MeilisearchService.php b/src/Services/MeilisearchService.php index 43e8d079..c899e212 100644 --- a/src/Services/MeilisearchService.php +++ b/src/Services/MeilisearchService.php @@ -5,6 +5,7 @@ namespace Meilisearch\Bundle\Services; use Doctrine\Common\Util\ClassUtils; +use Doctrine\ORM\Proxy\DefaultProxyClassNameResolver; use Doctrine\Persistence\ObjectManager; use Meilisearch\Bundle\Collection; use Meilisearch\Bundle\Engine; @@ -15,18 +16,15 @@ use Meilisearch\Bundle\SearchService; use Symfony\Component\Config\Definition\Exception\Exception; use Symfony\Component\PropertyAccess\PropertyAccess; -use Symfony\Component\PropertyAccess\PropertyAccessor; +use Symfony\Component\PropertyAccess\PropertyAccessorInterface; use Symfony\Component\Serializer\Normalizer\NormalizerInterface; -/** - * Class MeilisearchService. - */ final class MeilisearchService implements SearchService { private NormalizerInterface $normalizer; private Engine $engine; private Collection $configuration; - private PropertyAccessor $propertyAccessor; + private PropertyAccessorInterface $propertyAccessor; /** * @var list */ @@ -45,12 +43,12 @@ final class MeilisearchService implements SearchService private array $classToSerializerGroup; private array $indexIfMapping; - public function __construct(NormalizerInterface $normalizer, Engine $engine, array $configuration) + public function __construct(NormalizerInterface $normalizer, Engine $engine, array $configuration, ?PropertyAccessorInterface $propertyAccessor = null) { $this->normalizer = $normalizer; $this->engine = $engine; $this->configuration = new Collection($configuration); - $this->propertyAccessor = PropertyAccess::createPropertyAccessor(); + $this->propertyAccessor = $propertyAccessor ?? PropertyAccess::createPropertyAccessor(); $this->setSearchableEntities(); $this->setAggregatorsAndEntitiesAggregators(); @@ -60,9 +58,7 @@ public function __construct(NormalizerInterface $normalizer, Engine $engine, arr public function isSearchable($className): bool { - if (is_object($className)) { - $className = ClassUtils::getClass($className); - } + $className = $this->getBaseClassName($className); return in_array($className, $this->searchableEntities, true); } @@ -79,6 +75,8 @@ public function getConfiguration(): Collection public function searchableAs(string $className): string { + $className = $this->getBaseClassName($className); + $indexes = new Collection($this->getConfiguration()->get('indices')); $index = $indexes->firstWhere('class', $className); @@ -158,7 +156,7 @@ public function search( ): array { $this->assertIsSearchable($className); - $ids = $this->engine->search($query, $this->searchableAs($className), $searchParams); + $ids = $this->engine->search($query, $this->searchableAs($className), $searchParams + ['limit' => $this->configuration['nbResults']]); $results = []; // Check if the engine returns results in "hits" key @@ -210,7 +208,8 @@ public function count(string $className, string $query = '', array $searchParams public function shouldBeIndexed(object $entity): bool { - $className = ClassUtils::getClass($entity); + $className = $this->getBaseClassName($entity); + $propertyPath = $this->indexIfMapping[$className]; if (null !== $propertyPath) { @@ -224,6 +223,26 @@ public function shouldBeIndexed(object $entity): bool return true; } + /** + * @param object|class-string $objectOrClass + * + * @return class-string + */ + private function getBaseClassName($objectOrClass): string + { + foreach ($this->searchableEntities as $class) { + if (is_a($objectOrClass, $class, true)) { + return $class; + } + } + + if (is_object($objectOrClass)) { + return self::resolveClass($objectOrClass); + } + + return $objectOrClass; + } + private function setSearchableEntities(): void { $searchable = []; @@ -288,7 +307,7 @@ private function getAggregatorsFromEntities(ObjectManager $objectManager, array $aggregators = []; foreach ($entities as $entity) { - $entityClassName = ClassUtils::getClass($entity); + $entityClassName = self::resolveClass($entity); if (array_key_exists($entityClassName, $this->entitiesAggregators)) { foreach ($this->entitiesAggregators[$entityClassName] as $aggregator) { $aggregators[] = new $aggregator( @@ -316,7 +335,7 @@ private function makeSearchServiceResponseFrom( foreach (array_chunk($entities, $this->configuration->get('batchSize')) as $chunk) { $searchableEntitiesChunk = []; foreach ($chunk as $entity) { - $entityClassName = ClassUtils::getClass($entity); + $entityClassName = $this->getBaseClassName($entity); $searchableEntitiesChunk[] = new SearchableEntity( $this->searchableAs($entityClassName), @@ -349,4 +368,21 @@ private function assertIsSearchable(string $className): void throw new Exception('Class '.$className.' is not searchable.'); } } + + private static function resolveClass(object $object): string + { + static $resolver; + + $resolver ??= (function () { + // Doctrine ORM v3+ compatibility + if (\class_exists(DefaultProxyClassNameResolver::class)) { + return fn (object $object) => DefaultProxyClassNameResolver::getClass($object); + } + + // Legacy Doctrine ORM compatibility + return fn (object $object) => ClassUtils::getClass($object); // @codeCoverageIgnore + })(); + + return $resolver($object); + } } diff --git a/src/Services/SettingsUpdater.php b/src/Services/SettingsUpdater.php new file mode 100644 index 00000000..c027d2ba --- /dev/null +++ b/src/Services/SettingsUpdater.php @@ -0,0 +1,79 @@ +searchClient = $searchClient; + $this->eventDispatcher = $eventDispatcher; + $this->configuration = $searchService->getConfiguration(); + } + + /** + * @param non-empty-string $indice + * @param positive-int|null $responseTimeout + */ + public function update(string $indice, ?int $responseTimeout = null): void + { + $index = (new Collection($this->configuration->get('indices')))->firstWhere('prefixed_name', $indice); + + if (!is_array($index)) { + throw new InvalidIndiceException($indice); + } + + if (!is_array($index['settings'] ?? null) || [] === $index['settings']) { + return; + } + + $indexName = $index['prefixed_name']; + $indexInstance = $this->searchClient->index($indexName); + $responseTimeout = $responseTimeout ?? self::DEFAULT_RESPONSE_TIMEOUT; + + foreach ($index['settings'] as $variable => $value) { + $method = sprintf('update%s', ucfirst($variable)); + + if (!method_exists($indexInstance, $method)) { + throw new InvalidSettingName(sprintf('Invalid setting name: "%s"', $variable)); + } + + if (isset($value['_service']) && $value['_service'] instanceof SettingsProvider) { + $value = $value['_service'](); + } elseif (('distinctAttribute' === $variable || 'proximityPrecision' === $variable || 'searchCutoffMs' === $variable) && is_array($value)) { + $value = $value[0] ?? null; + } + + // Update + $task = $indexInstance->{$method}($value); + + // Get task information using uid + $indexInstance->waitForTask($task['taskUid'], $responseTimeout); + $task = $indexInstance->getTask($task['taskUid']); + + if ('failed' === $task['status']) { + throw new TaskException($task['error']); + } + + $this->eventDispatcher->dispatch(new SettingsUpdatedEvent($index['class'], $indexName)); + } + } +} diff --git a/src/Services/UnixTimestampNormalizer.php b/src/Services/UnixTimestampNormalizer.php new file mode 100644 index 00000000..93b527ae --- /dev/null +++ b/src/Services/UnixTimestampNormalizer.php @@ -0,0 +1,33 @@ +getTimestamp(); + } + + /** + * @param mixed $data + */ + public function supportsNormalization($data, ?string $format = null, array $context = []): bool + { + return $data instanceof \DateTimeInterface && true === ($context['meilisearch'] ?? null); + } + + public function getSupportedTypes(?string $format): array + { + return [ + \DateTimeInterface::class => true, // @codeCoverageIgnore + ]; + } +} diff --git a/templates/DataCollector/meilisearch.html.twig b/templates/DataCollector/meilisearch.html.twig new file mode 100644 index 00000000..fd5a1597 --- /dev/null +++ b/templates/DataCollector/meilisearch.html.twig @@ -0,0 +1,79 @@ +{% extends '@WebProfiler/Profiler/layout.html.twig' %} + +{% block toolbar %} + {% set _data = collector.meilisearch %} + {% set icon %} + {{ include('@Meilisearch/DataCollector/meilisearch.svg') }} + {# @todo: change class if _data is not null #} + Meilisearch + {% endset %} + {{ include('@WebProfiler/Profiler/toolbar_item.html.twig', { link: 'meilisearch' }) }} +{% endblock %} + +{% block menu %} + {% set _data = collector.meilisearch %} + + + {{ include('@Meilisearch/DataCollector/meilisearch.svg') }} + + Meilisearch + +{% endblock %} + +{% block panel %} +

Meilisearch

+ {% set _data = collector.meilisearch %} + {% if _data is not null %} +
+ {% for _name, _d in _data %} +
+

{{ _name|title }}

+
+
+
+ {{ '%.0f'|format(_d._duration) }} ms + Total execution time +
+
+ {{ '%.2f'|format(_d._memory / 1024 / 1024) }} MB + Peak memory usage +
+
+

Params

+ + {% for _pk, _pv in _d._params %} + + + + + {% endfor %} +
{{ _pk }}{{ profiler_dump(_pv) }}
+

Result

+ + {% for _rk, _rv in _d._results %} + + + + + {% endfor %} +
{{ _rk }}{{ profiler_dump(_rv) }}
+
+
+ {% endfor %} +
+

+ You can also see the HTTP Client & Performance{% if constant('Symfony\\Component\\HttpKernel\\Kernel::VERSION') >= '6.1.0' %} & Serializer{% endif %} tabs to learn more. +

+ {% else %} +
+

No Meilisearch data collected.

+
+ {% endif %} +

+ Read Meilisearch documentation +
+ Read Meilisearch PHP SDK documentation +
+ Read Meilisearch Symfony bundle documentation +

+{% endblock %} diff --git a/templates/DataCollector/meilisearch.svg b/templates/DataCollector/meilisearch.svg new file mode 100644 index 00000000..564dd22f --- /dev/null +++ b/templates/DataCollector/meilisearch.svg @@ -0,0 +1,23 @@ + + + Meilisearch + + + + + + + + + + + + + + + + + + + + diff --git a/tests/BaseKernelTestCase.php b/tests/BaseKernelTestCase.php index be12e70c..3d0ca34a 100644 --- a/tests/BaseKernelTestCase.php +++ b/tests/BaseKernelTestCase.php @@ -9,19 +9,23 @@ use Meilisearch\Bundle\Collection; use Meilisearch\Bundle\SearchableEntity; use Meilisearch\Bundle\SearchService; +use Meilisearch\Bundle\Tests\Entity\Article; use Meilisearch\Bundle\Tests\Entity\Comment; use Meilisearch\Bundle\Tests\Entity\Image; use Meilisearch\Bundle\Tests\Entity\Link; use Meilisearch\Bundle\Tests\Entity\ObjectId\DummyObjectId; use Meilisearch\Bundle\Tests\Entity\Page; +use Meilisearch\Bundle\Tests\Entity\Podcast; use Meilisearch\Bundle\Tests\Entity\Post; use Meilisearch\Bundle\Tests\Entity\Tag; +use Meilisearch\Client; use Meilisearch\Exceptions\ApiException; use Symfony\Bundle\FrameworkBundle\Test\KernelTestCase; abstract class BaseKernelTestCase extends KernelTestCase { protected EntityManagerInterface $entityManager; + protected Client $client; protected SearchService $searchService; protected function setUp(): void @@ -29,6 +33,7 @@ protected function setUp(): void self::bootKernel(); $this->entityManager = $this->get('doctrine.orm.entity_manager'); + $this->client = $this->get('meilisearch.client'); $this->searchService = $this->get('meilisearch.service'); $metaData = $this->entityManager->getMetadataFactory()->getAllMetadata(); @@ -39,10 +44,7 @@ protected function setUp(): void $this->cleanUp(); } - /** - * @param int|string|null $id - */ - protected function createPost($id = null): Post + protected function createPost(?int $id = null): Post { $post = new Post(); $post->setTitle('Test Post'); @@ -83,10 +85,7 @@ protected function createSearchablePost(): SearchableEntity ); } - /** - * @param int|string|null $id - */ - protected function createComment($id = null): Comment + protected function createComment(?int $id = null): Comment { $post = new Post(['title' => 'What a post!']); $comment = new Comment(); @@ -104,10 +103,7 @@ protected function createComment($id = null): Comment return $comment; } - /** - * @param int|string|null $id - */ - protected function createImage($id = null): Image + protected function createImage(?int $id = null): Image { $image = new Image(); $image->setUrl('https://docs.meilisearch.com/logo.png'); @@ -122,6 +118,34 @@ protected function createImage($id = null): Image return $image; } + protected function createArticle(?int $id = null): Article + { + $article = new Article(); + $article->setTitle('Test Article'); + if (null !== $id) { + $article->setId($id); + } + + $this->entityManager->persist($article); + $this->entityManager->flush(); + + return $article; + } + + protected function createPodcast(?int $id = null): Podcast + { + $podcast = new Podcast(); + $podcast->setTitle('Test Podcast'); + if (null !== $id) { + $podcast->setId($id); + } + + $this->entityManager->persist($podcast); + $this->entityManager->flush(); + + return $podcast; + } + protected function createSearchableImage(): SearchableEntity { $image = $this->createImage(random_int(100, 300)); @@ -185,6 +209,12 @@ protected function getFileName(string $indexName, string $type): string return sprintf('%s/%s.json', $indexName, $type); } + protected function waitForAllTasks(): void + { + $firstTask = $this->client->getTasks()->getResults()[0]; + $this->client->waitForTask($firstTask['uid']); + } + private function cleanUp(): void { (new Collection($this->searchService->getConfiguration()->get('indices'))) diff --git a/tests/Dbal/Type/DummyObjectIdType.php b/tests/Dbal/Type/DummyObjectIdType.php new file mode 100644 index 00000000..66f10d14 --- /dev/null +++ b/tests/Dbal/Type/DummyObjectIdType.php @@ -0,0 +1,65 @@ +getIntegerTypeDeclarationSQL($column); + } + + public function convertToPHPValue($value, AbstractPlatform $platform): ?DummyObjectId + { + if ($value instanceof DummyObjectId || null === $value) { + return $value; + } + + if (!\is_string($value) && !is_int($value)) { + $actualType = \get_debug_type($value); + $possibleTypes = ['null', 'string', 'int', self::class]; + throw new ConversionException(\sprintf("Could not convert PHP value '%s' of type '%s' to type '%s'. Expected one of the following types: %s", $value, $actualType, $this->getName(), \implode(', ', $possibleTypes))); + } + + return new DummyObjectId((int) $value); + } + + /** + * @throws ConversionException + */ + public function convertToDatabaseValue($value, AbstractPlatform $platform): ?int + { + if ($value instanceof DummyObjectId) { + return $value->toInt(); + } + + if (null === $value || '' === $value) { + return null; + } + + if (!\is_string($value) && !is_int($value)) { + $actualType = \get_debug_type($value); + $possibleTypes = ['null', 'string', 'int', self::class]; + throw new ConversionException(\sprintf("Could not convert PHP value '%s' of type '%s' to type '%s'. Expected one of the following types: %s", $value, $actualType, $this->getName(), \implode(', ', $possibleTypes))); + } + + return (int) $value; + } + + public function requiresSQLCommentHint(AbstractPlatform $platform): bool + { + return true; + } +} diff --git a/tests/Entity/Article.php b/tests/Entity/Article.php new file mode 100644 index 00000000..a1e588ba --- /dev/null +++ b/tests/Entity/Article.php @@ -0,0 +1,15 @@ + Article::class, 2 => Podcast::class])] +abstract class ContentItem +{ + /** + * @ORM\Id + * + * @ORM\GeneratedValue + * + * @ORM\Column(type="integer") + */ + #[ORM\Id] + #[ORM\GeneratedValue] + #[ORM\Column(type: Types::INTEGER)] + private int $id; + + /** + * @ORM\Column(type="string") + */ + #[ORM\Column(type: Types::STRING)] + private string $title = 'Title'; + + public function getId(): int + { + return $this->id; + } + + public function setId(int $id): self + { + $this->id = $id; + + return $this; + } + + public function getTitle(): string + { + return $this->title; + } + + public function setTitle(string $title): self + { + $this->title = $title; + + return $this; + } +} diff --git a/tests/Entity/ObjectId/DummyObjectId.php b/tests/Entity/ObjectId/DummyObjectId.php index 7dc511d9..6c1e41e1 100644 --- a/tests/Entity/ObjectId/DummyObjectId.php +++ b/tests/Entity/ObjectId/DummyObjectId.php @@ -4,7 +4,7 @@ namespace Meilisearch\Bundle\Tests\Entity\ObjectId; -class DummyObjectId +final class DummyObjectId { private int $id; @@ -13,7 +13,12 @@ public function __construct(int $id) $this->id = $id; } - public function __toString() + public function toInt(): int + { + return $this->id; + } + + public function __toString(): string { return (string) $this->id; } diff --git a/tests/Entity/Page.php b/tests/Entity/Page.php index eb820835..0aac8563 100644 --- a/tests/Entity/Page.php +++ b/tests/Entity/Page.php @@ -22,11 +22,11 @@ class Page * * @ORM\GeneratedValue(strategy="NONE") * - * @ORM\Column(type="object") + * @ORM\Column(type="dummy_object_id") */ #[ORM\Id] #[ORM\GeneratedValue(strategy: 'NONE')] - #[ORM\Column(type: Types::OBJECT)] + #[ORM\Column(type: 'dummy_object_id')] private $id; /** diff --git a/tests/Entity/Podcast.php b/tests/Entity/Podcast.php new file mode 100644 index 00000000..ff800217 --- /dev/null +++ b/tests/Entity/Podcast.php @@ -0,0 +1,15 @@ +createPost(); + $this->entityManager->clear(); - $postMetadata = $this->entityManager->getClassMetadata(Post::class); - $this->entityManager->getProxyFactory()->generateProxyClasses([$postMetadata]); - - $proxy = $this->entityManager->getProxyFactory()->getProxy($postMetadata->getName(), ['id' => 1]); + $proxy = $this->entityManager->getReference(Post::class, 1); + $this->assertInstanceOf(Proxy::class, $proxy); $contentAggregator = new ContentAggregator($proxy, ['objectId']); /** @var Serializer $serializer */ diff --git a/tests/Integration/CommandsTest.php b/tests/Integration/CommandsTest.php index 3f24a1c3..43adb283 100644 --- a/tests/Integration/CommandsTest.php +++ b/tests/Integration/CommandsTest.php @@ -8,21 +8,16 @@ use Meilisearch\Bundle\Tests\Entity\DummyCustomGroups; use Meilisearch\Bundle\Tests\Entity\DynamicSettings; use Meilisearch\Bundle\Tests\Entity\SelfNormalizable; -use Meilisearch\Client; use Meilisearch\Endpoints\Indexes; use Meilisearch\Exceptions\ApiException; use Meilisearch\Search\SearchResult; use Symfony\Bundle\FrameworkBundle\Console\Application; use Symfony\Component\Console\Tester\CommandTester; -/** - * Class CommandsTest. - */ class CommandsTest extends BaseKernelTestCase { private static string $indexName = 'posts'; - protected Client $client; protected Application $application; protected Indexes $index; @@ -34,7 +29,6 @@ protected function setUp(): void { parent::setUp(); - $this->client = $this->get('meilisearch.client'); $this->index = $this->client->index($this->getPrefix().self::$indexName); $this->application = new Application(self::createKernel()); } @@ -43,7 +37,7 @@ public function testSearchClearUnknownIndex(): void { $unknownIndexName = 'test'; - $command = $this->application->find('meili:clear'); + $command = $this->application->find('meilisearch:clear'); $commandTester = new CommandTester($command); $commandTester->execute([ @@ -68,7 +62,7 @@ public function testSearchImportAndClearAndDeleteWithoutIndices(): void $this->createTag(['id' => $i]); } - $importCommand = $this->application->find('meili:import'); + $importCommand = $this->application->find('meilisearch:import'); $importCommandTester = new CommandTester($importCommand); $importCommandTester->execute([]); @@ -81,11 +75,13 @@ public function testSearchImportAndClearAndDeleteWithoutIndices(): void Settings updated of "sf_phpunit__posts". Settings updated of "sf_phpunit__posts". Settings updated of "sf_phpunit__posts". +Settings updated of "sf_phpunit__posts". Importing for index Meilisearch\Bundle\Tests\Entity\Comment Importing for index Meilisearch\Bundle\Tests\Entity\Tag Indexed a batch of 6 / 6 Meilisearch\Bundle\Tests\Entity\Tag entities into sf_phpunit__tags index (6 indexed since start) Indexed a batch of 6 / 6 Meilisearch\Bundle\Tests\Entity\Tag entities into sf_phpunit__aggregated index (6 indexed since start) Importing for index Meilisearch\Bundle\Tests\Entity\Link +Importing for index Meilisearch\Bundle\Tests\Entity\ContentItem Importing for index Meilisearch\Bundle\Tests\Entity\Page Indexed a batch of 6 / 6 Meilisearch\Bundle\Tests\Entity\Page entities into sf_phpunit__pages index (6 indexed since start) Importing for index Meilisearch\Bundle\Tests\Entity\SelfNormalizable @@ -105,7 +101,7 @@ public function testSearchImportAndClearAndDeleteWithoutIndices(): void EOD, $importOutput); - $clearCommand = $this->application->find('meili:clear'); + $clearCommand = $this->application->find('meilisearch:clear'); $clearCommandTester = new CommandTester($clearCommand); $clearCommandTester->execute([]); @@ -117,6 +113,7 @@ public function testSearchImportAndClearAndDeleteWithoutIndices(): void Cleared sf_phpunit__aggregated index of Meilisearch\Bundle\Tests\Entity\ContentAggregator Cleared sf_phpunit__tags index of Meilisearch\Bundle\Tests\Entity\Tag Cleared sf_phpunit__tags index of Meilisearch\Bundle\Tests\Entity\Link +Cleared sf_phpunit__discriminator_map index of Meilisearch\Bundle\Tests\Entity\ContentItem Cleared sf_phpunit__pages index of Meilisearch\Bundle\Tests\Entity\Page Cleared sf_phpunit__self_normalizable index of Meilisearch\Bundle\Tests\Entity\SelfNormalizable Cleared sf_phpunit__dummy_custom_groups index of Meilisearch\Bundle\Tests\Entity\DummyCustomGroups @@ -125,7 +122,7 @@ public function testSearchImportAndClearAndDeleteWithoutIndices(): void EOD, $clearOutput); - $clearCommand = $this->application->find('meili:delete'); + $clearCommand = $this->application->find('meilisearch:delete'); $clearCommandTester = new CommandTester($clearCommand); $clearCommandTester->execute([]); @@ -136,6 +133,7 @@ public function testSearchImportAndClearAndDeleteWithoutIndices(): void Deleted sf_phpunit__comments Deleted sf_phpunit__aggregated Deleted sf_phpunit__tags +Deleted sf_phpunit__discriminator_map Deleted sf_phpunit__pages Deleted sf_phpunit__self_normalizable Deleted sf_phpunit__dummy_custom_groups @@ -145,13 +143,58 @@ public function testSearchImportAndClearAndDeleteWithoutIndices(): void EOD, $clearOutput); } + public function testImportWithoutUpdatingSettings(): void + { + for ($i = 0; $i <= 5; ++$i) { + $this->createPost(); + } + + $importCommand = $this->application->find('meilisearch:import'); + $importCommandTester = new CommandTester($importCommand); + $importCommandTester->execute(['--indices' => 'posts', '--no-update-settings' => true]); + + $importOutput = $importCommandTester->getDisplay(); + + $this->assertSame(<<<'EOD' +Importing for index Meilisearch\Bundle\Tests\Entity\Post +Indexed a batch of 6 / 6 Meilisearch\Bundle\Tests\Entity\Post entities into sf_phpunit__posts index (6 indexed since start) +Indexed a batch of 6 / 6 Meilisearch\Bundle\Tests\Entity\Post entities into sf_phpunit__aggregated index (6 indexed since start) +Done! + +EOD, $importOutput); + } + + public function testImportContentItem(): void + { + for ($i = 0; $i <= 5; ++$i) { + $this->createArticle(); + } + + for ($i = 0; $i <= 5; ++$i) { + $this->createPodcast(); + } + + $importCommand = $this->application->find('meilisearch:import'); + $importCommandTester = new CommandTester($importCommand); + $importCommandTester->execute(['--indices' => 'discriminator_map', '--no-update-settings' => true]); + + $importOutput = $importCommandTester->getDisplay(); + + $this->assertSame(<<<'EOD' +Importing for index Meilisearch\Bundle\Tests\Entity\ContentItem +Indexed a batch of 12 / 12 Meilisearch\Bundle\Tests\Entity\ContentItem entities into sf_phpunit__discriminator_map index (12 indexed since start) +Done! + +EOD, $importOutput); + } + public function testSearchImportWithCustomBatchSize(): void { for ($i = 0; $i <= 10; ++$i) { $this->createPage($i); } - $importCommand = $this->application->find('meili:import'); + $importCommand = $this->application->find('meilisearch:import'); $importCommandTester = new CommandTester($importCommand); $importCommandTester->execute([ '--indices' => 'pages', @@ -179,7 +222,7 @@ public function testSearchImportWithCustomResponseTimeout(): void $this->createPage($i); } - $importCommand = $this->application->find('meili:import'); + $importCommand = $this->application->find('meilisearch:import'); $importCommandTester = new CommandTester($importCommand); $return = $importCommandTester->execute([ '--indices' => 'pages', @@ -200,7 +243,7 @@ public function testSearchImportWithCustomResponseTimeout(): void } // test if it will work with a bad option - $importCommand = $this->application->find('meili:import'); + $importCommand = $this->application->find('meilisearch:import'); $importCommandTester = new CommandTester($importCommand); $return = $importCommandTester->execute([ '--indices' => 'pages', @@ -225,7 +268,7 @@ public function testImportDifferentEntitiesIntoSameIndex(): void $this->createLink(['id' => 60, 'isSponsored' => true]); $this->createLink(['id' => 61, 'isSponsored' => true]); - $command = $this->application->find('meili:import'); + $command = $this->application->find('meilisearch:import'); $commandTester = new CommandTester($command); $commandTester->execute([ '--indices' => 'tags', @@ -249,7 +292,7 @@ public function testSearchImportAggregator(): void $this->createPost(); } - $command = $this->application->find('meili:import'); + $command = $this->application->find('meilisearch:import'); $commandTester = new CommandTester($command); $return = $commandTester->execute([ '--indices' => $this->index->getUid(), @@ -268,7 +311,7 @@ public function testSearchImportWithSkipBatches(): void $this->createPage($i); } - $command = $this->application->find('meili:import'); + $command = $this->application->find('meilisearch:import'); $commandTester = new CommandTester($command); $return = $commandTester->execute([ '--indices' => 'pages', @@ -291,7 +334,7 @@ public function testImportingIndexNameWithAndWithoutPrefix(): void $this->createPost(); } - $command = $this->application->find('meili:import'); + $command = $this->application->find('meilisearch:import'); $commandTester = new CommandTester($command); $return = $commandTester->execute([ '--indices' => $this->index->getUid(), // This is the already prefixed name @@ -310,7 +353,7 @@ public function testImportingIndexNameWithAndWithoutPrefix(): void $this->createPost(); } - $command = $this->application->find('meili:import'); + $command = $this->application->find('meilisearch:import'); $commandTester = new CommandTester($command); $return = $commandTester->execute([ '--indices' => self::$indexName, // This is the already prefixed name @@ -323,22 +366,29 @@ public function testImportingIndexNameWithAndWithoutPrefix(): void $this->assertSame(0, $return); } - public function testSearchCreateWithoutIndices(): void + /** + * @testWith [false] + * [true] + */ + public function testSearchCreateWithoutIndices(bool $updateSettings): void { - $createCommand = $this->application->find('meili:create'); + $createCommand = $this->application->find('meilisearch:create'); $createCommandTester = new CommandTester($createCommand); - $createCommandTester->execute([]); + $createCommandTester->execute($updateSettings ? [] : ['--no-update-settings' => true]); $createOutput = $createCommandTester->getDisplay(); - $this->assertSame(<<<'EOD' + if ($updateSettings) { + $this->assertSame(<<<'EOD' Creating index sf_phpunit__posts for Meilisearch\Bundle\Tests\Entity\Post Settings updated of "sf_phpunit__posts". Settings updated of "sf_phpunit__posts". Settings updated of "sf_phpunit__posts". +Settings updated of "sf_phpunit__posts". Creating index sf_phpunit__comments for Meilisearch\Bundle\Tests\Entity\Comment Creating index sf_phpunit__tags for Meilisearch\Bundle\Tests\Entity\Tag Creating index sf_phpunit__tags for Meilisearch\Bundle\Tests\Entity\Link +Creating index sf_phpunit__discriminator_map for Meilisearch\Bundle\Tests\Entity\ContentItem Creating index sf_phpunit__pages for Meilisearch\Bundle\Tests\Entity\Page Creating index sf_phpunit__self_normalizable for Meilisearch\Bundle\Tests\Entity\SelfNormalizable Creating index sf_phpunit__dummy_custom_groups for Meilisearch\Bundle\Tests\Entity\DummyCustomGroups @@ -352,11 +402,28 @@ public function testSearchCreateWithoutIndices(): void Done! EOD, $createOutput); + } else { + $this->assertSame(<<<'EOD' +Creating index sf_phpunit__posts for Meilisearch\Bundle\Tests\Entity\Post +Creating index sf_phpunit__comments for Meilisearch\Bundle\Tests\Entity\Comment +Creating index sf_phpunit__tags for Meilisearch\Bundle\Tests\Entity\Tag +Creating index sf_phpunit__tags for Meilisearch\Bundle\Tests\Entity\Link +Creating index sf_phpunit__discriminator_map for Meilisearch\Bundle\Tests\Entity\ContentItem +Creating index sf_phpunit__pages for Meilisearch\Bundle\Tests\Entity\Page +Creating index sf_phpunit__self_normalizable for Meilisearch\Bundle\Tests\Entity\SelfNormalizable +Creating index sf_phpunit__dummy_custom_groups for Meilisearch\Bundle\Tests\Entity\DummyCustomGroups +Creating index sf_phpunit__dynamic_settings for Meilisearch\Bundle\Tests\Entity\DynamicSettings +Creating index sf_phpunit__aggregated for Meilisearch\Bundle\Tests\Entity\Post +Creating index sf_phpunit__aggregated for Meilisearch\Bundle\Tests\Entity\Tag +Done! + +EOD, $createOutput); + } } public function testSearchCreateWithIndices(): void { - $createCommand = $this->application->find('meili:create'); + $createCommand = $this->application->find('meilisearch:create'); $createCommandTester = new CommandTester($createCommand); $createCommandTester->execute([ '--indices' => 'posts', @@ -369,6 +436,7 @@ public function testSearchCreateWithIndices(): void Settings updated of "sf_phpunit__posts". Settings updated of "sf_phpunit__posts". Settings updated of "sf_phpunit__posts". +Settings updated of "sf_phpunit__posts". Done! EOD, $createOutput); @@ -376,11 +444,11 @@ public function testSearchCreateWithIndices(): void public function testCreateExecuteIndexCreation(): void { - $createCommand = $this->application->find('meili:create'); + $createCommand = $this->application->find('meilisearch:create'); $createCommandTester = new CommandTester($createCommand); $createCommandTester->execute([]); - $this->assertEquals($this->client->getTasks()->getResults()[0]['type'], 'indexCreation'); + $this->assertSame($this->client->getTasks()->getResults()[0]['type'], 'indexCreation'); } public function testImportsSelfNormalizable(): void @@ -391,7 +459,7 @@ public function testImportsSelfNormalizable(): void $this->entityManager->flush(); - $importCommand = $this->application->find('meili:import'); + $importCommand = $this->application->find('meilisearch:import'); $importCommandTester = new CommandTester($importCommand); $importCommandTester->execute(['--indices' => 'self_normalizable']); @@ -428,7 +496,7 @@ public function testImportsDummyWithCustomGroups(): void $this->entityManager->flush(); - $importCommand = $this->application->find('meili:import'); + $importCommand = $this->application->find('meilisearch:import'); $importCommandTester = new CommandTester($importCommand); $importCommandTester->execute(['--indices' => 'dummy_custom_groups']); @@ -446,20 +514,20 @@ public function testImportsDummyWithCustomGroups(): void 'objectID' => 1, 'id' => 1, 'name' => 'Dummy 1', - 'createdAt' => '2024-04-04T07:32:01+00:00', + 'createdAt' => 1712215921, ], [ 'objectID' => 2, 'id' => 2, 'name' => 'Dummy 2', - 'createdAt' => '2024-04-04T07:32:02+00:00', + 'createdAt' => 1712215922, ], ], $this->client->index('sf_phpunit__dummy_custom_groups')->getDocuments()->getResults()); } /** - * @testWith ["meili:create"] - * ["meili:import"] + * @testWith ["meilisearch:create"] + * ["meilisearch:import"] */ public function testImportWithDynamicSettings(string $command): void { @@ -475,7 +543,7 @@ public function testImportWithDynamicSettings(string $command): void $importOutput = $importCommandTester->getDisplay(); - if ('meili:import' === $command) { + if ('meilisearch:import' === $command) { $this->assertSame(<<<'EOD' Importing for index Meilisearch\Bundle\Tests\Entity\DynamicSettings Indexed a batch of 6 / 6 Meilisearch\Bundle\Tests\Entity\DynamicSettings entities into sf_phpunit__dynamic_settings index (6 indexed since start) @@ -507,4 +575,17 @@ public function testImportWithDynamicSettings(string $command): void self::assertSame(['a', 'n', 'the'], $getSetting($settings['stopWords'])); self::assertSame(['fantastic' => ['great'], 'great' => ['fantastic']], $getSetting($settings['synonyms'])); } + + /** + * @testWith ["meilisearch:clear", ["meili:clear"]] + * ["meilisearch:create", ["meili:create"]] + * ["meilisearch:delete", ["meili:delete"]] + * ["meilisearch:import", ["meili:import"]] + */ + public function testAliases(string $command, array $expectedAliases): void + { + $command = $this->application->find($command); + + self::assertSame($expectedAliases, $command->getAliases()); + } } diff --git a/tests/Integration/DependencyInjectionTest.php b/tests/Integration/DependencyInjectionTest.php index c16d09af..5779a40e 100644 --- a/tests/Integration/DependencyInjectionTest.php +++ b/tests/Integration/DependencyInjectionTest.php @@ -2,11 +2,13 @@ declare(strict_types=1); +namespace Meilisearch\Bundle\Tests\Integration; + use Matthias\SymfonyDependencyInjectionTest\PhpUnit\AbstractExtensionTestCase; use Meilisearch\Bundle\DependencyInjection\MeilisearchExtension; use Meilisearch\Bundle\MeilisearchBundle; -class MeilisearchExtensionTest extends AbstractExtensionTestCase +class DependencyInjectionTest extends AbstractExtensionTestCase { protected function getContainerExtensions(): array { @@ -17,14 +19,14 @@ protected function getContainerExtensions(): array public function testHasMeilisearchVersionDefinitionAfterLoad(): void { - $this->load(); + $this->load(['url' => 'http://meilisearch:7700', 'api_key' => null]); - $this->assertContainerBuilderHasServiceDefinitionWithArgument('meilisearch.client', '$clientAgents', ['%meili_symfony_version%']); + $this->assertContainerBuilderHasServiceDefinitionWithArgument('meilisearch.client', 4, [MeilisearchBundle::qualifiedVersion()]); } public function testHasMeilisearchVersionFromConstantAfterLoad(): void { - $this->load(); + $this->load(['url' => 'http://meilisearch:7700', 'api_key' => null]); $this->assertContainerBuilderHasParameter('meili_symfony_version', MeilisearchBundle::qualifiedVersion()); } diff --git a/tests/Integration/EventListener/DoctrineEventSubscriberTest.php b/tests/Integration/EventListener/DoctrineEventSubscriberTest.php index 47f45eb2..2ad03e46 100644 --- a/tests/Integration/EventListener/DoctrineEventSubscriberTest.php +++ b/tests/Integration/EventListener/DoctrineEventSubscriberTest.php @@ -4,126 +4,88 @@ namespace Meilisearch\Bundle\Tests\Integration\EventListener; -use Doctrine\Persistence\Event\LifecycleEventArgs; -use Meilisearch\Bundle\EventListener\DoctrineEventSubscriber; use Meilisearch\Bundle\Tests\BaseKernelTestCase; +use Meilisearch\Bundle\Tests\Entity\ObjectId\DummyObjectId; use Meilisearch\Bundle\Tests\Entity\Page; use Meilisearch\Bundle\Tests\Entity\Post; -use Meilisearch\Client; class DoctrineEventSubscriberTest extends BaseKernelTestCase { - protected Client $client; - - /** - * @throws \Exception - */ - public function setUp(): void - { - parent::setUp(); - - $this->client = $this->get('meilisearch.client'); - } - - /** - * This tests creates two posts in the database, but only one is triggered via an event to Meilisearch. - */ public function testPostPersist(): void { - $this->createPost(); $post = $this->createPost(); - $eventArgs = new LifecycleEventArgs($post, $this->entityManager); - - $subscriber = new DoctrineEventSubscriber($this->searchService); - $subscriber->postPersist($eventArgs); - $this->waitForAllTasks(); $result = $this->searchService->search($this->entityManager, Post::class, $post->getTitle()); $this->assertCount(1, $result); - $this->assertSame(2, $result[0]->getId()); + $this->assertSame($post->getId(), $result[0]->getId()); } public function testPostPersistWithObjectId(): void { - $this->createPage(1); - $page = $this->createPage(2); - - $eventArgs = new LifecycleEventArgs($page, $this->entityManager); - - $subscriber = new DoctrineEventSubscriber($this->searchService); - $subscriber->postPersist($eventArgs); + $page = $this->createPage(1); $this->waitForAllTasks(); $result = $this->searchService->search($this->entityManager, Page::class, $page->getTitle()); $this->assertCount(1, $result); - $this->assertSame((string) $page->getId(), (string) $result[0]->getId()); + $this->assertEquals(new DummyObjectId(1), $result[0]->getId()); } - /** - * This tests creates two posts in the database, but only one is triggered via an event to Meilisearch. - */ public function testPostUpdate(): void { - $this->createPost(); $post = $this->createPost(); - $eventArgs = new LifecycleEventArgs($post, $this->entityManager); + $this->waitForAllTasks(); + + $post->setTitle('Better post'); - $subscriber = new DoctrineEventSubscriber($this->searchService); - $subscriber->postUpdate($eventArgs); + $this->entityManager->flush(); $this->waitForAllTasks(); - $result = $this->searchService->search($this->entityManager, Post::class, $post->getTitle()); + $result = $this->searchService->search($this->entityManager, Post::class, 'better'); $this->assertCount(1, $result); - $this->assertSame(2, $result[0]->getId()); + $this->assertSame($post->getId(), $result[0]->getId()); + $this->assertSame('Better post', $result[0]->getTitle()); } public function testPostUpdateWithObjectId(): void { - $this->createPage(1); - $page = $this->createPage(2); + $page = $this->createPage(1); + + $this->waitForAllTasks(); - $eventArgs = new LifecycleEventArgs($page, $this->entityManager); + $page->setTitle('Better page'); - $subscriber = new DoctrineEventSubscriber($this->searchService); - $subscriber->postUpdate($eventArgs); + $this->entityManager->flush(); $this->waitForAllTasks(); - $result = $this->searchService->search($this->entityManager, Page::class, $page->getTitle()); + $result = $this->searchService->search($this->entityManager, Page::class, 'better'); $this->assertCount(1, $result); - $this->assertSame((string) $page->getId(), (string) $result[0]->getId()); + $this->assertEquals(new DummyObjectId(1), $result[0]->getId()); + $this->assertSame('Better page', $result[0]->getTitle()); } - /** - * This tests creates posts in the database, send it to Meilisearch via a trigger. Afterwards Doctrines 'preRemove' event - * is going to remove that entity from MS. - */ public function testPreRemove(): void { $post = $this->createPost(); - $eventArgs = new LifecycleEventArgs($post, $this->entityManager); - - $subscriber = new DoctrineEventSubscriber($this->searchService); - $subscriber->postPersist($eventArgs); - $this->waitForAllTasks(); $result = $this->searchService->search($this->entityManager, Post::class, $post->getTitle()); $this->assertCount(1, $result); - $this->assertSame(1, $result[0]->getId()); + $this->assertSame($post->getId(), $result[0]->getId()); - $subscriber->preRemove($eventArgs); + $this->entityManager->remove($post); + $this->entityManager->flush(); $this->waitForAllTasks(); @@ -136,19 +98,15 @@ public function testPreRemoveWithObjectId(): void { $page = $this->createPage(1); - $eventArgs = new LifecycleEventArgs($page, $this->entityManager); - - $subscriber = new DoctrineEventSubscriber($this->searchService); - $subscriber->postPersist($eventArgs); - $this->waitForAllTasks(); $result = $this->searchService->search($this->entityManager, Page::class, $page->getTitle()); $this->assertCount(1, $result); - $this->assertSame((string) $page->getId(), (string) $result[0]->getId()); + $this->assertEquals($page->getId(), $result[0]->getId()); - $subscriber->preRemove($eventArgs); + $this->entityManager->remove($page); + $this->entityManager->flush(); $this->waitForAllTasks(); @@ -156,13 +114,4 @@ public function testPreRemoveWithObjectId(): void $this->assertCount(0, $result); } - - /** - * Waits for all the tasks to be finished by checking the topest one (so the newest one). - */ - private function waitForAllTasks(): void - { - $firstTask = $this->client->getTasks()->getResults()[0]; - $this->client->waitForTask($firstTask['uid']); - } } diff --git a/tests/Integration/SearchTest.php b/tests/Integration/SearchTest.php index 33da6b75..95c0e815 100644 --- a/tests/Integration/SearchTest.php +++ b/tests/Integration/SearchTest.php @@ -9,9 +9,7 @@ use Meilisearch\Bundle\Tests\BaseKernelTestCase; use Meilisearch\Bundle\Tests\Entity\Post; use Meilisearch\Bundle\Tests\Entity\Tag; -use Meilisearch\Client; use Meilisearch\Endpoints\Indexes; -use Meilisearch\Exceptions\ApiException; use Symfony\Bundle\FrameworkBundle\Console\Application; use Symfony\Component\Console\Tester\CommandTester; @@ -22,16 +20,11 @@ class SearchTest extends BaseKernelTestCase { private static string $indexName = 'aggregated'; - protected Client $client; protected Connection $connection; protected ObjectManager $objectManager; protected Application $application; protected Indexes $index; - /** - * @throws ApiException - * @throws \Exception - */ protected function setUp(): void { parent::setUp(); @@ -57,7 +50,7 @@ public function testSearchImportAggregator(): void $this->createTag(['id' => 99]); - $command = $this->application->find('meili:import'); + $command = $this->application->find('meilisearch:import'); $commandTester = new CommandTester($command); $commandTester->execute([ '--indices' => $this->index->getUid(), @@ -102,7 +95,7 @@ public function testSearchPagination(): void $testDataTitles[] = $this->createPost()->getTitle(); } - $command = $this->application->find('meili:import'); + $command = $this->application->find('meilisearch:import'); $commandTester = new CommandTester($command); $commandTester->execute([ '--indices' => $this->index->getUid(), @@ -117,8 +110,20 @@ public function testSearchPagination(): void $this->assertEqualsCanonicalizing(array_slice($testDataTitles, 2, 2), $resultTitles); } - protected function tearDown(): void + public function testSearchNbResults(): void { - parent::tearDown(); + for ($i = 0; $i < 15; ++$i) { + $this->createPost(); + } + + $command = $this->application->find('meilisearch:import'); + $commandTester = new CommandTester($command); + $commandTester->execute([ + '--indices' => $this->index->getUid(), + ]); + + $results = $this->searchService->search($this->objectManager, Post::class, 'test'); + + $this->assertCount(12, $results); } } diff --git a/tests/Integration/SettingsTest.php b/tests/Integration/SettingsTest.php index 1781c2fc..bbc8c9ca 100644 --- a/tests/Integration/SettingsTest.php +++ b/tests/Integration/SettingsTest.php @@ -5,7 +5,6 @@ namespace Meilisearch\Bundle\Tests\Integration; use Meilisearch\Bundle\Tests\BaseKernelTestCase; -use Meilisearch\Client; use Meilisearch\Contracts\Index\TypoTolerance; use Symfony\Bundle\FrameworkBundle\Console\Application; use Symfony\Component\Console\Tester\CommandTester; @@ -27,7 +26,6 @@ class SettingsTest extends BaseKernelTestCase 'exactness', ]; - protected Client $client; protected Application $application; /** @@ -37,7 +35,6 @@ public function setUp(): void { parent::setUp(); - $this->client = $this->get('meilisearch.client'); $this->application = new Application(self::$kernel); } @@ -45,7 +42,7 @@ public function testUpdateSettings(): void { $index = $this->getPrefix().self::$indexName; - $command = $this->application->find('meili:import'); + $command = $this->application->find('meilisearch:import'); $commandTester = new CommandTester($command); $commandTester->execute([ '--indices' => $index, diff --git a/tests/Kernel.php b/tests/Kernel.php index 4091d159..d970cc35 100644 --- a/tests/Kernel.php +++ b/tests/Kernel.php @@ -4,38 +4,71 @@ namespace Meilisearch\Bundle\Tests; +use Doctrine\Bundle\DoctrineBundle\ConnectionFactory; use Doctrine\Bundle\DoctrineBundle\DoctrineBundle; +use Doctrine\ORM\Configuration; use Meilisearch\Bundle\MeilisearchBundle; +use Symfony\Bridge\Doctrine\ArgumentResolver\EntityValueResolver; use Symfony\Bundle\FrameworkBundle\FrameworkBundle; +use Symfony\Bundle\FrameworkBundle\Kernel\MicroKernelTrait; use Symfony\Component\Config\Loader\LoaderInterface; -use Symfony\Component\HttpKernel\Bundle\BundleInterface; +use Symfony\Component\DependencyInjection\ContainerBuilder; use Symfony\Component\HttpKernel\Kernel as HttpKernel; -/** - * Class Kernel. - */ class Kernel extends HttpKernel { - /** - * @return array - */ - public function registerBundles(): array + use MicroKernelTrait; + + public function registerBundles(): iterable { - return [ - new FrameworkBundle(), - new DoctrineBundle(), - new MeilisearchBundle(), - ]; + yield new FrameworkBundle(); + yield new DoctrineBundle(); + yield new MeilisearchBundle(); } - public function registerContainerConfiguration(LoaderInterface $loader): void + protected function configureContainer(ContainerBuilder $container, LoaderInterface $loader): void { if (PHP_VERSION_ID >= 80000) { $loader->load(__DIR__.'/config/config.yaml'); } else { $loader->load(__DIR__.'/config/config_php7.yaml'); } - $loader->load(__DIR__.'/../config/services.xml'); $loader->load(__DIR__.'/config/meilisearch.yaml'); + + if (defined(ConnectionFactory::class.'::DEFAULT_SCHEME_MAP')) { + $container->prependExtensionConfig('doctrine', [ + 'orm' => [ + 'report_fields_where_declared' => true, + 'validate_xml_mapping' => true, + ], + ]); + } + + // @phpstan-ignore-next-line + if (method_exists(Configuration::class, 'setLazyGhostObjectEnabled') && Kernel::VERSION_ID >= 60100) { + $container->prependExtensionConfig('doctrine', [ + 'orm' => [ + 'enable_lazy_ghost_objects' => true, + ], + ]); + } + + if (class_exists(EntityValueResolver::class)) { + $container->prependExtensionConfig('doctrine', [ + 'orm' => [ + 'controller_resolver' => [ + 'auto_mapping' => false, + ], + ], + ]); + } + + // @phpstan-ignore-next-line + if (Kernel::VERSION_ID >= 60400) { + $container->prependExtensionConfig('framework', [ + 'handle_all_throwables' => true, + 'php_errors' => ['log' => true], + ]); + } } } diff --git a/tests/Unit/ConfigurationTest.php b/tests/Unit/ConfigurationTest.php index 02643711..fca71a73 100644 --- a/tests/Unit/ConfigurationTest.php +++ b/tests/Unit/ConfigurationTest.php @@ -32,6 +32,7 @@ public function dataTestConfigurationTree(): array 'test empty config for default value' => [ [], [ + 'url' => 'http://localhost:7700', 'prefix' => null, 'nbResults' => 20, 'batchSize' => 500, @@ -42,11 +43,13 @@ public function dataTestConfigurationTree(): array ], 'Simple config' => [ [ + 'url' => 'http://meilisearch:7700', 'prefix' => 'sf_', 'nbResults' => 40, 'batchSize' => 100, ], [ + 'url' => 'http://meilisearch:7700', 'prefix' => 'sf_', 'nbResults' => 40, 'batchSize' => 100, @@ -69,6 +72,7 @@ public function dataTestConfigurationTree(): array ], ], [ + 'url' => 'http://localhost:7700', 'prefix' => 'sf_', 'nbResults' => 20, 'batchSize' => 500, @@ -119,6 +123,7 @@ public function dataTestConfigurationTree(): array 'doctrineSubscribedEvents' => ['postPersist', 'postUpdate', 'preRemove'], ], [ + 'url' => 'http://localhost:7700', 'prefix' => 'sf_', 'indices' => [ [ @@ -158,6 +163,7 @@ public function dataTestConfigurationTree(): array ], ], [ + 'url' => 'http://localhost:7700', 'prefix' => 'sf_', 'indices' => [ [ @@ -174,6 +180,74 @@ public function dataTestConfigurationTree(): array 'doctrineSubscribedEvents' => ['postPersist', 'postUpdate', 'preRemove'], ], ], + 'distinct attribute' => [ + [ + 'prefix' => 'sf_', + 'indices' => [ + [ + 'name' => 'items', + 'class' => 'App\Entity\Post', + 'settings' => [ + 'distinctAttribute' => 'product_id', + ], + ], + ], + ], + [ + 'url' => 'http://localhost:7700', + 'prefix' => 'sf_', + 'indices' => [ + [ + 'name' => 'items', + 'class' => 'App\Entity\Post', + 'enable_serializer_groups' => false, + 'serializer_groups' => ['searchable'], + 'index_if' => null, + 'settings' => [ + 'distinctAttribute' => ['product_id'], + ], + ], + ], + 'nbResults' => 20, + 'batchSize' => 500, + 'serializer' => 'serializer', + 'doctrineSubscribedEvents' => ['postPersist', 'postUpdate', 'preRemove'], + ], + ], + 'proximity precision' => [ + [ + 'prefix' => 'sf_', + 'indices' => [ + [ + 'name' => 'items', + 'class' => 'App\Entity\Post', + 'settings' => [ + 'proximityPrecision' => 'byWord', + ], + ], + ], + ], + [ + 'url' => 'http://localhost:7700', + 'prefix' => 'sf_', + 'indices' => [ + [ + 'name' => 'items', + 'class' => 'App\Entity\Post', + 'enable_serializer_groups' => false, + 'serializer_groups' => ['searchable'], + 'index_if' => null, + 'settings' => [ + 'proximityPrecision' => ['byWord'], + ], + ], + ], + 'nbResults' => 20, + 'batchSize' => 500, + 'serializer' => 'serializer', + 'doctrineSubscribedEvents' => ['postPersist', 'postUpdate', 'preRemove'], + ], + ], ]; } } diff --git a/tests/Unit/SerializationTest.php b/tests/Unit/SerializationTest.php index f53aba5f..93b614ce 100644 --- a/tests/Unit/SerializationTest.php +++ b/tests/Unit/SerializationTest.php @@ -18,10 +18,6 @@ class SerializationTest extends KernelTestCase public function testSimpleEntityToSearchableArray(): void { $datetime = new \DateTime(); - $dateNormalizer = static::getContainer()->get('serializer.normalizer.datetime'); - // This way we can test that DateTime's are serialized with DateTimeNormalizer - // And not the default ObjectNormalizer - $serializedDateTime = $dateNormalizer->normalize($datetime, Searchable::NORMALIZATION_FORMAT); $post = new Post( [ @@ -51,12 +47,12 @@ public function testSimpleEntityToSearchableArray(): void 'id' => 12, 'title' => 'a simple post', 'content' => 'some text', - 'publishedAt' => $serializedDateTime, + 'publishedAt' => $datetime->getTimestamp(), 'comments' => [ [ 'id' => null, 'content' => 'a great comment', - 'publishedAt' => $serializedDateTime, + 'publishedAt' => $datetime->getTimestamp(), ], ], ]; diff --git a/tests/baseline-ignore b/tests/baseline-ignore index 070e7c16..4397a01f 100644 --- a/tests/baseline-ignore +++ b/tests/baseline-ignore @@ -1,10 +1 @@ %Method "ArrayAccess::offsetGet\(\)" might add "mixed" as a native return type declaration in the future. Do the same in implementation "Meilisearch\\Contracts\\Data" now to avoid errors or add an explicit @return annotation to suppress this message.% -%The "Symfony\\Component\\HttpClient\\HttplugClient" class implements "Http\\Client\\HttpClient" that is deprecated since version 2.4, use Psr\\Http\\Client\\ClientInterface instead; see https://www.php-fig.org/psr/psr-18/% -%The "Symfony\\Component\\HttpClient\\HttplugClient" class implements "Http\\Message\\RequestFactory" that is deprecated since version 1.1, use Psr\\Http\\Message\\RequestFactoryInterface instead.% -%The "Symfony\\Component\\HttpClient\\HttplugClient" class implements "Http\\Message\\StreamFactory" that is deprecated since version 1.1, use Psr\\Http\\Message\\StreamFactoryInterface instead.% -%The "Symfony\\Component\\HttpClient\\HttplugClient" class implements "Http\\Message\\UriFactory" that is deprecated since version 1.1, use Psr\\Http\\Message\\UriFactoryInterface instead.% -%Doctrine\\DBAL\\Platforms\\AbstractPlatform::usesSequenceEmulatedIdentityColumns is deprecated. \(AbstractPlatform.php:\d+ called by ClassMetadataFactory.php:\d+, https://github.com/doctrine/dbal/pull/5513, package doctrine/dbal\)% -%Column::setCustomSchemaOptions\(\) is deprecated. Use setPlatformOptions\(\) instead. \(Column.php:\d+ called by Column.php:\d+, https://github.com/doctrine/dbal/pull/5476, package doctrine/dbal\)% -%SqlitePlatform::canEmulateSchemas\(\) is deprecated. \(SqlitePlatform.php:\d+ called by SchemaTool.php:\d+, https://github.com/doctrine/dbal/pull/4805, package doctrine/dbal\)% -%Doctrine\\DBAL\\Schema\\Table::getPrimaryKeyColumns is deprecated. Use getPrimaryKey\(\) and Index::getColumns\(\) instead. \(Table.php:\d+ called by Table.php:\d+, https://github.com/doctrine/dbal/pull/5731, package doctrine/dbal\)% -%The annotation mapping driver is deprecated and will be removed in Doctrine ORM 3.0, please migrate to the attribute or XML driver. \(AnnotationDriver.php:\d+ called by getDoctrine_Orm_DefaultAnnotationMetadataDriverService.php:20, https://github.com/doctrine/orm/issues/10098, package doctrine/orm\)% diff --git a/tests/config/config.yaml b/tests/config/config.yaml index f63d2135..87de9ee1 100644 --- a/tests/config/config.yaml +++ b/tests/config/config.yaml @@ -10,9 +10,11 @@ doctrine: default: driver: pdo_sqlite path: '%kernel.cache_dir%/test.sqlite' + types: + dummy_object_id: Meilisearch\Bundle\Tests\Dbal\Type\DummyObjectIdType orm: auto_generate_proxy_classes: true - validate_xml_mapping: true + report_fields_where_declared: true naming_strategy: doctrine.orm.naming_strategy.underscore_number_aware auto_mapping: true mappings: diff --git a/tests/config/config_php7.yaml b/tests/config/config_php7.yaml index 715f92d8..7f690a23 100644 --- a/tests/config/config_php7.yaml +++ b/tests/config/config_php7.yaml @@ -5,6 +5,8 @@ framework: annotations: true serializer: enable_annotations: true + router: + utf8: true doctrine: dbal: @@ -13,9 +15,10 @@ doctrine: default: driver: pdo_sqlite path: '%kernel.cache_dir%/test.sqlite' + types: + dummy_object_id: Meilisearch\Bundle\Tests\Dbal\Type\DummyObjectIdType orm: auto_generate_proxy_classes: true - validate_xml_mapping: true naming_strategy: doctrine.orm.naming_strategy.underscore_number_aware auto_mapping: true mappings: diff --git a/tests/config/meilisearch.yaml b/tests/config/meilisearch.yaml index 864f723f..50a06b90 100644 --- a/tests/config/meilisearch.yaml +++ b/tests/config/meilisearch.yaml @@ -1,57 +1,60 @@ meilisearch: - url: '%env(MEILISEARCH_URL)%' - api_key: '%env(MEILISEARCH_API_KEY)%' - prefix: '%env(MEILISEARCH_PREFIX)%_' - nbResults: 12 - batchSize: 100 - indices: - - name: posts - class: 'Meilisearch\Bundle\Tests\Entity\Post' - enable_serializer_groups: true - settings: - stopWords: ['the', 'a', 'an'] - filterableAttributes: ['title', 'publishedAt'] - typoTolerance: - enabled: true - disableOnAttributes: ['title'] - disableOnWords: ['york'] - minWordSizeForTypos: - oneTypo: 5 - twoTypos: 9 - - name: comments - class: 'Meilisearch\Bundle\Tests\Entity\Comment' - - name: aggregated - class: 'Meilisearch\Bundle\Tests\Entity\ContentAggregator' - index_if: isVisible - - name: tags - class: 'Meilisearch\Bundle\Tests\Entity\Tag' - index_if: isPublic - # Yes, we want to have links in the same index as tags - # We just set the same index name 'tags' - - name: tags - class: 'Meilisearch\Bundle\Tests\Entity\Link' - index_if: isSponsored - - name: pages - class: 'Meilisearch\Bundle\Tests\Entity\Page' - enable_serializer_groups: true - - name: self_normalizable - class: 'Meilisearch\Bundle\Tests\Entity\SelfNormalizable' - - name: dummy_custom_groups - class: 'Meilisearch\Bundle\Tests\Entity\DummyCustomGroups' - enable_serializer_groups: true - serializer_groups: ['public', 'private'] - - name: dynamic_settings - class: 'Meilisearch\Bundle\Tests\Entity\DynamicSettings' - settings: - filterableAttributes: - _service: '@Meilisearch\Bundle\Tests\Integration\Fixtures\FilterableAttributes' - searchableAttributes: - _service: '@Meilisearch\Bundle\Tests\Integration\Fixtures\SearchableAttributes' - stopWords: - _service: '@Meilisearch\Bundle\Tests\Integration\Fixtures\StopWords' - synonyms: - _service: '@Meilisearch\Bundle\Tests\Integration\Fixtures\Synonyms' + url: '%env(MEILISEARCH_URL)%' + api_key: '%env(MEILISEARCH_API_KEY)%' + prefix: '%env(MEILISEARCH_PREFIX)%_' + nbResults: 12 + batchSize: 100 + indices: + - name: posts + class: 'Meilisearch\Bundle\Tests\Entity\Post' + enable_serializer_groups: true + settings: + stopWords: ['the', 'a', 'an'] + filterableAttributes: ['title', 'publishedAt'] + searchCutoffMs: 1500 + typoTolerance: + enabled: true + disableOnAttributes: ['title'] + disableOnWords: ['york'] + minWordSizeForTypos: + oneTypo: 5 + twoTypos: 9 + - name: comments + class: 'Meilisearch\Bundle\Tests\Entity\Comment' + - name: aggregated + class: 'Meilisearch\Bundle\Tests\Entity\ContentAggregator' + index_if: isVisible + - name: tags + class: 'Meilisearch\Bundle\Tests\Entity\Tag' + index_if: isPublic + # Yes, we want to have links in the same index as tags + # We just set the same index name 'tags' + - name: tags + class: 'Meilisearch\Bundle\Tests\Entity\Link' + index_if: isSponsored + - name: discriminator_map + class: 'Meilisearch\Bundle\Tests\Entity\ContentItem' + - name: pages + class: 'Meilisearch\Bundle\Tests\Entity\Page' + enable_serializer_groups: true + - name: self_normalizable + class: 'Meilisearch\Bundle\Tests\Entity\SelfNormalizable' + - name: dummy_custom_groups + class: 'Meilisearch\Bundle\Tests\Entity\DummyCustomGroups' + enable_serializer_groups: true + serializer_groups: ['public', 'private'] + - name: dynamic_settings + class: 'Meilisearch\Bundle\Tests\Entity\DynamicSettings' + settings: + filterableAttributes: + _service: '@Meilisearch\Bundle\Tests\Integration\Fixtures\FilterableAttributes' + searchableAttributes: + _service: '@Meilisearch\Bundle\Tests\Integration\Fixtures\SearchableAttributes' + stopWords: + _service: '@Meilisearch\Bundle\Tests\Integration\Fixtures\StopWords' + synonyms: + _service: '@Meilisearch\Bundle\Tests\Integration\Fixtures\Synonyms' services: - Meilisearch\Bundle\Tests\Integration\Fixtures\: - resource: '../Integration/Fixtures/' + Meilisearch\Bundle\Tests\Integration\Fixtures\: + resource: '../Integration/Fixtures/'