Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Fix input type for smarty number formatting (more forgiving) #22429

Merged
merged 1 commit into from
Jan 10, 2022

Conversation

colemanw
Copy link
Member

@colemanw colemanw commented Jan 9, 2022

Overview

Fixes a hard-crash with CiviGrant, regression in 5.46 caused by #22309 .
This is a more forgiving version of #22427

Before

  • Enable CiviGrant
  • Create a grant
  • Hard crash on "Find Grants", "Grants Dashboard" or Grants tab of contact summary screen.

After

Fixed.

Technical Details

There were confusing mismatches between docblocks and the function signatures. smarty_modifier_crmMoney had $amount annotated as type float but passed it into Format::money which requires type string, which passes it onto Money::of which accepts types BigNumber|int|float|string.
I think I've straightened it out.

@civibot
Copy link

civibot bot commented Jan 9, 2022

(Standard links)

@@ -33,7 +33,10 @@ class Format {
* @noinspection PhpDocMissingThrowsInspection
* @noinspection PhpUnhandledExceptionInspection
*/
public function money(string $amount, ?string $currency = NULL, ?string $locale = NULL): string {
public function money($amount, ?string $currency = NULL, ?string $locale = NULL): string {
if (is_null($amount) || $amount === '') {
Copy link
Contributor

Choose a reason for hiding this comment

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

what about is (strlen($amount) === 0) ?

that would get FALSE too

Copy link
Member Author

Choose a reason for hiding this comment

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

Why would the value ever be FALSE? And if it could be a bool, what on earth would TRUE mean? How much money is TRUE worth?

Copy link
Contributor

Choose a reason for hiding this comment

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

@colemanw I doubt it would ever be TRUE but we DO sometimes fill our arrays with FALSE where we want to avoid enotices

Copy link
Member Author

Choose a reason for hiding this comment

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

I guess I'm wary of type coercion, passing numbers into strlen() sounds like an invitation for weird bugs. I'd rather just add || is_bool($amount).

Copy link
Contributor

@demeritcowboy demeritcowboy Jan 9, 2022

Choose a reason for hiding this comment

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

The monetary value of TRUE is USD $1 - I think it's in the Geneva Convention. Seriously though I'd argue it should throw an error if TRUE is passed in.

Between this and the other PR, I get the tradeoff between being able to catch bugs/typos early (being strict) and between not having to track down every existing caller (being forgiving), and normally I like the former, but my 2c on this one is to prefer this PR.

Regarding what the if should look like, maybe something like:

// Handle values that Brick Money doesn't like
// We also treat FALSE as '' because sometimes variables are mass-filled with FALSE to avoid being missing, so can't be treated as an error.
if (is_null($amount) || $amount === '' || $amount === FALSE) {
  return '';
}
elseif ($amount === TRUE) {
  throw new CRM_Core_Exception('TRUE is not a valid value for type money');
}

Copy link
Member Author

Choose a reason for hiding this comment

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

Ok I've updated this PR to forgive FALSE, NULL and '' but throw an exception for all other non-numeric values.

Copy link
Contributor

Choose a reason for hiding this comment

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

I'm happy with that!

@colemanw colemanw changed the base branch from master to 5.46 January 9, 2022 02:30
@civibot civibot bot added 5.46 and removed master labels Jan 9, 2022
@colemanw
Copy link
Member Author

colemanw commented Jan 9, 2022

retest this please

@demeritcowboy
Copy link
Contributor

@colemanw that looks like it was a real fail. It seems the TokenProcessor assigns objects of type Money to the token values when it thinks it's a money field, and then it always passes a BigDecimal as the value (via getAmount()). But because it has a __toString() method, that previously got autocast by the parameter typehint, but now it stays as a BigDecimal.

not ok 702 - Failure: CRM_Contribute_ActionMapping_ByTypeTest::testTokenRendering
  message: 'Failure in api call for job send_reminder:  Invalid value for type money'

From my local, and if I put the typehint back it passes, confirming the autocasting:

0 \Civi\Token\TokenProcessor.php(489): Civi\Core\Format->money(Object(Brick\Math\BigDecimal), 'EUR')
1 \Civi\Token\TokenProcessor.php(383): Civi\Token\TokenProcessor->filterTokenValue(Object(Brick\Money\Money), Array, Object(Civi\Token\TokenRow))
2 \Civi\Token\TokenProcessor.php(447): Civi\Token\TokenProcessor->Civi\Token\{closure}('{contribution.n...', 'contribution', 'non_deductible_...', NULL)
3 [internal function]: Civi\Token\TokenProcessor->Civi\Token\{closure}(Array)
4 \Civi\Token\TokenProcessor.php(431): preg_replace_callback(';\\{([\\w]+)\\.([\\...', Object(Closure), '\n      first na...')
5 \Civi\Token\TokenProcessor.php(396): Civi\Token\TokenProcessor->visitTokens('\n      first na...', Object(Closure))
6 \Civi\Token\TokenRow.php(320): Civi\Token\TokenProcessor->render('body_text', Object(Civi\Token\TokenRow))

if (is_null($amount) || $amount === '' || $amount === FALSE) {
return '';
}
elseif (!is_numeric($amount)) {
Copy link
Contributor

Choose a reason for hiding this comment

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

So what might work as a general catchall is elseif (!is_numeric((string) $amount)) {

Or explicitly only cast BigDecimals.

Copy link
Contributor

Choose a reason for hiding this comment

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

Oh no nevermind that would make TRUE be '1'

Copy link
Member Author

Choose a reason for hiding this comment

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

What about elseif ($amount === TRUE || !is_numeric((string) $amount)) {

Copy link
Contributor

Choose a reason for hiding this comment

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

murmurring supportive noises (I don't have much to add since you two seem to be on the right track here)

Some smarty templates pass NULL to crmMoney, so it needs to handle that possibilty
@demeritcowboy
Copy link
Contributor

Yay! Passing tests.
The code comments might not really explain to future devs why the if is the way it is, but not a blocker.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
Development

Successfully merging this pull request may close these issues.

3 participants