{"id":30587030,"url":"https://github.com/josbeir/cakephp-elastickit","last_synced_at":"2025-11-04T01:03:54.964Z","repository":{"id":310234823,"uuid":"1039154419","full_name":"josbeir/cakephp-elastickit","owner":"josbeir","description":"A lightweight CakePHP 5 plugin for working with Elasticsearch using the official PHP client.","archived":false,"fork":false,"pushed_at":"2025-08-23T17:54:21.000Z","size":54,"stargazers_count":0,"open_issues_count":0,"forks_count":0,"subscribers_count":0,"default_branch":"main","last_synced_at":"2025-08-23T23:45:31.345Z","etag":null,"topics":[],"latest_commit_sha":null,"homepage":null,"language":"PHP","has_issues":true,"has_wiki":null,"has_pages":null,"mirror_url":null,"source_name":null,"license":"mit","status":null,"scm":"git","pull_requests_enabled":true,"icon_url":"https://github.com/josbeir.png","metadata":{"files":{"readme":"README.md","changelog":null,"contributing":null,"funding":null,"license":"LICENSE.md","code_of_conduct":null,"threat_model":null,"audit":null,"citation":null,"codeowners":null,"security":null,"support":null,"governance":null,"roadmap":null,"authors":null,"dei":null,"publiccode":null,"codemeta":null,"zenodo":null}},"created_at":"2025-08-16T15:42:30.000Z","updated_at":"2025-08-23T17:54:24.000Z","dependencies_parsed_at":"2025-08-16T22:17:52.961Z","dependency_job_id":null,"html_url":"https://github.com/josbeir/cakephp-elastickit","commit_stats":null,"previous_names":["josbeir/cakephp-elastikit","josbeir/cakephp-elastickit"],"tags_count":4,"template":false,"template_full_name":null,"purl":"pkg:github/josbeir/cakephp-elastickit","repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/josbeir%2Fcakephp-elastickit","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/josbeir%2Fcakephp-elastickit/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/josbeir%2Fcakephp-elastickit/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/josbeir%2Fcakephp-elastickit/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/josbeir","download_url":"https://codeload.github.com/josbeir/cakephp-elastickit/tar.gz/refs/heads/main","sbom_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/josbeir%2Fcakephp-elastickit/sbom","scorecard":null,"host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":272680464,"owners_count":24975267,"icon_url":"https://github.com/github.png","version":null,"created_at":"2022-05-30T11:31:42.601Z","updated_at":"2022-07-04T15:15:14.044Z","status":"online","status_checked_at":"2025-08-29T02:00:10.610Z","response_time":87,"last_error":null,"robots_txt_status":"success","robots_txt_updated_at":"2025-07-24T06:49:26.215Z","robots_txt_url":"https://github.com/robots.txt","online":true,"can_crawl_api":true,"host_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub","repositories_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories","repository_names_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repository_names","owners_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners"}},"keywords":[],"created_at":"2025-08-29T12:02:10.538Z","updated_at":"2025-11-04T01:03:54.354Z","avatar_url":"https://github.com/josbeir.png","language":"PHP","funding_links":[],"categories":["Search"],"sub_categories":[],"readme":"# ElasticKit - a CakePHP Elasticsearch plugin \n[![CI](https://github.com/josbeir/cakephp-elastikit/actions/workflows/ci.yml/badge.svg?branch=main)](https://github.com/josbeir/cakephp-elastikit/actions/workflows/ci.yml)\n[![PHPStan](https://img.shields.io/badge/PHPStan-level%208-brightgreen.svg?style=flat)](https://phpstan.org/)\n[![codecov](https://codecov.io/github/josbeir/cakephp-elastickit/graph/badge.svg?token=4VGWJQTWH5)](https://codecov.io/github/josbeir/cakephp-elastickit)\n[![Minimum PHP Version](https://img.shields.io/badge/php-%3E%3D%208.2-8892BF.svg)](https://php.net/)\n[![Packagist Downloads](https://img.shields.io/packagist/dt/josbeir/cakephp-elastickit)](https://packagist.org/packages/josbeir/cakephp-elastickit)\n![GitHub License](https://img.shields.io/github/license/josbeir/cakephp-elastickit)\n\nA lightweight CakePHP 5 plugin for working with Elasticsearch using the [official PHP client](https://github.com/elastic/elasticsearch-php).\n\n## Table of Contents\n\n- [Design Philosophy](#design-philosophy)\n- [Why this plugin?](#why-this-plugin)\n- [How it differs from the original cakephp/elastic-search plugin](#how-it-differs-from-the-original-cakephpelastic-search-plugin)\n- [Requirements](#requirements)\n- [Installation](#installation)\n- [Configuration](#configuration)\n- [Defining an Index](#defining-an-index)\n- [Document Entities](#document-entities)\n- [Querying](#querying)\n- [ResultSet API](#resultset-api)\n- [CLI: manage indices](#cli-manage-indices)\n- [Accessing indices from your code](#accessing-indices-from-your-code)\n- [Logging and debugging](#logging-and-debugging)\n- [What this plugin does NOT do](#what-this-plugin-does-not-do)\n- [Contributing](#contributing)\n- [License](#license)\n\n## Design Philosophy\n\nThis plugin provides minimal abstraction over the official Elasticsearch client. It handles connection management, index location, and result decoration while leaving persistence and validation to you.\n\n**Not included**: ORM-style persistence, validation, or RepositoryInterface implementation.\n\n**Want full ORM features?** Use [cakephp/elastic-search](https://github.com/cakephp/elastic-search) instead, which provides an ORM similar to CakePHP's database layer.\n\n## Why this plugin?\n\n- **Official client**: Uses `elasticsearch/elasticsearch` instead of Elastica for better compatibility across ES versions\n- **Minimal abstraction**: Thin layer over the official client - no CakePHP ORM or persistence logic\n- **Flexible querying**: Use Spatie's query builder or the client API directly\n- **Upgrade-friendly**: Fewer breaking changes between Elasticsearch major releases\n\n## How it differs from the original cakephp/elastic-search plugin\n\n**cakephp/elastic-search** (Elastica-based) provides a full ORM experience with types, persistence, and validation. **ElasticKit** takes a minimal approach:\n\n- Uses the official Elasticsearch client (no Elastica)\n- Thin wrapper with no ORM/persistence layer\n- Choose your query method: Spatie's builder or direct client API\n- Fewer breaking changes between Elasticsearch versions\n\nUse the original plugin for ORM-style features. Use ElasticKit for clean access to the official client with CakePHP conveniences.\n\n## Requirements\n\n- PHP \u003e= 8.2\n- CakePHP \u003e= 5.2\n- Elasticsearch \u003e= 8.5 / 9.x\n\n\u003e **Note**: Lock the `elasticsearch/elasticsearch` package version in your composer file to match your Elasticsearch server version for optimal compatibility:\n\n## Installation\n\nInstall the dependencies in your Cake app:\n\n```bash\ncomposer require josbeir/cakephp-elastickit\n```\n\nEnsure the plugin is loaded (one of):\n\n- In `Application::bootstrap()`:\n\n```php\n$this-\u003eaddPlugin('ElasticKit');\n```\n\n- Or via `config/plugins.php` if you use the plugins file.\n\n## Configuration\n\nRegister an Elasticsearch connection using the plugin’s connection class. You can do this in `config/bootstrap.php` or in a config file loaded during bootstrap:\n\n```php\nuse Cake\\Datasource\\ConnectionManager;\nuse ElasticKit\\Datasource\\Connection;\n\nConnectionManager::setConfig('elasticsearch', [\n\t'className' =\u003e Connection::class,\n\t'hosts' =\u003e [\n\t\t// Use your cluster endpoints\n\t\t'http://localhost:9200',\n\t],\n\t// Optional: any PSR-3 logger name registered with Cake\\Log\\Log or a LoggerInterface instance\n\t'logger' =\u003e 'elasticsearch',\n\t\n    // All extra arguments are passed to \\Elasticsearch\\ClientBuilder\n]);\n```\n\nBy default, indices resolve to the `elasticsearch` connection name. You can override the connection per index class via options if needed.\n\n\n### HTTP Client Configuration\n\nBy default, the connection uses CakePHP's Http Client, which makes it easy to mock requests during testing. You can override this behavior using the `httpClient` option:\n\n```php\nuse Cake\\Datasource\\ConnectionManager;\nuse ElasticKit\\Datasource\\Connection;\n\nConnectionManager::setConfig('elasticsearch', [\n\t'className' =\u003e Connection::class,\n\t'hosts' =\u003e ['http://localhost:9200'],\n\t\n\t// Optional: Use a different HTTP client\n\t// Default: Uses CakePHP's Http Client (great for testing/mocking, hijacking the request/response)\n\t// 'httpClient' =\u003e new YourPsr18HttpClient(), // Any PSR-18 compatible client\n\t// 'httpClient' =\u003e new Elastic\\Transport\\Client\\Curl, // Use the default cURL client from elasticsearch/elasticsearch\n]);\n```\n\n## Defining an Index\n\nCreate an index class in `src/Model/Index`, e.g. `ArticlesIndex`:\n\n```php\nnamespace App\\Model\\Index;\n\nuse ElasticKit\\Index;\n\nclass ArticlesIndex extends Index\n{\n\tpublic function initialize(): void\n\t{\n\t\t// Optional: set index alias/name explicitly; otherwise class name is underscored\n\t\t// $this-\u003esetIndexName('articles');\n\n\t\t// Optional: provide settings/mappings used by createIndex()/updateIndex()\n\t\t$this-\u003esetSettings([\n\t\t\t'number_of_shards' =\u003e 1,\n\t\t\t'number_of_replicas' =\u003e 0,\n\t\t]);\n\n\t\t$this-\u003esetMappings([\n\t\t\t'properties' =\u003e [\n\t\t\t\t'title' =\u003e ['type' =\u003e 'text'],\n\t\t\t\t'created' =\u003e ['type' =\u003e 'date'],\n\t\t\t],\n\t\t]);\n\t}\n}\n```\n\n## Document Entities\n\nDocument entities are resolved automatically from your index name. For an index named `articles`, the plugin will try `App\\Model\\Document\\Article`. If not present, it falls back to the generic `ElasticKit\\Document`.\n\n```php\nnamespace App\\Model\\Document;\n\nuse ElasticKit\\Document;\n\nclass Article extends Document\n{\n\t// Add accessors/mutators/virtuals as you like.\n}\n```\n\nDocument entities follow CakePHP's EntityInterface with one key difference: the document ID and score are stored in a reserved property to avoid field name collisions.\n\nAccess these properties using:\n\n```php\n$doc = $Articles-\u003eget('some-id');\n$id = $doc-\u003egetDocumentId();     // Gets the Elasticsearch document ID\n$score = $doc-\u003egetScore();       // Gets the search score (if from a search result)\n```\n\n## Querying\n\nYou can query in two ergonomic ways.\n\n### 1) With [Spatie’s](https://github.com/spatie/elasticsearch-query-builder) query builder\n\nThe `Index::find()` method creates a `Spatie\\ElasticsearchQueryBuilder\\Builder`, sets the index, executes the search, and returns a `ResultSet` that yields `Document` instances.\n\n```php\nuse Spatie\\ElasticsearchQueryBuilder\\Builder;\nuse Spatie\\ElasticsearchQueryBuilder\\Aggregations\\MaxAggregation;\nuse Spatie\\ElasticsearchQueryBuilder\\Queries\\MatchQuery;\n\n$Articles = $this-\u003efetchIndex('Articles');\n\n// Using a closure\n$results = $Articles-\u003efind(function (Builder $builder) {\n\treturn $builder\n\t\t-\u003eaddQuery(MatchQuery::create('name', 'elastickit', fuzziness: 'AUTO'))\n\t\t-\u003eaddAggregation(MaxAggregation::create('score'))\n});\n\n// Or by creating a builder instance and passing it to the find() function.\n$query = $Articles-\u003ecreateBuilder()\n\t-\u003eaddQuery(MatchQuery::create('name', 'elastickit', fuzziness: 3));\n\n$results = $Articles-\u003efind($query);\n```\n\n### 2) Directly via the official client\nEvery unknown method call on your index instance proxies to the underlying `Elastic\\Elasticsearch\\Client`, so you can use the full API. Note that while `get()` is reserved by the Index class for fetching single documents, you can still access the client's `get()` method via `getClient()-\u003eget()`:\n\n```php\n// Index's get() - returns a Document|null\n$doc = $Articles-\u003eget('document_id');\n\n// Client's get() - returns raw Elasticsearch response\n$response = $Articles-\u003egetClient()-\u003eget([\n\t'index' =\u003e $Articles-\u003egetIndexName(),\n\t'id' =\u003e 'document_id'\n]);\n\n$response = $Articles-\u003esearch([\n\t'index' =\u003e $Articles-\u003egetIndexName(),\n\t'body' =\u003e [\n\t\t'query' =\u003e ['match_all' =\u003e ...],\n\t\t'size' =\u003e 10,\n\t],\n]);\n\n// Convert as needed\n$resultset = $Articles-\u003eresultSet($response);\n$array = $response-\u003easArray();\n$object = $response-\u003easObject();\n$ok = $response-\u003easBool();\n```\n\n### Convenience methods\n\n- `Index::get($id)`: Fetch a single document by id as a `Document|null`.\n- `Index::resultSet($response)`: Wrap any Elasticsearch response in the plugin’s `ResultSet`.\n\nThe `ResultSet` exposes helpers like:\n\n- `getTook()`, `getMaxScore()`, `getShards()`, `getHitsTotal()`\n- Iterates documents, handles both search and bulk-style responses.\n\n## ResultSet API\n\n`ElasticKit\\ResultSet` is an iterator over decorated `Document` instances and provides a few helpers around the Elasticsearch response.\n\n- `getTook(): ?int` — The time (ms) the search took (if present).\n- `getMaxScore(): ?float` — Max score for hits (if present).\n- `getShards(): ?array` — Shard info from the response (if present).\n- `getHitsTotal(): ?int` — Reported total hits value (may be `null` depending on ES settings like `track_total_hits`).\n- `getAggregations(): ?array` — Aggregation results from the response (if present).\n- `hasErrors(): bool` — Indicates whether the underlying response reported errors (useful for bulk responses).\n- `getResponse(): ResponseInterface` — Access the raw Elasticsearch response object.\n- `getBuilder(): Builder` — The builder instance.\n\nIteration\n\n```php\nforeach ($results as $doc) {\n\t// $doc is \\App\\Model\\Document\\YourEntity (if present) or the base Document\n}\n```\n\nForce a specific document class\n\n```php\nuse App\\Model\\Document\\Article;\n\n$results-\u003esetDocumentClass(Article::class);\n```\n\nNote: ResultSet also includes common collection utilities via Cake’s CollectionTrait (e.g., `map()`, `filter()`, `toList()`), which can be handy for quick transformations.\n\n## CLI: manage indices\n\nThe plugin ships a small command to create/update/delete indices based on your index class config (`settings`/`mappings`).\n\n```bash\nbin/cake elasticsearch index articles --create  # create if missing\nbin/cake elasticsearch index articles --update  # put mapping\nbin/cake elasticsearch index articles --delete  # delete index\n\n# Use a plugin or FQCN-style name if needed\nbin/cake elasticsearch index App.Articles --create\n```\n\nUse `-v` to print current settings/mappings of the target index.\n\n## Accessing indices from your code\n\nUse the `IndexLocatorAwareTrait` to fetch indices by alias (class name without the `Index` suffix):\n\n```php\nuse ElasticKit\\Locator\\IndexLocatorAwareTrait;\n\nclass ArticlesService\n{\n\tuse IndexLocatorAwareTrait;\n\n\tpublic function searchByTitle(string $q)\n\t{\n\t\t$Articles = $this-\u003efetchIndex('Articles');\n\n\t\treturn $Articles-\u003eget(1234);\n\t}\n}\n```\n\n### PHPstan notes\n\nWhen using `IndexLocatorAwareTrait::fetchIndex()`, PHPStan will complain about `assign.propertyType` when assigning the result to a typed property. This happens because PHPStan can't infer the specific index class being returned.\n\nTo resolve this while maintaining similarity with `IndexLocatorAwareTrait::fetchTable()`, add the following to your `phpstan.neon` configuration (requires `cakedc/cakephp-phpstan`):\n\n```neon\n# Support IndexLocatorAwareTrait\nservices:\n\t-\n\t\tfactory: CakeDC\\PHPStan\\Type\\BaseTraitExpressionTypeResolverExtension(ElasticKit\\Locator\\IndexLocatorAwareTrait, fetchIndex, %s\\Model\\Index\\%sIndex)\n\t\ttags:\n\t\t\t- phpstan.broker.expressionTypeResolverExtension\n```\n\nThis extension teaches PHPStan to recognize that `$this-\u003efetchIndex('Articles')` returns an `ArticlesIndex` instance, eliminating type errors.\n\n\n## Logging and debugging\n\n- Pass a PSR-3 logger (or the name of a Cake log engine) to the connection config via `logger` to capture client requests.\n- Convert responses with `-\u003easArray()` or `-\u003easObject()` while developing.\n\n### DebugKit panel\n\nThis plugin includes a DebugKit panel that displays Elasticsearch requests per configured connection.\n\n- Ensure `cakephp/debug_kit` is installed and enabled in development.\n- The panel appears as “Elasticsearch” in the DebugKit toolbar.\n- It hooks into `ElasticKit\\Datasource\\Connection` loggers at runtime and shows:\n\t- Count of requests per connection\n\t- Message and a prettified JSON body\n\nTo enable the panel, add it to your DebugKit configuration:\n\n```php\nConfigure::write('DebugKit.panels', ['ElasticKit.Elasticsearch']);\n```\n\n## What this plugin does NOT do\n\n- No ORM-like persistence (no `save()`, no automatic validation). You decide how to index/update/delete documents.\n- No type mapping or schema inference. Provide your own index settings/mappings.\n- No custom query DSL layer beyond the optional Spatie builder helper.\n\nThis is by design to keep the integration thin, explicit, and resilient to upstream changes.\n\n## Contributing\n\nContributions are welcome! Please feel free to submit a Pull Request. For major changes, please open an issue first to discuss what you would like to change.\n\n### Development Setup\n\n1. Clone the repository\n2. Install dependencies: `composer install`\n3. Run tests: `composer test`\n\nPlease make sure to update tests as appropriate and follow the existing code style.\n\n## License\n\nThis project is licensed under the MIT License - see the [LICENSE](LICENSE.md) file for details.\n","project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fjosbeir%2Fcakephp-elastickit","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Fjosbeir%2Fcakephp-elastickit","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fjosbeir%2Fcakephp-elastickit/lists"}