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
-
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.
-
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.
-
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?
-
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.
Why this RFC now
Currently
src/is flat: 7 feature classes directly underParisek\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
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:
Parisek\TimberKit\<Domain>\sub-namespaceNaming inside sub-namespaces: drop the domain prefix from the class name.
WpmlBlockOverrideWpml\BlockOverride(notWpml\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:
When 2nd WPML feature (e.g.
WpmlObjectIdHelper) lands:That PR contains: rename + new class + consumer updates (themes that called
WpmlBlockOverride::register()now callWpml\BlockOverride::register()).Tradeoffs
WpmlBlockOverride)Wpml\BlockOverride)Why this rule and not "sub-namespace everything from day one":
Wpml*classes side by side feel redundant immediately).Why this rule and not "let it happen organically":
Cross-cutting concerns: when to extract
Same logic, one level deeper:
<Domain>\Internal\<Helper>Parisek\TimberKit\Internal\<Helper>or extendHelpersProposed 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.mdexists. New ADR matches that pattern.)Open questions
PSR-4 vs classmap autoload: the kit's own
composer.jsonuses 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.Existing classes — refactor too? Recommendation: no preemptive refactor. Reorganize only when a 2nd class in a domain appears.
BlockRendererstays flat until aBlock\Inserteror similar lands.Internal\sub-namespace as signal: a strong convention thatInternal\*classes have no API stability promise — they can be renamed/restructured without major version bumps. Worth codifying?Naming repetition: how aggressively to drop the prefix?
Wpml\BlockOverride✅ clearWpml\Override❌ ambiguousRelated
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.