diff --git a/.bito.yaml b/.bito.yaml new file mode 100644 index 00000000..9fb28aaf --- /dev/null +++ b/.bito.yaml @@ -0,0 +1,18 @@ +# Generated by seed-golden-context | Last updated: 2026-05-11 +suggestion_mode: comprehensive +post_description: true +post_changelist: true +exclude_files: 'composer.lock' +exclude_draft_pr: false +secret_scanner_feedback: true +linters_feedback: true +repo_level_guidelines_enabled: true +sequence_diagram_enabled: true +custom_guidelines: + general: + - name: 'Review Posture' + path: './.bito/guidelines/review-posture.txt' + - name: 'Repo Truth And Alignment' + path: './.bito/guidelines/repo-truth-and-boundaries.txt' + - name: 'Domain Invariants' + path: './.bito/guidelines/domain-invariants.txt' diff --git a/.bito/guidelines/domain-invariants.txt b/.bito/guidelines/domain-invariants.txt new file mode 100644 index 00000000..217eb077 --- /dev/null +++ b/.bito/guidelines/domain-invariants.txt @@ -0,0 +1,35 @@ +Critical invariants for contentful.php. Violations of these rules are high-priority findings. + +BACKWARD COMPATIBILITY +- Any removal or signature change on a public class/method/interface is a BC break requiring a major version bump. +- Dropping support for a PHP version is also a breaking change (major bump required). +- The roave/backward-compatibility-check CI job must pass before merge. If the BC check is skipped or bypassed, flag it. + +RESOURCE POOL / IDENTITY MAP +- Resources must always be built via ResourceBuilder::build(), not instantiated directly. The ResourcePool is the identity map — bypassing it creates duplicate objects with inconsistent state. +- PSR-6 cache keys must be generated via ResourcePool::generateKey(). Never hard-code or construct cache keys manually. +- The locale wildcard '*' is significant: a resource fetched without an explicit locale is stored under '*'. Never coerce this to null in cache serialization. + +LINK RESOLUTION DEPTH +- MAX_DEPTH is 20. Recursive link resolution throws at depth 20. Never raise this limit without a memory analysis. +- Do not add synchronous recursive calls outside the existing ResourceBuilder/LinkResolver path. + +PHP VERSION MINIMUM +- The minimum supported PHP version is PHP ^8.0 (composer.json). Do not use syntax, functions, or attributes that require PHP 8.1 or higher. +- The one known exception: the LocalizedResource::$sys PHPStan violation (undefined property) is intentionally unfixed because the fix requires a PHP 8.2+ attribute. Do not add PHP 8.2+ attributes to fix new violations without a BC impact review. + +NAMESPACE DISCIPLINE +- All classes in this repo live under Contentful\Delivery\*. Do not add classes to Contentful\Core\* (that's contentful/core) or any other top-level namespace. + +SCOPE IMMUTABILITY +- A Client instance is scoped to exactly one space and one environment. Do not add methods that accept an arbitrary space or environment at call time — create a new Client instead. + +php-vcr CASSETTES +- Do not modify or delete existing php-vcr cassette files. The original recording space no longer exists — cassettes cannot be re-recorded. +- New tests requiring HTTP should use Guzzle mock handlers, not new cassettes. + +PHPSTAN +- PHPStan level 5 must pass. Do not suppress warnings globally. The LocalizedResource::$sys warning is the one accepted suppression. +- CI runs static analysis on PHP 8.1. Ensure new code passes on 8.1. + +Generated by seed-golden-context | Last updated: 2026-05-11 diff --git a/.bito/guidelines/repo-truth-and-boundaries.txt b/.bito/guidelines/repo-truth-and-boundaries.txt new file mode 100644 index 00000000..53bf3611 --- /dev/null +++ b/.bito/guidelines/repo-truth-and-boundaries.txt @@ -0,0 +1,11 @@ +Use the repository's written documentation as review context and check whether the change matches the documented intent. + +- Start from README.md, ARCHITECTURE.md, AGENTS.md, CONTRIBUTING.md, and docs/ADRs/ for architectural context. +- Check whether code, tests, and documentation all tell the same story. Flag mismatches between implementation and the documented architecture or ADRs. +- Treat AGENTS.md as the authoritative guide for sharp edges and invariants. If a change violates an invariant documented there, flag it. +- If CI or another required check already enforces a merge rule (lint, static analysis, BC check, tests), do not ask for duplicate PR sections or manual checklists. +- Ask for an ADR update when a change is architecture-significant: new module, new Composer dependency, new caching strategy, new integration point, change to minimum PHP version. +- Distinguish public API (classes/methods/types in src/ that are documented and not marked @internal) from internal implementation details. Public API changes require extra scrutiny for backward compatibility. +- The public API surface is what consumers pin in composer.json. Any removal or signature change is a BC break requiring a major version bump — flag these explicitly. + +Generated by seed-golden-context | Last updated: 2026-05-11 diff --git a/.bito/guidelines/review-posture.txt b/.bito/guidelines/review-posture.txt new file mode 100644 index 00000000..69bcce9f --- /dev/null +++ b/.bito/guidelines/review-posture.txt @@ -0,0 +1,11 @@ +Review this pull request like the tech lead of the contentful.php project — the official PHP library for the Contentful Content Delivery API. + +- Prefer a few high-signal findings to a long list of minor or style-only comments. +- Prioritise: backward compatibility breaks, cache correctness, link resolution correctness, and PSR interface contract violations. +- Prefer behavior, contract, runtime, and documentation issues over process-only suggestions. Do not ask for duplicate PR template sections, checklists, or manual validation acknowledgements when CI or required checks already enforce that policy. +- Keep feedback actionable: explain why it matters, how it would surface in practice, and the clearest next step. +- If a concern is only a risk or assumption rather than a confirmed bug, say that clearly and explain what evidence would confirm it. +- If you find no issues, say so explicitly and call out any residual uncertainty that still deserves human attention. +- This is a public open-source library. Be mindful that code and comments are visible to external contributors and end users. + +Generated by seed-golden-context | Last updated: 2026-05-11 diff --git a/AGENTS.md b/AGENTS.md new file mode 100644 index 00000000..09c62d9e --- /dev/null +++ b/AGENTS.md @@ -0,0 +1,67 @@ +# Agent Guide + + + +Read this file first. It tells you where to find context in this repo. + +## Quick Reference + +| What you need | Where to look | +|---|---| +| How this repo is structured | [ARCHITECTURE.md](./ARCHITECTURE.md) | +| How to build/test/run | [CONTRIBUTING.md](./CONTRIBUTING.md) | +| Why decisions were made | [docs/ADRs/](./docs/ADRs/) | +| What this repo does | [README.md](./README.md) | +| PR review rules | [.bito/guidelines/](./.bito/guidelines/) | +| Active specs/work | [docs/specs/](./docs/specs/) | + +## Sharp Edges & Invariants + +- **Namespace is `Contentful\Delivery\*`** — all public types live under this namespace. Do not add classes to `Contentful\Core\*` or any other namespace; cross-SDK concerns belong in `contentful/core`. +- **One `Client` = one space + one environment.** The space ID and environment ID are constructor arguments and immutable. There is no "multi-space client" — callers must instantiate multiple `Client` objects. +- **`MAX_DEPTH = 20`** — link resolution is recursive. Exceeding depth 20 throws. Do not raise this limit without understanding the memory implications on large entry graphs. +- **Backward compatibility is enforced by CI** (`roave/backward-compatibility-check`). Any removal or signature change to a public API method is a breaking change requiring a major version bump. Even dropping a PHP version is a breaking change (major bump required). +- **`ResourcePool` is the identity map** — do not instantiate domain resources directly outside of mappers. Always go through `ResourceBuilder::build()` so the pool is correctly populated and duplicates are deduplicated. +- **`php-vcr` cassettes are fragile** — the Contentful space used to record existing integration test cassettes no longer exists. Do not delete cassettes, but do not record new cassettes against the old space. New integration tests should mock at the HTTP layer directly or use a live test space. See [ADR-0005](./docs/ADRs/0005-php-vcr-integration-testing.md). +- **PSR-6 cache keys are generated by the resource pool** — do not construct or hard-code cache keys. Always call `ResourcePool::generateKey()`. +- **Locale wildcard `'*'`** is used internally when a resource is fetched without an explicit locale (meaning "all locales"). Never strip or override the locale to `null` when serializing to cache. +- **`phpstan` level 5 must pass.** There is a known warning about `LocalizedResource::$sys` undefined property on PHP 8.1 — it is intentionally left unresolved (fixing it breaks backward compat with PHP ≤ 8.1). Do not suppress it globally; see [ADR-0004](./docs/ADRs/0004-phpstan-level-and-known-violations.md). +- **php-cs-fixer auto-fixes style** — always run `tools/php-cs-fixer/vendor/bin/php-cs-fixer fix -v` before committing. CI will fail on unfixed style. The warning about running on a newer PHP version than the minimum supported is cosmetic and expected. + +## Key Conventions + +- **Commit format:** no enforced Conventional Commits hook, but the team uses `feat:`, `fix:`, `chore:`, `docs:`, `refactor:` prefixes by convention (see recent git history). +- **Branch strategy:** `master` is the default branch. Feature branches are short-lived; PRs merge to `master`. No separate `main` branch. +- **Test location:** `tests/` mirrors `src/` structure. Integration test cassettes live under `tests/` alongside the test classes. +- **PHP minimum:** PHP ^8.0 (see `composer.json`). Do not use syntax or functions unavailable in PHP 8.0. +- **Autoload:** `Contentful\Delivery\` → `src/`, `Contentful\Tests\Delivery\` → `tests/` (PSR-4). +- **Release:** Semver via manual PR + GitHub Release + Packagist webhook. All releases go through a PR with CI passing. See the release workflow documented in [CONTRIBUTING.md](./CONTRIBUTING.md) for the full checklist. + +## Integration Points + +**Upstream (this repo consumes):** +- Contentful CDA (`https://cdn.contentful.com`) — primary read-only content API +- Contentful CPA (`https://preview.contentful.com`) — preview (unpublished) content +- `contentful/core` ^4.0 — shared HTTP client and resource builder base +- `contentful/rich-text` ^4.0 — rich-text field parsing + +**Downstream (consumes this repo):** +- `contentful/contentful-laravel` — Laravel service provider +- `contentful/ContentfulBundle` — Symfony bundle +- Any PHP application fetching Contentful CDA content + +## Build & Quality + +```bash +# Open in dev container first (Docker required) +devcontainer up --workspace-folder . +devcontainer exec --workspace-folder . bash + +# Quick verification loop (inside container) +composer test-quick-fail + +# Full suite +composer test +composer run lint-static-analysis +composer run test-for-bc-breaks +``` diff --git a/ARCHITECTURE.md b/ARCHITECTURE.md new file mode 100644 index 00000000..708aa83e --- /dev/null +++ b/ARCHITECTURE.md @@ -0,0 +1,117 @@ +# Architecture + + + +## Overview + +`contentful.php` is the official PHP client library for the Contentful Content Delivery API (CDA) and Content Preview API (CPA). It is a publish-and-forget open-source library distributed via Packagist, scoped to a single space and environment per `Client` instance. It turns raw CDA JSON responses into typed PHP objects and provides caching, link resolution, rich-text parsing, and sync support. + +## System Context + +```mermaid +graph TD + App["PHP Application\n(Laravel / Symfony / plain PHP)"] --> Client["Contentful\\Delivery\\Client"] + Client --> CDA["Contentful CDA\nhttps://cdn.contentful.com"] + Client --> CPA["Contentful CPA\nhttps://preview.contentful.com"] + Client --> Core["contentful/core\n(BaseClient, ResourceBuilder, PSR abstractions)"] + Client --> RichText["contentful/rich-text\n(Rich-text node parsing)"] + Client --> Cache["PSR-6 Cache\n(optional — user-supplied)"] + Client --> Logger["PSR-3 Logger\n(optional — user-supplied)"] + ContentfulBundle["ContentfulBundle\n(Symfony integration)"] --> Client + LaravelPkg["contentful-laravel\n(Laravel integration)"] --> Client +``` + +## Internal Structure + +| Directory / File | Purpose | +|---|---| +| `src/Client.php` | Public entry point. One instance = one space + environment. Inherits HTTP plumbing from `contentful/core`. | +| `src/ClientOptions.php` | Fluent builder for client configuration (host, cache, logger, locale, query cache). | +| `src/ResourceBuilder.php` | Converts raw CDA JSON arrays into typed `Resource` objects. Extends `BaseResourceBuilder` from core. Pre-fetches all content-type definitions needed by a collection response in a single batched query. | +| `src/Mapper/` | One mapper class per resource type (`Asset`, `Entry`, `ContentType`, `Environment`, etc.). Each mapper is responsible for hydrating a single resource from its JSON representation. | +| `src/Resource/` | Typed domain objects: `Entry`, `Asset`, `ContentType`, `Space`, `Environment`, `Locale`, `Tag`, deleted-resource stubs. `LocalizedResource` is the base for locale-aware resources. | +| `src/ResourcePool/` | In-memory identity map. `Standard` pool holds all resolved resources; `Extended` pool adds PSR-6 persistence. Prevents redundant API calls within a request. | +| `src/Cache/` | Cache warm-up (`CacheWarmer`) and clear (`CacheClearer`) utilities. Serialises resolved resources into a user-supplied PSR-6 pool at deploy time. | +| `src/QueryPool/` | Optional PSR-6-backed query-result cache. Caches `getEntries()` responses by query fingerprint for a configurable TTL. | +| `src/Synchronization/` | Wraps the Contentful Sync API. `Manager` drives incremental sync by walking paginated result pages and emitting typed resource events. | +| `src/Console/` | Symfony Console commands for cache warm-up and clear, exposed via `bin/contentful`. | +| `src/LinkResolver.php` | Resolves `Link` objects to their target resources using the resource pool or a live API call. | +| `src/Query.php` | Fluent query builder for CDA query parameters (filters, ordering, select, locale, limit, skip). | +| `src/ScopedJsonDecoder.php` | Thin wrapper that validates space/environment scope in CDA responses before handing off to the resource builder. | +| `src/SystemProperties/` | Typed wrappers for the `sys` metadata block on every CDA resource. | +| `tests/` | PHPUnit test suite, using `php-vcr` cassettes for integration-level tests. Unit tests mirror the `src/` structure. | + +## Data Flow + +```mermaid +sequenceDiagram + participant App + participant Client + participant ResourcePool + participant CDA_API as Contentful CDA + participant ResourceBuilder + + App->>Client: getEntry($id) + Client->>ResourcePool: has('Entry', $id)? + alt cached + ResourcePool-->>Client: resource + else not cached + Client->>CDA_API: GET /spaces/.../entries/$id + CDA_API-->>Client: JSON payload + Client->>ResourceBuilder: build($data) + ResourceBuilder->>ResourcePool: save(resource) + ResourceBuilder-->>Client: typed Entry + end + Client-->>App: Entry object +``` + +For collection responses (`getEntries()`), `ResourceBuilder` additionally: +1. Batches content-type lookups for all entry types in the response. +2. Resolves `includes.Entry` and `includes.Asset` arrays before returning the top-level collection. + +## Key Dependencies + +| Dependency | Why it's here | +|---|---| +| `contentful/core` ^4.0 | Shared HTTP client base (`BaseClient`), resource pool interfaces, resource builder base, PSR abstractions. All Contentful PHP SDKs share this foundation — see [ADR-0001](./docs/ADRs/0001-contentful-core-foundation.md). | +| `contentful/rich-text` ^4.0 | Parses CDA Rich Text field values into a typed node tree. Separated into its own package so it can be reused by the management SDK. | +| `psr/cache` ^2.0\|^3.0 | PSR-6 cache interface. SDK accepts any compliant pool; `symfony/cache` ships as the default adapter. | +| `psr/log` ^1.0\|^2.0\|^3.0 | PSR-3 logging. Two log entries per request: a summary at INFO/ERROR and a full request+response dump at DEBUG. | +| `symfony/cache` ^5–7 | Default in-process NullAdapter and concrete cache adapters users can swap. | +| `symfony/console` ^2.7–7 | Powers the `bin/contentful` CLI for cache management. | +| `symfony/filesystem` ^2.7–7 | Used internally for filesystem cache operations. | +| `php-vcr/php-vcr` (dev) | Records/replays HTTP cassettes for integration tests without live API calls. **Flagged as deprecated** — the space used to record existing cassettes no longer exists; migration to a different test harness is needed. See [ADR-0005](./docs/ADRs/0005-php-vcr-integration-testing.md). | +| `phpstan/phpstan` (dev) | Static analysis at level 5. | +| `roave/backward-compatibility-check` (dev) | Automated BC break detection on every PR. | + +## Configuration + +All configuration flows through `ClientOptions`: + +| Option / Method | Purpose | Default | +|---|---|---| +| `withHost(string $host)` | Override the CDA base URL (useful for proxies or EU residency). | `https://cdn.contentful.com` | +| `usingPreviewApi()` | Switch to the Preview API host. | Off (Delivery API) | +| `withDefaultLocale(string $locale)` | Locale code applied to all requests that do not specify one explicitly. | `null` (CDA default locale) | +| `withLogger(LoggerInterface $logger)` | PSR-3 logger for request/response telemetry. | `NullLogger` | +| `withCache(CacheItemPoolInterface, $autoWarmup, $cacheContent)` | PSR-6 pool for space metadata and optionally entry/asset caching. | `NullAdapter` | +| `withHttpClient(GuzzleHttp\Client $client)` | Custom Guzzle instance with middleware (retry, circuit-breaker, etc.). | Default Guzzle client | +| `withoutMessageLogging()` | Disable in-memory request/response log (reduces memory in long-running processes). | Enabled | +| `withQueryCache(CacheItemPoolInterface, int $lifetime)` | PSR-6 pool + TTL for `getEntries()` query-result caching. | `NullAdapter`, 0 s | + +Environment-level scope is fixed at `Client` construction time (`$spaceId`, `$environmentId`). Multiple spaces require multiple `Client` instances. + +## Integration Points + +### Upstream (this repo consumes) + +- **Contentful CDA** (`https://cdn.contentful.com`) — primary read-only content API. +- **Contentful CPA** (`https://preview.contentful.com`) — preview (unpublished) content, same API surface. +- **`contentful/core`** — HTTP plumbing, resource builder protocol, PSR shims. +- **`contentful/rich-text`** — rich-text field parsing. + +### Downstream (consumes this repo) + +- **PHP applications** — any PHP app fetching Contentful content (standalone, Laravel, Symfony, etc.). +- **`contentful/contentful-laravel`** — Laravel service provider wrapping this SDK. +- **`contentful/ContentfulBundle`** — Symfony bundle wrapping this SDK. diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 8269d3d8..d188295f 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -37,3 +37,76 @@ composer run test-for-bc-breaks 1. Fork the repository and create a branch for your change. 2. Run the relevant checks from the dev container. 3. Open a pull request with a short summary of the change and any follow-up context. + + + +## Testing + +- **Framework:** PHPUnit ^8.5 +- **Location:** `tests/` (mirrors `src/` structure) +- **Run all tests:** + ```bash + composer test + ``` +- **Run with stop-on-failure:** + ```bash + composer test-quick-fail + ``` +- **Integration tests** use `php-vcr` cassettes (pre-recorded HTTP responses). The original recording space no longer exists — do not attempt to re-record cassettes against it. + +## Linting & Static Analysis + +```bash +# PHP CS Fixer (auto-fix code style) +tools/php-cs-fixer/vendor/bin/php-cs-fixer fix -v + +# PHPStan static analysis (level 5) +composer run lint-static-analysis + +# Syntax check all PHP files +git ls-files -z '*.php' | xargs -0 -n1 php -l +``` + +## Backward Compatibility + +```bash +# Check for BC breaks against the last tagged release +composer run test-for-bc-breaks +``` + +CI runs this check on every PR. Any public API removal or signature change requires a **major version bump**. Dropping a supported PHP version is also a breaking change. + +## Branch Strategy + +- `master` — default branch; represents the latest stable release line. +- Feature / fix branches — short-lived, named descriptively (e.g., `fix/link-resolution-edge-case`). +- Open PRs against `master`. + +## Release Process + +All releases follow this workflow: + +1. Open a PR with CI passing (lint, static analysis, BC check, tests across PHP 8.0–8.4). +2. Merge via merge commit (not squash). +3. Create a GitHub Release with the version tag and CHANGELOG notes. +4. Verify the Packagist webhook fires and the new version appears on [packagist.org](https://packagist.org/packages/contentful/contentful). + - If the webhook stalls, redeliver it from **GitHub → Settings → Webhooks → Recent Deliveries**. + +Versioning follows [Semantic Versioning](https://semver.org/): + +| Change type | Version bump | +|---|---| +| New feature / framework compat (non-breaking) | Minor | +| Bug fix | Patch | +| Breaking change (removed API, dropped PHP version) | Major | + +## CI/CD + +| Job | Trigger | What it does | +|---|---|---| +| `lint-syntax` | push/PR to `master` | Syntax-checks all `.php` files (PHP 8.1, via devcontainer) | +| `static-analysis` | push/PR to `master` | Runs PHPStan level 5 (PHP 8.1) | +| `backwards-compatibility` | push/PR to `master` | Runs `roave/backward-compatibility-check` (PHP 8.1) | +| `test` (matrix) | push/PR to `master` | PHPUnit across PHP 8.0, 8.1, 8.2, 8.3, 8.4 (via devcontainer) | + +All CI jobs run inside the repository's devcontainer for environment parity. diff --git a/README.md b/README.md index 9323e95d..3b617769 100644 --- a/README.md +++ b/README.md @@ -219,3 +219,17 @@ This repository is published under the [MIT](LICENSE) license. We want to provide a safe, inclusive, welcoming, and harassment-free space and experience for all participants, regardless of gender identity and expression, sexual orientation, disability, physical appearance, socioeconomic status, body size, ethnicity, nationality, level of experience, age, religion (or lack thereof), or other identity markers. [Read our full Code of Conduct](https://github.com/contentful-developer-relations/community-code-of-conduct). + +--- + + + +## For Agents & Contributors + +| Document | What it covers | +|---|---| +| [AGENTS.md](./AGENTS.md) | Agent-first context directory — read this before touching code | +| [ARCHITECTURE.md](./ARCHITECTURE.md) | Internal structure, data flows, integration points, key dependencies | +| [CONTRIBUTING.md](./CONTRIBUTING.md) | Development setup, test commands, release process, CI overview | +| [docs/ADRs/](./docs/ADRs/) | Architecture Decision Records — why things look the way they do | +| [docs/specs/](./docs/specs/) | Active implementation specs | diff --git a/docs/ADRs/0001-contentful-core-foundation.md b/docs/ADRs/0001-contentful-core-foundation.md new file mode 100644 index 00000000..389723d7 --- /dev/null +++ b/docs/ADRs/0001-contentful-core-foundation.md @@ -0,0 +1,28 @@ +# ADR-0001: Use `contentful/core` as the shared SDK foundation + + + +## Status + +Accepted + +## Context + +The Contentful PHP ecosystem includes multiple SDK packages: `contentful.php` (CDA), `contentful-management.php` (CMA), and supporting packages like `rich-text.php`. Without a shared foundation, each SDK would independently implement HTTP client plumbing, resource building, PSR adapter wiring, error handling, and SDK telemetry headers — creating duplication and divergence over time. + +The team created `contentful/contentful-core.php` (package name: `contentful/core`) to extract this common infrastructure. + +## Decision + +`contentful.php` inherits from `BaseClient` and `BaseResourceBuilder` in `contentful/core` rather than implementing its own HTTP layer or resource builder protocol. The core package provides: + +- `BaseClient` — Guzzle-based HTTP client with SDK telemetry headers, PSR-3 logging, and request/response message storage. +- `BaseResourceBuilder` — mapper dispatch and type-routing protocol. +- `ResourcePoolInterface` / `ResourceBuilderInterface` — contracts that allow the SDK and its integrations (ContentfulBundle, Laravel) to swap pool implementations. +- Shared PSR-6/PSR-3 shims and the `Link`/`LinkResolverInterface` types used across SDKs. + +## Consequences + +- **Enables:** Consistent behavior across CDA and CMA SDKs; shared bug fixes and security patches propagate to all SDKs. +- **Constrains:** Core package upgrades (`^4.0` pin) may require coordinated bumps across all dependent SDK packages. Breaking changes in core are major version events across the ecosystem. +- **Trade-off accepted:** `contentful.php` cannot diverge freely from the `core` API contract — new SDK-specific HTTP behaviors require either a PR to core or a local override. diff --git a/docs/ADRs/0002-psr-interfaces-for-cache-and-logging.md b/docs/ADRs/0002-psr-interfaces-for-cache-and-logging.md new file mode 100644 index 00000000..1001215c --- /dev/null +++ b/docs/ADRs/0002-psr-interfaces-for-cache-and-logging.md @@ -0,0 +1,29 @@ +# ADR-0002: Accept PSR-6 (cache) and PSR-3 (logging) interfaces, not concrete implementations + + + +## Status + +Accepted + +## Context + +The SDK is a library consumed by PHP applications with widely varying infrastructure: some use Redis, some use APCu, some use Memcache, and many run in environments where no shared cache exists at all. Similarly, logging infrastructure ranges from Monolog to custom PSR-3 implementations to no logger at all. + +Binding the SDK to a specific cache or log implementation would force dependency conflicts on consumers and make the SDK unusable in environments that cannot install those specific packages. + +## Decision + +`ClientOptions` accepts: +- `Psr\Cache\CacheItemPoolInterface` for space-metadata and query caching (PSR-6). +- `Psr\Log\LoggerInterface` for request telemetry (PSR-3). + +Defaults are `NullAdapter` (PSR-6, provided by `symfony/cache`) and `NullLogger` (PSR-3), so the SDK is fully functional without any cache or logger configuration. + +`symfony/cache` ships as a `require` dependency (not `require-dev`) because `NullAdapter` is used as the default — but consumers are free to substitute any PSR-6-compliant pool. + +## Consequences + +- **Enables:** Drop-in compatibility with any PSR-6/PSR-3 implementation in the consumer's stack. +- **Constrains:** The SDK cannot use cache-implementation-specific features (e.g., Redis tags). Cache invalidation is limited to TTL and explicit clear commands. +- **Trade-off accepted:** Slightly heavier `require` footprint (Symfony cache ships as a runtime dep) in exchange for zero-config usability. diff --git a/docs/ADRs/0003-devcontainer-for-reproducible-ci.md b/docs/ADRs/0003-devcontainer-for-reproducible-ci.md new file mode 100644 index 00000000..7fdb0368 --- /dev/null +++ b/docs/ADRs/0003-devcontainer-for-reproducible-ci.md @@ -0,0 +1,26 @@ +# ADR-0003: Use devcontainer for reproducible local and CI environments + + + +## Status + +Accepted + +## Context + +PHP SDK repos historically had drift between local developer setups and CI environments. Developers on different OS versions or PHP minor versions would hit linter warnings or test failures that CI did not reproduce (and vice versa). The linter (`php-cs-fixer`) is particularly sensitive to PHP runtime version — running it on PHP 8.1 against a project targeting PHP 8.0 produces version-mismatch warnings. + +In March 2026, DX-822 introduced repo-local devcontainers across all SDK repos, including `contentful.php` (PR #340). + +## Decision + +`.devcontainer/` provides a Dockerfile and `devcontainer.json` that pins the PHP version (defaulting to 8.1, parameterised via `PHP_VERSION` env var). The `post-create.sh` script installs dependencies with a stripped `composer.json` that omits `roave/backward-compatibility-check` (which can cause resolution failures during container build) — the BC checker is exercised separately in CI. + +GitHub Actions CI uses the same devcontainer for all jobs: lint-syntax, static-analysis, backwards-compatibility, and test matrix (PHP 8.0–8.4). This ensures local and CI environments are identical. + +## Consequences + +- **Enables:** Any engineer (or agent) can reproduce CI locally by opening the repo in the devcontainer. Eliminates "works on my machine" PHP version issues. +- **Constrains:** Docker is required for local development. The devcontainer CLI (`@devcontainers/cli`) is required in CI runners. +- **Trade-off accepted:** Slightly slower CI boot (container build step) in exchange for environment parity. +- **Note:** `roave/backward-compatibility-check` is excluded from the devcontainer install to avoid resolution conflicts. It runs in its own dedicated CI job. diff --git a/docs/ADRs/0004-phpstan-level-and-known-violations.md b/docs/ADRs/0004-phpstan-level-and-known-violations.md new file mode 100644 index 00000000..c295ef57 --- /dev/null +++ b/docs/ADRs/0004-phpstan-level-and-known-violations.md @@ -0,0 +1,26 @@ +# ADR-0004: Run PHPStan at level 5 and accept one known violation + + + +## Status + +Accepted + +## Context + +PHPStan provides incremental static analysis for PHP. The SDK has a long history and was not written with strict type annotations throughout. Running PHPStan at the highest levels (8–9) would require significant refactoring of internal classes for marginal safety benefit in a library whose public surface is already stable and tested. + +Additionally, a known PHPStan warning exists on `LocalizedResource::$sys`: "Access to an undefined property." The fix requires a PHP 8.2+ attribute that would break backward compatibility with PHP 8.1 (which the SDK still supports). + +## Decision + +PHPStan runs at **level 5** as part of CI (`composer run lint-static-analysis`). This catches the most impactful issues (undefined methods, wrong argument types, unreachable code) without requiring a full strict-type overhaul. + +The `LocalizedResource::$sys` violation is left in place. It is a static-analysis warning only — no runtime error occurs. Fixing it by adding a PHP 8.2+ attribute would silently break any consumer running PHP 8.1. The fix will be addressed in the next major version when PHP 8.1 support is dropped. + +## Consequences + +- **Enables:** Meaningful static analysis coverage in CI without requiring a large refactor. +- **Constrains:** Level 5 will not catch all potential type errors. Developers should not interpret "PHPStan passes" as a full type-safety guarantee. +- **Known violation:** `LocalizedResource::$sys` undefined property warning is intentionally unfixed until the next major version. +- **Trade-off accepted:** One suppressed violation is preferable to a BC break affecting PHP 8.1 users. diff --git a/docs/ADRs/0005-php-vcr-integration-testing.md b/docs/ADRs/0005-php-vcr-integration-testing.md new file mode 100644 index 00000000..9fd211e6 --- /dev/null +++ b/docs/ADRs/0005-php-vcr-integration-testing.md @@ -0,0 +1,32 @@ +# ADR-0005: Use php-vcr for integration tests (and known migration debt) + + + +## Status + +Accepted (with known follow-up: migrate away from php-vcr) + +## Context + +Integration tests for the Contentful PHP SDK historically required live API calls against a real Contentful space. This was slow, non-deterministic, and required managing API credentials in CI. `php-vcr` was introduced to record HTTP cassettes from a real space and replay them in future test runs without network access. + +As of 2026, two problems exist: +1. **`php-vcr` is no longer actively maintained.** +2. **The Contentful space used to record the existing cassettes no longer exists** — it cannot be accessed to re-record or extend cassettes. + +This was flagged explicitly in the PHP SDK Contractor Handover (March 2026). + +## Decision + +The existing cassettes are kept as-is and continue to serve as the integration test baseline. No new cassettes will be recorded against the old space. New integration-level tests should either: +- Mock at the HTTP layer directly (e.g., using Guzzle mock handlers), or +- Use a dedicated live test space if one is provisioned. + +Migration away from `php-vcr` to a supported test double mechanism is tracked as follow-up work. + +## Consequences + +- **Enables:** Existing integration tests continue to pass and provide regression coverage. +- **Constrains:** Integration test coverage cannot be extended using the cassette approach. The cassettes cannot be updated if the API response format changes. +- **Follow-up required:** Replace `php-vcr/php-vcr` and `covergenius/phpunit-testlistener-vcr` with a supported HTTP mocking approach. Prioritise this before the next major version release. +- **Risk:** If CDA response format changes, cassette-based tests may pass locally while failing against the live API. diff --git a/docs/ADRs/0006-semver-and-php-version-policy.md b/docs/ADRs/0006-semver-and-php-version-policy.md new file mode 100644 index 00000000..0b5be24b --- /dev/null +++ b/docs/ADRs/0006-semver-and-php-version-policy.md @@ -0,0 +1,32 @@ +# ADR-0006: Semver versioning with major bumps for PHP version drops + + + +## Status + +Accepted + +## Context + +PHP releases a new minor version roughly annually and drops active support for older versions. SDK consumers may lag behind in upgrading their PHP version. Without a clear policy, the team could silently drop PHP support in a minor release, breaking consumers who have not yet upgraded. + +## Decision + +The SDK follows [Semantic Versioning](https://semver.org/) with the explicit rule that **dropping support for a PHP version is a breaking change and requires a major version bump**, even if no public PHP API changes. + +| Change type | Version bump | +|---|---| +| New feature or framework compat addition (non-breaking) | Minor (e.g., 9.0.2 → 9.1.0) | +| Bug fix | Patch (e.g., 9.1.0 → 9.1.1) | +| Breaking change (removed API, dropped PHP version, removed dependency) | Major (e.g., 9.x → 10.0.0) | + +Commit convention (by team practice, not enforced by hook): +- `feat:` → Minor bump +- `fix:` → Patch bump +- `feat!:` with `BREAKING CHANGE:` footer → Major bump + +## Consequences + +- **Enables:** Consumers can safely pin `^X.0` in their `composer.json` and receive features and bug fixes without unexpected breakage. +- **Constrains:** Dropping old PHP support (e.g., removing PHP 8.0 when PHP 8.1 becomes the minimum) requires a new major version with associated migration documentation. +- **Trade-off accepted:** More frequent major versions in exchange for explicit compatibility guarantees for PHP version consumers. diff --git a/docs/ADRs/README.md b/docs/ADRs/README.md new file mode 100644 index 00000000..6887c026 --- /dev/null +++ b/docs/ADRs/README.md @@ -0,0 +1,18 @@ +# Architecture Decision Records + + + +This directory contains Architecture Decision Records (ADRs) for `contentful.php`. Each ADR documents a significant technical decision, including the context that drove it, the choice made, and the trade-offs accepted. + +Read ADRs before proposing architectural changes — if a decision has an ADR, the reasoning is documented. Don't relitigate it without reading it first. + +## Index + +| ADR | Title | Status | +|---|---|---| +| [0001](./0001-contentful-core-foundation.md) | Use `contentful/core` as the shared SDK foundation | Accepted | +| [0002](./0002-psr-interfaces-for-cache-and-logging.md) | Accept PSR-6 (cache) and PSR-3 (logging) interfaces, not concrete implementations | Accepted | +| [0003](./0003-devcontainer-for-reproducible-ci.md) | Use devcontainer for reproducible local and CI environments | Accepted | +| [0004](./0004-phpstan-level-and-known-violations.md) | Run PHPStan at level 5 and accept one known violation | Accepted | +| [0005](./0005-php-vcr-integration-testing.md) | Use php-vcr for integration tests (and known migration debt) | Accepted (follow-up pending) | +| [0006](./0006-semver-and-php-version-policy.md) | Semver versioning with major bumps for PHP version drops | Accepted | diff --git a/docs/specs/.gitkeep b/docs/specs/.gitkeep new file mode 100644 index 00000000..e69de29b diff --git a/docs/specs/README.md b/docs/specs/README.md new file mode 100644 index 00000000..79d000e6 --- /dev/null +++ b/docs/specs/README.md @@ -0,0 +1,21 @@ +# Specs + + + +This directory holds implementation-level specs for active work in `contentful.php`. + +Specs are small, focused documents that capture "what we're building now" for both engineers and agents. They are written before or during implementation and marked `status: done` once shipped. + +## Naming convention + +``` +YYYY-MM-DD-short-description.md +``` + +## Status values + +- `status: draft` — being written, not yet implementation-ready +- `status: active` — implementation in progress +- `status: done` — shipped; kept for historical reference + +No active specs at this time.