Skip to content

Enhancement: Build template parts system (header, footer, sidebar) - #198

Merged
ViewFromTheBox merged 2 commits into
add/phase-sevenfrom
enhancement/161-build-template-parts-system
Mar 19, 2026
Merged

ViewFromTheBox merged 2 commits into
add/phase-sevenfrom
enhancement/161-build-template-parts-system

Conversation

@ViewFromTheBox

@ViewFromTheBox ViewFromTheBox commented Mar 19, 2026 •

Copy link
Copy Markdown
Member

Description

Implements reusable template parts that can be shared across multiple templates. Template parts represent common layout sections (headers, footers, sidebars) that are edited once and reflected everywhere they're used.

Closes: #161

Type of Change

  • Bug fix (fixes an issue)
  • New feature (adds new functionality)
  • Enhancement (improves existing functionality)
  • Refactoring (code improvement, no behavior change)
  • Documentation update
  • Performance improvement
  • Security fix
  • Breaking change (breaks backward compatibility)

Related Issue

Issue: #161

Motivation and Context

Templates need reusable sections (headers, footers, sidebars) that can be shared across multiple templates. Without template parts, every template would need to duplicate common sections, making maintenance difficult. This mirrors Gutenberg's template parts concept.

Changes Made

  • Added TemplatePart model with area-based filtering (header, footer, sidebar, custom), revision support, duplication, and locking
  • Added TemplatePartManager service with in-memory registry + database-backed CRUD, matching the existing TemplateManager pattern
  • Added TemplatePartManager facade for static access
  • Added migration for visual_editor_template_parts table
  • Added helper functions: veRegisterTemplatePart(), veGetTemplatePart(), veGetTemplatePartsForArea(), veTemplatePartExists()
  • Added template_parts configuration section with allow_custom_parts, locked_parts, and areas settings
  • Registered default template parts (header, footer, sidebar) in the service provider
  • Added ap.visualEditor.templatePartsInit hook for third-party registration
  • Added duplicate slug validation in TemplatePartManager::create()

How Has This Been Tested?

Testing Environment:

  • Operating System: macOS
  • PHP Version: 8.4
  • Project Version: development

Tests Performed:

  1. Ran all 40 new TemplatePart tests — all passing
  2. Ran full test suite (1,373 tests) — all passing
  3. CodeRabbit review — clean after applying duplicate slug validation fix

Accessibility Tests Run

  • Keyboard navigation tested
  • Screen reader tested
  • Color contrast verified
  • ARIA labels checked

Details: N/A — this is a backend-only change (model, service, migration, facade, helpers) with no UI components.

Tests Added

  • Unit tests added/updated
  • Integration tests added/updated
  • All tests passing

Test details:

  • tests/Unit/Models/TemplatePartTest.php — 17 tests covering casts, defaults, fillable attributes, unique slug, all scopes (forArea, active, draft, custom, builtIn, byUser), revisions, duplication, and AREAS constant
  • tests/Unit/Services/TemplatePartManagerTest.php — 23 tests covering register/unregister, defaults, all/allActive/forArea queries, resolve priority (DB > registry), existence checks, CRUD, revision on content change, locked part protection, duplication, lock/unlock, seeding, and duplicate slug validation

Documentation

  • Inline code documentation added
  • README updated
  • Wiki updated
  • API documentation updated

Documentation details: Full PHPDoc blocks on all classes, methods, and properties following existing package conventions.

Pre-Submission Checklist

  • Followed contributing guidelines
  • Checked for other open PRs for same update
  • Code passes all tests
  • Code has been linted
  • Accessibility tests completed
  • Code follows project style guide
  • Self-review completed
  • Comments added for complex code
  • No new warnings generated

Summary by CodeRabbit

  • New Features

    • Template parts management: create, duplicate, lock/unlock, draft/active states, and area-based organization (header, footer, sidebar, custom)
    • Pre-configured default parts for enabled areas and seeding of registered parts
    • Automatic revision tracking for content changes
    • Helper functions and service accessors for registering and resolving template parts
  • Tests

    • Unit tests covering model behavior and manager operations (creation, update, deletion, locking, duplication, seeding, queries)

Implement reusable template parts that can be shared across multiple
templates. Adds TemplatePart model with area-based filtering (header,
footer, sidebar, custom), TemplatePartManager service with in-memory
registry and database-backed CRUD, facade, helper functions, config,
and default built-in parts registered via the service provider.

Includes 40 tests covering model scopes, service operations, revision
tracking, locking, duplication, seeding, and duplicate slug validation.

Closes #161

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
@coderabbitai

coderabbitai Bot commented Mar 19, 2026 •

Copy link
Copy Markdown
📝 Walkthrough

Walkthrough

Adds a template parts system: config, DB migration, Eloquent model, manager service + facade + helpers, service provider registration of defaults, and unit tests covering model and service behavior.

Changes

Cohort / File(s) Summary
Configuration
config/visual-editor.php
Add template_parts config with allow_custom_parts, locked_parts, and areas (header,footer,sidebar,custom).
Database
database/migrations/2024_01_01_000004_create_visual_editor_template_parts_table.php
New migration for visual_editor_template_parts table with fields: name, unique slug, description, area (default custom), JSON content, status, is_custom, is_locked, nullable user_id, timestamps, and index on [area,status].
Model
src/Models/TemplatePart.php
New Eloquent model with table mapping, default attributes, casts, $fillable, AREAS constant, scopes (forArea, active, draft, custom, builtIn, byUser), user() relation, revision methods (createRevision, revisions), and duplicate() behavior.
Service Layer
src/Services/TemplatePartManager.php
New manager service combining in-memory registry and DB persistence: register/unregister, merged listing (all, allActive, forArea), resolve/exists, CRUD (create, update, delete) with validation/locking/revisions, duplicate, lock/unlock, seedRegistered, and registry accessors.
Facade
src/Facades/TemplatePartManager.php
New facade mapping to visual-editor.template-parts with docblock @method annotations for the manager API.
Service Provider
src/VisualEditorServiceProvider.php
Register visual-editor.template-parts singleton and class binding; boot now calls registerDefaultTemplateParts() to register built-in header, footer, sidebar parts and fires ap.visualEditor.templatePartsInit.
Helpers
src/helpers.php
Add guarded helper functions: veRegisterTemplatePart(), veGetTemplatePart(), veGetTemplatePartsForArea(), veTemplatePartExists().
Tests
tests/Unit/Models/TemplatePartTest.php, tests/Unit/Services/TemplatePartManagerTest.php
New unit tests covering model casts/defaults/scopes/revisions/duplication and manager registry merging, CRUD, locking, duplication, seeding, and error conditions.

Sequence Diagram

sequenceDiagram
    participant App as Application
    participant Helpers as Helpers
    participant Facade as TemplatePartManager<br/>(Facade)
    participant Service as TemplatePartManager<br/>(Service)
    participant Registry as In-Memory<br/>Registry
    participant DB as Database<br/>(TemplatePart Model)
    participant Hooks as Action Hooks

    App->>Helpers: veRegisterTemplatePart(slug, config)
    Helpers->>Facade: register(slug, config)
    Facade->>Service: register(slug, config)
    Service->>Registry: store registered definition

    App->>Helpers: veGetTemplatePartsForArea(area)
    Helpers->>Facade: forArea(area)
    Facade->>Service: forArea(area)
    Service->>Registry: fetch registered for area
    Service->>DB: query active parts for area
    Service->>Service: merge DB (override) + registry, apply filters
    Service-->>Facade: return merged array
    Facade-->>Helpers: return to caller
    Helpers-->>App: parts array

    App->>Facade: create(data)
    Facade->>Service: create(data)
    Service->>DB: check slug uniqueness, validate area
    Service->>DB: persist TemplatePart
    Service->>Hooks: trigger ap.visualEditor.templatePartCreated
    Service-->>Facade: return TemplatePart
    Facade-->>App: created part
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~45 minutes

Possibly related PRs

  • PR #118: Modifies VisualEditorServiceProvider and service bindings — overlaps provider/DI changes in this PR.
  • PR #120: Also edits VisualEditorServiceProvider (Blade/component registration) and touches the same provider lifecycle where template parts are registered.

Poem

🐰 I hopped through code and found a part,
Headers, footers, sidebars—each a work of art.
Registry keeps secrets, DB holds the rest,
I seed and I duplicate, then take a tiny rest.
Revisions remembered, the visual garden's blessed.

🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title accurately summarizes the main change: implementation of a template parts system with the three key areas (header, footer, sidebar) mentioned in the issue.
Linked Issues check ✅ Passed All primary coding requirements from issue #161 are met: TemplatePart model with area-based filtering, TemplatePartManager service with CRUD operations, database persistence, helper functions, part sharing/revision support, duplication, locking, and default part registration.
Out of Scope Changes check ✅ Passed All changes are directly related to implementing the template parts system specified in issue #161; no unrelated modifications detected across configuration, models, services, helpers, migrations, and tests.
Docstring Coverage ✅ Passed Docstring coverage is 100.00% which is sufficient. The required threshold is 80.00%.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch enhancement/161-build-template-parts-system
📝 Coding Plan
  • Generate coding plan for human review comments

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

@ViewFromTheBox ViewFromTheBox moved this from Backlog to In Review in Visual Editor Package Mar 19, 2026
@ViewFromTheBox ViewFromTheBox linked an issue Mar 19, 2026 that may be closed by this pull request

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 4

🧹 Nitpick comments (4)
src/VisualEditorServiceProvider.php (1)

498-520: Consider honoring configured template-part areas when seeding defaults.

Lines 498–520 always register header, footer, and sidebar. If a project narrows artisanpack.visual-editor.template_parts.areas, this can register parts outside its configured area set.

💡 Proposed refactor
 	protected function registerDefaultTemplateParts(): void
 	{
 		$manager = $this->app->make( 'visual-editor.template-parts' );
+		$enabledAreas = (array) config(
+			'artisanpack.visual-editor.template_parts.areas',
+			[ 'header', 'footer', 'sidebar', 'custom' ]
+		);
 
-		$manager->register( 'header', [
-			'name'        => __( 'Header' ),
-			'description' => __( 'A default header area for site-wide navigation and branding.' ),
-			'area'        => 'header',
-			'content'     => [],
-			'is_custom'   => false,
-		] );
+		if ( in_array( 'header', $enabledAreas, true ) ) {
+			$manager->register( 'header', [
+				'name'        => __( 'Header' ),
+				'description' => __( 'A default header area for site-wide navigation and branding.' ),
+				'area'        => 'header',
+				'content'     => [],
+				'is_custom'   => false,
+			] );
+		}
@@
-		$manager->register( 'footer', [
+		if ( in_array( 'footer', $enabledAreas, true ) ) {
+			$manager->register( 'footer', [
 			'name'        => __( 'Footer' ),
 			'description' => __( 'A default footer area for site-wide links and information.' ),
 			'area'        => 'footer',
 			'content'     => [],
 			'is_custom'   => false,
-		] );
+			] );
+		}
@@
-		$manager->register( 'sidebar', [
+		if ( in_array( 'sidebar', $enabledAreas, true ) ) {
+			$manager->register( 'sidebar', [
 			'name'        => __( 'Sidebar' ),
 			'description' => __( 'A default sidebar area for supplementary content and widgets.' ),
 			'area'        => 'sidebar',
 			'content'     => [],
 			'is_custom'   => false,
-		] );
+			] );
+		}
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/VisualEditorServiceProvider.php` around lines 498 - 520, The code always
registers 'header', 'footer', and 'sidebar' via $manager->register which ignores
the configured areas; update the seeding logic to read the configured array at
config('artisanpack.visual-editor.template_parts.areas') and only call
$manager->register for each default part whose 'area' exists in that configured
array (use the existing 'header','footer','sidebar' definitions as templates),
so registration honors the template_part areas configuration.
src/Models/TemplatePart.php (1)

212-233: Consider extracting the template_part document type into a constant.

Line 215 and Line 232 repeat the same literal; a constant would reduce drift risk.

💡 Proposed refactor
 class TemplatePart extends Model
 {
+	public const REVISION_DOCUMENT_TYPE = 'template_part';
@@
 	public function createRevision( ?int $userId = null ): Revision
 	{
 		return Revision::create( [
-			'document_type' => 'template_part',
+			'document_type' => self::REVISION_DOCUMENT_TYPE,
 			'document_id'   => $this->id,
@@
 	public function revisions(): \Illuminate\Database\Eloquent\Collection
 	{
-		return Revision::forDocument( 'template_part', $this->id )
+		return Revision::forDocument( self::REVISION_DOCUMENT_TYPE, $this->id )
 			->orderByDesc( 'created_at' )
 			->get();
 	}
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/Models/TemplatePart.php` around lines 212 - 233, Extract the repeated
literal 'template_part' into a class constant on TemplatePart (e.g. protected
const DOCUMENT_TYPE = 'template_part') and replace both uses in createRevision()
and revisions() with self::DOCUMENT_TYPE; update the Revision::create call and
the Revision::forDocument(...) invocation to use that constant so the document
type is defined in one place.
src/Services/TemplatePartManager.php (2)

172-183: Consider consistent return type for resolve().

Returning either TemplatePart (model) or array (from registry) requires callers to check the type before using the result. Consider converting to a consistent type (e.g., always return array via $dbPart->toArray(), or always wrap registered arrays in a TemplatePart-like structure).

Option: Always return array for consistency
 	public function resolve( string $slug ): TemplatePart|array|null
 	{
 		$dbPart = TemplatePart::where( 'slug', $slug )->first();
 
 		if ( null !== $dbPart ) {
-			return $dbPart;
+			return $dbPart->toArray();
 		}
 
 		return $this->registered[ $slug ] ?? null;
 	}

This would require updating the return type to array|null and adjusting callers that rely on model methods.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/Services/TemplatePartManager.php` around lines 172 - 183, The resolve()
method currently returns mixed types (TemplatePart model or array) which forces
callers to branch; update resolve() to consistently return array|null by
converting any TemplatePart model ($dbPart) to an array with $dbPart->toArray()
and returning the registered entry ($this->registered[$slug]) as-is, and change
the method signature/return type accordingly (TemplatePart->toArray(),
resolve(): array|null) so all callers can expect an array or null and be updated
to stop using model methods on the result.

297-304: Consider validating newSlug uniqueness before delegating to model.

The duplicate() method delegates directly to $part->duplicate() without checking if $newSlug already exists. While the model may handle this, validating upfront provides a clearer error message and consistent behavior with create().

Proposed fix: Add slug existence check
 	public function duplicate( TemplatePart $part, string $newSlug, ?string $newName = null ): TemplatePart
 	{
+		if ( $this->exists( $newSlug ) ) {
+			throw new RuntimeException(
+				__( 'A template part with the slug ":slug" already exists.', [ 'slug' => $newSlug ] ),
+			);
+		}
+
 		$duplicate = $part->duplicate( $newSlug, $newName );
 
 		veDoAction( 'ap.visualEditor.templatePartDuplicated', $duplicate, $part );
 
 		return $duplicate;
 	}
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/Services/TemplatePartManager.php` around lines 297 - 304, Validate that
the requested new slug is not already in use before calling $part->duplicate in
TemplatePartManager::duplicate: perform a uniqueness check (e.g., via
TemplatePart::where('slug', $newSlug)->exists() or an existing repository/method
like TemplatePart::slugExists($newSlug)), throw or return a clear validation
error if the slug exists, and only call $part->duplicate($newSlug, $newName)
when the slug is unique to match the create() behavior and provide a clearer
error message.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In
`@database/migrations/2024_01_01_000004_create_visual_editor_template_parts_table.php`:
- Around line 37-40: The migration hardcodes the foreign key to 'users', which
conflicts with the configurable user model used by TemplatePart; update the
migration (in the create_visual_editor_template_parts_table up method) to
resolve the user table dynamically by reading the configured user model
(config('artisanpack.visual-editor.user_model')), instantiate it (or use
forward_static_call to get its table) and use that table name in the foreign key
definition instead of 'users' (i.e., $userTable = (new $userClass)->getTable();
then ->references('id')->on($userTable)->nullOnDelete()); keep the rest of the
column/index logic unchanged.

In `@src/Services/TemplatePartManager.php`:
- Around line 99-110: The veApplyFilters call returns mixed which can violate
the declared array return type of TemplatePartManager::all(); update all(),
allActive(), and forArea() to validate/cast the filter result: call
veApplyFilters('ap.visualEditor.templateParts', $parts) (and the corresponding
filter keys), check is_array on the result and if not an array fall back to the
original $parts (or cast to (array) safely), then return that validated array so
the method's array<string,array<string,mixed>> contract is preserved.
- Around line 243-260: When updating a TemplatePart in update( TemplatePart
$part, array $data, ?int $userId = null ) ensure slug uniqueness: if
$data['slug'] is set and differs from $part->slug, check the registry and the
database for existing TemplatePart records (excluding the current $part ID) and
throw a RuntimeException if a duplicate exists; perform this check before
calling $part->update($data) (but after the existing is_locked/content revision
checks) so createRevision($userId) and veDoAction remain unchanged.
- Around line 212-225: The create method in TemplatePartManager currently checks
duplicates with TemplatePart::where(...)->exists(), which only queries the DB
and ignores programmatically registered parts; replace that check by calling
TemplatePart::exists($slug) (or the existing exists() helper) to ensure both DB
and in-memory registry are considered, and throw the same RuntimeException when
exists() returns true; additionally validate the incoming $data['area'] against
TemplatePart::AREAS (or the configured areas list) before calling
TemplatePart::create and throw a clear exception if the area is missing/invalid
to prevent persisting invalid area values.

---

Nitpick comments:
In `@src/Models/TemplatePart.php`:
- Around line 212-233: Extract the repeated literal 'template_part' into a class
constant on TemplatePart (e.g. protected const DOCUMENT_TYPE = 'template_part')
and replace both uses in createRevision() and revisions() with
self::DOCUMENT_TYPE; update the Revision::create call and the
Revision::forDocument(...) invocation to use that constant so the document type
is defined in one place.

In `@src/Services/TemplatePartManager.php`:
- Around line 172-183: The resolve() method currently returns mixed types
(TemplatePart model or array) which forces callers to branch; update resolve()
to consistently return array|null by converting any TemplatePart model ($dbPart)
to an array with $dbPart->toArray() and returning the registered entry
($this->registered[$slug]) as-is, and change the method signature/return type
accordingly (TemplatePart->toArray(), resolve(): array|null) so all callers can
expect an array or null and be updated to stop using model methods on the
result.
- Around line 297-304: Validate that the requested new slug is not already in
use before calling $part->duplicate in TemplatePartManager::duplicate: perform a
uniqueness check (e.g., via TemplatePart::where('slug', $newSlug)->exists() or
an existing repository/method like TemplatePart::slugExists($newSlug)), throw or
return a clear validation error if the slug exists, and only call
$part->duplicate($newSlug, $newName) when the slug is unique to match the
create() behavior and provide a clearer error message.

In `@src/VisualEditorServiceProvider.php`:
- Around line 498-520: The code always registers 'header', 'footer', and
'sidebar' via $manager->register which ignores the configured areas; update the
seeding logic to read the configured array at
config('artisanpack.visual-editor.template_parts.areas') and only call
$manager->register for each default part whose 'area' exists in that configured
array (use the existing 'header','footer','sidebar' definitions as templates),
so registration honors the template_part areas configuration.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

Run ID: 9da2ca94-9876-4a9e-9359-7b192f85a888

📥 Commits

Reviewing files that changed from the base of the PR and between 32db73f and 742cd6d.

📒 Files selected for processing (9)
  • config/visual-editor.php
  • database/migrations/2024_01_01_000004_create_visual_editor_template_parts_table.php
  • src/Facades/TemplatePartManager.php
  • src/Models/TemplatePart.php
  • src/Services/TemplatePartManager.php
  • src/VisualEditorServiceProvider.php
  • src/helpers.php
  • tests/Unit/Models/TemplatePartTest.php
  • tests/Unit/Services/TemplatePartManagerTest.php

Comment on lines +37 to +40
$table->unsignedBigInteger( 'user_id' )->nullable();
$table->timestamps();
$table->foreign( 'user_id' )->references( 'id' )->on( 'users' )->nullOnDelete();
$table->index( [ 'area', 'status' ] );

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

Hard-coded users FK conflicts with configurable user model table.

Line 39 pins the foreign key to users, but the model resolves the user class via config('artisanpack.visual-editor.user_model') (see src/Models/TemplatePart.php Line 198). In apps with a custom user table, this can break migration integrity.

💡 Proposed fix
 		Schema::create( 'visual_editor_template_parts', function ( Blueprint $table ): void {
 			$table->id();
@@
 			$table->boolean( 'is_custom' )->default( false );
 			$table->boolean( 'is_locked' )->default( false );
 			$table->unsignedBigInteger( 'user_id' )->nullable();
 			$table->timestamps();
-			$table->foreign( 'user_id' )->references( 'id' )->on( 'users' )->nullOnDelete();
+			$userModelClass = config( 'artisanpack.visual-editor.user_model', 'App\\Models\\User' );
+			$userTable      = class_exists( $userModelClass ) ? ( new $userModelClass() )->getTable() : 'users';
+			$table->foreign( 'user_id' )->references( 'id' )->on( $userTable )->nullOnDelete();
 			$table->index( [ 'area', 'status' ] );
 		} );
 	}
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
$table->unsignedBigInteger( 'user_id' )->nullable();
$table->timestamps();
$table->foreign( 'user_id' )->references( 'id' )->on( 'users' )->nullOnDelete();
$table->index( [ 'area', 'status' ] );
$table->unsignedBigInteger( 'user_id' )->nullable();
$table->timestamps();
$userModelClass = config( 'artisanpack.visual-editor.user_model', 'App\\Models\\User' );
$userTable = class_exists( $userModelClass ) ? ( new $userModelClass() )->getTable() : 'users';
$table->foreign( 'user_id' )->references( 'id' )->on( $userTable )->nullOnDelete();
$table->index( [ 'area', 'status' ] );
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In
`@database/migrations/2024_01_01_000004_create_visual_editor_template_parts_table.php`
around lines 37 - 40, The migration hardcodes the foreign key to 'users', which
conflicts with the configurable user model used by TemplatePart; update the
migration (in the create_visual_editor_template_parts_table up method) to
resolve the user table dynamically by reading the configured user model
(config('artisanpack.visual-editor.user_model')), instantiate it (or use
forward_static_call to get its table) and use that table name in the foreign key
definition instead of 'users' (i.e., $userTable = (new $userClass)->getTable();
then ->references('id')->on($userTable)->nullOnDelete()); keep the rest of the
column/index logic unchanged.

Comment thread src/Services/TemplatePartManager.php
Comment thread src/Services/TemplatePartManager.php
Comment thread src/Services/TemplatePartManager.php
- create() now checks both DB and registry for duplicate slugs and
  validates area against configured areas list
- update() validates slug uniqueness when changing slugs
- duplicate() validates new slug uniqueness before duplicating
- veApplyFilters return values are type-checked to preserve array contract
- Default template part registration respects configured areas list
- seedRegistered() bypasses registry check since it intentionally
  persists registered items to DB
- Added 6 new tests for validation edge cases

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
@ViewFromTheBox

Copy link
Copy Markdown
Member Author

@coderabbitai review

@ViewFromTheBox
ViewFromTheBox marked this pull request as ready for review March 19, 2026 16:06
@ViewFromTheBox

Copy link
Copy Markdown
Member Author

@coderabbitai review

@coderabbitai

coderabbitai Bot commented Mar 19, 2026

Copy link
Copy Markdown
✅ Actions performed

Review triggered.

Note: CodeRabbit is an incremental review system and does not re-review already reviewed commits. This command is applicable only when automatic reviews are paused.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🧹 Nitpick comments (1)
src/Services/TemplatePartManager.php (1)

386-412: Consider validating area before seeding to database.

The seedRegistered() method persists registered template parts to the database without validating the area field against configured areas. If a programmatically registered part has an invalid area (e.g., via third-party code using register()), it would be persisted to the database, bypassing the validation that create() enforces.

This may be intentional if you trust programmatic registrations, but it creates an inconsistency with create().

🔧 Optional: Add area validation to seedRegistered()
 	public function seedRegistered(): array
 	{
 		$created = [];
+		$validAreas = (array) config( 'artisanpack.visual-editor.template_parts.areas', TemplatePart::AREAS );

 		foreach ( $this->registered as $slug => $config ) {
 			if ( TemplatePart::where( 'slug', $slug )->exists() ) {
 				continue;
 			}

+			$area = $config['area'] ?? 'custom';
+			if ( ! in_array( $area, $validAreas, true ) ) {
+				continue; // or throw, depending on desired strictness
+			}
+
 			$part = TemplatePart::create( [
 				'name'        => $config['name'],
 				'slug'        => $slug,
 				'description' => $config['description'] ?? null,
-				'area'        => $config['area'] ?? 'custom',
+				'area'        => $area,
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/Services/TemplatePartManager.php` around lines 386 - 412,
seedRegistered() persists registered parts without validating the 'area' value,
causing inconsistency with the validation enforced by TemplatePart::create();
update seedRegistered() to validate each $config['area'] against the allowed
areas (the same source used by create()/register()), and if invalid either skip
the entry or normalize it to a safe default (e.g., 'custom') before calling
TemplatePart::create() (or delegate to the create() helper) so persisted rows
always respect the same area rules as runtime creations.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Nitpick comments:
In `@src/Services/TemplatePartManager.php`:
- Around line 386-412: seedRegistered() persists registered parts without
validating the 'area' value, causing inconsistency with the validation enforced
by TemplatePart::create(); update seedRegistered() to validate each
$config['area'] against the allowed areas (the same source used by
create()/register()), and if invalid either skip the entry or normalize it to a
safe default (e.g., 'custom') before calling TemplatePart::create() (or delegate
to the create() helper) so persisted rows always respect the same area rules as
runtime creations.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

Run ID: ef33e667-d38b-4cca-bd59-ae1b0c3561f6

📥 Commits

Reviewing files that changed from the base of the PR and between 742cd6d and 4f74448.

📒 Files selected for processing (3)
  • src/Services/TemplatePartManager.php
  • src/VisualEditorServiceProvider.php
  • tests/Unit/Services/TemplatePartManagerTest.php

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

No open projects

Development

Successfully merging this pull request may close these issues.

Build template parts system (header, footer, sidebar)

1 participant