Skip to content

Implement handleEntityWrite for consistent validation error handling in remaining controllers #1791

@coderabbitai

Description

@coderabbitai

Background

In PR #1790, we introduced the handleEntityWrite method in BaseController to provide centralized error handling for entity write operations. This method specifically handles schema validation exceptions and presents validation errors in a user-friendly format.

Current Implementation

The handleEntityWrite method (in zmsadmin/src/Zmsadmin/BaseController.php) provides:

  • Centralized exception handling for entity write operations
  • Automatic transformation of validation errors to user-friendly format
  • Consistent error display using the schemavalidation.twig template
  • Fallback to appropriate error templates when available

Already Implemented

The following controllers have been updated to use handleEntityWrite:

  • Department.php
  • DepartmentAddScope.php
  • Organisation.php
  • Profile.php
  • Scope.php
  • SourceEdit.php
  • UseraccountAdd.php
  • UseraccountEdit.php

Controllers That Need Implementation

The following controllers currently perform direct readPostResult calls without centralized error handling and would benefit from handleEntityWrite:

1. Owner.php (Line 35)

Why: Updates owner entities via POST. Currently redirects on success but lacks validation error handling. Users won't see helpful validation messages if the API rejects the input.

Current code:

$entity = \App::$http->readPostResult('/owner/' . $entity->id . '/', $entity)
    ->getEntity();

Impact: Medium - Owner updates are administrative operations that benefit from clear validation feedback.


2. OwnerAdd.php (Line 28)

Why: Creates new owner entities via POST. Without proper error handling, validation failures during owner creation won't be presented clearly to users.

Current code:

$entity = \App::$http->readPostResult('/owner/add/', $entity)
    ->getEntity();

Impact: Medium - Owner creation is less frequent but critical for system setup.


3. OwnerAddOrganisation.php (Line 28)

Why: Creates organisations under an owner via POST. Validation errors (e.g., missing required fields, invalid data) should be displayed to help users correct their input.

Current code:

$entity = \App::$http->readPostResult('/owner/' . $parentId . '/organisation/', $entity)
    ->getEntity();

Impact: High - Organisation creation is a common administrative task and proper validation feedback is important.


4. OrganisationAddDepartment.php (Line 32)

Why: Creates departments under an organisation via POST. Complex form with dayoff lists and links that can have validation errors.

Current code:

$department = \App::$http->readPostResult('/organisation/' . $organisationId . '/department/', $entity)
    ->getEntity();

Impact: High - Department creation is frequent and the form has multiple fields that can fail validation.


5. DepartmentAddCluster.php (Line 33)

Why: Creates clusters under a department via POST. Involves scope assignments that can have validation errors.

Current code:

$entity = \App::$http
    ->readPostResult('/department/' . $department->id . '/cluster/', $entity)
    ->getEntity();

Impact: Medium - Cluster creation involves complex relationships that benefit from validation feedback.


6. Cluster.php (Line 45)

Why: Updates cluster entities via POST. Currently lacks error handling for validation failures during updates.

Current code:

$entity = \App::$http->readPostResult('/cluster/' . $entity->id . '/', $entity)->getEntity();

Impact: Medium - Cluster updates should provide clear feedback when validation fails.


7. ConfigInfo.php (Line 49)

Why: Updates system configuration via POST. Configuration changes should have robust validation to prevent system misconfigurations.

Current code:

$entity = \App::$http->readPostResult(
    '/config/',
    $entity
)->getEntity();

Impact: High - Configuration changes are critical system operations where validation errors must be clearly communicated.


8. DayoffByYear.php (Line 35)

Why: Updates dayoff collections via POST. Validation errors (e.g., invalid dates, missing names) should be displayed to users.

Current code:

\App::$http->readPostResult('/dayoff/' . $year . '/', $collection);

Impact: Medium - Dayoff management is important for scheduling but less frequently performed.


9. TicketprinterStatusByScope.php (Line 45)

Why: Updates scope status and preferences via POST. Should provide validation feedback if preferences are invalid.

Current code:

$scope = \App::$http->readPostResult('/scope/' . $scope->id . '/', $scope)->getEntity();

Impact: Low-Medium - Status updates are generally simple but should still handle validation errors gracefully.


Implementation Guidelines

For each controller:

  1. Create a helper method (following the pattern established in this PR):

    protected function writeUpdatedEntity($input, $entityId = null)
    {
        $entity = (new Entity($input))->withCleanedUpFormData();
        // ... entity preparation ...
        return $this->handleEntityWrite(function () use ($entity) {
            return \App::$http->readPostResult('/endpoint/', $entity)->getEntity();
        });
    }
  2. Update the readResponse method to:

    • Call the new helper method
    • Check if result is an Entity (success) or array (error)
    • Redirect on success
    • Preserve submitted input and pass error data to template on failure
  3. Update templates to:

    • Display validation errors using the error data structure
    • Repopulate form fields with submitted data on validation errors
  4. Add form repopulation logic:

    • Create a variable (e.g., $entityData) that uses submitted input on error or current entity data otherwise
    • Pass this to the template for form rendering

Benefits

Implementing handleEntityWrite in these controllers will:

  • Provide consistent validation error handling across the admin interface
  • Improve user experience by showing clear, actionable error messages
  • Reduce confusion when API validation fails
  • Maintain form state when validation errors occur
  • Follow the established pattern for error handling in the codebase

Priority

High priority: ConfigInfo, OrganisationAddDepartment, OwnerAddOrganisation
Medium priority: Owner, OwnerAdd, Cluster, DepartmentAddCluster, DayoffByYear
Low priority: TicketprinterStatusByScope

Related


Architectural Assessment

Is This a Clean Solution for Handling JSON Errors?

Yes, this is a clean and well-designed solution for handling JSON validation errors without a standard framework.

✅ Strong Points

1. Centralized Error Handling

The handleEntityWrite method eliminates scattered try-catch blocks across controllers:

// Before: Repeated in every controller
try {
    $entity = App::$http->readPostResult(...)->getEntity();
} catch (\BO\Zmsclient\Exception $exception) {
    // Handle error...
}

// After: Single reusable wrapper
return $this->handleEntityWrite(function () use ($entity) {
    return App::$http->readPostResult(...)->getEntity();
});
2. Smart Error Transformation

Converting JSON pointers to field names bridges the gap between backend JSON Schema validation and frontend form fields:

// Transforms "/id" → "id", "/contact/email" → "contact/email"
$fieldName = (strpos($pointer, '/') === 0) ? ltrim($pointer, '/') : $pointer;
3. Graceful Degradation

The fallback logic handles various scenarios:

  • Schema validation → custom template with transformed errors
  • Known exception templates → render if template exists
  • Unknown exceptions → rethrow for higher-level handling
4. User Experience
  • Preserves form state on validation errors
  • Filters out technical "must match schema" messages
  • Provides clear, actionable error messages
  • Consistent error display across all forms

📋 Considerations

Mixed Return Types

Returns either Entity (success) or array (error):

$result = $this->writeUpdatedEntity($input, $entityId);
if ($result instanceof Entity) {
    // Success path
} else {
    // Error path - $result is array
}

This is acceptable for PHP without strict typing when consistently checking with instanceof.

Manual Form Repopulation

Each controller needs logic like:

$entityData = ($result instanceof Entity) 
    ? $entity->toArray() 
    : array_merge($entity->toArray(), $input);

This is reasonable without a framework's form component and could be extracted to a helper method if it becomes repetitive.

🎯 Comparison to Framework Approaches

This solution mirrors what frameworks provide:

Framework Pattern This Solution
Symfony Form Component + Validator handleEntityWrite + transformValidationErrors
Laravel's validate() + redirectBack()->withErrors() Entity/array return + form repopulation
Framework middleware BaseController method

For a non-framework codebase, this is architecturally sound.

💡 Optional Future Enhancement

Consider a Result/Response object for type safety (optional, not necessary):

class WriteResult {
    public function __construct(
        public ?Entity $entity = null,
        public ?array $errors = null
    ) {}
    public function isSuccess(): bool { return $this->entity !== null; }
}

However, the current approach is perfectly clean and practical given the codebase constraints.

Summary

This solution:

  • ✅ Follows DRY principle
  • ✅ Provides consistent error handling
  • ✅ Enhances user experience
  • ✅ Maintains code clarity
  • ✅ Scales well (9 additional controllers can adopt it)

This is a well-thought-out approach that brings framework-like validation handling to a non-framework codebase without over-engineering.

Metadata

Metadata

Assignees

Labels

Type

No type

Projects

No projects

Milestone

No milestone

Relationships

None yet

Development

No branches or pull requests

Issue actions