Scenario
I have a system where a user can upload an Excel file which is validated and then used to provide data for the application.
There are two distinct operations I need to handle with this file:
- Validation: On upload, I want to validate report any problems with the file, so that the user can fix it. Some of the validation criteria are dynamic, and depend on other values in the database.
- Runtime integrity check: Later, during processing, I want to create value objects from the Excel file. While doing that I want to re-check the integrity of the Excel file, because the value objects cannot and should not be created otherwise. A failing integrity check will throw an exception.
Typical failures could be "Missing sheet names", "Unexpected sheet names", "A sheet does not have the expected column labels.", "Unsupported formula", etc.
Reporting and logging:
- If validation fails on upload, I want to show detailed messages to the user. There is not really a need to log anything. The messages should be localized.
- If the integrity check fails, I want to show a simple message to the user, telling them that the operation failed, and that the previously uploaded Excel file is no longer valid. I don't think this is the place to show details about the validation. In the logs I want to post a similar message, plus all the details I have available.
The failing integrity check should generally not happen, if the validation was successful. But I don't want to fully trust the validation process - perhaps the Excel file was uploaded earlier, when some parameters for the validation were different. Also if validation and integrity check are different code, I don't want to rely on them being fully equivalent.
Technology
I am using PHP + Drupal 9, which includes parts of symfony. But I am looking for a generic answer that could also apply to other languages / technologies.
Goal + Question
To have a single source of truth, I would like to reuse the code for user-facing validation and the integrity check when creating the value object.
Is there a pattern I can use to do this?
Or is it common practice to just have two fully separate parts of code?
My concern is that the two parts will easily get out of sync, and that there is at least a theoretical code path where the validation passes but the runtime integrity check still fails.
A side goal is to also have reporting information for positive cases. E.g. "3 of 10 sheets were imported" (compared to previous version).
What I considered so far
Exceptions with user messages
One option would be to have special exceptions that contain messages suitable for display to the user, as suggested here. One problem here is that this will only show the first fail, and not all the problems in the data. Also, all the arguments from here apply.
Injectable failure handler
Have an injectable object that deals with failures. One version would collect all failures, while another would immediately throw an exception on fail.
Complex return value.
There could be a return value that contains information about the failures, but that also provides the value object, if the data is valid. It could either produce a validation report, or it could provide methods like getMissingSheetNames()
etc which would then be used by another piece of code to produce failure messages. Tbh I would prefer to keep it all in one place.