Skip to content

feat: C4 · Navigation: model, migration, API, menu locations - #365

Merged
ViewFromTheBox merged 1 commit into
release/1.0from
feature/360-c4-navigation-backend
Apr 23, 2026
Merged

ViewFromTheBox merged 1 commit into
release/1.0from
feature/360-c4-navigation-backend

Conversation

@ViewFromTheBox

@ViewFromTheBox ViewFromTheBox commented Apr 23, 2026 •

Copy link
Copy Markdown
Member

Description

Lands the server-side backing for the wp_navigation entity the B1 core-data shim dispatches to /visual-editor/api/navigation. Adds the model, migration, CRUD REST controller, form-request validation, resource, policy, and the config-driven menu-locations surface core/navigation will read in D4/E4.

Closes: #360

Type of Change

  • New feature (adds new functionality)

Related Issue

Issue: #360

Motivation and Context

Per §4.3 of the V1 plan doc, navigation is the single biggest risk area in the expansion — core/navigation pulls a disproportionate share of the core-data surface with it. B1 (#353) absorbed the client-side shim work and B2 (#354) shipped the fixtures; C4 lands the backend contract so D4 (nav editor UI) can re-enable core/navigation.

Menu locations for V1 are configuration-driven per §8 of the plan doc — a navigation.locations array in config/artisanpack/visual-editor.php names the slugs the theme exposes and maps each to a nav record (or the fallback). No admin CRUD for locations in V1; that's 1.1+.

Changes Made

  • VisualEditorNavigation Eloquent model backing wp_navigation with {raw, blocks} envelope helpers, published() scope, and isEmpty() check.
  • Migration for visual_editor_navigations (id, unique slug, title, content JSON, status indexed, menu_order indexed, timestamps).
  • NavigationController with the five shim-compatible endpoints mounted under /visual-editor/api/navigation[/:id].
  • StoreNavigationRequest / UpdateNavigationRequest — status enum, {raw, blocks} envelope shape, TemplateBlockTreeRule for the block tree, unique slug (ignore-self on update).
  • NavigationResource shaping records into the B1 wp_navigation envelope (title.rendered, content.{raw,blocks}, menu_order, type: wp_navigation).
  • VisualEditorNavigationPolicy — auth-gated CRUD matching the Template policy default.
  • navigation.locations config section with a documented {slug, label, primary_id?} shape.
  • MenuLocationResolver::forLocation($slug) service — returns configured primary when assigned + non-empty, else falls back to the first published nav by menu_order, else null.
  • Policy + resolver wired into the service provider; routes mounted in routes/api.php alongside the rest of the C-phase surface.

How Has This Been Tested?

Testing Environment:

  • Operating System: macOS (Darwin 25.3.0)
  • PHP Version: 8.4.3
  • Project Version: release/1.0

Tests Performed:

  1. Full pest suite — 305 passed (952 assertions), no regressions.
  2. Navigation-specific suite — 32 new tests covering index / show (hit + 404) / store / update / destroy, validation, policy gating, nested.json round-trip, all B2 fixtures round-trip, and every MenuLocationResolver branch (configured hit, null primary_id, missing id, empty record, no published navs, unknown slug, malformed config).
  3. CodeRabbit CLI review against release/1.0 — no findings.

Accessibility Tests Run

  • N/A — backend-only change (no user-facing UI)

Details: Backend-only PR. No templates, views, or frontend code shipped.

Tests Added

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

Test details:

  • tests/Feature/VisualEditor/NavigationControllerTest.php — 22 tests covering the full REST surface plus bare-list rejection, null-title rejection, slug-collision updates, and the B2 fixture round-trip.
  • tests/Feature/VisualEditor/MenuLocationResolverTest.php — 10 tests covering configured-hit, fallback-hit for null/missing/empty primary, empty-DB null, draft exclusion, unknown-slug fallback, locations() normalization, malformed entry filtering.

Documentation

  • Inline code documentation added

Documentation details: PHPDoc blocks on every new class and public method. The navigation.locations config block is documented inline in config/visual-editor.php. No separate docs file needed — docs/core-data-shim.md §Navigation already locks the record shape.

Pre-Submission Checklist

  • Followed contributing guidelines
  • Checked for other open PRs for same update
  • Code passes all tests (305/305)
  • Code has been linted (no lint tooling configured in this package; follows existing Template/Global-Styles patterns)
  • Accessibility tests completed (N/A — backend-only)
  • Code follows project style guide
  • Self-review completed
  • Comments added for complex code
  • No new warnings generated

Summary by CodeRabbit

  • New Features

    • Added REST API endpoints for managing visual editor navigations with create, read, update, delete, and list operations.
    • Added support for storing navigation content with draft and published status options.
    • Added configurable menu location mapping to integrate theme menus with stored navigations.
  • Tests

    • Added comprehensive test coverage for navigation API functionality and menu location resolution.

Lands the server-side backing for the wp_navigation entity the B1
core-data shim addresses at /visual-editor/api/navigation. Adds the
VisualEditorNavigation Eloquent model, migration, CRUD controller,
store/update form requests (with TemplateBlockTreeRule for the
{raw, blocks} envelope), NavigationResource, and policy gating.

Menu locations are config-driven for V1 per the plan doc §8: a new
navigation.locations section in config/artisanpack/visual-editor.php
names theme-exposed slugs, and MenuLocationResolver::forLocation($slug)
falls back to the first published nav in menu_order when a location
has no assignment, points at a missing record, or points at an empty
block tree. Returns null when the database has no published navs.

Round-trip coverage includes B2's nested.json fixture to exercise
core/navigation-submenu with arbitrary innerBlocks depth.

core/navigation stays on disabled_blocks until D4/E4.

Closes #360
@coderabbitai

coderabbitai Bot commented Apr 23, 2026 •

Copy link
Copy Markdown
📝 Walkthrough

Walkthrough

Implements the server-side backing for wp_navigation REST API, including database table, Eloquent model, CRUD controller with form validation, JSON resource serialization, authorization policy, and a configuration-driven menu location resolver service with fallback navigation logic.

Changes

Cohort / File(s) Summary
Configuration & Database
config/visual-editor.php, database/migrations/2026_04_23_300000_create_visual_editor_navigations_table.php
Adds navigation configuration section with location metadata mapping; creates visual_editor_navigations table with slug, title, JSON content, status, and menu_order columns.
API Routing
routes/api.php
Registers five REST endpoints for wp_navigation CRUD operations under visual-editor.api.navigation.* named routes, importing NavigationController.
Request Validation
src/Http/Requests/StoreNavigationRequest.php, src/Http/Requests/UpdateNavigationRequest.php
Validates create/update payloads with slug uniqueness, enum status, optional title/menu_order fields, and content envelope shape (enforcing object-like structure with raw/blocks subfields).
API Controller & Resources
src/Http/Controllers/NavigationController.php, src/Http/Resources/NavigationResource.php
Implements index (paginated, filterable by slug/status), show, store (201), update, and destroy (204) endpoints; serializes navigation records with rendered title and normalized content envelope.
Domain Model & Authorization
src/Models/VisualEditorNavigation.php, src/Policies/VisualEditorNavigationPolicy.php
Defines Eloquent model with status constants, content envelope accessors/mutators, and empty-tree detection; gates all CRUD actions on authenticated user presence.
Menu Location Service
src/Services/MenuLocationResolver.php, src/VisualEditorServiceProvider.php
Reads configuration-driven location mappings, resolves requested location slug to navigation (or falls back to first published by menu_order), registers service singleton and policy binding.
Feature Tests
tests/Feature/VisualEditor/MenuLocationResolverTest.php, tests/Feature/VisualEditor/NavigationControllerTest.php
Comprehensive CRUD tests covering authorization, pagination, filtering, 404 handling, content round-trip; resolver tests verify primary_id fallback, empty-record handling, first-published selection, and configuration parsing.

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~45 minutes

Poem

🐰 Through the config came locations bright,
A nav table sprouted overnight,
REST controllers danced to CRUD's sweet song,
Fallback menus fixed what once went wrong,
Nested blocks nested, permission gates tight!

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 58.33% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title 'feat: C4 · Navigation: model, migration, API, menu locations' directly maps to the main changes: a new VisualEditorNavigation model, database migration, REST API controller, and MenuLocationResolver service.
Linked Issues check ✅ Passed All in-scope requirements from issue #360 are implemented: model with envelope helpers and published() scope, migration with slug/title/content/status/menu_order, complete REST CRUD controller, form validation with status enum and block-tree rules, config-driven navigation.locations with primary_id fallback, MenuLocationResolver service with forLocation() and fallback logic, policy-gated endpoints, and comprehensive Pest tests covering all branches including nested.json round-trip.
Out of Scope Changes check ✅ Passed All changes are directly aligned with issue #360 in-scope deliverables: no admin CRUD UI, no nav-editor screen, no front-end rendering, no link picker, no auto-derived menus, and core/navigation disabled state left unchanged per requirements.

✏️ 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 feature/360-c4-navigation-backend

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

@ViewFromTheBox ViewFromTheBox moved this from Backlog to In Review in ArtisanPack UI Overview Apr 23, 2026
@ViewFromTheBox ViewFromTheBox linked an issue Apr 23, 2026 that may be closed by this pull request
9 tasks
@ViewFromTheBox
ViewFromTheBox marked this pull request as ready for review April 23, 2026 19:16

@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 (2)
tests/Feature/VisualEditor/MenuLocationResolverTest.php (1)

47-48: Avoid repeated resolver calls within one assertion chain.

Each repeated forLocation() call can hit the DB again; storing the resolved value once makes these tests cheaper and easier to read.

Pattern to apply in these tests
-	expect( $resolver->forLocation( 'primary' ) )->not->toBeNull()
-		->and( $resolver->forLocation( 'primary' )->id )->toBe( $primary->id );
+	$resolved = $resolver->forLocation( 'primary' );
+	expect( $resolved )->not->toBeNull()
+		->and( $resolved->id )->toBe( $primary->id );

Also applies to: 65-67, 82-84, 107-109, 139-141, 150-152

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

In `@tests/Feature/VisualEditor/MenuLocationResolverTest.php` around lines 47 -
48, The test repeatedly calls $resolver->forLocation('primary') (and similar
locations) inside a single assertion chain causing extra DB queries; change each
test to call $resolver->forLocation(...) once, store the result in a local
variable (e.g., $resolved = $resolver->forLocation('primary')), then assert on
$resolved (not null and ->id equals $primary->id). Update all occurrences
referenced in the comment (lines around the chains with forLocation, including
the other ranges) to use the single-call pattern with a local variable.
database/migrations/2026_04_23_300000_create_visual_editor_navigations_table.php (1)

37-38: Consider a composite index for the published-fallback read path.

MenuLocationResolver::fallback() filters by status and orders by menu_order; a composite index can reduce sort/filter cost as this table grows.

Proposed migration tweak
-			$table->string( 'status' )->default( 'publish' )->index();
-			$table->unsignedInteger( 'menu_order' )->default( 0 )->index();
+			$table->string( 'status' )->default( 'publish' );
+			$table->unsignedInteger( 'menu_order' )->default( 0 );
+			$table->index( [ 'status', 'menu_order' ], 'visual_editor_nav_status_menu_order_idx' );
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In
`@database/migrations/2026_04_23_300000_create_visual_editor_navigations_table.php`
around lines 37 - 38, Add a composite index on (status, menu_order) in the
migration that creates the visual editor navigations table to optimize
MenuLocationResolver::fallback() queries that filter by status and order by
menu_order; in the migration class CreateVisualEditorNavigationsTable (inside
the up() method where $table->string('status')->... and
$table->unsignedInteger('menu_order')->... are defined) add a combined index
definition for status and menu_order so the DB can use one index for both
filtering and ordering.
🤖 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/Requests/StoreNavigationRequest.php`:
- Around line 77-82: The envelopeShapeRule() closure currently lets an empty
list pass because of the "&& [] !== $value" check; remove that exemption so any
list-shaped array (including empty lists like content: []) is rejected. Update
the condition in envelopeShapeRule() (the Closure returned by envelopeShapeRule)
from "is_array($value) && [] !== $value && array_is_list($value)" to simply
"is_array($value) && array_is_list($value)" (or add an explicit check to fail
when $value is an empty list) so the rule rejects bare lists including empty
arrays and calls $fail() accordingly.
- Around line 29-32: The request currently always returns true in
StoreNavigationRequest::authorize(), allowing expensive validation (e.g.,
TemplateBlockTreeRule) to run before the controller's policy check; change
authorize() to perform the permission check (for example using
$this->user()->can('create', Navigation::class) or Gate::allows('create',
Navigation::class)) and return that boolean so unauthorized requests are
rejected before validation runs, and remove/move the duplicate policy check from
the controller to rely on the request authorize() method.

In `@src/Http/Requests/UpdateNavigationRequest.php`:
- Around line 79-84: The envelopeShapeRule closure currently allows an empty
bare list (content: []) because it excludes [] from the failure condition;
update the closure in envelopeShapeRule so that any list-form array—including an
empty array detected by array_is_list($value)—triggers the $fail call. Locate
the envelopeShapeRule() method and remove the special-case exclusion (the [] !==
$value check) so the validation fails for both non-empty and empty bare lists
with the existing error message.
- Around line 29-32: The authorize() method in UpdateNavigationRequest currently
returns true unconditionally; move the authorization check into this method so
the policy is enforced before validation. Replace the unconditional return with
a call to Gate::authorize(...) or use $this->user()->can(...) inside
UpdateNavigationRequest::authorize() to run the same permission check used in
the controller (matching the Gate::authorize() call there) and return true only
when authorized—otherwise throw/return false so Laravel returns 403 prior to
validation.

---

Nitpick comments:
In
`@database/migrations/2026_04_23_300000_create_visual_editor_navigations_table.php`:
- Around line 37-38: Add a composite index on (status, menu_order) in the
migration that creates the visual editor navigations table to optimize
MenuLocationResolver::fallback() queries that filter by status and order by
menu_order; in the migration class CreateVisualEditorNavigationsTable (inside
the up() method where $table->string('status')->... and
$table->unsignedInteger('menu_order')->... are defined) add a combined index
definition for status and menu_order so the DB can use one index for both
filtering and ordering.

In `@tests/Feature/VisualEditor/MenuLocationResolverTest.php`:
- Around line 47-48: The test repeatedly calls $resolver->forLocation('primary')
(and similar locations) inside a single assertion chain causing extra DB
queries; change each test to call $resolver->forLocation(...) once, store the
result in a local variable (e.g., $resolved =
$resolver->forLocation('primary')), then assert on $resolved (not null and ->id
equals $primary->id). Update all occurrences referenced in the comment (lines
around the chains with forLocation, including the other ranges) to use the
single-call pattern with a local variable.
🪄 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: aa5b4e2b-29c5-42d0-b2b7-e582afebf144

📥 Commits

Reviewing files that changed from the base of the PR and between 3b6ac52 and 561d2df.

📒 Files selected for processing (13)
  • config/visual-editor.php
  • database/migrations/2026_04_23_300000_create_visual_editor_navigations_table.php
  • routes/api.php
  • src/Http/Controllers/NavigationController.php
  • src/Http/Requests/StoreNavigationRequest.php
  • src/Http/Requests/UpdateNavigationRequest.php
  • src/Http/Resources/NavigationResource.php
  • src/Models/VisualEditorNavigation.php
  • src/Policies/VisualEditorNavigationPolicy.php
  • src/Services/MenuLocationResolver.php
  • src/VisualEditorServiceProvider.php
  • tests/Feature/VisualEditor/MenuLocationResolverTest.php
  • tests/Feature/VisualEditor/NavigationControllerTest.php

Comment on lines +29 to +32
public function authorize(): bool
{
return true;
}

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

🧩 Analysis chain

🌐 Web query:

In Laravel FormRequest, does authorize() run before validation and before the controller action?

💡 Result:

Yes, in Laravel FormRequest, the authorize method runs before validation and before the controller action. When a FormRequest is type-hinted in a controller method, Laravel automatically calls validateResolved on it (via FormRequestServiceProvider) before injecting it into the controller and executing the action. This happens after middleware but before the controller runs. In validateResolved from ValidatesWhenResolvedTrait: 1. prepareForValidation (optional hook) 2. passesAuthorization → calls authorize if defined 3. If authorized, getValidatorInstance → creates validator using rules 4. Runs validation 5. If passes, passedValidation (optional hook) If authorize returns false, it throws AuthorizationException (403) before any validation occurs. Validation only runs if authorization passes. The entire process (auth + validation) completes before the controller action executes, as confirmed by official docs: "The incoming form request is validated before the controller method is called".

Citations:


🏁 Script executed:

# Find and examine the NavigationController around line 113
find . -name "NavigationController.php" -type f

Repository: ArtisanPack-UI/visual-editor

Length of output: 119


🏁 Script executed:

# Find and examine StoreNavigationRequest to see the rules
find . -name "StoreNavigationRequest.php" -type f

Repository: ArtisanPack-UI/visual-editor

Length of output: 118


🏁 Script executed:

# Search for VisualEditorNavigation class
fd -t f "VisualEditorNavigation" --type f

Repository: ArtisanPack-UI/visual-editor

Length of output: 155


🏁 Script executed:

# Search for TemplateBlockTreeRule
fd -t f "TemplateBlockTreeRule" --type f

Repository: ArtisanPack-UI/visual-editor

Length of output: 107


🏁 Script executed:

# Read NavigationController around line 113
cat -n ./src/Http/Controllers/NavigationController.php | sed -n '100,130p'

Repository: ArtisanPack-UI/visual-editor

Length of output: 1188


🏁 Script executed:

# Read StoreNavigationRequest - focus on authorize and rules
cat -n ./src/Http/Requests/StoreNavigationRequest.php

Repository: ArtisanPack-UI/visual-editor

Length of output: 3063


🏁 Script executed:

# Check VisualEditorNavigationPolicy for the create method
cat -n ./src/Policies/VisualEditorNavigationPolicy.php

Repository: ArtisanPack-UI/visual-editor

Length of output: 2030


🏁 Script executed:

# Check TemplateBlockTreeRule to understand its cost
cat -n ./src/Rules/TemplateBlockTreeRule.php | head -50

Repository: ArtisanPack-UI/visual-editor

Length of output: 1784


Move the authorization check to the authorize() method.

Line 31 always returns true, so the payload gets fully validated before the policy check in the controller (line 113). This means an authenticated caller can trigger expensive validation in TemplateBlockTreeRule even if they lack create permission. Policy checks should happen before validation runs.

Suggested change
 public function authorize(): bool
 {
-	return true;
+	return $this->user()?->can( 'create', VisualEditorNavigation::class ) ?? false;
 }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/Http/Requests/StoreNavigationRequest.php` around lines 29 - 32, The
request currently always returns true in StoreNavigationRequest::authorize(),
allowing expensive validation (e.g., TemplateBlockTreeRule) to run before the
controller's policy check; change authorize() to perform the permission check
(for example using $this->user()->can('create', Navigation::class) or
Gate::allows('create', Navigation::class)) and return that boolean so
unauthorized requests are rejected before validation runs, and remove/move the
duplicate policy check from the controller to rely on the request authorize()
method.

Comment on lines +77 to +82
protected function envelopeShapeRule(): Closure
{
return function ( string $attribute, mixed $value, Closure $fail ): void {
if ( is_array( $value ) && [] !== $value && array_is_list( $value ) ) {
$fail( 'The :attribute must be a { raw, blocks } envelope, not a bare list of blocks.' );
}

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 | 🟡 Minor

Reject content: [] too.

Line 80 exempts the empty list case, so content: [] is accepted and then normalized into an empty { raw, blocks } envelope. That leaves a contract hole even though this rule says the payload must be object-shaped.

Suggested change
 	return function ( string $attribute, mixed $value, Closure $fail ): void {
-		if ( is_array( $value ) && [] !== $value && array_is_list( $value ) ) {
+		if ( is_array( $value ) && array_is_list( $value ) ) {
 			$fail( 'The :attribute must be a { raw, blocks } envelope, not a bare list of blocks.' );
 		}
 	};
📝 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
protected function envelopeShapeRule(): Closure
{
return function ( string $attribute, mixed $value, Closure $fail ): void {
if ( is_array( $value ) && [] !== $value && array_is_list( $value ) ) {
$fail( 'The :attribute must be a { raw, blocks } envelope, not a bare list of blocks.' );
}
protected function envelopeShapeRule(): Closure
{
return function ( string $attribute, mixed $value, Closure $fail ): void {
if ( is_array( $value ) && array_is_list( $value ) ) {
$fail( 'The :attribute must be a { raw, blocks } envelope, not a bare list of blocks.' );
}
🧰 Tools
🪛 PHPMD (2.15.0)

[warning] 79-79: Avoid unused parameters such as '$attribute'. (undefined)

(UnusedFormalParameter)

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

In `@src/Http/Requests/StoreNavigationRequest.php` around lines 77 - 82, The
envelopeShapeRule() closure currently lets an empty list pass because of the "&&
[] !== $value" check; remove that exemption so any list-shaped array (including
empty lists like content: []) is rejected. Update the condition in
envelopeShapeRule() (the Closure returned by envelopeShapeRule) from
"is_array($value) && [] !== $value && array_is_list($value)" to simply
"is_array($value) && array_is_list($value)" (or add an explicit check to fail
when $value is an empty list) so the rule rejects bare lists including empty
arrays and calls $fail() accordingly.

Comment on lines +29 to +32
public function authorize(): bool
{
return true;
}

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

🧩 Analysis chain

🌐 Web query:

In Laravel FormRequest, does authorize() run before validation and before the controller action?

💡 Result:

Yes, in Laravel FormRequest, the authorize method runs before validation and before the controller action. The FormRequest implements ValidatesWhenResolved, which triggers validateResolved automatically when the request is resolved (type-hinted in controller). In validateResolved from ValidatesWhenResolvedTrait: 1. prepareForValidation 2. passesAuthorization which calls authorize 3. If authorization fails, failedAuthorization throws exception (controller not reached) 4. getValidatorInstance creates validator using rules 5. Validator runs (actual data validation) 6. If validation fails, failedValidation 7. If both pass, passedValidation, controller action executes This order is confirmed in Laravel 10.x source code and official docs stating FormRequest validation (including authorization) happens before controller method. If authorize returns false, 403 Forbidden before any validation or controller execution.

Citations:


🏁 Script executed:

# Check the UpdateNavigationRequest.php file content
cat -n src/Http/Requests/UpdateNavigationRequest.php

Repository: ArtisanPack-UI/visual-editor

Length of output: 3114


🏁 Script executed:

# Check the controller to see the policy check at line 141
sed -n '130,150p' src/Http/Controllers/NavigationController.php

Repository: ArtisanPack-UI/visual-editor

Length of output: 543


🏁 Script executed:

# Check if VisualEditorNavigation class exists and is properly imported
rg "VisualEditorNavigation" --type php -B 2 -A 5 src/Http/Requests/

Repository: ArtisanPack-UI/visual-editor

Length of output: 3295


🏁 Script executed:

# Check the StoreNavigationRequest to see if there's a similar pattern
cat -n src/Http/Requests/StoreNavigationRequest.php | head -50

Repository: ArtisanPack-UI/visual-editor

Length of output: 1670


🏁 Script executed:

# Check the routes to understand the route parameters
rg "navigation" --type php src/routes/ | grep -i update

Repository: ArtisanPack-UI/visual-editor

Length of output: 123


Move the update authorization check to the authorize() method.

Currently, authorize() returns true unconditionally, allowing any authenticated user to pass request validation. The actual authorization check (Gate::authorize()) happens in the controller after validation, meaning unauthorized users can trigger validation errors and receive 422 responses instead of being rejected immediately with 403. In Laravel, authorize() runs before validation; implement the policy check here to prevent unauthorized access from running the validator.

Suggested change
 public function authorize(): bool
 {
-	return true;
+	$navigation = $this->route( 'navigation' );
+
+	return $navigation instanceof VisualEditorNavigation
+		&& ( $this->user()?->can( 'update', $navigation ) ?? false );
 }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/Http/Requests/UpdateNavigationRequest.php` around lines 29 - 32, The
authorize() method in UpdateNavigationRequest currently returns true
unconditionally; move the authorization check into this method so the policy is
enforced before validation. Replace the unconditional return with a call to
Gate::authorize(...) or use $this->user()->can(...) inside
UpdateNavigationRequest::authorize() to run the same permission check used in
the controller (matching the Gate::authorize() call there) and return true only
when authorized—otherwise throw/return false so Laravel returns 403 prior to
validation.

Comment on lines +79 to +84
protected function envelopeShapeRule(): Closure
{
return function ( string $attribute, mixed $value, Closure $fail ): void {
if ( is_array( $value ) && [] !== $value && array_is_list( $value ) ) {
$fail( 'The :attribute must be a { raw, blocks } envelope, not a bare list of blocks.' );
}

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 | 🟡 Minor

Reject the empty bare-list payload on update as well.

Line 82 still allows content: [], which then gets normalized into an empty envelope. That makes the update path accept a payload shape this validator says should be rejected.

Suggested change
 	return function ( string $attribute, mixed $value, Closure $fail ): void {
-		if ( is_array( $value ) && [] !== $value && array_is_list( $value ) ) {
+		if ( is_array( $value ) && array_is_list( $value ) ) {
 			$fail( 'The :attribute must be a { raw, blocks } envelope, not a bare list of blocks.' );
 		}
 	};
📝 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
protected function envelopeShapeRule(): Closure
{
return function ( string $attribute, mixed $value, Closure $fail ): void {
if ( is_array( $value ) && [] !== $value && array_is_list( $value ) ) {
$fail( 'The :attribute must be a { raw, blocks } envelope, not a bare list of blocks.' );
}
protected function envelopeShapeRule(): Closure
{
return function ( string $attribute, mixed $value, Closure $fail ): void {
if ( is_array( $value ) && array_is_list( $value ) ) {
$fail( 'The :attribute must be a { raw, blocks } envelope, not a bare list of blocks.' );
}
};
}
🧰 Tools
🪛 PHPMD (2.15.0)

[warning] 81-81: Avoid unused parameters such as '$attribute'. (undefined)

(UnusedFormalParameter)

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

In `@src/Http/Requests/UpdateNavigationRequest.php` around lines 79 - 84, The
envelopeShapeRule closure currently allows an empty bare list (content: [])
because it excludes [] from the failure condition; update the closure in
envelopeShapeRule so that any list-form array—including an empty array detected
by array_is_list($value)—triggers the $fail call. Locate the envelopeShapeRule()
method and remove the special-case exclusion (the [] !== $value check) so the
validation fails for both non-empty and empty bare lists with the existing error
message.

@ViewFromTheBox
ViewFromTheBox merged commit dd7ea71 into release/1.0 Apr 23, 2026
1 check passed
@ViewFromTheBox
ViewFromTheBox deleted the feature/360-c4-navigation-backend branch April 23, 2026 19:31
@github-project-automation github-project-automation Bot moved this from In Review to Done in ArtisanPack UI Overview Apr 23, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

Development

Successfully merging this pull request may close these issues.

C4 · Navigation: model, migration, API, menu locations

1 participant