Skip to content

RFC: Long-term src/ structure — when to sub-namespace by domain #30

Description

@parisek

Why this RFC now

Currently src/ is flat: 7 feature classes directly under Parisek\TimberKit. This works at the current scale. As the kit grows (proposal #29 adds an 8th, and more WPML-related features are likely to follow), flat namespacing will start to lose navigability.

This RFC proposes a rule of thumb for when to introduce sub-namespaces, codified as an ADR so future contributors apply it consistently.

Current state

src/
├─ BlockRenderer.php
├─ Breadcrumb.php
├─ DevMediaProxy.php
├─ Helpers.php
├─ Resizer.php
├─ StarterBase.php
└─ WPFormsConfigBridge.php

Pattern: single descriptive class name per feature, no sub-namespaces, optional integration prefix when wrapping third-party (WPForms…).

Proposed rule

One feature = one class (already practiced, formalize).

Sub-namespace introduction triggers:

  • Domain has 2+ classes → extract into Parisek\TimberKit\<Domain>\ sub-namespace
  • Domain has 1 class → stays flat
  • Reorganization happens in the same PR that adds the 2nd class in that domain (atomic move + add)

Naming inside sub-namespaces: drop the domain prefix from the class name.

  • Flat: WpmlBlockOverride
  • After domain extraction: Wpml\BlockOverride (not Wpml\WpmlBlockOverride)

Cross-cutting helpers (logger, cache, internal utilities) extracted to <Domain>\Internal\ only when 2+ consumers in that domain need them. YAGNI until then.

Worked example

Today:

src/
├─ WpmlBlockOverride.php          (1 WPML class → flat)

When 2nd WPML feature (e.g. WpmlObjectIdHelper) lands:

src/
├─ Wpml/
│  ├─ BlockOverride.php            (renamed from WpmlBlockOverride)
│  └─ ObjectIdHelper.php           (new in same PR)

That PR contains: rename + new class + consumer updates (themes that called WpmlBlockOverride::register() now call Wpml\BlockOverride::register()).

Tradeoffs

Aspect Flat Sub-namespace
Discoverability at scale Degrades after ~15 files Stays good
Breaking changes None when staying flat Once per domain (at 2nd class)
Class name length Longer (WpmlBlockOverride) Shorter (Wpml\BlockOverride)
File system maps to mental model Eventually breaks Stays aligned

Why this rule and not "sub-namespace everything from day one":

  • Unilaterally re-organizing existing 7 classes is a kit-wide breaking change without proportional benefit at current scale.
  • "When in doubt, flat" reduces overhead for one-off utilities that may never grow.
  • The 2nd-class trigger is a natural inflection point — it's also where naming pressure surfaces (two Wpml* classes side by side feel redundant immediately).

Why this rule and not "let it happen organically":

  • Without a codified trigger, the 3rd or 5th WPML class will get added flat by inertia, and reorganization becomes politically expensive.
  • Explicit rule lets any contributor make the call without asking.

Cross-cutting concerns: when to extract

Same logic, one level deeper:

  • A pattern (logger, cache key prefix, ACF field resolver) duplicated in 2 classes → extract to <Domain>\Internal\<Helper>
  • Used across multiple domains → promote to Parisek\TimberKit\Internal\<Helper> or extend Helpers
  • Cross-cutting helpers stay internal (no public stability promise) unless explicitly promoted

Proposed deliverable

ADR: docs/adr/2026-05-28-src-structure-and-sub-namespacing.md — the rule as decision record, citing this issue.

(The kit already uses ADRs — 2026-05-24-breadcrumb-design.md exists. New ADR matches that pattern.)

Open questions

  1. PSR-4 vs classmap autoload: the kit's own composer.json uses PSR-4 ("Parisek\\TimberKit\\": "src/"). Sub-namespaces work automatically. Consumer themes using classmap autoload (e.g., atelier99 inline) also pick up sub-folders. No autoload concern.

  2. Existing classes — refactor too? Recommendation: no preemptive refactor. Reorganize only when a 2nd class in a domain appears. BlockRenderer stays flat until a Block\Inserter or similar lands.

  3. Internal\ sub-namespace as signal: a strong convention that Internal\* classes have no API stability promise — they can be renamed/restructured without major version bumps. Worth codifying?

  4. Naming repetition: how aggressively to drop the prefix?

    • Wpml\BlockOverride ✅ clear
    • Wpml\Override ❌ ambiguous
    • Rule: drop prefix only if remaining name stays self-explanatory.

Related


Filed as architectural discussion before further feature additions. Implementation of #29 will proceed under current flat convention. If this RFC lands, the next WPML feature will be the trigger for migration.

Metadata

Metadata

Assignees

Labels

No labels
No labels

Projects

No projects

Milestone

No milestone

Relationships

None yet

Development

No branches or pull requests

Issue actions