josbeir / cakephp-elastickit
A lightweight CakePHP 5 plugin for working with Elasticsearch using the official PHP client.
Installs: 17
Dependents: 0
Suggesters: 0
Security: 0
Stars: 0
Watchers: 0
Forks: 0
Open Issues: 0
Type:cakephp-plugin
Requires
- php: >=8.2
- cakephp/cakephp: ^5.2
- elasticsearch/elasticsearch: ^8.5 || ^9.0
- spatie/elasticsearch-query-builder: ^3.8
Requires (Dev)
- cakedc/cakephp-phpstan: ^4.0
- cakephp/cakephp-codesniffer: ^5.2
- cakephp/debug_kit: ^5.0
- cakephp/plugin-installer: ^2.0.1
- phpstan/phpstan: ^2.1
- phpunit/phpunit: ^11.0 || ^12.0
- rector/rector: ^2.1
README
A lightweight CakePHP 5 plugin for working with Elasticsearch using the official PHP client.
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 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:
composer require josbeir/cakephp-elastickit
Ensure the plugin is loaded (one of):
- In
Application::bootstrap()
:
$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:
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:
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
:
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
.
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:
$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 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.
use Spatie\ElasticsearchQueryBuilder\Builder; use Spatie\ElasticsearchQueryBuilder\Aggregations\MaxAggregation; use Spatie\ElasticsearchQueryBuilder\Queries\MatchQuery; $Articles = $this->fetchIndex('Articles'); $results = $Articles->find(function (Builder $builder) { return $builder ->addQuery(MatchQuery::create('name', 'elastickit', fuzziness: 3)) ->addAggregation(MaxAggregation::create('score')) });
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()
:
// 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 aDocument|null
.Index::resultSet($response)
: Wrap any Elasticsearch response in the plugin’sResultSet
.
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 benull
depending on ES settings liketrack_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.
Iteration
foreach ($results as $doc) { // $doc is \App\Model\Document\YourEntity (if present) or the base Document }
Force a specific document class
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
).
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):
use ElasticKit\Locator\IndexLocatorAwareTrait; class ArticlesService { use IndexLocatorAwareTrait; public function searchByTitle(string $q) { $Articles = $this->fetchIndex('Articles'); return $Articles->get(1234); } }
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:
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
- Clone the repository
- Install dependencies:
composer install
- 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 file for details.