Repository navigation
feat: C2 · Template parts model, migration, API - #363
Conversation
Lands the server-side backing for the `wp_template_part` entity the B1 core-data shim addresses at `/visual-editor/api/template-parts`. Adds the `VisualEditorTemplatePart` model, migration, REST controller, form requests, policy, and routes so the D2 site-editor browser and the E2 front-end template-part renderer have a real store to resolve against. The `show` response carries a `referenced_by` list — template slugs whose block tree embeds this part via a `core/template-part` block — computed by scanning templates in the same theme at read time (no FK, since parts and templates can outlive each other). D2 will use it to warn before deleting a part that is still in use. Area is validated against the enum (header, footer, sidebar, uncategorized) with a clear 422 message; `slug` is unique per theme so the same `header` slug can coexist across installed themes. Closes #358
📝 WalkthroughWalkthroughAdds full server-side support for wp_template_part: migration, Eloquent model, REST CRUD endpoints, validation, JSON resource, authorization policy, referenced_by computation (templates that embed a part), service-provider policy registration, and comprehensive feature tests. Changes
Sequence DiagramsequenceDiagram
actor Client
participant API as "HTTP API\n/routes/api.php"
participant Controller as "TemplatePartController"
participant DB as "VisualEditorTemplatePart\n(database)"
participant Templates as "Template fixtures\n/template storage"
Client->>API: GET /visual-editor/api/template-parts/{id}
API->>Controller: route → show(request, templatePart)
Controller->>DB: load template part record
DB-->>Controller: record (content envelope)
Controller->>Templates: scan templates in same theme\nfor core/template-part blocks (slug, theme)
Templates-->>Controller: matching template slugs (preserve order, dedupe)
Controller->>Controller: build referenced_by array
Controller->>API: TemplatePartResource + referenced_by (200)
API-->>Client: 200 OK + wp_template_part JSON
Estimated Code Review Effort🎯 3 (Moderate) | ⏱️ ~25 minutes Possibly related PRs
Poem
🚥 Pre-merge checks | ✅ 4 | ❌ 1❌ Failed checks (1 warning)
✅ Passed checks (4 passed)
✏️ Tip: You can configure your own custom pre-merge checks in the settings. ✨ Finishing Touches📝 Generate docstrings
🧪 Generate unit tests (beta)
Warning Review ran into problems🔥 ProblemsGit: Failed to clone repository. Please run the Comment |
There was a problem hiding this comment.
Actionable comments posted: 1
🧹 Nitpick comments (2)
database/migrations/2026_04_23_100000_create_visual_editor_template_parts_table.php (1)
31-42: Add a theme-leading index to support common filters efficiently.
forTheme()/theme-scoped listing will not benefit fromunique(slug, theme)becausethemeis not the leading column. Addindex(theme)orindex(theme, area).📈 Suggested migration tweak
Schema::create( 'visual_editor_template_parts', function ( Blueprint $table ) { $table->id(); $table->string( 'slug' ); $table->string( 'title' )->default( '' ); $table->json( 'content' )->nullable(); $table->string( 'area' )->default( 'uncategorized' )->index(); $table->string( 'theme' ); + $table->index( [ 'theme', 'area' ] ); $table->timestamps(); $table->unique( [ 'slug', 'theme' ] ); } );🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@database/migrations/2026_04_23_100000_create_visual_editor_template_parts_table.php` around lines 31 - 42, The migration currently indexes area and defines unique(slug, theme) but does not make theme the leading column for common theme-scoped queries; update the schema in the migration where $table is defined (the create_visual_editor_template_parts table) to add an index on theme (e.g. $table->index('theme')) or a composite index with theme first (e.g. $table->index(['theme', 'area'])) so theme-scoped lookups (forTheme()/theme-scoped listing) are efficient.tests/Feature/VisualEditor/TemplatePartControllerTest.php (1)
147-233: Add areferenced_byregression case for blocks withoutattributes.theme.All of these reference tests set
themeon thecore/template-partblock. The controller is intended to tolerate older exports that omit that attribute, so this path can regress unnoticed and under-report live references before delete.➕ Suggested test
+it( 'treats same-theme template-part blocks without a theme attribute as references', function () { + actingAsTemplatePartUser(); + + $part = createTemplatePart( [ 'slug' => 'header', 'theme' => 'artisanpack-base' ] ); + + VisualEditorTemplate::create( [ + 'slug' => 'single', + 'title' => 'Single', + 'content' => [ + 'raw' => '', + 'blocks' => [ + [ + 'name' => 'core/template-part', + 'attributes' => [ 'slug' => 'header' ], + 'innerBlocks' => [], + ], + ], + ], + 'status' => 'publish', + 'theme' => 'artisanpack-base', + 'source' => 'theme', + 'origin' => 'theme', + ] ); + + $this->getJson( "/visual-editor/api/template-parts/{$part->id}" ) + ->assertOk() + ->assertJsonPath( 'referenced_by', [ 'single' ] ); +} );🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@tests/Feature/VisualEditor/TemplatePartControllerTest.php` around lines 147 - 233, Add a regression test that ensures template references are detected when the core/template-part block omits the attributes.theme field: create a TemplatePart via createTemplatePart(['slug'=>'header','theme'=>'artisanpack-base']), then create a VisualEditorTemplate::create(...) whose blocks include a core/template-part entry that has 'attributes' => ['slug'=>'header'] (no 'theme'), call $this->getJson("/visual-editor/api/template-parts/{$part->id}") and assertJsonPath('referenced_by', ['single']) to verify the controller tolerates older exports missing the theme attribute.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.
Inline comments:
In `@src/Http/Controllers/TemplatePartController.php`:
- Around line 264-279: The blockTreeReferencesPart function currently only
treats 'core/template-part' blocks as references when the block's attributes
include an explicit theme; update the matching logic so that a block with a
matching slug is considered a reference if its attributes.theme is either equal
to the provided $theme OR is missing/empty (treat empty $attributeTheme as
same-theme). Modify the condition that returns true (inside
blockTreeReferencesPart) to allow $attributeTheme === '' as a valid match when
$slug === $attributeSlug, ensuring older exports without attributes.theme are
counted as references by resolveReferencedBy().
---
Nitpick comments:
In
`@database/migrations/2026_04_23_100000_create_visual_editor_template_parts_table.php`:
- Around line 31-42: The migration currently indexes area and defines
unique(slug, theme) but does not make theme the leading column for common
theme-scoped queries; update the schema in the migration where $table is defined
(the create_visual_editor_template_parts table) to add an index on theme (e.g.
$table->index('theme')) or a composite index with theme first (e.g.
$table->index(['theme', 'area'])) so theme-scoped lookups
(forTheme()/theme-scoped listing) are efficient.
In `@tests/Feature/VisualEditor/TemplatePartControllerTest.php`:
- Around line 147-233: Add a regression test that ensures template references
are detected when the core/template-part block omits the attributes.theme field:
create a TemplatePart via
createTemplatePart(['slug'=>'header','theme'=>'artisanpack-base']), then create
a VisualEditorTemplate::create(...) whose blocks include a core/template-part
entry that has 'attributes' => ['slug'=>'header'] (no 'theme'), call
$this->getJson("/visual-editor/api/template-parts/{$part->id}") and
assertJsonPath('referenced_by', ['single']) to verify the controller tolerates
older exports missing the theme attribute.
🪄 Autofix (Beta)
Fix all unresolved CodeRabbit comments on this PR:
- Push a commit to this branch (recommended)
- Create a new PR with the fixes
ℹ️ Review info
⚙️ Run configuration
Configuration used: Path: .coderabbit.yaml
Review profile: CHILL
Plan: Pro
Run ID: 70498dc7-4ea0-4c12-ba96-01fa7a2e4eb6
📒 Files selected for processing (10)
database/migrations/2026_04_23_100000_create_visual_editor_template_parts_table.phproutes/api.phpsrc/Http/Controllers/TemplatePartController.phpsrc/Http/Requests/StoreTemplatePartRequest.phpsrc/Http/Requests/UpdateTemplatePartRequest.phpsrc/Http/Resources/TemplatePartResource.phpsrc/Models/VisualEditorTemplatePart.phpsrc/Policies/VisualEditorTemplatePartPolicy.phpsrc/VisualEditorServiceProvider.phptests/Feature/VisualEditor/TemplatePartControllerTest.php
resolveReferencedBy() already scopes the template scan to the target theme, so an untagged `core/template-part` block inside one of those templates can only resolve to a part in that same theme. The docblock on blockTreeReferencesPart() already promised this behavior, but the implementation required an exact theme match — older exports without attributes.theme would silently drop out of the referenced_by list. Adds a regression test covering the untagged-reference case.
There was a problem hiding this comment.
🧹 Nitpick comments (1)
src/Http/Controllers/TemplatePartController.php (1)
177-184: Consider usingresponse()->noContent()for 204 responses.
response()->json(null, 204)sends a body containingnull. For a true 204 No Content response, Laravel'sresponse()->noContent()is more semantically correct and cleaner.♻️ Suggested improvement
public function destroy( VisualEditorTemplatePart $templatePart ): JsonResponse { Gate::authorize( 'delete', $templatePart ); $templatePart->delete(); - return response()->json( null, Response::HTTP_NO_CONTENT ); + return response()->noContent(); }Note: The return type would need to change to
Responseinstead ofJsonResponse.🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@src/Http/Controllers/TemplatePartController.php` around lines 177 - 184, In the destroy method for VisualEditorTemplatePart, replace the JSON 204 response with Laravel's response()->noContent() to send a proper 204 No Content response; update the method return type from JsonResponse to Illuminate\Http\Response (or Response) and ensure the Gate::authorize and $templatePart->delete() calls remain unchanged so the method returns response()->noContent() instead of response()->json(null, Response::HTTP_NO_CONTENT).
🤖 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/Http/Controllers/TemplatePartController.php`:
- Around line 177-184: In the destroy method for VisualEditorTemplatePart,
replace the JSON 204 response with Laravel's response()->noContent() to send a
proper 204 No Content response; update the method return type from JsonResponse
to Illuminate\Http\Response (or Response) and ensure the Gate::authorize and
$templatePart->delete() calls remain unchanged so the method returns
response()->noContent() instead of response()->json(null,
Response::HTTP_NO_CONTENT).
ℹ️ Review info
⚙️ Run configuration
Configuration used: Path: .coderabbit.yaml
Review profile: CHILL
Plan: Pro
Run ID: f80c5846-9bf4-44cd-a4af-211bfd6707a8
📒 Files selected for processing (2)
src/Http/Controllers/TemplatePartController.phptests/Feature/VisualEditor/TemplatePartControllerTest.php
Description
Lands the server-side backing for the
wp_template_partentity the B1 core-data shim dispatches at/visual-editor/api/template-parts. Adds theVisualEditorTemplatePartmodel, migration, REST controller, form requests, policy, and routes so the D2 site-editor browser and the E2 front-end template-part renderer have a real store to resolve against.Closes: #358
Type of Change
Related Issue
Issue: #358
Motivation and Context
Template parts are reusable fragments of a template —
header,footer,sidebar, etc. — referenced from templates viacore/template-partblocks. B1 stubbed out the REST contract and B2 shipped fixtures demonstrating the shape; C2 makes those calls resolve against real data.Because a single part can be used by several templates (the B2
header.json+footer.jsonare referenced by every template fixture), theshowresponse carries areferenced_bypointer listing the template slugs that currently include it. That lets D2 warn before deleting a part still in use.Changes Made
VisualEditorTemplatePartEloquent model with the{ raw, blocks }content envelope accessors andarea/theme/slugscopes, mirroringVisualEditorTemplateso block renderers can consume either surface without translation.visual_editor_template_parts(id,slug,title,contentJSON,area,theme, timestamps) with composite unique(slug, theme).TemplatePartControllerwithindex,show,store,update,destroy.showmerges a derivedreferenced_bylist — computed by recursively scanning block trees of templates in the same theme forcore/template-partblocks whoseslug+themematch the current part.StoreTemplatePartRequest+UpdateTemplatePartRequest— area validated against the enum,slugunique per theme (mirrored on both slug + theme fields so theme-only updates still trigger the check), bare-listcontentpayloads rejected to keepTemplateBlockTreeRulereachable.TemplatePartResourceemitting the shim-compatible{ id, slug, title.rendered, content.{raw,blocks}, area, theme, type }shape.VisualEditorTemplatePartPolicy— auth-only CRUD, mirroringVisualEditorTemplatePolicy. Host apps tighten access by rebinding the policy./visual-editor/api/template-partswith the package's existing auth middleware stack.How Has This Been Tested?
Testing Environment:
Tests Performed:
./vendor/bin/pest tests/Feature/VisualEditor/TemplatePartControllerTest.php— 31/31 green.cr review --base release/1.0 --prompt-only): No findings.Accessibility Tests Run
Tests Added
Test details:
tests/Feature/VisualEditor/TemplatePartControllerTest.php(31 tests, 144 assertions):{ data, meta }envelope, populated list, filters byslug/theme/area.referenced_by: correctly lists referencing templates; ignores parts in other themes; traverses nestedinnerBlocks; deduplicates when a template embeds a part twice.header,footer,sidebar,uncategorized) — dataset-driven; 422 on invalid/missing area with a message that names every legal value; 422 on duplicate(slug, theme); success on same slug across themes; 422 on malformed block tree; 422 on null title.(slug, theme), 422 on null title, 422 on bare-list content.storeand come back unchanged fromshow.Documentation
Documentation details: Every new class carries a PHPDoc block explaining its role and, where relevant, the design decisions (why
referenced_byis derived rather than persisted, whyareais a plain string column gated by application-layer validation, why uniqueness is mirrored on both slug + theme in the update request).The B1 core-data shim contract at
docs/core-data-shim.md§Template parts already documented the endpoint shape, so no doc changes are needed.Screenshots
N/A — backend change.
Pre-Submission Checklist
Summary by CodeRabbit