Merge pull request #19248 from eileenmcnaughton/dep_utils
[civicrm-core.git] / Civi / Payment / PropertyBag.php
index a97c8d113f86810d93c41cce2f5f512a0527fb6a..06f96d42fa65fa1a0c276fc3b29b04b53290f853 100644 (file)
@@ -2,7 +2,7 @@
 namespace Civi\Payment;
 
 use InvalidArgumentException;
-use Civi;
+use CRM_Core_Error;
 use CRM_Core_PseudoConstant;
 
 /**
@@ -21,11 +21,6 @@ use CRM_Core_PseudoConstant;
  *
  */
 class PropertyBag implements \ArrayAccess {
-  /**
-   * @var array
-   * - see legacyWarning
-   */
-  public static $legacyWarnings = [];
 
   protected $props = ['default' => []];
 
@@ -75,6 +70,14 @@ class PropertyBag implements \ArrayAccess {
     'isNotifyProcessorOnCancelRecur' => TRUE,
   ];
 
+
+  /**
+   * @var bool
+   * Temporary, internal variable to help ease transition to PropertyBag.
+   * Used by cast() to suppress legacy warnings.
+   */
+  protected $suppressLegacyWarnings = FALSE;
+
   /**
    * Get the property bag.
    *
@@ -109,7 +112,9 @@ class PropertyBag implements \ArrayAccess {
    */
   public function offsetExists ($offset): bool {
     $prop = $this->handleLegacyPropNames($offset, TRUE);
-    return $prop && isset($this->props['default'][$prop]);
+    // If there's no prop, assume it's a custom property.
+    $prop = $prop ?? $offset;
+    return array_key_exists($prop, $this->props['default']);
   }
 
   /**
@@ -118,8 +123,35 @@ class PropertyBag implements \ArrayAccess {
    * @param mixed $offset
    * @return mixed
    */
-  public function offsetGet ($offset) {
-    $prop = $this->handleLegacyPropNames($offset);
+  public function offsetGet($offset) {
+    try {
+      $prop = $this->handleLegacyPropNames($offset);
+    }
+    catch (InvalidArgumentException $e) {
+
+      CRM_Core_Error::deprecatedFunctionWarning(
+        "proper getCustomProperty('$offset') for non-core properties. "
+        . $e->getMessage(),
+        "PropertyBag array access to get '$offset'"
+      );
+
+      try {
+        return $this->getCustomProperty($offset, 'default');
+      }
+      catch (BadMethodCallException $e) {
+        CRM_Core_Error::deprecatedFunctionWarning(
+          "proper setCustomProperty('$offset', \$value) to store the value (since it is not a core value), then access it with getCustomProperty('$offset'). NULL is returned but in future an exception will be thrown."
+          . $e->getMessage(),
+          "PropertyBag array access to get unset property '$offset'"
+        );
+        return NULL;
+      }
+    }
+
+    CRM_Core_Error::deprecatedFunctionWarning(
+      "get" . ucfirst($offset) . "()",
+      "PropertyBag array access for core property '$offset'"
+    );
     return $this->get($prop, 'default');
   }
 
@@ -139,7 +171,15 @@ class PropertyBag implements \ArrayAccess {
       // This is fine if it's something particular to a payment processor
       // (which should be using setCustomProperty) however it could also lead to
       // things like 'my_weirly_named_contact_id'.
-      $this->legacyWarning($e->getMessage() . " We have merged this in for now as a custom property. Please rewrite your code to use PropertyBag->setCustomProperty if it is a genuinely custom property, or a standardised setter like PropertyBag->setContactID for standard properties");
+      //
+      // From 5.28 we suppress this when using PropertyBag::cast() to ease transition.
+      if (!$this->suppressLegacyWarnings) {
+        CRM_Core_Error::deprecatedFunctionWarning(
+          "proper setCustomProperty('$offset', \$value) for non-core properties. "
+          . $e->getMessage(),
+          "PropertyBag array access to set '$offset'"
+        );
+      }
       $this->setCustomProperty($offset, $value, 'default');
       return;
     }
@@ -155,6 +195,12 @@ class PropertyBag implements \ArrayAccess {
     // These lines are here (and not in try block) because the catch must only
     // catch the case when the prop is custom.
     $setter = 'set' . ucfirst($prop);
+    if (!$this->suppressLegacyWarnings) {
+      CRM_Core_Error::deprecatedFunctionWarning(
+        "$setter()",
+        "PropertyBag array access to set core property '$offset'"
+      );
+    }
     $this->$setter($value, 'default');
   }
 
@@ -168,37 +214,6 @@ class PropertyBag implements \ArrayAccess {
     unset($this->props['default'][$prop]);
   }
 
-  /**
-   * Log legacy warnings info.
-   *
-   * @param string $message
-   */
-  protected function legacyWarning($message) {
-    if (empty(static::$legacyWarnings)) {
-      // First time we have been called.
-      register_shutdown_function([PropertyBag::class, 'writeLegacyWarnings']);
-    }
-    // Store warnings instead of logging immediately, as calls to Civi::log()
-    // can take over half a second to work in some hosting environments.
-    static::$legacyWarnings[$message] = TRUE;
-
-    // For unit tests:
-    $this->lastWarning = $message;
-  }
-
-  /**
-   * Save any legacy warnings to log.
-   *
-   * Called as a shutdown function.
-   */
-  public static function writeLegacyWarnings() {
-    if (!empty(static::$legacyWarnings)) {
-      $message = "Civi\\Payment\\PropertyBag related deprecation warnings:\n"
-        . implode("\n", array_keys(static::$legacyWarnings));
-      Civi::log()->warning($message, ['civi.tag' => 'deprecated']);
-    }
-  }
-
   /**
    * @param string $prop
    * @param bool $silent if TRUE return NULL instead of throwing an exception. This is because offsetExists should be safe and not throw exceptions.
@@ -226,7 +241,10 @@ class PropertyBag implements \ArrayAccess {
       throw new \InvalidArgumentException("Unknown property '$prop'.");
     }
     // Remaining case is legacy name that's been translated.
-    $this->legacyWarning("We have translated '$prop' to '$newName' for you, but please update your code to use the propper setters and getters.");
+    if (!$this->suppressLegacyWarnings) {
+      CRM_Core_Error::deprecatedFunctionWarning("Canonical property name '$newName'", "Legacy property name '$prop'");
+    }
+
     return $newName;
   }
 
@@ -239,7 +257,7 @@ class PropertyBag implements \ArrayAccess {
    * @return mixed
    */
   protected function get($prop, $label) {
-    if (array_key_exists($prop, $this->props['default'])) {
+    if (array_key_exists($prop, $this->props[$label] ?? [])) {
       return $this->props[$label][$prop];
     }
     throw new \BadMethodCallException("Property '$prop' has not been set.");
@@ -291,16 +309,21 @@ class PropertyBag implements \ArrayAccess {
 
   /**
    * This is used to merge values from an array.
-   * It's a transitional function and should not be used!
+   * It's a transitional, internal function and should not be used!
    *
    * @param array $data
    */
   public function mergeLegacyInputParams($data) {
+    // Suppress legacy warnings for merging an array of data as this
+    // suits our migration plan at this moment. Future behaviour may differ.
+    // @see https://github.com/civicrm/civicrm-core/pull/17643
+    $this->suppressLegacyWarnings = TRUE;
     foreach ($data as $key => $value) {
       if ($value !== NULL && $value !== '') {
         $this->offsetSet($key, $value);
       }
     }
+    $this->suppressLegacyWarnings = FALSE;
   }
 
   /**
@@ -380,14 +403,26 @@ class PropertyBag implements \ArrayAccess {
   }
 
   /**
-   * Get the monetary amount.
+   * Set the monetary amount.
+   *
+   * - We expect to be called with a string amount with optional decimals using
+   *   a '.' as the decimal point (not a ',').
+   *
+   * - We're ok with floats/ints being passed in, too, but we'll cast them to a
+   *   string.
+   *
+   * - Negatives are fine.
+   *
+   * @see https://github.com/civicrm/civicrm-core/pull/18219
+   *
+   * @param string|float|int $value
+   * @param string $label
    */
   public function setAmount($value, $label = 'default') {
     if (!is_numeric($value)) {
       throw new \InvalidArgumentException("setAmount requires a numeric amount value");
     }
-
-    return $this->set('amount', $label, \CRM_Utils_Money::format($value, NULL, NULL, TRUE));
+    return $this->set('amount', $label, filter_var($value, FILTER_SANITIZE_NUMBER_FLOAT, FILTER_FLAG_ALLOW_FRACTION));
   }
 
   /**
@@ -1055,6 +1090,10 @@ class PropertyBag implements \ArrayAccess {
     if (isset(static::$propMap[$prop])) {
       throw new \InvalidArgumentException("Attempted to get '$prop' via getCustomProperty - must use using its getter.");
     }
+
+    if (!array_key_exists($prop, $this->props[$label] ?? [])) {
+      throw new \BadMethodCallException("Property '$prop' has not been set.");
+    }
     return $this->props[$label][$prop] ?? NULL;
   }