https://github.com/josbeir/cakephp-elastickit
A lightweight CakePHP 5 plugin for working with Elasticsearch using the official PHP client.
https://github.com/josbeir/cakephp-elastickit
Last synced: 8 months ago
JSON representation
A lightweight CakePHP 5 plugin for working with Elasticsearch using the official PHP client.
- Host: GitHub
- URL: https://github.com/josbeir/cakephp-elastickit
- Owner: josbeir
- License: mit
- Created: 2025-08-16T15:42:30.000Z (10 months ago)
- Default Branch: main
- Last Pushed: 2025-08-23T17:54:21.000Z (10 months ago)
- Last Synced: 2025-08-23T23:45:31.345Z (10 months ago)
- Language: PHP
- Size: 52.7 KB
- Stars: 0
- Watchers: 0
- Forks: 0
- Open Issues: 0
-
Metadata Files:
- Readme: README.md
- License: LICENSE.md
Awesome Lists containing this project
- awesome-cakephp - ElasticKit plugin - A lightweight plugin for working with Elasticsearch using the official PHP client. (Search)
README
# ElasticKit - a CakePHP Elasticsearch plugin
[](https://github.com/josbeir/cakephp-elastikit/actions/workflows/ci.yml)
[](https://phpstan.org/)
[](https://codecov.io/github/josbeir/cakephp-elastickit)
[](https://php.net/)
[](https://packagist.org/packages/josbeir/cakephp-elastickit)

A lightweight CakePHP 5 plugin for working with Elasticsearch using the [official PHP client](https://github.com/elastic/elasticsearch-php).
## Table of Contents
- [Design Philosophy](#design-philosophy)
- [Why this plugin?](#why-this-plugin)
- [How it differs from the original cakephp/elastic-search plugin](#how-it-differs-from-the-original-cakephpelastic-search-plugin)
- [Requirements](#requirements)
- [Installation](#installation)
- [Configuration](#configuration)
- [Defining an Index](#defining-an-index)
- [Document Entities](#document-entities)
- [Querying](#querying)
- [ResultSet API](#resultset-api)
- [CLI: manage indices](#cli-manage-indices)
- [Accessing indices from your code](#accessing-indices-from-your-code)
- [Logging and debugging](#logging-and-debugging)
- [What this plugin does NOT do](#what-this-plugin-does-not-do)
- [Contributing](#contributing)
- [License](#license)
## Design Philosophy
This 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.
**Not included**: ORM-style persistence, validation, or RepositoryInterface implementation.
**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.
## Why this plugin?
- **Official client**: Uses `elasticsearch/elasticsearch` instead of Elastica for better compatibility across ES versions
- **Minimal abstraction**: Thin layer over the official client - no CakePHP ORM or persistence logic
- **Flexible querying**: Use Spatie's query builder or the client API directly
- **Upgrade-friendly**: Fewer breaking changes between Elasticsearch major releases
## How it differs from the original cakephp/elastic-search plugin
**cakephp/elastic-search** (Elastica-based) provides a full ORM experience with types, persistence, and validation. **ElasticKit** takes a minimal approach:
- Uses the official Elasticsearch client (no Elastica)
- Thin wrapper with no ORM/persistence layer
- Choose your query method: Spatie's builder or direct client API
- Fewer breaking changes between Elasticsearch versions
Use the original plugin for ORM-style features. Use ElasticKit for clean access to the official client with CakePHP conveniences.
## Requirements
- PHP >= 8.2
- CakePHP >= 5.2
- Elasticsearch >= 8.5 / 9.x
> **Note**: Lock the `elasticsearch/elasticsearch` package version in your composer file to match your Elasticsearch server version for optimal compatibility:
## Installation
Install the dependencies in your Cake app:
```bash
composer require josbeir/cakephp-elastickit
```
Ensure the plugin is loaded (one of):
- In `Application::bootstrap()`:
```php
$this->addPlugin('ElasticKit');
```
- Or via `config/plugins.php` if you use the plugins file.
## Configuration
Register 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:
```php
use Cake\Datasource\ConnectionManager;
use ElasticKit\Datasource\Connection;
ConnectionManager::setConfig('elasticsearch', [
'className' => Connection::class,
'hosts' => [
// Use your cluster endpoints
'http://localhost:9200',
],
// Optional: any PSR-3 logger name registered with Cake\Log\Log or a LoggerInterface instance
'logger' => 'elasticsearch',
// All extra arguments are passed to \Elasticsearch\ClientBuilder
]);
```
By default, indices resolve to the `elasticsearch` connection name. You can override the connection per index class via options if needed.
### HTTP Client Configuration
By 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:
```php
use Cake\Datasource\ConnectionManager;
use ElasticKit\Datasource\Connection;
ConnectionManager::setConfig('elasticsearch', [
'className' => Connection::class,
'hosts' => ['http://localhost:9200'],
// Optional: Use a different HTTP client
// Default: Uses CakePHP's Http Client (great for testing/mocking, hijacking the request/response)
// 'httpClient' => new YourPsr18HttpClient(), // Any PSR-18 compatible client
// 'httpClient' => new Elastic\Transport\Client\Curl, // Use the default cURL client from elasticsearch/elasticsearch
]);
```
## Defining an Index
Create an index class in `src/Model/Index`, e.g. `ArticlesIndex`:
```php
namespace App\Model\Index;
use ElasticKit\Index;
class ArticlesIndex extends Index
{
public function initialize(): void
{
// Optional: set index alias/name explicitly; otherwise class name is underscored
// $this->setIndexName('articles');
// Optional: provide settings/mappings used by createIndex()/updateIndex()
$this->setSettings([
'number_of_shards' => 1,
'number_of_replicas' => 0,
]);
$this->setMappings([
'properties' => [
'title' => ['type' => 'text'],
'created' => ['type' => 'date'],
],
]);
}
}
```
## Document Entities
Document 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`.
```php
namespace App\Model\Document;
use ElasticKit\Document;
class Article extends Document
{
// Add accessors/mutators/virtuals as you like.
}
```
Document 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.
Access these properties using:
```php
$doc = $Articles->get('some-id');
$id = $doc->getDocumentId(); // Gets the Elasticsearch document ID
$score = $doc->getScore(); // Gets the search score (if from a search result)
```
## Querying
You can query in two ergonomic ways.
### 1) With [Spatie’s](https://github.com/spatie/elasticsearch-query-builder) query builder
The `Index::find()` method creates a `Spatie\ElasticsearchQueryBuilder\Builder`, sets the index, executes the search, and returns a `ResultSet` that yields `Document` instances.
```php
use Spatie\ElasticsearchQueryBuilder\Builder;
use Spatie\ElasticsearchQueryBuilder\Aggregations\MaxAggregation;
use Spatie\ElasticsearchQueryBuilder\Queries\MatchQuery;
$Articles = $this->fetchIndex('Articles');
// Using a closure
$results = $Articles->find(function (Builder $builder) {
return $builder
->addQuery(MatchQuery::create('name', 'elastickit', fuzziness: 'AUTO'))
->addAggregation(MaxAggregation::create('score'))
});
// Or by creating a builder instance and passing it to the find() function.
$query = $Articles->createBuilder()
->addQuery(MatchQuery::create('name', 'elastickit', fuzziness: 3));
$results = $Articles->find($query);
```
### 2) Directly via the official client
Every 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()->get()`:
```php
// Index's get() - returns a Document|null
$doc = $Articles->get('document_id');
// Client's get() - returns raw Elasticsearch response
$response = $Articles->getClient()->get([
'index' => $Articles->getIndexName(),
'id' => 'document_id'
]);
$response = $Articles->search([
'index' => $Articles->getIndexName(),
'body' => [
'query' => ['match_all' => ...],
'size' => 10,
],
]);
// Convert as needed
$resultset = $Articles->resultSet($response);
$array = $response->asArray();
$object = $response->asObject();
$ok = $response->asBool();
```
### Convenience methods
- `Index::get($id)`: Fetch a single document by id as a `Document|null`.
- `Index::resultSet($response)`: Wrap any Elasticsearch response in the plugin’s `ResultSet`.
The `ResultSet` exposes helpers like:
- `getTook()`, `getMaxScore()`, `getShards()`, `getHitsTotal()`
- Iterates documents, handles both search and bulk-style responses.
## ResultSet API
`ElasticKit\ResultSet` is an iterator over decorated `Document` instances and provides a few helpers around the Elasticsearch response.
- `getTook(): ?int` — The time (ms) the search took (if present).
- `getMaxScore(): ?float` — Max score for hits (if present).
- `getShards(): ?array` — Shard info from the response (if present).
- `getHitsTotal(): ?int` — Reported total hits value (may be `null` depending on ES settings like `track_total_hits`).
- `getAggregations(): ?array` — Aggregation results from the response (if present).
- `hasErrors(): bool` — Indicates whether the underlying response reported errors (useful for bulk responses).
- `getResponse(): ResponseInterface` — Access the raw Elasticsearch response object.
- `getBuilder(): Builder` — The builder instance.
Iteration
```php
foreach ($results as $doc) {
// $doc is \App\Model\Document\YourEntity (if present) or the base Document
}
```
Force a specific document class
```php
use App\Model\Document\Article;
$results->setDocumentClass(Article::class);
```
Note: ResultSet also includes common collection utilities via Cake’s CollectionTrait (e.g., `map()`, `filter()`, `toList()`), which can be handy for quick transformations.
## CLI: manage indices
The plugin ships a small command to create/update/delete indices based on your index class config (`settings`/`mappings`).
```bash
bin/cake elasticsearch index articles --create # create if missing
bin/cake elasticsearch index articles --update # put mapping
bin/cake elasticsearch index articles --delete # delete index
# Use a plugin or FQCN-style name if needed
bin/cake elasticsearch index App.Articles --create
```
Use `-v` to print current settings/mappings of the target index.
## Accessing indices from your code
Use the `IndexLocatorAwareTrait` to fetch indices by alias (class name without the `Index` suffix):
```php
use ElasticKit\Locator\IndexLocatorAwareTrait;
class ArticlesService
{
use IndexLocatorAwareTrait;
public function searchByTitle(string $q)
{
$Articles = $this->fetchIndex('Articles');
return $Articles->get(1234);
}
}
```
### PHPstan notes
When 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.
To resolve this while maintaining similarity with `IndexLocatorAwareTrait::fetchTable()`, add the following to your `phpstan.neon` configuration (requires `cakedc/cakephp-phpstan`):
```neon
# Support IndexLocatorAwareTrait
services:
-
factory: CakeDC\PHPStan\Type\BaseTraitExpressionTypeResolverExtension(ElasticKit\Locator\IndexLocatorAwareTrait, fetchIndex, %s\Model\Index\%sIndex)
tags:
- phpstan.broker.expressionTypeResolverExtension
```
This extension teaches PHPStan to recognize that `$this->fetchIndex('Articles')` returns an `ArticlesIndex` instance, eliminating type errors.
## Logging and debugging
- Pass a PSR-3 logger (or the name of a Cake log engine) to the connection config via `logger` to capture client requests.
- Convert responses with `->asArray()` or `->asObject()` while developing.
### DebugKit panel
This plugin includes a DebugKit panel that displays Elasticsearch requests per configured connection.
- Ensure `cakephp/debug_kit` is installed and enabled in development.
- The panel appears as “Elasticsearch” in the DebugKit toolbar.
- It hooks into `ElasticKit\Datasource\Connection` loggers at runtime and shows:
- Count of requests per connection
- Message and a prettified JSON body
To enable the panel, add it to your DebugKit configuration:
```php
Configure::write('DebugKit.panels', ['ElasticKit.Elasticsearch']);
```
## What this plugin does NOT do
- No ORM-like persistence (no `save()`, no automatic validation). You decide how to index/update/delete documents.
- No type mapping or schema inference. Provide your own index settings/mappings.
- No custom query DSL layer beyond the optional Spatie builder helper.
This is by design to keep the integration thin, explicit, and resilient to upstream changes.
## Contributing
Contributions 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.
### Development Setup
1. Clone the repository
2. Install dependencies: `composer install`
3. Run tests: `composer test`
Please make sure to update tests as appropriate and follow the existing code style.
## License
This project is licensed under the MIT License - see the [LICENSE](LICENSE.md) file for details.