├── .github ├── codecov.yml ├── dependabot.yml └── workflows │ ├── ci.yml │ ├── deploy_docs_3x.yml │ ├── deploy_docs_4x.yml │ └── stale.yml ├── .phive └── phars.xml ├── LICENSE.txt ├── README.md ├── composer.json ├── docker-compose.yml ├── docs ├── config │ ├── __init__.py │ └── all.py ├── en │ ├── 3-0-upgrade-guide.rst │ ├── 4-0-upgrade-guide.rst │ ├── conf.py │ ├── contents.rst │ └── index.rst ├── fr │ ├── conf.py │ ├── contents.rst │ └── index.rst ├── ja │ ├── conf.py │ ├── contents.rst │ └── index.rst └── pt │ ├── conf.py │ ├── contents.rst │ └── index.rst ├── phpstan-baseline.neon ├── phpstan.neon ├── psalm-baseline.xml ├── psalm.xml └── src ├── Association ├── EmbedMany.php ├── EmbedOne.php └── Embedded.php ├── Datasource ├── Connection.php ├── IndexLocator.php ├── Log │ └── ElasticLogger.php ├── MappingSchema.php └── SchemaCollection.php ├── Document.php ├── Exception ├── MissingDocumentException.php ├── MissingIndexClassException.php └── NotImplementedException.php ├── Index.php ├── IndexRegistry.php ├── Marshaller.php ├── Plugin.php ├── Query.php ├── QueryBuilder.php ├── ResultSet.php ├── Rule └── IsUnique.php ├── TestSuite ├── Fixture │ ├── DeleteQueryStrategy.php │ └── MappingGenerator.php ├── TestCase.php └── TestFixture.php └── View └── Form └── DocumentContext.php /.github/codecov.yml: -------------------------------------------------------------------------------- 1 | codecov: 2 | require_ci_to_pass: yes 3 | 4 | coverage: 5 | range: "90...100" 6 | 7 | comment: false 8 | -------------------------------------------------------------------------------- /.github/dependabot.yml: -------------------------------------------------------------------------------- 1 | version: 2 2 | updates: 3 | - package-ecosystem: composer 4 | directory: "/" 5 | schedule: 6 | interval: daily 7 | open-pull-requests-limit: 10 8 | ignore: 9 | - dependency-name: phpunit/phpunit 10 | versions: 11 | - "> 6.0" 12 | - dependency-name: ruflin/elastica 13 | versions: 14 | - ">= 7.a, < 8" 15 | -------------------------------------------------------------------------------- /.github/workflows/ci.yml: -------------------------------------------------------------------------------- 1 | name: CI 2 | 3 | on: 4 | push: 5 | branches: 6 | - 4.x 7 | pull_request: 8 | branches: 9 | - '*' 10 | workflow_dispatch: 11 | 12 | jobs: 13 | testsuite: 14 | runs-on: ubuntu-22.04 15 | strategy: 16 | fail-fast: false 17 | matrix: 18 | php-version: ['8.1'] 19 | prefer-lowest: [''] 20 | include: 21 | - php-version: '8.1' 22 | prefer-lowest: 'prefer-lowest' 23 | 24 | services: 25 | elasticsearch: 26 | image: elasticsearch:7.17.8 27 | ports: 28 | - 9200/tcp 29 | env: 30 | discovery.type: single-node 31 | ES_JAVA_OPTS: -Xms500m -Xmx500m 32 | options: >- 33 | --health-cmd "curl http://127.0.0.1:9200/_cluster/health" 34 | --health-interval 10s 35 | --health-timeout 5s 36 | --health-retries 10 37 | 38 | steps: 39 | - uses: actions/checkout@v4 40 | with: 41 | persist-credentials: false 42 | 43 | - name: Setup PHP 44 | uses: shivammathur/setup-php@v2 45 | with: 46 | php-version: ${{ matrix.php-version }} 47 | extensions: mbstring, intl, apcu 48 | ini-values: apc.enable_cli = 1 49 | coverage: pcov 50 | 51 | - name: Get composer cache directory 52 | id: composer-cache 53 | run: echo "::set-output name=dir::$(composer config cache-files-dir)" 54 | 55 | - name: Get date part for cache key 56 | id: key-date 57 | run: echo "::set-output name=date::$(date +'%Y-%m')" 58 | 59 | - name: Cache composer dependencies 60 | uses: actions/cache@v4 61 | with: 62 | path: ${{ steps.composer-cache.outputs.dir }} 63 | key: ${{ runner.os }}-composer-${{ steps.key-date.outputs.date }}-${{ hashFiles('composer.json') }}-${{ matrix.prefer-lowest }} 64 | 65 | - name: Composer install 66 | run: | 67 | if ${{ matrix.prefer-lowest == 'prefer-lowest' }}; then 68 | composer update --prefer-lowest --prefer-stable 69 | else 70 | composer update 71 | fi 72 | 73 | - name: Setup problem matchers for PHPUnit 74 | if: matrix.php-version == '8.1' 75 | run: echo "::add-matcher::${{ runner.tool_cache }}/phpunit.json" 76 | 77 | - name: Run PHPUnit 78 | env: 79 | DB_URL: Cake\ElasticSearch\Datasource\Connection://127.0.0.1:${{ job.services.elasticsearch.ports['9200'] }}?driver=Cake\ElasticSearch\Datasource\Connection 80 | run: | 81 | if [[ ${{ matrix.php-version }} == '8.1' ]]; then 82 | export CODECOVERAGE=1 && vendor/bin/phpunit --display-incomplete --display-skipped --coverage-clover=coverage.xml 83 | else 84 | vendor/bin/phpunit 85 | fi 86 | 87 | - name: Submit code coverage 88 | if: matrix.php-version == '8.1' 89 | uses: codecov/codecov-action@v3 90 | 91 | cs-stan: 92 | name: Coding Standard & Static Analysis 93 | runs-on: ubuntu-22.04 94 | 95 | services: 96 | elasticsearch: 97 | image: elasticsearch:7.17.8 98 | ports: 99 | - 9200/tcp 100 | env: 101 | discovery.type: single-node 102 | ES_JAVA_OPTS: -Xms500m -Xmx500m 103 | options: >- 104 | --health-cmd "curl http://127.0.0.1:9200/_cluster/health" 105 | --health-interval 10s 106 | --health-timeout 5s 107 | --health-retries 10 108 | 109 | steps: 110 | - uses: actions/checkout@v4 111 | with: 112 | persist-credentials: false 113 | 114 | - name: Setup PHP 115 | uses: shivammathur/setup-php@v2 116 | with: 117 | php-version: '8.1' 118 | extensions: mbstring, intl, apcu 119 | tools: cs2pr 120 | coverage: none 121 | 122 | - name: Composer install 123 | uses: ramsey/composer-install@v2 124 | 125 | - name: Install PHIVE 126 | uses: szepeviktor/phive@v1 127 | with: 128 | home: ${{ runner.temp }}/.phive 129 | binPath: ${{ github.workspace }}/tools/phive 130 | 131 | - name: Stan setup 132 | uses: szepeviktor/phive-install@v1 133 | with: 134 | home: ${{ runner.temp }}/.phive 135 | binPath: ${{ github.workspace }}/tools/phive 136 | trustGpgKeys: "CF1A108D0E7AE720,12CE0F1D262429A5,51C67305FFC2E5C0" 137 | 138 | - name: Run PHP CodeSniffer 139 | run: vendor/bin/phpcs --report=checkstyle src/ tests/ | cs2pr 140 | 141 | - name: Run psalm 142 | env: 143 | DB_URL: Cake\ElasticSearch\Datasource\Connection://127.0.0.1:${{ job.services.elasticsearch.ports['9200'] }}?driver=Cake\ElasticSearch\Datasource\Connection 144 | if: always() 145 | run: tools/psalm --output-format=github 146 | 147 | - name: Run phpstan 148 | env: 149 | DB_URL: Cake\ElasticSearch\Datasource\Connection://127.0.0.1:${{ job.services.elasticsearch.ports['9200'] }}?driver=Cake\ElasticSearch\Datasource\Connection 150 | if: always() 151 | run: tools/phpstan analyse --error-format=github 152 | -------------------------------------------------------------------------------- /.github/workflows/deploy_docs_3x.yml: -------------------------------------------------------------------------------- 1 | --- 2 | name: 'deploy_docs_3x' 3 | 4 | on: 5 | push: 6 | branches: 7 | - 3.x 8 | workflow_dispatch: 9 | 10 | jobs: 11 | deploy: 12 | runs-on: ubuntu-latest 13 | steps: 14 | - name: Cloning repo 15 | uses: actions/checkout@v2 16 | with: 17 | fetch-depth: 0 18 | 19 | - name: Push to dokku 20 | uses: dokku/github-action@master 21 | with: 22 | git_remote_url: 'ssh://dokku@apps.cakephp.org:22/elasticsearch-docs-3' 23 | ssh_private_key: ${{ secrets.DOKKU_SSH_PRIVATE_KEY }} 24 | -------------------------------------------------------------------------------- /.github/workflows/deploy_docs_4x.yml: -------------------------------------------------------------------------------- 1 | --- 2 | name: 'deploy_docs_4x' 3 | 4 | on: 5 | push: 6 | branches: 7 | - 4.x 8 | workflow_dispatch: 9 | 10 | jobs: 11 | deploy: 12 | runs-on: ubuntu-latest 13 | steps: 14 | - name: Cloning repo 15 | uses: actions/checkout@v2 16 | with: 17 | fetch-depth: 0 18 | 19 | - name: Push to dokku 20 | uses: dokku/github-action@master 21 | with: 22 | git_remote_url: 'ssh://dokku@apps.cakephp.org:22/elasticsearch-docs-4' 23 | ssh_private_key: ${{ secrets.DOKKU_SSH_PRIVATE_KEY }} 24 | -------------------------------------------------------------------------------- /.github/workflows/stale.yml: -------------------------------------------------------------------------------- 1 | name: Mark stale issues and pull requests 2 | 3 | on: 4 | schedule: 5 | - cron: "0 0 * * *" 6 | 7 | jobs: 8 | stale: 9 | 10 | runs-on: ubuntu-latest 11 | 12 | steps: 13 | - uses: actions/stale@v1 14 | with: 15 | repo-token: ${{ secrets.GITHUB_TOKEN }} 16 | stale-issue-message: 'This issue is stale because it has been open for 120 days with no activity. Remove the `stale` label or comment or this will be closed in 15 days' 17 | stale-pr-message: 'This pull request is stale because it has been open 30 days with no activity. Remove the `stale` label or comment on this issue, or it will be closed in 15 days' 18 | stale-issue-label: 'stale' 19 | stale-pr-label: 'stale' 20 | days-before-stale: 120 21 | days-before-close: 15 22 | exempt-issue-label: 'pinned' 23 | exempt-pr-label: 'pinned' 24 | -------------------------------------------------------------------------------- /.phive/phars.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /LICENSE.txt: -------------------------------------------------------------------------------- 1 | The MIT License 2 | 3 | CakePHP(tm) : The Rapid Development PHP Framework (https://cakephp.org) 4 | Copyright (c) 2005-present, Cake Software Foundation, Inc. 5 | 6 | Permission is hereby granted, free of charge, to any person obtaining a 7 | copy of this software and associated documentation files (the "Software"), 8 | to deal in the Software without restriction, including without limitation 9 | the rights to use, copy, modify, merge, publish, distribute, sublicense, 10 | and/or sell copies of the Software, and to permit persons to whom the 11 | Software is furnished to do so, subject to the following conditions: 12 | 13 | The above copyright notice and this permission notice shall be included in 14 | all copies or substantial portions of the Software. 15 | 16 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 17 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 18 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 19 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 20 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING 21 | FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER 22 | DEALINGS IN THE SOFTWARE. 23 | 24 | Cake Software Foundation, Inc. 25 | 1785 E. Sahara Avenue, 26 | Suite 490-204 27 | Las Vegas, Nevada 89104, 28 | United States of America. 29 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Elasticsearch Datasource for CakePHP 2 | 3 | ![Build Status](https://github.com/cakephp/elastic-search/actions/workflows/ci.yml/badge.svg?branch=master) 4 | [![Latest Stable Version](https://img.shields.io/github/v/release/cakephp/elastic-search?sort=semver&style=flat-square)](https://packagist.org/packages/cakephp/elastic-search) 5 | [![Total Downloads](https://img.shields.io/packagist/dt/cakephp/elastic-search?style=flat-square)](https://packagist.org/packages/cakephp/elastic-search/stats) 6 | [![Code Coverage](https://img.shields.io/coveralls/cakephp/elastic-search/master.svg?style=flat-square)](https://coveralls.io/r/cakephp/elastic-search?branch=master) 7 | [![Software License](https://img.shields.io/badge/license-MIT-brightgreen.svg?style=flat-square)](LICENSE) 8 | 9 | Use [Elastic Search](https://www.elastic.co/) as an alternative ORM backend in CakePHP 5.0+. 10 | 11 | You can [find the documentation for the plugin in the Cake Book](https://book.cakephp.org/elasticsearch). 12 | 13 | ## Installing Elasticsearch via composer 14 | 15 | You can install Elasticsearch into your project using 16 | [composer](https://getcomposer.org). For existing applications you can add the 17 | following to your `composer.json` file: 18 | 19 | "require": { 20 | "cakephp/elastic-search": "^4.0" 21 | } 22 | 23 | And run `php composer.phar update` 24 | 25 | ### Versions Table 26 | 27 | | Cake\ElasticSearch | CakePHP | ElasticSearch | 28 | | --- | --- | --- | 29 | | [1.x](https://github.com/cakephp/elastic-search/tree/1.0) | 3.0 - 3.5 | 2.x - 5.x | 30 | | [2.x](https://github.com/cakephp/elastic-search/tree/2.x) | 3.6+ | 6.x | 31 | | [>3, <3.4.0](https://github.com/cakephp/elastic-search/tree/3.3.0) | 4.0+ | 6.x | 32 | | [>=3.4.0](https://github.com/cakephp/elastic-search/tree/3.x) | 4.0+ | 7.x | 33 | | [4.x](https://github.com/cakephp/elastic-search/tree/4.x) | 5.0+ | 7.x | 34 | 35 | You are seeing the 3.x version. 36 | 37 | ## Connecting the Plugin to your Application 38 | 39 | After installing, you should tell your application to load the plugin: 40 | 41 | ```php 42 | use Cake\ElasticSearch\Plugin as ElasticSearchPlugin; 43 | 44 | class Application extends BaseApplication 45 | { 46 | public function bootstrap() 47 | { 48 | $this->addPlugin(ElasticSearchPlugin::class); 49 | 50 | // If you want to disable to automatically configure the Elastic model provider 51 | // and FormHelper do the following: 52 | // $this->addPlugin(ElasticSearchPlugin::class, [ 'bootstrap' => false ]); 53 | } 54 | } 55 | ``` 56 | 57 | ## Defining a connection 58 | 59 | Before you can do any work with Elasticsearch models, you'll need to define 60 | a connection: 61 | 62 | ```php 63 | // in config/app.php 64 | 'Datasources' => [ 65 | // other datasources 66 | 'elastic' => [ 67 | 'className' => 'Cake\ElasticSearch\Datasource\Connection', 68 | 'driver' => 'Cake\ElasticSearch\Datasource\Connection', 69 | 'host' => '127.0.0.1', 70 | 'port' => 9200 71 | ], 72 | ] 73 | ``` 74 | As an alternative you could use a link format if you like to use enviroment variables for example. 75 | 76 | ```php 77 | // in config/app.php 78 | 'Datasources' => [ 79 | // other datasources 80 | 'elastic' => [ 81 | 'url' => env('ELASTIC_URL', null) 82 | ] 83 | ] 84 | 85 | // and make sure the folowing env variable is available: 86 | // ELASTIC_URL="Cake\ElasticSearch\Datasource\Connection://127.0.0.1:9200?driver=Cake\ElasticSearch\Datasource\Connection" 87 | ``` 88 | 89 | You can enable request logging by setting the `log` config option to true. By 90 | default the `debug` Log profile will be used. You can also 91 | define an `elasticsearch` log profile in `Cake\Log\Log` to customize where 92 | Elasticsearch query logs will go. Query logging is done at a 'debug' level. 93 | 94 | ## Getting a Index object 95 | 96 | Index objects are the equivalent of `ORM\Table` instances in elastic search. You can 97 | use the `IndexRegistry` factory to get instances, much like `TableRegistry`: 98 | 99 | ```php 100 | use Cake\ElasticSearch\IndexRegistry; 101 | 102 | $comments = IndexRegistry::get('Comments'); 103 | ``` 104 | 105 | If you have loaded the plugin with bootstrap enabled you could load indexes using the model factory in your controllers 106 | ```php 107 | class SomeController extends AppController 108 | { 109 | public function initialize() 110 | { 111 | $this->loadModel('Comments', 'Elastic'); 112 | } 113 | 114 | public function index() 115 | { 116 | $comments = $this->Comments->find(); 117 | } 118 | 119 | ... 120 | ``` 121 | 122 | Each `Index` object needs a correspondent Elasticsearch _index_, just like most of `ORM\Table` needs a database _table_. 123 | 124 | In the above example, if you have defined a class as `CommentsIndex` and the `IndexRegistry` can find it, the `$comments` will receive a initialized object with inner configurations of connection and index. But if you don't have that class, a default one will be initialized and the index name on Elasticsearch mapped to the class. 125 | 126 | ## The Index class 127 | 128 | You must create your own `Index` class to define the name of internal _index_ 129 | for Elasticsearch, as well as to define the mapping type and define any entity 130 | properties you need like virtual properties. As you have to 131 | [use only one mapping type for each _index_](https://www.elastic.co/guide/en/elasticsearch/reference/master/removal-of-types.html), 132 | you can use the same name for both (the default behavior when _type_ is 133 | undefined is use singular version of _index_ name). Index types were removed 134 | in ElasticSearch 7. 135 | 136 | ```php 137 | use Cake\ElasticSearch\Index; 138 | 139 | class CommentsIndex extends Index 140 | { 141 | /** 142 | * The name of index in Elasticsearch 143 | * 144 | * @return string 145 | */ 146 | public function getName() 147 | { 148 | return 'comments'; 149 | } 150 | } 151 | ``` 152 | 153 | ## Running tests 154 | 155 | We recommend using the included `docker-compose.yml` for doing local 156 | development. The `Dockerfile` contains the development environment, and an 157 | Elasticsearch container will be downloaded and started on port 9200. 158 | 159 | ```bash 160 | # Start elasticsearch 161 | docker-compose up -d 162 | 163 | # Open an terminal in the development environment 164 | docker-compose run console bash 165 | ``` 166 | 167 | Once inside the container you can install dependencies and run tests. 168 | 169 | ```bash 170 | ./composer.phar install 171 | vendor/bin/phpunit 172 | ``` 173 | 174 | **Warning**: Please, be very carefully when running tests as the Fixture will 175 | create and drop Elasticsearch indexes for its internal structure. Don't run tests 176 | in production or development machines where you have important data into your 177 | Elasticsearch instance. 178 | 179 | Assuming you have PHPUnit installed system wide using one of the methods stated 180 | [here](https://phpunit.de/manual/current/en/installation.html), you can run the 181 | tests for CakePHP by doing the following: 182 | 183 | 1. Copy `phpunit.xml.dist` to `phpunit.xml` 184 | 2. Run `phpunit` 185 | -------------------------------------------------------------------------------- /composer.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "cakephp/elastic-search", 3 | "description": "An Elastic Search datasource and data mapper for CakePHP", 4 | "type": "cakephp-plugin", 5 | "keywords": ["cakephp", "elasticsearch"], 6 | "homepage": "https://github.com/cakephp/elastic-search", 7 | "license": "MIT", 8 | "authors": [ 9 | { 10 | "name": "CakePHP Community", 11 | "homepage": "https://github.com/cakephp/elastic-search/graphs/contributors" 12 | } 13 | ], 14 | "support": { 15 | "issues": "https://github.com/cakephp/elastic-search/issues", 16 | "forum": "https://stackoverflow.com/tags/cakephp", 17 | "irc": "irc://irc.freenode.org/cakephp", 18 | "source": "https://github.com/cakephp/elastic-search" 19 | }, 20 | "require": { 21 | "cakephp/cakephp": "^5.0.0", 22 | "ruflin/elastica": "^7.1" 23 | }, 24 | "require-dev": { 25 | "cakephp/cakephp-codesniffer": "^5.0", 26 | "phpunit/phpunit": "^10.1.0" 27 | }, 28 | "autoload": { 29 | "psr-4": { 30 | "Cake\\ElasticSearch\\": "src" 31 | } 32 | }, 33 | "autoload-dev": { 34 | "psr-4": { 35 | "Cake\\ElasticSearch\\Test\\": "tests", 36 | "TestApp\\": "tests/testapp/TestApp/src/", 37 | "TestPlugin\\": "tests/testapp/Plugin/TestPlugin/src", 38 | "TestPluginTwo\\": "tests/testapp/Plugin/TestPluginTwo/src" 39 | } 40 | }, 41 | "config": { 42 | "allow-plugins": { 43 | "dealerdirect/phpcodesniffer-composer-installer": true 44 | } 45 | }, 46 | "scripts": { 47 | "check": [ 48 | "@test", 49 | "@cs-check" 50 | ], 51 | "cs-check": "phpcs --colors -p ./src ./tests", 52 | "cs-fix": "phpcbf --colors -p src/ tests/", 53 | "test": "phpunit --colors=always", 54 | "stan": [ 55 | "@phpstan", 56 | "@psalm" 57 | ], 58 | "phpstan": "tools/phpstan analyse", 59 | "psalm": "tools/psalm --show-info=false", 60 | "phpstan-baseline": "tools/phpstan --generate-baseline", 61 | "psalm-baseline": "tools/psalm --set-baseline=psalm-baseline.xml", 62 | "stan-setup": "phive install" 63 | } 64 | } 65 | -------------------------------------------------------------------------------- /docker-compose.yml: -------------------------------------------------------------------------------- 1 | version: "3.9" 2 | services: 3 | console: 4 | build: "." 5 | environment: 6 | DB_URL: "Cake\\ElasticSearch\\Datasource\\Connection://elasticsearch:9200?driver=Cake\\ElasticSearch\\Datasource\\Connection" 7 | volumes: 8 | - .:/code 9 | elasticsearch: 10 | image: "elasticsearch:7.17.4" 11 | ports: 12 | - 9200/tcp 13 | environment: 14 | discovery.type: single-node 15 | ES_JAVA_OPTS: -Xms500m -Xmx500m 16 | healthcheck: 17 | test: "curl -f http://127.0.0.1:9200/_cluster/health || exit 1" 18 | interval: "10s" 19 | timeout: "5s" 20 | retries: 10 21 | start_period: "30s" 22 | -------------------------------------------------------------------------------- /docs/config/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/cakephp/elastic-search/7614466266d6cd7260c7c89ae6f0eb4faa0dcd60/docs/config/__init__.py -------------------------------------------------------------------------------- /docs/config/all.py: -------------------------------------------------------------------------------- 1 | # Global configuration information used across all the 2 | # translations of documentation. 3 | # 4 | # Import the base theme configuration 5 | from cakephpsphinx.config.all import * 6 | 7 | # The version info for the project you're documenting, acts as replacement for 8 | # |version| and |release|, also used in various other places throughout the 9 | # built documents. 10 | # 11 | 12 | # The full version, including alpha/beta/rc tags. 13 | release = '4.x' 14 | 15 | # The search index version. 16 | search_version = 'elasticsearch-4' 17 | 18 | # The marketing display name for the book. 19 | version_name = '' 20 | 21 | # Project name shown in the black header bar 22 | project = 'CakePHP ElasticSearch' 23 | 24 | # Other versions that display in the version picker menu. 25 | version_list = [ 26 | {'name': '4.x', 'number': '/elasticsearch/4', 'title': '4.x', 'current': True}, 27 | {'name': '3.x', 'number': '/elasticsearch/3', 'title': '3.x',}, 28 | {'name': '2.x', 'number': '/elasticsearch/2', 'title': '2.x'}, 29 | ] 30 | 31 | # Languages available. 32 | languages = ['en', 'fr', 'ja', 'pt'] 33 | 34 | # The GitHub branch name for this version of the docs 35 | # for edit links to point at. 36 | branch = '4.x' 37 | 38 | # Current version being built 39 | version = '4.x' 40 | 41 | show_root_link = True 42 | 43 | repository = 'cakephp/elastic-search' 44 | 45 | source_path = 'docs/' 46 | 47 | hide_page_contents = ('search', '404', 'contents') 48 | -------------------------------------------------------------------------------- /docs/en/3-0-upgrade-guide.rst: -------------------------------------------------------------------------------- 1 | 3.0 Upgrade Guide 2 | ################# 3 | 4 | If you are upgrading from an earlier version of this plugin, this page aims to 5 | collect all the changes you may need to make to your application while 6 | upgrading. 7 | 8 | Types Renamed to Indexes 9 | ======================== 10 | 11 | Because of the changes made in elasticsearch 5 and 6, this plugin no longer 12 | supports multiple types in the same index. The impact of this is that all of 13 | your type classes need to be renamed to indexes. For example 14 | ``App\Model\Type\ArticlesType`` needs to become 15 | ``App\Model\Index\ArticlesIndex``. Furthermore, Index classes assume that the 16 | type mapping has the singular name of the index. For example the ``articles`` 17 | index has a type mapping of ``article``. 18 | 19 | Breaking Changes 20 | ================ 21 | 22 | * ``Index::entityClass()`` was removed. Use ``getEntityClass()`` or 23 | ``setEntityClass()`` instead. 24 | * ``ResultSet::hasFacets()`` was removed as elastica no longer exposes this 25 | method. 26 | * ``ResultSet::getFacets()`` was removed as elastica no longer exposes this 27 | method. 28 | * ``ResultSet::getFacets()`` was removed as elastica no longer exposes this 29 | method. 30 | * The ``Type`` base class is now ``Index``. 31 | * ``TypeRegistry`` is now ``IndexRegistry``. 32 | -------------------------------------------------------------------------------- /docs/en/4-0-upgrade-guide.rst: -------------------------------------------------------------------------------- 1 | 4.0 Upgrade Guide 2 | ################# 3 | 4 | .. warning:: 5 | CakePHP ElasticSearch 4.x requires CakePHP 5.x. 6 | 7 | Breaking Changes 8 | ================ 9 | 10 | * All methods have had native types added where possible. This improves type 11 | safety within this plugin but may cause errors with application code. 12 | * ``Query`` no longer uses ``Cake\ORM\QueryTrait``. This has allowed several 13 | unused methods to be removed. 14 | * ``Query::isEagerLoaded()``, and ``Query::eagerLoaded()`` were removed. 15 | Previously these methods were inherited from ``QueryTrait`` but served no 16 | purpose here. 17 | 18 | 19 | Indexes 20 | ====== 21 | 22 | IndexRegistry has been deprecated. 23 | 24 | Old code example:: 25 | 26 | use Cake\ElasticSearch\IndexRegistry; 27 | 28 | $articles = IndexRegistry::get('Articles'); 29 | 30 | 31 | New code example:: 32 | 33 | use Cake\Datasource\FactoryLocator; 34 | 35 | $articles = FactoryLocator::get('ElasticSearch')->get('Articles'); 36 | -------------------------------------------------------------------------------- /docs/en/conf.py: -------------------------------------------------------------------------------- 1 | import sys, os 2 | 3 | # Append the top level directory of the docs, so we can import from the config dir. 4 | sys.path.insert(0, os.path.abspath('..')) 5 | 6 | # Pull in all the configuration options defined in the global config file.. 7 | from config.all import * 8 | 9 | # Language in use for this directory. 10 | language = 'en' 11 | -------------------------------------------------------------------------------- /docs/en/contents.rst: -------------------------------------------------------------------------------- 1 | Contents 2 | ######## 3 | 4 | .. toctree:: 5 | :maxdepth: 2 6 | :caption: CakePHP ElasticSearch 7 | 8 | /index 9 | /3-0-upgrade-guide 10 | /4-0-upgrade-guide 11 | -------------------------------------------------------------------------------- /docs/en/index.rst: -------------------------------------------------------------------------------- 1 | ElasticSearch 2 | ############# 3 | 4 | The ElasticSearch plugin provides an ORM-like abstraction on top of 5 | `elasticsearch `_. The plugin 6 | provides features that make testing, indexing documents and searching your 7 | indexes easier. 8 | 9 | Installation 10 | ============ 11 | 12 | To install the ElasticSearch plugin, you can use ``composer``. From your 13 | application's ROOT directory (where ``composer.json`` file is located) run the 14 | following:: 15 | 16 | php composer.phar require cakephp/elastic-search "^4.0" 17 | 18 | You will need to add the following line to your application's 19 | **src/Application.php** file:: 20 | 21 | $this->addPlugin('Cake/ElasticSearch'); 22 | 23 | Additionally, you will need to configure the 'elastic' datasource connection in 24 | your **config/app.php** file. An example configuration would be:: 25 | 26 | // in config/app.php 27 | 'Datasources' => [ 28 | // other datasources 29 | 'elastic' => [ 30 | 'className' => 'Cake\ElasticSearch\Datasource\Connection', 31 | 'driver' => 'Cake\ElasticSearch\Datasource\Connection', 32 | 'host' => '127.0.0.1', 33 | 'port' => 9200, 34 | 'index' => 'my_apps_index', 35 | ], 36 | ] 37 | 38 | If your endpoint requires https, use:: 39 | 40 | 'port' => 443, 41 | 'transport' => 'https' 42 | 43 | or you might get a 400 response back from the elasticsearch server. 44 | 45 | Overview 46 | ======== 47 | 48 | The ElasticSearch plugin makes it easier to interact with an elasticsearch index 49 | and provides an interface similar to the `ORM 50 | `__. To get started you should 51 | create an ``Index`` object. ``Index`` objects are the "Repository" or table-like 52 | class in elasticsearch:: 53 | 54 | // in src/Model/Type/ArticlesIndex.php 55 | namespace App\Model\Index; 56 | 57 | use Cake\ElasticSearch\Index; 58 | 59 | class ArticlesIndex extends Index 60 | { 61 | } 62 | 63 | You can then use your index class in your controllers:: 64 | 65 | public function beforeFilter(Event $event) 66 | { 67 | parent::beforeFilter($event); 68 | // Load the Index using the 'Elastic' provider. 69 | $this->loadModel('Articles', 'Elastic'); 70 | } 71 | 72 | public function add() 73 | { 74 | $article = $this->Articles->newEntity(); 75 | if ($this->request->is('post')) { 76 | $article = $this->Articles->patchEntity($article, $this->request->getData()); 77 | if ($this->Articles->save($article)) { 78 | $this->Flash->success('It saved'); 79 | } 80 | } 81 | $this->set(compact('article')); 82 | } 83 | 84 | We would also need to create a basic view for our indexed articles:: 85 | 86 | // in src/Template/Articles/add.ctp 87 | Form->create($article) ?> 88 | Form->control('title') ?> 89 | Form->control('body') ?> 90 | Form->button('Save') ?> 91 | Form->end() ?> 92 | 93 | You should now be able to submit the form and have a new document added to 94 | elasticsearch. 95 | 96 | Document Objects 97 | ================ 98 | 99 | Like the ORM, the Elasticsearch ODM uses ORM-like classes. The 100 | base class you should inherit from is ``Cake\ElasticSearch\Document``. Document 101 | classes are found in the ``Model\Document`` namespace in your application or 102 | plugin:: 103 | 104 | namespace App\Model\Document; 105 | 106 | use Cake\ElasticSearch\Document; 107 | 108 | class Article extends Document 109 | { 110 | } 111 | 112 | Outside of constructor logic that makes Documents work with data from 113 | elasticsearch, the interface and functionality provided by ``Document`` are the 114 | same as those in `Entities 115 | `__ 116 | 117 | Searching Indexed Documents 118 | =========================== 119 | 120 | After you've indexed some documents you will want to search through them. The 121 | ElasticSearch plugin provides a query builder that allows you to build search 122 | queries:: 123 | 124 | $query = $this->Articles->find() 125 | ->where([ 126 | 'title' => 'special', 127 | 'or' => [ 128 | 'tags in' => ['cake', 'php'], 129 | 'tags not in' => ['c#', 'java'] 130 | ] 131 | ]); 132 | 133 | foreach ($query as $article) { 134 | echo $article->title; 135 | } 136 | 137 | You can use the ``QueryBuilder`` to add filtering conditions:: 138 | 139 | $query->where(function ($builder) { 140 | return $builder->and( 141 | $builder->gt('views', 99), 142 | $builder->term('author.name', 'sally') 143 | ); 144 | }); 145 | 146 | The `QueryBuilder source 147 | `_ 148 | has the complete list of methods with examples for many commonly used methods. 149 | 150 | Validating Data & Using Application Rules 151 | ========================================= 152 | 153 | Like the ORM, the ElasticSearch plugin lets you validate data when marshalling 154 | documents. Validating request data, and applying application rules works the 155 | same as it does with the relational ORM. See the `validating request data 156 | `__ 157 | and `Application Rules 158 | `__ 159 | sections for more information. 160 | 161 | .. Need information on nested validators. 162 | 163 | Saving New Documents 164 | ==================== 165 | 166 | When you're ready to index some data into elasticsearch, you'll first need to 167 | convert your data into a ``Document`` that can be indexed:: 168 | 169 | $article = $this->Articles->newEntity($data); 170 | if ($this->Articles->save($article)) { 171 | // Document was indexed 172 | } 173 | 174 | When marshalling a document, you can specify which embedded documents you wish 175 | to marshall using the ``associated`` key:: 176 | 177 | $article = $this->Articles->newEntity($data, ['associated' => ['Comments']]); 178 | 179 | Saving a document will trigger the following events: 180 | 181 | * ``Model.beforeSave`` - Fired before the document is saved. You can prevent the 182 | save operation from happening by stopping this event. 183 | * ``Model.buildRules`` - Fired when the rules checker is built for the first 184 | time. 185 | * ``Model.afterSave`` - Fired after the document is saved. 186 | 187 | .. note:: 188 | There are no events for embedded documents, as the parent document and all 189 | of its embedded documents are saved as one operation. 190 | 191 | Updating Existing Documents 192 | =========================== 193 | 194 | When you need to re-index data, you can patch existing entities and re-save 195 | them:: 196 | 197 | $query = $this->Articles->find()->where(['user.name' => 'jill']); 198 | foreach ($query as $doc) { 199 | $doc->set($newProperties); 200 | $this->Articles->save($doc); 201 | } 202 | 203 | Additionally Elasticsearch ``refresh`` request can be triggered by passing 204 | ``'refresh' => true`` in the ``$options`` argument. A refresh makes recent 205 | operations performed on one or more indices available for search:: 206 | 207 | $this->Articles->save($article, ['refresh' => true]); 208 | 209 | Saving Multiple Documents 210 | ========================= 211 | 212 | Using this method you can bulk save multiple documents:: 213 | 214 | $result = $this->Articles->saveMany($documents); 215 | 216 | Here ``$documents`` is an array of documents. The result will be ``true`` on success or ``false`` on failure. 217 | ``saveMany`` can have second argument with the same options as accepted by ``save()``. 218 | 219 | 220 | Deleting Documents 221 | ================== 222 | 223 | After retrieving a document you can delete it:: 224 | 225 | $doc = $this->Articles->get($id); 226 | $this->Articles->delete($doc); 227 | 228 | You can also delete documents matching specific conditions:: 229 | 230 | $this->Articles->deleteAll(['user.name' => 'bob']); 231 | 232 | Embedding Documents 233 | =================== 234 | 235 | By defining embedded documents, you can attach entity classes to specific 236 | property paths in your documents. This allows you to provide custom behavior to 237 | the documents within a parent document. For example, you may want the comments 238 | embedded in an article to have specific application specific methods. You can 239 | use ``embedOne`` and ``embedMany`` to define embedded documents:: 240 | 241 | // in src/Model/Index/ArticlesIndex.php 242 | namespace App\Model\Index; 243 | 244 | use Cake\ElasticSearch\Index; 245 | 246 | class ArticlesIndex extends Index 247 | { 248 | public function initialize() 249 | { 250 | $this->embedOne('User'); 251 | $this->embedMany('Comments', [ 252 | 'entityClass' => 'MyComment' 253 | ]); 254 | } 255 | } 256 | 257 | The above would create two embedded documents on the ``Article`` document. The 258 | ``User`` embed will convert the ``user`` property to instances of 259 | ``App\Model\Document\User``. To get the Comments embed to use a class name 260 | that does not match the property name, we can use the ``entityClass`` option to 261 | configure a custom class name. 262 | 263 | Once we've setup our embedded documents, the results of ``find()`` and ``get()`` 264 | will return objects with the correct embedded document classes:: 265 | 266 | $article = $this->Articles->get($id); 267 | // Instance of App\Model\Document\User 268 | $article->user; 269 | 270 | // Array of App\Model\Document\Comment instances 271 | $article->comments; 272 | 273 | Configuring Connections 274 | ======================= 275 | 276 | By default all index instances use the ``elastic`` connection. If your 277 | application uses multiple connections you will want to configure which 278 | index use which connections. This is the ``defaultConnectionName()`` method:: 279 | 280 | namespace App\Model\Index; 281 | 282 | use Cake\ElasticSearch\Index; 283 | 284 | class ArticlesIndex extends Index 285 | { 286 | public static function defaultConnectionName() { 287 | return 'replica_db'; 288 | } 289 | } 290 | 291 | .. note:: 292 | 293 | The ``defaultConnectionName()`` method **must** be static. 294 | 295 | Getting Index Instances 296 | ======================= 297 | 298 | Like the ORM, the ElasticSearch plugin provides a factory/registry for getting 299 | ``Index`` instances:: 300 | 301 | use Cake\ElasticSearch\IndexRegistry; 302 | 303 | $articles = IndexRegistry::get('Articles'); 304 | 305 | Flushing the Registry 306 | --------------------- 307 | 308 | During test cases you may want to flush the registry. Doing so is often useful 309 | when you are using mock objects, or modifying a index's dependencies:: 310 | 311 | IndexRegistry::flush(); 312 | 313 | Test Fixtures 314 | ============= 315 | 316 | The ElasticSearch plugin provides a seamless test suite integration. Just like 317 | database fixtures, you can create test schema and fixture data elasticsearch. 318 | Much like database fixtures we load our Elasticsearch mappings during 319 | ``tests/bootstrap.php`` of our application:: 320 | 321 | // In tests/bootstrap.php 322 | use Cake\Elasticsearch\TestSuite\Fixture\MappingGenerator; 323 | 324 | $generator = new MappingGenerator('tests/mappings.php', 'test_elastic'); 325 | $generator->reload(); 326 | 327 | The above will create the indexes and mappings defined in ``tests/mapping.php`` 328 | and insert them into the ``test_elastic`` connection. The mappings in your 329 | ``mappings.php`` should return a list of mappings to create:: 330 | 331 | // in tests/mappings.php 332 | return [ 333 | [ 334 | // The name of the index and mapping. 335 | 'name' => 'articles', 336 | // The schema for the mapping. 337 | 'mapping' => [ 338 | 'id' => ['type' => 'integer'], 339 | 'title' => ['type' => 'text'], 340 | 'user_id' => ['type' => 'integer'], 341 | 'body' => ['type' => 'text'], 342 | 'created' => ['type' => 'date'], 343 | ], 344 | // Additional index settings. 345 | 'settings' => [ 346 | 'number_of_shards' => 2, 347 | 'number_of_routing_shards' => 2, 348 | ], 349 | ], 350 | // ... 351 | ]; 352 | 353 | Mappings use the `native elasticsearch mapping format 354 | `_. 355 | You can safely omit the type name and top level ``properties`` key. With our 356 | mappings loaded, we can define a test fixture for our Articles index with the 357 | following:: 358 | 359 | namespace App\Test\Fixture; 360 | 361 | use Cake\ElasticSearch\TestSuite\TestFixture; 362 | 363 | /** 364 | * Articles fixture 365 | */ 366 | class ArticlesFixture extends TestFixture 367 | { 368 | /** 369 | * The table/index for this fixture. 370 | * 371 | * @var string 372 | */ 373 | public $table = 'articles'; 374 | 375 | public $records = [ 376 | [ 377 | 'user' => [ 378 | 'username' => 'billy' 379 | ], 380 | 'title' => 'First Post', 381 | 'body' => 'Some content' 382 | ] 383 | ]; 384 | } 385 | 386 | .. versionchanged:: 3.4.0 387 | Prior to CakePHP 4.3.0 schema was defined on each fixture in the ``$schema`` 388 | property. 389 | 390 | Once your fixtures are created you can use them in your test cases by including 391 | them in your test's ``fixtures`` properties:: 392 | 393 | public $fixtures = ['app.Articles']; 394 | -------------------------------------------------------------------------------- /docs/fr/conf.py: -------------------------------------------------------------------------------- 1 | import sys, os 2 | 3 | # Append the top level directory of the docs, so we can import from the config dir. 4 | sys.path.insert(0, os.path.abspath('..')) 5 | 6 | # Pull in all the configuration options defined in the global config file.. 7 | from config.all import * 8 | 9 | # Language in use for this directory. 10 | language = 'fr' 11 | -------------------------------------------------------------------------------- /docs/fr/contents.rst: -------------------------------------------------------------------------------- 1 | Contents 2 | ######## 3 | 4 | .. toctree:: 5 | :maxdepth: 2 6 | :caption: CakePHP ElasticSearch 7 | 8 | /index 9 | -------------------------------------------------------------------------------- /docs/fr/index.rst: -------------------------------------------------------------------------------- 1 | ElasticSearch 2 | ############# 3 | 4 | Le plugin ElasticSearch fournit une abstraction de type ORM au-dessus de 5 | `elasticsearch `_. Le plugin 6 | fournit des fonctionnalités qui facilitent les tests, l'indexation des 7 | documents et la recherche de vos index. 8 | 9 | Installation 10 | ============ 11 | 12 | Pour installer le plugin elasticsearch, vous pouvez utiliser ``composer``. 13 | A partir du répertoire ROOT de votre application (où se trouve le fichier 14 | composer.json), lancez ce qui suit:: 15 | 16 | php composer.phar require cakephp/elastic-search "@stable" 17 | 18 | Vous devrez ajouter la ligne suivante au fichier **config/bootstrap.php** de 19 | votre application:: 20 | 21 | Plugin::load('Cake/ElasticSearch', ['bootstrap' => true]); 22 | 23 | De plus, vous devrez configurer la connection à la source de donnée 'elastic' 24 | dans votre fichier **config/app.php**. Un exemple de configuration serait:: 25 | 26 | // Dans config/app.php 27 | 'Datasources' => [ 28 | // Autres sources de données 29 | 'elastic' => [ 30 | 'className' => 'Cake\ElasticSearch\Datasource\Connection', 31 | 'driver' => 'Cake\ElasticSearch\Datasource\Connection', 32 | 'host' => '127.0.0.1', 33 | 'port' => 9200, 34 | 'index' => 'my_apps_index', 35 | ], 36 | ] 37 | 38 | Vue d'Ensemble 39 | ============== 40 | 41 | Le plugin elasticsearch facilite l'interaction avec un index elasticsearch 42 | et fournit une interface similaire à celle de l'`ORM 43 | `__. Pour commencer, 44 | vous devrez créer un objet ``Type``. Les objets ``Type`` sont le "Repository" 45 | ou la classe de type table dans elasticsearch:: 46 | 47 | // Dans src/Model/Type/ArticlesType.php 48 | namespace App\Model\Type; 49 | 50 | use Cake\ElasticSearch\Type; 51 | 52 | class ArticlesType extends Type 53 | { 54 | } 55 | 56 | Vous pouvez utiliser votre classe type dans vos controllers:: 57 | 58 | public function beforeFilter(Event $event) 59 | { 60 | parent::beforeFilter($event); 61 | // Charge le Type en utilisant le provider 'Elastic'. 62 | $this->loadModel('Articles', 'Elastic'); 63 | } 64 | 65 | public function add() 66 | { 67 | $article = $this->Articles->newEntity(); 68 | if ($this->request->is('post')) { 69 | $article = $this->Articles->patchEntity($article, $this->request->getData()); 70 | if ($this->Articles->save($article)) { 71 | $this->Flash->success('It saved'); 72 | } 73 | } 74 | $this->set(compact('article')); 75 | } 76 | 77 | Nous devrons aussi créer une vue basique pour nos articles indexés:: 78 | 79 | // Dans src/Template/Articles/add.ctp 80 | Form->create($article) ?> 81 | Form->control('title') ?> 82 | Form->control('body') ?> 83 | Form->button('Save') ?> 84 | Form->end() ?> 85 | 86 | Vous devriez maintenant pouvoir soumettre le formulaire et avoir un nouveau 87 | document ajouté à elasticsearch. 88 | 89 | Objets Document 90 | =============== 91 | 92 | Comme l'ORM, l'ODM Elasticsearch utilise les classes de type `Entities 93 | `__ . La classe de base que 94 | vous devrez hériter est ``Cake\ElasticSearch\Document``. Les classes de Document 95 | se trouvent dans le namespace ``Model\Document`` dans votre application ou votre 96 | plugin:: 97 | 98 | namespace App\Model\Document; 99 | 100 | use Cake\ElasticSearch\Document; 101 | 102 | class Article extends Document 103 | { 104 | } 105 | 106 | En dehors de la logique de constructeur qui fait fonctionner les Documents avec 107 | les données de elasticsearch, l'interface et les fonctionnalités fournies par 108 | ``Document`` sont les mêmes que celles des `Entities `__. 109 | 110 | Recherche des Documents Indexés 111 | =============================== 112 | 113 | Après avoir indexé quelques documents, vous voudrez chercher parmi ceux-ci. Le 114 | plugin elasticsearch fournit un constructeur de requête qui vous permet de 115 | construire les requêtes de recherche:: 116 | 117 | $query = $this->Articles->find() 118 | ->where([ 119 | 'title' => 'special', 120 | 'or' => [ 121 | 'tags in' => ['cake', 'php'], 122 | 'tags not in' => ['c#', 'java'] 123 | ] 124 | ]); 125 | 126 | foreach ($query as $article) { 127 | echo $article->title; 128 | } 129 | 130 | Vous pouvez utiliser le ``QueryBuilder`` pour ajouter des conditions de 131 | filtrage:: 132 | 133 | $query->where(function ($builder) { 134 | return $builder->and( 135 | $builder->gt('views', 99), 136 | $builder->term('author.name', 'sally') 137 | ); 138 | }); 139 | 140 | La `source de QueryBuilder 141 | `_ 142 | a la liste complète des méthodes avec des exemples pour beaucoup de méthodes 143 | couramment utilisées. 144 | 145 | Validation des Données & Utilisation des Règles d'Application 146 | ============================================================= 147 | 148 | Comme pour l'ORM, le plugin ElasticSearch vous laisse valider les données 149 | lors de la prise en compte des documents. Valider les données requêtées, et 150 | appliquer les règles d'application fonctionne de la même façon que pour 151 | l'ORM relationnel. Regardez les sections `Valider les Données Avant de Construire les Entities 152 | `__ 153 | et `Appliquer des Règles pour l’Application `__ s pour plus d'informations. 154 | 155 | Sauvegarder les Nouveaux Documents 156 | ================================== 157 | 158 | Quand vous êtes prêt à indexer quelques données dans elasticsearch, vous 159 | devrez d'abord convertir vos données dans un ``Document`` qui peut être 160 | indexé:: 161 | 162 | $article = $this->Articles->newEntity($data); 163 | if ($this->Articles->save($article)) { 164 | // Document a été indexé 165 | } 166 | 167 | Lors de la prise en compte d'un document, vous pouvez spécifier les documents 168 | intégrés que vous souhaitez prendre en compte en utilisant la clé 169 | ``associated``:: 170 | 171 | $article = $this->Articles->newEntity($data, ['associated' => ['Comments']]); 172 | 173 | Sauvegarder un document va récupérer les events suivants: 174 | 175 | * ``Model.beforeSave`` - Lancé avant que le document ne soit sauvegardé. En 176 | stoppant cet event, vous pouvez empêcher l'opération de sauvegarde de se 177 | produire. 178 | * ``Model.buildRules`` - Lancé quand les vérificateurs de règles sont 179 | construits pour la première fois. 180 | * ``Model.afterSave`` - Lancé après que le document est sauvegardé. 181 | 182 | .. note:: 183 | Il n'y a pas d'events pour les documents intégrés, puisque le document 184 | parent et tous ses documents intégrés sont sauvegardés en une opération. 185 | 186 | Mettre à Jour les Documents Existants 187 | ===================================== 188 | 189 | Quand vous devez réindexer les données, vous pouvez patch les entities 190 | existantes et les re-sauvegarder:: 191 | 192 | $query = $this->Articles->find()->where(['user.name' => 'jill']); 193 | foreach ($query as $doc) { 194 | $doc->set($newProperties); 195 | $this->Articles->save($doc); 196 | } 197 | 198 | Supprimer les Documents 199 | ======================= 200 | 201 | Après la récupération d'un document, vous pouvez le supprimer:: 202 | 203 | $doc = $this->Articles->get($id); 204 | $this->Articles->delete($doc); 205 | 206 | Vous pouvez aussi supprimer les documents qui matchent des conditions 207 | spécifiques:: 208 | 209 | $this->Articles->deleteAll(['user.name' => 'bob']); 210 | 211 | Documents Intégrés 212 | ================== 213 | 214 | En définissant les documents intégrés, vous pouvez attacher des classes entity 215 | à des chemins de propriété spécifique dans vos documents. Ceci vous permet 216 | de fournir un comportement personnalisé pour les documents dans un document 217 | parent. Par exemple, vous pouvez vouloir les commentaires intégrés à un 218 | article pour avoir des méthodes spécifiques selon l'application. Vous pouvez 219 | utiliser ``embedOne`` et ``embedMany`` pour définir les documents intégrés:: 220 | 221 | // Dans src/Model/Type/ArticlesType.php 222 | namespace App\Model\Type; 223 | 224 | use Cake\ElasticSearch\Type; 225 | 226 | class ArticlesType extends Type 227 | { 228 | public function initialize() 229 | { 230 | $this->embedOne('User'); 231 | $this->embedMany('Comments', [ 232 | 'entityClass' => 'MyComment' 233 | ]); 234 | } 235 | } 236 | 237 | Ce qui au-dessus va créer deux documents intégrés sur le document ``Article``. 238 | L'``User`` intégré va convertir la propriété ``user`` en instances de 239 | ``App\Model\Document\User``. Pour récupérer les Commentaires intégrés et 240 | utiliser un nom de classe qui ne correspond pas au nom de la propriété, nous 241 | pouvons utiliser l'option ``entityClass`` pour configurer un nom de classe 242 | personnalisé. 243 | 244 | Une fois que vous avez configuré nos documents intégrés, les résultats de 245 | ``find()`` et ``get()`` vont retourner les objets avec les bonnes classes 246 | de document intégré:: 247 | 248 | $article = $this->Articles->get($id); 249 | // Instance de App\Model\Document\User 250 | $article->user; 251 | 252 | // Array des instances App\Model\Document\Comment 253 | $article->comments; 254 | 255 | Récupérer les Instances Type 256 | ============================ 257 | 258 | Comme pour l'ORM, le plugin elasticsearch fournit un factory/registre pour 259 | récupérer les instances ``Type``:: 260 | 261 | use Cake\ElasticSearch\TypeRegistry; 262 | 263 | $articles = TypeRegistry::get('Articles'); 264 | 265 | Nettoyer le Registre 266 | -------------------- 267 | 268 | Pendant les cas de test, vous voudrez nettoyer le registre. Faire cela est 269 | souvent utile quand vous utilisez les objets de mock, ou quand vous modifiez 270 | les dépendances d'un type:: 271 | 272 | TypeRegistry::flush(); 273 | 274 | Fixtures de Test 275 | ================ 276 | 277 | Le plugin elasticsearch fournit seamless test suite integration. Un peu comme 278 | les fixtures de base de données, vous pouvez créer des fixtures de test pour 279 | elasticsearch. Nous pourrions définir une fixture de test pour notre type 280 | Articles avec ce qui suit:: 281 | 282 | namespace App\Test\Fixture; 283 | 284 | use Cake\ElasticSearch\TestSuite\TestFixture; 285 | 286 | /** 287 | * Articles fixture 288 | */ 289 | class ArticlesFixture extends TestFixture 290 | { 291 | /** 292 | * La table/type pour cette fixture. 293 | * 294 | * @var string 295 | */ 296 | public $table = 'articles'; 297 | 298 | /** 299 | * The mapping data. 300 | * 301 | * @var array 302 | */ 303 | public $schema = [ 304 | 'id' => ['type' => 'integer'], 305 | 'user' => [ 306 | 'type' => 'nested', 307 | 'properties' => [ 308 | 'username' => ['type' => 'string'], 309 | ] 310 | ] 311 | 'title' => ['type' => 'string'], 312 | 'body' => ['type' => 'string'], 313 | ]; 314 | 315 | public $records = [ 316 | [ 317 | 'user' => [ 318 | 'username' => 'billy' 319 | ], 320 | 'title' => 'First Post', 321 | 'body' => 'Some content' 322 | ] 323 | ]; 324 | } 325 | 326 | La propriété ``schema`` utilise le format de mapping `natif d'elasticsearch 327 | `_. 328 | Vous pouvez sans problème ne pas mettre le nom du type et la clé de niveau 329 | supérieur ``properties``. Une fois que vos fixtures sont créées, vous pouvez les 330 | utiliser dans vos cas de test en les incluant dans vos propriétés de test 331 | ``fixtures``:: 332 | 333 | public $fixtures = ['app.articles']; 334 | -------------------------------------------------------------------------------- /docs/ja/conf.py: -------------------------------------------------------------------------------- 1 | import sys, os 2 | 3 | # Append the top level directory of the docs, so we can import from the config dir. 4 | sys.path.insert(0, os.path.abspath('..')) 5 | 6 | # Pull in all the configuration options defined in the global config file.. 7 | from config.all import * 8 | 9 | # Language in use for this directory. 10 | language = 'ja' 11 | -------------------------------------------------------------------------------- /docs/ja/contents.rst: -------------------------------------------------------------------------------- 1 | Contents 2 | ######## 3 | 4 | .. toctree:: 5 | :maxdepth: 2 6 | :caption: CakePHP ElasticSearch 7 | 8 | /index 9 | -------------------------------------------------------------------------------- /docs/ja/index.rst: -------------------------------------------------------------------------------- 1 | ElasticSearch 2 | ############# 3 | 4 | ElasticSearch プラグインは、`elasticsearch `_ 5 | の上に ORM のような抽象化を提供します。そのプラグインは、テストの作成、 6 | ドキュメントのインデックス作成、インデックスをより簡単に検索などの機能を提供します。 7 | 8 | インストール 9 | ============ 10 | 11 | ElasticSearch プラグインをインストールするには、 ``composer`` が利用できます。(composer.json 12 | ファイルが置かれている) アプリケーションの ROOT ディレクトリーから次のコマンドを実行します。 :: 13 | 14 | php composer.phar require cakephp/elastic-search "@stable" 15 | 16 | 以下の1行をあなたのアプリケーションの **src/Application.php** ファイルに追加する必要があります。 :: 17 | 18 | $this->addPlugin('Cake/ElasticSearch', ['bootstrap' => true]); 19 | 20 | // 3.6.0 より前は、Plugin::load() を使用する必要があります 21 | 22 | 追加で 'elastic' のデータソースの接続を **config/app.php** ファイルに設定する必要があります。 23 | 設定例は以下のようになります。 :: 24 | 25 | // config/app.php の中で 26 | 'Datasources' => [ 27 | // 他のデータソース 28 | 'elastic' => [ 29 | 'className' => 'Cake\ElasticSearch\Datasource\Connection', 30 | 'driver' => 'Cake\ElasticSearch\Datasource\Connection', 31 | 'host' => '127.0.0.1', 32 | 'port' => 9200, 33 | 'index' => 'my_apps_index', 34 | ], 35 | ] 36 | 37 | 概要 38 | ==== 39 | 40 | ElasticSearch プラグインは elasticsearch インデックスと作用することを簡単にし、 41 | `ORM `__ に似たインターフェイスを提供します。まず最初に ``Type`` オブジェクトを 42 | 作成しなければいけません。 ``Type`` オブジェクトは elasticsearch 内では "Repository" 43 | もしくは Table のようなクラスです。 :: 44 | 45 | // src/Model/Type/ArticlesType.php の中で 46 | namespace App\Model\Type; 47 | 48 | use Cake\ElasticSearch\Type; 49 | 50 | class ArticlesType extends Type 51 | { 52 | } 53 | 54 | コントローラーで Type クラスを利用できます。 :: 55 | 56 | public function beforeFilter(Event $event) 57 | { 58 | parent::beforeFilter($event); 59 | // 'Elastic' プロバイダーを利用して Type を読み込む 60 | $this->loadModel('Articles', 'Elastic'); 61 | } 62 | 63 | public function add() 64 | { 65 | $article = $this->Articles->newEntity(); 66 | if ($this->request->is('post')) { 67 | $article = $this->Articles->patchEntity($article, $this->request->getData()); 68 | if ($this->Articles->save($article)) { 69 | $this->Flash->success('It saved'); 70 | } 71 | } 72 | $this->set(compact('article')); 73 | } 74 | 75 | インデックスされた articles の基本的なビューを作成する必要があります。 :: 76 | 77 | // src/Template/Articles/add.ctp の中で 78 | Form->create($article) ?> 79 | Form->control('title') ?> 80 | Form->control('body') ?> 81 | Form->button('Save') ?> 82 | Form->end() ?> 83 | 84 | これで、フォームの送信が可能になり、新しいドキュメントが elasticsearch に追加されました。 85 | 86 | Document オブジェクト 87 | ===================== 88 | 89 | ORM と同様に、Elasticsearch ODM は `エンティティー `__ のようなクラスを使用しています。 90 | 継承しなければならない基底クラスは ``Cake\ElasticSearch\Document`` です。 91 | Document クラスは、アプリケーションやプラグイン内の ``Model\Document`` 名前空間に配置します。 :: 92 | 93 | namespace App\Model\Document; 94 | 95 | use Cake\ElasticSearch\Document; 96 | 97 | class Article extends Document 98 | { 99 | } 100 | 101 | elasticsearch からのデータで Document を動作させるコンストラクターロジックの外側、 102 | インターフェイスと ``Document`` によって提供される機能は、 `Entities 103 | `__ 104 | 内にあるものと同じです。 105 | 106 | インデックス付きドキュメントの検索 107 | ================================== 108 | 109 | いくつかのドキュメントをインデックスに登録した後、あなたはそれらを検索したいと思うでしょう。 110 | ElasticSearch プラグインを使用すると、検索クエリーを構築するためのクエリービルダーを提供します。 :: 111 | 112 | $query = $this->Articles->find() 113 | ->where([ 114 | 'title' => 'special', 115 | 'or' => [ 116 | 'tags in' => ['cake', 'php'], 117 | 'tags not in' => ['c#', 'java'] 118 | ] 119 | ]); 120 | 121 | foreach ($query as $article) { 122 | echo $article->title; 123 | } 124 | 125 | フィルタリング条件を追加するために ``QueryBuilder`` を使用することができます。 :: 126 | 127 | $query->where(function ($builder) { 128 | return $builder->and( 129 | $builder->gt('views', 99), 130 | $builder->term('author.name', 'sally') 131 | ); 132 | }); 133 | 134 | `QueryBuilder のソース 135 | `_ 136 | は、多くの一般的に使用されるメソッドの例となるメソッドの完全なリストを持っています。 137 | 138 | データのバリデーションとアプリケーションルールの使用 139 | ==================================================== 140 | 141 | ORMと同様に、ElasticSearch プラグインは、ドキュメントをマーシャリングするときに 142 | データを検証することができます。リクエストデータのバリデート、およびアプリケーションルールの 143 | 適用は、リレーショナルORMと同じ動作をします。詳細については、 `エンティティー構築前のデータ検証 `__ と 144 | `条件付き/動的なエラーメッセージ `__ のセクションをご覧ください。 145 | 146 | .. ネストされたバリデータに関する情報を必要としています。 147 | 148 | 新しいドキュメントの保存 149 | ======================== 150 | 151 | elasticsearch にいくつかのデータをインデックスする準備ができたら、最初にインデックスが付けられる 152 | ``Document`` にデータを変換する必要があります。 :: 153 | 154 | $article = $this->Articles->newEntity($data); 155 | if ($this->Articles->save($article)) { 156 | // Document はインデックスされました 157 | } 158 | 159 | ドキュメントをマーシャリングするとき、 ``associated`` キーを使用してマーシャリングしたい 160 | 埋め込みドキュメントを指定することができます。 :: 161 | 162 | $article = $this->Articles->newEntity($data, ['associated' => ['Comments']]); 163 | 164 | ドキュメントを保存すると、次のイベントがトリガーされます: 165 | 166 | * ``Model.beforeSave`` - ドキュメントが保存される前に発生します。 167 | このイベントを停止することによって保存操作を防ぐことができます。 168 | * ``Model.buildRules`` - ルールチェッカーが最初に構築されているときに発生します。 169 | * ``Model.afterSave`` - ドキュメントが保存された後に発生します。 170 | 171 | .. note:: 172 | 親ドキュメントとすべての埋め込みドキュメントを1つの操作で保存するため、 173 | 埋め込みドキュメントのためのイベントはありません。 174 | 175 | 既存ドキュメントの更新 176 | ====================== 177 | 178 | データの再インデックスが必要な場合、既存のエンティティーにパッチを適用すると再保存できます。 :: 179 | 180 | $query = $this->Articles->find()->where(['user.name' => 'jill']); 181 | foreach ($query as $doc) { 182 | $doc->set($newProperties); 183 | $this->Articles->save($doc); 184 | } 185 | 186 | ドキュメントの削除 187 | ================== 188 | 189 | ドキュメントを検索した後、それを削除することができます。 :: 190 | 191 | $doc = $this->Articles->get($id); 192 | $this->Articles->delete($doc); 193 | 194 | また、特定の条件に一致するドキュメントを削除することができます。 :: 195 | 196 | $this->Articles->deleteAll(['user.name' => 'bob']); 197 | 198 | 埋め込みドキュメント 199 | ==================== 200 | 201 | 埋め込みドキュメントを定義することで、ドキュメント内の特定のプロパティーのパスに 202 | エンティティークラスを添付することができます。これは、親ドキュメント内のドキュメントに 203 | 独自の振る舞いを提供することができます。たとえば、あなたが記事に埋め込まれたコメントは、 204 | 特定のアプリケーション固有のメソッドを持っている場合があります。あなたが埋め込みドキュメントを 205 | 定義するために ``embedOne`` と ``embedMany`` を使用することができます。 :: 206 | 207 | // in src/Model/Type/ArticlesType.php 208 | namespace App\Model\Type; 209 | 210 | use Cake\ElasticSearch\Type; 211 | 212 | class ArticlesType extends Type 213 | { 214 | public function initialize() 215 | { 216 | $this->embedOne('User'); 217 | $this->embedMany('Comments', [ 218 | 'entityClass' => 'MyComment' 219 | ]); 220 | } 221 | } 222 | 223 | 上記の ``Article`` ドキュメント上の2つの埋め込みドキュメントを作成します。 224 | ``User`` 埋め込みは ``App\Model\Document\User`` のインスタンスに ``user`` プロパティーを変換します。 225 | プロパティー名と一致していないクラス名を使用する埋め込みコメントを得るためには、カスタムクラス名を 226 | 設定するための ``entityClass`` オプションを使用することができます。 227 | 228 | 埋め込みドキュメントをセットアップしたら、 ``find()`` と ``get`` の結果は 229 | 正しい埋め込みドキュメントクラスのオブジェクトを返します。 :: 230 | 231 | $article = $this->Articles->get($id); 232 | // App\Model\Document\User のインスタンス 233 | $article->user; 234 | 235 | // App\Model\Document\Comment インスタンスの配列 236 | $article->comments; 237 | 238 | Type インスタンスの取得 239 | ======================= 240 | 241 | ORM と同様に、ElasticSearch プラグインは ``Type`` のインスタンスを取得するための 242 | ファクトリー/レジストリーを提供します。 :: 243 | 244 | use Cake\ElasticSearch\TypeRegistry; 245 | 246 | $articles = TypeRegistry::get('Articles'); 247 | 248 | レジストリーのフラッシュ 249 | ------------------------ 250 | 251 | テストケースの中で、レジストリーをフラッシュすることができます。 252 | そうすることでモックオブジェクトを使用したり、Type の依存関係を変更する際に便利です。 :: 253 | 254 | TypeRegistry::flush(); 255 | 256 | テストフィクスチャー 257 | ==================== 258 | 259 | ElasticSearch プラグインは、シームレスなテストスイートの統合を提供します。ちょうどデータベースの 260 | フィクスチャーのように、elasticsearch のためのテストフィクスチャーを作成することができます。 261 | 次のように Articles タイプのテストフィクスチャーを定義することができます。 :: 262 | 263 | namespace App\Test\Fixture; 264 | 265 | use Cake\ElasticSearch\TestSuite\TestFixture; 266 | 267 | /** 268 | * Articles fixture 269 | */ 270 | class ArticlesFixture extends TestFixture 271 | { 272 | /** 273 | * The table/type for this fixture. 274 | * 275 | * @var string 276 | */ 277 | public $table = 'articles'; 278 | 279 | /** 280 | * The mapping data. 281 | * 282 | * @var array 283 | */ 284 | public $schema = [ 285 | 'id' => ['type' => 'integer'], 286 | 'user' => [ 287 | 'type' => 'nested', 288 | 'properties' => [ 289 | 'username' => ['type' => 'string'], 290 | ] 291 | ], 292 | 'title' => ['type' => 'string'], 293 | 'body' => ['type' => 'string'], 294 | ]; 295 | 296 | public $records = [ 297 | [ 298 | 'user' => [ 299 | 'username' => 'billy' 300 | ], 301 | 'title' => 'First Post', 302 | 'body' => 'Some content' 303 | ] 304 | ]; 305 | } 306 | 307 | ``schema`` プロパティーは `ネイティブ elasticsearch マッピングフォーマット 308 | `_ を使用します。 309 | 安全にタイプ名およびトップレベルの ``properties`` キーを省略することができます。 310 | フィクスチャーが作成されたら、あなたのテストの ``fixtures`` プロパティーに含めることによって、 311 | あなたのテストケースで使用することができます。 :: 312 | 313 | public $fixtures = ['app.Articles']; 314 | 315 | -------------------------------------------------------------------------------- /docs/pt/conf.py: -------------------------------------------------------------------------------- 1 | import sys, os 2 | 3 | # Append the top level directory of the docs, so we can import from the config dir. 4 | sys.path.insert(0, os.path.abspath('..')) 5 | 6 | # Pull in all the configuration options defined in the global config file.. 7 | from config.all import * 8 | 9 | # Language in use for this directory. 10 | language = 'pt' 11 | -------------------------------------------------------------------------------- /docs/pt/contents.rst: -------------------------------------------------------------------------------- 1 | Contents 2 | ######## 3 | 4 | .. toctree:: 5 | :maxdepth: 2 6 | :caption: CakePHP ElasticSearch 7 | 8 | /index 9 | -------------------------------------------------------------------------------- /docs/pt/index.rst: -------------------------------------------------------------------------------- 1 | ElasticSearch 2 | ############# 3 | 4 | O plugin ElasticSearch disponibiliza uma abstração semelhante a do ORM para 5 | conversar com o 6 | `elasticsearch `_. O plugin 7 | disponibiliza recursos que tornam testes, indexação de documentos e busca por 8 | seus índices uma tarefa mais fácil de se executar. 9 | 10 | Instalação 11 | ========== 12 | 13 | Para instalar o plugin ElasticSearch, você pode usar o ``composer``. A partir do 14 | diretório raiz de sua aplicação (onde o arquivo composer.json está localizado) 15 | execute o seguinte:: 16 | 17 | php composer.phar require cakephp/elastic-search "@stable" 18 | 19 | Você precisará adicionar a seguinte linha ao arquivo **config/bootstrap.php** da 20 | sua aplicação:: 21 | 22 | Plugin::load('Cake/ElasticSearch', ['bootstrap' => true]); 23 | 24 | Adicionalmente, você precisará configurar a conexão como um *datasource* chamado 25 | 'elastic' no seu arquivo **config/app.php**. Um exemplo de configuração ficaria 26 | assim:: 27 | 28 | // No config/app.php 29 | 'Datasources' => [ 30 | // outros datasources 31 | 'elastic' => [ 32 | 'className' => 'Cake\ElasticSearch\Datasource\Connection', 33 | 'driver' => 'Cake\ElasticSearch\Datasource\Connection', 34 | 'host' => '127.0.0.1', 35 | 'port' => 9200, 36 | 'index' => 'my_apps_index', 37 | ], 38 | ] 39 | 40 | Visão geral 41 | =========== 42 | 43 | O plugin ElasticSearch torna fácil interagir com um índice do *elasticsearch* e 44 | disponibiliza uma interface similar ao `ORM 45 | `__. Para começar, você deve 46 | criar um objeto ``Type``. Objetos ``Type`` são a classe similar a um 47 | "repositório" ou "tabela" no *elasticsearch*:: 48 | 49 | // No src/Model/Type/ArticlesType.php 50 | namespace App\Model\Type; 51 | 52 | use Cake\ElasticSearch\Type; 53 | 54 | class ArticlesType extends Type 55 | { 56 | } 57 | 58 | Você pode então usar a sua classe *type* nos seus *controllers*:: 59 | 60 | public function beforeFilter(Event $event) 61 | { 62 | parent::beforeFilter($event); 63 | // Carrega o Type usando o provedor 'Elastic' 64 | $this->loadModel('Articles', 'Elastic'); 65 | } 66 | 67 | public function add() 68 | { 69 | $article = $this->Articles->newEntity(); 70 | if ($this->request->is('post')) { 71 | $article = $this->Articles->patchEntity($article, $this->request->getData()); 72 | if ($this->Articles->save($article)) { 73 | $this->Flash->success('It saved'); 74 | } 75 | } 76 | $this->set(compact('article')); 77 | } 78 | 79 | Nós também precisamos criar um *template* básico para exibir nossos artigos 80 | indexados:: 81 | 82 | // No src/Template/Articles/add.ctp 83 | Form->create($article) ?> 84 | Form->input('title') ?> 85 | Form->input('body') ?> 86 | Form->button('Save') ?> 87 | Form->end() ?> 88 | 89 | Agora você deve conseguir submeter o formulário e ter um novo documento 90 | adicionado ao *elasticsearch*. 91 | 92 | Objetos Document 93 | ================ 94 | 95 | Como no ORM, o ODM do ElasticSearch usa classes semelhantes a `Entidades 96 | `__. A classe base a partir 97 | da qual você deve indicar herança é a ``Cake\ElasticSearch\Document``. Classes 98 | de documento podem ser encontradas sob o *namespace* ``Model\Document`` da sua 99 | aplicação ou *plugin*:: 100 | 101 | namespace App\Model\Document; 102 | 103 | class Article extends Document 104 | { 105 | } 106 | 107 | Fora da lógica do construtor que faz *Documents* trabalharem com dados do 108 | *elasticsearch*, a interface e as funcionalidades disponibilizadas pelo objeto 109 | ``Document`` são as mesmas do `Entidades 110 | `__. 111 | 112 | Buscando documentos indexados 113 | ============================= 114 | 115 | Depois que você indexar alguns documentos, é hora de buscar por eles. O plugin 116 | ElasticSearch disponibiliza um *query builder* que permite a você construir 117 | *queries* de busca:: 118 | 119 | $query = $this->Articles->find() 120 | ->where([ 121 | 'title' => 'special', 122 | 'or' => [ 123 | 'tags in' => ['cake', 'php'], 124 | 'tags not in' => ['c#', 'java'] 125 | ] 126 | ]); 127 | 128 | foreach ($query as $article) { 129 | echo $article->title; 130 | } 131 | 132 | Você pode usar o ``QueryBuilder`` para adicionar condições de filtragem:: 133 | 134 | $query->where(function ($builder) { 135 | return $builder->and( 136 | $builder->gt('views', 99), 137 | $builder->term('author.name', 'sally') 138 | ); 139 | }); 140 | 141 | A lista completa de métodos com exemplos práticos pode ser encontradda no código 142 | fonte do `QueryBuilder 143 | `_. 144 | 145 | Validando dados & Usando regras da aplicação 146 | ============================================ 147 | 148 | Como no ORM, o plugin ElasticSearch permite validar dados ao ordenar documentos. 149 | Validar dados da requisição e aplicar regras da aplicação funcionam da mesma 150 | forma como no ORM relacional. Veja a seção `validating request data 151 | `__ e a 152 | seção `Application Rules 153 | `__ para mais informações. 154 | 155 | .. Precisa de informações para validadores aninhados. 156 | 157 | Salvando novos documentos 158 | ========================= 159 | 160 | Quando você estiver pronto para indexar dados no *elasticsearch*, primeiramente 161 | será necessário converter seus dados em um ``Document`` para que possam ser 162 | indexados:: 163 | 164 | $article = $this->Articles->newEntity($data); 165 | if ($this->Articles->save($article)) { 166 | // Document indexado 167 | } 168 | 169 | Ao ordenar um documento, você pode especificar quais incorporações você deseja 170 | processar usando a chave ``associated``:: 171 | 172 | $article = $this->Articles->newEntity($data, ['associated' => ['Comments']]); 173 | 174 | Salvar um documento irá disparar os seguintes eventos: 175 | 176 | * ``Model.beforeSave`` - Disparado antes do documento ser salvo. Você pode 177 | prevenir a operação ao parar este evento. 178 | * ``Model.buildRules`` - Disparado quando o verificador de regras é construído 179 | pela primeira vez. 180 | * ``Model.afterSave`` - Disparado depois do documento ser salvo. 181 | 182 | .. note:: 183 | Não existem eventos para documentos incorporados devido ao documento pai e todos 184 | os seus documentos incorporados serem salvos em uma única operação. 185 | 186 | Atualizando documentos existentes 187 | ================================= 188 | 189 | Quando você precisar re-indexar dados, você pode acrescentar informações a 190 | *entities* existentes e salvá-las novamente:: 191 | 192 | $query = $this->Articles->find()->where(['user.name' => 'jill']); 193 | foreach ($query as $doc) { 194 | $doc->set($newProperties); 195 | $this->Articles->save($doc); 196 | } 197 | 198 | Deletando documentos 199 | ==================== 200 | 201 | Depois de requisitar um documento, você pode deletá-lo:: 202 | 203 | $doc = $this->Articles->get($id); 204 | $this->Articles->delete($doc); 205 | 206 | Você também pode deletar documentos que correspondem condições específicas:: 207 | 208 | $this->Articles->deleteAll(['user.name' => 'bob']); 209 | 210 | Incorporando documentos 211 | ======================= 212 | 213 | Ao definir documentos incorporados, você pode anexar classes de entidade a 214 | caminhos de propriedade específicos em seus documentos. Isso permite a você 215 | sobrescrever o comportamento padrão dos documentos relacionados a um 216 | parente. Por exemplo, você pode querer ter os comentários incorporados a um 217 | artigo para ter acesso a métodos específicos da aplicação. Você pode usar os 218 | métodos ``embedOne`` e ``embedMany`` para definir documentos incorporados:: 219 | 220 | // No src/Model/Type/ArticlesType.php 221 | namespace App\Model\Type; 222 | 223 | use Cake\ElasticSearch\Type; 224 | 225 | class ArticlesType extends Type 226 | { 227 | public function initialize() 228 | { 229 | $this->embedOne('User'); 230 | $this->embedMany('Comments', [ 231 | 'entityClass' => 'MyComment' 232 | ]); 233 | } 234 | } 235 | 236 | O código acima deve criar dois documentos incorporados ao documento ``Article``. 237 | O ``User`` incorporado irá converter a propriedade ``user`` em instâncias de 238 | ``App\Model\Document\User``. Para que os comentários incorporados usem um nome 239 | de classe que não correspondem ao nome da propriedade, podemos usar a opção 240 | ``entityClass`` para configurar um nome de classe opcional. 241 | 242 | Uma vez que configuramos nossos documentos incorporados, os resultados do 243 | ``find()`` e ``get()`` retornarão objetos com as classes de documentos 244 | incorporados corretas:: 245 | 246 | $article = $this->Articles->get($id); 247 | // Instância de App\Model\Document\User 248 | $article->user; 249 | 250 | // Array das instâncias App\Model\Document\Comment 251 | $article->comments; 252 | 253 | Recebendo instâncias Type 254 | ========================= 255 | 256 | Como no ORM, o plugin ElasticSearch disponibiliza um *factory/registry* para 257 | receber instâncias ``Type``:: 258 | 259 | use Cake\ElasticSearch\TypeRegistry; 260 | 261 | $articles = TypeRegistry::get('Articles'); 262 | 263 | Descarregando o Registry 264 | ------------------------ 265 | 266 | Durante casos de testes você pode querer descarregar o *registry*. Fazê-lo é 267 | frequentemente útil quando 268 | 269 | During test cases you may want to flush the registry. Doing so is often useful 270 | when you are using mock objects, or modifying a type's dependencies:: 271 | 272 | TypeRegistry::flush(); 273 | 274 | Suites de testes 275 | ================ 276 | 277 | O plugin ElasticSearch disponibiliza integração com suites de testes sem 278 | remendos. Tais como nas suites de banco de dados, você criar suites de testes 279 | para o *elasticsearch*. Podemos definir uma suite de teste para nosso *articles 280 | type* com o seguinte código:: 281 | 282 | namespace App\Test\Fixture; 283 | 284 | use Cake\ElasticSearch\TestSuite\TestFixture; 285 | 286 | /** 287 | * Articles fixture 288 | */ 289 | class ArticlesFixture extends TestFixture 290 | { 291 | /** 292 | * A table/type para essa fixture. 293 | * 294 | * @var string 295 | */ 296 | public $table = 'articles'; 297 | 298 | /** 299 | * O mapeamento de dados. 300 | * 301 | * @var array 302 | */ 303 | public $schema = [ 304 | 'id' => ['type' => 'integer'], 305 | 'user' => [ 306 | 'type' => 'nested', 307 | 'properties' => [ 308 | 'username' => ['type' => 'string'], 309 | ] 310 | ] 311 | 'title' => ['type' => 'string'], 312 | 'body' => ['type' => 'string'], 313 | ]; 314 | 315 | public $records = [ 316 | [ 317 | 'user' => [ 318 | 'username' => 'birl' 319 | ], 320 | 'title' => 'Primeiro post', 321 | 'body' => 'Conteúdo' 322 | ] 323 | ]; 324 | } 325 | 326 | A propriedade ``Schema`` usa o `formato de mapeamento para elasticsearch nativo 327 | `_. 328 | Você pode seguramente omitir o *type name* e a chave ``propertires``. Uma vez 329 | que suas *fixtures* estejam criadas, você pode usá-las nos seus casos de testes 330 | ao incluí-las nas propriedades dos seus ``fixtures`` de testes:: 331 | 332 | public $fixtures = ['app.articles']; 333 | 334 | -------------------------------------------------------------------------------- /phpstan-baseline.neon: -------------------------------------------------------------------------------- 1 | parameters: 2 | ignoreErrors: 3 | - 4 | message: '#^Strict comparison using \!\=\= between string and array\{\} will always evaluate to true\.$#' 5 | identifier: notIdentical.alwaysTrue 6 | count: 1 7 | path: src/Document.php 8 | 9 | - 10 | message: '#^Access to an undefined property Cake\\Datasource\\EntityInterface\:\:\$version\.$#' 11 | identifier: property.notFound 12 | count: 1 13 | path: src/Index.php 14 | 15 | - 16 | message: '#^Method Cake\\ElasticSearch\\Query\:\:find\(\) should return static\(Cake\\ElasticSearch\\Query\\) but returns Cake\\ElasticSearch\\Query\\.$#' 17 | identifier: return.type 18 | count: 1 19 | path: src/Query.php 20 | 21 | - 22 | message: '#^PHPDoc tag @return with type Cake\\ElasticSearch\\Query\ is not subtype of native type static\(Cake\\ElasticSearch\\Query\\)\.$#' 23 | identifier: return.phpDocType 24 | count: 1 25 | path: src/Query.php 26 | 27 | - 28 | message: '#^Strict comparison using \!\=\= between mixed and null will always evaluate to true\.$#' 29 | identifier: notIdentical.alwaysTrue 30 | count: 1 31 | path: src/QueryBuilder.php 32 | -------------------------------------------------------------------------------- /phpstan.neon: -------------------------------------------------------------------------------- 1 | includes: 2 | - phpstan-baseline.neon 3 | 4 | parameters: 5 | level: 6 6 | paths: 7 | - src/ 8 | bootstrapFiles: 9 | - tests/bootstrap.php 10 | ignoreErrors: 11 | - identifier: missingType.iterableValue 12 | - identifier: missingType.generics 13 | 14 | -------------------------------------------------------------------------------- /psalm-baseline.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | _client->getConfig()]]> 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | id]]> 17 | id]]> 18 | id]]> 19 | id]]> 20 | version]]> 21 | 22 | 23 | 24 | 25 | id]]> 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | -------------------------------------------------------------------------------- /psalm.xml: -------------------------------------------------------------------------------- 1 | 2 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | -------------------------------------------------------------------------------- /src/Association/EmbedMany.php: -------------------------------------------------------------------------------- 1 | 18 | * @psalm-suppress MoreSpecificReturnType 19 | */ 20 | public function hydrate(array $data, array $options): array 21 | { 22 | $class = $this->getEntityClass(); 23 | $out = []; 24 | foreach ($data as $row) { 25 | if (is_array($row)) { 26 | $out[] = new $class($row, $options); 27 | } 28 | } 29 | 30 | return $out; 31 | } 32 | 33 | /** 34 | * @inheritDoc 35 | */ 36 | public function type(): string 37 | { 38 | return static::ONE_TO_MANY; 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /src/Association/EmbedOne.php: -------------------------------------------------------------------------------- 1 | getEntityClass(); 24 | 25 | return new $class($data, $options); 26 | } 27 | 28 | /** 29 | * @inheritDoc 30 | */ 31 | public function type(): string 32 | { 33 | return static::ONE_TO_ONE; 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /src/Association/Embedded.php: -------------------------------------------------------------------------------- 1 | alias = $alias; 71 | $properties = [ 72 | 'entityClass' => 'setEntityClass', 73 | 'property' => 'setProperty', 74 | 'indexClass' => 'setIndexClass', 75 | ]; 76 | $options += [ 77 | 'entityClass' => $alias, 78 | ]; 79 | foreach ($properties as $prop => $method) { 80 | if (isset($options[$prop])) { 81 | $this->{$method}($options[$prop]); 82 | } 83 | } 84 | } 85 | 86 | /** 87 | * Get the property this embed is attached to. 88 | * 89 | * @return string The property name. 90 | */ 91 | public function getProperty(): string 92 | { 93 | if (!isset($this->property)) { 94 | $this->property = Inflector::underscore($this->alias); 95 | } 96 | 97 | return $this->property; 98 | } 99 | 100 | /** 101 | * Set the property this embed is attached to. 102 | * 103 | * @param string|null $name The property name to set. 104 | * @return $this 105 | */ 106 | public function setProperty(?string $name = null) 107 | { 108 | $this->property = $name; 109 | 110 | return $this; 111 | } 112 | 113 | /** 114 | * Get/set the property this embed is attached to. 115 | * 116 | * @deprecated 3.2.0 Use setProperty()/getProperty() instead. 117 | * @param string|null $name The property name to set. 118 | * @return string The property name. 119 | */ 120 | public function property(?string $name = null): string 121 | { 122 | deprecationWarning( 123 | '3.3.0', 124 | static::class . '::property() is deprecated. ' . 125 | 'Use setProperty()/getProperty() instead.', 126 | ); 127 | 128 | if ($name !== null) { 129 | $this->setProperty($name); 130 | } 131 | 132 | return $this->getProperty(); 133 | } 134 | 135 | /** 136 | * Get the entity/document class used for this embed. 137 | * 138 | * @return string The class name. 139 | */ 140 | public function getEntityClass(): string 141 | { 142 | if (!isset($this->entityClass)) { 143 | $default = Document::class; 144 | $self = static::class; 145 | $parts = explode('\\', $self); 146 | 147 | if ($self === self::class || count($parts) < 3) { 148 | return $this->entityClass = $default; 149 | } 150 | 151 | $alias = Inflector::singularize(substr(array_pop($parts), 0, -5)); 152 | $name = implode('\\', array_slice($parts, 0, -1)) . '\Document\\' . $alias; 153 | if (!class_exists($name)) { 154 | return $this->entityClass = $default; 155 | } 156 | 157 | $class = App::className($name, 'Model/Document'); 158 | if (!$class) { 159 | throw new MissingDocumentException([$name]); 160 | } 161 | $this->entityClass = $class; 162 | } 163 | 164 | return $this->entityClass; 165 | } 166 | 167 | /** 168 | * Sets the entity/document class used for this embed. 169 | * 170 | * @param string $name The name of the class to use 171 | * @return $this 172 | */ 173 | public function setEntityClass(string $name) 174 | { 175 | $class = App::className($name, 'Model/Document'); 176 | $this->entityClass = $class ?? Document::class; 177 | 178 | return $this; 179 | } 180 | 181 | /** 182 | * Get/set the entity/document class used for this embed. 183 | * 184 | * @deprecated 3.2.0 Use setEntityClass()/getEntityClass() instead. 185 | * @param string|null $name The class name to set. 186 | * @return string The class name. 187 | */ 188 | public function entityClass(?string $name = null): string 189 | { 190 | deprecationWarning( 191 | '3.3', 192 | static::class . '::entityClass() is deprecated. ' . 193 | 'Use setEntityClass()/getEntityClass() instead.', 194 | ); 195 | 196 | if ($name !== null) { 197 | $this->setEntityClass($name); 198 | } 199 | 200 | return $this->getEntityClass(); 201 | } 202 | 203 | /** 204 | * Get the index class used for this embed. 205 | * 206 | * @return string The class name. 207 | */ 208 | public function getIndexClass(): string 209 | { 210 | if (!isset($this->indexClass)) { 211 | $alias = Inflector::pluralize($this->alias); 212 | $class = App::className($alias . 'Index', 'Model/Index'); 213 | 214 | if ($class) { 215 | return $this->indexClass = $class; 216 | } 217 | 218 | $this->indexClass = Index::class; 219 | } 220 | 221 | return $this->indexClass; 222 | } 223 | 224 | /** 225 | * Set the index class used for this embed. 226 | * 227 | * @param \Cake\ElasticSearch\Index|string|null $name The class name to set. 228 | * @return $this 229 | */ 230 | public function setIndexClass(string|Index|null $name) 231 | { 232 | if ($name instanceof Index) { 233 | $this->indexClass = get_class($name); 234 | } elseif (is_string($name)) { 235 | $class = App::className($name, 'Model/Index'); 236 | $this->indexClass = $class; 237 | } 238 | 239 | return $this; 240 | } 241 | 242 | /** 243 | * Get/set the index class used for this embed. 244 | * 245 | * @deprecated 3.2.0 Use setIndexClass()/getIndexClass() instead. 246 | * @param \Cake\ElasticSearch\Index|string|null $name The class name to set. 247 | * @return string The class name. 248 | */ 249 | public function indexClass(string|Index|null $name = null): string 250 | { 251 | deprecationWarning( 252 | '3.3.0', 253 | static::class . '::indexClass() is deprecated. ' . 254 | 'Use setIndexClass()/getIndexClass() instead.', 255 | ); 256 | 257 | if ($name !== null) { 258 | $this->setIndexClass($name); 259 | } 260 | 261 | return $this->getIndexClass(); 262 | } 263 | 264 | /** 265 | * Get the alias for this embed. 266 | * 267 | * @return string 268 | */ 269 | public function getAlias(): string 270 | { 271 | return $this->alias; 272 | } 273 | 274 | /** 275 | * Hydrate instance(s) from the parent documents data. 276 | * 277 | * @param array $data The data to use in the embedded document. 278 | * @param array $options The options to use in the new document. 279 | * @return \Cake\ElasticSearch\Document|array 280 | */ 281 | abstract public function hydrate(array $data, array $options): Document|array; 282 | 283 | /** 284 | * Get the type of association this is. 285 | * 286 | * Returns one of the association type constants. 287 | * 288 | * @return string 289 | */ 290 | abstract public function type(): string; 291 | } 292 | -------------------------------------------------------------------------------- /src/Datasource/Connection.php: -------------------------------------------------------------------------------- 1 | configName = $config['name']; 77 | } 78 | if (isset($config['log'])) { 79 | $this->enableQueryLogging((bool)$config['log']); 80 | } 81 | 82 | $this->_client = new ElasticaClient($config, $callback, $this->getEsLogger()); 83 | } 84 | 85 | /** 86 | * Pass remaining methods to the elastica client (if they exist) 87 | * And set the current logger based on current logQueries value 88 | * 89 | * @param string $name Method name 90 | * @param array $attributes Method attributes 91 | * @return mixed 92 | */ 93 | public function __call(string $name, array $attributes): mixed 94 | { 95 | if (method_exists($this->_client, $name)) { 96 | return call_user_func_array([$this->_client, $name], $attributes); 97 | } 98 | 99 | throw new NotImplementedException($name); 100 | } 101 | 102 | /** 103 | * Returns a SchemaCollection stub until we can add more 104 | * abstract API's in Connection. 105 | * 106 | * @return \Cake\ElasticSearch\Datasource\SchemaCollection 107 | */ 108 | public function getSchemaCollection(): SchemaCollection 109 | { 110 | return new SchemaCollection($this); 111 | } 112 | 113 | /** 114 | * @inheritDoc 115 | */ 116 | public function configName(): string 117 | { 118 | return $this->configName; 119 | } 120 | 121 | /** 122 | * Enable/disable query logging 123 | * 124 | * @param bool $value Enable/disable query logging 125 | * @return $this 126 | */ 127 | public function enableQueryLogging(bool $value = true) 128 | { 129 | $this->logQueries = $value; 130 | 131 | return $this; 132 | } 133 | 134 | /** 135 | * Disable query logging 136 | * 137 | * @return $this 138 | */ 139 | public function disableQueryLogging() 140 | { 141 | $this->logQueries = false; 142 | 143 | return $this; 144 | } 145 | 146 | /** 147 | * Check if query logging is enabled. 148 | * 149 | * @return bool 150 | */ 151 | public function isQueryLoggingEnabled(): bool 152 | { 153 | return $this->logQueries; 154 | } 155 | 156 | /** 157 | * {@inheritDoc} 158 | * 159 | * Elasticsearch does not deal with the concept of foreign key constraints 160 | * This method just triggers the $callback argument. 161 | * 162 | * @param callable $operation The callback to execute within a transaction. 163 | * @return mixed The return value of the callback. 164 | * @throws \Exception Will re-throw any exception raised in $callback after 165 | * rolling back the transaction. 166 | */ 167 | public function disableConstraints(callable $operation) 168 | { 169 | return $operation($this); 170 | } 171 | 172 | /** 173 | * Get the config data for this connection. 174 | * 175 | * @return array 176 | */ 177 | public function config(): array 178 | { 179 | return $this->_client->getConfig(); 180 | } 181 | 182 | /** 183 | * Sets a logger 184 | * 185 | * @param \Cake\Database\Log\QueryLogger|\Psr\Log\LoggerInterface $logger Logger instance 186 | * @return $this 187 | */ 188 | public function setLogger(QueryLogger|LoggerInterface $logger) 189 | { 190 | $this->_logger = $logger; 191 | $this->getEsLogger()->setLogger($logger); 192 | 193 | return $this; 194 | } 195 | 196 | /** 197 | * Get the logger object 198 | * Will set the default logger to elasticsearch if found, or debug 199 | * If none of the above are found the default Es logger will be used. 200 | * 201 | * @return \Psr\Log\LoggerInterface logger instance 202 | */ 203 | public function getLogger(): LoggerInterface 204 | { 205 | if (!isset($this->_logger)) { 206 | $engine = Log::engine('elasticsearch') ?: Log::engine('debug'); 207 | 208 | if (!$engine) { 209 | $engine = new NullLogger(); 210 | } 211 | 212 | $this->setLogger($engine); 213 | } 214 | 215 | return $this->_logger; 216 | } 217 | 218 | /** 219 | * Return instance of ElasticLogger 220 | * 221 | * @return \Cake\ElasticSearch\Datasource\Log\ElasticLogger 222 | */ 223 | public function getEsLogger(): ElasticLogger 224 | { 225 | if (!isset($this->_esLogger)) { 226 | $this->_esLogger = new ElasticLogger($this->getLogger(), $this); 227 | } 228 | 229 | return $this->_esLogger; 230 | } 231 | 232 | /** 233 | * @inheritDoc 234 | */ 235 | public function setCacher(CacheInterface $cacher) 236 | { 237 | $this->cacher = $cacher; 238 | 239 | return $this; 240 | } 241 | 242 | /** 243 | * @inheritDoc 244 | */ 245 | public function getCacher(): CacheInterface 246 | { 247 | if (isset($this->cacher)) { 248 | return $this->cacher; 249 | } 250 | 251 | $configName = $this->_config['cacheMetadata'] ?? '_cake_model_'; 252 | if (!is_string($configName)) { 253 | $configName = '_cake_model_'; 254 | } 255 | 256 | if (!class_exists(Cache::class)) { 257 | throw new RuntimeException( 258 | 'To use caching you must either set a cacher using Connection::setCacher()' . 259 | ' or require the cakephp/cache package in your composer config.', 260 | ); 261 | } 262 | 263 | return $this->cacher = Cache::pool($configName); 264 | } 265 | 266 | /** 267 | * {@inheritDoc} 268 | * 269 | * @see \Cake\Datasource\ConnectionInterface::getDriver() 270 | * @return \Elastica\Client 271 | */ 272 | public function getDriver(string $role = self::ROLE_WRITE): ElasticaClient 273 | { 274 | return $this->_client; 275 | } 276 | 277 | /** 278 | * Returns the index for the given connection 279 | * 280 | * @param string|null $name Index name to create connection to, if no value is passed 281 | * it will use the default index name for the connection. 282 | * @return \Elastica\Index Index for the given name 283 | */ 284 | public function getIndex(?string $name = null): Index 285 | { 286 | $defaultIndex = $this->config()['index'] ?? $this->configName; 287 | 288 | return $this->_client->getIndex($name ?: $defaultIndex); 289 | } 290 | } 291 | -------------------------------------------------------------------------------- /src/Datasource/IndexLocator.php: -------------------------------------------------------------------------------- 1 | 38 | */ 39 | protected string $fallbackClassName = Index::class; 40 | 41 | /** 42 | * Whether fallback class should be used if a Index class could not be found. 43 | * 44 | * @var bool 45 | */ 46 | protected bool $allowFallbackClass = true; 47 | 48 | /** 49 | * Set fallback class name. 50 | * 51 | * The class that should be used to create a table instance if a concrete 52 | * class for alias used in `get()` could not be found. Defaults to 53 | * `Cake\Elasticsearch\Index`. 54 | * 55 | * @param string $className Fallback class name 56 | * @return $this 57 | * @psalm-param class-string<\Cake\ElasticSearch\Index> $className 58 | */ 59 | public function setFallbackClassName(string $className) 60 | { 61 | $this->fallbackClassName = $className; 62 | 63 | return $this; 64 | } 65 | 66 | /** 67 | * Set if fallback class should be used. 68 | * 69 | * Controls whether a fallback class should be used to create a index 70 | * instance if a concrete class for alias used in `get()` could not be found. 71 | * 72 | * @param bool $allow Flag to enable or disable fallback 73 | * @return $this 74 | */ 75 | public function allowFallbackClass(bool $allow) 76 | { 77 | $this->allowFallbackClass = $allow; 78 | 79 | return $this; 80 | } 81 | 82 | /** 83 | * @inheritDoc 84 | */ 85 | protected function createInstance(string $alias, array $options): RepositoryInterface 86 | { 87 | [, $classAlias] = pluginSplit($alias); 88 | $options += [ 89 | 'name' => Inflector::underscore($classAlias), 90 | 'className' => Inflector::camelize($alias), 91 | ]; 92 | $className = App::className($options['className'], 'Model/Index', 'Index'); 93 | if ($className) { 94 | $options['className'] = $className; 95 | } elseif ($this->allowFallbackClass) { 96 | if (!isset($options['name']) && strpos($options['className'], '\\') === false) { 97 | [, $name] = pluginSplit($options['className']); 98 | $options['name'] = Inflector::underscore($name); 99 | } 100 | $options['className'] = $this->fallbackClassName; 101 | } else { 102 | throw new MissingIndexClassException(['name' => $alias]); 103 | } 104 | 105 | if (empty($options['connection'])) { 106 | $connectionName = $options['className']::defaultConnectionName(); 107 | $options['connection'] = ConnectionManager::get($connectionName); 108 | } 109 | $options['registryAlias'] = $alias; 110 | 111 | return new $options['className']($options); 112 | } 113 | } 114 | -------------------------------------------------------------------------------- /src/Datasource/Log/ElasticLogger.php: -------------------------------------------------------------------------------- 1 | setLogger($logger); 56 | $this->_connection = $connection; 57 | } 58 | 59 | /** 60 | * Set the current cake logger 61 | * 62 | * @param \Cake\Database\Log\QueryLogger|\Psr\Log\LoggerInterface $logger Set logger instance to pass logging data to 63 | * @return $this 64 | */ 65 | public function setLogger(QueryLogger|LoggerInterface $logger) 66 | { 67 | $this->_logger = $logger; 68 | 69 | return $this; 70 | } 71 | 72 | /** 73 | * Return the current logger 74 | * 75 | * @return \Cake\Database\Log\QueryLogger|\Psr\Log\LoggerInterface [description] 76 | */ 77 | public function getLogger(): QueryLogger|LoggerInterface 78 | { 79 | return $this->_logger; 80 | } 81 | 82 | /** 83 | * Format log messages from the Elastica client _log method 84 | * 85 | * @param mixed $level The log level 86 | * @param \Stringable|string $message The log message 87 | * @param array $context log context 88 | * @return void 89 | */ 90 | public function log(mixed $level, Stringable|string $message, array $context = []): void 91 | { 92 | if ($this->_connection->isQueryLoggingEnabled()) { 93 | $this->_log($level, $message, $context); 94 | } 95 | } 96 | 97 | /** 98 | * Format log messages from the Elastica client and pass 99 | * them to the cake defined logger instance 100 | * 101 | * Elastica's log parameters 102 | * ------------------------- 103 | * error: 104 | * message: "Elastica Request Failure" 105 | * context: [ exception, request, retry ] 106 | * debug (request): 107 | * message: "Elastica Request" 108 | * context: [ request, response, responseStatus, query ] 109 | * debug (fallback?): 110 | * message: "Elastica Request" 111 | * context: [ message, query ] 112 | * 113 | * @param string $level The log level 114 | * @param string $message The log message 115 | * @param array $context log context 116 | * @return void 117 | */ 118 | protected function _log(string $level, string $message, array $context = []): void 119 | { 120 | $logData = $context; 121 | if ($level === LogLevel::DEBUG && isset($context['request'])) { 122 | $logData = [ 123 | 'method' => $context['request']['method'], 124 | 'path' => $context['request']['path'], 125 | 'data' => $context['request']['data'], 126 | ]; 127 | } 128 | $logData = json_encode($logData, JSON_PRETTY_PRINT); 129 | 130 | if (isset($context['request'], $context['response'])) { 131 | $took = 0; 132 | $numRows = $context['response']['hits']['total']['value'] ?? $context['response']['hits']['total'] ?? 0; 133 | if (isset($context['response']['took'])) { 134 | $took = $context['response']['took']; 135 | } 136 | $message = new LoggedQuery(); 137 | $message->setContext([ 138 | 'query' => $logData, 139 | 'took' => $took, 140 | 'numRows' => $numRows, 141 | ]); 142 | 143 | $context['query'] = $message; 144 | } 145 | $exception = $context['exception'] ?? null; 146 | if ($exception instanceof Exception) { 147 | throw $exception; 148 | } 149 | $this->getLogger()->log($level, $logData, $context); 150 | } 151 | } 152 | -------------------------------------------------------------------------------- /src/Datasource/MappingSchema.php: -------------------------------------------------------------------------------- 1 | name = $name; 47 | if (isset($data['properties'])) { 48 | $data = $data['properties']; 49 | } 50 | $this->data = $data; 51 | } 52 | 53 | /** 54 | * Get the name of the index for this mapping. 55 | * 56 | * @return string 57 | */ 58 | public function name(): string 59 | { 60 | return $this->name; 61 | } 62 | 63 | /** 64 | * Get the mapping information for a single field. 65 | * 66 | * Can access nested fields through dot paths. 67 | * 68 | * @param string $name The path to the field you want. 69 | * @return array|null Either field mapping data or null. 70 | */ 71 | public function field(string $name): ?array 72 | { 73 | if (strpos($name, '.') === false) { 74 | if (isset($this->data[$name])) { 75 | return $this->data[$name]; 76 | } 77 | 78 | return null; 79 | } 80 | $parts = explode('.', $name); 81 | $pointer = $this->data; 82 | foreach ($parts as $part) { 83 | if (isset($pointer[$part]['type']) && $pointer[$part]['type'] !== 'nested') { 84 | return (array)$pointer[$part]; 85 | } 86 | if (isset($pointer[$part]['properties'])) { 87 | $pointer = $pointer[$part]['properties']; 88 | } 89 | } 90 | 91 | return null; 92 | } 93 | 94 | /** 95 | * Get the field type for a field. 96 | * 97 | * Can access nested fields through dot paths. 98 | * 99 | * @param string $name The path to the field you want. 100 | * @return string|null Either type information or null 101 | */ 102 | public function fieldType(string $name): ?string 103 | { 104 | $field = $this->field($name); 105 | if (!$field) { 106 | return null; 107 | } 108 | 109 | return $field['type']; 110 | } 111 | 112 | /** 113 | * Get the field names in the mapping. 114 | * 115 | * Will only return the top level fields. Nested object field names will 116 | * not be included. 117 | * 118 | * @return array 119 | */ 120 | public function fields(): array 121 | { 122 | return array_keys($this->data); 123 | } 124 | } 125 | -------------------------------------------------------------------------------- /src/Datasource/SchemaCollection.php: -------------------------------------------------------------------------------- 1 | connection = $connection; 41 | } 42 | 43 | /** 44 | * Returns an empty array as a shim for fixtures 45 | * 46 | * @return array An empty array 47 | */ 48 | public function listTables(): array 49 | { 50 | try { 51 | $indexes = $this->connection->getDriver()->getStatus()->getIndexNames(); 52 | } catch (ResponseException $e) { 53 | return []; 54 | } 55 | 56 | return $indexes; 57 | } 58 | } 59 | -------------------------------------------------------------------------------- /src/Document.php: -------------------------------------------------------------------------------- 1 | getId(); 55 | $data = $data->getData(); 56 | if ($id !== []) { 57 | $data['id'] = $id; 58 | } 59 | } 60 | 61 | $options += [ 62 | 'useSetters' => true, 63 | 'markClean' => false, 64 | 'markNew' => null, 65 | 'guard' => false, 66 | 'source' => null, 67 | 'result' => null, 68 | ]; 69 | 70 | if (!empty($options['source'])) { 71 | $this->setSource($options['source']); 72 | } 73 | 74 | if ($options['markNew'] !== null) { 75 | $this->setNew($options['markNew']); 76 | } 77 | 78 | if ($options['result'] !== null) { 79 | $this->_result = $options['result']; 80 | } 81 | 82 | if (!empty($data) && $options['markClean'] && !$options['useSetters']) { 83 | $this->_fields = $data; 84 | 85 | return; 86 | } 87 | 88 | if (!empty($data)) { 89 | $this->set($data, [ 90 | 'setter' => $options['useSetters'], 91 | 'guard' => $options['guard'], 92 | ]); 93 | } 94 | 95 | if ($options['markClean']) { 96 | $this->clean(); 97 | } 98 | } 99 | 100 | /** 101 | * Returns the Elasticsearch index name from which this document came from. 102 | * 103 | * If this is a new document, this function returns null 104 | * 105 | * @return string|null 106 | */ 107 | public function index(): ?string 108 | { 109 | if (isset($this->_result)) { 110 | return $this->_result->getIndex(); 111 | } 112 | 113 | return null; 114 | } 115 | 116 | /** 117 | * Returns the version number of this document as returned by Elasticsearch 118 | * 119 | * If this is a new document, this function returns 1 120 | * 121 | * @return int 122 | */ 123 | public function version(): int 124 | { 125 | if (isset($this->_result)) { 126 | return intval($this->_result->getVersion()); 127 | } 128 | 129 | return 1; 130 | } 131 | 132 | /** 133 | * Returns the highlights array for document as returned by Elasticsearch 134 | * for the executed query. 135 | * 136 | * If this is a new document, or the query used to create it did not ask for 137 | * highlights, this function will return an empty array. 138 | * 139 | * @return array 140 | */ 141 | public function highlights(): array 142 | { 143 | if (isset($this->_result)) { 144 | return $this->_result->getHighlights(); 145 | } 146 | 147 | return []; 148 | } 149 | 150 | /** 151 | * Returns the explanation array for this document as returned from Elasticsearch. 152 | * 153 | * If this is a new document, or the query used to create it did not ask for 154 | * explanation, this function will return an empty array. 155 | * 156 | * @return array 157 | */ 158 | public function explanation(): array 159 | { 160 | if (isset($this->_result)) { 161 | return $this->_result->getExplanation(); 162 | } 163 | 164 | return []; 165 | } 166 | } 167 | -------------------------------------------------------------------------------- /src/Exception/MissingDocumentException.php: -------------------------------------------------------------------------------- 1 | setFallbackClassName($className); 70 | } 71 | 72 | /** 73 | * Get/Create an instance from the registry. 74 | * 75 | * When getting an instance, if it does not already exist, 76 | * a new instance will be created using the provide alias, and options. 77 | * 78 | * @param string $alias The name of the alias to get. 79 | * @param array $options Configuration options for the type constructor. 80 | * @return \Cake\ElasticSearch\Index 81 | */ 82 | public function get(string $alias, array $options = []): Index 83 | { 84 | $instance = static::getLocator()->get($alias, $options); 85 | assert($instance instanceof Index); 86 | 87 | return $instance; 88 | } 89 | 90 | /** 91 | * Check to see if an instance exists in the registry. 92 | * 93 | * @param string $alias The alias to check for. 94 | * @return bool 95 | */ 96 | public function exists(string $alias): bool 97 | { 98 | return static::getLocator()->exists($alias); 99 | } 100 | 101 | /** 102 | * Set an instance. 103 | * 104 | * @param string $alias The alias to set. 105 | * @param \Cake\Datasource\RepositoryInterface $repository The type to set. 106 | * @return \Cake\ElasticSearch\Index 107 | */ 108 | public function set(string $alias, RepositoryInterface $repository): Index 109 | { 110 | $instance = static::getLocator()->set($alias, $repository); 111 | assert($instance instanceof Index); 112 | 113 | return $instance; 114 | } 115 | 116 | /** 117 | * Clears the registry of configuration and instances. 118 | * 119 | * @return void 120 | */ 121 | public function clear(): void 122 | { 123 | static::getLocator()->clear(); 124 | } 125 | 126 | /** 127 | * Removes an instance from the registry. 128 | * 129 | * @param string $alias The alias to remove. 130 | * @return void 131 | */ 132 | public function remove(string $alias): void 133 | { 134 | static::getLocator()->remove($alias); 135 | } 136 | } 137 | -------------------------------------------------------------------------------- /src/Marshaller.php: -------------------------------------------------------------------------------- 1 | index = $index; 48 | } 49 | 50 | /** 51 | * Hydrate a single document. 52 | * 53 | * ### Options: 54 | * 55 | * * fieldList: A whitelist of fields to be assigned to the entity. If not present, 56 | * the accessible fields list in the entity will be used. 57 | * * accessibleFields: A list of fields to allow or deny in entity accessible fields. 58 | * * associated: A list of embedded documents you want to marshal. 59 | * 60 | * @param array $data The data to hydrate. 61 | * @param array $options List of options 62 | * @return \Cake\ElasticSearch\Document 63 | */ 64 | public function one(array $data, array $options = []): Document 65 | { 66 | $entityClass = $this->index->getEntityClass(); 67 | $entity = $this->createAndHydrate($entityClass, $data, $options); 68 | $entity->setSource($this->index->getRegistryAlias()); 69 | 70 | return $entity; 71 | } 72 | 73 | /** 74 | * Creates and Hydrates Document whilst honouring accessibleFields etc 75 | * 76 | * @param string $class Class name of Document to create 77 | * @param array $data The data to hydrate with 78 | * @param array $options Options to control the hydration 79 | * @param string $indexClass Index class to get embeds from (for nesting) 80 | * @return \Cake\ElasticSearch\Document 81 | */ 82 | protected function createAndHydrate( 83 | string $class, 84 | array $data, 85 | array $options = [], 86 | ?string $indexClass = null, 87 | ): Document { 88 | $entity = new $class(); 89 | 90 | $options += ['associated' => []]; 91 | 92 | [$data, $options] = $this->_prepareDataAndOptions($data, $options); 93 | 94 | if (isset($options['accessibleFields'])) { 95 | foreach ((array)$options['accessibleFields'] as $key => $value) { 96 | $entity->setAccess($key, $value); 97 | } 98 | } 99 | $errors = $this->_validate($data, $options, true); 100 | $entity->setErrors($errors); 101 | foreach (array_keys($errors) as $badKey) { 102 | unset($data[$badKey]); 103 | } 104 | 105 | if ($indexClass === null) { 106 | $embeds = $this->index->embedded(); 107 | } else { 108 | /** @var \Cake\ElasticSearch\Index $index */ 109 | $index = FactoryLocator::get('Elastic')->get($indexClass); 110 | $embeds = $index->embedded(); 111 | } 112 | 113 | foreach ($embeds as $embed) { 114 | $property = $embed->getProperty(); 115 | $alias = $embed->getAlias(); 116 | if (isset($data[$property])) { 117 | if (isset($options['associated'][$alias])) { 118 | $entity->set($property, $this->newNested($embed, $data[$property], $options['associated'][$alias])); 119 | unset($data[$property]); 120 | } elseif (in_array($alias, $options['associated'])) { 121 | $entity->set($property, $this->newNested($embed, $data[$property])); 122 | unset($data[$property]); 123 | } 124 | } 125 | } 126 | 127 | if (!isset($options['fieldList'])) { 128 | $entity->set($data); 129 | } else { 130 | foreach ((array)$options['fieldList'] as $field) { 131 | if (array_key_exists($field, $data)) { 132 | $entity->set($field, $data[$field]); 133 | } 134 | } 135 | } 136 | 137 | return $entity; 138 | } 139 | 140 | /** 141 | * Marshal an embedded document. 142 | * 143 | * @param \Cake\ElasticSearch\Association\Embedded $embed The embed definition. 144 | * @param array $data The data to marshal 145 | * @param array $options The options to pass on 146 | * @return \Cake\ElasticSearch\Document|array Either a document or an array of documents. 147 | */ 148 | protected function newNested(Embedded $embed, array $data, array $options = []): Document|array 149 | { 150 | $class = $embed->getEntityClass(); 151 | if ($embed->type() === Embedded::ONE_TO_ONE) { 152 | return $this->createAndHydrate($class, $data, $options, $embed->getIndexClass()); 153 | } else { 154 | $children = []; 155 | foreach ($data as $row) { 156 | if (is_array($row)) { 157 | $children[] = $this->createAndHydrate($class, $row, $options, $embed->getIndexClass()); 158 | } 159 | } 160 | 161 | return $children; 162 | } 163 | } 164 | 165 | /** 166 | * Merge an embedded document. 167 | * 168 | * @param \Cake\ElasticSearch\Association\Embedded $embed The embed definition. 169 | * @param \Cake\ElasticSearch\Document|array $existing The existing entity or entities. 170 | * @param array $data The data to marshal 171 | * @return \Cake\ElasticSearch\Document|array Either a document or an array of documents. 172 | */ 173 | protected function mergeNested(Embedded $embed, Document|array|null $existing, array $data): Document|array 174 | { 175 | $class = $embed->getEntityClass(); 176 | if ($embed->type() === Embedded::ONE_TO_ONE) { 177 | if (!($existing instanceof EntityInterface)) { 178 | $existing = new $class(); 179 | } 180 | $existing->set($data); 181 | 182 | return $existing; 183 | } else { 184 | if (!is_array($existing)) { 185 | $existing = []; 186 | } 187 | foreach ($existing as $i => $row) { 188 | if (isset($data[$i])) { 189 | $row->set($data[$i]); 190 | } 191 | unset($data[$i]); 192 | } 193 | foreach ($data as $row) { 194 | if (is_array($row)) { 195 | $new = new $class(); 196 | $new->set($row); 197 | $existing[] = $new; 198 | } 199 | } 200 | 201 | return $existing; 202 | } 203 | } 204 | 205 | /** 206 | * Hydrate a collection of entities. 207 | * 208 | * ### Options: 209 | * 210 | * * fieldList: A whitelist of fields to be assigned to the entity. If not present, 211 | * the accessible fields list in the entity will be used. 212 | * * accessibleFields: A list of fields to allow or deny in entity accessible fields. 213 | * 214 | * @param array $data A list of entity data you want converted into objects. 215 | * @param array $options Options 216 | * @return array An array of hydrated entities 217 | */ 218 | public function many(array $data, array $options = []): array 219 | { 220 | $output = []; 221 | foreach ($data as $record) { 222 | $output[] = $this->one($record, $options); 223 | } 224 | 225 | return $output; 226 | } 227 | 228 | /** 229 | * Merges `$data` into `$document`. 230 | * 231 | * ### Options: 232 | * 233 | * * fieldList: A whitelist of fields to be assigned to the entity. If not present 234 | * the accessible fields list in the entity will be used. 235 | * * associated: A list of embedded documents you want to marshal. 236 | * 237 | * @param \Cake\Datasource\EntityInterface $entity the entity that will get the 238 | * data merged in 239 | * @param array $data key value list of fields to be merged into the entity 240 | * @param array $options List of options. 241 | * @return \Cake\Datasource\EntityInterface 242 | */ 243 | public function merge(EntityInterface $entity, array $data, array $options = []): EntityInterface 244 | { 245 | $options += ['associated' => []]; 246 | [$data, $options] = $this->_prepareDataAndOptions($data, $options); 247 | $errors = $this->_validate($data, $options, $entity->isNew()); 248 | $entity->setErrors($errors); 249 | 250 | foreach (array_keys($errors) as $badKey) { 251 | unset($data[$badKey]); 252 | } 253 | 254 | foreach ($this->index->embedded() as $embed) { 255 | $property = $embed->getProperty(); 256 | if (in_array($embed->getAlias(), $options['associated']) && isset($data[$property])) { 257 | $data[$property] = $this->mergeNested($embed, $entity->{$property}, $data[$property]); 258 | } 259 | } 260 | 261 | if (!isset($options['fieldList'])) { 262 | $entity->set($data); 263 | 264 | return $entity; 265 | } 266 | 267 | foreach ((array)$options['fieldList'] as $field) { 268 | if (array_key_exists($field, $data)) { 269 | $entity->set($field, $data[$field]); 270 | } 271 | } 272 | 273 | return $entity; 274 | } 275 | 276 | /** 277 | * Update a collection of entities. 278 | * 279 | * Merges each of the elements from `$data` into each of the entities in `$entities`. 280 | * 281 | * Records in `$data` are matched against the entities using the id field. 282 | * Entries in `$entities` that cannot be matched to any record in 283 | * `$data` will be discarded. Records in `$data` that could not be matched will 284 | * be marshalled as a new entity. 285 | * 286 | * ### Options: 287 | * 288 | * * fieldList: A whitelist of fields to be assigned to the entity. If not present, 289 | * the accessible fields list in the entity will be used. 290 | * 291 | * @param iterable $entities An array of Elasticsearch entities 292 | * @param array $data A list of entity data you want converted into objects. 293 | * @param array $options Options 294 | * @return array An array of merged entities 295 | */ 296 | public function mergeMany(iterable $entities, array $data, array $options = []): array 297 | { 298 | $indexed = (new Collection($data)) 299 | ->groupBy(function ($element) { 300 | return $element['id'] ?? ''; 301 | }) 302 | ->map(function ($element, $key) { 303 | return $key === '' ? $element : $element[0]; 304 | }) 305 | ->toArray(); 306 | 307 | $new = $indexed[''] ?? []; 308 | unset($indexed['']); 309 | 310 | $output = []; 311 | foreach ($entities as $record) { 312 | if (!($record instanceof EntityInterface)) { 313 | continue; 314 | } 315 | $id = $record->id; 316 | if (!isset($indexed[$id])) { 317 | continue; 318 | } 319 | $output[] = $this->merge($record, $indexed[$id], $options); 320 | unset($indexed[$id]); 321 | } 322 | $new = array_merge($indexed, $new); 323 | foreach ($new as $newRecord) { 324 | $output[] = $this->one($newRecord, $options); 325 | } 326 | 327 | return $output; 328 | } 329 | 330 | /** 331 | * Returns the validation errors for a data set based on the passed options 332 | * 333 | * @param array $data The data to validate. 334 | * @param array $options The options passed to this marshaller. 335 | * @param bool $isNew Whether it is a new entity or one to be updated. 336 | * @return array The list of validation errors. 337 | * @throws \RuntimeException If no validator can be created. 338 | */ 339 | protected function _validate(array $data, array $options, bool $isNew): array 340 | { 341 | if (!$options['validate']) { 342 | return []; 343 | } 344 | 345 | if ($options['validate'] === true) { 346 | $options['validate'] = $this->index->getValidator('default'); 347 | } 348 | if (is_string($options['validate'])) { 349 | $options['validate'] = $this->index->getValidator($options['validate']); 350 | } 351 | if (!is_object($options['validate'])) { 352 | throw new RuntimeException( 353 | sprintf('validate must be a boolean, a string or an object. Got %s.', gettype($options['validate'])), 354 | ); 355 | } 356 | 357 | return $options['validate']->validate($data, $isNew); 358 | } 359 | 360 | /** 361 | * Returns data and options prepared to validate and marshall. 362 | * 363 | * @param array $data The data to prepare. 364 | * @param array $options The options passed to this marshaller. 365 | * @return array An array containing prepared data and options. 366 | */ 367 | protected function _prepareDataAndOptions(array $data, array $options): array 368 | { 369 | $options += ['validate' => true]; 370 | $data = new ArrayObject($data); 371 | $options = new ArrayObject($options); 372 | $this->index->dispatchEvent('Model.beforeMarshal', compact('data', 'options')); 373 | 374 | return [(array)$data, (array)$options]; 375 | } 376 | } 377 | -------------------------------------------------------------------------------- /src/Plugin.php: -------------------------------------------------------------------------------- 1 | on('View.beforeRender', function ($event): void { 43 | $view = $event->getSubject(); 44 | $view->Form->addContextProvider('elastic', function ($request, $data) { 45 | $first = null; 46 | if (is_array($data['entity']) || $data['entity'] instanceof Traversable) { 47 | $first = (new Collection($data['entity']))->first(); 48 | } 49 | if ($data['entity'] instanceof Document || $first instanceof Document) { 50 | return new DocumentContext($request, $data); 51 | } 52 | }); 53 | }); 54 | } 55 | } 56 | -------------------------------------------------------------------------------- /src/QueryBuilder.php: -------------------------------------------------------------------------------- 1 | range($field, [ 47 | 'gte' => $from, 48 | 'lte' => $to, 49 | ]); 50 | } 51 | 52 | /** 53 | * Returns a bool query that can be chained with the `addMust()`, `addShould()`, 54 | * `addFilter` and `addMustNot()` methods. 55 | * 56 | * @return \Elastica\Query\BoolQuery 57 | * @see https://www.elastic.co/guide/en/elasticsearch/reference/current/query-dsl-bool-query.html 58 | */ 59 | public function bool(): BoolQuery 60 | { 61 | return new Elastica\Query\BoolQuery(); 62 | } 63 | 64 | /** 65 | * Returns an Exists query object setup to query documents having a property present 66 | * or not set to null. 67 | * 68 | * @param string $field The field to check for existance. 69 | * @return \Elastica\Query\Exists 70 | * @see https://www.elastic.co/guide/en/elasticsearch/reference/current/query-dsl-exists-query.html 71 | */ 72 | public function exists(string $field): Exists 73 | { 74 | return new Elastica\Query\Exists($field); 75 | } 76 | 77 | /** 78 | * Returns a GeoBoundingBox query object setup to query documents having a property 79 | * bound by two coordinates. 80 | * 81 | * ### Example: 82 | * 83 | * {{{ 84 | * $query = $builder->geoBoundingBox('location', [40.73, -74.1], [40.01, -71.12]); 85 | * 86 | * $query = $builder->geoBoundingBox( 87 | * 'location', 88 | * ['lat => 40.73, 'lon' => -74.1], 89 | * ['lat => 40.01, 'lon' => -71.12] 90 | * ); 91 | * 92 | * $query = $builder->geoBoundingBox('location', 'dr5r9ydj2y73', 'drj7teegpus6'); 93 | * }}} 94 | * 95 | * @param string $field The field to compare. 96 | * @param array|string $topLeft The top left coordinate. 97 | * @param array|string $bottomRight The bottom right coordinate. 98 | * @return \Elastica\Query\GeoBoundingBox 99 | * @see https://www.elastic.co/guide/en/elasticsearch/reference/current/query-dsl-geo-bounding-box-query.html 100 | */ 101 | public function geoBoundingBox(string $field, array|string $topLeft, array|string $bottomRight): GeoBoundingBox 102 | { 103 | return new Elastica\Query\GeoBoundingBox($field, [$topLeft, $bottomRight]); 104 | } 105 | 106 | /** 107 | * Returns an GeoDistance query object setup to query documents having a property 108 | * in the radius distance of a coordinate. 109 | * 110 | * ### Example: 111 | * 112 | * {{{ 113 | * $query = $builder->geoDistance('location', ['lat' => 40.73, 'lon' => -74.1], '10km'); 114 | * 115 | * $query = $builder->geoBoundingBox('location', 'dr5r9ydj2y73', '5km'); 116 | * }}} 117 | * 118 | * @param string $field The field to compare. 119 | * @param array|string $location The coordinate from which to compare. 120 | * @param string $distance The distance radius. 121 | * @return \Elastica\Query\GeoDistance 122 | * @see https://www.elastic.co/guide/en/elasticsearch/reference/current/query-dsl-geo-distance-query.html 123 | */ 124 | public function geoDistance(string $field, array|string $location, string $distance): GeoDistance 125 | { 126 | return new Elastica\Query\GeoDistance($field, $location, $distance); 127 | } 128 | 129 | /** 130 | * Returns an GeoPolygon query object setup to query documents having a property 131 | * enclosed in the polygon induced by the passed geo points. 132 | * 133 | * ### Example: 134 | * 135 | * {{{ 136 | * $query= $builder->geoPolygon('location', [ 137 | * ['lat' => 40, 'lon' => -70], 138 | * ['lat' => 30, 'lon' => -80], 139 | * ['lat' => 20, 'lon' => -90], 140 | * ]); 141 | * 142 | * $query = $builder->geoPolygon('location', [ 143 | * 'drn5x1g8cu2y', 144 | * ['lat' => 30, 'lon' => -80], 145 | * '20, -90', 146 | * ]); 147 | * }}} 148 | * 149 | * @param string $field The field to compare. 150 | * @param array $geoPoints List of geo points that form the polygon 151 | * @return \Elastica\Query\GeoPolygon 152 | * @see https://www.elastic.co/guide/en/elasticsearch/reference/current/query-dsl-geo-polygon-query.html 153 | */ 154 | public function geoPolygon(string $field, array $geoPoints): GeoPolygon 155 | { 156 | return new Elastica\Query\GeoPolygon($field, $geoPoints); 157 | } 158 | 159 | /** 160 | * Returns an GeoShapeProvided query object setup to query documents having a property 161 | * enclosed in the specified geometrical shape type. 162 | * 163 | * ### Example: 164 | * 165 | * {{{ 166 | * $query = $builder->geoShape('location', [[13.0, 53.0], [14.0, 52.0]], 'envelope'); 167 | * 168 | * $query = $builder->geoShape('location', [ 169 | * [[-77.03653, 38.897676], [-77.009051, 38.889939]], 170 | * 'linestring' 171 | * ]); 172 | * }}} 173 | * 174 | * You can read about the supported shapes and how they are created here: 175 | * https://www.elastic.co/guide/en/elasticsearch/reference/current/geo-shape.html 176 | * 177 | * @param string $field The field to compare. 178 | * @param array $geoPoints List of geo points that form the shape. 179 | * @param string $type The shape type to use (envelope, linestring, polygon, multipolygon...) 180 | * @return \Elastica\Query\GeoShapeProvided 181 | * @see https://www.elastic.co/guide/en/elasticsearch/reference/current/query-dsl-geo-shape-query.html 182 | */ 183 | public function geoShape( 184 | string $field, 185 | array $geoPoints, 186 | string $type = Elastica\Query\GeoShapeProvided::TYPE_ENVELOPE, 187 | ): GeoShapeProvided { 188 | return new Elastica\Query\GeoShapeProvided($field, $geoPoints, $type); 189 | } 190 | 191 | /** 192 | * Returns an GeoShapePreIndex query object setup to query documents having a property 193 | * enclosed in the specified geometrical shape type. 194 | * 195 | * ### Example: 196 | * 197 | * {{{ 198 | * $query = $builder->geoShapeIndex('location', 'DEU', 'countries', 'shapes', 'location'); 199 | * }}} 200 | * 201 | * @param string $field The field to compare. 202 | * @param string $id The ID of the document containing the pre-indexed shape. 203 | * @param string $index Name of the index where the pre-indexed shape is. 204 | * @param string $path The field specified as path containing the pre-indexed shape. 205 | * @return \Elastica\Query\GeoShapePreIndexed 206 | * @see https://www.elastic.co/guide/en/elasticsearch/reference/current/query-dsl-geo-shape-query.html 207 | */ 208 | public function geoShapeIndex( 209 | string $field, 210 | string $id, 211 | string $index = 'shapes', 212 | string $path = 'shape', 213 | ): GeoShapePreIndexed { 214 | return new Elastica\Query\GeoShapePreIndexed($field, $id, $index, $path); 215 | } 216 | 217 | /** 218 | * Returns a Range query object setup to query documents having the field 219 | * greater than the provided value. 220 | * 221 | * @param string $field The field to query by. 222 | * @param mixed $value The value to compare with. 223 | * @return \Elastica\Query\Range 224 | * @see https://www.elastic.co/guide/en/elasticsearch/reference/current/query-dsl-range-query.html 225 | */ 226 | public function gt(string $field, mixed $value): Range 227 | { 228 | return $this->range($field, ['gt' => $value]); 229 | } 230 | 231 | /** 232 | * Returns a Range query object setup to query documents having the field 233 | * greater than or equal the provided value. 234 | * 235 | * @param string $field The field to query by. 236 | * @param mixed $value The value to compare with. 237 | * @return \Elastica\Query\Range 238 | * @see https://www.elastic.co/guide/en/elasticsearch/reference/current/query-dsl-range-query.html 239 | */ 240 | public function gte(string $field, mixed $value): Range 241 | { 242 | return $this->range($field, ['gte' => $value]); 243 | } 244 | 245 | /** 246 | * Accepts a query and the child type to run against, and results in parent 247 | * documents that have child docs matching the query. 248 | * 249 | * @param \Elastica\Query|\Elastica\Query\AbstractQuery|string $query The query. 250 | * @param string $type The child type to query against. 251 | * @return \Elastica\Query\HasChild 252 | * @see https://www.elastic.co/guide/en/elasticsearch/reference/current/query-dsl-has-child-query.html 253 | */ 254 | public function hasChild(Query|AbstractQuery|string $query, string $type): HasChild 255 | { 256 | return new Elastica\Query\HasChild($query, $type); 257 | } 258 | 259 | /** 260 | * Query by child documents having parent documents matching the query 261 | * 262 | * @param \Elastica\Query|\Elastica\Query\AbstractQuery|string $query The query. 263 | * @param string $type The parent type to query against. 264 | * @return \Elastica\Query\HasParent 265 | * @see https://www.elastic.co/guide/en/elasticsearch/reference/current/query-dsl-has-parent-query.html 266 | */ 267 | public function hasParent(Query|AbstractQuery|string $query, string $type): HasParent 268 | { 269 | return new Elastica\Query\HasParent($query, $type); 270 | } 271 | 272 | /** 273 | * Query documents that only have the provided ids. 274 | * 275 | * @param array $ids The list of ids to query by. 276 | * @return \Elastica\Query\Ids 277 | * @see https://www.elastic.co/guide/en/elasticsearch/reference/current/query-dsl-ids-query.html 278 | */ 279 | public function ids(array $ids = []): Ids 280 | { 281 | return new Elastica\Query\Ids($ids); 282 | } 283 | 284 | /** 285 | * Limits the number of documents (per shard) to execute on. 286 | * 287 | * @param int $limit The maximum number of documents to query. 288 | * @return \Elastica\Query\Limit 289 | * @see https://www.elastic.co/guide/en/elasticsearch/reference/current/query-dsl-limit-query.html 290 | */ 291 | public function limit(int $limit): Limit 292 | { 293 | return new Elastica\Query\Limit((int)$limit); 294 | } 295 | 296 | /** 297 | * A query that returns all documents. 298 | * 299 | * @return \Elastica\Query\MatchAll 300 | * @see https://www.elastic.co/guide/en/elasticsearch/reference/current/query-dsl-match-all-query.html 301 | */ 302 | public function matchAll(): MatchAll 303 | { 304 | return new Elastica\Query\MatchAll(); 305 | } 306 | 307 | /** 308 | * Returns a Range query object setup to query documents having the field 309 | * smaller than the provided value. 310 | * 311 | * @param string $field The field to query by. 312 | * @param mixed $value The value to compare with. 313 | * @return \Elastica\Query\Range 314 | * @see https://www.elastic.co/guide/en/elasticsearch/reference/current/query-dsl-range-query.html 315 | */ 316 | public function lt(string $field, mixed $value): Range 317 | { 318 | return $this->range($field, ['lt' => $value]); 319 | } 320 | 321 | /** 322 | * Returns a Range query object setup to query documents having the field 323 | * smaller or equals than the provided value. 324 | * 325 | * @param string $field The field to query by. 326 | * @param mixed $value The value to compare with. 327 | * @return \Elastica\Query\Range 328 | * @see https://www.elastic.co/guide/en/elasticsearch/reference/current/query-dsl-range-query.html 329 | */ 330 | public function lte(string $field, mixed $value): Range 331 | { 332 | return $this->range($field, ['lte' => $value]); 333 | } 334 | 335 | /** 336 | * Returns a Nested query object setup to query sub documents by a path. 337 | * 338 | * ### Example: 339 | * 340 | * {{{ 341 | * $builder->nested('comments', $builder->term('author', 'mark')); 342 | * }}} 343 | * 344 | * @param string $path A dot separated string denoting the path to the property to query. 345 | * @param \Elastica\Query\AbstractQuery $query The query conditions. 346 | * @return \Elastica\Query\Nested 347 | * @see https://www.elastic.co/guide/en/elasticsearch/reference/current/query-dsl-nested-query.html 348 | */ 349 | public function nested(string $path, AbstractQuery $query): Nested 350 | { 351 | $nested = new Elastica\Query\Nested(); 352 | $nested->setPath($path); 353 | 354 | $nested->setQuery($query); 355 | 356 | return $nested; 357 | } 358 | 359 | /** 360 | * Returns a BoolQuery query with must_not field that is typically ussed to negate another query expression 361 | * 362 | * @param \Elastica\Query\AbstractQuery|array $query The query to negate 363 | * @return \Elastica\Query\BoolQuery 364 | * @see https://www.elastic.co/guide/en/elasticsearch/reference/current/query-dsl-bool-query.html 365 | */ 366 | public function not(AbstractQuery|array $query): BoolQuery 367 | { 368 | $boolQuery = new Elastica\Query\BoolQuery(); 369 | $boolQuery->addMustNot($query); 370 | 371 | return $boolQuery; 372 | } 373 | 374 | /** 375 | * Returns a Prefix query to query documents that have fields containing terms with 376 | * a specified prefix 377 | * 378 | * @param string $field The field to query by. 379 | * @param string $prefix The prefix to check for. 380 | * @param float $boost The optional boost 381 | * @return \Elastica\Query\Prefix 382 | * @see https://www.elastic.co/guide/en/elasticsearch/reference/current/query-dsl-prefix-query.html 383 | */ 384 | public function prefix(string $field, string $prefix, float $boost = 1.0): Prefix 385 | { 386 | $prefixQuery = new Elastica\Query\Prefix(); 387 | $prefixQuery->setPrefix($field, $prefix, $boost); 388 | 389 | return $prefixQuery; 390 | } 391 | 392 | /** 393 | * Returns a Range query object setup to query documents having the field 394 | * greater than the provided values. 395 | * 396 | * The $args array accepts the following keys: 397 | * 398 | * - gte: greater than or equal 399 | * - gt: greater than 400 | * - lte: less than or equal 401 | * - lt: less than 402 | * 403 | * @param string $field The field to query by. 404 | * @param array $args An array describing the search range 405 | * @return \Elastica\Query\Range 406 | * @see https://www.elastic.co/guide/en/elasticsearch/reference/current/query-dsl-range-query.html 407 | */ 408 | public function range(string $field, array $args): Range 409 | { 410 | return new Elastica\Query\Range($field, $args); 411 | } 412 | 413 | /** 414 | * Returns a Regexp query to query documents based on a regular expression. 415 | * 416 | * ### Example: 417 | * 418 | * {{{ 419 | * $builder->regexp('name.first', 'ma.*'); 420 | * }}} 421 | * 422 | * @param string $field The field to query by. 423 | * @param string $regexp The regular expression. 424 | * @param float $boost Boost 425 | * @return \Elastica\Query\Regexp 426 | * @see https://www.elastic.co/guide/en/elasticsearch/reference/current/query-dsl-regexp-query.html 427 | */ 428 | public function regexp(string $field, string $regexp, float $boost = 1.0): Regexp 429 | { 430 | return new Elastica\Query\Regexp($field, $regexp, $boost); 431 | } 432 | 433 | /** 434 | * Returns a Script query object that allows to query based on the return value of a script. 435 | * 436 | * ### Example: 437 | * 438 | * {{{ 439 | * $builder->script("doc['price'].value > 1"); 440 | * }}} 441 | * 442 | * @param \Elastica\Script\AbstractScript|array|string $script The script. 443 | * @return \Elastica\Query\Script 444 | * @see https://www.elastic.co/guide/en/elasticsearch/reference/current/query-dsl-script-query.html 445 | */ 446 | public function script(AbstractScript|array|string $script): Script 447 | { 448 | return new Elastica\Query\Script($script); 449 | } 450 | 451 | /** 452 | * Returns a SimpleQueryString object that allows to query based on a search string. 453 | * 454 | * ### Example: 455 | * 456 | * {{{ 457 | * $builder->simpleQueryString(['body'], '"fried eggs" +(eggplant | potato) -frittata'); 458 | * }}} 459 | * 460 | * @param array|string $fields The fields to search within 461 | * @param string $string The pattern to find in the fields 462 | * @return \Elastica\Query\SimpleQueryString 463 | * @see https://www.elastic.co/guide/en/elasticsearch/reference/current/query-dsl-simple-query-string-query.html 464 | */ 465 | public function simpleQueryString(array|string $fields, string $string): SimpleQueryString 466 | { 467 | return new Elastica\Query\SimpleQueryString($string, (array)$fields); 468 | } 469 | 470 | /** 471 | * Returns a Match query object that query documents that have fields containing a match. 472 | * 473 | * ### Example: 474 | * 475 | * {{{ 476 | * $builder->match('user.name', 'jose'); 477 | * }}} 478 | * 479 | * @param string $field The field to query by. 480 | * @param string $value The match to find in field. 481 | * @return \Elastica\Query\MatchQuery 482 | * @see https://www.elastic.co/guide/en/elasticsearch/reference/current/query-dsl-match-query.html 483 | */ 484 | public function match(string $field, string $value): MatchQuery 485 | { 486 | return new Elastica\Query\MatchQuery($field, $value); 487 | } 488 | 489 | /** 490 | * Returns a Term query object that query documents that have fields containing a term. 491 | * 492 | * ### Example: 493 | * 494 | * {{{ 495 | * $builder->term('user.name', 'jose'); 496 | * }}} 497 | * 498 | * @param string $field The field to query by. 499 | * @param string|float|int|bool $value The term to find in field. 500 | * @return \Elastica\Query\Term 501 | * @see https://www.elastic.co/guide/en/elasticsearch/reference/current/query-dsl-term-query.html 502 | */ 503 | public function term(string $field, string|float|int|bool $value): Term 504 | { 505 | return new Elastica\Query\Term([$field => $value]); 506 | } 507 | 508 | /** 509 | * Returns a Terms query object that query documents that have fields containing some terms. 510 | * 511 | * ### Example: 512 | * 513 | * {{{ 514 | * $builder->terms('user.name', ['jose', 'mark']); 515 | * }}} 516 | * 517 | * @param string $field The field to query by. 518 | * @param array $values The list of terms to find in field. 519 | * @return \Elastica\Query\Terms 520 | * @see https://www.elastic.co/guide/en/elasticsearch/reference/current/query-dsl-terms-query.html 521 | */ 522 | public function terms(string $field, array $values): Terms 523 | { 524 | return new Elastica\Query\Terms($field, $values); 525 | } 526 | 527 | /** 528 | * Combines all the passed arguments in a single bool query 529 | * using the "must" clause. 530 | * 531 | * ### Example: 532 | * 533 | * {{{ 534 | * $bool = $builder->and( 535 | * $builder->terms('tags', ['cool', 'stuff']), 536 | * $builder->exists('comments') 537 | * ); 538 | * }}} 539 | * 540 | * @param \Elastica\Query\AbstractQuery ...$queries Queries to compare. 541 | * @return \Elastica\Query\BoolQuery 542 | */ 543 | public function and(AbstractQuery ...$queries): BoolQuery 544 | { 545 | $bool = $this->bool(); 546 | 547 | foreach ($queries as $query) { 548 | $bool->addMust($query); 549 | } 550 | 551 | return $bool; 552 | } 553 | 554 | /** 555 | * Combines all the passed arguments in a single BoolQuery query using should clause. 556 | * 557 | * ### Example: 558 | * 559 | * {{{ 560 | * $bool = $builder->or( 561 | * $builder->not($builder->exists('tags')), 562 | * $builder->exists('comments') 563 | * ); 564 | * }}} 565 | * 566 | * @param \Elastica\Query\AbstractQuery ...$queries Queries to compare. 567 | * @return \Elastica\Query\BoolQuery 568 | */ 569 | public function or(AbstractQuery ...$queries): BoolQuery 570 | { 571 | $bool = $this->bool(); 572 | 573 | foreach ($queries as $query) { 574 | $bool->addShould($query); 575 | } 576 | 577 | return $bool; 578 | } 579 | 580 | // @codingStandardsIgnoreStart 581 | /** 582 | * Combines all the passed arguments in a single bool query 583 | * using the "must" clause. 584 | * 585 | * ### Example: 586 | * 587 | * {{{ 588 | * $bool = $builder->and( 589 | * $builder->terms('tags', ['cool', 'stuff']), 590 | * $builder->exists('comments') 591 | * ); 592 | * }}} 593 | * 594 | * @param \Elastica\Query\AbstractQuery ...$queries Queries to compare. 595 | * @return \Elastica\Query\BoolQuery 596 | * @deprecated 3.0.1 Use `and()` instead. 597 | */ 598 | public function and_(...$queries) 599 | { 600 | return $this->and(...$queries); 601 | } 602 | 603 | /** 604 | * Combines all the passed arguments in a single BoolQuery query using should clause. 605 | * 606 | * ### Example: 607 | * 608 | * {{{ 609 | * $bool = $builder->or( 610 | * $builder->not($builder->exists('tags')), 611 | * $builder->exists('comments') 612 | * ); 613 | * }}} 614 | * 615 | * @param \Elastica\Query\AbstractQuery ...$queries Queries to compare. 616 | * @return \Elastica\Query\BoolQuery 617 | * @deprecated 3.0.1 Use `or()` instead. 618 | */ 619 | public function or_(...$queries) 620 | { 621 | return $this->or(...$queries); 622 | } 623 | // @codingStandardsIgnoreEnd 624 | 625 | /** 626 | * Converts an array into a single array of query objects 627 | * 628 | * ### Parsing a single array: 629 | * 630 | * {{{ 631 | * $query = $builder->parse([ 632 | * 'name' => 'mark', 633 | * 'age <=' => 35 634 | * ]); 635 | * 636 | * // Equivalent to: 637 | * $query = [ 638 | * $builder->term('name', 'mark'), 639 | * $builder->lte('age', 35) 640 | * ]; 641 | * }}} 642 | * 643 | * ### Creating "or" conditions: 644 | * 645 | * {{{ 646 | * $query = $builder->parse([ 647 | * 'or' => [ 648 | * 'name' => 'mark', 649 | * 'age <=' => 35 650 | * ] 651 | * ]); 652 | * 653 | * // Equivalent to: 654 | * $query = [$builder->or( 655 | * $builder->term('name', 'mark'), 656 | * $builder->lte('age', 35) 657 | * )]; 658 | * }}} 659 | * 660 | * ### Negating conditions: 661 | * 662 | * {{{ 663 | * $query = $builder->parse([ 664 | * 'not' => [ 665 | * 'name' => 'mark', 666 | * 'age <=' => 35 667 | * ] 668 | * ]); 669 | * 670 | * // Equivalent to: 671 | * $query = [$builder->not( 672 | * $builder->and( 673 | * $builder->term('name', 'mark'), 674 | * $builder->lte('age', 35) 675 | * ) 676 | * )]; 677 | * }}} 678 | * 679 | * ### Checking for field existance 680 | * {{{ 681 | * $query = $builder->parse([ 682 | * 'name is' => null, 683 | * 'age is not' => null 684 | * ]); 685 | * 686 | * // Equivalent to: 687 | * $query = [ 688 | * $builder->not($builder->exists('name')), 689 | * $builder->exists('age') 690 | * ]; 691 | * }}} 692 | * 693 | * ### Checking if a value is in a list of terms 694 | * 695 | * {{{ 696 | * $query = $builder->parse([ 697 | * 'name in' => ['jose', 'mark'] 698 | * ]); 699 | * 700 | * // Equivalent to: 701 | * $query = [$builder->terms('name', ['jose', 'mark'])] 702 | * }}} 703 | * 704 | * The list of supported operators is: 705 | * 706 | * `<`, `>`, `<=`, `>=`, `in`, `not in`, `is`, `is not`, `!=` 707 | * 708 | * @param \Elastica\Query\AbstractQuery|array $conditions The list of conditions to parse. 709 | * @return \Elastica\Query\AbstractQuery|array 710 | */ 711 | public function parse(array|AbstractQuery $conditions): AbstractQuery|array 712 | { 713 | if ($conditions instanceof AbstractQuery) { 714 | return $conditions; 715 | } 716 | 717 | $result = []; 718 | foreach ($conditions as $k => $c) { 719 | $numericKey = is_numeric($k); 720 | 721 | if ($numericKey) { 722 | $c = $this->parse($c); 723 | if (is_array($c)) { 724 | $c = $this->and(...$c); 725 | } 726 | $result[] = $c; 727 | continue; 728 | } 729 | 730 | $operator = strtolower($k); 731 | 732 | if ($operator === 'and') { 733 | $result[] = $this->and(...$this->parse($c)); 734 | continue; 735 | } 736 | 737 | if ($operator === 'or') { 738 | $result[] = $this->or(...$this->parse($c)); 739 | continue; 740 | } 741 | 742 | if ($operator === 'not') { 743 | $result[] = $this->not($this->and(...$this->parse($c))); 744 | continue; 745 | } 746 | 747 | if ($c instanceof AbstractQuery) { 748 | $result[] = $c; 749 | continue; 750 | } 751 | 752 | $result[] = $this->_parseQuery($k, $c); 753 | } 754 | 755 | return $result; 756 | } 757 | 758 | /** 759 | * Parses a field name containing an operator into a Filter object. 760 | * 761 | * @param string $field The filed name containing the operator 762 | * @param mixed $value The value to pass to the query 763 | * @return \Elastica\Query\AbstractQuery 764 | */ 765 | protected function _parseQuery(string $field, mixed $value): AbstractQuery 766 | { 767 | $operator = '='; 768 | $parts = explode(' ', trim($field), 2); 769 | 770 | if (count($parts) > 1) { 771 | [$field, $operator] = $parts; 772 | } 773 | 774 | $operator = strtolower(trim($operator)); 775 | 776 | if ($operator === '>') { 777 | return $this->gt($field, $value); 778 | } 779 | 780 | if ($operator === '>=') { 781 | return $this->gte($field, $value); 782 | } 783 | 784 | if ($operator === '<') { 785 | return $this->lt($field, $value); 786 | } 787 | 788 | if ($operator === '<=') { 789 | return $this->lte($field, $value); 790 | } 791 | 792 | if (in_array($operator, ['in', 'not in'])) { 793 | $value = (array)$value; 794 | } 795 | 796 | if ($operator === 'in') { 797 | return $this->terms($field, $value); 798 | } 799 | 800 | if ($operator === 'not in') { 801 | return $this->not($this->terms($field, $value)); 802 | } 803 | 804 | if ($operator === 'is' && $value === null) { 805 | return $this->not($this->exists($field)); 806 | } 807 | 808 | if ($operator === 'is not' && $value === null) { 809 | return $this->exists($field); 810 | } 811 | 812 | if ($operator === 'is' && $value !== null) { 813 | return $this->term($field, $value); 814 | } 815 | 816 | if ($operator === 'is not' && $value !== null) { 817 | return $this->not($this->term($field, $value)); 818 | } 819 | 820 | if ($operator === '!=') { 821 | return $this->not($this->term($field, $value)); 822 | } 823 | 824 | return $this->term($field, $value); 825 | } 826 | } 827 | -------------------------------------------------------------------------------- /src/ResultSet.php: -------------------------------------------------------------------------------- 1 | > 32 | * @implements \Cake\Datasource\ResultSetInterface 33 | */ 34 | class ResultSet extends IteratorIterator implements ResultSetInterface 35 | { 36 | use CollectionTrait; 37 | 38 | /** 39 | * Holds the original instance of the result set 40 | * 41 | * @var \Elastica\ResultSet 42 | */ 43 | protected ElasticaResultSet $resultSet; 44 | 45 | /** 46 | * Holds the Elasticsearch ORM query object 47 | * 48 | * @var \Cake\ElasticSearch\Query 49 | */ 50 | protected Query $queryObject; 51 | 52 | /** 53 | * The full class name of the document class to wrap the results 54 | * 55 | * @var string 56 | */ 57 | protected string $entityClass; 58 | 59 | /** 60 | * Embedded type references 61 | * 62 | * @var array 63 | */ 64 | protected array $embeds = []; 65 | 66 | /** 67 | * Name of the type that the originating query came from. 68 | * 69 | * @var string 70 | */ 71 | protected string $repoName; 72 | 73 | /** 74 | * Decorator's constructor 75 | * 76 | * @param \Elastica\ResultSet $resultSet The results from Elastica to wrap 77 | * @param \Cake\ElasticSearch\Query $query The Elasticsearch Query object 78 | */ 79 | public function __construct(ElasticaResultSet $resultSet, Query $query) 80 | { 81 | $this->resultSet = $resultSet; 82 | $this->queryObject = $query; 83 | $repo = $this->queryObject->getRepository(); 84 | foreach ($repo->embedded() as $embed) { 85 | $this->embeds[$embed->getProperty()] = $embed; 86 | } 87 | $this->entityClass = $repo->getEntityClass(); 88 | $this->repoName = $repo->getRegistryAlias(); 89 | parent::__construct($resultSet); 90 | } 91 | 92 | /** 93 | * Returns all results 94 | * 95 | * @return array<\Elastica\Result> Results 96 | */ 97 | public function getResults(): array 98 | { 99 | return $this->resultSet->getResults(); 100 | } 101 | 102 | /** 103 | * Returns true if the response contains suggestion results; false otherwise 104 | * 105 | * @return bool 106 | */ 107 | public function hasSuggests(): bool 108 | { 109 | return $this->resultSet->hasSuggests(); 110 | } 111 | 112 | /** 113 | * Return all suggests 114 | * 115 | * @return array suggest results 116 | */ 117 | public function getSuggests(): array 118 | { 119 | return $this->resultSet->getSuggests(); 120 | } 121 | 122 | /** 123 | * Returns all aggregation results 124 | * 125 | * @return array 126 | */ 127 | public function getAggregations(): array 128 | { 129 | return $this->resultSet->getAggregations(); 130 | } 131 | 132 | /** 133 | * Retrieve a specific aggregation from this result set 134 | * 135 | * @param string $name the name of the desired aggregation 136 | * @return array 137 | * @throws \Elastica\Exception\InvalidException if an aggregation by the given name cannot be found 138 | */ 139 | public function getAggregation(string $name): array 140 | { 141 | return $this->resultSet->getAggregation($name); 142 | } 143 | 144 | /** 145 | * Returns the total number of found hits 146 | * 147 | * @return int Total hits 148 | */ 149 | public function getTotalHits(): int 150 | { 151 | return $this->resultSet->getTotalHits(); 152 | } 153 | 154 | /** 155 | * Returns the max score of the results found 156 | * 157 | * @return float Max Score 158 | */ 159 | public function getMaxScore(): float 160 | { 161 | return $this->resultSet->getMaxScore(); 162 | } 163 | 164 | /** 165 | * Returns the total number of ms for this search to complete 166 | * 167 | * @return int Total time 168 | */ 169 | public function getTotalTime(): int 170 | { 171 | return $this->resultSet->getTotalTime(); 172 | } 173 | 174 | /** 175 | * Returns true if the query has timed out 176 | * 177 | * @return bool Timed out 178 | */ 179 | public function hasTimedOut(): bool 180 | { 181 | return $this->resultSet->hasTimedOut(); 182 | } 183 | 184 | /** 185 | * Returns response object 186 | * 187 | * @return \Elastica\Response Response object 188 | */ 189 | public function getResponse(): Response 190 | { 191 | return $this->resultSet->getResponse(); 192 | } 193 | 194 | /** 195 | * Returns the original \Elastica\Query instance 196 | * 197 | * @return \Elastica\Query 198 | */ 199 | public function getQuery(): ElasticaQuery 200 | { 201 | return $this->resultSet->getQuery(); 202 | } 203 | 204 | /** 205 | * Returns size of current set 206 | * 207 | * @return int Size of set 208 | */ 209 | public function count(): int 210 | { 211 | return (int)$this->resultSet->count(); 212 | } 213 | 214 | /** 215 | * Returns size of current suggests 216 | * 217 | * @return int Size of suggests 218 | */ 219 | public function countSuggests(): int 220 | { 221 | return $this->resultSet->countSuggests(); 222 | } 223 | 224 | /** 225 | * Returns the current document for the iteration 226 | * 227 | * @return \Cake\ElasticSearch\Document 228 | */ 229 | public function current(): Document 230 | { 231 | $result = $this->resultSet->current(); 232 | $options = [ 233 | 'markClean' => true, 234 | 'useSetters' => false, 235 | 'markNew' => false, 236 | 'source' => $this->repoName, 237 | 'result' => $result, 238 | ]; 239 | 240 | $data = $result->getData(); 241 | $data['id'] = $result->getId(); 242 | 243 | foreach ($this->embeds as $property => $embed) { 244 | if (isset($data[$property])) { 245 | $data[$property] = $embed->hydrate($data[$property], $options); 246 | } 247 | } 248 | 249 | return new $this->entityClass($data, $options); 250 | } 251 | 252 | /** 253 | * Returns a string representation of this object that can be used 254 | * to reconstruct it 255 | * 256 | * @return string 257 | */ 258 | public function serialize(): string 259 | { 260 | return serialize([ $this->resultSet, $this->queryObject ]); 261 | } 262 | 263 | /** 264 | * Magic method for serializing the ResultSet instance 265 | * 266 | * @return array 267 | */ 268 | public function __serialize(): array 269 | { 270 | return [$this->resultSet, $this->queryObject]; 271 | } 272 | 273 | /** 274 | * Unserializes the passed string and rebuilds the ResultSet instance 275 | * 276 | * @param string $serialized The serialized ResultSet information 277 | * @return void 278 | */ 279 | public function unserialize(string $serialized): void 280 | { 281 | $this->__construct(...unserialize($serialized)); 282 | } 283 | 284 | /** 285 | * Magic method for unserializing the ResultSet instance 286 | * 287 | * @param array $data The serialized data 288 | * @return void 289 | */ 290 | public function __unserialize(array $data): void 291 | { 292 | $this->__construct(...$data); 293 | } 294 | 295 | /** 296 | * Debug output hook method. 297 | * 298 | * @return array 299 | */ 300 | public function __debugInfo(): array 301 | { 302 | return [ 303 | 'items' => $this->resultSet->getResponse()->getData(), 304 | 'query' => $this->resultSet->getQuery(), 305 | ]; 306 | } 307 | } 308 | -------------------------------------------------------------------------------- /src/Rule/IsUnique.php: -------------------------------------------------------------------------------- 1 | _fields = $fields; 46 | } 47 | 48 | /** 49 | * Performs the uniqueness check 50 | * 51 | * @param \Cake\Datasource\EntityInterface $entity The entity from where to extract the fields 52 | * where the `repository` key is required. 53 | * 54 | * @param array $options Options passed to the check, 55 | * @return bool 56 | */ 57 | public function __invoke(EntityInterface $entity, array $options): bool 58 | { 59 | if (!$entity->extract($this->_fields, true)) { 60 | return true; 61 | } 62 | 63 | $fields = $entity->extract($this->_fields); 64 | $conditions = []; 65 | 66 | foreach ($fields as $field => $value) { 67 | $conditions[$field . ' is'] = $value; 68 | } 69 | 70 | if ($entity->isNew() === false) { 71 | $conditions['_id is not'] = $entity->get('id'); 72 | } 73 | 74 | return !$options['repository']->exists($conditions); 75 | } 76 | } 77 | -------------------------------------------------------------------------------- /src/TestSuite/Fixture/DeleteQueryStrategy.php: -------------------------------------------------------------------------------- 1 | 34 | */ 35 | protected array $fixtures = []; 36 | 37 | /** 38 | * Initialize strategy. 39 | */ 40 | public function __construct() 41 | { 42 | $this->helper = new FixtureHelper(); 43 | } 44 | 45 | /** 46 | * @inheritDoc 47 | */ 48 | public function setupTest(array $fixtureNames): void 49 | { 50 | $this->fixtures = $this->helper->loadFixtures($fixtureNames); 51 | $this->helper->runPerConnection(function (ConnectionInterface $connection, array $fixtures): void { 52 | if (!$connection instanceof Connection) { 53 | return; 54 | } 55 | 56 | foreach ($fixtures as $fixture) { 57 | $fixture->insert($connection); 58 | } 59 | }, $this->fixtures); 60 | } 61 | 62 | /** 63 | * Clear state in all elastic indexes. 64 | * 65 | * @return void 66 | */ 67 | public function teardownTest(): void 68 | { 69 | $this->helper->runPerConnection(function (ConnectionInterface $connection, array $fixtures): void { 70 | if (!$connection instanceof Connection) { 71 | return; 72 | } 73 | 74 | /** @var \Cake\ElasticSearch\TestSuite\TestFixture $fixture */ 75 | foreach ($fixtures as $fixture) { 76 | /** @var \Cake\ElasticSearch\Datasource\Connection $connection */ 77 | $esIndex = $connection->getIndex($fixture->getIndex()->getName()); 78 | $esIndex->deleteByQuery(new MatchAll()); 79 | $esIndex->refresh(); 80 | } 81 | }, $this->fixtures); 82 | } 83 | } 84 | -------------------------------------------------------------------------------- /src/TestSuite/Fixture/MappingGenerator.php: -------------------------------------------------------------------------------- 1 | 'articles', 20 | * 'mapping' => [...], 21 | * 'settings' => [...], 22 | * ] 23 | * ``` 24 | * 25 | * The `mapping` key should be compatible with Elasticsearch's 26 | * mapping API and Elastica. 27 | * 28 | * The `settings` key can contain Elastica compatible index creation 29 | * settings. 30 | * 31 | * @see https://elastica.io/getting-started/storing-and-indexing-documents.html#define-mapping 32 | */ 33 | class MappingGenerator 34 | { 35 | /** 36 | * @var string 37 | */ 38 | protected string $file; 39 | 40 | /** 41 | * @var string 42 | */ 43 | protected string $connection; 44 | 45 | /** 46 | * Constructor 47 | * 48 | * @param string $file The index definition file. 49 | * @param string $connection The connection to put indexes into. 50 | */ 51 | public function __construct(string $file, string $connection) 52 | { 53 | $this->file = $file; 54 | $this->connection = $connection; 55 | } 56 | 57 | /** 58 | * Drop and re-create indexes defined in the mapping schema file. 59 | * 60 | * @param array $indexes A subset of indexes to reload. Used for testing. 61 | * @return void 62 | */ 63 | public function reload(?array $indexes = null): void 64 | { 65 | $db = ConnectionManager::get($this->connection); 66 | if (!($db instanceof Connection)) { 67 | throw new RuntimeException("The `{$this->connection}` connection is not an ElasticSearch connection."); 68 | } 69 | $mappings = include $this->file; 70 | if (empty($mappings)) { 71 | throw new RuntimeException("The `{$this->file}` file did not return any mapping data."); 72 | } 73 | foreach ($mappings as $i => $mapping) { 74 | if (!isset($mapping['name'])) { 75 | throw new RuntimeException("The mapping at index {$i} does not have a name."); 76 | } 77 | $this->dropIndex($db, $mapping['name']); 78 | $this->createIndex($db, $mapping); 79 | } 80 | } 81 | 82 | /** 83 | * Drop an index if it exists. 84 | * 85 | * @param \Cake\ElasticSearch\Datasource\Connection $db The connection. 86 | * @param string $name The name of the index to drop. 87 | * @return void 88 | */ 89 | protected function dropIndex(Connection $db, string $name): void 90 | { 91 | $esIndex = $db->getIndex($name); 92 | if ($esIndex->exists()) { 93 | $esIndex->delete(); 94 | } 95 | } 96 | 97 | /** 98 | * Create an index. 99 | * 100 | * @param \Cake\ElasticSearch\Datasource\Connection $db The connection. 101 | * @param array $mapping The index mapping and settings. 102 | * @return void 103 | */ 104 | protected function createIndex(Connection $db, array $mapping): void 105 | { 106 | if (!isset($mapping['mapping'])) { 107 | throw new RuntimeException("Mapping for {$mapping['name']} does not define a `mapping` key"); 108 | } 109 | 110 | $esIndex = $db->getIndex($mapping['name']); 111 | 112 | $args = []; 113 | if (!empty($mapping['settings'])) { 114 | $args['settings'] = $mapping['settings']; 115 | } 116 | $esIndex->create($args); 117 | 118 | $esMapping = new ElasticaMapping(); 119 | $esMapping->setProperties($mapping['mapping']); 120 | 121 | $response = $esMapping->send($esIndex); 122 | if (!$response->isOk()) { 123 | $msg = sprintf( 124 | 'Fixture creation for "%s" failed "%s"', 125 | $mapping['name'], 126 | $response->getError(), 127 | ); 128 | throw new RuntimeException($msg); 129 | } 130 | } 131 | } 132 | -------------------------------------------------------------------------------- /src/TestSuite/TestCase.php: -------------------------------------------------------------------------------- 1 | ElasticLocator = FactoryLocator::get('Elastic'); 40 | } 41 | 42 | /** 43 | * @inheritDoc 44 | */ 45 | public function getFixtureStrategy(): FixtureStrategyInterface 46 | { 47 | return new DeleteQueryStrategy(); 48 | } 49 | } 50 | -------------------------------------------------------------------------------- /src/TestSuite/TestFixture.php: -------------------------------------------------------------------------------- 1 | connection)) { 93 | $connection = $this->connection; 94 | if (strpos($connection, 'test') !== 0) { 95 | $message = sprintf( 96 | 'Invalid datasource name "%s" for "%s" fixture. Fixture datasource names must begin with "test".', 97 | $connection, 98 | $this->table, 99 | ); 100 | throw new CakeException($message); 101 | } 102 | } 103 | 104 | $this->init(); 105 | } 106 | 107 | /** 108 | * Initialize the fixture. 109 | * 110 | * @return void 111 | */ 112 | public function init(): void 113 | { 114 | } 115 | 116 | /** 117 | * Return the index class from table name 118 | * 119 | * @return \Cake\ElasticSearch\Index 120 | */ 121 | public function getIndex(): Index 122 | { 123 | $name = Inflector::camelize($this->table); 124 | 125 | return (new IndexRegistry())->get($name); 126 | } 127 | 128 | /** 129 | * Create index and mapping for the type. 130 | * 131 | * @param \Cake\Datasource\ConnectionInterface $db The Elasticsearch connection 132 | * @return bool 133 | */ 134 | public function create(ConnectionInterface $db): bool 135 | { 136 | assert($db instanceof Connection, 'Requires an elasticsearch connection'); 137 | if (empty($this->schema)) { 138 | return false; 139 | } 140 | 141 | $esIndex = $db->getIndex($this->getIndex()->getName()); 142 | if ($esIndex->exists()) { 143 | $esIndex->delete(); 144 | } 145 | 146 | $args = []; 147 | if (!empty($this->indexSettings)) { 148 | $args['settings'] = $this->indexSettings; 149 | } 150 | $esIndex->create($args); 151 | 152 | $mapping = new ElasticaMapping(); 153 | $mapping->setProperties($this->schema); 154 | 155 | $response = $mapping->send($esIndex); 156 | if (!$response->isOk()) { 157 | $msg = sprintf( 158 | 'Fixture creation for "%s" failed "%s"', 159 | $this->table, 160 | $response->getError(), 161 | ); 162 | Log::error($msg); 163 | trigger_error($msg, E_USER_WARNING); 164 | 165 | return false; 166 | } 167 | 168 | $this->created[] = $db->configName(); 169 | 170 | return true; 171 | } 172 | 173 | /** 174 | * Insert fixture documents. 175 | * 176 | * @param \Cake\Datasource\ConnectionInterface $connection The Elasticsearch connection 177 | * @return bool 178 | */ 179 | public function insert(ConnectionInterface $connection): bool 180 | { 181 | assert($connection instanceof Connection, 'Requires an elasticsearch connection'); 182 | if (empty($this->records)) { 183 | return false; 184 | } 185 | $documents = []; 186 | $esIndex = $connection->getIndex($this->getIndex()->getName()); 187 | 188 | foreach ($this->records as $data) { 189 | $id = ''; 190 | if (isset($data['id'])) { 191 | $id = $data['id']; 192 | } 193 | unset($data['id']); 194 | $documents[] = $esIndex->createDocument($id, $data); 195 | } 196 | $esIndex->addDocuments($documents); 197 | $esIndex->refresh(); 198 | 199 | return true; 200 | } 201 | 202 | /** 203 | * Drops the index 204 | * 205 | * @param \Cake\Datasource\ConnectionInterface $db The Elasticsearch connection 206 | * @return bool 207 | */ 208 | public function drop(ConnectionInterface $db): bool 209 | { 210 | assert($db instanceof Connection, 'Requires an elasticsearch connection'); 211 | $esIndex = $db->getIndex($this->getIndex()->getName()); 212 | 213 | if ($esIndex->exists()) { 214 | $esIndex->delete(); 215 | 216 | return true; 217 | } 218 | 219 | return false; 220 | } 221 | 222 | /** 223 | * Truncate the fixture type. 224 | * 225 | * @param \Cake\Datasource\ConnectionInterface $connection The Elasticsearch connection 226 | * @return bool 227 | */ 228 | public function truncate(ConnectionInterface $connection): bool 229 | { 230 | $query = new MatchAll(); 231 | assert($connection instanceof Connection, 'Requires an elasticsearch connection'); 232 | 233 | $esIndex = $connection->getIndex($this->getIndex()->getName()); 234 | $esIndex->deleteByQuery($query); 235 | $esIndex->refresh(); 236 | 237 | return true; 238 | } 239 | 240 | /** 241 | * @inheritDoc 242 | */ 243 | public function connection(): string 244 | { 245 | return $this->connection; 246 | } 247 | 248 | /** 249 | * @inheritDoc 250 | */ 251 | public function sourceName(): string 252 | { 253 | return $this->table; 254 | } 255 | 256 | /** 257 | * No-op method needed because of the Fixture interface. 258 | * Elasticsearch does not deal with foreign key constraints. 259 | * 260 | * @param \Cake\Datasource\ConnectionInterface $db The Elasticsearch connection 261 | * @return void 262 | */ 263 | public function createConstraints(ConnectionInterface $db): void 264 | { 265 | } 266 | 267 | /** 268 | * No-op method needed because of the Fixture interface. 269 | * Elasticsearch does not deal with foreign key constraints. 270 | * 271 | * @param \Cake\Datasource\ConnectionInterface $db The Elasticsearch connection 272 | * connection 273 | * @return void 274 | */ 275 | public function dropConstraints(ConnectionInterface $db): void 276 | { 277 | } 278 | } 279 | -------------------------------------------------------------------------------- /src/View/Form/DocumentContext.php: -------------------------------------------------------------------------------- 1 | _request = $request; 74 | $context += [ 75 | 'entity' => null, 76 | 'index' => null, 77 | 'validator' => 'default', 78 | ]; 79 | $this->_context = $context; 80 | $this->_prepare(); 81 | } 82 | 83 | /** 84 | * Prepare some additional data from the context. 85 | * 86 | * If the table option was provided to the constructor and it 87 | * was a string, IndexRegistry will be used to get the correct table instance. 88 | * 89 | * If an object is provided as the index option, it will be used as is. 90 | * 91 | * If no index option is provided, the index name will be derived based on 92 | * naming conventions. This inference will work with a number of common objects 93 | * like arrays, Collection objects and ResultSets. 94 | * 95 | * @return void 96 | * @throws \RuntimeException When a table object cannot be located/inferred. 97 | */ 98 | protected function _prepare(): void 99 | { 100 | $index = $this->_context['index']; 101 | $entity = $this->_context['entity']; 102 | if (empty($index)) { 103 | if (is_array($entity) || $entity instanceof Traversable) { 104 | $entity = (new Collection($entity))->first(); 105 | } 106 | $isDocument = $entity instanceof Document; 107 | 108 | if ($isDocument) { 109 | $index = $entity->getSource(); 110 | } 111 | if (!$index && $isDocument) { 112 | [, $entityClass] = namespaceSplit(get_class($entity)); 113 | $index = Inflector::pluralize($entityClass); 114 | } 115 | } 116 | if (is_string($index)) { 117 | $index = FactoryLocator::get('Elastic')->get($index); 118 | } 119 | 120 | if (!is_object($index)) { 121 | throw new RuntimeException( 122 | 'Unable to find index class for current entity', 123 | ); 124 | } 125 | $this->_isCollection = ( 126 | is_array($entity) || 127 | $entity instanceof Traversable 128 | ); 129 | $this->_rootName = $index->getName(); 130 | $this->_context['index'] = $index; 131 | } 132 | 133 | /** 134 | * @inheritDoc 135 | */ 136 | public function getPrimaryKey(): array 137 | { 138 | return ['id']; 139 | } 140 | 141 | /** 142 | * @inheritDoc 143 | */ 144 | public function isPrimaryKey(string $field): bool 145 | { 146 | $parts = explode('.', $field); 147 | 148 | return array_pop($parts) === 'id'; 149 | } 150 | 151 | /** 152 | * @inheritDoc 153 | */ 154 | public function isCreate(): bool 155 | { 156 | $entity = $this->_context['entity']; 157 | if (is_array($entity) || $entity instanceof Traversable) { 158 | $entity = (new Collection($entity))->first(); 159 | } 160 | if ($entity instanceof Document) { 161 | return $entity->isNew() !== false; 162 | } 163 | 164 | return true; 165 | } 166 | 167 | /** 168 | * @inheritDoc 169 | */ 170 | public function val(string $field, array $options = []): mixed 171 | { 172 | $val = $this->_request->getData($field); 173 | if ($val !== null) { 174 | return $val; 175 | } 176 | 177 | if (empty($this->_context['entity'])) { 178 | return null; 179 | } 180 | 181 | $parts = explode('.', $field); 182 | $entity = $this->entity($parts); 183 | 184 | if ($entity instanceof Document) { 185 | return $entity->get(array_pop($parts)); 186 | } 187 | 188 | if ($this->_context['entity'] instanceof Document) { 189 | return Hash::get($this->_context['entity'], $field); 190 | } 191 | 192 | return null; 193 | } 194 | 195 | /** 196 | * Get the entity that is closest to $path. 197 | * 198 | * @param array $path The to get an entity for. 199 | * @return \Cake\Datasource\EntityInterface|array|false The entity or false. 200 | * @throws \RuntimeException when no entity can be found. 201 | */ 202 | protected function entity(array $path): object|array|false 203 | { 204 | $oneElement = count($path) === 1; 205 | if ($oneElement && $this->_isCollection) { 206 | return false; 207 | } 208 | 209 | $entity = $this->_context['entity']; 210 | if ($oneElement) { 211 | return $entity; 212 | } 213 | 214 | if ($path[0] === $this->_rootName) { 215 | $path = array_slice($path, 1); 216 | } 217 | 218 | $len = count($path); 219 | $last = $len - 1; 220 | for ($i = 0; $i < $len; $i++) { 221 | $prop = $path[$i]; 222 | $next = $this->getProp($entity, $prop); 223 | $isLast = ($i === $last); 224 | 225 | if (!$isLast && $next === null && $prop !== '_ids') { 226 | return false; 227 | } 228 | 229 | $isTraversable = ( 230 | is_array($next) || 231 | $next instanceof Traversable || 232 | $next instanceof Document 233 | ); 234 | 235 | if ($isLast || !$isTraversable) { 236 | return $entity; 237 | } 238 | $entity = $next; 239 | } 240 | 241 | throw new RuntimeException( 242 | sprintf( 243 | 'Unable to fetch property "%s"', 244 | implode('.', $path), 245 | ), 246 | ); 247 | } 248 | 249 | /** 250 | * Read property values or traverse arrays/iterators. 251 | * 252 | * @param mixed $target The entity/array/collection to fetch $field from. 253 | * @param string $field The next field to fetch. 254 | * @return mixed 255 | */ 256 | protected function getProp(mixed $target, string $field): mixed 257 | { 258 | if (is_array($target) && isset($target[$field])) { 259 | return $target[$field]; 260 | } 261 | 262 | if ($target instanceof Document) { 263 | return $target->get($field); 264 | } 265 | 266 | if ($target instanceof Traversable) { 267 | foreach ($target as $i => $val) { 268 | if ($i == $field) { 269 | return $val; 270 | } 271 | } 272 | 273 | return false; 274 | } 275 | 276 | return null; 277 | } 278 | 279 | /** 280 | * @inheritDoc 281 | */ 282 | public function isRequired(string $field): bool 283 | { 284 | $parts = explode('.', $field); 285 | $entity = $this->entity($parts); 286 | 287 | if (!$entity) { 288 | return false; 289 | } 290 | 291 | $isNew = true; 292 | if ($entity instanceof Document) { 293 | $isNew = $entity->isNew(); 294 | } 295 | $validator = $this->getValidator(); 296 | 297 | $field = array_pop($parts); 298 | if (!$validator->hasField($field)) { 299 | return false; 300 | } 301 | 302 | if ($this->type($field) !== 'boolean') { 303 | return $validator->isEmptyAllowed($field, $isNew) === false; 304 | } 305 | 306 | return false; 307 | } 308 | 309 | /** 310 | * @inheritDoc 311 | */ 312 | public function getRequiredMessage(string $field): ?string 313 | { 314 | $parts = explode('.', $field); 315 | 316 | $validator = $this->getValidator(); 317 | $fieldName = array_pop($parts); 318 | if (!$validator->hasField($fieldName)) { 319 | return null; 320 | } 321 | 322 | $ruleset = $validator->field($fieldName); 323 | 324 | $requiredMessage = $validator->getRequiredMessage($fieldName); 325 | $emptyMessage = $validator->getNotEmptyMessage($fieldName); 326 | 327 | if ($ruleset->isPresenceRequired() && $requiredMessage) { 328 | return $requiredMessage; 329 | } 330 | if (!$ruleset->isEmptyAllowed() && $emptyMessage) { 331 | return $emptyMessage; 332 | } 333 | 334 | return null; 335 | } 336 | 337 | /** 338 | * Get field length from validation 339 | * 340 | * @param string $field The dot separated path to the field you want to check. 341 | * @return int|null 342 | */ 343 | public function getMaxLength(string $field): ?int 344 | { 345 | $parts = explode('.', $field); 346 | $validator = $this->getValidator(); 347 | $fieldName = array_pop($parts); 348 | if (!$validator->hasField($fieldName)) { 349 | return null; 350 | } 351 | foreach ($validator->field($fieldName)->rules() as $rule) { 352 | if ($rule->get('rule') === 'maxLength') { 353 | return $rule->get('pass')[0]; 354 | } 355 | } 356 | 357 | return null; 358 | } 359 | 360 | /** 361 | * Get the validator for the current index. 362 | * 363 | * @return \Cake\Validation\Validator The validator for the index. 364 | */ 365 | protected function getValidator(): Validator 366 | { 367 | return $this->_context['index']->getValidator($this->_context['validator']); 368 | } 369 | 370 | /** 371 | * @inheritDoc 372 | */ 373 | public function fieldNames(): array 374 | { 375 | $schema = $this->_context['index']->schema(); 376 | 377 | return $schema->fields(); 378 | } 379 | 380 | /** 381 | * @inheritDoc 382 | */ 383 | public function type(string $field): ?string 384 | { 385 | $schema = $this->_context['index']->schema(); 386 | 387 | return $schema->fieldType($field); 388 | } 389 | 390 | /** 391 | * @inheritDoc 392 | */ 393 | public function attributes(string $field): array 394 | { 395 | return ['length' => null, 'precision' => null]; 396 | } 397 | 398 | /** 399 | * @inheritDoc 400 | */ 401 | public function hasError(string $field): bool 402 | { 403 | return $this->error($field) !== []; 404 | } 405 | 406 | /** 407 | * @inheritDoc 408 | */ 409 | public function error(string $field): array 410 | { 411 | $parts = explode('.', $field); 412 | $entity = $this->entity($parts); 413 | $entityErrors = []; 414 | $errors = []; 415 | 416 | if ($this->_context['entity'] instanceof Document) { 417 | $entityErrors = $this->_context['entity']->getErrors(); 418 | } 419 | 420 | $tailField = array_pop($parts); 421 | if ($entity instanceof Document) { 422 | $errors = $entity->getError($tailField); 423 | } 424 | 425 | // If errors couldn't be read from $entity and $entity 426 | // is either not an array, or the tail field is not Document 427 | // we want to extract errors from the root entity as we could 428 | // have nested validators, or structured fields. 429 | if ( 430 | !$errors && 431 | $entityErrors && 432 | (!is_array($entity) || !($entity[$tailField] instanceof Document)) 433 | ) { 434 | $errors = Hash::extract($entityErrors, $field) ?: []; 435 | } 436 | 437 | return (array)$errors; 438 | } 439 | } 440 | --------------------------------------------------------------------------------