Skip to content

Support exceptions and re-use of business logic for UI and APIs#1240

Merged
vboctor merged 29 commits intomantisbt:masterfrom
vboctor:commands
Dec 30, 2017
Merged

Support exceptions and re-use of business logic for UI and APIs#1240
vboctor merged 29 commits intomantisbt:masterfrom
vboctor:commands

Conversation

@vboctor
Copy link
Copy Markdown
Member

@vboctor vboctor commented Nov 17, 2017

This is running code demonstrating a command for adding users to monitor an issue. However, exceptions are not implemented yet, invalid users are silently skipped for now.

The goals of this pattern are:

  • Re-using of business logic across Web UI, REST API, and SOAP API.
  • Having commands that execute a specific intent e.g. assign, close, monitor or generic update.
  • Commands have access to all underlying APIs like configs, entities, email, triggering plugin events, etc.
  • Commands have a validation step that validate/normalize input, authorization, etc before doing the execution. Ideally a command would be expected to pass once it goes to execution phase, though there is always chance of DB connection errors, etc.
  • Commands should trigger exceptions rather than trigger UI errors. Such exceptions should be handled by the calling code to do the appropriate behavior (e.g. UI error, API error response, etc).
  • Enable having a clean model using ADODB or some higher level ORM library that is used by commands.
  • There is no reason a command can't call another one, though this is unlikely going to be a common pattern, since common logic should be encapsulated into a re-usable core APIs.

@vboctor
Copy link
Copy Markdown
Member Author

vboctor commented Nov 17, 2017

@cproensa With this proposal script files will validate its input, construct an array of command data and run a command. This enable plugins to hook on a semantic level rather than on BugData updates. Commands can also include before state in execution results as needed.

@vboctor vboctor self-assigned this Nov 17, 2017
@vboctor
Copy link
Copy Markdown
Member Author

vboctor commented Nov 17, 2017

Independent of whether we go with this approach or another one, I would like to gradually get rid of all business logic of action pages and move it to a common place that is used with REST APIs and web UI. One way to achieve that is to get rid of the action pages and have the web UI build fully on top of the REST API. But it currently the APIs trigger errors in an unfriendly way to REST API (rather than throwing exceptions) and hence REST API has to duplicate all the checks to avoid errors from underlying APIs. So in all cases, we need to refactor, so the goal of this discussion is to figure out the model to refactor to.

@rombert
Copy link
Copy Markdown
Member

rombert commented Nov 17, 2017

I like where this is heading. Some minor comments inline.

Comment thread core/commands/Command.php Outdated
*
* @return void
*/
function execute() {
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

This implementation means that callers would need to invoke validate() by themselves. I would suggest moving the validate call inside the execute function, so that it's done automatically.

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

The intention was to call validate() from within execute(), but I forget to call it.

Comment thread core/commands/MonitorCommand.php Outdated
function validate() {
# Validate issue id
if( !isset( $this->data['issue_id'] ) ) {
throw new CommandException( HTTP_STATUS_BAD_REQUEST, 'issue_id missing', ERROR_GPC_VAR_NOT_FOUND );
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

It might be worth extracting out a InvalidDataCommandException or similar that returns the 400 Bad Request status, since it's going to be a very often encountered situation.

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

Makes sense. Will revisit the exception hierarchy or having helper methods to create such exception instances. I have already done that for the REST/SOAP APIs.

@cproensa
Copy link
Copy Markdown
Contributor

semantic level rather than on BugData updates

More thoughts to consider:
How to treat actions that are composition of other atomic actions?

@vboctor
Copy link
Copy Markdown
Member Author

vboctor commented Nov 18, 2017

How to treat actions that are composition of other atomic actions?

@cproensa My thinking is that we have a command per user action / intent. A command can't use other commands. However, commands can build on re-usable APIs to capture common knowledge.

@cproensa
Copy link
Copy Markdown
Contributor

This enable plugins to hook on a semantic level rather than on BugData updates

This goes both ways. Let's have a status update, for example from "assigned" to "closed" status.
This update would imply some "semantic" commands: resolve issue, close issue.
So, would the status change be mapped to perform the events, notifications, etc... hooked to those two commands?
I understand that notifications, would then be tied to these commands, rather to the raw BugData diffs, or the hardcoded ones... So, in the same example, which command should be used to send notifications, "resolve" or "close"?

And still, this does not remove the need to ensure the correct execution of BugData events (#1098), if you think it's related.

@vboctor
Copy link
Copy Markdown
Member Author

vboctor commented Dec 6, 2017

@cproensa I have revised the pattern and got the issue monitor command working, though I didn't implement yet the exception triggering and handling.

  • Commands no longer have Context. The commands call APIs like authentication, config, etc to get context and configuration. We can improve the pattern to access this as an orthogonal effort.
  • Commands no longer have hooks in favor of re-using the existing plugin events. An update command can decide to trigger an issue resolved event instead of an issue update event if that is seen to be the primary change for example.
  • Core functionality will not use hooks, for example a command will call email API or the underlying bug_monitor() for example will call email API.

Think of the Command as a Controller but is not tied to HTTP concepts. It is a layer above our current core APIs that captures a set of business logic that needs to be executed to achieve a higher level action.

Hope this simplifies the model and make it easier to transition to from our current implementation.

@rombert The validate() method is automatically called now, which was missed from my initial iteration. We could technically just have a process method, but the current model provides separation that can be beneficial in the future.

All - let me know your thoughts in general and specifically if you have thoughts about exception modeling / handling, which is my next step.

cc @atrol @dregad @syncguru

@rombert
Copy link
Copy Markdown
Member

rombert commented Dec 6, 2017

API overall looks good to me, thanks.

As for exception handling, I usually prefer letting exceptions bubble up and having a top-level exception handler that displays the error page and potentially logs the error. This avoids having to handle the exception in each page and also is more flexible depending on the invocation context. We can easily swap different behaviours, for instance static HTML page vs JSON response.

@vboctor
Copy link
Copy Markdown
Member Author

vboctor commented Dec 7, 2017

I did some thinking about error handling. I think we can use a model where:

  • Core APIs would start throwing exceptions instead of trigger_error(). The exception is expected to have same information that was passed to trigger error (error code, error parameters).
  • Callers of core APIs can catch and handle exceptions if they so desire (APIs will do that).
  • Older code won't catch such exceptions and hence the exception would go to the unhandled exception handler.
  • The unhandled exception handler will know whether the script is running in context of Web UI or SOAP/REST API. For Web UI (default), it would call into the current trigger_error behavior while pulling the error code, parameters and stack trace from the exception. For APIs, it would trigger some sort of 5xx error.
  • Have a way to differentiate expected errors (user doesn't exist, access denied, invalid filter, etc) from errors that are due to code bugs (e.g. expected int got an array, etc). This way, we can easily have logging enabled for system issues.

The advantage of this model is that we can gradually make the transition to exceptions while still having a system that works end to end even with unmodified code calling into a method that throws an exception instead of trigger_error() before.

As I was looking into this, I also found a couple of issues that I reported in the bug tracker. These are 23705 and 23706. Would be good to get feedback on these.

@vboctor
Copy link
Copy Markdown
Member Author

vboctor commented Dec 8, 2017

@rombert @cproensa @dregad @atrol I have done the work to make the error handling work end-to-end, so you can get a full picture of how things work. I have the following work:

  • Make sure PHPDoc is complete.
  • Fix few failing unit tests.

The above is minor and shouldn't affect the review. Once the changes are checked-in, we can gradually convert scenarios as needed. For example, I will have follow up changes to reduce code redundancy between APIs logic and core APIs for existing and new APIs.

@vboctor
Copy link
Copy Markdown
Member Author

vboctor commented Dec 9, 2017

Few more changes:

  • PHP and headers done.
  • Fixed an issue in mci_get_user_id() that fixed 3 unit tests.
  • Fixed an issue with due_date that was due to wrong test case (applies to master).
  • There are failures in running PHP 7.2 that don't seem to be related to this PR.
  • Removed usage of __autoload which is deprecated in PHP 7.2.
  • Some other tweaks to the PR.

@vboctor vboctor changed the title Command Pattern Proposal Support for re-use of business between UI and APIs + support exceptions Dec 9, 2017
@vboctor vboctor changed the title Support for re-use of business between UI and APIs + support exceptions Support exceptions and re-use of business logic for UI and APIs Dec 9, 2017
@vboctor
Copy link
Copy Markdown
Member Author

vboctor commented Dec 12, 2017

Feedback from @atrol that was provided in #1253

@vboctor please don't merge

Changing core functionality like error handling is a certain risk and needs time for stabilization.
Maybe I am too fearful, but from my experience, it will introduce regressions.
I don't have time to have a deeper look at this PR at the moment, maybe next week.

We don't have that much automated tests, so there is some manual testing needed.
E.g. we have to ensure it runs fine with all PHP versions we support.

We should merge such kind of PR's quite soon after cutting a release and run the code in a
production environment. If we are not able to do so, we should think about creating an unstable
branch.

We could merge some more larger PR's in it (especially those ones from @cproensa) with the goal > to stabilize the branch and to merge it to master at a later time, but not later than early February
2018.

@vboctor
Copy link
Copy Markdown
Member Author

vboctor commented Dec 12, 2017

@atrol

I think feedback on such work item comes in three buckets:

  1. The overall approach and big picture feedback. Go / No Go and high level feedback.
  2. Testing, nit picks, and tweaks. It's a Go, but minor feedback.
  3. Risk / merge time / etc.

I have opened this PR 25 days ago and I tried to start with the simple big picture so as to get feedback in bucket 1 (the code was not even fully implemented, just a skeleton at that point). Got some feedback and progressed further. I think we should quickly close on 1 so I can invest further and make progress.

I have done a fair bit of testing for 2, and can do more. My goal was to get it in early after 2.9.0 and test, but I was waiting on review from more team members.

As for the risk and timing, I have tried not to boil the ocean in this PR, and just support the basic error handling approach and exercise it with an end-to-end scenario. So even though I expect to gradually use the new model in more code, I have tried to keep it to few code paths to keep the change scoped.

As for the release timing, I think given the end of year vacations, we could delay or skip the end of December release. But we can get to that once you have a look at the PR and give it few minutes or trying on your machines.

We could merge some more larger PR's in it (especially those ones from @cproensa) with the goal > to stabilize the branch and to merge it to master at a later time, but not later than early February 2018.

I'm not sure I want to put a bunch of big PRs into a single release, I would rather trickle them in as they are ready.

@cproensa
Copy link
Copy Markdown
Contributor

I have lost track on this.
It started as a some proposal for "Commands", now is for moving into exceptions, and it's due to merge.

@vboctor
Copy link
Copy Markdown
Member Author

vboctor commented Dec 12, 2017

I have lost track on this. It started as a some proposal for "Commands", now is for moving into exceptions, and it's due to merge.

The goal of this PR is to provide a way to share business logic between Web UI and REST API. In order to achieve that, we have to change error handling to bubble up errors instead of output to the screen. The PR also includes an example of adding users to monitor list that was changed to re-use a command between REST API and Web UI. The PR also does that while maintaining backward compatibility.

Comment thread core/commands/MonitorAddCommand.php Outdated
/**
* @var integer
*/
private $loggedInUserId;
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

not used

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

Will remove.

Comment thread core/constant_inc.php Outdated
define( 'ERROR_USER_CURRENT_PASSWORD_MISMATCH', 812 );
define( 'ERROR_USER_EMAIL_NOT_UNIQUE', 813 );
define( 'ERROR_USER_BY_EMAIL_NOT_FOUND', 814 );
define( 'ERROR_USER_BY_REALNAME_NOT_FOUND', 814 );
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Should be 815

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

Good catch, fixed.

Use Command Pattern to achieve the following:

- Re-using of business logic across Web UI, REST API, and SOAP API.
- Having commands that execute a specific intent e.g. assign, close, monitor rather than just update.
- Enable core code to hook into such commands to handle side effects like email messages.
- Enable plugins code to hook in such commands to modify changes, block the change, post to slack, etc.
- Use exceptions that can be surfaced to APIs or Web UI.
- Enable having a clean model using ADODB or some higher level ORM library that is used by commands.
- Simplify commands to just encapsulate business logic.
- Commands no longer have hooks, use existing events.
- Remove context and use standard APIs.
- Error handling not implemented.

The command is used from UI, but can easily be used from APIs.
The code assumes that such errors will halt executions, so such errors shouldn’t ever resume.

Fixes #23706
- Add MantisException, ServiceException, and ClientException.
- Add unhandled exception handler that forward to trigger error on error mode is display.
- Force termination on case of E_USER_ERROR (aliased to ERROR in Mantis).

This allows replacing trigger_error( …, ERROR) with throwing the appropriate exception
while maintaining backward compatibility for UI code.
- Map errors to API error buckets (e.g. not found, bad request, forbidden, etc)
- Implement unhandled exception handler for APIs.
- Remove redundant check in Issue Get and rely on exceptions instead.
- bug_ensure_exists() and getting bug now triggers an exception in case bug not found instead of trigger_error().
This exception should be used for bad state in db and configs.
The array can contain: id, name, realname, email.
Enforce for E_ERROR and E_RECOVERABLE_ERROR and not just E_USER_ERROR.

Updated documentation, config_defaults_inc.php samples and defaults.

Fixes #23706
- Specific route handlers don’t need to catch exceptions just to return them in the response.
- In case of errors include a json with Mantis error code, message, and localized message for thrown exceptions.
- Convert RestFault and throw, though in this case only include the message in json, no code or localized message.

LegacyApiFaultException can be removed once all APIs throw proper exceptions instead of returning RestFault.

Fixes #23714
This is to support the returning of SoapFault and throwing Mantis exceptions.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4 participants