|
| 1 | +# CLAUDE.md |
| 2 | + |
| 3 | +This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository. |
| 4 | + |
| 5 | +## Project Overview |
| 6 | + |
| 7 | +This is **Spameri/Elastic** - an ElasticSearch implementation for the Nette Framework. The library provides a typed object-oriented interface to ElasticSearch, where queries, documents, and responses are all represented as PHP objects rather than raw arrays. It follows an ORM-like pattern with an EntityManager for persistence operations. |
| 8 | + |
| 9 | +Key features: |
| 10 | +- Typed entities extending `AbstractElasticEntity` |
| 11 | +- EntityManager with persist/remove/find operations similar to Doctrine ORM |
| 12 | +- Event system for lifecycle hooks (pre/post persist, create, update, delete) |
| 13 | +- Single Table Inheritance (STI) support via `STIElasticEntityInterface` |
| 14 | +- Type-safe query building via `spameri/elastic-query` package |
| 15 | +- Index management commands via Symfony Console |
| 16 | +- Import system with locking and progress tracking |
| 17 | +- Identity Map pattern for entity caching |
| 18 | +- Tracy debug bar integration |
| 19 | + |
| 20 | +## Development Commands |
| 21 | + |
| 22 | +### Testing |
| 23 | +```bash |
| 24 | +# Run all tests |
| 25 | +make tests |
| 26 | + |
| 27 | +# Run tests locally (single thread) |
| 28 | +make tests-local |
| 29 | + |
| 30 | +# Run single test file |
| 31 | +vendor/bin/tester tests/SpameriTests/path/to/test.phpt |
| 32 | +``` |
| 33 | + |
| 34 | +### Code Quality |
| 35 | +```bash |
| 36 | +# Run PHPStan static analysis (level 6) |
| 37 | +make phpstan |
| 38 | + |
| 39 | +# Run PHPStan with lowest dependencies |
| 40 | +make phpstan-lowest |
| 41 | + |
| 42 | +# Check coding standards |
| 43 | +make cs |
| 44 | + |
| 45 | +# Fix coding standards automatically |
| 46 | +make csf |
| 47 | + |
| 48 | +# Generate code coverage |
| 49 | +make coverage |
| 50 | +``` |
| 51 | + |
| 52 | +### Dependencies |
| 53 | +```bash |
| 54 | +# Update composer dependencies (stable) |
| 55 | +make composer |
| 56 | + |
| 57 | +# Update to lowest stable versions |
| 58 | +make composer-lowest |
| 59 | +``` |
| 60 | + |
| 61 | +### ElasticSearch Index Management |
| 62 | +```bash |
| 63 | +# Create an index (requires entity mapping class) |
| 64 | +php bin/console elastic:create-index <index-name> |
| 65 | + |
| 66 | +# Delete an index |
| 67 | +php bin/console elastic:delete-index <index-name> |
| 68 | + |
| 69 | +# Initialize all indexes from configuration |
| 70 | +php bin/console elastic:initialize-indexes |
| 71 | + |
| 72 | +# Dump index data to file |
| 73 | +php bin/console elastic:dump-index <index-name> |
| 74 | + |
| 75 | +# Load dumped data back |
| 76 | +php bin/console elastic:load-dump <file-path> |
| 77 | + |
| 78 | +# Add/remove aliases |
| 79 | +php bin/console elastic:add-alias <index-name> <alias> |
| 80 | +php bin/console elastic:remove-alias <index-name> <alias> |
| 81 | +``` |
| 82 | + |
| 83 | +## Architecture |
| 84 | + |
| 85 | +### Core Components |
| 86 | + |
| 87 | +**EntityManager** (`src/EntityManager.php`) |
| 88 | +- Central entry point for all entity operations |
| 89 | +- Methods: `find()`, `findOneBy()`, `findBy()`, `findAll()`, `persist()`, `remove()` |
| 90 | +- Uses Identity Map pattern to cache entities and prevent duplicates |
| 91 | +- Dispatches lifecycle events through EventManager |
| 92 | +- All find methods return `ElasticEntityCollection` (except `findOneBy`) |
| 93 | + |
| 94 | +**Entity Layer** (`src/Entity/`) |
| 95 | +- All entities extend `AbstractElasticEntity` which implements `ElasticEntityInterface` |
| 96 | +- Required property: `ElasticIdInterface $id` - managed by ElasticSearch |
| 97 | +- Required methods: `id()`, `entityVariables()` |
| 98 | +- Entity properties should use value objects implementing `ValueInterface` for validation |
| 99 | +- STI support: entities can implement `STIElasticEntityInterface` for polymorphic storage |
| 100 | + |
| 101 | +**Model Layer** (`src/Model/`) |
| 102 | +- Service classes for specific operations: `Insert`, `GetAllBy`, `Delete`, `Search`, `Aggregate`, `Scroll` |
| 103 | +- `EntitySettingsLocator` - maps entity classes to their index configurations |
| 104 | +- `IdentityMap` - tracks loaded entities to prevent duplicates and enable references |
| 105 | +- `ChangeSet` - tracks whether entity is new or existing for event dispatching |
| 106 | +- `VersionProvider` - manages ElasticSearch version compatibility |
| 107 | + |
| 108 | +**Factory Pattern** (`src/Factory/`) |
| 109 | +- `EntityFactory` - creates entity instances from ElasticSearch Hit objects |
| 110 | +- Uses reflection to construct entities with proper property types |
| 111 | +- Integrates with Identity Map to reuse existing instances |
| 112 | +- Handles nested objects and collections recursively |
| 113 | + |
| 114 | +**Event System** (`src/EventManager.php`, `src/EventManager/`) |
| 115 | +- Events: `PRE_PERSIST`, `POST_PERSIST`, `POST_CREATE`, `POST_UPDATE`, `PRE_DELETE`, `POST_DELETE` |
| 116 | +- Listeners implement `ListenerInterface` with `getEvent()`, `getEntityClass()`, `handle()` methods |
| 117 | +- Auto-discovered from DI container |
| 118 | +- Both global event manager and per-entity `dispatchEvents()` support |
| 119 | + |
| 120 | +**Import System** (`src/Import/`) |
| 121 | +- `Run` and `SimpleRun` - orchestrate data imports with progress tracking |
| 122 | +- `LockInterface` implementations - prevent concurrent imports (`FileLock`, `NullLock`) |
| 123 | +- `DataProviderInterface` - source of import data |
| 124 | +- `PrepareImportDataInterface` - transforms data before import |
| 125 | +- `AfterImportInterface` - post-import hooks |
| 126 | +- Exception hierarchy for error handling: `Fatal`, `Error`, `Omit`, `AlreadyLocked` |
| 127 | + |
| 128 | +**Mapping System** (`src/Mapping/`) |
| 129 | +- Attributes for entity mapping: `@Entity`, `@Collection`, `@ElasticCollection`, `@STIEntity`, `@STIElasticEntity`, `@Ignored` |
| 130 | +- Used by reflection to understand entity structure during persistence/hydration |
| 131 | + |
| 132 | +**Settings & Configuration** (`src/Settings/`, `src/DI/`) |
| 133 | +- `SpameriElasticSearchExtension` - Nette DI extension for configuration |
| 134 | +- Config options: `host`, `port`, `debug`, `version`, `synonymPath`, `entities` |
| 135 | +- `IndexConfigInterface` - defines index mappings via `provide()` returning `\Spameri\ElasticQuery\Mapping\Settings` |
| 136 | +- Index configurations use `spameri/elastic-query` objects: `Field`, `SubFields`, `FieldObject`, `FieldCollection` |
| 137 | + |
| 138 | +**Query Building** |
| 139 | +- Uses external `spameri/elastic-query` package for type-safe queries |
| 140 | +- Main entry point: `\Spameri\ElasticQuery\ElasticQuery` |
| 141 | +- Query types: `Term`, `Match`, `Range`, `Bool`, aggregations, etc. |
| 142 | +- Passed to EntityManager's `findBy()` or Model layer services |
| 143 | + |
| 144 | +### Important Patterns |
| 145 | + |
| 146 | +**Entity Construction** |
| 147 | +- Entities must accept all properties via constructor (used by EntityFactory) |
| 148 | +- ID property is always first parameter and typed as `ElasticIdInterface` |
| 149 | +- Value objects implementing `ValueInterface` provide type safety and validation |
| 150 | +- Use `EmptyElasticId` for new entities, `ElasticId` for existing ones |
| 151 | + |
| 152 | +**Persistence Flow** |
| 153 | +1. Call `EntityManager->persist($entity)` |
| 154 | +2. EventManager dispatches `PRE_PERSIST` event |
| 155 | +3. `Insert` service converts entity to array via reflection |
| 156 | +4. Data sent to ElasticSearch |
| 157 | +5. EventManager dispatches `POST_PERSIST`, plus `POST_CREATE` or `POST_UPDATE` based on ChangeSet |
| 158 | +6. Entity added to Identity Map |
| 159 | + |
| 160 | +**Retrieval Flow** |
| 161 | +1. Call `EntityManager->findBy($query, $class)` |
| 162 | +2. `GetAllBy` executes query against index (determined by EntitySettingsLocator) |
| 163 | +3. Each Hit processed by `EntityFactory->create()` |
| 164 | +4. Factory checks Identity Map first (returns cached if exists) |
| 165 | +5. Factory uses reflection to resolve constructor parameters |
| 166 | +6. Nested entities/collections created recursively |
| 167 | +7. New entity added to Identity Map |
| 168 | +8. Returns `ElasticEntityCollection` containing results |
| 169 | + |
| 170 | +**Single Table Inheritance (STI)** |
| 171 | +- Parent entity implements `STIElasticEntityInterface` |
| 172 | +- Child entities extend parent |
| 173 | +- Index stores `entityClass` field to identify concrete type |
| 174 | +- Factory automatically instantiates correct child class on retrieval |
| 175 | +- Enabled via `hasSti()` in index config |
| 176 | + |
| 177 | +## Configuration Example |
| 178 | + |
| 179 | +```neon |
| 180 | +extensions: |
| 181 | + spameriElasticSearch: \Spameri\Elastic\DI\SpameriElasticSearchExtension |
| 182 | +
|
| 183 | +spameriElasticSearch: |
| 184 | + host: 127.0.0.1 |
| 185 | + port: 9200 |
| 186 | + debug: true # Enables Tracy debug bar panel |
| 187 | + version: 8 # ElasticSearch version |
| 188 | + entities: |
| 189 | + - App\Model\Entity\MyEntity |
| 190 | +``` |
| 191 | + |
| 192 | +## Code Style |
| 193 | + |
| 194 | +- PHP 8.2+ with strict types (`declare(strict_types = 1)`) |
| 195 | +- Constructor property promotion with readonly where applicable |
| 196 | +- Fully qualified class names in code (enforced by coding standard) |
| 197 | +- Slevomat Coding Standard rules (see `ruleset.xml`) |
| 198 | +- PHPStan level 6 analysis |
| 199 | +- Properties typed strictly; use union types sparingly |
| 200 | +- Nette Tester for unit tests (`.phpt` files) |
| 201 | + |
| 202 | +## Important Conventions |
| 203 | + |
| 204 | +**Never use field named `id` in entity mapping** - it conflicts with ElasticSearch's internal `_id`. Use `databaseId`, `externalId`, etc. |
| 205 | + |
| 206 | +**Entity property order matters** - constructor parameter order must match the order reflection discovers them in. |
| 207 | + |
| 208 | +**Index naming** - use consistent naming between entity config, mapping class, and actual index names. |
| 209 | + |
| 210 | +**Test structure** - tests in `tests/SpameriTests/` mirror `src/` structure; bootstrap purges test indexes before each run. |
| 211 | + |
| 212 | +**ElasticQuery integration** - always use `\Spameri\ElasticQuery\ElasticQuery` objects for queries, not raw arrays, to maintain type safety. |
| 213 | + |
| 214 | +**Event listeners** - must implement `ListenerInterface` and be registered in DI container to be auto-discovered by EventManager. |
| 215 | + |
| 216 | +**Value objects** - prefer value objects implementing `ValueInterface` for entity properties to encapsulate validation logic. |
| 217 | + |
| 218 | +## Testing Notes |
| 219 | + |
| 220 | +- Tests require running ElasticSearch instance (configured via `SpameriTests\Elastic\Config`) |
| 221 | +- Bootstrap automatically deletes test indexes before test run |
| 222 | +- Tests use `.phpt` format (Nette Tester) |
| 223 | +- Temporary directory: `tests/tmp/` (auto-cleaned) |
| 224 | +- Test data: `tests/SpameriTests/data.json` (1.2MB fixture file) |
0 commit comments