Skip to content
Merged
Show file tree
Hide file tree
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
18 changes: 18 additions & 0 deletions .bito.yaml
Original file line number Diff line number Diff line change
@@ -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'
35 changes: 35 additions & 0 deletions .bito/guidelines/domain-invariants.txt
Original file line number Diff line number Diff line change
@@ -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
11 changes: 11 additions & 0 deletions .bito/guidelines/repo-truth-and-boundaries.txt
Original file line number Diff line number Diff line change
@@ -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
11 changes: 11 additions & 0 deletions .bito/guidelines/review-posture.txt
Original file line number Diff line number Diff line change
@@ -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
67 changes: 67 additions & 0 deletions AGENTS.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,67 @@
# Agent Guide

<!-- Generated by seed-golden-context | Last updated: 2026-05-11 -->

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
```
117 changes: 117 additions & 0 deletions ARCHITECTURE.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,117 @@
# Architecture

<!-- Generated by seed-golden-context | Last updated: 2026-05-11 -->

## 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.
Loading
Loading