diff --git a/.code-samples.meilisearch.yaml b/.code-samples.meilisearch.yaml
index 10b413a0..f1811b7c 100644
--- a/.code-samples.meilisearch.yaml
+++ b/.code-samples.meilisearch.yaml
@@ -1,15 +1,12 @@
landing_getting_started_1: |-
- ```yml
- # Configure your entities by adding them in config/packages/meilisearch.yaml
- meilisearch:
- url: 'http://127.0.0.1:7700'
- api_key: 'masterKey'
- indices:
- - name: movies
- class: App\Entity\Movie
- ```
-
- ```php
+ // # Configure your entities by adding them in config/packages/meilisearch.yaml
+ // meilisearch:
+ // url: 'http://127.0.0.1:7700'
+ // api_key: 'masterKey'
+ // indices:
+ // - name: movies
+ // class: App\Entity\Movie
+
"$temp_file"
+latest_rc_release=$(cat "$temp_file" \
+ | grep -E 'tag_name' | grep 'rc' | head -1 \
+ | tr -d ',"' | cut -d ':' -f2 | tr -d ' ')
+rm -rf "$temp_file"
+echo "$latest_rc_release"
diff --git a/.github/workflows/pre-release-tests.yml b/.github/workflows/pre-release-tests.yml
index 50b88b0b..c1f33723 100644
--- a/.github/workflows/pre-release-tests.yml
+++ b/.github/workflows/pre-release-tests.yml
@@ -3,33 +3,98 @@ 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
+ meilisearch-version:
+ runs-on: ubuntu-latest
+ steps:
+ - name: Checkout code
+ uses: actions/checkout@v5
+
+ - name: Grep docker latest rc version of Meilisearch
+ id: grep-step
+ run: |
+ MEILISEARCH_VERSION=$(sh .github/scripts/get-latest-meilisearch-rc.sh)
+ echo $MEILISEARCH_VERSION
+ echo "version=$MEILISEARCH_VERSION" >> $GITHUB_OUTPUT
+ outputs:
+ version: ${{ steps.grep-step.outputs.version }}
+
+ integration-tests:
+ runs-on: ubuntu-latest
+ needs: ['meilisearch-version']
+ services:
+ meilisearch:
+ image: getmeili/meilisearch:${{ needs.meilisearch-version.outputs.version }}
+ ports:
+ - '7700:7700'
+ env:
+ MEILI_MASTER_KEY: masterKey
+ MEILI_NO_ANALYTICS: true
+ strategy:
+ matrix:
+ php-version: ['7.4', '8.1', '8.2', '8.3', '8.4']
+ sf-version: ['5.4', '6.4', '7.0', '7.1', '7.2', '7.3']
+ exclude:
+ - php-version: '7.4'
+ sf-version: '6.4'
+ - php-version: '7.4'
+ sf-version: '7.0'
+ - php-version: '7.4'
+ sf-version: '7.1'
+ - php-version: '7.4'
+ sf-version: '7.3'
+ - php-version: '8.1'
+ sf-version: '5.4'
+ - php-version: '8.1'
+ sf-version: '7.0'
+ - php-version: '8.1'
+ sf-version: '7.1'
+ - php-version: '8.1'
+ sf-version: '7.3'
+ - php-version: '8.2'
+ sf-version: '5.4'
+ - php-version: '8.3'
+ sf-version: '5.4'
+ - php-version: '8.4'
+ sf-version: '5.4'
+ - php-version: '7.4'
+ sf-version: '7.2'
+ - php-version: '8.0'
+ sf-version: '7.2'
+ - php-version: '8.1'
+ sf-version: '7.2'
+
+ name: integration-tests-against-rc (PHP ${{ matrix.php-version }}) (Symfony ${{ matrix.sf-version }}.*)
+ steps:
+ - name: Checkout code
+ uses: actions/checkout@v5
+
+ - name: Install PHP
+ uses: shivammathur/setup-php@v2
+ with:
+ php-version: ${{ matrix.php-version }}
+ 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 }}.*
+ with:
+ dependency-versions: 'highest'
+
+ - name: Run test suite
+ run: composer test:unit -- --coverage-clover coverage.xml
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 18299d3f..4557343e 100644
--- a/.github/workflows/tests.yml
+++ b/.github/workflows/tests.yml
@@ -8,6 +8,11 @@ on:
- trying
- staging
- main
+ schedule:
+ - cron: '0 3 * * 1'
+
+env:
+ fail-fast: true
jobs:
integration-tests:
@@ -15,61 +20,146 @@ jobs:
# 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', '8.4']
+ sf-version: ['5.4', '6.4', '7.0', '7.1', '7.2', '7.3']
+ 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: '7.4'
+ sf-version: '7.3'
- php-version: '8.1'
- sf-version: '6.0.*'
+ sf-version: '5.4'
- php-version: '8.1'
- sf-version: '6.1.*'
+ sf-version: '7.0'
+ - php-version: '8.1'
+ sf-version: '7.1'
+ - php-version: '8.1'
+ sf-version: '7.3'
- php-version: '8.2'
- sf-version: '6.2.*'
+ sf-version: '5.4'
+ - php-version: '8.3'
+ sf-version: '5.4'
+ - php-version: '8.4'
+ sf-version: '5.4'
+ - php-version: '7.4'
+ sf-version: '7.2'
+ - php-version: '8.0'
+ sf-version: '7.2'
+ - php-version: '8.1'
+ sf-version: '7.2'
- 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@v5
+
- 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: 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'
+ env:
+ PHP_CS_FIXER_IGNORE_ENV: 1
steps:
- - uses: actions/checkout@v3
+ - uses: actions/checkout@v5
- name: Install PHP
uses: shivammathur/setup-php@v2
with:
- php-version: '8.1'
+ php-version: 8.4
- 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.2.*
+ with:
+ composer-options: '--no-progress --quiet'
+ dependency-versions: 'highest'
- name: PHP CS Fixer
run: composer lint:check
- - name: PHPstan
- run: composer phpstan
+ - name: PHP MD
+ run: composer phpmd
+ continue-on-error: true
+
+ - name: PHPStan
+ run: |
+ vendor/bin/simple-phpunit --version
+ composer phpstan
+
+ yaml-lint:
+ name: Yaml linting check
+ runs-on: ubuntu-latest
+ steps:
+ - uses: actions/checkout@v5
+ - 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@v5
+ with:
+ fetch-depth: 2
+
+ - name: Download coverage files
+ uses: actions/download-artifact@v5
+ with:
+ path: reports
+
+ - name: Upload to Codecov
+ uses: codecov/codecov-action@v5
+ 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 084c2808..de29a333 100644
--- a/.php-cs-fixer.dist.php
+++ b/.php-cs-fixer.dist.php
@@ -6,20 +6,23 @@
->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,
- ],
- ]
-)
+return (new PhpCsFixer\Config())
+ ->setParallelConfig(PhpCsFixer\Runner\Parallel\ParallelConfigFactory::detect())
->setRiskyAllowed(true)
->setFinder($finder)
-;
-
-return $config;
+ ->setRules([
+ '@Symfony' => true,
+ '@PHP80Migration:risky' => true,
+ 'native_function_invocation' => ['include' => ['@compiler_optimized'], 'scope' => 'namespaced'],
+ 'global_namespace_import' => [
+ 'import_classes' => false,
+ 'import_functions' => false,
+ 'import_constants' => false,
+ ],
+ 'no_superfluous_phpdoc_tags' => false,
+ // @todo: when we'll support only PHP 8.0 and upper, we can enable `parameters` for `trailing_comma_in_multiline` rule
+ 'trailing_comma_in_multiline' => ['after_heredoc' => true, 'elements' => ['array_destructuring', 'arrays', 'match'/* , 'parameters' */]],
+ // @todo: when we'll support only PHP 8.0 and upper, we can enable this
+ 'get_class_to_class_keyword' => 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 81a5b72f..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
@@ -61,6 +61,8 @@ composer lint:check
composer lint:fix
# PHPstan
composer phpstan
+# PHPMD
+composer phpmd
```
### Using the Docker Environment
diff --git a/LICENSE b/LICENSE
index b1f0d22c..1ea8bbc9 100644
--- a/LICENSE
+++ b/LICENSE
@@ -1,6 +1,6 @@
MIT License
-Copyright (c) 2019-2022 Meili SAS
+Copyright (c) 2019-2025 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 af353ddf..1323a7d4 100644
--- a/README.md
+++ b/README.md
@@ -6,18 +6,20 @@
+
-
+
⚡ The Meilisearch bundle for Symfony
@@ -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..0db50c48 100644
--- a/bors.toml
+++ b/bors.toml
@@ -1,8 +1,21 @@
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.2) (Symfony 7.0.*)',
+ 'integration-tests (PHP 8.2) (Symfony 7.1.*)',
+ 'integration-tests (PHP 8.2) (Symfony 7.2.*)',
+ 'integration-tests (PHP 8.2) (Symfony 7.3.*)',
+ 'integration-tests (PHP 8.3) (Symfony 6.4.*)',
+ 'integration-tests (PHP 8.3) (Symfony 7.0.*)',
+ 'integration-tests (PHP 8.3) (Symfony 7.1.*)',
+ 'integration-tests (PHP 8.3) (Symfony 7.2.*)',
+ 'integration-tests (PHP 8.3) (Symfony 7.3.*)',
+ 'integration-tests (PHP 8.4) (Symfony 6.4.*)',
+ 'integration-tests (PHP 8.4) (Symfony 7.0.*)',
+ 'integration-tests (PHP 8.4) (Symfony 7.1.*)',
+ 'integration-tests (PHP 8.4) (Symfony 7.2.*)',
+ 'integration-tests (PHP 8.4) (Symfony 7.3.*)',
'Code style'
]
# 1 hour timeout
diff --git a/composer.json b/composer.json
index 7aa046b7..47c04655 100644
--- a/composer.json
+++ b/composer.json
@@ -20,29 +20,36 @@
"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",
- "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",
- "phpunit/phpunit": "^9.6.5",
- "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-config-test": "^4.3 || ^5.2",
+ "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": {
@@ -62,9 +69,10 @@
}
},
"scripts": {
- "phpstan": "./vendor/bin/phpstan --memory-limit=1G --ansi",
- "test:unit": "./vendor/bin/phpunit --colors=always --verbose",
- "test:unit:coverage": "XDEBUG_MODE=coverage ./vendor/bin/phpunit --colors=always --coverage-html=tests/coverage",
+ "phpmd": "./vendor/bin/phpmd src text phpmd.xml",
+ "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",
"lint:fix": "./vendor/bin/php-cs-fixer fix -v --using-cache=no"
}
diff --git a/config/services.xml b/config/services.xml
index d8d20c82..00fda01f 100644
--- a/config/services.xml
+++ b/config/services.xml
@@ -4,29 +4,90 @@
xsi:schemaLocation="http://symfony.com/schema/dic/services https://symfony.com/schema/dic/services/services-1.0.xsd">
-
+
+
+
+
+
+
+
+ normalizer
+
+ configuration
+
+
+
+ The "%alias_id%" service alias is deprecated. Use "meilisearch.service" instead.
+
+
+
+
+
+
+ The "%alias_id%" service alias is deprecated. Use "meilisearch.search_indexer_subscriber" instead.
+
+
+
+ url defined in MeilisearchExtension
+ api key defined in MeilisearchExtension
+ http client defined in MeilisearchExtension
+ null
+ client agents defined in MeilisearchExtension
+ null
+
+
+ The "%alias_id%" service alias is deprecated. Use "meilisearch.client" instead.
+
-
+
+
+ The "%alias_id%" service alias is deprecated. Use "meilisearch.client" instead.
+
+
+
+
+
+
+
+
+
+
+
+
+
+
-
-
-
-
-
+
+
+
+
+
+
-
- %meili_url%
- %meili_api_key%
-
-
- %meili_symfony_version%
-
+
+
+
-
-
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/phpmd.baseline.xml b/phpmd.baseline.xml
new file mode 100644
index 00000000..1e8da586
--- /dev/null
+++ b/phpmd.baseline.xml
@@ -0,0 +1,23 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/phpmd.xml b/phpmd.xml
new file mode 100644
index 00000000..ad3ff2c3
--- /dev/null
+++ b/phpmd.xml
@@ -0,0 +1,13 @@
+
+
+ Ruleset for PHP Mess Detector that enforces coding standards
+
+
+
+
+
+
+
diff --git a/phpstan.neon b/phpstan.neon
deleted file mode 100644
index b35f3d4e..00000000
--- a/phpstan.neon
+++ /dev/null
@@ -1,5 +0,0 @@
-parameters:
- level: 5
- paths:
- - src
- - tests
diff --git a/phpstan.neon.dist b/phpstan.neon.dist
new file mode 100644
index 00000000..28e5679a
--- /dev/null
+++ b/phpstan.neon.dist
@@ -0,0 +1,9 @@
+parameters:
+ bootstrapFiles:
+ - vendor/bin/.phpunit/phpunit/vendor/autoload.php
+ level: 5
+ 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 e6554d3a..d50518cb 100644
--- a/phpunit.xml.dist
+++ b/phpunit.xml.dist
@@ -1,7 +1,7 @@
-
+ xsi:noNamespaceSchemaLocation="vendor/bin/.phpunit/phpunit/phpunit.xsd">
src/
@@ -14,8 +14,7 @@
-
-
+
diff --git a/src/Collection.php b/src/Collection.php
index c09e0d5c..2e0ef277 100644
--- a/src/Collection.php
+++ b/src/Collection.php
@@ -4,6 +4,12 @@
namespace Meilisearch\Bundle;
+/**
+ * This class was extracted from illuminate.
+ * This will suppress all the PMD warnings in this class.
+ *
+ * @SuppressWarnings(PHPMD)
+ */
final class Collection implements \ArrayAccess, \Countable, \IteratorAggregate
{
/**
@@ -11,9 +17,6 @@ final class Collection implements \ArrayAccess, \Countable, \IteratorAggregate
*/
private array $items;
- /**
- * @param mixed $items
- */
public function __construct($items = [])
{
$this->items = $this->getArrayableItems($items);
@@ -24,15 +27,9 @@ public function all(): array
return $this->items;
}
- /**
- * @param mixed $key
- * @param mixed $default
- *
- * @return mixed
- */
public function get($key, $default = null)
{
- if (array_key_exists($key, $this->items)) {
+ if (\array_key_exists($key, $this->items)) {
return $this->items[$key];
}
@@ -68,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));
@@ -126,7 +123,7 @@ public function each(callable $callback)
*/
public function unique($key = null, bool $strict = false)
{
- if (is_null($key) && false === $strict) {
+ if (\is_null($key) && false === $strict) {
return new self(array_unique($this->items, SORT_REGULAR));
}
@@ -135,7 +132,7 @@ public function unique($key = null, bool $strict = false)
$exists = [];
return $this->reject(function ($item, $key) use ($callback, $strict, &$exists) {
- if (in_array($id = $callback($item, $key), $exists, $strict)) {
+ if (\in_array($id = $callback($item, $key), $exists, $strict)) {
return true;
}
@@ -145,14 +142,10 @@ public function unique($key = null, bool $strict = false)
/**
* Get the first item from the collection passing the given truth test.
- *
- * @param mixed $default
- *
- * @return mixed
*/
- public function first(callable $callback = null, $default = null)
+ public function first(?callable $callback = null, $default = null)
{
- if (is_null($callback)) {
+ if (\is_null($callback)) {
if (empty($this->items)) {
return $default instanceof \Closure ? $default() : $default;
}
@@ -175,27 +168,18 @@ public function first(callable $callback = null, $default = null)
* Get the first item by the given key value pair.
*
* @param string $key
- * @param mixed $operator
- * @param mixed $value
- *
- * @return mixed
*/
public function firstWhere($key, $operator = null, $value = null)
{
- return $this->first($this->operatorForWhere(...func_get_args()));
+ return $this->first($this->operatorForWhere(...\func_get_args()));
}
- /**
- * @param mixed $offset
- */
public function offsetExists($offset): bool
{
return isset($this->items[$offset]);
}
/**
- * @param mixed $offset
- *
* @return mixed
*/
#[\ReturnTypeWillChange]
@@ -204,22 +188,15 @@ public function offsetGet($offset)
return $this->items[$offset];
}
- /**
- * @param mixed $offset
- * @param mixed $value
- */
public function offsetSet($offset, $value): void
{
- if (is_null($offset)) {
+ if (\is_null($offset)) {
$this->items[] = $value;
} else {
$this->items[$offset] = $value;
}
}
- /**
- * @param mixed $offset
- */
public function offsetUnset($offset): void
{
unset($this->items[$offset]);
@@ -227,7 +204,7 @@ public function offsetUnset($offset): void
public function count(): int
{
- return count($this->items);
+ return \count($this->items);
}
public function getIterator(): \ArrayIterator
@@ -235,12 +212,9 @@ public function getIterator(): \ArrayIterator
return new \ArrayIterator($this->items);
}
- /**
- * @param mixed $items
- */
private function getArrayableItems($items): array
{
- if (is_array($items)) {
+ if (\is_array($items)) {
return $items;
}
@@ -263,12 +237,9 @@ private function getArrayableItems($items): array
return (array) $items;
}
- /**
- * @param mixed $value
- */
private function useAsCallable($value): bool
{
- return !is_string($value) && is_callable($value);
+ return !\is_string($value) && \is_callable($value);
}
/**
@@ -286,31 +257,27 @@ private function valueRetriever($value)
}
/**
- * @param mixed $target
* @param string|array|int|null $key
- * @param mixed $default
- *
- * @return mixed
*/
private static function getDeepData($target, $key, $default = null)
{
- if (is_null($key)) {
+ if (\is_null($key)) {
return $target;
}
- $key = is_array($key) ? $key : explode('.', $key);
+ $key = \is_array($key) ? $key : explode('.', $key);
foreach ($key as $i => $segment) {
unset($key[$i]);
- if (is_null($segment)) {
+ if (\is_null($segment)) {
return $target;
}
if ('*' === $segment) {
if ($target instanceof self) {
$target = $target->all();
- } elseif (!is_array($target)) {
+ } elseif (!\is_array($target)) {
return $default instanceof \Closure ? $default() : $default;
}
@@ -320,12 +287,12 @@ private static function getDeepData($target, $key, $default = null)
$result[] = self::getDeepData($item, $key);
}
- return in_array('*', $key, true) ? self::arrayCollapse($result) : $result;
+ return \in_array('*', $key, true) ? self::arrayCollapse($result) : $result;
}
if (self::accessible($target) && self::existsInArray($target, $segment)) {
$target = $target[$segment];
- } elseif (is_object($target) && isset($target->{$segment})) {
+ } elseif (\is_object($target) && isset($target->{$segment})) {
$target = $target->{$segment};
} else {
return $default instanceof \Closure ? $default() : $default;
@@ -342,7 +309,7 @@ public static function arrayCollapse(iterable $array): array
foreach ($array as $values) {
if ($values instanceof self) {
$values = $values->all();
- } elseif (!is_array($values)) {
+ } elseif (!\is_array($values)) {
continue;
}
@@ -352,12 +319,9 @@ public static function arrayCollapse(iterable $array): array
return array_merge([], ...$results);
}
- /**
- * @param mixed $value
- */
public static function accessible($value): bool
{
- return is_array($value) || $value instanceof \ArrayAccess;
+ return \is_array($value) || $value instanceof \ArrayAccess;
}
/**
@@ -372,21 +336,18 @@ private static function existsInArray($array, $key): bool
return $array->offsetExists($key);
}
- return array_key_exists($key, $array);
+ return \array_key_exists($key, $array);
}
- /**
- * @param mixed $value
- */
private function operatorForWhere(string $key, ?string $operator = null, $value = null): \Closure
{
- if (1 === func_num_args()) {
+ if (1 === \func_num_args()) {
$value = true;
$operator = '=';
}
- if (2 === func_num_args()) {
+ if (2 === \func_num_args()) {
$value = $operator;
$operator = '=';
@@ -395,10 +356,10 @@ private function operatorForWhere(string $key, ?string $operator = null, $value
return static function ($item) use ($key, $operator, $value) {
$retrieved = self::getDeepData($item, $key);
- $strings = array_filter([$retrieved, $value], fn ($value) => is_string($value) || (is_object($value) && method_exists($value, '__toString')));
+ $strings = array_filter([$retrieved, $value], fn ($value) => \is_string($value) || (\is_object($value) && method_exists($value, '__toString')));
- if (count($strings) < 2 && 1 === count(array_filter([$retrieved, $value], 'is_object'))) {
- return in_array($operator, ['!=', '<>', '!==']);
+ if (\count($strings) < 2 && 1 === \count(array_filter([$retrieved, $value], 'is_object'))) {
+ return \in_array($operator, ['!=', '<>', '!==']);
}
switch ($operator) {
diff --git a/src/Command/IndexCommand.php b/src/Command/IndexCommand.php
index 5b54d373..8260b12e 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;
@@ -26,23 +26,13 @@ public function __construct(SearchService $searchService)
parent::__construct();
}
- protected function getIndices(): Collection
- {
- return (new Collection($this->searchService->getConfiguration()->get('indices')))
- ->transform(function (array $item) {
- $item['name'] = $this->prefix.$item['name'];
-
- return $item;
- });
- }
-
protected function getEntitiesFromArgs(InputInterface $input, OutputInterface $output): Collection
{
- $indices = $this->getIndices();
+ $indices = new Collection($this->searchService->getConfiguration()->get('indices'));
$indexNames = new Collection();
if ($indexList = $input->getOption('indices')) {
- $list = \explode(',', $indexList);
+ $list = explode(',', $indexList);
$indexNames = (new Collection($list))->transform(function (string $item): string {
// Check if the given index name already contains the prefix
if (!str_contains($item, $this->prefix)) {
@@ -53,7 +43,7 @@ protected function getEntitiesFromArgs(InputInterface $input, OutputInterface $o
});
}
- if (0 === count($indexNames) && 0 === count($indices)) {
+ if (0 === \count($indexNames) && 0 === \count($indices)) {
$output->writeln(
'No indices specified. Please either specify indices using the cli option or YAML configuration.'
);
@@ -61,8 +51,8 @@ protected function getEntitiesFromArgs(InputInterface $input, OutputInterface $o
return new Collection();
}
- if (count($indexNames) > 0) {
- return $indices->reject(fn (array $item) => !in_array($item['name'], $indexNames->all(), true));
+ if (\count($indexNames) > 0) {
+ return $indices->reject(fn (array $item) => !\in_array($item['prefixed_name'], $indexNames->all(), true));
}
return $indices;
diff --git a/src/Command/MeilisearchClearCommand.php b/src/Command/MeilisearchClearCommand.php
index 2cb8330b..6de03ccf 100644
--- a/src/Command/MeilisearchClearCommand.php
+++ b/src/Command/MeilisearchClearCommand.php
@@ -4,29 +4,17 @@
namespace Meilisearch\Bundle\Command;
+use Symfony\Component\Console\Attribute\AsCommand;
use Symfony\Component\Console\Input\InputInterface;
use Symfony\Component\Console\Input\InputOption;
use Symfony\Component\Console\Output\OutputInterface;
-/**
- * Class MeilisearchClearCommand.
- */
+#[AsCommand(name: 'meilisearch:clear', description: 'Clear the index documents', aliases: ['meili:clear'])]
final class MeilisearchClearCommand extends IndexCommand
{
- public static function getDefaultName(): string
- {
- return 'meili:clear';
- }
-
- public static function getDefaultDescription(): string
- {
- return 'Clear the index documents';
- }
-
protected function configure(): void
{
$this
- ->setDescription(self::getDefaultDescription())
->addOption('indices', 'i', InputOption::VALUE_OPTIONAL, 'Comma-separated list of index names');
}
@@ -36,17 +24,19 @@ protected function execute(InputInterface $input, OutputInterface $output): int
/** @var array $index */
foreach ($indexToClear as $index) {
- $indexName = $index['name'];
+ $indexName = $index['prefixed_name'];
$className = $index['class'];
+ $msg = "Cleared $indexName index of $className";
$array = $this->searchService->clear($className);
+
if ('failed' === $array['status']) {
- $output->writeln('Index '.$indexName.' couldn\'t be cleared');
- } else {
- $output->writeln('Cleared '.$indexName.' index of '.$className.'');
+ $msg = "Index $indexName couldn\'t be cleared";
}
+
+ $output->writeln($msg);
}
- if (0 === count($indexToClear)) {
+ if (0 === \count($indexToClear)) {
$output->writeln('Cannot clear index. Not found.');
}
diff --git a/src/Command/MeilisearchCreateCommand.php b/src/Command/MeilisearchCreateCommand.php
index 86fa1ac9..7058ab4d 100644
--- a/src/Command/MeilisearchCreateCommand.php
+++ b/src/Command/MeilisearchCreateCommand.php
@@ -5,63 +5,63 @@
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\Services\SettingsUpdater;
use Meilisearch\Client;
+use Symfony\Component\Console\Attribute\AsCommand;
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;
+#[AsCommand(name: 'meilisearch:create', description: 'Create indexes', aliases: ['meili:create'])]
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;
- }
-
- public static function getDefaultName(): string
- {
- return 'meili:create';
- }
-
- public static function getDefaultDescription(): string
- {
- return 'Create indexes';
+ $this->settingsUpdater = $settingsUpdater;
+ $this->eventDispatcher = $eventDispatcher;
}
protected function configure(): void
{
$this
- ->setDescription(self::getDefaultDescription())
- ->addOption('indices', 'i', InputOption::VALUE_OPTIONAL, 'Comma-separated list of index names');
+ ->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
{
- $indexes = $this->getEntitiesFromArgs($input, $output);
+ $this->eventDispatcher->addSubscriber(new ConsoleOutputSubscriber(new SymfonyStyle($input, $output)));
- 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()
- )
- ));
- }
- }
-
- $entitiesToIndex = array_unique($indexes->all(), SORT_REGULAR);
+ $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) {
@@ -71,27 +71,15 @@ protected function execute(InputInterface $input, OutputInterface $output): int
continue;
}
- $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']);
+ $indexName = $index['prefixed_name'];
- if (isset($index['settings']) && is_array($index['settings'])) {
- foreach ($index['settings'] as $variable => $value) {
- $method = sprintf('update%s', ucfirst($variable));
+ $output->writeln('Creating index '.$indexName.' for '.$entityClassName.'');
- if (false === method_exists($indexInstance, $method)) {
- throw new InvalidSettingName(sprintf('Invalid setting name: "%s"', $variable));
- }
+ $task = $this->searchClient->createIndex($indexName);
+ $this->searchClient->waitForTask($task['taskUid'], $responseTimeout);
- $task = $indexInstance->{$method}($value);
- $task = $indexInstance->getTask($task['taskUid']);
-
- if ('failed' === $task['status']) {
- throw new TaskException($task['error']);
- }
- }
+ if ($updateSettings) {
+ $this->settingsUpdater->update($indexName, $responseTimeout);
}
}
@@ -99,4 +87,27 @@ protected function execute(InputInterface $input, OutputInterface $output): int
return 0;
}
+
+ private function entitiesToIndex(Collection $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/Command/MeilisearchDeleteCommand.php b/src/Command/MeilisearchDeleteCommand.php
index a316ed19..bae54e53 100644
--- a/src/Command/MeilisearchDeleteCommand.php
+++ b/src/Command/MeilisearchDeleteCommand.php
@@ -6,29 +6,17 @@
use Meilisearch\Bundle\Collection;
use Meilisearch\Exceptions\ApiException;
+use Symfony\Component\Console\Attribute\AsCommand;
use Symfony\Component\Console\Input\InputInterface;
use Symfony\Component\Console\Input\InputOption;
use Symfony\Component\Console\Output\OutputInterface;
-/**
- * Class MeilisearchDeleteCommand.
- */
+#[AsCommand(name: 'meilisearch:delete', description: 'Delete the indexes', aliases: ['meili:delete'])]
final class MeilisearchDeleteCommand extends IndexCommand
{
- public static function getDefaultName(): string
- {
- return 'meili:delete';
- }
-
- public static function getDefaultDescription(): string
- {
- return 'Delete the indexes';
- }
-
protected function configure(): void
{
$this
- ->setDescription(self::getDefaultDescription())
->addOption('indices', 'i', InputOption::VALUE_OPTIONAL, 'Comma-separated list of index names');
}
@@ -38,17 +26,20 @@ protected function execute(InputInterface $input, OutputInterface $output): int
/** @var array $index */
foreach ($indexToDelete as $index) {
- $indexName = $index['name'];
+ $indexName = $index['prefixed_name'];
+
try {
$this->searchService->deleteByIndexName($indexName);
} catch (ApiException $e) {
$output->writeln('Cannot delete '.$indexName.': '.$e->getMessage());
+
continue;
}
+
$output->writeln('Deleted '.$indexName.'');
}
- if (0 === count($indexToDelete)) {
+ if (0 === \count($indexToDelete)) {
$output->writeln('Cannot delete index. Not found.');
}
diff --git a/src/Command/MeilisearchImportCommand.php b/src/Command/MeilisearchImportCommand.php
index 1f97c661..37da172e 100644
--- a/src/Command/MeilisearchImportCommand.php
+++ b/src/Command/MeilisearchImportCommand.php
@@ -6,55 +6,59 @@
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\Services\SettingsUpdater;
use Meilisearch\Client;
+use Meilisearch\Exceptions\TimeOutException;
+use Symfony\Component\Console\Attribute\AsCommand;
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.
- */
+#[AsCommand(name: 'meilisearch:import', description: 'Import given entity into search engine', aliases: ['meili:import'])]
final class MeilisearchImportCommand extends IndexCommand
{
- private const DEFAULT_RESPONSE_TIMEOUT = 5000;
+ private const TEMP_INDEX_PREFIX = '_tmp_';
- protected Client $searchClient;
- protected ManagerRegistry $managerRegistry;
+ private Client $searchClient;
+ private ManagerRegistry $managerRegistry;
+ private SettingsUpdater $settingsUpdater;
+ private EventDispatcherInterface $eventDispatcher;
- 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;
- }
-
- public static function getDefaultName(): string
- {
- return 'meili:import';
- }
-
- public static function getDefaultDescription(): string
- {
- return 'Import given entity into search engine';
+ $this->settingsUpdater = $settingsUpdater;
+ $this->eventDispatcher = $eventDispatcher;
}
protected function configure(): void
{
$this
- ->setDescription(self::getDefaultDescription())
->addOption('indices', 'i', InputOption::VALUE_OPTIONAL, 'Comma-separated list of index names')
->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(
+ 'skip-batches',
+ null,
+ InputOption::VALUE_REQUIRED,
+ 'Skip the first N batches and start importing from the N+1 batch',
+ 0
+ )
->addOption(
'response-timeout',
't',
@@ -62,105 +66,113 @@ protected function configure(): void
'Timeout (in ms) to get response from the search engine',
self::DEFAULT_RESPONSE_TIMEOUT
)
+ ->addOption(
+ 'swap-indices',
+ null,
+ InputOption::VALUE_NONE,
+ 'Import to temporary indices and use index swap to prevent downtime'
+ )
;
}
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();
+ $swapIndices = $input->getOption('swap-indices');
+ $initialPrefix = $config['prefix'] ?? '';
+ $prefix = null;
- 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()
- )
- ));
- }
+ if ($swapIndices) {
+ $prefix = self::TEMP_INDEX_PREFIX;
+ $config['prefix'] = $prefix.($config['prefix'] ?? '');
}
- $entitiesToIndex = array_unique($indexes->all(), SORT_REGULAR);
- $batchSize = $input->getOption('batch-size');
+ $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;
/** @var array $index */
foreach ($entitiesToIndex as $index) {
$entityClassName = $index['class'];
+
if (!$this->searchService->isSearchable($entityClassName)) {
continue;
}
+ $totalIndexed = 0;
+
$manager = $this->managerRegistry->getManagerForClass($entityClassName);
$repository = $manager->getRepository($entityClassName);
+ $classMetadata = $manager->getClassMetadata($entityClassName);
+ $entityIdentifiers = $classMetadata->getIdentifierFieldNames();
+ $sortByAttrs = array_combine($entityIdentifiers, array_fill(0, \count($entityIdentifiers), 'ASC'));
$output->writeln('Importing for index '.$entityClassName.'');
- $page = 0;
+ if ($updateSettings) {
+ $this->settingsUpdater->update($index['prefixed_name'], $responseTimeout, $prefix ? $prefix.$index['prefixed_name'] : null);
+ }
+
+ $page = max(0, (int) $input->getOption('skip-batches'));
+
+ if ($page > 0) {
+ $output->writeln(
+ \sprintf(
+ 'Skipping first %d batches (%d records)',
+ $page,
+ $page * $batchSize,
+ )
+ );
+ }
+
do {
$entities = $repository->findBy(
[],
- null,
+ $sortByAttrs,
$batchSize,
$batchSize * $page
);
$responses = $this->formatIndexingResponse($this->searchService->index($manager, $entities), $responseTimeout);
+ $totalIndexed += \count($entities);
foreach ($responses as $indexName => $numberOfRecords) {
$output->writeln(
- sprintf(
- 'Indexed %s / %s %s entities into %s index',
+ \sprintf(
+ 'Indexed a batch of %d / %d %s entities into %s index (%d indexed since start)',
$numberOfRecords,
- count($entities),
+ \count($entities),
$entityClassName,
- ''.$indexName.''
+ ''.$indexName.'',
+ $totalIndexed,
)
);
}
- 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 (false === method_exists($indexInstance, $method)) {
- throw new InvalidSettingName(sprintf('Invalid setting name: "%s"', $variable));
- }
-
- // 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']);
- } else {
- $output->writeln('Settings updated.');
- }
- }
- }
+ $manager->clear();
++$page;
- } while (count($entities) >= $batchSize);
+ } while (\count($entities) >= $batchSize);
$manager->clear();
}
+ if ($swapIndices) {
+ $this->swapIndices($indexes, $prefix, $output);
+
+ $config['prefix'] = $initialPrefix;
+ }
+
$output->writeln('Done!');
return 0;
}
- /*
+ /**
* @throws TimeOutException
*/
private function formatIndexingResponse(array $batch, int $responseTimeout): array
@@ -169,7 +181,7 @@ private function formatIndexingResponse(array $batch, int $responseTimeout): arr
foreach ($batch as $chunk) {
foreach ($chunk as $indexName => $apiResponse) {
- if (!array_key_exists($indexName, $formattedResponse)) {
+ if (!\array_key_exists($indexName, $formattedResponse)) {
$formattedResponse[$indexName] = 0;
}
@@ -180,7 +192,7 @@ private function formatIndexingResponse(array $batch, int $responseTimeout): arr
$task = $indexInstance->getTask($apiResponse['taskUid']);
if ('failed' === $task['status']) {
- throw new TaskException($task['error']);
+ throw new TaskException($task['error']['message']);
}
$formattedResponse[$indexName] += $task['details']['indexedDocuments'];
@@ -189,4 +201,55 @@ private function formatIndexingResponse(array $batch, int $responseTimeout): arr
return $formattedResponse;
}
+
+ private function entitiesToIndex(Collection $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);
+ }
+
+ private function swapIndices(Collection $indexes, string $prefix, OutputInterface $output): void
+ {
+ $indexPairs = [];
+
+ foreach ($indexes as $index) {
+ $tempIndex = $index;
+ $tempIndex['name'] = $prefix.$tempIndex['prefixed_name'];
+ $pair = [$tempIndex['name'], $index['prefixed_name']];
+
+ // Indexes must be declared only once during a swap
+ if (!\in_array($pair, $indexPairs, true)) {
+ $indexPairs[] = $pair;
+ }
+ }
+
+ // swap indexes
+ $output->writeln('Swapping indices...');
+ $this->searchClient->swapIndexes($indexPairs);
+ $output->writeln('Indices swapped.');
+ $output->writeln('Deleting temporary indices...');
+
+ // delete temp indexes
+ foreach ($indexPairs as $pair) {
+ $this->searchService->deleteByIndexName($pair[0]);
+ $output->writeln('Deleted '.$pair[0].'');
+ }
+ }
}
diff --git a/src/Command/MeilisearchUpdateSettingsCommand.php b/src/Command/MeilisearchUpdateSettingsCommand.php
new file mode 100644
index 00000000..a91fdcf6
--- /dev/null
+++ b/src/Command/MeilisearchUpdateSettingsCommand.php
@@ -0,0 +1,93 @@
+settingsUpdater = $settingsUpdater;
+ $this->eventDispatcher = $eventDispatcher;
+ }
+
+ protected function configure(): void
+ {
+ $this
+ ->addOption('indices', 'i', InputOption::VALUE_OPTIONAL, 'Comma-separated list of index names')
+ ->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);
+ $responseTimeout = ((int) $input->getOption('response-timeout')) ?: self::DEFAULT_RESPONSE_TIMEOUT;
+
+ /** @var array $index */
+ foreach ($entitiesToIndex as $index) {
+ $entityClassName = $index['class'];
+
+ if (!$this->searchService->isSearchable($entityClassName)) {
+ continue;
+ }
+
+ $this->settingsUpdater->update($index['prefixed_name'], $responseTimeout);
+ }
+
+ $output->writeln('Done!');
+
+ return 0;
+ }
+
+ private function entitiesToIndex(Collection $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/DependencyInjection/Configuration.php b/src/DependencyInjection/Configuration.php
index 0ce0f6e7..97dd0f84 100644
--- a/src/DependencyInjection/Configuration.php
+++ b/src/DependencyInjection/Configuration.php
@@ -4,27 +4,29 @@
namespace Meilisearch\Bundle\DependencyInjection;
+use Meilisearch\Bundle\Searchable;
use Symfony\Component\Config\Definition\Builder\TreeBuilder;
use Symfony\Component\Config\Definition\ConfigurationInterface;
+use Symfony\Component\Config\Definition\Exception\InvalidConfigurationException;
final class Configuration implements ConfigurationInterface
{
public function getConfigTreeBuilder(): TreeBuilder
{
- $treeBuilder = new TreeBuilder('meili_search');
+ $treeBuilder = new TreeBuilder('meilisearch');
$rootNode = $treeBuilder->getRootNode();
$rootNode
->children()
- ->scalarNode('url')->end()
+ ->scalarNode('url')->defaultValue('http://localhost:7700')->end()
->scalarNode('api_key')->end()
->scalarNode('prefix')
- ->defaultValue(null)
+ ->defaultNull()
->end()
- ->scalarNode('nbResults')
+ ->integerNode('nbResults')
->defaultValue(20)
->end()
- ->scalarNode('batchSize')
+ ->integerNode('batchSize')
->defaultValue(500)
->end()
->arrayNode('doctrineSubscribedEvents')
@@ -34,6 +36,9 @@ public function getConfigTreeBuilder(): TreeBuilder
->scalarNode('serializer')
->defaultValue('serializer')
->end()
+ ->scalarNode('http_client')
+ ->defaultValue('psr18.http_client')
+ ->end()
->arrayNode('indices')
->arrayPrototype()
->children()
@@ -49,14 +54,40 @@ public function getConfigTreeBuilder(): TreeBuilder
->info('When set to true, it will call normalize method with an extra groups parameter "groups" => [Searchable::NORMALIZATION_GROUP]')
->defaultFalse()
->end()
+ ->arrayNode('serializer_groups')
+ ->info('When setting a different value, normalization will be called with it instead of "Searchable::NORMALIZATION_GROUP".')
+ ->defaultValue([Searchable::NORMALIZATION_GROUP])
+ ->scalarPrototype()->end()
+ ->end()
->scalarNode('index_if')
->info('Property accessor path (like method or property name) used to decide if an entry should be indexed.')
->defaultNull()
->end()
- ->arrayNode('settings')
- ->info('Configure indices settings, see: https://docs.meilisearch.com/guides/advanced_guides/settings.html')
- ->arrayPrototype()
- ->variablePrototype()->end()
+ ->variableNode('settings')
+ ->defaultValue([])
+ ->info('Configure indices settings, see: https://www.meilisearch.com/docs/reference/api/settings')
+ ->beforeNormalization()
+ ->always()
+ ->then(static function ($value) {
+ if (null === $value) {
+ return [];
+ }
+
+ if (!\is_array($value)) {
+ throw new InvalidConfigurationException('Settings must be an array.');
+ }
+
+ $stringSettings = ['distinctAttribute', 'proximityPrecision', 'searchCutoffMs'];
+
+ foreach ($stringSettings as $setting) {
+ if (isset($value[$setting]) && !\is_array($value[$setting])) {
+ $value[$setting] = (array) $value[$setting];
+ }
+ }
+
+ return $value;
+ })
+ ->end()
->end()
->end()
->end()
diff --git a/src/DependencyInjection/MeilisearchExtension.php b/src/DependencyInjection/MeilisearchExtension.php
index ed1ce5c5..2ffcc300 100644
--- a/src/DependencyInjection/MeilisearchExtension.php
+++ b/src/DependencyInjection/MeilisearchExtension.php
@@ -4,24 +4,18 @@
namespace Meilisearch\Bundle\DependencyInjection;
-use Meilisearch\Bundle\Engine;
use Meilisearch\Bundle\MeilisearchBundle;
-use Meilisearch\Bundle\Services\MeilisearchService;
+use Meilisearch\Bundle\Services\UnixTimestampNormalizer;
use Symfony\Component\Config\FileLocator;
use Symfony\Component\DependencyInjection\ContainerBuilder;
-use Symfony\Component\DependencyInjection\Definition;
+use Symfony\Component\DependencyInjection\ContainerInterface;
+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
{
- /**
- * {@inheritdoc}
- */
public function load(array $configs, ContainerBuilder $container): void
{
$loader = new Loader\XmlFileLoader($container, new FileLocator(__DIR__.'/../../config'));
@@ -34,23 +28,56 @@ public function load(array $configs, ContainerBuilder $container): void
$config['prefix'] = $container->getParameter('kernel.environment').'_';
}
+ 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']);
+ }
+
$container->setParameter('meili_url', $config['url'] ?? null);
$container->setParameter('meili_api_key', $config['api_key'] ?? null);
$container->setParameter('meili_symfony_version', MeilisearchBundle::qualifiedVersion());
- if (\count($doctrineSubscribedEvents = $config['doctrineSubscribedEvents']) > 0) {
- $container->getDefinition('search.search_indexer_subscriber')->setArgument(1, $doctrineSubscribedEvents);
+ if (\count($doctrineEvents = $config['doctrineSubscribedEvents']) > 0) {
+ $subscriber = $container->getDefinition('meilisearch.search_indexer_subscriber');
+
+ foreach ($doctrineEvents as $event) {
+ $subscriber->addTag('doctrine.event_listener', ['event' => $event]);
+ $subscriber->addTag('doctrine_mongodb.odm.event_listener', ['event' => $event]);
+ }
} else {
- $container->removeDefinition('search.search_indexer_subscriber');
+ $container->removeDefinition('meilisearch.search_indexer_subscriber');
}
- $engineDefinition = new Definition(Engine::class, [new Reference('search.client')]);
+ $container->findDefinition('meilisearch.client')
+ ->replaceArgument(0, $config['url'])
+ ->replaceArgument(1, $config['api_key'])
+ ->replaceArgument(2, new Reference($config['http_client'], ContainerInterface::IGNORE_ON_INVALID_REFERENCE))
+ ->replaceArgument(4, [MeilisearchBundle::qualifiedVersion()]);
+
+ $container->findDefinition('meilisearch.service')
+ ->replaceArgument(0, new Reference($config['serializer']))
+ ->replaceArgument(2, $config);
- $searchServiceDefinition = (new Definition(
- MeilisearchService::class,
- [new Reference($config['serializer']), $engineDefinition, $config]
- ));
+ if (Kernel::VERSION_ID >= 70100) {
+ $container->removeDefinition(UnixTimestampNormalizer::class);
+ }
+ }
+
+ /**
+ * @param array $settings
+ *
+ * @return array
+ */
+ private function findReferences(array $settings): array
+ {
+ foreach ($settings as $key => $value) {
+ if (\is_array($value)) {
+ $settings[$key] = $this->findReferences($value);
+ } elseif ('_service' === substr((string) $key, -8) || str_starts_with((string) $value, '@') || 'service' === $key) {
+ $settings[$key] = new Reference(ltrim($value, '@'));
+ }
+ }
- $container->setDefinition('search.service', $searchServiceDefinition->setPublic(true));
+ return $settings;
}
}
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..50b54528 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;
@@ -24,21 +21,20 @@ public function __construct(Client $client)
* This method allows you to create records on your index by sending one or more objects.
* Each object contains a set of attributes and values, which represents a full record on an index.
*
- * @param array|SearchableEntity $searchableEntities
+ * @param array|SearchableEntity $searchableEntities
*
* @throws ApiException
*/
public function index($searchableEntities): array
{
if ($searchableEntities instanceof SearchableEntity) {
- /** @var SearchableEntity[] $searchableEntities */
$searchableEntities = [$searchableEntities];
}
$data = [];
foreach ($searchableEntities as $entity) {
$searchableArray = $entity->getSearchableArray();
- if (null === $searchableArray || 0 === \count($searchableArray)) {
+ if ([] === $searchableArray) {
continue;
}
@@ -65,23 +61,17 @@ public function index($searchableEntities): array
* Remove objects from an index using their object UIDs.
* This method enables you to remove one or more objects from an index.
*
- * @param array|SearchableEntity $searchableEntities
+ * @param array|SearchableEntity $searchableEntities
*/
public function remove($searchableEntities): array
{
if ($searchableEntities instanceof SearchableEntity) {
- /** @var SearchableEntity[] $searchableEntities */
$searchableEntities = [$searchableEntities];
}
$data = [];
- /** @var SearchableEntity $entity */
foreach ($searchableEntities as $entity) {
- $searchableArray = $entity->getSearchableArray();
- if (0 === \count($searchableArray)) {
- continue;
- }
$indexUid = $entity->getIndexUid();
if (!isset($data[$indexUid])) {
@@ -93,9 +83,12 @@ public function remove($searchableEntities): array
$result = [];
foreach ($data as $indexUid => $objects) {
- $result[$indexUid] = $this->client
- ->index($indexUid)
- ->deleteDocument(reset($objects));
+ $result[$indexUid] = [];
+ foreach ($objects as $object) {
+ $result[$indexUid][] = $this->client
+ ->index($indexUid)
+ ->deleteDocument($object);
+ }
}
return $result;
@@ -104,16 +97,15 @@ 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
*/
public function clear(string $indexUid): array
{
$index = $this->client->index($indexUid);
- $task = $index->deleteAllDocuments();
- return $task;
+ return $index->deleteAllDocuments();
}
/**
@@ -129,11 +121,7 @@ public function delete(string $indexUid): ?array
*/
public function search(string $query, string $indexUid, array $searchParams): array
{
- if ('' === $query) {
- $query = null;
- }
-
- return $this->client->index($indexUid)->rawSearch($query, $searchParams);
+ return $this->client->index($indexUid)->rawSearch('' !== $query ? $query : null, $searchParams);
}
/**
@@ -146,7 +134,7 @@ public function count(string $query, string $indexName, array $searchParams): in
private function normalizeId($id)
{
- if (is_object($id) && method_exists($id, '__toString')) {
+ if (\is_object($id) && method_exists($id, '__toString')) {
return (string) $id;
}
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..61070374
--- /dev/null
+++ b/src/Event/SettingsUpdatedEvent.php
@@ -0,0 +1,61 @@
+index = $index;
+ $this->class = $class;
+ $this->setting = $setting;
+ }
+
+ /**
+ * @return class-string
+ */
+ public function getClass(): string
+ {
+ return $this->class;
+ }
+
+ /**
+ * @return non-empty-string
+ */
+ public function getIndex(): string
+ {
+ return $this->index;
+ }
+
+ /**
+ * @return non-empty-string
+ */
+ public function getSetting(): string
+ {
+ return $this->setting;
+ }
+}
diff --git a/src/EventListener/ConsoleOutputSubscriber.php b/src/EventListener/ConsoleOutputSubscriber.php
new file mode 100644
index 00000000..c497e90d
--- /dev/null
+++ b/src/EventListener/ConsoleOutputSubscriber.php
@@ -0,0 +1,31 @@
+io = $io;
+ }
+
+ public function afterSettingsUpdate(SettingsUpdatedEvent $event): void
+ {
+ $this->io->writeln('Setting "'.$event->getSetting().'" updated of "'.$event->getIndex().'".');
+ }
+
+ public static function getSubscribedEvents(): array
+ {
+ return [
+ SettingsUpdatedEvent::class => 'afterSettingsUpdate',
+ ];
+ }
+}
diff --git a/src/EventListener/DoctrineEventSubscriber.php b/src/EventListener/DoctrineEventSubscriber.php
index 7ece68c0..9f523fac 100644
--- a/src/EventListener/DoctrineEventSubscriber.php
+++ b/src/EventListener/DoctrineEventSubscriber.php
@@ -4,24 +4,16 @@
namespace Meilisearch\Bundle\EventListener;
-use Doctrine\Common\EventSubscriber;
use Doctrine\Persistence\Event\LifecycleEventArgs;
use Meilisearch\Bundle\SearchService;
-final class DoctrineEventSubscriber implements EventSubscriber
+final class DoctrineEventSubscriber
{
private SearchService $searchService;
- private array $subscribedEvents;
- public function __construct(SearchService $searchService, array $subscribedEvents)
+ public function __construct(SearchService $searchService)
{
$this->searchService = $searchService;
- $this->subscribedEvents = $subscribedEvents;
- }
-
- public function getSubscribedEvents(): array
- {
- return $this->subscribedEvents;
}
public function postUpdate(LifecycleEventArgs $args): void
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..83d0f75b
--- /dev/null
+++ b/src/Exception/InvalidIndiceException.php
@@ -0,0 +1,13 @@
+entity = $entity;
- if (count($entityIdentifierValues) > 1) {
+ if (\count($entityIdentifierValues) > 1) {
throw new InvalidEntityForAggregator("Aggregators don't support more than one primary key.");
}
@@ -71,16 +66,13 @@ public static function getEntityClassFromObjectID(string $objectId): string
{
$type = explode('::', $objectId)[0];
- if (in_array($type, static::getEntities(), true)) {
+ if (\in_array($type, static::getEntities(), true)) {
return $type;
}
throw new EntityNotFoundInObjectID("Entity class from ObjectID $objectId not found.");
}
- /**
- * {@inheritdoc}
- */
public function normalize(NormalizerInterface $normalizer, ?string $format = null, array $context = []): array
{
return array_merge(['objectID' => $this->objectID], $normalizer->normalize($this->entity, $format, $context));
diff --git a/src/SearchService.php b/src/SearchService.php
index c8bb2b02..9fbdcfef 100644
--- a/src/SearchService.php
+++ b/src/SearchService.php
@@ -6,25 +6,27 @@
use Doctrine\Persistence\ObjectManager;
-/**
- * Interface SearchService.
- */
interface SearchService
{
public const RESULT_KEY_HITS = 'hits';
public const RESULT_KEY_OBJECTID = 'objectID';
/**
- * @param string|object $className
+ * @param class-string|object $className
*/
public function isSearchable($className): bool;
+ /**
+ * @return list
+ */
public function getSearchable(): array;
public function getConfiguration(): Collection;
/**
* Get the index name for the given `$className`.
+ *
+ * @param class-string $className
*/
public function searchableAs(string $className): string;
@@ -32,12 +34,25 @@ public function index(ObjectManager $objectManager, $searchable): array;
public function remove(ObjectManager $objectManager, $searchable): array;
+ /**
+ * @param class-string $className
+ */
public function clear(string $className): array;
+ /**
+ * @param class-string $className
+ */
public function delete(string $className): ?array;
public function deleteByIndexName(string $indexName): ?array;
+ /**
+ * @template T of object
+ *
+ * @param class-string $className
+ *
+ * @return list
+ */
public function search(
ObjectManager $objectManager,
string $className,
@@ -49,6 +64,8 @@ public function search(
* Get the raw search result.
*
* @see https://docs.meilisearch.com/reference/api/search.html#response
+ *
+ * @param class-string $className
*/
public function rawSearch(
string $className,
@@ -56,5 +73,10 @@ public function rawSearch(
array $searchParams = []
): array;
+ /**
+ * @param class-string $className
+ *
+ * @return int<0, max>
+ */
public function count(string $className, string $query = '', array $searchParams = []): int;
}
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 abe88181..705be5c8 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;
@@ -25,14 +24,15 @@ final class SearchableEntity
private ?NormalizerInterface $normalizer;
- private bool $useSerializerGroups;
+ /**
+ * @var list
+ */
+ private array $normalizationGroups;
/** @var int|string */
private $id;
/**
- * SearchableEntity constructor.
- *
* @param object $entity
* @param ClassMetadata