├── .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 | 
4 | [](https://packagist.org/packages/cakephp/elastic-search)
5 | [](https://packagist.org/packages/cakephp/elastic-search/stats)
6 | [](https://coveralls.io/r/cakephp/elastic-search?branch=master)
7 | [](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 | = $this->Form->create($article) ?>
88 | = $this->Form->control('title') ?>
89 | = $this->Form->control('body') ?>
90 | = $this->Form->button('Save') ?>
91 | = $this->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 | = $this->Form->create($article) ?>
81 | = $this->Form->control('title') ?>
82 | = $this->Form->control('body') ?>
83 | = $this->Form->button('Save') ?>
84 | = $this->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 | = $this->Form->create($article) ?>
79 | = $this->Form->control('title') ?>
80 | = $this->Form->control('body') ?>
81 | = $this->Form->button('Save') ?>
82 | = $this->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 | = $this->Form->create($article) ?>
84 | = $this->Form->input('title') ?>
85 | = $this->Form->input('body') ?>
86 | = $this->Form->button('Save') ?>
87 | = $this->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 |
--------------------------------------------------------------------------------