Skip to content
Closed
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
54 changes: 54 additions & 0 deletions CLAUDE.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,54 @@
# CLAUDE.md

This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository.

## What this is

`netlogix/xml-processor` — a PHP library that streams XML files through `\XMLReader` and dispatches events per node, so huge XML files can be processed with low, constant memory usage instead of loading a full DOM. PSR-4 namespace root: `Netlogix\XmlProcessor\` → `src/`.

## Commands

```bash
composer install # install dependencies
composer test # full suite: unit + behat
composer test:unit # PHPUnit only (composer test:unit -- --filter=testName for a single test)
composer test:behat # Behat behaviour specs only
composer test:coverage # Xdebug-based coverage, writes .coverage/ HTML (requires Xdebug)
composer lint # mago lint (static analysis)
composer format # mago fmt --dry-run
composer format:fix # mago fmt (apply formatting)
composer apply-coding-standard # composer:normalize:fix + rector:fix + format:fix, full pipeline
```

Run a single PHPUnit test directly with `vendor/bin/phpunit --filter <name> tests/Unit/...` when iterating.

CI (`.github/workflows/ci.yml`) runs the matrix across PHP 7.4–8.5 on every PR, then PHPUnit (with coverage) and Behat. Keep changes compatible with PHP 7.4+ (`symfony/polyfill-php80` is a real dependency, not incidental) — Rector (`rector.php`) is configured to target PHP 7.4 and enforces this.

## Architecture

**Event-driven streaming, not a DOM.** `XmlProcessor::processFile()` (`src/XmlProcessor.php`) opens the file with `\XMLReader` and loops `$xml->read()`, translating each `XMLReader` node type into an event name (`XmlProcessor::NODE_TYPE_*`, e.g. `NodeType_1` for `ELEMENT`) plus two synthetic events, `openFile` and `endOfFile`. Node processors subscribe to these events by node path and event name; matched callables are invoked with a context object.

**Node processors** implement `NodeProcessorInterface::getSubscribedEvents(string $nodePath, XmlProcessorContext $context): iterable` and yield `[eventName => callable]` pairs. Most processors instead extend `AbstractNodeProcessor` (`src/NodeProcessor/AbstractNodeProcessor.php`), define a `const NODE_PATH`, and implement one or more of:
- `OpenNodeProcessorInterface::openElement(OpenContext $context)` — start tag
- `CloseNodeProcessorInterface::closeElement(CloseContext $context)` — end tag
- `TextNodeProcessorInterface::textElement(TextContext $context)` — text content

`AbstractNodeProcessor::isNode()` matches the current node path against `NODE_PATH` via `XmlProcessor::checkNodePath()`, which accepts an exact path, a leading-`/`-anchored root path, or a path suffix — so `NODE_PATH` can target a node regardless of its ancestry, or anchor it to the document root.

**Context objects** (`src/NodeProcessor/Context/`) carry per-callback state and form a small hierarchy: `NodeProcessorContext` (base: current node path, access to `XmlProcessorContext`) → `AbstractElementContext` (adds `selfClosing`) → `OpenContext` (adds parsed attributes) / `CloseContext`. `TextContext` extends `NodeProcessorContext` directly and adds the text value. `XmlProcessor::createContext()` picks the right subclass and populates it before invoking processors — do not add new context state without also wiring it there.

**`XmlProcessorContext`** (`src/XmlProcessorContext.php`) is the object handed to `getSubscribedEvents()` and reachable from every `NodeProcessorContext`. It exposes the raw `\XMLReader`, lets processors look up sibling processors by class (`getProcessor()`), and exposes `skipCurrentNode()` — a closure-based hook into `XmlProcessor` that lets a processor abort processing of the current element (used together with `setSkipNodes()`/`checkNodePath()` for path-based skip rules).

**Event dispatch caching.** `XmlProcessor::getProcessorForEvent()` memoizes, per node path + event, the list of matching processor callables in `$this->eventCache` — subscriptions are only recomputed the first time a given path/event pair is seen. Any change to how `getSubscribedEvents()` decides subscriptions must stay a pure function of `($nodePath, $context)`, since results are cached across repeated visits to the same path.

**Skip and whitelist controls**, set on `XmlProcessor` before `processFile()`:
- `setSkipNodes(?array $skipNodes)` — paths (matched via `checkNodePath`) whose subtree is skipped entirely (fast-forwarded via `$xml->next()`).
- `setWhitelistEvents(?array $whitelistEvents)` — if non-null, only these event names are dispatched at all; everything else is a no-op, independent of processor subscriptions.

**Self-closing elements** are detected via `$xml->isEmptyElement` at `ELEMENT` time; `XmlProcessor` synthesizes an immediate `eventCloseElement()` for them (and for skipped nodes) since `\XMLReader` never emits a separate `END_ELEMENT` for `<tag/>`.

## Testing conventions

- Unit tests live in `tests/Unit/...`, mirroring `src/` namespaces; fixtures (test XML, fixture processor classes) live in `tests/Fixtures/<TestClass>/`.
- Behat scenarios (`features/*.feature`) exercise processors under `Netlogix\XmlProcessor\Behat\NodeProcessor` (defined in `features/NodeProcessor/`) via `features/bootstrap/FeatureContext.php`; these are also unit-tested directly under `tests/Unit/Behat/NodeProcessor/`.
- `performance/` holds standalone benchmark scripts (not part of the test suite) for checking throughput/memory on large XML fixtures — run them directly with `php performance/test.php`, not via Composer/PHPUnit.
Loading